文章目录
- ✅ **CubeMX配置**
- 1. UART配置(RS485通信)
- 2. Timer配置(RTU字符间隔检测)
- 3. GPIO配置(RS485方向控制)
- ✅ **STM32F103 + RS485 + FreeModbus RTU 配置概览**
- **1️⃣ CubeMX硬件配置**
- **2️⃣ FreeModbus源码文件**
- **3️⃣ 配置文件修改**
- **4️⃣ 移植层代码编写**
- **5️⃣ 主程序配置**
- **6️⃣ 中断处理配置**
- **7️⃣ 工程配置**
- **📋 配置检查清单**
- ✅ **STM32F103 + HAL库 + CubeMX的FreeModbus RTU实现方案**
- 必须保留的源码文件
- ✅ **配置文件**
- mbconfig.h
- port.h
- ✅ **移植层实现**
- portserial.c
- porttimer.c
- portevent.c
- ✅ **中断处理函数**
- stm32f1xx_it.c中添加
- ✅ **主程序实现**
- main.c
- ✅ **工程配置要点**
- 1. 包含路径添加
- 2. 源文件添加到工程
- 3. 编译宏定义(可选)
- 🔄 **RS485方向控制原理**
- **RS485是半双工通信**
- 📡 **PA1 (RS485_DE) 的作用**
- **DE/RE引脚说明**
- **方向控制逻辑**
- 🔄 **完整通信流程**
- **1. STM32发送数据给从站**
- **2. STM32接收从站返回的数据**
- 📋 **实际工作时序**
- **Modbus RTU主从通信过程**
- **举例:读取保持寄存器**
- ⚡ **关键要点**
- **✅ 能双向通信**
- **🎯 方向控制的意义**
- **📝 代码中的自动切换**
缩写/单词 | 全称 | 含义 |
---|---|---|
ModBus | Modicon Bus | 最早由 Modicon(现施耐德) 开发的工业通信协议 |
RTU | Remote Terminal Unit | 远程终端单元,指一种紧凑、二进制的传输格式(区别于 ASCII 模式) |
下载freemodbus,解压。
名称 | 作用说明 |
---|---|
modbus | 核心源码目录,包含 FreeModbus 协议栈的所有 .c/.h 文件(如 mb.c , mbport.h , mbrtu.c 等)。 |
demo | 示例工程目录,包含 FreeModbus 在不同平台(如 AVR、Win32、STR71x)上的完整示例项目。你可以参考这些例子移植到 STM32。 |
doc | 文档目录,包含协议栈的说明文档(如 modbus.txt 、demo.txt ),但内容较旧。 |
tools | 辅助工具目录,包含一些生成 CRC 表的小工具(如 crcgen.py ),通常用不到。 |
名称 | 作用说明 |
---|---|
__MACOSX | macOS 压缩时自动生成的隐藏文件夹,可以删除,对代码无影响。 |
Changelog.txt | FreeModbus 的版本更新日志。 |
gpl.txt | GNU GPL 开源协议(FreeModbus 采用 GPL v3 授权)。 |
lgpl.txt | GNU LGPL 协议(部分文件可能用 LGPL 授权)。 |
bsd.txt | BSD 协议(某些平台移植代码可能用 BSD 授权)。 |
📁 目录说明
目录名 | 作用说明 |
---|---|
ascii | Modbus ASCII 模式的源码(基于文本的通信方式,用得少)。 |
rtu | Modbus RTU 模式的源码(二进制、高效,最常用)。 |
tcp | Modbus TCP 模式的源码(基于以太网 TCP/IP,用于网口通信)。 |
functions | 各种 Modbus 功能码的实现(如 0x03 读保持寄存器、0x06 写单个寄存器等)。 |
include | 公共头文件(如 mb.h , mbport.h 等)。 |
mb.c | 主协议栈入口文件(初始化、轮询、状态机等)。 |
使用场景 | 需要哪些目录 |
---|---|
串口 RTU 从站 | rtu + functions + include + mb.c |
串口 ASCII 从站 | ascii + functions + include + mb.c |
以太网 TCP 从站 | tcp + functions + include + mb.c |
✅ 关于 tcp
目录
- 作用:实现 Modbus TCP 协议,用于 以太网通信(如通过 W5500、ENC28J60 等模块)。
- 可以不用:
如果你只用 串口(RS-485/RS-232)通信,完全可以不用tcp
目录,甚至可以从工程中移除,节省空间。
✅ CubeMX配置
1. UART配置(RS485通信)
Connectivity -> USART1:
├── Mode: Asynchronous
├── Baud Rate: 9600 (或其他)
├── Word Length: 8 Bits
├── Parity: None
├── Stop Bits: 1
├── Data Direction: Receive and Transmit
└── NVIC Settings: ✅ USART1 global interrupt
2. Timer配置(RTU字符间隔检测)
Timers -> TIM2:
├── Clock Source: Internal Clock
├── Prescaler: 71 (得到1MHz时钟)
├── Counter Period: 1750 (3.5字符时间@9600bps)
└── NVIC Settings: ✅ TIM2 global interrupt
定时器需要配置定时器的中断
字符间隔检测:
作用:
- 检测Modbus RTU数据帧之间的静默期(3.5字符时间)
- 当接收到数据后,如果在3.5字符时间内没有新数据到达,则认为一帧数据接收完成
- 用于帧同步和数据完整性判断
工作原理:
数据帧: [地址][功能码][数据][CRC] ----静默期(≥3.5字符)---- [下一帧...]↑定时器检测这段时间
时钟频率计算:
// STM32F103时钟配置 系统时钟: 72MHz APB1时钟: 36MHz (通常是SYSCLK/2) TIM2时钟: 72MHz (当APB1预分频≠1时,定时器时钟×2)// 定时器频率计算 定时器频率 = TIM2时钟 / (Prescaler + 1) 定时器频率 = 72MHz / (71 + 1) = 1MHz 每个计数 = 1μs
字符时间计算:
// 以9600bps为例 波特率 = 9600 bps 每位时间 = 1/9600 ≈ 104.17μs 每字符位数 = 10位 (1起始位 + 8数据位 + 1停止位) 每字符时间 = 10 × 104.17μs = 1041.7μs 3.5字符时间 = 3.5 × 1041.7μs ≈ 3646μs// 对应的计数值 Counter Period = 3646 (定时器频率1MHz时)
// 反推波特率 1750μs / 3.5 = 500μs (每字符时间) 500μs / 10位 = 50μs (每位时间) 波特率 = 1/50μs = 20000 bps// 或者可能是针对19200bps 19200bps每位时间 = 1/19200 ≈ 52.08μs 每字符时间 = 10 × 52.08μs = 520.8μs 3.5字符时间 = 3.5 × 520.8μs ≈ 1823μs ```
Modbus RTU常用两种格式:
- 8-N-1: 8数据位 + 无校验 + 1停止位 = 10位/字符
- 8-E-1/8-O-1: 8数据位 + 奇偶校验 + 1停止位 = 11位/字符
// 9600bps, 8-N-1格式 (10位/字符)
每位时间 = 1/9600 = 104.167 μs
每字符时间 = 104.167 × 10 = 1041.67 μs
3.5字符时间 = 1041.67 × 3.5 = 3645.83 μs
定时器计数值 = 3646// 9600bps, 8-E-1格式 (11位/字符)
每位时间 = 1/9600 = 104.167 μs
每字符时间 = 104.167 × 11 = 1145.83 μs
3.5字符时间 = 1145.83 × 3.5 = 4010.42 μs
定时器计数值 = 4010
// 根据Modbus标准,波特率 > 19200 时使用固定值
// 不管是8-N-1还是8-E-1格式,都使用固定的1.75ms定时器计数值 = 1750 μs (固定值)
# 9600
Timers -> TIM2:
├── Clock Source: Internal Clock
├── Prescaler: 71
├── Counter Period: 3646 (8-N-1) 或 4010 (8-E-1)
├── Counter Mode: Up
└── NVIC Settings: ✅ TIM2 global interrupt
#115200
Timers -> TIM2:
├── Clock Source: Internal Clock
├── Prescaler: 71
├── Counter Period: 1750 (固定值)
├── Counter Mode: Up
└── NVIC Settings: ✅ TIM2 global interrupt
波特率 | 格式 | 每字符时间 | 3.5字符时间 | 定时器Period值 |
---|---|---|---|---|
9600 | 8-N-1 | 1041.67μs | 3645.83μs | 3646 |
9600 | 8-E-1 | 1145.83μs | 4010.42μs | 4010 |
115200 | 任意 | - | 1750μs | 1750 |
3. GPIO配置(RS485方向控制)
GPIO -> PA1:
├── GPIO mode: GPIO_Output
├── GPIO Pull-up/Pull-down: No pull-up and no pull-down
└── User Label: RS485_DE
串口也最好配置下中断
✅ STM32F103 + RS485 + FreeModbus RTU 配置概览
1️⃣ CubeMX硬件配置
📌 UART配置(RS485通信):USART1 -> Asynchronous, 9600, 8N1, 开启中断📌 Timer配置(RTU字符间隔): TIM2 -> 内部时钟, 分频71, 周期1750, 开启中断📌 GPIO配置(RS485方向控制):PA1 -> 输出模式, 标签RS485_DE
2️⃣ FreeModbus源码文件
必须包含的源码文件:
├── mb.c, mbutils.c (协议栈主体)
├── mbrtu.c, mbcrc.c (RTU模式+CRC)
├── mbfunccoils.c, mbfuncholding.c (功能码实现)
├── mbfuncinput.c, mbfuncdisc.c
├── 所有include/*.h文件 (头文件)
└── port/*.c文件 (移植层-自己写)
3️⃣ 配置文件修改
📝 mbconfig.h:
#define MB_RTU_ENABLED 1
#define MB_ASCII_ENABLED 0
#define MB_TCP_ENABLED 0
#define MB_FUNC_READ_HOLDING_ENABLED 1 // 按需开启功能码📝 port.h:
// 定义数据类型、外部句柄声明、RS485引脚宏
4️⃣ 移植层代码编写
📝 portserial.c (4个函数):xMBPortSerialInit() - UART初始化vMBPortSerialEnable() - 使能收发+RS485方向控制 xMBPortSerialPutByte() - 发送1字节xMBPortSerialGetByte() - 接收1字节📝 porttimer.c (3个函数):xMBPortTimersInit() - 定时器初始化vMBPortTimersEnable() - 启动定时器vMBPortTimersDisable() - 停止定时器📝 portevent.c (3个函数): xMBPortEventInit/Post/Get() - 事件处理(简单实现)
5️⃣ 主程序配置
📝 main.c:
1. 包含头文件: #include "mb.h"
2. 定义寄存器数组: usRegHoldingBuf[], usRegInputBuf[]
3. 初始化: eMBInit(MB_RTU, 1, 1, 9600, MB_PAR_NONE)
4. 启用: eMBEnable()
5. 轮询: while(1) { eMBPoll(); }
6. 实现4个回调函数:- eMBRegHoldingCB() (保持寄存器)- eMBRegInputCB() (输入寄存器) - eMBRegCoilsCB() (线圈)- eMBRegDiscreteCB() (离散输入)
6️⃣ 中断处理配置
📝 stm32f1xx_it.c:
USART1_IRQHandler():RXNE中断 -> pxMBFrameCBByteReceived()TXE中断 -> pxMBFrameCBTransmitterEmpty()TIM2_IRQHandler(): UPDATE中断 -> pxMBPortCBTimerExpired()
7️⃣ 工程配置
📌 包含路径添加:../freemodbus/modbus/include../freemodbus/modbus/rtu../freemodbus/port📌 源文件添加:将所有.c文件加入工程编译
📋 配置检查清单
- CubeMX生成代码(UART1+TIM2+GPIO中断已开启)
- FreeModbus源码文件已添加到工程
- mbconfig.h已配置(RTU=1, ASCII=0, TCP=0)
- port.h已定义类型和句柄声明
- portserial.c已实现4个串口函数
- porttimer.c已实现3个定时器函数
- portevent.c已实现3个事件函数
- main.c已添加Modbus初始化和轮询
- main.c已实现4个寄存器回调函数
- stm32f1xx_it.c已添加UART和TIM中断处理
- 工程包含路径和源文件已配置
完成以上配置后,STM32F103就可以作为Modbus RTU从机通过RS485与主机通信!
✅ STM32F103 + HAL库 + CubeMX的FreeModbus RTU实现方案
必须保留的源码文件
freemodbus/
├── modbus/
│ ├── mb.c ✅ 协议栈主入口
│ ├── mbutils.c ✅ 工具函数(你漏了这个)
│ ├── rtu/
│ │ ├── mbrtu.c ✅ RTU模式核心
│ │ ├── mbrtu.h
│ │ └── mbcrc.c ✅ CRC计算(你漏了这个)
│ ├── functions/
│ │ ├── mbfunccoils.c ✅ 线圈功能码
│ │ ├── mbfuncdisc.c ✅ 离散输入功能码
│ │ ├── mbfuncholding.c ✅ 保持寄存器功能码
│ │ ├── mbfuncinput.c ✅ 输入寄存器功能码
│ │ └── mbfuncother.c ✅ 其他功能码(可选)
│ └── include/
│ ├── mb.h ✅ 主头文件
│ ├── mbconfig.h ✅ 配置文件
│ ├── mbport.h ✅ 移植层接口
│ ├── mbproto.h ✅ 协议定义
│ ├── mbframe.h ✅ 帧处理
│ ├── mbfunc.h ✅ 功能码定义
│ └── mbutils.h ✅ 工具函数
└── port/├── port.h ✅ 移植层总头文件├── portserial.c ✅ 串口移植├── porttimer.c ✅ 定时器移植└── portevent.c ✅ 事件移植
✅ 配置文件
mbconfig.h
#ifndef _MB_CONFIG_H
#define _MB_CONFIG_H/* ----------------------- RTU specific defines ---------------------------*/
#define MB_RTU_ENABLED 1
#define MB_ASCII_ENABLED 0
#define MB_TCP_ENABLED 0/* ----------------------- Function codes defines --------------------------*/
#define MB_FUNC_OTHER_REP_SLAVEID_BUF 34
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED 1#define MB_FUNC_READ_INPUT_ENABLED 1
#define MB_FUNC_READ_HOLDING_ENABLED 1
#define MB_FUNC_WRITE_HOLDING_ENABLED 1
#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED 1
#define MB_FUNC_READ_COILS_ENABLED 1
#define MB_FUNC_WRITE_COIL_ENABLED 1
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED 1
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED 1
#define MB_FUNC_READWRITE_HOLDING_ENABLED 1#endif
port.h
#ifndef _PORT_H
#define _PORT_H#include "stm32f1xx_hal.h"
#include <stdint.h>
#include <stdbool.h>/* ----------------------- Type definitions ---------------------------------*/
typedef uint8_t BOOL;
typedef uint8_t UCHAR;
typedef int8_t CHAR;
typedef uint16_t USHORT;
typedef int16_t SHORT;
typedef uint32_t ULONG;
typedef int32_t LONG;#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif/* ----------------------- Critical section ---------------------------------*/
#define ENTER_CRITICAL_SECTION() __disable_irq()
#define EXIT_CRITICAL_SECTION() __enable_irq()/* ----------------------- Hardware definitions -----------------------------*/
extern UART_HandleTypeDef huart1;
extern TIM_HandleTypeDef htim2;#define RS485_DE_Pin GPIO_PIN_1
#define RS485_DE_GPIO_Port GPIOA/* ----------------------- Function prototypes ------------------------------*/
// 这些函数需要在中断中调用
extern BOOL pxMBFrameCBByteReceived(void);
extern BOOL pxMBFrameCBTransmitterEmpty(void);
extern BOOL pxMBPortCBTimerExpired(void);#endif
✅ 移植层实现
portserial.c
#include "port.h"
#include "mb.h"
#include "mbport.h"/* ----------------------- Start implementation -----------------------------*/
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{if (xRxEnable) {// 启用接收中断__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);// RS485设为接收模式HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);} else {__HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE);}if (xTxEnable) {// 启用发送中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE);// RS485设为发送模式HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);} else {__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);// 发送完成后切换到接收模式HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);}
}BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)
{// CubeMX已经初始化了UART,这里可以重新配置参数huart1.Init.BaudRate = ulBaudRate;switch (eParity) {case MB_PAR_NONE:huart1.Init.Parity = UART_PARITY_NONE;huart1.Init.WordLength = UART_WORDLENGTH_8B;break;case MB_PAR_ODD:huart1.Init.Parity = UART_PARITY_ODD;huart1.Init.WordLength = UART_WORDLENGTH_9B;break;case MB_PAR_EVEN:huart1.Init.Parity = UART_PARITY_EVEN;huart1.Init.WordLength = UART_WORDLENGTH_9B;break;default:return FALSE;}if (HAL_UART_Init(&huart1) != HAL_OK) {return FALSE;}// 初始化RS485为接收模式HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);return TRUE;
}BOOL xMBPortSerialPutByte(CHAR ucByte)
{huart1.Instance->DR = ucByte;return TRUE;
}BOOL xMBPortSerialGetByte(CHAR * pucByte)
{*pucByte = huart1.Instance->DR;return TRUE;
}
porttimer.c
#include "port.h"
#include "mb.h"
#include "mbport.h"/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us)
{// 设置定时器周期:usTim1Timerout50us * 50us// 1MHz时钟下,1us = 1个计数uint32_t ulTimerReload = usTim1Timerout50us * 50;__HAL_TIM_SET_AUTORELOAD(&htim2, ulTimerReload - 1);__HAL_TIM_SET_COUNTER(&htim2, 0);return TRUE;
}void vMBPortTimersEnable(void)
{// 重置计数器并启动定时器__HAL_TIM_SET_COUNTER(&htim2, 0);__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);HAL_TIM_Base_Start(&htim2);
}void vMBPortTimersDisable(void)
{HAL_TIM_Base_Stop(&htim2);__HAL_TIM_DISABLE_IT(&htim2, TIM_IT_UPDATE);
}// 延时函数(如果需要)
void vMBPortTimersDelay(USHORT usTimeOutMS)
{HAL_Delay(usTimeOutMS);
}
portevent.c
#include "port.h"
#include "mb.h"
#include "mbport.h"/* ----------------------- Variables ----------------------------------------*/
static eMBEventType eQueuedEvent;
static BOOL xEventInQueue;/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortEventInit(void)
{xEventInQueue = FALSE;return TRUE;
}BOOL xMBPortEventPost(eMBEventType eEvent)
{xEventInQueue = TRUE;eQueuedEvent = eEvent;return TRUE;
}BOOL xMBPortEventGet(eMBEventType * eEvent)
{BOOL xEventHappened = FALSE;if (xEventInQueue) {*eEvent = eQueuedEvent;xEventInQueue = FALSE;xEventHappened = TRUE;}return xEventHappened;
}
✅ 中断处理函数
stm32f1xx_it.c中添加
/* USER CODE BEGIN Includes */
#include "port.h"
/* USER CODE END Includes *//* USER CODE BEGIN EV */
void USART1_IRQHandler(void)
{/* USER CODE BEGIN USART1_IRQn 0 */// 接收中断if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) && __HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE)) {pxMBFrameCBByteReceived();}// 发送中断if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) && __HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_TXE)) {pxMBFrameCBTransmitterEmpty();}/* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(&huart1);/* USER CODE BEGIN USART1_IRQn 1 *//* USER CODE END USART1_IRQn 1 */
}void TIM2_IRQHandler(void)
{/* USER CODE BEGIN TIM2_IRQn 0 */if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) && __HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE)) {__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);pxMBPortCBTimerExpired();}/* USER CODE END TIM2_IRQn 0 */HAL_TIM_IRQHandler(&htim2);/* USER CODE BEGIN TIM2_IRQn 1 *//* USER CODE END TIM2_IRQn 1 */
}
/* USER CODE END EV */
✅ 主程序实现
main.c
/* USER CODE BEGIN Includes */
#include "mb.h"
#include "mbutils.h"
/* USER CODE END Includes *//* USER CODE BEGIN PV */
// 寄存器定义
#define REG_HOLDING_START 1
#define REG_HOLDING_NREGS 10
#define REG_INPUT_START 1
#define REG_INPUT_NREGS 10
#define REG_COILS_START 1
#define REG_COILS_SIZE 16// 寄存器数组
USHORT usRegHoldingBuf[REG_HOLDING_NREGS];
USHORT usRegInputBuf[REG_INPUT_NREGS];
UCHAR ucRegCoilsBuf[REG_COILS_SIZE / 8];
/* USER CODE END PV */int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();MX_TIM2_Init();/* USER CODE BEGIN 2 */// 初始化Modbus RTU从机:地址1,UART1端口,9600波特率,无校验if (eMBInit(MB_RTU, 1, 1, 9600, MB_PAR_NONE) != MB_ENOERR) {Error_Handler();}// 启用Modbus协议栈if (eMBEnable() != MB_ENOERR) {Error_Handler();}// 初始化寄存器数据for (int i = 0; i < REG_HOLDING_NREGS; i++) {usRegHoldingBuf[i] = i + 100;}/* USER CODE END 2 */while (1){/* USER CODE BEGIN 3 */// 轮询Modbus协议栈eMBPoll();// 更新输入寄存器(模拟传感器数据)usRegInputBuf[0]++;usRegInputBuf[1] = HAL_GetTick() & 0xFFFF;HAL_Delay(10);/* USER CODE END 3 */}
}/* USER CODE BEGIN 4 */
// ==================== Modbus回调函数实现 ====================// 保持寄存器回调(功能码03/06/16)
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:while (usNRegs > 0) {*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] >> 8);*pucRegBuffer++ = (UCHAR)(usRegHoldingBuf[iRegIndex] & 0xFF);iRegIndex++;usNRegs--;}break;case MB_REG_WRITE:while (usNRegs > 0) {usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;iRegIndex++;usNRegs--;}break;}} else {eStatus = MB_ENOREG;}return eStatus;
}// 输入寄存器回调(功能码04)
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;}return eStatus;
}// 线圈回调(功能码01/05/15)
eMBErrorCode eMBRegCoilsCB(UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode)
{eMBErrorCode eStatus = MB_ENOERR;int iNCoils = (int)usNCoils;int usBitOffset;// 检查地址范围if ((usAddress >= REG_COILS_START) && (usAddress + usNCoils <= REG_COILS_START + REG_COILS_SIZE)) {usBitOffset = (int)(usAddress - REG_COILS_START);switch (eMode) {case MB_REG_READ:while (iNCoils > 0) {*pucRegBuffer++ = xMBUtilGetBits(ucRegCoilsBuf, usBitOffset, (UCHAR)(iNCoils > 8 ? 8 : iNCoils));iNCoils -= 8;usBitOffset += 8;}break;case MB_REG_WRITE:while (iNCoils > 0) {xMBUtilSetBits(ucRegCoilsBuf, usBitOffset, (UCHAR)(iNCoils > 8 ? 8 : iNCoils), *pucRegBuffer++);iNCoils -= 8;usBitOffset += 8;}break;}} else {eStatus = MB_ENOREG;}return eStatus;
}// 离散输入回调(功能码02)
eMBErrorCode eMBRegDiscreteCB(UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete)
{// 简单实现:返回一些固定值return MB_ENOREG;
}
/* USER CODE END 4 */
✅ 工程配置要点
1. 包含路径添加
Project Settings -> C/C++ -> Include Paths:
├── ../Core/freemodbus/modbus/include
├── ../Core/freemodbus/modbus/rtu
├── ../Core/freemodbus/port
└── ../Core/freemodbus/modbus
2. 源文件添加到工程
将所有 .c
文件添加到Keil/CubeIDE工程中
3. 编译宏定义(可选)
// 在工程设置中添加(如果需要)
#define MB_RTU_ENABLED 1
🔄 RS485方向控制原理
RS485是半双工通信
- 同一时刻只能单向传输:要么发送,要么接收,不能同时进行
- 需要方向控制信号来切换收发模式
📡 PA1 (RS485_DE) 的作用
DE/RE引脚说明
RS485收发器(如MAX485)通常有两个控制引脚:
├── DE (Driver Enable): 高电平=使能发送驱动器
└── RE (Receiver Enable): 低电平=使能接收器大多数情况下:DE和RE连接在一起,或者RE = !DE
所以用一个GPIO就能控制收发方向
方向控制逻辑
// 发送模式:STM32 -> RS485总线 -> 从站
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); // DE=1
// 此时:DE=1(发送使能), RE=0(接收禁用)// 接收模式:从站 -> RS485总线 -> STM32
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); // DE=0
// 此时:DE=0(发送禁用), RE=1(接收使能)
🔄 完整通信流程
1. STM32发送数据给从站
// 在portserial.c的vMBPortSerialEnable()函数中:
if (xTxEnable) {HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); // 切换到发送模式__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE);
}
2. STM32接收从站返回的数据
if (xRxEnable) {HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); // 切换到接收模式__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
}
📋 实际工作时序
Modbus RTU主从通信过程
时间轴: 发送请求 → 等待响应 → 接收响应
STM32: Master发送 → 切换到接收 → 读取Slave回复
RS485_DE: 1(发送模式) → 0(接收模式) → 0(接收模式)
举例:读取保持寄存器
1. 主机准备发送:DE=1,进入发送模式
2. 主机发送:01 03 00 00 00 01 84 0A (读从机1的寄存器)
3. 发送完成:DE=0,切换到接收模式
4. 从机响应:01 03 02 01 F4 B8 FA (返回数据500)
5. 主机接收:通过UART接收中断读取从机数据
⚡ 关键要点
✅ 能双向通信
- 能发送:DE=1时,STM32可以通过RS485发送数据给从站
- 能接收:DE=0时,STM32可以通过RS485接收从站返回的数据
🎯 方向控制的意义
- 防止总线冲突:确保同一时刻只有一个设备在发送
- 实现半双工:在发送和接收之间正确切换
- 保护硬件:避免多个发送器同时驱动总线造成损坏
📝 代码中的自动切换
// FreeModbus会自动调用vMBPortSerialEnable()来控制方向
// 你不需要手动控制,协议栈会:
// 1. 发送时自动设置DE=1
// 2. 发送完成后自动设置DE=0等待接收
// 3. 接收完成后保持DE=0等待下次发送
参考博客1
参考博客2