40-Pin Expansion Header

Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.


Quectel Pi smart controller board provides a standard 40-pin GPIO expansion header that supports various peripheral interfaces, including GPIO, I2C, SPI, UART, and PWM. The following sections describe how to test these interfaces.

../../_images/image_TRxPb48LaodiXrxWfhecWPx0nuc.webp

Pin Definition


Function10

Function9

Function8

Function7

Function6

Function5

Function4

Function3

M2 GPIO#

Pin#

Pin#

M2 GPIO#

Function3

Function4

Function5

Function6

Function7

Function8

Function9

Function10

VCC 3V3

1

2

VCC 5V










CAN1_RX_M2

I2C3_SDA_M0

UART2_RX_M1

-




GPIO4_B4_d(I2C3_SDA)

3

4

VCC 5V







CAN1_TX_M2

PCIE0_CLKREQN_M2

I2C3_SCL_M0

UART2_TX_M1

-




GPIO4_B5_d(I2C3_SCL)

5

6

GND







I2C8_SDA_M2

UART8_CTSN_M1

UART7_RX_M0

SAI0_LRCK_M0

ETH0_RXD2_M1




GPIO2_B7_d

7

8

GPIO2_B0_d(UART1_TXD)




SDMMC1_D2_M1

ETH0_TXD1_M1

SAI0_SDI0_M0

PDM0_SDI3_M3

UART1_TX_M1







GND

9

10

GPIO2_B1_d(UART1_RXD)




SDMMC1_D3_M1

ETH0_TXD0_M1

SAI0_SDI1_M0

PDM0_SDI2_M3

UART1_RX_M1










I2C8_SCL_M2

UART8_RTSN_M1

UART7_TX_M0

SAI0_SCLK_M0




-




GPIO2_B6_d

11

12

GPIO2_C7_d




ETH1_TXD1_M0

SAI4_LRCK_M3

UART4_RTSN_M0

I2C5_SDA_M2

PWM0_CH1_M2










SPI4_CSN1_M3

I2C4_SCL_M2

UART8_TX_M1

SAI0_SDO0_M0




SDMMC1_D0_M1




GPIO2_A6_d

13

14

GND




-

I2C4_SDA_M2

UART8_RX_M1

SAI0_SDO1_M0




SDMMC1_D1_M1




GPIO2_A7_d

15

16

GPIO4_A4_d

SAI4_SCLK_M0

PDM1_SDI3_M1

-

SPI3_MOSI_M2

UART6_TX_M0

I2C4_SCL_M1

CAN0_TX_M2




VCC 3V3

17

18

GPIO4_A6_d

SAI4_LRCK_M0

PDM1_CLK0_M1

-

SPI3_MISO_M2

UART6_RX_M0

I2C4_SDA_M1

CAN0_RX_M2




PWM1_CH2_M2

SPI1_MOSI_M1

UART11_CTSN_M1

PDM1_SDI2_M0

SAI2_SCLK_M1

-







GPIO2_C2_d(SPI_MOSI)

19

20

GND




PWM0_CH0_M2

SPI1_MISO_M1

UART11_RTSN_M1

PDM1_SDI3_M0

SAI2_LRCK_M1







GPIO2_C3_d(SPI_MISO)

21

22

GPIO2_B4_d




SDMMC1_PWREN_M1

ETH0_TXD2_M1

SAI0_SDI3_M0




UART7_CTSN_M0

SPI4_MOSI_M3

SATA0_ACTLED_M0




PWM1_CH4_M2

SPI1_CLK_M1

UART11_RX_M1

PDM1_CLK0_M0

SAI2_SDI_M1







GPIO2_C5_d(SPI_CLK)

23

24

GPIO2_C4_d(SPI_CE0)







SAI2_SDO_M1

PDM1_SDI0_M0

UART11_TX_M1

SPI1_CSN0_M1

PWM1_CH3_M2

-

GND

25

26

GPIO2_C1_d(SPI_CE1)







-

SAI2_MCLK_M1




UART9_TX_M0

SPI1_CSN1_M1

PWM1_CH1_M2




I2C7_SDA_M1

