STM32CUBUMX配置RS485 modbus STM32(从机)亲测可用

————————————————————————————————————
⏩ 大家好哇!我是小光,嵌入式爱好者,一个想要成为系统架构师的大三学生。
⏩最近在开发一个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;//超时时间 单位:msuint32_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; //从机设备地址为2modbus.timrun = 0;    //modbus定时器停止计算modbus.slave_add=0x02;//主机要匹配的从机地址(本设备作为主机时)modbus.reflag = 0;//无数据包处理modbus.Host_time_flag = 0;//发送数据标志modbus.recount = 0;//接收到的字节数modbus.timout = 0;//超时时间 单位:msmodbus.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校验位// 读取数据帧的CRCrccrc = 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);//获取接收到的CRCif(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; //接收标志清零}/*********************************************************************************主机:0301  03      00 01     00 01          D5 CA	从地址01开始读读取一个寄存器的数据内容ID 功能码  起始地址  读取寄存器的个数从机返回:01  03       02       00 03          F8 45 返回了两个字节的数据,数据是00 03ID  功能码  几个字节  返回的数据内容*********************************************************************************/// 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);    //计算要返回数据的CRCmodbus.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指令即十进制16void 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);  //启动定时器TIM3Modbus_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:接收到四个数据:0001001200130004
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的测试,你们可以把这个都测试一遍,甚至可以把他的功能写全面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/20660.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

怎么才能远程控制笔记本电脑?

为什么选择AnyViewer远程控制软件&#xff1f; 为什么AnyViewer是远程控制笔记本电脑软件的首选&#xff1f;以下是选择AnyViewer成为笔记本电脑远程控制软件的主要因素。 跨平台能力 AnyViewer作为一款跨平台远程控制软件&#xff0c;不仅可以用于从一台Windows电…

【BEV感知】3-BEV开源数据集

3-BEV开源数据集 1 KITTI1.1 KITTI数据怎么采集?1.2 KITTI数据规模有多大?1.3 KITTI标注了哪些目标?1.4 转换矩阵1.5 标签文件 2 nuScenes2.1 nuScenes Vs KITTI2.2 标注文件 1 KITTI KITTI 1.1 KITTI数据怎么采集? 通过车载相机、激光雷达等传感器采集。 只提供了相机正…

蓝桥杯上岸每日N题 第七期(小猫爬山)!!!

蓝桥杯上岸每日N题 第七期(小猫爬山)&#xff01;&#xff01;&#xff01; 同步收录 &#x1f447; 蓝桥杯上岸必背&#xff01;&#xff01;&#xff01;(第四期DFS) 大家好 我是寸铁&#x1f4aa; 冲刺蓝桥杯省一模板大全来啦 &#x1f525; 蓝桥杯4月8号就要开始了 &a…

C/C++的5大内存分区

1、堆区&#xff08;heap&#xff09;——由程序员分配和释放&#xff0c; 若程序员不释放&#xff0c;程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事 2、栈区&#xff08;stack&#xff09;——由编译器自动分配释放 &#xff0c;存放函数的参数值&#xff0c;局…

DevExpress WPF Tree List组件,让数据可视化程度更高!(二)

DevExpress WPF Tree List组件是一个功能齐全、数据感知的TreeView-ListView混合体&#xff0c;可以把数据信息显示为REE、GRID或两者的组合&#xff0c;在数据绑定或非绑定模式下&#xff0c;具有完整的数据编辑支持。 在上文中&#xff08;点击这里回顾DevExpress WPF Tree …

shell脚本练习

#include <myhead.h> //递归实现输入一个数,输出这个数的每一位 void fun1(int data) {if(data 0) return;fun1(data/10);printf("%d\t",data%10);}//递归实现输入一个数,输出这个数的二进制 void fun2(int data) {if(data 0) return;fun2(data/2);printf(&q…

华为流程体系:流程架构「OES方法」

目录 内容简介 OES方法 端到端的流程 专栏列表 CSDN学院 作者简介 内容简介 今天继续来谈谈华为流程体系中的流程架构。 在前期的内容已经介绍过 POS 流程架构的方法。 这里就先回顾一下 POS 方法的相关内容&#xff1a; 关于 POS&#xff0c;大家可以参看上面的这张图…

现代C++中的从头开始深度学习:激活函数

一、说明 让我们通过在C中实现激活函数来获得乐趣。人工神经网络是生物启发模型的一个例子。在人工神经网络中&#xff0c;称为神经元的处理单元被分组在计算层中&#xff0c;通常用于执行模式识别任务。 在这个模型中&#xff0c;我们通常更喜欢控制每一层的输出以服从一些约束…

