STM32实现基于RS485的简单的Modbus协议

背景

我这里用STM32实现,其实可以搬移到其他MCU,之前有项目使用STM32实现Modbus协议

这个场景比较正常,很多时候都能碰到

这里主要是Modbus和变频器通信

最常见的是使用Modbus实现传感器数据的采集,我记得之前用过一些传感器都是Modbus协议

这就需要MCU实现Modbus协议,不过实际使用的Modbus协议往往都是简化版本的

可能只是几条Modbus协议格式的指令而已

初学者,网上一搜Modubus协议,往往越看越糊涂

原理图

如下图所示,使用STM32 UART2,采用485接口设计引出

解释一下为什么这里的485电路设计的这么复杂

这里考虑485带电插拔操作,以及客户要求隔离功能等,所以硬件上设计比常用电路复杂很多

其实主要功能都是一致的

软件设计 

初始化串口,这里写的比较复杂,因为考虑了串口2也就是485接口的波特率是可以配置的,并且配置后掉电保存,所以有个波特率的接口,当然同时也有校验位可配置

如下配置,串口采用中断模式,使用串口2,对应管脚PA2/PA3

void Bsp_usart2_cfg(u8 baud, u8 checkbit)
{	
	NVIC_InitTypeDef   NVIC_InitStructure;  
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure; 
	u32 BaudRate;
	
	switch(baud)
	{	
		case 0:
		{
			BaudRate = 300;
			break;
		}
		case 1:
		{
			BaudRate = 600;
			break;
		}
		case 2:
		{
			BaudRate = 1200;
			break;
		}
		case 3:
		{
			BaudRate = 2400;
			break;
		}
		case 4:
		{
			BaudRate = 4800;
			break;
		}
		case 5:
		{
			BaudRate = 9600;
			break;
		}
		case 6:
		{
			BaudRate = 19200;
			break;
		}
		case 7:
		{
			BaudRate = 38400;
			break;
		}
		case 8:
		{
			BaudRate = 57600;
			break;
		}
		case 9:
		{
			BaudRate = 115200;
			break;
		}
		default:
		{
			BaudRate = 9600;
			break;
		}
	}
		
	
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);
	
	/*
	*  USART2_TX -> PA2 , USART2_RX ->	PA3
	*/				
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;	         
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
	GPIO_Init(GPIOA, &GPIO_InitStructure);		   

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;	        
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	USART_InitStructure.USART_BaudRate = BaudRate;
	///USART_InitStructure.USART_WordLength = USART_WordLength_9b;//9位数据
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	//if(checkbit == 0)
	//USART_InitStructure.USART_StopBits = USART_StopBits_2;//1位停止位
	//else
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//1位停止位
		
	if((checkbit == 0) || (checkbit == 3))
	USART_InitStructure.USART_Parity = USART_Parity_No;//
	else if(checkbit == 1)
	USART_InitStructure.USART_Parity = USART_Parity_Even;//偶校验
	else if(checkbit == 2)
	USART_InitStructure.USART_Parity = USART_Parity_Odd;//奇校验
	else
	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(USART2, &USART_InitStructure); 

  
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;	  
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = USART2_IRQCHANNELPP;// 设置抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = USART2_IRQCHANNELSP;	
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

//	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);    // 使能USART接收中断,这里先不开启接收中断
	
	USART_Cmd(USART2, ENABLE); 
	USART_ClearITPendingBit(USART2, USART_IT_TC);//清除中断TC位
	
	while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);//等待传输完成,否则第一位数据容易丢失
	

	
}

串口2的中断处理函数如下

这里很简单,就是把串口2的数据收集起来放到队列comrx2xQueue中

void USART2_IRQHandler(void)
{
	portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
	uint8_t cChar;
	uint16_t msg;
	
	if (USART_GetFlagStatus(USART2, USART_FLAG_ORE) != RESET)   // ORE中断
	{
		USART_ReceiveData(USART2);
	}
	if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)   // 接收数据中断			
	{
		cChar = USART_ReceiveData( USART2 );
		msg = MSG_USART_EVT | (cChar);
		xQueueSendFromISR( comrx2xQueue, &msg, &xHigherPriorityTaskWoken );
	}
	portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
}

