欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

RS485温湿度传感器与modbus协议

程序员文章站 2022-07-09 13:46:51
...

一、modbus协议介绍

Modbus协议是应用于 电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一通用工业标准。有了它,不同厂商生产的控制设备可以连成 工业网络,进行集中监控。此协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一控制器请求访问其它设备的过程,如何回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。
Modbus具有以下几个特点:
(1)标准、开放,用户可以免费、放心地使用Modbus协议,不需要交纳许可证费,也不会侵犯知识产权。
(2)Modbus可以支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。
(3)Modbus的帧格式简单、紧凑,通俗易懂。用户使用容易,厂商开发简单。

二、modbus与RS485的关系

首先RS485是硬件层的协议,而Modbus是在这个硬件层之上的软件层协议,是应用层报文传输协议。通俗点来讲,ModBus规约了主从机,主机要分别发送什么命令给从机。ModBus规定主从机之间数据的交互,需要遵循什么样的格式,如何保证数据在传输过程中不发生冲突。只要都遵循这个协议,那么不同厂家的主从机就可以共用了。Modbus协议包括RTU、ASCII、TCP。其中MODBUS-RTU最常用,比较简单。
ModBus一般是工作在一主多从的场景,示意图如下:
RS485温湿度传感器与modbus协议

三、传感器使用

3.1传感器介绍

此处的传感器,我选择了威海晶合公司的485空气温湿度传感器,因为是485通信,所以需要一个TTL转485的转换器,经过这个转换器,才能在传感器和单片机之间建立连接,另外此传感器也可以直接与PC机进行通信,所以此处还需要一个USB转485转换器。该传感器连接示意图如下:
RS485温湿度传感器与modbus协议
与PC机相连如下:
RS485温湿度传感器与modbus协议
与单片机相连如下:
RS485温湿度传感器与modbus协议
经过查阅该传感器使用手册,该传感器通讯协议数据位为8位,无奇偶校验位,停止位1位,错误校验CRC(16),波特率9600。采用 Modbus-RTU 通讯规约,格式如下: 初始结构>=4 字节的时间 ;地址码= 1 字节; 功能码= 1 字节 ;数据区= N 字节;错误校验= 16 位 CRC 码; 结束结构>=4 字节的时间 ;

地址码:为变送器的地址,在通讯网络中是唯一的(出厂默认 0x01)。
功能码:主机所发指令功能提示,本变送器只用到功能码 0x03(读取存器 数据)。
数据区:数据区是具体通讯数区,注意 16bits 数据高字节在前
CRC 码:二字节的校验码。

向该设备发送问询帧的格式如下图:
RS485温湿度传感器与modbus协议
例如查询该设备的地址:
FF 03 00 0F 00 00 60 17
传感器返回的数据应答帧的格式如下图:
RS485温湿度传感器与modbus协议

3.2 PC机与传感器

首先,先按照规定接线方式,进行安装连接,连接好后,打开CommMontor串口监控精灵,如下图:

RS485温湿度传感器与modbus协议
点击左侧窗口中的打开串口,就可以在此处与传感器进行通信了。
注意:对传输的参数设置,如下图。
RS485温湿度传感器与modbus协议
RS485温湿度传感器与modbus协议
依据数据帧的格式,向传感器发送查询温湿度的命令
RS485温湿度传感器与modbus协议
传感器返回一段应答帧,依据应答帧的格式,则此处的第4、第5位为温度数据,第6、第7位为湿度数据,因为是16进制,所以将其转换为10进制后,即为国际单位下的温湿度值,例如此处返回的温度值为01 3B,湿度值为 02 25。
RS485温湿度传感器与modbus协议
经过进制转换,得到此时温度值为31.5度。RS485温湿度传感器与modbus协议
类似的把02 25经过进制转换,得到此时湿度值54.9%RH。

3.3 单片机与传感器

按照安装要求,传感器与TTL转485转换器相连,然后该转换器连接dan单片机的串口。
注:该传感器需要单独的电源适配器进行供电。

单片机做主机,传感器做从机,主机程序首先定义modbus控制器

m_protocol_dev_typedef m_ctrl_dev;	//定义modbus控制器

在主机程序中使用的主要函数如下

首先是数据帧解析函数:

//解析一帧数据,解析结果存在fx指针里边
//fx是帧指针
//返回值:解析结果,0,OK,其他,错误代码。
m_result mb_unpack_frame(m_frame_typedef *fx)
{
 	u16 rxchkval=0;   	 		//接受到的校验值 
	u16 calchkval=0;			//计算得到的校验值
 	u8 datalen=0; 				//有效数据长度
	fx->datalen=0; 				//数据长度清0
	if(m_ctrl_dev.rxlen>M_MAX_FRAME_LENGTH||m_ctrl_dev.rxlen<M_MIN_FRAME_LENGTH)
	{
		m_ctrl_dev.rxlen=0;			//清除rxlen
		m_ctrl_dev.frameok=0;		//清除framok标记,以便下次可以正常接受,
		return MR_FRAME_FORMAT_ERR;//帧格式错误
	}
	datalen=m_ctrl_dev.rxbuf[3];
	switch(m_ctrl_dev.checkmode)
	{
		case M_FRAME_CHECK_SUM:							//校验和
			calchkval=mc_check_sum(m_ctrl_dev.rxbuf,datalen+4);
			rxchkval=m_ctrl_dev.rxbuf[datalen+4];
			break;
		case M_FRAME_CHECK_XOR:							//异或校验
			calchkval=mc_check_xor(m_ctrl_dev.rxbuf,datalen+4);
			rxchkval=m_ctrl_dev.rxbuf[datalen+4];
			break;
		case M_FRAME_CHECK_CRC8:						//CEC8校验
			calchkval=mc_check_crc8(m_ctrl_dev.rxbuf,datalen+4);
			rxchkval=m_ctrl_dev.rxbuf[datalen+4];
			break;
		case M_FRAME_CHECK_CRC16:						//CRC16校验
			calchkval=mc_check_crc16(m_ctrl_dev.rxbuf,datalen+4);
			rxchkval=((u16)m_ctrl_dev.rxbuf[datalen+4]<<8)+m_ctrl_dev.rxbuf[datalen+5];
			break;
	} 	
	m_ctrl_dev.rxlen=0;			//清除rxlen
	m_ctrl_dev.frameok=0;		//清除framok标记,以便下次可以正常接受,
	if(calchkval==rxchkval)		//校验正常
	{
		fx->address=m_ctrl_dev.rxbuf[0];
		fx->function=m_ctrl_dev.rxbuf[1];
		fx->count=m_ctrl_dev.rxbuf[2];
		fx->datalen=m_ctrl_dev.rxbuf[3];
		if(fx->datalen)
		{
			fx->data=mymalloc(SRAMIN,fx->datalen);		//申请内存
			for(datalen=0;datalen<fx->datalen;datalen++)
			{
				fx->data[datalen]=m_ctrl_dev.rxbuf[4+datalen];		//拷贝数据
			}
		}
		fx->chkval=rxchkval;	//记录校验值	
	}else return MR_FRAME_CHECK_ERR;
	return MR_OK;	
}

然后是打包数据帧函数

//fx指向需要打包的帧¡  
void mb_packsend_frame(m_frame_typedef *fx)
{  
	u16 i;
	u16 calchkval=0;			//计算得到的校验值
	u16 framelen=0;				//打包后的帧长度
	u8 *sendbuf;				//发送缓冲区
	
	if(m_ctrl_dev.checkmode==M_FRAME_CHECK_CRC16)framelen=6+fx->datalen;
	else framelen=5+fx->datalen;
	sendbuf=mymalloc(SRAMIN,framelen);	//申请内存
	sendbuf[0]=fx->address;
	sendbuf[1]=fx->function;
	sendbuf[2]=fx->count;
	sendbuf[3]=fx->datalen; 
	for(i=0;i<fx->datalen;i++)
	{
		sendbuf[4+i]=fx->data[i];
	}	
	switch(m_ctrl_dev.checkmode)
	{
		case M_FRAME_CHECK_SUM:							//校验和
			calchkval=mc_check_sum(sendbuf,fx->datalen+4); 
			break;
		case M_FRAME_CHECK_XOR:							//异或校验
			calchkval=mc_check_xor(sendbuf,fx->datalen+4); 
			break;
		case M_FRAME_CHECK_CRC8:						//CRC8校验
			calchkval=mc_check_crc8(sendbuf,fx->datalen+4); 
			break;
		case M_FRAME_CHECK_CRC16:						//CRC16校验
			calchkval=mc_check_crc16(sendbuf,fx->datalen+4); 
			break;
	} 
	
	if(m_ctrl_dev.checkmode==M_FRAME_CHECK_CRC16)		//如果是CRC16,则有2个字节的CRC¸
	{
		sendbuf[4+fx->datalen]=(calchkval>>8)&0XFF; 	//高字节在前
		sendbuf[5+fx->datalen]=calchkval&0XFF;			//低字节在后
	}else sendbuf[4+fx->datalen]=calchkval&0XFF;
	mp_send_data(sendbuf,framelen);	//发送这一帧数据
	myfree(SRAMIN,sendbuf);			//释放内存
}

然后是modbus初始化函数

m_result mb_init(u8 checkmode)
{
	m_ctrl_dev.rxbuf=mymalloc(SRAMIN,M_MAX_FRAME_LENGTH);	//申请最大的帧接受缓存
	m_ctrl_dev.rxlen=0;
	m_ctrl_dev.frameok=0;
	m_ctrl_dev.checkmode=checkmode;
	if(m_ctrl_dev.rxbuf)return MR_OK; //根据返回值判断初始化成功
	else return MR_MEMORY_ERR;//初始化不成功
}

然后是modbus结束函数

void mb_destroy(void)
{
	myfree(SRAMIN,m_ctrl_dev.rxbuf);	//申请最大的帧接受缓存
	m_ctrl_dev.rxlen=0;
	m_ctrl_dev.frameok=0; 
}

串口中断服务函数

void USART1_IRQHandler(void)
{
	u8 res;	
	if(USART1->SR&(1<<5))			//接受到数据
	{	 
		res=USART1->DR; 
		if(m_ctrl_dev.frameok==0)	//接收未完成
		{ 
			m_ctrl_dev.rxbuf[m_ctrl_dev.rxlen]=res;
			m_ctrl_dev.rxlen++;
			if(m_ctrl_dev.rxlen>(M_MAX_FRAME_LENGTH-1))m_ctrl_dev.rxlen=0;//接收数据错误,重新开始接受
  		}  		 									     
	}else if(USART1->SR&(1<<4))		//空闲中断
	{
		res=USART1->DR; 			//
		m_ctrl_dev.frameok=1;		//标记完成一帧数据接收
	}
} 

然后是对串口进行初始化函数

void mp_init(u32 pclk2,u32 bound)
{  	  
	RCC->APB2ENR|=1<<2;   //使能PORTA口时钟
	RCC->APB2ENR|=1<<14;  //使能串口时钟
	GPIOA->CRH&=0XFFFFF00F;//IO口状态设置
	GPIOA->CRH|=0X000008B0;//IO口状态设置
	RCC->APB2RSTR|=1<<14;   //复位串口1
	RCC->APB2RSTR&=~(1<<14);//,停止复位 	   
 	USART1->BRR=(pclk2*1000000)/bound;//设置波特率
	USART1->CR1|=0X200C;  //1位停止位,无校验位
	USART1->CR1|=1<<4;	  //开启串口总线空闲中断
	USART1->CR1|=1<<5;    // 接收缓冲区非空中断使能	
	MY_NVIC_Init(3,3,USART1_IRQn,2);//最低优先级组2

然后是发送指定数据长度函数

void mp_send_data(u8* buf, u16 len)		
{
	u16 i=0;
	for(i=0;i<len;i++)
	{
		while((USART1->SR&0X40)==0);	//等待上一次串口数据发送完成
		USART1->DR=buf[i];      		//串口1将发送数据
	}	
}

使用CRC16校验函数

u16 mc_check_crc16(u8 *buf,u16 len)//buf是待检验缓冲区首地址,len是检验长度
{
	u8 index;
	u16 check16=0;
	u8 crc_low=0XFF;
	u8 crc_high=0XFF;
	while(len--)
	{
		index=crc_high^(*buf++);
		crc_high=crc_low^CRC16HiTable[index];
		crc_low=CRC16LoTable[index];
	}
	check16 +=crc_high;
	check16 <<=8;
	check16+=crc_low;
	return check16; //返回CRC16校验值,高字节在前,低字节在后
}

CRC16位校验,又称循环校验码,是数据通信领域中最常用的一种差错校验码,其特征是信息字段和校验字段的长度可以任意选定。任意一个由二进制位串组成的代码都可以和一个系数仅为‘0’和‘1’取值的多项式一一对应。例如:代码1010111对应的多项式为x^6+ x^4+ x^2+x+1, 而多项式为x^5+ x^3+ x^2+ x+1对应的代码101111。在modbus通信时,发送方发送出信息数据和校验码,接收方则使用相同的计算方法计算出信息字段的校验码,对比接收到的实际校验码,如果相等及信息正确,不相等则信息错误;或者将接受到的所有信息除多项式,如果能够除尽,则信息正确。常用查表法和计算法。计算方法一般都是:

(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寄存器的高、低
字节进行交换;

(8)最后得到的CRC寄存器内容即为:CRC码。

CRC16高字节位校验表

const u8 CRC16HiTable[]=
{ 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
	0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
	0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
	0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
	0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
	0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
	0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
	0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
	0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
	0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
	0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
	0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
	0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40   
};

CRC16低字节位校验表

const u8 CRC16LoTable[]=
{
	0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
	0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
	0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
	0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
	0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
	0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
	0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
	0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
	0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
	0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
	0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
	0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
	0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
	0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
	0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
	0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
	0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
	0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
	0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
	0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
	0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
	0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
	0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
	0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
	0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
	0x43, 0x83, 0x41, 0x81, 0x80, 0x40     
};

modbus发送一帧数据函数

void modbus_send_frame(m_frame_typedef * fx)//fx是帧指针
{
    static u8 fcnt=0;
    u8 i;
    u8 res;
    m_frame_typedef rxframe;
    fx->address=0X01;
    fx->function=0X03;
    fx->count=fcnt;
    fx->datalen=5;
    fx->data=mymalloc(SRAMIN,5);
    for(i=0;i<5;i++)
    {
        fx->data[i]=fcnt+i;
    }
    for(i=0;i<10;i++)
    {
        mb_packsend_frame(fx);
        delay_ms(50); 					//发送间隔
        if(m_ctrl_dev.frameok)          //解析应答数据
        {
            m_ctrl_dev.frameok=0;
            res=mb_unpack_frame(&rxframe);
			if(res==MR_OK)	            //解析成功
			{
                myfree(SRAMIN,rxframe.data);
				if(rxframe.count==fcnt)
                { 
                    fcnt++;
                    break;//判断响应正常
                }
			}   
        }
    } 

}

四、modbus RTU常用功能码

常用功能码有10个,如下图,其中每个功能前边的数字即为实现该功能的功能码。
RS485温湿度传感器与modbus协议