
1. 先说清楚一个问题为什么“进阶”偏偏要自己编译 SOF 固件和 topologySOFSound Open Firmware这些年已经是很多 x86 平台音频方案的事实标准我一开始接触它的时候也和大多数人一样直接从/lib/firmware/intel/sof/里拿现成的.ri文件和.tplg文件装好、重启能出声就行。但等真正开始调板子、改声卡配置的时候才发现光会用厂商给的二进制根本不够。你面对的所谓“固件”并不是一个单纯的.bin它是一段跑在 DSP 上的完整程序而 topology 也不是内核里某个配置项它是描述音频数据从哪里进来、经过哪些组件处理、最后从哪里出去的“连通图”。这两个东西如果不匹配哪怕驱动加载成功pipeline 也可能起不来最终表现就是“设备节点都正常但录音全是空的”或者“打开播放直接被 app kill”。这篇文章就是把我自己从源码编译 SOF 固件和 topology 的完整过程、踩过的坑和排查思路整理出来。很适合已经能在板子上正常加载官方固件、现在想改音频路径、加 DSP 组件、或者想理解 ABI 版本为什么不一致的开发者。后面所有操作都以 Intel 平台为例子但思路对其它 SOF 支持平台同样适用。很多人第一次编译 SOF 会问一个问题官方不是提供了编译好的sof-xxx.ri和sof-xxx-xxx.tplg吗为什么还要自己动手我的回答很简单如果你不需要改任何拓扑、不需要加自定义的处理组件、也不需要打印 DSP 侧 trace那确实没必要。可一旦你发现“目前的 tplg 不支持我板子上的麦克风阵列布局”或“我想把一条 I2S 通道改成回环测试”不去动源码是不可能的。预编译产物不会给你开放修改入口而 SOF 的源码方式又能把固件、topology、logger 工具链全部串起来这才是“进阶”二字真正的价值。还有一点很重要SOF 的编译并不是把源码扔进 gcc 里编一个静态固件就完事它背后有 Xtensa 工具链、平台宏、IPC ABI 版本、内核驱动配合、topology 编译器等一整套生态。我把这一整套结构拆开讲你后面遇到问题就不会只会在网上搜“unknown pipeline”这种错误关键词了。1.1 固件、topology、IPC 三个概念先把位摆正先打个比方。SOF 固件可以理解成 DSP 上的“操作系统”你给它一个二进制文件它才能在 DSP 核上创建工作线程、管理 DMA 缓冲区、调度各个音频处理模块。topology 则是描述“这套音频系统应该长成什么样”的“施工图纸”你画了一个 pipelineBuffer - Volume - Mux - Demux - 后处理内核会把这张图推给固件固件再按照图去创建真实的处理链路。固件和 topology 之间通过 IPC 通信。内核里的 SOF 驱动把 topology 文件解析成一种驱动和 DSP 都认识的二进制布局然后把其中一个叫做“pipeline 组件信息”的结构体通过 mailbox IPC 发给固件。固件收到 IPC 之后才知道自己要去哪块内存里拿参数、要建几个 pipeline、每个 pipeline 用哪个计算模块。所以“自己编译 SOF”不只是编一个.ri。你至少要把三样东西对齐sof-platform.riDSP 固件本体。sof-platform-board.tplg拓扑二进制数据由.m4文件展开后通过alsatplg编译生成。内核驱动里对应的snd_sof_*驱动模块它决定 IPC 协议版本和 topology parser 的兼容程度。如果你只更新固件、不更新 topology或者从源码编译出来的固件 ABI 版本和旧工厂 tplg 不匹配驱动在内核日志里会毫不客气地提示 ABI mismatch或者直接报 failed to load firmware。1.2 官方预编译文件到底“缺了什么”预编译.ri文件并不是不好它在量产阶段非常省事。问题是它是通用配置基本每个平台只保留极少数默认 topology 变体。而实际板子往往会遇到这几个场景声卡 codec 不是默认型号需要换一条 I2S 控制路径。你希望把麦克风数组的 PDM 映射改一下或者关掉某些不用的话筒减少 DMA 带宽。你想在 playback 路径里加一个普通 EQ或者在 capture 路径里临时加一个 ASR 组件。你觉得某个 pipeline 在低功耗 idle 切换的时候会爆音想关掉动态 pipeline 或者修改组件触发条件。这些改动全部落在 topology 源文件里而不是改内核驱动。你拿到的/lib/firmware/intel/sof/目录里那些.tplg已经是编译后的产物翻遍整个系统也没有一份“源图”能直接改。SOF 仓库里保留了这些拓扑的.m4源文件和构建脚本从源码编译的本质就是将它们的修改能力还给你。1.3 自编译心态准备慢但值得我见过很多人第一次跑 SOF 编译脚本发现要先拉源码、更新子模块、准备 Xtensa 工具链、再调用 CMake十分钟没出结果就想放弃。这里提前说清楚第一次编译花两三个小时很正常这不代表环境坏了。SOF 是一个多模块项目既包含需要交叉编译的 DSP 固件也需要生成本机解析用的工具还有一堆.m4宏文件要做文本展开。但只要你把整个流程跑通一遍后续每次修改基本都在几分钟内完成而且你会对“内核加载 SOF 时日志里那一堆 tplg 字符串到底从哪来”有更本质的理解。2. 环境与源码准备先把“地基”打稳最理想的情况是找一台干净而且磁盘空间充足的 Linux 主机。我在 Ubuntu 22.04 / 24.04 x86_64 上都编译过依赖安装上没有太大区别。编译过程中最怕的是两件事磁盘不够以及工具链和目标平台弄混。前者很容易被忽略因为你拉源码、更新子模块、生成固件产物后整个目录轻轻松松超过 10GB。后者更隐蔽不同平台有不同 Xtensa 配置用错工具链甚至会导致编译出来的固件在运行时立刻 crash。2.1 确认目标平台再动手先说你到底要给哪个平台编译。Intel 平台在 SOF 里有很多代号APL、CNL、ICL、TGL、ADL、MTL 等。不同平台对应不同的 DSP 硬件版本、不同的启动方式也会影响要用哪套工具链。你可以先在 SOF 源码根目录跑一下git tag --list v* --sort-version:refname | head选定一个你需要的 release 版本或者保持主线也可以但主线往往要求内核驱动也是最新版。我自己一般喜欢先用v2.x这种稳定 tag避免开发板上驱动老、固件新导致 ABI 被强制升级。然后确认平台列表./scripts/xtensa-build-all.sh -h脚本输出里会有当前版本支持的所有平台参数。不同版本的脚本参数不完全一样有些用-a指定平台有些直接把平台名当位置参数传所以先看 help 永远没错。你要找的就是自己板子 SoC 对应的那一项比如 TGL、ADL、MTL 之类。2.2 拉取源码和子模块一个都不能少SOF 官方仓库已经不止“一个 git 仓库”那么简单固件主体、工具链脚本、部分平台代码都被组织成子模块。拉源码时最好直接递归git clone --recursive https://github.com/thesofproject/sof.git cd sof如果你之前已经普通 clone 了一定要把子模块补全git submodule update --init --recursive这一步少做了后面编译会报出各种“找不到头文件”“找不到某个模块”的错误而且出错位置看起来毫无规律。尤其src/arch/xtensa、IPC 定义、以及部分 vendor 平台代码依赖子模块中的内容缺失时你根本走不到实际编译阶段。有一个经验分享不要用git clone --depth1去拉 SOF。哪怕是只编一个平台我也建议完整 clone 或者使用 release tar 包。因为编译脚本可能需要从 git 元数据里读取版本号、生成sof_version.h浅克隆有时会造成版本信息缺失最后产出的.ri文件名和预期不一致。2.3 Xtensa 工具链别自己重复造轮子SOF 固件很大一部分运行在 Xtensa DSP 上所以你的主机需要用 Xtensa 交叉编译器。这里有个常见误区拿xtensa-lx106-elf-gcc这种 ESP 生态的交叉工具链直接编译 SOF会报错误或生成完全不能启动的固件。SOF 需要的是针对具体 DSP core 的 GCC 变体这个工具链很多平台并不一样。工具链获得方式我推荐以下几种按省事程度排序:直接使用 SOF 社区编译好的 Docker 镜像镜像里已经配好了对应平台工具链。看仓库 README 里提供的命令用 docker 跑构建脚本这种方式对系统环境最友好。从 SOF CI 使用的工具链包下载对应 release。不同项目时期包名和放置路径不一样建议严格按官方 README 来。用 crosstool-NG 配合 SOF 提供的 Xtensa overlay 自己编。这个最折腾除非你要深度定制工具链否则不推荐新手碰。选择方式 2 或 3 时通常要把工具链根目录导出成环境变量。常见变量名是XTENSA_TOOLS_DIR或XTENSA_ROOT。你可以在根目录的scripts脚本里搜一下看到脚本里去哪个环境变量取路径就用哪个。我自己更偏爱 Docker 方式。一是工具链版本不会被系统里的自定义交叉编译器污染二是官方 Docker 镜像里的 CMake、ninja、m4 等构建工具版本基本都验证过省掉了大量“为什么我这编译出来之后 ABI 版本号对不上”的玄学问题。2.4 内核里的 SOF 驱动也要跟着看如果你只是改了 topology 源文件内核驱动可以不动。但如果你同时更新了固件源码尤其是 IPC 相关代码或者 module 结构那么内核驱动版本必须严格匹配。SOF 在运行时会检查 IPC ABI 版本固件和驱动任何一边版本过新或过老都会导致启动失败。我的经验是先跑一次lsmod | grep snd_sof确认当前使用的驱动模块属于旧版本还是新版本。再根据模块路径反查内核源码版本modinfo snd_sof_pci_intel_adl | grep ^version如果内核自带的 SOF 驱动版本太老而你编了一个很新的固件建议先更新内核。不用最新 mainline至少是 distro 提供的较新 LTS 内核。毕竟 SOF 的 ABI 稳定性并没有你想象的那么“向后兼容到远古”。这一步检查最多花五分钟但能避免后面所有日志都是“failed”。3. 固件编译完整实操跑通一次比看十遍文档更管用环境准备好之后真正的编译其实并没有那么难。SOF 项目已经做了相当多自动化工作我在这里不会让你手写一堆平台寄存器地址而是重点解释编译脚本、CMake 参数、产物验证这些东西。3.1 进入源码根目录用官方脚本编固件以当前主流版本为例官方会对各平台提供编译脚本。通常只需要在 SOF 源码根目录执行./scripts/xtensa-build-all.sh platform其中platform换成 2.1 里 help 输出所确认的平台名。比如你用的是 Alder Lake可能就是adl也可能是tgl某种变体一切以脚本支持列表为准。如果帮助信息里你看到有-l这种列出平台列表的选项先跑一次不要靠猜。脚本会帮你做很多原本很麻烦的事创建独立构建目录、设置平台相关宏、选择正确的 Xtensa core、调用 CMake/Ninja 生成固件。它的另一个好处是后续你修改了源码里的 C 文件再跑一次同样命令增量编译结果会很干净。如果你不希望脚本全自动想自己手动看看 CMake 参数也可以直接进构建目录mkdir -p build_adl cd build_adl然后从源码根目录跑 CMake。注意这里需要指定平台、工具链以及 SOF 源码目录。不同分支的参数有差异但核心思路一致cmake -S ../sof \ -B . \ -DCMAKE_TOOLCHAIN_FILE../sof/scripts/xtensa-toolchain.cmake \ -DPLATFORMadlCMake 配置成功之后再用 ninjaninja无论用哪种方式固件编译出来的主体产物通常是一个.ri文件。这个.ri不是普通裸二进制里面带了平台信息、版本信息、签名/加密相关元数据。你要做的不是手动把里面某个.bin抠出来而是保留整个.ri文件并让内核加载它。3.2 编完固件之后单独处理 topology很多人编完固件就以为结束了这是最大的误区。topology 虽然在 SOF 源码仓库内但它和固件不是同一个构建产物。topology 的源文件通常是.m4这些文件里写满了宏定义和声卡相关抽象。实际流程是先用 m4 把.m4展开成中间.conf格式再用 ALSA 的拓扑编译器alsatplg把.conf编译成.tplg。你可以在源码树的tools/topology目录下找到一整套与拓扑相关的构建脚本和平台配置文件。通常直接执行make -C tools/topology如果这个目录里有README那么先看 README因为个别版本里make的目标名可能不同。想让alsatplg能在主机上直接跑你还需要安装 ALSA 开发包sudo apt install alsa-topology-conf libasound2-devalsatplg这个工具一般由alsa-utils或单独软件包提供。它本身运行在主机上生成出来的.tplg要能被目标内核驱动解析所以在编译拓扑时主机架构和目标架构是否一致并不重要。这里我给大家一个很硬核的排查技巧如果你改了.m4之后生成的 topology 在 dmesg 里解析失败先不要怀疑 DSP 固件而是回看.conf中间文件有没有正确展开。可以把构建目录下的中间.conf打开重点看object.pipeline、object.control等关键段确认你想要改的通道数、格式、组件名称都已经出现在里面。很多“topology parse error”其实是你的 ALSA 编译器版本太旧遇到某些新增 token 不识别导致的。3.3 产物检查不要对着“看起来像”的文件乱装编译完成后建议先用 find 快速确认产物都在哪里find . -name *.ri -type f find . -name *.tplg -type f你常见的目录结构大概是build_adl/...下有一个或多个.ritools/topology/下生成对应的.tplg。有些构建脚本会在目录里带上版本号比如sof-adl.ri sof-adl-nocodec.tplg如果是这种格式说明它已经把平台信息写进文件名了。你要确认自己板子实际的 ABI 名称是哪一个。大多数 Intel 平台 kernel driver 加载时都会去默认路径找sof-platform.ri和对应 topology 文件。如果你编出来的平台名和内核默认找的名字不一致就算文件放在路径里也不会被加载。更好的做法是先看内核当前在找哪个文件。加载完驱动之后dmesg 里通常会直接打印固件路径和文件名sudo dmesg | grep firmware根据日志里的关键信息去对产物比你自己猜文件名靠谱得多。3.4 关于增量编译和配置缓存源码编译过程中你会反复修改 C 文件和.m4文件这时候增量编译能省下大量重复时间。但有个问题要特别注意如果改了平台相关配置或工具链路径旧的 CMakeCache 会把旧路径一直留着。遇到改了环境变量却不生效的情况我建议直接删掉整个build_*目录重新 configure而不是在现有目录里硬跑。我在调试过程中就遇到过明明把XTENSA_TOOLS_DIR换到了新路径但重新跑脚本后编出来的固件还是旧的特征。后来才发现是 build 目录里的 CMake cache 里残留旧值。删掉重建之后问题立刻消失。所以不要舍不得那个已经编译了一半的目录和后面浪费时间比删掉重来反而是最高效的选择。4. 安装到系统并验证不是 copy 完就万事大吉固件和 topology 都编译出来后表面上看只是 copy 文件到/lib/firmware/下但实际有几个坑会导致你怀疑“是不是编译错了”。比如文件确实在场但 initramfs 里缓存了旧文件重启后又自动还原成老版本或者 driver 模块没有真正卸载重新加载一直用的是内存里旧固件。4.1 备份原文件再替换先把原来系统里的 sof 固件目录整个备份掉。不要直接重命名某个单独.ri最好把整个目录备份一次因为不同类型的 topology 可能被不同声卡引用sudo cp -a /lib/firmware/intel/sof /lib/firmware/intel/sof.$(date %Y%m%d)然后把你编译出来的.ri和.tplg放到对应目录sudo cp 你的构建目录/sof-adl.ri /lib/firmware/intel/sof/ sudo cp 你的构建目录/sof-adl-nocodec.tplg /lib/firmware/intel/sof/并不是所有平台固件都放在/lib/firmware/intel/sof/下如果内核驱动报的路径不是这个请以 dmesg 报出来的路径为准。改完文件之后要同步一下让文件落盘sync这一步看起来多余但在部分系统里过快重启可能造成固件文件还没真正写入磁盘。4.2 更新 initramfs这步经常被忽略。如果你的根文件系统在 initramfs 阶段就加载了音频驱动而且需要的sof-*.ri被塞进过 initramfs那么你只在/lib/firmware里替换文件是没用的。重启之后系统用的还是 initramfs 里的旧文件。保险做法是更新 initramfssudo update-initramfs -u做成之后你可以通过下面命令确认 initramfs 里包含的固件版本是否已经更新lsinitramfs /boot/initrd.img-$(uname -r) | grep intel/sof | head如果你不开 initramfs 加载音频驱动跳过这步也不是不行但既然都手动编固件了多花十秒敲一下命令能省掉一次“重启后固件为什么没变”的困惑。4.3 卸载并重新加载 SOF 平台驱动替换完文件之后最好卸载对应平台驱动再重新加载。注意要先保证没有进程正在使用声卡否则 module 卸载会提示 busy。你可以先关掉播放音乐的软件再执行sudo modprobe -r snd_sof_pci_intel_tgl sudo modprobe snd_sof_pci_intel_tgl模块名不一定固定要以lsmod | grep snd_sof看到的实际模块为准。如果对应平台是 ADL模块可能是snd_sof_pci_intel_tgl也可能是统一的snd_sof_pci_intel_cnl一切以你内核 modules 目录里真实存在的模块文件为准。重新加载后立刻看 dmesgsudo dmesg | grep -E sof|firmware|tplg如果 firmware 加载成功日志里会出现类似sof-audio-pci-intel-tgl 0000:00:1f.3: firmware: direct-loading firmware intel/sof/sof-adl.ri sof-audio-pci-intel-tgl 0000:00:1f.3: Firmware info: version 2:x.y.z ... sof-audio-pci-intel-tgl 0000:00:1f.3: Firmware: ABI x.y.z接着会看到 topology 相关解析日志。只要没有出现failed或error再用aplay -l确认 PCM 设备节点已经生成。能列出声卡但不代表所有 PCM 都可用最好分别试一下播放和录音。4.4 验证当前加载的固件版本和 topology 来源系统到底加载了哪个固件不能靠“我觉得”。我最常用的验证方式是看 debugfs。如果内核开启了 SOF debugfs可以执行sudo cat /sys/kernel/debug/sof/fw_version它会打印当前实际加载固件的版本信息。你可以对比自己在源码里git describe得到的版本号确认是不是同一个。如果 debugfs 路径不存在可能是内核没开CONFIG_DEBUG_FS那就只能靠 dmesg 启动日志来判断。topology 文件是否被正确解析则可以通过 dmesg 里有没有出现 pipeline 创建成功的日志来看。有些驱动版本还会在/sys/kernel/debug/sof/下导出 topology 相关状态。如果某个 PCM 节点创建失败多半能在 dmesg 里看到是哪个widget或 pipeline 解析失败这就把问题缩小到 topology 源文件里的对应宏定义了。4.5 常见加载问题速查我整理一个自己经常遇到的排查表不一定覆盖所有情况但能解决大多数“自编译后不工作”的问题:现象常见原因处理方式dmesg 提示找不到 firmware 文件文件名或目录路径与内核期望不一致根据 dmesg 实际找文件路径重命名或移动固件加载后 version 还是旧的initramfs 里有缓存sudo update-initramfs -u后重启ABI 版本不匹配固件与内核驱动版本不同源同步更新内核或拉低 SOF 版本topology parse erroralsatplg版本过旧或.m4宏展开错误检查中间.conf升级 ALSA 工具node 建立失败但没有 fw errortopology 中引用了固件不支持的组件查看日志中具体 widget 名字回到 topology 源码删除或替换录音静音但播放正常麦克风 PDM 配置或 DMIC 引脚不对检查 topology 里 DMIC/PDM token 配置播放出现卡死和 DSP panic平台工具链不对删除 build 目录重新确认 Xtensa 工具链来自官方这些问题的共同点是日志里不会有人直接告诉你“你应该改哪一行”但会把widget、pipeline、tplg这些关键词给出来。定位的时候顺着关键词去 topology 源文件里搜索通常都能找到对应宏或组件定义。5. 一些更进一步的经验自编译不是终点而是调试入口当你把自编译流程跑通之后你会发现编译本身只占后面调试工作的一小部分。真正花时间的往往是每次改完 topology 之后对整个音频图是否正确、固件是否按照预期创建了 DSP 组件的验证。这里我分享一条很实用的链路。如果内核日志里的 pipeline 创建失败但是拓扑解析并没有报错那么你可以先用sof-logger拿到 DSP 侧的 trace。SOF 源码编译出来的工具里通常包含sof-logger它读的日志元数据需要和固件版本保持一致所以千万不要拿仓库里新版 logger 去读一个旧固件那样读出来全是一堆无法解析的数字。正确做法是把日志工具的版本和固件源码 tag 锁定在同一个提交。我自己习惯编译完固件后先把tools/logger/目录下的对应二进制放到一个固定位置并和当前.ri文件放在一起。这样之后现场调试时固件和 logger 永远是对应的不会出现“日志工具打不开”这种额外干扰。还有一个坑想提醒你手动编译成功并不说明 topology 一定会被“原样加载”。很多板子在设备树或 ACPI 里已经指定了 topology 文件名比如某个主板上厂家写死了要用sof-adl-rt5682.tplg。你如果只改了sof-adl-nocodec.tplg系统照样不会用你新生成的那份文件。遇到这种情况要查一下你的 ACPI/设备树配置里sof-tplg的字符串指向哪个文件然后把编译产物文件名改成那个再试。最后说一句体会第一次从源码编译 SOF 时你会觉得构建脚本像黑盒但当你能从 dmesg 里看到自己改动的 topology 成功被解析、DSP pipeline 重新建立起来时那种打通完整链路的感觉会让人对音频子系统有完全不一样的认识。以后再遇到声卡配置问题你就不会只想“刷个固件试试”而是知道该从哪份.m4源码、哪个 ABI token、哪一条 IPC 日志下手了。