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.1DMA的初始化函数 -- HAL_DMA_Init)

3.2DMA的时钟使能和失能

[3.3DMA的中断处理函数 -- HAL_DMA_IRQHandler](#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字节对齐) 0x20000000, 0x20000001 无(所有地址合法) 无问题
16位 (Half-Word) 🚗 轿车 2字节对齐(偶数地址) 0x20000000, 0x20000002 0x20000001, 0x20000003 BusFault 或数据截断
32位 (Word) 🚛 大卡车 4字节对齐(4的倍数地址) 0x20000000, 0x20000004 0x20000001, 0x20000002, 0x20000003 数据错乱 / BusFault / 传输失败

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

2.5DMA的指针递增

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

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

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

3.HAL库中常用的DMA函数

3.1DMA的初始化函数 -- HAL_DMA_Init

cpp 复制代码
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

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

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

参数:DMA_HandleTypeDef *hdma。

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

成员变量 描述 常用取值(HAL库宏)
Instance DMA通道实例(如DMA1_Channel1 DMA1_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.Mode DMA传输模式 DMA_NORMAL(单次传输) DMA_CIRCULAR(循环传输)
Init.Priority 通道优先级 DMA_PRIORITY_LOW DMA_PRIORITY_MEDIUM DMA_PRIORITY_HIGH DMA_PRIORITY_VERY_HIGH

3.2DMA的时钟使能和失能

通过宏函数控制:

cpp 复制代码
__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

cpp 复制代码
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);

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

参数介绍过了。

3.4DMA与外设的链接宏函数

cpp 复制代码
__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

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

cpp 复制代码
#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等,因此在这个函数中要完成所用到的资源进行初始化。如以下代码所示:

cpp 复制代码
/**
  * @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的串口初始化

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

cpp 复制代码
/**
  * @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 外设,比如 UART4
    sp70c_g_huart.Init.BaudRate = 115200;                   // 波特率:115200
    sp70c_g_huart.Init.WordLength = UART_WORDLENGTH_8B;     // 数据位:8 bit
    sp70c_g_huart.Init.StopBits = UART_STOPBITS_1;          // 停止位:1 bit
    sp70c_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中断,与外设链接,编写中断函数。

cpp 复制代码
/**
  * @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()函数。

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

cpp 复制代码
/**
  * @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

cpp 复制代码
#include "stdio.h"
#include "string.h"

#include "sp70c.h"


/*	数据类型定义	*/
typedef struct
{
	float rcs;						//目标反射截面积
	float range;					//目标距离,单位,m
	float vrel;						//目标速度,单位,m/s
	int 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 外设,比如 UART4
    sp70c_g_huart.Init.BaudRate = 115200;                   // 波特率:115200
    sp70c_g_huart.Init.WordLength = UART_WORDLENGTH_8B;     // 数据位:8 bit
    sp70c_g_huart.Init.StopBits = UART_STOPBITS_1;          // 停止位:1 bit
    sp70c_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读取传感器

相关推荐
点灯小铭9 小时前
基于单片机的多路热电偶温度监测与报警器
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
tianyue10014 小时前
STM32G431 ADC 多个channel 采集
stm32·单片机·嵌入式硬件
清风66666615 小时前
基于单片机的水泵效率温差法测量与报警系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
z203483152017 小时前
定时器练习报告
单片机·嵌入式硬件
zk0017 小时前
内容分类目录
单片机·嵌入式硬件
安生生申17 小时前
STM32 ESP8266连接ONENET
c语言·stm32·单片机·嵌入式硬件·esp8266
广药门徒17 小时前
电子器件烧毁的底层逻辑与避坑指南
单片机·嵌入式硬件
点灯小铭1 天前
基于单片机的社区医院小型高压蒸汽灭菌自动控制器设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
youcans_1 天前
【动手学STM32G4】(3)STM32G431之定时器
stm32·单片机·嵌入式硬件·定时器
悠哉悠哉愿意1 天前
【嵌入式学习笔记】AD/DA
笔记·单片机·嵌入式硬件·学习