
折腾嵌入式这几年MODBUS协议基本是避不开的坎。无论是RS485总线上挂的电表、温控器还是和PLC、变频器、传感器打交道只要你想和设备“对话”十有八九绕不开MODBUS RTU帧格式。这篇《嵌入式调试笔记7》就来聊聊MODBUS协议从帧结构到寄存器映射再到我用串口调试助手把一帧帧数据逼出来的实战内容。适合正在被RS485通信折磨的嵌入式工程师、设备维护工程师以及刚接触MODBUS从站开发的单片机同学。我会按照一次真实调试的顺序来写先拆报文再讲存储区接着处理物理层和代码实现最后给出CRC与异常码的排查方法。我最早接触MODBUS是因为一个温控器项目。用户说“你帮我把设备的实时温度读上来”设备后面只有一个RS485接口厂家给了一份英文手册最后几页是寄存器列表0x006B是当前温度0x006C是目标温度。我当时连MODBUS是什么都说不清百度来的第一句话是“MODBUS是一种串行通信协议”剩下的全靠试。拿着USB转485和串口调试助手我照着网上的例子发01 03 00 6B 00 03 76 87设备毫无反应。换地址、换功能码、换波特率折腾了大半天最后才意识到我对这个协议的理解完全是“知其然不知其所以然”。这种经历太普遍了。MODBUS作为1979年Modicon公司推出的协议从PLC开始一路蔓延到工业现场几乎所有设备电表、温控器、变频器、HMI、传感器、光伏逆变器。为什么这么多年还没淘汰核心原因就一个字简单。主站发请求帧从站收帧、校验、执行、回复一问一答没有握手机制没有会话管理物理层从RS232到RS485再到以太网都能跑代码量小门槛低。所以对嵌入式工程师来说MODBUS算是最值得花一个下午彻底搞懂的协议之一。别被网上各种“详解”吓住它跟TCP/IP那套动不动几百页的协议栈比起来就是个迷你玩具。但恰恰因为它太简单“细节藏在报文里”很多人掉坑不是看不懂帧而是不会调试。这也是我这篇笔记想重点展开的。MODBUS适合数据量小、轮询式的采集场景。一次请求最大也就一两百个字节读取寄存器的数量还有上限。如果要做固件升级、高实时运动控制这类大数据或低延迟场景就别选它。它不是万能的但在低速采集领域它几乎是默认选项。1. 从一线现场说起为什么搞嵌入式避不开MODBUS协议1.1 一个让我折腾了大半天的真实现场那次温控器调试我手里的资料其实挺全手册写明默认站地址是1波特率96008数据位无校验1停止位寄存器表也给了。可我就是调不通。现在回头看原因特别蠢我发的帧里CRC算错了。但当时我对CRC完全没有概念以为串口发出去就行压根不知道MODBUS RTU每帧末尾都带两个字节的校验码。更蠢的是我当时用一个串口调试助手手动发HEX但没开HEX显示从站回了一串ASCII码我也没认出来。后来把界面切到HEX显示才发现从站其实一直在回异常帧只是我根本没看懂0x83代表什么。那一刻我才意识到调MODBUS这类协议“看懂十六进制”是基本功跟看串口打印字符还不一样你必须像读汇编一样把每个字节掰开看。这个场景在调试现场太典型了设备是新的参数是对的工具是有的但就是“不通”。问题往往不是设备坏了而是你对协议帧的认知有缺口。所以我决定把MODBUS RTU从帧格式到调试手段完整写一遍哪怕只是帮一个人少走我当年那个下午的弯路也算值了。1.2 三十年后为什么还在用简单得恰到好处MODBUS能活四十多年靠的不是性能而是性价比。对芯片资源紧张的MCU来说协议栈只占几百字节RAM一个定时器加一个串口就能跑起来。对工控现场来说RS485半双工总线成本低、抗干扰强几十个设备挂一条总线很常见MODBUS的“一问一答”天然适配这种拓扑。从应用场景看凡是需要“周期性采集状态、下发设定值”的设备MODBUS几乎都是标准配置。电能表、温控器、变频器、IO模块、光照传感器、光伏逆变器你随手拆开一个带RS485口的工业设备说明书里大概率有MODBUS寄存器表。做嵌入式Linux网关的、做STM32设备固件的、做上位机软件的都绕不开这个协议。用一句话概括我的体会MODBUS不一定是最优雅的协议但它是工业设备之间最大的公约数。你不需要喜欢它但需要能在串口助手里熟练地拼出正确的帧和任何陌生设备顺利对话。2. 先把RTU报文帧拆开嚼碎一位字节一位字节过2.1 请求帧的四段结构MODBUS RTU的一帧报文从结构上看就四段从站地址、功能码、数据段、CRC16。看起来简单但它是一切调试的基础。字段长度说明从站地址1字节1~247为正常地址0为广播地址所有从站接收但不应答功能码1字节区分读/写和对象类型比如0x03表示读保持寄存器数据段N字节按功能码不同携带起始地址、寄存器数量、写入数据等CRC162字节对前面所有字节计算先发低字节再发高字节请求帧最少8字节地址1字节功能码1字节数据段至少4字节起始地址2字节加数量或数据2字节CRC2字节。像0x03读保持寄存器、0x06写单个寄存器、0x05写单个线圈都是8字节。写多个寄存器的功能码0x10数据段会长一些。这里有个容易忽略的点MODBUS RTU属于“主从问答”模式。主站发一帧请求被寻址的从站必须回一帧响应。主站不发从站绝对不主动上报。所以你在调试时会发现一个现象设备挂在总线上但总线上什么都没发生只有主站轮询的时候才有波形。2.2 把“11 03 00 6B 00 03 76 87”翻译成人话官方手册里最爱用的示例帧是11 03 00 6B 00 03 76 87看起来像天书拆开就明白了11从站地址十六进制0x11等于十进制17。很多设备默认地址是1如果你手上的设备默认地址是1那这里应该填01。03功能码读保持寄存器。保持寄存器是16位可读可写的数据区工业设备最常用。00 6B起始寄存器地址0x006B等于十进制107。也就是说从107号寄存器开始读。00 03读多少个寄存器这里是3个所以会读107、108、109三个寄存器。76 87CRC16校验码。对前面11 03 00 6B 00 03这一串字节算CRC结果是0x8776发送时先低字节76再高字节87。如果从站正常响应回帧大概是这样的结构11 03 06 [6字节数据] [CRC低] [CRC高]。其中06是后面数据的字节数因为3个寄存器乘以2字节等于6。每个寄存器内部都是高字节在前所以回帧数据段是一个完整的“高|低高|低高|低”序列。把这一串hex掰开之后你会发现自己也能徒手读报文了。调试时拿串口助手看一眼回帧心里就能估算出设备有没有在工作。2.3 3.5个字符时间没有帧头帧尾靠“静默”切帧MODBUS RTU没有类似0x7E这样的帧定界符接收端靠时间间隔判断一帧是否结束。协议规定帧与帧之间的间隔必须大于等于3.5个字符时间帧内相邻两个字节的间隔不能超过1.5个字符时间。这个参数直接影响代码实现。以9600波特率、8N1格式为例一个字符包含1个起始位、8个数据位、1个停止位共10bit传输时间约1.04ms。3.5个字符就是约3.65ms1.5个字符约1.56ms。波特率1字符时间3.5字符时间9600约1.04ms约3.65ms19200约0.52ms约1.82ms115200约0.09ms约0.30ms实际工程里很多MCU代码并不严格掐1.5T而是用固定几毫秒的定时器判断“超过X毫秒没有新字节就认为帧结束”。这个方法在9600波特率下很好用但到了115200就危险3.5T只有0.3ms如果你仍然用10ms空闲判帧主机连续发两帧时可能被合并成一帧CRC自然过不了。反过来低波特率下用太短的判帧时间又会在帧中间误判结束。所以高波特率下最好把判帧时间跟着波特率一起算而不是写死一个固定值。3. 寄存器存储区模型线圈、位输入、输入寄存器、保持寄存器3.1 四类存储对象的区别MODBUS协议把设备内部数据分成四类存储对象很多人一开始记混其实表格一列就清楚了。存储区数据宽度读写属性常用功能码PLC地址前缀线圈Coil1位可读可写0x01读、0x05写单个、0x0F写多个0x离散输入Discrete Input1位只读0x02读1x输入寄存器Input Register16位只读0x04读3x保持寄存器Holding Register16位可读可写0x03读、0x06写单个、0x10写多个4x怎么记“线圈”这个词来自PLC输出继电器线圈既能读也能写“离散输入”对应按钮、行程开关这类干接点信号只能读“输入寄存器”一般放模拟量采集值只读“保持寄存器”相当于PLC内存里带断电保持的变量能读能写。实际工业仪表上最常见的还是保持寄存器温度、压力、电流这些测量值经常放在只读的保持寄存器里用03功能码就能读。但也有一些设备把只读测量值放在输入寄存器里用04读。所以拿到一台新设备必须以手册的寄存器表为准。3.2 地址编号的“减一”偏移MODBUS协议的数据地址从0x0000开始但PLC和组态软件界面上经常用40001来表示第一个保持寄存器。也就是说界面上的40001对应报文里的0x000040002对应0x0001差了个1。这个“减一”偏移特别容易坑人。你要是拿着组态软件给的寄存器列表直接把40001填进自己写的主站程序发送的起始地址就错了。反过来设备手册如果直接用协议地址比如“地址0x006B当前温度”那报文里就是0x006B不用减。另外老式5位地址最大只能到9999现在很多设备支持6位地址能超过10000但前缀规则不变0/1/3/4开头分别对应线圈、离散输入、输入寄存器、保持寄存器。除了地址偏移还要注意不同功能码读写同一地址时指向的存储区可能不一样。比如0x0000这个地址用03功能码访问的是保持寄存器用04功能码访问的是输入寄存器它们互不干扰。这也是为什么协议不直接说“读取第几个寄存器”而是必须把“功能码起始地址长度”三者一起说清楚。3.3 拿到新设备后怎么快速搞清寄存器表调试新设备时如果手册给了寄存器表一切好办按表填即可。如果手册没给全或者你压根没见过这个设备可以先探测。第一步确定从站地址、波特率、校验方式。大多数设备默认地址是1波特率9600无校验。第二步从地址0x0000开始试着读两个保持寄存器。如果回正常数据说明设备支持03功能码且这段地址存在。如果回异常帧比如0x83 0x02说明起始地址越界可以试试04功能码读输入寄存器或者01功能码读线圈。第三步用“二分扩展法”确定设备支持的最大地址范围先用数量2、4、8、32、64、125递增试探直到从站回异常03或02再缩小范围精确定位。这个方法在做通用采集网关、对接第三方设备时几乎必用。我曾经靠这个办法在没有手册的情况下摸清了一台老式电能表的所有寄存器分布包括电压、电流、功率和电量省去了向厂家要资料的等待时间。当然如果设备支持广播或厂家有专用调试工具直接用那个工具更快。3.4 一次最多能读多少对象MODBUS规范规定了单次读写上限03读保持寄存器一次最多125个04读输入寄存器一次最多125个01读线圈一次最多2000个02读离散输入一次最多2000个。原因很简单响应帧里要有一个字节表示后续数据长度数据段上限就是253字节寄存器按2字节一个算自然限制在125个以内。调试时如果你在工具里一次性填了300个寄存器从站可能回异常码03非法数据值也可能直接无视。批量采集时一定要分帧每帧控制在125个寄存器以内。4. 串口侧调通有多难物理层故障首先排除4.1 USB转485模块和接线很多MODBUS调不通根本不是协议问题而是物理层没通。我用的调试工具通常是USB转RS485模块、几根杜邦线、一个120欧终端电阻、示波器或逻辑分析仪。USB转485模块建议选带隔离的半双工模式下自动切收发省心很多。接线时最要注意A/B线的识别。RS485是差分信号A对应差分正B对应差分负。不同厂家的模块丝印不统一有的标“A/B”有的标“D/D-”还有的标“485/485-”接反以后设备不会回应。更要命的是有些模块号称支持自动极性识别但兼容性并不好不能完全依赖。终端电阻是个老话题。MODBUS RTU在RS485上跑总线两端各需要接一个120欧终端电阻。短距离点对点比如调试桌上几米长的线不接也能通几十米甚至上百米的现场总线不接就会出现反射数据偶发错误。如果从站端没接至少在主站端接一个。供电问题也容易被忽略。USB转485模块一般从USB取电5V输出能力有限。如果从站设备还需要从总线取电或者线拉得很长模块供电跌落就会出现“短距离能通拉长线就不通”的怪象。现场调试如果发现收发指示灯暗淡最好外接独立电源给总线侧供电。4.2 串口调试助手最容易被忽略的三件事串口调试助手是MODBUS调试的必备工具但用错的人很多。第一件事HEX发送和HEX显示必须同时打开。如果不打开HEX发送你输入的“01 03 00 6B 00 03”会被当成ASCII字符串逐个字符转成十六进制后发送帧早就面目全非。第二件事波特率、数据位、停止位、校验位要和设备手册一致。绝大多数设备是9600/8/N/1但总有例外老式进口设备可能用19200甚至偶校验2停止位。波特率不对时从站根本不会回帧表面看起来像断线。第三件事连续轮询时发送间隔要留足一般建议200ms以上。有些从站主循环里面有ADC采样、EEPROM处理响应速度并不快主站发得太急容易把从站逼进死循环。工具方面串口调试助手和SSCOM都能干粗活。需要模拟MODBUS主站和从站时可以用Modbus Poll和Modbus Slave这类专用工具寄存器表、CRC、异常码全部自动处理效率高很多。但我的建议是调试初期宁可用最朴素的串口助手手动发把每一帧都亲手拼一遍这样你对帧结构的理解才扎实不会一上来就被工具掩盖了底层细节。4.3 “主机发了但从站不理”的完整排查链路遇到“主机发了但从站毫无反应”不要慌按链路一层层查。第一层看串口助手的发送计数有没有增长。如果PC串口本身就没发出去先检查串口号有没有被占用或者把USB转485模块的A/B短接做回环测试确认PC侧收发正常。第二层看从站侧的RX指示灯。主机点发送时从站RX灯如果闪说明信号已经到从站如果不闪说明信号没到先用万用表量A/B之间电压。RS485空闲时A比B高2~6V如果测到0V可能是收发器没使能、线缆断了、或者A/B被错误短路。第三层如果RX灯闪但从站无回复用示波器或逻辑分析仪夹到RS485芯片的RO脚看MCU有没有收到完整UART字节。如果MCU收到了字节但程序没回应那就是地址、功能码、CRC或寄存器范围的问题如果MCU压根没收到查芯片RE脚是否被错误拉高、芯片供电是否正常。最后一层很隐蔽MCU收到了而且CRC也对但A/B线上就是量不到从站发送的差分电平。这种情况我遇到过最后查出来是485芯片的DE/RE控制脚问题。很多设计把DE和RE合并成一个GPIO发送时拉高发送完拉低。如果GPIO方向配错、发送完成中断没触发从站就会变成“只能收不能发”。排查时要分清是MCU的发串口TXD没有波形还是TXD有波形但485芯片没有输出差分信号这两者的处理方向完全不同。5. 串口数据不是“等”来的MODBUS从站状态机怎么写5.1 为什么用状态机而不是“收一个处理一个”串口是字节流没有帧边界。MODBUS RTU靠时间间隔和CRC来界定帧所以你写的接收代码必须有一个基本框架接收中断只管收字节周期定时器判断帧是否结束帧结束后统一解析。我见过很多人写串口接收是这样每收到一个字节就进入if判断尝试解析功能码。这在帧结构固定且简单时可能跑通但一旦出现半帧、粘帧、CRC错程序就乱套。正确做法是引入状态机把“接收”和“解析”解耦IDLE空闲状态等待第一个字节收到后进入RECEIVING。RECEIVING持续接收字节每次收到都重置间隔定时器。如果间隔超时认为一帧结束进入CRC_CHECK。CRC_CHECK对缓冲区前N-2字节计算CRC和帧尾两个字节比较。不一致就丢弃回到IDLE。HANDLERCRC正确后按功能码执行解析构造响应帧发送完回到IDLE。下面是一段简化但能说明核心逻辑的C代码骨架typedef struct { uint8_t buf[256]; uint16_t len; uint32_t last_rx_tick; uint8_t frame_ready; } modbus_ctx_t; void modbus_uart_rx_byte(uint8_t byte) { if (ctx.len sizeof(ctx.buf)) { ctx.buf[ctx.len] byte; } else { // 缓冲区溢出丢弃整帧 ctx.len 0; } ctx.last_rx_tick now_tick_get(); } void modbus_poll(void) { if (ctx.len 0) { return; } if (now_tick_get() - ctx.last_rx_tick T35) { // 超过了3.5字符时间认为一帧已经结束 uint16_t crc_calc modbus_crc16(ctx.buf, ctx.len - 2); uint16_t crc_recv ctx.buf[ctx.len - 2] | (ctx.buf[ctx.len - 1] 8); if (crc_calc crc_recv) { ctx.frame_ready 1; } ctx.len 0; } }这段代码把“接收”和“解析”分开后主循环只需要周期性调用modbus_poll()检查frame_ready标志再去处理完整帧。这样做的好处是接收不会因为主循环被其他任务占用而丢字节解析也不会因为半帧数据而出错。5.2 主站轮询怎么设计主站的角色和从站完全不同。从站是“被动应答”主站必须“主动调度”。一个常见的主站逻辑是维护一张轮询表每条记录包括从站地址、功能码、起始地址、寄存器数量、超时时间。主循环依次发送请求发完一帧后进入等待状态收到响应就解析超时没收到就记录错误并继续下一条。超时时间的设置是门学问。太短从站处理不完会误判离线太长整个轮询周期变慢尤其挂了几十个从站时用户体验会很差。一般建议50~200ms左右具体看从站设备的响应速度。广播地址0x00是例外。主站发广播帧时所有从站都会执行操作但都不会回帧。比如用0x10功能码广播写多个寄存器主站发送后不能傻等响应应该用固定延时后继续下一条。实际工程中广播用得不多主要用于对时、批量启停这类场景。5.3 从站处理功能码的边界地址合法性与回帧长度从站侧最容易出错的地方是“合法地址判断”。比如设备支持保持寄存器0x0000到0x0009主站请求读0x0008地址、数量5因为85-112已经超过9从站必须回异常而不是返回一段越界数据。这个判断在寄存器读写功能码里通用起始地址数量-1必须在设备实际支持的范围内。另一个容易错的是回帧长度。读保持寄存器成功回帧时数据段第一个字节是“数据字节数”等于寄存器数量乘2。接下来每个寄存器必须按高字节在前填充。比如地址0x006B的值是0x012C报文里就要写01 2C。如果高低字节写反主站读到的数值就会面目全非。写多个寄存器功能码0x10的回帧和读不一样它是原样返回请求里的从站地址、功能码、起始地址、寄存器数量然后再补CRC。所以0x10的回帧固定是8字节地址1字节、功能码1字节、起始地址2字节、数量2字节、CRC2字节。掌握了这个规律调试时一眼就能判断回帧格式对不对。5.4 一个表现诡异的问题回帧太慢从站必须在主站超时之前发出响应这要求处理链路足够快。我遇到过一种情况从站MCU里跑着一个很长的ADC滤波函数每次要占用十几毫秒而主站超时设的是10ms结果从站“时灵时不灵”主站经常报超时。后来把协议栈的轮询提到任务最高优先级收到请求后先构造响应帧、置发送标志其他事情后面排队做问题才解决。如果从站要做写EEPROM这类耗时操作更要注意。老练的做法是先回成功响应帧再慢慢擦写EEPROM或者回一个“忙”状态让主站稍后再查。千万别在主站等的过程中做耗时操作否则你会被“偶发超时”折磨到怀疑人生。6. CRC16这关不过后面全是白费6.1 MODBUS CRC16的实现原理MODBUS RTU的CRC16用初值0xFFFF多项式迭代时用反转后的0xA001这是LSB-first的典型写法。很多文档直接写“CRC16多项式为0xA001”其实完整说法是“对0x8005这个多项式做位反转后得到0xA001配合初值0xFFFF使用”。工程上不用纠结太多照着实现就对了。逐位实现最直观适合理解原理uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }计算范围是从从站地址开始到数据段最后一个字节为止CRC本身不参与计算。对前面官方例子11 03 00 6B 00 03调用这个函数结果就是0x8776。6.2 低字节在前最容易倒的字节序CRC算出来是0x8776但发送时必须先发0x76再发0x87。这个“低字节在前”的规矩让不少人栽过跟头包括我自己。如果高低字节发反接收端算出来的CRC和帧尾对不上从站会静默丢弃整帧主站看到的现象就是“发出去没回应”。调试时把串口助手收到的HEX复制到Python或任意调试工具里独立算一遍CRC很快就能判断是发送方字节序错了还是接收方解析错了。如果独立计算匹配但程序依旧报CRC错误那要检查接收长度是不是把多余的字节也纳入了计算范围如果独立计算根本不匹配那就是物理层误码或者发送方发出来的帧本身就是错的。6.3 从站对CRC错误的静默处理理解CRC错误时从站的行为很重要从站收到CRC不对的帧默认不回复任何数据也不回异常码。主站看到的就是超时。所以调试时遇到“无声无息”别急着怀疑从站死机先用串口助手手动发送一帧CRC正确、地址匹配的请求看从站有没有反应。如果正确帧有反应错帧没反应说明协议栈的静默丢弃逻辑是正常的。现场通信偶尔受干扰时从站会时不时不回帧。抓包看数据通常能看到帧内容错位或者CRC错误。这时候问题往往出在布线、屏蔽、接地、终端电阻或者波特率偏差上不要一头扎进协议代码里找原因。7. 异常码与边界场景从站答非所问时你该先看状态字7.1 一眼读懂异常响应从站收到请求后如果发现问题会回一帧异常响应。格式是功能码的最高位置1数据段放一个异常码。以读保持寄存器功能码0x03为例如果发生异常从站会回0x83而不是0x03。所以看到0x83、0x86、0x90这类“高位是1”的功能码就代表请求被拒了真正的原因看后面的异常码。异常码含义常见触发场景0x01非法功能从站不支持该功能码0x02非法数据地址起始地址数量超出寄存器范围0x03非法数据值请求数据长度超限或内容不合法0x04从站设备故障从站内部出错无法执行请求在串口助手里看到异常帧先别慌把异常码翻译成人话02说明地址不对03说明数量超限01说明这个功能码设备不支持。这三种情况占了MODBUS调试里绝大多数问题。7.2 没有手册时怎么探明寄存器空间面对一台不认识的MODBUS设备又没有寄存器表可以靠异常响应反推它的寄存器布局。先用03功能码读0x0000地址、数量1如果正常说明保持寄存器存在再把数量逐步增大直到从站回异常03。数量超限时从站回03说明起始地址是合法的只是长度太大如果从站回02说明起始地址本身已经越界。用这个办法再配合二进制搜索很快能画出一台设备的寄存器地址分布图。这个方法还有个衍生用法故意读一个很远的地址比如0xFFFF如果从站回02说明设备有明确的地址空间边界如果从站不回帧那说明它可能压根没实现标准异常处理或者物理层又有新问题。异常响应是MODBUS最实用的调试信号之一一定要学会看。7.3 32位数据的字节序魔咒MODBUS寄存器是16位的一个32位整数或浮点数要占两个寄存器。规范只规定了单个寄存器内高字节在前但没有强制规定两个寄存器的先后顺序以及32位数据的内部排列。现场常见的就有ABCD、CDAB、BADC等多种模式不同厂家的协议栈实现各不相同。如果你读出来的数值是个天文数字或者小数点位完全不对先别怀疑传感器坏了八成是字节序没配。最快的验证方法找一个你知道真实值的参数比如工作电压220V分别用几种字节序解析看哪个解析结果落在合理范围内那就是设备的正确字节序。我在调某逆变器功率时数值差了几万倍折腾半天才发现是寄存器顺序问题改个排列方式立刻恢复正常。7.4 从站完全不回的定位清单最后把“从站完全不回”的定位思路总结成表格方便现场排查时对照。观察点排查动作PC视角串口助手里发送的HEX是否完全正确手动算CRC是否正确地址是否匹配总线视角逻辑分析仪抓A/B差分确认主机发出的字节和从站发出的字节在物理层都存在从站视角MCU的UART是否进入接收中断DMA长度是否增长CRC校验是否通过地址是否匹配三种视角都查一遍基本能把问题定位到具体环节。我自己吃过最大的亏就是总在“从站程序”里找原因结果问题出在USB转485模块的驱动坏了换个模块立刻就好。最后分享一个我的习惯每次调MODBUS设备我都会先在PC上把一个从站的一个寄存器读通再谈其他。具体动作是先用串口助手手动把“地址功能码起始地址数量CRC”写完整发出去看到合理的回帧再切换到更高级的工具或者写代码。这个过程看着原始但对理解协议特别有帮助。另外调试时故意发一帧CRC错的、发一帧地址不存在的观察从站是静默还是回异常能在很短时间内判断从站协议栈“活没活”。我踩过的坑里有一半其实不在协议层而在物理层和字节序上把这些先弄干净MODBUS调试就是个体力活不是脑力活。