多Arduino(Nano Every)连接时如何程序化匹配对应ttyUSBx设备
程序化关联Arduino与ttyUSB设备的解决方案(无需udev权限)
场景说明
我有一台连接三台Arduino(Nano Every)的PC,每台Arduino运行相同程序,作为PC与对应设备的接口:
┌────────────────────────────────────┐ │ P C │ └───┬─────────────┬─────────────┬────┘ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ Arduino#1 │ │ Arduino#2 │ │ Arduino#3 │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ Device #1 │ │ Device #2 │ │ Device #3 │ └───────────┘ └───────────┘ └───────────┘
开发的C程序通过打开/dev/ttyUSBx设备与Arduino通信,但启动时需手动指定对应端口;使用者频繁插拔Arduino,手动查找不可行,且受限于管理员政策无法添加udev规则,需要程序化的方式自动关联Arduino与对应的ttyUSBx设备。
可行解决方案
1. 读取sysfs中的USB设备唯一标识(无需修改Arduino程序)
Linux下所有USB设备的硬件信息都存放在/sys文件系统中,可通过ttyUSBx对应的sysfs路径读取设备的序列号(每个Arduino唯一)、厂商ID、产品ID来识别设备。
实现步骤:
- 遍历系统中所有
/dev/ttyUSB*设备 - 对每个设备,通过
readlink()获取/sys/class/tty/ttyUSBx/device的实际路径,进而读取该路径下的serial文件获取设备序列号 - 预先记录三台Arduino的序列号,在程序中匹配即可关联对应
ttyUSBx
C代码示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <glob.h> #define SYSFS_BUF_SIZE 256 void get_arduino_serial(const char* tty_dev, char* serial_buf, size_t buf_len) { char sysfs_link[SYSFS_BUF_SIZE]; char device_path[SYSFS_BUF_SIZE]; // 构造sysfs符号链接路径(去掉/dev/前缀) snprintf(sysfs_link, sizeof(sysfs_link), "/sys/class/tty/%s/device", tty_dev + 5); ssize_t link_len = readlink(sysfs_link, device_path, sizeof(device_path) - 1); if (link_len == -1) { perror("readlink failed"); strcpy(serial_buf, "ERROR"); return; } device_path[link_len] = '\0'; // 构造序列号文件路径 char serial_path[SYSFS_BUF_SIZE]; snprintf(serial_path, sizeof(serial_path), "/sys/%s/serial", device_path); FILE* serial_file = fopen(serial_path, "r"); if (!serial_file) { perror("fopen serial file failed"); strcpy(serial_buf, "ERROR"); return; } fgets(serial_buf, buf_len, serial_file); fclose(serial_file); // 去除换行符 serial_buf[strcspn(serial_buf, "\n")] = '\0'; } int main() { glob_t usb_devs; int glob_ret = glob("/dev/ttyUSB*", GLOB_NOSORT, NULL, &usb_devs); if (glob_ret != 0) { perror("glob failed to find ttyUSB devices"); return 1; } // 预存三台Arduino的序列号 const char* target_serials[] = {"SERIAL_1", "SERIAL_2", "SERIAL_3"}; for (size_t i = 0; i < usb_devs.gl_pathc; i++) { char serial[64]; get_arduino_serial(usb_devs.gl_pathv[i], serial, sizeof(serial)); printf("Found %s, serial: %s\n", usb_devs.gl_pathv[i], serial); // 匹配预存序列号,关联对应设备 for (size_t j = 0; j < 3; j++) { if (strcmp(serial, target_serials[j]) == 0) { printf("Matched Device #%zu\n", j+1); // 这里可以保存该tty设备与Device的映射关系 } } } globfree(&usb_devs); return 0; }
2. 自定义串口握手协议(需修改Arduino程序)
如果可以修改Arduino的程序,可给每台设备烧录唯一标识(比如写入EEPROM或直接硬编码),在串口连接建立后通过握手指令获取标识,从而关联ttyUSBx。
Arduino端代码示例:
#include <EEPROM.h> // 每台Arduino烧录不同的设备ID,比如DEVICE_1/DEVICE_2/DEVICE_3 #define DEVICE_ID "DEVICE_1" #define EEPROM_ADDR 0 void setup() { Serial.begin(9600); while (!Serial); // 等待串口连接 // 首次烧录时写入EEPROM(之后可注释) // EEPROM.put(EEPROM_ADDR, DEVICE_ID); // EEPROM.commit(); } void loop() { if (Serial.available() > 0) { String cmd = Serial.readStringUntil('\n'); if (cmd.trim() == "IDENTIFY") { // 从EEPROM读取ID(如果写入过) // char stored_id[10]; // EEPROM.get(EEPROM_ADDR, stored_id); // Serial.println(stored_id); // 直接发送硬编码ID Serial.println(DEVICE_ID); } } }
C程序端识别代码示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <glob.h> #include <fcntl.h> #include <termios.h> int open_serial_port(const char* dev_path) { int fd = open(dev_path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd == -1) { perror("Failed to open serial port"); return -1; } // 配置串口参数(9600波特率,8位数据,无校验,1停止位) struct termios serial_opts; tcgetattr(fd, &serial_opts); cfsetispeed(&serial_opts, B9600); cfsetospeed(&serial_opts, B9600); serial_opts.c_cflag |= (CLOCAL | CREAD); serial_opts.c_cflag &= ~PARENB; serial_opts.c_cflag &= ~CSTOPB; serial_opts.c_cflag &= ~CSIZE; serial_opts.c_cflag |= CS8; tcsetattr(fd, TCSANOW, &serial_opts); sleep(2); // 等待Arduino重启完成 return fd; } void get_device_id(int serial_fd, char* id_buf, size_t buf_len) { // 发送识别指令 write(serial_fd, "IDENTIFY\n", 9); usleep(150000); // 等待设备响应 ssize_t read_len = read(serial_fd, id_buf, buf_len - 1); if (read_len > 0) { id_buf[read_len] = '\0'; id_buf[strcspn(id_buf, "\n")] = '\0'; // 去除换行符 } else { strcpy(id_buf, "UNKNOWN"); } } int main() { glob_t usb_devs; int glob_ret = glob("/dev/ttyUSB*", GLOB_NOSORT, NULL, &usb_devs); if (glob_ret != 0) { perror("glob failed to find ttyUSB devices"); return 1; } const char* target_ids[] = {"DEVICE_1", "DEVICE_2", "DEVICE_3"}; for (size_t i = 0; i < usb_devs.gl_pathc; i++) { int fd = open_serial_port(usb_devs.gl_pathv[i]); if (fd == -1) continue; char device_id[64]; get_device_id(fd, device_id, sizeof(device_id)); printf("Port %s -> Device ID: %s\n", usb_devs.gl_pathv[i], device_id); // 匹配目标ID,建立映射 for (size_t j = 0; j < 3; j++) { if (strcmp(device_id, target_ids[j]) == 0) { printf("Associated with Device #%zu\n", j+1); } } close(fd); } globfree(&usb_devs); return 0; }
3. 通过USB物理端口路径识别(适合固定插拔位置)
如果使用者会固定将Arduino插入同一物理USB端口,可通过sysfs中的USB端口路径来识别:
- 手动记录每个物理端口对应的sysfs路径(比如
/sys/bus/usb/devices/usb1/1-1) - 程序中遍历
ttyUSBx设备,找到其对应的USB端口路径,与预存路径匹配即可关联
该方法无需修改Arduino程序,但如果使用者改变插拔端口,需要重新记录路径。
内容的提问来源于stack exchange,提问作者Silverspur
相关产品推荐
相关产品推荐

