前置准备 (CubeMX 配置)
- USART 模式 : 选择
Asynchronous(异步)。

- DMA Settings :
- 点击
Add添加USARTx_TX(发送) 和USARTx_RX(接收)。 - Direction : TX 选
Memory to Peripheral,RX 选Peripheral to Memory。 - Mode : 通常选
Normal(单次) 或Circular(循环,用于连续接收)。- 普通模式,接收完成后 DMA 会停止,因此必须在接收完成回调函数中重新启动接收,否则只能收一次数据
- Data Width : 通常选
Byte(8位)。 - Priority :
High或Very High。
- 点击
- NVIC Settings : 勾选
USARTx global interrupt(如果需要接收完成中断) 和DMAx Stream/Channel global interrupt。

- 使能USART的全局中断():

普通模式 (Normal Mode)的 UART DMA 收发
- 由于是普通模式 ,接收完成后 DMA 会停止,因此必须在接收完成回调函数中重新启动接收,否则只能收一次数据。
- 循环模式不需要重启,但需要处理指针回绕;普通模式简单直接,收满即停,适合处理定长数据包。
LED (PC13) DMA 控制器 (TX) CPU (主循环) 内存 (aRxBuffer) DMA 控制器 (RX) 上位机/外部设备 LED (PC13) DMA 控制器 (TX) CPU (主循环) 内存 (aRxBuffer) DMA 控制器 (RX) 上位机/外部设备 #mermaid-svg-zNZ8n0MfAeyVlxKl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zNZ8n0MfAeyVlxKl .error-icon{fill:#552222;}#mermaid-svg-zNZ8n0MfAeyVlxKl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zNZ8n0MfAeyVlxKl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zNZ8n0MfAeyVlxKl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zNZ8n0MfAeyVlxKl .marker.cross{stroke:#333333;}#mermaid-svg-zNZ8n0MfAeyVlxKl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zNZ8n0MfAeyVlxKl p{margin:0;}#mermaid-svg-zNZ8n0MfAeyVlxKl .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zNZ8n0MfAeyVlxKl text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zNZ8n0MfAeyVlxKl .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zNZ8n0MfAeyVlxKl .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zNZ8n0MfAeyVlxKl #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zNZ8n0MfAeyVlxKl .sequenceNumber{fill:white;}#mermaid-svg-zNZ8n0MfAeyVlxKl #sequencenumber{fill:#333;}#mermaid-svg-zNZ8n0MfAeyVlxKl #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zNZ8n0MfAeyVlxKl .messageText{fill:#333;stroke:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zNZ8n0MfAeyVlxKl .labelText,#mermaid-svg-zNZ8n0MfAeyVlxKl .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .loopText,#mermaid-svg-zNZ8n0MfAeyVlxKl .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zNZ8n0MfAeyVlxKl .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zNZ8n0MfAeyVlxKl .noteText,#mermaid-svg-zNZ8n0MfAeyVlxKl .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zNZ8n0MfAeyVlxKl .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zNZ8n0MfAeyVlxKl .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zNZ8n0MfAeyVlxKl .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zNZ8n0MfAeyVlxKl .actorPopupMenu{position:absolute;}#mermaid-svg-zNZ8n0MfAeyVlxKl .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zNZ8n0MfAeyVlxKl .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zNZ8n0MfAeyVlxKl .actor-man circle,#mermaid-svg-zNZ8n0MfAeyVlxKl line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zNZ8n0MfAeyVlxKl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动首次 HAL_UART_Receive_DMA 收满 128 字节,触发中断 1. rxDataReady = 12. 重启 HAL_UART_Receive_DMA (准备下一包) loop主循环轮询 发完 128 字节,触发中断 发送 128 字节数据自动搬运数据 (无 CPU 参与)触发 HAL_UART_RxCpltCallback检测 rxDataReady == 1?调用 HAL_UART_Transmit_DMA读取 aRxBuffer 数据自动发送数据 (无 CPU 参与)触发 HAL_UART_TxCpltCallback翻转 LED 状态
添加全局变量和函数声明
- 在
/* Private variables */和/* Private function prototypes */区域添加缓冲区和回调声明。
c
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
DMA_HandleTypeDef hdma_usart2_tx;
/* USER CODE BEGIN PV */
// 定义接收和发送缓冲区 (大小根据需要调整)
#define RX_BUFFER_SIZE 128
#define TX_BUFFER_SIZE 128
uint8_t aRxBuffer[RX_BUFFER_SIZE];
uint8_t aTxBuffer[TX_BUFFER_SIZE];
volatile uint8_t rxDataReady = 0; // 标志位:数据接收完成
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
// 添加 DMA 回调函数声明
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END PFP */
配置 DMA 与 UART 关联
- 在
MX_USART2_UART_Init函数的末尾(/* USER CODE BEGIN USART2_Init 2 */处),将 DMA 句柄绑定到 UART 句柄上。
c
/**
* @brief USART2 Initialization Function
*/
static void MX_USART2_UART_Init(void)
{
// ... (原有的初始化代码) ...
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
// 【关键步骤】将 DMA 句柄关联到 UART 句柄
// 确保 hdma_usart2_rx 和 hdma_usart2_tx 已经在 MX_DMA_Init 或其他地方初始化过
huart2.hdmarx = &hdma_usart2_rx;
huart2.hdmatx = &hdma_usart2_tx;
/* USER CODE END USART2_Init 2 */
}
在 main 函数中启动接收
- 在
main函数的无限循环之前(或循环的第一行),启动第一次 DMA 接收。普通模式下,必须手动重启。
c
int main(void)
{
// ... (HAL_Init, SystemClock_Config, 外设初始化) ...
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
// 【关键步骤】启动第一次 DMA 接收
// 参数:句柄, 缓冲区地址, 数据长度
if(HAL_UART_Receive_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 主循环逻辑
// 检查是否接收到数据 (由回调函数置位)
if (rxDataReady == 1)
{
rxDataReady = 0; // 清除标志
// 示例:将收到的数据原样发回 (Echo)
// 注意:发送也会用 DMA,不会阻塞 CPU
HAL_UART_Transmit_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE);
// 在这里处理 aRxBuffer 中的数据...
}
}
/* USER CODE END 3 */
}
实现回调函数 (核心逻辑)
- 当 DMA 接收完指定长度的数据后,硬件会触发中断,进入
HAL_UART_RxCpltCallback。你必须在这里再次调用HAL_UART_Receive_DMA,否则系统无法接收下一帧数据。
c
/* USER CODE BEGIN 4 */
/**
* @brief Rx Transfer completed callback
* @param huart: UART handle
* @retval None
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
// 1. 置位标志,通知主循环处理数据
rxDataReady = 1;
// 2. 【至关重要】普通模式下,DMA 传输一次后会自动关闭。
// 必须在这里重新启动 DMA 接收,以便接收下一批数据。
// 如果不调用这一行,只能接收一次数据,之后串口就"死"了。
HAL_UART_Receive_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE);
}
}
/**
* @brief Tx Transfer completed callback
* @param huart: UART handle
* @retval None
*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
// 发送完成后的处理逻辑
// 例如:切换 GPIO 指示灯,或者准备下一个发送包
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
/* USER CODE END 4 */
可选优化:双缓冲区示例 (防止数据覆盖)
- 如果数据处理速度慢于接收速度,上面的单缓冲区会导致新数据覆盖未处理的旧数据。改进方案:
- 在主循环处理
aRxBuffer时,DMA 可能正在填充新的数据(如果是双缓冲机制会更安全)。对于简单的 Echo 应用,上述代码足够;如果数据处理时间长,建议使用双缓冲区(Ping-Pong Buffer)策略,即在回调中切换到另一个缓冲区启动 DMA,让主循环处理旧缓冲区的数据。
- 在主循环处理
c
// 定义两个缓冲区
uint8_t aRxBuffer0[RX_BUFFER_SIZE];
uint8_t aRxBuffer1[RX_BUFFER_SIZE];
volatile uint8_t currentRxBuffer = 0; // 0 表示 Buffer0 就绪,1 表示 Buffer1 就绪
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
if (currentRxBuffer == 0) {
rxDataReady = 1; // 通知处理 Buffer0
currentRxBuffer = 1; // 下次切换到 Buffer1
// 启动 DMA 接收至 Buffer1
HAL_UART_Receive_DMA(&huart2, aRxBuffer1, RX_BUFFER_SIZE);
} else {
rxDataReady = 2; // 通知处理 Buffer1 (用不同值区分)
currentRxBuffer = 0; // 下次切换到 Buffer0
// 启动 DMA 接收至 Buffer0
HAL_UART_Receive_DMA(&huart2, aRxBuffer0, RX_BUFFER_SIZE);
}
}
}
- 在主循环中根据
rxDataReady的值决定处理aRxBuffer0还是aRxBuffer1。
完整代码
c
/* USER CODE BEGIN Header */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
DMA_HandleTypeDef hdma_usart2_tx;
/* USER CODE BEGIN PV */
// 定义接收和发送缓冲区 (大小根据需要调整)
#define RX_BUFFER_SIZE 128
#define TX_BUFFER_SIZE 128
uint8_t aRxBuffer[RX_BUFFER_SIZE];
uint8_t aTxBuffer[TX_BUFFER_SIZE];
volatile uint8_t rxDataReady = 0; // 标志位:数据接收完成
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */
// 添加 DMA 回调函数声明
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
// 【关键步骤】启动第一次 DMA 接收
// 参数:句柄, 缓冲区地址, 数据长度
if(HAL_UART_Receive_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 主循环逻辑
// 检查是否接收到数据 (由回调函数置位)
if (rxDataReady == 1)
{
rxDataReady = 0; // 清除标志
// 示例:将收到的数据原样发回 (Echo)
// 注意:发送也会用 DMA,不会阻塞 CPU
HAL_UART_Transmit_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE);
// 在这里处理 aRxBuffer 中的数据...
}
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief USART2 Initialization Function
* @param None
* @retval None
*/
static void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 0 */
/* USER CODE END USART2_Init 0 */
/* USER CODE BEGIN USART2_Init 1 */
/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
// 【关键步骤】将 DMA 句柄关联到 UART 句柄
// 确保 hdma_usart2_rx 和 hdma_usart2_tx 已经在 MX_DMA_Init 或其他地方初始化过
huart2.hdmarx = &hdma_usart2_rx;
huart2.hdmatx = &hdma_usart2_tx;
/* USER CODE END USART2_Init 2 */
}
/**
* Enable DMA controller clock
*/
static void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA interrupt init */
/* DMA1_Channel6_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);
/* DMA1_Channel7_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn);
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
/*Configure GPIO pin : PC13 */
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
/* USER CODE BEGIN 4 */
/**
* @brief Rx Transfer completed callback
* @param huart: UART handle
* @retval None
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
// 1. 置位标志,通知主循环处理数据
rxDataReady = 1;
// 2. 【至关重要】普通模式下,DMA 传输一次后会自动关闭。
// 必须在这里重新启动 DMA 接收,以便接收下一批数据。
// 如果不调用这一行,只能接收一次数据,之后串口就"死"了。
HAL_UART_Receive_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE);
}
}
/**
* @brief Tx Transfer completed callback
* @param huart: UART handle
* @retval None
*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
// 发送完成后的处理逻辑
// 例如:切换 GPIO 指示灯,或者准备下一个发送包
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
运行效果

连续模式(RX Circular Mode)的 UART DMA 收发
STM32CubeMX配置
- 配置TX Normal + RX Circular 实现 "连续接收不定长数据 + 按需发送" :
| 配置项 | TX (发送) | RX (接收) | 解释 |
|---|---|---|---|
| DMA Request | USARTx_TX |
USARTx_RX |
选择对应的通道请求 |
| Direction (方向) | Memory to Peripheral(内存 → 外设) | Peripheral to Memory(外设 → 内存) | **最关键的区别!**TX 是把 RAM 里的数据搬到串口寄存器发出去;RX 是把串口寄存器收到的数据搬到 RAM 里。 |
| Mode (模式) | Normal (普通) |
Circular (循环) |
TX : 通常用 Normal,发完即停。RX: 推荐 Circular (配合 IDLE 中断),实现连续接收不丢失。 |
| Data Width | Byte |
Byte |
串口通常是 8 位数据,所以选 Byte。 |
| Priority | Low / Medium |
High / Very High |
RX 优先级建议更高,防止接收缓冲区溢出导致丢包。TX 稍微慢一点通常没关系。 |
修改 main.c 全局变量
我们需要一个变量来记录"上次读到哪里了",以便计算新数据的长度。
c
/* USER CODE BEGIN PV */
#define RX_BUFFER_SIZE 256
uint8_t aRxBuffer[RX_BUFFER_SIZE];
volatile uint16_t rxDataLen = 0; // 本次接收到的数据长度
volatile uint8_t rxFrameReady = 0; // 帧接收完成标志
uint16_t lastRxPos = 0; // 上一次处理时的 DMA 位置
/* USER CODE END PV */
修改 main 函数初始化部分
- 关键点 :
- 开启 IDLE 中断(用于检测一帧数据结束)。
- 只启动一次 RX DMA(因为是 Circular 模式,它会自动转圈,不需要重启)。
c
int main(void)
{
// ... (HAL_Init, SystemClock_Config, 外设初始化) ...
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
// 1. 确保 DMA 已关联 (通常在 MX_USART2_UART_Init 中已完成,这里双重保险)
huart2.hdmarx = &hdma_usart2_rx;
huart2.hdmatx = &hdma_usart2_tx;
// 2. 【关键】开启 UART 空闲中断 (IDLE IE)
// 这是检测 Circular 模式下"一帧数据结束"的唯一可靠方法
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
// 3. 【关键】启动 DMA 接收 (只需调用一次!)
// 因为配置了 Circular 模式,DMA 会一直在缓冲区转圈写入
if(HAL_UART_Receive_DMA(&huart2, aRxBuffer, RX_BUFFER_SIZE) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
while (1)
{
/* USER CODE BEGIN WHILE */
// 检查是否收到新的一帧数据
if (rxFrameReady == 1)
{
rxFrameReady = 0; // 清除标志
// --- 处理数据逻辑 ---
// 此时 aRxBuffer 中包含新数据
// 注意:如果是 Circular 模式且数据跨越了缓冲区末尾,需要特殊处理(见下文提示)
// 简单场景下(数据未绕回),直接发送回去
HAL_UART_Transmit_DMA(&huart2, aRxBuffer, rxDataLen);
// 更新上次位置,为下一次计算做准备
lastRxPos = (lastRxPos + rxDataLen) % RX_BUFFER_SIZE;
}
/* USER CODE END WHILE */
}
}
修改中断处理函数 (stm32f1xx_it.c)
- 需要拦截
USART2_IRQHandler,检测 IDLE 标志,并计算数据长度。不要 在这里调用HAL_UART_Receive_DMA。
c
/* USER CODE BEGIN 0 */
// 引入外部变量 (如果在 main.c 定义)
extern UART_HandleTypeDef huart2;
extern volatile uint16_t rxDataLen;
extern volatile uint8_t rxFrameReady;
extern uint8_t aRxBuffer[];
extern uint16_t lastRxPos;
#define RX_BUFFER_SIZE 256
/* USER CODE END 0 */
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
// 1. 检查是否是 IDLE 中断 (总线空闲,表示一帧数据发完了)
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) != RESET)
{
// 2. 【必须】清除 IDLE 标志
// F1 系列清除方法:先读 SR,再读 DR
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
// 3. 获取当前 DMA 的位置
// 公式:总长度 - 剩余计数器 = 已写入位置
uint16_t current_pos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
// 4. 计算本帧数据长度
if (current_pos >= lastRxPos)
{
// 情况 A: 数据没有绕回 (正常情况)
rxDataLen = current_pos - lastRxPos;
}
else
{
// 情况 B: 数据绕回了 (DMA 指针回到了头部)
// 长度 = (尾部到结尾) + (开头到当前位置)
rxDataLen = (RX_BUFFER_SIZE - lastRxPos) + current_pos;
}
// 5. 更新上次位置
lastRxPos = current_pos;
// 6. 置位标志,通知主循环处理
// 只有当确实有数据时才置位,避免空帧干扰
if (rxDataLen > 0)
{
rxFrameReady = 1;
}
// 【重要】不需要调用 HAL_UART_Receive_DMA!
// 因为是 Circular 模式,DMA 正在后台持续运行。
}
// 调用 HAL 库默认处理 (处理其他中断如错误中断)
HAL_UART_IRQHandler(&huart2);
/* USER CODE END USART2_IRQn 0 */
}