物联网实战--驱动篇之(二)Modbus协议

news2024/10/7 3:18:24

目录

一、modbus简介

二、功能码01、02

三、modbus解析

四、功能码03、04

五、功能码05

六、功能码06

七、功能码16


一、modbus简介

        我们在网上查阅modbus的资料发现很多很杂,modbus-RTU   ASCII  TCP等等,还有跟PLC结合的,地址还分1开头的,4开头的,搞得有点懵,那其实是各行业有各自差异化的规则而已,实际上modbus就是  地址码+功能码+数据区+校验码  了,这是核心,具体可以看这篇比较简洁。modbus rtu六种功能码详细解析-电子发烧友网

        对于我们物联网领域而言,就是方便在主机端接入其它厂家的传感器或执行器设备就是了,比如温湿度、PH计等等,厂家在生产的时候产品定位就是配套应用商,所以modbus协议基本是标配,只是不同厂家的传感器modbus的数据地址定义不一样而已;当然,也有少数厂家用自定义协议,非要用的话就得多费时间去了解它的协议了,那modbus协议的传感器我们如果有驱动的话请求或解析就可以直接复用了,下图是TB上随便搜索的485温湿度传感器的通讯协议。

        另外,modbus对于我们来讲,物理层一般都是RS485的,当然了,要用RS232或TTL串口,甚至是4G、LORA也都是可以的。modbus协议使用的时候一般是主机发送请求,从机回复结果的流程,主机可以请求传感器数据,也可以设置内容,比如控制继电器开关,一应一答,每次轮询根据波特率要有一定间隔,一般几十到几百毫秒,像RS485的波特率不要太高,一般是9600,如果距离有几百米建议更低,4800或2400。在实际工程项目中,主机跟从机的485通讯经常会有莫名其妙的问题(通讯不上或者乱码),这个就要靠经验解决了。

        那我们写modbus驱动文件的意义在哪里,要怎么写?就跟之前用过的mqtt协议一样,我们这个驱动程序主要作用就是组合报文和解析报文,这样当你拿到一个厂家的modbus传感器的时候,直接调用这个驱动文件的函数就可以请求数据了,最后再根据厂家对数据的定义进行应用层解析就行了。接下来我们就几个常用的功能码做详细介绍。

二、功能码01、02

        01的作用就是读取开关量输出状态,02的作用是读取IO输入状态,其实都差不多,返回的数据中,正常每个bit位代表各自的开关/输入状态,比如数据如下:

请求:01 02 00 00 00 04 79 C9

返回:01 02 01 0B E0 4F

这里面的含义是请求起始数据地址为0x0000的4个输入状态,返回的是地址码01、功能码02、数据长度01、数据区0B和CRC校验码E0 4F,那对于应用层来讲,有用的数据就是0B了,我们现在把0B换成二进制显示是 0000 1011,那么这个数据正常理解就是1、2、4路输入触发,3路正常,具体的要以厂家提供的资料为准。功能码01也是一个道理的。这里面,如果你请求的寄存器数量小于8个,那从机会返回一个字节数据,每个bit代表一个寄存器状态,多余的是高位无效,一般用0代替。

下面是具体的请求代码:


