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 为什么需要环形缓冲区?
串口通信有两个特点:
- 速率不匹配:CPU
可以一次产生大量数据,但串口只能一个一个字节地发送
- 异步性:数据到达的时间不可预测,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 环形缓冲区的数据结构
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;
uartPort->port.txBuffer[uartPort->port.txBufferHead] = ch;
if (uartPort->port.txBufferHead + 1 >= uartPort->port.txBufferSize) { uartPort->port.txBufferHead = 0; } else { uartPort->port.txBufferHead++; }
#ifdef USE_DMA if (uartPort->txDMAResource) { uartTryStartTxDMA(uartPort); } else #endif { uartEnableTxInterrupt(uartPort); } }
|
写入过程图解:
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) { ch = uartPort->port.rxBuffer[uartPort->port.rxBufferSize - uartPort->rxDMAPos]; if (--uartPort->rxDMAPos == 0) uartPort->rxDMAPos = uartPort->port.rxBufferSize; } else #endif { 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) { uint32_t rxDMAHead = dmaGetDataLength(uartPort->rxDMAResource); if (uartPort->rxDMAPos >= rxDMAHead) { return uartPort->rxDMAPos - rxDMAHead; } else { return uartPort->port.rxBufferSize + uartPort->rxDMAPos - rxDMAHead; } } #endif 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
| 情况1:head >= tail(正常) ┌────┬────┬────┬────┬────┐ │ │ A │ B │ C │ │ └────┴────┴────┴────┴────┘ ↑ ↑ tail head 可读字节数 = head - tail = 3 - 1 = 2
情况2:head < 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) { 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
| #define USE_UART1_TX_DMA #define USE_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
| void uartTryStartTxDMA(uartPort_t *s) { ATOMIC_BLOCK(NVIC_PRIO_SERIALUART_TXDMA) { if (IS_DMA_ENABLED(s->txDMAResource)) { return; }
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; }
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); } }
|
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
| void uartIrqHandler(uartPort_t *s) { USART_TypeDef *USARTx = (USART_TypeDef *)s->USARTx;
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); } }
if (USART_GetITStatus(USARTx, USART_IT_TC) == SET) { uartTxMonitor(s); 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
| 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);
|
关键点: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
| 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
| 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, .txDMAResource = (dmaResource_t *)DMA2_Stream7, .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 };
|
4.3 中断优先级分层
1 2 3 4 5 6 7 8 9
|
#define NVIC_PRIO_SERIALUART1_TXDMA NVIC_BUILD_PRIORITY(1, 1) #define NVIC_PRIO_SERIALUART1_RXDMA NVIC_BUILD_PRIORITY(1, 1) #define NVIC_PRIO_SERIALUART1 NVIC_BUILD_PRIORITY(1, 2)
#define NVIC_PRIO_SERIALUART2_TXDMA NVIC_BUILD_PRIORITY(1, 0) #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
|
void uartTryStartTxDMA(uartPort_t *s) { ATOMIC_BLOCK(NVIC_PRIO_SERIALUART_TXDMA) { if (IS_DMA_ENABLED(s->txDMAResource)) { return; } 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
| void serialInit(bool softserialEnabled) { memset(&serialPortUsageList, 0, sizeof(serialPortUsageList));
for (int index = 0; index < SERIAL_PORT_COUNT; index++) { serialPortUsageList[index].identifier = serialPortIdentifiers[index]; } }
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
| void uartReconfigure(uartPort_t *uartPort) { USART_TypeDef *USARTx = (USART_TypeDef *)uartPort->USARTx;
USART_Cmd(USARTx, DISABLE);
USART_InitStructure.USART_BaudRate = uartPort->port.baudRate; USART_InitStructure.USART_WordLength = ...; USART_InitStructure.USART_StopBits = ...; USART_InitStructure.USART_Parity = ...; USART_Init(USARTx, &USART_InitStructure);
if (uartPort->port.mode & MODE_RX) { if (uartPort->rxDMAResource) { DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; xDMA_Cmd(uartPort->rxDMAResource, ENABLE); USART_DMACmd(USARTx, USART_DMAReq_Rx, ENABLE); } else { USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE); USART_ITConfig(USARTx, USART_IT_IDLE, ENABLE); } }
if (uartPort->port.mode & MODE_TX) { if (uartPort->txDMAResource) { DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE); } else { USART_ITConfig(USARTx, USART_IT_TXE, ENABLE); } USART_ITConfig(USARTx, USART_IT_TC, ENABLE); }
USART_Cmd(USARTx, ENABLE); }
|
六、中断处理详解
6.1 中断服务函数注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #define UART_IRQHandler(type, number, dev) \ FAST_IRQ_HANDLER void type ## number ## _IRQHandler(void) \ { \ uartPort_t *uartPort = &(uartDevice[(dev)].port); \ uartIrqHandler(uartPort); \ }
#ifdef USE_UART1 UART_IRQHandler(USART, 1, UARTDEV_1) #endif #ifdef USE_UART2 UART_IRQHandler(USART, 2, UARTDEV_2) #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
| void uartIrqHandler(uartPort_t *s) { USART_TypeDef *USARTx = (USART_TypeDef *)s->USARTx;
if (!s->rxDMAResource && (USART_GetITStatus(USARTx, USART_IT_RXNE) == SET)) { uint8_t data = USARTx->DR;
if (s->port.rxCallback) { 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; } }
if (USART_GetITStatus(USARTx, USART_IT_TC) == SET) { uartTxMonitor(s); USART_ClearITPendingBit(USARTx, USART_IT_TC); }
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); } }
if (USART_GetITStatus(USARTx, USART_IT_ORE) == SET) { USART_ClearITPendingBit(USARTx, USART_IT_ORE); }
if (USART_GetITStatus(USARTx, USART_IT_IDLE) == SET) { if (s->port.idleCallback) { s->port.idleCallback(); } (void) USARTx->SR; (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
| 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);
if (s->txDMAEmpty) { uartTxMonitor(s); } }
|
七、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
| 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);
CLEAR_BIT(((USART_TypeDef *)s->USARTx)->CR1, USART_CR1_TE);
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)) { 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 { 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
| typedef enum { FUNCTION_NONE = 0, FUNCTION_MSP = (1 << 0), FUNCTION_GPS = (1 << 1), FUNCTION_TELEMETRY_FRSKY_HUB = (1 << 2), FUNCTION_TELEMETRY_HOTT = (1 << 3), FUNCTION_TELEMETRY_LTM = (1 << 4), FUNCTION_TELEMETRY_SMARTPORT = (1 << 5), FUNCTION_RX_SERIAL = (1 << 6), FUNCTION_BLACKBOX = (1 << 7), FUNCTION_TELEMETRY_MAVLINK = (1 << 9), FUNCTION_ESC_SENSOR = (1 << 10), FUNCTION_VTX_SMARTAUDIO = (1 << 11), FUNCTION_TELEMETRY_IBUS = (1 << 12), FUNCTION_VTX_TRAMP = (1 << 13), FUNCTION_RCDEVICE = (1 << 14), FUNCTION_VTX_MSP = (1 << 17), } 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
| serialPort_t *openSerialPort( serialPortIdentifier_e identifier, serialPortFunction_e function, serialReceiveCallbackPtr rxCallback, void *rxCallbackData, uint32_t baudRate, portMode_e mode, portOptions_e options) { serialPortUsage_t *serialPortUsage = findSerialPortUsageByIdentifier(identifier); if (!serialPortUsage || serialPortUsage->function != FUNCTION_NONE) { return NULL; }
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; }
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 中断 |