基于STM32的MODBUS-RTU框架的实现

---------------------------------------------------------------------------------------手动分割线--------------------------------------------------------------------------------

---------------------------------------------------------------------------------------文章开始--------------------------------------------------------------------------------

一、协议简介

Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。Modbus比其他通信协议使用的更广泛的主要原因有:

  1. 公开发表并且无版权要求
  2. 易于部署和维护
  3. 对供应商来说,修改移动本地的比特或字节没有很多限制

Modbus允许多个 (大约240个) 设备连接在同一个网络上进行通信,举个例子,一个测量温度和湿度的装置,并且将结果发送给计算机。在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。

Modbus协议目前存在用于串口、以太网以及其他支持互联网协议的网络的版本。对于串行连接,存在两个变种,它们在数值数据表示不同和协议细节上略有不同。Modbus RTU是一种紧凑的,采用二进制表示数据的方式,Modbus ASCII是一种人类可读的,冗长的表示方式。

本文介绍的为MODBUS-RTU协议在STM32单片机上的实现。

二、协议框架

MODBUS的帧(报告)形式:RTU帧。框架的一般形式如下图所示:
RTU帧的一般形式
MODBUS的帧根据主从方式分为两种:询问帧和应答帧。

下图为RTU帧的询问帧:
询问帧
下图为RTU帧的一般应答帧:
应答帧
下图为一般潜在长度的帧的响应格式:
潜在长度的帧

三、与标准的RTU帧的差异

标准RTU帧:
使用RTU模式,消息发送至少要以3.5个字符时间的停顿间隔开始。在网络波特率下多样的字符时间,这是最容易实现的。
传输的第一个域是设备地址。可以使用的传输字符是十六进制的0…9,A…F。网络设备不断侦测网络总线,包括停顿间隔时间内。
当第一个域(地址域)接收到,每个设备都进行解码以判断是否发往自己的。
在最后一个传输字符之后,一个至少3.5个字符时间的停顿标定了消息的结束。一个新的消息可在此停顿后开始。

整个消息帧必须作为一连续的流转输。如果在帧完成之前有超过1.5个字符时间的停顿时间,接收设备将刷新不完整的消息并假定下一字节是一个新消息的地址域。
同样地,如果一个新消息在小于3.5个字符时间内接着前个消息开始,接收的设备将认为它是前一消息的延续。
这将导致一个错误,因为在最后的CRC域的值不可能是正确的。

STM32的处理方式:
采用标准的RTU帧实现每一帧的数据分割有点麻烦,需要使用单独的定时器来进行接收字符时间判断。
在STM32上,采用串口空闲接收中断实现对每帧数据的分割,从而简化STM32上的MODBUS的RTU协议,实现快速实现。

四、串口空闲接收中断

关于串口空闲中断网上有很多教程,我这里就直接提供代码,后续有需要再单独出个帖子。


//空闲中断初始化函数
XL_StatusTypeDef XL_UartIdle_DMA_Init(UART_HandleTypeDef *huart,uint8_t *pData)
{
	__HAL_UART_CLEAR_IDLEFLAG(huart);       //清除空闲中断标志
	__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);//启动串口空闲中断
	return (XL_StatusTypeDef)HAL_UARTEx_ReceiveToIdle_DMA(huart,pData,XL_UartRx_Len);//开启DMA接收
}

//空闲中断回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	void XL_Uart_Idle(UART_HandleTypeDef *huart, uint8_t *pData,uint16_t Size);
	if(huart == &hlpuart1)
		XL_Uart_Idle(huart,LPU1_RxBuff.pData,Size);
	if(huart == &huart3)
		XL_Uart_Idle(huart,U3_RxBuff.pData,Size);	
}

五、RTU协议框架

//申明函数
ModbusState BackCheckFunction(uint8_t subcode);
ModbusState ReadEventFunction(void);