/*		
================================================================================
描述 :modbus 0x01的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun01(u8 slave_addr, u16 reg_start, u16 reg_num, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x01;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_num>>8;
	make_buff[make_len++]=reg_num;
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

/*		
================================================================================
描述 :modbus 0x02的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun02(u8 slave_addr, u16 reg_start, u16 reg_num, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x02;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_num>>8;
	make_buff[make_len++]=reg_num;
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

        代码的核心就是根据从机地址、寄存器起始地址、寄存器数量来组合报文,应用层再把这个报文发送出去,至于是使用RS485还是RS232都是可以的,驱动层不关心。

三、modbus解析

        对于协议解析就很有讲究了,我看到的大部分解析代码都是数据包丢进解析函数里,然后函数直接就CRC校验,出错就返回了,这样的解析代码其实稳定性不太好,因为实际传输的时候会经常莫名其妙的数据头或尾巴多出个00或FF或者其它数据,但是完整正确的数据包又在里面,这样CRC校验肯定是错的,所以这里我把解析函数升级了下,这样鲁棒性会好点。

/*		
================================================================================
描述 : modbus 基础数据解析
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_parse_base(u8 slave_addr, u8 fun_code, u8 *in_buff, u16 in_len, u8 *out_buff, u16 out_size)
{
  u16 recv_len=in_len, crcValue;
  u8 *pData=in_buff;
  u8 data_len=0;
  if(recv_len<4 || recv_len>250)
    return 0;
  for(u8 i=0;i+4<recv_len;i++,pData++)
  {
    if(pData[0]==slave_addr && pData[1]==fun_code)//比较地址和功能码
    {
      data_len=pData[2];
      crcValue=pData[data_len+3]<<8|pData[data_len+4];
      if(crcValue==drv_crc16(pData, data_len+3))
      {
        if(data_len<out_size)
        {
          memcpy(out_buff, &pData[3], data_len);
        }
        else
        {
          data_len=0;
        }          
        break;
      }						

    }				
  }
  return data_len;
}

        首先要声明的是这个代码适合读取数据的功能码,比如01、02、03和04,对于设置功能码05、06成功了直接返回相同报文,可以用字符串匹配的方式进行确认,这里先略过。这段代码的核心是匹配用户需要的从机地址和功能码,这就相当于数据标识头了,有了这个以后,至少可以过滤掉前面无用的干扰数据了,之后的第三字节就是数据长度了,有了这个长度值就能够准确地做CRC校验了, 校验成功之后把数据区的内容复制出来就可以了,剩下的是应用层的事情了。

        对于功能码01和02,对应的解析函数就是调用上面的函数就行了,具体如下。


/*		
================================================================================
描述 : modbus 0x01 数据解析
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_parse_fun01(u8 slave_addr, u8 *in_buff, u16 in_len, u8 *out_buff, u16 out_size)
{
  return drv_modbus_parse_base(slave_addr, 0x01, in_buff, in_len, out_buff, out_size);
}


/*		
================================================================================
描述 : modbus 0x02 数据解析
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_parse_fun02(u8 slave_addr, u8 *in_buff, u16 in_len, u8 *out_buff, u16 out_size)
{
  return drv_modbus_parse_base(slave_addr, 0x02, in_buff, in_len, out_buff, out_size);
}

四、功能码03、04

        03和04功能码比较相似,差别在于03是保持寄存器,可读可写,04是输入寄存器,只读。比如热敏温度头的数据一般就用04来读取,主机不能改变;空调的设定温度一般用03来读取,同时可以用06来设置更改。具体组合报文代码如下:


/*		
================================================================================
描述 :modbus 0x03的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun03(u8 slave_addr, u16 reg_start, u16 reg_num, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x03;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_num>>8;
	make_buff[make_len++]=reg_num;
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

/*		
================================================================================
描述 :modbus 0x04的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun04(u8 slave_addr, u16 reg_start, u16 reg_num, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x04;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_num>>8;
	make_buff[make_len++]=reg_num;
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

        跟01、02功能码是差不多的,解析代码也是类似:

/*		
================================================================================
描述 : modbus 0x03 数据解析
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_parse_fun03(u8 slave_addr, u8 *in_buff, u16 in_len, u8 *out_buff, u16 out_size)
{
  return drv_modbus_parse_base(slave_addr, 0x03, in_buff, in_len, out_buff, out_size);
}

/*		
================================================================================
描述 : modbus 0x04 数据解析
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_parse_fun04(u8 slave_addr, u8 *in_buff, u16 in_len, u8 *out_buff, u16 out_size)
{
  return drv_modbus_parse_base(slave_addr, 0x04, in_buff, in_len, out_buff, out_size);
}

        应用层数据解析以文章开头的温湿度为例,调用drv_modbus_parse_fun03函数后,out_buff内的数据就是02 92 FF 9B四个字节,具体的转换如下图所示。

五、功能码05

        其作用是设置单路输出,比如第二路继电器开,开就往寄存器内设置FF 00,关就设置00 00,设置成功后就直接返回原数据。那么,对于05功能码的返回要如何处理呢?两个选择,一个是用我的工程里drv_common.c的memstr函数做匹配,看下返回的数据包里有没有包含刚才设置的数据串;另一个选择是直接忽略,有没有设置成功不要在这里观测,而是用01功能码实时读取输出状态值,如果状态不匹配要怎么处理由应用层自己决定,比如重复执行3次后仍然失败那就向用户发端出故障信息,人工介入等等。

/*		
================================================================================
描述 :modbus 0x05的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun05(u8 slave_addr, u16 reg_start, u16 reg_value, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x05;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_value>>8;
	make_buff[make_len++]=reg_value;	
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

六、功能码06

        06和03对应的寄存器是一样的,03读06写,比如空调预设温度、净化器预设转速等这些都可以叫保持寄存器。有点区别是 03可以批量读取连续的寄存器,06只能单个设置,06的具体代码如下:


/*		
================================================================================
描述 :modbus 0x06的报文组合
输入 : 
输出 :  
================================================================================
*/
u16 drv_modbus_send_fun06(u8 slave_addr, u16 reg_start, u16 reg_value, u8 *make_buff, u16 make_size)
{
  if(make_size<20)
  {
    return 0;
  }
	u16 make_len=0;
	u16 crcValue;
	
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x06;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start;
	make_buff[make_len++]=reg_value>>8;
	make_buff[make_len++]=reg_value;	
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	return make_len;
}

        06的返回解析跟05类似,正常直接忽略就行了,用03功能码去监测到底有没有设置成功。

