40-Pin Expansion Header¶
The Quectel Pi M1/L1 intelligent main control board provides a standard 40‑pin GPIO expansion header, which supports multiple peripheral interfaces including GPIO, I2C, SPI, UART, and PWM. The following section describes how to test the functionality of these interfaces.
Pin Definition¶
Function3 |
Function2 |
Function1 |
L1 GPIO# |
M1 GPIO# |
Pin# |
Pin# |
M1 GPIO# |
L1 GPIO# |
Function1 |
Function2 |
Function3 |
|---|---|---|---|---|---|---|---|---|---|---|---|
VCC 3V3 |
1 |
2 |
VCC 5V |
||||||||
Common IIC SDA |
GPIO_109(I2C1_SDA) |
3 |
4 |
VCC 5V |
|||||||
Common IIC SCL |
GPIO_110(I2C1_SCL) |
5 |
6 |
GND |
|||||||
GPIO_60 |
GPIO_83 |
7 |
8 |
GPIO_69(UART1_TXD) |
SPI1_SCLK |
UART1_TX |
|||||
GND |
9 |
10 |
GPIO_70(UART1_RXD) |
SPI1_CS_N |
UART1_RX |
||||||
GPIO_106 |
11 |
12 |
GPIO_98 |
||||||||
GPIO_31 |
13 |
14 |
GND |
||||||||
UART2_TX |
SPI2_SCLK |
GPIO_71 |
15 |
16 |
GPIO_84 |
PMU_GPIO3 |
|||||
VCC 3V3 |
17 |
18 |
GPIO_25 |
GPIO_32 |
|||||||
I2C0_SCL |
UART0_RTS |
SPI0_MOSI |
GPIO_1(SPI_MOSI) |
19 |
20 |
GND |
|||||
I2C0_SDA |
UART0_CTS |
SPI0_MISO |
GPIO_0(SPI_MISO) |
21 |
22 |
GPIO_80 |
SPI2_CS_N |
UART2_RX |
|||
UART0_TX |
SPI0_SCLK |
GPIO_2(SPI_CLK) |
23 |
24 |
GPIO_3(SPI_CE0) |
SPI0_CS_N_0 |
UART0_RX |
||||
GND |
25 |
26 |
GPIO_82(SPI_CE1) |
||||||||
I2C5_SDA |
UART5_CTS |
SPI5_MISO |
GPIO_14(I2C0_SDA) |
27 |
28 |
GPIO_15(I2C0_SCL) |
SPI5_MOSI |
UART5_RTS |
I2C5_SCL |
||
I2C1_SCL |
UART1_RTS |
SPI1_MOSI |
GPIO_5 |
29 |
30 |
GND |
|||||
I2C1_SDA |
UART1_CTS |
SPI1_MISO |
GPIO_4 |
31 |
32 |
GPIO_67 |
PMU_GPIO8 |
||||
PWM |
PMU_GPIO2(PWM) |
PMU_GPIO8(PWM) |
33 |
34 |
GND |
||||||
GPIO_99 |
35 |
36 |
GPIO_16 |
SPI5_SCLK |
UART5_TX |
||||||
UART5_RX |
SPI5_CS_N |
GPIO_17 |
37 |
38 |
GPIO_101 |
||||||
GND |
39 |
40 |
GPIO_100 |
||||||||
GPIO Test¶
Hardware Connection¶
Take Pin13 (GPIO_31) as an example: connect the positive probe of the multimeter to Pin13, and the negative probe to GND (such as Pin14). When testing other GPIOs, simply change the line offset in the command to the corresponding GPIO number.
Test Method¶
After entering adb shell, use the gpiod command to test GPIO. For regular SoC GPIOs, use gpiochip0, and GPIO_31 corresponds to line 31 of gpiochip0.
adb shell
# View the GPIO controller
gpiodetect
# Check the status of the specified GPIO
gpioinfo gpiochip0 | grep -E "line[[:space:]]+31:"
# Read the current GPIO level
gpioget gpiochip0 31
# Pull the GPIO high and hold for 10 seconds.
gpioset --mode=time --sec=10 gpiochip0 31=1
# Pull the GPIO low and hold for 10 seconds.
gpioset --mode=time --sec=10 gpiochip0 31=0
GPIO interrupt test method: Connect the GPIO under test to a key or an external signal source to make the pin level change; use gpiomon to monitor the rising edge and falling edge events of this GPIO. Here, GPIO_31 is still taken as an example.
# Check the current level of GPIO_31
gpioget gpiochip0 31
# Monitor the rising edge and falling edge events of GPIO_31, and exit after being triggered 5 times.
gpiomon --num-events=5 --rising-edge --falling-edge gpiochip0 31
Note: If an error occurs gpiodetect: error while loading shared libraries: libgpiod.so.2: cannot open shared object file: No such file or directory,Execute first export LD_LIBRARY_PATH=/opt/qcom/lib:$LD_LIBRARY_PATH and then run the gpiod command.
Multimeter Measurement Results: If the measured voltage is close to 3.3V when a high-level output is executed, and close to 0V when a low-level output is executed, the GPIO output function can be determined to be normal.
I2C Test¶
Pins 3 and 5 on the 40‑pin header are by default the I2C data and clock pins. To test the I2C interface, we need to connect an external I2C device. Here we use the Waveshare environmental sensor expansion board, and its corresponding device node is /dev/i2c-1.
Test Preparation¶
|
|
|
|
|---|---|---|---|
WSL25911FN |
0x29 |
0x12 |
0x50 |
BME280 |
0x76 |
0xD0 |
0x60 |
MPU9250 |
0x68 |
0x75 |
0x71 |
LTR390-UV-1 |
0x53 |
0x06 |
0xB2 |
PS: For the WSL25911FN used here, the command bit (0xA0) needs to be included when reading the ID register. That is, when accessing register 0x12, it is actually written as 0xA0 | 0x12 = 0xB2; no other registers require this.
This test uses the Waveshare environmental sensor expansion board, which is connected via the 40‑pin header.
Hardware Connection Diagram:
Quectel Pi M1 connected to the expansion board
Test Procedure¶
adb shell # Enter the ADB shell
ls -al /dev/i2c* # Check whether I2C-1 exists in the corresponding directory
i2cdetect -y 1 # Check how many devices are attached to the corresponding I2C bus
I2C Read/Write¶
i2cget -y 1 0x53 0x05 b # Read the value of register 0x05 at device address 0x53 on i2c‑1, size one byte
i2cset -y 1 0x53 0x05 0x03 b # Set the value of register 0x05 at device address 0x53 on i2c‑1 to 0x03, size one byte
SPI Test¶
Here we use the 2.23-inch OLED expansion board.
The device chip-select nodes for the SPI function on the 40‑pin header are /dev/spidev0.0 and /dev/spidev0.1. To view these nodes, enter the following in adb:
adb shell # Enter the ADB shell
ls /dev/spidev* # View SPI devices
UART Test¶
On the 40‑pin header, pin8 and 10 are configured by default as UART functions, with the corresponding device node being /dev/ttyHS0.
Viewing Serial Devices
You can use the following command to view all serial devices in the system:
ls /dev/tty*
UART Loopback Test¶
This test verifies whether the UART transmit and receive functions are working properly by shorting pins 8 and 10 together.
Hardware Connection: Short pin 8 (TX) and pin 10 (RX) on the 40‑pin header.
Create the UART test file on the Linux host:
Create a new file named uart_loopback.c with the following content:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <signal.h>
static int serial_fd = -1;
static volatile int running = 1;
void signal_handler(int sig) {
printf("\n用户中断测试\n");
running = 0;
if (serial_fd >= 0) {
close(serial_fd);
printf("串口已关闭\n");
}
exit(0);
}
int configure_serial(int fd, int baudrate) {
struct termios tty;
if (tcgetattr(fd, &tty) != 0) {
printf("获取串口属性失败: %s\n", strerror(errno));
return -1;
}
speed_t speed;
switch (baudrate) {
case 9600: speed = B9600; break;
case 19200: speed = B19200; break;
case 38400: speed = B38400; break;
case 57600: speed = B57600; break;
case 115200: speed = B115200; break;
case 230400: speed = B230400; break;
default: speed = B115200; break;
}
cfsetospeed(&tty, speed);
cfsetispeed(&tty, speed);
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~CRTSCTS;
tty.c_cflag |= CREAD | CLOCAL;
tty.c_iflag &= ~(IXON | IXOFF | IXANY);
tty.c_iflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_oflag &= ~OPOST;
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_cc[VTIME] = 10;
tty.c_cc[VMIN] = 0;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
printf("设置串口属性失败: %s\n", strerror(errno));
return -1;
}
return 0;
}
int serial_loopback_test(const char* port, int baudrate) {
char test_data[] = "Hello, Serial Loopback!";
char received_data[256];
ssize_t bytes_written, bytes_read;
serial_fd = open(port, O_RDWR | O_NOCTTY | O_SYNC);
if (serial_fd < 0) {
printf("无法打开串口 %s: %s\n", port, strerror(errno));
return -1;
}
if (configure_serial(serial_fd, baudrate) != 0) {
close(serial_fd);
return -1;
}
printf("串口 %s 已打开,开始回环测试(按Ctrl+C退出)...\n", port);
while (running) {
tcflush(serial_fd, TCIOFLUSH);
bytes_written = write(serial_fd, test_data, strlen(test_data));
if (bytes_written < 0) {
printf("发送数据失败: %s\n", strerror(errno));
break;
}
printf("发送: %s\n", test_data);
usleep(100000);
bytes_read = read(serial_fd, received_data, sizeof(received_data) - 1);
if (bytes_read < 0) {
printf("读取数据失败: %s\n", strerror(errno));
break;
}
received_data[bytes_read] = '\0';
if (bytes_read == (ssize_t)strlen(test_data) &&
strncmp(received_data, test_data, strlen(test_data)) == 0) {
printf("接收: %s → 测试通过\n\n", received_data);
} else {
printf("接收异常: 发送[%zu] vs 接收[%zd] → 测试失败\n",
strlen(test_data), bytes_read);
if (bytes_read > 0) {
printf("接收内容: %s\n\n", received_data);
} else {
printf("未接收到数据\n\n");
}
}
sleep(1);
}
close(serial_fd);
printf("串口 %s 已关闭\n", port);
return 0;
}
void print_usage(const char* prog) {
printf("UART回环测试程序\n\n");
printf("用法:\n");
printf(" %s -n <HS编号> [-b 波特率] 指定ttyHS编号\n", prog);
printf(" %s -d <设备路径> [-b 波特率] 指定完整设备路径\n", prog);
printf(" %s 默认使用 /dev/ttyHS5 @ 115200\n\n", prog);
printf("示例:\n");
printf(" %s -n 3 使用 /dev/ttyHS3 @ 115200\n", prog);
printf(" %s -n 5 -b 9600 使用 /dev/ttyHS5 @ 9600\n", prog);
printf(" %s -d /dev/ttyUSB0 使用 /dev/ttyUSB0 @ 115200\n", prog);
}
int main(int argc, char* argv[]) {
char port_buf[64];
const char* port = "/dev/ttyHS5";
int baudrate = 115200;
int opt;
while ((opt = getopt(argc, argv, "n:d:b:h")) != -1) {
switch (opt) {
case 'n':
snprintf(port_buf, sizeof(port_buf), "/dev/ttyHS%s", optarg);
port = port_buf;
break;
case 'd':
port = optarg;
break;
case 'b':
baudrate = atoi(optarg);
break;
case 'h':
default:
print_usage(argv[0]);
return (opt == 'h') ? 0 : 1;
}
}
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
printf("UART回环测试程序\n");
printf("使用串口: %s\n", port);
printf("波特率: %d\n", baudrate);
printf("=====================================\n");
return serial_loopback_test(port, baudrate);
}
After the file is created, compile it into an executable:
mkdir -p out # Create the output directory
aarch64-linux-gnu-gcc uart_loopback.c -O2 -Wall -Wextra -o out/uart_loopback # Compile into an executable
Copy the compiled file from the Linux environment to the same directory as the ADB shell.
Execution steps:
adb push uart_loopback /tmp/uart_loopback
adb shell chmod +x /tmp/uart_loopback
adb shell
/tmp/uart_loopback /dev/ttyHS1
Test results:
When pins 8 and 10 are correctly shorted, the program will continuously send data and verify whether the received data matches.
Using serial port: /dev/ttyHS1
Baud rate: 115200
=====================================
Serial port /dev/ttyHS1 opened, starting loopback test (Press Ctrl+C to exit)...
Sent: Hello, Serial Loopback!
Received: Hello, Serial Loopback! → Test passed
Sent: Hello, Serial Loopback!
Received: Hello, Serial Loopback! → Test passed
Sent: Hello, Serial Loopback!
Received: Hello, Serial Loopback! → Test passed
Press Ctrl+C to exit the test program. The program will automatically close the serial port and exit.
Temperature‑Controlled Fan Test¶
A background process monitors /sys/class/thermal/thermal_zone*, periodically reads the temperatures of several CPU cores, and takes the highest value. Based on this highest temperature, it configures the fan’s PWM duty cycle.
|
|
|---|---|
<30℃ |
0 |
30~40℃ |
64 |
40~50℃ |
128 |
50~60℃ |
192 |
>60℃ |
255 |
Wiring Diagram¶
This test uses a Raspberry Pi fan. Connect it to the corresponding pins on the M1/L1 40‑pin header as shown in the diagram below.
Fan Pin |
M1 Development Board Pin (40‑PIN) |
|---|---|
Red wire (Power positive) |
PIN2 |
Black wire (Power negative) |
PIN6 |
Blue wire (PWM) |
PIN33 |
Yellow wire (Speed detection / tachometer) |
Not connected |
Test Procedure¶
By default, the fan function is disabled. After power‑on, enter the following commands in adb to check the status of the fan service.
adb shell
40pin-ctrl fan status # View the current status of the fan service (bound if not enabled, enable if active)
40pin-ctrl fan enable # Enable the fan service; after enabling, you can check whether the status shows "enable"
40pin-ctrl fan disable # Disable the fan service (only needed to stop the test)
Enter the following in the first terminal.
40pin-ctrl fan enable
40pin-ctrl fan status
#M1
watch -n 1 'PWM=/sys/bus/platform/devices/1c40000.qcom,spmi:qcom,pm6125@1:qcom,pwms@b300/pwm/pwmchip0/pwm0; p=$(cat "$PWM/period"); d=$(cat "$PWM/duty_cycle"); echo $(( (d * 255 + p / 2) / p ))'
#L1
watch -n 1 'PWM=/sys/bus/platform/devices/1c40000.qcom,spmi:qcom,pm2250@1:qcom,pwms@be00/pwm/pwmchip2/pwm0; p=$(cat "$PWM/period"); d=$(cat "$PWM/duty_cycle"); echo $(( (d * 255 + p / 2) / p ))'
Check whether the current temperature and PWM duty cycle match.
Open a second terminal and perform the heating (temperature‑raising) operation.
for i in 1 2 3 4; do
yes > /dev/null &
done
If the temperature rise in step 3 is insufficient, you can increase the load.
for i in $(seq 1 $(nproc)); do
yes > /dev/null &
done
Check whether the temperatures and fan PWM duty cycles in the two terminals match the values in the table.
After the test ends, stop the load.
killall yes