不知不觉调试笔记已经写到第七篇了。前几篇我们聊过gdb、串口调试助手、Linux下调试串口的配置,今天终于轮到工业通信里的常青树——MODBUS协议。
在做嵌入式项目的这些年里,MODBUS几乎是无处不在:PLC与传感器通信、变频器控制、智能电表采集、充电桩与后台交互,甚至很多自研板卡之间的内部通信也在用它。原因无他:协议简单、帧格式公开、主从架构清晰,MCU资源再紧张也能轻松实现。哪怕你今天用的是STM32裸机,明天换到嵌入式Linux,后天又要跟昆仑通态触摸屏对接,MODBUS这套规则基本通用,学会一次到处能用。
这篇笔记我会把MODBUS里最核心的RTU模式讲透:消息帧格式、CRC校验的计算原理与C语言实现、存储区与功能码的对应关系,再结合我在真实项目中用串口调试助手和sscom抓包分析的过程,把常见问题和排查方法一并整理出来。无论是刚入门嵌入式想搞懂协议栈的兄弟,还是已经在调试现场被通信问题折磨过的人,这篇应该都能给你一些参考。
1. 内容整体设计与思路拆解
1.1 为什么嵌入式领域离不开MODBUS协议
先说个实际感受。早几年我做过一个环境监测项目,板子上有温湿度传感器和几个继电器,主控是STM32F103,上位机是PC上的组态软件。当初我在Modbus RTU和自研协议之间犹豫了很久,最后还是选了MODBUS。原因很现实:
第一,上位机和触摸屏普遍原生支持MODBUS,不用额外写上位机驱动。第二,调试手段非常成熟,串口调试助手、Modbus Poll这些现成工具一大堆,抓包就能看数据对不对。第三,现场维护人员对MODBUS的熟悉程度远超自研协议,出了问题沟通成本低很多。
从协议设计角度看,MODBUS是个典型的主从式应用层协议,一主多从,主站发起请求,从站响应。物理层可以跑在RS232、RS485、以太网(MODBUS TCP)甚至无线模块上。对嵌入式来说,最常用的就是RS485+MODBUS RTU组合。因为RS485是差分信号,抗干扰能力强,总线可以挂32个设备,传输距离上千m,非常适合工业现场。
协议本身不关心数据是由什么传感器采集的,也不限制寄存器里放的是什么含义的量,它只负责一件事:把数据按固定格式从A点搬到B点。这种“只管传输不管业务”的设计,恰恰是它长寿的秘诀。
1.2 方案选型:RTU还是ASCII
MODBUS协议一共有三种模式:RTU、ASCII和TCP。实际调试中90%以上遇到的都是RTU模式。
RTU模式用二进制方式传输数据,每个字节就是8位二进制数,帧紧凑、效率高、CRC校验强。ASCII模式把每个字节拆成两个ASCII字符发送,可读性好些,但数据量翻倍,校验用的是LRC,相对弱一些。TCP模式则是把MODBUS报文封装在TCP/IP包里跑以太网,一般用于上位机与网关之间的通信。
选型建议很简单:串口通信优先RTU,网口通信用TCP,除非客户明确指定ASCII,否则不要主动选ASCII。效率差两倍不说,调试工具的支持也没RTU好。
1.3 主从架构中的角色分配
MODBUS的通信模型必须有一个主站和一个或多个从站。主站通常是PLC、触摸屏、PC上位机或者我们做的网关设备;从站就是那些被采集/控制的设备,比如传感器、仪表、变频器、继电器板。
主站发命令,从站只能被动响应。主站可以读取从站的寄存器、写入单个或多个寄存器,从站收到请求后执行操作并返回响应。这里有个容易踩坑的地方:同一总线上不允许同时存在两个主站,否则两个设备同时发包会造成总线冲突,数据直接乱掉。
我见过有人在调试时把电脑上的Modbus Poll当作主站去轮询设备,同时又用串口调试助手往总线发报文,结果总线上的数据乱成一团。这种问题排查起来很容易让人怀疑人生,其实就是多了一个“主站”在捣乱。
2. 核心细节解析与实操要点
2.1 MODBUS RTU消息帧格式逐字节拆解
MODBUS RTU的帧结构非常紧凑,一条完整的请求帧由地址码、功能码、数据区和CRC校验四部分组成。
从站地址占1个字节,取值范围1~247,0是广播地址,248~255是保留地址。功能码占1个字节,决定了这条消息要干什么,比如03是读保持寄存器,06是写单个寄存器,16是写多个寄存器。数据区的长度可变,根据功能码不同,包含寄存器起始地址、寄存器数量、字节计数或者实际写入的数据。CRC校验占2个字节,低字节在前、高字节在后,用来保证整条帧在传输过程中没有被干扰。
举个读保持寄存器的例子,主站发送:
01 03 00 00 00 02 C4 0B- 01:从站地址
- 03:读保持寄存器功能码
- 00 00:寄存器起始地址(从0000H开始)
- 00 02:读取2个寄存器
- C4 0B:CRC16校验值
从站正常响应格式是:
01 03 04 00 01 00 02 7B 9A- 01:从站地址回显
- 03:功能码回显
- 04:数据区字节数(2个寄存器,每个2字节,共4字节)
- 00 01 00 02:两个寄存器的值,分别是1和2
- 7B 9A:CRC16校验
帧与帧之间的空闲时间也有讲究。RTU模式要求帧内部字节间隔不能超过1.5个字符时间,帧与帧之间间隔必须大于3.5个字符时间。如果串口收到数据时把这套时间约束破坏了,很多从站会直接把整条帧丢弃。这个时间参数在调试时经常被人忽略,后文排查部分我会细说。
2.2 功能码背后的存储区映射
MODBUS协议定义了几种数据对象:线圈(Coil)、离散输入(Discrete Input)、输入寄存器(Input Register)和保持寄存器(Holding Register)。在RTU报文里,这些对象通过功能码来区分操作类型。
| 功能码 | 操作对象 | 操作类型 | 典型应用 |
|---|---|---|---|
| 01 | 线圈 | 读 | 继电器状态、开关量输出 |
| 02 | 离散输入 | 读 | 按钮状态、开关量输入 |
| 03 | 保持寄存器 | 读 | 参数配置、运行数据 |
| 04 | 输入寄存器 | 读 | 传感器采集值、只读数据 |
| 05 | 线圈 | 写单个 | 控制单个继电器、开关 |
| 06 | 保持寄存器 | 写单个 | 修改单个参数 |
| 15 | 线圈 | 写多个 | 批量控制继电器 |
| 16 | 保持寄存器 | 写多个 | 批量修改参数 |
这里要区分清楚两种寄存器的差异。输入寄存器只读,一般用来存传感器实时采集值;保持寄存器可读可写,用来存配置参数和运行状态。工业现场最常见的组合是:用04功能码轮询实时数据,用03功能码读取参数,用06或者16功能码修改参数。
还有一点需要特别注意:MODBUS协议里寄存器地址从0开始,但很多设备厂商的资料里用的是PLC地址,比如40001、30001这类,它们之间的换算关系是:40001对应保持寄存器地址0000H,30001对应输入寄存器地址0000H。调试的时候如果发现明明寄存器地址写对了,设备却不响应,十有八九是PLC地址和协议地址没有换算清楚。
2.3 CRC16校验的算法原理与C语言实现
CRC校验是MODBUS RTU防数据错乱的关键机制。传输过程中任何一位被干扰翻转,接收端计算出的CRC都会和发送端附带的CRC不一致,从而判定帧无效。MODBUS RTU使用的CRC算法是CRC16-IBM,多项式为0x8005,初始值为0xFFFF。
它的原理不复杂:发送方对整条帧(从站地址到数据末尾)按位处理,将数据左移后与多项式进行异或运算,最终得到一个16位的校验值,附加在帧的末尾发送出去。接收方用同样的算法重算一遍,再与收到的CRC比对。
下面这段C语言代码我在STM32和嵌入式Linux环境都用过,纯软件实现,不依赖硬件CRC外设,占用的资源很少,适合MCU平台:
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 = crc >> 1; } } } return crc; }注意这里的多项式是0xA001,也就是0x8005按位反转后的值。MODBUS RTU的数据传输是LSB First,所以代码里用的是右移版本。这个细节我在新手阶段困惑过很久,用左移版的CRC16算出结果总对不上,后来查了标准才明白是针对字节序做过反转处理。
帧里CRC的低字节放在前面,高字节放在后面。比如计算出的CRC是0x0BC4,那么在帧里应该写成C4 0B,跟前面例子里的报文字节序一致。这个字节序问题也是调试时常见的坑,后面专门说。
2.4 串口参数配置与RS485方向切换
MODBUS RTU跑在串口上,串口的参数配置直接决定通信能否建立。最常用的一组参数是波特率9600、数据位8、无校验、停止位1,也就是8N1。也有不少设备用19200或115200,具体以设备手册为准。这里有一个隐性要求:总线上所有设备的串口参数必须完全一致,否则根本收不到对方的有效数据。
用RTU模式时,官方规定数据位通常为8位,如果配置了校验位则数据位+校验位合计仍然是11位。在实际项目中,保持8N1是最省心的选择,遇到一些老设备强制要求偶校验时再做调整。
RS485是半双工通信,同一时刻只能收或者只能发,所以需要有一个方向控制引脚(通常叫DE/RE),发送数据前把引脚拉高,发完再拉低。很多人在调试时发现从站不回包,用示波器一看才知道主站的发送使能脚一直处于接收状态,数据根本没发出去。
另外还有一个实操细节:RS485总线的A/B端要接120Ω终端电阻,尤其总线长度超过几十米或者挂载设备较多时,不接终端电阻会导致信号反射,表现为通信时好时坏、偶发误码。初期调试建议先把终端电阻接上,减少一个变量。
3. 实操过程与核心环节实现
3.1 调试前的准备工作和工具清单
在我自己的调试流程里,工具准备是第一步,也是最容易被低估的一步。硬件方面需要一台USB转RS485的转换器,注意是RS485不是RS232,两者电平不同不能混用。软件方面,我常用的是Windows下的sscom串口调试助手和Modbus Poll,前者用来直接观察收发的原始字节,后者用来模拟主站轮询设备。
串口调试助手的配置很简单:选择对应的COM口号,波特率设为9600(按设备手册来),数据位8、停止位1、无校验。连接之前先确认USB转485模块被系统识别到了,设备管理器里看有没有出现对应的COM口。如果插入后完全没有反应,大概率是驱动没装或者线材问题。
一个建议:调试初期不要直接用Modbus Poll,先用串口调试助手手动发送一条读寄存器的报文。原因很简单,Modbus Poll封装得太好,你看到的是一行“地址、功能码、起始地址、数据长度”的表单,而看不到真正发到总线上的原始报文。如果报文本身拼错了,Poll反而会掩盖问题。
3.2 从站设备响应验证的完整流程
拿到一台新的从站设备,我一般按照下面的顺序去验证它是不是正常通信。
第一步,确认接线。A接A、B接B,不要把A/B接反,否则通信完全不通。有个小技巧:如果手头设备没有标注A/B,接反了之后主站发请求时从站会没有响应,交换一下A/B就好了。
第二步,用串口调试助手发送读请求帧。比如从站地址是1,要读取起始地址0000H处的2个保持寄存器,对应的报文是:
01 03 00 00 00 02 C4 0B发送后观察接收区是否有数据返回。正常的响应帧格式为地址、功能码、字节数、数据、CRC,比如:
01 03 04 00 01 00 02 7B 9A如果收到这样的响应,说明物理层和数据链路层已经打通,通信链路是正常的。
第三步,通过修改报文中的起始地址和数量,逐段确认设备的寄存器映射范围。比如读地址0002H处2个寄存器,就应该把报文改成:
01 03 00 02 00 02 65 CBCRC会随数据变化,这个需要用计算工具重新生成,不要手动改。
如果在第二步就收不到响应,优先检查三样东西:串口参数是否配置正确、RS485方向控制是否正常、地址码是否匹配设备实际地址。按照这个顺序排查,绝大多数问题都能定位到。
3.3 用sscom捕捉并分析异常数据帧的方法
sscom是我用得最多的纯串口工具,因为它既能当作普通的收发助手,又能按Hex方式显示数据,非常适合观察MODBUS这类二进制协议。
打开sscom后,在“串口设置”里选择COM口和波特率等参数,勾选“HEX显示”和“HEX发送”。把上面的读请求帧以十六进制形式填入发送区,发送后看接收区。如果设备正常,你会看到对应响应帧。如果不正常,接收区可能是空的,也可能是一堆读不懂的字符——后者通常是波特率不匹配或者设备在发乱码。
sscom有几个好用的选项,我日常调试一定会开:时间戳显示,可以精确看到收发之间的时间间隔;自动保存接收数据,长时间跑的时候直接把日志存下来,万一后面要复现问题,这些日志就是直接证据。
我在一次调试中遇到过一个问题:设备时而响应、时而不响应,抓不到规律。后来用sscom的时间戳一看,发现响应延迟越来越长,最后就不回了。查了一圈,是主站几秒内疯狂重发请求,把从站的处理队列堵死了。用时间戳分析出重发频率之后,调整了主站轮询间隔,问题迎刃而解。
3.4 从零写一个最小MODBUS RTU从站程序
有时候手头没有现成的从站设备,但你又想验证主站程序对不对,这时候自己用STM32写个最小从站是最快的办法。当时我用标准库在STM32F103上写过一版,核心逻辑就是串口收中断+定时器帧超时判断+应答帧拼装。
串口接收中断里把每个字节存进缓冲区,同时启动一个定时器。每次收到新字节就重置定时器,定时时间设为3.5个字符时间。如果定时器溢出,说明一帧数据收完了,进入帧解析。
3.5个字符时间在9600波特率下大概是4ms,计算方法是:9600波特率每字节约1ms(10位),3.5字节就是3.5ms,向上取整到4ms。波特率越高,这个时间越短,115200下大概是0.35ms。这也是很多人把波特率调高之后通信变得不稳定的原因之一:帧间隔时间太短,主从站稍有延迟就把一帧数据切成了两段。
收到完整帧后,先判断从站地址是否匹配,再算CRC,校验通过后根据功能码执行对应操作。以读保持寄存器为例,解析出起始地址和寄存器数量,把对应数据按字节拼接成响应帧发回去。
uint8_t rx_buffer[256]; uint8_t rx_len = 0; uint8_t frame_ready = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); frame_ready = 1; TIM_Cmd(TIM2, DISABLE); } } void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t byte = USART_ReceiveData(USART1); rx_buffer[rx_len++] = byte; TIM_Cmd(TIM2, ENABLE); TIM_SetCounter(TIM2, 0); } }这是一个非常骨架化的实现,实际项目里还需要处理缓冲区溢出、异常帧应对、广播地址等边界情况。但它足够用来调试:把主站报文发过来,从站正确回应,就证明你的主站协议解析逻辑是对的。
3.5 Modbus Poll配合调试的技巧
Modbus Poll是一个专门模拟MODBUS主站的PC软件,日常调试效率比串口助手高很多。它最大的优势是自动轮询:设定好从站地址、功能码、起始地址和长度后,它会周期性发送请求,并把你关心的寄存器值实时显示在表格里。
我用Modbus Poll的典型场景是查寄存器地址映射。一个设备几十个寄存器,光靠串口助手一个个手动发报文,效率低还容易出错。用Poll把起始地址设成0,长度设成需要的寄存器数量,然后观察表格里的数据。如果读回来的值跟设备液晶屏上显示的数值对得上,说明该寄存器地址是正确的。
Poll还有一个好用的功能:可以手动写入单个或多个寄存器。比如要给设备下发一个启动命令,用06功能码往指定寄存器写入1。如果设备没有反应,马上切回sscom,自己拼一条写单个寄存器的报文发出去。要是sscom手动发能成功而Poll发不成功,那就基本能确认问题出在Poll的地址解析或格式配置上。
4. 常见问题与排查技巧实录
4.1 设备没响应:从物理层到协议层的排查顺序
设备完全不响应是调试中出现频率最高的问题,处理思路按分层来走,不要一上来就怀疑协议写错了。
先看硬件。用万用表量一下A/B之间的电压,静态时应该在0.2V〜6V之间,一般是1.5V〜5V。如果A/B间电压是0V,大概率是接线问题或者收发器芯片没工作。这时重点检查供电、共地、A/B有没有接反。
再看串口参数。确认主站发送的波特率、数据位、停止位、校验位与从站要求一致。可以尝试用sscom发送几个任意字节,观察从站是否有任何回包动作。很多现场设备上电后会主动发送一条状态帧,如果你打开串口界面能看到设备主动发来的数据,说明物理层和串口参数都没问题。
然后检查帧本身。地址对不对、CRC算错没有、寄存器地址有没有超出设备支持的地址范围。这里有一个容易忽略的点:有些设备从站的地址默认是1,但你手上这台设备可能被人改过地址。可以尝试扫描地址范围1~247逐个发请求,看哪个地址有响应。
最后检查时序。用示波器看总线波形,确认主站发送的帧之间是否有足够的间隔时间。有的主站程序在串口发送之后立即切换到接收模式,而RS485方向切换有延时,导致最后几个字节被截断。
4.2 响应帧CRC错误:大概率是字节序和计算方式不一致
CRC校验错误是调试里最常见的第二个问题。每次在串口助手看到"帧校验失败"或者自己手动算CRC对不上,先别急着怀疑算法写错,绝大多数是下面几个原因。
一是CRC字节序写反了。帧里的CRC是低字节在前、高字节在后,很多人在组帧时按高字节在前发送,结果接收方算出来的CRC一致,但比对时发现完全对不上。解决的办法是写一个用在线CRC计算工具验证的例子,把自己的发帧逻辑跑一遍,确认计算值和工具一致,再核对帧里字节的排列顺序。
二是CRC计算的覆盖范围不对。CRC只覆盖地址码、功能码和数据区,不包括帧末尾附加的CRC字节本身。如果计算时把CRC自己也算进去了,肯定算不对。
三是计算方式不对。前面提到MODBUS RTU用的是LSB First的右移算法,多项式是0xA001。如果你从网上复制了一个CRC16-MODBUS算法,先验证一下:对01 03 00 00 00 02这5个字节计算,结果应该是0xC40B,低字节是0B,高字节是C4。这个验证样例是MODBUS官方文档里提供的,可以用来快速判断算法实现对不对。
4.3 寄存器数据全是0xFF或0x00:地址映射和读写权限问题
如果请求能收到响应,但读回来的数据要么全是0xFF要么全是0x00,这通常不是通信问题,而是寄存器地址映射或者设备状态本身的问题。
先确认你读的是哪个存储区。同一台设备,保持寄存器和输入寄存器的数据是完全不同的两组。比如设备手册里说运行温度在寄存器40001,那对应的是保持寄存器地址0000H,应该用功能码03。如果手册说测量值在寄存器30001,那是输入寄存器地址0000H,应该用功能码04。功能码用错了,设备照样响应,但返回的数据是你没料到的另一片存储区的内容。
还有一种情况是读操作成功但写入操作没效果。优先确认功能码用的是05还是06、15还是16。线圈操作和寄存器操作是两组不同的地址空间,某型号的继电器输出模块,线圈地址可能从0000H开始,但保持寄存器地址也在0000H,两个功能码操作的是完全独立的存储区,不要混用。
读回来的数据字节序也要检查。MODBUS默认是大端模式,即每个寄存器的高字节在前、低字节在后。如果你把两个字节按小端解析,本来应该读到的0x0102会被解析成0x0201。很多设备支持配置字节序,但默认都是大端。
4.4 通信时好时坏:干扰与总线时序的双重排查
时好时坏的问题最折磨人,因为它的触发条件不总是稳定复现。这类问题我从三个方向排查。
第一个方向是干扰。RS485虽然抗干扰能力不错,但是在电机启停、变频器工作的时候,总线上的浪涌还是会影响通信质量。排查方法是在出问题时用示波器抓A/B波形,看信号上有没有叠加毛刺。解决手段包括:用带屏蔽的双绞线、屏蔽层单端接地、A/B端加TVS管、总线两端正确接入120Ω终端电阻。
第二个方向是帧超时参数。前面提到RTU要求帧间间隔大于3.5个字符时间、帧内字节间隔小于1.5个字符时间。如果主站发送的两个字节之间间隔太长,从站会认为前一帧结束了,把一帧数据拆成两段处理,接收方看到的自然是错帧。这种问题在波特率较高时尤其明显。
第三个方向是总线上的其他设备干扰。用Modbus Poll轮询一台上位机,不过它在同一总线上往其它寄存器地址写值,或者存在两个主站同时发报文。用sscom挂着观察一段时间总线上的原始报文,如果发现某些报文不是你发的,那就要考虑总线上是不是还有别的设备在自发发送数据。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无响应 | 接线错误、A/B接反 | 检查接线,量A/B间电压 |
| 完全无响应 | 串口参数不匹配 | 检查波特率/数据位/停止位/校验位 |
| 完全无响应 | 从站地址不匹配 | 扫描1~247地址范围 |
| 收到但CRC错 | CRC字节序反了 | 核对低字节在前还是高字节在前 |
| 收到但CRC错 | CRC算法实现有误 | 用官方样例验证CRC计算函数 |
| 收到但数据不对 | 功能码选错存储区 | 确认是03/04哪个功能码 |
| 收到但数据不对 | 寄存器地址换算错误 | 区分PLC地址与协议地址 |
| 收到但数据不对 | 字节序解析错误 | 按大端解析寄存器值 |
| 时好时坏 | 总线干扰 | 示波器抓波形,加屏蔽和TVS |
| 时好时坏 | 帧间隔时间不足 | 检查主站收发切换延时 |
| 时好时坏 | 多个主站在线 | 用sscom观察总线上原始报文 |
4.6 几个值得分享的排查技巧
最后分享几个这几年积累下来的小技巧。
第一个技巧是善用“单一变量法”。调试通信问题时,每次只改变一个因素,改完做一轮测试,记录结果。我见过有人一次性把波特率、校验方式、终端电阻、从站地址全改了,然后问为什么还没好。这样做不仅无法定位问题,还容易把原本能通的状态也改坏。
第二个技巧是保留报文日志。把每一轮测试发送的报文和收到的响应都记录下来,最好带时间戳。很多间歇性问题的规律是从日志里看出来的,比如某个操作之后设备必然超时、每过多少秒会出现一帧异常数据等等。
第三个技巧是给自己写一个CRC校验小工具。在PC上写一个命令行工具,输入十六进制字节串,立即输出CRC16值。调试时手工拼报文,先用工具算出CRC,再组织完整的帧发送出去。这样至少能排除CRC计算错误这个变量。
5. 嵌入式Linux环境下的MODBUS调试经验
5.1 Linux下串口设备识别与权限配置
现在越来越多的嵌入式产品跑的是Linux系统,MODBUS调试的环境也跟着变了。在Linux下,串口设备一般是/dev/ttyS0、/dev/ttyUSB0这样节点。USB转485模块插上去之后通常显示为ttyUSB0,板载串口则是ttyS0/ttymxc0/ttyAMA0这类命名,具体取决于平台。
插上设备后先确认节点是否存在:
ls /dev/ttyUSB*如果没有看到设备节点,检查驱动是否加载:
dmesg | grep tty权限问题也经常遇到:普通用户打开串口设备提示Permission denied。临时解决办法是把当前用户加入dialout用户组:
sudo usermod -aG dialout $USER重新登录后即可直接访问串口设备。在嵌入式开发板里,更常见的方式是直接把串口节点的权限改成666,开发阶段图省事可以这么干,但产品阶段建议还是用规范的用户组管理。
5.2 用Python快速实现MODBUS调试脚本
Linux环境下我习惯用Python配合pyserial库写调试脚本。它的优势是灵活、可重复、能自动记录日志。比Windows下的串口助手更适合Linux场景下的批量验证。
import serial import time ser = serial.Serial( port='/dev/ttyUSB0', baudrate=9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1 ) def crc16(data): crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc = crc >> 1 return crc def build_frame(slave_id, func_code, data): frame = bytes([slave_id, func_code]) + data crc = crc16(frame) frame += bytes([crc & 0xFF, crc >> 8]) return frame frame = build_frame(0x01, 0x03, bytes([0x00, 0x00, 0x00, 0x02])) ser.write(frame) resp = ser.read(20) print(resp.hex())这段脚本实现了发送读保持寄存器请求并打印响应字节的功能。它的意义在于:当你需要反复修改报文内容、长期监控设备状态或者跑回归测试时,脚本比手动操作工具更可靠。
5.3 在Linux下用命令行工具抓取和分析MODBUS数据
Windows有sscom这类图形化工具,Linux下我更习惯用命令行。stty命令可以配置串口参数,cat和echo配合可以做最原始的收发测试,python脚本负责协议解析。
先配置串口参数:
stty -F /dev/ttyUSB0 9600 cs8 -cstopb -parenb然后从串口读取设备主动上报的数据:
cat /dev/ttyUSB0 | xxd发送自定义帧可以把前面python脚本拼出来的字节流用echo写到串口设备:
echo -ne '\x01\x03\x00\x00\x00\x02\xc4\x0b' > /dev/ttyUSB0这种方法虽然粗糙,但在系统还没完全跑起来的时候,往往是最快的验证手段。至少在写正式的MODBUS应用层代码之前,先用这种方式确认硬件链路是通的,能省去后面无数排查时间。
5.4 嵌入式Linux中MODBUS应用的架构建议
在嵌入式Linux中实现MODBUS从站或主站,我不建议直接在一段main函数里把所有逻辑写完。更稳妥的做法是分成几个层次:串口驱动层负责收发字节、帧协议层负责组帧/解析帧/CRC校验、业务逻辑层负责寄存器读写和命令处理。每一层只做一件事,出问题的时候能快速定位。
多设备轮询的时候注意调度策略。有些主站是单线程轮询,设备数量一多,单次轮询周期拉得很长,实时性就差了。可以考虑把读和写分开,紧急的写操作走独立通道,读操作按优先级分组轮流执行。寄存器映射表用结构体数组维护,含起始地址、长度、读写权限和对应的内部存储指针,这样新增一个寄存器不需要改动核心代码,只要在表格里加一行。
从站程序方面,在Linux下可以用一个线程接收串口数据,一个线程处理请求,通过消息队列解耦。收到完整帧后立即解析,解析通过后把处理结果放回队列,由发送线程统一回包。这比在接收线程里直接做处理要安全,因为业务逻辑可能涉及文件操作、数据库查询,耗时不可控,容易把串口接收卡死。
6. 写在最后的一点体会
做嵌入式这些年,MODBUS是我接触过的协议里最“老”也最“稳”的一个。它不像一些现代协议那样功能丰富,但正是这种简洁性让它成为了工业通信的事实标准。很多时候你不需要背下所有功能码,只要理解了帧结构、存储区、CRC校验和主从时序这几件事,就足以应付大多数调试场景。
如果这篇笔记能在你被MODBUS调试折磨到怀疑人生的时候帮上一点忙,我就很满足了。后续我打算把蓝牙BLE和网络调试相关的实战笔记也整理一下,到时候再和大家继续聊。