STM32中使用usart实现modbus RTU通讯

  • Post author:
  • Post category:其他




modbus介绍



modbus简介

modbus 协议是应用于电子控制器上的一种通用协议,它已经成为通用工业标准。只要遵循此协议,不同厂商生产的控制设备可以连成工业网络,进行集中控制。modbus协议能实现控制器互相之间、控制器经网络和设备之间进行通信。

modbus协议是请求响应模式(应答),即控制器向设备发起访问请求,然后设备进行响应。

modbus协议也是主从通信,所以请求只能由主机发起,从设备不能主动发起通信请求,从设备和从设备之间不能进行通信。

modbus协议的基本单元是信息帧,RTC在modbus中的帧结构如下:

在这里插入图片描述

  • modbus信息帧所允许的最大长度为256个字节。所以数据的最大长度是252个字节。
  • address (地址域):有效的从机设备地址范围是0-247,各从机设备的寻址范围为1-247。主设备想要和从设备进行通信,需要把从设备的地址填入此域;从设备响应主设备时,也需要把自己的地址填入此处,用于告诉主设备这条消息的来源某某从设备。

    0为广播地址,所有从机均可以识别
  • function(功能域):功能域告诉被寻址到的设备需要执行何种操作,有效码范围1-225。功能代码


    1


    在从设备响应主设备时也需要填入。具体规则如下:

    从设备正常响应主设备时,填入原始的功能码;不正常响应时,从设备把原始功能码的最高有效位置“1”。
  • Data(数据域):数据域包含需要执行特定功能所需要的数据或者是从设备响应访问采集到的数据(或者是

    异常码

    )。
  • check(错误校验域):使用16位循环冗余的方法(CRC),用来检测数据在传输过程中是否发生变化。CRC


    2


    计算后填入该区域。

    计算时只关注有效数据,校验位和起止位都会忽略


    信息帧的传输:每个字节传输时,最低有效位在前面。

    CRC传输时低字节在前高字节在后

从设备在接收到主设备发送过来的请求后,若发现由通讯、无法处理、地址不对等问题,则返回一个错误性质的消息。消息的异常类型如下图,而出现由于通信问题引起的无法接受数据,主设备只能依赖超时处理。超时后主设备会再次发送一次查询动作。

异常和正常返回的主要区别有两个:正常返回时,功能码使用从设备接受到的,异常则最高位置1;正常返回时数据域返回响应的数据,异常则只填写一个异常码。


在这里插入图片描述
在这里插入图片描述

以下列举常见功能的数据帧格式,使用modbusRTU协议通讯,主要是为了获取数据。这里说下获取寄存器的数值以及设置寄存器的数值。

注意使用的寄存器是16bit

  • 读寄存器的值(功能码0x3),格式:

    address | function | h_start | l_start | h_count | l_count | l_crc | h_crc

    Tx:5A 03 00 00 00 0A C8 E6

    Rx:5A

    03 14

    00 93 01 02 01 71 00 7B 01 C8 03 15 30 39 14 64 1A 0A 1C F4 3A 78

    Tx:5A 03 00 00 00 0A C8 E6

    Rx:5A

    83 08

    31 25

    上面的数据是从modbus poll上面截取的。5A是从设备的地址,03表示这条信息的功能是获取寄存器中的值。接着的两个00 表示起始的寄存器地址,接下来的00 0A是一次获取寄存器的个数,然后就是CRC了。

    返回中5A是从设备地址,03是功能码,14则是后面数据的长度(单位是字节),然后就是数据,最后两个就是CRC的值了。

    数据的长度只占一个字节,异常返回数据域填入的是异常码,并没有数据长度。
  • 写寄存的值(设置寄存器),设置单个时功能码是06 多个时功能码是16(0x10),格式

    address | function | h_start | l_strat | h_data | l_data | l_crc | h_crc

    Tx:5A 06 00 01 01 02 55 70

    Rx:5A 06 00 01 01 02 55 70

    00 01是设置的寄存器的地址,01 02 是设置这个寄存器的值。正常响应后修改寄存器中的值,然后将接收到的数据传送回去。

    协议有关部分可以去官网查阅,这里就不深究了。



