黑苹果内置麦克风修复实录:当 PDM 数字麦挂在 Intel SOF DSP 上

黑苹果上"扬声器正常、耳机正常、就是内置麦克风没声"是很常见的一类问题。大多数教程会让你去换 AppleALC 的 layout-id,但有一类机器换多少个 layout 都没用——因为它的麦克风根本不走 codec。

这篇博客分两部分:先说清楚黑苹果上内置麦克风的两种硬件路径和通用的驱动配置方法(以 TGLDmic 为例),再完整记录我这台机器踩到的坑、定位过程和最终修复。

结论先放这儿:AppleALC + layout-id 只解决模拟麦克风;PDM 数字麦克风需要把 Intel SOF 固件跑在 cAVS DSP 上,而 TGLDmic 在部分 Comet Lake 机型上会因为"平台没把 DSP 状态归位"卡在第一步。补丁很小,但要知道去哪儿补。

一、先搞清楚:你的麦克风接在哪

笔记本内置麦克风有两条完全不同的通路:

模拟麦克风PDM 数字麦克风
物理连接驻极体/MEMS → 模拟线 → HD Audio codec 的 ADCMEMS 数字麦 → PDM 总线 → Intel SST/audio DSP
谁在驱动AppleHDA + AppleALC(layout)Intel SOF 固件,跑在 DSP 核上
macOS 原生支持✅ 有❌ 没有(Mac 不用这架构)
黑苹果解法选对 layout-id / alc-layout-idTGLDmic 这类社区驱动

四麦克风阵列、远场拾音、Dolby Atmos 这类卖点的机器,内部麦几乎都是第二种。这类机器上 AppleALC 能给你一个叫 “Internal Microphone” 的输入设备,但它没有声音——因为 codec 里根本没有接那条 ADC。

怎么判断自己属于哪一类

我用了三个互相独立的证据:

  1. Windows 侧驱动(最硬)。挂载 NTFS 分区只读,看 Windows/System32/DriverStore/FileRepository:出现 intcaudiobus.inf、intcdmic.inf、lnvsst.inf 这类包,说明这台机器的麦克风走 Intel SST 数字麦。
  2. Linux 侧 dmesg。Live USB 启动后看 dmesg | grep -i sof,出现 DMICs detected in NHLT tables: 4 之类,就是 PDM 阵列。
  3. macOS 侧"假麦克风"检测。选中 Built-in Input 录一段,用 ffmpeg 看电平:
1
2
ffmpeg -f avfoundation -i ":2" -t 5 -ar 48000 -ac 1 bi.wav
ffmpeg -i bi.wav -af volumedetect -f null - 2>&1 | grep -E 'mean_volume|max_volume'

我的机器结果:开头一声爆音(max -6 dB),之后全是 ±1 / ±2 的数字静音。看起来设备存在、有电平表,其实是假的。

只有第 1、2 条成立时才值得折腾 DSP 驱动;否则应该回去调 AppleALC。


二、TGLDmic 是什么,怎么配

TGLDmic 是一个社区项目,由两部分组成:

  • TGLDmic.kext:内核驱动。加载 Intel 官方签名的 SOF 固件(sof-tgl.ri / sof-cml.ri)到 cAVS DSP,按 Linux SOF 的 IPC 序列配置 DMIC 采集链路,再用 DMA 把 4 通道 48 kHz 音频搬到 ring buffer。
  • TGLDmic.driver:Core Audio HAL 插件,把这个 ring buffer 包装成输入设备 “Internal Microphone (TGLDmic)"。

支持平台:

平台cAVSPCI ID
Comet Lake / Cannon Lake / Coffee Lake1.88086:02C8 8086:9DC8 8086:06C8 8086:A348
Tiger Lake2.58086:A0C8 8086:43C8

安装四步

  1. TGLDmic.kext 放进 EFI/OC/Kexts/,在 Kernel → Add 里加在 AppleALC 之后;
  2. NVRAM → boot-args 加 -tgldmic;
  3. 装 release 里的 pkg(装 HAL 插件);
  4. 系统设置 → 声音 → 输入,选 “Internal Microphone (TGLDmic)"。

扬声器和耳机仍然由 AppleALC 负责,两边是并行的,不需要二选一。

自检

1
ioreg -l -w0 -c TGLDmic | grep -E '"BootResult"|"TGLDmicPhase"|"Streaming"'

