betaflight多串口环形缓冲区和dma实现原理

Betaflight 串口 UART 深度解析:环形缓冲区与 DMA 实现

本文基于 Betaflight 源码,重点讲解串口系统的环形缓冲区原理和 DMA/中断 两种工作模式。


一、串口系统整体架构

1.1 四层架构

Betaflight 的串口系统采用四层分离架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────┐
│ 应用层 - io/serial │
│ openSerialPort / closeSerialPort / serialInit │
└─────────────────────────┬───────────────────────────────┘
│ 调用

┌─────────────────────────────────────────────────────────┐
│ 驱动抽象层 - drivers/serial │
│ serialPort_t(通用串口结构体) │
│ serialPortVTable(虚函数表:serialWrite/serialRead...) │
└─────────────────────────┬───────────────────────────────┘
│ 继承 + 实现

┌─────────────────────────────────────────────────────────┐
│ UART实现层 - drivers/serial_uart │
│ uartPort_t(UART扩展结构体) │
│ uartVTable(UART实现) │
│ uartIrqHandler(中断处理函数) │
└─────────────────────────┬───────────────────────────────┘
│ 调用硬件操作

┌─────────────────────────────────────────────────────────┐
│ 平台层 - platform/STM32 │
│ uartHardware(硬件描述表) │
│ uartReconfigure(寄存器配置) │
│ uartTryStartTxDMA(DMA启动) │
└─────────────────────────────────────────────────────────┘

1.2 各层职责

层级 目录 核心文件 职责
应用层 src/main/io/ serial.c serial.h 串口生命周期管理、功能分配
驱动抽象层 src/main/drivers/ serial.c serial.h 定义串口抽象接口和虚函数表
UART 实现层 src/main/drivers/ serial_uart.c serial_uart.h UART 协议实现、中断处理、DMA 管理
平台层 src/platform/STM32/ serial_uart_stdperiph.c serial_uart_stm32f4xx.c STM32 寄存器操作、硬件配置表

二、环形缓冲区(Ring Buffer)详解

2.1 什么是环形缓冲区?

环形缓冲区(也叫循环缓冲区)是一块固定大小的内存,用来暂存数据。它的特点是:

  • 写数据读数据可以同时进行,互不干扰
  • 写满后自动回到开头继续写("环形"的含义)
  • 读数据不会删除缓冲区里的内容,只是移动读取位置

2.2 为什么需要环形缓冲区?

串口通信有两个特点:

  1. 速率不匹配:CPU 可以一次产生大量数据,但串口只能一个一个字节地发送
  2. 异步性:数据到达的时间不可预测,CPU 可能正在忙其他事情

如果没有缓冲区,CPU 就必须等待每个字节发送完才能做下一件事——这太浪费了。

环形缓冲区解决了这个问题:它把"生产数据"和"消费数据"两个过程解耦。

2.3 生活类比

想象一个奶茶店

  • 生产者(CPU/应用层) = 做奶茶的师傅,做好一杯就放在柜台上
  • 环形缓冲区 = 柜台上的托盘,可以放多杯奶茶
  • 消费者(串口/DMA) = 外卖员,一次拿一杯送走
1
2
3
4
5
6
7
师傅做奶茶(生产)        托盘(缓冲区)         外卖员送走(消费)
│ │ │
├── 做好第1杯 ──────→ [第1杯] ←─────── 拿走第1
├── 做好第2杯 ──────→ [第1杯][第2杯]
├── 做好第3杯 ──────→ [第1杯][第2杯][第3杯] ←── 拿走第2
│ │
└── 师傅不用等外卖员回来,继续做第4杯 外卖员也不用等师傅做好

关键点:师傅和外卖员各干各的,互不等待。

2.4 环形缓冲区的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 环形缓冲区用两个指针(或索引)来管理:
//
// 缓冲区(以 TX 为例):
// ┌────┬────┬────┬────┬────┬────┬────┬────┐
// │ │ │ D5 │ D6 │ D7 │ │ │ │
// └────┴────┴────┴────┴────┴────┴────┴────┘
// ↑ ↑
// tail head
//
// head:下一个要写入的位置(生产者用)
// tail:下一个要读取的位置(消费者用)
//
// 规则:
// - head == tail → 缓冲区为空
// - (head + 1) % size == tail → 缓冲区满(留一个空位区分空和满)

2.5 环形缓冲区核心操作

