尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

嵌入式Linux下Modbus RTU主站开发:从串口配置到传感器轮询

嵌入式Linux下Modbus RTU主站开发:从串口配置到传感器轮询 搞嵌入式Linux开发的十有八九会跟Modbus打交道。不管是接个温湿度传感器、读个电表还是跟PLC对数据Modbus RTU基本就是工业现场默认的“普通话”。这篇东西想把整个流程完整串一遍从Linux下串口设备怎么配、termios参数怎么调到Modbus RTU报文怎么组帧、CRC怎么算、传感器数据怎么读回来再到代码架构怎么设计才能稳定轮询——全是我实际碰过的问题和踩过的坑。这个项目适合刚接触嵌入式Linux不久、想用C语言在板子上写一个Modbus主站去读从站传感器的朋友也适合那些已经在用第三方库、但遇到稀奇古怪问题不知道怎么排查的人。我会尽量说清楚每一步为什么要这么干而不是只丢一段能跑能用的代码。1. 项目思路与方案选型为什么说Modbus RTU是嵌入式Linux的“必修课”1.1 Modbus到底解决什么问题Modbus是1979年莫迪康Modicon现在属于施耐德搞出来的一个应用层串行通信协议。它最核心的设计思想就两个主从Master/Slave模型以及查询—响应Request/Response机制。总线上只有主机能主动发请求从机收到请求以后回一个响应同一时刻只允许一个主机存在。这么设计放到今天看确实显得简陋但也正是这份简陋让它在工业现场整整活了四十多年——稳定、简单、透明几乎没有解析成本。RTURemote Terminal Unit是Modbus在串行链路上的一个帧编码方式数据用二进制传输每一帧包含地址码、功能码、数据区和CRC校验。跟ASC-II模式相比同样功能码和寄存器数量的报文RTU模式起码少传一半字节在9600波特率这种公用频段上优势非常明显。所以工业传感器、PLC、变频器、电表默认情况下绝大多数都支持RTU协议这也是嵌入式Linux端最常遇到的一个通信需求。还有一个很多人纠结的问题工业现场Modbus TCP也很多为什么偏要用RTU其实很简单——你拿不到网线只能走RS485两根线。我做过一个采集项目现场几十个温湿度传感器用RS485手拉手串成总线一个板子就能把所有数据轮询回来如果硬要上TCP就得给节点全部重新拉网线成本完全不是一个量级。1.2 硬件怎么选串口、USB转485、还是板载UART做嵌入式Linux第一步就得在硬件上分清你手上到底有哪些串口资源。这里我按实际经验给个选型结论如果只是开发调试阶段用USB转RS485的线是最快的方案也就是ttyUSB0节点插上就能用缺点是有偶发的掉线风险长期跑项目不建议。如果是小批量产强烈建议用板载UART配合SP3485、MAX3485这类收发器芯片对应设备节点一般是ttyS0、ttymxc1、ttymxc2这类。板载串口的稳定性和中断响应在可靠性和实时性上比USB转出来的要好很多。如果需要长期高波特率跑大流量尽量选支持FIFO深度大一点的串口控制器像i.MX的UART和全志的UART基本都带64字节FIFO默认配置可能只开16字节触发中断这个后面还要在驱动层调不过那是另一个话题了。我自己的习惯是开发时先用USB转485接从站设备代码跑通以后再切到板载UART中间只要把设备节点名改一下其他代码完全复用。这篇后面所有的示例我都以Linux通用串口设备节点来写具体是ttyS0还是ttyUSB0自己对应改一下就行。1.3 软件架构划分在动手写代码之前先把整个软件分成三块这个问题就清楚多了串口抽象层负责打开设备、配置termios参数、读写字节流。这一层不应该知道Modbus是什么它只负责可靠地收发字节。Modbus协议层负责组帧、解帧、CRC校验、功能码处理、超时重试。这一层是一台Modbus主机的核心。业务应用层负责配置轮询列表、处理传感器数据的单位换算、浮点数拼接、脏数据过滤、日志输出等等。这个分层的好处是以后如果你要把RTU换成TCP只需要把串口抽象层换成socket收发协议层基本不用动。我自己做项目的时候一开始图省事把组帧和串口收发写在一个文件里后来要支持第二个传感器型号时改得一塌糊涂逼着我重构了一遍。所以不管项目多小分层这个习惯值得从一开始就养成。2. 动手前必须吃透的Modbus RTU协议细节2.1 帧结构地址、功能码、数据、CRCModbus RTU的一帧报文大概长这样字段长度说明地址码1字节从站地址0x01~0xF70x00是广播地址功能码1字节告诉从站要干什么数据区0~252字节寄存器地址、数量、具体数据CRC162字节低字节在前高字节在后举个例子读1号从站的保持寄存器从寄存器0x0000开始读2个寄存器主机发出的请求是01 03 00 00 00 02 C4 0B拆开看01是从站地址03是功能码读保持寄存器00 00是起始寄存器地址00 02是寄存器数量C4 0B是CRC16。从机正常响应则是01 03 04 00 63 00 64 7D 5701地址、03功能码、04字节数后面跟4个字节数据最后CRC。如果出错了从机会回一个功能码高位加1的异常帧比如03变成0x83并且在数据区带回一个异常码01表示非法功能码02表示非法寄存器地址03表示非法数据值04表示从站设备故障。我在调试的时候经常一开始没看异常码反复给对方厂家打电话结果发现是自己寄存器地址写错了这个教训实在太多。2.2 数据模型与三种常用功能码Modbus给设备定义了几种不同的数据区线圈Coil可读可写按位操作、离散输入Discrete Input只读按位操作、输入寄存器Input Register只读按16位操作、保持寄存器Holding Register可读可写按16位操作。实际做传感器采集用到最多的就三个功能码0x03读保持寄存器读温湿度传感器的校准参数或者某些设备的设定值。0x04读输入寄存器大量传感器设备出厂就把测量的实时值放在输入寄存器里。0x06写单个寄存器偶尔用来下发传感器修改地址或恢复出厂设置的命令。这里有一个特别容易踩的坑0x03和0x04读回来的数据在大多数设备里都是大端序的也就是高字节在前。但是也有个别国产传感器厂子二话不说按小端序出导致你读回来的温度值变成了几百倍几十倍。调试的时候如果读数明显离谱先别怀疑代码拿Modbus Poll之类的工具直接读一次原始寄存器值看看AB/CD到底哪个方向对然后再决定要不要做字节序交换。2.3 CRC16计算算法推导与C语言实现Modbus RTU用的CRC16是CRC-16/MODBUS多项式是0x8005初始值是0xFFFF输出的时候低字节在前。它跟常见的CRC-16/IBM不是一回事千万别混用否则组出来的帧怎么对都对不上。实际C语言里更常见的写法是右移查找表法但其实直接按位右移的算法也就几行代码性能在嵌入式Linux完全没问题static uint16_t modbus_crc16(const uint8_t *buf, size_t len) { uint16_t crc 0xFFFF; for (size_t i 0; i len; i) { crc ^ buf[i]; for (int bit 0; bit 8; bit) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }发送的时候需要注意把CRC的低字节先发出去再发高字节。比如上面那个读寄存器的例子CRC算出来是0x0BC4那么发送顺序就是C4 0B。这个坑我只能说几乎每个人都会踩一回我最初写主站时直接按内存顺序memcpy发出去了从站完全不回包对着协议看了老半天才反应过来字节序错了。2.4 3.5字符间隔为什么帧间隔在Linux上要自己careRTU模式有一个硬性要求两个帧之间至少要隔3.5个字符的静默时间否则接收方会把两帧当成一帧一帧内部相邻字节之间间隔不能超过1.5个字符时间否则接收方判定帧不完整直接丢掉。3.5个字符时间怎么算以9600波特率、8位数据位、1位停止位、无校验为例一个字符是11位1起始位8数据位1停止位1额外开销严谨点说是10位或11位一位时间是1/9600秒约104微秒一个字符约1.04毫秒3.5个字符大概就是3.65毫秒。在Linux应用层你没有硬件级的帧中断去精确卡这个时间。一般有两类做法接收方靠read函数加上termios的VTIME超时来定帧尾。这个后面第三章会细说。发送方在连续发送下一帧之前确保串口已经完全把上一帧发完了调用tcdrain或者tcdrain之后加一个3.5字符时间的延时。实际项目里我通常是“两帧间隔最小5ms”这种拍脑袋值因为Linux应用层的调度延迟本身就不小与其卡3.65毫秒这种理论值不如干脆留足余量牺牲一丁点吞吐量换来稳定。当然如果你跑的是工业级高速轮询每秒要刷几百个点那就另外一回事需要上线程调度和更精细的时序控制。3. Linux串口配置实操从设备节点到termios3.1 找到并验证串口设备节点Linux下串口设备文件一般是/dev/ttyS0板载串口、/dev/ttyUSB0USB转串口、还有i.MX平台的/dev/ttymxc0、全志平台的/dev/ttyS1等。拿到板子第一件事是把设备节点搞清楚ls -l /dev/tty* dmesg | grep tty插上USB转485线以后dmesg会打印类似“usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0”的日志。确认好节点名以后先用一个最简单的回环测试验证串口好坏用杜邦线把开发板的TXD和RXD短接然后执行echo hello /dev/ttyUSB0 cat /dev/ttyUSB0能读到hello说明这个串口驱动基本正常。如果这一步都过不了后面Modbus就更不用谈了。这里有一个Linux老手都会提一句的权限坑非root用户访问串口权限不足。很多时候程序open设备节点返回Permission denied直接在后面加sudo就跑通了然后也想不起来去修复权限。推荐长期方案是把用户加到dialout组里sudo usermod -aG dialout $USER然后重新登录一下用户一劳永逸。3.2 termios参数配置解析Linux对串口的行为控制完全是靠termios结构体完成的。写串口配置的时候最容易搞错的就是“规范模式”和“非规范模式”的区别。规范模式下内核会对输入做行编辑处理读到换行符才返回给应用这对Modbus这种二进制帧来说是灾难——因为你不知道帧里哪个字节恰好是换行符。所以第一件事就是用cfmakeraw把串口设置成原始模式让内核不加工任何字节、不解析任何特殊字符应用程序收到什么就是什么。一个我自己验证过的完整配置函数int uart_setup(int fd, int baudrate) { struct termios tty; if (tcgetattr(fd, tty) ! 0) { perror(tcgetattr); return -1; } cfmakeraw(tty); /* 使能接收和本地回环抑制 */ tty.c_cflag | CLOCAL | CREAD; /* 波特率 */ cfsetispeed(tty, baudrate); cfsetospeed(tty, baudrate); /* 8N18数据位、无校验、1停止位 */ tty.c_cflag ~CSIZE; tty.c_cflag | CS8; tty.c_cflag ~PARENB; tty.c_cflag ~CSTOPB; /* 非规范模式不启用流控 */ tty.c_iflag 0; tty.c_oflag 0; tty.c_lflag 0; tty.c_cflag ~CRTSCTS; /* 按字节返回超时100ms */ tty.c_cc[VMIN] 1; tty.c_cc[VTIME] 10; tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, tty) ! 0) { perror(tcsetattr); return -1; } return 0; }这里稍微解释一下免得你抄完代码还是一脑袋雾水CLOCAL忽略调制解调器控制线保证即使DCD线没接也能读数据。CREAD使能接收这个忘了开的话发出去倒是正常读回来永远是0字节。PARENB关闭校验位Modbus RTU常用的是8N1如果设备是8E1或8O1需要把校验位打开并设置PARODD。CSTOPB停止位8N1就清掉这个位2停止位就置上。CRTSCTS这是RTS/CTS硬件流控Modbus主站和从站之间通常不开硬件流控要关掉。VMIN和VTIME这两个配合起来定义read的阻塞行为VMIN1表示至少读到一个字节才返回VTIME10表示每字节之间的等待超时为1秒单位是0.1秒10就是1秒。我推荐这个组合既能确保不会永远死等又能把不定长的Modbus响应包快点收完。3.3 打开串口与常用读写模式打开串口要用open函数注意不要漏了O_NOCTTY和O_NDELAYint fd open(/dev/ttyUSB0, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open); return -1; }O_NOCTTY的意思是如果这个设备是终端不要把它设为控制终端O_NDELAY表示打开的时候不阻塞等待设备准备好。如果你是接RS485芯片没收到数据还有一个容易忽略的地方有些开发板的TTL串口和RS485收发器之间的方向切换是有延时的打开设备后建议先tcflush(fd, TCIOFLUSH)把缓冲区清一遍然后发送完每一帧后调用tcdrain(fd)等待数据全部从驱动缓冲区真正写到硬件上再进行方向切换。读数据的时候因为Modbus响应帧长度不确定常规做法是先按功能码预判响应长度然后循环read拼到缓冲区里uint8_t buf[256]; size_t pos 0; while (pos need_len) { int n read(fd, buf pos, need_len - pos); if (n 0) { perror(read); break; } else if (n 0) { /* 超时未收到数据 */ break; } pos n; }read返回0只有一种情况VMIN1但VTIME超时了都没等到一个字节。这时候上层就可以判定从站没响应。3.4 权限、开机自启与常见配置坑权限问题上面已经说过了这里再补两个我实际踩过坑的小细节第一个是“波特率写错但看起来没问题”。很多USB转串口芯片在驱动里默认猜测波特率你实际想配9600不小心传了19200结果设备也能收到乱七八糟的字节但就是不出正确数据。排查的时候先用stty -F /dev/ttyUSB0看当前配置能省不少时间。第二个是cfmakeraw会把输出处理OPOST清掉这本来是对的但如果有人手贱在后面重新把OPOST置位你会看到发出去的字节在某些串口上多了回车或换行转换帧就彻底废了。所以配置完以后重点关注c_iflag和c_oflag是不是都是0。4. Modbus RTU主站程序实现4.1 查询-响应模型和轮询状态机Modbus RTU通信是典型的单主机轮询模型。单片机时代写主站经常就是一个while死循环一次一次地阻塞收发但是到了嵌入式Linux上如果一个传感器响应特别慢而你还有其他传感器要轮询那就得设计一个非阻塞的轮询调度器否则整个CPU就被卡死了。我的做法是给每个从站地址维护一个结构体typedef struct { uint8_t addr; uint8_t func; uint16_t reg_addr; uint16_t reg_cnt; uint8_t *data; uint8_t state; /* 0:idle, 1:wait_response, 2:success, 3:error */ uint8_t retry; struct timespec sent_time; } modbus_slave_t;然后主循环每隔固定时间比如100ms遍历一遍轮询列表找到状态为idle的从站发下一帧请求并把状态置为wait_response。后续每次read到数据就根据地址去匹配对应的从站收到完整一帧后置为success等待下一次调度。这样即使某个从站掉线了也只会影响它自己的轮询间隔不会把整个采集系统拖垮。等待响应的超时用poll定时器或者select都行不要用sleep阻塞理由很简单——sleep没法在收到数据以后立刻返回会白白浪费几毫秒甚至几十毫秒。Linux上最省事的是poll(fd, 200)200ms超时没数据就返回0然后你决定重试还是放弃。4.2 报文拼装与CRC实现拼一个Modbus RTU请求帧就是个搬砖活我用一个通用函数处理int modbus_build_request(uint8_t addr, uint8_t func, uint16_t reg_addr, uint16_t reg_cnt, uint8_t *out) { int len 0; out[len] addr; out[len] func; out[len] (reg_addr 8) 0xFF; out[len] reg_addr 0xFF; out[len] (reg_cnt 8) 0xFF; out[len] reg_cnt 0xFF; uint16_t crc modbus_crc16(out, len); out[len] crc 0xFF; /* 低字节在前 */ out[len] (crc 8) 0xFF; return len; }响应帧的处理比请求稍微麻烦一点因为如果是异常响应数据区格式完全不同。所以解帧时我建议先校验CRC再判断地址和功能码最后判断功能码是不是异常功能码正常功能码高位bit7会是0异常时bit7置1。顺序千万别搞反否则你拿一个CRC错误的帧去解析地址很容易被脏数据误导。4.3 超时、重试与错误恢复工业现场没有不丢包的通信链路RS485总线虽然稳定性不错但也挡不住强电干扰、接线松脱、设备死机。超时重试是必须实现的但重试策略要有设计感不能无脑循环。我推荐的做法是“3次重试指数退避”第一次请求失败以后等100ms重试第二次失败等200ms第三次失败等400ms三次都失败就把这个从站标记为离线等下一轮轮询周期再试。这么做的好处是既能容忍偶发干扰又不会在从站彻底掉线的时候疯狂发报文把总线打爆。还有一个小细节每次重试之前一定要把串口收缓冲区清干净——tcflush(fd, TCIOFLUSH)。否则上一次响应虽然超时了但数据可能晚到一步被下一次请求的read函数当成新响应读出来你得到的就是一个错位的帧或者一个地址不匹配的帧。这个问题不解决就算你不重试通信也会越来越乱。4.4 试运行时的轮询节奏轮询间隔到底设多少合适我的经验是先读传感器的数据手册看厂商标称的响应时间。很多传感器RTU响应时间是几十到几百毫秒如果你把轮询间隔设成跟响应时间差不多的值那整条总线基本就在冲突和重试中度过了。稳妥的做法是发送请求后留出至少“最大响应时间 × 2 10ms”的等待窗口。比如某传感器最大响应时间200ms那么一次完整轮询周期至少给400ms加上串口收发和调度开销500ms一个周期比较靠谱。如果有多台从站轮询总周期就是每台从站的等待窗口之和。这时候可以考虑提升波特率来压缩响应时间从9600升到57600理论传输时间能缩短将近6倍但注意总线上所有设备的波特率都要一致而且要确认距离和线缆质量足够好。5. 传感器数据读写实战温湿度寄存器解析5.1 联调工具ModbusPoll和Modbus Slave的用法我在写Linux端主站代码前一定会先用PC上的Modbus调试工具把从站的协议看清楚把寄存器地址、数据长度、数值换算公式摸透再回来写代码。两大利器是ModbusPoll主机模拟和Modbus Slave从站模拟。ModbusPoll配置起来很简单选串口端口、波特率、数据位、停止位、校验位设置从站地址和功能码然后点连接它就会定时轮询并把寄存器值用表格列出来。它有“密钥”的说法网上很多但正规使用直接去官网下载试用版就行功能足够做调试。我用它做的第一件事就是读一遍传感器所有寄存器把温度和湿度原始值记下来和Modbus Slave里我预先填的测试值对比确认字节序没有问题。Modbus Slave则可以用来模拟一台虚拟从站。当Linux端代码还没连到真设备时先用Modbus Slave在PC上开一个从站这样协议层和串口层都能先用虚拟设备验证一遍再上现场不会手忙脚乱。联调时有个非常坑的地方PC端Modbus工具和Linux开发板同时开着、都连到总线上会造成地址冲突——因为总线上有两个同样地址的主站在抢发请求。调试时一定要保证总线上同时只有一台设备处于“主站”模式。5.2 实际读取流程从轮询到数据落盘我做的那个项目用的是一款支持Modbus RTU的温湿度传感器从站地址1温度在输入寄存器0x0001湿度在0x0002都是32位浮点数。第一步用ModbusPoll读到原始值假设温度寄存器4字节是44 32 8F 5C湿度是43 7A 00 00然后用IEEE浮点数解析工具转换得到25.65℃和62.0%RH。第二步确认这个值是真实传感器测量值之后就开始在Linux板上写轮询代码。代码逻辑跟前面第4章一致区别只在于响应数据的解析static void parse_sensor_frame(const uint8_t *buf, int len) { /* buf[0]addr buf[1]func buf[2]byte_count */ if (buf[2] 4) { float temp bytes_to_float(buf[3], buf[4], buf[5], buf[6]); printf(temp%.2f\n, temp); } }最后把读回来的值格式化以后写到一个环形缓冲区由另一个线程负责上传到MQTT或者用SQLite落盘。这一块不是本项目的核心但一个稳定的采集程序总得有地方放数据我建议至少先加一个带时间戳的日志函数这样排查问题的时候才有据可查。5.3 IEEE 754浮点数还原的坑Modbus寄存器和浮点数之间的转换是最容易翻车的地方。一个32位浮点数占用2个寄存器16位一个。Modbus协议本身规定寄存器传输时高字节在前但不同厂家的传感器在寄存器排列上却有两种习惯方案A高16位在前低16位在后就是标准大端序。方案B低16位在前高16位在后。同样4个字节内部的字节序也可能有大小端差别。我提供一个通用的解析函数内部可以灵活调整字节顺序static float bytes_to_float(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) { uint32_t u ((uint32_t)b0 24) | ((uint32_t)b1 16) | ((uint32_t)b2 8) | b3; float f; memcpy(f, u, sizeof(f)); return f; }实际拿到传感器数据以后先把原始寄存器值打出来然后在PC上手动试几种顺序看哪个最像真实物理量。千万不要凭感觉猜。我一个同行做过一个项目因为浮点数高低字节顺序没对读回来的温度常年相差2℃整整调了三天最后发现是传感器手册里寄存器字节序写的是“float little endian”跟常规做法恰好相反。5.4 数据清洗与上层应用对接工业传感器读回来的原始数据不能直接放心用要想清楚“哪些数据是可信的”。我做数据清洗时有几条固定规则超出合理物理范围的数据直接丢弃。比如温度-40~85℃超出这个范围不管是什么原因都不能上报。连续两次采集值跳变超过阈值时多读一次确认。比如1秒内从25℃跳到78℃不是传感器坏了就是干扰。数据带上时戳写入队列后由消费者统一处理。这样即使某次轮询失败也不影响历史数据的连续性和可追溯性。上层对接一般有两种方向一种是供本地GUI或HMI显示把解析好的数据结构体传给UI线程即可另一种是走网络上传把数据封装成JSON或MQTT报文发到云端。无论哪种Modbus开发本身都只是“取得可靠数据”这一步后面的数据价值和业务逻辑都建立在数据正确之上所以数据清洗这一节千万别省。6. 常见问题与排查技巧实录6.1 设备无响应怎么查这个问题是新手问得最多的我也碰到过无数次。我总结了一个排查顺序按这个顺序走九成问题都能解决现象可能原因排查方法open返回Permission denied用户无权限加入dialout组或用sudo发数据后无任何回包波特率不对stty看实际配置核对8N1发数据后无回包TX/RX接反两根线对调再试发数据后无回包地址不对用ModbusPoll扫1~247地址收到数据但CRC总是错干扰或波特率偏差示波器看波形适当降低波特率收到数据但解析不对从站是异常响应看功能码bit7是否为1还有一个最隐蔽的问题RS485总线是半双工的如果你在总线上串了一个全双工的USB转TTL线然后收发各用一根线数据往总线上一发A/B线自己就把信号短路了自然没有回包。我给个最简单的判断方法先用PC加一个USB转485工具直接接从站设备如果PC能正常读写而你Linux板不行那么问题基本出在Linux板的接线或配置上如果PC也不通那肯定是总线或者从站设备本身的问题。6.2 数据偶发错乱电气噪声与接地数据错乱比完全无响应更难排查因为它是“偶发”的。经验告诉我串口通信数据偶尔错一两个字节大多数情况是信号质量问题而不是程序逻辑问题。RS485是差分信号抗干扰能力本身不弱但有两个使用禁忌一是总线没有终端电阻二是总线没有共地。长距离传输时RS485总线两端需要各接一个120欧终端电阻否则信号在末端反射波形畸变数据自然错。另外现场两个设备的电源不共地A/B线之间就容易存在地电位差轻则数据错乱重则烧毁芯片。我刚入行时有一次去现场调试发现数据每隔几分钟就跳一下乱码最后工程师傅拿万用表量了下两块板子的GND电压差了2V多瞬间明白了。解决办法是RS485线用双绞屏蔽线屏蔽层单点接地。6.3 RS485方向切换的两种方案RS485是半双工通信需要一个方向控制信号来决定当前是发送还是接收。在嵌入式Linux上方向切换通常有两种做法硬件自动方向型很多新式RS485收发器芯片比如MAX13487内置自动方向控制直接接TTL串口的TXD就能工作不用额外控制这个是最省心的。软件RTS切换型老式芯片需要程序控制RTS引脚。Linux提供了TIOCSRS485这个ioctl来配置串口的RS485模式配置好以后驱动会在发送前自动拉高RTS发送完自动拉低。如果驱动不支持TIOCSRS485那就只能在应用层用GPIO库手动切换。发送前拉高方向发送后用tcdrain(fd)确保所有字节都已经从硬件发出去了再拉低方向。这个时序非常关键如果拉得太早后半个帧还没发完总线就被切回接收方向从站收的数据就是残帧拉得太晚从站开始回数据时方向还没切回来你又把自己的发送端和数据回线短接了。我遇到过一个很诡异的现场偶尔能通信偶尔不行排查到最后就是方向切换早了0.5毫秒。加一个usleep(1000)或利用tcdrain彻底等发送完成问题就解决了。6.4 调试工具推荐从命令行到逻辑分析仪遇到疑难杂症光靠printf太慢了这里分享几个我从常用的调试手段straceLinux下看程序的系统调用可以确认read返回了多少字节、耗时多少。strace -f -e traceread,write,ioctl ./modbus_app查“是否发出去了”、“内核有没有收到数据”特别管用。socat串口调试神器socat -v -x /dev/ttyUSB0,raw,echo0 pty,raw,echo0可以把你需要的串口重定向到一个虚拟串口还能打印所有经过的字节适合在PC上模拟一套虚拟串口对来测试协议代码。逻辑分析仪我强烈建议手头常备一台便宜的8通道逻辑分析仪专门用来抓UART波形。因为Linux应用层看到的数据是经过驱动处理的逻辑分析仪直接看RS485芯片TTL侧的波形才能确认硬件到底收发了什么。很多“程序没收到数据”的问题一抓波形就知道是根本没发出来还是压根没进来。ModbusPoll / Modbus Slave前面提过的主从模拟工具联调和协议确认阶段必备。还有一个小技巧写程序时把原始收发帧十六进制打印出来排错效率能翻倍。很多问题你看打印的报文一眼就明白是地址错了还是CRC错了比debugger还直观。排查完记得去掉或者做成日志开关不然后面跑业务的时候日志刷得飞快。做Modbus开发这几年我最深的体会是Modbus这个协议本身简单到几乎没什么可讲的真正的难点全在它外面那一圈——串口驱动、时序控制、总线电气特性、超时重试策略。你把这圈基本功练扎实了写个主站读传感器就是水到渠成的事。最后再分享一个我一直保留的习惯每次接新设备先用PC把协议摸清楚再动板子写代码遇到问题先抓原始帧再改代码这能帮你省掉一大半无意义的加班。
返回列表