# modbus **Repository Path**: bh3afl/modbus ## Basic Information - **Project Name**: modbus - **Description**: 全新的、可裁剪的modbus。一定是你没见过的版本。易移植易上手,大量注释爱不释手。 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 250 - **Created**: 2023-08-22 - **Last Updated**: 2023-08-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # modbus ## 介绍 全新的、可裁剪的modbus。一定是你没见过的版本。易移植易上手,大量注释爱不释手。 modbus协议是工控设备的标准通信协议,详细说明可见[百科说明](https://baike.baidu.com/item/Modbus%E9%80%9A%E8%AE%AF%E5%8D%8F%E8%AE%AE/5972462?fromtitle=ModBus&fromid=305501&fr=aladdin)。简单来说,modbus协议就是为了在主机设备上能拿到从机里的数据的协议。本仓库主要是收录了基础型的modbus框架,因此支持线圈(1位)和寄存器(16位)这两种数据类型(有的modbus支持浮点型或者32位型)。支持的功能码有:01读线圈、02读离散量输入、03读保持寄存器、04读输入寄存器、05写单个线圈、06写单个寄存器和10写多个寄存器。 ## 软件架构 本modbus基于状态机框架实现,因此具有极快的相应速度。不过状态机没有自己的缓存可以保存信息,因此需要额外的变量来存放对应的数据,所以该框架占用的内存会比缓存对比框架要多。 ## 安装教程 1. 先准备好一个工程,可以是新建的空的工程也可以是旧的工程。这里以新工程来举例。双击“新建工程.bat”,名字可以任意,只要是英文就行。这里随便写个modbus。 ![新建工程](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-1.gif) 2. 复制modbus.h和modbus.c到工程文件夹。 ![复制文件到工程](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-2.png) 3. 打开工程,在任意工程文件夹里双击,比如双击DEVICE文件夹,在弹出的选择框中双击modbus.c将其添加到工程中。 ![加入文件](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-3.gif) 4. 打开main.c,加载modbus的头文件。如下第二句: ```c #include "ecbm_core.h" //加载库函数的头文件。 #include "modbus.h" //加载modbus的头文件。 void main(){ //main函数,必须的。 system_init(); //系统初始化函数,也是必须的。 while(1){ } } ``` 至此modbus组件已完整的添加到工程中。但只是添加到工程中还不能使用modbus。modbus是基于串口的协议,因此我们还需要初始化串口。 5. 好在ECBM默认就是打开串口的,先到ecbm_core.h去设置当前使用单片机型号。当然设置的时候别忘了使用ECBM强大的图形化配置界面,只需要点击窗口左下角的【Configuration Wizard】标签就行。实例的单片机我用的是STC8F2K32S2,于是按下图的步骤设置。确保单片机时钟设置是【内部高速时钟HSI(标准)】,这样设置的话ECBM库会自己识别出你在stc-isp工具上设置的时钟频率。有了这个自动识别,你就不会因为时钟没调好、波特率不对而收不到数据啦。然后确保【自动下载功能】是打开的,一方面自动下载会让你的调试更加方便,另一方面该功能只要开启就会自动初始化串口,省事。 ![设置工程](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-4.gif) 6. 打开uart.h,进入到图形化配置界面。将波特率修改成实际使用的波特率,比如115200。使能接收并打开串口1的接收回调函数。 ![设置串口](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-5.png) 7. 在main.c中定义串口1回调函数,函数名为uart1_receive_callback。然后将modbus的接收函数放到串口1回调函数中。接着定义modbus读写串口的两个函数ecbm_modbus_rtu_set_data和ecbm_modbus_rtu_get_data。 ```c #include "ecbm_core.h" //加载库函数的头文件。 #include "modbus.h" //加载modbus的头文件。 void main(){ //main函数,必须的。 system_init(); //系统初始化函数,也是必须的。 while(1){ } } void uart1_receive_callback(void){//接收处理部分。 ecbm_modbus_rtu_receive(); } void ecbm_modbus_rtu_set_data(emu8 dat){//发送数据部分。 uart_char(1,dat);//ECBM库的发送函数。 } emu8 ecbm_modbus_rtu_get_data(void){//获取串口值部分。 return SBUF;//串口1的寄存器 } ``` 8. modbus还有接收超时的设定,因此还需要定时器的帮助。打开timer.h,进入图形化设置界面,任选一个定时器,比如定时器0。设置成定时器模式,然后定时时间为1mS。因为在实验平台上单片机工作在24MHz,所以定时1mS的初值是24000。 ![设置定时器](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-6.png) 9. 回到main.c,添加初始化定时器和运行定时器的代码。最后把modbus的运行函数写到循环中就行了。此时modbus已安装完毕,可以使用了。 ```c #include "ecbm_core.h" //加载库函数的头文件。 #include "modbus.h" //加载modbus的头文件。 void main(){ //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);//ECBM库的发送函数。 } emu8 ecbm_modbus_rtu_get_data(void){//获取串口值部分。 return SBUF;//串口1的寄存器 } void tim0_fun(void)TIMER0_IT_NUM {//定时器处理部分 ECBM_MODBUS_RTU_TIMEOUT_RUN(); } ``` ## 使用说明 modbus是基于串口的通信协议,用于电脑访问设备的寄存器来完成设置或者执行某些动作。其固定的数据格式为:【设备地址】+【功能码】+【起始地址】+【功能码相关】+【CRC校验】。本库目前支持01,02,03,04,05,06,10共7个功能码。 ### 功能码详解 #### 【01】读线圈 | 设备地址 | 功能码 | 起始地址 | 线圈数量 | CRC | | -------- | ------ | ------------- | -------- | ------------ | | 1~247 | 01 | 0x0000~0xFFFF | 1~2000 | 先低位后高位 | 举例:主机发送【01 01 00 00 00 01 FD CA】。意思是读取地址为01的设备中0000号线圈的值。 #### 【02】读离散量输入 | 设备地址 | 功能码 | 起始地址 | 线圈数量 | CRC | | -------- | ------ | ------------- | -------- | ------------ | | 1~247 | 02 | 0x0000~0xFFFF | 1~2000 | 先低位后高位 | 举例:主机发送【01 02 00 00 00 03 38 0B】。意思是读取地址为01的设备中0000~0002号3个离散量的值。 #### 【03】读保持寄存器 | 设备地址 | 功能码 | 起始地址 | 寄存器数量 | CRC | | -------- | ------ | ------------- | ---------- | ------------ | | 1~247 | 03 | 0x0000~0xFFFF | 1~125 | 先低位后高位 | 举例:主机发送【01 03 00 0A 00 03 25 C9 】。意思是读取地址为01的设备中000A~000C号3个寄存器的值。 #### 【04】读输入寄存器 | 设备地址 | 功能码 | 起始地址 | 寄存器数量 | CRC | | -------- | ------ | ------------- | ---------- | ------------ | | 1~247 | 04 | 0x0000~0xFFFF | 1~125 | 先低位后高位 | 举例:主机发送【01 04 00 00 00 01 31 CA 】。意思是读取地址为01的设备中0000号输入寄存器的值。 #### 【05】写单个线圈 | 设备地址 | 功能码 | 起始地址 | 线圈数量 | CRC | | -------- | ------ | ------------- | -------------- | ------------ | | 1~247 | 05 | 0x0000~0xFFFF | 0x0000或0xFF00 | 先低位后高位 | 举例:主机发送【01 05 00 0A FF 00 AC 38 】。意思是将地址为01的设备中000A号线圈的值设置为1。 #### 【06】写单个寄存器 | 设备地址 | 功能码 | 起始地址 | 寄存器值 | CRC | | -------- | ------ | ------------- | ------------- | ------------ | | 1~247 | 06 | 0x0000~0xFFFF | 0x0000~0xFFFF | 先低位后高位 | 举例:主机发送【01 06 00 01 12 34 D5 7D 】。意思是将地址为01的设备中0001号线圈的值设置为0x1234。 #### 【10】写多个寄存器 | 设备地址 | 功能码 | 起始地址 | 寄存器数量 | 字节计数 | 寄存器值 | CRC | | -------- | ------ | ------------- | ---------- | ------------ | ------------- | ------------ | | 1~247 | 10 | 0x0000~0xFFFF | 1~78 | 寄存器数量*2 | 0x0000~0xFFFF | 先低位后高位 | 举例:主机发送【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可以一次性写多个数据到数组里。 但是如果需要本库来对接旧项目,或者您的项目更加复杂,不是单纯的存取寄存器,那么可以取消自带的数组,然后定义新的读写函数即可。 比如不需要库自带的线圈缓存,第一步,关闭线圈缓存使能。![](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-7.png) 第二步,重建线圈读写函数。因为原来的读写函数都是操作自带缓存的,取消缓存之后函数也会失效。重建很简单,定义一个ecbm_modbus_cmd_write_bit函数和ecbm_modbus_cmd_read_bit函数就行。下面举个例子 ```c void ecbm_modbus_cmd_write_bit(emu16 addr,emu8 dat){ if(addr==101){//比如地址为101的线圈是管理者LED的亮灭状态的。 if(dat==0){//如果往101写入了0, LED_OFF;//就关闭LED。 }else{//否则 LED_ON;//就打开LED。 } } if(addr==0){//比如地址0,对应了板子上DCDC的使能。 dc_dc_en=dat;//将写入数据赋予使能。 } } void ecbm_modbus_cmd_read_bit(emu16 addr,emu8 * dat){ if(addr==101){//比如地址为101的线圈是管理者LED的亮灭状态的。 if(LED_PIN==0){//如果该引脚是低电平,说明LED是亮的。 *dat=1;//注意,这里返回1的原因是通常LED用低电平点亮。 }else{//否则 *dat=0;//返回0。 } } if(addr==0){//比如地址0,对应了板子上DCDC的使能。 *dat=dc_dc_en;//将使能状态返回去。 } } ``` 同理,如果不需要自带的寄存器缓存。也是分两步就OK。第一步,关闭寄存器缓存使能。![317-8](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-8.png) 第二步,重建寄存器读写函数,理由同上。分别是ecbm_modbus_cmd_write_reg和ecbm_modbus_cmd_read_reg。举个例子: ```c void ecbm_modbus_cmd_write_reg(emu16 addr,emu16 dat){ if(addr<512){//可以把512以下的地址作为OLED的缓存(128*64个点需要128x64/16=512个16位寄存器)。 OLED_BUF[addr]=dat; }else{//512以上的地址作为另一组。 MCU_SETTING[addr-512]=dat; if(addr==512){//当地址为512的寄存器的D0位 if(dat&0x0001){//被写1的时候,定义为用户要更新OLED。 OLED_SHOW();//刷新OLED显示。 } } } } void ecbm_modbus_cmd_read_reg(emu16 addr,emu16 * dat){ if(addr<512){//可以把512以下的地址作为OLED的缓存(128*64个点需要128x64/16=512个16位寄存器)。 *dat=OLED_BUF[addr]; }else{//512以上的地址作为另一组。 *dat=MCU_SETTING[addr-512]; } } ``` 由此可见,自定义的modbus寄存器不仅仅可以完成普通的储存功能,还能具备一些触发功能,可以通过下发指令来让单片机执行某些动作。这将极大的扩展modbus的使用场景。 ### 图形化配置界面说明 众所周知,图形化配置界面是ECBM系列的特点,自然在本modbus库也不例外的。![317-9](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-9.png) 首先用keil打开modbus.h,在左下角找到Configuration Wizard标签。点击即可进入图形化配置界面。这个界面是keil的功能,因此用IAR和GCC是没有办法享受到的喔。![317-10](https://gitee.com/ecbm/modbus/raw/master/%E5%9B%BE%E7%89%87/317-10.png) 下面我来一一说明: #### 本机地址/ID 顾名思义,在总线中为了区分不同设备而设立的一个唯一的地址。在使用中要确保该地址是唯一的,否则在总线中就会冲突。由于图形化配置界面设置的信息在编译后就不能修改了,所以要实现动态ID地址可以通过修改变量ecbm_modbus_rtu_id来实现。 #### 超时时间 在串口通信中,可能会遇到通信中断的情况。由于modbus-rtu在传输上使用了原始数据传输,也就是说0x00到0xFF都可以用来表示数据,那么就没有哪个值是可以**专门**用来当帧头帧尾的了。在这一点上,modbus-ascii就做得很好,它只用字符0到9和A到F表示数据,用:号表示帧头,这样就可以通过帧头来判断一个数据帧的开始和结束。由于modbus-rtu不能这样操作,那么为了能使modbus从机在通信中断后可以恢复到待接收状态,需要设定一个时间,这个时间就是超时时间。在图形化配置界面设置中,设置值代表ECBM_MODBUS_RTU_TIMEOUT_RUN执行的次数。比如图中设置为5,如果ECBM_MODBUS_RTU_TIMEOUT_RUN每10mS运行一次,那么只要超过5*10=50mS没收到串口数据,就认为是通信中断,然后modbus恢复待接收态。 #### 线圈读写功能设置 线圈这个概念来源于以前的工业设备,一个线圈就是指一个继电器,继电器通常是单刀双掷可以输出高低电平,所以说白了线圈就是一个位寄存器可以储存一个比特的信息。 ##### 线圈缓存 这个缓存其实也就是一个u8型数组,由于线圈就是一个比特的数据,用一个字节来存一个比特太浪费空间了。为了方便大家往u8型数组里存比特数据,本库已经把这个算法写好了。**使能这个选项时**,本库会定义一个数组ecbm_modbus_rtu_bit_buf,同时会将对应的读写函数ecbm_modbus_cmd_write_bit,ecbm_modbus_cmd_read_bit定义好,通过调用读写函数就可以读写对应的线圈。如果是移植本库到以前的工程中,也就是说在原来的工程中已经有了缓存了,**那么可以不使能本选项**。不使能的话,数组ecbm_modbus_rtu_bit_buf就不会定义,同时读写函数也需要你自己去写了。毕竟作者也无法预料你的旧工程用的是什么缓存嘛。 ##### 线圈缓存总数 这个设置涉及modbus通信的地址和数据总数判断,所以**一定要根据实际情况填写**。数值的单位是字节,如果需要10个线圈,要两个字节可以放得下,那么就应该填上2。 ##### 线圈起始地址 这个是应对特殊要求的,对于大部分用户而言,保持为0就可以了。 ##### 线圈指令使能 主要涉及两个指令:一个用于读线圈的01指令,一个用于写线圈的05指令。使能对应指令,可以让本库编译相应的指令解析代码。所以有需求就可以使能,没有需求就不使能以节省程序空间。 #### 寄存器读写功能设置 寄存器这个概念比线圈好理解,就和单片机的寄存器差不多一个意思,然后modbus的寄存器是16位的。 ##### 寄存器缓存 和上面的线圈一样,为了方便大家,读写的算法已经写好。**使能缓存**就会定义u16型数组ecbm_modbus_rtu_reg_buf还有读写函数ecbm_modbus_cmd_write_reg和ecbm_modbus_cmd_read_reg。如果是移植到旧工程或者想自定义读写函数,就**不使能这个选项**,然后自己定义读写函数就好了。 ##### 寄存器缓存总数 这个参数是用于定义缓存数组,请根据实际情况填写。由于u16型是可以直接定义的,所以本选项的单位是字(1字节=8比特、一字=16比特)。用多少就填多少。 ##### 寄存器起始地址 这个是应对特殊要求的,对于大部分用户而言,保持为0就可以了。 ##### 寄存器指令使能 主要涉及三个指令:一个用于读寄存器的03指令,两个用于写寄存器的06指令和10指令。其中06指令一次只能写一个寄存器。10指令可以一次写多个寄存器。使能对应指令,可以让本库编译相应的指令解析代码。所以有需求就可以使能,没有需求就不使能以节省程序空间。另外在10指令那里,还有个选项是写入缓存总数的,这是因为modbus通信都是需要计算CRC的。如果CRC错误则本次传输的信息全部作废。因此在CRC结果出来之前,传入的数据都会放在写入缓存中。而这个选项就是定义这个缓存的大小,单位也是字(16比特)。如果写入缓存的定义为10,实际通信传输了15,那么就会因为数据溢出导致程序出问题,所以还是注意要符合实际。 #### IO系统指令使能 这里包含了两个指令,一个是02指令读离散量输入,另一个是04指令读输入寄存器。由于外界输入千变万化,所以这两个指令对应函数没有定义。如果有需要就先使能,然后定义相应的读函数。02指令对应的是ecbm_modbus_cmd_read_io_bit函数。04指令对应的是ecbm_modbus_cmd_read_io_reg函数。 ## 更新历史 ### V1.0.4(2023-01-29) 1. 放宽了地址的限制,在不适用内部数组缓存的情况下,将不会对协议地址的范围进行二次限定。 ### V1.0.3(2021-12-22) 1. 续写了modbus的使用手册(添加了图形化配置界面的说明)。 ### V1.0.3(2021-10-23) 1. 剔除了stream库,因为stream库已经独立。 2. 丰富了modbus库的使用手册(添加自定义寄存器说明和操作说明)。 ### V1.0.2(2021-8-16) 1. 修改了例程中uint16_t和uint8_t类型未定义的问题。 ### V1.0.2(2021-03-18) 1. 修改u8、u16、u32数据类型的名字,解决了重定义错误的出现。 2. 新增安装说明。 3. 更新图片。 ### V1.0.1(2021-03-16) 1. 优化了图形界面的逻辑。 2. 扩大了宏定义的作用范围,现在支持自定义读写函数和共用逻辑块。 3. 验证了01、03、05、06功能码。 4. 新增了0xFF的广播地址。 5. modbus自组头文件,成为独立的库。 6. 02和04指令暂时移出,需要用户自己填写。同时他们的异常码04无法使用。 ### V1.0.0(2021-03-15) 1. 更新了modbus第一版,支持01、02、03、04、05、06、16功能码。 2. 增加了基于硬件(STC8F2K32S2)和软件(ECBM V3库)的示例工程。