STM32实战总结:HAL之modbus

news2025/8/13 5:43:55

什么是modbus?

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

要注意的是::::::MODBUS协议是一种软件协议,是一种人为约定的协议,他和SPI,IIC,CAN总线协议还是有些不同的,SPI,IIC,CAN总线这些协议必须是设备在硬件上支持的,可以说SPI,IIC,CAN总线是一种软硬件的结合体,也就是常分为两层即物理层和协议层,MODBUS本身就是类似于协议层的东西。Modbus通信标准协议可以通过各种传输方式传播,如 RS232C、RS485、光纤、无线电等。

Modbus比其他通信协议使用的更广泛的主要原因有:

  1. 公开发表并且无版权要求

  2. 易于部署和维护

  3. 对供应商来说,修改移动本地的比特或字节没有很多限制

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

Modbus协议大致分为以下两种串行传输模式:

  • Modbus-RTU

  • Modbus-ASCII

一个设备只会使用一种协议,一般来说大部分的设备都是Modbus-RTU协议。

设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是RTU,ASCII作为可选项。一般学习Modbus协议,只需要了解RTU协议,ASCll了解即可。

Modbus通讯过程

Modbus是一种单主站的主/从通信模式。,主机发送,从机应答,主机不发送,总线上就没有数据通信。 

举例: 一个总线上有一个主机,多个从机,主机查询其中一个从机,首先得给这些从机分配地址(每个地址必须唯一),分配好地址后,主机先查询,然后发数据,从机得到主机发送的数据,然后对应地址的从机回复,主机得到从机数据。

注意:

Modbus不能判断从机是否忙,也没有对应的仲裁机制,我们只能通过软件对数据进行适当的处理!

帧结构

帧结构 = 地址 + 功能码+ 数据 + 校验

●地址: 占用一个字节,范围0-255,其中有效范围是1-247,其他有特殊用途。Modbus网络上只能有一个主站存在,主站在 Modbus网络上没有地址,从站的地址范围为 0 - 247,其中 0 为广播地址,从站的实际地址范围为 1 - 247。

●功能码:占用一个字节,功能码的意义就是,知道这个指令是干啥的,比如你可以查询从机的数据,也可以修改数据,所以不同功能码对应不同功能。

部分功能码:

其中,功能码03和06是比较常用的。

●数据:占用一个或多个字节,根据功能码不同,有不同结构。

●校验:为了保证数据不错误,增加这个,然后再把前面的数据进行计算看数据是否一致,如果一致,就说明这帧数据是正确的,我再回复;如果不一样,说明你这个数据在传输的时候出了问题,数据不对的,所以就抛弃了。

举例说明

我们大部分时候都是用modbus来和传感器通信。如果要查询传感器上的信息,用03查询功能码,如果需要修改传感器寄存器的值就用06修改功能码,其他的不需要过多关注,用到的时候再去了解。

查询功能码

比如我们现在要使用STM32查询某传感器的数据,该传感器的地址为01。

主机发送: 01 03 00 00 00 01 84 0A
从机回复: 01 03 02 19 98 B2 7E

什么意思?解析如下:

发送数据解析

01-地址,也就是你传感器的地址
03-功功能码,03代表查询功能,查询传感器的数据
00 00-代表查询的起始寄存器地址.说明从0x0000开始查询。这里需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据
00 01-代表查询了一个寄存器.结合前面的00 00,意思就是查询从0开始的1个寄存器值
84 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到84前面为止。

回复数据解析

01-地址,也就是你传感器的地址
03-功功能码,03代表查询功能,查询传感器的数据。这里要注意的是注意发给从机的功能码是啥,从机就要回复同样的功能码,如果不一样说明这一帧数据有错误
02-代表后面数据的字节数,因为上面说到,一个寄存器有2个字节,所以后面的字节数肯定是2*查询的寄存器个数;
19 98-寄存器的值是19 98,结合发送的数据看出,01这个寄存器的值为19 98
B2 7E-循环冗余校验

总结就是:

发送:从机的地址+我要干嘛的功能码+我要查的寄存器的地址+我要查的寄存器地址的个数+校验码

回复:从机的地址+主机发我的功能码+要发送给主机数据的字节数+数据+校验码

修改功能码

主机发送: 01 06 00 00 00 01 48 0A
从机回复: 01 06 00 00 00 01 48 0A

看上去怎么一样的啊?是不是错了?答案是这是正确的。

发送数据解析

