FreeModbus是一个轻量级、可移植的오픈소스Modbus프로토콜栈,专为嵌入式系统设计,支持Modbus RTU、ASCII和TCP三种프로토콜,可运行在STM32、AVR、MSP430、ESP32等多种MCU平台。本文从프로토콜栈架构、源码분석、移植步骤、回调函数、기능 코드实现到完整的STM32移植实例,深度분석FreeModbus的工作원리和使用方法。
一、FreeModbus개요
1.1 什么是FreeModbus
FreeModbus是由Christian Walter개발的오픈소스Modbus프로토콜栈,采用BSD许可证,可무료用于商业项目。它实现了Modbus슬레이브(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 支持的기능 코드
| 기능 코드 | 名称 | 源文件 | 설명 |
|---|---|---|---|
| 01 | 读코일 | mbfunccoils.c | 读多个코일 상태 |
| 02 | 读이산 입력 | mbfunccoils.c | 读多个이산 입력状态 |
| 03 | 读유지 레지스터 | mbfuncholding.c | 读多个유지 레지스터 |
| 04 | 读입력 레지스터 | mbfuncinput.c | 读多个입력 레지스터 |
| 05 | 写单个코일 | mbfunccoils.c | 写一个코일 |
| 06 | 写单个레지스터 | mbfuncholding.c | 写一个유지 레지스터 |
| 07 | 读异常状态 | mbfuncother.c | 读8个异常코일 |
| 08 | 诊断 | mbfuncother.c | 回送测试等 |
| 11 | 获取事件计数器 | mbfuncother.c | 통신事件计数 |
| 12 | 获取事件로그 | mbfuncother.c | 통신事件로그 |
| 15 | 写多个코일 | mbfunccoils.c | 批量写코일 |
| 16 | 写多个레지스터 | mbfuncholding.c | 批量写유지 레지스터 |
| 17 | 보고서슬레이브ID | mbfuncother.c | 设备标识 |
| 22 | 屏蔽写레지스터 | mbfuncholding.c | 掩码修改레지스터 |
| 23 | 读写多个레지스터 | 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 # 기능 코드01/02/05/15
│ ├── mbfuncholding.c # 기능 코드03/06/16/22/23
│ ├── mbfuncinput.c # 기능 코드04
│ └── mbfuncother.c # 기능 코드07/08/11/12/17
├── port/ # 平台相关代码(需要移植)
│ ├── port.h # 平台인터페이스声明
│ ├── port.c # 平台通用函数
│ ├── portserial.c # 시리얼 포트操作(RTU/ASCII)
│ ├── porttimer.c # 定时器操作(RTU超时)
│ └── portevent.c # 事件队列
└── demo/ # 예제代码
├── AVR/ # AVR예제
├── STR71x/ # STR71x예제
├── LPC21xx/ # LPC21xx예제
└── 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;
// 主循环函数(必须周期性调用)
eMB오답Code eMBPoll(void) {
// 1. 检查是否有事件(收到전체 프레임)
if (xMBPortEventGet(&eEvent)) {
switch (eEvent) {
case EV_READY:
// 프로토콜栈就绪
break;
case EV_FRAME_RECEIVED:
// 收到전체 프레임,处理
eMBProcessRxFrame();
break;
case EV_EXECUTE:
// 执行기능 코드回调
eMBExecute기능();
break;
case EV_FRAME_SENT:
// 帧보내기完成
break;
}
}
return MB_ENOERR;
}
2.3 RTU帧处理流程
// mbrtu.c - RTU수신流程
// 1. 시리얼 포트中断:每收到一个바이트,调用prvvMBRTUReceive()
// - 重置3.5문자超时定时器
// - 将바이트存入수신缓冲区
// - 状态:STATE_RX_RCV
//
// 2. 定时器中断:3.5문자时间到(RTU帧종료标志)
// - 调用prvvMBRTUTimerT35Expired()
// - 如果收到了데이터,触发EV_FRAME_RECEIVED事件
// - 状态:STATE_RX_WAIT → 通知主循环
//
// 3. 主循环eMBPoll()收到EV_FRAME_RECEIVED
// - 验证CRC16체크섬
// - 检查슬레이브 주소是否일치
// - 查找기능 코드处理函数
// - 调用기능 코드回调(읽기/쓰기적용데이터)
// - 构建响应帧
// - 触发EV_EXECUTE → 보내기响应
//
// 4. 보내기响应
// - 계산CRC16
// - 시리얼 포트逐바이트보내기
// - 보내기完成触发EV_FRAME_SENT
三、설정文件mbconfig.h详解
// mbconfig.h - FreeModbus설정文件
// 通过宏开关裁剪功能,最小化자료占用
// ====== 프로토콜选择 ======
#define MB_RTU_ENABLED 1 // 활성화RTU
#define MB_ASCII_ENABLED 0 // 禁用ASCII
#define MB_TCP_ENABLED 0 // 禁用TCP
// ====== 기능 코드选择 ======
#define MB_FUNC_READ_COILS_ENABLED 1 // 기능 코드01
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED 1 // 기능 코드02
#define MB_FUNC_READ_HOLDING_ENABLED 1 // 기능 코드03
#define MB_FUNC_READ_INPUT_ENABLED 1 // 기능 코드04
#define MB_FUNC_WRITE_COIL_ENABLED 1 // 기능 코드05
#define MB_FUNC_WRITE_SINGLE_ENABLED 1 // 기능 코드06
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED 1 // 기능 코드15
#define MB_FUNC_WRITE_MULTIPLE_ENABLED 1 // 기능 코드16
#define MB_FUNC_READWRITE_MULTIPLE_ENABLED 0 // 기능 코드23(禁用)
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED 1 // 기능 코드17
#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默认포트
// ====== 디버깅 ======
#define MB_DEBUG_LEVEL 0 // 디버깅级别 0=닫기
// ====== 缓冲区大小 ======
#define MB_PDU_SIZE_MAX 253 // PDU最大길이
#define MB_SER_PDU_SIZE_MAX 256 // 시리얼 포트PDU最大길이
四、移植详解
4.1 需要实现的平台函数
FreeModbus的移植非常简单,只需在port目录中实现以下平台相关函数:
| 函数 | 文件 | 功能 |
|---|---|---|
| xMBPortSerialInit | portserial.c | 初始化시리얼 포트(통신 속도、데이터 비트、패리티) |
| vMBPortSerial닫기 | portserial.c | 닫기시리얼 포트 |
| vMBPortSerialEnable | portserial.c | 使能/禁用收发中断 |
| xMBPortSerialPutByte | portserial.c | 보내기一个바이트 |
| xMBPortSerialGetByte | portserial.c | 읽기一个바이트 |
| xMBPortTimersInit | porttimer.c | 初始化定时器(3.5문자超时) |
| vMBPortTimers닫기 | porttimer.c | 닫기定时器 |
| vMBPortTimersEnable | porttimer.c | 启动定时器 |
| vMBPortTimersDisable | porttimer.c | 정지定时器 |
| xMBPortEventInit | portevent.c | 初始化事件队列 |
| xMBPortEventPost | portevent.c | 投递事件 |
| xMBPortEventGet | portevent.c | 获取事件 |
| vMBPort닫기 | port.c | 닫기所有포트자료 |
| xMBPort닫기 | 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 uc데이터Bits, eMBParity eParity) {
// 설정시리얼 포트 매개변수
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.WordLength = (uc데이터Bits == 8) ? UART_WORDLENGTH_8B : UART_WORDLENGTH_9B;
// 패리티(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);
// 使能수신中断(空闲+수신)
__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데이터+체크섬+1정지) / 통신 속도
// 例如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_PeriodElapsed호명back(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:读입력 레지스터(기능 코드04) ======
eMB오답Code eMBRegInputCB(UCHAR *pucRegBuffer, USHORT us주소,
USHORT usNRegs) {
eMB오답Code eStatus = MB_ENOERR;
int iRegIndex;
// 检查주소范围
if ((us주소 >= REG_INPUT_START) &&
(us주소 + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS)) {
iRegIndex = (int)(us주소 - 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:读/写유지 레지스터(기능 코드03/06/16/22/23) ======
eMB오답Code eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT us주소,
USHORT usNRegs, eMB회원가입Mode eMode) {
eMB오답Code eStatus = MB_ENOERR;
int iRegIndex;
if ((us주소 >= REG_HOLDING_START) &&
(us주소 + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS)) {
iRegIndex = (int)(us주소 - REG_HOLDING_START);
switch (eMode) {
case MB_REG_READ: // 读操作(기능 코드03)
while (usNRegs > 0) {
*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE: // 写操作(기능 코드06/16)
while (usNRegs > 0) {
usRegHoldingBuf[iRegIndex] =
((USHORT)*pucRegBuffer++ << 8) | *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
// 쓰기后可以触发적용动作
// 例如:레지스터2000是设定值,쓰기后更新PWM출력
break;
}
} else {
eStatus = MB_ENOREG;
}
return eStatus;
}
// ====== 回调3:读/写코일(기능 코드01/05/15) ======
eMB오답Code eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT us주소,
USHORT usNCoils, eMB회원가입Mode eMode) {
eMB오답Code eStatus = MB_ENOERR;
int iBitIndex;
if ((us주소 >= REG_COILS_START) &&
(us주소 + usNCoils <= REG_COILS_START + REG_COILS_NCOILS)) {
iBitIndex = (int)(us주소 - 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:读이산 입력(기능 코드02) ======
eMB오답Code eMBRegDiscreteCB(UCHAR *pucRegBuffer, USHORT us주소,
USHORT usNDiscrete) {
// 与读코일类似,但只能读不能写
eMB오답Code eStatus = MB_ENOERR;
int iBitIndex;
if ((us주소 >= REG_DISCRETE_START) &&
(us주소 + usNDiscrete <= REG_DISCRETE_START + REG_DISCRETE_NCOILS)) {
iBitIndex = (int)(us주소 - 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 UpdateApplication데이터(void);
int main(void) {
// HAL库初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
// ====== 初始化FreeModbus RTU슬레이브 ======
// 매개변수:슬레이브 주소=1, 시리얼 포트=0, 통신 속도=9600, 체크섬=패리티 없음
eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE);
// 활성화프로토콜栈
eMBEnable();
printf("FreeModbus RTU슬레이브已启动\r\n");
printf("슬레이브 주소: 1, 통신 속도: 9600, 형식: 8N1\r\n");
// 主循环
while (1) {
// ====== 必须周期性调用eMBPoll() ======
// 这是FreeModbus的核心,处理所有프로토콜事件
eMBPoll();
// ====== 적용逻辑:更新센서데이터 ======
UpdateApplication데이터();
// 其他적용任务...
HAL_Delay(10);
}
}
// 更新적용데이터(模拟센서采集)
void UpdateApplication데이터(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; // 온도 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, prvxMBTCP오답);
} 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 *puc데이터 = p->payload;
USHORT usLength = p->len;
// 验证MBAP头(事务ID、프로토콜ID=0、길이、单元ID)
if (usLength >= 7 && puc데이터[2] == 0 && puc데이터[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 검증错误 | 통신 속도误差大或시리얼 포트 설정错 | 核对통신 속도/데이터 비트/패리티,使用外部晶振 |
| 주소不일치 | 슬레이브 주소设置错误 | eMBInit()的第二个매개변수是슬레이브 주소 |
| 读레지스터返回异常码02 | 주소超出回调函数范围 | 检查回调函数中的주소范围判断 |
| 多슬레이브冲突 | 总线上슬레이브 주소重复 | 确保每个슬레이브 주소唯一(1-247) |
| 보내기不完整 | 보내기中断未正确使能 | 检查vMBPortSerialEnable()中TX中断使能 |
| TCP연결后断开 | MBAP头분석错误 | 检查프로토콜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/
- GitHub源码:https://github.com/cwalter-at/freemodbus
- Modbus프로토콜사양:https://modbus.org/specs.php
- STM32 HAL문서: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回调、레지스터读写적용예제。
开通VIP即可下载完整代码,同时解锁 30+ 엔지니어링实战자료包:디버깅스크립트、速查表、项目模板、排查案例……
前往VIP자료库下载 → 月费仅9.9元 / 年费199元