一、系统概述
Modbus是工业领域广泛应用的串行通信协议,支持RTU(二进制)和ASCII(文本)模式,本方案基于STM32F103C8T6(Cortex-M3,72MHz)实现Modbus RTU从机,通过RS485总线与主机(如PLC、PC)通信,支持保持寄存器(0x03/0x06/0x10)、线圈(0x01/0x05/0x0F) 读写,可扩展至其他功能码。核心功能包括:Modbus协议解析、寄存器管理、RS485收发控制、CRC校验。
二、硬件设计
2.1 核心组件
| 模块 | 型号/参数 | 功能说明 |
|---|---|---|
| 主控 | STM32F103C8T6(64KB Flash,20KB RAM) | Modbus协议处理、寄存器管理、串口通信 |
| 通信接口 | MAX485(RS485物理层) | 实现Modbus RTU差分信号传输(半双工) |
| 串口 | USART1(PA9-TX,PA10-RX,115200bps) | 连接MAX485,配置8N1(8数据位、无校验、1停止位) |
| 定时器 | TIM2(16位通用定时器) | 计算Modbus RTU帧超时(3.5字符时间,如9600bps时≈3.64ms) |
| 控制引脚 | PB0(DE/RE) | 控制MAX485收发模式(高电平发送,低电平接收) |
2.2 硬件连接
| 模块 | 引脚(STM32F103C8T6) | 说明 |
|---|---|---|
| USART1 | PA9(TX)→ MAX485 DI | 发送数据 |
| PA10(RX)← MAX485 RO | 接收数据 | |
| MAX485 | DE/RE → PB0 | 收发控制(高=发送,低=接收) |
| RS485总线 | A/B端子 | 连接主机(A-A,B-B) |
三、软件设计(STM32标准库3.5)
3.1 系统架构
RS485
数据接收
解析请求
返回响应
数据发送
RS485
3.5T超时中断
主机(Modbus RTU)
STM32 USART1
FreeModbus协议栈
寄存器管理(保持寄存器/线圈)
TIM2
3.2 核心原理
-
Modbus RTU帧格式 :
[地址码(1)][功能码(1)][数据(n)][CRC16(2)],CRC16校验确保数据完整性。 -
从机工作流程:
-
接收主机请求帧,校验CRC;
-
解析功能码,操作对应寄存器(读/写);
-
生成响应帧(含数据+CRC),通过RS485返回主机。
3.3 开发步骤
3.3.1 FreeModbus协议栈移植
-
获取源码 :从FreeModbus官网下载V1.6稳定版,关键文件:
-
modbus.c/modbus.h:协议核心(从机模式); -
port/portserial.c/portserial.h:串口操作(需移植); -
port/porttimer.c/porttimer.h:定时器操作(需移植); -
port/portother.c/portother.h:临界区保护(关中断/开中断)。
3.3.2 STM32硬件初始化
(1)时钟与GPIO配置
c
#include "stm32f10x.h"
void RCC_Config(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART1 | RCC_APB1Periph_TIM2, ENABLE); // 串口1+定时器2时钟
}
void GPIO_Config(void) {
GPIO_InitTypeDef GPIO_InitStruct;
// USART1_TX(PA9):复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// USART1_RX(PA10):浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// MAX485_DE/RE(PB0):推挽输出(高=发送,低=接收)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 初始接收模式
}
(2)USART1初始化(Modbus RTU,115200bps,8N1)
c
void USART1_Init(void) {
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStruct);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
USART_Cmd(USART1, ENABLE);
}
(3)TIM2初始化(3.5T超时定时器,1ms基准)
c
void TIM2_Init(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_TimeBaseStruct.TIM_Period = 999; // 1ms重装值(72MHz/72=1MHz,1ms=1000计数,重装值999)
TIM_TimeBaseStruct.TIM_Prescaler = 71; // 预分频72(72MHz/72=1MHz)
TIM_TimeBaseStruct.TIM_ClockDivision = 0;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断
TIM_Cmd(TIM2, DISABLE); // 初始关闭,接收数据时启动
}
3.3.3 FreeModbus接口移植
(1)串口操作(portserial.c)
c
#include "portserial.h"
#include "stm32f10x_usart.h"
// 串口初始化(已在USART1_Init中实现)
BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity) {
return TRUE;
}
// 使能串口收发(DE/RE控制)
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) {
if (xRxEnable) {
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // MAX485接收模式(DE=0)
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
} else {
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
}
if (xTxEnable) {
GPIO_SetBits(GPIOB, GPIO_Pin_0); // MAX485发送模式(DE=1)
} else {
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
}
}
// 发送单个字节
BOOL xMBPortSerialPutByte(CHAR ucByte) {
USART_SendData(USART1, ucByte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
return TRUE;
}
// 接收单个字节(中断中调用)
BOOL xMBPortSerialGetByte(CHAR *pucByte) {
*pucByte = USART_ReceiveData(USART1);
return TRUE;
}
(2)定时器操作(porttimer.c)
c
#include "porttimer.h"
#include "stm32f10x_tim.h"
// 定时器初始化(3.5T超时,单位:50μs,115200bps时3.5T≈364μs=7.28×50μs→取整8)
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us) {
return TRUE; // 已在TIM2_Init中配置1ms基准,此处无需额外初始化
}
// 启动定时器(超时时间=usTim1Timerout50us×50μs)
void vMBPortTimersEnable(void) {
TIM_SetCounter(TIM2, 0); // 清零计数器
TIM_Cmd(TIM2, ENABLE); // 启动定时器
}
// 关闭定时器
void vMBPortTimersDisable(void) {
TIM_Cmd(TIM2, DISABLE); // 关闭定时器
TIM_SetCounter(TIM2, 0); // 清零计数器
}
// TIM2中断服务函数(1ms触发一次,累计超时)
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
pxMBPortCBTimerExpired(); // 通知FreeModbus帧超时
}
}
(3)寄存器定义(modbus_slave.c)
定义保持寄存器(0x03功能码)和线圈(0x01功能码):
c
#include "modbus.h"
// 保持寄存器(地址0-9,16位整数)
USHORT usRegHoldingBuf[10] = {0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006};
// 线圈(地址0-7,1位布尔值)
UCHAR ucRegCoilsBuf[1] = {0x01}; // 线圈0=ON(bit0=1),其余OFF
// 保持寄存器回调函数(读/写)
eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) {
if (usAddress + usNRegs > 10) return MB_ENOREG; // 地址越界
if (eMode == MB_REG_READ) {
memcpy(pucRegBuffer, &usRegHoldingBuf[usAddress], usNRegs * 2); // 16位寄存器,2字节/个
} else { // 写
memcpy(&usRegHoldingBuf[usAddress], pucRegBuffer, usNRegs * 2);
}
return MB_ENOERR;
}
// 线圈回调函数(读/写)
eMBErrorCode eMBRegCoilsCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode) {
if (usAddress + usNCoils > 8) return MB_ENOREG; // 地址越界(8个线圈)
if (eMode == MB_REG_READ) {
for (int i=0; i<usNCoils; i++) {
UCHAR bit = (ucRegCoilsBuf[usAddress/8] >> (usAddress%8)) & 0x01;
pucRegBuffer[i/8] |= (bit << (i%8));
usAddress++;
}
} else { // 写
for (int i=0; i<usNCoils; i++) {
if (pucRegBuffer[i/8] & (1 << (i%8))) {
ucRegCoilsBuf[usAddress/8] |= (1 << (usAddress%8));
} else {
ucRegCoilsBuf[usAddress/8] &= ~(1 << (usAddress%8));
}
usAddress++;
}
}
return MB_ENOERR;
}
3.3.4 主函数(Modbus轮询)
c
#include "stm32f10x.h"
#include "modbus.h"
int main(void) {
RCC_Config(); // 时钟配置
GPIO_Config(); // GPIO配置(USART1+PB0)
USART1_Init(); // USART1初始化(115200bps)
TIM2_Init(); // TIM2初始化(超时定时器)
NVIC_Config(); // 中断优先级配置(USART1+TIM2)
// 初始化FreeModbus从机(地址0x01,RTU模式,串口1,115200bps,无校验)
eMBInit(MB_RTU, 0x01, 0, 115200, MB_PAR_NONE);
eMBEnable(); // 使能Modbus
while (1) {
eMBPoll(); // 轮询Modbus事件(解析请求、发送响应)
}
}
// 中断优先级配置(NVIC)
void NVIC_Config(void) {
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 分组2(2位抢占,2位响应)
// USART1中断(接收)
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// TIM2中断(超时)
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
}
// USART1中断服务函数(接收数据)
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
pxMBFrameCBByteReceived(); // 通知FreeModbus接收到一个字节
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
参考代码 利用STM32单片机实现modbus通信 www.youwenfan.com/contentcst/56553.html
四、测试与验证
4.1 测试工具
-
Modbus Poll(PC端):发送0x03功能码读取保持寄存器(地址0-9),0x01功能码读取线圈(地址0-7);
-
串口助手 :监控USART1数据,验证帧格式(如读寄存器请求:
01 03 00 00 00 0A C5 CD,响应:01 03 14 12 34 56 78 9A BC DE F0 00 01 00 02 00 03 00 04 00 05 00 06 XX XX,XX为CRC)。
4.2 关键参数
-
波特率:115200bps时,1字符=10位(8数据+1停止+1校验,无校验时为9位),3.5T≈3.5×9/115200≈273μs,TIM2中断1ms累计3次触发超时;
-
CRC校验:FreeModbus自动处理CRC16(多项式0x8005),无需手动计算。
五、关键问题与解决方案
5.1 通信乱码
-
原因:波特率不匹配(如STM32用115200,主机用9600)、串口参数错误(数据位/校验位);
-
解决 :用示波器测量USART_TX引脚,确认波特率(115200bps时1位≈8.68μs),检查
USART_InitStruct配置。
5.2 帧超时错误
-
原因:TIM2定时不准确(如预分频/重装值错误)、3.5T计算偏差;
-
解决:根据波特率调整TIM2重装值(如9600bps时3.5T≈364μs,TIM2 1ms中断累计1次触发超时)。
5.3 寄存器读写异常
-
原因:寄存器地址映射错误(如主机请求地址10,但从机仅定义0-9);
-
解决 :在
eMBRegHoldingCB中添加地址越界判断(if (usAddress + usNRegs > 10) return MB_ENOREG;)。
六、总结
基于STM32F103实现了Modbus RTU从机,核心是FreeModbus协议栈移植+RS485收发控制+寄存器管理。通过简单修改寄存器定义(如扩展保持寄存器数量、添加输入寄存器),可支持更多功能码(0x04读输入寄存器、0x05写单线圈等),适用于工业传感器、执行器等设备的通信接口。