写入数据(生产者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void uartWrite(serialPort_t *instance, uint8_t ch)
{
uartPort_t *uartPort = (uartPort_t *)instance;

// 第1步:把数据放到 head 指向的位置
uartPort->port.txBuffer[uartPort->port.txBufferHead] = ch;

// 第2步:head 向前移动一位(如果到末尾就回到开头)
if (uartPort->port.txBufferHead + 1 >= uartPort->port.txBufferSize) {
uartPort->port.txBufferHead = 0; // 回绕到开头
} else {
uartPort->port.txBufferHead++;
}

// 第3步:触发实际发送(DMA 或中断)
// 注意:这里只是"触发",不等待发送完成
#ifdef USE_DMA
if (uartPort->txDMAResource) {
uartTryStartTxDMA(uartPort); // 启动 DMA 传输
} else
#endif
{
uartEnableTxInterrupt(uartPort); // 使能发送中断
}
// ← 函数立即返回,CPU 可以继续做其他事
}

写入过程图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
初始状态(空缓冲区):
head = 0, tail = 0
┌────┬────┬────┬────┬────┐
│ │ │ │ │ │
└────┴────┴────┴────┴────┘

head/tail

写入一个字节 'A'
txBuffer[0] = 'A'
head = 1
┌────┬────┬────┬────┬────┐
│ A │ │ │ │ │
└────┴────┴────┴────┴────┘
↑ ↑
tail head

再写入 'B''C'
head = 3
┌────┬────┬────┬────┬────┐
│ A │ B │ C │ │ │
└────┴────┴────┴────┴────┘
↑ ↑
tail head

消费者取走 'A'
tail = 1
┌────┬────┬────┬────┬────┐
│ A │ B │ C │ │ │
└────┴────┴────┴────┴────┘
↑ ↑
tail head
'A' 还在内存里,但 tail 已经越过它,下次会被覆盖)

继续写入 'D''E'
head = 0(回绕到开头!)
┌────┬────┬────┬────┬────┐
│ E │ B │ C │ D │ │
└────┴────┴────┴────┴────┘
↑ ↑
head tail

注意:head 已经追上了 tail,但 (head+1)%size == tail
所以缓冲区实际上是满的,不能再写了

