Robotics
Cấu hình Raspberry Pi Zero 2 W dùng đồng thời USB Ethernet và USB Serial

Cấu hình Raspberry Pi Zero 2 W dùng đồng thời USB Ethernet và USB Serial

Sau khi hoàn thành bài hướng dẫn trước, Raspberry Pi đã có thể kết nối với máy tính qua USB Ethernet để SSH tại địa chỉ:

10.12.194.1

Tuy nhiên, cấu hình mặc định của rpi-usb-gadget chỉ tạo USB Ethernet. Trong một số trường hợp, đặc biệt khi kết nối Raspberry Pi với Android (trong dự án cá nhân của tôi), điện thoại có thể không nhận Raspberry Pi như một card mạng.

Bài này cấu hình Raspberry Pi thành một USB composite gadget, cho phép cùng một cổng USB cung cấp hai chức năng:

Raspberry Pi Zero 2 W
│
└── USB composite gadget
    ├── RNDIS Ethernet
    │   └── usb0
    │       └── SSH và SFTP với Windows
    │
    └── CDC-ACM Serial
        └── /dev/ttyGS0
            └── Truyền dữ liệu với Android

Kết quả cuối cùng:

  • Máy tính Windows vẫn SSH vào Raspberry Pi qua 10.12.194.1.
  • Android giao tiếp trực tiếp với Raspberry Pi qua USB serial.
  • Không cần Wi-Fi cho đường truyền dữ liệu giữa Android và Raspberry Pi.
  • Có thể gửi lệnh, JSON, kết quả cảm biến, trạng thái hệ thống và dữ liệu nhị phân nhỏ.

Hướng dẫn đã được thử nghiệm trên Raspberry Pi Zero 2 W chạy Raspberry Pi OS Trixie.

Chuẩn bị

Cần có:

  • Raspberry Pi Zero 2 W.
  • Raspberry Pi OS Trixie.
  • rpi-usb-gadget đã được cài theo bài hướng dẫn trước.
  • Raspberry Pi vẫn truy cập được qua Wi-Fi trong lúc thay đổi cấu hình.
  • Cáp micro-USB có hỗ trợ truyền dữ liệu.
  • Cáp hoặc đầu chuyển USB-C OTG khi kết nối với Android.

Trên Raspberry Pi Zero 2 W, phải sử dụng cổng micro-USB có nhãn USB, nằm gần cổng mini-HDMI. Không sử dụng cổng PWR IN để truyền dữ liệu.

Kiểm tra cấu hình hiện tại

Trước khi thay đổi, kiểm tra USB Ethernet đang hoạt động:

nmcli connection show --active

Kết quả thường có:

USB Gadget (shared)    ethernet    usb0

Kiểm tra địa chỉ IP:

ip -4 addr show usb0

Kết quả cần có địa chỉ:

10.12.194.1/28

Kiểm tra module USB gadget:

lsmod | grep -E "g_ether|dwc2|libcomposite"

Với cấu hình mặc định của rpi-usb-gadget, thường sẽ thấy:

g_ether

Module g_ether chỉ cung cấp USB Ethernet. Để có thêm USB serial, cần thay nó bằng một composite gadget tạo qua ConfigFS.

Sao lưu cấu hình USB hiện tại

Sao lưu cấu hình module:

sudo cp \
  /etc/modules-load.d/usb-gadget.conf \
  /etc/modules-load.d/usb-gadget.conf.backup

Sao lưu cấu hình khởi động:

sudo cp \
  /boot/firmware/config.txt \
  /boot/firmware/config.txt.backup

Kiểm tra overlay USB OTG:

grep -n "dtoverlay=dwc2" /boot/firmware/config.txt

Kết quả cần có dạng:

dtoverlay=dwc2,dr_mode=peripheral

Nếu chưa có, thêm vào cuối file:

echo "dtoverlay=dwc2,dr_mode=peripheral" \
  | sudo tee -a /boot/firmware/config.txt

Thay g_ether bằng libcomposite

Ghi lại danh sách module được nạp khi Raspberry Pi khởi động:

printf "dwc2\nlibcomposite\n" \
  | sudo tee /etc/modules-load.d/usb-gadget.conf

Nội dung file sau khi thay đổi:

dwc2
libcomposite

Từ bước này, không chạy lại:

sudo rpi-usb-gadget on

