news 2026/6/10 20:47:05

第 3 篇:双缓冲模式 (Double Buffering) —— 榨干 DMA 的性能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第 3 篇:双缓冲模式 (Double Buffering) —— 榨干 DMA 的性能

前两篇我们分别解决了“内存怎么分”和“模块怎么管”的问题。今天这一篇,我们要解决嵌入式开发中最硬核、也最考验功底的性能问题——数据吞吐量

当你的波特率飙升到 2Mbps,或者 ADC 采样率达到 1Msps 时,单纯靠中断(ISR)一个字节一个字节地收,CPU 早就累吐血了。这时候 DMA(直接存储器访问)是救星,但如果 DMA 用不好,数据覆盖和撕裂问题会让你怀疑人生。

是时候祭出双缓冲模式了。

专栏导读:在高速数据采集或显示系统中,CPU 处理数据的速度往往跟不上硬件产生数据的速度。如果只有一个缓冲区,CPU 在读,硬件在写,要么读到脏数据,要么必须关硬件等待。双缓冲(Ping-Pong Buffer)通过“空间换时间”,实现了硬件传输与 CPU 处理的完美并行。


1. 场景还原 (The Pain)

假设你正在做一个高保真录音笔,ADC 以 44.1kHz 采样,通过 DMA 往内存里搬运数据。每采集 1024 个点,你需要把数据写入 SD 卡。

菜鸟的写法:单缓冲的竞态冒险

#define BUF_SIZE 1024
volatile uint16_t g_adc_buffer[BUF_SIZE];

// DMA 传输完成中断
void DMA_TC_Handler(void) {
// 痛点:在把数据写入 SD 卡期间,必须停止 ADC 采样,
// 否则 DMA 会从头开始覆盖数据,导致 CPU 写卡读到的前半段是旧数据,后半段是新数据(数据撕裂)。

HAL_ADC_Stop_DMA(); // 1. 停硬件(导致采样丢失,录音断续)

Write_SD_Card(g_adc_buffer, BUF_SIZE); // 2. 耗时操作(比如 10ms)

HAL_ADC_Start_DMA(); // 3. 重新开硬件
}

架构师的审视

这种**“停-读-开”**的逻辑,在高频采样下是无法接受的。

  1. 数据丢失:写 SD 卡的那 10ms 里,麦克风采集的声音全丢了。

  2. CPU 利用率低:CPU 写卡时 DMA 闲着,DMA 搬运时 CPU 闲着(如果没其他任务),没有实现流水线并行。


2. 模式图解 (The Concept)

双缓冲模式(也叫 Ping-Pong Buffer)准备了两个一样大的缓冲区:Buffer A (Ping) 和 Buffer B (Pong)。

  • 状态 0:DMA 正在疯狂填充Buffer A,CPU 闲置(或处理其他业务)。

  • 状态 1:Buffer A 填满了。DMA 立即自动切换到Buffer B继续填充(硬件无缝切换)。

  • 状态 2:此时 CPU 醒来,处理刚刚填满的Buffer A(比如写卡、DSP计算),与此同时,DMA 正在默默填充 Buffer B。

  • 循环:Buffer B 填满后,DMA 切回 A,CPU 处理 B。

核心优势:硬件(DMA)永远不需要停,CPU 永远在处理“静态”的数据。


3. 代码实战 (The Code)

现代 MCU(如 STM32)的 DMA 通常自带“循环模式”和“半传输/全传输中断”,这天然支持双缓冲。但为了通用性,我们写一个逻辑层的封装,让它看起来更像一个通用的设计模式。

3.1 定义数据结构

#include <stdint.h>
#include <stdbool.h>

#define SAMPLE_COUNT 1024 // 单个缓冲区大小
#define BUFFER_TOTAL 2 // 双缓冲(也可以扩展成三缓冲)

