当前位置: 首页 > news >正文

STM32HAL库 -- 10.DMA外设实战(UART串口+DMA读取传感器数据)

目录

1.简介

2.DMA介绍

2.1什么是DMA?

2.2DMA的通道和优先级

2.3DMA的传输模式

2.4DMA的数据对齐

2.5DMA的指针递增

3.HAL库中常用的DMA函数

3.1DMA的初始化函数 -- HAL_DMA_Init

3.2DMA的时钟使能和失能

3.3DMA的中断处理函数 -- HAL_DMA_IRQHandler

3.4DMA与外设的链接宏函数

4.DMA的使用流程

5.项目实战

5.1代码编写

5.1.1sp70c.h

5.1.2sp70c传感器初始化

5.1.3sp70c的串口初始化

5.1.4sp70c的DMA初始化

5.1.5开启UART的DMA数据传输

6.结尾


 

1.简介

        这个文章会介绍SHM32的DMA,并使用DMA和UART读取传感器数据。以sp70c毫米波雷达举例。笔者使用的是STM32F103ZET6的开发板。

2.DMA介绍

2.1什么是DMA?

        DMA(Direct Memory Access,直接存储器访问)是STM32微控制器中的一个重要外设,它允许在外设(如ADC、USART、SPI等)和存储器之间,或者存储器和存储器之间直接传输数据,而无需CPU的干预。这样可以显著提高数据传输的效率,减轻CPU的负担,使得CPU可以执行其他任务。

2.2DMA的通道和优先级

        下图是DMA的框图:

       从上图可以看到,Cortex-M3核心有两个DMA。分别是DMA1和DMA2。DMA1有7个通道,DMA2有5个通道,不同的 DMA 控制器的通道对应着不同的外设请求,DMA1的7个通道对应的外设如下图所示: 

        DMA2的5个通道对应的外设如下图所示:

       如果外设想要通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求,DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。

       当有多个DMA通道同时发送请求时,仲裁器会根据预设的优先级规则(硬件固定优先级或软件可编程优先级)进行裁决,优先响应高优先级通道的传输请求。

        软件优先级是在编程中可以预先设置的,分为最高级、高级、中级和低级4个等级。如果两个DMA的请求具有相同的软件优先级,此时比较硬件优先级,通道数小的优先级高,例如DMA2_CH1高于DMA2_CH3。如下图所示:

        仲裁器会动态管理总线资源,确保高优先级通道的数据及时传输,同时通过中断或轮询机制协调低优先级通道的等待与恢复,避免数据丢失或总线冲突。

2.3DMA的传输模式

        DMA的传输有两种模式:正常模式和循环模式。

        正常模式就是当一次DMA数据传输完后,自动停止DMA 传输,然后需要手动的调用函数再启动下一次DMA的数据传输,这种方式用的多。

        循环模式就是当一次DMA传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。

2.4DMA的数据对齐

        STM32的DMA控制器支持不同宽度的数据传输(8位、16位、32位),但需确保源地址和目标地址的对齐方式匹配,否则可能导致数据错误或性能下降。

        DMA数据对齐:像停车位一样,你的车必须停对位置!想象一下,你开着不同大小的车(数据)去停车场(内存/外设),但停车位(内存地址)有严格的规定:

数据类型车辆比喻对齐要求合法地址示例非法地址示例违规后果
8位 (Byte)🏍️ 摩托车无要求(1字节对齐)0x200000000x20000001无(所有地址合法)无问题
16位 (Half-Word)🚗 轿车2字节对齐(偶数地址)0x200000000x200000020x200000010x20000003BusFault 或数据截断
32位 (Word)🚛 大卡车4字节对齐(4的倍数地址)0x200000000x200000040x200000010x200000020x20000003数据错乱 / BusFault / 传输失败

💡 关键点:数据就像车,必须停在符合它大小的车位(对齐地址),否则会出问题!

2.5DMA的指针递增

        指针递增决定DMA传输过程中源地址和目标地址是否自动偏移,适用于连续数据块的传输。

        DMA指针递增:像快递员送包裹,一栋楼一栋楼跑!想象一下,DMA的指针就像一个快递员,而内存和外设的地址就像一栋栋楼的门牌号。

指针模式比喻说明数据宽度影响适用场景注意事项
固定地址模式快递员始终往同一栋楼送货无步进(地址不变)ADC连续采样到同一变量数据会覆盖,仅保留最后一次结果
内存递增模式快递员按门牌号顺序送货8位:+1, 16位:+2, 32位:+4USART接收数据存入数组必须确保内存地址对齐
双递增模式快递员从不同仓库取货并顺序送货双方同步步进内存块搬移/数据重组

需严格匹配两端数据宽度

3.HAL库中常用的DMA函数

3.1DMA的初始化函数 -- HAL_DMA_Init

HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

功能:初始化DMA通道,配置传输方向、数据宽度、优先级等参数。

返回值:HAL_OK:初始化成功。HAL_ERROR:参数错误或初始化失败

参数:DMA_HandleTypeDef *hdma。

        hdma 是一个指向DMA配置结构体的指针,其关键成员如下:

成员变量描述常用取值(HAL库宏)
InstanceDMA通道实例(如DMA1_Channel1DMA1_Channel1~DMA2_Channel7(依型号而定)
Init.Direction数据传输方向DMA_MEMORY_TO_PERIPH(内存→外设)
DMA_PERIPH_TO_MEMORY(外设→内存)
DMA_MEMORY_TO_MEMORY(内存→内存)
Init.PeriphInc外设地址是否递增DMA_PINC_ENABLE(递增)
DMA_PINC_DISABLE(固定)
Init.MemInc内存地址是否递增DMA_MINC_ENABLE(递增)
DMA_MINC_DISABLE(固定)
Init.PeriphDataAlignment外设数据宽度(对齐方式)DMA_PDATAALIGN_BYTE(8位)
DMA_PDATAALIGN_HALFWORD(16位)
DMA_PDATAALIGN_WORD(32位)
Init.MemDataAlignment内存数据宽度(对齐方式)同外设宽度选项
Init.ModeDMA传输模式DMA_NORMAL(单次传输)
DMA_CIRCULAR(循环传输)
Init.Priority通道优先级DMA_PRIORITY_LOW
DMA_PRIORITY_MEDIUM
DMA_PRIORITY_HIGH
DMA_PRIORITY_VERY_HIGH

3.2DMA的时钟使能和失能

通过宏函数控制:

__HAL_RCC_DMA1_CLK_ENABLE();    //DMA1的时钟使能
__HAL_RCC_DMA1_CLK_DISABLE();   //DMA1的时钟失能__HAL_RCC_DMA2_CLK_ENABLE();    //DMA2的时钟使能
__HAL_RCC_DMA2_CLK_DISABLE();   //DMA2的时钟失能

3.3DMA的中断处理函数 -- HAL_DMA_IRQHandler

void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);

功能:DMA中断的统一处理函数,用于处理传输完成、半传输、错误等中断事件,并调用对应的回调函数。需在DMA通道的中断服务函数(如DMA1_Channel1_IRQHandler())中调用。它会自动触发用户注册的回调函数(如HAL_DMA_TxCpltCallback())。不同的中断触发不同的回调函数。

参数介绍过了。

3.4DMA与外设的链接宏函数

__HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD__, __DMA_HANDLE__)

功能:用于将 DMA 控制器与外设关联起来,确保外设(如 USART、ADC、SPI 等)在需要 DMA 传输时能正确调用对应的 DMA 通道。

4.DMA的使用流程

        我的传感器使用的是UART外设,下面以此举例说明:

        首先,正常配置UART外设(波特率、停止位、数据位等);

        其次,查找你所使用的外设的DMA通道,比如我使用的是UART4,而UART4_RX使用的DMA2_CH3;UART4_TX不使用就不关注。

        然后,匹配到了使用的DMA通道后,就进行按顺序的打开时钟、参数配置、链接外设、初始化DMA和打开DMA的中断。

        然后,重写DMA的中断函数,如DMA2_Channel3_IRQHandler(),在这个函数中调用DMA的中断处理函数。然后再重写你使用的外设的相应处理函数。比如HAL_UART_RxCpltCallback()。

5.项目实战

        笔者使用的是一个毫米波雷达的传感器,型号是SP70C,使用的是UART协议。数据手册如下:

        为什么使用DMA呢? 按照SP70C数据手册的说明,20ms一个报文,一个报文14字节。这个数据太频繁了,换算下来数据量0.7KB/s。如果直接使用UART空闲中断的方式接收数据,就会频繁的占用CPU搬运数据,这样效率就低,因此使用DMA去搬运数据提高效率。

        鉴于大家可能没有合适的传感器,我就主要把DMA和UART的初始化配置代码,在下面进行介绍,数据解析等放在项目工程里。我会一步一步介绍代码的构建。

5.1代码编写

5.1.1sp70c.h

        在写一个传感器的代码时,首先定义出这个传感器所使用的引脚和中断、中断函数等,并进行宏替换,这样就增加了代码的可读性和移植性!

