STM32H743-ARM例程30-Modbus
目录
- 实验平台
- Modbus
- Modbus主/从协议原理
- Modbus消息帧
- 传输模式
- 字符如何串行传送
- Modbus报文RTU帧
- CRC计算
- Modbus功能码定义
- 常用功能码描述
- freeModbus移植
- 实验代码
- 实验现象
实验平台
硬件:银杏科技GT7000双核心开发板-ARM-STM32H743XIH6,银杏科技iToolXE仿真器
软件:最新版本STM32CubeH7固件库,STM32CubeMX v6.10.0,开发板环境MDK v5.35,串口工具
Modbus
Modbus 是由 Modicon(现为施耐德电气公司的一个品牌)在 1979 年发明的,是全球第
一个真正用于工业现场的总线协议。
Modbus 网络是一个工业通信系统,由带智能终端的可编程序控制器和计算机通过公用
线路或局部专用线路连接而成。其系统结构既包括硬件、也包括软件。
Modbus 协议是应用于电子控制器上的一种通信语言。通过此协议,控制器互相之间、
控制器经由网络和其它设备之间可以通信。它已经成为一通用工业标准。不同厂商生产的控
制设备可以连成工业网络,进行集中监控。
Modbus 是 OSI 模型第七层上的应用层报文传输协议,它在连接至不同类型总线或网络
的设备之间提供客户机、服务器通信。
| 层数 | OSI模型 | 对应协议或硬件 |
|---|---|---|
| 7 | 应用层 | Modbus协议 |
| 6 | 表示层 | 空 |
| 5 | 会话层 | 空 |
| 4 | 传输层 | 空 |
| 3 | 网络层 | 空 |
| 2 | 数据链路层 | Modbus 串行链路协议 |
| 1 | 屋里层 | RS-485/RS-232 |
Modbus 串行链路协议是一个主/从协议,该协议位于 OSI 模型的第二层。一个主从类型
的系统有一个向某个“子”节点发出显式命令并处理响应的节点(主节点)。典型的子节点在没有收到主节点的请求时并不主动发送数据,也不与其它子节点通信,简单来说就是子节点只听老大的命令,也不与其它同事交流。
在物理层,可以使用的物理接口是:RS485 和 RS232,最常用的是 TIA/EIA-485(RS485)
两线制接口。
Modbus主/从协议原理
Modbus 串行链路协议是一个主-从协议,在同一时刻,只有一个主节点,一个或多个子
节点连接于同一串行总线。子节点不会主动发送数据,只有在收到来自主节点的请求时才会
发送,主节点在同一时刻只会发起一个 Modbus 事务处理。
为了方便理解,我们将主节点以及子节点分别称为主设备和从设备。
主设备可单独与从设备通信,也能以广播方式和所有从设备通信。如果是单独通信,从
设备返回一消息作为回应;如果是广播方式查询的,则不作任何回应。
当数据帧到达终端设备(从设备)时,它通过一个简单的“端口”进入被寻址到的设备,该设
备去掉数据帧的“信封”(数据头),读取数据,如果没有错误,就执行数据所请求的任务,
然后将自己生成的数据加入到取得的“信封”中,把数据帧返回给发送者。返回的响应数据中
包含了以下内容:终端从机地址、被执行了的命令、执行命令生成的被请求数据和一个校验
码。发生任何错误都不会有成功的响应,或者返回一个错误指示帧。
Modbus消息帧
Modbus 协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。特定总线或网络上的 Modbus 协议映射能够在应用数据单元(ADU)上引入一些附加域。

