Vol. NXP SOF Arch

把音频处理搬进 DSP:i.MX8M Plus 上的 SOF 架构解析

做嵌入式音频久了,会形成一种肌肉记忆:一条播放链路,无非是 AudioFlinger 把 PCM 推给 HAL,HAL 通过 tinyalsa 写进内核的 ASoC,CPU DAI 驱动(在 NXP 平台上就是 fsl_sai.c)配置好 SAI 的 BCLK/帧同步/TDM,把数据从 FIFO 经 SDMA 送进 I2S 总线,codec 把数字流变成模拟声音。整条链路里,SAI 这个硬件是内核驱动直接控制的

SOF(Sound Open Firmware)把这套肌肉记忆打破了。

i.MX8M Plus 上有一颗 Cadence HiFi4 DSP。SOF 的核心主张是:让音频处理------混音、音量、EQ、采样率转换、甚至 MP3/AAC 解码------都跑在这颗 DSP 上,A53 主核只负责把压缩或 PCM 数据丢过去,然后睡觉。这意味着 SAI 这个外设的控制权,要从内核手里交到 DSP 固件手里。

这篇文章讲两件事:SOF 是怎么嵌进 ASoC 框架、并悄悄夺走 SAI 控制权的 ;以及DSP 固件这一侧,音频到底是按什么逻辑被处理的。两件事其实是一枚硬币的两面------理解了控制权的转移,就理解了为什么 DSP 侧要长成那个样子。

平台:i.MX8M Plus EVK / FRDM,HiFi4 DSP,SAI3 接 WM8960/WM8962 codec。所有结论都来自本仓库内核 sound/soc/sof/ 与固件 external/sof/src/ 的实际代码。


一、SOF 在 ASoC 里的位置:一个反转

内核侧退化成了 IPC 代理

传统 ASoC 里,CPU DAI 驱动是干实事的。fsl_sai.c 的 DAI ops 里,hw_params 会去写 TCR/RCR/CR4/CR5,set_tdm_slot 配 slot 掩码,trigger 拉起 TERE 使能位。这些寄存器操作是音频能出声的关键。

SOF 模式下,这一层被彻底掏空了。

SOF 把自己注册成一个 ASoC platform component ,名字叫 sof-audio-componentcore.csof_probe_continuedevm_snd_soc_register_component)。它实现了完整的 snd_soc_component_driver ops------open / hw_params / trigger / pointer / hw_free 一个不少。但你去翻这些回调的实现就会发现,它们没有一行碰 SAI 寄存器

  • sof_pcm_hw_params → 组装一个 sof_ipc_pcm_params 结构,通过 sof_ipc_tx_message 发一条 SOF_IPC_STREAM_PCM_PARAMS 给 DSP;
  • sof_pcm_trigger → 发 SOF_IPC_STREAM_TRIG_START/STOP;
  • sof_pcm_pointer → 不是去读 SAI 的计数器,而是从共享内存的 mailbox 里读 DSP 写回来的 host_posn

换句话说,内核侧的 PCM 操作全部被翻译成了发给 DSP 的消息。SOF 的 component 就是一个 IPC 代理,真正的活在 DSP 固件里。

那 CPU DAI 呢?一个空壳

既然 ASoC 的 DAI link 还要求有一个 CPU DAI,SOF 怎么应付?答案是给一个空壳。imx8m.c 里静态定义了 imx8m_dai[]

c 复制代码
static struct snd_soc_dai_driver imx8m_dai[] = {
    { .name = "sai1", .playback = {...}, .capture = {...} },
    { .name = "sai3", .playback = {...}, .capture = {...} },
    { .name = "micfil", .capture = {...} },
};

注意这几个 DAI driver 没有 .ops 字段 ------没有 hw_params,没有 trigger,没有 set_tdm_slot。它纯粹是个名字占位符,让 ASoC 框架在绑定 DAI link 时有东西可绑。对比 fsl_sai.c 里那个塞满寄存器操作的 DAI,差距一目了然:SOF 路径下根本没有一个会去碰 SAI 的 CPU DAI

这就是那个"反转":SAI 的 BCLK、帧同步、TDM 配置,全部由 DSP 固件直接编程。内核连 SAI 的寄存器基址都不再关心了------在 DTS 里 sai3 节点的状态甚至是 disabled