SPI3_MOSI_M0

UART3_RX_M0

SAI3_LRCK_M2

ETH0_MDC_M1







GPIO3_A1_d(I2C7_SDA)

27

28

GPIO3_A0_d(I2C7_SCL)







-

SAI3_SCLK_M2

UART3_TX_M0

SPI3_CLK_M0

I2C7_SCL_M1

-






 

PWM2_CH5_M2

I2C9_SCL_M2

UART6_CTSN_M1







GPIO2_D5_d

29

30

GND






 

PWM2_CH4_M2

I2C9_SDA_M2

UART6_RTSN_M1







GPIO2_D4_d

31

32

GPIO2_B5_d




SDMMC1_DETN_M1

ETH0_RXCLK_M1

SAI0_MCLK_M0

PDM0_CLK0_M3

UART7_RTSN_M0

SPI4_MISO_M3

SATA1_ACTLED_M0






 

PWM2_CH3_M2

I3C1_SDA_M0

UART6_RX_M1







GPIO2_D3_d(PWM)

33

34

GND







PWM1_CH5_M2

I2C5_SCL_M2

UART4_CTSN_M0

SAI4_SCLK_M3







GPIO2_C6_d

35

36

GPIO2_D2_d

CAM_CLK0_OUT_M1




SAI4_MCLK_M3

UART6_TX_M1

I3C1_SCL_M0

PWM2_CH2_M2







PWM2_CH7_M2

SPI3_CSN1_M0

UART9_CTSN_M0

-

SAI0_SDO3_M0

ETH_CLK0_25M_OUT_M1




CAM_CLK2_OUT_M1

GPIO2_D7_d

37

38

GPIO2_D0_d

-
 

SAI4_SDI_M3

UART4_TX_M0

I2C6_SCL_M2

PWM2_CH0_M2







GND

39

40

GPIO2_D1_d

ETH1_RXD0_M0

SAI4_SDO_M3

UART4_RX_M0

I2C6_SDA_M2

PWM2_CH1_M2

-






GPIO Test

Hardware Connection

  1. Install the GPIO terminal expansion board directly onto the 40Pin header. When a signal is driven high or low, the LED corresponding to that Pin turns on or off.

../../_images/image_C2QFb8QbhoKdpFxVIOZc2pELn4d.webp

Note: There are currently 16 general-purpose GPIOs. The pins corresponding to the following multiplexed functions are occupied. You can use qpi-config to disable the corresponding multiplexed functions and use the pins as general-purpose GPIOs.

  • Pin3/5:I2C3

  • Pin8/10:UART1

  • Pin19/21/23/24/26:SPI1

  • Pin27/28:I2C7

  • Pin33:PWM

  1. Use a multimeter to measure the voltage at the Pin corresponding to the GPIO. The high level should be 3.3V, and the low level should be 0V.

Test Method

Using Pin13(GPIO2_A6_d) as an example, enter adb shell and use the gpiod commands to test the GPIO.

adb shell


# List GPIO controllers
gpiodetect


# View Pin13 GPIO2_A6 information
gpioinfo -c gpiochip2 6


# Read the current level
gpioget -c gpiochip2 6


# Drive high for 10 seconds
gpioset -c gpiochip2 -t 10s,0 6=1


# Drive low for 10 seconds
gpioset -c gpiochip2 -t 10s,0 6=0

GPIO Interrupt Test Method: Connect the GPIO under test to a button or an external signal source so that the pin level changes. Use gpiomon to monitor rising-edge and falling-edge events on the GPIO. GPIO2_A6_d is again used as the example here.

# Read the current level of GPIO2_A6
gpioget -c gpiochip2 6


# Monitor rising-edge and falling-edge events on GPIO2_A6 and exit after 5 events
gpiomon -n 5 -e rising  -c gpiochip2 6
gpiomon -n 5 -e falling -c gpiochip2 6

I2C Test

Environment Sensor HAT Test:

pin3 and pin5 of the 40-pin header are configured as the I2C data and clock pins by default. To test the I2C interface, an external I2C device is required. This test uses the Waveshare Environment Sensor HAT, whose corresponding device node is /dev/i2c- 3。

