嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]

【嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]】之前我移植了freemodbus的modbusTCP协议,感觉很好用,最近需要写一个基于STM32的modbusRTU就让我比较僵硬,本人至今没有搞懂freemodbus中modbusRTU的串口和定时器是怎么配合的,而且发现很多是要求使用的是RS232或者RS485接口,本人的目的是直接通过电脑串口完成,所以就自己写了一个Modbus的处理协议。
关键:只用了一个串口就搞定!!!
主要想法:

1.接收一个完整的Modbus数据帧 2.写一个处理函数,将数据帧进行状态机分析,生成一个返回数据帧 3.对数据帧进行CRC校验,校验码放在返回数据帧结尾 4.通过串口发送返回数据帧

生成STM32cube程序 1、根据自己的stm32的芯片型号来选择,我这里是STM32F767IGTx
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

2、选好芯片之后照旧设置RCC为外部时钟
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

3、使能串口1(usart1)以及中断,如图:
模式设为异步(Asynchronous)其他默认,波特率可以自己改,默认为115200Bits/s。
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

勾选中断使能
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

4、看原理图,找到串口对应引脚,如图:
我这里是
PA10——>USART1_RX PA9——>USART1_TX

嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

5、根据对应引脚设置串口引脚,如图:
找到PA9、PA10引脚左键点击分别选择USART1_TX和USART1_RX
(不用担心选错选反,针脚的功能是ST公司已经定义好了的)
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

6、设置时钟树,如图:
这里会搞的按自己习惯搞,不会搞的默认就好,但是不能有里面是红色的框(红色框就是错了意思)
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

7、项目设置,如图:
红框里的按照自己的Keil版本来
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

个人喜欢把.c/.h文件分开
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

8、点击右上角的‘GENERATE CODE’直接生成代码,如图:
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

9、实现串口中断能够完整接收一个Modbus数据包:
打开项目后先打开Application/User的中断控制函数stm32f7xx_it.c在stm32f7xx_it.c中找到串口1的中断位置添加modbus串口相关代码
我们首先要根据modbus的功能码来推测接收包的位数
对于保持寄存器的读写:
A.读保持寄存器
读保持寄存器的请求帧图下所示:
SlaveID FunctionCode StartADDR Length CRC
从站ID 功能码 起始地址 寄存器数量 CRC校验
1位 1位 2位 2位 2位
总和为0~7位,共8位
B.写保持寄存器
SlaveID FunctionCode StartADDR Length Code Length Code CRC
从站ID 功能码 起始地址 寄存器数量 字节数 寄存器值 CRC校验
1位 1位 2位 2位 1位 Code Length位 2位
总和为 0~8 ~8+Code Length 位,共9+Code Length位
所以得到串口中断函数如下:
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ extern uint8_t buff1[100]; //接收的数据包 extern int buff1stat; //接收数据包当前位 extern uint8_t RxByte; //中断接收暂存位置 extern int flag; //接收结束标志符 /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); //中断启动使能标识 /* USER CODE BEGIN USART1_IRQn 1 */ buff1[buff1stat]=RxByte; //将暂存内容放入接收数据包 buff1stat++; //数据包位置后移1位 HAL_UART_Receive_IT(&huart1,&RxByte,1); //重新启动接收中断,等待下一位数据到来 if(buff1[1]==0x03)//判断功能码,功能码位0x30则为读寄存器,请求数据包应为8位 { if(buff1stat==8)//数据到8位接收结束符置1,清除接收数据包当前位 { buff1stat=0; flag=1; } } if(buff1[1]==0x10)//判断功能码,功能码位0x30则为读寄存器,请求数据包应为9+Code Length位 { if(buff1[6]!=0x00)//当Code Length位不为0 且数据到9+Code Length位时,接收结束符置1,清除接收数据包当前位 { if(buff1stat==9+buff1[6]) { buff1stat=0; flag=1; } } } /* USER CODE END USART1_IRQn 1 */ }

并在mian函数的 /* USER CODE BEGIN 2 */处使能接收中断:
/* USER CODE BEGIN 2 */ HAL_UART_Receive_IT(&huart1,&RxByte,1); /* USER CODE END 2 */

以上,串口接收函数编写结束!!!可以实现根据Modbus数据内容来确定数据包长度并储存下来的功能了
10、分析接收数据包,生成发送数据包:
先建立一个保持寄存器功能的.c.h文件包,按照状态机来分析处理接收到的modbus数据包
以下为我画的状态转移流程图:
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

状态处理函数如下:(想让懒狗博主一条条分析是不可能的!)
void ReadHoldRegister(uint8_t buffIN[100],uint8_t buffOUT[100]) { int i,j; e.stat = 0; for (i = 0; i < 10; i++) { modbus_onebit(buffIN[i]); if (e.stat == 100) {} if (e.stat == 0) {} if (e.stat == 1) { buffOUT[i]= buffIN[i]; } if (e.stat == -1) {} if (e.stat == 2) { buffOUT[i] = buffIN[i]; } if (e.stat == 3) { } if (e.stat == 4) { buffOUT[2] = bufflen*2; } if (e.stat == 5) { for (j = 0,n=0; j < bufflen * 2; j= j + 2,n++) { buffOUT[3 + j] = (DB_R[n+startaddr] >> 8) & 0x00FF; buffOUT[3 + j+1] = DB_R[n+startaddr]& 0x00FF; } }if (e.stat == 12) { buffOUT[i] = buffIN[i]; } if (e.stat == 13) { for (j = 0,n=0; j < bufflen; j= j + 2,n++) { DB_R[n+startaddr]=(buffIN[7 + j] << 8)+buffIN[7 + j + 1]; } } } if(buffIN[1]==0x03) CrcCheck(buffOUT,3+bufflen*2); else if(buffIN[1]==0x10) CrcCheck(buffOUT,6); }