Lệnh trên có thể khôi phục cấu hình g_ether, làm mất composite gadget vừa tạo.

Các NetworkManager profile của rpi-usb-gadget vẫn được giữ lại để quản lý interface usb0.

Tạo script USB composite gadget

Tạo script:

sudo nano /usr/local/sbin/usb-composite-gadget

Dán nội dung sau:

#!/usr/bin/env bash
 
set -euo pipefail
 
GADGET_NAME="pi_composite"
GADGET_ROOT="/sys/kernel/config/usb_gadget"
GADGET="${GADGET_ROOT}/${GADGET_NAME}"
 
stop_gadget() {
    if [[ ! -d "${GADGET}" ]]; then
        return 0
    fi
 
    if [[ -e "${GADGET}/UDC" ]]; then
        printf '' > "${GADGET}/UDC" 2>/dev/null || true
    fi
 
    find "${GADGET}/configs" -type l -delete 2>/dev/null || true
 
    if [[ -L "${GADGET}/os_desc/c.1" ]]; then
        rm -f "${GADGET}/os_desc/c.1"
    fi
 
    rmdir "${GADGET}/functions/acm.usb0" 2>/dev/null || true
    rmdir "${GADGET}/functions/rndis.usb0" 2>/dev/null || true
 
    rmdir "${GADGET}/configs/c.1/strings/0x409" \
        2>/dev/null || true
 
    rmdir "${GADGET}/configs/c.1" 2>/dev/null || true
    rmdir "${GADGET}/strings/0x409" 2>/dev/null || true
    rmdir "${GADGET}" 2>/dev/null || true
}
 
start_gadget() {
    modprobe libcomposite
 
    if ! mountpoint -q /sys/kernel/config; then
        mount -t configfs none /sys/kernel/config
    fi
 
    stop_gadget
 
    mkdir -p "${GADGET}"
    cd "${GADGET}"
 
    echo 0x1d6b > idVendor
    echo 0x0104 > idProduct
    echo 0x0100 > bcdDevice
    echo 0x0200 > bcdUSB
 
    echo 0xEF > bDeviceClass
    echo 0x02 > bDeviceSubClass
    echo 0x01 > bDeviceProtocol
 
    mkdir -p strings/0x409
 
    SERIAL="$(
        tr -d '\0' < /proc/device-tree/serial-number \
            2>/dev/null || true
    )"
 
    if [[ -z "${SERIAL}" ]]; then
        SERIAL="$(
            awk '/Serial/ {print $3}' /proc/cpuinfo
        )"
    fi
 
    echo "${SERIAL:-00000001}" \
        > strings/0x409/serialnumber
 
    echo "Raspberry Pi" \
        > strings/0x409/manufacturer
 
    echo "Pi Ethernet and Serial Gadget" \
        > strings/0x409/product
 
    mkdir -p configs/c.1/strings/0x409
 
    echo "RNDIS Ethernet + CDC ACM" \
        > configs/c.1/strings/0x409/configuration
 
    echo 250 > configs/c.1/MaxPower
 
    mkdir functions/rndis.usb0
 
    echo "02:12:34:56:78:01" \
        > functions/rndis.usb0/dev_addr
 
    echo "02:12:34:56:78:02" \
        > functions/rndis.usb0/host_addr
 
    mkdir functions/acm.usb0
 
    if [[ -d os_desc ]]; then
        echo 1 > os_desc/use
        echo 0xCD > os_desc/b_vendor_code
        echo "MSFT100" > os_desc/qw_sign
 
        RNDIS_DESC="functions/rndis.usb0/os_desc/interface.rndis"
 
        if [[ -d "${RNDIS_DESC}" ]]; then
            echo "RNDIS" \
                > "${RNDIS_DESC}/compatible_id"
 
            echo "5162001" \
                > "${RNDIS_DESC}/sub_compatible_id"
        fi
    fi
 
    ln -s functions/rndis.usb0 configs/c.1/
    ln -s functions/acm.usb0 configs/c.1/
 
    if [[ -d os_desc ]]; then
        ln -s configs/c.1 os_desc/c.1
    fi
 
    UDC="$(ls /sys/class/udc | head -n 1)"
 
    if [[ -z "${UDC}" ]]; then
        echo "Không tìm thấy USB Device Controller" >&2
        exit 1
    fi
 
    echo "${UDC}" > UDC
}
 