读取数据(消费者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static uint8_t uartRead(serialPort_t *instance)
{
uint8_t ch;
uartPort_t *uartPort = (uartPort_t *)instance;

#ifdef USE_DMA
if (uartPort->rxDMAResource) {
// DMA 模式:从缓冲区末尾倒数的位置读取
ch = uartPort->port.rxBuffer[uartPort->port.rxBufferSize - uartPort->rxDMAPos];
if (--uartPort->rxDMAPos == 0)
uartPort->rxDMAPos = uartPort->port.rxBufferSize;
} else
#endif
{
// 中断模式:从 tail 位置读取
ch = uartPort->port.rxBuffer[uartPort->port.rxBufferTail];
uartPort->port.rxBufferTail = (uartPort->port.rxBufferTail + 1) % uartPort->port.rxBufferSize;
}

return ch;
}

2.6 缓冲区状态判断

判断有多少字节可读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static uint32_t uartTotalRxBytesWaiting(const serialPort_t *instance)
{
const uartPort_t *uartPort = (const uartPort_t*)instance;

#ifdef USE_DMA
if (uartPort->rxDMAResource) {
// DMA 模式:通过 DMA 当前计数位置计算
uint32_t rxDMAHead = dmaGetDataLength(uartPort->rxDMAResource);
if (uartPort->rxDMAPos >= rxDMAHead) {
return uartPort->rxDMAPos - rxDMAHead;
} else {
return uartPort->port.rxBufferSize + uartPort->rxDMAPos - rxDMAHead;
}
}
#endif
// 中断模式:head - tail(环形缓冲区标准算法)
if (uartPort->port.rxBufferHead >= uartPort->port.rxBufferTail) {
return uartPort->port.rxBufferHead - uartPort->port.rxBufferTail;
} else {
return uartPort->port.rxBufferSize + uartPort->port.rxBufferHead - uartPort->port.rxBufferTail;
}
}

图解可读字节数计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
情况1head >= tail(正常)
┌────┬────┬────┬────┬────┐
│ │ A │ B │ C │ │
└────┴────┴────┴────┴────┘
↑ ↑
tail head
可读字节数 = head - tail = 3 - 1 = 2

情况2head < tail(回绕)
┌────┬────┬────┬────┬────┐
│ D │ E │ │ B │ C │
└────┴────┴────┴────┴────┘
↑ ↑
head tail
可读字节数 = size + head - tail = 5 + 1 - 3 = 3

判断缓冲区还有多少空位可写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static uint32_t uartTotalTxBytesFree(const serialPort_t *instance)
{
const uartPort_t *uartPort = (const uartPort_t*)instance;

// 计算已使用的字节数
uint32_t bytesUsed;
if (uartPort->port.txBufferHead >= uartPort->port.txBufferTail) {
bytesUsed = uartPort->port.txBufferHead - uartPort->port.txBufferTail;
} else {
bytesUsed = uartPort->port.txBufferSize + uartPort->port.txBufferHead - uartPort->port.txBufferTail;
}

#ifdef USE_DMA
if (uartPort->txDMAResource) {
// DMA 模式:还要加上正在 DMA 传输中但尚未完成的字节
bytesUsed += dmaGetDataLength(uartPort->txDMAResource);
if (bytesUsed >= uartPort->port.txBufferSize - 1) {
return 0; // 缓冲区满
}
}
#endif

return (uartPort->port.txBufferSize - 1) - bytesUsed;
}

2.7 环形缓冲区要点总结

要点 说明
大小必须是 2 的幂 方便用 & (size-1) 代替 % size 做回绕运算
留一个空位 (head+1)%size == tail 判断满,而不是 head == tail(那是空)
volatile 关键字 缓冲区的 head/tail 会被中断和任务同时访问,需要 volatile 防止编译器优化
生产者和消费者解耦 写数据的人不用等读数据的人,反之亦然
DMA 额外计算 DMA 模式下,已用字节数还要加上正在 DMA 传输中的字节

三、两种工作模式:DMA 模式 vs 中断模式

3.1 模式选择

每个 UART 可以独立选择 DMA 模式或中断模式:

1
2
3
4
// target.h 中定义
#define USE_UART1_TX_DMA // 使能 UART1 的 TX DMA
#define USE_UART1_RX_DMA // 使能 UART1 的 RX DMA
// 如果不定义,则使用中断模式

3.2 发送模式对比

特性 DMA 模式 中断模式
CPU 负载 极低(DMA 硬件自动传输) 中等(每字节触发一次中断)
吞吐量 高(适合大批量数据) 中(适合小数据量)
实时性 好(DMA 独立于 CPU) 受中断响应时间影响
适用场景 MSP、黑盒、GPS 等大数据量 CRSF、SBUS 等小数据量协议

3.3 发送流程对比

DMA 模式发送流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
应用层 → uartWrite(写入数据到环形缓冲区) → 立即返回


uartTryStartTxDMA(启动DMA)


DMA控制器自动从内存读取数据


USART外设逐字节发送


DMA传输完成 → 中断通知CPU


CPU检查是否有更多数据 → 再次启动DMA

中断模式发送流程:

1
2
3
4
5
6
7
8
9
10
11
12
应用层 → uartWrite(写入数据到环形缓冲区) → 立即返回


使能TXE中断


TXE中断触发 → CPU发送一个字节


CPU检查缓冲区是否还有数据
├── 有数据 → 继续发送下一个字节
└── 无数据 → 关闭TXE中断

3.4 DMA 发送实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// serial_uart_stdperiph.c
void uartTryStartTxDMA(uartPort_t *s)
{
// ATOMIC_BLOCK 防止中断竞争
ATOMIC_BLOCK(NVIC_PRIO_SERIALUART_TXDMA) {
if (IS_DMA_ENABLED(s->txDMAResource)) {
return; // DMA 正在传输中
}

if (s->port.txBufferHead == s->port.txBufferTail) {
s->txDMAEmpty = true; // 没有数据要发送
return;
}

// 计算要发送的数据块
unsigned chunk;
if (s->port.txBufferHead > s->port.txBufferTail) {
chunk = s->port.txBufferHead - s->port.txBufferTail;
s->port.txBufferTail = s->port.txBufferHead;
} else {
chunk = s->port.txBufferSize - s->port.txBufferTail;
s->port.txBufferTail = 0;
}

// 配置 DMA 传输
xDMA_MemoryTargetConfig(s->txDMAResource,
(uint32_t)&s->port.txBuffer[s->port.txBufferTail], DMA_Memory_0);
s->txDMAEmpty = false;
xDMA_SetCurrDataCounter(s->txDMAResource, chunk);
xDMA_Cmd(s->txDMAResource, ENABLE); // 启动 DMA
}
}

3.5 中断模式发送实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// serial_uart_stm32f4xx.c
void uartIrqHandler(uartPort_t *s)
{
USART_TypeDef *USARTx = (USART_TypeDef *)s->USARTx;

// TXE 中断:可以发送下一个字节
if (!s->txDMAResource && (USART_GetITStatus(USARTx, USART_IT_TXE) == SET)) {
if (s->port.txBufferTail != s->port.txBufferHead) {
// 还有数据要发送
USART_SendData(USARTx, s->port.txBuffer[s->port.txBufferTail]);
s->port.txBufferTail = (s->port.txBufferTail + 1) % s->port.txBufferSize;
} else {
// 没有数据了,关闭 TXE 中断
USART_ITConfig(USARTx, USART_IT_TXE, DISABLE);
}
}

// TC 中断:发送完成
if (USART_GetITStatus(USARTx, USART_IT_TC) == SET) {
uartTxMonitor(s); // 切换到 TX 引脚监控模式
USART_ClearITPendingBit(USARTx, USART_IT_TC);
}
}

3.6 接收模式对比

特性 DMA 模式(循环) 中断模式
CPU 负载 几乎为零 每字节一次中断
数据丢失风险 极低(硬件自动存储) 高负载下可能丢失
配置 DMA_Mode_Circular 循环模式 USART_IT_RXNE 中断
读取方式 通过 rxDMAPos 计算位置 rxBufferTail 读取

3.7 DMA 循环接收原理

1
2
3
4
5
6
7
8
9
// serial_uart_stdperiph.c - RX DMA 配置
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式!
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)uartPort->port.rxBuffer;
DMA_InitStructure.DMA_BufferSize = uartPort->port.rxBufferSize;