七、功能码16

        16是十进制的,也就是16进制的0x10功能码,是06的扩展,它可以批量设置寄存器,稍微复杂点,具体看如下代码:

/*		
================================================================================
描述 : modbus 0x10的报文组合
输入 : 
输出 : 
================================================================================
*/
u16 drv_modbus_send_fun16(u8 slave_addr, u16 reg_start, u16 reg_num, u8 *reg_data, u8 *make_buff, u16 make_size)
{
	u16 make_len=0;
	u16 crcValue;
	u8 	data_len=reg_num*2;

	if(make_size<10+data_len)
		return 0;
	make_buff[make_len++]=slave_addr;
	make_buff[make_len++]=0x10;
	make_buff[make_len++]=reg_start>>8;
	make_buff[make_len++]=reg_start; //寄存器起始地址
	make_buff[make_len++]=reg_num>>8;
	make_buff[make_len++]=reg_num;	//寄存器数量
	make_buff[make_len++]=data_len;//数据区长度
	memcpy(&make_buff[make_len], reg_data, data_len);//数据区
	make_len+=data_len;
	crcValue=drv_crc16(make_buff, make_len);
	make_buff[make_len++]=crcValue>>8;
	make_buff[make_len++]=crcValue;
	
	return make_len;		
}

        返回也是忽略就行了,用03去读取监测。

modbus的解析大概就是这样了,完整版的内容比较多,但是根据平时项目的使用频率来看,常用的就这些了,其他的要学习只能自己再找找资料了。

具体代码在这里下载https://download.csdn.net/download/ypp240124016/89091325

工程原来上传过了,自己添加驱动程序测试就行了。https://download.csdn.net/download/ypp240124016/89044525

本项目的交流QQ群:701889554

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

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

相关文章

WCH恒沁单片机-CH32V307学习记录2----FreeRTOS移植

RISC-V 单片机 FreeRTOS 移植 前面用了 5 篇博客详细介绍了 FreeRTOS 在 ARM Cortex-M3 MCU 上是如何运行的。 FreeRTOS从代码层面进行原理分析系列 现在我直接用之前的 RISC-V MCU 开发板子&#xff08;CH32V307VCT6&#xff09;再次对 FreeRTOS 进行移植&#xff0c;其实也…

