找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1089

积分

0

好友

141

主题
发表于 4 小时前 | 查看: 2| 回复: 0

Modbus协议是工业控制设备的标准通信协议,其核心目的是让主机设备能够获取或设置从机设备内部的数据。本仓库提供了一个基础型Modbus框架,支持线圈(1位)和寄存器(16位)这两种最常用的数据类型。

支持的功能码包括:

  • 01 读线圈
  • 02 读离散量输入
  • 03 读保持寄存器
  • 04 读输入寄存器
  • 05 写单个线圈
  • 06 写单个寄存器
  • 10 写多个寄存器

软件架构

本Modbus库基于状态机框架实现,因此具有极快的响应速度。不过,状态机本身没有缓存来保存信息,需要额外的变量存放对应数据,所以该框架占用的内存会比基于缓存对比的框架稍多一些。

安装教程

  1. 准备工程:准备好一个工程,可以是新建的空工程,也可以是旧工程。这里以新建工程为例。双击“新建工程.bat”,输入任意英文工程名,例如 modbus

ECBM库新建工程批处理操作界面

  1. 复制库文件:将 modbus.hmodbus.c 文件复制到你的工程文件夹中。

工程文件夹目录结构

  1. 添加文件到工程:打开工程,在任意工程文件夹(例如 DEVICE 文件夹)上双击,在弹出的选择框中双击 modbus.c 将其添加到工程。

Keil IDE中添加modbus.c文件到工程

  1. 包含头文件:打开 main.c,加载Modbus的头文件。
#include "ecbm_core.h" //加载ECBM库函数的头文件。
#include "modbus.h"     //加载Modbus的头文件。

void main(){            //main函数,必须的。
    system_init();      //系统初始化函数,也是必须的。
    while(1){
    }
}

至此,Modbus组件已完整添加到工程中。但仅仅添加还不够,因为 Modbus是基于串口的协议,我们还需要初始化串口。

  1. 配置单片机型号与时钟:好在ECBM库默认就支持串口。首先需要在 ecbm_core.h 中设置当前使用的单片机型号。强烈建议使用ECBM强大的图形化配置界面,只需点击窗口左下角的【Configuration Wizard】标签即可。
    例如,使用的单片机是STC8F2K32S2,按下图步骤设置。确保单片机时钟设置为【内部高速时钟HSI(标准)】,这样ECBM库会自动识别你在STC-ISP工具中设置的时钟频率,有效避免因时钟和波特率不匹配导致的通信问题。同时,确保【自动下载功能】开启,这不仅方便调试,也会自动初始化串口。

ECBM库单片机型号与系统时钟图形化配置

  1. 配置串口:打开 uart.h,进入图形化配置界面。将波特率修改为实际使用的值(例如115200)。使能接收,并打开串口1的接收回调函数。

串口1配置界面

  1. 实现串口回调与Modbus数据接口:在 main.c 中定义串口1接收回调函数 uart1_receive_callback,并将Modbus的接收函数放入其中。接着,定义Modbus读写串口所需的两个函数 ecbm_modbus_rtu_set_dataecbm_modbus_rtu_get_data
#include "ecbm_core.h" //加载库函数的头文件。
#include "modbus.h"     //加载modbus的头文件。

void main(){            //main函数,必须的。
    system_init();      //系统初始化函数,也是必须的。
    while(1){
    }
}

void uart1_receive_callback(void){ //串口1接收中断回调函数。
    ecbm_modbus_rtu_receive();     //将接收到的字节送入Modbus协议栈。
}

void ecbm_modbus_rtu_set_data(emu8 dat){ //Modbus发送数据接口函数。
    uart_char(1,dat);                    //调用ECBM库的串口发送函数。
}

emu8 ecbm_modbus_rtu_get_data(void){    //Modbus获取接收数据接口函数。
    return SBUF;                         //返回串口1的接收寄存器值。
}
  1. 配置定时器用于超时检测:Modbus-RTU协议需要超时机制来判断帧结束。打开 timer.h,进入图形化设置界面,任选一个定时器(例如定时器0)。设置为定时器模式,定时时间设为1ms。注意:定时初值需根据系统时钟计算,例如在24MHz下,1ms定时初值为24000。

定时器0配置界面

  1. 完成集成:回到 main.c,添加定时器初始化和启动代码,最后将Modbus的运行函数放入主循环。
#include "ecbm_core.h" //加载库函数的头文件。
#include "modbus.h"     //加载modbus的头文件。

void main(){
    system_init();
    timer_init();        //初始化定时器。
    timer_start(0);      //开启定时器0。
    while(1){
        ecbm_modbus_rtu_run(); //主循环中运行Modbus协议栈。
    }
}