xDMA_Init(uartPort->rxDMAResource, &DMA_InitStructure);
xDMA_Cmd(uartPort->rxDMAResource, ENABLE); // 使能后持续运行
USART_DMACmd(USARTx, USART_DMAReq_Rx, ENABLE); // 连接 USART 和 DMA

关键点DMA_Mode_Circular 使 DMA 在到达缓冲区末尾后自动回绕到开头,永远不需要重新配置。每个接收到的字节自动存入缓冲区,CPU 完全不需要干预。

3.8 DMA 接收位置计算

DMA 的计数器是递减的(从 rxBufferSize 递减到 0),所以位置计算是:

1
2
DMA 当前位置 = rxBufferSize - DMA_GetCurrDataCounter()
已接收字节数 = rxDMAPos - DMA_GetCurrDataCounter() // 考虑回绕
1
2
3
4
5
6
// serial_uart.c
if (uartPort->rxDMAPos >= rxDMAHead) {
return uartPort->rxDMAPos - rxDMAHead; // 正常情况
} else {
return uartPort->port.rxBufferSize + uartPort->rxDMAPos - rxDMAHead; // 回绕
}

四、多串口并发机制

4.1 独立资源

每个 UART 拥有完全独立的资源:

资源 说明
环形缓冲区 每个 UART 独立的 TX/RX 缓冲区(256 字节)
DMA 通道 每个 UART 独立的 TX/RX DMA 通道和流
中断向量 每个 UART 独立的中断服务函数
NVIC 优先级 每个 UART 可独立配置中断优先级
USART 外设 每个 UART 对应独立的 USART 硬件外设

4.2 STM32F405 的 UART 硬件资源表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// serial_uart_stm32f4xx.c
const uartHardware_t uartHardware[UARTDEV_COUNT] = {
#ifdef USE_UART1
{
.identifier = SERIAL_PORT_USART1,
.reg = (usartResource_t *)USART1,
.rxDMAChannel = DMA_Channel_4,
.txDMAChannel = DMA_Channel_4,
.rxDMAResource = (dmaResource_t *)DMA2_Stream5, // RX DMA:DMA2 流 5
.txDMAResource = (dmaResource_t *)DMA2_Stream7, // TX DMA:DMA2 流 7
.rxPins = { PA10, PB7 },
.txPins = { PA9, PB6 },
.af = GPIO_AF_USART1,
.rcc = RCC_APB2(USART1),
.irqn = USART1_IRQn,
.txPriority = NVIC_PRIO_SERIALUART1_TXDMA,
.rxPriority = NVIC_PRIO_SERIALUART1,
.txBuffer = uart1TxBuffer,
.rxBuffer = uart1RxBuffer,
.txBufferSize = sizeof(uart1TxBuffer),
.rxBufferSize = sizeof(uart1RxBuffer),
},
#endif
// UART2 ~ UART6 类似定义...
};

4.3 中断优先级分层

1
2
3
4
5
6
7
8
9
// nvic.h - 每个串口有 3 个优先级
// TX DMA > RX DMA > 中断模式
#define NVIC_PRIO_SERIALUART1_TXDMA NVIC_BUILD_PRIORITY(1, 1) // UART1 TX DMA
#define NVIC_PRIO_SERIALUART1_RXDMA NVIC_BUILD_PRIORITY(1, 1) // UART1 RX DMA
#define NVIC_PRIO_SERIALUART1 NVIC_BUILD_PRIORITY(1, 2) // UART1 中断模式

#define NVIC_PRIO_SERIALUART2_TXDMA NVIC_BUILD_PRIORITY(1, 0) // UART2 TX DMA(更高)
#define NVIC_PRIO_SERIALUART2_RXDMA NVIC_BUILD_PRIORITY(1, 1)
#define NVIC_PRIO_SERIALUART2 NVIC_BUILD_PRIORITY(1, 2)

优先级设计原则(数值越小优先级越高):

1
2
3
4
5
6
7
8
9
10
11
12
13
高优先级 ←────────────────────────────────────────→ 低优先级
NVIC_PRIO_MPU_INT_EXTI (0,0) ← 陀螺仪中断(最高!)
NVIC_PRIO_SPI_DMA (0,0) ← SPI DMA
NVIC_PRIO_SDIO_DMA (0,0) ← SDIO DMA
NVIC_PRIO_I2C_ER/EV (0,0) ← I2C 中断
NVIC_PRIO_SERIALUARTx_TXDMA (1,0) ← 各 UART TX DMA(最高 UART 优先级)
NVIC_PRIO_SERIALUARTx_RXDMA (1,1) ← 各 UART RX DMA
NVIC_PRIO_SERIALUART1_TXDMA (1,1) ← UART1 TX DMA(特殊)
NVIC_PRIO_WS2811_DMA (1,2) ← WS2812 DMA
NVIC_PRIO_SERIALUARTx (1,2) ← 各 UART 中断模式
NVIC_PRIO_TIMER (1,1) ← 定时器
NVIC_PRIO_USB (2,0) ← USB
NVIC_PRIO_DSHOT_DMA (2,1) ← DShot DMA