正常应该看到:

1
2
3
"BootResult" = "OK FW_READY"
"TGLDmicPhase" = 4
"Streaming" = No          # 空闲时 DSP 是掉电的,有人录音才上电

TGLDmicPhase 是模式:1 = 只读观察,2 = 启动一次 DSP 自检,3 = 采集测试,4 = 常驻服务。日常用 4。


三、通用的排错顺序(不针对某台机器)

出问题时不要一上来就换固件、换 boot-args,按这个顺序看信息量最大:

  1. kext 有没有加载、认成哪个平台 ioreg -l -w0 -c TGLDmic 里看 TGLDmicVersion、Platform、BootResult。
  2. 失败在哪一步 BootResult 后面括号里的值就是失败时寄存器的原始值,比日志还直接。
  3. 看最后 128 行日志 ioreg -l -w0 -c TGLDmic | tr ',' '\n' | grep -E '"(boot|fix|cap):' —— 驱动自己维护了一个环形缓冲区,log show 里不一定能看到。
  4. 区分"软件没配好"和"硬件没给你” DSP 相关失败绝大多数是后者:固件没跑起来之前,改什么都白搭。
  5. 用 Linux 做对照 这个项目自带了 linux/capture-sof-dmic.sh,Ubuntu Live 跑一遍就能知道同一台机器上 Linux SOF 能不能点亮 DSP。能 → kext 的差距;不能 → BIOS/平台层,别在 macOS 里死磕。

常见失败点大致长这样(按流程顺序):

BootResult含义
FAIL ADSPCS unreadableBAR4 没映射上 / 控制器不在 D0
FAIL core0 power up (CPA)DSP 核没上电(本文的情况)
FAIL ROM IPC ack / ROM init done核起来了,ROM 没响应
FAIL FW_ENTERED + ROM error 44固件没通过 CSE 验签(多半用错/改过的固件)
FAIL FW_READY固件跑起来了但没回就绪消息

看到一个失败值,先去和能用的机器比同一条日志,差异通常就藏在里面。


四、我这台机器:从 0x001d003c 说起

机器:联想 Comet Lake-U(i7-10710U),HDA 8086:02C8,SSID 17aa:380f,codec ALC257,SMBIOS 伪装 MacBookAir9,1,OpenCore REL-104,macOS 14.8.5。(从 EFI 里 SSDT-OCBAT1-lenovoPRO13 这套 ACPI 命名推测是联想小新 Pro 13 一代的配置,仅供参考。)

版本:TGLDmic v0.5.0。这里有个关键背景——Comet Lake 支持是 2026-10-30 的 v0.5.0 才合入的,作者验证的机器是 ThinkPad X1 Carbon Gen 7(ALC285,SSID 17aa:2292),和我的不是同一块主板。

现象

1
2
3
"BootResult"  = "FAIL core0 power up (CPA) (0x001d003c)"
"TGLDmicPhase" = 4
"Streaming"    = No

日志:

1
2
3
4
5
boot: ADSPCS 0x001d003c PPCTL 0x00000000 before boot
boot: sof-cml.ri (cAVS 1.8 Comet Lake), 557824 bytes total, payload 557056 bytes ...
boot: clock/power gating off (CGCTL 0x007b09ff PGCTL 0x00000000 EM2 0x84806000)
boot: PPCTL 0x00008000 (PROCEN bit 15 set)
boot: FAIL core0 power up (CPA) (0x001d003c)

读懂 ADSPCS

cAVS 的 ADSPCS(DSP BAR + 0x04)每个核占 4 个位段:

位段名称含义
[7:0]CRST核心复位
[15:8]CSTALL核心停顿
[23:16]SPA请求上电
[31:24]CPA已上电(硬件置位)
  • 0x001d003c → CRST=0x3c、CSTALL=0x00、SPA=0x1d、CPA=0x00
  • 参考机器的正常空闲值 → 0x00000f0f:4 个核都处于 stall + reset、未上电
  • 这次写入 SPA 后的正常值 → 0x0f0f0f0f:SPA/CPA 全部置位

三个观察让方向变得很明确:

  1. 本机的初始值不是"干净空闲”,而是一个半复位的残留状态(core0 甚至不在 reset 里,SPA 却已经有位);
  2. 对 ADSPCS 的写入全部无效——连 corePowerDown() 里的 CSTALL/CRST 都改不动它,每次重试读回的还是同一个 0x001d003c;
  3. PPCTL 是 0x00000000,而参考机器的日志里是 0x40000000。