01-主机要查询的从机地址
06-功能码,06代表修改单个寄存器功能,修改有些不同,有修改一个寄存器和修改多个寄存器;
00 00-代表修改的起始寄存器地址.说明从0x0000开始.
00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止

回复数据解析

01-从机返回给主机自己的地址,说明这就是主机查的从机
06-功能码,代表修改单个寄存器功能,主机发啥功能码,从机就必须回什么功能码;
00 00-代表修改的起始寄存器地址.说明是0x0000.
00 01-代表修改的值为00 01.结合前面的00 00,意思就是修改0号寄存器值为00 01;
48 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止;

如果回复的一样,说明这个数据是修改成功的;如果功能码不是06,而是别的,说明从机回复的数据有误,主机可以做相应的处理。

如果我要修改多个寄存器,难道用06发好几次,这样不会太傻了吗?所以Modbus RTU协议包含了修改连续多个寄存器的方法,就是功能码为0x10;这个大家自己去查询,基本和上面的数据格式差不多。

注意,ModBus只是一个软件协议,传输时可以通过串口来进行通信。 

为了提升传输效率,可以结合DMA使用。

MX配置

配置串口

配置DMA

发送和接收都采用DMA

开启空闲中断

关键代码

/* Includes ------------------------------------------------------------------*/
#include "MyApplication.h"

/* Private define-------------------------------------------------------------*/
#define FunctionCode_Read_Register 		(uint8_t)0x03
#define FunctionCode_Write_Register 	(uint8_t)0x06
#define Modbus_Order_LENGTH           (uint8_t)8

/* Private variables----------------------------------------------------------*/

/* Private function prototypes------------------------------------------------*/      
static void Protocol_Analysis(UART_t*);  //协议分析

static void Modbus_Read_Register(UART_t*);   //读寄存器
static void Modbus_Wrtie_Register(UART_t*);  //写寄存器

/* Public variables-----------------------------------------------------------*/
Modbus_t  Modbus = 
{
	1,
	
	Protocol_Analysis
};

/*
	* @name   Protocol_Analysis
	* @brief  协议分析
	* @param  UART -> 串口指针
	* @retval None      
*/
static void Protocol_Analysis(UART_t* UART) 
{
	UART_t* const  COM = UART;
	uint8_t i = 0,Index = 0;
	
  //串口3停止DMA接收
	HAL_UART_DMAStop(&huart3);
	
	//过滤干扰数据,首字节为modbus地址,共8字节
	for(i=0;i<UART3_Rec_LENGTH;i++)
	{
		//检测键值起始数据Modbus.Addr
		if(Index == 0)
		{
			if(*(COM->pucRec_Buffer+i) != Modbus.Addr)
				continue;
		}
		
		*(COM->pucRec_Buffer+Index) = *(COM->pucRec_Buffer+i);

		//已读取7个字节
		if(Index == Modbus_Order_LENGTH)
			break;
		
		Index++;
	}
	
	//计算CRC-16
	CRC_16.CRC_Value   =  CRC_16.CRC_Check(COM->pucRec_Buffer,6); //计算CRC值
	CRC_16.CRC_H       = (uint8_t)(CRC_16.CRC_Value >> 8);
	CRC_16.CRC_L       = (uint8_t)CRC_16.CRC_Value;
	
	//校验CRC-16
	if(((*(COM->pucRec_Buffer+6) == CRC_16.CRC_L) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_H))
																								||
	   ((*(COM->pucRec_Buffer+6) == CRC_16.CRC_H) && (*(COM->pucRec_Buffer+7) == CRC_16.CRC_L)))
  {
		//校验地址
		if((*(COM->pucRec_Buffer+0)) == Modbus.Addr)
		{
			//处理数据
			if((*(COM->pucRec_Buffer+1)) == FunctionCode_Read_Register)
			{
				Modbus_Read_Register(COM);
			}
			else if((*(COM->pucRec_Buffer+1)) == FunctionCode_Write_Register)
			{
				Modbus_Wrtie_Register(COM);
			}	
		}
	}
	
	//清缓存
	for(i=0;i<UART3_Rec_LENGTH;i++)
	{
		*(COM->pucRec_Buffer+i) = 0x00;
	}
}