4.4 多串口并发数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
应用层同时向 UART1 和 UART2 写入数据:

应用层

├── uartWrite(UART1, data1) → 写入 UART1 环形缓冲区 → 立即返回
│ │
│ ▼
uartTryStartTxDMA(UART1)
│ │
└── uartWrite(UART2, data2) → 写入 UART2 环形缓冲区 → 立即返回


uartTryStartTxDMA(UART2)

两个 DMA 通道独立并行运行:
DMA1 (UART1) ──→ USART1 外设 ──→ TX1 引脚
DMA2 (UART2) ──→ USART2 外设 ──→ TX2 引脚

CPU 在两个 DMA 传输期间可以自由处理其他任务(PID、陀螺仪等)

传输完成后:
DMA1 → 中断 → CPU 检查 UART1 缓冲区是否有更多数据
DMA2 → 中断 → CPU 检查 UART2 缓冲区是否有更多数据

4.5 ATOMIC_BLOCK 防竞争

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// uartTryStartTxDMA 可能被两个上下文调用:
// 1. uartWrite(任务上下文)
// 2. uartDmaIrqHandler(中断上下文)
// 所以需要原子保护

void uartTryStartTxDMA(uartPort_t *s)
{
ATOMIC_BLOCK(NVIC_PRIO_SERIALUART_TXDMA) {
// 在这个块内,优先级低于 NVIC_PRIO_SERIALUART_TXDMA 的中断被屏蔽
// 确保 DMA 配置操作的原子性
if (IS_DMA_ENABLED(s->txDMAResource)) {
return;
}
// ... 配置 DMA ...
xDMA_Cmd(s->txDMAResource, ENABLE);
}
}

五、串口生命周期

5.1 初始化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
系统启动


serialInit() → 初始化串口使用列表,填充标识符


uartPinConfigure() → 配置串口引脚(TX/RX)


应用层调用 openSerialPort(identifier, function, baudRate, ...)


查找串口设备 uartDeviceFromIdentifier(identifier)

├── 设备不存在或引脚未配置 → 返回 NULL

└── 设备可用


调用 uartOpen()


调用 serialUART() 创建端口实例


初始化环形缓冲区(head = tail = 0


调用 uartReconfigure() 配置硬件寄存器

├── 使用 DMA → 配置 DMA 通道并使能

└── 使用中断 → 使能 USART 中断


返回 serialPort_t 指针

5.2 关键初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io/serial.c - 串口初始化
void serialInit(bool softserialEnabled)
{
// 清空串口使用列表
memset(&serialPortUsageList, 0, sizeof(serialPortUsageList));

// 填充串口标识符
for (int index = 0; index < SERIAL_PORT_COUNT; index++) {
serialPortUsageList[index].identifier = serialPortIdentifiers[index];
}
}

// drivers/serial_uart.c - 打开串口
serialPort_t *uartOpen(serialPortIdentifier_e identifier,
serialReceiveCallbackPtr rxCallback, void *rxCallbackData,
uint32_t baudRate, portMode_e mode, portOptions_e options)
{
uartDevice_t *uartDevice = uartDeviceFromIdentifier(identifier);
if (!uartDevice || !uartDevice->hardware) {
return NULL; // 设备不存在或引脚未配置
}

uartPort_t *uartPort = serialUART(uartDevice, baudRate, mode, options);
if (!uartPort) {
return NULL;
}

// 初始化环形缓冲区
uartPort->port.rxBufferHead = uartPort->port.rxBufferTail = 0;
uartPort->port.txBufferHead = uartPort->port.txBufferTail = 0;
uartPort->port.rxCallback = rxCallback;
uartPort->port.mode = mode;
uartPort->port.baudRate = baudRate;

// 配置硬件寄存器
uartReconfigure(uartPort);

return (serialPort_t *)uartPort;
}

5.3 硬件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// platform/STM32/serial_uart_stdperiph.c
void uartReconfigure(uartPort_t *uartPort)
{
USART_TypeDef *USARTx = (USART_TypeDef *)uartPort->USARTx;

// 1. 禁用 USART
USART_Cmd(USARTx, DISABLE);

// 2. 配置波特率、字长、停止位、校验
USART_InitStructure.USART_BaudRate = uartPort->port.baudRate;
USART_InitStructure.USART_WordLength = ...;
USART_InitStructure.USART_StopBits = ...;
USART_InitStructure.USART_Parity = ...;
USART_Init(USARTx, &USART_InitStructure);

// 3. 配置 RX(DMA 或中断)
if (uartPort->port.mode & MODE_RX) {
if (uartPort->rxDMAResource) {
// DMA 模式:循环模式
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
// ... 配置 DMA ...
xDMA_Cmd(uartPort->rxDMAResource, ENABLE);
USART_DMACmd(USARTx, USART_DMAReq_Rx, ENABLE);
} else {
// 中断模式:使能 RXNE 中断
USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE);
USART_ITConfig(USARTx, USART_IT_IDLE, ENABLE);
}
}

// 4. 配置 TX(DMA 或中断)
if (uartPort->port.mode & MODE_TX) {
if (uartPort->txDMAResource) {
// DMA 模式:正常模式
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
// ... 配置 DMA ...
USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE);
} else {
// 中断模式:使能 TXE 中断
USART_ITConfig(USARTx, USART_IT_TXE, ENABLE);
}
USART_ITConfig(USARTx, USART_IT_TC, ENABLE);
}