Java框架(九)--Spring Boot入门(1)

SpringBoot 2.x入门简介 学前基础 Maven Spring MVC理念 开发环境 Spring Boot官网版本介绍 https://spring.io/projects/spring-boot#learn 我们点击 Reference Doc. &#xff0c;再点击Getting Started&#xff0c;就可以看到官网系统环境说明了 官网系统环境说明 Sp…

LangChain+ChatGLM大模型应用落地实践(一)

LLMs的落地框架&#xff08;LangChain&#xff09;&#xff0c;给LLMs套上一层盔甲&#xff0c;快速构建自己的新一代人工智能产品。 一、简介二、LangChain源码三、租用云服务器实例四、部署实例 一、简介 LangChain是一个近期非常活跃的开源代码库&#xff0c;目前也还在快速…

使用vscode进行远程开发服务器配置

1.下载vscode 2.给vscode 安装python 和 remote ssh插件 remote—SSH扩展允许您使用任何具有SSH服务器的远程机器作为您的开发环境。 3.安装remote-SSH插件之后&#xff0c;vscode左侧出现电脑图标&#xff0c;即为远程服务&#xff0c;按图依次点击&#xff0c;进行服务器配置…

运维:18工作中常用 Shell 脚本, 强烈推荐

1、检测两台服务器指定目录下的文件一致性 #!/bin/bash ###################################### 检测两台服务器指定目录下的文件一致性 ##################################### #通过对比两台服务器上文件的md5值,达到检测一致性的目的 dir=/data/web b_ip=192…

访问者模式——操作复杂对象结构

1、简介 1.1、概述 访问者模式是一种较为复杂的行为型设计模式&#xff0c;它包含访问者和被访问元素两个主要组成部分。这些被访问的元素通常具有不同的类型&#xff0c;且不同的访问者可以对它们进行不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系…

TensorRT学习笔记--基于TensorRT部署YoloV3, YoloV5和YoloV8

目录 1--完整项目 2--模型转换 3--编译项目 4--序列化模型 5--推理测试 1--完整项目 以下以 YoloV8 为例进行图片和视频的推理&#xff0c;完整项目地址如下&#xff1a;https://github.com/liujf69/TensorRT-Demo git clone https://github.com/liujf69/TensorRT-Demo.…

PostgreSql 进程及内存结构

一、进程及内存架构 PostgreSQL 数据库运行时&#xff0c;使用如下命令可查询数据库进程&#xff0c;正对应上述结构图。 [postgreslocalhost ~]$ ps -ef|grep post postgres 8649 1 0 15:05 ? 00:00:00 /app/pg13/bin/postgres -D /data/pg13/data postgres …

一篇聊聊JVM优化:堆

一、Java 堆概念 1、简介 对于Java应用程序来说&#xff0c;Java堆&#xff08;Java Heap&#xff09;是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享 的一块内存区域&#xff0c;在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例&#xff0c;Java 世界…

MongoDB文档--架构体系

阿丹&#xff1a; 在开始学习先了解以及目标知识的架构体系。就能事半功倍。 架构体系 MongoDB的架构体系由以下几部分组成&#xff1a; 存储结构&#xff1a;MongoDB采用文档型存储结构&#xff0c;一个数据库包含多个集合&#xff0c;一个集合包含多个文档。存储形式&#…

Quartz使用文档,使用Quartz实现动态任务,Spring集成Quartz,Quartz集群部署,Quartz源码分析

文章目录 一、Quartz 基本介绍二、Quartz Java 编程1、文档2、引入依赖3、入门案例4、默认配置文件 三、Quartz 重要组件1、Quartz架构体系2、JobDetail3、Trigger&#xff08;1&#xff09;代码实例&#xff08;2&#xff09;SimpleTrigger&#xff08;3&#xff09;CalendarI…

python-Excel数据模型文档转为MySQL数据库建表语句(需要连接数据库)-工作小记

将指定Excel文档转为create table 建表语句。该脚本适用于单一且简单的建表语句 呈现效果 代码 # -*- coding:utf-8 -*- # Time : 2023/8/2 17:50 # Author: 水兵没月 # File : excel_2_mysql建表语句.py import reimport pandas as pd import mysql.connectordb 库名mydb m…

ELK高级搜索(一)

文章目录 ELK搜索1&#xff0e;简介1.1 内容1.2 面向 2&#xff0e;Elastic Stack2.1 简介2.2 特色2.3 组件介绍 3&#xff0e;Elasticsearch3.1 搜索是什么3.2 数据库搜索3.3 全文检索3.4 倒排索引3.5 Lucene3.6 Elasticsearch3.6.1 Elasticsearch的功能3.6.2 Elasticsearch使…