#ifndef __SP70C_H__
#define __SP70C_H__#include "stm32f1xx.h"/*	使用的是UART4,UART4_RX使用的是DMA2_CH3	*/
#define SP70C_UART				UART4
#define SP70C_UART_RX_DMA		DMA2_Channel3
#define SP70C_DMA_IRQn			DMA2_Channel3_IRQn
#define SP70C_DMA_IRQHandler	DMA2_Channel3_IRQHandler/*	SP70C-RX ---- PC10	*/
/*	SP70C-TX ---- PC11	*/
#define SP70C_UART_TX_PORT		GPIOC
#define SP70C_UART_TX_PIN		GPIO_PIN_10
#define SP70C_UART_RX_PORT		GPIOC
#define SP70C_UART_RX_PIN		GPIO_PIN_11#define SP70C_DATA_LENGTH		14
#define SP70C_DATA_TEMP_NUM		10/*	宏函数	*/
#define SP70C_UART_CLK_ENABLE()				do{__HAL_RCC_UART4_CLK_ENABLE();}while(0)
#define SP70C_UART_TX_GPIO_CLK_ENABLE()		do{__HAL_RCC_GPIOC_CLK_ENABLE();}while(0)
#define SP70C_UART_RX_GPIO_CLK_ENABLE()		do{__HAL_RCC_GPIOC_CLK_ENABLE();}while(0)
#define SP70C_DMA_CLK_ENABLE()				do{__HAL_RCC_DMA2_CLK_ENABLE();}while(0)/*	函数声明	*/
void sp70c_init(void);
float sp70c_get_rcs(void);
float sp70c_get_range(void);
float sp70c_get_vrel(void);
int sp70c_get_azimuth(void);#endif

5.1.2sp70c传感器初始化

        我们期望直接调用一个函数就完成对这个传感器的初始化,例如sp70c_init();

        然而这个传感器用到了UART和DMA等,因此在这个函数中要完成所用到的资源进行初始化。如以下代码所示:


/*** @brief  SP70C 传感器模块初始化函数* @note   该函数用于初始化与 SP70C 通信相关的所有硬件和软件资源,包括:*         - 串口底层 GPIO 引脚初始化(TX/RX)*         - UART 外设初始化(波特率 115200,8N1,收发模式)*         - DMA 控制器初始化(用于 UART 接收)*         - 启动一次 DMA 异步接收,开始监听传感器数据* * @retval void:无返回值*/
void sp70c_init(void)
{printf("NOTE: sp70c init....\r\n");/*	1.串口底层引脚初始化	*/sp70c_uart_msp_init();/*	2.串口初始化	*/if(sp70c_uart_init() < 0){while(1);}/*	3.DMA初始化	*/if(sp70c_dma_init() < 0){while(1);}/*	4.启动DMA接收数据	*/if(sp70c_start_dma_receive() < 0){while(1);}printf("OK: sp70c init ok\r\n");
}

5.1.3sp70c的串口初始化

        根据数据手册提供的信息初始化串口:

/*** @brief  初始化 SP70C 模块使用的 UART 外设(如 UART4)* @note   配置为:波特率 115200,8 数据位,1 停止位,无校验,支持收发,无硬件流控* @retval int 0:初始化成功;-1:初始化失败      */
int sp70c_uart_init(void)
{/*	1.打开外设时钟	*/SP70C_UART_CLK_ENABLE();/* 2. 配置 UART 参数 */sp70c_g_huart.Instance = SP70C_UART;                    // 指定 UART 外设,比如 UART4sp70c_g_huart.Init.BaudRate = 115200;                   // 波特率:115200sp70c_g_huart.Init.WordLength = UART_WORDLENGTH_8B;     // 数据位:8 bitsp70c_g_huart.Init.StopBits = UART_STOPBITS_1;          // 停止位:1 bitsp70c_g_huart.Init.Parity = UART_PARITY_NONE;           // 校验位:无校验sp70c_g_huart.Init.Mode = UART_MODE_TX_RX;              // 模式:收发模式(全双工)sp70c_g_huart.Init.HwFlowCtl = UART_HWCONTROL_NONE;     // 无硬件流控(如 RTS/CTS)sp70c_g_huart.Init.OverSampling = UART_OVERSAMPLING_16; // 过采样:16 倍(标准,稳定)/* 3. 调用 HAL 库初始化 UART */if (HAL_UART_Init(&sp70c_g_huart) != HAL_OK){// 初始化失败,可打印调试信息(调试用,发布时可注释掉)printf("ERR: sp70c_uart_init\r\n");return -1;  // 返回错误码}else{// 初始化成功printf("OK: sp70c_uart_init\r\n");return 0;  // 返回成功}
}/*** @brief  初始化 SP70C 传感器 UART 的底层 GPIO 引脚(TX 和 RX)* @note   包括:*         - 使能 TX 和 RX 引脚的 GPIO 时钟*         - 配置 TX 引脚为复用推挽输出(AF_PP),用于 UART 发送*         - 配置 RX 引脚为复用输入(AF_INPUT),用于 UART 接收*         - 设置上拉电阻、GPIO速度等参数* @retval 无*/
void sp70c_uart_msp_init(void)
{/*	1. 使能 UART TX 和 RX 引脚的 GPIO 时钟	*/SP70C_UART_TX_GPIO_CLK_ENABLE();SP70C_UART_RX_GPIO_CLK_ENABLE();/*	2. 配置GPIO	*/GPIO_InitTypeDef gpio = {0};gpio.Pin = SP70C_UART_TX_PIN;			// UART的TX引脚gpio.Mode = GPIO_MODE_AF_PP;			// 复用功能,推挽输出(用于 UART TX)gpio.Pull = GPIO_PULLUP;				// 上拉模式gpio.Speed = GPIO_SPEED_FREQ_MEDIUM;	// GPIO 速度:中等(也可选 HIGH)HAL_GPIO_Init(SP70C_UART_TX_PORT, &gpio);gpio.Pin = SP70C_UART_RX_PIN;			// UART的RX引脚gpio.Mode = GPIO_MODE_AF_INPUT;			// 复用功能,输入模式(用于 UART RX)HAL_GPIO_Init(SP70C_UART_RX_PORT, &gpio);
}