声卡从哪来:audio-graph-card2 + ignore_machine

这里有个容易被 AI 生成的文档讲错的点。很多说法是"SOF 通过 sof_machine_register() 程序化地创建声卡"。在 i.MX8M Plus 上这是错的 ------sof_imx8m_ops 压根没有 .machine_register 回调,snd_sof_machine_register() 直接返回 0,什么都不做。

真正的声卡来自 DTS 里的 audio-graph-card2 节点:

dts 复制代码
sof-sound-wm8960 {
    compatible = "audio-graph-card2";
    links = <&cpu>;
};

它把 codec 端点(wm8960/wm8962)绑到 DSP 节点上的 CPU port。那 SOF 的 component 和这个独立探测出来的声卡怎么共存?靠 ASoC 的 ignore_machine 机制:snd_sof_new_platform_drv 里把 pd->ignore_machine 设成 "asoc-audio-graph-card2",告诉 DPCM 核心"让这张卡和我的 component 并存,别替我建卡"。

所以一条完整的链路实际上是这样拼起来的:

  • 声卡 / DAI link ← DTS 的 audio-graph-card2
  • PCM / 数据搬运 ← SOF platform component(IPC 代理)
  • codec 控制(I2C、FLL/PLL、DAPM、jack) ← 内核的 wm8962.c / wm8960.c这部分没变
  • SAI 数据通路 ← DSP 固件

值得强调最后两条的分工:codec 还是老老实实由内核驱动通过 I2C 控制时钟和路由,DSP 只接管了 I2S/SAI 这条数字数据通路。两者在物理上同接 SAI3,逻辑上各管各的。

启动与 IPC:把固件 memcpy 进 OCRAM

DSP 的引导比想象的朴素。imx8m_probedevm_ioremap 把几块地址映射进来:DSP 的 OCRAM/IRAM、memory-region 指过去的一块保留 DDR(0x92400000,32MB,做数据缓冲)、还有 DAP 调试寄存器。固件加载不走 DMA------就是一次 __iowrite32_copy,把 .bin 直接 memcpy 进 OCRAM,然后解复位让 HiFi4 跑起来,等它通过 mailbox 回一个 FW_READY。

A53 和 DSP 之间靠 i.MXMU(Messaging Unit) 通信。DTS 里 DSP 节点的 mboxes = <&mu2 0 0>, <&mu2 1 0>, <&mu2 3 0> 对应 tx / rx / rxdb 三个通道。内核发 IPC 时 imx_dsp_ring_doorbell 敲 MU 的 doorbell,DSP 收到中断后从共享 mailbox 取消息。位置上报、stream 触发、参数下发,全走这条 MU 通道。i.MX8M Plus 用的是 IPC3 协议(不是 Intel 新平台的 IPC4)。


二、DSP 侧:音频是怎么被处理的

控制权交到 DSP 手里之后,问题就变成:固件这一侧拿什么模型来组织音频处理?SOF 的答案是一套 组件(component)---缓冲(buffer)---流水线(pipeline)---调度(scheduler) 的体系。拓扑文件(.tplg)下发后,DSP 据此把这套图搭起来,然后由调度器以固定节拍驱动它跑。

组件:处理的最小单元

每个处理单元是一个 struct comp_devaudio/component.h),它持有运行状态(state、当前 pipeline 指针、上下游缓冲链表 bsource_list / bsink_list)。组件的行为由一组 comp_ops 定义,关键的几个是:

  • create / free --- 实例化与销毁
  • params --- 协商格式(采样率、声道、位深)
  • prepare --- 跑前准备(分配缓冲、复位状态)
  • trigger --- start/stop/pause
  • copy --- 核心:处理一个 period 的数据

组件类型通过 comp_register() 把自己的 comp_driver 挂进全局链表,通常由 DECLARE_MODULE 注册的 init 函数完成。运行时 comp_copy() 就是去调 dev->drv->ops.copy(dev)。整个处理体系的本质,就是把一堆 copy 按正确顺序串起来反复调用。

流水线:用缓冲把组件连成图

组件之间靠 struct comp_buffer 连接。pipeline_connect() 把一个 buffer 挂到上游组件的 sink 链表、下游组件的 source 链表,并设好 buffer->source / buffer->sink 指针。于是一条播放链路在 DSP 内部就是:

