前言
1、通信协议包括两种,硬件层协议(解决传输问题0和1的问题,比如RS232,RS485、CAN、IIC、SPI等)和软件层协议(比如modbus、TCP/IP)
2、RS485如下差分方式传输信号,当A比B高时 = 高电平。常用的是MAX485芯片。这个设计因为接收器和发送器是集成在一起的,某一时刻不是处于发送状态就是处于接收状态,属于半双工,当RE=0 的时候,为接收模式,当 RE=1 的时候,为发送模式,
3、通信方式
- 单工:数据只在一个方向上传输,不能实现双方通信。电视、广播。
- 半双工:允许数据在两个方向上传输,但是同一时间数据只能在一个方向上传输,其实际上是切换的单工。对讲机。
- 全双工:允许数据在两个方向上同时传输。手机通话。
4、主从模式:
- 从机不能主动向主机发送数据
- 系统中只有一个设备是主机
- 任何一次的数据交换,都要由主机来发起,从机一直处于监听状态
- 发起一次通信,首先主机将自己转换为发送状态,按照预先约定的格式发送一组寻址数据帧
- 每个从机必须有一个唯一的地址
5、多主模式:网络通信,CAN通信等,为避免大家都发导致线上乱套,会采用硬件机制,对线上的冲突进行检测,有一个防冲突载波监听技术。485是不具备冲突检测,所以必须要遵从主从模式。
6、Mbps = Mbit/s,每秒比特数;Mpps = Mpacket/s,每秒包数。比如你每秒发1M的512比特长度的包,那么pps速度就是1Mpps,bps速度就是512Mbps。
7、1波特每秒即指每秒传输1个码元符号(通过不同的调制方式,可以在一个码元符号上负载多个bit位信息),1比特每秒是指每秒传输1比特(bit)。
准备
规定
1、modbus是主从模式(也就是说,不能同步进行通信,总线上每次只有一个数据进行传输,即主机发送,从机应答,主机不发送,总线上就没有数据通信。),通信必须遵从主从模式(下图是基于RS485的modbus)
2、modbus上的地址是从0-247号,其中0号是广播地址主机保留(即主机向0号地址发送数据包的时候,所有从机都需要接收到,从机接收后不予回应,因为如果所有从机都回应了也没什么用)
3、设置地址有两个目的
- 为了主机有针对性的找某个从机
- 从机发回数据包的时候不至于让其他从机产生误会
4、modbus两种传输模式
- RTU(远程传输单元):也叫作十六进制或者二进制方式,工作时通常用这种,传输效率高。例如
要发0x03,就发0000 0011有一位发一位,按照485先发低位 - ASC,发送每一位的ASCII码。例如
要发0x03,先发’0’的ASCII码0x30(发送0011 0000),再发‘3’的ASCII码0x33(发送0011 0011),所以相对的效率比较低,但是可以直接打印传输的数字,也就是在总线上安装一个监控设备,直接对传输的设备进行打印。方便调试 - 设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是RTU,ASCII作为选项。(也就是说,一般的设备只有RTU这个协议,ASCII一般很少)所以说,一般学习Modbus协议,只需要了解RTU的协议,ASCII作为学习的了解就足够了。
帧格式
帧结构 = 地址 + 功能吗 + 数据 + 校验
- 地址: 占用一个字节,范围0-255,其中有效范围是1-247,其他有特殊用途,比如255是广播地址(广播地址就是应答所有地址,正常的需要两个设备的地址一样才能进行查询和回复)。
- 功能码:占用一个字节,功能码的意义就是,知道这个指令是干啥的,比如你可以查询从机的数据,也可以修改数据,所以不同功能码对应不同功能。
- 数据:根据功能码不同,有不同结构,在后续的实例中有说明。
- 校验:为了保证数据不错误,增加这个,然后再把前面的数据进行计算看数据是否一致,如果一致,就说明这帧数据是正确的,我再回复;如果不一样,说明你这个数据在传输的时候出了问题,数据不对的,所以就抛弃了。
注意RTU没有帧头和帧尾,所以协议里明确两帧之间要大于3.5个字节时间间隔,作为一帧结束的判断依据。对于RS485来说,总线上一般允许最大32个设备。ASCII用到了再说 基本是相同的。
主机发送帧格式
1、传输模式有两种,ASC和RTU。其中ASC就是将RTU中的数据的ASCII码发送出去,所以发送量是RTU的一倍(比如0x03,就需要一个0的ASCII和一个3的ASCII)。RTU没有开始停止位,以超过3.5(10个bit)的时间作为停止;ASC以" : "开始,以\r\n结束
2、从机以接收数据停止时间达到3.5byte,就认为数据包发送完成。所以在做主机程序的人,要将数据包一口气发完不要中间停顿
3、3.5字节所需时间,就是波特率9600bit/s,一位为为8个数据为和一个停止位+一个起始位组成,这样的组合3.5个所需时间,大约是4个ms。0x0D(asc码是13) 指的是“回车” \r是把光标置于本行行首;0x0A(asc码是10) 指的是“换行” \n是把光标置于下一行的同一列
从设备的回应数据包格式
1、回应的数据包与主机查询的数据包格式是一致的,就下面这个需要修改
2、正常的回应时,返回的功能码与主机发送过来的功能码一致(也就是1-127);如果是异常的回应(告知主机我返回的数据包存在问题,可能是从机传感器读取到的数据本身有问题的,但是不得不回应主机,所以发送这个回应给主机),则在主机发送过来的功能码上,加上128,当主机发现收到的功能 >=128了,就知道数据是错的
3、返回数据包,发校验码的时候,应该是先发低字节的,但是所用的调试助手,需要先发高字节而已
协议实现
1、硬件上具备串口或者485接口、网口
2、硬件上必须具备一个定时器,且需要精确到ms级
功能码
Modbus-RTU协议只需要看懂功能码0x03、0x06、0x10这三个基本的就已经足够了;分别回想下其数据域部分:
0x03–主机需要发送起始地址+寄存器数量,从机回复总字节数+数据;
0x06–主机发送起始地址+数据内容(因为你只需要修改一个,所以起始地址就是所要修改的地址),从机返回起始地址+数据内容(发现居然一样!)
0x10–主机发送起始地址+寄存器个数+总字节数+数据,从机返回起始地址+寄存器数量
功能码03,读寄存器
主机发送
- 04 设备地址 第0个字节
- 03 功能码 第1个字节
- 00 05 查询的起始寄存器地址(即从0x 00 05开始查询),占用两个字节 2 3
- 00 02读取的数量(查询两个) 4 5
- D4 5F 校验码(从机需对除这个校验码之外所有的数进行CRC校验,得到的数需要与这个发送过去的校验码相同)6 7
主机接收到的为:
- 04,设备地址
- 03,功能码
- 04,代表后面数据的字节数 即有4个字节的数要返回(因为读两个寄存器,一个寄存器是2个字节)
- 00 05,五号寄存器里的值
- 00 06,六号寄存器里的值
- 3F 30 CRC校验码(返回数据包,发校验码的时候,应该是先发低字节的,但是所用的调试助手,需要先发高字节而已)
u16 Reg[]={
0x0000, //本设备寄存器中的值,从0开始的
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
功能码06,写寄存器
6号寄存器改成6666
主机发送
- 04,地址 0
- 06,功能码 1
- 00 06 修改六号寄存器 2 3
- 66 66 要修改的值 4 5
- C2 14 循环冗余校验,是modbus的校验公式,从首个字节开始到48前面为止; 6 7
如果回复的一样,说明这个数据是修改成功的;
接收,和发送的相同代表数据修改是成功的
```c
u16 Reg[]={
0x0000, //本设备寄存器中的值,从0开始的
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
0x10 修改多个
开关量的读写
也就是真与假只有这么两个 对应的是线圈状态的读写
读对应功能码01 写对应功能码05
下位机实现
调试
02 03 00 02 00 01 25 F9
02 03 00 05 00 06 3F 30
02 03 00 03 00 02 34 38
1号寄存器写入6666
02 06 00 01 66 66 73 B3
使用定时器基本接收
1、创建MODBUS结构体
2、初始化modbus
- 设置从设备地址
- modbus.Tim_Run设置为0,1定时器就开始计时
- 开启接收中断
3、定时器回调函数
当modbus.Tim_Run!=0开始计时,即串口有数据进来的时候开启计时 modbus.Timout++;没进入一次串口中断回调函数,就会将其清零 当接收完一帧数据的时候(3.5个接收间隔,没有接收到数据),将modbus.R_Flag置1,开始对数据包进行处理
4、串口回调函数
保存接收到的数据,启动定时器计时,并不断将modbus.Timout清零
5、对数据包进行处理
- 判断校验码对不对
- 判断是不是发送给自己的,还是广播的
- 根据功能码,调用相应的函数对其进行处理
使用IDLE
调试小精灵上不行,但在串口上实现了
main()
#include <stdio.h>
#include "usart.h"
#include "modbus.h"
Modbus_Init();
while (1)
{
Modbus_Event();
}
modbus.c
#include "modbus.h"
#include "modbusCRC.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>
MODBUS modbus;
extern DMA_HandleTypeDef hdma_usart1_rx;
uint16_t Reg[]={
0x0000, //本设备寄存器中的值
0x0001,
0x0002,
0x0003,
0x0004,
0x0005,
0x0006,
0x0007,
0x0008,
0x0009,
0x000A,
};
extern uint8_t rx_buffer[100];
/*
因为波特率 9600
1位数据的时间为 1000000us/9600bit/s=104us
一个字节为 104us*10位 =1040us
所以 MODBUS确定一个数据帧完成的时间为 1040us*3.5=3.64ms ->10ms
*/
void Modbus_Init()
{
modbus.My_Add=2; //本从设备的地址
HAL_UART_Receive_DMA(&huart1,modbus.R_buf,100);
}
void Modbus_fun3() //3号功能码处理 ---主机要读取本从机的寄存器
{
uint16_t Regadd;//要读的地址
uint16_t Reglen;//寄存器的数量
uint16_t byte;
uint16_t i,j;
uint16_t crc;
Regadd=modbus.R_buf[2]*256+modbus.R_buf[3]; //得到要读取的寄存器的首地址 这个因为是16进制的两个字节为一个数,所