5.1.4sp70c的DMA初始化

        在配置好UART之后,进行DMA的配置:打开时钟,基本参数配置,打开DMA中断,与外设链接,编写中断函数。


/*** @brief  初始化用于 UART 接收的 DMA 控制器* @note   配置如下:*         - 使能 DMA 时钟*         - 设置 DMA 从外设(UART RX)搬运数据到内存*         - 数据对齐:字节对齐*         - 模式:普通模式(非循环)*         - 优先级:中等* @retval int 0:初始化成功;-1:初始化失败*/
int sp70c_dma_init(void)
{/*	1.打开DMA时钟	*/SP70C_DMA_CLK_ENABLE();/*	2.配置DMA	*/sp70c_g_hdma.Instance = SP70C_UART_RX_DMA;				 //使用的DMA通道sp70c_g_hdma.Init.Direction = DMA_PERIPH_TO_MEMORY;      // 数据方向:外设 -> 内存(UART接收)sp70c_g_hdma.Init.PeriphInc = DMA_PINC_DISABLE;          // 外设地址不递增(固定为UART数据寄存器)sp70c_g_hdma.Init.MemInc = DMA_MINC_ENABLE;              // 内存地址递增(接收多字节数据)sp70c_g_hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 外设数据宽度:1 字节sp70c_g_hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;     // 内存数据宽度:1 字节sp70c_g_hdma.Init.Mode = DMA_NORMAL;                     // 循环模式sp70c_g_hdma.Init.Priority = DMA_PRIORITY_MEDIUM;        // 中等优先级/*  3. DMA与UART绑定	*/__HAL_LINKDMA(&sp70c_g_huart, hdmarx, sp70c_g_hdma);/*	4. 打开DMA中断	*/HAL_NVIC_SetPriority(SP70C_DMA_IRQn, 2, 2); // 根据实际DMA中断号修改HAL_NVIC_EnableIRQ(SP70C_DMA_IRQn);/*  5. 初始化 DMA */if(HAL_DMA_Init(&sp70c_g_hdma) != HAL_OK){printf("ERR: sp70c_dma_init\r\n");return -1; }else{printf("OK: sp70c_dma_init\r\n");return 0; }
}/*** @brief  DMA 中断服务函数(通常是 HAL 默认封装,用户一般不需要修改)* @note   当 DMA 传输发生中断(如传输完成、错误等)时,硬件会跳转到此函数。*         此函数内部调用了 HAL 提供的默认 DMA 中断处理函数:HAL_DMA_IRQHandler(),*         它会处理中断标志、调用回调等。一般用户无需修改此函数。* * @retval void:无返回值*/
void SP70C_DMA_IRQHandler(void)
{HAL_DMA_IRQHandler(&sp70c_g_hdma);
}

5.1.5开启UART的DMA数据传输

        基本的配置好以后,就编写数据传输函数,每调用一次这个函数,数据就进行一次传输。当数据传输完成之后,就会触发中断,进入到中断服务函数。在中断服务函数中,会根据中断的标志位自动的调用相应的处理函数。比如串口接收数据完成,就调用HAL_UART_RxCpltCallback()函数。

        一般地,这种函数都是虚函数,需要我们根据自己的想法重写函数。代码如下:

