RS485通讯–Modbus物理层:https://blog.csdn.net/qq_36883460/article/details/105630712
Modbus RTU通讯协议中OSI模型,数据链路层和应用层是通讯关键部分,下面是对Modbus RTU的介绍。
总所周知,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那样发送数据那么快,所以
发送每个字节的数据之间,时间限制在小于1.5个字符之内,这个就自己算一下大概多少个吧…
正因如此,这个机制需要定时器来计时,计算数据有无超时。
总的来说就是在应用层基础上,头部加一个从机地址,尾部增加一个CRC校验,这个应该不难理解。
一帧的数据结构如下:
从机地址 | 功能码 | 数据值 | CRC校验 |
---|---|---|---|
1个字节 | 2个字节 | N个字节 | 2个字节 |
下面是来自Modbus中文手册的彩图,图中单位为位(Bit)
总长度为: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个字节 |
以上对于使用串口来说已经够用了。
软件开发平台:KEIL 5
STM32软件库 :HAL函数库
使用freemodbus-v1.5.0,这个版本已经很久没有更新了,freemodbus应用于嵌入式的通讯协议,一个奥地利的大佬写的,在此处感谢这个大佬,如果是linux的话有libmodbus库。
freemodbus文件结构
文件名 | 说明 |
---|---|
demo | 存放实际的使用案例,底层驱动已经编写好,供参考 |
doc | 这个用浏览器打不开。。。我不知道这个干吗的。。 |
modbus | 实际Modbus通讯协议代码,没有底层驱动代码 |
tools | 测试工具,作用不大。。 |
不过这样套用他们代码,显然不够灵活,我更推荐自己编写代码逻辑。
如果只想使用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]
//调用自己编写的发送缓冲区函数
}
一般使用中断的方式,将接收数据放入缓冲区,然后使用定时器判定一帧数据有没有完成接收。
一帧数据完成接收机制,按照实际设定波特率,能否完成一帧数据来计算,并不需要循规蹈矩。