This test uses the Waveshare Environment Sensor HAT, connected through the 40-pin header.

Hardware Connection Diagram:

../../_images/image_M0U8bVhsqouXilxKWjRcytPmnJc.webp
                                              Quectel Pi M2 with the expansion board installed

I2C Interface Verification (Waveshare Environment Sensor HAT)

Bus and Device Detection

adb shell
ls /dev/i2c*            # Confirm that /dev/i2c-3 exists
i2cdetect -y 3          # 0x29 0x53 0x68 0x76 should be listed (this board also lists 0x28)

Read the ID Register of Each IC

# TSL2591: ID reg 0x12 requires command bit 0xA0; therefore, 0xA0|0x12=0xB2
i2cget -y 3 0x29 0xB2 b     # Expected: 0x50
# BME280:ID reg 0xD0
i2cget -y 3 0x76 0xD0 b     # Expected: 0x60
# LTR390:ID reg 0x06
i2cget -y 3 0x53 0x06 b     # Expected: 0xB2
# IMU: Select bank 0 first, then read WHO_AM_I (reg 0x00)
i2cset -y 3 0x68 0x7F 0x00 b
i2cget -y 3 0x68 0x00 b     # Expected: 0xEA

Note: When the sensor board is connected directly to the 40PIN header, Pin27 and Pin28 are not supported. The board’s 40PIN header connects only to the IIC signals on Pin3 and Pin5, not Pin27 and Pin28. Pin27 and Pin28 can be connected using jumper wires.

SPI Test

On the 40-pin header, Pin19/21/23/24/26 are configured for the SPI function by default. The corresponding device node is SPI1 。

View Serial Devices
Use the following command to view all serial devices in the system:

ls /dev/spidev*


/dev/spidev1.0  /dev/spidev1.1 

SPI Loopback Test

Hardware connection: Pin19 MOSI and Pin21 MISO must be shorted together.

#!/bin/bash
# SPI loopback test (extracted from 40Pin-ctrl_M2.sh)
#
# Usage:
#   ./spi_test.sh
#
# Wiring: connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO).
# Tests /dev/spidev1.0 and /dev/spidev1.1 at 100 kHz, 8 bits/word, mode 0.
# NOTE: MOSI/MISO loopback verifies SPI transfer and CLK.
#       Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer.


die()
{
    echo "ERROR: $*" >&2
    exit 1
}


require_root()
{
    [ "$(id -u)" -eq 0 ] || die "must be run as root"
}


require_command()
{
    command -v "$1" >/dev/null 2>&1 || die "$1 is not installed"
}


spi_test()
{
    require_command python3
    [ -c /dev/spidev1.0 ] || die "/dev/spidev1.0 does not exist"
    [ -c /dev/spidev1.1 ] || die "/dev/spidev1.1 does not exist"


    echo "Connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO)."
    python3 - /dev/spidev1.0 /dev/spidev1.1 <<'PY'
import ctypes
import fcntl
import os
import struct
import sys


SPI_IOC_WR_MODE = 0x40016B01
SPI_IOC_WR_BITS_PER_WORD = 0x40016B03
SPI_IOC_WR_MAX_SPEED_HZ = 0x40046B04
SPI_IOC_MESSAGE_1 = 0x40206B00
PAYLOAD = bytes((0x55, 0xAA, 0x00, 0xFF, 0x12, 0x34, 0x56, 0x78))


def test_device(device):
    tx = (ctypes.c_ubyte * len(PAYLOAD))(*PAYLOAD)
    rx = (ctypes.c_ubyte * len(PAYLOAD))()
    transfer = bytearray(struct.pack(
        "=QQIIHBBBBBB",
        ctypes.addressof(tx), ctypes.addressof(rx), len(PAYLOAD),
        100000, 0, 8, 0, 0, 0, 0, 0,
    ))


    fd = os.open(device, os.O_RDWR)
    try:
        fcntl.ioctl(fd, SPI_IOC_WR_MODE, struct.pack("=B", 0))
        fcntl.ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, struct.pack("=B", 8))
        fcntl.ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, struct.pack("=I", 100000))
        fcntl.ioctl(fd, SPI_IOC_MESSAGE_1, transfer)
    finally:
        os.close(fd)


    received = bytes(rx)
    print("%s TX: %s" % (device, PAYLOAD.hex(" ").upper()))
    print("%s RX: %s" % (device, received.hex(" ").upper()))
    if received != PAYLOAD:
        raise RuntimeError("%s loopback data mismatch" % device)
    print("PASS: %s loopback data matched" % device)


