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 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 | 写一1つのコイル |
| 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) {
// リードコイルを読む:パッキング成バイト(每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_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集成到自己的エンベデッド·イン项目中。关键是正确実装シリアルポート、タイマータイマー。和事件队列三个プラットフォーム层,并在应用回调函数中做好レジスター·アドレスマッピング。
VIP专属:FreeModbusプロトコル栈STM32コードパッケージの移植
FreeModbusコアアーキテクチャ解析、portレイヤーの完全なマイグレーション、eMBInit/eMBPoll回调、レジスタ読み书き込み例。
Activate VIP即可下载完全代码,同时解锁 30+ 工程実践パッケージの内容:スクリプトのデバッグ、クイックチェック·テーブル、项目模板、トラブルシューティング案例……
前往VIP资料库下载 → 月费仅9.9元 / 年费199元