在串口2的接收任务中进行

协议帧格式匹对

如下代码,使用状态机跳转到接收处理位置


void tskcomrx2( void *pvParameters )
{
	uint16_t Msg;
	QueueHandle_t pq = pvParameters;
	uint8_t stt = FSM_IDLE,*prx;
	uint16_t tmp16,len;	
	
	
	while(1) 
	{
		if( xQueueReceive( pq, &Msg, 20 ) == pdPASS )
		{
			if(MSG_NAME(Msg) == MSG_USART_EVT)
			{
				tmp16 = MSG_DATA(Msg);
				//调试语句,打印接受数据到调试串口1
//				while((USART1->SR&0x40)==0);//等待上一次发送完毕   
//				USART1->DR = tmp16; 
//				
				switch (stt) 
				{
					case FSM_IDLE :
					{
						prx = StdDatBufIn2;
						*prx = 0;
						len = 0;
						if (tmp16 == FlashParagma.addr)			
						{
							/*数据开始*/
							len++;
							*prx++ = tmp16;
							*prx = 0;
							stt = FSM_HEAD;
						}
						break;
					}
					case FSM_HEAD :
					{
						len++;
						*prx++ = tmp16;
						*prx = 0;
						if ((tmp16 == 0x03) ||  (tmp16 == 0x06))
						{
							stt = FSM_ASCII_DATA;
						}
						else
						{
							stt = FSM_IDLE;//异常处理
						}
						break;
					}
					case FSM_ASCII_DATA :
					{						
						len++;
						*prx++ = tmp16;
						*prx = 0;							
						if(len > 7)
						{
							//处理接收数据
							modbusEventPro(StdDatBufIn2, len);
							stt = FSM_IDLE;//异常处理
						}
						break;
					}
					default:
					{
						stt = FSM_IDLE;
						break;
					}
				}					
			}/**end of if(MSG_NAME(pMsg)*/	
		}			
	}/*end of while(1)*/
}/*end of void tskDatRxCOM1(void * pdata) */

根据modbus协议指令分类进行数据处理,代码如下

功能码03、06进行处理


// Modbus事件处理函数
void modbusEventPro(u8 *src, u16 len)
{
	u16 crc,rccrc;
	
	//收到数据包长度判定

	//通过读到的数据帧计算CRC
	crc = Modbus_CRC16(&src[0], len - 2); 
	// 读取数据帧CRC
	rccrc = src[len - 2] + src[len - 1] * 256;
	if(crc == rccrc) //CRC校验成功,开始分包
	{        
		if(src[0] == FlashParagma.addr)  //检测是否是自己的地址
		{
			switch(src[1])   //分析modbus功能码
			{
				case 3:      
				{
					Modbus_Func3(src, len);
					break;
				}
				case 6:
				{
					Modbus_Func6(src, len);
					break;
				}
				default:
					break;				
			}
		}
		else if(src[0] == 0) //广播地址不予回应
		{

		}         
	}
}

发送modbus协议指令,这里需要先把发送模式打开,发送数据完成后,注意要延时一段时间再切换为接收模式,这个延时时间需要自己根据调试情况进行实际调整

控制不同类型的从机,延时时间要求可能不太一样

void Modbus_USRAT2_SendStr(u8 *scr, u16 len)
{
  u16 i;
	// 开始返回Modbus数据
	Modbus_USART2_TX_Mode;
	vTaskDelay(5);
	for(i = 0; i < len; i++)
	{
		while((USART2->SR&0x40)==0);//等待上一次发送完毕   
		USART2->DR = scr[i];        
	}
	vTaskDelay(5);
	Modbus_USART2_RX_Mode;
}

总结

这实现的比较简单,且常用的Modbus协议

协议格式如下,采用高字节在前方式

地址

功能码

从机地址

数据

校验

