《上一篇》  《主目录》  《下一篇》

文章目录

一、基础知识点二、开发环境三、STM32CubeMX相关配置1、STM32CubeMX基本配置2、STM32CubeMX RS485 相关配置

四、Vscode代码讲解五、结果演示以及报文解析六、代码下载

一、基础知识点

了解 RS485 Modbus协议技术 。本实验是基于STM32F103开发 实现 通过RS-485实现modbus协议。

准备好了吗?开始我的show time。

二、开发环境

1、硬件开发准备 主控:STM32F103ZET6 RS485收发器:SP3485P

2、软件开发准备 软件开发使用虚拟机 + VScode + STM32Cube 开发STM32,在虚拟机中直接完成编译下载。 该部分可参考:软件开发环境构建

三、STM32CubeMX相关配置

1、STM32CubeMX基本配置

本实验基于CubeMX详解构建基本框架 进行开发。

2、STM32CubeMX RS485 相关配置

(1)发送接收控制脚配置(GPIO配置)

gpio输出电平: 低(控制引脚默认低电平,芯片处于读状态) gpio模式: 推挽输出 gpio上下拉设置: 不上下拉 gpio输出速度: 低速 gpio命名: RS485_DE_nRE (与硬件标识一致,便于代码编写)

(2)串口UART3配置

根据硬件引脚连接,RS485芯片连接UART3通信

基本配置: 实验波特率采用9600、数据位8bit、无奇偶校验、停止位1bit 数据方向: 接收发送 DMA配置: Add添加发送和接收的DMA,DMA参数保持默认状态

(3)中断配置 实验中接收数据采用空闲触发;发送数据采用DMA发送触发后发送完成中断 UART3总中断(USART3 global interrupt)必须打开(为了发送完成中断实现) UART_RX (DMA1 channel3 global interrupt) DMA接收中断不打开,取消对钩(这里对钩无法改变,后续解决) UART_TX (DMA1 channel2 global interrupt) DMA发送中断打开。

进行NVIC中断等级配置(0等级最高) 上述讲到无法取消DMA接收中断,原因是选中了强制DMA中断(右上角蓝色框,取消对钩就ok)

四、Vscode代码讲解

1、初始化相关中断

#ifdef STM32_F407_RS485_Modbus

printf("----DWB 此程序通过RS-485实现modbus协议----\r\n");

__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE); // 使能串口3空闲中断

HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH);

#endif

2、RS485 结构体 以及函数实现

typedef struct

{

uint8_t* pucSend_Buffer; //发送缓存指针

uint8_t* pucRec_Buffer; //接收缓存指针

void (*SendArray)(uint8_t*, uint16_t); //串口发送数组

void (*SendString)(uint8_t*); //串口发送字符串

void (*RS485_Set_SendMode)(void); //RS-485接口设置为发送模式

void (*RS485_Set_RecMode)(void); //RS-485接口设置为接收模式

/* data */

} UART_t;

// 串口发数组

static void SendArray(uint8_t* p_Arr,uint16_t LEN)

{

UART3.RS485_Set_SendMode();

HAL_UART_Transmit_DMA(&huart3,p_Arr,LEN);

}

// RS485接口设置发送模式

static void RS485_Set_SendMode()

{

HAL_GPIO_WritePin(RS485_DE_nRE_GPIO_Port, RS485_DE_nRE_Pin,,GPIO_PIN_SET);

}

// RS485接口设置接收模式

static void RS485_Set_RecMode()

{

HAL_GPIO_WritePin(RS485_DE_nRE_GPIO_Port, RS485_DE_nRE_Pin,,GPIO_PIN_RESET);

}

3、RS485 Modbus发送 重构接收回调函数(整个DMA发送过程后面有讲解)

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)

{

/* Prevent unused argument(s) compilation warning */

if(huart->Instance == huart3.Instance)

{

UART3.RS485_Set_RecMode();

}

}

4、RS485 Modbus接收 接收使用空闲中断 ,在串口总中断中添加空闲中断检测。

void USART3_IRQHandler(void)

{

/* USER CODE BEGIN USART3_IRQn 0 */

if(__HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE)) // 判断空闲中断标志位

{

__HAL_UART_CLEAR_IDLEFLAG(&huart3); // 1、清除中断标志位

HAL_UART_IdleCallback(&huart3); // 2、空闲中断回调函数

}

/* USER CODE END USART3_IRQn 0 */

HAL_UART_IRQHandler(&huart3);

/* USER CODE BEGIN USART3_IRQn 1 */