scss 复制代码
host(从 DDR 取数) → buf → volume(PGA) → buf → dai(推向 SAI)

这正好对应拓扑里 pipe-volume-playback.m4 定义的图。处理顺序不是写死的,而是由 pipeline_for_each_comp() 沿着 buffer 链表走出来的。pipeline_copy() 对播放从 sink_comp(即 dai)出发往上游 走,对采集从 source_comp 出发往下游走------方向相反,但都保证数据被生产者算完再被消费者取走。

调度:固定节拍把数据"拉"过去

DSP 不是事件驱动地处理音频,而是按固定周期拉 。SOF 的低延迟(LL)调度器有两种节拍源:定时器域(SOF_SCHEDULE_LL_TIMER)和 DMA 域(SOF_SCHEDULE_LL_DMA),由 pipeline_is_timer_driven() 决定用哪个。

节拍周期等于流水线的 period,低延迟流上通常是 1ms 。每次 tick 触发,调度器跑 pipeline_task()pipeline_copy(),把一个 period 的音频从图的一头拉到另一头:host 组件从 DDR 取一帧,volume 处理,dai 组件交给链路 DMA 送进 SAI FIFO。下个 tick 再来一帧。整条链路的实时性,就建立在"每 1ms 必须算完一个 period"这个硬约束上。

两端的搬运:一个反常识的"双 DMA"

流水线的两个端点------host 和 dai------各自负责把数据搬进/搬出 DSP。这里有 i.MX8M Plus 上一个很容易被讲错的细节:两端用的 DMA 根本不是一回事

Host 端(DDR ↔ 流水线缓冲):其实没有 DMA。platform/imx8m/lib/dma.c,host DMA 这一项的 .ops = &dummy_dma_ops------一个"假"DMA 驱动,实现就是同步的 memcpy_s()。原因很实在:HiFi4 对 DDR 有直接的内存映射访问,A53 把 PCM 写进那块保留 DDR(就是前面 probe 时 ioremap 的 0x92400000),DSP 直接 memcpy 过来就行,犯不着动用硬件 DMA 引擎。host 组件每搬够一个 host period,就把位置写进 mailbox 并发 IPC 通知内核------这就是前面 sof_pcm_pointer 读到的 host_posn 的来源。

DAI 端(流水线缓冲 ↔ SAI FIFO):真 DMA,走 SDMA3。 同一个文件里,SAI 这条路的 .ops = &sdma_ops,.devs = DMA_DEV_SAI,.base = SDMA3_BASE。这条路是硬实时的------SAI FIFO 会以采样率的节奏被掏空,必须靠硬件 SDMA 精确续上,CPU 拷贝顶不住。

scss 复制代码
   A53 写入                  DSP 处理               硬件节拍
DDR ──(CPU memcpy)──→ pipeline buf ──(SDMA3)──→ SAI3 FIFO ──→ I2S ──→ codec
   ↑ dummy dma                          ↑ 真 DMA

记住这个分工:离 DSP 近的一端是软拷贝,离硬件外设近的一端是硬 DMA。这不是偷懒,而是对各自约束的合理回应。

SAI 由谁编程:坐实那个"反转"

回到第一节留的悬念。drivers/imx/sai.csai_set_config() 实打实地在写 SAI 寄存器:

  • REG_SAI_XCR2 --- 位时钟极性、主从方向、时钟源(MCLK1)、分频
  • REG_SAI_XCR4 --- 帧同步使能/极性/方向、TDM slot 数(FRSZ)、同步字宽
  • REG_SAI_XCR5 --- 字宽
  • REG_SAI_XMR --- TDM slot 掩码
  • i.MX8M 上还会写 REG_SAI_MCTL 使能 MCLK 输出

sai_start() 拉起 FRDE(DMA 请求)、TRCE(数据通道)、TERE(收发使能)。这些寄存器,在传统链路里是 fsl_sai.c 写的,现在是 DSP 固件写的------内核侧那个空壳 DAI 一个比特都不碰。第一节说的"反转",到这里完全坐实。