485从机地址

03H(读)、06H(写)

CRC

1byte

1byte

2byte

4byte

2byte

上述Modbus协议,实现03、06指令,即可完成对从机地址的读写。

上述代码实现,也是根据表格中的格式进行实现的,可以和代码对的上。

其他

网上搜集了一下关于RS485和Modbus协议的解释,这里拿出来比较关键的,供参考

关于RS485(主要是关注传输距离、接口线、电平)

RS-485是美国电子工业协会(EIA)在1983年批准了一个新的平衡传输标准(balanced transmission standard),EIA一开始将RS(Recommended Standard)做为标准的前缀,不过后来为了便于识别标准的来源,已将RS改为EIA/TIA。目前标准名称为TIA-485,但工程师及应用指南仍继续使用RS-485来称呼此标准。

RS-485仅是一个电气标准,描述了接口的物理层,像协议、时序、串行或并行数据以及链路全部由设计者或更高层协议定义。RS-485定义的是使用平衡(也称作差分)多点传输线的驱动器(driver)和接收器(receiver)的电气特性。

  • 差分传输增加噪声抗扰度,减少噪声辐射
  • 长距离链路,最长可达4000英尺(约1219米)
  • 数据速率高达10Mbps(40英寸内,约12.2米)
  • 同一总线可以连接多个驱动器和接收器
  • 宽共模范围允许驱动器和接收器之间存在地电位差异,允许最大共模电压-7-12V

关于Modbus协议

MODBUS 是 OSI 模型第 7 层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。

Modbus协议包括ASCII、RTU、TCP等,并没有规定物理层。此协议定义了控制器能够认识和使用的消息结构,而不管它们是经过何种网络进行通信的。标准的Modicon控制器使用RS232C实现串行的Modbus。Modbus的ASCII、RTU协议规定了消息、数据的结构、命令和就答的方式,数据通讯采用Maser/Slave方式,Master端发出数据请求消息,Slave端接收到正确消息后就可以发送数据到Master端以响应请求;Master端也可以直接发消息修改Slave端的数据,实现双向读写。

Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16位CRC校验,但TCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。另外,Modbus采用主从方式定时收发数据,在实际使用中如果某Slave站点断开后(如故障或关机),Master端可以诊断出来,而当故障修复后,网络又可自动接通。因此,Modbus协议的可靠性较好。

对于Modbus的ASCII、RTU和TCP协议来说,其中TCP和RTU协议非常类似,我们只要把RTU协议的两个字节的校验码去掉,然后在RTU协议的开始加上5个0和一个6并通过TCP/IP网络协议发送出去即可。

1通讯传送方式:

通讯传送分为独立的信息头,和发送的编码数据。以下的通讯传送方式定义也与ModBusRTU通讯规约相兼容:

初始结构 = ≥4字节的时间

地址码 = 1 字节

功能码 = 1 字节

数据区 = N 字节

错误校检 = 16位CRC码

结束结构 = ≥4字节的时间

地址码:地址码为通讯传送的第一个字节。这个字节表明由用户设定地址码的从机将接收由主机发送来的信息。并且每个从机都有具有唯一的地址码,并且响应回送均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机发送的地址码表明回送的从机地址。

功能码:通讯传送的第二个字节。ModBus通讯规约定义功能号为1到127。本仪表只利用其中的一部分功能码。作为主机请求发送,通过功能码告诉从机执行什么动作。作为从机响应,从机发送的功能码与从主机发送来的功能码一样,并表明从机已响应主机进行操作。如果从机发送的功能码的最高位为1(比如功能码大与此同时127),则表明从机没有响应操作或发送出错。

数据区:数据区是根据不同的功能码而不同。数据区可以是实际数值、设置点、主机发送给从机或从机发送给主机的地址。

CRC码:二字节的错误检测码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/21523.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

学习RHCSA的day.03