modbus测试工具的简介

Modbus Poll :Modbus主机仿真器,用于测试和调试Modbus从设备。该软件支持ModbusRTU、ASCII、TCP/IP。用来帮助开发人员测试Modbus从设备,或者其它Modbus协议的测试和仿真。它支持多文档接口,即,可以同时监视多个从设备/数据域。每个窗口简单地设定从设备ID,功能,地址,大小和轮询间隔。你可以从任意一个窗口读写寄存器和线圈。如果你想改变一个单独的寄存器,简单地双击这个值即可。或者你可以改变多个寄存器/线圈值。提供数据的多种格式方式,比如浮点、双精度、长整型(可以字节序列交换)。这个软件能在百度上搜索直接下载。

  • 安装注册:安装完毕,在桌面能够看到ModbusPoll的快捷方式,双击该快捷方式,打开软件,打开后界面如下图所示。

    在这里插入图片描述

    单击Connection->Connect,弹出注册窗口;打开压缩包解压后的readme文件,复制ModbusPoll的序列号5A5742575C5D10,粘贴到注册窗口的注册栏,如下图所示,点击OK,注册完毕。
  • modbus poll 程序主窗口介绍。其中:Tx 表示向从设备发送数据帧次数; Error 表示通讯错误次数; ID 表示Modbus从设备的设备地址;F 表示所使用的Modbus功能码; SR 表示扫描周期。红字部分,表示当前的错误状态,“No Connection”表示未连接状态。
  • 参数设置:单击菜单【Setup】中【Read/Write Definition… F8】进行参数设置,会弹出参数设置对话框。这里可以从设备的ID以及功能码、访问地址、访问数量、扫描时间等参数。

    在这里插入图片描述
  • 串口连接:单击菜单【Connection】中【Connect… F3】进行串口连接,可以设置串口的通讯参数。Response Timeout,表示读取超时时间,从站在超时时间内没有返回数据,则认为通讯失败。Delay Between Polls,每次扫描的最小间隔时间,默认为10ms。

    在这里插入图片描述
  • 寄存器值改变:在主窗口寄存器地址上双击鼠标,弹出修改对话框。在value输入框中输入值确认即可。Slave为要访问的Modbus从站的地址,对应主画面中的ID值,默认为1。 Address 为当前操作的寄存器的地址。图中为对寄存器40001操作。 Use Function为所使用的功能码,可以选择06或16功能码进行写入。

    在这里插入图片描述

    -查看通讯数据帧:单击【Display】菜单中的【Communication…】或者单击工具栏上【101】按钮,可以调出串口收发数据帧监视信息对话框“CommunicationTraffic”,用来查看分析收发的数据帧。如下图所示:

    在这里插入图片描述

    其中:前6位为数据帧的序号。 Rx表示接收的数据帧。 Tx表示发送的数据帧。

    对modbus RTU通讯格式不清楚的话可以通过这里获取帧,然后分析下格式

  • 断开连接:点击【Disconnect F4】即可断开连接结束测试,此时主窗口中出现红色的“No Connection”表示未连接状态。



在STM32F103中实现modbus RTU通讯

在stm32中使用串口usart1来现实modbus传输协议。由于串口传输信息是把一连串的数据都发送出去,所以数据什么时候结束,咱们并不知道。这里使用modbus协议中的帧和帧之前至少要有3.5字节的时间间隙来做帧的分隔。

在这里插入图片描述

设计的流程是:设备开启后初始化串口、系统时钟、以及基础定时器6和7。串口的配置主要有开启串口的接受中断使能、校验中断使能。在串口中断服务函数中把接受到的数据保存到数组中,同时写系统定时器的val寄存器(这里从新开始计时)。系统定时器配置为3.5个字节的传输时间为定时时间,定时器中断触发,说明最后接受到的数据到现在已经超过了3.5个字节的时间间隙,可以认为上一帧数据传输结束。在定时器中断服务函数中处理接受到的帧,并响应主机。数据接受保存在120个字节的数组中,并用len标记其长度。

  • 串口初始化。开启RX/TX引脚的时钟,初始化RX/TX引脚,TX引脚配置为复用推挽输出,RX配置为浮空输入(完全取决于外部),配置usart为115200、801,并且先清除中断挂起寄存器,然后配置中断使用,配置usart1的中断,最后开始usart1的时钟。

    这里有个疑问,就是配置为浮空输入后,这个引脚是怎样复用到RX的?希望大神可以指点下


    配置如下:
