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

基于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)/~、评论(▽)!

  • 35
    点赞
  • 159
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
STM32 Modbus-RTU主机程序是一种针对STM32单片机开发的软件程序,用于实现Modbus通信协议中主站(主机)的功能。Modbus-RTU是一种常见的串行通信协议,用于在工业自动化系统中进行数据交换。 STM32 Modbus-RTU主机程序的设计目的是实现STM32单片机作为Modbus通信系统中的主站,具备与从站进行通信的能力。主机程序中通过串口与从站进行通信,接收和发送数据。主机程序需要实现Modbus协议的相关功能,包括函数码解析、地址解析、数据读写操作等。 在设计STM32 Modbus-RTU主机程序时,需要考虑以下几个关键因素。首先,需要确定从站的地址和通信参数,包括波特率、数据位数、停止位等。其次,需要实现Modbus通信协议的各种功能,如读取保持寄存器、读取输入寄存器、写入单个线圈等。此外,还要考虑主机与从站的通信方式和通信频率,以及数据的处理和解析方式。 在编写STM32 Modbus-RTU主机程序时,可以使用STM32的开发环境进行开发,如Keil等。首先建立串口通信功能,然后根据主机与从站的通信协议,实现相应的Modbus函数码解析和数据读写操作。最后进行测试和调试,确保主机程序能够正确地与从站进行通信,并实现所需的数据交换功能。 总之,STM32 Modbus-RTU主机程序是一种通过STM32单片机实现Modbus通信协议的软件程序。它能够使STM32单片机具备作为主站与从站进行通信的能力,并实现相关的数据读写操作。通过该主机程序,能够在工业自动化系统中实现高效可靠的通信。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhuzhu、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值