嵌入式Linux下Modbus RTU开发实战:串口配置与传感器数据读取 搞嵌入式Linux的好几年了最常被刚入行的朋友问到的就是“怎么在板子上跟传感器通信”。说实话Modbus RTU这套东西芯片端玩得转的人不少但一跑到嵌入式Linux下串口配置、设备节点、RS485方向切换这些问题就劝退了一大波人。这篇文章就是聊聊我实际做项目时怎么搞定嵌入式Linux下的Modbus开发从串口配置到RTU读写传感器数据把那些踩过的坑和经验一起放出来。1. 项目整体设计与技术选型思路1.1 为什么嵌入式Linux下选Modbus RTU很多工控现场的设备比如温湿度传感器、压力变送器、光照度传感器、气体检测模块走的都是RS485总线加Modbus RTU协议。原因不复杂RS485抗干扰能力强能拉1200米的线一条总线上还能挂32个设备这对工业现场来说太实用了。嵌入式Linux平台跟裸机MCU不太一样它跑着完整的操作系统有文件系统、有进程管理、有驱动框架。这也意味着我们不能像STM32那样直接操作寄存器写UART而是要依赖Linux的串口驱动层和tty设备节点来做数据收发。用Modbus RTU在Linux下开发本质上就是做好三件事打开并配置串口、按RTU报文格式组装和解析数据、处理时序和异常。我在实际项目里遇到过不少团队纠结要不要自己写协议栈我的建议是如果你的业务逻辑简单只读几个寄存器完全可以自己封装如果设备多、功能码复杂、后续还要扩展那直接用libmodbus这类成熟库更稳妥。自己写的好处是可控性强代码量也不大用库的好处是边界情况都处理好了比如CRC校验、超时机制、广播帧处理这些细节。1.2 硬件接线与串口资源规划做Modbus RTU开发前第一步是把硬件链路理清楚。最常见的组合是嵌入式Linux板卡比如IMX6ULL、全志H3、瑞芯微RV1126引出UART通过一个RS485收发芯片比如SP3485、MAX3485转成RS485差分信号再连到传感器总线上。这时候有个很关键的点你的板子上RS485方向切换是硬件自动完成还是GPIO控制。市面上不少开发板用的是硬件自动流控方案比如有人在UART的RTS引脚上接了方向控制逻辑驱动层配好RTS后就能自动切换收发方向但也有很多板卡是用一个普通GPIO来控制DE/RE引脚的这种情况下你的应用程序就得在发送数据前拉高GPIO、发送完再拉低。在Linux系统里串口设备节点一般是/dev/ttyS0、/dev/ttyUSB0、/dev/ttymxc0、/dev/ttyTHS1这些。不同的SoC对应的设备节点名字差异很大我遇到过IMX6ULL的8路UART是ttymxc0~ttymxc7RK平台的是ttyS0~ttyS4树莓派是ttyAMA0或者ttyS0。所以项目初期第一件事就是确认你的串口对应哪个设备节点最简单的方法是把串口短接回环测试或者接一个USB转串口调试助手看看数据能不能通。1.3 方案选型libmodbus 还是自研协议栈先说说libmodbus这个库是纯C写的移植性很好在嵌入式Linux上直接交叉编译就能用。它同时支持RTU和TCP两种模式API设计得比较清晰modbus_new_rtu()创建上下文modbus_connect()连接串口modbus_read_registers()读寄存器modbus_write_register()写寄存器整个调用链很自然。但libmodbus也有它的问题。第一个是它超时机制比较粗粒度内部用一个全局的timeval结构控制收发超时在复杂的多设备轮询场景下响应时间波动比较大的时候表现不够灵活。第二是它自带的日志输出比较简陋排查线上问题时不太方便。第三是有些用过老版本的朋友可能遇到过头帧间隔处理不当导致的数据粘包问题虽然新版改进不少但心里还是得有这根弦。自研方案的话核心其实只有三个部分串口读写封装、CRC16校验、报文组装解析。因为Modbus RTU报文本身不复杂请求帧和响应帧的格式都很好理解自己写基本就是一两百行代码的事。对于只用到03功能和04功能的项目我甚至觉得自研更顺手因为不用引入额外依赖程序体积也小。我个人在这种项目上的习惯是如果工期紧、协议功能简单自研一个精简的Modbus RTU客户端如果设备多、功能码覆盖广、后续还可能对接PLC和触摸屏优先用libmodbus打底再在它外面包一层业务逻辑。这次就按自研的路线走一遍把串口配置、报文组装、CRC、数据解析、异常处理全部说透。2. 嵌入式Linux串口配置的核心细节2.1 设备节点与权限问题嵌入式Linux下打开串口设备跟打开普通文件是一样的open(/dev/ttymxc1, O_RDWR | O_NOCTTY | O_NDELAY)就能拿到文件描述符。但实际项目里新手最容易掉进去的坑就是权限问题。有些系统里普通用户访问不了串口设备节点ls -l /dev/ttymxc1一看用户和组都是root权限是crw-------这时候你的程序以非root用户跑起来open直接返回Permission denied。解决方式有这么几种。简单粗暴的就是直接在root下跑程序适合原型验证正规做法是写一个udev规则把串口设备节点加到dialout或者tty用户组里。我习惯在项目的README里写清楚sudo usermod -a -G dialout $USER然后重新登录或者newgrp dialout让用户组生效。还有些板子用的buildroot或者yocto系统需要检查一下/etc/group里有没有对应的组没有的话直接改/etc/udev/rules.d/下的规则文件。另外还有一个细节嵌入式Linux的串口设备节点可能在你加载驱动或者设备树配置变了之后换名字。我就遇到过一次本来程序里写死的是/dev/ttymxc3换了一版内核之后这个节点变成了/dev/ttymxc1排查了好久才发现是设备树里aliases配置的问题。后来学乖了要么用udev创建符号链接要么在应用层做设备节点扫描。2.2 termios结构体与关键参数配置Linux下配置串口核心就是操作struct termios这个结构体。先tcgetattr(fd, tio)拿到当前配置改完再tcsetattr(fd, TCSANOW, tio)写回去。需要关注的参数无非就是波特率、数据位、停止位、校验位还有接收超时和最少字节数这些。波特率这块常规的9600、19200、115200都是直接支持的标准值用cfsetispeed()和cfsetospeed()设置就行。但有些传感器波特率可能是4800、2400这种标准值也还好。我记得以前调试一个工控仪表它的默认波特率是57600这个也算是标准值没遇到麻烦。真正麻烦的是那种非标准的波特率比如之前有块板子的RS485芯片接的晶振比较特殊需要用BOTHER和struct termios2才能设置非标准波特率比如500000这种。遇到这种情况用ioctl(fd, TCGETS2, tio)和TCSETS2来处理配置c_ispeed和c_ospeed字段具体代码还依赖架构后面要是有人感兴趣可以单独写一篇非标准波特率的话题。数据位和停止位配置数据位是CS7或CS8要先清掉CSIZE位再设置停止位是靠CSTOPB控制的置位了就是2位停止位不置位是1位。校验位对应PARENB和PARODDPARENB使能校验PARODD置位为奇校验清零为偶校验无校验就是两个都不置位。原始模式最关键的一点是关闭掉行处理、规范模式和回显否则Linux内核会把串口收到的数据按回车换行去做特殊处理。我在早期刚接触Linux串口时总收到一些莫名其妙的字节最后排查发现是ICANON没关终端驱动把输入数据按行缓冲了数据量少的时候根本发不出来必须凑够换行符才一起上报。所以配置串口时cfmakeraw(tio)这个函数要优先调用它能把ICANON、ECHO、ISIG、IEXTEN这些标志一次性都清掉省得一点点处理。还有一组容易忽略的参数是VMIN和VTIME。VMIN是read操作最少要读到的字节数0表示不等待直接返回VTIME是等待超时单位是0.1秒0表示无限等待。Modbus RTU这种一问一答的模式我一般把VMIN设0、VTIME设10也就是100毫秒内没数据就返回这样应用层能及时感知超时。2.3 RS485方向控制与RTS配置这是嵌入式Linux下做Modbus RTU跟普通串口开发最大的区别之一。RS485是半双工通信同一时间只能收或者只能发。如果你用的是外置的RS485转TTL模块比如常见的MAX3485模块它的DE/RE引脚通常接在UART的RTS上那就需要在代码里处理RTS电平时序。在Linux下有几种做法。第一种是直接用ioctl的TIOCMBIS和TIOCMBIC操作TIOCM_RTS位发送前置高、发送完毕置低第二种是用内核里serial_rs485结构体通过ioctl(fd, TIOCSRS485, rs485conf)把串口设置成RS485模式让内核驱动自动处理RTS方向切换。第二种方式最省心但前提是底层的串口驱动支持这个ioctl命令。很多小伙伴在开发板上试发现设置TIOCSRS485返回错误ENOTTY这就说明驱动不支持只能用GPIO切换。用GPIO切换的做法是发送之前gpiod_set_value(gpio, 1)拉高DE数据发完之后再gpiod_set_value(gpio, 0)。这里有个很细微的点你调write()写串口数据只是进了内核的tty缓冲区write()返回并不代表数据已经从UART发送完毕。如果写完立刻拉低DE/RE很可能数据还没发完RS485芯片就把输出关了导致对方收到一个残帧。我实际的解决办法是写完数据后加一个小的延时延时时间根据波特率来算。比如波特率9600一个字节大约1.04ms发8个字节的数据帧加上3.5字符时间的帧间隔大概需要10ms左右我就延时12ms再切方向。这个方法虽然土但在没有硬件自动流控的板子上是最稳的。也可以用tcdrain(fd)等tty层把数据全部刷出去tcdrain会阻塞直到内核发送队列清空这个比固定延时更可靠。我项目里的习惯是write()之后立即tcdrain(fd)再根据实际芯片特性决定要不要额外加一点延时。3. Modbus RTU协议核心细节拆解3.1 报文结构与常用功能码Modbus RTU的报文结构其实很固定我用个最简单的读保持寄存器请求帧来举例。假设设备地址是1要读起始地址0x0000的2个寄存器01 03 00 00 00 02 C4 0B拆开看就是01是设备地址03是功能码读保持寄存器00 00是寄存器起始地址00 02是寄存器数量C4 0B是CRC16校验值低字节在前。一个完整的请求帧就这么简单。设备正常响应的时候格式是01 03 04 00 01 00 02 某个CRC其中04是返回的字节数后面紧跟的就是寄存器原始值。如果设备返回异常功能码最高位置1变成0x83然后跟一个异常码常见的异常码有01非法功能、02非法数据地址、03非法数据值、04从站设备故障。常用功能码我整理成表格方便查阅功能码含义操作类型典型用途0x01读线圈状态位读读取开关量输出状态0x02读离散输入位读读取外部开关状态0x03读保持寄存器字读读取可读写的参数/数据0x04读输入寄存器字读读取只读的测量值/状态0x05写单个线圈位写控制单个DO输出0x06写单个寄存器字写修改单个参数0x0F写多个线圈位写批量控制DO输出0x10写多个寄存器字写批量修改参数我做传感器数据采集的时候90%的场景只用得到0x03和0x04。读取温湿度传感器、光照度传感器、气体检测模块之类的设备传感器数据一般都映射为输入寄存器0x04功能码或保持寄存器0x03功能码看具体的设备手册。寄存器数量和数据类型的映射是另一个容易踩坑的地方。Modbus协议里寄存器是16位一个单位传感器返回的原始数据可以是无符号16位、有符号16位甚至32位浮点数或者32位整数。如果是32位数据通常会占据连续的两个寄存器这时候大小端顺序就是决定数据能不能解析对的关键。不同的设备厂商在这个问题上很随意有的高字节在前有的低字节在前有的还分字序。所以拿到一个不熟悉的传感器时第一件事就是先读它几个寄存器的原始值跟手册上的预期值对比确定字节顺序和数据类型。3.2 CRC16校验原理与代码实现Modbus RTU的CRC16校验算法是基于多项式0x8005的初始值是0xFFFF数据在计算时是以字节为单位先跟CRC低字节异或然后右移8次每次检测最低位如果为1就与0xA001异或。原文里提到的计算方式和很多资料里的描述是一致的这里给出一个简洁的查表法实现运行效率更高嵌入式设备上很有优势。查表法分为两步先预生成256个CRC表项然后逐字节查表更新。不过我实际项目里常用的是更省内存的位运算法因为表要占512字节虽然不多但在某些资源抠得紧的板子上我还是倾向用位运算。两种方式在正确性上没有区别只看你的资源和性能取舍。#include stdint.h uint16_t modbus_crc16(uint8_t *buffer, uint16_t length) { uint16_t crc 0xFFFF; uint16_t i, j; for (i 0; i length; i) { crc ^ buffer[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }代码本身很简单但使用时有几个细节要说清楚。首先发送数据时CRC要低字节在前、高字节在后所以发送完数据区后先发crc 0xFF再发(crc 8) 0xFF。其次接收数据时要把帧尾两个字节的CRC单独摘出来跟本地计算值比较相等才认为数据有效。比较的时候注意大小端别把顺序搞反了。3.3 寄存器数据解析与类型转换实际的Modbus设备返回的寄存器数据往往不是直接的物理量需要根据设备手册做换算。比如很多温湿度传感器它返回的湿度值是0到1000实际湿度是除以10也就是655表示65.5%RH温度可能是0.1℃为单位比如235表示23.5℃。这种换算逻辑是设备出厂就定好的每个厂商都有自己的定义只能仔细看设备手册。还有一些传感器返回的是有符号数。比如一个压力传感器量程是-100kPa到500kPa它可能用有符号16位整数表示那么0x8000到0xFFFF就代表负数。如果直接用无符号方式读出来再贴到浮点数里就会得到65535这样的错误值。处理办法是先判断原始值是否大于0x8000如果是就减去0x10000变成负数。32位数据处理是坑最多的。以两个寄存器为例设备返回0x01和0x02两个寄存器组成32位整数时有两种方式value (reg0 16) | reg1这是高16位在前value (reg1 16) | reg0这是低16位在前。很多设备手册会写“32位浮点数遵循IEEE754标准寄存器顺序为AB CD”。比如读取的寄存器原始值是0x40490FDB按IEEE754单精度解析就是3.14159。解析浮点数的时候我用的方法就是拿一个联合体或者直接用memcpy把字节拷进一个float变量里。C语言的位运算和类型转换在这个场景下容易出错直接复制字节是最安全的方式。#include string.h #include stdint.h float regs_to_float(uint16_t reg_high, uint16_t reg_low) { uint32_t tmp ((uint32_t)reg_high 16) | reg_low; float result; memcpy(result, tmp, sizeof(result)); return result; }4. 实操过程C语言实现RTU读写传感器数据4.1 串口初始化函数封装串口初始化的代码每个项目都会用到我一般封装成一个独立模块方便多平台复用。下面的代码是串口初始化的核心部分兼容了我前面讲到的termios配置、超时参数和RS485方向控制的预留接口。#include stdio.h #include fcntl.h #include unistd.h #include termios.h #include string.h #include errno.h #include sys/ioctl.h #include stdint.h int uart_open(const char *dev, int baudrate) { int fd open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial port failed); return -1; } struct termios tio; memset(tio, 0, sizeof(tio)); if (tcgetattr(fd, tio) ! 0) { perror(tcgetattr failed); close(fd); return -1; } cfmakeraw(tio); cfsetispeed(tio, baudrate); cfsetospeed(tio, baudrate); tio.c_cflag | CLOCAL | CREAD; tio.c_cflag ~CSIZE; tio.c_cflag | CS8; tio.c_cflag ~CSTOPB; tio.c_cflag ~PARENB; tio.c_cc[VMIN] 0; tio.c_cc[VTIME] 10; tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, tio) ! 0) { perror(tcsetattr failed); close(fd); return -1; } return fd; }配置项里我特意最后执行tcflush(fd, TCIOFLUSH)把之前残留的缓冲数据清掉防止历史脏数据影响判断。VTIME设为10也就是read最多等100毫秒这个时间在Modbus RTU轮询模式里比较合适既不会导致单个设备故障时卡死整个轮询队列也不会因为响应稍慢就频繁判定超时。这里要注意波特率的参数类型是speed_t可以直接传B9600、B115200这些宏。如果你想把波特率写到配置项里用整数表示就需要做个映射表或者像我一样把波特率直接作为参数传入调用时写uart_open(/dev/ttymxc1, B9600)。4.2 Modbus RTU报文发送与接收发送函数需要做几件事把CRC附加到报文尾部然后一次性写入串口。写入之后最好调用tcdrain(fd)确保数据真正从串口硬件发送完毕再切换RS485方向。如果你用的板子驱动支持TIOCSRS485自动切换那这步可以省掉如果不支持就要在tcdrain之后再拉一下GPIO。int modbus_send(int fd, uint8_t *frame, int len) { uint16_t crc modbus_crc16(frame, len); frame[len] crc 0xFF; frame[len] (crc 8) 0xFF; int ret write(fd, frame, len); if (ret ! len) { perror(write serial failed); return -1; } tcdrain(fd); /* 如果用了GPIO控制RS485方向在这里拉低 */ /* gpio_set_value(rs485_de, 0); */ return len; }接收函数的核心是处理超时和读到的响应帧校验。因为没有使用O_NONBLOCKread本身就是带超时的所以我们只要循环读直到读够一个完整的响应帧或者等到超时返回为止。再在应用层做校验和解析读到帧头后根据功能码判断期望的响应长度避免多读或少读。int modbus_receive(int fd, uint8_t *buf, int max_len, int expected_len) { int total 0; fd_set fds; struct timeval timeout; while (total expected_len) { FD_ZERO(fds); FD_SET(fd, fds); timeout.tv_sec 0; timeout.tv_usec 200000; int ret select(fd 1, fds, NULL, NULL, timeout); if (ret 0) { break; } ret read(fd, buf total, max_len - total); if (ret 0) { total ret; } } return total; }这种select加超时的写法比直接用VMIN/VTIME更直观也方便调整超时策略。Modbus RTU从站响应时间一般是几十毫秒特殊情况下慢的设备可能到500毫秒所以select超时设成200毫秒在多数场景够用。如果轮询的设备响应比较慢看各自的设备手册适当加大。4.3 读取传感器数据完整流程下面给一个完整的示例演示轮询一个Modbus RTU从站设备地址为1、读取起始地址为0的2个保持寄存器的流程。基于上面封装的串口和Modbus收发函数整个读取逻辑就只有几行。int main(void) { int fd uart_open(/dev/ttymxc1, B9600); if (fd 0) { return -1; } uint8_t req[8]; uint8_t resp[256]; req[0] 0x01; req[1] 0x03; req[2] 0x00; req[3] 0x00; req[4] 0x00; req[5] 0x02; int len modbus_send(fd, req, 6); if (len 0) { close(fd); return -1; } int total modbus_receive(fd, resp, sizeof(resp), 7); if (total 7) { uint16_t crc_calc modbus_crc16(resp, total - 2); uint16_t crc_recv resp[total - 2] | (resp[total - 1] 8); if (crc_calc ! crc_recv) { printf(CRC error: calc0x%04x recv0x%04x\n, crc_calc, crc_recv); close(fd); return -1; } if (resp[1] 0x03) { uint16_t reg0 (resp[3] 8) | resp[4]; uint16_t reg1 (resp[5] 8) | resp[6]; printf(reg00x%04X (%d)\n, reg0, reg0); printf(reg10x%04X (%d)\n, reg1, reg1); } } else { printf(Timeout, no valid response\n); } close(fd); return 0; }这段代码里我判断响应长度用的是期望值7字节也就是1字节地址 1字节功能码 1字节字节计数 2个寄存器各2字节 2字节CRC。如果你读3个寄存器那就是1116211字节这个期望长度要根据读取的寄存器数量动态计算。还有一种做法是先读到帧尾CRC之后再来判断但那样代码会复杂一点。对于协议相对简单、链路环境不糟糕的场景先算好期望长度更实用。4.4 多传感器轮询策略实际项目里一个串口上往往挂了不止一个传感器比如一个气象站项目一条RS485总线上挂了风速、风向、温湿度、气压四台设备轮询就是个绕不开的话题。最简单的轮询逻辑就是for循环遍历从站地址表逐个发送请求等待响应解析然后进入下一个从站。代码不复杂但问题在于如果有某个从站掉线了它的响应超时时间会拖慢整个轮询周期。假设每个从站超时200毫秒四个从站都正常还好一轮几十毫秒但要是掉线两个一轮就得400多毫秒风速这种对实时性有要求的传感器就来不及更新了。我的做法是把轮询任务拆成独立线程超时时间和往返时间分开控制。整个轮询周期设定为1000毫秒每次循环里按顺序向每个从站发请求但每个从站的等待时间根据设备手册单独设定。比如风速传感器响应快给它80毫秒气压传感器那个老旧设备响应慢给它300毫秒。超时不盲目统一最大程度保证整个总线周期稳定。另外对掉线设备需要做连续失败计数连续失败5次以上就把它的轮询频率降为原来的十分之一避免它一直拖累总线。轮询线程和业务处理线程之间用共享缓冲区加互斥锁传递数据。Modbus本身是一问一答式的所以轮询线程里不需要考虑并发但业务线程读数据时一定要加锁否则可能取到半个更新状态。我记得第一次做多传感器项目时没加锁业务端偶尔会读到全零的数据排查半天最后发现是共享结构体在两个线程之间裸奔加个pthread_mutex_lock就解决了。5. 常见问题与排查技巧实录5.1 问题速查表下面这张表汇总了我做Modbus RTU开发过程中最常遇到的几类问题以及对应的排查方向。每个问题都是实际项目里被反复问到的。现象可能原因排查与解决串口open返回Permission denied用户权限不够设备节点owner是root加用户到dialout组或者udev规则改权限发送数据后传感器无响应RS485方向没有切换数据没发出去用示波器或逻辑分析仪看TX/RX和DE电平时序响应断断续续偶尔收到完整帧偶尔没有波特率、数据位、校验位配置错误先用串口助手验证参数再检查termios配置能收到数据但CRC一直失败数据被干扰或者RS485没接终端电阻检查双绞线屏蔽、加120欧终端电阻或者按字节打印响应帧比预期长上一个请求的响应或设备主动上报的数据残留在缓冲区每次请求前tcflush清缓冲区或者收到完整帧后多读一次清空同一帧数据偶尔分两次收到VMIN和VTIME配置不合适read被拆分成多次返回用select加指定期望长度的循环读取保证收够完整帧读寄存器返回异常码0x02起始地址或寄存器数量超出了设备地址范围仔细对照设备手册的寄存器映射表读寄存器返回异常码0x03请求的数据值非法比如寄存器数量为零检查请求帧里起始地址和数量的组合是否符合设备定义多个从站设备一起挂的时候总线上有冲突有设备地址冲突或者某个设备响应时间异常快/慢逐一断开设备排查确保每个从站地址唯一且总线拓扑规范5.2 逻辑分析仪和示波器的使用心得调试Modbus RTU最靠谱的工具不是printf而是逻辑分析仪或者示波器。一个二三十块钱的USB逻辑分析仪就能解出串口波形查看完整数据帧这对排查RS485通信问题帮助巨大。我调试RS485方向切换问题时就习惯把通道1接UART的TX引脚通道2接RS485芯片的DE/RE引脚通道3接RS485的A-B差分信号。三路同时抓一眼就能看出来DE拉高的时间是不是覆盖了整个数据帧发送时间。如果DE在数据发完前就拉低了那RS485总线上必然出现残缺帧从站根本不会识别。逻辑分析仪还能直接解出Modbus RTU的报文内容省去用串口助手盲猜数据的过程。市面上常见的逻辑分析仪软件都内置了UART协议解析把波特率设成跟串口一致接好线就能看到十六进制字节流。配合UART解码的采样率设置把采样率调到2M以上解码稳定可靠。5.3 字节打印与现场排查技巧很多嵌入式设备上没有逻辑分析仪也没有示波器只能靠打印调试信息。这种情况下一开始就要做对一件事所有收发数据都用十六进制完整打印出来不要只打印业务解析后的结果。我之前遇到过一个很奇怪的问题传感器返回的温湿度数据偶尔跳变到65535一开始怀疑是线缆干扰后来把所有收发字节打印出来发现是bus上出现了一个额外的0xFF字节数据帧整体右移了一位。再查下去发现是PCB板子上RS485芯片的RO引脚上拉电阻焊接不良导致空闲状态下收到全1的电平。如果不是完整打印十六进制数据这个问题根本排查不出来。另外还有一个实用的技巧就是可以在程序里加一个debug开关打开之后每个请求帧和响应帧都打印同时打印出CRC计算结果和解析出来的寄存器值。现场调试的时候把日志切到debug模式问题往往一看就明白了。我习惯把这类日志做成环形的内存buffer调试完通过串口导出不直接打到大串口上避免影响Modbus通信本身的时间特性。从实际效果来说嵌入式Linux下的Modbus RTU开发并不难真正决定项目成败的往往是对串口细节和时序的把控。把串口初始化配置做扎实把CRC校验和超时机制写严谨多花点时间在协议分析工具上一套稳定可靠的传感器采集系统很快就能搭起来。做嵌入式开发遇到问题多从物理层和数据链路层的角度去想大部分疑难杂症都能找到根因。