void ConfigUsart(void)
{

	GPIO_InitTypeDef  GPIO_InitStruct;
	USART_InitTypeDef USART_InitStruct;
	
	GPIO_CLK_FUNC(GPIO_CLK_ENABLE,ENABLE);
	U_CLK_FUNC(U_CLK_ENABLE,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin = RX_PIN;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(RX_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = TX_PIN;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(TX_PORT, &GPIO_InitStruct);
	
	USART_InitStruct.USART_BaudRate = U_BAUDRATE;
	USART_InitStruct.USART_WordLength = U_WORDLENGTH;
	USART_InitStruct.USART_Parity = U_PARITY;
	USART_InitStruct.USART_StopBits =U_STOPBITS;
	USART_InitStruct.USART_Mode = U_MODE;
	USART_InitStruct.USART_HardwareFlowControl =U_HDFC;
	USART_Init(USARTx, &USART_InitStruct);
	
}


void InitUsart(void)
{
	ConfigUsart();
	
	USART_ITConfig(USARTx,U_IT,ENABLE);
	
	USART_ClearFlag(USARTx,U_IT);
	
	nvic_usart_config();
	
	USART_Cmd(USARTx, ENABLE);
}

串口中断和基本定时器的配置

void nvic_usart_config(void)
{
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	
	NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
	
	NVIC_Init(&NVIC_InitStruct);
}

void nvic_time6_config(void)
{
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	
	NVIC_InitStruct.NVIC_IRQChannel = TIM6_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 4;
	
	NVIC_Init(&NVIC_InitStruct);

}

系统定时器的配置

	SysTick_Config(time_val);
	/*time_val是根据波特率计算出来的3.5个字节传输的定时时间*/

串口中断服务函数,这里主要是接收数据,存放在数组recvz_data中,并用len标记一帧数据的长度

void USART1_IRQHandler(void)
{

	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){
		/*这里写系统定时器的VAL,val值会清零从新计时。只要接收到数据就重置为3.5个字节的定时时间*/
		SysTick->VAL   = 0xff; 
		/*recv_occur用来标记串口接收到数据了、len标记数组中有效数据的长度,这里暂时没有把校验中断的处理补上,后面再补上*/
		recv_occur = 1;
		recv_data[len++] = USART1->DR&0xff;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}

}

系统定时器的中断服务函数,系统定时器中断触发说明接收最后一个字节到现在已经过了3.5个字节的时间间隙,可以认为上一个帧数据传输已经结束。

void SysTick_Handler(void)
{
	if(recv_occur == 1){
		/*recv_occur 用来标记是否接收了数据,因为系统定时器在开机的时候就配置了。 */
		respend_resquest();
		recv_occur = 0;
		/*处理响应后,把数组中的数据清除,接收到的数据从数组的头部开始存放*/
		len = 0;
	}
}

基本的架构流程就是上述的。然后就是响应主设备的请求函数。CRC计算方法在文末,要是想检验是否正确,可以使用

CRC在线计算

uint16_t GenerateCrc(uint8_t *c,int num)
{
	uint16_t crc = 0xffff,code = 0xa001;
	char i,j;
	for(i = 0;i< num;i++){
		crc ^= c[i]; 
		for(j = 0;j < 8;j++){
			if(crc&1){
				crc>>=1;
				crc^=code;
			}else{
				crc>>=1;
			}
		}
	}	
	return crc;
}

void respend_resquest(void)
{
	uint8_t i=0,j = 0;
	uint16_t crc,data,tempcrc;
	
	function = recv_data[1];	
	
	if((recv_data[0] != local_addr) && (recv_data[0] != 0)){
		/*校验是否发给自己,即从设备的地址是否合格*/
		send_info[i++] = local_addr;
		send_info[i++] = function|0x80;
		send_info[i++] = 2;
		crc = GenerateCrc(send_info,i);
		send_info[i++] = crc&0xff;
		send_info[i++] = crc>>8&0xff;
		SendNo((char*)send_info,i);
		return;
	}

	crc = GenerateCrc(recv_data,len-2);
	tempcrc = (recv_data[len-2] & 0xff)|((recv_data[len-1]&0xff) <<8);
	/*校验传输中是否收到干扰*/
	if(tempcrc != crc){
		send_info[i++] = local_addr;
		send_info[i++] = function|0x80;
		send_info[i++] = 8;
		crc = GenerateCrc(send_info,i);
		send_info[i++] = crc&0xff;
		send_info[i++] = crc>>8&0xff;
		SendNo((char*)send_info,i);
		return;		
	}
	/*由于时间有限,这里只实现了部分功能码,要是想实现别的,直接添加case xx:就可以*/
	switch(function){
		case 03:
			send_info[i++] = local_addr;
			send_info[i++] = function;
			reg_no = recv_data[i]<<8|recv_data[i+1];
			reg_count = recv_data[i+2]<<8 | recv_data[i+3];
			if(reg_no < 0 || reg_no >=10 || reg_no+reg_count > 10){
				send_info[i++] = 3;
			}else{
				respond = reg_count*2;
				//send_info[i++] = respond>>8&0xff;
				send_info[i++] = respond & 0xff;
				for(j = 0;j < reg_count;j++){
					send_info[i++] = test_reg[reg_no+j]>>8&0xff;
					send_info[i++] = test_reg[reg_no+j]&0xff;
				}
			}
				
			break;
		case 04:
	
			break;
		case 06:
			send_info[i++] = local_addr;
			send_info[i++] = function;
			reg_no = recv_data[i]<<8|recv_data[i+1];
			data   = recv_data[i+2]<<8|recv_data[i+3];
			if(reg_no < 0 || reg_no >= 10 /*|| reg_no + reg_count > 6*/){
				send_info[i++] = 3;
			}else{
				test_reg[reg_no] = data;
				send_info[i++] = reg_no>>8&0xff;
				send_info[i++] = reg_no&0xff;
				send_info[i++] = test_reg[reg_no]>>8&0xff;
				send_info[i++] = test_reg[reg_no]&0xff;
			}
			break;
		case 16:
			
			break;
		default:
			send_info[i++] = local_addr;
			send_info[i++] = function|0x80;
			send_info[i++] = 1;
	}
	crc = GenerateCrc(send_info,i);
	send_info[i++] = crc&0xff;
	send_info[i++] = crc>>8&0xff;
	SendNo((char*)send_info,i);

}

void SendNo(char* c,char num)
{	/*这个函数用于把生成好的信息帧返回给主设备*/
	char i = 0;
	for(i = 0; i < num;i++){
		SendChar(c[i]);
	}
	while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}

上面就是M3使用串口实现的modbus通讯步骤和流程。当然实现的方法有很多,希望有好的想法的大神不吝赐教。编译通过后,使用上述讲解中的modbus poll来检测,结果如下。

在这里插入图片描述

到这里基本的通讯部分功能已经实现。后续把整个项目的流程都会写出来的。


  1. 功能域的代码如下图。

    在这里插入图片描述

    ↩︎

  2. CRC计算流程:预置一个16位寄存器为0xffff——>把数据帧中的第一个字节数据与CRC寄存器中的低字节进行异或运算——>将CRC寄存器向右移1位,最高位填0,最低位被移出并检测——>如果是0继续移位;如果是1则与预设固定值0xA001进行异或运算。——>移位8次后,继续与数据帧中的下一个字节数据进行同样的操作,如此循环操作直到数据帧中的数据都处理完了。CRC填入帧时低字节在前,高字节再后面。

    ↩︎



版权声明:本文为lzj_linux188原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。