十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式Linux Modbus RTU开发:串口配置到协议实现

嵌入式Linux Modbus RTU开发:串口配置到协议实现 1. 串口配置嵌入式Linux下搞定物理链路做嵌入式Linux上的Modbus RTU开发第一关就是串口。RTU跑在RS485或者RS232物理层上Linux应用层就是操作一个终端设备文件比如/dev/ttyS0、/dev/ttymxc1、/dev/ttyUSB0这类节点。开头必须先讲清楚一个容易被新手忽略的坑Modbus RTU是8位数据位、1位停止位也可配置2位、无校验或者偶校验波特率常见9600、19200、115200这组参数必须在打开串口后立刻配置好否则后续读传感器数据全是乱码或者压根没响应。Linux下配置串口有两种路子。一种是命令行直接拿stty工具调适合快速验证硬件链路另一种是写C代码用termios结构体配置这是应用开发的正道。我建议先做第一步的物理链路连通性测试再进入协议开发阶段不然协议写完了才发现RS485的收发方向没控制住排查起来头大。先看stty的快速验证法子。假设传感器接在/dev/ttyS1上波特率96008N1执行stty -F /dev/ttyS1 9600 cs8 -cstopb -parenb这里cs8是8位数据位-cstopb表示1位停止位如果cstopb就是2位停止位-parenb表示无校验。如果是偶校验用parenb -parodd奇校验用parenb parodd。验证配置是否生效执行stty -F /dev/ttyS1 -a会打印出当前所有终端参数重点看speed、cs8、parenb这些字段。但实话说stty只是临时工具真正的产品代码必须用termios写。下面是一段我常用的串口初始化函数直接复制改改就能用#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include termios.h #include errno.h int uart_init(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 options; memset(options, 0, sizeof(options)); tcgetattr(fd, options); /* 设置波特率 */ speed_t speed; switch (baudrate) { case 9600: speed B9600; break; case 19200: speed B19200; break; case 38400: speed B38400; break; case 115200: speed B115200; break; default: fprintf(stderr, unsupported baudrate: %d\n, baudrate); close(fd); return -1; } cfsetispeed(options, speed); cfsetospeed(options, speed); /* 8N1: 8数据位、无校验、1停止位 */ options.c_cflag | (CLOCAL | CREAD); /* 忽略调制解调器控制线使能接收 */ options.c_cflag ~CSIZE; options.c_cflag | CS8; options.c_cflag ~PARENB; /* 无校验 */ options.c_cflag ~CSTOPB; /* 1位停止位 */ /* 关闭流控关键Modbus RTU一般不用硬件或软件流控 */ options.c_cflag ~CRTSCTS; options.c_iflag ~(IXON | IXOFF | IXANY); /* 原始模式输入输出不做任何转换 */ options.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); options.c_oflag ~OPOST; /* 读取超时设置后面细讲 */ options.c_cc[VTIME] 10; /* 1秒超时 */ options.c_cc[VMIN] 0; /* 非阻塞模式 */ tcsetattr(fd, TCSANOW, options); /* 清空串口缓冲 */ tcflush(fd, TCIOFLUSH); return fd; }这个函数里有几个细节值得展开说说。CLOCAL和CREAD必须置位。CLOCAL表示不关心调制解调器的载波检测信号对RS232设备来说如果这个位没置位串口会一直等待DCD信号很多USB转串口模块直接就不工作。CREAD是使能接收不设置的话你读串口永远是空的。关闭流控CRTSCTS这行对RS485通信尤其重要。很多工控板卡的RS485收发器是自动方向切换的但如果你打开了硬件流控RTS/CTS引脚状态会被驱动层接管大概率造成收发混乱。Modbus RTU是主从问答制物理时序本来就紧张千万别在这里给自己挖坑。VTIME和VMIN这两个参数直接决定read()的行为。这是新手最容易懵的地方。VMIN是read返回前需要读到的字节数最小值VTIME是等待时间单位0.1秒。我上面配的是VMIN0、VTIME10意思是read()最多阻塞1秒如果这段时间内没有数据就返回0。这种超时模式对Modbus主站特别合适因为你发完请求帧后要知道从站有没有响应、响应是否超时。如果用默认的VMIN1read会一直阻塞到收到至少1个字节这样你根本没法实现超时机制程序一旦卡死就是永久阻塞。我自己的习惯是VMIN0、VTIME5到20即0.5秒到2秒具体看从站的响应时间要求。Modbus RTU标准规定从站收到请求后必须在8个字符时间内开始响应但那是理想情况实际工业传感器有的就是慢性子尤其是那些内部带协议转换的模块可能拖到几十毫秒甚至上百毫秒。我的建议是先按从站手册给的响应时间上限加一倍余量去设置。比如手册说典型响应时间50ms那就设置500ms超时既不会因为网络抖动误判也不会因为等待太久拖慢轮询周期。还有个细节是读串口前要tcflush。这个操作清空内核缓冲区里可能残留的脏数据。特别是你刚打开串口、或者上一帧处理出错时缓冲区里可能还躺着半截旧数据不清洗的话会被当成新帧读到造成帧错位。我一般是在打开串口后做一次tcflush在每次发请求前也做一次。2. Modbus RTU协议核心从报文格式到CRC校验串口通了接下来就是协议本身。Modbus RTU是工业现场用得最多的串行通信协议本质就是主从问答。一个总线上只能有一个主站通常是PLC、触摸屏或者我们的嵌入式Linux主板最多挂247个从站地址1到2470是广播地址。从站之间不直接通信所有数据交换都靠主站发起。一个完整的RTU请求帧长这样字段长度说明从站地址1字节目标从站地址范围1~247功能码1字节告诉从站要做什么操作数据段N字节寄存器地址、数量、数据等CRC162字节低字节在前高字节在后响应帧结构类似只是数据段内容不同但如果你读到的响应帧功能码最高位是1比如读保持寄存器0x03返回0x83说明从站报错了后面跟的那个字节是异常码。03是非法数据值02是非法数据地址01是非法功能码04是从站设备故障这几种最常碰到。功能码用得最多的是0x03读保持寄存器、0x04读输入寄存器、0x06写单个保持寄存器、0x10写多个保持寄存器。读传感器数据主要是0x03和0x04区别在于0x03读的是可写的保持寄存器0x04读的是只读的输入寄存器。很多温湿度传感器、压力传感器用的是0x04但也有的设备用0x03封装所有参数具体以你手上设备的寄存器表为准。这个最基础也最不能想当然。CRC校验是RTU和ASCII另一种Modbus变体基本淘汰了最大的区别之一。RTU用CRC16多项式是0xA001计算时初始值为0xFFFF。我不会拿CRC表去水文章直接给一个最常用的查表法实现你直接嵌入到代码里比逐位计算的性能高好几个量级static const unsigned char crc_hi_table[] { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, ... }; static unsigned short crc16(unsigned char *buf, unsigned int len) { unsigned char crc_hi 0xFF; unsigned char crc_lo 0xFF; unsigned int i; while (len--) { unsigned char index crc_hi ^ *buf; crc_hi crc_lo ^ crc_hi_table[index]; crc_lo crc_lo_table[index]; } return (unsigned short)((crc_hi 8) | crc_lo); }上面的crc_hi_table我故意截断了你别直接抄。完整的表太长不贴网上搜“MODBUS CRC16查表法 C语言”到处都有完整实现。重点是发送时先发CRC低字节再发高字节这是很多新手第一次调RTU就失败的坑明明把请求帧发过去了从站就是不回最后发现是CRC字节序搞反了。记住一句话RTU的CRC是低字节在前。接收端校验的时候把你收到的完整帧含CRC用同一个crc16函数算一遍结果应该等于0这是Modbus RTU校验最优雅的特性不需要把收到的CRC抠出来再和自己算的结果比对。为了保险我再给一个逐位计算的版本适合嵌入式平台对存储空间有极限要求、不想放两张256字节表的情形unsigned short crc16_bitwise(unsigned char *buf, unsigned int len) { unsigned short crc 0xFFFF; unsigned int i, j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; }这个版本理解起来容易也方便你对照协议文档去验证自己的CRC实现是否正确。实际项目中如果CPU主频在300MHz以上两种算法性能差异几乎感觉不出来但在主频几十兆的MCU上查表法优势明显。你在嵌入式Linux上开发CPU资源一般不是瓶颈用哪个都行。3. 应用层代码实现读传感器数据与写寄存器协议抄明白了就开始写应用层逻辑。这里给一个完整的读取保持寄存器功能码0x03的C语言实现包含请求帧组包、发送、接收、CRC校验、解析一套走完。先定义帧结构体方便后续扩展typedef struct { unsigned char addr; unsigned char func; unsigned char data[254]; unsigned char crc_lo; unsigned char crc_hi; } modbus_frame_t; typedef struct { int fd; unsigned char slave_addr; unsigned int timeout_ms; /* RTU inter-frame delay: 3.5字符时间 */ } modbus_rtu_t; int modbus_read_holding_registers(modbus_rtu_t *ctx, unsigned short start_addr, unsigned short quantity, unsigned short *regs) { unsigned char req[8]; unsigned char rsp[256]; int len; unsigned char *buf; unsigned short crc; if (quantity 1 || quantity 125) { fprintf(stderr, quantity must be 1~125\n); return -1; } req[0] ctx-slave_addr; req[1] 0x03; req[2] (start_addr 8) 0xFF; req[3] start_addr 0xFF; req[4] (quantity 8) 0xFF; req[5] quantity 0xFF; crc crc16(req, 6); req[6] crc 0xFF; /* 低字节在前 */ req[7] (crc 8) 0xFF; tcflush(ctx-fd, TCIOFLUSH); if (write(ctx-fd, req, 8) ! 8) { perror(write failed); return -1; } len read(ctx-fd, rsp, sizeof(rsp)); if (len 0) { fprintf(stderr, read timeout or no data\n); return -1; } /* 收到异常响应 */ if ((rsp[1] 0x80) ! 0) { fprintf(stderr, slave exception code: 0x%02X\n, rsp[2]); return -1; } /* 校验从站地址和功能码 */ if (rsp[0] ! ctx-slave_addr || rsp[1] ! 0x03) { fprintf(stderr, invalid slave address or function code\n); return -1; } /* CRC校验整个响应帧 */ if (crc16(rsp, len) ! 0x0000) { fprintf(stderr, response CRC check failed\n); return -1; } /* 解析寄存器数据 */ int reg_count rsp[2] / 2; for (int i 0; i reg_count; i) { regs[i] (rsp[3 i * 2] 8) | rsp[4 i * 2]; } return reg_count; }重点讲几处。第一请求帧长度是固定的8字节闭着眼都能写出来。第二连续读寄存器数量上限是125个0x7D这是协议限定的因为响应帧最多253字节除去从站地址、功能码、字节数计数字段和CRC数据区最多250字节也就是125个寄存器。第三响应帧的合法性检查至少做三层从站地址是不是我请求的、功能码对不对、CRC校验过不过。少了任何一层你都可能在总线上有其他设备干扰时拿到一帧脏数据却浑然不觉。读取输入寄存器功能码0x04的代码几乎一模一样只需要把01改成03再把req[1]和rsp[1]的判断改成0x04。我经常看到有人把03和04搞混导致读了半天全是0。这里有个经验性判断方法大多数国产温湿度传感器、光照传感器、土壤传感器用的都是04功能码读输入寄存器因为数据是传感器采集结果本身不可写而很多支持参数配置的设备比如变频器、PID控制器用03读保持寄存器。但也有例外比如一些厂商把校准参数放在04输入寄存器里读出来是能修改的。拿到新设备第一步永远是翻手册看寄存器表别猜。再说写单个寄存器功能码0x06的实现。这个在传感器应用中不如读频繁但很多标定设备会用到。比如某些气体传感器你需要写寄存器来触发零点校准。请求帧是8字节和读请求长度一样int modbus_write_single_register(modbus_rtu_t *ctx, unsigned short reg_addr, unsigned short reg_value) { unsigned char req[8]; unsigned char rsp[8]; int len; unsigned short crc; req[0] ctx-slave_addr; req[1] 0x06; req[2] (reg_addr 8) 0xFF; req[3] reg_addr 0xFF; req[4] (reg_value 8) 0xFF; req[5] reg_value 0xFF; crc crc16(req, 6); req[6] crc 0xFF; req[7] (crc 8) 0xFF; if (write(ctx-fd, req, 8) ! 8) { return -1; } len read(ctx-fd, rsp, sizeof(rsp)); if (len 0) { return -1; } return crc16(rsp, len) 0; }写寄存器后从站会原样回显请求帧所以响应长度也是8字节。如果你收到的响应帧内容和请求一模一样基本就是写成功了。有些设备写寄存器后还需要读一遍确认值回读这时你可以再发一个0x03请求看看目标寄存器当前值是否等于刚才写的值。实测下来有些国产设备写操作时序比较慢接收到写请求后要几十毫秒才能完成内部EEPROM擦写如果紧接着马上读读到旧值是正常的。这种设备建议写完成后延时100到200ms再读。还有一个非常实用的技巧Modbus RTU没有广播式的“读取”操作但地址0是广播地址可以发写操作让所有从站同时执行比如统一启动。广播帧从站不会回复所以你的主站软件要绕过等待响应的逻辑否则会白白等一个超时周期。4. 帧间隔与时序RTU最容易翻车的地方Modbus RTU对时间有严格定义两个字节之间的间隔不能超过1.5个字符时间超过则从站认为是两帧数据一帧结束的标志是静默时间达到3.5个字符时间以上。字符时间T 1比特时间 × 11比特1个起始位 8个数据位 1个校验位如果有 1个停止位总共11比特。以9600波特率为例1比特时间是104.2μs3.5个字符时间大约是4ms1.5个字符时间大约是1.7ms。这个时序要求在实际项目中意味着什么意味着你不能一上来就裸写串口然后把收发的时序搞得乱七八糟。举个例子很多从站的实现是在接收到完整帧后内部处理一小段时间再回响应但也有一些从站特别严格如果主站发数据的时候中途停顿超过了1.5个字符时间从站直接丢弃这帧数据不回任何响应。这在调试串口传感器时常见的现象就是用printf打印调试信息到同一个串口或者USB转串口驱动不稳定导致数据流有间隙从站莫名不响应了。而且这种问题极其恶心可能跑100次才出现一次因为间隙偶尔才会超过1.5个字符时间。解决方案有几个。第一不要在发送请求帧的代码路径里加任何调试打印尤其是那种会锁串口的调试方式一旦锁了就影响发送连续性。第二如果发送和接收在不同线程发送期间接收线程要暂停读取避免内核缓冲区被响应数据填满导致后续读取异常。第三在驱动层面把串口设置为无延迟发送这个在Linux下是默认行为但如果你用了某些串口工具库比如libserialport要注意默认配置是否做了制约。另一个时序关键点就是我在前面串口配置里提到的VTIME/VMIN。主站发送完请求后读取响应应该有一个明确的超时窗口。你不可能无限等下去否则轮询中断。常见的超时公式是响应超时 从站最大响应时间 3.5字符时间 × 2。我习惯直接给300ms到500ms的固定值但如果你要轮询几十个传感器每个节点浪费500ms一轮下来就是几十秒根本不现实所以轮询间隔要和超时时间匹配着调。再分享一个调参技巧。当你发现读某个从站偶尔超时时不要急着加大超时时间先用逻辑分析仪或示波器抓一下串口波形看从站到底有没有回数据、回的数据是否在请求结束后的固定间隔内。如果从站回了但你的程序没读到大概率是VTIME设置过短内核缓冲还没把数据攒完整就返回了。如果从站没回反而可能是你发出去的请求帧格式不对从站压根没识别出来。抓波形一次就能定位比盲改参数快十倍。没有示波器也可以先用USB转TTL板连接RX线用另一个串口工具比如minicom、cutecom观察原始数据流。5. 多从站轮询与临界资源保护实际项目中不会只接一个传感器。一条RS485总线上挂多个从站设备是常态这时你的主站程序就得按地址轮询。我见过很多初学者的代码读第一个从站数据没问题加了第二个从站就各种乱套核心原因是没做好状态隔离。轮询的基本结构是定时器驱动每个周期内按顺序访问所有从站。伪代码大致是这样while (1) { for (int i 0; i slave_count; i) { int ret modbus_read_holding_registers(ctx[i], start_addr, quantity, regs); if (ret 0) { save_to_database(ctx[i].slave_addr, regs, ret); } else { record_error(ctx[i].slave_addr); } usleep(10000); /* 帧间隔保护 */ } sleep(poll_period); }这个简单结构里有几个隐藏问题。第一如果某个从站没有响应read会等待超时这个超时时间内CPU空转后面的从站全部排队延迟。解决思路是给每个从站设置可配置的优先级和超时统计连续多次失败的从站可以临时跳过等下一周期再重试。第二总线上所有从站的请求必须是串行的绝不可能两个线程同时往总线上发数据。所以在多线程架构里一定要用一个互斥锁把“发送请求帧 接收响应帧 CRC校验”这个整块逻辑包起来确保同一个时刻只有一帧在总线上。哪怕你用的是多个串口连多个485总线只要共用同一个应用层代码也要注意上下文隔离。第三数据保存的时机也很讲究。我建议读到一帧数据后立刻拷贝到应用层缓存不要带着锁去做数据库操作或日志打印否则会把总线的锁占用时间拉长影响其他从站的轮询节奏。很多人在这一步做错导致轮询周期越来越长最后系统假死。这里分享一个踩过的坑某次项目里我在读取传感器数据的回调里直接打印到云端结果每当网络抖动严重时传感器的轮询就大批量超时后来抓日志才发现打印机房的网络请求最长会阻塞几百毫秒而这期间485总线锁被占用了所有传感器全部错过了轮询窗口。后来把所有网络操作全部异步化轮询响应才恢复正常。做嵌入式Linux一定要谨记串口时序是硬实时要求绝不能在临界区内做任何可能长时间阻塞的操作。6. 常见问题与排查技巧实录把这个项目浓缩成一份问题速查表你在调试时对着查至少能解决九成的故障。现象可能原因排查/解决方式发请求后无任何响应串口参数配置错误波特率/校验位不一致用stty核对参数逻辑分析仪抓波形看TX有无数据发请求后无任何响应RS485收发方向控制不当检查DE/RE引脚是否由GPIO控制确认发送完成后是否恢复接收模式发请求后无任何响应从站地址错误逐帧解析请求用串口调试助手手动发同样帧验证响应回来了但CRC校验不过接线干扰或波特率误差用带屏蔽的485线检查终端电阻120Ω检查地线共地响应回来了但CRC校验不过收帧不完整半帧被截断加大VTIME或检查串口读取逻辑是否收到半个帧就返回了响应帧长度不对寄存器数量超限确认quantity不超过125响应帧字节数按要求计算偶发超时总线上有其他干扰源示波器看波形检查485总线末端是否缺少偏置电阻从站报异常码03请求的起始地址数量超出范围对照从站寄存器表修正起始地址和数量读取的数值一直是0功能码用错读输入寄存器用了03翻手册确认传感器数据在03还是04地址空间数值读出来是乱码数据格式没解析对可能是有符号/无符号、大小端、字节序未处理确认寄存器数据端序和数据类型定义有几点展开说说。RS485的收发方向控制是硬件设计中特别要注意的。很多RS485芯片支持自动方向切换比如MAX13487、SP3485这类带自动换向功能的芯片软件不用管DE/RE引脚。但如果你用的是MAX485这种经典芯片DE和RE需要手动控制。嵌入式Linux应用里通常会有一个GPIO来控制收发方向。我遇到过的坑发送完请求后忘了及时把GPIO拉低切回接收模式导致响应帧前半段被自己吃掉后半段才收到CRC必然不过。如果你不想手动控制方向硬件设计时直接选自动换向芯片省去软件的麻烦稳定性还更高。终端电阻是另一个经典坑。485总线两端需要各并联一个120Ω终端电阻。如果只是短距离几米点对点调试不加终端电阻一般也能跑但一旦总线长度超过几十米或者挂载节点多没有终端电阻就会出现信号反射表现为数据偶发错误、时好时坏。排查方式是读取的时候不断打印CRC错误次数如果CRC错误随距离增加而增多检查终端电阻大概率不会错。大小端问题也要特别注意。Modbus RTU协议本身规定寄存器数据是16位大端传输也就是高字节先发送。但传感器内部的数据格式不一定就是高字节在前。我遇到过一款湿度传感器数据手册写着寄存器地址0x0001保存湿度值但没明确写字节序实际读回来发现高低字节是反的。这种问题没有捷径只能对照手册实际校准值去确认。更复杂的情况是32位float数据很多传感器会把浮点数拆成两个16位寄存器存储这时不仅要处理字节序还要处理寄存器顺序。比如有的设备低地址存高16位高地址存低16位有的恰恰相反。我的做法是写一个通用的32位float解析函数支持寄存器大小端切换参数化配置实测省了很多事。还有一个排查技巧值得单独说在串口上接一个USB转TTL板子用PC端的串口调试助手同时监听总线上主站发出的请求和从站返回的响应。这相当于给Modbus总线装了监控探头能瞬间看出是哪一方出了问题。PC端可以用MThings、Modbus Poll这类工具或者最简单的方式是拿一个USB转485的板子接在总线上用sscom之类的工具打开观察原始数据。这是我排查Modbus问题的第一反应比在代码里加一百句printf都高效。调试期间的日志打印也有一些心得。不要直接把read的原始字节全部printf出去那样日志又长又难读。我习惯封装一个hex dump函数只在DEBUG宏开启时输出帧数据、方向和错误原因。格式大致是#ifdef DEBUG_MODBUS printf([MODBUS] TX(8): %02X %02X %02X %02X %02X %02X %02X %02X\n, req[0], req[1], req[2], req[3], req[4], req[5], req[6], req[7]); #endif这种日志在调试完成后的Release版本里会通过条件编译完全去掉不影响线上性能。另外如果在同一个调试会话里既要打印日志又要发Modbus帧注意调试串口不要和Modbus总线串口搞混否则日志本身就会污染总线触发我在第4节说的帧间隔问题。7. 嵌入式Linux平台上的工程化落地说到工程化还有几层事情需要补齐很多项目死在不该忽视的细节上。首先是设备树和驱动层面。在嵌入式Linux上串口对应的设备节点如果不存在或者名称和你程序里硬编码的不一致程序直接打不开设备。常见板卡上ttymxc0到ttymxc4对应不同的UART外设z-turn和树莓派可能是ttyS0、ttyAMA0或者ttyUSB0。我建议把串口设备路径做成配置文件或者命令行参数不要写死在代码里。同时要确认目标串口有没有被其他服务占用比如systemd的getty进程默认会占用console串口你的Modbus程序肯定打不开它。用systemctl getty相关命令检查一下把不需要的getty服务禁用掉。其次是打开串口时的权限问题。普通用户访问/dev/ttyS0或/dev/ttyUSB0可能会碰到permission denied有两个解决办法一是把当前用户加入dialout组sudo usermod -aG dialout $USER二是用udev规则给串口设备设置666权限。产品化阶段推荐用udev规则按设备路径或厂家ID创建稳定的符号链接比如/dev/sensor_bus这样即使串口设备号变了程序里还是能稳定访问同一个物理串口。这个细节在批量部署时特别有用因为USB转串口设备在重启后设备节点可能从ttyUSB0变成ttyUSB1程序配置全部得改用符号链接就一劳永逸。还有就是把Modbus通信封装成库。上面的代码都是面向过程的函数但实际工程里我会把这套东西封装成一个modbus_client结构体包含串口fd、从站地址、超时配置、错误统计字段对外暴露read_register、write_register、read_multiple_registers这几个接口。上层业务代码根本不需要知道Modbus协议细节只要调用接口传参就行。这样做的最大好处是隔离变化如果后期把某个传感器从Modbus RTU换成了Modbus TCP上层业务代码几乎不用改只需要换一个底层通信回调函数。这种分层思路在真实项目里的价值是你不会在改协议时不小心碰坏了业务逻辑。我曾经在一个项目里吃过亏为了临时支持一个走私有协议的传感器在业务代码里塞了一堆if-else判断设备类型半年后代码已经没人愿意改了。后来花了两个晚上把所有通信逻辑统一收敛到modbus_client层用不同的协议实现注册同一套接口整个系统清爽了不少。再提醒一个不算技术问题的坑RS485总线的GND。很多人在调试时只接A、B两根线设备少时可能正常但设备一多、距离一远共模电压漂移就会导致通信不稳定甚至芯片烧毁。正确做法是确保所有485节点的GND电气连接在一起。如果距离特别远考虑使用带隔离的RS485模块。这个坑属于“调试时一切正常现场部署就出事”的典型前期节约的一根GND线后期可能花一整天都排查不出来。如果你想快速跑通一个Demo不必先把代码写到产品级。直接用Python的pymodbus库配上pyserial几分钟就能读一次传感器数据。pymodbus里面的ModbusSerialClient类封装了RTU协议栈你只需要设置串口、波特率、超时、从站地址和寄存器地址。这特别适合做原型验证确认传感器能读到数据之后再决定用C语言还是继续用Python做正式产品。如果产品对实时性和稳定性要求高我用C如果是小批量工具类应用Python完全够用。最后补充一点字节对齐时间。在嵌入式Linux里写Modbus主站性能瓶颈几乎不会出现在CPU计算CRC上而是IO等待上。你轮询几十个传感器的周期主要消耗在串口超时等待上。所以如果你感觉系统轮询太慢不要急着优化CRC算法先看看有没有从站总是超时、有没有串口缓冲区设置太小导致频繁唤醒。我见过最夸张的情况一个从站固件有bug每次响应都要延迟1秒多才回直接把整个轮询周期拖垮。定位到是它的问题之后直接在代码里把它标记为慢速设备给它单独的轮询周期整个系统的响应立刻恢复正常。这套从串口到协议到工程化落地的思路基本覆盖了嵌入式Linux上Modbus RTU开发的完整链路。我个人的体会是Modbus协议本身不复杂真正的复杂度都在物理层和时序细节上串口参数配错了表现是什么RS485方向没切换好表现是什么帧间隔超了表现是什么。把这些底层的东西吃透了上层写什么功能码、读什么寄存器都是按图索骥的事。你踩的这些坑绝大多数在动手之前看一遍类似的经验记录就能避开这也是我写这篇内容最想达到的目的。
返回列表