FreeModbus协议栈深度解析与STM32移植完整教程:RTU/TCP从站开发 回调函数与源码详解

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

🌐 This page is not yet available in English. Showing the Chinese version. Back to Chinese page.
本文目录
  1. 1. 一、FreeModbus概述
  2. 2. 二、协议栈架构与源码解析
  3. 3. 三、配置文件mbconfig.h详解
  4. 4. 四、移植详解
  5. 5. 五、应用层回调函数
  6. 6. 六、完整的STM32主程序
  7. 7. 七、Modbus TCP移植要点
  8. 8. 八、常见问题与排查
  9. 9. 九、性能优化与资源占用
  10. 10. 十、学习资源
  11. 11. VIP专属:FreeModbus协议栈STM32移植代码包

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 codeName源文件说明
01读线圈mbfunccoils.c读多个线圈状态
02read discrete inputsmbfunccoils.c读多个离散输入状态
03read holding registersmbfuncholding.c读多个保持寄存器
04read input registersmbfuncinput.c读多个输入寄存器
05write single coilmbfunccoils.c写一个线圈
06write single registermbfuncholding.c写一个保持寄存器
07读异常状态mbfuncother.c读8个异常线圈
08诊断mbfuncother.c回送测试等
11获取事件计数器mbfuncother.c通信事件计数
12获取事件日志mbfuncother.c通信事件日志
15write multiple coilsmbfunccoils.c批量写线圈
16write multiple registersmbfuncholding.c批量写保持寄存器
17报告从站IDmbfuncother.c设备标识
22屏蔽写寄存器mbfuncholding.c掩码修改寄存器
23read and write multiple registersmbfuncholding.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
xMBPortSerialInitportserial.c初始化串口(Baud rate、data bit、check digit)
vMBPortSerialCloseportserial.cClose the serial port
vMBPortSerialEnableportserial.c使能/禁用收发中断
xMBPortSerialPutByteportserial.c发送一个字节
xMBPortSerialGetByteportserial.c读取一个字节
xMBPortTimersInitporttimer.c初始化定时器(3.5字符超时)
vMBPortTimersCloseporttimer.c关闭定时器
vMBPortTimersEnableporttimer.c启动定时器
vMBPortTimersDisableporttimer.c停止定时器
xMBPortEventInitportevent.c初始化事件队列
xMBPortEventPostportevent.c投递事件
xMBPortEventGetportevent.c获取事件
vMBPortCloseport.c关闭所有端口资源
xMBPortCloseport.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_ENABLEDmbconfig.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集成到自己的嵌入式项目中。关键是正确实现串口、定时器和事件队列三个平台层,并在应用回调函数中做好寄存器地址映射。

📦

VIP专属:FreeModbus协议栈STM32移植代码包

FreeModbus核心架构解析、port层完整移植、eMBInit/eMBPoll回调、寄存器读写应用示例。

Activate VIP即可下载完整代码,同时解锁 30+ 工程实战资料包:调试脚本、速查表、项目模板、排查案例……

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
技术术语(共 11 个)—— Click to Expand
Modbus RTU基于串行链路的ModbusProtocol,使用二进制编码和CRC check
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
Modbus ASCII使用ASCII字符传输的ModbusProtocol,以冒号开头、CR/LF结尾
function codeModbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
registerModbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
CRC check循环冗余校验,用于检测数据传输中的错误
Baud rate串行通信每秒传输符号数,Modbus RTU常用9600/19200
serial port计算机与外部设备进行串行通信的物理接口
Sensors将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— Click to Expand
来源 Modbus Chinese Network(modbus.cn) —— China leadingModbuscommunication protocol technical community Category Modbus programming development 字数 15310 字 · 阅读约 39 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53014.html
Recommended Tool: Modbus Debug Assistant WeChat Mini Program
Modbus Chinese Network官方推出的Modbus debugging tool,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 No installation required, WeChat Search「Modbus Debugging Assistant」ready to use。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support