前言:
...
Modbus 协议简介
Modbus协议是工业领域中一种非常流行的通信协议,用于连接工业电子设备,如可编程逻辑控制器(PLCs)、传感器、执行器和其他工业自动化组件,Modbus协议最初由Modicon公司(现在的施耐德电气的一部分)在1979年开发,目的是为了使不同制造商的设备能够在同一网络中通信,由于其开放性和简单性,Modbus迅速成为工业通信的标准之一。
Modbus 协议特性
开放性:Modbus是一个公开的协议,任何设备制造商都可以实现和使用它,无需支付许可费用。
简单性:Modbus的设计相对简单,易于理解和实现,这使得它成为工业现场总线技术中最为人所熟知的一种。
多功能性:Modbus支持多种功能代码,可以用于读取或写入设备的输入/输出状态、寄存器值、诊断信息等。
可靠性:Modbus支持错误检查机制,如CRC校验,确保数据传输的准确性和完整性
Modbus 通信模式
Modbus通信基于主从架构:
- 主设备(Master):负责发起所有通信请求。
- 从设备(Slave):接收并响应主设备的请求,每个从设备都有一个唯一的地址。
Modbus物理层
Modbus协议可以在不同的物理层上运行,主要有三种类型:
- Modbus RTU (Remote Terminal Unit):使用串行接口(如RS-485),适合于噪声较大的工业环境,数据以二进制格式传输(最常使用)。
- Modbus ASCII:也使用串行接口,但数据以ASCII字符格式传输,主要用于调试。
- Modbus TCP/IP:通过以太网传输,利用TCP/IP协议栈,提供更快的速度和更大的网络覆盖范围。
Modbus消息结构
Modbus的消息通常包含以下部分:
- 设备地址:用于识别目标从设备。
- 功能代码:指定要执行的操作,如读取寄存器、写入寄存器等。
- 数据:根据功能代码,包含读取或写入的具体数据。
- 错误检测:如CRC校验或LRC校验,用于确保数据的完整性和准确性。
RS485 和 Modbus的关系
Modbus RTU协议
Modbus RTU (Remote Terminal Unit):使用串行接口(如RS-485),适合于噪声较大的工业环境,数据以二进制格式传输。
• 从机地址: 每个从机都有唯一地址(机没有地址),占用一个字节,范围0-255,其中从机有效地址范围是1-247;
• 功能码: 占用一个字节,功能码的意义是告诉从机这帧数据是干啥的,可以查询从机的数据,也可以修改从机的数据;
• 数据: 根据功能码不同,对应不同内容,比如功能码是查询从机的数据,这里就包括要查询哪些数据和查询字节数等;
• 校验: 在传输过程中数据坑会发生错误,CRC检验可以检测接收的数据是否正确
CRC 校验用于检测接收的数据是否正确,如果使用Modbus协议,尽量对数据进行CRC校验,否则很有可能数据在传输的过程中出现数据跳变,使用CRC校验保证数据的完整性,一帧数据最大是250个字节。
Modbus 功能码
框起来的是比较常用的功能字
注:这里的寄存器是一个虚拟的不是物理上的寄存器,可以理解为软件当中的一个控制项,如我们在写程序的时候去控制传感器或者继电器获取数据和开关是控制项,不是单片机片上外设上的寄存器,功能码是定义好的不需要去深究,03 06 16 是按照2个字节进行访问的。
寄存器,可以理解为设备上的某一个控制项,比如传感器或者继电器,对应的需要在软件中为每一个分配码值,在Mdobus中叫做寄存器地址。
对应的地址就是我们的寄存器地址是我们给每一个控制项分配明确的唯一的地址,和控制项中跟着的是对应的数据,获取的数据可以根据自己实际的需求进行设计,控制项在软件设计的时候要明确是可读的还是可读和可写的(在软件设计的过程中是需要明确的)。
Modbus 通信演示
主机给从机发送数据:
控制从机开机:
使用的功能码是06写一个控制器,地址是06,发送的数据高字节是00,低字节是01,然后是两个CRC校验码。这个控制项只有两种状态 0 代表的是关机,01代表的是开机。从机返回的数据是将数据原封不动的返回。
Modbus 协议规定
为了使软件更容易判断一帧数据包是否结束,Modbus规定两帧数据包之间至少有3.5 个字符时间的空闲间隔,叫做3.5T:(也就是在一包数据的前后都要有3.5T的间隔时间)因为发送的数据是不定长度的,直到数据都被解析完毕。
当串口波特率大于19200时,那么3.5T固定为1750us;1.75ms
当串口波特率小于等于19200时,假如串口设置为:起始位1bit + 数据位8bit + 停止位1bit,
• 1个字符传送所需时间,charTime=(10*1000/baudRate)ms ;
• 假设波特率为9600,charTime=11*1000/9600=1.15ms;
• 3.5个字符需要的时间,3.5*charTime=4ms。
注:当串口波特率小于等于19200时 那么3.5T就需要进行计算了,如何计算:假如我么将串口的模式设置为起始位 1 位, 数据位 8 位, 停止位 1 位,那么总共数10位,那么一个字符传输所需要的时间1个字符传送所需时间,charTime=(10*1000/baudRate)ms = 1.51ms,此时3.5个字符说需要的时间就是3.5 * 1.14 = 4ms左右,“一般我们使用定时器来实现”,这是Moodbus协议规定的无需去深究记住即可。
Modbus 异常处理
错误码表示:
01 表示的是非法的功能码,表示从机不支持功能码
02 表示非法地址,指定的地址在从机中不存在
03 表示非法数据值,指定的数据超过范围不允许使用
04 从机故障 ,表示从机处理响应的过程中,出现未知错误
注:以下的内容仅仅是异常处理的引子,用于更好的理解后续异常处理的功能值设置为什么是0x86,
在C或C++编程语言中,表达式 P_SW1 |= (1<<4);
是用于操作寄存器的典型方式,尤其是在嵌入式系统编程中。这里的每个部分都有特定的意义:
P_SW1
是一个寄存器的名字,它通常控制着一些硬件功能,比如I/O端口的状态。|=
是按位或赋值运算符,它会将左边的操作数与右边的操作数进行按位或运算,然后将结果赋值给左边的操作数。(1<<4)
这是一个位移操作,它将数字1左移四位。这意味着原本二进制表示的1(即0001)现在变成了0001 << 4,即10000,或者在二进制下是0000 0001左移四位变成0001 0000。在十进制中,这相当于16。
整个表达式 P_SW1 |= (1<<4);
的作用是将寄存器 P_SW1
中第5位(从右到左数,第0位是最低位)设置为1。如果 P_SW1
原来的值是 0000 0000
或者任何其他值,执行此操作后,第5位会被置为1,而不会改变其他位的状态。
例如,如果 P_SW1
的原始值是 0000 0011
,则执行 P_SW1 |= (1<<4);
后,其值会变为 0001 0011
。这是因为 0000 0011
和 0001 0000
进行按位或运算后,得到的结果是 0001 0011
。
Modbus有关异常的处理:
Modbus 软件架构
软件架构的分层,应用层,中间件对应的是freeModbus库,驱动就是程序的驱动,底层是hal库。
拿来主义使用Modbus的库函数文件,使用已经写好的库,我们只需要实现驱动的处理进行设置。
Moodbus的移植
注:如果没有资料包就去github上获取下载。
Modubus的内容文件夹
拷贝BARE问价夹下的4个C语言文件
具体的结果如下所示:
function里面只保留以下的几部分:
代码工程如下所示:
只这个代码工程还将我们刚才添加的文件添加进去,同时在C/C++这个位置添加头文件的路径,具体操作如下所示:
注:将没有使用到的设置为0
以下是使能串口的外部中断,modbus是由freemodbus库控制的
切换485芯片工作模式的IO口,用于切换模式,看是用来发送还是用来接收。
初始化串口函数:设置的是八位的数据长度,没有起始位,没有停止位,没有校验位
static void UartInit(uint32_t baudRate)
{
/* 使能UART时钟;*/
rcu_periph_clock_enable(g_uartHwInfo.rcuUart);
/* 复位UART;*/
usart_deinit (g_uartHwInfo.uartNo);
/* 在USART_BAUD寄存器中设置波特率;*/
usart_baudrate_set(g_uartHwInfo.uartNo, baudRate);
/* 在USART_CTL0寄存器中设置TEN位,使能发送功能;*/
usart_transmit_config(g_uartHwInfo.uartNo, USART_TRANSMIT_ENABLE);
/* 在USART_CTL0寄存器中设置TEN位,使能接收功能;*/
usart_receive_config(g_uartHwInfo.uartNo, USART_RECEIVE_ENABLE);
/* 使能串口中断;*/
nvic_irq_enable(g_uartHwInfo.irq, 0, 0);
/* 在USART_CTL0寄存器中置位UEN位,使能UART;*/
usart_enable(g_uartHwInfo.uartNo);
}
为了减少告警将参数前面加上强制类型转换:同时将return false 修改位return ture
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
(void)ucPORT;
(void)ucDataBits;
(void)eParity;
SwitchInit();
GpioInit();
UartInit(ulBaudRate);
return TRUE;
}
如果设置为100us表示的是定时器100us以后就要产生中断进入中断服务函数
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
/* 使能定时器时钟;*/
rcu_periph_clock_enable(RCU_TIMER3);
/* 复位定时器;*/
timer_deinit(TIMER3);
timer_parameter_struct timerInitPara;
timer_struct_para_init(&timerInitPara);
/* 设置预分频器值;*/
timerInitPara.prescaler = 5999; // 频率120MHZ / 6000 = 20khz,对应周期50us
/* 设置自动重装载值;*/
timerInitPara.period = usTim1Timerout50us - 1;
timer_init(TIMER3, &timerInitPara);
/* 使能定时器的计数更新中断;*/
timer_interrupt_enable(TIMER3, TIMER_INT_UP);
/* 使能定时器中断和优先级;*/
nvic_irq_enable(TIMER3_IRQn, 0, 0);
/* 使能定时器;*/
//timer_enable(TIMER3);
return TRUE;
}
如何换算出来的
Modus库的执行流程
FreeModubus主流程
FreeModubs串口接收中断部分
定时器中断的执行流程
Modubus 定时器中断服务函数部分
/*
* FreeModbus Libary: BARE Port
* Copyright (C) 2006 Christian Walter <wolti@sil.at>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* File: $Id$
*/
/* ----------------------- Platform includes --------------------------------*/
#include "port.h"
#include "gd32f30x.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR( void );
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
/* 使能定时器时钟;*/
rcu_periph_clock_enable(RCU_TIMER3);
/* 复位定时器;*/
timer_deinit(TIMER3);
timer_parameter_struct timerInitPara;
timer_struct_para_init(&timerInitPara);
/* 设置预分频器值;*/
timerInitPara.prescaler = 5999; // 频率120MHZ / 6000 = 20khz,对应周期50us
/* 设置自动重装载值;*/
timerInitPara.period = usTim1Timerout50us - 1;
timer_init(TIMER3, &timerInitPara);
/* 使能定时器的计数更新中断;*/
timer_interrupt_enable(TIMER3, TIMER_INT_UP);
/* 使能定时器中断和优先级;*/
nvic_irq_enable(TIMER3_IRQn, 0, 0);
/* 使能定时器;*/
//timer_enable(TIMER3);
return TRUE;
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
timer_counter_value_config(TIMER3, 0);//清零定时器
timer_interrupt_flag_clear(TIMER3, TIMER_INT_UP);//清除定时器更新中断
timer_interrupt_enable(TIMER3, TIMER_INT_UP);//使能定时器更新中断
timer_enable(TIMER3);//使能定时器
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
timer_counter_value_config(TIMER3,0);//清零定时器
timer_interrupt_flag_clear(TIMER3, TIMER_INT_UP);//清除定时器更新中断
timer_interrupt_disable(TIMER3, TIMER_INT_UP);//禁用定时器更新中断
timer_disable(TIMER3);//失能定时器
}
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
void TIMER3_IRQHandler(void)
{
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) == SET)
{
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
timer_counter_value_config(TIMER3, 0);
prvvTIMERExpiredISR();
}
}
注:以下的部分代码是定时器的初始化部分
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
/* 使能定时器时钟;*/
rcu_periph_clock_enable(RCU_TIMER3);
/* 复位定时器;*/
timer_deinit(TIMER3);
timer_parameter_struct timerInitPara;
timer_struct_para_init(&timerInitPara);
/* 设置预分频器值;*/
timerInitPara.prescaler = 5999; // 频率120MHZ / 6000 = 20khz,对应周期50us
/* 设置自动重装载值;*/
timerInitPara.period = usTim1Timerout50us - 1;
timer_init(TIMER3, &timerInitPara);
/* 使能定时器的计数更新中断;*/
timer_interrupt_enable(TIMER3, TIMER_INT_UP);
/* 使能定时器中断和优先级;*/
nvic_irq_enable(TIMER3_IRQn, 0, 0);
/* 使能定时器;*/
//timer_enable(TIMER3);
return TRUE;
}
注:以下的部分代码是使能定时器的更新中断部分,定时器的自动开启和关闭
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
timer_counter_value_config(TIMER3, 0);//清零定时器
timer_interrupt_flag_clear(TIMER3, TIMER_INT_UP);//清除定时器更新中断
timer_interrupt_enable(TIMER3, TIMER_INT_UP);//使能定时器更新中断
timer_enable(TIMER3);//使能定时器
}
注:以下的部分代码是失能定时器的更新中断
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
timer_counter_value_config(TIMER3,0);//清零定时器
timer_interrupt_flag_clear(TIMER3, TIMER_INT_UP);//清除定时器更新中断
timer_interrupt_disable(TIMER3, TIMER_INT_UP);//禁用定时器更新中断
timer_disable(TIMER3);//失能定时器
}
注:以下的代码是定时器的中断函数部分,
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
void TIMER3_IRQHandler(void)
{
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) == SET)
{
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
timer_counter_value_config(TIMER3, 0);
prvvTIMERExpiredISR();
}
}
在程序的最后部分函数调用了prvvTIMERExpiredISR();产生中断之后表示定时器有3.5T(周期)的间隔,表示接收完一包的数据。
Modbus 串口驱动层
putbyte发送一个字节的数据:在中断服务函数中间接调用到的
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
usart_data_transmit(g_uartHwInfo.uartNo, ucByte);
return TRUE;
}
getbyte接收一个字节的数据
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
*pucByte = usart_data_receive(g_uartHwInfo.uartNo);
return TRUE;
}
串口使能和使能中断服务函数
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if(TRUE == xRxEnable)
{
usart_interrupt_enable(g_uartHwInfo.uartNo, USART_INT_RBNE);//使能接收中断
gpio_bit_reset(GPIOC, GPIO_PIN_5);
}
else
{
usart_interrupt_disable(g_uartHwInfo.uartNo, USART_INT_RBNE);//失能接收中断
gpio_bit_set(GPIOC, GPIO_PIN_5);
}
if(TRUE == xTxEnable)
{
usart_interrupt_enable(g_uartHwInfo.uartNo, USART_INT_TC);//使能发送中断
gpio_bit_set(GPIOC, GPIO_PIN_5);
}
else
{
usart_interrupt_disable(g_uartHwInfo.uartNo, USART_INT_TC);//失能发送中断
gpio_bit_reset(GPIOC, GPIO_PIN_5);
}
}
注:这个函数的含义是当在库函数中调用vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )这个函数时,如果设置为true就是使能中断,或者是失能中断,后面的是发送使能和发送失能,(gpio_bit_reset(GPIOC, GPIO_PIN_5);)这个的作用是切换485芯片工作的模式,具体参考原理图进行设置。
TBE: 中断标志位表示的是数据寄存器为空,产生标志位
TC: 发送移位寄存器为空,产生标志位
串口中断服务函数
static void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
/* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
static void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
void USART1_IRQHandler(void)
{
//判断接收还是发送中断
if (RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE))
{
//接收完成中断
prvvUARTRxISR();
usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE_ORERR);
}
if(RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_TC))
{
//发送完成中断
prvvUARTTxReadyISR();
usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_TC);
}
}
注:如果是接收中断就调用 prvvUARTRxISR(); 接收中断函数,如果是发送中断就调用prvvUARTTxReadyISR();发送中断服务函数 ,需要记得清空标志位。
Modbus的一些小问题修改
注:以上移植的工作就完成了,后续是业务逻辑的实现
Modbus 业务应用层的代码
这个函数的声明位于mb.h这个头文件
第一个参数是指针类型指向的是数组,如果表示的是通过这个接口函数是要写入数据比如控制从机开机,那么对应表示的是这两个字节的数据。
如果表示的是从从机读走数据那么他所表示的是从内容的数据字节开始到CRC校验码之前。
第二个参数表示的含义:第二个参数表示的含义不是从机的地址而是寄存器的地址
第三个参数表示的是寄存器的数量
最后一个参数代表是读还是写是一个枚举类型
Modbus 实现接口函数
查看这个函数的返回值
注:这里面的返回值代表的是错误码,以下是对应的返回值错误码参数
typedef enum
{
MB_ENOERR, /*!< no error. */
MB_ENOREG, /*!< illegal register address. */
MB_EINVAL, /*!< illegal argument. */
MB_EPORTERR, /*!< porting layer error. */
MB_ENORES, /*!< insufficient resources. */
MB_EIO, /*!< I/O error. */
MB_EILLSTATE, /*!< protocol stack in illegal state. */
MB_ETIMEDOUT /*!< timeout error occurred. */
} eMBErrorCode;
实现函数回调:间接的使用业务逻辑层的代码
Modbus 回调编写
应用层向中间件传递数据
#ifndef _MODBUS_SLAVE_H_
#define _MODBUS_SLAVE_H_
#include <stdint.h>
#include "mb.h"
typedef struct {
eMBErrorCode (*ReadRegs)(uint8_t startAddr, uint8_t regNum, uint8_t *buf);
eMBErrorCode (*WriteRegs)(uint8_t startAddr, uint8_t regNum, uint8_t *buf);
} ModbusFuncCb_t;
typedef struct {
uint8_t slaveAddr; //从机地址
uint32_t baudRate; //波特率
ModbusFuncCb_t cb; //回调函数
} ModbusSlaveInstance_t;
void ModbusSlaveInit(ModbusSlaveInstance_t *mbInstance);
#endif
注:应用层向中间件传输数据主要包含,从机的地址 ,波特率,回调函数(函数指针)包含在一个结构体里,读寄存器和写寄存器的函数指针变量返回值是刚才返回的业务应用代码。
void ModbusSlaveInit(ModbusSlaveInstance_t *mbInstance);
注:这行代码的含义是给业务应用层开辟的接口函数,通过这个函数业务应用层调用这个接口函数传递参数数据。
Modbus_slave.c代码
static ModbusFuncCb_t g_modbusFuncCb;
注:以上定义的是一个静态全局变量,对应的结构体类型是以下部分代码的结构体
typedef struct {
eMBErrorCode (*ReadRegs)(uint8_t startAddr, uint8_t regNum, uint8_t *buf);
eMBErrorCode (*WriteRegs)(uint8_t startAddr, uint8_t regNum, uint8_t *buf);
} ModbusFuncCb_t;
之后通过eMBEnable()使能 modbus,然后将应用层的值赋值给全局的结构体变量
void ModbusSlaveInit(ModbusSlaveInstance_t *mbInstance)
{
eMBInit(MB_RTU, mbInstance->slaveAddr, 0, mbInstance->baudRate, MB_PAR_NONE);
eMBEnable();
//g_modbusFuncCb.ReadRegs = mbInstance->cb.ReadRegs;
//g_modbusFuncCb.WriteRegs = mbInstance->cb.WriteRegs;
g_modbusFuncCb = mbInstance->cb;
}
实现回调函数部分的代码:
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode state;
if (eMode == MB_REG_READ){
state = g_modbusFuncCb.ReadRegs(usAddress, usNRegs, pucRegBuffer);
}
else
{
state = g_modbusFuncCb.WriteRegs(usAddress, usNRegs, pucRegBuffer);
}
return state;
}
....