try:
    for path in sys.argv[1:]:
        test_device(path)
except (OSError, RuntimeError) as exc:
    print("FAIL: %s" % exc, file=sys.stderr)
    sys.exit(1)
PY
    status=$?
    [ "$status" -eq 0 ] || return "$status"


    echo "NOTE: MOSI/MISO loopback verifies SPI transfer and CLK."
    echo "      Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer."
}


require_root
spi_test


The script tests /dev/spidev1.0 and /dev/spidev1.1, prints the TX/RX data, and compares the results.

Test Result:

Connect Pin 19 (SPI1 MOSI) to Pin 21 (SPI1 MISO).
/dev/spidev1.0 TX: 55 AA 00 FF 12 34 56 78
/dev/spidev1.0 RX: 55 AA 00 FF 12 34 56 78
PASS: /dev/spidev1.0 loopback data matched
/dev/spidev1.1 TX: 55 AA 00 FF 12 34 56 78
/dev/spidev1.1 RX: 55 AA 00 FF 12 34 56 78
PASS: /dev/spidev1.1 loopback data matched
NOTE: MOSI/MISO loopback verifies SPI transfer and CLK.
      Verify Pin 24/26 CS waveforms with an oscilloscope or logic analyzer.

UART Test

On the 40-pin header, pin8 and pin10 are configured for the UART function by default. The corresponding device node is /dev/ttyS 1。

View Serial Devices
Use the following command to view all serial devices in the system:

ls /dev/tty*

UART Loopback Test

This test verifies serial-port transmit and receive operation by shorting pin8 and pin10 together.

Hardware connection: Short pin8(TX) and pin10(RX) on the 40-pin header together.

Create the uart_loopback_test.sh file with the following content:

#!/bin/bash
# UART loopback test (extracted from 40Pin-ctrl_M2.sh)
#
# Usage:
#   ./uart_loopback_test.sh [-n 1] [-b 115200]
#       -n <num>  UART number, maps to /dev/ttyS<num>  (default 1)
#       -b <bps>  baud rate                          (default 115200)
#
# Wiring: connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX).


die()
{
    echo "ERROR: $*" >&2
    exit 1
}


require_root()
{
    [ "$(id -u)" -eq 0 ] || die "must be run as root"
}


require_command()
{
    command -v "$1" >/dev/null 2>&1 || die "$1 is not installed"
}


uart_test()
{
    device=/dev/ttyS1
    baud=115200


    while [ "$#" -gt 0 ]; do
        case "$1" in
        -n)
            [ "$#" -ge 2 ] || die "missing UART number after -n"
            case "$2" in
            ''|*[!0-9]*) die "invalid UART number: $2" ;;
            esac
            device="/dev/ttyS$2"
            shift 2
            ;;
        -b)
            [ "$#" -ge 2 ] || die "missing baud rate after -b"
            case "$2" in
            ''|*[!0-9]*) die "invalid baud rate: $2" ;;
            esac
            baud=$2
            shift 2
            ;;
        *)
            die "usage: $0 [-n 1] [-b 115200]"
            ;;
        esac
    done


    [ -c "$device" ] || die "$device does not exist"
    require_command stty
    require_command timeout
    require_command cmp


    echo "Connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX)."
    echo "Testing $device at $baud baud..."


    tmp_dir=$(mktemp -d /tmp/uart-loopback.XXXXXX) || die "cannot create temporary directory"
    trap 'rm -rf "$tmp_dir"' EXIT INT TERM
    printf 'UART1_LOOPBACK\n' > "$tmp_dir/expected"


    stty -F "$device" "$baud" raw -echo cs8 -cstopb -parenb \
        -crtscts -ixon -ixoff || die "failed to configure $device"


    timeout 5 dd if="$device" of="$tmp_dir/received" bs=1 count=15 status=none &
    rx_pid=$!
    sleep 1
    printf 'UART1_LOOPBACK\n' > "$device"
    wait "$rx_pid"
    rx_status=$?


    if [ "$rx_status" -ne 0 ]; then
        die "UART loopback timed out or received incomplete data"
    fi
    if ! cmp -s "$tmp_dir/expected" "$tmp_dir/received"; then
        echo "Received bytes:"
        od -An -tx1 "$tmp_dir/received"
        die "UART loopback data mismatch"
    fi


    echo "PASS: UART loopback sent and received 15 identical bytes"
    rm -rf "$tmp_dir"
    trap - EXIT INT TERM
}


