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

目录

一、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代替。

下面是具体的请求代码:

cpp 复制代码
/*		
================================================================================
描述 :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校验肯定是错的,所以这里我把解析函数升级了下,这样鲁棒性会好点。

cpp 复制代码
/*		
================================================================================
描述 : 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,对应的解析函数就是调用上面的函数就行了,具体如下。

cpp 复制代码
/*		
================================================================================
描述 : 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来设置更改。具体组合报文代码如下:

cpp 复制代码
/*		
================================================================================
描述 :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功能码是差不多的,解析代码也是类似:

cpp 复制代码
/*		
================================================================================
描述 : 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次后仍然失败那就向用户发端出故障信息,人工介入等等。

cpp 复制代码
/*		
================================================================================
描述 :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的具体代码如下:

cpp 复制代码
/*		
================================================================================
描述 :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的扩展,它可以批量设置寄存器,稍微复杂点,具体看如下代码:

cpp 复制代码
/*		
================================================================================
描述 : 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

相关推荐
TDengine (老段)5 小时前
TDengine 数学函数 DEGRESS 用户手册
大数据·数据库·sql·物联网·时序数据库·iot·tdengine
TDengine (老段)5 小时前
TDengine 数学函数 GREATEST 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
朱嘉鼎5 小时前
状态机的介绍
c语言·单片机
清风6666666 小时前
基于单片机的噪声波形检测与分贝测量仪设计
单片机·嵌入式硬件·毕业设计·课程设计
易享电子7 小时前
基于单片机车窗环境监测控制系统Proteus仿真(含全部资料)
单片机·嵌入式硬件·fpga开发·51单片机·proteus
三佛科技-134163842127 小时前
LED氛围灯方案开发MCU控制芯片
单片机·嵌入式硬件·智能家居·pcb工艺
小莞尔8 小时前
【51单片机】【protues仿真】基于51单片机主从串行通信系统
c语言·单片机·嵌入式硬件·物联网·51单片机
Hello_Embed8 小时前
STM32 环境监测项目笔记(一):DHT11 温湿度传感器原理与驱动实现
c语言·笔记·stm32·单片机·嵌入式软件
三佛科技-134163842129 小时前
便携式榨汁机方案开发,榨汁机果汁机MCU控制方案设计
单片机·嵌入式硬件·智能家居·pcb工艺
yongui4783410 小时前
基于TMS320F28027实现光伏MPPT控制
单片机·嵌入式硬件