/*
	* @name   Modbus_Read_Register
	* @brief  读寄存器
	* @param  UART -> 串口指针
	* @retval None      
*/
static void Modbus_Read_Register(UART_t* UART)
{
	UART_t* const  COM = UART;
	
	//校验地址
	if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x41))
	{
		回应数据
		//地址码
		*(COM->pucSend_Buffer+0)  = Modbus.Addr;
		//功能码
		*(COM->pucSend_Buffer+1)  = FunctionCode_Read_Register;
		//数据长度
		*(COM->pucSend_Buffer+2)  = 8;
		//SHT30温度
		*(COM->pucSend_Buffer+3)  = ((uint16_t)((SHT30.fTemperature+40)*10))/256;
		*(COM->pucSend_Buffer+4)  = ((uint16_t)((SHT30.fTemperature+40)*10))%256;
		//SHT30湿度
		*(COM->pucSend_Buffer+5)  = 0;
		*(COM->pucSend_Buffer+6)  = SHT30.ucHumidity;
		//继电器状态
		*(COM->pucSend_Buffer+7)  = 0;
		*(COM->pucSend_Buffer+8)  = Relay.Status;
		//蜂鸣器状态
		*(COM->pucSend_Buffer+9)  = 0;
		*(COM->pucSend_Buffer+10) = Buzzer.Status;
		
		//插入CRC
		CRC_16.CRC_Value = CRC_16.CRC_Check(COM->pucSend_Buffer,11); //计算CRC值
		CRC_16.CRC_H     = (uint8_t)(CRC_16.CRC_Value >> 8);
		CRC_16.CRC_L     = (uint8_t)CRC_16.CRC_Value;
		
		*(COM->pucSend_Buffer+11) = CRC_16.CRC_L;
		*(COM->pucSend_Buffer+12) = CRC_16.CRC_H;
		
		//发送数据
		UART3.SendArray(COM->pucSend_Buffer,13);
	}
}

/*
	* @name   Modbus_Read_Register
	* @brief  写寄存器
	* @param  UART -> 串口指针
	* @retval None      
*/
static void Modbus_Wrtie_Register(UART_t* UART)
{
	UART_t* const  COM = UART;
	uint8_t i;
	
	回应数据
	//准备数据
	for(i=0;i<8;i++)
	{
		*(COM->pucSend_Buffer+i) = *(COM->pucRec_Buffer+i);
	}
	//发送数据
	UART3.SendArray(COM->pucSend_Buffer,8);
	
	//提取数据
	//校验地址 -> 继电器
	if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x43))
	{
		//控制继电器
		if(*(COM->pucRec_Buffer+5) == 0x01)
		{
			Relay.Relay_ON();
		}
		else
		{
			Relay.Relay_OFF();
		}
	}
	
	//校验地址 -> 蜂鸣器
	if((*(COM->pucRec_Buffer+2) == 0x9C) && (*(COM->pucRec_Buffer+3) == 0x44))
	{
		//控制蜂鸣器
		if(*(COM->pucRec_Buffer+5) == 0x01)
		{
			Buzzer.ON();
		}
		else
		{
			Buzzer.OFF();
		}
	}
}
/********************************************************
  End Of File
********************************************************/

CRC校验代码

/* Includes ------------------------------------------------------------------*/
#include "MyApplication.h"

/* Private define-------------------------------------------------------------*/

/* Private variables----------------------------------------------------------*/

/* Private function prototypes------------------------------------------------*/      
static uint16_t CRC_Check(uint8_t*,uint8_t);  //CRC校验

/* Public variables-----------------------------------------------------------*/
CRC_16_t  CRC_16 = {0,0,0,CRC_Check};

/*******************************************************
说明:CRC添加到消息中时,低字节先加入,然后高字

CRC计算方法:
 1.预置1个16位的寄存器为十六进制FFFF(即全为1);称此寄存器为CRC寄存器;
 2.把第一个8位二进制数据(既通讯信息帧的第一个字节)与16位的CRC寄存器的低
 8位相异或,把结果放于CRC寄存器;
 3.把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检查右移后的移出位;
 4.如果移出位为0:重复第3步(再次右移一位);
 如果移出位为1:CRC寄存器与多项式A001(1010 0000 0000 0001)进行异或;
 5.重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理;
 6.重复步骤2到步骤5,进行通讯信息帧下一个字节的处理;
 7.将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低
 字节进行交换;
********************************************************/

