嵌入式Linux Modbus RTU串口通信实战与避坑指南 📅 发布时间:2026/9/11 9:23:28 👁 浏览次数: 搞嵌入式搞了快十年串口和Modbus算是打交道最多的东西之一。最近有朋友让我帮忙看看他板子上Linux端读传感器数据的代码问题一堆要么读不到数据要么读回来的数值明显不对要么跑一会儿整个程序就挂掉。这让我想起自己刚接触嵌入式Linux时被Modbus RTU折磨的经历干脆把这些经验沉淀成一篇实操向的文章把串口配置、RTU协议、数据读写这些环节里容易踩的坑一次说清楚。这篇文章适合这么几类人刚把Linux系统跑起来、准备接工业传感器的嵌入式开发从MCU裸机开发转到嵌入式Linux、对termios这套API还不熟的工程师以及需要用Linux设备做Modbus主站去读各种仪表、温湿度传感器、电量采集模块的兄弟。我会带着完整的C语言实现和调试验证思路来讲尽量做到你看完就能在自己的板子上跑起来。1. 方案选型为什么是Modbus RTU而不是Modbus TCP1.1 Modbus RTU在嵌入式Linux中的定位Modbus是工业现场应用最广的通信协议没有之一。它本身只是一个应用层协议定义了报文格式和数据的组织方式而底层传输可以由串口、网口甚至光纤来承载。按传输方式划分最常见的是Modbus RTU串口、二进制编码和Modbus TCP以太网把RTU帧包在TCP报文里。在嵌入式Linux设备上你大概率会遇到这两种情况设备本身作为一个网关一边接以太网做Modbus TCP服务器一边接串口和下面的传感器用RTU通信或者设备就是个数据采集终端多个RS485传感器挂一条总线上设备做主站依次轮询。我这次就以第二种场景展开因为做数据采集时RTU才是最常用的实时性够、接线简单、成本低一条双绞线就能带几十个节点。为什么不用TCP传感器的RS485接口本质上就是串口物理层不匹配。虽然可以加串口转网口模块但成本和复杂度上去了还得给每个传感器配IP现场维护麻烦。而且很多工业传感器只支持RTU不支持TCP没得选。1.2 硬件接线与电气层面的准备开始写代码之前先把硬件搞清楚。绝大多数带Modbus RTU的传感器都是RS485接口少数是RS232。RS232是一对一的电平逻辑简单Linux板子一般直接引出UART_TX/UART_RX接个MAX3232电平转换就能用。RS485则是差分信号需要A、B两根线半双工工作靠方向控制引脚来切换收发状态。接线有个很经典的坑A接A、B接B这是对的但有些设备标的是D、D-有的标的是485、485-厂商命名混乱接反了表现就是完全收不到数据或者数据乱码。判断方法很简单示波器量发送引脚有波形而总线没反应多半是A/B反了或者你把A、B对调再试一次很多时候问题就解决了。另外注意共地。RS485虽然是差分传输理论上是隔离的但实际应用中如果总线上设备距离较远还是建议把信号地GND接上否则共模电压太高可能导致通信不稳定甚至烧毁接口芯片。我遇到过一次现场通信时好时坏的案例就是总线长度超过一百米还没接地加上布线离电机电缆太近干扰严重后来把GND接上、换成屏蔽双绞线才彻底解决。1.3 确认传感器从机的寄存器地址协议开发前先把手头传感器的Modbus寄存器表读明白。每个传感器厂商提供的文档会告诉你设备地址Slave ID、支持的功能码、寄存器地址、寄存器数量、数据类型uint16、int16、float32等、缩放系数、字节序。比如常见的温湿度变送器寄存器0x0001存温度0x0002存湿度都是16位整数真实值寄存器值/10也就是370对应37.0摄氏度。而一些高精度传感器会用两个16位寄存器拼一个32位浮点数这时你必须搞清楚字节序是大端在前还是小端在前、低地址是高字节还是低字节不对的话读回来就是乱数。这个细节后面数据解析部分我会专门讲。写代码前拿一个Modbus Poll之类的上位机工具先把寄存器地址、功能码验证一遍这一步能节省大量调试时间。我当时调一个新传感器寄存器文档写着地址从0开始结果用起始地址0去读返回异常改为从1读就正常了。这种事情太常见了因为文档里寄存器地址有的是协议层的偏移量有的是物理地址同一家厂商不同产品线都可能不一致。先在上位机里确认再固化成代码里的逻辑才是正确顺序。2. 串口配置嵌入式Linux下必须吃透的termios2.1 核心参数拆解波特率、数据位、校验位、停止位在Linux下操作串口核心就是配置termios结构体。很多从单片机转过来的工程师容易在这块栽跟头因为MCU的串口初始化就是往寄存器里写几个值而Linux的termios抽象层级更高一些选项配置不好就会导致读不到数据或者数据乱码。先看一个我整理的标准配置函数参数对应关系非常直观#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include termios.h #include errno.h #include sys/ioctl.h #include linux/serial.h int uart_set(int fd, int baudrate, int data_bits, char parity, int stop_bits) { struct termios opt; if (tcgetattr(fd, opt) ! 0) { perror(tcgetattr); return -1; } // 设置波特率注意这里用的是 cfsetispeed / cfsetospeed cfsetispeed(opt, baudrate); cfsetospeed(opt, baudrate); // 控制模式标志CLOCAL 忽略调制解调器控制线CREAD 使能接收 opt.c_cflag | (CLOCAL | CREAD); // 清空数据位设置再按需设置 opt.c_cflag ~CSIZE; switch (data_bits) { case 7: opt.c_cflag | CS7; break; case 8: default: opt.c_cflag | CS8; break; } // 校验位 switch (parity) { case N: case n: opt.c_cflag ~PARENB; // 无校验 opt.c_iflag ~INPCK; break; case O: case o: opt.c_cflag | (PARENB | PARODD); // 奇校验 opt.c_iflag | INPCK; break; case E: case e: opt.c_cflag | PARENB; opt.c_cflag ~PARODD; // 偶校验 opt.c_iflag | INPCK; break; default: return -1; } // 停止位 if (stop_bits 2) opt.c_cflag | CSTOPB; else opt.c_cflag ~CSTOPB; // 禁用硬件流控工业传感器一般不启RTS/CTS opt.c_cflag ~CRTSCTS; // raw mode禁止ICANON把输入按行处理否则read会被截断 opt.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); // 禁止软件流控控制字符 opt.c_iflag ~(IXON | IXOFF | IXANY | ICRNL | INLCR | IGNCR); // 原始输出不做换行转换 opt.c_oflag ~OPOST; // VMIN/VTIME是串口阻塞读的核心后面单独讲 opt.c_cc[VMIN] 0; opt.c_cc[VTIME] 100; // 单位是0.1秒100表示10秒超时 // 清空缓冲把配置立即应用 tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, opt) ! 0) { perror(tcsetattr); return -1; } return 0; }2.2 打开串口和初始化完整流程配置好了终端参数还需要正确的打开串口设备。Linux下串口设备通常位于/dev/ttyS*原生串口、/dev/ttyUSB*USB转串口、/dev/ttyAMA*树莓派等平台或/dev/ttymxc*NXP i.MX系列等路径。打开时要注意不能用默认的O_RDWR方式还得加上O_NOCTTY防止串口成为控制终端和O_NDELAY非阻塞后续read由VMIN/VTIME控制。int uart_open(const char *dev_path) { int fd open(dev_path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial); return -1; } // 恢复阻塞状态配合VMIN/VTIME实现超时阻塞读 if (fcntl(fd, F_SETFL, 0) 0) { perror(fcntl); close(fd); return -1; } return fd; }然后初始化组合int fd; fd uart_open(/dev/ttyUSB0); if (fd 0) return -1; // 常见传感器默认9600 8 N 1 if (uart_set(fd, B9600, 8, N, 1) 0) { close(fd); return -1; }注意uart_set里波特率参数传的是B9600这类的termios宏值而不是9600这个数字。这是个很要命的细节cfsetispeed只认宏定义。如果你在代码里写成uart_set(fd, 9600, 8, N, 1)编译不会报错但它会把9600作为一个整型值传给cfsetispeed结果完全无效。2.3 VMIN和VTIME阻塞读的关键组合这是很多从MCU转过来的朋友最不习惯的地方。Linux串口read是带缓冲的系统底层可能只读到一部分数据就返回了如果你的代码一次性read期望拿满一整帧很可能会卡在数据不完整和不知道什么时候才算完整的困境里。VMIN和VTIME这两个参数定义了read的阻塞策略VMIN 0, VTIME 0一直等到收到VMIN个字节才返回。VMIN 0, VTIME 0每等待VTIME个0.1秒的超时时间只要期间收到哪怕1个字节就返回返回的是实际读到的字节数。VMIN 0, VTIME 0收到VMIN字节后才返回数据不足一直等。VMIN 0, VTIME 0读到第一个字节后启动计时依次类推。在Modbus RTU主站场景我推荐VMIN0VTIME稍微给大一点。因为它不会因为期望一帧N个字节而一直阻塞read能返回实际收到的数据你再按Modbus帧格式去校验完整性。我常用VTIME5到10也就是500ms到1s的超时然后在上层再按超时重试逻辑处理。3. Modbus RTU协议实现从报文帧到CRC校验3.1 RTU报文格式解析Modbus RTU报文没有起始标识靠的是帧间空闲时间来区分。一帧报文的格式为地址码1字节 功能码1字节 数据段N字节 CRC校验2字节低字节在前。帧与帧之间至少要有3.5个字符时间的静默间隔如果间隔太短从机就会认为这是一个帧的数据导致接收错乱。之前做MCU侧Modbus从机时这个帧间隔要求是通过定时器来严格保证的。但Linux下做主机时系统调度本身就有不确定性你只要保证发送报文时数据是一次性连续写出去的并且报文内部的字节间隔远大于3.5个字符时间即可。真正影响通信稳定性的反而是发送完到接收这事儿之间的时序。地址码就是从机地址范围1~2470是广播地址对RTU传感器基本用不到。功能码里最常用的是0x03读保持寄存器、0x04读输入寄存器、0x06写单个寄存器、0x10写多个寄存器。读传感器值基本就是0x03或0x04具体用哪个看设备文档。功能码0x01读线圈、0x02读离散输入用于开关量场合我的项目里用得少。3.2 CRC16校验的实现与验证CRC是Modbus RTU里最容易写错也最容易验证错的部分。Modbus的CRC16采用多项式0x8005的宽度16的算法但具体实现是回读模式校验初始值是0xFFFF。很多网上代码用查表法空间换时间其实在嵌入式Linux上CPU完全不是瓶颈逐位计算就够了代码还更好理解uint16_t modbus_crc16(uint8_t *buffer, uint16_t length) { uint16_t crc 0xFFFF; for (uint16_t i 0; i length; i) { crc ^ buffer[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }注意最后一个返回值调用者需要把CRC的低字节放在前、高字节在后填充到报文里。比如CRC算出来是0x1234那你发出去就是0x34 0x12。这个字节序非常有迷惑性初学时很多人在这里栽跟头。怎么验证CRC对不对最简单的办法拿网上现成的Modbus CRC在线计算工具输入报文的前缀对比结果。或者你可以用校验工具读一帧完整的数据看它报CRC校验失败还是CRC正常。我个人的习惯是写单元测试时把一帧打印出来对着工具核对确保字节序排列完全一致后才往下写。3.3 功能码0x03和0x04的报文结构以0x03功能码为例主站读保持寄存器的请求帧是8个字节字节偏移内容说明0从机地址例如0x011功能码0x032起始地址高字节寄存器地址高位3起始地址低字节寄存器地址低位4寄存器数量高字节例如读2个寄存器就是0x005寄存器数量低字节0x026CRC低字节由前6个字节计算7CRC高字节低字节在前从机的正常响应帧格式为字节偏移内容说明0从机地址与请求一致1功能码0x032字节数等于寄存器数*23~3N*2-1寄存器数据每个寄存器2字节高字节在前最后2CRC覆盖除CRC外的部分如果发生异常从机响应帧的字节1会把功能码的最高位置1也就是0x80 | 功能码字节2是异常码比如0x02表示非法数据地址0x03表示非法数据值0x01表示非法功能码。这里我强烈建议调试期间把原始十六进制报文都打出来看。因为只要和文档对一次一眼就能看出是地址不对、数量不对还是CRC问题比自己瞎猜快得多。4. 读写传感器数据的完整实现4.1 主从机通信流程设计Modbus RTU采用严格的请求-响应模式主机发送请求从机在收到完整帧后才会响应。Linux主站的完整流程是构造请求帧 → 校验CRC → 一次性write出去 → 等待从机响应 → read到数据 → 校验CRC → 解析。在这个环节程序的核心不在发而在读——你要根据请求帧的不同在合理时间内期待不同长度的响应还需要处理从机异常响应。我建议专门建两个接口modbus_read_registers和modbus_write_register底层封装完整的组帧、收帧、校验流程。上层逻辑只管调用不用再关心协议细节。typedef struct { int fd; int timeout_ms; int retries; } modbus_t;这个结构体作为Modbus上下文fd是串口描述符timeout_ms是等待响应的超时时间retries是失败后的重试次数。把上下文封装起来后续如果要把某个设备从串口切换为TCP只要把底层发送接收替换掉上层逻辑完全不用动。4.2 读寄存器完整代码实现int modbus_read_registers(modbus_t *ctx, uint8_t slave_id, uint16_t start_addr, uint16_t count, uint16_t *regs) { uint8_t req[8]; uint8_t resp[256]; uint16_t crc; int len; int expected_data_len; int ret; // 1. 构造请求帧 req[0] slave_id; req[1] 0x03; req[2] (start_addr 8) 0xFF; req[3] start_addr 0xFF; req[4] (count 8) 0xFF; req[5] count 0xFF; crc modbus_crc16(req, 6); req[6] crc 0xFF; req[7] (crc 8) 0xFF; // 2. 发送请求清空串口接收缓冲避免读到脏数据 tcflush(ctx-fd, TCIFLUSH); ret write(ctx-fd, req, sizeof(req)); if (ret ! sizeof(req)) { perror(write); return -1; } // 3. 读取响应。因为VMIN0read会按VTIME超时返回这里在循环里累积收帧 len 0; while (len 5) { // 至少先收5字节地址功能码字节数CRC低CRC高 ret read(ctx-fd, resp len, sizeof(resp) - len); if (ret 0) { if (errno EAGAIN || errno EWOULDBLOCK) { continue; } perror(read); return -1; } if (ret 0) break; // 超时未收到数据 len ret; } if (len 5) return -1; // 超时 // 4. 检查从机异常码 if ((resp[1] 0x80) ! 0) { printf(slave exception, code0x%02X\n, resp[2]); return -2; } // 5. 校验CRC crc (resp[len - 1] 8) | resp[len - 2]; if (modbus_crc16(resp, len - 2) ! crc) { printf(CRC error\n); return -3; } // 6. 解析数据 expected_data_len resp[2]; // 数据字节数 if (len ! 5 expected_data_len) { printf(resp length mismatch\n); return -4; } for (int i 0; i expected_data_len / 2; i) { regs[i] (resp[3 i * 2] 8) | resp[4 i * 2]; } return expected_data_len / 2; }4.3 数据解析从寄存器值到真实物理量读回来的寄存器是16位无符号整数要把它们变成有意义的物理量就要根据传感器文档做转换。两个寄存器拼浮点数是最常见的一个坑。比如一个压力变送器数据是32位IEEE754浮点数存放在两个16位寄存器里寄存器地址0和1。你读回来后假设regs[0]是高16位regs[1]是低16位拼接后uint32_t raw (regs[0] 16) | regs[1];然后再memcpy成floatuint32_t raw ((uint32_t)regs[0] 16) | ((uint32_t)regs[1] 0xFFFF); float value; memcpy(value, raw, sizeof(value)); printf(pressure %f MPa\n, value);但这里必须非常小心大小端。我遇到过一款传感器寄存器0是高字、寄存器1是低字但另一款的寄存器顺序恰好相反如果不看文档想当然处理读出来的浮点数就是一个天文数字。文档里面通常会画图说明高16位寄存器在前还是低16位寄存器在前把这一条确认清楚再写转换代码。有些传感器不是用浮点而是用整型加缩放系数。比如温度寄存器0x0001返回的值是370实际温度是37.0度也就是除以10。很多仪表的参数寄存器还会有无符号和有符号两种情况如果值是负数比如-25度在内部以补码存储直接转uint16后再除以10就会得到65536-2506528.6这个离谱的数。正确做法是先把uint16转成int16做符号扩展再除以缩放系数int16_t raw_temp (int16_t)regs[0]; float temp raw_temp / 10.0f;类似这样的转换细节是整个Modbus应用开发中最考验细心程度的地方。4.4 多传感器轮询策略实际项目里一条总线往往会接多台传感器需要主机依次轮询。轮询的基本原则对每个从机地址依次发送请求接收完当前从机的响应后再切换到下一个从机。不能同时对多个从机发请求因为RS485是半双工总线同一时刻只能有一个设备在发送。uint8_t slave_ids[] {1, 2, 3, 4}; #define SLAVE_NUM sizeof(slave_ids) while (1) { for (int i 0; i SLAVE_NUM; i) { uint16_t regs[2]; int ret modbus_read_registers(ctx, slave_ids[i], 0, 2, regs); if (ret 2) { float temp (int16_t)regs[0] / 10.0f; float humi (int16_t)regs[1] / 10.0f; printf(slave %d: temp%0.1f humi%0.1f\n, slave_ids[i], temp, humi); } else { // 上报错误做重试或标记离线 printf(slave %d read fail, ret%d\n, slave_ids[i], ret); } // 帧间间隔给总线和从机一点喘息时间 usleep(20 * 1000); } // 一轮完成后根据需要决定下轮间隔 usleep(100 * 1000); }轮询周期有多快取决于从机数量和响应速度。工业环境下我一般不让轮询周期低于100ms因为太频繁的轮询会让从机响应不过来还容易在RS485总线上产生冲突。如果是采集模拟量温度、湿度这种变化缓慢的量1秒轮一轮都完全够用。5. 常见问题排查与实测笔记把这些坑提前帮你踩完5.1 总是不通信先查电气再接协议有人写了串口读写代码read永远返回0或者超时第一反应就是代码哪里有问题。但我实际遇到的情况里有接近一半是因为硬件没通、压根没有数据回包。所以我强烈建议调试顺序严格按电气 → 物理 → 协议来第一步用示波器或逻辑分析仪测量串口发送引脚确认UART信号在了。如果是RS485用A-B通道量数据波形如果看不到波形说明Linux侧没在发送或者发送被系统拦了。第二步用USB转RS485的调试器挂到总线上配合Modbus Poll或串口助手确认总线上的数据帧。只要能看到请求帧就说明Linux侧配置没有大问题此时如果从机没回复就要检查从机地址、寄存器地址对不对。第三步才是去翻代码的CRC、VMIN/VTIME这些协议层问题。这样排查路径清晰效率最高。5.2 数据中间夹杂乱码重点查终端回显和流控有时候串口发是发出去了但收到的数据里多出一些奇怪的字符最常见原因是终端行规则没有设置成raw mode。如果ICANON没关内核会把输入按行缓冲并且对回车、换行做转换你明明发的是0x01 0x03这样的二进制它可能给你插入或吞掉字符。我在前面uart_set里专门用那一行opt.c_lflag ~(ICANON | ECHO | ECHOE | ISIG);就是为了关闭这些干扰。另一个容易被忽略的是软件流控IXON/IXOFF。有的模块在遇到数据中有0x11或0x13这些字符时会自动暂停/恢复发送如果把软件流控开着你对从机发送的报文内容里正好有XON/XOFF字符时从机可能认为你在控制它报文直接乱套。为了杜绝这个隐患无论是否用到流控建议全部关掉。5.3 收不到完整帧read返回但数据不完整Modbus RTU响应帧可能超过10字节而串口read一次收到的字节数是不固定的。有的Linux驱动底层FIFO就那么大你一次read可能只拿到头几字节剩余的还在内核缓冲区里。如果只read一次就解析会因为帧长度不够而失败。解决办法就是我在modbus_read_registers里写的循环读先根据功能码计算期望的响应总长度然后一直read直到凑满或者超时退出。这里要特别注意如果从机因为异常码只回了5个字节而你还在傻等后面的数据就会一直等到VTIME超时。所以最好先读前5个字节判断是不是异常帧再决定要不要继续读。5.4 RS485方向切换半双工的隐藏坑RS485半双工通信方向切换时机不对就会丢数据。有些USB转485调试器是硬件自动切换方向不需要软件干预但很多嵌入式Linux板子上的RS485电路是依靠RTS信号或者普通GPIO来控制方向。如果驱动的485模式没配好发送完立刻切到接收而总线上一旦有回波或者从机响应太快这部分数据就丢了。Linux内核串口驱动提供了标准的RS485控制接口#include linux/serial.h struct serial_rs485 rs485conf; memset(rs485conf, 0, sizeof(rs485conf)); // 使能RS485模式 rs485conf.flags SER_RS485_ENABLED; // 发送时RTS拉高表示方向 rs485conf.flags | SER_RS485_RTS_ON_SEND; // 关闭RTS在发送后立即拉低的延迟视电路而定 rs485conf.flags | SER_RS485_RTS_AFTER_SEND; rs485conf.delay_rts_before_send 0x0001; rs485conf.delay_rts_after_send 0x0001; if (ioctl(fd, TIOCSRS485, rs485conf) 0) { perror(TIOCSRS485); }用这个ioctl后驱动会在串口数据发送完成后自动检查RTS状态保证方向切换的时序正确。如果你的板子不是走RTS控制方向而是用独立GPIO那你只能在驱动层做适配把发送完成中断和接收使能时机关联起来。这块如果搞不定通信成功率会非常不稳定尤其是波特率较高、报文较长的时候。5.5 超时和重试策略单点故障不拖垮整个系统总线上一个从机掉线如果主机傻等它的响应不仅这台设备的数据更新不了后面所有从机都会被卡住。所以一定要给每次请求设置合理的超时时间。我的经验值对于9600波特率一般从机响应在几十毫秒内最多不会超过200毫秒所以超时时间设500ms就已经很保守了。重试机制我建议最多重试2次。第一次失败后立即重试第二次失败后放弃标记该从机离线或上报异常。不要无限重试否则一个坏设备会把整条总线拖成抖死状态。每次重试前记得把串口接收缓冲区的脏数据清掉否则上一次response可能残留下来被误认为是本次响应。int modbus_read_registers_with_retry(modbus_t *ctx, uint8_t slave_id, uint16_t start_addr, uint16_t count, uint16_t *regs) { for (int i 0; i ctx-retries; i) { int ret modbus_read_registers(ctx, slave_id, start_addr, count, regs); if (ret 0) return ret; if (ret -2) // 从机异常响应不必重试 return -2; usleep(10 * 1000); } return -1; }5.6 并发和多线程这个业务真不适合多线程并发有些开发会把每个传感器的读操作丢到一个线程里想着并行读取能提高效率。在Modbus RTU半双工总线上这是个绝对的错误多个线程同时写同一个串口fd报文会交叉混叠从机听到的全是碎帧。哪怕加锁也避免不了同一时刻总线上只有一个报文在进行的事实线程调度反而增加了不确定性。我建议所有设备用一个单线程轮询循环去处理。如果担心某个从机响应慢阻塞其他设备那就用我前面介绍的VMINVTIME超时机制配合自定义的间隔控制即可。想要更高实时性可以把串口读取放到单独线程主循环只负责下发请求并等待结果但无论如何串口的write和read都必须由同一个线程调度或者加全局互斥锁保护。6. 最后再分享一点我的实际体会做嵌入式Linux上的Modbus通信和玩单片机最大的不同在于Linux内核的串口驱动、调度机制、buffer管理都替你做了很多事但也因此屏蔽了很多细节。你需要直面的是如何利用termios规则控制好底层行为同时保住Modbus协议层的时序和数据完整性。从我的经验来看一个能长期稳定运行的Modbus采集程序靠的不是一把梭的代码堆叠而是对串口配置每个位都了然于胸对报文格式有肌肉记忆对异常场景做好防御式编程。建议你在把代码部署到无人值守现场之前先做一个长时间的跑批测试让程序连续跑24小时以上期间人为插拔某个从机、短接总线制造CRC错误看看程序能不能靠超时重试机制扛过去会不会因为异常数据导致崩溃。这类测试暴露出的问题远比你在正常环境里调一天更能提升代码的健壮性。串口通信要踩的坑永远踩不完每次换一个新传感器、新从站设备总会有新的问题冒出来。但只要你把基础协议细节理解透彻把排查问题的路径刻在脑子里大多数问题都不过是想清楚电气通没通、报文对不对、解析合不合理这三件事。希望这篇文章能帮你少走一些弯路。