简介:STM32-RS485-Modbus 是一份基于 STM32 HAL 库的嵌入式通信工程源码,围绕 RS485 总线接口与 Modbus RTU 协议实现展开,并演示了中菱轮毂电机 ZLAC8015D 的控制方法。项目在 CLion 环境中以 CMake 构建,代码结构清晰,适合 STM32 开发者、工业自动化学习者理解单片机串口通信与现场总线协议,既可作为课程设计参考,也能直接改造成项目基础。压缩包共 187 个文件,大小约 1015KB,包含 48 个 .h 头文件、25 个 .c 源文件、官方 HAL 库驱动、CMake 构建脚本、编译产生的 .obj/.d 中间文件、STM32CubeMX 生成的 .ioc 配置、.ld 链接脚本以及可直接烧录的 .hex/.bin 固件,按功能模块划分便于检索。已有 1614 人学习下载,代码可复现、易扩展。通过该工程,读者能够查看 UART、定时器和 RS485 收发控制的初始化逻辑,学习 Modbus RTU 报文构造、CRC 校验和数据帧解析的具体过程,并掌握电机控制指令的发送顺序与参数设置,为后续开发 PLC 互联、传感器采集或类似工业驱动项目奠定基础。
1. 用 CLion 给 STM32 写 Modbus 从站:一个 RS485 驱动的工业通信小项目
调试台上那块仪表又在"沉默"。上位机软件读不到任何数据,示波器戳在 RS485 的 A/B 线上,波形倒是干干净净——问题不在硬件,在我的固件里 Modbus 协议栈根本没把响应发回去。这正是我后来把整套 STM32 + RS485 + Modbus 从站实现整理成一个独立工程的起因:不是写一个酷炫的协议栈,而是把"能跑通、能对着波形调、能快速改"的串口通信链路完整落地。这份工程以 CLion 作为开发环境,基于 CMake 构建,覆盖从串口空闲中断收帧、CRC16 校验、寄存器读写到 RS485 方向切换的完整闭环,适合正在学 Modbus 的嵌入式开发者、需要做设备联调的工程师,以及那些想把 Keil 工程迁到现代 IDE 的人。
2. 工程骨架:CLion 下 STM32 开发环境与系统结构
2.1 目录结构与文件职责
这份工程的目录组织思路很直接:把所有和硬件相关的初始化收拢到 Board 层,把 Modbus 协议逻辑独立出来,不跟 HAL 库强绑定。这样做的直接好处是,想把 Modbus 层搬到另一块芯片上,只需要改底层接口,协议解析部分一行不用动。
stm32-rs485-modbus/ ├── CMakeLists.txt # 顶层构建脚本,配置交叉编译链与链接脚本 ├── ldscript.ld # STM32 链接脚本,定义 Flash/RAM 布局 ├── src/ │ ├── main.c # 主函数,初始化外设并进入主循环 │ ├── modbus.c # Modbus RTU 从站核心:帧解析、CRC、响应组装 │ ├── modbus.h │ ├── serial.c # UART 初始化、DMA 收帧、空闲中断回调 │ ├── serial.h │ ├── rs485.c # RS485 收发方向控制(DE/RE 引脚切换) │ ├── rs485.h │ ├── reg_table.c # 寄存器表:保持寄存器与线圈的定义 │ └── reg_table.h └── openocd.cfg # OpenOCD 调试/烧录配置每个文件的职责边界很清楚。serial.c 只负责把字节从串口收进来,收到一帧完整数据后往全局缓冲区放,不碰协议;modbus.c 从缓冲区拿数据,做地址匹配、功能码分发、CRC 校验,然后填充响应缓冲区;rs485.c 在发送前把引脚拉高,完成后拉低。寄存器表单独放一个文件,是为了让二次开发时可以快速定义自己的数据区,而不需要去翻协议栈内部。
2.2 CMake 核心配置:交叉编译链与烧录
STM32 在 CLion 下的难点不是写代码,而是把 CMake 和工具链配置顺。工程在 CMakeLists.txt 里做了三件事:指定 arm-none-eabi-gcc 交叉编译器、把芯片型号对应的宏定义和链接脚本传给编译器、把所有源文件收集起来生成 elf 文件。
cmake_minimum_required(VERSION 3.20) project(stm32_rs485_modbus C ASM) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(MCU_FLAGS "-mcpu=cortex-m3 -mthumb -mfloat-abi=soft" "-DSTM32F103xE -DUSE_HAL_DRIVER" ) set(CMAKE_C_FLAGS "${MCU_FLAGS} -Wall -fdata-sections -ffunction-sections") add_executable(stm32_rs485_modbus src/main.c src/modbus.c src/serial.c src/rs485.c src/reg_table.c ) target_link_options(stm32_rs485_modbus PRIVATE "-Wl,--gc-sections" "-Wl,--print-memory-usage" "-Wl,-T${CMAKE_SOURCE_DIR}/ldscript.ld" )这里有几个参数值得说明。-mcpu=cortex-m3告诉编译器目标内核是 Cortex-M3,对应 F103 系列是不能带浮点单元的,所以-mfloat-abi=soft指启用软件浮点运算,防止生成硬件 FPU 指令导致 HardFault。-DSTM32F103xE和-DUSE_HAL_DRIVER是给 HAL 库用的宏定义,前者决定了芯片 Flash 容量版本,后者让 HAL 层的源文件参与编译。--gc-sections在链接阶段把没用到的函数和段剪掉,对 64KB Flash 的小芯片来说能省出不少空间。
烧录配置写在openocd.cfg里,CLion 的烧录按钮会调用 OpenOCD 把生成的 elf 写进芯片:
source [find interface/stlink.cfg] source [find target/stm32f1x.cfg] transport select hla_swd adapter speed 4000stlink.cfg声明用的是 ST-Link 调试器,stm32f1x.cfg是 STM32F1 系列的 OpenOCD 目标配置文件,transport select hla_swd把调试接口从默认的 JTAG 切到 SWD 两线模式,这样只占用 PA13/PA14 两个引脚,把其他引脚留给应用。速度 4000kHz 在大多数 ST-Link 上都稳定,如果线比较长可以降到 1000 避免同步失败。
工程在 CLion 里的调试体验比传统 IDE 舒服的地方在于能直接看到结构体变量展开后的字段值,配合 SVD 文件还能在调试器里查看 USART 外设寄存器的实时状态。第一次配置时记得在 Settings 里把 Toolchain 指向 arm-none-eabi-gcc 的安装目录,Build 目标选stm32_rs485_modbus,调试器选 OpenOCD,剩下的交给 IDE 处理。
3. 核心代码拆解:Modbus RTU 从站的一收一发全链路
3.1 串口收帧:DMA + 空闲中断判断一帧结束
Modbus RTU 是帧协议,一帧数据的结构固定是"地址(1字节) + 功能码(1字节) + 数据区(若干字节) + CRC16(2字节)"。收帧的关键问题在于判断"什么时候算一帧结束"。Modbus 规范里用的是帧间隔:两个字节之间的空闲时间超过 3.5 个字符时间,就认为前面是一个完整的帧。
工程里选了另一种工程上更常用的方案:UART 空闲中断。串口线路上持续一段时间没有新的字节到达,硬件就会触发 IDLE 标志,这个时间粒度和 3.5 字符时间在常见的 9600/115200 波特率下都能正确区分帧边界,而且不用额外开定时器做超时判断,实现最简单。
// serial.c static uint8_t rx_buf[256]; static volatile uint16_t rx_len; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 单字节接收完成:记录收到一个字节,继续接收下一个 if (huart->Instance == USART1) { rx_len++; HAL_UART_Receive_IT(huart, &rx_buf[rx_len - 1], 1); } } void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { // 总线噪声或多从站冲突时 UART 会报错,这里清掉错误标志恢复接收 if (huart->Instance == USART1) { __HAL_UART_CLEAR_OREFLAG(huart); HAL_UART_Receive_IT(huart, &rx_buf[rx_len - 1], 1); } }这里用的是单字节中断接收而不是 DMA,因为 Modbus 帧最长也就 256 字节,中断开销完全可以接受,代码反而更直白。HAL_UART_Receive_IT每次启动一个字节的接收,收到后触发回调,rx_len自增并继续开启下一次接收。注意rx_len在协议层处理完一帧后要手动清零复位,这个后面再说。
帧结束用空闲中断来判断:
void serial_idle_poll(void) { // 检查 USART1 的空闲中断标志,置位表示串口线空闲,一帧收完了 if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); modbus_frame_received(rx_buf, rx_len); rx_len = 0; } }serial_idle_poll放在主循环里轮询调用,HAL 库会把 IDLE 事件和接收回调一起管理。置位后先清标志再交给协议层处理,避免重复触发。这个做法的坏处是主循环里如果还有其他任务耗时太长,帧处理的实时性会受影响,但 Modbus 本身是主从轮询机制,从站响应慢一点(只要在上位机超时时间之前)问题不大。
3.2 Modbus 帧解析与 CRC16 校验
协议层拿到的是一串裸字节,第一步要做的是:地址匹配、CRC 校验、长度合法性检查。三者全部通过才继续解析功能码。CRC16 校验是 Modbus RTU 最容易写错、也最值得仔细检查的部分,下面这段是工程里实际的实现。
// modbus.c uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; // Modbus 规定 CRC 初值为 0xFFFF while (len--) { crc ^= *data++; // 先和当前字节异或 for (int i = 0; i < 8; i++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; // LSB 为 1 时右移并异或多项式 } else { crc >>= 1; } } } return crc; }0xA001是 Modbus 使用的 CRC16 多项式 0x8005 反序后的值,算法是逐字节、逐位处理:初值0xFFFF,每个字节先和 CRC 低字节异或,然后右移一位,如果移出的最低位是 1 就再异或0xA001。这里有个容易踩的细节:Modbus 报文里 CRC 是低字节在前、高字节在后,所以比较时要反着来。
// modbus.c int modbus_frame_received(const uint8_t *buf, uint16_t len) { if (len < 4) return -1; // 最短帧:地址+功能码+2字节CRC if (buf[0] != MODBUS_ADDR) return -1; // 地址不匹配,本机不起响应 uint16_t crc_received = buf[len - 1] << 8 | buf[len - 2]; // 低字节在前 uint16_t crc_calc = modbus_crc16(buf, len - 2); // CRC 不计入自身 if (crc_received != crc_calc) return -1; // 校验失败,直接丢弃 uint8_t fcode = buf[1]; switch (fcode) { case 0x03: // 读保持寄存器 return handle_read_holding(buf, len); case 0x06: // 写单个保持寄存器 return handle_write_single(buf, len); default: return send_exception(buf, 0x01); // 不支持的功能码 } }传入的buf是串口层收下来的原始数据。先检查长度下限 4 字节(一帧最短就是地址、功能码、两个 CRC 字节),再匹配从站地址,然后单独取出帧尾的 CRC 和重新计算的 CRC 比较。crc_received的拼接方式是buf[len-1] << 8 | buf[len-2],因为收到的第二个字节是 CRC 低字节,放进uint16_t的高位还是低位这一步写反了会导致所有帧都被判为校验失败,我在调试时被这个问题折腾过整整一个下午。
3.3 寄存器读写:从帧数据到响应组装
功能码 0x03 是读保持寄存器,上位机请求里带着起始地址和寄存器数量,从站从自己的寄存器表里取数据拼成响应帧。寄存器表在前面提到的 reg_table.c 里定义:
// reg_table.c static uint16_t holding_regs[16] = { 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F }; uint16_t reg_table_read(uint16_t addr) { if (addr < 16) return holding_regs[addr]; return 0xFFFF; } void reg_table_write(uint16_t addr, uint16_t value) { if (addr < 16) holding_regs[addr] = value; }这个表只有 16 个保持寄存器,生产环境里会换成真正的数据源,比如把第 0 号寄存器绑到 ADC 转换结果,把第 1 号寄存器做成设备状态字。表结构保持"读写接口加数组实现"这种朴素形态,是为了让后续替换成 Flash 存储或外设映射时不需要改动协议层。
响应组装在 handle_read_holding 函数里进行:
// modbus.c int handle_read_holding(const uint8_t *buf, uint16_t len) { if (len != 8) return -1; // 0x03 请求固定 8 字节 uint16_t start_addr = (buf[2] << 8) | buf[3]; uint16_t reg_count = (buf[4] << 8) | buf[5]; if (reg_count < 1 || reg_count > 16) { return send_exception(buf, 0x03); // 数量越界,异常响应 } uint8_t resp[3 + 2 * reg_count + 2]; resp[0] = buf[0]; // 从站地址 resp[1] = buf[1]; // 功能码 resp[2] = reg_count * 2; // 数据字节数 for (int i = 0; i < reg_count; i++) { uint16_t val = reg_table_read(start_addr + i); resp[3 + 2 * i] = val >> 8; // 寄存器值高字节 resp[4 + 2 * i] = val & 0xFF; // 寄存器值低字节 } uint16_t crc = modbus_crc16(resp, 3 + 2 * reg_count); resp[3 + 2 * reg_count] = crc & 0xFF; // CRC 低字节在前 resp[3 + 2 * reg_count + 1] = crc >> 8; return rs485_send(resp, sizeof(resp)); }起始地址和寄存器数量都是大端字节序,buf[2] << 8 | buf[3]得到起始地址,这个顺序不能反。响应帧的字节编排是:地址、功能码、字节数、数据区、CRC,Modbus 协议里数据区每个寄存器固定两个字节,高字节在前。3 号功能码最多允许读 16 个寄存器,超过就回异常帧 0x03,这是协议规定的上限,防止缓冲区溢出。
3.4 RS485 方向切换:发送完成后才能拉低使能脚
RS485 是半双工总线,收发共用一个物理链路。收发器芯片的 DE/RE 引脚控制方向:拉高进发送模式,拉低进接收模式。时序写错会导致帧发出去一半就被掐断,这是 RS485 联调最经典的翻车现场。
// rs485.c int rs485_send(const uint8_t *data, uint16_t len) { // 拉高 DE,切换为发送模式 HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); // 串口把数据发完,这里必须等待发送完成标志再切回接收 HAL_StatusTypeDef st = HAL_UART_Transmit(&huart1, (uint8_t *)data, len, 100); // 等最后一个字节从移位寄存器移出后再拉低 DE,否则帧尾会被砍掉 while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET); HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); return (st == HAL_OK) ? 0 : -1; }HAL_UART_Transmit的最后一个参数是超时时间,单位毫秒。如果发送数据过程中串口掉线或者被其他任务打断,这个调用会返回超时错误。但哪怕返回错误,也有可能已经发出去了几个字节,所以不能光看返回值,一定要等到UART_FLAG_TC置位——TC 标志表示移位寄存器已经全部移完,线路确实空闲了,这时候拉低 DE 才安全。这里拉得太早会导致帧尾的 CRC 字节少发一个或多个,上位机那边看到的现象就是"响应帧 CRC 校验失败",而波形上最后一个字节只剩一半。等 TC 标志的这种方式就叫"血泪换来的顺序"。
4. 避坑记录:Modbus 联调中常见的 5 个翻车现场
4.1 串口收到的最后一字节丢失
现象:上位机总是报"响应超时",但示波器上看从站明明发了响应,帧尾少了一个字节。
原因是 DE 拉低太早。我最早是在HAL_UART_Transmit返回后立刻拉低 DE,但 HAL 函数返回在最后一个字节进入移位寄存器之后、完全发出之前。RS485 收发器从发送模式切到接收模式需要几十微秒的切换时间,这期间电路把还没发完的数据给截断了。
解决方法是等UART_FLAG_TC置位后再拉低 DE,代码见 3.4 节。从那以后我每次写 RS485 发送都会强制检查"TC 等了吗",这已经成习惯。
4.2 帧数据收重了:DMA 缓冲区和处理逻辑撞车
现象:通信偶发异常,上位机重发一次就恢复正常,抓到的响应帧内容被整体延迟了。
原因是清空接收缓冲区的时机错误。我最初在serial_idle_poll里用了memset(rx_buf, 0, sizeof(rx_buf))来"清空"缓冲区,而这个函数执行期间如果新的字节刚好进来,DMA 或中断接收到的数据会被先写的清零操作覆盖,造成一帧数据错乱。
解决方法是不要清空缓冲区,只把rx_len清零让下一帧从头写。缓冲区里的脏数据在计算 CRC 时自然会暴露问题,不影响新帧的解析。
4.3 CRC 校验天生失败
现象:用串口助手手动发 03 功能码指令,从站永远不响应,逻辑分析仪上看收到的字节数和内容都对。
原因是主机按照 Modbus 协议文档的 CRC 算法实现有偏差。Modbus CRC 用的多项式是 0xA001,初值是 0xFFFF,很多人照着网上通用 CRC16 代码直接把多项式写成 0x8005,这两个不能用同一个实现。另外报文里 CRC 低字节在前,比较时要反着拼接。
解决方法是先拿协议规范里的标准测试向量验证:对 01 03 00 00 00 01 这帧数据,正确 CRC 应该是 0x840A,如果你的代码算出来不是这个值,算法实现一定有问题。
4.4 多从站组网后相互干扰
现象:单台从站通信完全正常,挂两台从站之后两边都开始出现偶发错误,而且表现为"特定从站地址无法访问"。
原因是 RS485 总线没有接终端电阻,或者 A/B 线接反了。RS485 链路两端需要各接一个 120Ω 终端电阻吸收反射信号,只接一端或者不接,总线空闲时 A/B 之间的电平差不够稳定,接收端会读到随机数据。A/B 接反会导致所有设备都收不到数据,但如果中途有一台焊反了,表现就是地址不匹配加上 CRC 乱跳。
解决方法是先确认总线两端有 120Ω 电阻,然后断电情况下用万用表量 A 和 B 之间的电阻值,应该约等于 60Ω(两个 120Ω 并联)。这个检查在组网前做一次,能省三天的定位时间。
4.5 调试器和设备连不上
现象:ST-Link 连接失败,目标板断电重启后又能连上,但一运行就断。
原因是 RS485 通信的时候 A/B 线上有共模电压波动,通过收发器和地线串到了 SWD 调试接口上。如果调试器和设备是分别供电的,两边的地电位不一致时,调试器可能被干扰甚至烧毁。
解决方法是调试时先断掉 RS485 总线,或者使用带隔离的收发器模块。最省事的办法是把 SWD 接口的频率从 4000kHz 降到 1000kHz,同时给调试器单独供电不和设备共地。后来我在所有带 RS485 的板卡上都留了"调试/运行"切换开关,排查这类玄学问题快得多。
5. 跑通工程:从编译烧录到与上位机联调
5.1 硬件接线与参数配置
在跑工程之前先把硬件连接确认清楚。RS485 这边用的是常见的 SP3485 或 MAX3485 收发器芯片,具体要求看下面这个表:
| 引脚 | 连接对象 | 说明 |
|---|---|---|
| A / B | 总线两根线 | A 接 A,B 接 B,不能交叉 |
| DE / RE | STM32 某个 GPIO | 两个引脚并在一起,拉高发送、拉低接收 |
| RO / DI | 分别接 USART1 的 RX / TX | 收发器和 MCU 之间的数据通路 |
| 终端电阻 | 总线两端 | 各并联 120Ω 电阻,排除波反射 |
串口参数配置在main.c的MX_USART1_UART_Init函数里,默认 9600 波特率、8 数据位、无校验、1 停止位。Modbus RTU 没有硬性规定波特率,部分现场设备用的是 9600 或 19200,如果你的上位机固定用 115200,可以把huart1.Init.BaudRate改成对应值,但要记住两个通信节点必须一致。
从站地址的默认值是 0x01,在modbus.h里用宏定义:
#define MODBUS_ADDR 0x01如果要把地址改到 0x02,直接改这个宏重新编译烧录。多个从站组网时每个站的地址不能重复,这是总线通信冲突的第一前提。
5.2 烧录与调试流程
CLion 里配置好 Toolchain 和 OpenOCD 之后,点右上角的烧录按钮就能把固件写进 Flash。首次烧录如果报target not found,检查 SWD 接口的四根线,尤其是 GND 必须和设备共地,只连 SWDIO、SWCLK、3V3 不连 GND 是典型的"连不上调试器"原因。
调试时在modbus_frame_received函数入口打一个断点,运行等待后,用上位机发一帧读寄存器指令,断点应该命中。此时在 Debugger 面板里能看到buf缓冲区里的原始字节,对照协议格式检查。更高级的用法是加载 SVD 文件——CLion 支持在 Debug 配置里指定 SVD 路径,之后调试器里可以直接以 "USART1->SR"、"USART1->DR" 这种名字查看外设寄存器,RXNE 位是否置位、TC 标志有没有起来一眼就能看出来。
调试还会用到一个功能:CLion 的 Live Watch 窗口可以实时查看变量值,把rx_len和rx_buf放进去,每当上位机发来一帧,能看到它们的变化。这比串口打印日志快,不用为了调试加printf再删掉。
5.3 与主站上位机联调验证
先用串口助手确认基础通信,再用 Modbus 主站模拟工具做协议验证。推荐的做法很简单:先发固定报文,不要上来就用图形化界面点来点去,因为界面工具会把 CRC 封装好递给你,出问题反而不知道是哪层错了。
用串口助手手动发送这帧数据测试读寄存器功能:
01 03 00 00 00 01 84 0A逐字节拆开:01是地址,03是功能码,00 00是起始寄存器地址,00 01是读 1 个寄存器,84 0A是 CRC16。如果工程正常工作,串口助手的接收区应该出现这样一帧响应:
01 03 02 00 00 B8 4402是数据字节数,00 00对应寄存器 0 的当前值是 0x0000。不信的话拿 4.3 节提到的标准向量验证算法实现,跑出来的 CRC 应该和报文里完全一致。
验证结果不符合预期时,按第 4 章的顺序排查:先查 CRC 算得对不对,再查 DE 切换时序,然后用示波器看 A/B 线上波形。波形是方波还是近似正弦波不重要,重要的是有没有正确的帧间间隔和字节数。我在联调这种项目时从来没有绕过波形定位过问题,示波器在手边和不在手边,排查效率差三五倍。
6. 二次开发:从读寄存器到写寄存器、改主站、换芯片
工程当前只实现了 0x03 读保持寄存器和 0x06 写单寄存器两个功能码,对很多实际场景不够用。最常见的改造是加 0x10 写多个连续寄存器,上位机下发一条指令设置一组参数,比如电机驱动器的速度、加速度和位置。在modbus_frame_received的 switch 语句里加一个分支:
case 0x10: return handle_write_multi(buf, len);新功能码的完整逻辑大约六十行:请求帧格式是"地址 + 0x10 + 起始地址 + 数量 + 字节数 + 数据 + CRC",响应帧只需要"地址 + 0x10 + 起始地址 + 数量 + CRC"。写实现的时候注意两点:请求里的数量是寄存器个数,而字节数是数量的两倍,解析时不要踩到越界;以及数据区的每个寄存器值是大端序,(data[i] << 8) | data[i+1]的组合方向错了会把参数写入完全错误的位置。
另一个方向是把这份从站代码改成主站。Modbus 主站是主动方,负责周期轮询总线上所有从站的寄存器。实现思路是:用定时器或主循环按固定的时间间隔发送读请求,然后等待对应从站回复。如果超时没回复,记录下来并继续下一台从站,不能让整个轮询停住。轮询周期的确定原则是:总线上所有从站的响应时间加上帧间隔,每个站按最快 50ms 的响应时间估算,挂 10 台从站时轮询周期至少在 500ms 以上。
改动需要重构的部分主要在 modbus.c:把modbus_frame_received从"被动响应"改成"按需发送",更实用做法是抽象一层"应用调度器":它维护一个从站地址列表 + 寄存器读取计划,到时间点就调用modbus_build_request往串口发请求,然后读串口响应。响应发送和帧解析可以复用现有代码,但要增加一个超时状态机,用 HAL 库的HAL_GetTick()记录等待时间。
以我的习惯,每次改完协议代码都会先跑一遍完整的自测流程:先用串口助手按手工组帧的方式把支持的每一个功能码全发一遍,确认响应字节逐一正确;再把多台从站挂上总线组网跑一小时,看有没有偶发错误;最后做一次压力测试,用上位机以 100ms 周期连续读写 24 小时,同时检查串口错误计数寄存器有没有增长。这套流程翻过太多车才固定下来,从那以后我每次加功能码都会强制走一遍,"能响应单帧"和"能连续稳定工作一整天"是两种完全不同的完成度。希望帮到你。
本文还有配套的精品资源,点击获取