Modbus协议是工业控制设备的标准通信协议,其核心目的是让主机设备能够获取或设置从机设备内部的数据。本仓库提供了一个基础型Modbus框架,支持线圈(1位)和寄存器(16位)这两种最常用的数据类型。
支持的功能码包括:
- 01 读线圈
- 02 读离散量输入
- 03 读保持寄存器
- 04 读输入寄存器
- 05 写单个线圈
- 06 写单个寄存器
- 10 写多个寄存器
软件架构
本Modbus库基于状态机框架实现,因此具有极快的响应速度。不过,状态机本身没有缓存来保存信息,需要额外的变量存放对应数据,所以该框架占用的内存会比基于缓存对比的框架稍多一些。
安装教程
- 准备工程:准备好一个工程,可以是新建的空工程,也可以是旧工程。这里以新建工程为例。双击“新建工程.bat”,输入任意英文工程名,例如
modbus。

- 复制库文件:将
modbus.h 和 modbus.c 文件复制到你的工程文件夹中。

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

- 包含头文件:打开
main.c,加载Modbus的头文件。
#include "ecbm_core.h" //加载ECBM库函数的头文件。
#include "modbus.h" //加载Modbus的头文件。
void main(){ //main函数,必须的。
system_init(); //系统初始化函数,也是必须的。
while(1){
}
}
至此,Modbus组件已完整添加到工程中。但仅仅添加还不够,因为 Modbus是基于串口的协议,我们还需要初始化串口。
- 配置单片机型号与时钟:好在ECBM库默认就支持串口。首先需要在
ecbm_core.h 中设置当前使用的单片机型号。强烈建议使用ECBM强大的图形化配置界面,只需点击窗口左下角的【Configuration Wizard】标签即可。
例如,使用的单片机是STC8F2K32S2,按下图步骤设置。确保单片机时钟设置为【内部高速时钟HSI(标准)】,这样ECBM库会自动识别你在STC-ISP工具中设置的时钟频率,有效避免因时钟和波特率不匹配导致的通信问题。同时,确保【自动下载功能】开启,这不仅方便调试,也会自动初始化串口。

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

- 实现串口回调与Modbus数据接口:在
main.c 中定义串口1接收回调函数 uart1_receive_callback,并将Modbus的接收函数放入其中。接着,定义Modbus读写串口所需的两个函数 ecbm_modbus_rtu_set_data 和 ecbm_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的接收寄存器值。
}
- 配置定时器用于超时检测:Modbus-RTU协议需要超时机制来判断帧结束。打开
timer.h,进入图形化设置界面,任选一个定时器(例如定时器0)。设置为定时器模式,定时时间设为1ms。注意:定时初值需根据系统时钟计算,例如在24MHz下,1ms定时初值为24000。

- 完成集成:回到
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】读保持寄存器

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

* 举例:主机发送 `01 04 00 00 00 01 31 CA`
* 含义:读取地址为01的设备中,0000号输入寄存器的值。

* 举例:主机发送 `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_bit 和 ecbm_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_reg 和 ecbm_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】标签即可进入。

下面对各配置项进行说明:
本机地址/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
希望这篇详细的移植指南能帮助你快速上手。如果在使用中遇到问题,或对嵌入式通信协议有更深入的探讨需求,欢迎到云栈社区的相关板块与更多开发者交流。