【电路笔记 STM32】stm32f103 实现HAL_UART_Transmit_DMA + 普通模式/连续模式的 UART DMA 收发

前置准备 (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 : HighVery 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 */
}
相关推荐
wuyk5551 小时前
第 6 章 FOC 完整系统整合:从算法到真机跑起来
c语言·开发语言·stm32·单片机·嵌入式硬件
恒锐丰科技韩生2 小时前
率能 SS6810H|10‑32V 双通道 H 桥步进电机驱动 ETSSOP20 打印机安防舞台灯光专用
嵌入式硬件·硬件工程
156082072192 小时前
直流耦合采集卡前端电路调试
嵌入式硬件·fpga开发
北京海得康2 小时前
DMD基因治疗deramiocel遭FDA质疑,Ultragenyx DTX401即将获批
笔记
zjnlswd2 小时前
C#学习笔记2 刘铁锰
笔记·学习
Mr_liu_6662 小时前
Claude非专业入门实战笔记(4):Claude运行方式简单解析——从RAG到代理循环
笔记·ai编程·claude
凯尔萨厮2 小时前
Git(学习笔记)
笔记·git·学习
蒸蒸yyyyzwd3 小时前
cpp选手备注秋招 MySQL学习笔记 day8
笔记·学习·求职招聘
AI直播技术杂谈3 小时前
本周技术笔记:直播推流的心跳检测与断线重连逻辑
笔记