// 5. 使能 USART
USART_Cmd(USARTx, ENABLE);
}

六、中断处理详解

6.1 中断服务函数注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// serial_uart.c - 宏定义批量生成中断服务函数
#define UART_IRQHandler(type, number, dev) \
FAST_IRQ_HANDLER void type ## number ## _IRQHandler(void) \
{ \
uartPort_t *uartPort = &(uartDevice[(dev)].port); \
uartIrqHandler(uartPort); \
}

// 为每个使能的 UART 生成中断函数
#ifdef USE_UART1
UART_IRQHandler(USART, 1, UARTDEV_1) // → void USART1_IRQHandler(void)
#endif
#ifdef USE_UART2
UART_IRQHandler(USART, 2, UARTDEV_2) // → void USART2_IRQHandler(void)
#endif
// ... 以此类推

6.2 中断处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// serial_uart_stm32f4xx.c
void uartIrqHandler(uartPort_t *s)
{
USART_TypeDef *USARTx = (USART_TypeDef *)s->USARTx;

// === RX 处理(仅中断模式)===
if (!s->rxDMAResource && (USART_GetITStatus(USARTx, USART_IT_RXNE) == SET)) {
uint8_t data = USARTx->DR; // 读取接收数据

if (s->port.rxCallback) {
// 有回调:直接交给上层处理(如 CRSF 协议)
s->port.rxCallback(data, s->port.rxCallbackData);
} else {
// 无回调:存入环形缓冲区
s->port.rxBuffer[s->port.rxBufferHead] = data;
s->port.rxBufferHead = (s->port.rxBufferHead + 1) % s->port.rxBufferSize;
}
}

// === TC 中断:发送完成 ===
if (USART_GetITStatus(USARTx, USART_IT_TC) == SET) {
uartTxMonitor(s); // 切换到 TX 引脚监控
USART_ClearITPendingBit(USARTx, USART_IT_TC);
}

// === TXE 中断:可以发送下一个字节(仅中断模式)===
if (!s->txDMAResource && (USART_GetITStatus(USARTx, USART_IT_TXE) == SET)) {
if (s->port.txBufferTail != s->port.txBufferHead) {
USART_SendData(USARTx, s->port.txBuffer[s->port.txBufferTail]);
s->port.txBufferTail = (s->port.txBufferTail + 1) % s->port.txBufferSize;
} else {
USART_ITConfig(USARTx, USART_IT_TXE, DISABLE); // 无数据,关闭中断
}
}

// === ORE 溢出错误处理 ===
if (USART_GetITStatus(USARTx, USART_IT_ORE) == SET) {
USART_ClearITPendingBit(USARTx, USART_IT_ORE);
}

// === IDLE 空闲线检测 ===
if (USART_GetITStatus(USARTx, USART_IT_IDLE) == SET) {
if (s->port.idleCallback) {
s->port.idleCallback(); // 通知上层空闲线事件
}
(void) USARTx->SR; // 读 SR 和 DR 清除 IDLE 标志
(void) USARTx->DR;
}
}

6.3 DMA 中断处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// serial_uart_stm32f4xx.c
void uartDmaIrqHandler(dmaChannelDescriptor_t* descriptor)
{
uartPort_t *s = &(((uartDevice_t*)(descriptor->userParam))->port);

if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) {
// 传输完成
DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF);
DMA_CLEAR_FLAG(descriptor, DMA_IT_HTIF);

handleUsartTxDma(s); // 尝试发送更多数据
}

if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TEIF)) {
DMA_CLEAR_FLAG(descriptor, DMA_IT_TEIF); // 传输错误
}
}

static void handleUsartTxDma(uartPort_t *s)
{
uartTryStartTxDMA(s); // 尝试启动下一批 DMA 传输

if (s->txDMAEmpty) {
uartTxMonitor(s); // 无数据了,监控 TX 引脚
}
}

七、TX 引脚监控机制

7.1 为什么需要 TX 引脚监控?

当外设(如接收机、图传)未上电时,其 RX 引脚可能处于高阻或下拉状态,这会通过 UART 的 TX 引脚将主控的 TX 电平拉低,导致: 1. 功耗增加(电流从主控流向未上电的外设) 2. 可能损坏引脚

7.2 监控实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// serial_uart_stm32f4xx.c
void uartTxMonitor(uartPort_t *s)
{
uartDevice_t *uart = container_of(s, uartDevice_t, port);

if (uart->txPinState == TX_PIN_ACTIVE) {
IO_t txIO = IOGetByTag(uart->tx.pin);

// 1. 禁用 USART 发送器
CLEAR_BIT(((USART_TypeDef *)s->USARTx)->CR1, USART_CR1_TE);

// 2. 将 TX 引脚切换为输入 + 上拉模式
uart->txPinState = TX_PIN_MONITOR;
IOConfigGPIO(txIO, IOCFG_IPU); // 输入 + 上拉
}
}

bool checkUsartTxOutput(uartPort_t *s)
{
uartDevice_t *uart = container_of(s, uartDevice_t, port);

if ((uart->txPinState == TX_PIN_MONITOR) && txIO) {
if (IORead(txIO)) {
// TX 引脚为高电平 → 外设已上电,可以发送
uart->txPinState = TX_PIN_ACTIVE;
IOConfigGPIOAF(txIO, IOCFG_AF_PP, uart->hardware->af);
SET_BIT(((USART_TypeDef *)s->USARTx)->CR1, USART_CR1_TE);
return true;
} else {
// TX 引脚被拉低 → 外设未上电,不发送
return false;
}
}
return true;
}

7.3 工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
发送数据

├── TX_PIN_ACTIVE → 正常发送
│ │
│ ▼
│ 发送完成
│ │
│ ▼
│ 切换为 TX_PIN_MONITOR
│ TX 引脚改为输入+上拉
│ │
└── TX_PIN_MONITOR → 读取 TX 引脚电平

┌─────────────┴─────────────┐
│ │
▼ ▼
高电平(外设已上电) 低电平(外设未上电)
│ │
▼ ▼
切换回 AF 推挽模式 不发送数据
使能发送器


正常发送

八、串口功能分配与管理

8.1 串口功能标识

每个串口可以被分配一个或多个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io/serial.h
typedef enum {
FUNCTION_NONE = 0,
FUNCTION_MSP = (1 << 0), // 与地面站通信
FUNCTION_GPS = (1 << 1), // GPS
FUNCTION_TELEMETRY_FRSKY_HUB = (1 << 2), // FrSky Hub 遥测
FUNCTION_TELEMETRY_HOTT = (1 << 3), // HoTT 遥测
FUNCTION_TELEMETRY_LTM = (1 << 4), // LTM 遥测
FUNCTION_TELEMETRY_SMARTPORT = (1 << 5), // SmartPort 遥测
FUNCTION_RX_SERIAL = (1 << 6), // 串口接收机
FUNCTION_BLACKBOX = (1 << 7), // 黑盒日志
FUNCTION_TELEMETRY_MAVLINK = (1 << 9), // MAVLink 遥测
FUNCTION_ESC_SENSOR = (1 << 10), // ESC 传感器
FUNCTION_VTX_SMARTAUDIO = (1 << 11), // SmartAudio 图传
FUNCTION_TELEMETRY_IBUS = (1 << 12), // IBUS 遥测
FUNCTION_VTX_TRAMP = (1 << 13), // Tramp 图传
FUNCTION_RCDEVICE = (1 << 14), // RC 设备
FUNCTION_VTX_MSP = (1 << 17), // MSP 图传
} serialPortFunction_e;

8.2 串口打开流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io/serial.c
serialPort_t *openSerialPort(
serialPortIdentifier_e identifier,
serialPortFunction_e function,
serialReceiveCallbackPtr rxCallback,
void *rxCallbackData,
uint32_t baudRate,
portMode_e mode,
portOptions_e options)
{
// 1. 查找串口使用记录
serialPortUsage_t *serialPortUsage = findSerialPortUsageByIdentifier(identifier);
if (!serialPortUsage || serialPortUsage->function != FUNCTION_NONE) {
return NULL; // 串口已被占用
}

// 2. 根据串口类型调用对应的打开函数
serialPort_t *serialPort = NULL;
switch (serialType(identifier)) {
case SERIALTYPE_USB_VCP:
serialPort = usbVcpOpen();
break;
case SERIALTYPE_UART:
case SERIALTYPE_LPUART:
serialPort = uartOpen(identifier, rxCallback, rxCallbackData, baudRate, mode, options);
break;
case SERIALTYPE_SOFTSERIAL:
serialPort = softSerialOpen(identifier, rxCallback, rxCallbackData, baudRate, mode, options);
break;
}

// 3. 标记串口为已使用
if (serialPort) {
serialPortUsage->function = function;
serialPortUsage->serialPort = serialPort;
}

return serialPort;
}