状态转移函数如下:(想让懒狗博主一条条分析是不可能的!)
voidmodbus_onebit(char a) { switch (e.stat) { case 0: if (a == 0x01) e.stat = 1; break; case 1: if(a==0x03) e.stat = 2; if(a==0x10) e.stat=12; break; case 2: addr[cnt] = a; cnt++; e.stat = 3; break; case 3: addr[cnt] = a; cnt++; if (cnt > 3) { cnt = 0; startaddr= addr[1]; bufflen= addr[3]; e.stat = 4; } break; case 4: e.stat = 5; break; case 5: if(n==bufflen) e.stat = 100; break; case 6: e.stat = 6; break; case 7: e.stat = -1; break; case 8: e.stat = 100; break; /********D′è??à????′??÷********/ case 12: addr[cnt]=a; cnt++; if(cnt>4) { cnt=0; startaddr= addr[1]; bufflen= addr[4]; e.stat = 13; //×a′?íê±? } break; case 13: if(n==bufflen) e.stat = 100; break; } }

CRC校验函数如下:(想让懒狗博主一条条分析是不可能的!)
void CrcCheck(unsigned char *buf,int len) { unsigned short crc = 0xFFFF; unsigned char i,j=0; while(j < len) { crc ^= buf[j]; for(i = 0; i < 8; i++) { if(crc & 0x01) { crc >>= 1; crc ^= 0xA001; } else crc >>= 1; } j++; } buf[j] = crc % 0x100; buf[j+1]=crc / 0x100; }

按着上面的函数就可以处理一个数据包
之后就是把接收到的数据包放到函数处理的过程了
11、生成数据包,完成操作,发送数据包,游戏结束!:
在main函数主循环添加如下代码:
/* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */if(flag==1) { ReadHoldRegister(buff1,buff2); //上面写的函数,不懂的滑上去看 if(buff1[1]==0x03)//判断功能码,0x03为读寄存器,发送5+2*length位,不懂为什么的的滑回前面去看 { for(count=0; count<5+buff1[5]*2; count++) HAL_UART_Transmit(&huart1, &buff2[count],1,0xFFFF); } else if(buff1[1]==0x10)//判断功能码,0x10为写寄存器,发送8位,不懂为什么的的滑回前面去看 { for(count=0; count<=7; count++) HAL_UART_Transmit(&huart1, &buff2[count],1,0xFFFF); } //for(count = 0; count < 100; count++)buff1[count]=0x00; //数据接收数组清空,看你们心情加不加,我这里加和不加不影响使用,我留着是图个debug flag=0; //数据接收结束位清0,等待下次接收 } }

防止有些大哥脑卡,不会自己定义变量,搬过去说我的函数用不了,我粘一下我的变量定义以及位置
1.main.c函数下int main(void)后面
int main(void) { /* USER CODE BEGIN 1 */ DB_R[0] = 0x0102; DB_R[1] = 0x0204; DB_R[2] = 0x0306; DB_R[3] = 0x0408; int count; /* USER CODE END 1 */

2.main.c函数下/* USER CODE BEGIN PD */内
/* USER CODE BEGIN PD */ uint8_t buff1[100]; int buff1stat=0; uint8_t buff2[100]; int flag=0; uint8_t RxByte; extern uint16_t DB_R[100]; /* USER CODE END PD */

3.自己写的modbushr.c文件下,定义void ReadHoldRegister(uint8_t buffIN[100],uint8_t buffOUT[100])函数前
#include #include "main.h" uint16_t DB_R[100]; MB e; int cnt,n,bufflen,startaddr; uint8_t addr[100];

4.自己写的modbushr.h文件下
#ifndef __MODBUSHR_H #define __MODBUSHR_H typedef struct { int stat; //Modbusμ±?°??×′ì? }MB; void CrcCheck(unsigned char *buf,int len); void ReadHoldRegister(unsigned char *buff1,unsigned char *buff2); voidmodbus_onebit(char a); #endif // 2_H_INCLUDED

5.main.c函数下包含
/* USER CODE BEGIN Includes */ #include "stdio.h" #include //自己写的modbus,内容就是上面的那三个处理函数 /* USER CODE END Includes */

12、实验结果:
本次设计是modbus的从站设计,此时需要一个modbus的主站与其通讯测试
这里在github上下了一个测试工具ModbusTOOL
1.嵌入式串口连上电脑USB,并启动keil的debug,运行程序,监视DB_R寄存器(我自己初始化了寄存器内容如图)
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

2.打开Modbus Tool的Modbus Master
3.找到当前使用的串口是多少,设定波特率与嵌入式一致后点击connect
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

4.点击读、写保持寄存器测试功能
这里我们先读初始化保持寄存器的内容
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

由图可知,我们已经读取到初始化的保持寄存器内容
此时可以写入自己想要写入的数字,点击写入保持寄存器(写数据的位置只能在上图中Start往后数Size个位置之内)我讲0-3寄存器写了0x1111,0x2222,0x3333,0x4444四个数字
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

下方绿字说写入成功,功能码为16
此时我们看keil的debug的BD_R有没有改变
嵌入式|基于STM32Cube的ModbusRTU编写[保持寄存器读写]
文章图片

由图可知,寄存器BD_R的0-3位置已经变成了0x1111,0x2222,0x3333,0x4444证明试验结果正确,至此,实验结束!!!
Good Game!!!!!!接下来会推出一系列的关于串口使用的分享,有需要的猿们敬请关注!!!!!

以上内容欢迎大家转载引用,标明出处即可!!!!!

    推荐阅读