【实战】STM32F103实现Modbus RTU从站协议栈(完整源码+详细解析)
·
前言
Modbus RTU作为工业自动化领域的“通用语言”,是嵌入式工程师必须掌握的通信协议之一。本文基于STM32F103C8T6开发板,从零实现一套完整的Modbus RTU从站协议栈,包含核心协议解析、CRC16校验、寄存器读写等关键功能,代码可直接移植到实际项目中。
一、项目概述
1.1 核心目标
基于STM32F103C8T6 + RS485模块,实现Modbus RTU从站功能,支持工业场景中最常用的03(读取保持寄存器) 和 06(写入单个寄存器) 功能码,可与Modbus Poll等主站软件完成数据交互。
1.2 硬件需求
| 硬件名称 | 说明 |
|---|---|
| STM32F103C8T6开发板 | 核心控制单元 |
| RS485通信模块 | 工业级串行通信(需控制DE/RE引脚) |
| 串口转USB模块 | 调试与数据监控 |
| 杜邦线 | 硬件连接 |
1.3 项目结构
modbus-stm32/
├── Inc/ # 头文件目录
│ └── modbus.h # Modbus协议头文件
├── Src/ # 源文件目录
│ ├── modbus.c # Modbus协议核心实现
│ ├── main.c # 主程序(初始化+中断处理)
│ └── stm32f1xx_it.c # 串口初始化
├── STM32F1xx/ # STM32底层驱动
│ ├── Core/
│ └── Drivers/
└── Makefile # 编译脚本(或使用STM32CubeIDE)
二、核心代码实现
2.1 头文件定义(modbus.h)
该文件定义了Modbus协议的核心常量、寄存器地址、函数声明,是整个协议栈的“骨架”:
#ifndef __MODBUS_H
#define __MODBUS_H
#include "stm32f1xx_hal.h"
/* Modbus功能码 */
#define MODBUS_FUNC_READ_HOLD_REG 0x03 // 读取保持寄存器
#define MODBUS_FUNC_WRITE_SINGLE_REG 0x06 // 写入单个寄存器
/* Modbus错误码 */
#define MODBUS_EXCEPTION_ILLEGAL_FUNCTION 0x01 // 非法功能码
#define MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR 0x02 // 非法寄存器地址
/* CRC16校验常量 */
#define CRC16_INIT 0xFFFF
#define CRC16_POLY 0xA001
/* 寄存器地址映射(模拟工业参数) */
#define REG_TEMP 0x0000 // 温度寄存器(2500 = 25.00℃)
#define REG_HUMIDITY 0x0001 // 湿度寄存器(6000 = 60.00%)
#define REG_PRESSURE 0x0002 // 压力寄存器(1013 hPa)
#define REG_STATUS 0x0003 // 状态寄存器(0x0001=运行中)
/* 从站地址与串口配置 */
#define MODBUS_SLAVE_ADDR 0x01
#define MODBUS_USART USART1
/* 全局缓冲区(中断接收/发送) */
extern uint8_t modbus_rx_buffer[256];
extern uint16_t modbus_rx_len;
extern uint8_t modbus_tx_buffer[256];
extern uint16_t modbus_tx_len;
/* 核心函数声明 */
void Modbus_Init(void); // 寄存器初始化
void Modbus_Process(void); // 协议帧解析
uint16_t Modbus_CRC16(uint8_t *data, uint16_t len); // CRC16校验
void Modbus_SendResponse(uint8_t *frame, uint16_t len); // 发送响应
uint16_t Modbus_ReadRegister(uint16_t reg_addr); // 读寄存器
void Modbus_WriteRegister(uint16_t reg_addr, uint16_t value); // 写寄存器
#endif /* __MODBUS_H */
2.2 Modbus协议核心实现(modbus.c)
(1)基础初始化
初始化模拟寄存器的默认值,模拟工业现场的传感器初始数据:
#include "modbus.h"
#include <string.h>
/* 模拟寄存器存储(实际项目可替换为真实传感器数据) */
static uint16_t registers[100] = {0};
uint8_t modbus_rx_buffer[256] = {0};
uint16_t modbus_rx_len = 0;
uint8_t modbus_tx_buffer[256] = {0};
uint16_t modbus_tx_len = 0;
void Modbus_Init(void) {
// 初始化默认参数
registers[REG_TEMP] = 2500; // 25.00°C
registers[REG_HUMIDITY] = 6000; // 60.00%
registers[REG_PRESSURE] = 1013; // 1013 hPa
registers[REG_STATUS] = 0x0001; // 运行状态
}
(2)CRC16校验(Modbus必备)
Modbus RTU协议的核心校验算法,确保数据传输的完整性:
uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
uint16_t crc = CRC16_INIT;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= CRC16_POLY;
} else {
crc >>= 1;
}
}
}
return crc;
}
(3)协议帧解析(核心逻辑)
处理主站发来的请求帧,解析功能码并执行对应操作,返回响应帧:
void Modbus_Process(void) {
if (modbus_rx_len < 3) return; // 帧长度不足,直接返回
uint8_t slave_addr = modbus_rx_buffer[0];
uint8_t func_code = modbus_rx_buffer[1];
// 校验从站地址(只处理目标为本机的帧)
if (slave_addr != MODBUS_SLAVE_ADDR) {
modbus_rx_len = 0;
return;
}
switch (func_code) {
case MODBUS_FUNC_READ_HOLD_REG: {
// 解析寄存器起始地址和数量
uint16_t start_addr = (modbus_rx_buffer[2] << 8) | modbus_rx_buffer[3];
uint16_t reg_count = (modbus_rx_buffer[4] << 8) | modbus_rx_buffer[5];
// 校验寄存器地址范围
if (start_addr >= 100 || (start_addr + reg_count) > 100) {
// 返回非法地址异常
modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
modbus_tx_buffer[1] = func_code | 0x80; // 异常标识(最高位置1)
modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR;
modbus_tx_len = 3;
Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
break;
}
// 构建响应帧
modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
modbus_tx_buffer[1] = func_code;
modbus_tx_buffer[2] = reg_count * 2; // 返回字节数(每个寄存器2字节)
for (uint16_t i = 0; i < reg_count; i++) {
uint16_t value = Modbus_ReadRegister(start_addr + i);
modbus_tx_buffer[3 + i * 2] = (value >> 8) & 0xFF; // 高字节
modbus_tx_buffer[4 + i * 2] = value & 0xFF; // 低字节
}
modbus_tx_len = 3 + reg_count * 2;
Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
break;
}
case MODBUS_FUNC_WRITE_SINGLE_REG: {
// 解析目标寄存器地址和写入值
uint16_t reg_addr = (modbus_rx_buffer[2] << 8) | modbus_rx_buffer[3];
uint16_t value = (modbus_rx_buffer[4] << 8) | modbus_rx_buffer[5];
// 校验地址范围
if (reg_addr >= 100) {
modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
modbus_tx_buffer[1] = func_code | 0x80;
modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_DATA_ADDR;
modbus_tx_len = 3;
Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
break;
}
// 写入寄存器
Modbus_WriteRegister(reg_addr, value);
// 返回确认帧(原样返回地址和值)
modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
modbus_tx_buffer[1] = func_code;
modbus_tx_buffer[2] = (reg_addr >> 8) & 0xFF;
modbus_tx_buffer[3] = reg_addr & 0xFF;
modbus_tx_buffer[4] = (value >> 8) & 0xFF;
modbus_tx_buffer[5] = value & 0xFF;
modbus_tx_len = 6;
Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
break;
}
default:
// 不支持的功能码,返回非法功能异常
modbus_tx_buffer[0] = MODBUS_SLAVE_ADDR;
modbus_tx_buffer[1] = func_code | 0x80;
modbus_tx_buffer[2] = MODBUS_EXCEPTION_ILLEGAL_FUNCTION;
modbus_tx_len = 3;
Modbus_SendResponse(modbus_tx_buffer, modbus_tx_len);
break;
}
modbus_rx_len = 0; // 清空接收缓冲区
}
2.3 主程序与中断处理(main.c)
通过串口中断接收数据,主循环处理协议帧,是STM32端的“入口”:
#include "main.h"
#include "modbus.h"
#include "usart.h"
uint8_t modbus_rx_buffer[256];
uint16_t modbus_rx_len = 0;
int main(void) {
/* 初始化HAL库 */
HAL_Init();
/* 配置系统时钟(72MHz) */
SystemClock_Config();
/* 初始化USART1(9600 8N1) */
MX_USART1_UART_Init();
/* 初始化Modbus寄存器 */
Modbus_Init();
/* 使能串口接收中断(每次接收1字节) */
HAL_UART_Receive_IT(&huart1, modbus_rx_buffer, 1);
/* 主循环 */
while (1) {
/* 接收到完整帧后处理 */
if (modbus_rx_len >= 3) {
Modbus_Process();
}
HAL_Delay(10); // 降低CPU占用
}
}
/* 串口接收中断回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 存储接收到的字节
modbus_rx_buffer[modbus_rx_len++] = modbus_rx_buffer[0];
// 缓冲区溢出保护
if (modbus_rx_len >= 256) modbus_rx_len = 0;
// 重新开启中断,等待下一字节
HAL_UART_Receive_IT(&huart1, modbus_rx_buffer, 1);
}
}
三、硬件连接与配置
3.1 关键连接(RS485模块)
| STM32引脚 | RS485模块引脚 | 说明 |
|---|---|---|
| PA9(USART1_TX) | DI | 数据发送 |
| PA10(USART1_RX) | RO | 数据接收 |
| PA8(GPIO) | DE/RE | 收发方向控制(高电平发送,低电平接收) |
| GND | GND | 共地(必须连接,否则通信异常) |
3.2 串口配置
- 波特率:9600(工业常用,可根据需求修改)
- 数据位:8
- 停止位:1
- 校验位:无
- 流控:无
四、测试步骤与效果
4.1 测试工具
推荐使用Modbus Poll(Windows)或QModMaster(跨平台),配置如下:
- 选择串口模式(RTU),绑定USB转串口的COM口;
- 波特率9600、数据位8、停止位1、无校验;
- 从站地址:1(与代码中MODBUS_SLAVE_ADDR一致)。
4.2 测试操作
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| 读取温度 | 功能码03,起始地址0,寄存器数量1 | 返回2500(对应25.00℃) |
| 读取湿度 | 功能码03,起始地址1,寄存器数量1 | 返回6000(对应60.00%) |
| 写入温度 | 功能码06,地址0,写入值2400 | 寄存器0的值变为2400(24.00℃) |
五、扩展与优化建议
- 功能码扩展:增加01(读线圈)、02(读离散输入)、16(写多个寄存器)支持;
- 硬件优化:DE/RE引脚通过代码自动控制(发送前置高,发送完成后置低);
- 数据可靠性:添加帧超时判断(Modbus RTU帧间隔>3.5字符时间视为帧结束);
- 多从站支持:通过配置从站地址,实现单总线多设备通信;
- 实际传感器对接:将模拟寄存器替换为DS18B20(温度)、DHT11(温湿度)等真实传感器数据;
- Modbus TCP扩展:结合ETH模块,实现Modbus TCP/IP协议。
六、核心知识点总结
- Modbus RTU帧结构:从站地址(1B) + 功能码(1B) + 数据(NB) + CRC16(2B);
- 中断驱动通信:STM32串口中断接收避免轮询占用CPU,提高实时性;
- CRC16校验:Modbus协议的“安全锁”,确保数据传输不被篡改;
- 寄存器映射:工业协议的核心思想,通过地址映射实现数据交互标准化。
结语
本文实现的Modbus RTU从站协议栈是工业嵌入式开发的基础框架,掌握该协议不仅能应对工业通信场景,也能理解串行通信、协议解析、数据校验等核心嵌入式开发思想。实际项目中,可根据需求扩展功能、优化性能,适配不同的工业传感器和控制器。
如果本文对你有帮助,欢迎点赞+收藏+关注,后续会更新更多STM32工业通信实战内容!
🎁欢迎关注,获取更多技术干货!
公众号:BackCatK Chen,文章末尾可以扫码关注
🎁资料包亮点
这份资料包涵盖了从硬件电路设计到STM32单片机开发,再到Linux系统学习的全链路内容,适合不同阶段的学习者:
- 硬件基础:包含硬件电路合集、硬件设计开发工具包,帮你打牢底层基础。
- STM32专项:从环境搭建、开发工具、传感器模块到项目实战,还有书籍和芯片手册,一站式搞定STM32学习。
- C语言进阶:C语言学习资料包,助你掌握嵌入式开发的核心语言。
- 面试求职:嵌入式面试题合集,提前备战技术面试。
- Linux拓展:Linux相关学习资料包,拓宽技术视野。
📂资料包目录
- 00-STM32单片机环境搭建
- 01-硬件电路合集
- 02-硬件设计开发工具包
- 03-C语言学习资料包
- 04-STM32单片机开发工具包
- 05-STM32传感器模块合集
- 06-STM32项目合集
- 07-STM32单片机书籍&芯片手册
- 08-Linux相关学习资料包

更多推荐

所有评论(0)