typedef struct {
// 定义一个二维数组:buffer[2][1024]
uint16_t raw_data[BUFFER_TOTAL][SAMPLE_COUNT];

// 当前 CPU 应该处理哪个 Buffer 的索引
volatile uint8_t process_index;

// 标志位:告诉主循环有数据准备好了
volatile bool data_ready;
} PingPongBuffer;

static PingPongBuffer g_adc_pp_buf;

3.2 中断逻辑 (The Core Logic)

这里利用 DMA 的两个中断事件:

  1. Half Transfer (HT):表示前一半(Buffer 0)填满了。

  2. Transfer Complete (TC):表示整个大数组填满了(即 Buffer 1 也填满了),此时 DMA 会自动循环回到开头。

// 伪代码:对应具体的硬件中断回调
void DMA_IRQ_Handler(void) {
uint32_t status = DMA_GetStatus();

// 1. 半传输中断 (Half Transfer) -> Buffer 0 满了
if (status & DMA_FLAG_HT) {
g_adc_pp_buf.process_index = 0; // 告诉 CPU 去处理 Buffer 0
g_adc_pp_buf.data_ready = true;
DMA_ClearFlag_HT();
}

// 2. 传输完成中断 (Transfer Complete) -> Buffer 1 满了
if (status & DMA_FLAG_TC) {
g_adc_pp_buf.process_index = 1; // 告诉 CPU 去处理 Buffer 1
g_adc_pp_buf.data_ready = true;
DMA_ClearFlag_TC();
}
}

3.3 主循环处理 (Consumer)

// 模拟复杂的 DSP 处理或写卡操作
void Process_Data(uint16_t* data, uint32_t len) {
// 在这里写 SD 卡,或者做 FFT
// 由于是双缓冲,这里的操作即使耗时 5ms,
// 只要小于 DMA 填满另一个 Buffer 的时间,系统就是安全的。
}

void Main_Loop(void) {
// 启动 DMA,长度设为 2 * SAMPLE_COUNT
// 必须开启 Circular Mode (循环模式)
HAL_DMA_Start(..., (uint32_t)g_adc_pp_buf.raw_data, SAMPLE_COUNT * 2);

while (1) {
if (g_adc_pp_buf.data_ready) {
// 1. 关中断保护标志位(简单处理)
// 实际上 data_ready 最好用信号量 (Semaphore)
g_adc_pp_buf.data_ready = false;

// 2. 获取当前应该处理的 Buffer 指针
uint16_t* current_buf = g_adc_pp_buf.raw_data[g_adc_pp_buf.process_index];

// 3. 处理数据 (此时 DMA 正在写另一个 Buffer,互不干扰)
Process_Data(current_buf, SAMPLE_COUNT);
}
}
}

4. 内存与性能分析 (The Cost)

空间开销

  • RAM 翻倍:这是显而易见的代价。如果本来需要 1KB 缓冲,现在需要 2KB。

  • 权衡:在存储廉价的今天,用 1KB RAM 换取 100% 的数据完整性和 CPU 并行度,这笔买卖极其划算。

时间约束 (Time Constraints)

双缓冲不是万能的,它有一个硬性物理约束

CPU 处理一个 Buffer 的时间 < DMA 填满一个 Buffer 的时间

  • 如果 ADC 采样极快(填满一个 Buffer 只要 1ms),而写 SD 卡需要 10ms,那么双缓冲也会爆(Overrun)。

  • 解法:这种情况下,你需要的不是更多的缓冲,而是压缩数据、降低采样率,或者换更快的 CPU/存储介质。


5. 变种与延伸 (The Evolution)

5.1 环形缓冲区 (Ring Buffer / FIFO)

很多初学者分不清双缓冲和 Ring Buffer。

  • Ring Buffer:适合字节流 (Byte Stream),如串口不定长接收。通常是生产者(ISR)和消费者(Task)都在操作同一个大数组的读写指针。

  • Double Buffer:适合块传输 (Block Transfer),如 ADC、摄像头图像、USB 数据包。

  • 结合体:你可以用 DMA 往 Ring Buffer 里写,但逻辑上依然可以把 Ring Buffer 切割成 n 个“片段”来管理,这其实就是多缓冲。