8.3 串口共享规则

Betaflight 允许一个串口被多个功能共享,但有限制:

共享组合 是否允许 说明
MSP + 遥测 分时共享(解锁时遥测,锁定后 MSP)
串口接收机 + 遥测 RX 用 RX 线,遥测用 TX 线(半双工)
MSP + 黑盒 分时共享
其他组合 不允许

九、完整数据流示例

9.1 发送数据:从应用到引脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
应用层 serialWrite(port, data)


drivers/serial.c: instance->vTable->serialWrite(instance, ch)


drivers/serial_uart.c: uartWrite(instance, ch)

├── 写入环形缓冲区 txBuffer[head]
├── head++

├── 有 txDMAResource?
│ │
│ ├── 是 → platform/STM32: uartTryStartTxDMA(uartPort)
│ │ │
│ │ ├── ATOMIC_BLOCK 保护
│ │ ├── 计算要发送的数据块大小
│ │ ├── 配置 DMA 源地址 = txBuffer[tail]
│ │ ├── 配置 DMA 目标地址 = USART->DR
│ │ ├── 设置传输计数
│ │ └── 使能 DMA
│ │ │
│ │ ▼
│ │ DMA 控制器自动从内存读取数据
│ │ │
│ │ ▼ (循环每个字节)
│ │ USART 外设 → TX 引脚逐位发送
│ │ │
│ │ ▼
│ │ DMA 传输完成中断
│ │ │
│ │ ▼
│ │ uartDmaIrqHandler → handleUsartTxDma
│ │ │
│ │ ├── uartTryStartTxDMA(尝试下一批)
│ │ └── 无数据 → uartTxMonitor
│ │
│ └── 否 → 使能 TXE 中断
│ │
│ ▼
TXE 中断触发 → CPU 发送一个字节
│ │
│ ▼
CPU 检查缓冲区是否还有数据
│ ├── 有 → 继续发送
│ └── 无 → 关闭 TXE 中断

└── 立即返回(不等待发送完成)

9.2 接收数据:从引脚到应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
RX 引脚逐位接收数据


USART 外设接收完成一个字节

├── DMA 模式(USE_RX_DMA 已定义)
│ │
│ ▼
│ DMA 控制器自动将数据存入 rxBuffer(循环模式)
│ CPU 完全不需要参与
│ │
│ ▼
│ 应用层调用 serialRead(port)
│ │
│ ▼
uartRead(): 从 rxBuffer[rxBufferSize - rxDMAPos] 读取
│ rxDMAPos-- (递减,到 0 回绕到 rxBufferSize)
│ │
│ ▼
│ 返回数据

└── 中断模式(USE_RX_DMA 未定义)


RXNE 中断触发


uartIrqHandler: 读取 USART->DR

├── 有 rxCallback → 直接回调上层(如 CRSF 协议)

└── 无 rxCallback → 存入 rxBuffer[head]
head++


应用层调用 serialRead(port)


uartRead(): 从 rxBuffer[tail] 读取
tail++


返回数据

十、总结

10.1 串口系统核心设计思想

设计 目的 实现方式
虚函数表 VTable 多态:同一套 API 支持 UART/USB/SoftSerial serialPortVTable 结构体
环形缓冲区 生产者和消费者解耦,非阻塞通信 txBuffer/rxBuffer + head/tail
DMA 卸载 减少 CPU 负载,提高吞吐量 DMA 控制器自动传输
循环 DMA 接收 零 CPU 开销的持续接收 DMA_Mode_Circular
ATOMIC_BLOCK 防止任务和中断的竞态条件 基于 NVIC 优先级屏蔽
优先级分层 确保关键通信不被阻塞 每个串口 3 级优先级
TX 引脚监控 防止未上电外设拉低 TX 电平 发送完成后切换为输入模式
功能分配 灵活配置每个串口的用途 serialPortFunction_e 位掩码

10.2 关键文件索引

文件 核心内容
src/main/io/serial.h 串口标识符、功能枚举、配置结构体
src/main/io/serial.c 串口生命周期管理、配置验证
src/main/drivers/serial.h 抽象串口接口、虚函数表
src/main/drivers/serial.c 抽象串口操作实现
src/main/drivers/serial_uart.h UART 端口结构体
src/main/drivers/serial_uart.c UART 核心实现、VTable、中断注册
src/main/drivers/serial_uart_impl.h 硬件描述结构体、缓冲区宏
src/main/drivers/nvic.h 所有外设的中断优先级定义
src/platform/STM32/serial_uart_stdperiph.c F4 平台:寄存器配置、DMA 启动
src/platform/STM32/serial_uart_stm32f4xx.c F4 平台:硬件表、中断处理、DMA 中断

betaflight多串口环形缓冲区和dma实现原理
http://jiangno.com/2026/08/02/26_08_02_betaflight_uart_serial_deep_dive/
作者
江の
发布于
2026年8月2日
许可协议