MODBUS开发实战指南:STM32 HAL库通讯控制485详解,MODBUS开发传感器实战项目启动章节——串口485协议通讯全解析
第零章-STM32工程模板
0.1-新建工程
调试器
晶振
设置PC13 输出
英文保存路径
生成代码
0.2-点灯
编译-烧录-测试
0.3-工程模板
添加四个文件
内容如下
CallBack.c
//此文件主要放自己重新实现的回调函数
/* Includes ------------------------------------------------------------------*/
#include "MyApp.h"
/* Private define-------------------------------------------------------------*/
/* Private variables----------------------------------------------------------*/
/* Public variables-----------------------------------------------------------*/
/* Private function prototypes------------------------------------------------*/
/********************************************************
End Of File
********************************************************/
MyApp.h
#ifndef __MyApp_H_
#define __MyApp_H_
//这个文件包含所有使用的.h 文件
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
#endif
/********************************************************
End Of File
********************************************************/
Public.c
//这个文件主要放公共文件
/* Includes ------------------------------------------------------------------*/
#include "MyApp.h"
/* Private define-------------------------------------------------------------*/
/* Private variables----------------------------------------------------------*/
/* Public variables-----------------------------------------------------------*/
/* Private function prototypes------------------------------------------------*/
/********************************************************
End Of File
********************************************************/
Public.h
#ifndef __PUBLIC_H_
#define __PUBLIC_H_
//放公共变量
#include "MyApp.h"
/* Public define-------------------------------------------------------------*/
/* extern variables-----------------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
编译一下、0错误无警告
第一章-485相关
1.1-串口外设初始化和接收
串口一用于打印调试:波特率:115200。
串口二连接485电平转换芯片,用于modbus通信
串口一设置截图:
串口二设置截图:
发送:使用DMA+TC中断
接收:使用DMA+空闲中断
这里的优先级应该调整到最高!!!
开启的中断
有部分DMA中断软件无法关闭可以自己写代码关闭
https://shequ.stmicroelectronics.cn/thread-615792-1-1.html
在main之前开启
__HAL_UART_ENABLE_IT(&huart2,UART_IT_IDLE);//使能串口2的空闲中断
HAL_UART_Receive_DMA(&huart2,UART2.pRec_Buffer,UART2_Rec_LENGTH);//串口2开启DMA接收
新建UART.h、在其中创建结构体类型
#ifndef __UART_H_
#define __UART_H_
#include "MyApp.h"
typedef struct
{
uint8_t* pSend_Buffer;//发送缓存指针
uint8_t* pRec_Buffer;//接收缓存指针
void (*SendArray)(uint8_t*,uint16_t);//串口发送数组函数
void (*SendString)(uint8_t*);//发送字符串函数
void (*RS485_Set_SendMode)(void);//设置485为发送模式
void (*RS485_Set_RecMode)(void);//设置485接收模式
} UART_t;
/* extern variables-----------------------------------------------------------*/
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
创建UART2.h文件、
#ifndef __UART2_H__
#define __UART2_H__
/* define ------------------------------------------------------------------*/
#include "MyApp.h"
//#include "UART.h"
#define UART2_Send_LENGTH 20 //发送缓冲长度
#define UART2_Rec_LENGTH 20 //接收缓冲长度
/* extern variables-----------------------------------------------------------*/
extern UART_t UART2; //声明结构体类型变量
void UART2_VarInit(void); //初始化
#endif
/********************************************************
End Of File
********************************************************/
新建UART2.c 、
/* Includes ------------------------------------------------------------------*/
#include "MyApp.h"
/* Private define-------------------------------------------------------------*/
/* Private variables----------------------------------------------------------*/
static uint8_t ucSend_Buffer[UART2_Send_LENGTH] = {0x00};//发送缓冲区
static uint8_t ucRec_Buffer [UART2_Rec_LENGTH] = {0x00};//接收缓冲区
/* Private function prototypes------------------------------------------------*/
static void SendArray(uint8_t*,uint16_t); //串口发送数组
static void SendString(uint8_t*); //串口发送字符串
static void RS485_Set_SendMode(void); //设置485为发送模式
static void RS485_Set_RecMode(void); //设置485为接收模式
/* Public variables-----------------------------------------------------------*/
UART_t UART2 = {0}; //定义并初始化结构类型变量
/*******************
* @brief 赋值结构体变量
* @param
* @return
*
*******************/
void UART2_VarInit(void)
{
UART2.pSend_Buffer = ucSend_Buffer;
UART2.pRec_Buffer = ucRec_Buffer;
UART2.SendArray = SendArray;
UART2.SendString = SendString;
UART2.RS485_Set_RecMode = RS485_Set_RecMode;
UART2.RS485_Set_SendMode = RS485_Set_SendMode;
}
/*******************
* @brief 串口发送数组
* @param
* @return
*
*******************/
static void SendArray(uint8_t* pArray,uint16_t Len)
{
}
/*******************
* @brief 发送字符串
* @param
* @return
*
*******************/
static void SendString(uint8_t* pString)
{
HAL_UART_Transmit(&huart2,pString, strlen((const char *)pString), 0xFF);
}
/*******************
* @brief 设置485为发送模式
* @param
* @return
*
*******************/
static void RS485_Set_SendMode(void)
{
}
/*******************
* @brief 设置485接收模式
* @param
* @return
*
*******************/
static void RS485_Set_RecMode(void)
{
}
/********************************************************
End Of File
********************************************************/
在stm32f1xx_if.c的串口全局中断里面检测串口空闲中断标志位、调用串口空闲回调函数
//判断串口空闲中断标志位
if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);//清除串口空闲中断标志位
HAL_UART_IdleCallBack(&huart2); //调用串口空闲回调函数
}
我们在CallBack.c 创建串口空闲回调函数
/*******************
* @brief 自己实现的串口空闲回调函数
* @param UART_HandleTypeDef * huart 处理的串口
* @return
*
*******************/
void HAL_UART_IdleCallBack(UART_HandleTypeDef * huart)
{
if(huart->Instance == huart2.Instance)
{
//这里解析->解析时候可以关闭DMA接收
HAL_UART_DMAStop(&huart2);//这是关闭DM接收
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
UART2.SendString(UART2.pRec_Buffer);
HAL_UART_Receive_DMA(&huart2,UART2.pRec_Buffer,UART2_Rec_LENGTH);//开启DMA接收
}
}
我们在CallBack.h
#ifndef __CallBack_H__
#define __CallBack_H__
/* Includes ------------------------------------------------------------------*/
#include "MyApp.h"
/* extern variables-----------------------------------------------------------*/
/* extern function prototypes-------------------------------------------------*/
//串口空闲中断的回调函数
extern void HAL_UART_IdleCallBack(UART_HandleTypeDef * huart);
#endif
/********************************************************
End Of File
********************************************************/
然后不要忘记添加初始化代码
UART2_VarInit();//初始化参数
现象 发送字符串(要有回车换号结束),然后会返回字符串、
1.2-串口外设发送
我们先用LED表示485设置引脚标志串口处于发送或者接收状态
/*******************
* @brief 串口发送数组
* @param
* @return
*
*******************/
static void SendArray(uint8_t* pArray,uint16_t Len)
{
UART2.RS485_Set_SendMode();//设置485芯片为发送模式
HAL_UART_Transmit_DMA(&huart2, pArray, Len);//使用DMA发送
}
/*******************
* @brief 发送字符串
* @param
* @return
*
*******************/
static void SendString(uint8_t* pString)
{
UART2.RS485_Set_SendMode();//设置485芯片为发送模式
HAL_UART_Transmit(&huart2,pString, strlen((const char *)pString), 0xFF);
}
/*******************
* @brief 设置485为发送模式
* @param
* @return
*
*******************/
static void RS485_Set_SendMode(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
HAL_Delay(1);
}
/*******************
* @brief 设置485接收模式
* @param
* @return
*
*******************/
static void RS485_Set_RecMode(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);
HAL_Delay(1);
}
/********************************************************
End Of File
********************************************************/
在TC中断中添加设置485为接收模式的函数
因为在发送完之后要马上设置485芯片为接收模式
这是弱定义的函数
我们在CallBack.c中重新实现
/**
* @brief Tx Transfer completed callbacks.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == huart2.Instance)
{
UART2.RS485_Set_RecMode();
}
}
注释掉这个
这里我们测试发现,发送字符串后STM32 就会卡死,那么我们如何调试SMT32卡死的具体位置那
主函数点灯方便调试
设置硬件仿真
开启仿真->全速运行(如果点击全速运行,没有运行需要按一下复位键
然后点击串口发送后灯就不闪了,
可以查看当前死在那个中断里面
在这里有中断号
这里我们就找到是串口中断调用了延时函数导致异常的
我们要把这个去掉
我们初始化一个其他引脚作为485芯片模式控制引脚
发送也更改为使用DMA发送
HAL_UART_Transmit_DMA(&huart2,pString, strlen((const char *)pString));
然后给单片机接上485模块和电脑接TTL转485模块进行通信
记录使用485芯片发送1234
这是接到A上 ,不接B ,不接GND(单片机发送数据)
这是接到A 也接到B上(单片机发送数据)
这是也接GND 和AB(单片机发送数据)
这是接B 不接A 接GND(单片机发送数据)
总结发现好像是A电平和TTL传输的一样的,然后B好像是相反的
A在空闲是高电平,B在空闲是低电平
其实原理图设置的时候可以给A加上拉电阻,给B 加下拉电阻
然后我们做一下、电脑发送数据到单片机
无论电脑发送单片机还是单片机发送电脑,都是A线 传输的信号和TTL中发送线TX电平一致,而B就是A的反向
模块暂时使用淘宝的具有自动切换功能的485转换模块
1.3-认识MODBUS协议
信号传输的物理层
485是物理层对01 信号传输方式的规定:使用差分信号传输、是一个半双工的部分485信号是有方向控制引脚的。
主从通信
MODBUS协议规定
MODBUS为了提供通用性、增加了对数据包格式的规定,规定如下
地址码:主要是从机地址,
0 : 0地址是广播指令
1-255:从机使用地址,掉电保持的
功能码:要执行的功能,协议里面规定了常用的功能码,
0x03 读保持寄存器
0x04读输入寄存器
0x06写单个保持寄存器
0x10写多个保持寄存器
这里补充什么是保持寄存器什么是输入寄存器:
保持寄存器:主要是参数存储掉电不丢失,比如地址、波特率等
输入寄存器:主要是保存一些A/D转化结果,比如传感器的温湿度数据,光照
数据区:读写寄存器的地址,或者读写的数据
校验码: 高位校验+低位校验
O3功能码的介绍
主要是 地址码(8bit)+功能码(8bit)+寄存器起始地址+寄存器个数(读寄存器个数)+CRC校验
06写单个寄存器向从机写单个数据
16写多个寄存器向从机写多个数据
1.4-测试MODBUS传感器
我们下面用电脑串口软件+485转USB模块+MODBUS传感器
完成电脑读取传感器数据和设置传感器地址等
先看手册对寄存器的描述
主要说用可读可写的保持寄存器保存模块的参数,比如地址,波特率等掉电不丢失
只读的输入寄存器保存保存的是传感器数据,比如温湿度
通过设置地址+寄存器地址就可以访问他们
!
我们查看的例子是
如何读输入寄存器
注意校验码,主机发送的 低字节在前,高字节在后,
设备回应的 高字节在前面,低字节在后面
注意上面的校验码
http://www.ip33.com/crc.html
然后我们测试一下
如何写单个保存寄存器
主机发送的:写设备地址+写功能码+写寄存器高字节+写寄存器低字节+数据高字节+数据低字节+CRC16校验 高字节+CRC校验低字节
注意这个是.CRC的高字节在前,低字节在后
写之后他们给回应的
设备地址+功能码+寄存器地址高字节+寄存器地址低字节+数据位高字节+数据位低字节+CRC16校验高字节+CRC16校验低字节
举例设置设备地址然后读数据
1.设置新地址,然后断电重启
2.然后新地址读数据
方便后面调试,咱们还是把地址设置为01
然后断电上电,地址就是01了、可以再读一下温湿度数据、能够设置波特率了
1.5-编写STM32代码
STM32编写MODBUS的传感器驱动流程:
1.使用电脑串口助手+USB转485模块作为辅助,调试单片机的程序、就是电脑模拟传感器发单片机看单片机是否可以解析。
2.然后再接上传感器到单片机,
电脑模拟传感器
串口一:设置115200用于打印输出调试
串口二:设置9600用于连接modbus 传感器
方标调试,使用串口一实现printf
/**
* @brief 重定向printf (重定向fputc),
使用时候记得勾选上魔法棒->Target->UseMicro LIB
可能需要在C文件加typedef struct __FILE FILE;
包含这个文件#include "stdio.h"
* @param
* @return
*/
int fputc(int ch,FILE *stream)
{
HAL_UART_Transmit(&huart1,( uint8_t *)&ch,1,0xFFFF);
return ch;
}
勾选
在myAPP.h添加
#include "stdio.h"
硬件连接:
单片机串口一>USB转TTL->电脑USB口
单片机串口二->TTL转485->485转USB->电脑USB口
然后我们的程序是
float Temp = 0;
uint8_t Humidty = 0;//定义温度、湿度
uint8_t Modbus_SendArray[] = {0x01,0x04,0x00,0x00,0x00,0x02,0x71,0xCB};//要发送的数据
HAL_Delay(1000);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
printf("串口一测试发送数据:%.1f %d",Temp,Humidty);
//串口发送读取温湿度
UART2.SendArray(Modbus_SendArray,8);
上面说明单片机发送16进制指令没有问题,下面测试单片机接收16进制,然后再发回来
解析函数内容先写把接收的数据都返回回去
#ifndef __MODBUS_H__
#define __MODBUS_H__
#include "MyApp.h"
//声明一下变量
extern uint8_t ucSend_Buffer[UART2_Send_LENGTH];//发送缓冲区
extern uint8_t ucRec_Buffer [UART2_Rec_LENGTH] ;//接收缓冲区
void MODBUS_ProtocolAnalysis(UART_t * UART);
#endif
#include "MyApp.h"
/*******************
* @brief MODBUS协议解析
* @param 某个串口的的结构体变量地址
* @return 无
*
*******************/
void MODBUS_ProtocolAnalysis(UART_t * UART)
{
UART2.SendArray(ucRec_Buffer,sizeof(ucRec_Buffer));
}
然后把协议解析函数在空闲中断调用
发送什么就会收到,使用串口二的串口助手查看
然后我们编写 MODBUS解析函数的内容了
1.先判断地址对不。
2.然后进行CRC校验,校验把高字节在前和低字节在前的情况都写了
3.然后根据位置 把温度高位字节左移八位,和低位相加
void MODBUS_ProtocolAnalysis(UART_t * UART)
{
//UART2.SendArray(UART->pRec_Buffer,sizeof(UART->pRec_Buffer));
//UART2.SendArray(ucRec_Buffer,sizeof(ucRec_Buffer));
//停止串口二DMA接收
HAL_UART_DMAStop(&huart2);
//接收后的流程:地址对,CRC校验,校验对就看功能码,然后根据位读数据
if(*(UART->pRec_Buffer+0) != TEMP_HUM_ADDR)
{
return ;
}
else if(*(UART->pRec_Buffer+0) == TEMP_HUM_ADDR)//地址正确
{
//计算CRC
CRC_16.CRC_Value = CRC_16.CRC_Check(UART->pRec_Buffer,7);//根据接收到数据的前6个字节计算CRC值
CRC_16.CRC_H = (CRC_16.CRC_Value >> 8); //计算出高位和低位CRC
CRC_16.CRC_L = CRC_16.CRC_Value;
//校验CRC值
if( ((*(UART->pRec_Buffer+7) == CRC_16.CRC_L) && (*(UART->pRec_Buffer+8) == CRC_16.CRC_H)) //CRC 高位在前或者低位在前都可以
||
((*(UART-> pRec_Buffer+7) == CRC_16.CRC_H) && (*(UART->pRec_Buffer+8) == CRC_16.CRC_L)))
{
Temp = ((*(UART-> pRec_Buffer+3) << 8) + *(UART-> pRec_Buffer+4))*0.1;//计算温度
Humidty = ((*(UART-> pRec_Buffer+5) << 8) + *(UART-> pRec_Buffer+6))*0.1;//计算湿度
}
}
//清缓存
for(uint8_t i=0;i<UART2_Rec_LENGTH;i++)
{
*(UART->pRec_Buffer+i) = 0x00;
}
}
1.6-连接传感器测试
然后不要忘记出函数发送查询指令
float Temp = 0;
uint8_t Humidty = 0;//定义温度、湿度
uint8_t Modbus_SendArray[] = {0x01,0x04,0x00,0x00,0x00,0x02,0x71,0xCB};//要发送的数据
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_Delay(2000);
HAL_Delay(2000);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
printf("串口一测试发送数据:%.1f %d",Temp,Humidty);
//串口发送读取温湿度
UART2.SendArray(Modbus_SendArray,8);//单片机发送读传感器命令
}
然后接线
单片机串口二->TTL转485模块->485传感器
单片机串口一->TTL转USB->电脑
作者:好家伙VCC