一个细节呼应:拓扑里 SAI_CLOCK(bclk, 3072000, codec_master) 把 BCLK/帧同步的 provider 设成了 codec。也就是说 SAI 在这套配置里其实是从模式 ,真正产生时钟的是 WM8960/8962 的 FLL------而 codec 的 FLL 仍由内核 wm896x.c 通过 I2C 配置。DSP 管数据搬运,内核管时钟源,各司其职。

可用的处理组件

DSP 侧能插进流水线的处理组件就是音频工程师熟悉的那些,且大多有 HiFi4 向量优化版本:

  • volume --- 带斜坡的音量/增益
  • src --- 多相滤波采样率转换(src_hifi4.c)
  • eq_iir / eq_fir --- 双二阶级联参数 EQ / FIR EQ
  • drc --- 动态范围压缩
  • mixer / mixin_mixout --- 多路相加混音(后者是 IPC4 风格的拆分/合并)

拓扑里那个 PPROC 参数(volume / eq-iir-volume / eq-fir-volume / drc),决定的就是流水线中间插哪个处理组件------同一份 M4 源,换个 -DPPROC= 就编出一个不同的 .tplg。

还有一个分量很重的:Cadence codec adapter (module_adapter/module/cadence.c)。它通过 module_adapter 框架把 Cadence 的 HiFi4 解码库(AAC / MP3 / SBC / Vorbis 等)包装成标准 SOF 组件,插在 host 之后。这就是"压缩 offload"的实现------A53 把 MP3 码流直接丢给 DSP,解码、音量、混音、送 SAI 全在 DSP 上完成,主核基本不参与。对应的拓扑是 sof-imx8mp-compr-wm8960-mixer.m4,它用 mixer 把 PCM 流和解码后的压缩流合到一起,且 DAI period 数从 2 提到 8 以吸收解码抖动。


三、回头看这条链路

把两节连起来,一次 MP3 播放的完整旅程是这样的:

  1. AudioFlinger 经 HAL、tinyalsa 把码流写进内核;
  2. SOF 的 platform component 不解码、不碰 SAI,只把数据指针和 PCM_PARAMS / TRIG_START 打包成 IPC,经 MU 发给 DSP;
  3. DSP 固件按拓扑搭好的流水线,用 LL 调度器每 1ms 拉一个 period:host 组件从 DDR 软拷贝取数 → Cadence adapter 解码 → mixer 混音 → volume → dai 组件;
  4. dai 组件直接编程 SAI3 寄存器,并通过 SDMA3 把 PCM 续进 SAI FIFO;
  5. SAI 在 codec(FLL 主时钟)的节拍下把数据打上 I2S,codec 还音;
  6. DSP 周期性把播放位置写回 mailbox,内核 sof_pcm_pointer 读出来上报给上层。

SOF 带来的本质改变,是把音频处理的"主语"从 CPU 换成了 DSP:内核退化为控制面与 IPC 代理,数据面与外设控制权整体下沉到固件。理解这一点,前面那些看起来反常识的设计------空壳 CPU DAI、disabled 的 SAI 节点、host 端的 dummy DMA、SAI 的从模式------就都顺理成章了。它们不是奇技淫巧,而是"处理搬进 DSP"这个决定的必然推论。


平台:i.MX8M Plus / HiFi4 / SOF(基于 v2.5)。本文结论均取自本仓库 sound/soc/sof/external/sof/src/ 实际源码,涉及具体寄存器与函数名处可按文中线索回查。

相关推荐
用户805533698032 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
荣--5 天前
在 strip 二进制 + 基址随机化的栈里做崩溃去重 —— 三阶段算法与一行 Crash Flag
嵌入式·崩溃分析·栈指纹·去重算法
释然小师弟5 天前
Android开发十年:反思与回顾
android·后端·嵌入式
FreakStudio6 天前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
bush411 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
国产化创客11 天前
ESP32 CameraWebServer 原生摄像头项目全解析
物联网·开源·嵌入式·实时音视频·智能硬件
goldenrolan11 天前
学习型红外控制系统稳定性挂测工装专项总结
软件测试·python·stm32·嵌入式·红外
w4ysonch11 天前
我手搓了一套适用于任何嵌入式项目的跨线程通信API
嵌入式
海砥装备HardAus11 天前
大载重工业无人机动力容错控制:单电机失效下的应急重构算法设计
算法·重构·嵌入式·无人机