十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F4 DMA双缓冲实战:DBM位配置与ADC连续采样防丢数据

STM32F4 DMA双缓冲实战:DBM位配置与ADC连续采样防丢数据 简介STM32F4 DMA双缓冲的正确打开方式是一份面向嵌入式开发者的技术笔记聚焦STM32F2/F4/F7系列通用DMA的双缓冲模式应用适合需要在大批量数据传输中提升CPU利用率的工程师参考。文档从原理入手讲清双缓冲涉及的两个存储区、DMA_SxM0AR/DMA_SxM1AR指针以及CT标志的切换机制并指出双缓冲仅在内存与外设之间传输、不支持memory-to-memory等容易被忽略的要点同时对比了单缓冲与双缓冲的应用差异。包内仅1个doc文档、约40KB内容紧凑已有6144人学习下载。通过文中配置示例和作者拆解的真实故障案例比如Buffer1起始地址设置错误导致输出异常读者能快速理解双缓冲初始化顺序、缓冲区大小与当前存储区选择的关联从而避开常见配置陷阱文档还以“双茶壶”比喻辅助理解DMA与CPU并行工作时的数据交接过程能帮助开发者真正用好DMA双缓冲。1. 为什么把 DMA 双缓冲当成“两个普通缓冲轮流用”的人都会丢数据在 STM32F4 上做 ADC 连续采样或 DAC 波形输出时很多人先开一个大内存数组再打开 DMA 的 Circular 模式想靠中断把数据搬走。低频下没事采样率一高中断晚到几十微秒DMA 已经把同一块内存覆盖了一遍——你拿到的不是“刚才的数据”而是“最新的覆盖结果”。DMA 双缓冲的价值不是把数组从一个变成两个而是让 DMA 控制器在两块内存之间自动切换一块被 CPU 处理时另一块仍在被 DMA 写入。STM32F4 硬件本来就支持这个能力很多人却没配对要么把 Circular 单缓冲当成双缓冲要么被 HAL 库的 HAL_ADC_Start_DMA 带偏。这篇文章以 ADC 连续采样为主线把 F4 的 DBM/CT 机制、CubeMX 参数和可编译驱动一次说清。2. STM32F4 双缓冲的硬件行为DBM 位、M0AR/M1AR 与 CT 状态2.1 双缓冲在 F4 上到底改了什么寄存器STM32F4 的 DMA1/DMA2 各有 8 个 stream每个 stream 除了外设地址寄存器 DMA_SxPAR 和传输计数寄存器 DMA_SxNDTR 之外还有两个内存地址寄存器 DMA_SxM0AR、DMA_SxM1AR。常规 DMA 初始化只配置 M0AR数据搬完或者完成一次循环后永远只对着同一个内存地址操作。而把控制寄存器 DMA_SxCR 中的 DBM 位置 1 之后M0AR 与 M1AR 会同时生效DMA 控制器在两者之间按节拍切换。这里有一个容易被忽略的细节切换动作完全由硬件在每次“当前目标 buffer 传输完成”时自动执行不需要 CPU 在中断里修改寄存器。那些“在中断里把 buffer_index 异或一下再重启 DMA”的软乒乓做法本质是单缓冲加手动切换DMA 一停外设请求还会持续到达丢数据的风险一点没少。DBM 位只能用于外设到内存或内存到外设方向。内存到内存Memory-to-Memory模式下不允许开启双缓冲因为 M2M 传输由 DMA 引擎连续搬运没有外设请求作为天然的分批边界。在 F4 上ADC、DAC、SPI、串口等外设驱动的 DMA 传输都满足这个前提所以双缓冲最常见的落地点就是 ADC 连续采样、DAC 波形输出、SPI 收发缓冲。2.2 CT 位判断哪一块正被 DMA 占用当 DBM1 时DMA_SxCR 中的 CT 位Current Target表示 DMA 当前正在服务的目标内存。CT0 表示当前目标是 Memory0CT1 表示当前目标是 Memory1。软件读取这个位就能知道哪块缓冲正在被 DMA 写入哪块是刚刚填满、可以被安全处理的。需要注意F4 上双缓冲的 NDTR 表示的是单个 buffer 的数据项数量而不是两块缓冲的总数。每完成一个 bufferNDTR 重载为初始值CT 位翻转同时产生一次传输完成事件。在中断回调里读取 CT 位时DMA 已经完成了本次 buffer 的切换所以读到 CT1 时刚完成的实际是 Memory0读到 CT0 时刚完成的实际是 Memory1。这个“取反”关系是双缓冲编程最容易写错的地方后面代码部分会专门处理。2.3 Normal 与 Circular 组合出来的四种行为差异配置组合传输行为中断表现Normal 单缓冲传完一个 buffer 后停止只进一次 TC 中断Normal DBMM0 满后自动切 M1M1 满后停止进入两次 TC 中断Circular 单缓冲一直往同一个地址重复写每个周期进一次 TC旧数据被覆盖Circular DBMM0 满切 M1M1 满切 M0无限交替每块 buffer 完成都进一次 TC真正需要长期连续采集的场景必须选最后一行。把 Mode 设成 Normal 的人即使配置了 DBM也只是跑完两块缓冲就停工采样曲线会在开头多了一段“看似正常”的数据然后静默停摆。而 Circular 单缓冲虽然中断频率一样但 CPU 没来得及处理的数据会被 DMA 下一次循环覆盖是典型的“看起来能跑、实际丢点”的配置。3. CubeMX 里把 DMA 设成 Circular 只是开始双缓冲参数逐项配置3.1 CubeMX 中 ADC 的 DMA 请求配置CubeMX 里并没有一个叫“Double Buffer”的开关。这个事实很多人没注意到F4 的双缓冲开关在寄存器层叫 DBMCubeMX 生成的 DMA 初始化代码里默认没有把它打开。它的图形界面只负责配置 DMA 的外设请求、方向和模式双缓冲的开启要留给 HAL_DMAEx 相关函数手动完成。若是从 G0/F0 系列工程迁移过来还会发现另一个差别F4 的 CubeMX 没有 “Continuous Requests” 选项请求持续使能在 F4 上等效于把 DMA Mode 设为 Circular。这个位在 STM32G0/F0 上是个独立使能位在 F4 上则由 Circular 模式承担不要去找一个不存在的开关。在 ADC1 的 DMA Settings 里添加 DMA Request选择 DMA2 Stream0方向为 Peripheral-to-Memory。ADC 在 F4 上的 DMA 请求映射是固定的ADC1 对应 DMA2 的 Stream0/Stream4 的 Channel0选错 stream 或 channel 时ADC 数据永远不会进入内存且不会报错。NVIC 设置里勾选 DMA2 Stream0 的全局中断否则后续中断回调不会被触发。3.2 六个必调的 DMA 参数下面是这套配置里最关键的 DMA 参数表照着逐项核对比记住“模板代码”更可靠参数推荐值理由DirectionPeripheral to MemoryADC 产生数据DMA 负责搬运到内存ModeCircular只有 Circular 才能让 DBM 在 M0/M1 之间无限循环Periph IncrementDisableADC 数据寄存器地址固定不递增Memory IncrementEnable每个采样依次写入 buffer 的下一个数据单元Data AlignmentWord/WordADC1-DR 是 32 位寄存器按字读取最稳FIFO ModeDisable双缓冲边界处突发传输容易踩坑关闭 FIFO 最省心Periph Data Alignment 和 Mem Data Alignment 必须一致。F4 的 DMA 在 Direct 模式下不允许外设和内存宽度不一致若打开 FIFO 做数据打包又会在双缓冲切换时遇到一次事务跨过 M0/M1 边界的问题。工程上稳妥做法就是 FIFO 关闭、突发设为 Single把“双缓冲切换必须发生在完整数据项边界”这个约束交给硬件去保证。3.3 生成代码后的三处手动改动生成代码后CubeMX 会在 main() 里调用 HAL_ADC_Start_DMA。这个函数只接受一个内存地址即使你之前把 DMA Mode 配置成了 Circular它也只是让 DMA 不断循环覆盖同一块 buffer真正意义上的双缓冲没有生效。需要手动改三处/* 错误示范HAL_ADC_Start_DMA 只有一个目标地址 */ HAL_ADC_Start_DMA(hadc1, (uint32_t *)single_buf, ADC_BUF_LEN); /* 正确打开方式指定 M0 和 M1 两个目标地址 */ HAL_DMAEx_MultiBufferStart_IT(hdma_adc, (uint32_t)hadc1.Instance-DR, (uint32_t)adc_buf[0], (uint32_t)adc_buf[1], ADC_BUF_LEN); HAL_ADC_Start(hadc1);第一把 HAL_ADC_Start_DMA 换成 HAL_DMAEx_MultiBufferStart_IT并在它后面调用不带 DMA 参数的 HAL_ADC_Start第二确认 hdma_adc.Init.Mode 在生成代码里确实是 DMA_CIRCULARCubeMX 默认有时会给 Normal第三如果 CubeMX 生成的 DMA 处理函数在别的 C 文件里需要把 hdma_adc 句柄设为非 static或者在中断服务函数里用 extern 引用。启动顺序上DMA 必须先于 ADC 运行否则 ADC 一旦开启转换完成的第一个请求就可能落在错误的地址上。4. 用 HAL_DMAEx_MultiBufferStart_IT 接 ADC 连续采样的完整驱动4.1 缓冲区声明对齐比大小更优先DMA 访问内存时要求地址按数据宽度对齐。按 Word 传输时缓冲区必须 4 字节对齐如果你把数据宽度设为 HalfWord则 2 字节对齐。F4 没有 D-Cache所以不需要像 H7 那样做 cache clean/invalidate但前提是缓冲不能定义成局部数组否则栈地址的不确定性会带来随机性死机。#define ADC_BUF_WORDS 128 static ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc; static uint32_t adc_buf[2][ADC_BUF_WORDS] __attribute__((aligned(4))); static volatile uint8_t dma_tc_flag; static volatile uint8_t dma_done_idx; void HAL_DMA_RxCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma-Instance ! DMA2_Stream0) { return; } /* CT1 时 DMA 正在写 buf[1]刚填满的是 buf[0] */ if (HAL_DMAEx_GetCurrentTarget(hdma) DMA_MEMORY1) { dma_done_idx 0; } else { dma_done_idx 1; } dma_tc_flag 1; }两个缓冲共 128 * 2 * 4 1KB对 F4 的内存来说很小。ADC_BUF_WORDS 并非越大越好它决定了中断频率和处理延迟一般取 64 到 2048 之间。NDTR 是 16 位寄存器单个 buffer 最多 65535 个数据项按 32 位传输就是每块最多约 256KB超过这个值只能拆成多个小的双缓冲周期。4.2 初始化与启动顺序先配置 DMA再启动 ADC整个初始化里最容易出错的是 HAL_ADC_Start_DMA 和 HAL_DMAEx_MultiBufferStart_IT 两条路径混用。前面第三章已经解释过生成代码的问题这里给出完整可编译的初始化函数void adc_dma_double_buffer_init(void) { GPIO_InitTypeDef gpio {0}; ADC_ChannelConfTypeDef cfg {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, gpio); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); cfg.Channel ADC_CHANNEL_0; cfg.Rank 1; cfg.SamplingTime ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(hadc1, cfg); hdma_adc.Instance DMA2_Stream0; hdma_adc.Init.Channel DMA_CHANNEL_0; hdma_adc.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc.Init.MemInc DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma_adc.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_adc.Init.Mode DMA_CIRCULAR; hdma_adc.Init.Priority DMA_PRIORITY_HIGH; hdma_adc.Init.FIFOMode DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma_adc); HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); /* 先让 DMA 以双缓冲方式等待请求 */ HAL_DMAEx_MultiBufferStart_IT(hdma_adc, (uint32_t)hadc1.Instance-DR, (uint32_t)adc_buf[0], (uint32_t)adc_buf[1], ADC_BUF_WORDS); /* 再启动 ADC转换完成后自动产生 DMA 请求 */ HAL_ADC_Start(hadc1); }HAL_DMAEx_MultiBufferStart_IT 的五个参数分别是 DMA 句柄、外设地址、Memory0 地址、Memory1 地址、单个 buffer 的数据项数量。对 ADC 来说外设地址是 ADC1-DR对定时器触发的 DAC 输出外设地址是 DAC-DHR12R1DMA 方向变成 Memory-to-Peripheral且请求通常由 TIM6 的更新事件触发主体配置思路完全一致。4.3 主循环消费数据时的边界条件双缓冲允许 CPU 在处理一块数据的同时让 DMA 填另一块但它只是一个两级流水线。如果主循环处理一块数据的时间超过了 DMA 填满一块缓冲的时间DMA 就会绕回来把正在处理的那块也覆盖掉。所以中断回调里只置标志位不在回调里做数据搬移和运算while (1) { if (dma_tc_flag) { dma_tc_flag 0; process_samples(adc_buf[dma_done_idx], ADC_BUF_WORDS); } }process_samples 是这个驱动留给应用层的处理函数可以放均值滤波、FFT、阈值判断。要注意 dma_done_idx 和 dma_tc_flag 都是 volatile主循环里先读标志再读索引如果你在处理过程中又一次触发了 TC 中断说明处理速度已经到极限此时调大 ADC_BUF_WORDS 会增加延迟真正的解法是降低采样率、减少单样本处理开销或者用三级缓冲队列换时间。5. 验证双缓冲在工作CT 位、GPIO 测频与三个常见坑5.1 用 GPIO 翻转频率判断双缓冲是否真的在切换最直接的验证手段是拿一个空余 GPIO 在 TC 回调里翻转用示波器或逻辑分析仪看频率。理论中断频率等于 ADC 实际采样率除以 ADC_BUF_WORDS。如果测出的频率是理论值的一半说明 DMA 没有在 M0/M1 之间正常交替如果只翻转了一两次就停住说明 Mode 被配成了 Normal传完两块后 DMA 已经停止。也可以用 DWT 或定时器在主循环里统计每秒中断次数这就是嵌入式圈常说的 DMA 测速中断频率稳定且等于理论值才能证明双缓冲路径是通的。把调试引脚放到回调里时GPIO 翻转本身也要消耗时间。频率很高时翻转代码的耗时会影响中断延迟建议观察完就移除或在 ADC_BUF_WORDS 足够大的情况下再做这个实验。5.2 三个常见坑Normal 模式、内存递增、处理过慢第一Mode 不是 Circular。很多工程模板把 DMA 初始化成 Normal进两次中断后整个采集停止现象是波形前面正常、后面直线。第二Memory Increment 被关掉。DMA 会反复往 buf[0] 的第一个字写入两个 buffer 看起来在切换实际数据全挤在第一个地址里。第三处理速度跟不上双缓冲。双缓冲只是躲开了“处理期间被覆盖”的问题不代表处理可以无限慢在回调里做 memcpy 或浮点运算是常见死因把确认标志置位、延迟到主循环消费才是正常做法。5.3 F4 与带 DMAMUX 的 H7 有什么不同从 F4 切到 STM32H7 时最明显的差异是多了 DMAMUX 这一层DMA 请求不再靠 stream 和 channel 的固定映射而是由 DMAMUX 把外设请求任意路由到 DMA stream。双缓冲本身的配置思路不变但 H7 的 D-Cache 会带来新的问题DMA 写入的内存如果仍在 Cache 里CPU 读到的可能是旧数据。F4 没有 Cache反而省掉这层麻烦若在 H7 上用双缓冲做 DDS 波形生成需要为 DMA buffer 配置 MPU 区域为 Non-cacheable或在每次切换时做 cache 维护。F4 上做双缓冲DDS 时还要注意定时器位数F4 的 TIM6 是 16 位自动重载相位累加超过 65535 后需要软件扩展否则高精度波形会周期性跳变。先把 F4 的 DBM/CT 和中断时序吃透这些边界问题就都能顺藤摸瓜查出来。本文还有配套的精品资源点击获取
返回列表