/*** @brief  启动一次 DMA 异步接收,从 UART 接收数据到缓冲区* @note   该函数使用 HAL_UART_Receive_DMA() 启动 DMA 接收,数据接收是异步的,*         即函数调用后立即返回,实际的数据接收由 DMA 在后台完成。*         当接收完成后,HAL 会调用回调函数 HAL_UART_RxCpltCallback()。* * @retval int *         - 0:DMA 接收启动成功*         - -1:DMA 接收启动失败(HAL_UART_Receive_DMA 返回非 HAL_OK)*/
int sp70c_start_dma_receive(void)
{memset(sp70c_g_rx_temp, 0, sizeof(sp70c_g_rx_temp));if(HAL_UART_Receive_DMA(&sp70c_g_huart, sp70c_g_rx_temp, sizeof(sp70c_g_rx_temp)) == HAL_OK){return 0;}else{printf("ERR: start dma receive data\r\n");return -1;}
}/*** @brief  UART DMA 接收完成回调函数* @note   当 UART 通过 DMA 接收完指定长度的数据后,HAL 库会自动调用此函数。*         此函数用于判断是否是目标 UART(SP70C_UART),然后对接收到的数据*         进行解析处理,包括解析目标状态、目标信息,并打印结果。*         最后会重新启动 DMA 接收,实现循环接收。* * @param  huart:指向 UART 句柄的指针,由 HAL 自动传入* @retval void:无返回值*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{	if(huart->Instance == SP70C_UART){/*	解析数据	*/printf("OK: read sp70c data ok\r\n");sp70c_parse_target_status();if(sp70c_g_data_pack.target_num > 0){sp70c_parse_target_info();sp70c_print_data_pack();}else{memset(&sp70c_g_data_pack, 0, sizeof(sp70c_g_data_pack));}/*	开启DMA接收	*/sp70c_start_dma_receive();}
}

        我的做法是,DMA接收数据完成之后,进行数据解析,完成之后再进行数据接收,再解析.....

基本上主要的UART和DMA的配置就是以上的代码,数据解析的代码,没有体现在上面。完整的代码放在下面。

5.1.6sp70c.c