//modbus函数框架
ModbusState XL_Modbus_RTU_Frame(uint8_t Addr,uint8_t *pData,uint16_t len)
{
	//判断地址是否正确、及数据长度
	if(len<3 || Addr != pData[0] )
		return MB_NULL;
	
	//通信为低位在前
	uint16_t crc  = crc16tablefast(pData,len-2);
	
	//判断CRC
	if(pData[len-2] != (uint8_t )crc||pData[len-1] != (uint8_t )(crc>>8) )
		return MB_NULL;
	
	//对应功能码的函数检查
	ModbusState State = LenCheck((FunCode)pData[1],pData,len);
	if(State != MB_OK)
	{
		//返回错误功能码
		ErrorSend((FunCode)pData[1],State);
		return MB_ERROR;
	}
	
	//接收处理计数++
	EventCountNum++;
	
	switch(pData[1])
	{
		case ReadReg: //读取多个寄存器
			ReadRegFunction(pData,len);
			break;
		
		case ReadInputReg: //读取多个输入寄存器
			ReadRegFunction(pData,len);
			break;
		
		case WriteSingleReg: //写入单个寄存器
			WriteRegFunction(pData,len);
			break;
			
		case ReadEventCount: //读取事件计数
			ReadEventFunction();
			break;	
		
		case WriteReg: //写入多个寄存器
			WriteRegFunction(pData,len);
			break;	
		
		default: //异常回复,不存在的功能码
			ErrorSend((FunCode)pData[1],UNFUNCODE);
			break;
	}
	
	
	return MB_OK;
}


//寄存器映射--MARK()
ModbusState RegMap(uint16_t regaddr,uint16_t *reg,ModbusState code )
{
	//先进行地址偏移
	
	/*这部分代码需要自己实现*/
	
	return MB_OK;
}


//读取多个寄存器
ModbusState ReadRegFunction(uint8_t *pData,uint16_t len)
{
	//定义发送数组
	uint8_t bData[310] = {pData[0],pData[1],2*((pData[4]<<8)+pData[5])};
	//定义返回数据长度
	uint16_t blen = 2*((pData[4]<<8)+pData[5])+5;
	//寄存器开始地址
	uint16_t addr = (pData[2]<<8)+pData[3];
	//读取寄存器数
	uint16_t allnum =  (pData[4]<<8)+pData[5];
	
	//遍历寄存器
	for(int i = 0 ;i<allnum;i++)
	{
		uint16_t reg;
		RegMap(addr+i,&reg,MB_READ);
		bData[3+i*2] = reg>>8;
		bData[3+i*2+1] = reg;
	}
	
	//发送数据出去
	MBSendCRC(bData,blen);
	return MB_OK;
}


//写入寄存器
ModbusState WriteRegFunction(uint8_t *pData,uint16_t len)
{
	//寄存器开始地址
	uint16_t addr = (pData[2]<<8)+pData[3];
	//定义发送数组
	uint8_t bData[8] = {pData[0],pData[1],pData[2],pData[3]};
	//单个寄存器
	if(pData[1] == WriteSingleReg)
	{
		//填充长度
		bData[4] = 0;
		bData[5] = 1;
		uint16_t preg = (pData[4]<<8)+pData[5];
		RegMap(addr,&preg,MB_WRITE);
	}
	
	
	//多个寄存器
	if(pData[1] == WriteReg)
	{
		//填充长度
		bData[4] = pData[4];
		bData[5] = pData[5];
		//读取字节数
		uint16_t allnum =  pData[6]/2;				
		//遍历寄存器
		for(int i = 0 ;i<allnum;i++)
		{
			uint16_t preg = (pData[7+i*2]<<8)+pData[7+i*2+1];
			RegMap(addr+i,&preg,MB_WRITE);
		}
	}	
	//发送数据出去
	MBSendCRC(bData,8);
	return MB_OK;		
	
}


//modbus的通信状态校验的函数
ModbusState BackCheckFunction(uint8_t subcode)
{
	switch(subcode)
	{
		case 0x00: //返回询问数据
		{
			uint8_t data[8] = {SlaveAddr,BackCheck,0x00,0x00,0x00,0x01};
			MBSendCRC(data,8);
			break;		
		}

		default:
			ErrorSend(BackCheck,UNFUNCODE);
			break;
	}
	return MB_OK;
}


//modbus的读取事件计数的函数
ModbusState ReadEventFunction(void)
{
	uint8_t data[8] = {SlaveAddr,ReadEventCount,0x00,0x00,0x00,EventCountNum};
	
	MBSendCRC(data,8);
	
	return MB_OK;
}