/*
	* @name   CRC_Check
	* @brief  CRC校验
	* @param  CRC_Ptr->数组指针,LEN->长度
	* @retval CRC校验值      
*/
uint16_t CRC_Check(uint8_t *CRC_Ptr,uint8_t LEN)
{
	uint16_t CRC_Value = 0;
	uint8_t  i         = 0;
	uint8_t  j         = 0;

	CRC_Value = 0xffff;
	for(i=0;i<LEN;i++)
	{
		CRC_Value ^= *(CRC_Ptr+i);
		for(j=0;j<8;j++)
		{
			if(CRC_Value & 0x00001)
				CRC_Value = (CRC_Value >> 1) ^ 0xA001;
			else
				CRC_Value = (CRC_Value >> 1);
		}
	}
	CRC_Value = ((CRC_Value >> 8) +  (CRC_Value << 8)); //交换高低字节

	return CRC_Value;
}
/********************************************************
  End Of File
********************************************************/

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OsgEarth3基础3D图形实现

OsgEarth3基础3D图形实现主要难点Geometry能力姿态支持任意立方体 PolygonCube矩形立方体 Box圆锥体 Cone圆柱体 Cylinder四棱锥 Pyramid球体 Sphere源码示例ElementGeometry圆锥Cone这里尝试在通过OsgEarth提供的各种图形绘制方法&#xff0c;实现基础的3D图形。每个图形除了基…

推荐一个非常实用的程序员导航网站,码农必备!

这是一个非常好用的程序员导航网站&#xff0c;拥有该网站&#xff0c;就拥有一站式导航&#xff0c;再也不用为找网址而发愁了&#xff01; 先直接上网址&#xff1a;https://hao.panziye.com/ 现在来说说为什么推荐该程序员导航网站&#xff01; 1、支持自定义导航网址 该…

pytorch的安装教程

1. 官网 pytorch.org 哎呀呀呀&#xff0c;这是嘛呀哪里有download的按钮啊 别急 &#xff0c;往下拉就ok了哈哈 得看你自己电脑的配置了&#xff0c;自己选就行 、、注意哦 我们得复制一条指令 、 有anaconda的吧 在自己想要的环境里面 黏贴就行 有了 pytorch 可以干嘛呢…

Electron:BrowserView使用方法

我们知道&#xff0c;使用BrowserWindow来创建一个新的窗口&#xff0c;那么如果想在窗口中战胜斯更多的web内容&#xff0c;比如嵌入其他网站的内容&#xff0c;那就使用BrowserView了。 BrowserView的位置是相对于父窗口&#xff0c;比如&#xff1a; 代码如下&#xff1a;…

python项目使用pyinstaller打包

一、安装pyinstaller 打包要使用pyinstaller,使用pip来安装一下这个第三方库,打开命令行输入pip install pyinstaller 二、打包含有多个文件的python项目 1、首先打开电脑的cmd命令行,并切换到项目的根目录 项目路径输入cmd回车,可直接打开含有项目路径的cmd窗口 2、…

【推荐系统】行列式点过程(DPP)算法推导

一、背景 推荐系统主要解决用户和物品之间的相关性&#xff0c;以及推荐列表的多样性。相关性主要通过用户兴趣和物品之间的匹配程度来衡量&#xff0c;希望把用户感兴趣的物品推荐给用户&#xff0c;可以通过CTR预估模型来构建。多样性的衡量没有那么直观&#xff0c;一种方法…

std::unique_ptr(基础和仿写)

目录 一、C参考手册说明 1、解释注释 2、 参考代码 二、对std::unique_ptr分析 1、创建一个unique_ptr 2、无法进行复制构造和赋值操作 3、可以进行移动赋值 4.可以返回unique——ptr 5、管理动态数组 6、在容器中保存指针 三、对std::unique_ptr设计 1、对于的那个…

关于生命周期的面试题vue

1.第一次进入到页面&#xff08;组件&#xff09;会执行哪些生命周期 beforeCreate 》 没有data&#xff0c;没有elcreated 》 有data&#xff0c;没有elbeforeMount 》 有data&#xff0c;没有el&#xff08;其实已经在准备了&#xff09;mounted 》 有data&#xff0c;有el …

SRM供应商管理系统有什么作用?

目前国内稍具规模的企业都导入了企业资源管理ERP系统&#xff0c;实现了内部管理数字化转型&#xff0c;提升了内部各部门之间的协同能力。但是企业供应链管理涉及大量的外部资源&#xff0c;特别是数量庞大的供应商资源&#xff0c;而大部分的ERP系统很难实现采购同供应商之间…

感恩节,感谢2022的转变,有在好好生活!

