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读取传感器

相关推荐
Hello_Embed44 分钟前
STM32HAL 快速入门(三):从 HAL 函数到寄存器操作 —— 理解 HAL 库的本质
c语言·stm32·单片机·嵌入式硬件·学习
学不动CV了1 小时前
FreeRTOS入门知识(初识RTOS任务调度)(三)
c语言·arm开发·stm32·单片机·物联网·算法·51单片机
Shang131130487917 小时前
ISL9V3040D3ST-F085C一款安森美 ON生产的汽车点火IGBT模块,绝缘栅双极型晶体管ISL9V3040D3ST汽车点火电路中的线圈驱动器
单片机·嵌入式硬件·安森美 on生产·汽车点火igbt模块·isl9v3040d3st
亿道电子Emdoor7 小时前
【ARM】MDK Debug模式下Disassembly窗口介绍
stm32·单片机·嵌入式硬件
点灯小铭7 小时前
基于STM32单片机的无线鼠标设计
stm32·单片机·计算机外设·毕业设计·课程设计
嵌入式×边缘AI:打怪升级日志10 小时前
【无标题】
单片机·嵌入式硬件
嵌入式小李11 小时前
stm32项目(24)——基于STM32的汽车CAN通信系统
stm32·嵌入式硬件·汽车
优信电子13 小时前
基于STM32F103驱动SI5351 3通道时钟信号发生器输出不同频率信号
单片机·嵌入式
酷飞飞1 天前
库函数版独立按键用位运算方式实现(STC8)
单片机·嵌入式硬件