目录 2.6 Linux系统的目录结构 2.7 目录操作命令 2.8 文件操作命令 2.6 Linux系统的目录结构 1、Linux目录结构的特点 分区加载于目录结构&#xff1a; 使用树形目录结构来组织和管理文件。整个系统只有一个位于根分区的一个根目录&#xff08;树根&#xff09;、一棵树。…

盘点 | 10大类企业管理系统有哪些

人类的发展史也是一部工具的进化史&#xff0c;企业管理手段同样不例外。移动互联网时代给了传统低下的手工操作方式致命一击&#xff0c;应运而生的各类企业管理系统工具为企业管理插上腾飞的翅膀&#xff0c;彻底颠覆了手动低效率的历史&#xff0c;变得更加移动化、智能化。…

求最小生成树(Prim算法与Kruskal算法与并查集)

目录 1、案例要求2、算法设计与实现2.1 Prim算法2.1.1 构造无向图2.1.2 编写Prim算法函数2.1.3 实现代码2.1.4 运行结果截图 2.2 Kruskal算法2.2.1 构造无向图2.2.2 编写并查集UnionFind类2.2.3 编写Kruskal算法2.2.4 实现代码2.2.5 运行结果截图 3、总结 1、案例要求 利用贪心…

低代码与其拓荒,不如颠覆开发行业

目录 一、前言 二、低代码是一个值得信赖的“黑盒子” 粗略总结&#xff0c;开发者对低代码平台所见即所得设计器有两种反应&#xff1a; 三、人人都爱黑盒子 四、用“低代码平台”来开发是什么样的感受&#xff1f; 五、结论 一、前言 在科幻电影中&#xff0c;我们看到…

【OpenCV】C++红绿灯轮廓识别+ROS话题实现

目录 前言 一、背景知识 Opencv轮廓检测 ROS相关知识 二、环境依赖 三、具体实现 Step1&#xff1a;初始化ROS&#xff0c;订阅话题 Step2&#xff1a;接收话题&#xff0c;进入回调 1. 帧处理 2. 膨胀腐蚀处理 Step3&#xff1a;红绿特征处理 1. 提取绘制轮廓 2…

【网络协议详解】——数据链路层协议(学习笔记)

&#x1f4d6; 前言&#xff1a;数据链路层是 OSI 模型中的第二层&#xff0c;位于物理层之上&#xff0c;是通信网络中的重要组成部分之一。数据链路层协议负责将网络层传输的数据分组封装成帧&#xff0c;传输到物理层&#xff0c;并通过物理介质进行传输。同时&#xff0c;数…

算法笔记:A2-A4-RSRQ切换算法

1 LTE 切换 LTE切换是移动通信网络中的一个过程&#xff0c;移动设备在保持无间断服务的情况下&#xff0c;将其连接从一个基站切换到另一个基站。当移动设备离开当前基站的覆盖范围或网络资源拥塞时&#xff0c;就需要进行切换。LTE切换通常是基于特定的条件触发的&#xff0…

makefile 学习(1):C/C++ 编译过程

1. GCC 介绍 1.1 介绍 GCC 官方文档 https://gcc.gnu.org/onlinedocs/ 官方文档是最权威的&#xff0c;网上所有的答案都来自官方文档国内论坛参差不齐&#xff0c;找到好的答案比较花时间&#xff0c;并且很容易被错误的文档误导。所以推荐看官方文档靠谱点&#xff0c;并且…

二、数据字典开发

文章目录 二、数据字典开发1、搭建service-cmn模块1.1 搭建service-cmn模块1.2 修改配置1.3 启动类 2、数据字典列表2.1 数据字典列表接口2.1.1 model模块添加数据字典实体2.1.2 添加数据字典mapper2.1.4 添加数据字典controller 2.2 数据字典列表前端2.2.1 添加路由2.2.2 定义…

【Java算法题】剑指offer_01数据结构

前言 刷题链接&#xff1a; https://www.nowcoder.com/exam/oj/ta?page2&tpId13&type265 1. 链表 JZ24 反转链表 思路&#xff1a;基本操作&#xff0c;如下所示。 /* public class ListNode {int val;ListNode next null;ListNode(int val) {this.val val;} }…

