如果是没有开启 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:
- DMA Transfer: The DMA moves data from your RAM to the UART Data Register.
- DMA Complete: When the DMA finishes moving the last byte, the DMA Interrupt fires (
DMA1_Channel4_IRQHandler). - 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."
- Enabling UART IT: Consequently, the HAL library enables the UART Transmission Complete (TC) Interrupt inside the DMA Interrupt handler.
- UART Complete: When the final bit leaves the TX pin, the UART Interrupt fires (
USART1_IRQHandler). - Cleanup: Only inside this UART interrupt does the HAL set the handle state to
Readyand call yourHAL_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).