/* USER CODE END USART3_IRQn 1 */

}

在 Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_uart.h 文件中回调函数并没有串口空闲中断回调函数 重构空闲中断回调函数

void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)

{

if(huart->Instance == huart3.Instance)

{

Modbus.Protocol_Analysis(&UART3); // 接收数据解析

HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, UART3_Rec_LENGTH); // 重新开启接收DMA(在数据解析中会暂时关闭接收DMA)

}

}

5、Modbus 收发数据详解

(1)Modbus结构体

typedef struct

{

uint16_t addr;

void (*Protocol_Analysis)(UART_t*);

} Modbus_t;

(2)Modbus接收数据整体框架

#define UART_Order_Index 8

#define FunctionCode_Read_Register 0x03

#define FunctionCode_Write_Register 0x06

#define UART3_Send_LENGTH 20

#define UART3_Rec_LENGTH 20

static void Protocol_Analysis(UART_t* UART)

{

UART_t* const COM_UART = UART;

uint8_t i = 0, Index = 0;

// 1、关闭接收

HAL_UART_AbortReceive(&huart3);

// 2、整理接收数据

for(i=0; i

{

if(Index == 0)

{

if(*(COM_UART->pucRec_Buffer+i) != Modbus.addr)

continue;

}

*(COM_UART->pucRec_Buffer + Index) = *(COM_UART->pucRec_Buffer + i);

// 取7字节

if(Index == UART_Order_Index)

break;

Index++;

}

// 4、校验码

CRC_16.CRC_Value = CRC_16.CRC_Check(COM_UART->pucRec_Buffer, 6);

CRC_16.CRC_H = (u_int8_t)(CRC_16.CRC_Value >> 8);

CRC_16.CRC_L = (u_int8_t)CRC_16.CRC_Value;

if(((*(COM_UART->pucRec_Buffer+6) == CRC_16.CRC_L) && (*(COM_UART->pucRec_Buffer+7) == CRC_16.CRC_H))

||

((*(COM_UART->pucRec_Buffer+6) == CRC_16.CRC_H) && (*(COM_UART->pucRec_Buffer+7) == CRC_16.CRC_L)))

{

//校验地址

if((*(COM_UART->pucRec_Buffer+0)) == Modbus.addr)

{

// 5、数据处理

if((*(COM_UART->pucRec_Buffer+1)) == FunctionCode_Read_Register)

{

Modbus_Read_Register(COM_UART);

}

else if((*(COM_UART->pucRec_Buffer+1)) == FunctionCode_Write_Register)

{

Modbus_Wrtie_Register(COM_UART);

}

}

}

//清缓存

for(i=0;i

{

*(COM_UART->pucRec_Buffer+i) = 0x00;

}

}

Modbus_Read_Register函数数据解析(协议数据:地址码+功能码+数据长度(字节)+发送数据+CRC)连续读取从设备寄存器值返回给主设备。

static void Modbus_Read_Register(UART_t* UART)

{

UART_t* const COM_UART = UART;

//校验地址

if((*(COM_UART->pucRec_Buffer+2) == 0x9C) && (*(COM_UART->pucRec_Buffer+3) == 0x41))

{

回应数据

//地址码

*(COM_UART->pucSend_Buffer+0) = Modbus.addr;

//功能码

*(COM_UART->pucSend_Buffer+1) = FunctionCode_Read_Register;

//数据长度(字节)

*(COM_UART->pucSend_Buffer+2) = 2;

//发送数据

// deep status

*(COM_UART->pucSend_Buffer+3) = 0;

*(COM_UART->pucSend_Buffer+4) = Deep.Read_Deep();

*(COM_UART->pucSend_Buffer+5) = 0;

*(COM_UART->pucSend_Buffer+6) = 0x66;

//插入CRC

CRC_16.CRC_Value = CRC_16.CRC_Check(COM_UART->pucSend_Buffer,7); //计算CRC值

CRC_16.CRC_H = (uint8_t)(CRC_16.CRC_Value >> 8);

CRC_16.CRC_L = (uint8_t)CRC_16.CRC_Value;

*(COM_UART->pucSend_Buffer+7) = CRC_16.CRC_L;

*(COM_UART->pucSend_Buffer+8) = CRC_16.CRC_H;

//发送数据

UART3.SendArray(COM_UART->pucSend_Buffer,9);

}

}

Modbus_Wrtie_Register函数数据解析。从主设备获取控制从设备外设的数值,解析后控制外设。

static void Modbus_Wrtie_Register(UART_t* UART)

{

UART_t* const COM_UART = UART;

uint8_t i=0;

//回应数据

for(i=0;i<8;i++)

{

*(COM_UART->pucSend_Buffer+i) = *(COM_UART->pucRec_Buffer+i);

}

//发送数据

UART3.SendArray(COM_UART->pucSend_Buffer,8);

//解析数据,控制外设

if((*(COM_UART->pucRec_Buffer+2) == 0x9C) && (*(COM_UART->pucRec_Buffer+3) == 0x42))

{

if(*(COM_UART->pucRec_Buffer+5) == Deep_Status_ON )

Deep.Deep_Enable();

else

Deep.Deep_Disable();

}

}

为什么要使能DMA发送完成中断才会触发UART的发送完成中断? 答案就在代码里,带大家解析一遍相关代码:

// 调用HAL_UART_Transmit_DMA函数实现DMA发送

HAL_UART_Transmit_DMA

-> huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt; // 设置发送完成回调函数

static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma)

{

UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;

/* DMA Normal mode*/

if ((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)

{

huart->TxXferCount = 0x00U;

/* Disable the DMA transfer for transmit request by setting the DMAT bit

in the UART CR3 register */

CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);

/* Enable the UART Transmit Complete Interrupt */

SET_BIT(huart->Instance->CR1, USART_CR1_TCIE); // 当DMA发送完成后,会使能串口发送完成中断

}

/* DMA Circular mode */

else

{

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)

/*Call registered Tx complete callback*/

huart->TxCpltCallback(huart);

#else

/*Call legacy weak Tx complete callback*/

HAL_UART_TxCpltCallback(huart);

#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

}

}

