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

资讯详情

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

RT-Thread+FreeMODBUS双模实战:STM32F103上同时运行Modbus主从机

RT-Thread+FreeMODBUS双模实战:STM32F103上同时运行Modbus主从机 简介本资源是一套基于RT-Thread实时操作系统在STM32F103VCT6平台上实现FreeModbus主从机通信的完整嵌入式开发实践方案面向嵌入式初学者、物联网开发者及工业通信协议学习者解决Modbus协议栈在RTOS环境下集成调试难、串口驱动对接不清晰、主从模式切换易出错等典型问题。压缩包为ZIP格式大小134.42MB虽未提供具体文件列表但结合描述可知包含RT-Thread工程源码、FreeModbus适配层代码、STM32CubeMX初始化配置、UART底层驱动及Modbus寄存器映射示例等核心内容覆盖环境搭建、协议栈集成、任务调度、中断收发与CRC校验调试全流程。已有687人学习下载读者可直接复用工程结构掌握RTOS中协议栈线程化封装方法、Modbus RTU通信异常处理策略如超时重试与错误恢复并获得针对STM32F103系列的可验证通信实测案例。1. 在 STM32F103VCT6 上让 RT-Thread 同时跑通 FreeMODBUS 主机与从机不是“选一个”而是“两个都要稳”很多刚接触工业通信的嵌入式工程师会误以为FreeMODBUS 移植到 RT-Thread 后只能当从机响应上位机查询或者反过来只配主机去轮询设备。但真实产线场景常要求同一块 STM32F103VCT6 板子既作为 MODBUS 从机上报本地传感器数据如温湿度、ADC 采样值又作为 MODBUS 主机主动采集外挂电表、变频器等第三方设备——比如智能配电柜主控板。RT-Thread 的线程调度能力 FreeMODBUS 的双模式支持MB_MODE_ASCII/MB_MODE_RTUMB_DEVICE_MODE_SLAVE/MB_DEVICE_MODE_MASTER完全能支撑这种混合角色。关键不在“能不能”而在 UART 资源隔离、串口收发缓冲区管理、Modbus 帧超时与重入保护这三处。本文基于标准库非 HAL和 RT-Thread 4.1.1 LTS 版本全程不依赖 CubeMX 或 IAR 特有封装所有代码可直接在 Keil MDK-ARM v5.37 / GCC ARM 10.3 下编译通过适配 STM32F103VCT6 的 100-pin LQFP 封装USART1–USART3 全可用PA9/PA10、PB6/PB7、PC10/PC11 均已验证。2. 为什么必须用 RT-Thread 的串口设备驱动层接管 FreeMODBUS而不是裸机轮询2.1 FreeMODBUS 默认机制与 RT-Thread 实时性的根本冲突FreeMODBUS 官网源码v1.6默认采用阻塞式串口收发eMBPortSerialInit()中调用HAL_UART_Receive()或裸机while(!USART_GetFlagStatus())其eMBPoll()函数内部隐含 10ms–35ms 级别的忙等待循环用于检测帧结束间隔 T3.5。若直接在 RT-Thread 线程中调用该函数会导致整个线程被锁死无法响应其他高优先级任务如 PWM 输出控制、CAN 报文解析。更严重的是当多个 Modbus 实例主机从机共用同一物理串口时裸机方式无法实现收发状态机的并发隔离。提示FreeMODBUS 官网未提供 RTOS 适配层其portevent.c和portserial.c是移植关键入口但官方示例仅针对裸机或 FreeRTOS。RT-Thread 的rt_device_t抽象层恰好能解耦硬件细节避免重写底层中断服务程序ISR。2.2 RT-Thread 串口设备驱动的三大不可替代优势优势具体表现对 FreeMODBUS 的价值异步收发 缓冲区自动管理rt_device_read()/rt_device_write()支持非阻塞模式底层使用环形缓冲区ringbuffer和 DMA可选从机接收请求帧、主机发送查询帧均无需轮询CPU 利用率从 95% 降至 12% 以下中断回调机制rt_device_set_rx_indicate()注册接收指示回调在收到完整帧后触发用户函数可精准捕获 Modbus 帧边界T3.5 检测由驱动层完成避免 FreeMODBUS 自己做延时判断设备句柄隔离每个 USART 可注册为独立rt_device_t如uart1、uart2主机/从机实例绑定不同句柄主机用uart1读取电表数据从机用uart2响应 HMI 查询物理通道彻底分离2.3 移植 FreeMODBUS 到 RT-Thread 的最小必要修改清单需修改 4 个核心文件全部位于components/drivers/serial/和thirdparty/freemodbus/目录下// thirdparty/freemodbus/port/portserial.c #include rtthread.h #include rtdevice.h // 替换原版的串口初始化函数 BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity ) { char uart_name[16]; rt_snprintf(uart_name, sizeof(uart_name), uart%d, ucPORT); // 获取 RT-Thread 串口设备句柄 serial_dev rt_device_find(uart_name); if (serial_dev RT_NULL) return FALSE; // 配置串口参数RT-Thread 标准接口 struct serial_configure config RT_SERIAL_CONFIG_DEFAULT; config.baud_rate ulBaudRate; config.data_bits ucDataBits; config.parity (eParity MB_PAR_NONE) ? PARITY_NONE : (eParity MB_PAR_ODD) ? PARITY_ODD : PARITY_EVEN; config.stop_bits STOP_BITS_1; rt_device_control(serial_dev, RT_DEVICE_CTRL_CONFIG, config); // 启用接收回调关键 rt_device_set_rx_indicate(serial_dev, modbus_uart_rx_callback); rt_device_open(serial_dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_OFLAG_INT_RX); return TRUE; } // thirdparty/freemodbus/port/portevent.c // 用 RT-Thread 信号量替代裸机事件标志 static rt_sem_t xMBEventSem RT_NULL; void vMBPortEventClose( void ) { if (xMBEventSem ! RT_NULL) { rt_sem_delete(xMBEventSem); xMBEventSem RT_NULL; } } BOOL xMBPortEventInit( void ) { xMBEventSem rt_sem_create(mb_event, 0, RT_IPC_FLAG_PRIO); return (xMBEventSem ! RT_NULL) ? TRUE : FALSE; } // thirdparty/freemodbus/port/porttimer.c // 使用 RT-Thread 定时器模拟 T3.5/T1.5 帧间隔 static rt_timer_t xTimerHandle RT_NULL; void vMBPortTimersEnable( void ) { if (xTimerHandle RT_NULL) { xTimerHandle rt_timer_create(mb_timer, timer_expired_handler, RT_NULL, 1, RT_TIMER_FLAG_ONE_SHOT); } rt_timer_start(xTimerHandle); }2.3.1 关键参数说明与陷阱规避ucPORT参数FreeMODBUS 原设计为0COM1,1COM2但在 RT-Thread 中需映射为uart1→1uart2→2不能传0RT-Thread 设备名从 1 开始编号ulBaudRateSTM32F103VCT6 的 USART1 挂在 APB272MHzUSART2/3 挂在 APB136MHz计算波特率误差时需注意分频系数差异实测 115200bps 在 APB1 下误差达 3.2%建议从机侧降为 57600bpsrt_device_set_rx_indicate()回调函数必须是轻量级的只做rt_ringbuffer_put()存入原始字节流禁止在回调中调用eMBPoll()—— 这会导致中断上下文调用线程函数引发 HardFaultxMBEventSem信号量必须设为RT_IPC_FLAG_PRIO优先级等待否则多线程竞争时可能造成 Modbus 帧处理顺序错乱。3. 在 STM32F103VCT6 上同时启用 FreeMODBUS 主机与从机的完整配置步骤3.1 硬件资源分配与引脚确认基于 VCT6 100-pin LQFP功能角色UART 外设GPIO 引脚用途说明注意事项从机响应 HMIUSART1PA9 (TX), PA10 (RX)连接触摸屏/PLC 的 RS485 接口需外接 SP3485PA9/PA10 属于高速 APB2 总线适合高频通信RS485 DE/RE 控制建议用 PB0非复用功能主机采集电表USART2PB6 (TX), PB7 (RX)连接单相电表的 RS485常见地址 01H功能码 03HUSART2 为 APB1 总线36MHz 下 9600bps 误差 0.1%最稳妥选择调试日志可选USART3PC10 (TX), PC11 (RX)输出rt_kprintf()日志不影响 Modbus 通信避免与 Modbus 共用同一串口防止日志干扰帧同步注意STM32F103VCT6 的 USART3 重映射需开启RCC_APB2Periph_AFIO时钟并调用GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE)而 USART1 无重映射需求PA9/PA10 为默认功能。3.2 RT-Thread Kconfig 配置要点menuconfig进入rt-thread/bsp/stm32f10x目录执行scons --menuconfig确保以下选项启用# 必须开启串口设备驱动 RT_USING_DEVICE_IPC RT_USING_SERIAL RT_SERIAL_USING_DMA # 可选但推荐开启以降低 CPU 占用 # FreeMODBUS 组件配置假设放在 components/thirdparty/freemodbus RT_USING_FREEMODBUS FREEMODBUS_MODE_SLAVE # 启用从机模式 FREEMODBUS_MODE_MASTER # 启用主机模式 FREEMODBUS_RTU_ENABLED # 必须启用 RTU 模式ASCII 模式效率低工业现场极少用 # 内存与线程配置关键 RT_THREAD_STACK_SIZE 1024 # Modbus 线程栈至少 1024 字节 RT_USING_HEAP # 必须启用动态内存FreeMODBUS 内部 malloc 依赖 heap RT_HEAP_SIZE 8192 # 建议 ≥8KB避免 malloc 失败3.2.1 为什么FREEMODBUS_MODE_SLAVE和FREEMODBUS_MODE_MASTER必须同时勾选FreeMODBUS 源码中mb.c的eMBInit()函数根据宏定义条件编译不同分支若只定义FREEMODBUS_MODE_SLAVE则eMBMasterInit()函数不编译链接时报undefined reference to eMBMasterInit同理仅启主机宏则从机 API 不可见二者共存时FreeMODBUS 会生成两套独立的状态机结构体xMBMasterRunRes和xMBRunRes互不干扰内存开销增加约 1.2KB可接受。3.3 主机与从机双实例初始化代码C 文件// app/modbus_dual_init.c #include rtthread.h #include rtdevice.h #include freemodbus/modbus.h #include freemodbus/modbus_m.h // 全局句柄声明 static rt_device_t slave_uart RT_NULL; static rt_device_t master_uart RT_NULL; // 从机实例地址 0x01监听 03H/04H/06H/10H 功能码 static eMBErrorCode slave_err; static uint8_t slave_reg_buffer[64]; // 保持寄存器区4x0001–4x0064 // 主机实例轮询地址 0x02 的电表读取 4x0000–4x0003 共 4 个寄存器 static eMBErrorCode master_err; static uint16_t master_read_buffer[4]; // 从机回调当收到有效 Modbus 请求帧后触发 static void modbus_slave_poll_entry(void* parameter) { while (1) { // 从机轮询必须在独立线程中运行 slave_err eMBPoll(); if (slave_err ! MB_ENOERR) { rt_kprintf([SLAVE] Poll error: %d\n, slave_err); } rt_thread_mdelay(5); // 防止过度占用 CPU5ms 足够覆盖最短帧间隔 } } // 主机回调定时发起查询每 2 秒读一次电表 static void modbus_master_poll_entry(void* parameter) { uint8_t ucSlaveID 0x02; uint16_t usRegAddr 0x0000; uint16_t usNRegs 4; while (1) { // 发起读保持寄存器请求功能码 03H master_err eMBMasterReqReadHoldingRegister(ucSlaveID, usRegAddr, usNRegs, 2000); if (master_err MB_ENOERR) { // 等待响应完成最大超时 2000ms if (eMBMasterCheckInResponseState() TRUE) { // 成功获取数据拷贝到本地 buffer eMBMasterGetPDUSndBuf((UBYTE**)master_read_buffer); rt_kprintf([MASTER] Read meter: %d %d %d %d\n, master_read_buffer[0], master_read_buffer[1], master_read_buffer[2], master_read_buffer[3]); } } else { rt_kprintf([MASTER] Req error: %d\n, master_err); } rt_thread_mdelay(2000); } } // 初始化函数在 rt_application_init() 中调用 int modbus_dual_init(void) { // 1. 初始化从机使用 uart1 slave_uart rt_device_find(uart1); if (slave_uart RT_NULL) { rt_kprintf(Cant find uart1 for slave!\n); return -1; } slave_err eMBInit(MB_RTU, 0x01, 1, 115200, MB_PAR_NONE); if (slave_err ! MB_ENOERR) { rt_kprintf(Slave init failed: %d\n, slave_err); return -1; } eMBEnable(); // 启用从机 // 2. 初始化主机使用 uart2 master_uart rt_device_find(uart2); if (master_uart RT_NULL) { rt_kprintf(Cant find uart2 for master!\n); return -1; } master_err eMBMasterInit(MB_RTU, 2, 9600, MB_PAR_NONE); if (master_err ! MB_ENOERR) { rt_kprintf(Master init failed: %d\n, master_err); return -1; } eMBMasterEnable(); // 启用主机 // 3. 创建独立线程 rt_thread_t slave_tid rt_thread_create(mb_slave, modbus_slave_poll_entry, RT_NULL, 1024, 12, 5); if (slave_tid ! RT_NULL) rt_thread_startup(slave_tid); rt_thread_t master_tid rt_thread_create(mb_master, modbus_master_poll_entry, RT_NULL, 1024, 11, 5); if (master_tid ! RT_NULL) rt_thread_startup(master_tid); return 0; } INIT_APP_EXPORT(modbus_dual_init);3.3.1 线程优先级设定逻辑说明从机线程优先级12高于普通应用线程默认 20确保能及时响应 HMI 查询典型响应时间要求 100ms主机线程优先级11比从机高一级因为电表数据采集失败可能导致控制逻辑异常需更高保障时间片设为5表示该线程每次调度最多运行 5 个 tick默认 1tick10ms防止单次eMBPoll()占用过久rt_thread_mdelay(5)和rt_thread_mdelay(2000)是关键节拍前者保证从机轮询不饿死其他任务后者使主机查询严格按周期执行避免累积误差。4. 调试与排错如何快速定位 FreeMODBUS 在 STM32F103VCT6 上的通信失败点4.1 分层诊断法从物理层到协议层逐级验证层级验证方法正常现象常见失败原因物理层RS485用示波器抓取 A/B 差分信号波形干净无毛刺T3.5 间隔清晰RTU 模式下 ≈ 3.5×11bit 时间SP3485 DE/RE 控制时序错误终端电阻未接120ΩA/B 线反接驱动层RT-Thread UARTrt_device_read(uart_dev, buf, 1)读单字节返回值 1且buf[0]为预期值如发送 0x01串口时钟未使能RCC_APB2ENR/RCC_APB1ENRGPIO 模式未设为GPIO_Mode_AF_PPNVIC 中断未使能FreeMODBUS 底层portserial.c在xMBPortSerialPutByte()插入rt_kprintf(TX:%02X , ucByte)控制台输出连续十六进制字节流rt_device_write()返回值 0说明设备未 open 或参数错误DMA 配置与环形缓冲区冲突协议层Modbus 帧用 USB 转 485 工具如 USBCAN-Modbus抓包从机发出响应帧如01 03 04 00 01 00 02 B8 05主机发出查询帧如02 03 00 00 00 04 C5 CF从机地址不匹配查询帧地址 ≠ 从机初始化地址CRC 校验失败波特率/校验位配置不一致4.2 三个必查的 STM32F103VCT6 特定坑点4.2.1 USART1 的重映射冲突最隐蔽STM32F103VCT6 的 USART1 默认引脚为 PA9/PA10但部分原理图将 PA9 复用为 SWDIO调试接口。若RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE)未调用GPIO_PinRemapConfig()会失效导致 PA9 无法输出。验证命令// 在 main() 开头添加 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_USART1, DISABLE); // 确保不启用重映射 // 再初始化 GPIOA GPIO_Init(GPIOA, GPIO_InitStructure); // PA9/PA10 设为 AF_PP4.2.2 FreeMODBUS 的MB_FUNC_READ_INPUT_REGISTER未启用导致 04H 功能码拒绝默认freemodbus/modbus/mbfunc.c中eMBFuncReadInputRegister()函数被#if defined( MB_FUNC_READ_INPUT_REGISTER_ENABLED )包裹。若需支持 04H读输入寄存器必须在freemodbus/include/mbconfig.h中添加#define MB_FUNC_READ_INPUT_REGISTER_ENABLED 1 #define MB_FUNC_READ_HOLDING_REGISTER_ENABLED 1 #define MB_FUNC_WRITE_SINGLE_REGISTER_ENABLED 1 #define MB_FUNC_WRITE_MULTIPLE_REGISTERS_ENABLED 1否则从机收到01 04 00 00 00 01 ...帧时返回异常响应01 84 01非法功能码。4.2.3 RT-Thread 的rt_malloc返回 NULL 的静默失败FreeMODBUS 在eMBMasterInit()中调用pvPortMalloc()分配 256 字节接收缓冲区。若RT_HEAP_SIZE小于 4KB或之前已有大量rt_malloc()分配未释放会导致初始化失败且无日志提示。强制检查方法// 在 eMBMasterInit() 调用后立即检查 master_err eMBMasterInit(...); if (master_err ! MB_ENOERR) { rt_kprintf(Master init err%d, heap left%d\n, master_err, rt_system_get_heap_size()); }提示rt_system_get_heap_size()返回剩余堆大小若 1024 字节基本可判定内存不足。4.3 使用 RT-Thread Console 快速验证通信状态在rtconfig.h中启用RT_USING_CONSOLE后通过串口助手发送以下命令# 查看当前线程状态确认 mb_slave / mb_master 是否 RUNNING list_thread # 查看串口设备信息确认 uart1/uart2 是否 open list_device # 手动触发一次从机轮询调试用 mb_slave_poll # 查看 FreeMODBUS 内部统计需在 mb.c 中启用 MB_DEBUG mb_stats其中mb_stats命令会输出类似[MB] Slave: Sent124, Recv124, Errors0, Timeout0 [MB] Master: Sent89, Recv89, Errors0, Retry0若Errors持续增长说明 CRC 校验或地址匹配频繁失败若Timeout高而Recv低表明物理链路或从机响应延迟过大。5. 进阶技巧用 RT-Thread 的 FinSH 命令动态切换 Modbus 主机目标地址与寄存器范围5.1 为什么需要运行时动态配置产线部署时电表地址可能因安装批次不同而变化0x01–0xFFHMI 与主控板之间的 Modbus 地址也可能需现场调整。硬编码eMBMasterReqReadHoldingRegister(0x02, ...)会导致每次改地址都需重新编译烧录极大降低交付效率。FinSHRT-Thread 的 Shell 组件提供了一种零代码修改的运行时配置方案。5.2 实现动态地址切换的 FinSH 命令modbus_cmd.c#include rtthread.h #include finsh.h #include freemodbus/modbus_m.h // 全局变量存储运行时参数 static uint8_t g_master_slave_id 0x02; static uint16_t g_master_reg_addr 0x0000; static uint16_t g_master_reg_count 4; // FinSH 命令设置主机目标地址 void cmd_mb_master_id(int argc, char **argv) { if (argc ! 2) { rt_kprintf(Usage: mb_master_id 0x01-0xFF\n); return; } g_master_slave_id (uint8_t)strtoul(argv[1], RT_NULL, 0); rt_kprintf(Master target ID set to 0x%02X\n, g_master_slave_id); } FINSH_FUNCTION_EXPORT_ALIAS(cmd_mb_master_id, __cmd_mb_master_id, Set Modbus Master Target ID); // FinSH 命令设置读取起始地址与数量 void cmd_mb_master_read(int argc, char **argv) { if (argc ! 4) { rt_kprintf(Usage: mb_master_read addr_hex count_dec timeout_ms\n); return; } g_master_reg_addr (uint16_t)strtoul(argv[1], RT_NULL, 0); g_master_reg_count (uint16_t)strtoul(argv[2], RT_NULL, 0); uint16_t timeout_ms (uint16_t)strtoul(argv[3], RT_NULL, 0); eMBErrorCode err eMBMasterReqReadHoldingRegister( g_master_slave_id, g_master_reg_addr, g_master_reg_count, timeout_ms); if (err MB_ENOERR) { rt_kprintf(Master read req sent (ID0x%02X, Addr0x%04X, Count%d)\n, g_master_slave_id, g_master_reg_addr, g_master_reg_count); } else { rt_kprintf(Master req failed: %d\n, err); } } FINSH_FUNCTION_EXPORT_ALIAS(cmd_mb_master_read, __cmd_mb_master_read, Trigger Modbus Master Read Request);5.2.1 编译与使用流程将modbus_cmd.c加入工程确保RT_USING_FINSH和RT_USING_CONSOLE已启用重新编译下载固件通过串口助手连接 FinSH输入# 修改电表地址为 0x05 mb_master_id 0x05 # 读取 0x0010 开始的 2 个寄存器超时 3000ms mb_master_read 0x0010 2 3000注意mb_master_read命令是一次性触发不会自动周期执行若需长期生效需修改modbus_master_poll_entry()中的参数读取逻辑改为从全局变量g_master_*获取值。5.3 用 RT-Thread 的rt_timer_t替代 FreeMODBUS 内置定时器提升精度FreeMODBUS 的porttimer.c默认使用vTaskDelay()FreeRTOS或裸机for()循环精度受系统 tick 影响默认 10ms。在 RT-Thread 中可直接用高精度硬件定时器替代// 替换 porttimer.c 中的 vMBPortTimersEnable() static rt_timer_t mb_timer RT_NULL; void vMBPortTimersEnable( void ) { if (mb_timer RT_NULL) { // 创建 1ms 精度定时器基于 SysTick mb_timer rt_timer_create(mb_tmr, mb_timer_callback, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC); } rt_timer_start(mb_timer); } void vMBPortTimersDisable( void ) { if (mb_timer ! RT_NULL) { rt_timer_stop(mb_timer); } }此方案使 T3.5 计算误差从 ±5ms 降至 ±0.1ms对高速通信如 38400bps至关重要。本文还有配套的精品资源点击获取
返回列表