《图解Vue3.0》- 调试

如何对vue3项目进行调试 调试是开发过程中必备的一项技能&#xff0c;掌握了这项技能&#xff0c;可以很好的定义bug所在。一般在开发vue3项目时&#xff0c;有三种方式。 代码中添加debugger;使用浏览器调试&#xff1a;sourcemap需启用vs code 调试&#xff1a;先开启node服…

Android APP加固利器:深入了解混淆算法与混淆配置

Android APP 加固是优化 APK 安全性的一种方法&#xff0c;常见的加固方式有混淆代码、加壳、数据加密、动态加载等。下面介绍一下 Android APP 加固的具体实现方式。 混淆代码 使用 ipaguard工具可以对代码进行混淆&#xff0c;使得反编译出来的代码很难阅读和理解&#xff…

VMwear桥接网络正确配置+静态IP设置

1.桥接网络配置 很多时候在VMware安装完虚拟机之后&#xff0c;会发现配置的桥接网络没有起作用&#xff0c;如果是Linux下输入ifconfig发现只有ipv6的地址而没有ipv4&#xff0c;说明没有桥接没有启用成功&#xff0c;需要按照以下方式来设置 在VMware的左上角打开编辑&#…

注解式 WebSocket - 构建 群聊、单聊 系统

目录 前言 注解式 WebSocket 构建聊天系统 群聊系统&#xff08;基本框架&#xff09; 群聊系统&#xff08;添加昵称&#xff09; 单聊系统 WebSocket 作用域下无法注入 Spring Bean 对象&#xff1f; 考虑离线消息 前言 很久之前&#xff0c;咱们聊过 WebSocket 编程式…

Nuxt 3 项目中配置 Tailwind CSS

官方文档&#xff1a;https://www.tailwindcss.cn/docs/guides/nuxtjs#standard 安装 Tailwind CSS 及其相关依赖 执行如下命令&#xff0c;在 Nuxt 项目中安装 Tailwind CSS 及其相关依赖 npm install -D tailwindcss postcss autoprefixerpnpm install -D tailwindcss post…

字符迁移.

3.字符迁移【算法赛】 - 蓝桥云课 (lanqiao.cn) 问题描述 小蓝最近获得了一个长度为N 的字符串S&#xff0c;他对它爱不释手。 小桥为了考验小蓝对字符串的处理能力&#xff0c;决定给他提出一个挑战&#xff0c;她会进行 Q次操作&#xff1a; 每次操作给定三个整数 l , r , k …

Vue3调试

如何对vue3项目进行调试 调试是开发过程中必备的一项技能&#xff0c;掌握了这项技能&#xff0c;可以很好的定义bug所在。一般在开发vue3项目时&#xff0c;有三种方式。 代码中添加debugger;使用浏览器调试&#xff1a;sourcemap需启用vs code 调试&#xff1a;先开启node服…

夯实智慧新能源数据底座,TiDB Serverless 在 Sandisolar+ 的应用实践

本文介绍了 SandiSolar通过 TiDB Serverless 构建智慧新能源数据底座的思路与实践。作为一家致力于为全球提供清洁电力解决方案的新能源企业&#xff0c;SandiSolar面临着处理大量实时数据的挑战。为了应对这一问题&#xff0c;SandiSolar选择了 TiDB Serverless 作为他们的数据…

PostgrerSQL基本使用与数据备份

前言 上篇了解了 PostgrerSQL 数据库的部署PostgreSQL关系型数据库介绍与部署-CSDN博客&#xff0c;本篇将继续就其基本操作、备份与还原内容做相关介绍。 目录 一、数据库的操作 1. 本机登录 2. 开启远程登录 2.1 开放远程端口 2.2 编辑配置文件 2.3 修改配置密码 2.…

基于单片机高压输电线路微机保护系统设计

