————————————————————————————————————
⏩ 大家好哇!我是小光,嵌入式爱好者,一个想要成为系统架构师的大三学生。
⏩最近在开发一个STM32H723ZGT6的板子,使用STM32CUBEMX做了很多驱动,包括ADC、UART、RS485、EEPROM(IIC)、FLASH(SPI)、modbus等等。
⏩本篇文章对STM32CUBEMX在RS485通信的基础上做modbus通信做一个详细的使用教程。
⏩感谢你的阅读,不对的地方欢迎指正。
————————————————————————————————————
modbuspoll
- 工具下载
- modbus协议简介
- Modbus协议类型
- Modbus-Rtu协议
- Modbus功能码
- STM32CUBEMX配置
- RS485配置
- 定时器配置
- 驱动代码
- 测试结果
- XCOM串口调试助手作为主机测试
- modbus poll作为主机测试
- 总结
工具下载
Modbus Poll是一个模拟Modbus协议主机的上位机软件,主要用于模拟测试跟其他从机设备通信的过程。与之成套存在的另一个软件–Modbus Slave,则是模拟Modbus协议从机的上位机软件。该软件内部封装标准Modbus协议栈,通过图形化界面使得操作更为简便。目前软件支持01、02、03、04、05、06、15、16功能码,异常报文检测,原始报文查看,数据记录等功能,是调试Modbus协议栈的好帮手。
下载链接:
链接:百度网盘下载链接modbus poll 7.0.1
提取码:lft0
modbus协议简介
参考:
值得收藏 Modbus RTU 协议详解
详解Modbus通信协议—清晰易懂
Modbus协议类型
串行端口存在多个版本的Modbus协议,而最常见的是下面四种:
- Modbus-Rtu 远程终端控制系统 CRC16校验
- Modbus-Ascii Ascii码表示数据 LRC校验
- Modbus-Tcp TCP三种报文类型 无校验
- ModbusPlus
我们这里使用Modbus-Rtu进行编写代码和测试。
Modbus-Rtu协议
Modbus功能码
Modbus规定了多个功能,那么为了方便的使用这些功能,我们给每个功能都设定一个功能码,也就是指代码。
既然搞清楚了原理,那么后面我们开始程序讲解:
STM32CUBEMX配置
RS485配置
参考我之前的文章:
STM32CUBUMX配置RS485(中断接收)–保姆级教程
一定要根据这个文章把RS485调通
定时器配置
首先我们要知道modbus通信的一帧数据是通过每一帧数据之间的间隔时间来确认的。
- 当bps<19200时:超时时间是大于3.5个字节时间。
- 当bps>19200时:超时时间是大于1750us
例如:
bps = 9600: 传输一个字节的时间是1/9600*10 = 10.4ms,3.5个字节时间就是3.5ms
bps = 115200:超时时间就是17500
定时器定时时间计算可以参考我之前的文章:
STM32CUBEMX配置 定时器中断
下面进行定时器配置
我们配置的PSC = 27500-1 ARR = 50-1 TIM3的时钟是275Mhz
所以:定时时间 T = 27500 * 50/275 * 10^6 = 0.00005s = 50us
定时器计数35次,也就是1800 刚好大于1750us ,符合modbus协议
开启定时器中断,优先级需要比串口中断更低
驱动代码
modbus.h
#ifndef MODBUS_H_
#define MODBUS_H_
#include "stm32H7xx_hal.h" //HAL库文件声明
#include "gpio.h"
#include "usart.h"
#define BUFFER_SIZE 600 //最大数据帧
typedef struct {
uint8_t myadd;//从机设备地址
uint8_t timrun;//定时器
uint8_t slave_add;//主机要匹配的从机地址(本设备作为主机时)
uint8_t reflag;//接收完成标志位,1:完成 0:未完成
uint8_t Host_time_flag;//发送数据标志
uint8_t recount;//接收到的字节数
unsigned char rcbuf[BUFFER_SIZE];//接受数据帧
unsigned char sendbuf[BUFFER_SIZE];//发送数据帧
uint32_t timout;//超时时间 单位:ms
uint32_t Host_Sendtime;//发送完上一帧后的时间计数 单位:ms
}MODBUS;
// Modbus初始化函数
void Modbus_Init(void);
void Modbus_Event(void);
void Modbus_Func3(void);
void Modbus_Func6(void);
void Modbus_Func16(void);
void Modbus_Send_Byte( uint8_t ch );
int Modbus_CRC16(uint8_t buff[],int len);
#endif
modbus.c
#include "modbus.h"
MODBUS modbus;
uint16_t Reg[] ={0x0001,
0x0012,
0x0013,
0x0004,
0x0025,
0x0036,
0x0007,
0X0008,
};//reg是提前定义好的寄存器和寄存器数据,要读取和改写的部分内容
// Modbus初始化函数
void Modbus_Init(void)
{
modbus.myadd = 0x01; //从机设备地址为2
modbus.timrun = 0; //modbus定时器停止计算
modbus.slave_add=0x02;//主机要匹配的从机地址(本设备作为主机时)
modbus.reflag = 0;//无数据包处理
modbus.Host_time_flag = 0;//发送数据标志
modbus.recount = 0;//接收到的字节数
modbus.timout = 0;//超时时间 单位:ms
modbus.Host_Sendtime = 0;//发送完上一帧后的时间计数 单位:ms
}
// Modbus事件处理函数
void Modbus_Event(void)
{
uint16_t crc,rccrc;//crc和接收到的crc
//没有收到数据包
if(modbus.reflag == 0) //如果接收未完成则返回空
{
return;
}
//收到数据包(接收完成)
//通过读到的数据帧计算CRC
//参数1是数组首地址,参数2是要计算的长度(除了CRC校验位其余全算)
crc = Modbus_CRC16(&modbus.rcbuf[0],modbus.recount-2); //获取CRC校验位
// 读取数据帧的CRC
rccrc = modbus.rcbuf[modbus.recount-2]*256+modbus.rcbuf[modbus.recount-1];//计算读取的CRC校验位
//等价于下面这条语句
//rccrc=modbus.rcbuf[modbus.recount-1]|(((uint16_t)modbus.rcbuf[modbus.recount-2])<<8);//获取接收到的CRC
if(crc == rccrc) //CRC检验成功 开始分析包
{
if(modbus.rcbuf[0] == modbus.myadd) // 检查地址是否时自己的地址
{
switch(modbus.rcbuf[1]) //分析modbus功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbus_Func3();break;//这是读取寄存器的数据
case 4: break;
case 5: break;
case 6: Modbus_Func6(); break;//这是写入单个寄存器数据
case 7: break;
case 8: break;
case 9: break;
case 16: Modbus_Func16(); break;//写入多个寄存器数据
}
}
else if(modbus.rcbuf[0] == 0) //广播地址不予回应
{
}
}
modbus.recount = 0;//接收计数清零
modbus.reflag = 0; //接收标志清零
}
/*
********************************************************************************
主机:03
01 03 00 01 00 01 D5 CA 从地址01开始读读取一个寄存器的数据内容
ID 功能码 起始地址 读取寄存器的个数
从机返回:
01 03 02 00 03 F8 45 返回了两个字节的数据,数据是00 03
ID 功能码 几个字节 返回的数据内容
********************************************************************************
*/
// Modbus 3号功能码函数
// Modbus 主机读取寄存器值
void Modbus_Func3(void)
{
uint16_t Regadd,Reglen,crc;
uint8_t i,j;
//得到要读取寄存器的首地址
Regadd = modbus.rcbuf[2]*256+modbus.rcbuf[3];//读取的首地址
//得到要读取寄存器的数据长度
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
//发送回应数据包
i = 0;
modbus.sendbuf[i++] = modbus.myadd; //ID号:发送本机设备地址
modbus.sendbuf[i++] = 0x03; //发送功能码
modbus.sendbuf[i++] = ((Reglen*2)%256); //返回字节个数
for(j=0;j<Reglen;j++) //返回数据
{
//reg是提前定义好的16位数组(模仿寄存器)
modbus.sendbuf[i++] = Reg[Regadd+j]/256;//高位数据
modbus.sendbuf[i++] = Reg[Regadd+j]%256;//低位数据
}
crc = Modbus_CRC16(modbus.sendbuf,i); //计算要返回数据的CRC
modbus.sendbuf[i++] = crc/256;//校验位高位
modbus.sendbuf[i++] = crc%256;//校验位低位
//数据包打包完成
// 开始返回Modbus数据
RS485DIR_TX;//这是开启485发送
for(j=0;j<i;j++)//发送数据
{
Modbus_Send_Byte(modbus.sendbuf[j]);
}
RS485DIR_RX;//这里是关闭485发送
}
// Modbus 6号功能码函数
// Modbus 主机写入寄存器值
void Modbus_Func6()
{
uint16_t Regadd;//地址16位
uint16_t val;//值
uint16_t i,crc,j;
i=0;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改后的值(要写入的数据)
Reg[Regadd]=val; //修改本设备相应的寄存器
//以下为回应主机
modbus.sendbuf[i++]=modbus.myadd;//本设备地址
modbus.sendbuf[i++]=0x06; //功能码
modbus.sendbuf[i++]=Regadd/256;//写入的地址
modbus.sendbuf[i++]=Regadd%256;
modbus.sendbuf[i++]=val/256;//写入的数值
modbus.sendbuf[i++]=val%256;
crc=Modbus_CRC16(modbus.sendbuf,i);//获取crc校验位
modbus.sendbuf[i++]=crc/256; //crc校验位加入包中
modbus.sendbuf[i++]=crc%256;
//数据发送包打包完毕
RS485DIR_TX;;//使能485控制端(启动发送)
for(j=0;j<i;j++)
{
Modbus_Send_Byte(modbus.sendbuf[j]);
}
RS485DIR_RX;//失能485控制端(改为接收)
}
//这是往多个寄存器器中写入数据
//功能码0x10指令即十进制16
void Modbus_Func16()
{
uint16_t Regadd;//地址16位
uint16_t Reglen;
uint16_t i,crc,j;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //要修改内容的起始地址
Reglen = modbus.rcbuf[4]*256+modbus.rcbuf[5];//读取的寄存器个数
for(i=0;i<Reglen;i++)//往寄存器中写入数据
{
//接收数组的第七位开始是数据
Reg[Regadd+i]=modbus.rcbuf[7+i*2]*256+modbus.rcbuf[8+i*2];//对寄存器一次写入数据
}
//写入数据完毕,接下来需要进行打包回复数据了
//以下为回应主机内容
//内容=接收数组的前6位+两位的校验位
modbus.sendbuf[0]=modbus.rcbuf[0];//本设备地址
modbus.sendbuf[1]=modbus.rcbuf[1]; //功能码
modbus.sendbuf[2]=modbus.rcbuf[2];//写入的地址
modbus.sendbuf[3]=modbus.rcbuf[3];
modbus.sendbuf[4]=modbus.rcbuf[4];
modbus.sendbuf[5]=modbus.rcbuf[5];
crc=Modbus_CRC16(modbus.sendbuf,6);//获取crc校验位
modbus.sendbuf[6]=crc/256; //crc校验位加入包中
modbus.sendbuf[7]=crc%256;
//数据发送包打包完毕
RS485DIR_TX;;//使能485控制端(启动发送)
for(j=0;j<8;j++)
{
Modbus_Send_Byte(modbus.sendbuf[j]);
}
RS485DIR_RX;//失能485控制端(改为接收)
}
void Modbus_Send_Byte( uint8_t ch )
{
/* 发送一个字节数据到USART2 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xff);
//while(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_TC)!=SET); //等待发送结束
}
int Modbus_CRC16(uint8_t buff[],int len)
{
unsigned short tmp = 0xffff;
unsigned short ret1 = 0;
for(int n = 0; n < len; n++) //此处的6 -- 要校验的位数为6个
{
tmp = buff[n] ^ tmp;
for(int i = 0;i < 8;i++) //此处的8 -- 指每一个char类型又8bit,每bit都要处理
{
if(tmp & 0x01)
{
tmp = tmp >> 1;
tmp = tmp ^ 0xA001;
}
else
{
tmp = tmp >> 1;
}
}
}
ret1 = tmp >> 8; //将CRC校验的高低位对换位置
ret1 = ret1 | (tmp << 8);
return ret1;
}
目前只写了03(读寄存器)、06(写一个寄存器)、16(写多个寄存器)三个功能
stm32_h7xx_it.c
extern MODBUS modbus;
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if( modbus.reflag==1) //有数据包正在处理
{
return ;
}
modbus.rcbuf[modbus.recount++] = USART1_aRxBuffer[0];
modbus.timout = 0;
if(modbus.recount == 1) //已经收到了第二个字符数据
{
modbus.timrun = 1; //开启modbus定时器计时
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)USART1_aRxBuffer,1); //添加的一行代码
/* USER CODE END USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
/******************************************************************************
* @ 函数名 : HAL_TIM_PeriodElapsedCallback
* @ 功 能 : 定时器超时中断回调函数
* @ 参 数 : htim 定时器名
* @ 返回值 : 无
******************************************************************************/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
if(modbus.timrun != 0)//运行时间!=0表明
{
modbus.timout++;
if(modbus.timout >=35)//大于1750us
{
modbus.timrun = 0;
modbus.reflag = 1;//接收数据完毕
modbus.timout = 0;
USART1_RX_STA|=0x8000;
}
}
modbus.Host_Sendtime++;//发送完上一帧后的时间计数
if(modbus.Host_Sendtime>1000)//距离发送上一帧数据1s了
{
//1s时间到
modbus.Host_time_flag=1;//发送数据标志位置1
}
}
}
上面写了RS485串口中断处理函数和定时器中断处理函数
main.c
HAL_TIM_Base_Start_IT(&htim3); //启动定时器TIM3
Modbus_Init();//本机作为从机使用时初始化
RS485DIR_RX;//拉低PB5,更改RS485模式为接收
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
Modbus_Event();//本机作为从机使用时
}
/* USER CODE END 3 */
}
主函数只需要写一个初始化,在循环中调用Modbus_Event()函数循环查询就可以了。
测试结果
XCOM串口调试助手作为主机测试
主机发送解析:
01 03 00 00 00 04 44 09
01:从机地址
03:功能码,读寄存器
00 00:读的起始地址
00 04:要读的数据的个数
44 09:校验码
从机返回解析:
==01 03 08 00 01 00 12 00 13 00 04 CD 12 ==
01:从机地址
03:功能码,读寄存器
08:数据的位数,8个字节
00 01 00 12 00 13 00 04:接收到四个数据:0001,0012,0013,0004
CD 12:校验码
校验方式是CRC16
我们可以计算出来,从机返回的数据是没有问题的,然后我们后面使用modbus poll作为主机进行测试:
modbus poll作为主机测试
1.打开modbus poll,Setup->read/write defination:
2.Connection->connect
3.查看结果
开始显示的不是16进制,可以选中这些数据 Display->Hex - Ascall,这样就是16进制显示了,可以看到读取了我们程序中写的数据。
点击这个可以查看发送和返回的数据包
可以看到和我们刚才串口调试的是一样的结果。
总结
本次实验,我们在RS485通信的基础上实现了modbus-RTU协议,当然只写了03、06、16功能码,测试都是没有问题的,图方便我只放上了03的测试,你们可以把这个都测试一遍,甚至可以把他的功能写全面。