去年做一个现场采集网关的时候我接了一批RS485接口的温湿度变送器到一块ARM Linux板子上。板子跑的是嵌入式Linux应用层用C语言写硬件上自带一路UART外接SP3485做电平转换后直接拉到变送器。整体架构很简单但真正调试起来我才发现从termios参数到RTU帧间隔再到CRC的字节顺序几乎每一步都有“看着正常、一上总线就翻车”的坑。这篇文章就是把嵌入式Linux端通过Modbus RTU读取传感器数据的完整链路从串口配置、报文组帧到现场排障一次性讲透。标题里的几个词其实已经划出了本文的边界嵌入式Linux是平台串口是物理通路RTU是协议格式传感器是最终目标。无论你是在做智慧农业、机房动环监控还是工业数据采集这套流程基本通用。这篇文章适合两类人一是刚接触嵌入式Linux通信开发、想快速把Modbus跑通的新手二是已经在MCU上写过FreeModbus、想了解Linux端有哪些不一样的老手。我会把代码、参数和排查思路都放出来你可以直接照着抄也能从中理解背后的原理。1. 为什么这类项目都绕不开Modbus RTU场景与技术选型1.1 现场设备几乎全是RS485加Modbus RTU做工业现场和物联网网关的人应该都有体感你跑到现场看到的传感器变送器——温湿度、液位、风速、光照、CO2、水浸——十有八九是RS485接口出厂默认支持Modbus RTU协议。原因并不复杂RS485只需两根线就能在半双工模式下跑上千米抗共模干扰能力强现场接线成本低Modbus RTU则是目前兼容性最好的工业总线协议之一帧结构简单到可以在几KB内存的单片机上实现所以从PLC到仪表到传感器几乎都把它作为标配。我在项目里遇到的大部分传感器用户手册里都会给出类似“从站地址默认1波特率96008数据位无校验1停止位保持寄存器0~1为温度浮点数”的说明。这就意味着只要你的嵌入式Linux设备能通过串口把Modbus RTU请求帧发出去再把返回帧解析出来剩下的事情就只是查手册填寄存器地址了。这里有个容易混淆的点需要先理顺Modbus本身分主站和从站。本文的场景是嵌入式Linux作为Modbus主站Master主动去读传感器从站Slave的数据。如果你的板子是想作为从站被别人读取那思路完全不同通常会用上FreeModbus这类现成栈。主站和从站的代码结构差异很大别一上来就找错方向。1.2 嵌入式Linux与MCU方案的本质差别很多从STM32转过来的人一开始都会不自觉地沿用MCU的思维打开串口中断在中断里收字节按状态机组帧超时重置。这套思路在Linux上不能说错但不划算。Linux内核已经把串口驱动、中断、环形缓冲区都处理好了应用层只需要做三件事配置termios、用read/write读写设备节点、按Modbus RTU协议解析数据。选型上还有一条路线直接用libmodbus这个开源的Modbus库。libmodbus功能完整支持RTU和TCP封装了底层串口配置和协议收发交叉编译也不难。但我的建议是第一版不要立刻上库先手写一版最简的读取函数把协议帧结构彻底搞明白。因为一旦遇到传感器数据解析不出来你需要知道问题出在组帧、CRC还是寄存器映射上这时候“自己写的帧”比“库帮你拼的帧”更容易排查。等流程通了再决定要不要上libmodbus提升开发效率。2. 串口配置是第一个分水岭termios关键参数逐个说清楚2.1 打开串口前的几个底层事实Linux把串口抽象成设备节点常见的有/dev/ttyS0、/dev/ttymxc0、/dev/ttyAMA0等具体名字和SoC平台相关可以用ls /dev/tty*确认。打开方式用open()但三个标志位必须注意O_RDWR是读写O_NOCTTY防止串口成为控制终端否则你按CtrlC可能把信号发给整个进程组O_NDELAY或O_NONBLOCK表示打开时不以阻塞方式等待DCD信号。int fd open(/dev/ttyS0, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial); return -1; }打开之后第一件事是tcgetattr(fd, opt)把当前属性读出来然后在这份属性上去修改改完再tcsetattr写回去。千万不要直接弄一个全新的struct termios就往上填因为里面有大量保留字段和平台相关的标志直接覆盖会出莫名其妙的问题。2.2 一组可以直接用的串口初始化代码初始化代码的核心是cfmakeraw()加c_cflag配置。cfmakeraw()会把串口设置为“裸模式”关闭回显、关闭ICANON规范模式、关闭信号控制、关闭输入输出转换这是Modbus这类二进制协议通信必须具备的前提。如果不开raw模式内核默认会把输入里的\r\n做转换甚至可能把收到的0x0d吃掉你的Modbus帧就永远不完整。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include errno.h #include termios.h #include stdint.h int serial_open(const char *dev, speed_t baud) { int fd open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial); return -1; } struct termios opt; if (tcgetattr(fd, opt) ! 0) { perror(tcgetattr); close(fd); return -1; } cfmakeraw(opt); cfsetispeed(opt, baud); cfsetospeed(opt, baud); opt.c_cflag | (CLOCAL | CREAD); opt.c_cflag ~CSIZE; opt.c_cflag | CS8; opt.c_cflag ~CSTOPB; opt.c_cflag ~PARENB; opt.c_iflag ~(IXON | IXOFF | IXANY); opt.c_iflag ~(INLCR | ICRNL | IGNCR); opt.c_oflag ~OPOST; opt.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); opt.c_cc[VMIN] 1; opt.c_cc[VTIME] 0; tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, opt) ! 0) { perror(tcsetattr); close(fd); return -1; } return fd; }重点解释几个容易被忽视的位CLOCAL表示忽略调制解调器控制线防止没有载波信号时内核自动挂断开CREAD允许接收数据不设置这个你就只能发不能收CSTOPB清零表示1位停止位置位是2位停止位PARENB清零表示无校验对应变送器手册里常见的8N1。c_iflag里必须注意IXON和IXOFF这是软件流控。如果不清掉对方发了0x11XON字符内核可能会暂停输出你的请求帧就被“雪藏”在缓冲区里了。我做项目时第一次对接一个风速仪就是这个原因导致发出去的请求对方根本没收到排查了很久。2.3 VTIME与VMIN读不到数据还卡死的根源VMIN和VTIME是termios里两个非常影响使用体验的参数很多人在这里踩坑。简单理解VMIN是read返回前需要的最小字节数VTIME是等待超时时间单位0.1秒。把它们组合起来可以分成四种模式VMIN1, VTIME0有1个字节数据就立即返回无限等待。这是最常见的阻塞读模式。VMIN0, VTIME0定时读取每次read最多等待VTIME*0.1秒有数据就返回数据没数据超时返回0。VMIN0, VTIME0阻塞等待凑够VMIN个字节才返回但如果一次只来了3个字节而你要8个就会一直卡住。VMIN0, VTIME0跨字节超时模式第一个字节到达后计时超时则返回已收到的数据。Modbus RTU响应长度是可以通过寄存器数量算出来的所以更稳妥的做法是用select()做超时控制。先把串口设成VMIN0, VTIME0的非阻塞读取然后用select(fd1, rdfs, NULL, NULL, tv)等待数据到达到达后再循环read收满期望长度。这样既不会卡死又能精确控制单次请求的超时时间。3. Modbus RTU报文拆解地址、功能码、CRC和3.5T间隔3.1 读保持寄存器这一个功能码覆盖绝大多数传感器Modbus协议的功能码很多读线圈、读离散输入、读输入寄存器、读保持寄存器、写单寄存器、写多寄存器……但在“传感器数据采集”这个场景里你用得最多的只有两个功能码0x03读保持寄存器和0x04读输入寄存器。区别在于保持寄存器Holding Register通常可读可写输入寄存器Input Register只读。大部分变送器把采集到的温度、湿度、压力等数据放在保持寄存器或输入寄存器里具体看手册。一次完整的读操作只需要两条帧。请求帧格式如下字段长度示例值从站地址1字节0x01功能码1字节0x03起始寄存器地址2字节高字节在前0x0000寄存器数量2字节高字节在前0x0002CRC162字节低字节在前0xC4, 0x0B如果起始地址是0、数量是2请求帧就是01 03 00 00 00 02 C4 0B。帧里每一个字节都是关键地址错了别的从站可能不回应功能码错了从站回异常帧地址和数量反了读到的数据完全不对。正常响应帧格式字段长度示例值从站地址1字节0x01功能码1字节0x03数据字节数1字节0x04寄存器数据N字节0x42, 0x0F, 0x3C, 0xCDCRC162字节低字节在前—数据字节数等于寄存器数量乘2。响应帧里的数据就是你真正要的传感器值。如果请求的寄存器不存在或功能码不支持从站会返回异常帧功能码最高位置1例如0x83后面跟一个异常码。异常码1表示非法功能2表示非法地址3表示非法数据这个在调试时非常有用。3.2 CRC16计算小端发送顺序别搞反Modbus RTU的CRC16采用多项式0xA001初始值0xFFFF这是CRC-16/MODBUS标准和常见的CRC-16/IBM的初值不同不能混用。计算范围是从站地址开始到数据区结束不包括CRC本身发送时低字节在前、高字节在后。很多初学者第一次写代码算出来的CRC和协议工具对不上基本就是这两个问题初值没用0xFFFF或者发送字节序弄反了。uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (int j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }这段代码是逐位计算的虽然慢但逻辑清晰、便于移植。实际使用中寄存器数量最多也就几十个CPU开销可忽略不计。如果后续要高频轮询多个从站可以换成查表法速度能快一个量级。组装请求时尤其注意uint16_t crc crc16_modbus(req, 6); req[6] crc 0xFF; // 低字节在前 req[7] crc 8; // 高字节在后接收响应时同样要校验CRC防止总线上有干扰导致数据错位。校验通过后再做数据解析否则宁可丢弃这次响应重新请求。3.3 帧间隔3.5T轮询太快反而什么都读不到Modbus RTU是异步串口协议没有时钟线靠字节之间的时间间隔来分帧。标准规定帧内两个字节之间的间隔不能超过1.5个字符时间帧与帧之间必须至少静默3.5个字符时间。也就是说如果从站收到一帧数据中间停顿时间过长它会把一帧拆成两帧处理并丢弃或者把连续两帧当成一帧。9600波特率下一个字符包含起始位、8数据位、停止位大约是1.04ms3.5个字符时间约3.64ms。这意味着发送完一帧请求后至少要留出3.5ms以上的静默时间再从站才认为“一帧结束”。嵌入式Linux虽然进程调度的精度足够但如果你在循环里紧挨着发两个请求很可能第二帧的开头被从站当成第一帧的延续。考虑到Linux调度和串口驱动的缓冲延迟我通常把两次请求之间的间隔设到20ms以上9600波特率下完全足够。如果换成115200波特率字符时间缩短到约0.087ms3.5T间隔约0.3ms间隔可以适当缩短但出于稳定考虑我仍然保留10ms的余量。4. 从组帧到解析完整读写传感器数据的代码落地4.1 一个可直接移植的读保持寄存器函数现在把前面的串口配置、CRC计算、组帧、收包校验串起来写一个modbus_read_holding_registers函数。这个函数的设计目标是传入从站地址、起始寄存器地址、寄存器数量和超时时间返回一个存有原始寄存器字节的缓冲区。上层再根据传感器手册去解析这些字节。static uint32_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec * 1000 ts.tv_nsec / 1000000; } int modbus_read_holding_registers(int fd, uint8_t slave, uint16_t addr, uint16_t qty, uint8_t *out, int timeout_ms) { if (qty 0 || qty 125) return -1; uint8_t req[8]; req[0] slave; req[1] 0x03; req[2] addr 8; req[3] addr 0xFF; req[4] qty 8; req[5] qty 0xFF; uint16_t crc crc16_modbus(req, 6); req[6] crc 0xFF; req[7] crc 8; tcflush(fd, TCIOFLUSH); if (write(fd, req, sizeof(req)) ! (ssize_t)sizeof(req)) return -1; tcdrain(fd); int expect_len 5 (int)qty * 2; uint8_t buf[256]; int pos 0; uint32_t start now_ms(); while (pos expect_len) { uint32_t elapsed now_ms() - start; if (elapsed (uint32_t)timeout_ms) break; fd_set rdfs; FD_ZERO(rdfs); FD_SET(fd, rdfs); struct timeval tv; tv.tv_sec 0; tv.tv_usec ((uint32_t)timeout_ms - elapsed) * 1000; int ret select(fd 1, rdfs, NULL, NULL, tv); if (ret 0) break; int n read(fd, buf pos, expect_len - pos); if (n 0) { if (errno EAGAIN || errno EINTR) continue; break; } if (n 0) continue; pos n; } if (pos expect_len) return -1; if (buf[0] ! slave) return -1; if (buf[1] ! 0x03) return -1; if (buf[2] ! (uint8_t)(qty * 2)) return -1; crc crc16_modbus(buf, pos - 2); if (buf[pos - 2] ! (crc 0xFF) || buf[pos - 1] ! (crc 8)) return -1; memcpy(out, buf 3, (size_t)qty * 2); return 0; }几个设计细节说明一下。expect_len 5 qty * 2是从站地址1字节加功能码1字节加字节计数1字节加数据区qty*2字节加CRC 2字节。用select()做整体超时循环读取直到收满或者超时。tcdrain(fd)在RS485半双工场景特别关键write()只是把数据交给内核缓冲区就返回了如果不等待真正发完就立刻进入读状态方向切换早了会截断请求帧。等效的做法是发完数据后按波特率估算延时但tcdrain()更准确。4.2 把寄存器拼成float字节序是最容易翻车的地方传感器数据最常见的表示方式是IEEE 754单精度浮点数占用4字节即两个16位寄存器。Modbus协议规定多字节数据高字节先发大端序所以请求里地址和数量都是高字节在前。但寄存器里的float数据有两种排列可能寄存器N存float的高16位、寄存器N1存低16位大端排列或者反过来小端排列。不同传感器厂商对此没有统一标准只能看手册。很多国产变送器手册里写的“浮点数高字节在前”指的就是大端排列。对应代码float regs_to_float_be(const uint8_t *regs) { uint32_t u ((uint32_t)regs[0] 24) | ((uint32_t)regs[1] 16) | ((uint32_t)regs[2] 8) | (uint32_t)regs[3]; float f; memcpy(f, u, sizeof(f)); return f; }如果设备是小端排列交换位置即可float regs_to_float_le(const uint8_t *regs) { uint32_t u ((uint32_t)regs[2] 24) | ((uint32_t)regs[3] 16) | ((uint32_t)regs[0] 8) | (uint32_t)regs[1]; float f; memcpy(f, u, sizeof(f)); return f; }调试时最有效的办法是给传感器通上电知道当前实际温度大约是多少然后用两种方式解析看哪个结果符合常识。如果温度显示25.6但解析出来是几百万那不用怀疑字节序反了。4.3 多传感器轮询节奏与超时策略实际项目里不会只读一个寄存器区而是每隔几百毫秒或一秒轮询一遍所有数据。如果寄存器地址是连续的尽量一次0x03读多个不要一个寄存器发一帧。以温湿度变送器为例温度在保持寄存器0~1湿度在保持寄存器2~3那一次读4个寄存器0~3就能把两个数据都拿回来节省一半总线流量。int main(void) { int fd serial_open(/dev/ttyS0, B9600); if (fd 0) { fprintf(stderr, serial open failed\n); return 1; } for (;;) { uint8_t regs[8]; if (modbus_read_holding_registers(fd, 1, 0, 4, regs, 500) 0) { float temp regs_to_float_be(regs); float humi regs_to_float_be(regs 4); printf(temp%.2f humi%.2f\n, temp, humi); } else { printf(read failed\n); } usleep(1000 * 1000); } close(fd); return 0; }超时参数的设置也有讲究。9600波特率下读4个寄存器对应响应帧是13字节按每字节约1.04ms算整帧传输约13.5ms。但从站不是收到请求立刻回复的某些变送器的固件处理时间可能高达几十毫秒所以第一次对接时超时给300~500ms比较安全。确认设备响应速度后再根据项目实时性要求缩小超时值。轮询周期不建议低于50ms否则不仅违背3.5T帧间隔要求也会给主站和RS485总线带来不必要的负担。5. 最诡异的现场故障单测正常、对接不正常到底哪里出了问题5.1 先复盘现象主机单独测试正常从机单独测试也正常“485 modbus 主机 从机 分别测试都正常 主机连接从机就不正常”是搜索热词里的一句话也是我在现场被问得最多的问题。什么叫分别测试正常主机单独测试一般是拿串口助手或Modbus Poll接一个调试从站能正常发帧收帧从机单独测试则是拿PC上的Modbus调试软件去读传感器能正常读到数据。听起来两边都没问题可一旦把主机嵌入式Linux板子和从机真实传感器接在同一个RS485总线上要么完全没响应要么数据随机出错。这个现象背后的深层原因是Modbus是链路层和物理层强耦合的协议。单独测试时PC端的USB转485模块和嵌入式板卡的串口电路在电气特性上往往有差异调试从站和真实传感器的响应时序也不完全一样。很多问题只有在真实环境下才会暴露。按照我的经验正常顺序是先查物理层再查协议参数最后才怀疑代码逻辑。5.2 物理层的三个隐藏问题A/B反接、共地、终端电阻RS485是差分信号但不是说两根线一接就万事大吉。第一个最常见的问题是A/B反接。不同厂商对端子的标注不一样有的标A/B有的标D/D-有的标485/485-。判断方法很简单用万用表直流电压档量两根线之间的静态电压正常情况下A相对B应该是2V到5V之间。如果量出来是负的说明接反了交换两根线即可。第二个问题是共地。RS485虽然用差分方式传输但收发器的共模电压范围是有限的通常-7V到12V。当主机和从机使用两个独立电源供电而且距离较远时两边的地电位可能有几伏甚至更高的压差一旦超出共模范围收发的波形就会畸变表现就是时好时坏、偶尔乱码。解决方法是拉一根公共地线或者使用带隔离的RS485收发器。第三个问题是终端电阻。RS485标准要求在总线两端各并联一个120Ω的电阻用于匹配线路阻抗、减少反射。但在实验室里短距离对接不接终端电阻通常也能工作。一旦线缆拉长到几十米以上或者现场电磁干扰较强没有终端电阻就会出现波形振铃数据一多就丢包。现场判断方法是把波特率降到9600甚至4800试试如果故障频率明显降低很大概率就是反射和终端匹配问题。5.3 协议层的五个检查项物理层排除干净之后再看协议层。我总结了一个“裸奔检查单”每次对接新设备都按这个顺序过一遍从站地址变送器出厂默认地址一般是1但可能被改过。用Modbus工具扫一下1到247看看哪个地址有响应。波特率、数据位、校验位、停止位很多传感器出厂是9600 8N1但有些日本设备默认是9600 8E1甚至4800 8N2。这四个参数只要有一个不一致从站就完全不应答。寄存器地址偏移Modbus协议地址从0x0000开始但很多PLC和组态软件习惯用40001这种从1开始的地址。如果手册写的是40001那协议地址就是0如果手册直接写寄存器地址1那协议地址可能是0也可能是1两种都试一下。CRC计算和发送字节序请求帧CRC低字节在前响应帧CRC同样低字节在前。用工具对比一下你组出来的帧和工具组出来的帧CRC对不对一目了然。功能码是否支持部分传感器只实现了0x04读输入寄存器你用0x03去读它就会返回异常帧0x83。5.4 用逻辑分析仪直接看总线波形如果上面检查完还是不正常别猜了上逻辑分析仪。把逻辑分析仪的通道A和通道B分别接到RS485的A和B线上采样率设到1MHz以上触发方式设置成林瑞下降沿也就是总线从空闲到开始传输的瞬间然后抓一段波形。RS485是差分信号逻辑分析仪解析时需要把A-B的差分电平转换成一串二进制流很多逻辑分析仪软件自带RS485解码器直接把波特率设成9600就能解出实际发送的字节序列。抓出来的数据有两个排查价值一是看请求帧实际发出来了没有二是看从站回复的帧是否和你预期一致。有一次我排查一个稳定性问题发现主站发送的帧中间偶尔多了0x00字节原因是GPIO模拟的RS485方向切换引脚在write和tcdrain之间被中断打断时序乱了。这类问题在纯代码层面极难发现但波形上一眼就能看出来。我把最常见的“单测正常对接不正常”原因整理成一张表现象可能原因判断方法处理方式完全无响应A/B反接万用表量静态电压交换A/B接线完全无响应参数不匹配用工具扫描从站统一改到9600 8N1偶发乱码RS485未共地量两边地电位差拉公共地线或加隔离长线丢包缺终端电阻降低波特率好转两端并联120Ω数据值离谱寄存器地址偏移对比工具有效地址地址换算后重试数据值离谱float字节序不对两种解析结果对比按设备手册调整能通但偶发超时轮询间隔过短抓帧间距小于3.5T增大轮询周期5.5 一个真实的排查案例做一个补充案例帮我记住这套排查链路。有一回现场反映“上位机读数据十分钟必死一次”我去了之后先看了总线波形发现从站响应正常主站请求正常但每隔一段时间主站会在上一次请求超时后立刻重发而重发时的总线电平还没有完全稳定导致从站收到了半个字节的毛刺。问题根因是主站超时设成了200ms但某些时刻从站的处理时间刚好超过200ms于是触发重发重发又太快破坏了3.5T规则。解决方案很朴素把超时提到500ms两次请求之间固定间隔50ms之后再也没复现过。6. 调试工具与效率心得不止Modbus Poll一条路6.1 主站从站模拟工具的搭配用法很多人一搜Modbus调试工具出来的全是Modbus Poll和Modbus Slave这两个软件。确实它们一个是主站模拟器、一个是从站模拟器配合使用能模拟绝大多数场景。但我必须说实话这两个软件是商业软件找注册码折腾半天不如直接用开源替代方案来得痛快。QModMaster是一个免费开源的主站模拟工具支持Modbus RTU和TCP界面里能看到完整的收发帧还能自动计算CRC非常适合新手拿它对比自己的代码组帧是否正确。Linux命令行下还有更轻量的工具mbpoll一条命令就能发起一次读请求mbpoll -m rtu -b 9600 -p none -a 1 -t 4 -r 1 -c 4 /dev/ttyS0这条命令的含义是RTU模式波特率9600无校验从站地址1读保持寄存器-t 4对应保持寄存器从协议地址1开始读4个寄存器。看到输出里的数字值和你的程序解析结果对比一下就能判断是组帧问题还是字节序问题。从站模拟方面Modbus Slave是比较顺手的工具但也同样有授权限制。开源方案里diagslave是mbpoll作者写的配套从站模拟工具命令行用法差不多在Windows或Linux下都能跑。把mbpoll和diagslave配合使用完全能支撑起从零到协议的验证环节。6.2 在嵌入式Linux板上直接旁路监听现场调试有时不方便把设备拆下来接电脑尤其是那些已经装到机柜里的。这时候可以利用嵌入式Linux板卡本身做旁路监听如果板卡上还有一个空闲串口比如调试串口用一根线把调试串口和RS485总线上的差分信号连起来是不现实的电平不一样但你可以用一个USB转RS485模块插到板卡的USB口把模块的A/B也挂到同一根485总线上然后执行cat /dev/ttyUSB1 | xxd这样就能在板卡上直接查看总线上流动的原始字节。如果你希望PC端也看到同样的数据可以用socat把两个串口桥接起来让数据同时转发到调试口socat /dev/ttyS0,raw,echo0 /dev/ttyUSB1,raw,echo0注意桥接连接的是TTL串口电平的设备如果直接桥接RS485和RS232设备需要确保电平兼容否则可能损坏接口。最稳妥的做法是始终保持USB转485模块监听总线PC端通过这个模块观察。6.3 我踩过几次坑后的固定套路项目做多了我给自己定了一个固定的联调流程现在分享出来可以帮你少走很多弯路拿到新传感器第一步不写代码先用mbpoll或QModMaster在PC上读取数据确认设备地址、波特率和寄存器地址。再在嵌入式板卡上用stty命令配好串口配合xxd手动发一帧数据验证板卡到传感器的通路是否正常。手写代码先只实现读固定寄存器功能用log把收发帧的每一个字节打出来和工具里的帧逐字节对比。确认CRC和帧结构无误后再加轮询逻辑和数据处理。最后做稳定性测试至少跑24小时观察有没有偶发超时、乱码甚至程序异常。这套流程看起来慢但每一步都有明确目标能确保异常发生时你清楚问题出在哪一层而不是把时间浪费在瞎猜上。调试Modbus RTU的时间80%其实是花在串口和物理层上的协议本身反而简单。把termios吃透、把帧间隔和CRC字节序弄对再把物理层那几个坑提前规避掉整个项目就顺了。我后来把自己常用的串口配置和Modbus RTU读写函数封装成一个小工具库之后再接新的传感器只需要查一下寄存器表和字节序改两行参数就能稳定出数据。希望这次的分享也能让你的嵌入式Linux通信之路少一点“明明都正常却连不上”的夜。