HUAWEI MateBook D15 2022 Ubuntu 指纹识别修复:Goodix GDIX51C0 采图超时
本文摘要
HUAWEI MateBook D15 2022(BoF-XX)的 Goodix GDIX51C0 指纹设备在 Ubuntu 上能被 fprintd 识别,却无法录入:采图静默期内 IRQ 升高,监听线程消耗中断边沿后未重新检查电平,导致图像数据超时。修复监听线程的唤醒时机后,本机完成指纹录入并得到一次 verify-match。
AI 辅助声明:AI 协助排查驱动、修改代码并整理本文。硬件信息、报错和录入验证结果来自这台电脑的实际操作;尚未做过的登录与重启测试会明确写出。
同一台 MateBook D15,前脚刚把内置扬声器的外放问题解决,后脚又轮到电源键上的指纹识别。Windows 下有驱动,Ubuntu 的设置里却用不了。我手边还有华为的 Windows 驱动包,最初也想过能不能靠 Wine 把它带起来。
折腾到后来,真正卡住的地方已经不是“系统有没有看到设备”:fprintd 能找到指纹传感器,录入却一次次报 enroll-unknown-error。图像模式的 ACK 已经到了,后面的图像数据就是等不到。这篇记下我是怎么把超时追到 SPI 中断监听,再把它修好的。
先认准这颗指纹传感器
这台机器是华为 HUAWEI MateBook D15 2022,DMI 产品名 BoF-XX、版本 M1010;系统是 Ubuntu 26.04.1 LTS,排查时内核为 7.0.0-38-generic。Goodix(汇顶科技)是指纹传感器厂家,华为是笔记本整机厂家。本机的设备由 ACPI 枚举到 SPI 总线上,不是一个插在 USB 上的 Goodix 设备。
| 项目 | 本机观察 |
|---|---|
| ACPI 设备 ID | GDIX51C0,Windows INF 中写作 ACPI\GDIX51C0 |
| Linux 设备路径 | /sys/bus/spi/devices/spi-GDIX51C0:00 |
| 传感器 | Goodix GDIX51C0,芯片 ID 0x2504,ChicagoHS,80 × 64 图像 |
| 用户态接口 | libfprint / fprintd |
这几个值比“MateBook D15”这个商品名更有用。同系列电脑可能换传感器或接到另一种总线上;下文的补丁针对我这台 BoF-XX 上的 GDIX51C0。
先从系统给出的设备路径查起,而不是按产品名搜一个“MateBook D15 通用指纹驱动”。在本机可以这样确认 ACPI 设备是否映射到了 SPI:
cat /sys/class/dmi/id/{sys_vendor,product_name,product_version,board_name}ls -ld /sys/bus/spi/devices/spi-GDIX51C0:00这里有三个容易混淆的层次:fprintd 是给桌面和 PAM 调用的服务,libfprint 负责指纹设备逻辑,最底层才是 SPI、GPIO 和传感器。GNOME 的录入界面不能替代驱动读出一帧有效图像。排查时我把每一层能证明的事分开:
| 层次 | 当时能确认的事 | 还不能证明的事 |
|---|---|---|
| ACPI / SPI | 系统枚举出 spi-GDIX51C0:00 |
指纹数据已经可读 |
libfprint / fprintd |
设备能被列出 | 完成录入或匹配 |
| GNOME | 可以尝试调用录入流程 | 采图超时不是界面提示能解决的 |
Windows 驱动和 Wine 走到了哪一步
华为驱动包里的 gfspi.inf 确实包含 ACPI\GDIX51C0,所以它对确认设备身份有用。但包里的 gfspi.dll 是 Windows UMDF 驱动组件,不能直接装进 Linux 的 libfprint。我试过在 Wine 里运行 devcon update:命令提示成功,接着 devcon find ACPI\GDIX51C0 却说 No matching devices found。Wine 没有把 Linux 枚举到的 SPI 设备变成 Windows 可用的硬件节点,这条路就到此为止。
这一步的价值是把“安装程序显示成功”和“硬件真的被驱动接管”区分开。Windows INF 匹配的是 Windows 的 ACPI 设备实例;Wine 给程序提供 Windows API 环境,却不会替 Linux 内核绑定这颗 SPI 传感器。我没有把那个成功提示当成指纹功能已可用的证据,也没有把厂商 DLL 放进公开仓库。
接下来用的是 berkekbgz/libfprint-goodix-spi 上游项目。上游原本面向 MateBook 16s,我在它的源码上适配这台 D15,并把改动放到公开仓库:xingwangzhe/libfprint-goodix-spi-d15。我的分支基于上游提交 010a665f54089a1632b1ab7be588b316ace934e2;具体差异可以直接看采图中断修复提交。
把 SPI 设备绑定给 spidev、安装适配后的 libfprint 之后,fprintd-list 已能看到设备。但“能列出来”和“能读到指纹图像”之间,还隔着一次真正的采图。
我本机安装的是驱动构建出的 libfprint 共享库,路径为 /opt/goodix-spi-driver/libfprint/libfprint-2.so.2.0.0;fprintd 的 systemd 配置在 /etc/systemd/system/fprintd.service.d/20-goodix-spi-libfprint.conf,SPI 绑定规则在 /etc/udev/rules.d/70-libfprint-2.rules。这些是本机安装结果,不是说读者复制这几个文件就完成了安装。仓库安装器还涉及 libfprint 版本、TLS PSK 和服务配置;应按仓库 README 核对自己的设备后操作。
ACK 已经到了,图像却超时
录入时反复得到 enroll-unknown-error。驱动的关键报错是:
image read failed after setmode ACK: gdix51c0: timed out awaiting cmd 0x17 after 1000000 ussetmode ACK 说明切换图像模式的命令有回应;0x17 是后续等待的 TLS 图像数据。此时再把问题归咎于 GNOME 没弹提示,已经说不通了:失败发生在驱动读图像的环节。
为了弄清这里到底在等什么,我顺着 gdix51c0_capture_image_raw() 的顺序往下看。驱动发送图像模式命令前会清掉上一次遗留的 0x17 数据;ACK 成功后,传感器花约 73 ms 逐行读出图像,驱动让 SPI 总线安静约 80 ms,然后再等新的图像记录。这里发送的图像模式命令和接收的 0x17 不是同一个字节:0x17 是收到的 TLS application data 记录类型。如果把请求命令号当成响应类型去等,也会误读这条日志。
静默不是随意加上的延时。上游实现的注释记录过:采图期间过早访问 SPI,可能使图像在固定行以后变成无效数据。所以“超时了就一直轮询 SPI”看起来积极,实际上可能把刚要读好的那帧图像破坏掉。修复必须同时满足两个条件:静默窗口内不抢读;窗口结束后不能漏读。
我加了临时诊断日志观察时序。传感器在收到图像模式 ACK 后,需要大约 80 ms 的 SPI 静默时间完成读出;这段时间驱动主动禁止监听线程读 SPI。偏偏 IRQ 在这段静默期内升高,并一直保持高电平。监听线程消耗了那次中断边沿,静默结束后又等下一次边沿;新的边沿没有来,已经准备好的数据也就没被取走。
| 时刻 | 观察到的状态 | 对驱动的影响 |
|---|---|---|
| 图像模式 ACK 到达 | 命令有回应 | 开始等待传感器读出图像 |
| 约 80 ms 静默期内 | IRQ 从低变高 | SPI 读取被暂时禁止,中断边沿已被监听线程消耗 |
| 静默期结束 | IRQ 仍为高电平 | 旧代码继续等下一次边沿,没有检查当前电平 |
| 等待结束 | cmd 0x17 超时 |
fprintd 报录入失败 |
再看监听线程的旧控制流,问题落到一个很不起眼的 continue 上。它用 poll() 同时等 GPIO 事件 fd 和内部 eventfd;收到内部唤醒后读掉计数,旧代码立刻跳回下一轮。即使 IRQ 早已保持高电平,这一轮也不会调用检查电平并读取 SPI 包的 listener_drain_irq_high()。
if (poll_fds[1].revents & POLLIN) { uint64_t wake_count; (void) read (self->wake_fd, &wake_count, sizeof (wake_count)); poll_fds[1].revents = 0; continue; }
/* 后面的 IRQ 电平检查被 continue 跳过 */listener_drain_irq_high (self);而 listener_drain_irq_high() 的第一道门就是 suppress:静默中直接返回;解除静默后只要 IRQ 仍为高,它才会读 SPI。旧实现的问题不是没有这段“按电平补读”的代码,而是静默结束时没有一个可靠的执行机会。GPIO 的边沿事件已经被取走,等下一次边沿可能要等到超时。
if (g_atomic_int_get (&self->suppress)) return;
enum gpiod_line_value v = gpiod_line_request_get_value (self->irq_req, self->irq_offset);if (v != GPIOD_LINE_VALUE_ACTIVE) return;
/* IRQ 仍为高电平,继续读取待处理的 SPI 包 */真正要补的是这个唤醒时机。我在 drivers/gdix51c0/ 做了两处配合修改:收到图像模式 ACK 的监听线程立即进入静默,关掉静默时通过 eventfd 唤醒监听线程,让它重新检查仍为高电平的 IRQ 并读取待处理数据。原先监听线程处理唤醒事件后直接 continue,这一行也要改掉,否则主动唤醒仍然读不到图像。代码在我的分支的监听线程实现和采图流程里。
修复一:解除静默时唤醒,并继续检查 IRQ
gdix51c0_listener_set_suppress(FALSE) 原来只是改一个原子变量。现在解除静默时还向 wake_fd 写入计数,唤醒 poll();监听线程读掉这个计数后不再 continue,让流程走到 listener_drain_irq_high()。函数会重新读取 IRQ 当前电平,不再要求传感器额外制造一次新边沿。
voidgdix51c0_listener_set_suppress (Gdix51c0Listener *self, gboolean on){ if (!self) return; g_atomic_int_set (&self->suppress, on ? 1 : 0); if (!on) { uint64_t wake_count = 1; (void) write (self->wake_fd, &wake_count, sizeof (wake_count)); }}这里用 eventfd 的原因很朴素:监听线程可能正堵在 poll(),单纯修改 suppress 不会让它醒。唤醒之后若还保留旧的 continue,效果也一样落空,所以这两处必须一起改。实际 diff 中,关键就是让处理完唤醒事件的线程继续向下执行:
(void) read (self->wake_fd, &wake_count, sizeof (wake_count));poll_fds[1].revents = 0;continue;/* Recheck IRQ level: the edge may have been consumed during capture. */注释在这里是解释控制流,真正起作用的是去掉 continue。后面的 listener_drain_irq_high() 会先检查 suppress 和 IRQ 电平,不会因为每次内部唤醒都无条件读 SPI。
修复二:在读到 ACK 的线程里进入静默
还有一个更窄的时间窗口:原实现先让等待 ACK 的调用者返回,再由调用者设置 suppress = TRUE。从 ACK 被监听线程读到,到调用者真正关掉 SPI 读取之间,监听线程仍可能继续碰总线。我增加了 suppress_on_ack 标志,在发命令前预先设好;监听线程识别到 ACK 时,原子地取走标志并立即开始静默,然后才把 ACK 放入队列唤醒调用者。
g_mutex_lock (&self->dispatch_lock);if (kind == GDIX51C0_PKT_READ && g_atomic_int_compare_and_exchange (&self->suppress_on_ack, 1, 0)) g_atomic_int_set (&self->suppress, 1);listener_enqueue_locked (self, kind, payload, n);g_cond_broadcast (&self->cond);g_mutex_unlock (&self->dispatch_lock);采图函数则在发送图像模式命令之前先“布防”,ACK 失败会清掉这个状态;ACK 成功后睡眠约 80 ms,再解除静默。下面是关键调用顺序,省略了错误对象、日志和重试分支,完整代码仍以仓库为准:
gdix51c0_listener_drain_cmd (self->listener, 0x17);gdix51c0_listener_arm_suppress_on_ack (self->listener, TRUE);
if (!gdix51c0_listener_send_ack (self->listener, img_setmode, sizeof (img_setmode), "img-setmode", &local_error)) { gdix51c0_listener_arm_suppress_on_ack (self->listener, FALSE); gdix51c0_listener_set_suppress (self->listener, FALSE); return NULL; }
g_usleep ((gulong) settle_ms * 1000); /* 默认 80 ms */gdix51c0_listener_set_suppress (self->listener, FALSE);这不是单纯把等待时间拉长。超时上限原本就到 1 秒,问题是等待期间没人把 IRQ 保持高电平对应的数据取出来;继续加超时只会让报错更晚出现。
我用什么判定它修好了
这次没有停在“补丁编译成功”或“设置里出现了指纹选项”。修复后的本机结果是:右手食指录入完成,fprintd-list 列出了 right-index-finger,随后一次实际触摸的 fprintd-verify -f right-index-finger xingwangzhe 返回 verify-match (done)。
| 阶段 | 本机结果 |
|---|---|
| 修复前 | fprintd 可识别设备;录入反复报 enroll-unknown-error,图像数据 0x17 超时 |
| 修复后录入 | enroll-completed,已录入右手食指 |
| 修复后验证 | 一次手指触摸得到 verify-match (done) |
我用的验证命令分别对应三种不同证据:列出设备和已录入手指、执行录入、再用手指实际匹配。别把第一条的“看到了设备”当成第三条的“指纹可用”。
fprintd-list xingwangzhefprintd-enroll -f right-index-finger xingwangzhefprintd-verify -f right-index-finger xingwangzhe录入最终出现 enroll-completed,验证出现 verify-match (done)。这两项结果比 GNOME 页面有没有动画更直接,也给补丁提供了从 SPI 数据读取到匹配的端到端证据。不过它只覆盖当时这一次本机操作,不代表所有手指、长时间运行或睡眠恢复都已测过。
后来我去掉临时诊断日志并重启服务,fprintd 仍能列出已录入的手指。再做的一次验证因为当时没有触摸传感器而超时,不能算作第二次成功,也不能据此判断补丁回退了。目前还没有做重启后的录入检查或 GDM 登录测试,所以我只把结论写到“本机完成录入,并实际匹配成功一次”。
同型号机器怎么参考这份修复
要在同型号机器上尝试,先核对自己的 DMI、ACPI\GDIX51C0 和 SPI 设备路径,再读我的仓库 README与上游安装说明。我本机保留了发行版的 fprintd,使用的安装选项是 --without-fprintd;仓库默认流程还会安装带持久化匹配学习补丁的 fprintd,两种选择的范围不同。不要只复制文章里的 C 片段到系统目录;驱动要随对应的 libfprint 源码构建,并正确配置服务和 SPI 访问。
git clone https://github.com/xingwangzhe/libfprint-goodix-spi-d15.gitcd libfprint-goodix-spi-d15./install.sh --without-fprintdfprintd-enroll -f right-index-fingerfprintd-verify这里的命令来自仓库说明,本文没有在写作时对另一台机器重新执行。安装器可能配置传感器首次使用所需的 TLS PSK;Windows/Linux 双系统共用传感器时,上游提示两边可能改写彼此的密钥,切换系统后的首次操作也可能变慢。安装前要先看上游首次运行说明,别把密钥切换和本文的采图超时混为一谈。
这份适配仓库按 GPL-3.0-only 发布,保留上游作者信息;PROVENANCE.md 记录了源码来源和许可证处理。华为的 Windows DLL、指纹图像、模板及密钥都没有放进仓库。
最让我意外的是,最后修掉的不是一整套“Linux 不支持华为指纹”的大问题,而是一个已经拉高的 IRQ 在静默窗口结束后没人再看。fprintd 识别到设备只是起点;手指真正能录进去,才算跨过了这道坎。
HUAWEI MateBook D15 2022 Ubuntu 指纹识别修复:Goodix GDIX51C0 采图超时
作者:xingwangzhe
本文链接:https://xingwangzhe.fun/posts/matebook-d15-goodix-gdix51c0-fingerprint/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论