ARTICLE DETAIL

资讯详情

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

SOF音频固件与Topology源码编译实战指南

SOF音频固件与Topology源码编译实战指南 如果你已经折腾过 Linux 音频、做过开发板上的声卡适配那多半听过 SOF 这个名字。SOF全称 Sound Open Firmware是一套运行在 DSP 上的开源音频固件框架简单说就是英特尔平台以及部分 ARM 平台用来干音频信号处理、语音唤醒、HDMI/DP 音频、DMA 搬运这些脏活累活的底层系统。正文里这个“从源码编译 SOF 固件与 topology”的标题基本就是进阶玩家绕不开的一个坎。预编译固件固然能跑但一旦你要改 DSP 行为、开实验性特性、调声卡路由或者接入一块冷门板卡预编译包就露馅了。这篇文章我就从拿到源码开始把 SOF 固件和 topology 的整套编译流程、目录逻辑、参数选择、烧录调试方式以及我实际踩过的一堆坑一次性讲清楚。适合已经有 Linux 驱动、嵌入式开发基础想进一步深入 SOF 或者正在做音频 DSP 适配的同学参考。1. SOF 项目整体认识1.1 SOF 到底解决了什么问题过去在 x86 平台上音频 DSP 基本被各家闭源固件垄断。板子上的 DSP 芯片出厂刷好一坨二进制驱动只能通过固定的 IPC 接口去调用想改里面的算法、增删 pipeline、调整路由基本没门。SOF 想做的是把这一层全部开源DSP 上跑的是一个轻量级 RTOS上面挂音频处理组件主机侧有标准 ALSA 驱动配合两者之间用 IPC 通信。这样声卡的采集、播放、混音、回声消除、关键词唤醒等能力都变成了可以编译、可以配置、可以替换的软件组件。对你做开发来说这个改变最直接的好处有三点。第一固件逻辑是透明的出问题可以看日志、看代码不需要对着黑盒猜。第二平台适配灵活一个平台换一块 Codec、改一路 DMIC大部分时候只要改 topology 而不用动固件。第三可以按需裁剪DSP 内存就那么大不需要的功能在编译时直接关掉能把 footprint 压得很低。但这也意味着别人编好的固件未必适合你的板子。源码编译在这个项目里不是“想折腾才去折腾”而是做板级移植、功能调试、产线定制时的刚需。1.2 固件与 topology 的分工关系初次接触 SOF 的人很容易把“固件”和“topology”搞混以为固件里已经定义了所有音频路径。实际上它们是两层东西。固件通常编译产出一个 .ri 文件是跑在 DSP 上的程序主体它包含内核调度、IPC 处理、以及一堆音频模块的实现代码。但固件里并不会写死“哪条 DMA 接到哪个 DAI”“这个 widget 连到那个 widget”这些运行时拓扑信息全部由 topology 提供。topology 本质上是一份描述音频图audio graph的配置数据。它告诉内核和 DSP系统里有哪些 PCM 设备、有哪些 DAI 接口、pipeline 怎么连、每个 pcm 用几个 dma buffer、格式是多少、增益范围多少、是否启用 tone 等。这份数据最终交给内核的 ALSA topology 框架解析再由驱动通过 IPC 发给 DSP 固件加载配置。所以流程上很清楚先编固件再编 topology两个都部署到目标机驱动启动时先加载固件再加载 topology。固件错了DSP 跑不起来topology 错了声卡能 probe 但音频路径必然有问题。这两个东西必须版本匹配混搭最容易出诡异问题。2. 编译环境准备2.1 工具链选择与安装SOF 固件的目标平台主要是 Xtensa 架构的 DSP少数新平台开始用 RISC-V 或者自研 DSP 核。因此你本机需要安装对应的交叉编译工具链而不是直接用系统自带 gcc。SOF 仓库里提供了自动下载工具链的脚本路径是scripts/xtensa-build-all.py可以直接通过参数触发工具链安装。我在 Ubuntu 22.04 上的实测流程是这样的。先确保系统里有 cmake、gcc、g、make 这些基础包然后进入 sof 目录执行sudo apt install -y cmake gcc g make patch python3-pip python3 scripts/xtensa-build-all.py -t-t参数会检查并下载当前源码版本对应的 Xtensa 工具链安装到脚本约定的目录下。不同平台对应的工具链前缀也不一样比如 APL 平台通常用xtensa-apl-elf-TGL 平台用xtensa-cnl-elf-。这一步看着简单但网络不好时下载容易中断脚本重跑会断点续传一般多试两次就能过。需要注意工具链版本和 SOF 源码版本是有对应关系的。不要拿老版本工具链编新固件否则会在编译过程中报一堆莫名其妙的链接错误。如果你是从 git 拉的主线代码务必用仓库里记录的最新工具链版本。2.2 获取源码与子模块SOF 的代码分散在多个仓库核心是thesofproject/sof里面包含固件源码、build 脚本、以及 tools 子目录。另外拓扑定义虽然现在也收在主仓库的tools/topology下但历史上一段时间是独立仓库所以 clone 时一定要带--recursive拉子模块不然编译的时候会发现缺少一堆头文件和 m4 宏文件。我习惯的做法是这样git clone --recursive https://github.com/thesofproject/sof.git cd sof git submodule update --init --recursive如果你已经 clone 过但没拉子模块第二句命令就能补救。这里尤其提醒一句xtensa-build-all.py默认会假定子模块完整缺了东西时它报错信息还不直观往往让你以为是工具链装坏了。先确认子模块状态能省掉一大半烦恼。另外拿到源码后先看一眼git tag -l找最近发布版本尽量切到 release tag 上。主线代码可能包含正在开发的内容稳定性不如发布版本。比如git checkout v2.9.0 git submodule update --init --recursive这个操作会把子模块也切到对应版本保证整体一致性。3. 固件编译流程3.1 编译命令与输出产物SOF 固件编译脚本用起来很直白核心就是xtensa-build-all.py。指定平台指定核数跑就完了。比如我想编 TGL 平台的固件python3 scripts/xtensa-build-all.py -p tgl -j 8如果你不指定平台脚本会列出当前支持的平台列表。常见的像apl、cnl、icl、tgl、mtl、adl等。-j指定并行编译任务数我一般给 8太快内存不够容易 OOM。编译成功后固件会出现在类似build_tgl_toolchain/sof-tgl.ri的路径下。文件名里的tgl是平台名.ri是通过 rimage 工具封装后的最终镜像里面包含了签名、元数据和固件主体。这个.ri文件才是最终要部署的目标裸的 elf比如sof-tgl.elf通常只是用来调试和看符号表。我之前编译时碰到过一个困惑编完没看到.ri只看到.elf。原因通常是 rimage 步骤失败了多半是因为平台签名 key 没找到或者版本号格式不对。这时候看构建日志最后几百行就能定位一般不是大问题。3.2 影响固件形态的关键配置项SOF 固件不像内核那样有交互式 menuconfig但它在编译时也支持通过CONFIG_*宏来控制功能开关。这些宏定义在顶层Kconfig体系里编译脚本会生成autoconf.h你在源码里经常能看到#ifdef CONFIG_X这种条件编译。实际开发中最常用的几个调整点包括平台 SPI、I2S、DMIC、SDW 等接口模块决定固件能驱动哪些物理外设日志等级和日志后端CONFIG_DEBUG_IPC、CONFIG_DEBUG_LOGS等开低了定位问题缺少线索开高了固件体积和内存占用会变大是否有 HDA 或 DW-DMA 控制器驱动涉及到 PCM 数据搬运方式是否启用某些算法库比如CONFIG_IIR_FIR、CONFIG_AGC等改这些配置有两种路径。一种是在编译命令里直接覆盖另一种是直接改源码里的默认配置。更推荐的做法是使用sof/app/下面按平台定义的配置文件把你的定制项单独放进去不要东改一下西改一下不然下次拉新代码合并冲突让你头疼。如果你只是给现成板卡编一个“能用的固件”默认配置基本就够了。但如果要减内存、加日志、关掉用不上的模块那就需要认真过一遍这些配置项这也是源码编译相对预编译固件最大的优势。4. topology 编译流程4.1 topology 的生成原理先说个容易误导人的地方topology 的源文件并不是一段直接能加载的二进制配置而是一堆扩展名为.m4的文本宏定义文件。这些文件经过 m4 预处理器、再配合 alsa-lib 的 topology 工具alsatplg最后才生成一个二进制.tplg文件。至于为什么要搞 m4 这一层而不是直接写二进制配置原因也很实际。音频拓扑往往有大量重复结构比如四个声道的 pcm 配置除了 channel index 不同其他字段完全一样。用宏抽象出来写一次展开多次维护成本低得多也不容易手抖写错。你可以在tools/topology/topology1目录里看到大量m4文件它们按平台、按用途分类比如sof-hda-generic.m4、sof-tgl.m4、sof-apl-nocodec.m4等。编译 topology 不依赖 Xtensa 工具链你只要装好alsa-tools和alsa-lib开发头文件就行。在 Ubuntu 上直接sudo apt install alsa-tools libasound2-dev然后进入tools/topology目录执行make就会根据 Makefile 里规划的列表生成对应的 tplg 文件。生成产物默认也会拷贝到固件同级的构建目录里。4.2 修改一个拓扑并编译的实操思路真正需要你动手写 m4 的场景通常是板卡的声卡路由和默认模板不一致。我这里拿一个典型需求举例想把默认的 HDMI 播放路径加一个一路 DMIC 的采集管线。大致的操作节奏是第一找到对应平台的默认 m4 文件。比如 TGL 平台看sof-tgl.m4里面会拼装出整个声卡中 pcm 设备、pipeline、DAI 互联的定义。第二仿照现有的SECTION_PCM_PLAYBACK宏加一个SECTION_PCM_CAPTURE段落。第三在pipeline段里引入 DMIC 对应的 dapm widget 和 dai 定义。第四确认 PCM ID 没有和现有配置冲突然后重新执行 make。写完后编译新的 tplg 文件会生成到拓扑构建目录。部署时注意和固件保持一致版本用alsatplg -c 1 -v 1 file.tplg之类的参数可以校验文件语法哪怕不放到目标机上也能先验一把能提前暴露很多低级错误。这里我想特别提醒一个坑修改 m4 时管道里的 buffer 大小、格式、位宽这些字段一定要和 PCM 配置、DAI 的能力对上。你定义了一个 32 位格式的 pipeline但 DAI 只支持 16 位固件加载时往往不报错真正录音或播放时数据就全乱了。这类问题排查起来非常隐蔽因为它在 dmesg 里几乎不留痕迹。5. 部署与调试5.1 固件与拓扑的部署路径x86 平台上内核通过snd-intel-dspcfg、snd-sof-intel-hda-common等驱动加载 SOF 固件。固件和拓扑的查找路径默认都在/lib/firmware/intel/sof/下面。其中固件文件名一般形如sof-tgl.ri拓扑文件名形如sof-tgl.tplg驱动会根据 PCI 设备 ID 自动拼出它要加载的固件名。如果你改了名字或者放在别的路径就需要通过模块参数或内核配置去指路径早期调试时我建议直接用默认位置少给自己找麻烦。部署命令很简单sudo cp build_tgl_toolchain/sof-tgl.ri /lib/firmware/intel/sof/ sudo cp build_tools/topology/sof-tgl.tplg /lib/firmware/intel/sof/ sudo update-initramfs -u最后这步update-initramfs容易漏尤其是你用了 initramfs 引导的环境。不更新的话重启后系统加载的还是 initramfs 里的旧固件你当场改了却发现没生效还以为是版本没编译进去白白浪费时间。部署完成后重启或者重新加载相关模块。最直接的方式sudo modprobe -r snd_sof_intel_hda_common sudo modprobe snd_sof_intel_hda_common5.2 日志核查与功能验证加载完模块第一件事不是急着去aplay而是看内核日志确认固件和拓扑的加载情况dmesg | grep -i sof dmesg | grep -i topology正常的日志里能看到类似sof-audio-pci-intel-tgl 0000:00:1f.3: firmware: direct-loading firmware intel/sof/sof-tgl.ri以及topology: ABI X.Y.Z这样的信息。如果在这里发现/lib/firmware/intel/sof/sof-tgl.ri加载失败优先怀疑文件路径、文件名或者权限。固件没问题之后用aplay -l和arecord -l看声卡设备节点是否枚举出来。如果声卡节点出现但播放没有声音先把拓扑、PCM 格式、DAI 这几个因素按顺序排查。经常出现的一种情况是默认拓扑把某个 DAI 的格式配成了 16 位但你用aplay参数传了-f S32_LE这就会导致数据路径不匹配表现就是无声或爆音。更深入的调试手段是用sof-logger抓 DSP 内部日志。SOF 固件编译时会生成.ldc文件log dictionary file它像符号表一样把 DSP 日志里的文件行号翻译成可读信息。加载固件时驱动会尝试加载/lib/firmware/intel/sof/sof-tgl.ldc然后你本机用 sof-logger 就能看到 DSP 侧的动态日志。这一步对定位 topology 加载失败、pipeline 创建失败等问题特别有用。6. 常见问题与排查技巧6.1 编译阶段典型问题我在给不同平台编固件时遇到最多的一个是子模块缺失导致的编译失败。报错往往是“找不到 header”或者“undefined reference”看起来像是代码不完整其实只是子模块没拉下来。再一个是工具链选错。不同平台对应的 Xtensa 配置不同比如 APL 和多核平台的工具链不能混用。用错工具链时会直接编不过去或者编出了产物但链接地址错误烧进去跑不起来。排查最有效的办法就是看xtensa-build-all.py日志开头打印出的工具链路径确认是不是你预装的那个。还有一个很典型的坑是并行编译资源不够。-j 8甚至更大核数一旦内存小于 16GB编译过程中常有进程被 OOM 杀掉。所以我现在都是先看机器内存再定核数稳妥优先。6.2 运行时加载失败排查固件加载失败时dmesg 常见几种表现。一种是err: request firmware intel/sof/sof-tgl.ri failed这种先检查文件和路径一种是error: failed to load firmware通常是镜像格式和驱动版本不匹配比如用了老版本驱动去加载新版本格式的固件还有一种是timeout waiting for DSP boot大概率是固件本身损坏、版本不匹配、或者 memory window 配置有问题。拓扑加载失败的话日志里会有tplg相关字样还可能直接导致声卡节点消失。此时先确认 tplg 文件版本和固件 ABI 是否兼容。SOF 固件和 topology 各有一个 ABI 版本号主版本不一致几乎必然失败。我自己排查这类问题有一个固定顺序先dmesg | grep -i sof看固件加载再dmesg | grep -i tplg看 topology然后cat /proc/asound/cards看声卡是否注册最后再考虑用 sof-logger 深入。按这个顺序走一遍大多数问题都能在几分钟内锁定范围。6.3 几个容易忽略的小细节最后分享几个容易忽略的细节都是实际开发时坑过我的。部署固件和 topology 文件后文件权限不能太随意。部分发行版在加载固件时会做安全校验典型表现是文件存在但加载失败。顺手chmod 644一下避免权限问题干扰判断。如果你在改完拓扑后明明部署了还是没有生效建议先清一遍 initramfs 缓存再重启。我遇到过一次改了/lib/firmware下文件后忘了update-initramfs结果重启后跑的还是旧拓扑排查半天以为是 m4 配置没改对。另外使用 git 管理自定义改动非常值得。无论是 Kconfig 的修改还是 m4 拓扑文件都建议单一分支维护每次改动附上说明。SOF 迭代很快隔几个月再回头看你可能完全不记得当初为什么加了这个配置有 commit 记录能省很多时间。我在实际使用中最深的体会就是源码编译 SOF 固件和 topology 这条链路困难不在编译本身而在于理解固件、拓扑、驱动三者之间的匹配关系。只要摸清这一层后面不管是新平台适配还是功能裁剪都会顺手很多。上面这些操作都是我在真实项目里一遍遍验证过的方法照着走基本不会出大偏差。如果编译过程中遇到别的古怪报错先翻日志再对照版本多数问题都能找到答案。
返回列表