STM32串口和DMA

如果是没有开启 DMA 功能则可以参考: STM32串口通信实验

最大的区别是开启 DMA,如下图

然后由 STM32CubeMX 生成代码

usart.h中添加

/* USER CODE BEGIN Includes */

#include "stdio.h"
#include "stdint.h"

/* USER CODE END Includes */
...
/* USER CODE BEGIN Prototypes */

#define UART_TX_BUF_SIZE 256    // Size of the ring buffer

void UART_Printf_Init(UART_HandleTypeDef *huart);

/* USER CODE END Prototypes */
...

usart.c

/* USER CODE BEGIN 0 */

static UART_HandleTypeDef *debug_uart = NULL;

static uint8_t  uart_tx_buf[UART_TX_BUF_SIZE];
static volatile uint16_t uart_tx_head = 0;  // next write position
static volatile uint16_t uart_tx_tail = 0;  // next read position
static volatile uint8_t  uart_tx_busy = 0;  // DMA currently running?

static void UART_StartTxDMA(void)
{
    if (debug_uart == NULL) return;

    // Nothing to send?
    if (uart_tx_head == uart_tx_tail) {
        uart_tx_busy = 0;
        return;
    }

    uart_tx_busy = 1;

    uint16_t tail = uart_tx_tail;
    uint16_t head = uart_tx_head;
    uint16_t len;

    if (head > tail) {
        // Continuous region
        len = head - tail;
    } else {
        // Wrap-around: send from tail to end of buffer
        len = UART_TX_BUF_SIZE - tail;
    }

    if (HAL_UART_Transmit_DMA(debug_uart, &uart_tx_buf[tail], len) != HAL_OK)
    {
        // If DMA failed to start, clear the busy flag so we can try again
        uart_tx_busy = 0;
    }
}

static void UART_BufferWrite(const uint8_t *data, uint16_t len)
{
    for (uint16_t i = 0; i < len; i++) {
        uint16_t next_head = (uart_tx_head + 1) % UART_TX_BUF_SIZE;

        // If buffer full: either block, drop, or overwrite.
        // Here we simply block until space is available.
        while (next_head == uart_tx_tail) {
            // Buffer full – wait (simple version; could yield, etc.)
        }

        uart_tx_buf[uart_tx_head] = data[i];
        uart_tx_head = next_head;
    }

    // Start DMA if not running
    __disable_irq();               // short critical section
    if (!uart_tx_busy) {
        UART_StartTxDMA();
    }
    __enable_irq();
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == debug_uart) {
        uint16_t sent = huart->TxXferSize; // number of bytes just sent

        uart_tx_tail = (uart_tx_tail + sent) % UART_TX_BUF_SIZE;

        if (uart_tx_head != uart_tx_tail) {
            // More data pending – start next chunk
            UART_StartTxDMA();
        } else {
            uart_tx_busy = 0; // Buffer empty
        }
    }
}

void UART_Printf_Init(UART_HandleTypeDef *huart)
{
    debug_uart = huart;
}

int fputc(int ch, FILE *f)
{
    uint8_t c = (uint8_t)ch;

    if (debug_uart == NULL) {
        return ch;   // or EOF
    }

    UART_BufferWrite(&c, 1);
    return ch;
}

/* USER CODE END 0 */

main.c中添加

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_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  UART_Printf_Init(&huart1);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    printf("Hello DMA UART\r\n");
    HAL_Delay(1000);
  }
  /* USER CODE END 3 */
}

这里最大的疑惑是为什么使用 DMA 后还需要 UART 中断?最开始 GPT-5.1 给我的方案也是没有开启 UART 中断的,结果串口只接到一个字符 "H"。

后来问了 Gemini-3-Pro 后,它告诉我还是需要开启 UART 中断。以下的它的一些回应:

If you use the STM32 HAL library, the UART Interrupt is mandatory even if you are using DMA. 如果你使用 STM32 HAL 库,即使启用了 DMA,UART 中断也是必需的。

You might think: "If the DMA moves the data, why do I need the UART interrupt?" 你可能会想:"既然 DMA 负责移动数据,为什么还需要 UART 中断呢?"

This is a specific design choice in the STM32 HAL Library. Here is the sequence of events when you call HAL_UART_Transmit_DMA:

  1. DMA Transfer: The DMA moves data from your RAM to the UART Data Register.
  2. DMA Complete: When the DMA finishes moving the last byte, the DMA Interrupt fires (DMA1_Channel4_IRQHandler).
  3. The HAL Logic: Inside the DMA interrupt, the HAL library thinks: "Okay, I moved all the data to the UART, but the UART hardware is still shifting the last byte out onto the wire. I cannot say the transaction is 'Complete' yet."
  4. Enabling UART IT: Consequently, the HAL library enables the UART Transmission Complete (TC) Interrupt inside the DMA Interrupt handler.
  5. UART Complete: When the final bit leaves the TX pin, the UART Interrupt fires (USART1_IRQHandler).
  6. Cleanup: Only inside this UART interrupt does the HAL set the handle state to Ready and call your HAL_UART_TxCpltCallback.

If you do not enable USART1_IRQHandler:
The DMA finishes, enables the UART interrupt, and then... nothing happens. The CPU never jumps to the cleanup code. The UART handle remains in HAL_UART_STATE_BUSY_TX forever. That is why you only saw one "h" (the buffer filled up, sent once, and never cleared the busy flag).


参考资料:STM32 UART: Polling and DMA modes using HAL API