void uart1_receive_callback(void){
    ecbm_modbus_rtu_receive();
}

void ecbm_modbus_rtu_set_data(emu8 dat){
    uart_char(1,dat);
}
emu8 ecbm_modbus_rtu_get_data(void){
    return SBUF;
}

void tim0_fun(void) TIMER0_IT_NUM { //定时器0中断服务函数。
    ECBM_MODBUS_RTU_TIMEOUT_RUN(); //执行Modbus超时检测。
}

至此,Modbus库已安装并配置完毕,可以正常使用。

使用说明

Modbus是基于串口的通信协议,主机通过访问从机的寄存器来完成参数设置或执行特定动作。其数据帧固定格式为:
【设备地址】+【功能码】+【起始地址】+【数据/长度】+【CRC校验】

本库目前支持01, 02, 03, 04, 05, 06, 10共7个功能码。

功能码详解

  • 【01】读线圈

    • 举例:主机发送 01 01 00 00 00 01 FD CA
    • 含义:读取地址为01的设备中,0000号线圈的值。
  • 【02】读离散量输入

    • 举例:主机发送 01 02 00 00 00 03 38 0B
    • 含义:读取地址为01的设备中,0000~0002号共3个离散输入量的值。
  • 【03】读保持寄存器

Modbus 03功能码格式

*   举例:主机发送 `01 03 00 0A 00 03 25 C9`
*   含义:读取地址为01的设备中,000A~000C号共3个保持寄存器的值。
  • 【04】读输入寄存器

Modbus 04功能码格式

*   举例:主机发送 `01 04 00 00 00 01 31 CA`
*   含义:读取地址为01的设备中,0000号输入寄存器的值。
  • 【05】写单个线圈

Modbus 05功能码格式

*   举例:主机发送 `01 05 00 0A FF 00 AC 38`
*   含义:将地址为01的设备中,000A号线圈的值设置为1(ON)。
  • 【06】写单个寄存器

    • 举例:主机发送 01 06 00 01 12 34 D5 7D
    • 含义:将地址为01的设备中,0001号寄存器的值设置为 0x1234
  • 【10】写多个寄存器

    • 举例:主机发送 01 10 00 0A 00 04 08 11 11 22 22 33 33 44 44 5D 5E
    • 含义:将地址为01的设备中,000A~000D号共4个寄存器的值分别设置为 0x1111, 0x2222, 0x3333, 0x4444

如何自定义Modbus寄存器

为方便使用,库默认提供了两个数组作为Modbus通信的寄存器:

  • ecbm_modbus_rtu_bit_buf: 用于存放线圈(位)数据。功能码01读、05写操作此数组。
  • ecbm_modbus_rtu_reg_buf: 用于存放寄存器(字)数据。功能码03读、06和10写操作此数组。

如果需要对接旧项目,或实现更复杂的逻辑(而非简单的数据存取),可以禁用自带的缓存数组,并自定义读写函数。

一、不使用库自带的线圈缓存

第一步:关闭线圈缓存使能。

关闭线圈缓存使能配置界面

第二步:自定义线圈读写函数。
定义 ecbm_modbus_cmd_write_bitecbm_modbus_cmd_read_bit 函数。

void ecbm_modbus_cmd_write_bit(emu16 addr,emu8 dat){
    if(addr==101){ //例如,地址101的线圈控制LED。
        if(dat==0){
            LED_OFF; //写入0则关闭LED。
        }else{
            LED_ON;  //写入非0则打开LED。
        }
    }
    if(addr==0){ //例如,地址0对应板载DCDC使能信号。
        dc_dc_en=dat; //将写入数据直接赋给使能变量。
    }
}

void ecbm_modbus_cmd_read_bit(emu16 addr,emu8 * dat){
    if(addr==101){ //读取LED状态。
        if(LED_PIN==0){ //假设LED低电平点亮。
            *dat=1; //LED亮,则返回1。
        }else{
            *dat=0; //LED灭,则返回0。
        }
    }
    if(addr==0){ //读取DCDC使能状态。
        *dat=dc_dc_en;
    }
}

二、不使用库自带的寄存器缓存

第一步:关闭寄存器缓存使能。

关闭寄存器缓存使能配置界面

第二步:自定义寄存器读写函数。
定义 ecbm_modbus_cmd_write_regecbm_modbus_cmd_read_reg 函数。

