若该文为原创文章,转载请注明原文出处。
前言
在嵌入式Linux开发中,4G模块的适配是一个常见但容易踩坑的环节。最近我们在SigmaStar平台上适配移远(Quectel)EC200T 4G模块时,遇到了一系列问题:模块识别不了、AT命令端口找不到、应用程序被日志端口"冲垮"......本文将完整记录整个排查与解决过程,希望对遇到类似问题的开发者有所帮助。
一、问题背景
-
硬件平台:SigmaStar SSD201(ARM Cortex-A7)
-
操作系统:Linux 4.9.84
-
4G模块 :移远 EC200T(USB VID/PID =
2c7c:6026) -
已有兼容模块 :移远 EC25(VID/PID =
2c7c:0125)
项目原本使用 EC25 模块,工作正常。切换到 EC200T 后,系统无法识别模块,/dev/ttyUSB* 设备节点没有生成。
二、问题排查过程
2.1 第一步:检查 USB 硬件识别
插入模块后,执行 dmesg | tail 查看内核日志:
usb 2-1: new high-speed USB device number 2 using Sstar-ehci-1 usb 2-1: New USB device found, idVendor=2c7c, idProduct=6026 usb 2-1: Product: Android
结论:USB 物理层识别正常,硬件没有问题。
2.2 第二步:检查驱动绑定
查看 /dev/ttyUSB*,发现没有任何节点生成。原因是 Linux 内核的 option 驱动虽然支持移远模块,但 EC200T 的 PID 0x6026 不在默认的设备 ID 表中。
内核 option 驱动中的 ID 表:
/* Quectel products using Quectel vendor ID */ #define QUECTEL_PRODUCT_EC21 0x0121 #define QUECTEL_PRODUCT_EC25 0x0125 #define QUECTEL_PRODUCT_BG96 0x0296 // 0x6026 不在表中!
2.3 第三步:手动绑定驱动
执行以下命令,将 EC200T 的 VID/PID 动态添加到 option 驱动:
echo "2c7c 6026" > /sys/bus/usb-serial/drivers/option1/new_id
此时内核日志显示:
option 2-1:1.0: GSM modem (1-port) converter detected option 2-1:1.1: GSM modem (1-port) converter detected option 2-1:1.2: GSM modem (1-port) converter detected usb 2-1: GSM modem (1-port) converter now attached to ttyUSB2 usb 2-1: GSM modem (1-port) converter now attached to ttyUSB3 usb 2-1: GSM modem (1-port) converter now attached to ttyUSB4
成功! /dev/ttyUSB2、/dev/ttyUSB3、/dev/ttyUSB4 都生成了。
三、新的问题:AT命令端口在哪里?
3.1 端口映射的差异
原本 EC25 模块的 AT 命令端口是 /dev/ttyUSB2。但向 EC200T 的 /dev/ttyUSB2 发送 AT 命令后,收到的却是大量模块内部日志:
[QUEC][POWER_MANAGER][quec_power_manager.c, quec_sleep_flag_check, 385] pm_usb_busy=1 not allow sleep [QUEC][ATCMD_NW][quec_atcmd_network.c, quec_regstatus_asr2quec, 400] asr_status=11
原因 :EC200T 的 ttyUSB2 被固件配置为 调试日志输出口 (Debug UART),而 AT 命令口在 /dev/ttyUSB3。
⚠️ 重要提醒 :不同移远模块、甚至同一型号不同固件版本,USB 接口映射都可能不同。EC25 用
ttyUSB2,EC200T 用ttyUSB3,不能硬编码端口号。
3.2 误开日志端口的后果
如果应用程序直接打开 ttyUSB2,会不断收到海量调试日志(每秒几十条),导致:
-
串口缓冲区溢出,程序卡死
-
CPU 被频繁中断占用
-
程序"崩溃"(实际上是逻辑被垃圾数据冲垮)
四、解决方案
4.1 方案一:USB PID 精准识别(推荐)
通过读取 /sys/bus/usb/devices/ 下的 idProduct,判断当前插入的是哪个型号的模块,然后选择正确的 AT 端口。
Shell 脚本实现(启动脚本)
bash
#!/bin/sh
# 绑定驱动(兼容多个型号)
echo "2c7c 6026" > /sys/bus/usb-serial/drivers/option1/new_id 2>/dev/null
echo "2c7c 0125" > /sys/bus/usb-serial/drivers/option1/new_id 2>/dev/null
# 通过 PID 识别模块类型
PID=$(cat /sys/bus/usb/devices/*/idProduct 2>/dev/null | grep -E "0125|6026" | head -1)
case "$PID" in
"0125")
AT_PORT="/dev/ttyUSB2" # EC25 用 USB2
echo "Detected EC25, using $AT_PORT"
;;
"6026")
AT_PORT="/dev/ttyUSB3" # EC200T 用 USB3
echo "Detected EC200T, using $AT_PORT"
;;
*)
AT_PORT="/dev/ttyUSB3" # 默认尝试新模块
echo "Unknown module, fallback to $AT_PORT"
;;
esac
# 将端口传递给主程序
export AT_PORT
C 语言实现(嵌入应用程序)
#include <stdio.h>
#include <string.h>
#include <dirent.h>
const char* get_at_port_by_pid() {
DIR *dir;
struct dirent *entry;
FILE *fp;
char path[256];
char pid[10];
const char *default_port = "/dev/ttyUSB3";
dir = opendir("/sys/bus/usb/devices/");
if (!dir) return default_port;
while ((entry = readdir(dir)) != NULL) {
if (strstr(entry->d_name, ":") == NULL) continue;
// 检查 Vendor ID 是否为 Quectel (2c7c)
snprintf(path, sizeof(path), "/sys/bus/usb/devices/%s/idVendor", entry->d_name);
fp = fopen(path, "r");
if (fp) {
char vendor[10];
fgets(vendor, sizeof(vendor), fp);
fclose(fp);
if (strncmp(vendor, "2c7c", 4) != 0) continue;
} else {
continue;
}
// 读取 Product ID
snprintf(path, sizeof(path), "/sys/bus/usb/devices/%s/idProduct", entry->d_name);
fp = fopen(path, "r");
if (!fp) continue;
fgets(pid, sizeof(pid), fp);
fclose(fp);
pid[strcspn(pid, "\n")] = '\0';
if (strcmp(pid, "0125") == 0) {
closedir(dir);
return "/dev/ttyUSB2"; // EC25
} else if (strcmp(pid, "6026") == 0) {
closedir(dir);
return "/dev/ttyUSB3"; // EC200T
}
}
closedir(dir);
return default_port;
}
4.2 方案二:安全探测(备选)
如果不想依赖 PID 表(比如未来可能换其他品牌模块),可以用带超时的安全探测,避免被日志端口冲垮。
关键原则:
-
非阻塞打开 (
O_NONBLOCK) -
发送前清空缓冲区 (
tcflush) -
严格超时控制 (
VMIN=0, VTIME=5,即 500ms 超时) -
限定读取长度(最多读 64 字节)
int safe_at_probe(const char *port) {
int fd = open(port, O_RDWR | O_NONBLOCK | O_NOCTTY);
if (fd < 0) return 0;// 清空旧数据 tcflush(fd, TCIOFLUSH); // 设置超时:VMIN=0, VTIME=5 (500ms) struct termios tty; tcgetattr(fd, &tty); tty.c_cc[VMIN] = 0; tty.c_cc[VTIME] = 5; cfsetispeed(&tty, B115200); cfsetospeed(&tty, B115200); tcsetattr(fd, TCSANOW, &tty); // 发送 AT write(fd, "AT\r\n", 4); // 读取响应,最多 64 字节 char buf[64]; int n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n > 0) { buf[n] = '\0'; return strstr(buf, "OK") != NULL; } return 0;}
五、其他踩坑记录
5.1 开机自启动绑定
手动 echo 绑定只在当前运行有效,重启后失效。需要在启动脚本中添加:
# /customer/drv_init.sh 或 /etc/init.d/rcS echo "2c7c 6026" > /sys/bus/usb-serial/drivers/option1/new_id
5.2 关于 malloc 断言失败
日志中出现的 __libc_malloc: Assertion 错误与 4G 模块无关,是应用程序自身的内存管理 bug(如内存越界、double free),需要单独用 valgrind 或 AddressSanitizer 定位。
六、经验总结
| 要点 | 说明 |
|---|---|
| 不要硬编码端口号 | EC25 用 ttyUSB2,EC200T 用 ttyUSB3,不同型号/固件映射不同 |
| 通过 PID 识别最可靠 | 读取 /sys/bus/usb/devices/*/idProduct,零风险 |
| 不要直接打开日志端口 | 日志口会源源不断输出调试信息,冲垮应用程序 |
| 开机自动绑定 | 将 echo 命令加入启动脚本,避免重启后失效 |
| 启动脚本要容错 | 检查文件是否存在,失败时 sleep,避免疯狂重启 |
七、附录:EC200T 常用 AT 命令
找到正确的 AT 端口(ttyUSB3)后,可以测试:
# 设置波特率 stty -F /dev/ttyUSB3 115200 raw -echo # 发送 AT echo -e "AT\r\n" > /dev/ttyUSB3 cat /dev/ttyUSB3 & # 预期返回:OK # 查询信号强度 echo -e "AT+CSQ\r\n" > /dev/ttyUSB3 # 查询运营商 echo -e "AT+COPS?\r\n" > /dev/ttyUSB3
八、后续工作
-
将 PID 识别逻辑合并到主程序,彻底废弃硬编码
-
使用
valgrind定位主程序的 malloc 崩溃点 -
配置
pppd实现 4G 自动拨号 -
增加断线重连机制
如有侵权,或需要完整代码,请及时联系博主。