目录
[3.1DMA的初始化函数 -- HAL_DMA_Init](#3.1DMA的初始化函数 -- HAL_DMA_Init)
[3.3DMA的中断处理函数 -- HAL_DMA_IRQHandler](#3.3DMA的中断处理函数 -- HAL_DMA_IRQHandler)
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.结尾
这是我的学习记录笔记,仅供参考学习。