#include "stdio.h"
#include "string.h"#include "sp70c.h"/*	数据类型定义	*/
typedef struct
{float rcs;						//目标反射截面积float range;					//目标距离,单位,mfloat vrel;						//目标速度,单位,m/sint azimuth;					//目标方位角unsigned char target_index;		//目标序号
}sp70c_target_info_t;typedef struct 
{unsigned char target_num;			//目标个数sp70c_target_info_t target_info;	//目标信息
}sp70c_data_pack_t;/*	全局变量		*/
UART_HandleTypeDef sp70c_g_huart = {0};	//uart的句柄
DMA_HandleTypeDef sp70c_g_hdma = {0};	//dma的句柄
unsigned char sp70c_g_rx_temp[SP70C_DATA_LENGTH*SP70C_DATA_TEMP_NUM] = {0};		//数据的接收缓冲区
sp70c_data_pack_t sp70c_g_data_pack = {0};		//存放解析的数据 /*** @brief  初始化 SP70C 模块使用的 UART 外设(如 UART4)* @note   配置为:波特率 115200,8 数据位,1 停止位,无校验,支持收发,无硬件流控* @retval int 0:初始化成功;-1:初始化失败      */
int sp70c_uart_init(void)
{/*	1.打开外设时钟	*/SP70C_UART_CLK_ENABLE();/* 2. 配置 UART 参数 */sp70c_g_huart.Instance = SP70C_UART;                    // 指定 UART 外设,比如 UART4sp70c_g_huart.Init.BaudRate = 115200;                   // 波特率:115200sp70c_g_huart.Init.WordLength = UART_WORDLENGTH_8B;     // 数据位:8 bitsp70c_g_huart.Init.StopBits = UART_STOPBITS_1;          // 停止位:1 bitsp70c_g_huart.Init.Parity = UART_PARITY_NONE;           // 校验位:无校验sp70c_g_huart.Init.Mode = UART_MODE_TX_RX;              // 模式:收发模式(全双工)sp70c_g_huart.Init.HwFlowCtl = UART_HWCONTROL_NONE;     // 无硬件流控(如 RTS/CTS)sp70c_g_huart.Init.OverSampling = UART_OVERSAMPLING_16; // 过采样:16 倍(标准,稳定)/* 3. 调用 HAL 库初始化 UART */if (HAL_UART_Init(&sp70c_g_huart) != HAL_OK){// 初始化失败,可打印调试信息(调试用,发布时可注释掉)printf("ERR: sp70c_uart_init\r\n");return -1;  // 返回错误码}else{// 初始化成功printf("OK: sp70c_uart_init\r\n");return 0;  // 返回成功}
}/*** @brief  初始化 SP70C 传感器 UART 的底层 GPIO 引脚(TX 和 RX)* @note   包括:*         - 使能 TX 和 RX 引脚的 GPIO 时钟*         - 配置 TX 引脚为复用推挽输出(AF_PP),用于 UART 发送*         - 配置 RX 引脚为复用输入(AF_INPUT),用于 UART 接收*         - 设置上拉电阻、GPIO速度等参数* @retval 无*/
void sp70c_uart_msp_init(void)
{/*	1. 使能 UART TX 和 RX 引脚的 GPIO 时钟	*/SP70C_UART_TX_GPIO_CLK_ENABLE();SP70C_UART_RX_GPIO_CLK_ENABLE();/*	2. 配置GPIO	*/GPIO_InitTypeDef gpio = {0};gpio.Pin = SP70C_UART_TX_PIN;			// UART的TX引脚gpio.Mode = GPIO_MODE_AF_PP;			// 复用功能,推挽输出(用于 UART TX)gpio.Pull = GPIO_PULLUP;				// 上拉模式gpio.Speed = GPIO_SPEED_FREQ_MEDIUM;	// GPIO 速度:中等(也可选 HIGH)HAL_GPIO_Init(SP70C_UART_TX_PORT, &gpio);gpio.Pin = SP70C_UART_RX_PIN;			// UART的RX引脚gpio.Mode = GPIO_MODE_AF_INPUT;			// 复用功能,输入模式(用于 UART RX)HAL_GPIO_Init(SP70C_UART_RX_PORT, &gpio);
}/*** @brief  初始化用于 UART 接收的 DMA 控制器* @note   配置如下:*         - 使能 DMA 时钟*         - 设置 DMA 从外设(UART RX)搬运数据到内存*         - 数据对齐:字节对齐*         - 模式:普通模式(非循环)*         - 优先级:中等* @retval int 0:初始化成功;-1:初始化失败*/
int sp70c_dma_init(void)
{/*	1.打开DMA时钟	*/SP70C_DMA_CLK_ENABLE();/*	2.配置DMA	*/sp70c_g_hdma.Instance = SP70C_UART_RX_DMA;				 //使用的DMA通道sp70c_g_hdma.Init.Direction = DMA_PERIPH_TO_MEMORY;      // 数据方向:外设 -> 内存(UART接收)sp70c_g_hdma.Init.PeriphInc = DMA_PINC_DISABLE;          // 外设地址不递增(固定为UART数据寄存器)sp70c_g_hdma.Init.MemInc = DMA_MINC_ENABLE;              // 内存地址递增(接收多字节数据)sp70c_g_hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 外设数据宽度:1 字节sp70c_g_hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;     // 内存数据宽度:1 字节sp70c_g_hdma.Init.Mode = DMA_NORMAL;                     // 循环模式sp70c_g_hdma.Init.Priority = DMA_PRIORITY_MEDIUM;        // 中等优先级/*  3. DMA与UART绑定	*/__HAL_LINKDMA(&sp70c_g_huart, hdmarx, sp70c_g_hdma);/*	4. 打开DMA中断	*/HAL_NVIC_SetPriority(SP70C_DMA_IRQn, 2, 2); // 根据实际DMA中断号修改HAL_NVIC_EnableIRQ(SP70C_DMA_IRQn);/*  5. 初始化 DMA */if(HAL_DMA_Init(&sp70c_g_hdma) != HAL_OK){printf("ERR: sp70c_dma_init\r\n");return -1; }else{printf("OK: sp70c_dma_init\r\n");return 0; }
}/*** @brief  启动一次 DMA 异步接收,从 UART 接收数据到缓冲区* @note   该函数使用 HAL_UART_Receive_DMA() 启动 DMA 接收,数据接收是异步的,*         即函数调用后立即返回,实际的数据接收由 DMA 在后台完成。*         当接收完成后,HAL 会调用回调函数 HAL_UART_RxCpltCallback()。* * @retval int *         - 0:DMA 接收启动成功*         - -1:DMA 接收启动失败(HAL_UART_Receive_DMA 返回非 HAL_OK)*/
int sp70c_start_dma_receive(void)
{memset(sp70c_g_rx_temp, 0, sizeof(sp70c_g_rx_temp));if(HAL_UART_Receive_DMA(&sp70c_g_huart, sp70c_g_rx_temp, sizeof(sp70c_g_rx_temp)) == HAL_OK){return 0;}else{printf("ERR: start dma receive data\r\n");return -1;}
}/*** @brief  解析接收到的目标信息数据帧* @note   该函数在全局接收缓冲区 sp70c_g_rx_temp[] 中查找特定的目标信息帧头,*         校验帧尾完整性后,解析出目标索引、RCS、距离、方位角、相对速度等信息,*         并存储到全局数据结构 sp70c_g_data_pack.target_info 中。* * @retval int *         - 0:解析成功*         - -1:未找到有效帧头 或 帧不完整 或 数据越界*/
int sp70c_parse_target_info(void)
{int start_pos = 0;		//开始标识位置/*	查找开始标识	*/while((!(sp70c_g_rx_temp[start_pos] == 0xAA && sp70c_g_rx_temp[start_pos + 1] == 0xAA && \sp70c_g_rx_temp[start_pos + 2] == 0x0C && sp70c_g_rx_temp[start_pos + 3] == 0x07)))	{if(start_pos > sizeof(sp70c_g_rx_temp) - SP70C_DATA_LENGTH){return -1;}start_pos++;}/*	判断数据帧是否完整	*/if(!(sp70c_g_rx_temp[start_pos + SP70C_DATA_LENGTH - 2] == 0x55 && sp70c_g_rx_temp[start_pos + SP70C_DATA_LENGTH - 1] == 0x55)){return -1;}/*	解析目标输出信息	*/	sp70c_g_data_pack.target_info.target_index = sp70c_g_rx_temp[start_pos + 4];sp70c_g_data_pack.target_info.rcs = sp70c_g_rx_temp[start_pos + 5] * 0.5 - 50;sp70c_g_data_pack.target_info.range = (sp70c_g_rx_temp[start_pos + 6] * 0x100 + sp70c_g_rx_temp[start_pos + 7]) * 0.01;sp70c_g_data_pack.target_info.azimuth = sp70c_g_rx_temp[start_pos + 8] * 2 - 90;sp70c_g_data_pack.target_info.vrel = (sp70c_g_rx_temp[start_pos + 9] * 256 + sp70c_g_rx_temp[start_pos + 10]) * 0.05 - 35;return 0;
}/*** @brief  解析接收到的目标状态数据帧* @note   该函数在全局接收缓冲区 sp70c_g_rx_temp[] 中查找特定的目标状态帧头,*         校验帧尾完整性后,解析出目标数量信息,并存储到全局数据结构 sp70c_g_data_pack.target_num 中。* * @retval int *         - 0:解析成功*         - -1:未找到有效帧头 或 帧不完整 或 数据越界*/
int sp70c_parse_target_status(void)
{int start_pos = 0;		//开始标识位置/*	查找开始标识	*/while((!(sp70c_g_rx_temp[start_pos] == 0xAA && sp70c_g_rx_temp[start_pos + 1] == 0xAA && \sp70c_g_rx_temp[start_pos + 2] == 0x0B && sp70c_g_rx_temp[start_pos + 3] == 0x07)))	{if(start_pos > sizeof(sp70c_g_rx_temp) - SP70C_DATA_LENGTH){return -1;}start_pos++;}/*	判断数据帧是否完整	*/if(!(sp70c_g_rx_temp[start_pos + SP70C_DATA_LENGTH - 2] == 0x55 && sp70c_g_rx_temp[start_pos + SP70C_DATA_LENGTH - 1] == 0x55)){return -1;}/*	解析目标输出状态	*/sp70c_g_data_pack.target_num = sp70c_g_rx_temp[start_pos + 4];return 0;
}/*** @brief  打印解析后的目标数据包内容* @note   该函数用于将全局变量 sp70c_g_data_pack 中解析得到的目标信息(如目标序号、RCS、距离、速度、方位角等)*         通过串口(printf)打印输出,便于调试和观察传感器返回的目标数据。*         通常在数据解析成功后调用此函数,将关键信息显示在串口助手等工具中。* * @retval void 无返回值*/
void sp70c_print_data_pack(void)
{printf("data:\r\n");printf("目标序号:%d\r\n", sp70c_g_data_pack.target_info.target_index);printf("目标反射截面积:%.2f\r\n", sp70c_g_data_pack.target_info.rcs);printf("目标距离:%.2f m\r\n", sp70c_g_data_pack.target_info.range);printf("目标速度:%.2f m/s\r\n", sp70c_g_data_pack.target_info.vrel);printf("目标方位角:%d\r\n", sp70c_g_data_pack.target_info.azimuth);printf("\r\n");
}/*** @brief  UART DMA 接收完成回调函数* @note   当 UART 通过 DMA 接收完指定长度的数据后,HAL 库会自动调用此函数。*         此函数用于判断是否是目标 UART(SP70C_UART),然后对接收到的数据*         进行解析处理,包括解析目标状态、目标信息,并打印结果。*         最后会重新启动 DMA 接收,实现循环接收。* * @param  huart:指向 UART 句柄的指针,由 HAL 自动传入* @retval void:无返回值*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{	if(huart->Instance == SP70C_UART){/*	解析数据	*/printf("OK: read sp70c data ok\r\n");
//		printf("data:\r\n");
//		for(int i = 0; i < SP70C_DATA_sp70c_g_rx_temp_NUM ; i++)
//		{
//			for(int j = 0; j < SP70C_DATA_LENGTH; j++)
//			{
//				printf("%02X ", sp70c_g_rx_temp[i*14 + j]);
//			}
//			printf("\r\n");
//		}
//		printf("\r\n");sp70c_parse_target_status();if(sp70c_g_data_pack.target_num > 0){sp70c_parse_target_info();sp70c_print_data_pack();}else{memset(&sp70c_g_data_pack, 0, sizeof(sp70c_g_data_pack));}/*	开启DMA接收	*/sp70c_start_dma_receive();}
}/*** @brief  DMA 中断服务函数(通常是 HAL 默认封装,用户一般不需要修改)* @note   当 DMA 传输发生中断(如传输完成、错误等)时,硬件会跳转到此函数。*         此函数内部调用了 HAL 提供的默认 DMA 中断处理函数:HAL_DMA_IRQHandler(),*         它会处理中断标志、调用回调等。一般用户无需修改此函数。* * @retval void:无返回值*/
void SP70C_DMA_IRQHandler(void)
{HAL_DMA_IRQHandler(&sp70c_g_hdma);
}/*** @brief  SP70C 传感器模块初始化函数* @note   该函数用于初始化与 SP70C 通信相关的所有硬件和软件资源,包括:*         - 串口底层 GPIO 引脚初始化(TX/RX)*         - UART 外设初始化(波特率 115200,8N1,收发模式)*         - DMA 控制器初始化(用于 UART 接收)*         - 启动一次 DMA 异步接收,开始监听传感器数据* * @retval void:无返回值*/
void sp70c_init(void)
{printf("NOTE: sp70c init....\r\n");/*	1.串口底层引脚初始化	*/sp70c_uart_msp_init();/*	2.串口初始化	*/if(sp70c_uart_init() < 0){while(1);}/*	3.DMA初始化	*/if(sp70c_dma_init() < 0){while(1);}/*	4.启动DMA接收数据	*/if(sp70c_start_dma_receive() < 0){while(1);}printf("OK: sp70c init ok\r\n");
}/*** @brief  获取解析后的目标反射截面积(Radar Cross Section)* @note   该值由 sp70c_parse_target_info() 解析得到,单位可能是 dBsm* @retval float:目标 RCS 值*/
float sp70c_get_rcs(void)
{return sp70c_g_data_pack.target_info.rcs;
}/*** @brief  获取解析后的目标距离* @note   单位:米(m),由 sp70c_parse_target_info() 解析得到* @retval float:目标距离值*/
float sp70c_get_range(void)
{return sp70c_g_data_pack.target_info.range;
}/*** @brief  获取解析后的目标相对速度* @note   单位:米每秒(m/s),由 sp70c_parse_target_info() 解析得到* @retval float:目标相对速度值*/
float sp70c_get_vrel(void)
{return sp70c_g_data_pack.target_info.vrel;
}/*** @brief  获取解析后的目标方位角* @note   单位:度(°),由 sp70c_parse_target_info() 解析得到* @retval int:目标方位角值(可能是 int 类型,根据原代码中的 %d 打印推断)*/
int sp70c_get_azimuth(void)
{return sp70c_g_data_pack.target_info.azimuth;
}