固件一个字节都还没写进去,所以这不是 sof-cml.ri 的问题,也不是 AppleALC、boot-args、HAL 插件的问题:平台没把 DSP 从被按住的状态里放开。


五、对照 Linux SOF 源码找差异

TGLDmic 的启动序列是照着 Linux sound/soc/sof/intel/hda-loader.c 写的,但它省略了两步"通常 AppleHDA 已经替它做过"的操作:

  1. PPCTL.GPROCEN = 1 Linux 在 hda_dsp_probe() 里调用 hda_dsp_ctrl_ppcap_enable(sdev, true),也就是给 PPCTL 的 bit30 置位(Global Processing Enable)。TGLDmic 只设置了 loader stream 自己的 PROCEN 位,从来没设过 GPROCEN。
  2. 一个干净的核状态 Linux 的 hda_dsp_pre_fw_run() 会先检查 DSP 是否已经使能,若使能先完整掉电;然后关掉 clock/power gating。cl_dsp_init() 再对 host_managed_cores_mask(CML 是 0xF)置 SPA、等 CPA。TGLDmic 只在 CPA(CORE0) 已经置位时才会尝试复位,本机 CPA=0,所以直接跳过了。

在作者那台 X1C7 上,AppleHDA 留下的状态恰好是干净的 0x0f0f,这两步没做也没事。在我的机器上不是,于是 SPA 写下去永远等不到 CPA。


六、补丁:在启动前把状态归位

思路很直接:把 Linux 会做的那两步补上,并加上足够的读回日志和重试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool TGLDmic::preBootFix() {
    int fix = 1, wide = 0, gctl = 0;
    PE_parse_boot_argn("tgldmicfix", &fix, sizeof(fix));      // =0 关闭补丁,用于 A/B
    PE_parse_boot_argn("tgldmicwide", &wide, sizeof(wide));   // =1 连保留的 core 4..7 一起复位
    PE_parse_boot_argn("tgldmicgctl", &gctl, sizeof(gctl));   // =1 额外复位 HDA 控制器(有副作用)
    if (fix == 0) { LOG("fix: disabled (tgldmicfix=0)"); return true; }

    /* 1. 全局处理使能:Linux 在 probe 时一定会设 */
    uint32_t pp = r32(ppOffset + HDA::PP_PPCTL);
    if (!(pp & HDA::PPCTL_GPROCEN)) {
        w32(ppOffset + HDA::PP_PPCTL, pp | HDA::PPCTL_GPROCEN);
        LOG("fix: GPROCEN set, PPCTL 0x%08x -> 0x%08x", pp, r32(ppOffset + HDA::PP_PPCTL));
    }

    /* 2. stall -> reset -> 清 SPA,回到 Linux 期望的干净状态 */
    uint32_t mask   = targetCoreMask();
    uint32_t fields = wide ? 0xFFu : mask;
    uint32_t cs     = d32(DSP::ADSPCS);
    dw32(DSP::ADSPCS, (cs & ~SPA(0xFFu)) | CSTALL(fields) | CRST(fields));
    pollDsp(DSP::ADSPCS, CRST(fields), CRST(fields), 50000);
    dw32(DSP::ADSPCS, d32(DSP::ADSPCS) & ~SPA(0xFFu));
    pollDsp(DSP::ADSPCS, CPA(fields), 0, 50000);
    return true;
}

配合两处改动:

  • bootDsp() 里在 gating(false) 之后、prepareLoaderStream() 之前调用 preBootFix();
  • corePowerUp() 改成最多重试 3 次,每次写 SPA 前后都打印 ADSPCS,失败时重新 preBootFix() 再试。

另外把 gating(false) 之后 CGCTL/PGCTL/EM2 的读回值也打印出来——这能直接判断 PCI 配置空间的写到底生效没有:

1
2
boot: clock/power gating off (CGCTL 0x007b09ff PGCTL 0x00000000 EM2 0x84806000)
boot: gating read-back CGCTL 0x007b09fd PGCTL 0x00000004 EM2 0x84804000

编译与安装

