
简介针对STM32F103移植FreeModbus并同时实现Modbus RTU与Modbus TCP的完整工程资源适合嵌入式开发人员、工业控制领域工程师以及需要快速落地Modbus通信的物联网开发者。包内包含601个文件以C源码.c、头文件.h、编译中间文件.d/.o/.crf以及Keil工程配置文件.uvprojx/.uvoptx为主另有hex烧录文件、map映射文件等整体约12.37MB可直接查看或二次移植。已有4955人学习下载。工程不仅提供FreeModbus协议栈在STM32F103上的底层适配还包含了W5500以太网驱动、IPC通信、定时器/ADC等外设配置以及LED控制等验证代码能够帮助开发者同时掌握RTU串行通信与TCP网络通信的实现方法并可直接基于“主控板APP”工程进行扩展缩短开发周期。 说实话在STM32F103这种72MHz的Cortex-M3上跑FreeModbus属于典型的“小芯片干大事”。这颗老芯片把ModbusRTU跑顺一点不难难的是同一颗芯片上既要把串口侧的ModbusRTU伺候好又要开一路ModbusTCP跟HMI、组态软件、上位机打交道。但只要你把这套架构理清楚后面换F1、F0或是换成GD32、AT32逻辑都是通的。这篇文章我把整个移植过程拆开讲从FreeModbus的文件结构、串口和定时器驱动怎么改、W5500怎么接入、MBAP头怎么理解到后面寄存器数据区怎么映射、调试时容易踩的坑全都过一遍。适合两类人看一是准备在项目里用Modbus和外部设备通讯的嵌入式工程师二是目前正用串口调ModbusRTU想叠加网口功能又不想推倒重来的开发者。压缩包里给你的是一套能直接落地的工程结构下面我把每一步选型的原因和落地细节都交代清楚。1. 方案选型与整体架构1.1 为什么是FreeModbus而不是自己写协议做嵌入式Modbus方案大致有三条路纯自己写、用libmodbus、用FreeModbus。自己写一套RTU还能勉强应付但要做到广播处理、异常码回复、功能码覆盖、寄存器边界保护这些细节工作量一下子就上来了而且越到后面越觉得自己在重复造轮子。libmodbus在工控机和上位机上用得很多但它是偏主站与通用设备的库结构对单片机资源不友好。FreeModbus的定位就非常清晰面向嵌入式从站设备RTU、ASCII、TCP三种模式都齐了纯C编写裸机也能跑代码量小移植点集中在port层。选型的时候还有一点很关键FreeModbus已经把Modbus的状态机、帧结束判定、CRC16、功能码分发表都包好了我只需要解决四件事——串口怎么收字节、串口怎么发字节、RS485方向怎么切、帧间隔定时器怎么对齐。这就像买了一个功能完整的家电我只需要接好水电不用自己造压缩机。1.2 硬件平台STM32F103最小系统、RS485与W5500STM32F103系列本身不带以太网MAC所以ModbusTCP一般靠外挂以太网芯片解决。常见选择有W5500、ENC28J60、CH395实话说W5500是最省心的方案它内置了全硬件TCP/IP协议栈MCU只通过SPI读写寄存器就能完成TCP连接。我做这个项目时用的是STM32F103C8T6最小系统板搭配一个SP3465的RS485收发器网口部分用W5500模块整体物料成本很低非常适合做采集终端。资源规划上USART1跑ModbusRTUPA9/PA10接485芯片再拿一个普通GPIO做DE/RE方向切换SPI1接W5500PA5/PA6/PA7分别做SCK/MISO/MOSI片选和复位随便挑两个GPIO。整个工程跑下来Flash占用大概20KB左右RAM占用不到4KB对F103来说非常宽裕。如果你的项目还需要跑FreeRTOS这套架构也能平移过去后面我会说裸机和RTOS的差异。1.3 标准库v3.5和FreeModbus v1.6的搭配网上搜“STM32F103 标准库 FreeModbus”能搜出一大量资料绝大多数示例都基于标准库v3.5和FreeModbus v1.6这是一个非常成熟的组合。HAL库当然也能用但FreeModbus的port层例程基本是寄存器风格用HAL库还要额外处理超时机制和中断回调反而多一层转换。我自己做这个项目时选了标准库原因是可参考的代码多、寄存器操作清晰出问题也好查。有一点要提醒FreeModbus v1.6的官方包里有多个demo目录不要直接复制demo里的main.c而是重点把freemodbus源码目录和port目录拷贝到自己的工程按你的芯片重新实现port层。直接复制demo通常会带上一堆用不到的配置比如ASCII模式的回调、RTOS相关代码反而干扰调试。2. ModbusRTU移植串口与定时器才是核心2.1 需要动哪几个文件FreeModbus的协议栈部分不用改真正需要动手的是两个文件portserial.c和porttimer.c。前者负责串口的收发和RS485方向切换后者负责产生ModbusRTU要求的3.5字符时间。主流程上先调用eMBInit(MB_RTU, ucSlaveAddr, 0, 38400, MB_PAR_EVEN)初始化协议栈参数依次是模式、从站地址、串口编号、波特率、校验方式。然后调用eMBEnable()启动协议栈最后在while(1)里反复调用eMBPoll()。协议栈内部的帧接收、CRC校验、功能码分发都是靠这两个port文件配合完成的。压包里我保留了完整的main.c和中断服务函数你可以直接对照工程看。注意eMBPoll()不是阻塞函数它每次调用只处理当前状态所以主循环里可以同时跑其他业务逻辑但注意不要让其他任务阻塞超过一个字符时间否则串口会把帧丢掉。2.2 串口收发与485方向切换串口驱动的第一个重点是xMBPortSerialInit()要做的事情包含GPIO配置、USART参数设置、中断优先级配置。中断优先级建议设为中等偏低不要高过定时器中断否则帧间隔定时器会被串口中断挤掉。接收中断里每来一个字节就调用一次pxMBFrameCBByteReceived()把字节喂给协议栈代码大概长这样void USART1_IRQHandler(void) { uint8_t ucByte; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ucByte USART_ReceiveData(USART1); USART_ClearITPendingBit(USART1, USART_IT_RXNE); pxMBFrameCBByteReceived(ucByte); } }发送方向要小心RS485的半双工特性。xMBPortSerialEnable(bool xRxEnable, bool xTxEnable)这个函数就是用来切方向的当xTxEnable为TRUE时把485的DE引脚拉高让总线进入发送状态当xRxEnable为TRUE时把DE拉低回到接收状态。方向切换之间最好加一点延时至少等一个字节的发送时间否则最后一个字节还没发完就被切回接收对端会收到一帧残缺数据CRC必然不过。xMBPortSerialPutByte()就是往USART_DR寄存器写一个字节发送中断里调用pxMBFrameCBTransmitterFinished()通知协议栈发完了。这里有个细节TXE中断和TC中断不要混用TXE表示发送寄存器空可以写下一个字节TC表示整个移位寄存器传输完成适合用来做485方向切换的时机。如果只用TXE就切方向最后几个字会有被截断的风险。2.3 定时器里藏着的3.5字符问题ModbusRTU没有帧头帧尾靠静默时间来划分一帧数据这个静默时间就是3.5个字符时间。FreeModbus用一个定时器来做这件事收到任意字节后重置定时器定时器超时说明一帧结束了协议栈开始做CRC校验和地址匹配。这个定时器实现的精确度直接决定通讯稳不稳。定时器装载值的计算公式是T35 11 * 3.5 * 1000000 / baudrate单位是微秒。因为Modbus的串口帧是1起始位8数据位1校验位1停止位一共11位所以1个字符时间等于11位时间。以9600波特率算T35 11 * 3.5 * 1000000 / 9600 ≈ 4009us38400波特率下约1002us。实际操作时把定时器预分频做到1MHz也就是72MHz主频下预分频71然后装载值用上面算出来的数。超时中断里调用pxMBPortCBTimerExpired()进入帧处理状态机。有一个经验值如果你发现通讯偶尔丢帧或者粘连先不要怀疑协议栈把逻辑分析仪挂到RS485的A/B线上看帧间隔再用示波器对比定时器超时时间基本都是装载值算错或者用了8位数据位没加校验位的错误帧结构。2.4 DMA要不要上热词里有人搜“freemodbus dma”我专门说下我的结论在STM32F103上ModbusRTU用逐字节中断完全够用不建议上DMA。Modbus一帧最长256字节115200波特率下也就22ms左右但正常工控通讯通常只有8到20个字节微秒级中断占用根本不影响CPU。FreeModbus原生的RTU状态机是逐字节驱动的硬套DMA反而要自己处理环形缓冲、帧结束判断、半途重传等逻辑改造成本远大于收益。如果你真的遇到高速率、长帧、大流量的极端场景比如把Modbus当采集总线跑那也没必要直接改FreeModbus内核可以做一个独立DMA接收缓冲在定时器判定帧结束后把一帧完整数据一次性喂给协议栈但这已经属于协议栈的自定义扩展了。对绝大多数项目保持官方逻辑最干净也最利于后面升级协议栈版本。3. ModbusTCP落地从MBAP头到W55003.1 MBAP头到底在说什么ModbusTCP和RTU最直观的区别就是没有CRC。TCP是可靠字节流协议链路层保证不会丢字节所以不需要再做数据完整性校验取而代之的是一个7字节的MBAP头。MBAP是Modbus Application Protocol的缩写结构如下表字段长度说明Transaction Identifier2字节事务ID请求和响应对应一致Protocol Identifier2字节协议IDModbus协议固定为0x0000Length2字节后续Unit ID加PDU的总长度Unit Identifier1字节单元ID类似RTU里的从站地址比如要读保持寄存器地址0的1个寄存器整帧的十六进制是00 01 00 00 00 06 01 03 00 00 00 01其中00 01是事务ID00 00是协议ID00 06表示后面还有6个字节01是Unit ID接着就是RTU里去掉CRC后的那部分PDU。理解了这条线你就明白为什么ModbusTCP和RTU的寄存器操作能共用一个应用层了。3.2 用W5500直接把TCP/IP承包了W5500内部集成了完整的TCP/IP协议栈MCU通过SPI访问它的寄存器就可以建立TCP连接。这个方案在F103上非常理想主频72MHz跑上层数据处理绰绰有余底层协议全在W5500内部处理不会占用MCU资源。初始化的流程固定是复位W5500设置网关、子网掩码、MAC地址、本机IP然后打开一个Socket并监听502端口。端口可以换成自定义的但上位机和HMI如果默认走502还是建议保持一致少一层配置麻烦。处理TCP请求时主循环里轮询Socket的接收中断寄存器有数据就recv解析MBAP头拿到了完整PDU后交给上层的寄存器读写处理处理完后再组一个带MBAP头的响应包用send发回去。这里有个关键点TCP是流协议一包recv返回的数据不一定正好是一整帧Modbus请求可能半个帧也可能粘了两帧。所以你的TCP接收侧必须自己维护一个缓冲区和帧长度判断用MBAP头里的Length字段来切帧不能像RTU那样靠静默时间。很多新手第一次调TCP就是在这里栽的跟头。3.3 双协议的寄存器共用方案同时支持RTU和TCP最忌讳的就是各写一套寄存器处理逻辑两边的地址表和数据类型定义一旦不同步现场调试就是灾难。我的做法是抄一层薄薄的应用层隔离对外提供统一的读写接口比如uint16_t app_get_holding_register(uint16_t addr)和void app_set_holding_register(uint16_t addr, uint16_t val)RTU侧的FreeModbus回调函数和TCP侧自己的解析函数都只调这层API不直接碰寄存器数组。好处有两个第一数据一致性有保障两边读到的永远是同一份数组第二新增业务逻辑时只需要改应用层API内部的实现协议侧完全不用动。如果你想让上下位机通过不同协议访问到同一批参数这套结构一定要提前设计好不然后期想加功能会发现四个读写的回调函数里全是重复代码。3.4 主循环怎么安排裸机下的主循环伪代码非常直白while (1) { eMBPoll(); // ModbusRTU协议栈轮询 w5500_poll(); // 网口数据收发与TCP请求处理 user_logic(); // 你自己的业务逻辑 }有个细节值得注意eMBPoll()内部会处理串口收到的帧如果主循环被某个任务卡住太长时间串口上后续到达的字节就会覆盖接收缓冲丢失整帧数据。所以主循环里不要出现长延时、Flash写入、复杂浮点运算这类操作要么拆成状态机要么用FreeRTOS把RTU轮询和TCP轮询分到两个任务里优先级拉高一些。实测下来裸机只要主循环周期控制在1ms以内9600到115200波特率都能稳定跑。4. 寄存器映射与上位机对接4.1 四个数据区如何分配Modbus把数据分成四类线圈0x、离散输入1x、输入寄存器3x、保持寄存器4x。其中保持寄存器是读写型一般用来放控制参数和设定值输入寄存器是只读型放采集到的实时数据线圈和离散输入分别对应开关量输出和输入。我建议在工程里直接建几个数组来映射这些地址区域#define REG_HOLDING_NUM 40 #define REG_INPUT_NUM 20 #define REG_COIL_NUM 8 #define REG_DISCRETE_NUM 8 uint16_t usHoldingRegisters[REG_HOLDING_NUM]; uint16_t usInputRegisters[REG_INPUT_NUM]; uint8_t ucCoils[REG_COIL_NUM]; uint8_t ucDiscreteInputs[REG_DISCRETE_NUM];分配规则可以按业务分段比如保持寄存器地址0到9存设备参数10到29存控制设定值30到39留作触发命令输入寄存器0到9存电压电流采样10到19存状态字。因为RTU和TCP共用这组数组HMI通过网口写入的控制字上位机通过串口读到的数值两边天然一致省去同步逻辑。4.2 台达HMI的映射表怎么设台达DOP系列触屏在新建工程时选择“MODBUS TCP/IP”驱动指定PLC IP和端口502然后设备地址要和STM32端FreeModbus里设置的从站地址一致。接下来就是HMI里的元件地址设置格式一般类似“4x 1”表示保持寄存器40001地址对应Modbus协议里的数据地址0“3x 1”对应输入寄存器30001地址。这个偏移量是Modbus的经典老套路——地址从1开始编号协议包里的地址从0开始换算关系就是HMI地址减1。映射表真正要操心的不是怎么填而是Unit ID和IP别配错。HMI发过来的请求里Unit ID如果和MCU端的从站地址对不上协议栈会直接丢弃表现为HMI一直报“通讯超时”但用Modbus调试助手却能正常读到数据。遇到这种问题先确认HMI设备地址、IP、端口三个参数是否一致。4.3 浮点数寄存器和端序的坑Modbus寄存器是16位的一个32位浮点数要占两个连续寄存器。这里最大的坑是字节顺序有的设备按ABCD顺序即高16位在前、低16位在后有的按CDAB即低16位在前。台达、西门子、以及一些国产上位机的默认设置都不一样经常出现“读出来是个天文数字”或者“32位浮点被截成16位整数”的错乱现象。我的做法是固定使用一种端序并写清楚注释。在STM32端把float拆成两个寄存器的常用写法如下void float_to_modbus_regs(float value, uint16_t *regs) { uint32_t tmp; memcpy(tmp, value, 4); regs[0] (uint16_t)(tmp 16); regs[1] (uint16_t)(tmp 0xFFFF); } float modbus_regs_to_float(uint16_t reg0, uint16_t reg1) { uint32_t tmp ((uint32_t)reg0 16) | reg1; float value; memcpy(value, tmp, 4); return value; }上位机侧如果用的是组态软件直接把浮点字节顺序设置成“高字在前低字在后”就能对上。真有调试不通的就用Modbus Poll手动连续读两个寄存器看数值的十六进制再对照上面的字节序规则一下子就能定位是设备端拆错还是上位机组错。5. 调试工具与疑难杂症实录5.1 工具清单从Modbus Poll到逻辑分析仪做Modbus调试软件工具里我必装的是Modbus Poll和Modbus Slave两个经典小工具。Modbus Poll用来模拟主机定时周期读写从站快速验证RTU和TCP链路Modbus Slave用来模拟从站便于在没有真实设备时验证上位机的配置。串口侧我还会开一个带CRC计算的串口助手比如“野人调试助手”或者“正点原子XCOM”能直接看原始十六进制帧排查地址和CRC问题比用上位机软件高效得多。硬件工具方面逻辑分析仪比示波器更适合日常调帧。把LA的通道挂在RS485的A、B引脚上解码Modbus的UART信号可以直接看到每一帧的起始间隔判断是不是定时器超时时间设错了。网口这边不太方便抓包时可以用W5500的寄存器读回统计信息查连接状态、接收缓冲区剩余空间基本能定位到是TCP层断链还是应用层没回应。5.2 搜热词里那些高频问题我顺手把搜索热词里几个看起来八竿子打不着的坑也解释一下。有人搜“stm32f103定时器输出占空比到不了100”如果你正好用TIM1产生PWM且用到了PA8要注意高级定时器默认主输出是关闭的必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)开启主输出否则占空比永远出不来。还有人搜“PA8引时钟”多半是想把内部时钟输出到PA8的MCO引脚这跟Modbus无关但在硬件设计时容易和TIM1_CH1功能冲突两者共用PA8要重新规划引脚。“stm32f103的map文件”这个问题也常见编译后查看.map文件尾部的Memory Summary就能看到Flash和RAM的使用量FreeModbus本身增加大概3到4KB Flash几百B RAM整体无压力。如果发现内存占用暴增先看是不是在协议栈里开了多个实例或者启用了ASCII模式这些都会额外消耗资源。5.3 稳定性测试经验调试通只能算第一步稳定运行才是过现场验收的关键。我的例行测试方法是用Modbus Poll以100ms周期连续读保持寄存器再开一个窗口写线圈24小时不间断跑观察有没有异常响应、超时、或者帧丢失。RTU和TCP两边同时打压力测的是数据一致性和双协议并发时的寄存器完整性。如果发现TCP读数据的时候和RTU写数据同时发生出现了半新半旧的数据那就需要在应用层读写函数里加临界区保护或者用关中断的方式保证32位变量不会跨操作被撕开。稳定性测试快结束前我会故意给总线上灌垃圾数据比如用另一个串口工具发随机十六进制字节看协议栈会不会崩或死循环。FreeModbus本身对非法帧和错误地址有处理能力但只要主循环不死、定时器不断基本不会出大问题。加一个独立看门狗做最后兜底即使现场出现极端干扰导致协议栈卡死也能自动复位恢复。这套移植做下来我最大的感受是Modbus协议本身不复杂难的是底层时序、硬件配合和工程管理。FreeModbus把协议状态机包圆了你需要替它把串口节拍、网口收数、寄存器一致性这三件事想清楚。如果你也要同时跑RTU和TCP我的建议是先按RTU打通链路再叠加网口别一上来就并行开发。HMI或PLC跟不上你调串口的节奏时可以用一个串口转以太网的小盒子做过渡两边独立验证。最后多留一个心眼给每个寄存器地址写文档命名尽量有意义半年后你回过来维护的时候会感谢自己当时记下的这些备注。本文还有配套的精品资源点击获取