一款轻量、高通用性、分层解耦的RS-485 Modbus-RTU驱动框架,专为嵌入式MCU设计,解决工业控制/物联网数据采集中Modbus设备碎片化、代码冗余、跨平台移植困难、多路485设备并行通信难等问题,适配STM32等主流MCU平台,裸机/RTOS环境均兼容,移植仅需修改硬件抽象层,核心逻辑无需改动。
Modbus-RTU, RS-485, 嵌入式驱动, 工业通信, 分层解耦, 多设备并行, 硬件无关性
本项目命名为tiny485-mbrtu,其中:
-
tiny= 轻量,代表框架静态内存分配、无堆使用,资源占用低,适配小型嵌入式系统; -
485= RS-485,代表框架基于RS-485总线实现物理层通信; -
mbrtu= Modbus-RTU,代表框架实现的工业现场总线协议;
因此tiny485-mbrtu本质是标准化、可扩展的嵌入式Modbus-RTU驱动框架,区别于传统项目中为单个设备编写的零散485通信代码。
✅ 分层解耦设计:核心通信逻辑与硬件操作完全分离,移植仅需修改硬件抽象层,核心代码跨平台复用
✅ 硬件无关性:适配任意带UART外设的MCU,支持STM32/ESP32/AT32等主流嵌入式平台
✅ 多设备并行:通过LUN(逻辑单元号)标识多路RS-485总线,每路总线可接247个Modbus从机,通信互不干扰
✅ 完善的协议校验:实现标准Modbus-RTU 16位CRC校验,自动过滤错误帧、异常帧
✅ 鲁棒性保护:内置接收缓冲区溢出防护、帧超时判断、异常响应识别,工业场景稳定可靠
✅ 轻量无依赖:静态内存分配,无动态内存申请,无第三方库依赖,适合资源受限的MCU
✅ 裸机/RTOS兼容:预留RTOS临界区保护接口,裸机可留空,RTOS下实现资源互斥访问
✅ 简洁易用的API:封装Modbus-RTU核心操作(读/写寄存器),无需关注帧结构、CRC计算等底层细节
✅ 易调试:支持日志打印,通信失败时精准返回错误码(超时/CRC错误/长度错误/异常响应)
驱动仅包含4个核心文件,结构清晰,可直接集成到任意嵌入式工程中,无需额外修改核心代码:
tiny485-mbrtu/
├── mbrtu.c # 核心驱动层:Modbus-RTU帧构建、CRC校验、数据收发、解析等通用逻辑
├── mbrtu.h # 核心驱动头文件:对外暴露所有API接口、错误码枚举
├── mbrtu_port.c # 硬件抽象层:串口操作、延时、485引脚控制等硬件相关实现(需用户适配)
└── mbrtu_port.h # 硬件抽象层头文件:宏定义、LUN配置、硬件层函数声明采用三层架构实现硬件与逻辑的解耦,大幅降低移植成本和维护难度,核心逻辑一次开发多平台复用:
应用层(工业业务逻辑:传感器数据采集、执行器控制、PLC通信)
↕️
核心驱动层(mbrtu.c/h):通用Modbus-RTU协议逻辑(帧构建、CRC校验、数据收发、解析、超时判断)
↕️
硬件抽象层(mbrtu_port.c/h):平台相关硬件操作(串口收发、485引脚控制、延时、Tick、临界区)移植和使用框架的核心仅需修改硬件抽象层(mbrtu_port.c/h),核心驱动层无需任何改动,以下为通用适配步骤(以STM32为例)。
根据实际项目需求修改核心宏定义,定义RS-485总线数量、缓冲区大小等:
// 逻辑单元号定义:一个LUN对应一路独立的RS-485总线
#define MBRTU_LUN_USER1 0 /* 自定义LUN1,对接第一路485总线 */
#define MBRTU_LUN_USER2 1 /* 自定义LUN2,对接第二路485总线 */
// 核心配置参数
#define MBRTU_NUM 1 /* 实际使用的485总线数量(LUN数量) */
#define MBRTU_RECV_BUF_SIZE 64 /* 接收缓冲区大小,按需调整 */
#define MBRTU_LOG(...) //printf(__VA_ARGS__) /* 调试日志宏,默认printf,可自定义关闭 */实现与平台相关的硬件操作函数,对接MCU的HAL/LL库,LUN为多路485总线的唯一标识,不同LUN对应不同串口/485引脚:
// 1. 获取系统毫秒级Tick(用于超时判断)
uint32_t mbrtu_port_get_tick(void)
{
return HAL_GetTick(); // 对接平台Tick函数,如ESP32的xTaskGetTickCount()
}
// 2. 毫秒级延时函数
void mbrtu_port_delay_ms(uint32_t ms)
{
HAL_Delay(ms); // 对接平台延时函数
}
// 3. 485总线初始化(串口+DE/RE引脚)
void mbrtu_port_init(uint8_t lun)
{
switch (lun)
{
case MBRTU_LUN_USER1:
HAL_UART_Receive_IT(&huart2, mbrtu_rx_data[lun], 1); // 开启串口中断接收
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // DE/RE拉低,初始为接收模式
break;
// 新增LUN只需添加case,实现对应串口/引脚初始化
default:
break;
}
}
// 4. 485数据发送(含DE/RE收发切换)
void mbrtu_port_send(uint8_t lun, uint8_t *buf, uint16_t len)
{
switch (lun)
{
case MBRTU_LUN_USER1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); // 切换为发送模式
HAL_Delay(1); // 引脚切换延时,按需调整
HAL_UART_Transmit(&huart2, buf, len, 1000); // 串口发送数据
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // 切回接收模式
break;
default:
break;
}
}
// 5. 临界区保护(裸机留空,RTOS下实现互斥锁/关中断)
void mbrtu_port_enter_critical(uint8_t lun) {}
void mbrtu_port_exit_critical(uint8_t lun) {}-
将
tiny485-mbrtu文件夹下的4个核心文件添加到嵌入式工程,新建分组tiny485-mbrtu管理; -
在编译器(Keil/STM32CubeIDE/VSCode)中添加该文件夹的头文件路径;
-
若使用日志宏
MBRTU_LOG,需重定向printf到调试串口,并开启编译器微库(如Keil的Use MicroLIB)。
-
驱动初始化:在工程主函数中,完成MCU平台初始化(时钟、GPIO、串口)后,调用驱动初始化函数;
-
串口回调对接:将MCU的串口接收中断回调指向驱动的接收处理函数,为驱动提供数据输入。
#include "mbrtu.h"
int main(void)
{
// 1. 平台初始化:时钟、GPIO、串口、延时等
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init(); // 对接485的串口初始化
// 2. 初始化tiny485-mbrtu驱动
mbrtu_init();
while(1)
{
// 3. 业务逻辑:Modbus-RTU读/写寄存器操作
}
}
// 串口接收中断回调函数(以STM32 HAL库为例)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
// 驱动接收核心入口:传递LUN、接收数据、长度
mbrtu_recv_handler(MBRTU_LUN_USER1, mbrtu_rx_data[MBRTU_LUN_USER1], 1);
// 重新开启串口中断接收,持续获取数据
HAL_UART_Receive_IT(&huart2, mbrtu_rx_data[MBRTU_LUN_USER1], 1);
}
}驱动对外封装了基础初始化、数据收发、数据解析三类核心API,屏蔽了Modbus-RTU底层细节,直接调用即可实现寄存器读/写,满足99%的工业485通信需求。
void mbrtu_init(void); // 驱动初始化:清空缓冲区、初始化硬件层
void mbrtu_clear_recv_buffer(uint8_t lun); // 清空指定LUN的接收缓冲区| 函数声明 | 功能描述 |
|---|---|
uint8_t mbrtu_build_read_holding(uint8_t lun, uint8_t slave, uint16_t regAddr, uint16_t quantity, uint16_t timeout) |
构建读保持寄存器请求帧(功能码0x03),自动发送并等待响应 |
void mbrtu_build_write_single(uint8_t lun, uint8_t slave, uint16_t regAddr, uint16_t value) |
构建写单个寄存器请求帧(功能码0x06),自动发送 |
uint8_t mbrtu_recv_handler(uint8_t lun, const uint8_t *data, uint16_t len) |
驱动接收核心入口,串口接收后必须调用,传递数据 |
uint8_t mbrtu_get_recv_data(uint8_t lun, uint8_t **data, uint16_t *dataLen) |
获取指定LUN的接收原始数据,返回数据指针和长度 |
uint8_t mbrtu_parse_data(uint8_t lun, uint8_t *raw, uint16_t rawLen, uint8_t *pdu, uint8_t *pduLen);功能:解析Modbus-RTU原始接收数据,完成CRC校验、异常帧识别,提取去除CRC的PDU有效数据。
驱动通过错误码精准标识通信状态,所有API的返回值均遵循以下枚举:
typedef enum
{
MBRTU_OK = 0, /* 操作成功 */
MBRTU_ERR_CRC, /* CRC校验错误 */
MBRTU_ERR_EXCEPTION, /* 从机异常响应(功能码最高位为1) */
MBRTU_ERR_LEN, /* 数据帧长度错误 */
MBRTU_ERR_TIMEOUT, /* 通信超时,未收到从机响应 */
MBRTU_ERR_RECV, /* 接收错误,无有效数据 */
MBRTU_ERR_PARAM, /* 入参错误(如从站地址/寄存器数量非法) */
} mbrtu_err_t;以下为最常用的读保持寄存器和写单个寄存器示例,基于STM32裸机环境,直接复用即可。
读取指定从机的保持寄存器数据,自动发送请求帧、等待响应、解析数据:
#include "mbrtu.h"
#include <stdio.h>
#define LUN_485_1 MBRTU_LUN_USER1 // 定义当前使用的485总线LUN
#define SLAVE_ADDR 0x01 // 从站设备地址
#define REG_ADDR 0x0000 // 寄存器起始地址
#define REG_NUM 0x0002 // 读取寄存器数量
#define TIMEOUT 1000 // 通信超时时间(ms)
int main(void)
{
// 平台+驱动初始化...
mbrtu_init();
uint8_t state;
uint8_t *recv_data;
uint16_t recv_len;
uint8_t pdu[64];
uint8_t pdu_len;
while(1)
{
// 1. 构建并发送读保持寄存器请求
state = mbrtu_build_read_holding(LUN_485_1, SLAVE_ADDR, REG_ADDR, REG_NUM, TIMEOUT);
if (state == MBRTU_ERR_TIMEOUT)
{
MBRTU_LOG("[MBRTU][ERR] Read timeout\r\n");
HAL_Delay(1000);
continue;
}
// 2. 获取接收的原始数据
state = mbrtu_get_recv_data(LUN_485_1, &recv_data, &recv_len);
if (state == MBRTU_ERR_RECV)
{
MBRTU_LOG("[MBRTU][ERR] No recv data\r\n");
HAL_Delay(1000);
continue;
}
MBRTU_LOG("[MBRTU][OK] Recv raw data: ");
for (uint16_t i = 0; i < recv_len; i++) MBRTU_LOG("%02X ", recv_data[i]);
MBRTU_LOG("\r\n");
// 3. 解析数据(CRC校验+提取PDU)
state = mbrtu_parse_data(LUN_485_1, recv_data, recv_len, pdu, &pdu_len);
if (state != MBRTU_OK)
{
MBRTU_LOG("[MBRTU][ERR] Parse error: %d\r\n", state);
HAL_Delay(1000);
continue;
}
MBRTU_LOG("[MBRTU][OK] Parse PDU data: ");
for (uint16_t i = 0; i < pdu_len; i++) MBRTU_LOG("%02X ", pdu[i]);
MBRTU_LOG("\r\n");
HAL_Delay(1000);
}
}向指定从机的单个寄存器写入数值,自动构建并发送请求帧:
#include "mbrtu.h"
#define LUN_485_1 MBRTU_LUN_USER1 // 485总线LUN
#define SLAVE_ADDR 0x01 // 从站地址
#define REG_ADDR 0x0001 // 要写入的寄存器地址
#define REG_VALUE 0x1234 // 要写入的寄存器数值
int main(void)
{
// 平台+驱动初始化...
mbrtu_init();
while(1)
{
// 构建并发送写单个寄存器请求帧
mbrtu_build_write_single(LUN_485_1, SLAVE_ADDR, REG_ADDR, REG_VALUE);
MBRTU_LOG("[MBRTU][OK] Write register 0x%04X with value 0x%04X\r\n", REG_ADDR, REG_VALUE);
HAL_Delay(2000);
}
}框架使用中常见问题及解决方法,按优先级排查,快速定位485通信问题:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
读寄存器返回MBRTU_ERR_TIMEOUT(超时) |
1. 485接线错误(A/B接反) 2. 串口波特率/校验位不匹配 3. DE/RE引脚未正确切换 4. 从站地址错误 |
1. 核对485模块A/B接线,与从机一致 2. 确保MCU与从机的通信参数(9600/8/N/1)一致 3. 检查 mbrtu_port_send中DE/RE引脚控制逻辑4. 确认从机实际Modbus地址,非0x00/0xFF |
解析数据返回MBRTU_ERR_CRC(CRC错误) |
1. 485总线干扰严重 2. 串口接收数据丢包 3. 波特率过高导致采样错误 |
1. 485总线增加终端电阻(120Ω) 2. 优化串口接收(如改用DMA空闲接收) 3. 降低波特率(如9600),增加收发切换延时 |
返回MBRTU_ERR_EXCEPTION(从机异常) |
1. 寄存器地址越界 2. 读取/写入的寄存器数量非法 3. 从机不支持对应功能码 |
1. 核对从机寄存器映射表,确保地址有效 2. 读寄存器数量≤123,写寄存器仅支持单个 3. 确认从机支持0x03/0x06功能码 |
返回MBRTU_ERR_LEN(长度错误) |
1. 接收缓冲区过小 2. 从机返回异常帧长度 |
1. 增大MBRTU_RECV_BUF_SIZE宏定义2. 用串口助手抓取原始帧,核对从机返回格式 |
| 485总线通信乱码 | 1. 串口初始化错误 2. DE/RE切换延时过短 3. 电源纹波干扰 |
1. 检查MCU串口初始化代码,确保参数正确 2. 增大 mbrtu_port_send中的HAL_Delay时间(如2ms)3. 485模块使用隔离电源,增加滤波电容 |
| 多路485总线数据串扰 | 1. LUN与串口/引脚映射错误 2. RTOS下无临界区保护 |
1. 检查mbrtu_port_init/send中LUN的case分支,确保一一对应2. 在 mbrtu_port_enter/exit_critical中实现互斥锁 |
本项目基于MIT开源协议发布,可自由用于商业/非商业项目,保留作者版权声明即可。
-
嵌入式技术交流群:181921938
如果这个项目对你的工业控制/物联网开发有帮助,请给它一个 ⭐ !