case "${1:-start}" in
    start)
        start_gadget
        ;;
    stop)
        stop_gadget
        ;;
    restart)
        stop_gadget
        start_gadget
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}" >&2
        exit 1
        ;;
esac

Lưu file và cấp quyền thực thi:

sudo chmod +x /usr/local/sbin/usb-composite-gadget

Script tạo hai USB function:

functions/rndis.usb0
functions/acm.usb0

Trong đó:

  • rndis.usb0 tạo card mạng USB cho Windows.
  • acm.usb0 tạo cổng serial /dev/ttyGS0 trên Raspberry Pi.

Hai function được liên kết vào cùng một configuration nên không tranh chấp USB Device Controller.

VID 0x1d6b và PID 0x0104 phù hợp cho thử nghiệm và phát triển. Khi phát triển sản phẩm thương mại, cần sử dụng USB VID/PID hợp lệ của sản phẩm.

Tạo systemd service

Tạo service:

sudo nano /etc/systemd/system/usb-composite-gadget.service

Dán nội dung:

[Unit]
Description=Raspberry Pi RNDIS and CDC ACM Composite Gadget
After=systemd-modules-load.service
Before=NetworkManager.service
 
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/usb-composite-gadget start
ExecStop=/usr/local/sbin/usb-composite-gadget stop
RemainAfterExit=yes
 
[Install]
WantedBy=multi-user.target

Service này tạo composite gadget mỗi khi Raspberry Pi khởi động.

Giữ lại dịch vụ quản lý USB Ethernet

Package rpi-usb-gadget đã tạo NetworkManager profile cho usb0, gồm:

USB Gadget (client)
USB Gadget (shared)

Để dịch vụ quản lý mạng chỉ chạy sau khi composite gadget đã tạo xong usb0, thêm systemd drop-in:

sudo mkdir -p \
  /etc/systemd/system/rpi-usb-gadget-ics.service.d

Tạo file:

sudo nano \
  /etc/systemd/system/rpi-usb-gadget-ics.service.d/composite.conf

Dán nội dung:

[Unit]
Requires=usb-composite-gadget.service
After=usb-composite-gadget.service NetworkManager.service

Kích hoạt cấu hình

Nạp lại systemd:

sudo systemctl daemon-reload

Bật composite gadget:

sudo systemctl enable usb-composite-gadget.service

Bảo đảm dịch vụ USB Ethernet vẫn được bật:

sudo systemctl enable rpi-usb-gadget-ics.service

Khởi động lại Raspberry Pi:

sudo reboot

Không nên chạy service composite bằng --now trước khi reboot vì module g_ether cũ có thể vẫn đang chiếm USB Device Controller.

Hoàn tác về USB Ethernet mặc định

Nếu cần quay lại cấu hình ban đầu của rpi-usb-gadget, chạy:

sudo systemctl disable usb-composite-gadget.service

Xóa systemd drop-in:

sudo rm -f \
  /etc/systemd/system/rpi-usb-gadget-ics.service.d/composite.conf

Khôi phục file module:

sudo cp \
  /etc/modules-load.d/usb-gadget.conf.backup \
  /etc/modules-load.d/usb-gadget.conf

Nạp lại systemd:

sudo systemctl daemon-reload

Khởi động lại:

sudo reboot

Sau khi khởi động lại, kiểm tra:

lsmod | grep g_ether
ip -br addr show usb0

Nếu cần, bật lại package:

sudo rpi-usb-gadget on
sudo reboot

Kết quả cuối cùng

Sau khi hoàn tất, Raspberry Pi Zero 2 W cung cấp đồng thời hai đường giao tiếp qua cùng một cổng USB:

Windows
   │
   └── RNDIS Ethernet
       ├── SSH
       ├── SFTP
       └── 10.12.194.1

Android
   │
   └── CDC-ACM Serial
       ├── USB Host API
       ├── Flutter usb_serial
       └── /dev/ttyGS0

Cách tổ chức này phù hợp khi:

  • Cần giữ đường SSH ổn định để phát triển và sửa lỗi.
  • Android không nhận Raspberry Pi như card USB Ethernet.
  • Ứng dụng Android cần truyền dữ liệu trực tiếp với Raspberry Pi.
  • Không muốn phụ thuộc vào Wi-Fi cho lệnh điều khiển và trạng thái hệ thống.

Bình luận & Cảm xúc