1
2
3
4
# 需要 Xcode Command Line Tools
git clone --depth 1 https://github.com/acidanthera/MacKernelSDK
cd TGLDmic/kext && make SDK=/path/to/MacKernelSDK
# 产物:build/TGLDmic.kext

装回 EFI 前先把原版备份出来,出问题一条命令就能退回:

1
2
3
4
diskutil mount disk0s6
sudo rm -rf /Volumes/Untitled/EFI/OC/Kexts/TGLDmic.kext
sudo cp -R build/TGLDmic.kext /Volumes/Untitled/EFI/OC/Kexts/
diskutil unmount /Volumes/Untitled

只改了 kext 本身,config.plist、boot-args、AppleALC、HAL 插件都不用动。


七、结果

重启后:

1
2
3
4
"BootResult"     = "OK FW_READY"
"TGLDmicVersion" = "0.5.0-test1"
"Streaming"      = No
"ADSPCS"         = 3855      # 0x00000f0f,空闲时正常掉电

启动日志:

1
2
3
4
5
6
7
8
9
boot: ADSPCS 0x00000f0f PPCTL 0x40000000 before boot
fix: GPROCEN already set (PPCTL 0x40000000)
fix: stall+reset (mask 0xf): ADSPCS 0x00000f0f -> 0x00000f0f
boot: power-up 1: ADSPCS 0x00000f0f -> 0x0f0f0f0f (want CPA 0x0f000000)
boot: core powered, ADSPCS 0x0f0f0f0f
boot: core0 running, ADSPCS 0x0f0f0e0e
boot: ROM init done, FSR 0x05000001
boot: code loader FW_ENTERED, FSR 0x00000005, DPIB 557056
boot: SUCCESS - SOF firmware running (FW_READY)

录音实测(两次独立会话,间隔十几秒,DSP 会自动重新上电):

会话格式meanmax
第 1 次48 kHz 立体声-30.7 dB-9.3 dB
第 2 次48 kHz 立体声-38.7 dB-25.5 dB

安静房间里的真实环境声,不是数字静音。扬声器和耳机不受影响。

一点诚实的说明

补丁里放了两步(GPROCEN + 强制复位),但第一次打补丁那轮启动的日志已经被 128 行环形缓冲区顶掉了,现在能看到的每一轮开机都已经是干净状态。所以我能确认的是:补丁兼容、机器从此每次都能正常启动;但我没法百分百断言是哪一步起的作用,也排除不了"换 kext 时那次完整断电顺带清掉了卡死状态"。

真要归因,做一次 A/B 就行:把原版 0.5.0 装回去、重启,看 0x001d003c 会不会回来。这一步我留着当后续实验——毕竟现在麦克风能用,能不折腾就不折腾。


八、给后来人的 checklist

  • 先判断麦克风类型,再决定要不要折腾 DSP。Windows 驱动目录 / Linux dmesg / “假麦” 录音测试,三个里凑够两个再动手。
  • 先读 BootResult 的括号值。它是失败时的寄存器原值,信息量比任何描述都大。
  • 和已知可用的机器比同一条日志。我这次的突破口就是 ADSPCS 和 PPCTL 两行和作者机器对不上。
  • 写入是否生效要读回。寄存器写下去、读回不变,说明问题根本不在你写的值上。
  • 别在 DSP 没点亮时换固件。ROM error 44 才是固件/验签问题,CPA 不上电跟固件无关。
  • 保留原版 kext 和 patch 源码。上游更新会覆盖补丁;我这台机器一旦装回官方的 0.5.0,麦克风就回到原形。
  • 失败时 TGLDmic 会抢默认输入,而且从它录音可能直接卡住(我实测 ffmpeg 打开设备两分钟无响应)。修好之前先把默认输入切回 Built-in Input,或者卸掉 HAL 插件。
  • DSP 只在录音时上电,空闲时日志里 Streaming = No 是正常现象,不代表坏了。

参考

  • labrouss/TGLDmic —— 项目主页、安装与测试文档
  • Linux 内核 sound/soc/sof/intel/:hda-ctrl.c(hda_dsp_ctrl_ppcap_enable)、hda-dsp.c(hda_dsp_core_power_up、hda_dsp_core_stall_reset)、hda-loader.c(cl_dsp_init)、hda.c(hda_dsp_pre_fw_run)
  • Intel High Definition Audio 规范中的 ADSPCS 定义
build with Hugo, theme Stack, visits 0