ad18学习笔记一

如何自学altium designer如何自学altium designer&#xff1f; - 知乎如何自学altium designer 这里面有ad官方推荐的b站的视频&#xff1a;可以直接去b站关注ad官方账号 AltiumChina&#xff0c;它本身就发布了很多实用教程。 在知乎的这个界面也有Altium Designer Ver18_官…

c++ 11标准模板(STL) std::set(六)

定义于头文件 <set> template< class Key, class Compare std::less<Key>, class Allocator std::allocator<Key> > class set;(1)namespace pmr { template <class Key, class Compare std::less<Key>> using se…

如何使用SCQA模型提高表达能力

SCQA架构是“结构化表达”工具。 一、什么是“SCQA架构”&#xff1f;‍ S&#xff08;Situation&#xff09;情景——由熟悉的情境或事实引入 C&#xff08;Complication&#xff09;冲突——指出实际面临的困境或冲突 Q&#xff08;Question&#xff09;疑问——你如何分析…

文本三剑客正则表达式3

文章目录 文本三剑客&正则表达式31 awk工作原理2 awk的基本格式及其内置变量2.1 基本格式2.2 内置变量2.3 示例2.3.1 直接打印所有内容2.3.2 取每一行的第一列2.3.3 打印行号&#xff0c;及所有内容2.3.4 打印第三行2.3.5 打印2-4行2.3.6 打印第2行和第4行2.3.7 用正则表达…

基于harbor安装私有镜像仓库

目录 Harbor介绍 Harbor安装 下载完成后&#xff0c;在压缩包解压到/usr/local目录下&#xff1a; 修改Harbor配置文件 推送本地镜像到harbor上 1、给本地镜像打一个标签 2、 设置docker的daemon.json 3、重启docker 4、使用docker登录harbor 5、把本地的镜像push到harbor…

银豆信息张雪灿:钻石级合作伙伴的增长秘诀

编者按&#xff1a; 杭州银豆信息技术有限公司&#xff08;简称“银豆”&#xff09;&#xff0c;是一家专注于云计算服务的高科技企业&#xff0c;目前已为2000家企业级客户提供了专业的行业解决方案, 与人民网、光大银行、长安汽车金融、vivo金融、浙江省农科院、淄博市大数…

MediaPipe虹膜检测:实时虹膜跟踪和深度估计

包括计算摄影(例如,人像模式和闪光反射)和增强现实效果(例如,虚拟化身)在内的大量实际应用都依赖于通过跟踪虹膜来估计眼睛位置。一旦获得了准确的光圈跟踪,我们就可以确定从相机到用户的公制距离,而无需使用专用的深度传感器。反过来,这可以改善各种用例,从计算摄影…

机器学习之SVM分类器介绍——核函数、SVM分类器的使用

系类文章目录 机器学习算法——KD树算法介绍以及案例介绍 机器学习的一些常见算法介绍【线性回归&#xff0c;岭回归&#xff0c;套索回归&#xff0c;弹性网络】 文章目录 一、SVM支持向量机介绍 1.1、SVM介绍 1.2、几种核函数简介 a、sigmoid核函数 b、非线性SVM与核函…

从内网护卫到零信任尖兵:腾讯iOA炼成记

腾讯既是企业产品的服务商又是使用者&#xff0c;很多产品最原始的出发点最早只是为了解决腾讯自身某一个需求&#xff0c;经过不断地发展完善和业务场景锤炼&#xff0c;最终进化成一个成熟的企服产品。本系列文章讲述的是这样一组Made in Tencent故事&#xff0c;这是系列的第…

Word批量更改图片环绕方式与=尺寸大小

前提&#xff1a;一份Word文档里面有100张图片&#xff0c;有大有小&#xff0c;需要将100张图片更改为统一大小&#xff0c;宽度与高度均为5厘米&#xff0c;同时环绕方式也需要改成四周型。 默认Word图片的默认环绕方式为嵌入型&#xff0c;需要统一更改为四周型&#xff0c;…