require_root
uart_test "$@"

Usage (on the board):

# Make the script executable
chmod +x uart_loopback_test.sh 
# Default: /dev/ttyS1, 115200; wiring: Pin8(TX)<->Pin10(RX)
./uart_loopback_test.sh
# Specify the UART number and baud rate
./uart_loopback_test.sh -n 1 -b 9600

Test Result:

Connect Pin 8 (UART1 TX) to Pin 10 (UART1 RX).
Testing /dev/ttyS1 at 115200 baud...
PASS: UART loopback sent and received 15 identical bytes

PWM Test

Wiring: Pin33 (PWM, GPIO2_D3) ↔ Pin31 (GPIO92, GPIO2_D4) (default)

Create the pwm_test.sh file with a duty cycle of 50% and a frequency of 1000Hz, using the following content:

#!/bin/bash
# PWM output + GPIO edge capture test (extracted from 40Pin-ctrl_M2.sh)
#
# Usage:
#   ./pwm_test.sh [capture-gpio]
#       capture-gpio  global GPIO number to capture PWM edges on
#                     (default 92 = Pin 31; 93 = Pin 29)
#
# Wiring: connect Pin 33 (PWM2_CH3, GPIO2_D3) to the capture GPIO.
#         default Pin 31 / GPIO92 / GPIO2_D4 ;  93 = Pin 29 / GPIO2_D5.
# Output: 1000 Hz, 50% duty cycle.
# PASS criteria: measured 900-1100 Hz and 40-60% duty cycle, >=3 complete periods.


GPIO_SYSFS=/sys/class/gpio


die()
{
    echo "ERROR: $*" >&2
    exit 1
}


require_root()
{
    [ "$(id -u)" -eq 0 ] || die "must be run as root"
}


require_command()
{
    command -v "$1" >/dev/null 2>&1 || die "$1 is not installed"
}