5.2 三缓冲 (Triple Buffering)

在图形显示(GUI)领域非常常见。

  • Buffer A: 显示屏正在显示这一帧。

  • Buffer B: GPU/DMA 正在渲染/搬运下一帧。

  • Buffer C: CPU 正在计算逻辑准备再下一帧。

  • 这样可以彻底消除画面撕裂,实现丝般顺滑的 UI 体验。

5.3 零拷贝 (Zero-Copy) 驱动链

在很多高级协议栈(如 LwIP)中,双缓冲的指针会直接传递给下一层,而不是memcpy。 比如:DMA 收到了 Ping Buffer -> 传递指针给网络层 -> 网络层处理完 -> 归还指针给 DMA。这需要结合第一篇的对象池技术来实现高级的内存流转。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 16:13:52

商城活动说明

一、活动说明抽奖活动凭借着以小博大的杠杆效应、低门槛参与、高奖励诱惑的活动机制&#xff0c;无论是线下门店促销&#xff0c;还是线上活动&#xff0c;都被广泛用于拉新、促活、获客等增长环节。二、功能说明系统提供的抽奖环节有&#xff1a;积分抽奖&#xff0c;下单支付…

作者头像 李华
网站建设 2026/6/9 23:43:30

25.4 进度类

&#x1f31f; 一、单代号网络图&#xff08;PDM / 前导图法&#xff09;✅ 定义前导图法&#xff08;Precedence Diagramming Method, PDM&#xff09;&#xff1a;用矩形/方框&#xff08;节点&#xff09;表示活动&#xff0c;箭头表示逻辑关系。节点需编号&#xff0c;箭线…

作者头像 李华
网站建设 2026/5/23 15:21:56

收藏!2025秋招真相:IT仍是王者,AI算法岗年薪40万领跑全场

秋招战场的冰火两重天&#xff0c;今年格外刺眼。一边是无数毕业生为“月薪过万”辗转焦虑&#xff0c;投出的简历石沉大海&#xff1b;另一边&#xff0c;不少瞄准热门赛道的毕业生&#xff0c;早已将“年薪40万”纳入囊中之物&#xff0c;成为秋招里的“天选赢家”。 近日&a…

作者头像 李华
网站建设 2026/6/10 16:02:46

Therma Wave 14-004693

产品概述Therma Wave 14-004693 Rev D是用于半导体制造或检测设备的电气总成组件&#xff0c;通常与晶圆处理或真空系统相关。该型号可能涉及温度控制、真空环境维持或信号传输功能&#xff0c;具体应用取决于设备型号。功能特点晶圆处理&#xff1a;可能包含晶圆定位、温度监控…

作者头像 李华
网站建设 2026/6/10 16:02:17

[运营进阶] 店铺图片风格杂乱?浅析如何利用 AI 批量统一视觉规范,打造“大牌感”Listing

品牌出海 视觉营销 跨境电商 AI工具 自动化办公 图片处理前言在跨境电商从“野蛮生长”转向“品牌出海”的今天&#xff0c;Listing 的视觉质量 往往决定了买家对品牌的首因效应。对于拥有独立供应链的大卖来说&#xff0c;他们可以花费巨资统一拍摄、统一修图。但对于大多数中…

作者头像 李华
网站建设 2026/5/31 12:14:19

基于级联前向BP神经网络(CFBP)的数据回归预测及Matlab实现

基于级联前向BP神经网络(CFBP)的数据回归预测 CFBP回归 matlab代码 注&#xff1a;暂无Matlab版本要求 -- 推荐 2018B 版本及以上 在数据预测领域&#xff0c;神经网络一直是备受瞩目的工具。今天咱们来聊聊基于级联前向BP神经网络&#xff08;CFBP&#xff09;的数据回归预测&…

作者头像 李华