FreeModbusプロトコル栈深度解析与STM32移植完全チュートリアル:RTU/TCPスレーブ开发 回调函数与源码详解

freeFree Technical Resource

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

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和TCP3つの伝送方式,代码量小(约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写一1つのコイル
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) {
            // リードコイルを読む:パッキング成バイト(每81つのコイル一バイト単位。)
            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元
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