**单片机设计介绍&#xff0c;基于单片机高压输电线路微机保护系统设计 文章目录 一 概要二、功能设计三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机高压输电线路微机保护系统设计是一个涉及电力系统继电保护的复杂工程。该系统主要利用单片机作为控制核心&…

【深度学习】海洋生物数据集,图片分类

文章目录 任务描述数据收集数据处理模型训练指标评测web app代码和帮助 任务描述 收集9种以上的海洋生物图片&#xff0c;然后基于深度学习做一个分类模型&#xff0c;训练完成后&#xff0c;分类模型就可以对未知图片进行分类。 在之后随便传一张图片&#xff0c;分类模型就…

MySQL 50 道查询题汇总,足以巩固大部分查询(附带数据准备SQL、题型分析、演示、50道题的完整SQL)

目录 MySQL 50 道查询题&#xff0c;足以巩固大部分查询数据准备&#xff1a;创建表sql添加表数据sql 50道查询题目汇总01 - 05 题&#xff1a;1、查询 “01” 语文成绩比 “02” 数学成绩高的学生的信息及课程分数2、查询 "01语文课程"比"02数学课程"成绩…

【前端】JavaScript(概念+语法+形式+变量+数组+函数+作用域)

文章目录 JavaScript一、JavsScript概念1.JavaScript的开发方向2.JavaScript和CSS、HTML的关系3.JavaScript运行过程4.JavaScript的组成 二、JavaScript的语法1.JS的书写形式1.行内式2.内嵌式3.外部式4.注释5.输入输出1.prompt和alert2.输出: console.log 2.变量的使用1.创建变…

如何使用CSS构建一个瀑布流布局

如何使用CSS构建一个瀑布流布局 瀑布流布局是一种常见的网页布局方式&#xff0c;其中元素以不同的大小排列&#xff0c;且行与列之间没有不均匀的间隙。在瀑布流布局中&#xff0c;即使某一行或列中的元素较短&#xff0c;下一个元素也会占据空间。 如何实现瀑布流布局 实现…

双连通分量算法

1. 连通图概念 连通图&#xff1a;无向图任意两点之间存在通路。 强连通&#xff1a;有向图&#xff08;前提&#xff09;中&#xff0c;任意两点都有至少一条通路&#xff0c;则此图为强连通图。 弱连通图&#xff1a;将有向图的有向边换成无向边得到的图是连通图&#xff0c…

如何在 Ubuntu 上安装和配置 Tomcat 服务器?

简介&#xff1a;最近有粉丝朋友在问如何在 Ubuntu 上安装和配置 Tomcat 服务器&#xff1f;今天特地写这篇文章进行解答&#xff0c;希望能够帮助到大家。 文章目录 Ubuntu上安装和配置Tomcat的详细步骤Tomcat在Linux环境下的安装与配置一、下载并上传Tomcat压缩包二、启动To…

【单片机】CJSH22-CH2O,甲醛传感器,甲醛传感器数据读取处理

原理图 解析程序 逻辑是&#xff1a; 1、初始化串口和定时器10ms中断 2、循环读取一帧数据到rev_CH2O_bufferdata 3、在主函数解析数据rev_CH2O_bufferdata 4、最终的pm2.5数值就是CH2O_value 使用CH2O_value的数据即可。 PPB单位&#xff0c;除以1000就是ppm&#xff0c;再…

华为ensp中高级acl (控制列表) 原理和配置命令 (详解)

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月6日23点18分 高级acl&#xff08;Access Control List&#xff09;是一种访问控制列表&#xff0c;可以根据数据包的源IP地址、目标IP地址、源端口、目标端口、协议…

每日五道java面试题之ZooKeeper篇(三)

目录&#xff1a; 第一题. 会话管理第二题. 服务器角色第三题. Zookeeper 下 Server 工作状态第四题. 数据同步第五题. zookeeper 是如何保证事务的顺序一致性的&#xff1f; 第一题. 会话管理 分桶策略&#xff1a;将类似的会话放在同一区块中进行管理&#xff0c;以便于 Zoo…