pwm_test()
{
    pwm_chip=
    capture_gpio=${1:-92}
    case "$capture_gpio" in
    ''|*[!0-9]*) die "invalid capture GPIO: $capture_gpio" ;;
    esac
    capture_chip=gpiochip$((capture_gpio / 32))
    capture_line=$((capture_gpio % 32))
    tmp_file=$(mktemp /tmp/pwm-test.XXXXXX) || die "cannot create temporary file"


    require_command gpiomon
    require_command timeout
    require_command awk
    for chip in /sys/class/pwm/pwmchip*; do
        case "$(readlink -f "$chip/device" 2>/dev/null)" in
        */2ade3000.pwm) pwm_chip=$chip; break ;;
        esac
    done
    [ -n "$pwm_chip" ] || die "PWM2_CH3 controller 2ade3000.pwm was not found"
    pwm=$pwm_chip/pwm0


    if [ -d "$GPIO_SYSFS/gpio$capture_gpio" ]; then
        echo "$capture_gpio" > "$GPIO_SYSFS/unexport" 2>/dev/null || \
            die "GPIO$capture_gpio is busy"
    fi
    if [ ! -d "$pwm" ]; then
        echo 0 > "$pwm_chip/export" 2>/dev/null || die "cannot export pwm0"
    fi


    pwm_cleanup()
    {
        if [ -e "$pwm/enable" ] && [ "$(cat "$pwm/enable" 2>/dev/null)" = 1 ]; then
            echo 0 > "$pwm/enable" 2>/dev/null || true
        fi
        rm -f "$tmp_file"
    }
    pwm_exit()
    {
        exit_status=$?
        trap - EXIT INT TERM
        pwm_cleanup
        exit "$exit_status"
    }
    trap pwm_exit EXIT
    trap 'exit 130' INT
    trap 'exit 143' TERM


    if [ "$(cat "$pwm/enable" 2>/dev/null)" = 1 ]; then
        echo 0 > "$pwm/enable" 2>/dev/null || die "cannot disable pwm0"
    fi
    if [ "$(cat "$pwm/period" 2>/dev/null)" != 0 ]; then
        echo 0 > "$pwm/duty_cycle" 2>/dev/null || die "cannot reset PWM duty cycle"
    fi
    echo 1000000 > "$pwm/period" 2>/dev/null || die "cannot set PWM period"
    echo 500000 > "$pwm/duty_cycle" 2>/dev/null || die "cannot set PWM duty cycle"


    if [ "$capture_gpio" -eq 92 ]; then
        echo "Connect Pin 33 (PWM, GPIO2_D3) to Pin 31 (GPIO92, GPIO2_D4)."
    elif [ "$capture_gpio" -eq 93 ]; then
        echo "Connect Pin 33 (PWM, GPIO2_D3) to Pin 29 (GPIO93, GPIO2_D5)."
    else
        echo "Connect Pin 33 (PWM, GPIO2_D3) to GPIO$capture_gpio."
    fi
    echo "Output: 1000 Hz, 50% duty cycle"
    timeout 5 gpiomon -c "$capture_chip" -n 21 \
        "$capture_line" > "$tmp_file" 2>&1 &
    monitor_pid=$!
    sleep 1
    echo 1 > "$pwm/enable" 2>/dev/null || die "cannot enable pwm0"
    wait "$monitor_pid"
    monitor_status=$?
    echo 0 > "$pwm/enable" 2>/dev/null || true


    [ "$monitor_status" -eq 0 ] || die "no valid PWM edges captured on GPIO$capture_gpio"


    awk -v capture_gpio="$capture_gpio" '
        $2 == "rising" {
            if (have_rise && have_fall) {
                period = $1 - prev_rise
                high = fall_time - rise_time
                if (period > 0 && high >= 0) {
                    period_sum += period
                    high_sum += high
                    samples++
                }
            }
            prev_rise = $1
            rise_time = $1
            have_rise = 1
            have_fall = 0
        }
        $2 == "falling" && have_rise {
            fall_time = $1
            have_fall = 1
        }
        END {
            if (samples < 3) {
                print "FAIL: insufficient complete PWM periods" > "/dev/stderr"
                exit 1
            }
            frequency = samples / period_sum
            duty = 100 * high_sum / period_sum
            printf "Measured: %.2f Hz, %.2f%% duty cycle (%d periods)\n", frequency, duty, samples
            if (frequency < 900 || frequency > 1100 || duty < 40 || duty > 60) {
                print "FAIL: PWM measurement is outside the expected range" > "/dev/stderr"
                exit 1
            }
            printf "PASS: PWM output and GPIO%d capture are working\n", capture_gpio
        }
    ' "$tmp_file" || exit 1


    pwm_cleanup
    trap - EXIT INT TERM
}


require_root
pwm_test "${1:-92}"

Usage (on the board):

# Make the script executable
chmod +x pwm_test.sh 
pwm_test.sh        # Capture GPIO92 = Pin31
pwm_test.sh 93     # Capture GPIO93 = Pin29 (reconnect Pin33↔Pin29)
pwm_test.sh 86     # Any global GPIO number (reconnect the corresponding pin)

Test Result:

Connect Pin 33 (PWM, GPIO2_D3) to Pin 31 (GPIO92, GPIO2_D4).
Output: 1000 Hz, 50% duty cycle
Measured: 1000.35 Hz, 49.98% duty cycle (10 periods)
PASS: PWM output and GPIO92 capture are working