3、低功耗架构设计:分帧处理、乒乓缓冲、事件驱动调度
好,咱们进入第三章。这一章讲的是低功耗架构的三大支柱——分帧处理、乒乓缓冲、事件驱动调度。说实话,这三个东西单独拎出来都不难,但把它们组合成一个高效的系统,才是真正的功夫。
我在助听器项目里见过太多方案,算法跑得挺溜,但功耗一测就崩。为什么?因为架构没想清楚。你想想看,一个音频算法再牛,如果架构设计不合理,电池撑不过半天,那产品就是废的。
3.1 分帧处理:为什么非分不可?
音频信号是连续的,但处理器不是。它只能一段一段地吃数据。这就是分帧处理的根本原因。
我个人习惯把帧长设在 8ms 到 20ms 之间。太短了,比如 2ms,中断来得太频繁,CPU 大部分时间都在上下文切换,功耗反而上去了。太长了,比如 40ms,延迟就大了,助听器用户会感觉「声音慢半拍」,很难受。
经验值参考:
| 应用场景 | 推荐帧长 | 延迟感受 |
|---|---|---|
| 助听器(高端) | 8ms | 几乎无感 |
| 助听器(中端) | 12ms | 轻微可接受 |
| 耳机通话 | 16ms | 正常范围 |
| 语音唤醒 | 20ms | 可接受 |
我在项目中遇到过一个问题:采样率 48kHz,帧长 8ms,那一帧就是 384 个采样点。但算法模块要求 512 点 FFT。怎么办?硬补零?不行,会引入频谱泄漏。后来我用了重叠保留法,帧与帧之间留 50% 重叠,既满足了 FFT 长度,又保证了连续性。
小技巧:分帧时别忘了考虑「帧偏移」。比如帧长 8ms,帧移 4ms,那相邻两帧就有 4ms 的重叠。这样处理出来的声音更平滑,不会出现「咔咔」的拼接声。
3.2 乒乓缓冲:双倍内存,零等待
分帧之后,数据怎么喂给算法?最笨的办法是:等一帧采集完,然后处理,处理完再采下一帧。但这样 CPU 就闲着了,浪费。
乒乓缓冲就是解决这个问题的。说白了,就是准备两个缓冲区。一个给 DMA 写数据,一个给 CPU 读数据。写满一个,交换角色。
// 伪代码示例:乒乓缓冲切换
#define FRAME_SIZE 384
int16_t ping_buffer[FRAME_SIZE];
int16_t pong_buffer[FRAME_SIZE];
volatile uint8_t active_buffer = 0; // 0: ping, 1: pong
void DMA_IRQHandler() {
// DMA 写完了当前缓冲区
active_buffer ^= 1; // 切换
// 通知算法模块处理另一个缓冲区
process_audio(active_buffer ? ping_buffer : pong_buffer);
}
嗯,这里要注意:乒乓缓冲的关键是「谁在写,谁在读」不能冲突。我见过一个同事,DMA 还在写,CPU 就去读了,结果读出来一半新数据一半旧数据,声音直接炸了。后来加了双缓冲互斥锁才解决。
避坑指南:我曾经在低功耗芯片上踩过一个坑——DMA 传输完成后触发中断,中断里切换缓冲区。但中断优先级设得太高,把算法任务给抢了。结果算法还没处理完,DMA 又把新数据写进来了,缓冲区被覆盖。解决方案:中断里只做「标记」,真正的处理放到主循环或任务调度里。
乒乓缓冲的代价是内存翻倍。但换来的是 CPU 利用率接近 100%,没有等待时间。对于助听器这种实时性要求高的设备,这笔买卖划算。
3.3 事件驱动调度:别让 CPU 空转
有了分帧和乒乓缓冲,数据流已经通了。但 CPU 什么时候干活?
最原始的做法是轮询——CPU 不停地问「数据好了没?好了没?」。这在低功耗场景下是灾难。CPU 空转也在耗电,而且耗得不比干活少。
事件驱动调度就是:没事干就睡觉,有事干再醒来。
我习惯把系统设计成这样:
- 空闲态:CPU 进入深度睡眠,只留 RTC 和 DMA 工作。功耗可以降到微安级别。
- 事件触发:DMA 写完一帧数据,触发中断。中断里设置一个事件标志,然后唤醒 CPU。
- 任务处理:CPU 醒来,检查事件标志,执行对应的算法模块。处理完再睡回去。
事件优先级设计:
- 最高优先级:音频数据就绪(必须立即处理,否则丢数据)
- 中等优先级:用户按键/旋钮操作(响应要快,但可以稍微延迟)
- 低优先级:电量检测、日志上报(可以等空闲时再做)
你想想看,如果 CPU 每 8ms 只醒来工作 2ms,那它的占空比只有 25%。剩下的 75% 时间都在睡觉。功耗直接降到原来的四分之一。
我在项目中遇到过一个问题:事件太多,CPU 刚睡下又被唤醒,根本睡不踏实。后来我用了「事件合并」——把多个小事件攒到一起处理。比如按键事件,不立即响应,而是等下一个音频帧处理完再一起处理。用户感觉不到延迟,但 CPU 的睡眠时间大大增加了。
实战建议:事件驱动调度最好配合 RTOS 使用。比如 FreeRTOS 的 vTaskDelayUntil() 可以精确控制任务周期。但如果你用的是裸机,那就用状态机 + 事件标志位,效果也不差。
3.4 三者如何协同?
分帧处理、乒乓缓冲、事件驱动调度,这三个不是孤立的。它们是一个整体。
我画个流程你就明白了:
- 麦克风采集音频,DMA 自动写入乒乓缓冲区(当前活跃的 buffer)
- DMA 写满一帧,触发中断。中断里切换缓冲区,并设置「数据就绪」事件
- 事件调度器被唤醒,检查到「数据就绪」事件,调用算法处理函数
- 算法从另一个缓冲区读取数据,执行降噪、增益、反馈抑制等处理
- 处理完的数据送入 DAC,播放出去
- CPU 再次进入睡眠,等待下一个事件
这个流程里,每一步都是事件驱动的。没有轮询,没有空转。功耗控制得死死的。
注意:事件驱动调度有一个隐藏陷阱——事件丢失。如果 CPU 在处理一个事件时,另一个事件来了,但没来得及响应,就可能丢数据。我的做法是:用位图记录事件状态,每个事件一个 bit。处理完一个,清除对应的 bit。如果发现还有 pending 的事件,继续处理,不急着睡觉。
好了,这一章就讲到这里。分帧处理是基础,乒乓缓冲是桥梁,事件驱动调度是灵魂。三者缺一不可。下一章我们会聊具体的算法模块怎么嵌入到这个架构里,到时候你会更深刻地理解为什么架构设计这么重要。