当DMA发送完成后,会使能串口发送完成中断。配置打开UART3中断总开关。

HAL_UART_IRQHandler

-> if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))

-> UART_EndTransmit_IT(huart);

-> HAL_UART_TxCpltCallback(huart); // 回调函数为弱函数,可重构

五、结果演示以及报文解析

实验测试使用USB转RS485工具。从设备板子上A B接口连接USB转RS485工具上对应A B接口。主设备为PC端安装的MThings进行Modbus收发数据测试。有兴趣的小伙伴可以体验下MTings官网 发送数据报文解析

[2023-03-05 13:13:03-802]COM34-发送:01 06 9c 42 00 01 c6 4e [2023-03-05 13:13:03-827]COM34-接收:01 06 9c 42 00 01 c6 4e 0x01:主机要查询的从设备地址 0x06:功能码 修改写操作 0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002 0x00 0x01:写入地址的数值为0x01 (控制从设备蜂鸣器打开) 0xc6 0x4e:CRC校验码

[2023-03-05 13:13:04-980]COM34-发送:01 06 9c 42 00 00 07 8e [2023-03-05 13:13:05-012]COM34-接收:01 06 9c 42 00 00 07 8e 0x01:主机要查询的从设备地址 0x06:功能码 修改写操作 0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002 0x00 0x01:写入地址的数值为0x00 (控制从设备蜂鸣器关闭) 0xc6 0x4e:CRC校验码

接收数据报文解析

[2023-03-05 13:41:54-954]COM34-发送:01 06 9c 42 00 01 c6 4e [2023-03-05 13:41:54-977]COM34-接收:01 06 9c 42 00 01 c6 4e 0x01:从设备地址 0x06:功能码 修改写操作 0x9c 0x42:寄存器地址0x9c42转十进制地址为40,002 0x00 0x01:写入地址的数值为0x00 (控制从设备蜂鸣器关闭) 0xc6 0x4e:CRC校验码

[2023-03-05 13:41:56-289]COM34-发送:01 03 9c 41 00 02 ba 4f 0x01:主机要查询的从设备地址 0x03:功能码 查询读操作 0x9c 0x42:寄存器地址0x9c41转十进制地址为40,001 0x00 0x02:读取两个数据(一个数据2字节) 0xba 0x4f:CRC校验码

[2023-03-05 13:41:56-320]COM34-接收:01 03 02 00 01 00 66 d9 a3 0x01:告诉主机自己从设备地址 0x03:功能码 读操作 0x00 0x01:读出第一个数据为0x01,当前蜂鸣器打开状态 0x00 0x66:读取第二个数据为0x66(该值是本猿在代码中写死的值,后续功能会结合本章节modbus功能通信,敬请期待) 0xd9 0xa3:CRC校验码

六、代码下载

STM32开发(六)STM32F103 RS485 Modbus通信代码

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: