MODBUS协议学习
一、理论了解
经过查资料了解了什么是modbus、通过什么实现、用所学的STM32实现modbus通讯协议需 要做的准备工作有哪些?如何通过代码实现modbus通讯协议!
又经过师兄强力火箭助攻!愚昧的还是不能开窍
只能比葫芦画瓢。。。不过经过努力一定可以玩通这个协议的!
接下来写自己学习modbus的过程以及理解
什么是modbus
不查不知道一查吓一跳没想到应用可以这么广!相当于世界通用语言“英语”一样,是各个国家交流的通用语言!不过modbus协议是在电子控制器上的一种通用语言!modbus协议定义了一个控制器能认识使用的消息结构。赶紧查了查这个消息结构,原来通讯过程中主机是老大哥,从机小弟们通过协议只能响应主机大哥不能私自发送数据到通讯总线中!
① 硬件层协议
然而主机大哥和从机小弟之间怎么通讯呢?肯定的有电话线一类的硬件层的东西!看了看自己野火霸道STM32开发板小弟,有RS485,还有RS232那么从 机小弟的电话线接口有了,怎么和电脑作为主机的大哥连线呢?为了让电脑作为主机大哥和开发板小弟实现modbus通讯得配备这些连接线usb转认rs232、usb转rs485.不然的话通讯不了,压差不一样,定义的0、1也不一样。最后选择rs485总线实现主从机通讯。看了看连接电路,A接A,B接B.需要注意的是需要在设备终端接上120欧姆的电阻,以避免数据传输造成的回流噪声干扰AB线的电压。不过STM32的RS485模块接的有终端电阻,省了点事。这电话线搭好了,该如何实现主机大哥和从机小弟打电话时沟通的时候没有语言障碍并且实现主机大哥准确知道自己命令传给哪个从机小弟,又是哪个从机小弟发过来的信息呢?新的问题已经出现,怎么能停止不前,好好努力,协议一定能实现。
② 软件层协议(数据帧定义)
查了资料发现他们之间完成通讯是靠共同协定的一种数据帧完成交流。数据帧可以理解为若干个具有特殊功能意义的字节的组合。那么Modbus如何定义一个数据帧?对于Modbus来说,当进行数据传输过程中,出现空闲时间超过3.5个字节持续时间,就认为一次数据帧的结束,之前接收到的字节就是这次数据帧的所有字节。之后再接收到的字节则为下一个数据帧的字节。例如,在9600bit/s的传输速率下,一个字节传输的时间约为0.8ms,那么当数据传输中,出现约3ms的空闲时间时,设备就认为一帧数据接收完成。每一帧数据要实现功能,传递信息,就需要通讯双方采用相同的语法规则,因此对于每个数据帧的构成,Modbus进行了一些规定,一般来说一个数据帧分为:设备码、功能码、数据码、效验码这四部分。对于一个数据帧的四部分理解我是比较抽象的。
设备码:对于一主多从的通讯架构中,主机大哥的设备码为0,从机小弟都有自己独立的设备码。主机大哥在AB总线发布特定从机任务,各个从机在AB总线上看看是不是大哥给自己的任务,要完成任务的设备码不是给自己的,便放回总线。是自己的设备码便做相应的处理后,对主机大哥汇报自己任务情况。通过设备码,主机可以和多个从机进行数据通讯而避免了其他设备错误响应不属于该设备的通讯。
功能码:是表明该通讯帧的功能或目的。可以用户自行定义的功能码。具体功能码对应的含义还需自己查找关于modbus通信协议的文章查阅,借鉴,https://blog.csdn.net/lakerszhy/article/details/68927178
数据码:对功能码的进一步补充和解释。可以根据用户自行定义。https://blog.csdn.net/lin_duo/article/details/80540768
效验码:一般采用CRC效验,具体效验过程有博客可以参考:https://blog.csdn.net/tjd10061/article/details/48808633
3. STM32代码实现
①代码的总体框架:
STM32实现RS485的Modbus通讯过程。会用到STM32的串口功能(用于收发数据)、I/O功能(用于使能和失能485的收发)、定时器功能(用于对接收的数据的间隔进行计时,以判断数据帧是否接收完 成)、CRC功能(进行CRC校验)以及Modbus的服务函数。
②整体的代码框架为:如果STM32作为从机,在进行串口初始化后,通过I/O使485常态处于接收态,并采用串口中断读取接收到的每一个字节,在每次接收字节时,开启计时器,如果计时器计时溢出,表明 时间间隔大于3.5个字节接收时间,即一帧接收完成,此时进入计时器中断,可以进行Modbus的处理函数。Modbus的处理函数首先会判断设备是否是该设备,如果不是,则直接结束处理。如果是,则会进行 CRC校验,如果CRC校验正确,则根据不同的功能码进行不同的服务函数。如果CRC校验不正确,则返回相应的错误代码。当STM32需要发送数据时,先把485置于发送态,然后通过串口发送数据即可。(借鉴已经做好的大佬的思路,方法有了,接下来就是码代码写BUG了。今天就写到这吧,明天继续学习,更新!)
二、实战操作
1.准备材料:usb转485电平转换模块、STM32F103ZET6(这里用的是野火霸道的板子)、杜邦线若干、
编译环境:keil5
串口调试:
接下来就是接线、码代码了!这里使用32作为从机、
2.硬件连接:要使用霸道板子上的MAX485芯片需要把跳线帽这样接然后485A连接转换模块的A、485B连转换模块B。也可根据协议基于485总线连接多个从机。连接方法一样A连A、B连B。不过需要注意的是在485总线两端连接终端电阻,提高485通讯的稳定性和可靠性。
3.软件代码部分:
①:串口初始化配置
//配置的是USART2 static void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 嵌套向量中断控制器组选择 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); /* 配置USART为中断源 */ NVIC_InitStructure.NVIC_IRQChannel = MODBUS_USART_IRQ; /* 抢断优先级*/ NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /* 子优先级 */ NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; /* 使能中断 */ NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; /* 初始化配置NVIC */ NVIC_Init(&NVIC_InitStructure); } void USART_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 打开串口GPIO的时钟 MODBUS_USART_GPIO_APBxClkCmd(MODBUS_USART_GPIO_CLK, ENABLE); // 打开串口外设的时钟 MODBUS_USART_APBxClkCmd(MODBUS_USART_CLK, ENABLE); // 将USART Tx的GPIO配置为推挽复用模式 GPIO_InitStructure.GPIO_Pin = MODBUS_USART_TX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(MODBUS_USART_TX_GPIO_PORT, &GPIO_InitStructure); // 将USART Rx的GPIO配置为浮空输入模式 GPIO_InitStructure.GPIO_Pin = MODBUS_USART_RX_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(MODBUS_USART_RX_GPIO_PORT, &GPIO_InitStructure); // 配置串口的工作参数 // 配置波特率 USART_InitStructure.USART_BaudRate = MODBUS_USART_BAUDRATE; // 配置 针数据字长 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(MODBUS_USARTx, &USART_InitStructure); // 串口中断优先级配置 NVIC_Configuration(); // 使能串口接收中断 USART_ITConfig(MODBUS_USARTx, USART_IT_RXNE, ENABLE); //接收到数据就进入中断 // 使能串口 USART_Cmd(MODBUS_USARTx, ENABLE); Modbus_485_RX_Mode; //使485处于接受常态 } /* 发送字节 */ void RS485_Send_Data(u8 *buf,u8 len) { u8 t; //将数据帧数据发出去 Modbus_485_TX_Mode;//使能485芯片 for(t=0;t<len;t++) { while(USART_GetFlagStatus(USART2,USART_FLAG_TC)==RESET) USART_SendData(USART1,buf[t]); } while(USART_GetFlagStatus(USART2,USART_FLAG_TC)==RESET)//判断是否全部发送完成 Modbus_485_RX_Mode;//失能485芯片 } /*串口收到代码后进入中断,将读取串口每个字节赋给usart,计时器重新赋值,使能计时器开启中断计时 将接收到的数据赋予USART_RX_BUF[],*/ //modbus串口中断服务程序 void MODBUS_USART_IRQHandler() { //串口2中断服务程序,modbus串口接受接口 u8 Res; if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //判断是否接收到数据确定产生中断 { Res =USART_ReceiveData(USART2);//串口产生中断,读取外设串口数据赋值给Res TIM_SetCounter(TIM2,0); //TIM2计数器寄存器值归零 TIM_Cmd(TIM2, ENABLE); //启用指定的TIM2外围设备 //(进一次串口中断便重新开始计一次时,若定时器溢出了!说明485串口已经3.5字节的时间没传数据了,意味着数据帧传输结束进入计时器中断。) USART_RX_BUF[RS485_RX_CNT]=Res ;//modbus接受缓冲区 RS485_RX_CNT++; //串口中断读取接收到的每一个字节 //RS485_RX_CNT接收到的数据个数 } }
②定时器初始化配置:
//配置的是通用定时器TIM2 // 中断优先级配置 static void MODBUS_TIM_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; // 设置中断组为0 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置中断来源 NVIC_InitStructure.NVIC_IRQChannel = MODBUS_TIM_IRQ ; // 设置主优先级为 0 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置抢占优先级为3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } static void MODBUS_TIM_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 开启定时器时钟,即内部时钟CK_INT=72M MODBUS_TIM_APBxClock_FUN(MODBUS_TIM_CLK, ENABLE); // 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断 TIM_TimeBaseStructure.TIM_Period = MODBUS_TIM_Period; // 时钟预分频数为 TIM_TimeBaseStructure.TIM_Prescaler= MODBUS_TIM_Prescaler; // 时钟分频因子 ,基本定时器没有,不用管 TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 计数器计数模式,基本定时器只能向上计数,没有计数模式的设置 TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; // 重复计数器的值,基本定时器没有,不用管 //TIM_TimeBaseStructure.TIM_RepetitionCounter=0; // 初始化定时器 TIM_TimeBaseInit(MODBUS_TIM, &TIM_TimeBaseStructure); // 清除计数器中断标志位 TIM_ClearFlag(MODBUS_TIM, TIM_FLAG_Update); // 开启计数器中断 TIM_ITConfig(MODBUS_TIM,TIM_IT_Update,ENABLE); // 使能计数器 TIM_Cmd(MODBUS_TIM, ENABLE); } void MODBUS_TIM_Init(void) { MODBUS_TIM_NVIC_Config(); MODBUS_TIM_Config(); } /* Modbus 定时器中断函数 当计时器溢出时(3.5字节时间)进入中断处理*/ void Modbus_TIM_IRQHandler(void) { if(TIM_GetITStatus(MODBUS_TIM, TIM_IT_Update)!=RESET) //判断更新中断是否来临 { TIM_ClearITPendingBit(MODBUS_TIM, TIM_IT_Update);//清除中断标志位 TIM_Cmd(MODBUS_TIM, DISABLE); //计时中断失能不计时,在串口中断中开启使能计时 *Flag_of_Modbus_Ok=1;//modbus一帧数据接受完成标志位 Modbus_Work();//modbus处理函数 } }
③MAX485芯片初始化:
void Modbus_485_Init() { //Modbus_485_RT GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(Modbus_485_RT_Clock, ENABLE); //使能485_RT时钟 GPIO_InitStructure.GPIO_Pin = Modbus_485_RT_Pin; //485_RT端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz GPIO_Init(Modbus_485_RT_Port, &GPIO_InitStructure); //根据设定参数初始化 Modbus_485_RX_Mode; //初始化为接收状态 //根据原理图可知芯片使能引脚为PC2 }
④MODBUS协议:有BUG......容我在多试试。。。
⑤CRC校验:模2除法
//1、除数与被除数最高几位(与除数位数相同)做异或,商1。(除数首位必须为1) //2、余数先去掉首位,若此时余数最高位为1,商1,并对以它为除数继续模2除。 // 若最高位为0,则商0,重复步骤2。 // 3、直到余数位数小于除数位数时,运算结束。 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] ;//每位都异或上0xffff for (j = 0; j < 8; j++)//一字节 { if ((TCPCRC & 0x0001) != 0) { TCPCRC >>= 1;//右移一位 TCPCRC ^= POLYNOMIAL; } else { TCPCRC >>= 1; } } } return TCPCRC; }