简介:面向单片机STM32开发者的MODBUS RTU工业通讯实例包,完整覆盖读寄存器、写单个寄存器、写多个寄存器等常用功能码,适合需要快速在单片机中集成MODBUS从站或主站通信的工程师,也适合学习工业串行协议栈的初学者。包内文件共787个,以C语言源码和Keil/IAR工程文件为主,同时包含HEX/AXF固件、PDF说明、HTML页面、PNG原理图及调试脚本,既可直接烧录验证,也可对照源码排错,帮助快速上手与深度排查。资源压缩包仅11.14MB,已有5991人浏览学习,属于轻量但信息量密集的工程型参考包,适合系统学习与二次开发。内容从RS485收发控制、UART参数配置、中断接收,到CRC校验、功能码解析与响应封装,形成了完整的通信逻辑链路;涵盖离散输入、线圈、保持寄存器、输入寄存器四类对象的读写操作,并处理异常应答;同时附带NAND Flash、SD卡、LCD等外设驱动示例,便于在此基础上扩展实用嵌入式产品。随包的目标文件、列表文件及MAP文件有助于定位编译细节,批量备份脚本也能直接纳入现有工程管理流程。
1. 需求背景:来自现场的MODBUS通讯需求与一份全功能码资源
做过单片机通讯的人大概都有这样的经历:设备调试得好好的,现场触摸屏或PLC一接上来,对方点名要“读保持寄存器”还顺手要“写多个寄存器”,你发现自己的协议栈只写了03和06两个功能码,剩下的全是未定义。于是现场一堆人等你改代码,那种感觉并不好受。这篇文章想讲的,就是一套以STM32为主控、把MODBUS RTU从站功能码做到位(01读出线圈、02读离散输入、03读保持寄存器、04读输入寄存器、05写单线圈、06写单寄存器、0F写多线圈、10写多寄存器)的完整方案。整套东西下来,通信就会很“规整”。如果你是正在做单片机/STM32方向课设、毕设的学生,或者要交付一套带MODBUS从站功能的设备,这份资源都适合拿来做底子。
2. MODBUS RTU协议基础与选型:帧结构、八个功能码与CRC16
2.1 帧结构与功能码映射:为什么功能码“全”是一种刚需
MODBUS RTU的帧结构极简,从站收到的每一帧都是同样的套路:地址域(1字节)+ 功能码(1字节)+ 数据域(N字节)+ CRC16校验(2字节)。地址域决定这一帧是给谁的,功能码决定这一帧要干什么,数据域是具体的寄存器地址、数量、写入值,CRC16用来确认整帧在传输中没有被干扰。
需要明确一点:MODBUS是一个主从协议,主站通常是PLC、触摸屏、组态软件或者PC调试工具,从站才是你的STM32设备。主站发出请求帧,从站处理后回响应帧。这里的八个功能码其实对应了MODBUS里最常见的两组资源,一组是“线圈”和“离散输入”这种位(bit)资源,另一组是“输入寄存器”和“保持寄存器”这种16位字资源。功能码的读写关系可以按下面这张表来记:
| 功能码 | 名称 | 资源类型 | 操作 | 单个请求最大数量 |
|---|---|---|---|---|
| 01 | 读线圈 | 位 | 只读 | 2000 bit |
| 02 | 读离散输入 | 位 | 只读 | 2000 bit |
| 03 | 读保持寄存器 | 16位字 | 只读 | 125 个寄存器 |
| 04 | 读输入寄存器 | 16位字 | 只读 | 125 个寄存器 |
| 05 | 写单线圈 | 位 | 写单个 | 1 bit |
| 06 | 写单寄存器 | 16位字 | 写单个 | 1 个寄存器 |
| 0F | 写多线圈 | 位 | 写多个 | 1968 bit |
| 10 | 写多寄存器 | 16位字 | 写多个 | 123 个寄存器 |
为什么八种都要做?因为从站没法预先知道主站会用哪个功能码访问。现场的经验是:部分主站读开关量只发01,读模拟量只发03,但另一些组态软件里的“设备配置向导”会默认用04去读输入寄存器,用0F去批量写输出点。功能码不缺,兼容性就上来了。做课设或毕设时,“功能码很全”也是一个好卖点,答辩时可以一条条列给评审老师看。
2.2 CRC16的实现细节:校验错了,一切白搭
MODBUS RTU的CRC16叫CRC-16/MODBUS,多项式是0xA001(反向多项式),初始值是0xFFFF。常见的处理方式是逐字节异或后按位循环右移,遇到最低位是1就异或多项式。核心代码如下:
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 bit = 0; bit < 8; bit++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; // 反向多项式 else crc >>= 1; } } return crc; }这段代码算出的CRC是16位无符号数,但MODBUS RTU在线路上的顺序是“低字节在前”。举例验证:发送数据01 03 00 00 00 01,算出的CRC为0x840A,发送时应先发0x0A再发0x84。调试时先用这种标准测试向量把CRC函数跑对,再去调试协议栈,会少走很多弯路。
这里有个容易忽略的细节:在从站里,收到帧后计算CRC的覆盖范围应该是从地址域到数据域末尾,不包含接收帧里的CRC两个字节。如果你把整个接收帧(含CRC)一起算,结果理论上应该是0,因为发送端本来就是在帧末尾附加的CRC。但主站判定是否合法,通常是把收到的CRC字节从帧里摘出来,和自己计算的结果比对,而不是把整帧拿来算。
2.3 通讯方式选型:用空闲中断而不是定时轮询
实现一个MODBUS从站,最关键的选型是“什么时候认为一帧收完了”。STM32上常见做法有两种:一种是开一个定时器,每个字节到达后重置定时器,超时未收到下一字节就认为帧结束;另一种是直接用串口空闲中断(UART IDLE),收到一帧后总线出现了空闲就触发中断。
我一般优先选用空闲中断,理由很直接:不用额外占用一个定时器资源,IDLE标志由硬件直接产生,帧间隔的判断更接近芯片原生的节奏。在代码里的体现是使能串口的接收中断+空闲中断:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 接收字节中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 空闲中断,代表一帧结束需要留意的是MODBUS规范要求帧间间隔不小于3.5个字符时间。空闲中断本质上是“总线持续一个字符时间内没有新字节”,它与3.5字符时间的含义不完全相同。实际工程里,如果波特率较低或者线路干扰比较大,可以在空闲中断判断的基础上,在主循环里再加一个最大帧长超时作为兜底,避免极端情况下数据卡死在缓冲里。
三段式结构是:串口驱动层负责收字节和帧完成标志;协议解析层负责地址过滤、CRC校验、功能码分发;应用层只负责把寄存器数组里的数据填好。分工明确之后,把协议栈从STM32移植到其他单片机上,工作量就只集中到串口驱动这个小文件里。
3. 总体架构设计:三层模块划分与寄存器表解耦
3.1 模块划分:驱动层、协议层、应用层各管什么
做MODBUS从站最忌讳的是把所有逻辑堆在一个中断里,几百行代码看着热闹,后面加一个功能码改一处变量,牵一发动全身。这个项目的模块划分比较清楚:
| 文件 | 职责 | 对外接口 |
|---|---|---|
| modbus_port.c | 串口收发、485方向控制、帧完成标志 | modbus_uart_rx_byte(),modbus_uart_send() |
| modbus_core.c | 地址过滤、CRC校验、功能码分发、异常码生成 | modbus_poll() |
| modbus_reg.c | 寄存器表定义、线圈表定义、写回调 | reg_coils[],reg_discrete[],reg_holding[],reg_input[] |
应用层不和协议帧直接打交道。比如设备上有一个温度值和一组开关量输出,温度值放在reg_holding[0],开关量放在reg_coils[0]。主站通过03功能码读温度,上层只需要往数组里填数;主站通过05功能码改开关量,协议层做完解析后修改数组,再通过写回调通知应用层真正去动作继电器。这样一来,你调试协议栈时可以用一组假数据测逻辑,不需要传感器真的在线,这就是“把寄存器数组当黑匣子”带来的好处。
3.2 帧接收的核心实现:串口空闲中断与接收缓冲
接收部分通常使用HAL库的串口中断回调,在中断里把字节存进缓冲区,同时判断空闲标志。典型实现如下:
#define MODBUS_RX_BUF_MAX 256 static uint8_t modbus_rx_buf[MODBUS_RX_BUF_MAX]; static volatile uint16_t modbus_rx_len = 0; static volatile uint8_t modbus_frame_ready = 0; void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // RXNE:每收到一个字节就存进接收缓冲区 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE) != RESET) { uint8_t byte = (uint8_t)(huart->Instance->RDR & 0xFF); if (modbus_rx_len < MODBUS_RX_BUF_MAX) { modbus_rx_buf[modbus_rx_len++] = byte; } } // IDLE:总线空闲,认为一帧收完 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) != RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart); if (modbus_rx_len > 0) { modbus_frame_ready = 1; } } } HAL_UART_IRQHandler(huart); }这段逻辑需要注意几点。RDR是数据寄存器,读取后硬件自动清除RXNE标志。如果读取的字节数超过缓冲区最大长度,多余数据会被丢弃,这是防止异常长帧把内存写穿;实际请求帧长度通常远小于256字节,所以这个判断主要是兜底。
帧完成标志modbus_frame_ready设了一个volatile修饰,是为了防止编译优化导致主循环看不到中断里的修改。另外一个关键点是:空闲中断触发的是“帧已收到”,真正解析动作要在主循环里做,不要在中断里做CRC计算和功能码分发,否则当主站以较高频率连续读取时,中断里耗时会抬高整个系统的中断延迟,影响其他实时性任务。
3.3 协议解析主流程:地址过滤、CRC验证与功能码分发
主循环里不断调用modbus_poll(),这个函数只做一件事:看有没有完整的帧,有就解析、回复,没有就直接返回。核心流程如下:
void modbus_poll(void) { if (!modbus_frame_ready) { return; // 没有收到完整帧 } modbus_frame_ready = 0; // 1. 地址过滤 uint8_t addr = modbus_rx_buf[0]; if ((addr != modbus_slave_addr) && (addr != 0)) { modbus_rx_len = 0; // 不是发给本设备的,丢弃 return; } // 2. 校验CRC:注意CRC域在帧的末尾两字节 uint16_t crc_received = modbus_rx_buf[modbus_rx_len - 2] | (modbus_rx_buf[modbus_rx_len - 1] << 8); uint16_t crc_calc = modbus_crc16(modbus_rx_buf, modbus_rx_len - 2); if (crc_received != crc_calc) { modbus_rx_len = 0; // 校验失败,静默丢弃 return; } // 3. 功能码分发 uint8_t fcode = modbus_rx_buf[1]; switch (fcode) { case 0x01: fc01_read_coils(); break; case 0x02: fc02_read_discrete_inputs(); break; case 0x03: fc03_read_holding_regs(); break; case 0x04: fc04_read_input_regs(); break; case 0x05: fc05_write_single_coil(); break; case 0x06: fc06_write_single_reg(); break; case 0x0F: fc0F_write_multiple_coils(); break; case 0x10: fc10_write_multiple_regs(); break; default: build_exception_response(fcode, 0x01); // 非法功能码 break; } modbus_rx_len = 0; }主站发请求帧给从站,从站回响应帧,两个方向都可能出现CRC错误,这里处理的是“收到坏帧时静默丢弃”。所谓静默丢弃,就是不回任何数据,由主站侧根据超时自行判断重试,这是一条编程经验:CRC不对的时候宁可不理它,也不要回一个可能已经错乱的响应,否则会造成链路层混乱。
地址过滤里有一个特殊的处理:地址0是广播地址。MODBUS协议规定广播地址只执行写操作,不回响应帧。主流程里先判断了“地址等于自己或者等于0”都能进入处理;在具体的写功能码实现中,还要根据地址是否为0决定回不回显。这样设计可以把广播和单播逻辑统一,又不会让广播帧把自己卡死。
4. 功能码模块实现:从01到10的读写流程与关键代码
4.1 位操作功能码:01读线圈、02读离散输入、05写单线圈、0F写多线圈
MODBUS里的“线圈”和“离散输入”都是按位寻址的,一个字节里有8个位。STM32里的位域操作并不复杂,关键是寻址时要把“线圈号”转成“字节序+位偏移”。这里用一个uint8_t数组存储线圈状态,每个元素对应一个实际线圈的开关状态。读线圈的响应帧结构是:地址+功能码+字节数+线圈数据+CRC,其中线圈数据按地址从低到高排列,不足8位的部分高位补0。写单线圈时,主站发送的数据域固定是5、6两个字节,0xFF00代表置ON,0x0000代表置OFF,其他值判定为非法数据值。
void fc05_write_single_coil(void) { uint16_t coil_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3]; uint16_t coil_val = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5]; if (coil_addr >= MODBUS_COIL_NUM) { build_exception_response(0x05, 0x02); // 非法数据地址 return; } if (coil_val != 0xFF00 && coil_val != 0x0000) { build_exception_response(0x05, 0x03); // 非法数据值 return; } uint16_t byte_index = coil_addr >> 3; uint8_t bit_offset = coil_addr & 0x07; if (coil_val == 0xFF00) modbus_coils[byte_index] |= (1 << bit_offset); else modbus_coils[byte_index] &= ~(1 << bit_offset); // 05功能码要求原样回显请求帧 uint8_t resp[8]; resp[0] = modbus_rx_buf[0]; // 从站地址 resp[1] = 0x05; resp[2] = modbus_rx_buf[2]; resp[3] = modbus_rx_buf[3]; resp[4] = modbus_rx_buf[4]; resp[5] = modbus_rx_buf[5]; uint16_t crc = modbus_crc16(resp, 6); resp[6] = crc & 0xFF; resp[7] = crc >> 8; modbus_uart_send(resp, 8); }对于写多线圈0F功能码,请求数据域是:起始地址2字节+数量2字节+字节数1字节+线圈数据N字节。这里的数量是线圈个数,而字节数是线圈个数除以8向上取整。最容易翻车的地方是中继数量的边界检查:一次写入的线圈数量超过MODBUS规范上限,或着数据字节数与声明的字节数不符,都应该回异常码而不是继续执行。另外,0F功能码在写完毕后正常响应应该回显“起始地址+数量”,不是回显数据内容,这和05的回显策略明显不同。
4.2 寄存器操作功能码:03读保持寄存器、04读输入寄存器、06写单寄存器、10写多寄存器
寄存器操作相比位操作要直观得多,因为STM32里天然有16位整数类型,寄存器表直接用一个uint16_t数组来定义。03读保持寄存器的响应帧是:地址+功能码+字节数(2×数量)+寄存器数据+CRC。实现时先核对起始地址和数量,再做边界检查,最后从数组取值拼帧。这里有一个协议层面的细节必须留意:MODBUS规定03单次最多读125个寄存器,超过这个数量属于非法数据值,应回异常码,而不是照常处理。
void fc03_read_holding_regs(void) { uint16_t start_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3]; uint16_t reg_count = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5]; if (reg_count == 0 || reg_count > 125) { build_exception_response(0x03, 0x03); // 非法数据值 return; } // 防越界:起始地址+数量 不能超过寄存器表长度 if (start_addr + reg_count > MODBUS_HOLDING_NUM) { build_exception_response(0x03, 0x02); // 非法数据地址 return; } uint16_t resp_len = 3 + reg_count * 2; uint8_t resp[MODBUS_RX_BUF_MAX]; resp[0] = modbus_rx_buf[0]; resp[1] = 0x03; resp[2] = reg_count * 2; // 数据字节数 for (uint16_t i = 0; i < reg_count; i++) { resp[3 + i * 2] = (modbus_holding_regs[start_addr + i] >> 8) & 0xFF; resp[4 + i * 2] = modbus_holding_regs[start_addr + i] & 0xFF; } uint16_t crc = modbus_crc16(resp, resp_len); resp[resp_len] = crc & 0xFF; resp[resp_len + 1] = crc >> 8; modbus_uart_send(resp, resp_len + 2); }06写单寄存器的实现和05写单线圈的套路相似,但有一点容易漏:06功能码的响应是原样回显请求帧,而对主站来说,它并不关心你的从站内部到底有没有真正把值“永久保存”。实际设备里的参数,比如量程上下限、零点校准值,写进RAM数组里,设备掉电就丢失了,主站往往认为写成功了。如果要让设备掉电保存,就得在写回调里额外触发一次Flash写入操作,这是应用层的事,协议层不做。
10写多寄存器的实现,我建议先整体校验,再写入。校验包括起始地址、数量、字节数是否匹配、目标范围是否越界;全部合法后,再把请求里的值逐个复制进寄存器表。这样就不会出现“前两个寄存器写进去了,第三个地址越界回异常,主站重试又覆盖一遍”的半程写入问题。从站回显响应时回“地址+数量”即可,不需要回整个数据内容。
4.3 异常码与广播地址处理:设备“规不规范”的差别就在这里
异常响应帧的格式是固定的:源地址+(功能码|0x80)+异常码+CRC。协议里常用的异常码有三种:0x01非法功能码、0x02非法数据地址、0x03非法数据值。实现时,build_exception_response()函数统一处理组帧,各功能码在发现错误时只需调用这个函数指定功能码和异常码即可。
广播地址(0)的处理是个细节活。按MODBUS规范,广播帧不回应任何响应,它只适用于写命令。也就是说01、03、04这些读功能码收到地址为0的帧不应该处理;而05、06、0F、10这些写功能码可以执行写入,但不回显。很多简化实现会把广播帧直接丢弃,这在大部分工况下没问题,但如果主站配置了广播写同步多个从站,就会失效。完整做法是协议层把“地址过滤后是否允许执行”分别判断出来,在写功能码里检查地址域是否为0,决定是否调用发送函数。
另外,异常码的生成也不该是一个黑匣子。拿了这套工程以后,你可以把每个功能码的异常分支当成测试点,在调试助手里故意发送越界地址和错误数量,检查从站是不是正确回了对应的异常码。这个动作在验收阶段是加分项,它能在答辩现场直接证明你理解协议。
5. 通讯调试与常见问题排查:CRC校验、485时序、半帧处理的五个翻车点
5.1 现象一:调试助手收到的数据能显示,但CRC校验总失败
刚把工程烧进板子,打开串口助手给从站发一个03读寄存器的请求,响应帧能看到,可是用工具自带的CRC校验一算,总是提示错误。有些手册直接告诉你“把工具里的CRC校验勾选去掉再用”,但这不是解决问题的态度。
原因通常是两个:一是发送方(主站)请求帧本身CRC就是错的,你测试时用模拟器手动输入数据,CRC字节填了00 00,那从站当然会静默丢弃;二是从站响应帧里CRC的高低字节顺序反了。MODBUS线上顺序是低字节在前,如果你按大端方式把CRC拼进帧,工具就会报错。
解决:先用标准测试向量校准CRC函数,比如01 03 00 00 00 01对应的CRC是0x840A,线上应该发送0A 84。确认CRC函数正确后,再用串口助手抓包,核对请求帧和响应帧各自的CRC字节位置。实在不行,可以用支持“CRC自动计算”的调试工具来组帧,减少手输错误。
5.2 现象二:功能码和数据都对,但主站收到的数据是“错位”的
有时从站明明回了完整响应帧,数据值看着也对,主站那边解析出来却是乱码,或者第一个寄存器读出来是第二个的值。这是典型的字节序问题。
原因:MODBUS协议规定寄存器数据是大端(高字节在前)传输,而STM32内存里是低字节在低地址的小端存储。如果你直接把uint16_t变量按内存字节序塞进发送缓冲区,就送成了低字节在前,主站按大端解析自然错位。
解决:在拼帧时显式拆分高低字节。我的习惯是写一个宏:数据进帧时用(reg >> 8)拿高字节,(reg & 0xFF)拿低字节。无论是03、04功能码输出数据,还是10功能码解析收到的寄存器数据,都走这套拆装逻辑,不要依赖编译器对齐。
5.3 现象三:单独用USB转485调试正常,接上PLC回复就时好时坏
这个现象在带485总线隔离的产品里很常见。你在工位上用一颗USB转485模块测,一切正常;换到现场接上PLC的总线,就出现一帧成功一帧失败的诡异现象。
原因基本绕不开两点:一是485方向切换时序不对,主站发完请求后总线还没完全释放,从站的发送就插进去了,导致数据碰撞;二是从站的A/B线接反,或者没有共地,总线电平漂移严重。
解决:用示波器同时观测DE(发送使能)引脚和总线波形。正确的时序是:从站检测到完整帧之后再拉高DE开始发送,发送完最后一字节后延时至少0.5个字符时间再拉低DE。如果DE切换太快,最后一个字节的停止位会被截断,主站那边就会认为这一帧的长度不对。我一般会在modbus_uart_send的末尾加一个小延时,延时时间等于当前波特率下2个字节的传输时间,再加一点余量。
5.4 现象四:主站能正常读,但一写就有部分从站没反应
用调试工具同时挂多个从站设备做读写测试,发现01、02、03这些读功能码所有从站都正常,一旦发送0F或10这种写多数据的帧,部分从站就不回复,但另外一部分从站回复正常。
原因多出在“帧长度”上。写多数据的帧长度比读请求长得多,部分实现里从站接收缓冲区的长度不够,或者主站在发送完请求后切换为接收状态过早,把响应帧的前几个字节吃掉了。也可能是从站协议栈在解析时没有先校验“数据字节数”字段与实际数据长度是否一致。
解决:检查接收缓冲区是否足够容纳最大请求帧。MODBUS RTU最大帧长度通常是256字节,缓冲区至少给到256;同时在解析0F和10功能码时,用帧里的字节数字段和解析的数据长度做交叉核对,对不上就应该回异常码,而不是继续解析错位的数据。
5.5 现象五:帧收得快了,偶发解析出半帧数据
使用串口空闲中断后,在高波特率下用压力工具快速连续发送请求,从站偶尔会出现解析错误,再仔细看接收缓冲区里的数据,发现存在“半帧”情况——前一次请求的后半段和这次请求的前半段拼在了一起。
原因:串口波特率较高时,主站连续发送两帧之间如果间隔小于3.5字符时间,MODBUS规范上就是同一帧的两个分片。而空闲中断按“单个字符传输时间的空闲”判定,可能会在异常间隔处误判。
解决:加一个滚动空闲超时判断。从站每收到一个字节就刷新一个软件计时器,如果计时器计到超时仍未收到下一字节,才认为帧真正结束。这个计时器在STM32上可以用DWT计数器或者一个1ms时基实现。单独靠串口IDLE中断在低波特率下问题不大,但是到了115200这种高速率,加软件超时兜底是更稳的工程做法。
6. 验收与答辩:从Modbus Poll逐项测试到异常码注入
6.1 三层验收清单:按功能码逐项扫一遍
验收阶段不要只测“读保持寄存器正常”就算过。我的习惯是写三张表:第一张是八个功能码每个都测一遍读写;第二张是异常注入,故意给越界地址和超范围数量,确认异常码正确;第三张是边界值,比如读125个寄存器的上限、写123个寄存器的上限。
| 测试项 | 操作 | 期望结果 |
|---|---|---|
| 01读线圈 | 读连续的线圈地址 | 数据字节按位排列正确 |
| 02读离散输入 | 模拟输入信号置位 | 对应位状态正确 |
| 03读保持寄存器 | 逐个寄存器比对 | 高字节在前,顺序一致 |
| 04读输入寄存器 | 检查输入寄存器表 | 数据区与保持寄存器独立 |
| 05写单线圈 | 写0xFF00和0x0000 | 回显帧与原请求帧一致 |
| 06写单寄存器 | 写边界值0x0000、0xFFFF | 回显帧正确,寄存器值变化 |
| 0F写多线圈 | 批量写入16个线圈 | 回显地址+数量,不包含数据体 |
| 10写多寄存器 | 批量写5个寄存器 | 回显地址+数量,寄存器值全部更新 |
| 异常注入 | 读地址越界、数量超过125 | 回功能码 |
| 广播地址 | 请求中地址域写0并发送写命令 | 执行写入但不回响应 |
工具上我一般用Modbus Poll作为主站调试软件,它可以轮询一组寄存器,也能手动发送单帧,响应超时时间可以自己设。做压力测试时,把轮询周期调到10ms,连续跑20分钟看是否有超时报错,这是简单又有效的稳定性验证手段。
6.2 答辩和交付时的三个加分点
第一,把帧超时时间这个参数讲清楚。MODBUS规定帧间间隔为3.5个字符时间,你可以现场算一笔账:9600波特率下,一个字符约1.1ms,3.5字符时间约3.85ms,这就是为什么很多从站的帧超时定时器设置在4ms附近。把这段话背熟,答辩时你就能解释清自己代码里每个超时参数的来历。
第二,把CRC高低字节的坑讲出来。说“我一开始按大端组帧,用调试工具一测就错位,后来确认MODBUS线上是低字节在前”,这种踩坑经历比背协议定义有说服力得多,它证明你是真的接过外设。
第三,把寄存器表和实际设备量的映射关系讲明白。比如保持寄存器0号存温度原始值、1号存湿度原始值、2号存设备状态字,主站读写的就是这张表。答辩时画一张只有地址和含义的表,评委一眼就知道你的工程不是只跑了一个demo。
说到底,功能码全只是协议层面的完整,真正让人信服的还是异常分支处理得干不干净、边界条件想得周到不周到。我自己的血泪教训是,第一版协议栈只做了03和06功能码,拿去对接某现场屏,对方点了一串变量进来,我这边直接都没反应,后来把所有功能码补齐、又把异常码分支加完整,才算真正收摊。从那以后我每次交付MODBUS工程,都强制走一遍“全功能码扫描+异常码注入”的验收流程,这个习惯帮我拦下了不少不必要的返工。希望帮到你。
本文还有配套的精品资源,点击获取