6.结尾

这是我的学习记录笔记,仅供参考学习。

STM32HAL库--UART+DMA读取传感器

 

http://www.dtcms.com/a/323434.html

相关文章:

  • Tangram官网教程
  • Qt Graphics View框架概述
  • 夺宝奇兵 古老之圈 送修改器(The Great Circle)免安装中文版
  • openvela之STM32开发板部署
  • 力扣(轮转数组)
  • 智慧水务漏检率↓75%:陌讯水下视觉监测方案实战解析
  • 北京天津唐山廊坊沧州打捞日记
  • Nvidia 开源 KO 驱动 开发入门
  • 车流高峰漏检率↓85%!陌讯时序建模方案在智慧交通的实时优化​
  • AtCoder Beginner Contest 418
  • LLVM编译器入门
  • 力扣面试150(51/100)
  • 【Python 工具人快餐 · 第 2 份】
  • 使用SPM进行核磁数据预处理
  • 【无标题】六边形结构在二维拓扑量子色动力学模型中确实具有独特优势,并构建完整的二维拓扑量子色动力学模型。
  • Redis三种特殊数据类型
  • 【深度学习2】logistic回归以及梯度下降
  • synchronized和RentrantLock用哪个?
  • Datawhale AI夏令营第三期,多模态RAG方向 Task2
  • 小白成长之路-Docker部署
  • 第二十八天(cookiesessiontokeny验证)
  • JVM性能调优的原则有哪些?
  • 深入理解C++构造函数与初始化列表
  • P1025 [NOIP 2001 提高组] 数的划分 题解
  • 【嵌入式DIY实例-Arduino篇】-水质检测系统
  • SQL面试题及详细答案150道(01-20) --- 基础概念与语法篇
  • python踩坑之识别错误...
  • 如何分析需求的可行性
  • Spring——Spring懒加载设计使用场景
  • 深入解析进程创建与终止机制