Skip to content

ZeroOneLab/tiny485-mbrtu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

tiny485-mbrtu

License: MIT

一款轻量、高通用性、分层解耦的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为例)。

步骤1:硬件抽象层适配(唯一需要修改的部分)

1.1 宏定义配置(mbrtu_port.h

根据实际项目需求修改核心宏定义,定义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,可自定义关闭 */

1.2 硬件层函数实现(mbrtu_port.c

实现与平台相关的硬件操作函数,对接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) {}

步骤2:工程集成

  1. tiny485-mbrtu文件夹下的4个核心文件添加到嵌入式工程,新建分组tiny485-mbrtu管理;

  2. 在编译器(Keil/STM32CubeIDE/VSCode)中添加该文件夹的头文件路径;

  3. 若使用日志宏MBRTU_LOG,需重定向printf到调试串口,并开启编译器微库(如Keil的Use MicroLIB)。

步骤3:驱动初始化与串口回调对接

  1. 驱动初始化:在工程主函数中,完成MCU平台初始化(时钟、GPIO、串口)后,调用驱动初始化函数;

  2. 串口回调对接:将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

驱动对外封装了基础初始化数据收发数据解析三类核心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裸机环境,直接复用即可。

示例1:读保持寄存器(功能码0x03)

读取指定从机的保持寄存器数据,自动发送请求帧、等待响应、解析数据:

#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);
    }
}

示例2:写单个寄存器(功能码0x06)

向指定从机的单个寄存器写入数值,自动构建并发送请求帧:

#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开源协议发布,可自由用于商业/非商业项目,保留作者版权声明即可。

🔗 相关链接

如果这个项目对你的工业控制/物联网开发有帮助,请给它一个 ⭐ !

About

轻量、高通用性的 RS-485 Modbus-RTU 嵌入式驱动框架,分层解耦设计,适配 STM32/ESP32 等 MCU,支持多设备通信,裸机 / RTOS 兼容,移植简单,适用于工业控制、物联网数据采集场景。

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages