Modbus是一种串行通信协议,因此在介绍MODBUS之前,有必要了解一下更为基础的知识,即串行通讯。并且对于很多初学者来说,分清串行通讯、485通讯、MODBUS通讯协议是进行MODBUS通讯的第一步。那么首先我们先区别这三个通讯分别是什么,之间的关系又是什么。
一个通讯的完成,根据计算机系统的层级划分,可以分为物理层、数据链路层、应用层等等等等,在这里,我们只介绍MODBUS一对一的通讯方式,因此只涉及到物理层、数据链路层、应用层等部分分。对于上述各个层级,可能比较抽象,这里我自己的理解是:
电气层(属于物理层):电气层是我为了读者方便理解,自己加的。其具体含义是,两个设备要进行通讯,首先应该进行电气参数的匹配,如果一个设备输出电压是24V,另一个设备输入电压是5V,那么很有可能通讯的时候设备直接被烧坏了。此外,什么是高点平、什么是低电平,两边通讯时,需要共同约定一个规则来定义1和0,这种规则往往是一种阈值电压或差分电压等。换一种角度理解,通讯其实就是各个设备在进行讲话,电气层就是要讲的一句话中,一个单词里的一个字母如何定义。RS485就是一种电气上的接口,它规定了正差分就是1,负差分就是0。
物理层:物理层规定了一个字节是怎么发送的,我们都知道,一个字节是8位,一般来说,如果只有一根通讯线,需要发8位字节的话,就需要用同一根线在不同的时刻发8次,这种通讯方式就是串行通讯。如果我们有8根通讯线,那么就只需要在同一时刻8根通讯线同时发送,就能够将一个字节的数据发送出去。换种角度理解,物理层就是规定,所要说的话中,一个单词是怎么说出来的。串行通讯就是物理层的规则定义,它定义了一个字节是通过怎样的方式进行发送和接收。
数据链路层:数据链路层规定了一个数据帧如何发送和接受。数据帧可以理解为一段能够表达通讯需求的若干个字节。即如何将数据组合成数据块,在数据链路层中称这种数据块为帧(frame),帧是数据链路层的传送单位;如何控制帧在物理信道上的传输,包括如何处理传输差错,如何调节发送速率以使与接收方相匹配;以及在两个网络实体之间提供数据链路通路的建立、维持和释放的管理(来源于百度百科)。换种角度理解,数据链路层就是规定了如何将一个一个的单词(字节)组合成一句话,并且一句话应该按照怎样的方式去说和理解。Modbus就是一种在数据链路层中的协议规定,他规定了一种开源的通用数据帧格式,按照这种格式进行通讯的两个设备就能较为稳定的进行通讯。
应用层:应用层就是用户对于已经接受到的数据进行何种处理来实现用户的应用的需求,这一部分是软件上用户自己定义的。
这里我所做的内容是通过STM32进行控制,通过RS485电气接口进行串行通讯,并采用Modbus通讯协议。
在理解了上述三个的联系和区别以后,下面就对各个内容进行分别介绍。
串行通讯是一种数据通讯方式,与并行通讯的同一时刻多条数据线同时发送数据不同,串行通讯是在不同时刻依次发送一个字节的各个位。
根据串行通讯的收发功能不同,串行通讯一般可以分为单工、半双工、全双工。单工是指该串行通讯只能收或者只能发,只能进行单方向的工作。半双工是指,该串行通讯既可以收,也可以发,但是同一时刻下,只能进行收或发。全双工是指,该通讯可以同时进行收和发。
根据串行通讯是否需要进行时钟的同步,可以分为同步通讯和异步通讯两种。这里采用RS485的通讯接口,接口电路中没有专门传递时钟信号的传输线,因此只能采用异步的串行通讯的方式进行收发。
在进行串行通讯时,我们的目标是正确的传输一个字节(8位二进制),那么在数据传输过程中,就需要输出和输入两方对串口协议进行一个统一的规定。相关的参数如下:
波特率:波特率是指每秒中传递的二进制的位数。为了避免数据接收时按照错误的节拍接收数据,串口通讯需要提前设置好接收和发送端的波特率。举例说明,假设波特率是9600bts/s,那么传递一个字节(8位),需要的时间是0.83ms,每个位持续的时间是0.104ms。此时如果接收端按照每0.104ms是一个位来对数据进行判断,那么接收端能够接收到正确的信息。但此时如果接收端按照每0.008ms(115200bit/s)一个位的话,可以看出,当发送端还未将全部的数据位发送完成,接收端就已经有了8位数据。这就是接受和发送没有统一波特率带来的通讯出错。
起始位:起始位必须是持续一个比特时间的逻辑“0”电平,标志传送一个字节的开始。
数据位:数据位是需要传递的数据,一般来说是8位,也有7位、6位等。
奇偶校验位:为了保证传递数据的正确性,可以在传递完数据后进行一次奇偶校验,可以选择奇校验或者偶校验或者不校验。奇偶校验是指数据位中,1或者0的个数是奇数还是偶数。
停止位:在上述数据位发送结束后,再发送1bit时间的高电平,表示发送结束。也可设定为1.5或2位时间。
在进行使用时,读者可以仅了解一次串行发送时的电平时序关系,但要始终注意发送和接受两端要采用相同的串行通讯协议,也就是采用相同的波特率、奇偶校验方式、停止位持续时间等。
STM32的串行通讯的使用可以参照STM32的参考手册USART章节,需要注意的是,STM32的USART是全双工异步串口通讯,有两个传输线TX和RX,其中TX为发送,RX为接收。在进行与其他串口连接时,一般来说,是STM32的TX接外部设备的RX,STM32的RX接外部设备的TX。
RS485是一种电气接口和电平定义,我们已经知道了串行通讯能够传递一个字节,可是这是建立在两个设备能正确传递一个位的基础上才能实现。RS485就是解决如何传递一个位。这就需要设备两端在电气层面上有连接,并且采用相同的电气参数来定义0和1。而RS485就是一种电气接口和电平定义。RS485具有A、B两根线,并且通过A、B两根线之间的压差来定义1和0。A和B的压差在+(2-6)V内为高电平,A和B的压差在-(2-6)V内为低电平。由此就定义了RS485的电气接口和电平。
因为RS485虽然有两根线,但是是通过两根线的压差来表示1和0。这就造成了RS485其实是只有一个数据传输通道,同一时刻只能收或者只能发,因此RS485是一种半双工的通讯方式。
此外还应注意的问题是,RS485设备之间是A接A,B接B。
延伸来说,RS485通讯逐渐取代RS232通讯,是因为RS485的接口电平更小,不易损坏芯片,并且兼容TTL(5V-0V)电平。此外RS485通过两根AB线通讯,可以多台设备组成通讯网络,实现总线通讯。
在STM32中采用RS485,因为STM32的串口输出为全双工,输出电压为3.3V或0V,而RS485为半双工,需要甚至-6V到+6V的电压,因此STM32串口的数据不能直接通过RS485进行递。因此在使用过程中,常常通过485芯片来对电平进行转换。例如SP3485或MAX3485等芯片。
485芯片中,和STM32相连的有一个发送引脚、一个输出引脚、和一个使能引脚,当使能引脚为0时,485此时为接收态,AB中的压差会作为数据发送给STM32。当使能引脚为1时,485为发送态,STM32发送的数据会改变AB线的压差。
此外,在进行硬件电路设计时,如果两个设备之间的传输距离较长, 需要在两个设备的终端各加一个120R的终端匹配电阻,以避免数据传输造成的回流噪声干扰AB线的电压。
并且在没有任何数据传输的情况下,AB线中的压差是不确定的,有可能存在因为外部干扰等因素,而造成AB线中的压差超过±2V,从而产生了错误的接收。因此有些一硬件设计在AB线中加入了弱上拉或者弱下拉,来限定无数据通讯时的电压。也需要注意在长距离、强干扰下的电磁屏蔽等问题。
ModbusRTU是一种主从通讯模式的通讯协议,也就是说,Modbus有一个主机,可以进行通讯的主动要求,其他从机只能对主机进行响应而不能主动发送数据到通讯总线中。这种方法规定了通讯过程中的通讯次序等关系,避免了多个设备同时工作的情况下通讯冲突的产生。
Modbus协议不规定一个字节如何传输,而是规定如何进行一次数据帧的传输。数据帧可以理解为若干个具有特殊功能意义的字节的组合。那么Modbus如何定义一个数据帧?对于Modbus来说,当进行数据传输过程中,出现空闲时间超过3.5个字节持续时间,就认为一次数据帧的结束,之前接收到的字节就是这次数据帧的所有字节。之后再接收到的字节则为下一个数据帧的字节。例如,在9600bit/s的传输速率下,一个字节传输的时间约为0.8ms,那么当数据传输中,出现约3ms的空闲时间时,设备就认为一帧数据接收完成。
每一帧数据要实现功能,传递信息,就需要通讯双方采用相同的语法规则,因此对于每个数据帧的构成,Modbus进行了一些规定,例如一个主机发送的具有寄存器读取功能的数据帧,其组成为:
设备地址 | 功能码 | 寄存器起始地址 | 读取寄存器的个数 | CRC校验 |
---|---|---|---|---|
1字节 | 1字节 | 2字节 | 2字节 | 2字节 |
0x02 | 0x03 | 0x00 0x00 | 0x00 0x01 (n) | 0x44 0x3F |
而一个从机响应的数据帧为:
设备地址 | 功能码 | 返回的字节的个数 | 对应寄存器的数据 | CRC校验 |
---|---|---|---|---|
1字节 | 1字节 | 1字节 | 2字节(2*n字节) | 2字节 |
0x02 | 0x03 | 0x02(2*n) | 0x00 0x01(…) | 0x… 0x… |
更一般的来说,一个数据帧可以分为:设备码、功能码、数据码、校验码这四个部分。
设备码代表的含义是,该数据帧的目标设备是什么或者该数据帧的来源设备是什么。对于一主多从的通讯架构中,主机的设备码为0,其他各个从机都有独立的设备码,总共分配255个从机设备码,在通讯过程中,主机把数据发送大AB总线中,各个从机从AB总线中接收数据,对于不属于该设备码的数据帧,将数据放回至总线,对于属于该设备码的数据帧,设备进行相应的处理后,对主机进行响应,此时设备码的含义就是指定主机的通讯目标设备。设备对主机的响应,即返回一组新的数据帧,此时数据帧的设备地址仍为0x03,但是此时设备地址的含义则变为“向主机表明该数据的来源”。通过设备码,主机可以和多个从机进行数据通讯而避免了其他设备错误响应不属于该设备的通讯。
功能码的含义是表明该通讯帧的功能或目的。具体来说,例如功能码3表明主机要求读取从机若干个寄存器的数值,从机接收到通讯帧后进行一系列的处理,返回给主机相同的功能码,表明从机对该功能进行了响应。此外如果数据帧存在错误,例如格式不正确、漏发、校验不正确等,从机在返回功能码时,会在原有的功能码的基础上加128,来表示是哪一段功能码出错,主机在接收到后就能根据此时返回的功能码来进行错误判断。因此功能码既有表达该帧功能的作用,又有表示该次通讯是否错误的功能。在Modbus协议中可以有0到255个功能码,其中有许多约定好的功能码例如:3为度寄存器功能码,16为写寄存器功能码等等,也有一些用户可以自行定义的功能码。具体功能码对应的含义,可以找Modbus通用协议的文章或书本来查阅。
数据码是对功能码的进一步补充和解释,常见的功能码的数据吗格式一般在Modbus通用协议中已经做了规范,例如对于3功能码,后面跟的数据码包括2个字节表示寄存器个数,2个字节表示读取的寄存器个数(寄存器的位数为16位,因此一个寄存器有两个字节的数据),而返回的3功能码,后接1个字节的返回字节个数(该个数应为上述读取寄存器个数的两倍,因为一个寄存器对应两个字节),和若干个字节的数据。此外,有些功能码时用户自己定义的,后接的数据码也可以根据用户需求自行定义。
Modbus一般采用的16位的CRC校验。什么是CRC校验呢,简单来说,比如你要发一段数据,最后想在后面加两个字节,这两个字节是通过前面所发的数据通过某种算法计算出来的唯一的两个字节。如果发送过程中无问题,那么接收端通过相同的算法能够得到同样的最后两位字节,这就表明该帧的所有的数据都是正确的。如果接收端通过同样的算法算出来的两个字节数据和发送过来的最后两个字节不同,那么就表明在发送和接收过程中,有些数据接收或发送错误。通过校验位来判断该帧是否正确发送和接收。
CRC就是其中一种校验方法,其算法是将所发的数据左移一定位数(例如Modbus就是左移16位,从而空出两个字节)后,与一个约定好的17位二进制数进行模2除法(例如Modbus就是 1 1000 0000 0000 0101),最后得到的余数就是我们所需要的校验码。接收端在接收到数据帧后,把整个带有校验码的数据进行同样的操作,最后得到的余数为0,则表明校验码正确,如果不为0,则表明校验码出错。
具体模2除法是什么东西,简单来说,就是对除数和被除数进行按位异或,然后除数移位,然后两个进行异或,然后除数移位,如此循环,直到除数和被除数位数相同,结束运算。具体过程可以参考被人写的博客,例如:https://blog.csdn.net/tjd10061/article/details/48808633
CRC的具体实现参考下述代码部分。
STM32实现RS485的Modbus通讯过程。根据上述我们对串口、485、Modbus的讲解,这里我们会用到STM32的串口功能(用于收发数据)、I/O功能(用于使能和失能485的收发)、定时器功能(用于对接收的数据的间隔进行计时,以判断数据帧是否接收完成)、CRC功能(进行CRC校验)以及Modbus的服务函数。
整体的代码框架为:如果STM32作为从机,在进行串口初始化后,通过I/O使485常态处于接收态,并采用串口中断读取接收到的每一个字节,在每次接收字节时,开启计时器,如果计时器计时溢出,表明时间间隔大于3.5个字节接收时间,即一帧接收完成,此时进入计时器中断,可以进行Modbus的处理函数。Modbus的处理函数首先会判断设备是否是该设备,如果不是,则直接结束处理。如果是,则会进行CRC校验,如果CRC校验正确,则根据不同的功能码进行不同的服务函数。如果CRC校验不正确,则返回相应的错误代码。
当STM32需要发送数据时,先把485置于发送态,然后通过串口发送数据即可。
1.串口代码:包括串口和I/O的初始化、串口中断、串口发送
//串口和I/O的初始化
void rs485_uart1_init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//外设的时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIO A 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIO B 时钟
//GPIO参数设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); X
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//串口参数设置
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_ClearFlag(USART1, USART_FLAG_TC);
//中断设置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
GPIO_ResetBits(RS485_TX_EN);
}
//串口中断
void USART1_IRQHandler(void)
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判断是否接收到数据
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取数据
TIM_SetCounter(TIM2,0);
TIM_Cmd(TIM2, ENABLE); //开启计时器
USART_RX_BUF[RS485_RX_CNT]=Res ;
RS485_RX_CNT++;
}
}
//串口发送
void RS485_Send_Data(u8 *buf,u8 len)
{
u8 t;
GPIO_SetBits(RS485_TX_EN);
for(t=0;t
2.定时器代码:定时器的初始化、定时器中断
//定时器初始化
void TIM2_Init(u16 arr,u16 psc) //PSC=7200-1 一次计数0.1ms
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //TIMER2使能
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler =psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE );
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_SetCounter(TIM2,0);
TIM_Cmd(TIM2, DISABLE);
}
//定时器中断
void TIM2_IRQHandler()
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update)!=RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
TIM_Cmd(TIM2, DISABLE); //¹Ø±ÕʱÖÓ
// LED_Change();
*Flag_of_Modbus_Ok=1;
Modbus_Work();
}
}
3.CRC校验函数
u16 CRC_16( u8 *vptr, u8 len)
{
uint16_t TCPCRC = 0xffff;
uint16_t POLYNOMIAL = 0xa001;
uint8_t i, j;
for (i = 0; i < len; i++)
{
TCPCRC ^= vptr[i] ;
for (j = 0; j < 8; j++)
{
if ((TCPCRC & 0x0001) != 0)
{
TCPCRC >>= 1;
TCPCRC ^= POLYNOMIAL;
}
else
{
TCPCRC >>= 1;
}
}
}
return TCPCRC;
}
4.Modbus函数:包括Modbus处理函数和Modbus功能服务函数
//Modbus处理函数
void Modbus_Work(void)
{
u8 t;
if(*Flag_of_Modbus_Ok==1)
{
if(USART_RX_BUF[0]==0x03) //判断设备地址码是否正确
{
if((CRC_16(USART_RX_BUF,RS485_RX_CNT))==0x0000) //判断CRC校验是否正确
{
switch(USART_RX_BUF[1]) //根据功能码选择服务函数
{
case 0x03: //功能码03
Modbus_03_Solve();
break;
case 0x10: //功能码16
Modbus_16_Solve();
break;
default: //未定义的功能码
USART_TX_BUF[0]=0x03;
USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
USART_TX_BUF[2]=0X01;
((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
RS485_Send_Data(USART_TX_BUF,5);
break;
}
}
else //CRC校验不正确
{
USART_TX_BUF[0]=0x03;
USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
USART_TX_BUF[2]=0X04;
((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
RS485_Send_Data(USART_TX_BUF,5);
}
}
else memset(USART_RX_BUF,0,sizeof(USART_RX_BUF)); //非设备地址码,清除接收
RS485_RX_CNT=0;
*Flag_of_Modbus_Ok=0;
}
}
//Modbus 03功能码处理函数
void Modbus_03_Solve(void)
{
u8 t;
if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
{
if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))
{
USART_TX_BUF[0]=0x03;
USART_TX_BUF[1]=0x03;
USART_TX_BUF[2]=USART_RX_BUF[5]*2 ;
for(t=0;t<(USART_TX_BUF[2]);t++)
{
USART_TX_BUF[3+t]=(t%2==0)?(work_register[USART_RX_BUF[3]+(t/2)]/256):(work_register[USART_RX_BUF[3]+(t/2)]%256);
}
((u16)*(USART_TX_BUF+(3+USART_TX_BUF[2])))=(CRC_16(USART_TX_BUF,3+USART_TX_BUF[2]));
RS485_Send_Data(USART_TX_BUF,5+USART_TX_BUF[2]);
}
else
{
USART_TX_BUF[0]=0x03;
USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
USART_TX_BUF[2]=0x02;
((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
RS485_Send_Data(USART_TX_BUF,5);
}
}
else
{
USART_TX_BUF[0]=0x03;
USART_TX_BUF[1]=0x80 | USART_RX_BUF[1];
USART_TX_BUF[2]=0x02;
((u16)*(USART_TX_BUF+3))=CRC_16(USART_TX_BUF,3);
RS485_Send_Data(USART_TX_BUF,5);
}
}
//Modbus 16功能码服务函数
void Modbus_16_Solve(void)
{
u8 t;
if((USART_RX_BUF[2]==0x00)&&(USART_RX_BUF[3]<=0xff))
{
if((USART_RX_BUF[4]==0x00)&&(USART_RX_BUF[5]<=0xff))
{
for(t=0;t
1.STM32采集到的数据帧的位数和主机发送的数据帧的位数一致,但是接收到的数据内容为FF FE EF类似的乱码。
原因:有可能是RS485的A、B线接反,一般来说,A接A、B接B,但是不排除有些板子内部接反了。此外,RS485中A、B线的电压不稳定等原因也有可能造成乱码的存在。
2.STM32可以收到数据,但是没有数据发出。
原因:有可能是RS485的使能I/O没有起作用。
3.确定主机有发数据出来,但是STM32无数据接收。
原因:检查是否是STM32的USART TX和RX和RS485芯片的接收和发送端接反了。
4…(更多问题等待实践的进一步探索,未完待续)