疫情之下的感恩节 原来今天是感恩节。2022年快要结束了&#xff0c;在这样一个特别的节日里&#xff0c;就不写技术类文章了&#xff0c;写一写我的2022年&#xff0c;这一年&#xff0c;我的学习、工作和生活都有发生改变&#xff0c;感谢过去的时光&#xff0c;改变让我有在…

Linux下:文件与路径、用户管理、常用命令、vim

文章目录第一章&#xff1a; Linux文件与路径1.1 文件结构1.2 基本概念1.3 基本命令信息1.3.1 查看linux 系统信息&#xff08;修改主机名&#xff09;1.3.2 ls1.3.3 cd/pwd1.3.4 pushd命令创建目录栈 第二章&#xff1a; 文件/目录的创建、删除、复制、阅读2.1 mkdir2.2 touc…

书籍Java8 实战 笔记

第5章 使用流 本章内容 1.筛选、切片和匹配 2.查找、匹配和归约 3.使用数值范围等数值流 4.从多个源创建流 5.无限流 5.1 筛选和切片 用谓词筛选&#xff0c;筛选出各不相同的元素&#xff0c;忽略流中的头几个元素&#xff0c;或将流截短至指定长度。 5.1.1 用谓词筛选 就是…

【优化分配】粒子群算法求解火车票分配优化问题【含Matlab源码 1137期】

⛄一、粒子群算法简介 1 引言 自然界中的鸟群和鱼群的群体行为一直是科学家的研究兴趣所在。生物学家Craig Reynolds在1987年提出了一个非常有影响的鸟群聚集模型&#xff0c;在他的仿真中&#xff0c;每一个个体都遵循&#xff1a;避免与邻域个体相撞&#xff1a;匹配邻域个体…

vue3+vite中使用vuex

前言&#xff1a; 在vue3vite创建的项目中使用vuex&#xff0c;要注意的是vite有部分写法和之前的webpack是不同的&#xff0c;比如&#xff0c;他不支持 require&#xff0c;想把vue2的项目直接升级到vue3的时候&#xff0c;需要改很多地方&#xff0c;如果非要使用vite也可以…

Caffeine《二》

《Caffeine&#xff08;Java顶级缓存组件&#xff09;二》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《Caffeine&#xff08;Java顶级缓存组件&#xff09;》《Caffeine&#xff08;Java顶级缓存组件&#xff09;二》8. 缓存驱逐算法8.1 FIFO(First …

ThingsBoard源码解析-设备连接

整体流程 在MqttTransportHandler中进行Mqtt消息处理&#xff0c;以AccessToken认证的设备举例&#xff0c;核心处理流程如下&#xff1a; //MqttTransportHandler 132 processMqttMsg(ctx, (MqttMessage) msg); //MqttTransportHandler 154 processConnect(ctx, (MqttConne…

视频文件转换器有哪些?什么视频文件转换器好用?

视频承载着丰富的文字、声音、图像&#xff0c;能够多维度地调用人的感知能力&#xff0c;可以说是当今时代信息输入的重要载体。 而视频有avi、rm、rmvb、3 gp等多种格式&#xff0c;当我们使用不同设备来观看视频时&#xff0c;就涉及到视频文件格式转换这一问题&#xff0c;…

RabbitMQ系列【14】备份交换机

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言代码实现测试前言 在之前&#xff0c;我们分析了消息可靠性之发布确认、退回机制。当消息到达交换机后&#xff0c;但是没有找到匹配的队列时&#xff0c;退回模式&#xff08;return&…

ms10-046漏洞利用+bypassuac提权

目录 前期准备 漏洞利用 上传文件到目标主机 UAC介绍 使用bypassuac模块绕过uac进行提权。 关于钓鱼链接的拓展 前期准备 Win xp sp3关闭防火墙 实验前提 保证连通性&#xff0c;进行互ping 漏洞利用 进入msf查看需要利用的漏洞&#xff1a;ms10-046 search ms10-046 …

【Kafka】单分区单副本增加至多分区多副本

一、背景 系统&#xff1a;CentOS Linux release 7.9.2009 (Core) Kafka版本&#xff1a;2.11-2.0.0.3.1.4.0-315 [scala版本2.11&#xff1b;kafka 2.0.0版本&#xff1b;基于ambari3.1.4.0-315的版本 ] 二、现象 业务系统中总是报警&#xff1a;kafka消费延迟。 三、问题…