//ModbusRTU的长度检测函数
ModbusState LenCheck(FunCode code,uint8_t *pData,uint16_t len)
{
	switch(pData[1])
	{
		case ReadReg: //读取多个寄存器
			if(pData[4]!= 0|| pData[5] > Max_RegNum)	//判断读取的最大长度是否超过125寄存器
				return LENERROR;			
			if(len != 8) 										 	//数据主机发送的不彻底或字节的数量是错误的
				return LENERROR;
			return MB_OK;											//通过筛选
			
		case ReadInputReg: //读取多个输入寄存器
			if(pData[4]!= 0|| pData[5] > Max_RegNum)	//判断读取的最大长度是否超过125寄存器
				return LENERROR;			
			if(len != 8) 										 	//数据主机发送的不彻底或字节的数量是错误的
				return LENERROR;
			return MB_OK;											//通过筛选		
			
		case WriteSingleReg: //写入单个寄存器
			if(len != 8) 										 	//数据主机发送的不彻底或字节的数量是错误的
				return LENERROR;
			return MB_OK;											//通过筛选				
		
		case BackCheck: //回送诊断校验,等待与陈铭讨论
			if(len < 8 || len>310)
				return LENERROR;
			return MB_OK;
		
		case ReadEventCount: //读取事件计数
			if(len < 4 || len>310)
				return LENERROR;
			return MB_OK;
			
		case WriteReg: //写入多个寄存器
			if(pData[4]!= 0|| pData[5] > Max_RegNum || len != (pData[6]+9)||(2*((pData[4]<<8)+pData[5]))!=pData[6])	//判断读取的最大长度是否超过125寄存器
				return LENERROR;
			return MB_OK;
			
		default: //异常回复
			return UNFUNCODE;
	}	
}



/*-----crc校验查表----------------
辅助完成CRC校验,是CRC校验的快速查表法
----------------------------------*/
const uint16_t crctalbeabs[] = { 
	0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 
	0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400 
};



/*-------CRC校验函数----------------
输入为字符指针(ptr)、字符指针长度(len)
//ptr为校验数组的指针,len为校验数组的元素个数
返回CRC校验结果,为16位
根据实际需求进行CRC校验码
----------------------------------*/
uint16_t crc16tablefast(uint8_t *ptr, uint16_t len) 
{
	uint16_t crc = 0xffff; 
	uint16_t i;
	uint8_t ch;
 
	for (i = 0; i < len; i++)
	{
		ch = *ptr++;
		crc = crctalbeabs[(ch ^ crc) & 15] ^ (crc >> 4);
		crc = crctalbeabs[((ch >> 4) ^ crc) & 15] ^ (crc >> 4);
	} 
	
	return crc;
}

//modbus 发送函数
void MBSend(uint8_t *pData,uint16_t len)
{
	//需要自己修改串口发送驱动函数
	//XL_Transmit(&hlpuart1,pData,len,Max_SendUart_Time);
	//XL_Transmit(&huart3,pData,len,Max_SendUart_Time);
}

//modbus 发送函数
void MBSendCRC(uint8_t *pData,uint16_t len)
{
	uint16_t crc  = crc16tablefast(pData,len-2);
	
	pData[len-2] = (uint8_t )crc;
	
	pData[len-1] = (uint8_t )(crc>>8);		
	
	//需要自己修改串口发送驱动函数
	//XL_Transmit(&hlpuart1,pData,len,Max_SendUart_Time);
	//XL_Transmit(&huart3,pData,len,Max_SendUart_Time);
}


//异常、错误代码发送
void ErrorSend(FunCode code,ModbusState state)
{
	uint8_t data[5] = {SlaveAddr,code+0x80,state};
	
	MBSendCRC(data,5);	
}

六、总结

工程文件(不需要积分):

https://download.csdn.net/download/qq_40824852/85022712?spm=1001.2014.3001.5503

目前采用STM32实现了部分MODBUS的简易框架,能实现较为简单的协议通信,后续有需求会更新。

----------------------------------------------------------------------------------到这里就结束了-------------------------------------------------------------------------------

时间流逝、年龄增长,是自己的磨炼、对知识技术的应用,还有那不变的一颗对嵌入式热爱的心!
在这里插入图片描述

到这里就结束了,希望大家点赞o( ̄▽ ̄)d、关注(o)/~、评论(▽)!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