一、前言
本篇笔记讲解modbus-RTU通讯的大概配置流程,梳理通讯过程。
二、通讯原理
2.1 modbus基础概念:
modbus数据类型有四种:
离散输入: 用于只读开关状态,功能码02.
线圈: 用于读写开关状态,功能码01,05,15.
输入寄存器: 用于只读整型,浮点型的字节信息,功能码04.
保持寄存器: 用于读写整型,浮点型,字符型的字节信息,功能码03,06,16
这四种类型对应四个存储区,分别是0 1 3 4,其中,1区3区只读,0区读写布尔量,常用的4区可读可写16位寄存器,因此一些通讯配置上常见4X___起始的地址设置。
2.2 modbus协议类型:
串行端口存在多个版本的Modbus协议,而最常见的是下面四种:
Modbus-RTU
Modbus-ASCII
Modbus-TCP
ModbusPlus
Modbus协议使用串口传输时可以选择RTU或ASCII模式,并规定了消息、数据结构、命令和应答方式并需要对数据进行校验。ASCII 模式采用LRC校验,RTU模式采用16 位CRC校验。通过以太网传输时使用TCP,这种模式不使用校验,因为TCP协议是一个面向连接的可靠协议。
无论modbus怎么传输,都遵循基本的数据帧格式:
因此在程序处理上,需要将接收到的modbus帧进行解析处理,还原出地址,功能码,数据,以及校验,不同的功能码按照上面的功能码表,分别对应不同的读写功能,将数据按照接收到的地址写入或读出。
三、配置步骤
modbus使用差分信号传输数据,而ARM只能发出单线串口信号,因此stm32需要使用串口usart和外部485芯片差分出modbus信号,stm32配置modbus通讯有两种方式,一种是采用USART+定时器方式,另一种是USART+DMA方式。推荐使用后者,因为串口发送比较占用时间(毫秒级),在高系统调用的项目中(微秒级),至少要使TX部分采用DMA发送。
在STM32上使用Modbus通信协议配置从机步骤:
步骤1:配置串口模块
可以使用串口通信的例程加以改动,设置波特率、数据位、停止位和校验位等参数,以确保与Modbus设备的通信兼容。
步骤2:解析Modbus数据包
在STM32上编写代码以解析从Modbus主设备接收到的数据包。在Modbus协议中,每个数据包都包含多个数据字段,例如地址码、功能码、数据字节和CRC校验码等。因此,需要编写代码来识别这些不同的字段,并将它们提取出来以便后续的数据处理。
步骤3:处理Modbus数据
将接收到的Modbus数据将被转换为与实际应用程序相关的数据类型。
步骤4:发送Modbus命令
将数据装入指定内存,通过串口发送。
需要注意的是,modbus RTU通信中,报文帧与报文帧的间隔一般在3.5个字符之间,在程序中对应4ms左右,即当通信总线超过4ms无数据时,认为当前报文已结束,落实到程序上就是当接收完成时定时4ms以上的时间内无接收中断,记当前报文结束;或者接收空闲时开启DMA传输,这也是定时器方式和DMA方式的主要区别。
四、主要代码
usart配置
void initModbusUSART()
{
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(MODBUS_USART_CLK, ENABLE);
// USART6 初始化设置
USART_InitStructure.USART_BaudRate = MODBUS_USART_BAUDRATE;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(MODBUS_USART, &USART_InitStructure);
#ifdef NODMA
USART_ClearITPendingBit(MODBUS_USART, USART_IT_RXNE);
USART_ITConfig(MODBUS_USART, USART_IT_RXNE, ENABLE);
USART_ITConfig(MODBUS_USART, USART_IT_ORE, ENABLE);
#else
USART_ClearITPendingBit(MODBUS_USART, USART_IT_TC | USART_IT_IDLE);
USART_ITConfig(MODBUS_USART, USART_IT_IDLE, ENABLE);
USART_ITConfig(MODBUS_USART, USART_IT_TC, ENABLE);
#endif // NODMA
USART_Cmd(MODBUS_USART, ENABLE);
}
中断服务函数
void USART6_IRQHandler(void)
{
// USART6 发送完成中断处理
if (USART_GetITStatus(MODBUS_USART, USART_IT_TC) != RESET)
{
USART_ClearITPendingBit(MODBUS_USART, USART_IT_TC); // 清除中断标志
DMA_ClearITPendingBit(MODBUS_USART_DMA_STREAM_TX, DMA_IT_TCIF6);
DMA_Cmd(MODBUS_USART_DMA_STREAM_TX, DISABLE);
MODBUS_TX_EN = MODBUS_RX; // 数据包发送完成, 切换为接收模式
DMA_Cmd(MODBUS_USART_DMA_STREAM_RX, ENABLE);
}
if (USART_GetITStatus(MODBUS_USART, USART_IT_IDLE) != RESET)
{
USART_ClearITPendingBit(MODBUS_USART, USART_IT_IDLE); // 清除中断标志
USART_ReceiveData(MODBUS_USART); // 读接收到的字节,同时相关标志自动清除
RS485_RX_CNT = USART_MAX_LEN - DMA_GetCurrDataCounter(MODBUS_USART_DMA_STREAM_RX);
DMA_Cmd(MODBUS_USART_DMA_STREAM_RX, DISABLE); // 关闭DMA传输
DMA_ClearITPendingBit(MODBUS_USART_DMA_STREAM_RX, DMA_IT_TCIF1);
DMA_SetCurrDataCounter(MODBUS_USART_DMA_STREAM_RX, USART_MAX_LEN); // 重新设置DMA数据流的数据计数器
MODBUS_TX_EN = MODBUS_TX; // 停止接收,切换为发送状态
RS485_FrameFlag = 1; // 置位帧结束标记
}
}
DMA配置
static void USART6_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
/*开启DMA时钟*/
RCC_AHB1PeriphClockCmd(MODBUS_USART_DMA_CLK, ENABLE);
/* 复位初始化DMA数据流 */
DMA_DeInit(MODBUS_USART_DMA_STREAM_RX);
/*usart6 rx对应dma2,通道5,数据流2*/
DMA_InitStructure.DMA_Channel = MODBUS_USART_DMA_CHANNEL_RX;
/*设置DMA源:串口数据寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = MODBUS_USART_DR_BASE_RX;
/*内存地址(要传输的变量的指针)*/
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)RS485_RX_BUFF;
/*方向:从外设到内存*/
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
/*传输大小DMA_BufferSize=SENDBUFF_SIZE*/
DMA_InitStructure.DMA_BufferSize = USART_MAX_LEN;
/*外设地址不增*/
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
/*内存地址自增*/
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
/*外设数据单位*/
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
/*内存数据单位 8bit*/
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
/*DMA模式:不循环*/
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/*优先级:中*/
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
/*禁用FIFO*/
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
/*存储器突发传输 16个节拍*/
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
/*外设突发传输 1个节拍*/
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
/*配置DMA2的数据流*/
USART_DMACmd(MODBUS_USART, USART_DMAReq_Rx, ENABLE);
DMA_Init(MODBUS_USART_DMA_STREAM_RX, &DMA_InitStructure);
// DMA_ITConfig(MODBUS_USART_DMA_STREAM_RX, DMA_IT_TC, ENABLE);
/*使能DMA*/
/* 复位初始化DMA数据流 */
DMA_DeInit(MODBUS_USART_DMA_STREAM_TX);
/*usart6 tx对应dma2,通道4,数据流7*/
DMA_InitStructure.DMA_Channel = MODBUS_USART_DMA_CHANNEL_TX;
/*设置DMA源:串口数据寄存器地址*/
DMA_InitStructure.DMA_PeripheralBaseAddr = MODBUS_USART_DR_BASE_TX;
/*内存地址(要传输的变量的指针)*/
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)RS485_TX_BUFF;
/*方向:从内存到外设*/
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
/*传输大小DMA_BufferSize=SENDBUFF_SIZE*/
DMA_InitStructure.DMA_BufferSize = USART_MAX_LEN;
/*外设地址不增*/
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
/*内存地址自增*/
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
/*外设数据单位*/
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
/*内存数据单位 8bit*/
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
/*DMA模式:不循环*/
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/*优先级:中*/
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
/*禁用FIFO*/
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
/*存储器突发传输 16个节拍*/
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
/*外设突发传输 1个节拍*/
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
/*配置DMA2的数据流*/
DMA_Init(MODBUS_USART_DMA_STREAM_TX, &DMA_InitStructure);
USART_DMACmd(MODBUS_USART, USART_DMAReq_Tx, ENABLE);
}
modbus 数据处理函数
void RS485_Service(void)
{
u16 recCRC;
if (RS485_FrameFlag == 1)
{
if (RS485_RX_BUFF[0] == RS485_Addr) // 地址正确
{
if ((RS485_RX_BUFF[1] == 01) || (RS485_RX_BUFF[1] == 02) || (RS485_RX_BUFF[1] == 03) || (RS485_RX_BUFF[1] == 05) || (RS485_RX_BUFF[1] == 06) || (RS485_RX_BUFF[1] == 15) || (RS485_RX_BUFF[1] == 16))
{
startRegAddr = (((u16)RS485_RX_BUFF[2]) << 8) | RS485_RX_BUFF[3]; // 获取寄存器起始地址
// FLASH_SAVE_ADDR = FLASH_START_ADDR + startRegAddr * 2; // 计算需要读写的首地址
if (startRegAddr < 1000) // 寄存器地址在范围内
{
calCRC = CRC_Compute(RS485_RX_BUFF, RS485_RX_CNT - 2); // 计算所接收数据的CRC
recCRC = RS485_RX_BUFF[RS485_RX_CNT - 1] | (((u16)RS485_RX_BUFF[RS485_RX_CNT - 2]) << 8); // 接收到的CRC(低字节在前,高字节在后)
if (calCRC == recCRC) // CRC校验正确
{
///
switch (RS485_RX_BUFF[1]) // 根据不同的功能码进行处理
{
case 2: // 读输入开关量
{
Modbus_02_Solve();
break;
}
case 1: // 读输出开关量
{
Modbus_01_Solve();
break;
}
case 5: // 写单个输出开关量
{
Modbus_05_Solve();
break;
}
case 15: // 写多个输出开关量
{
Modbus_15_Solve();
break;
}
case 03: // 读多个寄存器
{
Modbus_03_Solve();
break;
}
case 06: // 写单个寄存器
{
Modbus_06_Solve();
break;
}
case 16: // 写多个寄存器
{
Modbus_16_Solve();
break;
}
}
//
}
else // CRC校验错误
{
RS485_TX_BUFF[0] = RS485_RX_BUFF[0];
RS485_TX_BUFF[1] = RS485_RX_BUFF[1] | 0x80;
RS485_TX_BUFF[2] = 0x04; // 异常码
RS485_SendData(RS485_TX_BUFF, 3);
}
}
else // 寄存器地址超出范围
{
RS485_TX_BUFF[0] = RS485_RX_BUFF[0];
RS485_TX_BUFF[1] = RS485_RX_BUFF[1] | 0x80;
RS485_TX_BUFF[2] = 0x02; // 异常码
RS485_SendData(RS485_TX_BUFF, 3);
}
}
else // 功能码错误
{
RS485_TX_BUFF[0] = RS485_RX_BUFF[0];
RS485_TX_BUFF[1] = RS485_RX_BUFF[1] | 0x80;
RS485_TX_BUFF[2] = 0x01; // 异常码
RS485_SendData(RS485_TX_BUFF, 3);
}
}
RS485_FrameFlag = 0; // 复位帧结束标志
RS485_RX_CNT = 0; // 接收计数器清零
}
}
附录:
Modbus协议详解