void ecbm_modbus_cmd_write_reg(emu16 addr,emu16 dat){
    if(addr<512){ //将地址0-511映射为OLED显存(128x64分辨率需512个16位寄存器)。
        OLED_BUF[addr]=dat;
    }else{ //地址512以上映射为MCU设置参数区。
        MCU_SETTING[addr-512]=dat;
        if(addr==512){ //当地址512的寄存器D0位被写1时,触发OLED刷新。
            if(dat & 0x0001){
                OLED_SHOW();
            }
        }
    }
}

void ecbm_modbus_cmd_read_reg(emu16 addr,emu16 * dat){
    if(addr<512){
        *dat=OLED_BUF[addr];
    }else{
        *dat=MCU_SETTING[addr-512];
    }
}

通过自定义函数,Modbus通信不仅能完成数据存储,还能直接触发设备动作(如控制LED、刷新屏幕),极大地扩展了应用场景。对于想深入理解底层机制的开发者,这是一个很好的开源实战案例。

图形化配置界面说明

图形化配置是ECBM库的特色。用Keil打开 modbus.h,点击左下角的【Configuration Wizard】标签即可进入。

MODBUS_RTU图形化配置界面总览

下面对各配置项进行说明:

本机地址/ID

在Modbus总线中用于区分不同设备的唯一地址。务必确保地址唯一,否则会导致总线冲突。

  • 注意:图形化界面设置的ID在编译后固定。如需动态修改地址,可以在程序中直接修改变量 ecbm_modbus_rtu_id 的值。

超时时间

用于处理串口通信中断。由于 Modbus-RTU 协议使用原始数据帧(0x00-0xFF皆可为数据),无法像Modbus-ASCII那样用特定字符标识帧头帧尾。因此,需要通过判断字符间隔时间(即超时)来判定一帧数据接收完毕。

  • 设置值含义:代表 ECBM_MODBUS_RTU_TIMEOUT_RUN() 函数需要执行的次数。
  • 计算示例:如图中设置为5,若 ECBM_MODBUS_RTU_TIMEOUT_RUN() 每10ms被调用一次,则超过 5 * 10 = 50ms 未收到新数据,即认为通信中断,Modbus状态机恢复为待接收状态。

线圈读写功能设置

“线圈”源于工业继电器,在Modbus中代表一个位(bit)寄存器。

  • 线圈缓存:使能后,库会定义 u8 型数组 ecbm_modbus_rtu_bit_buf 并实现配套的读写函数。如果移植到已有缓存的旧工程,或想完全自定义,请勿使能此选项。
  • 线圈缓存总数必须根据实际需求填写。单位是字节(Byte)。例如,需要10个线圈,需要2个字节存储,此处应填2。
  • 线圈起始地址:用于地址偏移,通常保持为0即可。
  • 线圈指令使能:勾选[01]和[05]以编译对应的指令解析代码。无需该功能可关闭以节省程序空间。

寄存器读写功能设置

寄存器为16位数据单元。

  • 寄存器缓存:使能后,库会定义 u16 型数组 ecbm_modbus_rtu_reg_buf 并实现读写函数。如需自定义,请勿使能。
  • 寄存器缓存总数:定义缓存数组大小,单位是(Word,16-bit)。按需填写。
  • 寄存器起始地址:用于地址偏移,通常为0。
  • 寄存器指令使能
    • [03] 读寄存器
    • [06] 写单个寄存器
    • [10] 写多个寄存器
    • 写入缓存总数:这是为功能码10准备的临时缓存。因为在CRC校验通过前,接收到的多个寄存器数据需要暂存。此处定义该缓存的大小(单位:字)。必须确保其大小不小于单次通信可能写入的最大寄存器数量,否则会导致数据溢出。

IO系统指令使能

用于处理只读的输入信号。

  • [02] 读离散量输入:需自定义 ecbm_modbus_cmd_read_io_bit 函数。
  • [04] 读输入寄存器:需自定义 ecbm_modbus_cmd_read_io_reg 函数。
    使能后,需要用户自己实现上述函数,以返回实际的输入状态或值。

项目仓库

本易移植、注释详尽的Modbus-RTU库托管在Gitee上,对于从事网络/系统通信或C/C++嵌入式开发的工程师来说,是一个很好的学习和参考资源。

https://gitee.com/ecbm/modbus

希望这篇详细的移植指南能帮助你快速上手。如果在使用中遇到问题,或对嵌入式通信协议有更深入的探讨需求,欢迎到云栈社区的相关板块与更多开发者交流。




上一篇:AI辅助重构KMP项目:用Codex将万行代码从Kotlin-inject迁移到Koin与Navigation3
下一篇:kkFileView Docker镜像从源码构建到部署实战指南(基于4.4.0版本)
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-7 20:38 , Processed in 0.307596 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表