地址域在帧的开始部分,由一个字节(8位二进制)组成,十进制位0255,在我们系统中只使用1147,其它地址保留。这些位标明了用户指定的从设备的地址,该设备将接受来自与之相连主设备数据。每个从设备的地址必须是唯一的,仅仅被寻址到的从设备会响应包含了该地址的查询。当从设备发送回一个响应,响应中的从设备地址数据便告诉了主设备是哪台设备与之进行通信。
功能码的作用是指明从设备要执行的动作。
数据域包括附加信息,从设备使用这个信息执行功能码定义的操作。这个域还包括离散项目和寄存器地址、处理的项目数量以及域中的实际数据字节数。在某种请求中,数据域可以是不存在的(0长度),在此情况下服务器不需要任何附加信息,功能码仅说明操作。
错误校验域是对报文内容执行“冗余校验”的计算结果。根据不同的传输模式(RTU或ASCII)使用两种不同的计算方法。
传输模式
控制器能设置为两种传输模式(ASCII和RTU)中的任何一种在标准的Modbus网络通信。用户选择想要的模式,包括串口通信参数(波特率、校验方式等),在配置每个控制器的时候,在一个Modbus网络上所有设备都必须选择相同的传输模式和串口参数。

当设备使用RTU(RemoteTerminal Unit)模式在Modbus串行链路通信,消息中每个8位域都是一个两个十六进制字符组成。该模式的主要优点是较高的数据密度,在相同的波特率下比ASCII模式有更高的吞吐率。RTU模式的每个报文必须以连续的字符流传送。
RTU模式每个字节(11位)的格式为:
编码系统:8位二进制,报文中每个8位字节含有两个4位十六进制字符(0-9,A-F)。
每字节bit流:1起始位、8数据位,首先发送最低有效位、1位奇偶检验、1停止位。
偶校验是要求的,其它模式(奇校验、无校验)也可以使用,为了保证兼容性,同时支持无校验模式是建议的。默认校验模式模式必须为偶校验。
字符如何串行传送
每个字符或字节均由此顺序发送(从左到右),最低有效位(LSB)…最高有效位(MSB)。


设备配置为奇校验、偶校验或无校验都可以接受,如果无奇偶校验,就传送一个附加的停止位以填充字符帧。
帧检验域:
循环冗余检验(CRC)。
帧描述:
| 子字节地址 | 功能代码 | 数据 | CRC |
|---|---|---|---|
| 1字节 | 1字节 | 0~255字节 | 2字节 |
Modbus报文RTU帧
由发送设备将Modbus报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到,而错误标志必须作为结果被设置。
在RTU模式中,报文帧由时长至少为3.5个字符时间的空闲间隔区分。在后续部分,这个时间区间被称为t3.5。

整个报文帧必须以连续的字符流发送。
如果两个字符直接的空闲间隔大于1.5个字符时间,则报文被认为不完整应该被接收设备丢弃,如下图。

注:RTU接受驱动程序的实现,由于t1.5和t3.5的定时,隐含了大量的对中断的管理。在高速通信速率下,这导致CPU负担加重。因此,在通信速率等于或低于19200bps时,这两个定时必须严格遵守;对于波特率大于19200bps的情形,应该使用2个定时的固定值:建议的字符间超时时间(t1.5)位750us,帧间的超时时间(t1.5)位1.750ms。
下图表示了对RTU传输模式状态图的描述。“主设备”和“从设备”的不同角度均在相同的图中表示:

从“初始”态到“空闲”态转换需要t3.5定时超时:这保证帧间延迟。
“空闲”态是没有发送和接收报文要处理的正常状态。
在RTU模式,当没有活动的传输的实际间隔打达3.5个字符长时,通信链路被认为在“空闲”态。
在链路空闲时,在链路上检测到的任何传输的字符都被识别为帧起始。链路变为“活动”状态。然后,当链路上没有字符传输的时间间隔达到t3.5后,被识别为帧结束。
检测到帧结束后,完成CRC计算和校验。然后,分析地址域以确定帧是否发往此设备,如果不是,则丢弃此帧。为了减少接收处理时间,地址域可以在一接到就分析,而不需要等到整个帧结束。这样,CRC计算只需要在帧寻址到该节点(包括广播帧)时进行。
CRC计算
在RTU模式包含一个对全部报文内容执行的,基于循环冗余校验(CRC-Cyclical Redundancy Checking)算法的错误检验域。CRC域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
CRC包含由两个8位字节组成的一个16位值。
CRC域作为报文的最后的域附加在报文之后。计算后,首先附加低字节。然后是高字节。CRC高字节为报文发送的最后一个字节。
附加在报文后面的CRC的值由发送设备计算。接收设备在接收报文时重新计算CRC的值,并将计算结果于实际接收到的CRC值相比较,如果两个值不相等,则为错误。
CRC的计算,开始对一个16位寄存器预装全“1”,然后将报文中连续的8位字节对其进行后续的计算。只有字符中的8个数据位参与到生成CRC的运算,起始位、停止位和校验位不参与CRC计算。
CRC生成的过程中,每个8位字符与寄存器中的值异或,然后结果向最低有效位(LSB)方向移动1位,而最高有效位(MSB)置0.然后提取并检查LSB:如果LSB为1,则寄存器中的值与一个固定的预置值异或;如果LSB为0,则不进行异或操作。
这个过程将重复直到执行完8次移位,完成最后一次(第八次)移位及相关操作后,下一个8位字节与寄存器的当前值异或,然后又同上面描述过的一样重复8次。当所有报文中字节都预算之后得到的寄存器中的最终值,就是CRC。
Modbus功能码定义
三类功能码分别为:公共功能码、用户定义功能码、保留功能码。

公共功能码: 是较好地被定义的功能码,保证是唯一的,MODBUS组织可改变的,公开证明的,具有可用的一致性测试,MB IETF RFC中证明的,包含已被定义的公共指配功能码和未来使用的未指配保留功能码。
用户定义功能码: 有两个用户定义功能码的定义范围,即65至72和十进制100至110用户没有MODBUS组织的任何批准就可以选择和实现一个功能码不能保证被选功能码的使用是唯一的如果用户要重新设备功能作为一个公共功能码,那么用户需要启动RFC,以便将改变引入公共分类中,并且指配一个新的公共功能码
保留功能码: 一些公司对传统产品通常使用的功能码,并且对公共使用是无效的功能码。
公共功能码定义: Modbus数据模型有四种,通过不同的功能码来读写这些数据对象。



常用功能码描述
读线圈寄存器01H:
在一个远程设备中,使用该功能码读取线圈的1至2000连续状态。指令列表详细说明了起始地址,即指定的第一个线圈地址和线圈编号。从零开始寻找线圈,因此寻址线圈1-16为0-15。
根据数据域的每个比特将响应报文中的线圈分成一个线圈。指示状态1=ON和0=OFF。第一个数据字节的LSB(最低有效位)包括在询问中寻址的输出。其它线圈依次类推,一直到这个字节的高位端为止,并在后续字节中从低位到高位的顺序。
如果返回的输出数量不是八的倍数,将用零填充最后字节中的剩余比特(一直到字节的高位端)。字节数量域说明了数据的完整字节数。
指令:
例如从机地址为01H,线圈寄存器的起始地址为0023H,结束地址为0038H,总共读取21个线圈,协议如下

响应:
回数据的每一位对应线圈状态,1=ON、0=OFF,如下

在上表中Data1表示0x0023-0x002a的线圈状态,Data1的最低位代表低地址的线圈状态,可以理解为小端模式:


Data2表示地址0x002b-0x0033的线圈状态,如下表:

Data3表示地址0x0034-0x0038的线圈状态,不够8位,字节高位填充0,如下:

读离散输入寄存器02H:
该功能码作用是读离散输入寄存器,位操作,可读单个或多个,协议类似功能码0x01,具体的就不讲解了,参考0x01功能码即可。
读保持寄存器03H:
读保存寄存器,字节指令操作,可读单个或多个。
指令
发送指令从机地址0x01,保存寄存器起始地址0x0032,读2个保存寄存器。

响应

数据存储顺序

输入寄存器04H:
读输入寄存器,字节指令操作,可读单个或多个。发送指令以及响应都和03H一样。
写单个线圈寄存器05H:
写单个线圈,位操作,只能写一个,写0xff00表示设置线圈状态为ON,写0x0000表示设置线圈状态为OFF。
指令
设置0x0032线圈位ON

响应
和发送指令相同。
写单个保持寄存器06H:
写单个保持寄存器,字节指令操作,只能写一个。
指令
写0x0032保持寄存器为0x1232

响应
和发送指令相同。
写多个线圈寄存器0FH:
写多个线圈寄存器,若数据区的某位值为“1”表示被请求的相应线圈状态为ON,若某位值为“0”,则状态为OFF。
指令
线圈地址为0x04a5,写12个线圈

上表格中的DATA1为0x0c,表示:

DATA2为0x02,不够8位,字节高位填充0

响应

写多个保持寄存器10H:
写多个保持寄存器,字节指令操作,可写多个。
指令
保持寄存器起始地址为0x0034,写2个寄存器4个字节数据


响应

freeModbus移植
本章实验我们通过freeModbus移植,完成freeModbusRTU,网址: GitHub - cwalter-at/freemodbus: BSD licensed MODBUS RTU/ASCII and TCP slave。
下载完成后,流程如下:
打开freeModbus代码包的demo文件夹,新建一个名为STM32MB的文件夹,之后将BARE文件夹内所有内容复制到STM32MB文件夹下,复制完成如图

回到freeModbus代码包,复制整个modbus文件夹也粘贴到STM32MB文件夹内,完成效果如图

最后STM32MB文件夹内如下图

将STM32MB文件夹移动到stm32cubeMX生成的工程目录下。我们参考前面章节STM32H743-结合CubeMX新建HAL库MDK工程,打开CubeMX软件,重复步骤不再展示。我们来看配置RS232串口通信如下图所示:



TIM4配置

打开工程,引入STM32MB内的所有头文件,并新建名为MB和MB_Port的组,MB内添加STM32MB文件夹下modbus文件夹内所有c文件以及根目录的demo.c文件,MB_Port内添加STM32MB文件夹下port文件夹内所有c文件,如图所示




移植完成后我们看代码,本章我们采用RS232-uart4,代码中修改地方注意注释部分。
实验代码
1.主函数
int main(void)
{MPU_Config();HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_TIM4_Init();MX_UART4_Init();eMBInit( MB_RTU, 0x01, 1, 115200, MB_PAR_NONE);//初始化modbus,走modbusRTU,从站地址为0x01,端口为1。eMBEnable( );//使能modbuswhile (1){( void )eMBPoll( );//启动modbus侦听}}
2. Portserial.c函数
#include "port.h"
#include "stm32h7xx.h" // Device header#include "usart.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"/* ----------------------- static functions ---------------------------------*/
//static void prvvUARTTxReadyISR( void );
//static void prvvUARTRxISR( void );/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{/* If xRXEnable enable serial receive interrupts. If xTxENable enable* transmitter empty interrupts.*/if (xRxEnable) //将串口收发中断和modbus联系起来,下面的串口改为自己使能的串口{__HAL_UART_ENABLE_IT(&huart4,UART_IT_RXNE); //我用的是串口4,故为&huart4}else{__HAL_UART_DISABLE_IT(&huart4,UART_IT_RXNE);}if (xTxEnable){__HAL_UART_ENABLE_IT(&huart4,UART_IT_TXE);}else{__HAL_UART_DISABLE_IT(&huart4,UART_IT_TXE);}
}BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{return TRUE; //改为TURE,串口初始化在usart.c定义,mian函数已完成
}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. */if(HAL_UART_Transmit (&huart4 ,(uint8_t *)&ucByte,1,0x01) != HAL_OK ) //添加发送一位代码return FALSE ;elsereturn TRUE;
}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.*/if(HAL_UART_Receive (&huart4 ,(uint8_t *)pucByte,1,0x01) != HAL_OK )//添加接收一位代码return FALSE ;elsereturn TRUE;
}/* Create an interrupt handler for the transmit buffer empty interrupt* (or an equivalent) for your target processor. This function should then* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that* a new character can be sent. The protocol stack will then call * xMBPortSerialPutByte( ) to send the character.*/
//static
void prvvUARTTxReadyISR( void ) //删去前面的static,方便在串口中断使用
{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 ) //删去前面的static,方便在串口中断使用
{pxMBFrameCBByteReceived( );
}
3.porttimer.c函数
#include "port.h"
#include "stm32h7xx.h" // Device header
#include "tim.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"/* ----------------------- static functions ---------------------------------*/
//static void prvvTIMERExpiredISR( void );/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us ) //定时器初始化直接返回TRUE,已经在mian函数初始化过
{return TRUE;
}inline void
vMBPortTimersEnable( ) //使能定时器中断,我用的是定时器4,所以为&htim4
{/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */__HAL_TIM_CLEAR_IT(&htim4,TIM_IT_UPDATE);__HAL_TIM_ENABLE_IT(&htim4,TIM_IT_UPDATE);__HAL_TIM_SET_COUNTER(&htim4,0);__HAL_TIM_ENABLE(&htim4);
}inline void
vMBPortTimersDisable( ) //取消定时器中断
{/* Disable any pending timers. */__HAL_TIM_DISABLE(&htim4);__HAL_TIM_SET_COUNTER(&htim4,0);__HAL_TIM_DISABLE_IT(&htim4,TIM_IT_UPDATE);__HAL_TIM_CLEAR_IT(&htim4,TIM_IT_UPDATE);
}/* 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 ) //modbus定时器动作,需要在中断内使用
{( void )pxMBPortCBTimerExpired( );
}
4.中断回调
void USART4_IRQHandler(void)
{/* USER CODE BEGIN USART4_IRQn 0 *//* USER CODE END USART4_IRQn 0 */HAL_UART_IRQHandler(&huart4);/* USER CODE BEGIN USART4_IRQn 1 */if(__HAL_UART_GET_IT_SOURCE(&huart4, UART_IT_RXNE)!= RESET) {prvvUARTRxISR();//接收中断}if(__HAL_UART_GET_IT_SOURCE(&huart4, UART_IT_TXE)!= RESET) {prvvUARTTxReadyISR();//发送中断}HAL_NVIC_ClearPendingIRQ(USART2_IRQn);HAL_UART_IRQHandler(&huart4);
}/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器中断回调函数,用于连接porttimer.c文件的函数
{/* NOTE : This function Should not be modified, when the callback is needed,the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file*/prvvTIMERExpiredISR( );
}
5.demo.c函数
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"/* ----------------------- Defines ------------------------------------------*/
#define REG_INPUT_START 0
#define REG_INPUT_NREGS 5/* ----------------------- Static variables ---------------------------------*/
static USHORT usRegInputStart = REG_INPUT_START;
//static
uint16_t usRegInputBuf[REG_INPUT_NREGS];
uint16_t InputBuff[5];eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{eMBErrorCode eStatus = MB_ENOERR;int iRegIndex;int i;InputBuff[0] = 0x11;InputBuff[1] = 0x22;InputBuff[2] = 0x33;InputBuff[3] = 0x44;if( ( usAddress >= REG_INPUT_START )&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) ){iRegIndex = ( int )( usAddress - usRegInputStart );for(i=0;i<usNRegs;i++){*pucRegBuffer=InputBuff[i+usAddress-1]>>8;pucRegBuffer++;*pucRegBuffer=InputBuff[i+usAddress-1]&0xff;pucRegBuffer++;}}else{eStatus = MB_ENOREG;}return eStatus;
}eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,eMBRegisterMode eMode )
{return MB_ENOREG;
}eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,eMBRegisterMode eMode )
{return MB_ENOREG;
}eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{return MB_ENOREG;
}
实验现象
运行程序,打开串口助手:
1、选择Modbus指令,输入设备号、功能号和寄存器地址等参数
2、点击发送,数据日志中回接收到GT7000的响应
3、对比demo.c中寄存器的值
4、发现数值相符,验证成功

