STM32开发(六)STM32F103 通信 —— RS485 Modbus通信编程详解
创始人
2024-05-29 00:12:28
0

文章目录

    • 一、基础知识点
    • 二、开发环境
    • 三、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_Modbusprintf("----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   20static 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; iif(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;//插入CRCCRC_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();elseDeep.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 bitin 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校验码

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...