RS485通讯---Modbus数据链路层与应用层(二)

前言

RS485通讯–Modbus物理层:https://blog.csdn.net/qq_36883460/article/details/105630712

Modbus RTU通讯协议中OSI模型,数据链路层和应用层是通讯关键部分,下面是对Modbus RTU的介绍。
RS485通讯---Modbus数据链路层与应用层(二)_第1张图片
总所周知,RS485只是一种远距离传输手段,最根本的也就是串口消息发送,也就是串口TLL电平转出RS485特定电平而已。

一、连接通讯材料

如果想实践一下这个Modbus RTU通讯协议,老衲有几个方案。

A方案:
一路USART/UART串口通讯(单片机)
一个Modbus主机仿真软件
一个USB转串口
一台电脑

这个比较好对于学习很有帮助,而且比较经济实惠,还可以学习编写底层代码实现。

B方案:
两路USART/UART串口通讯(单片机)

自己发出去自己收回来,有点像电脑本地网络回环测试,这个老衲突发奇想应该行不通。。。

C方案:
一个主机仿真软件,比如 Modbus Poll
一个从机仿真软件,比如 Modbus Slave

不过是基于TCP本地回环网络进行测试,也就是(127.0.0.1:端口号)自己发送自己接收,这种办法最省钱上网找两软件,适合玩玩仿真。

D方案:
一路USART/UART串口通讯(单片机)
一个USART/UART转485电平电路
一个RS485转USB
一个Modbus主机仿真软件
一台电脑
一条自制简易STP双绞线(或者自己买条双绞线)

自制简易STP双绞线,找两个线(杜邦线)每米30转,自买铝箔纸或是锡纸包住两根线。材料怎么用就不用多说了吧,这一个比较切实际,但是自己话学习的成本挺高。

二、应用层

长度:功能码(1个字节)+ 数据值(N个字节)

总的长度不能超过253个字节,也就是说数据部分最多是252个字节。

一般四个都够用了0x1、0x3、0x5、0x10,下面是常用的功能码:

功能码 说明
0x01 读一个或多个线圈
0x03 读一个或多个寄存器
0x05 写单个线圈
0x06 写单个寄存器
0x0F 写多个线圈
0x10 写一个或多个寄存器

线圈就是读取一个位(Bit)寄存器就是读取两个字节(Byte)

功能码:0x01

主机发送:

功能码 数据起始地址 线圈个数
01 00 00 00 02
1个字节 2个字节 2个字节

从机响应:

功能码 字节数 线圈值
01 01 02
1个字节 1个字节 1个字节

三、数据链路层

数据链路层把是把上层进行封装,也就是将应用层的东西进行一个组装,有点像是TCP/IP协议。
数据链路层中单位为,每帧之间隔发送消息的间隔至少3.5个字符,一个字符就是8位,总共就是28位。由于单片机使用串口发送,不像TCP、UDP那样发送数据那么快,所以
RS485通讯---Modbus数据链路层与应用层(二)_第2张图片
发送每个字节的数据之间,时间限制在小于1.5个字符之内,这个就自己算一下大概多少个吧…
RS485通讯---Modbus数据链路层与应用层(二)_第3张图片
正因如此,这个机制需要定时器来计时,计算数据有无超时。

总的来说就是在应用层基础上,头部加一个从机地址,尾部增加一个CRC校验,这个应该不难理解。

一帧的数据结构如下

从机地址 功能码 数据值 CRC校验
1个字节 2个字节 N个字节 2个字节

下面是来自Modbus中文手册的彩图,图中单位为位(Bit)
RS485通讯---Modbus数据链路层与应用层(二)_第4张图片

总长度为:256个字节

从机地址的范围在 0 ~ 248
广播地址范围 0
广播就会让所有的从机节点接收到数据,一般用来完成总体设备控制

单播地址范围 1 ~ 47
主机跟从机进行单独通讯

保留地址范围 55 ~ 248

CRC校验码
这个代码不用担心写不出来,已经有前辈写了好了。上网搜索一下CRC16位校验,大部分都是Modbus RTU有关,freemodbus中也有相应的函数。

又或者这里我给出一段计算CRC-16校验代码,直接调用crc16_updata计算就好

/* CRC校验码高位 */
const unsigned char auchCRCHi_updata[] = {
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
};

/* CRC校验码低位 */
const unsigned char auchCRCLo_updata[]  = {
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
};

/*-------------------------------------------------------------------------------------- 
函数功能:CRC-16校验码计算
入口参数:m_u8hMsg		计算校验数据缓冲区
		 m_u8DataLen	缓冲区长度
		 m_Crc			存放计算校验码缓冲区
---------------------------------------------------------------------------------------*/
void crc16_updata(uint8_t * m_u8hMsg,uint16_t m_u8DataLen,uint8_t *m_Crc)
{
	uint8_t mid_u8CRCHi = 0xFF;
	uint8_t mid_u8CRCLo = 0xFF;
	uint16_t uIndex;					
	while(m_u8DataLen--)
	{
		uIndex = mid_u8CRCHi^ *m_u8hMsg++;
		mid_u8CRCHi = mid_u8CRCLo^ auchCRCHi_updata[uIndex];
		mid_u8CRCLo = auchCRCLo_updata[uIndex];
	}
	m_Crc[0]=mid_u8CRCHi;  
	m_Crc[1]=mid_u8CRCLo;  
}

四、功能码实例

1、功能码 0x01
读多个线圈,一个线圈代表一个位。
主机发送

从机地址 功能码 数据起始地址 线圈个数 CRC校验
01 01 00 00 00 02 BD CB
1个字节 1个字节 2个字节 2个字节 2个字节

从机响应

从机地址 功能码 字节数 线圈值 CRC校验
01 01 01 02 D0 49
1个字节 1个字节 1个字节 1个字节 2个字节

2、功能码 0x03
读多个寄存器,每个寄存器16位。
主机发送

从机地址 功能码 数据起始地址 寄存器个数 CRC校验
01 03 00 00 00 02 BD CB
1个字节 1个字节 2个字节 2个字节 2个字节

从机响应

从机地址 功能码 字节个数 寄存器值 CRC校验
01 03 04 02 01 02 22 G1 49
1个字节 1个字节 1个字节 4个字节 2个字节

3、功能码 0x10
写多个寄存器,即发送多个数据,写每个寄存器16位,可以写入1至120个寄存器。
主机发送

从机地址 功能码 数据起始地址 写入寄存器个数 数据长度 数据 CRC校验
01 10 00 00 00 xx xx 00 xx
1个字节 1个字节 2个字节 2个字节 1个字节 多个字节 2个字节

从机响应

从机地址 功能码 已经写入的寄存器个数 CRC校验
01 10 xx xx xx xx
1个字节 1个字节 2个字节 2个字节

以上对于使用串口来说已经够用了。

五、freemodbus库

软件开发平台:KEIL 5

STM32软件库 :HAL函数库

使用freemodbus-v1.5.0,这个版本已经很久没有更新了,freemodbus应用于嵌入式的通讯协议,一个奥地利的大佬写的,在此处感谢这个大佬,如果是linux的话有libmodbus库

freemodbus文件结构

RS485通讯---Modbus数据链路层与应用层(二)_第5张图片

文件名 说明
demo 存放实际的使用案例,底层驱动已经编写好,供参考
doc 这个用浏览器打不开。。。我不知道这个干吗的。。
modbus 实际Modbus通讯协议代码,没有底层驱动代码
tools 测试工具,作用不大。。

不过这样套用他们代码,显然不够灵活,我更推荐自己编写代码逻辑。

六、组织Modbus RTU报文

如果只想使用Modbus RTU协议的访问寄存器,可以通过C语言组织modbus rtu协议报文,即向发送缓冲区一个值一个值的写入。

void SendModbusRTU(void)
{
	unsigned char buffer[1024]; //发送缓冲区
	
	buffer[0]=0xFF; //从机地址
	buffer[1]=0x03;//访问寄存器
	buffer[2]=0x03;//寄存器地址(高位)
	buffer[3]=0xe8;//寄存器地址(地位)
	
	//访问寄存器3个数(高位) 
	buffer[4]=0x00;
	buffer[5]=0x03
	
	//调用crc16函数校验代码,并写入buffer[6]和buffer[7]
	
	//调用自己编写的发送缓冲区函数
	
}

一般使用中断的方式,将接收数据放入缓冲区,然后使用定时器判定一帧数据有没有完成接收。

一帧数据完成接收机制,按照实际设定波特率,能否完成一帧数据来计算,并不需要循规蹈矩。

你可能感兴趣的:(STM32,c语言,stm32,modbus)