- 1. 一、FreeModbus概述
- 2. 二、协议栈架构与源码解析
- 3. 三、配置文件mbconfig.h详解
- 4. 四、移植详解
- 5. 五、应用层回调函数
- 6. 六、完整的STM32主程序
- 7. 七、Modbus TCP移植要点
- 8. 八、常见问题与排查
- 9. 九、性能优化与资源占用
- 10. 十、学习资源
FreeModbus是一个轻量级、可移植的开源Modbus协议栈,专为嵌入式系统设计,支持Modbus RTU、ASCII和TCP三种协议,可运行在STM32、AVR、MSP430、ESP32等多种MCU平台。本文从协议栈架构、源码解析、移植步骤、回调函数、功能码实现到完整的STM32移植实例,深度解析FreeModbus的工作原理和使用方法。
一、FreeModbus概述
1.1 什么是FreeModbus
FreeModbus是由Christian Walter开发的开源Modbus协议栈,采用BSDLicense,可免费用于商业项目。它实现了Modbusslave(Slave)协议栈,支持RTU、ASCII和TCP三种传输方式,代码量小(约5000行),资源占用低(RAM约2KB,Flash约10KB),非常适合资源受限的嵌入式设备。
1.2 核心特性
- 多协议支持:Modbus RTU、Modbus ASCII、Modbus TCP可单独或组合使用
- 完整功能码:支持01/02/03/04/05/06/07/08/11/12/15/16/17/22/23/24/43等功能码
- 高度可移植:平台相关代码集中在port目录,移植只需实现几个函数
- 资源占用低:最小配置RAM约512字节,Flash约8KB
- 非阻塞设计:基于状态机,不占用CPU等待,可在RTOS或裸机运行
- 可配置:通过mbconfig.h宏开关裁剪功能,按需编译
- BSD开源:商业免费,无GPL传染性
- 成熟稳定:2006年首发,经过多年工业验证
1.3 支持的功能码
| function code | Name | 源文件 | 说明 |
|---|---|---|---|
| 01 | 读线圈 | mbfunccoils.c | 读多个线圈状态 |
| 02 | read discrete inputs | mbfunccoils.c | 读多个离散输入状态 |
| 03 | read holding registers | mbfuncholding.c | 读多个保持寄存器 |
| 04 | read input registers | mbfuncinput.c | 读多个输入寄存器 |
| 05 | write single coil | mbfunccoils.c | 写一个线圈 |
| 06 | write single register | mbfuncholding.c | 写一个保持寄存器 |
| 07 | 读异常状态 | mbfuncother.c | 读8个异常线圈 |
| 08 | 诊断 | mbfuncother.c | 回送测试等 |
| 11 | 获取事件计数器 | mbfuncother.c | 通信事件计数 |
| 12 | 获取事件日志 | mbfuncother.c | 通信事件日志 |
| 15 | write multiple coils | mbfunccoils.c | 批量写线圈 |
| 16 | write multiple registers | mbfuncholding.c | 批量写保持寄存器 |
| 17 | 报告从站ID | mbfuncother.c | 设备标识 |
| 22 | 屏蔽写寄存器 | mbfuncholding.c | 掩码修改寄存器 |
| 23 | read and write multiple registers | mbfuncholding.c | 同时读写 |
二、协议栈架构与源码解析
2.1 目录结构
freemodbus/
├── modbus/ # 协议栈核心(平台无关)
│ ├── include/
│ │ ├── mb.h # 主头文件,API声明
│ │ ├── mbconfig.h # 配置文件(功能裁剪)
│ │ ├── mbframe.h # 帧定义
│ │ ├── mbproto.h # 协议常量
│ │ ├── mbrtu.h # RTU模块
│ │ ├── mbascii.h # ASCII模块
│ │ ├── mbtcp.h # TCP模块
│ │ ├── mbfunccoils.h # 线圈功能码
│ │ ├── mbfuncholding.h # 保持寄存器功能码
│ │ ├── mbfuncinput.h # 输入寄存器功能码
│ │ └── mbfuncother.h # 其他功能码
│ ├── mb.c # 协议栈主逻辑(状态机)
│ ├── mbrtu.c # RTU收发处理
│ ├── mbascii.c # ASCII收发处理
│ ├── mbtcp.c # TCP收发处理
│ ├── mbfunccoils.c # function code01/02/05/15
│ ├── mbfuncholding.c # function code03/06/16/22/23
│ ├── mbfuncinput.c # function code04
│ └── mbfuncother.c # function code07/08/11/12/17
├── port/ # 平台相关代码(需要移植)
│ ├── port.h # 平台接口声明
│ ├── port.c # 平台通用函数
│ ├── portserial.c # 串口操作(RTU/ASCII)
│ ├── porttimer.c # 定时器操作(RTU超时)
│ └── portevent.c # 事件队列
└── demo/ # 示例代码
├── AVR/ # AVRexample
├── STR71x/ # STR71xexample
├── LPC21xx/ # LPC21xxexample
└── WIN32/ # Windows模拟示例
2.2 核心状态机
// mb.c - 协议栈主状态机
typedef enum {
STATE_ENABLED, // 协议栈已启用,等待接收
STATE_DISABLED, // 协议栈已禁用
STATE_NOT_INITIALIZED // 未初始化
} eMBState;
// 接收状态
typedef enum {
STATE_RX_IDLE, // 空闲,等待新帧
STATE_RX_RCV, // 正在接收
STATE_RX_WAIT, // RTU等待超时判断
STATE_RX_ERROR // 接收错误
} eMBRcvState;
// 发送状态
typedef enum {
STATE_TX_IDLE, // 空闲
STATE_TX_XMIT // 正在发送
} eMBSndState;
// 主循环函数(必须周期性调用)
eMBErrorCode eMBPoll(void) {
// 1. 检查是否有事件(收到完整帧)
if (xMBPortEventGet(&eEvent)) {
switch (eEvent) {
case EV_READY:
// 协议栈就绪
break;
case EV_FRAME_RECEIVED:
// 收到完整帧,处理
eMBProcessRxFrame();
break;
case EV_EXECUTE:
// 执行功能码回调
eMBExecuteFunction();
break;
case EV_FRAME_SENT:
// 帧发送完成
break;
}
}
return MB_ENOERR;
}
2.3 RTU帧处理流程
// mbrtu.c - RTU接收流程
// 1. 串口中断:每收到一个字节,调用prvvMBRTUReceive()
// - Reset3.5字符超时定时器
// - 将字节存入接收缓冲区
// - status:STATE_RX_RCV
//
// 2. 定时器中断:3.5字符时间到(RTU帧结束标志)
// - 调用prvvMBRTUTimerT35Expired()
// - 如果收到了数据,触发EV_FRAME_RECEIVEDevent
// - status:STATE_RX_WAIT → 通知主循环
//
// 3. 主循环eMBPoll()收到EV_FRAME_RECEIVED
// - 验证CRC16校验
// - 检查从站地址是否匹配
// - 查找功能码处理函数
// - 调用功能码回调(read/写入应用数据)
// - 构建响应帧
// - 触发EV_EXECUTE → 发送响应
//
// 4. 发送响应
// - Calculate CRC16
// - 串口逐字节发送
// - 发送完成触发EV_FRAME_SENT
三、配置文件mbconfig.h详解
// mbconfig.h - FreeModbus配置文件
// 通过宏开关裁剪功能,最小化资源占用
// ====== 协议选择 ======
#define MB_RTU_ENABLED 1 // EnableRTU
#define MB_ASCII_ENABLED 0 // 禁用ASCII
#define MB_TCP_ENABLED 0 // 禁用TCP
// ====== 功能码选择 ======
#define MB_FUNC_READ_COILS_ENABLED 1 // function code01
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED 1 // function code02
#define MB_FUNC_READ_HOLDING_ENABLED 1 // function code03
#define MB_FUNC_READ_INPUT_ENABLED 1 // function code04
#define MB_FUNC_WRITE_COIL_ENABLED 1 // function code05
#define MB_FUNC_WRITE_SINGLE_ENABLED 1 // function code06
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED 1 // function code15
#define MB_FUNC_WRITE_MULTIPLE_ENABLED 1 // function code16
#define MB_FUNC_READWRITE_MULTIPLE_ENABLED 0 // function code23(禁用)
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED 1 // function code17
#define MB_FUNC_OTHER_REPORT_SLAVEID 1 // 报告从站ID
// ====== 高级功能 ======
#define MB_FUNC_HANDLERS_MAX 16 // 最大功能码处理函数数
#define MB_ASCII_TIMEOUT_SEC 1 // ASCII超时(秒)
#define MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS 0 // ASCII发送前等待
#define MB_TCP_PORT_USE_DEFAULT 502 // TCP默认端口
// ====== debug ======
#define MB_DEBUG_LEVEL 0 // 调试级别 0=Close
// ====== 缓冲区大小 ======
#define MB_PDU_SIZE_MAX 253 // PDU最大长度
#define MB_SER_PDU_SIZE_MAX 256 // serial portPDU最大长度
四、移植详解
4.1 需要实现的平台函数
FreeModbus的移植非常简单,只需在port目录中实现以下平台相关函数:
| 函数 | 文件 | Function |
|---|---|---|
| xMBPortSerialInit | portserial.c | 初始化串口(Baud rate、data bit、check digit) |
| vMBPortSerialClose | portserial.c | Close the serial port |
| vMBPortSerialEnable | portserial.c | 使能/禁用收发中断 |
| xMBPortSerialPutByte | portserial.c | 发送一个字节 |
| xMBPortSerialGetByte | portserial.c | 读取一个字节 |
| xMBPortTimersInit | porttimer.c | 初始化定时器(3.5字符超时) |
| vMBPortTimersClose | porttimer.c | 关闭定时器 |
| vMBPortTimersEnable | porttimer.c | 启动定时器 |
| vMBPortTimersDisable | porttimer.c | 停止定时器 |
| xMBPortEventInit | portevent.c | 初始化事件队列 |
| xMBPortEventPost | portevent.c | 投递事件 |
| xMBPortEventGet | portevent.c | 获取事件 |
| vMBPortClose | port.c | 关闭所有端口资源 |
| xMBPortClose | port.c | 关闭端口 |
4.2 STM32串口移植(portserial.c)
// portserial.c - STM32串口移植实现
#include "port.h"
#include "mb.h"
#include "mbport.h"
#include "stm32f1xx_hal.h"
extern UART_HandleTypeDef huart1; // 使用USART1
static uint8_t rx_byte; // 接收字节缓冲
// 初始化串口
BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate,
UCHAR ucDataBits, eMBParity eParity) {
// 配置串口参数
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.WordLength = (ucDataBits == 8) ? UART_WORDLENGTH_8B : UART_WORDLENGTH_9B;
// check digit(Modbus RTU: 8N1/8E1/8O1)
switch (eParity) {
case MB_PAR_NONE:
huart1.Init.Parity = UART_PARITY_NONE;
break;
case MB_PAR_ODD:
huart1.Init.Parity = UART_PARITY_ODD;
break;
case MB_PAR_EVEN:
huart1.Init.Parity = UART_PARITY_EVEN;
break;
}
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
// 使能接收中断(空闲+receive)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
return TRUE;
}
// 使能收发(xRxEnable=TRUE使能接收,xTxEnable=TRUE使能发送)
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) {
if (xRxEnable) {
// 使能接收中断,开始接收
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
} else {
__HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE);
}
if (xTxEnable) {
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE); // 使能发送空中断
} else {
__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);
}
}
// 发送一个字节(由发送中断调用)
BOOL xMBPortSerialPutByte(CHAR ucByte) {
huart1.Instance->DR = (uint8_t)ucByte; // 直接写数据寄存器
return TRUE;
}
// 读取一个字节(由接收中断调用)
BOOL xMBPortSerialGetByte(CHAR *pucByte) {
*pucByte = rx_byte;
return TRUE;
}
// USART1中断服务函数
void USART1_IRQHandler(void) {
// 接收中断
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
pxMBFrameCBByteReceived(); // 通知协议栈收到字节
}
// 发送空中断
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE)) {
pxMBFrameCBTransmitterEmpty(); // 通知协议栈可以发送下一个字节
}
}
4.3 STM32定时器移植(porttimer.c)
// porttimer.c - STM32定时器移植(RTU 3.5字符超时)
#include "port.h"
#include "mb.h"
#include "mbport.h"
#include "stm32f1xx_hal.h"
extern TIM_HandleTypeDef htim2; // 使用TIM2
// 初始化定时器
// usTim1Timerout50us = 超时时间,单位50微秒
// RTU 3.5字符时间 = 3.5 * (1起始+8数据+check+1停止) / Baud rate
// 例如9600波特率8N1:3.5*11/9600 = 4.01ms = 80 * 50us
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 3600 - 1; // 72MHz/3600 = 20kHz = 50us
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = usTim1Timerout50us - 1; // 超时周期
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
return TRUE;
}
// 启动定时器
void vMBPortTimersEnable(void) {
__HAL_TIM_SET_COUNTER(&htim2, 0); // 清零计数器
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器中断
}
// 停止定时器
void vMBPortTimersDisable(void) {
HAL_TIM_Base_Stop_IT(&htim2);
}
// 定时器中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
pxMBPortCBTimerExpired(); // 通知协议栈超时(RTU帧结束)
}
}
4.4 事件队列移植(portevent.c)
// portevent.c - 事件队列移植(裸机版本)
#include "port.h"
#include "mb.h"
#include "mbport.h"
// 事件队列(简单的单事件,因为FreeModbus是顺序处理的)
static eMBEventType eQueuedEvent;
static BOOL xEventInQueue = FALSE;
// 初始化事件队列
BOOL xMBPortEventInit(void) {
xEventInQueue = FALSE;
return TRUE;
}
// 投递事件(在中断中调用)
BOOL xMBPortEventPost(eMBEventType eEvent) {
eQueuedEvent = eEvent;
xEventInQueue = TRUE;
return TRUE;
}
// 获取事件(在主循环中调用)
BOOL xMBPortEventGet(eMBEventType *eEvent) {
BOOL xEventHappened = FALSE;
if (xEventInQueue) {
*eEvent = eQueuedEvent;
xEventInQueue = FALSE;
xEventHappened = TRUE;
}
return xEventHappened;
}
// 如果使用RTOS,可以用队列替代:
// static QueueHandle_t xEventQueue;
// BOOL xMBPortEventInit(void) {
// xEventQueue = xQueueCreate(1, sizeof(eMBEventType));
// return TRUE;
// }
// BOOL xMBPortEventPost(eMBEventType eEvent) {
// xQueueSendFromISR(xEventQueue, &eEvent, NULL);
// return TRUE;
// }
// BOOL xMBPortEventGet(eMBEventType *eEvent) {
// return xQueueReceive(xEventQueue, eEvent, 0);
// }
五、应用层回调函数
FreeModbus通过回调函数与应用层交互,应用程序需要实现四个回调函数来提供数据存储。
// app_mb.c - 应用层回调函数实现
#include "mb.h"
#include "mbport.h"
// ====== 应用数据存储 ======
#define REG_INPUT_START 1000 // 输入寄存器起始地址
#define REG_INPUT_NREGS 4 // 输入寄存器数量
#define REG_HOLDING_START 2000 // 保持寄存器起始地址
#define REG_HOLDING_NREGS 10 // 保持寄存器数量
#define REG_COILS_START 1 // 线圈起始地址
#define REG_COILS_NCOILS 16 // 线圈数量
#define REG_DISCRETE_START 1 // 离散输入起始地址
#define REG_DISCRETE_NCOILS 8 // 离散输入数量
static USHORT usRegInputBuf[REG_INPUT_NREGS]; // 输入寄存器(只读)
static USHORT usRegHoldingBuf[REG_HOLDING_NREGS]; // 保持寄存器(读写)
static UCHAR ucRegCoilsBuf[REG_COILS_NCOILS / 8 + 1]; // 线圈(读写)
static UCHAR ucRegDiscreteBuf[REG_DISCRETE_NCOILS / 8 + 1]; // 离散输入(只读)
// ====== 回调1:read input registers(function code04) ======
eMBErrorCode eMBRegInputCB(UCHAR *pucRegBuffer, USHORT usAddress,
USHORT usNRegs) {
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
// 检查地址范围
if ((usAddress >= REG_INPUT_START) &&
(usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS)) {
iRegIndex = (int)(usAddress - REG_INPUT_START);
// 复制数据到响应缓冲区(大端模式,高字节在前)
while (usNRegs > 0) {
*pucRegBuffer++ = (UCHAR)(usRegInputBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (UCHAR)(usRegInputBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
} else {
eStatus = MB_ENOREG; // 地址不存在,返回异常码02
}
return eStatus;
}
// ====== 回调2:读/写保持寄存器(function code03/06/16/22/23) ======
eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress,
USHORT usNRegs, eMBRegisterMode eMode) {
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if ((usAddress >= REG_HOLDING_START) &&
(usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS)) {
iRegIndex = (int)(usAddress - REG_HOLDING_START);
switch (eMode) {
case MB_REG_READ: // 读操作(function code03)
while (usNRegs > 0) {
*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE: // 写操作(function code06/16)
while (usNRegs > 0) {
usRegHoldingBuf[iRegIndex] =
((USHORT)*pucRegBuffer++ << 8) | *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
// 写入后可以触发应用动作
// 例如:register2000是设定值,写入后更新PWM输出
break;
}
} else {
eStatus = MB_ENOREG;
}
return eStatus;
}
// ====== 回调3:读/写线圈(function code01/05/15) ======
eMBErrorCode eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress,
USHORT usNCoils, eMBRegisterMode eMode) {
eMBErrorCode eStatus = MB_ENOERR;
int iBitIndex;
if ((usAddress >= REG_COILS_START) &&
(usAddress + usNCoils <= REG_COILS_START + REG_COILS_NCOILS)) {
iBitIndex = (int)(usAddress - REG_COILS_START);
if (eMode == MB_REG_READ) {
// 读线圈:打包成字节(每8个线圈一个字节)
while (usNCoils > 0) {
UCHAR ucResult = 0;
for (int i = 0; i < 8 && usNCoils > 0; i++) {
if (ucRegCoilsBuf[iBitIndex / 8] & (1 << (iBitIndex % 8))) {
ucResult |= (1 << i);
}
iBitIndex++;
usNCoils--;
}
*pucRegBuffer++ = ucResult;
}
} else {
// 写线圈
while (usNCoils > 0) {
UCHAR ucBits = *pucRegBuffer++;
for (int i = 0; i < 8 && usNCoils > 0; i++) {
if (ucBits & (1 << i)) {
ucRegCoilsBuf[iBitIndex / 8] |= (1 << (iBitIndex % 8));
} else {
ucRegCoilsBuf[iBitIndex / 8] &= ~(1 << (iBitIndex % 8));
}
iBitIndex++;
usNCoils--;
}
}
}
} else {
eStatus = MB_ENOREG;
}
return eStatus;
}
// ====== 回调4:read discrete inputs(function code02) ======
eMBErrorCode eMBRegDiscreteCB(UCHAR *pucRegBuffer, USHORT usAddress,
USHORT usNDiscrete) {
// 与读线圈类似,但只能读不能写
eMBErrorCode eStatus = MB_ENOERR;
int iBitIndex;
if ((usAddress >= REG_DISCRETE_START) &&
(usAddress + usNDiscrete <= REG_DISCRETE_START + REG_DISCRETE_NCOILS)) {
iBitIndex = (int)(usAddress - REG_DISCRETE_START);
while (usNDiscrete > 0) {
UCHAR ucResult = 0;
for (int i = 0; i < 8 && usNDiscrete > 0; i++) {
if (ucRegDiscreteBuf[iBitIndex / 8] & (1 << (iBitIndex % 8))) {
ucResult |= (1 << i);
}
iBitIndex++;
usNDiscrete--;
}
*pucRegBuffer++ = ucResult;
}
} else {
eStatus = MB_ENOREG;
}
return eStatus;
}
六、完整的STM32主程序
// main.c - FreeModbus STM32完整主程序
#include "stm32f1xx_hal.h"
#include "mb.h"
#include "mbport.h"
// 外部变量(应用数据)
extern USHORT usRegInputBuf[];
extern USHORT usRegHoldingBuf[];
extern UCHAR ucRegCoilsBuf[];
extern UCHAR ucRegDiscreteBuf[];
// 函数声明
void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_USART1_UART_Init(void);
void MX_TIM2_Init(void);
void UpdateApplicationData(void);
int main(void) {
// HAL库初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
// ====== 初始化FreeModbus RTUslave ======
// parameter:slave address=1, serial port=0, Baud rate=9600, check=无校验
eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE);
// 启用协议栈
eMBEnable();
printf("FreeModbus RTU从站已启动\r\n");
printf("slave address: 1, Baud rate: 9600, format: 8N1\r\n");
// 主循环
while (1) {
// ====== 必须周期性调用eMBPoll() ======
// 这是FreeModbus的核心,处理所有协议事件
eMBPoll();
// ====== 应用逻辑:更新传感器数据 ======
UpdateApplicationData();
// 其他应用任务...
HAL_Delay(10);
}
}
// 更新应用数据(模拟传感器采集)
void UpdateApplicationData(void) {
static uint32_t last_update = 0;
uint32_t now = HAL_GetTick();
// 每100ms更新一次
if (now - last_update >= 100) {
last_update = now;
// 输入寄存器(只读):模拟传感器
usRegInputBuf[0] = 250 + (now / 100) % 10; // temperature 25.0-25.9°C
usRegInputBuf[1] = 650 + (now / 150) % 20; // 湿度 65.0-66.9%
usRegInputBuf[2] = 2200; // 电压 220.0V
usRegInputBuf[3] = 135; // 电流 1.35A
// 离散输入:模拟开关状态
ucRegDiscreteBuf[0] = (now / 500) & 0x01; // 每秒翻转
}
// 保持寄存器的写入由Modbus主站触发
// 应用可以根据保持寄存器的值执行动作
// 例如:usRegHoldingBuf[0]是PWM设定值
// __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, usRegHoldingBuf[0]);
}
// 串口发送printf重定向(调试用)
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
return ch;
}
七、Modbus TCP移植要点
如果需要支持Modbus TCP,除了串口和定时器移植外,还需要实现TCP端口层。
// porttcp.c - Modbus TCP端口层(使用lwIP)
#include "port.h"
#include "mb.h"
#include "mbport.h"
#include "lwip/tcp.h"
static struct tcp_pcb *pxListenPCB = NULL;
static struct tcp_pcb *pxClientPCB = NULL;
static UCHAR pucTCPBuffer[MB_TCP_PDU_SIZE_MAX];
// 初始化TCP监听
BOOL xMBTCPPortInit(USHORT usTCPPort) {
pxListenPCB = tcp_new();
if (pxListenPCB) {
tcp_bind(pxListenPCB, IP_ADDR_ANY, usTCPPort);
pxListenPCB = tcp_listen(pxListenPCB);
tcp_accept(pxListenPCB, prvxMBTCPAccept);
return TRUE;
}
return FALSE;
}
// 接受新连接
static err_t prvxMBTCPAccept(void *arg, struct tcp_pcb *pcb, err_t err) {
if (pxClientPCB == NULL) {
pxClientPCB = pcb;
tcp_recv(pcb, prvxMBTCPReceive);
tcp_err(pcb, prvxMBTCPError);
} else {
tcp_close(pcb); // 只允许一个连接
}
return ERR_OK;
}
// 接收数据
static err_t prvxMBTCPReceive(void *arg, struct tcp_pcb *pcb,
struct pbuf *p, err_t err) {
if (p) {
// 复制数据到缓冲区
UCHAR *pucData = p->payload;
USHORT usLength = p->len;
// 验证MBAP Head(事务ID、ProtocolID=0、Length、单元ID)
if (usLength >= 7 && pucData[2] == 0 && pucData[3] == 0) {
// 通知协议栈收到帧
pxMBFrameCBByteReceived(); // 简化处理
}
tcp_recved(pcb, p->tot_len);
pbuf_free(p);
}
return ERR_OK;
}
// 发送响应
BOOL xMBTCPPortSendResponse(UCHAR *pucMBTCPFrame, USHORT usLength) {
if (pxClientPCB) {
tcp_write(pxClientPCB, pucMBTCPFrame, usLength, TCP_WRITE_FLAG_COPY);
tcp_output(pxClientPCB);
return TRUE;
}
return FALSE;
}
八、常见问题与排查
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 主站无响应 | eMBPoll()未调用或调用不及时 | 确保主循环中频繁调用eMBPoll(),不要长时间阻塞 |
| RTU通信不稳定 | 3.5字符超时定时器不准 | 检查定时器时钟和预分频,确保50us精度 |
| CRC checkError | 波特率误差大或串口配置错 | 核对波特率/data bit/check digit,使用外部晶振 |
| 地址不匹配 | 从站地址设置错误 | eMBInit()的第二个参数是从站地址 |
| 读寄存器返回异常码02 | 地址超出回调函数范围 | 检查回调函数中的地址范围判断 |
| 多从站冲突 | 总线上从站地址重复 | 确保每个从站地址唯一(1-247) |
| 发送不完整 | 发送中断未正确使能 | 检查vMBPortSerialEnable()中TX中断使能 |
| TCP连接后断开 | MBAP Head解析错误 | 检查协议ID是否为0,长度字段是否正确 |
| 内存不足 | 缓冲区太大 | 减小MB_PDU_SIZE_MAX,裁剪不需要的功能码 |
| ASCII模式不工作 | 未启用MB_ASCII_ENABLED | mbconfig.h中设置MB_ASCII_ENABLED=1 |
九、性能优化与资源占用
- 裁剪功能码:只启用需要的功能码,每个功能码约节省200-500字节Flash
- 减小PDU缓冲区:MB_PDU_SIZE_MAX默认253,如果设备寄存器少可减小
- 单协议模式:只启用RTU或TCP,不编译不需要的协议模块
- 优化中断优先级:串口中断优先级高于定时器,确保字节不丢失
- eMBPoll调用频率:建议至少每10ms调用一次,避免事件积压
- 寄存器映射优化:回调函数中避免复杂计算,直接数组索引
- 典型资源占用:最小RTU配置 Flash~8KB, RAM~512B;完整功能 Flash~15KB, RAM~2KB
十、学习资源
- 官方网站:https://www.freemodbus.org/
- GitHubsource code:https://github.com/cwalter-at/freemodbus
- Modbus协议规范:https://modbus.org/specs.php
- STM32 HALDocument:https://www.st.com/
- FreeMODBUS TCP扩展:https://github.com/htl-weiz/freemodbus-tcp
FreeModbus作为嵌入式领域最经典的开源Modbus从站协议栈,以其精简的代码、清晰的架构和极低的资源占用,成为MCU设备接入Modbus总线的首选方案。本文提供的从架构解析、源码解读到STM32完整移植的详细教程,可帮助开发者快速将FreeModbus集成到自己的嵌入式项目中。关键是正确实现串口、定时器和事件队列三个平台层,并在应用回调函数中做好寄存器地址映射。