---
title: HUAWEI MateBook D15 2022 Ubuntu 指纹识别修复：Goodix GDIX51C0 采图超时
tags:
    - linux
    - ubuntu
    - huawei
    - matebook-d15
    - goodix
    - libfprint
    - fingerprint
categories:
    - linux
date: "2026-10-11 10:25:00"
desc: HUAWEI MateBook D15 2022（BoF-XX）的 Goodix GDIX51C0 指纹设备在 Ubuntu 上能被 fprintd 识别，却无法录入：采图静默期内 IRQ 升高，监听线程消耗中断边沿后未重新检查电平，导致图像数据超时。修复监听线程的唤醒时机后，本机完成指纹录入并得到一次 verify-match。
abbrlink: matebook-d15-goodix-gdix51c0-fingerprint
---
> **AI 辅助声明**：AI 协助排查驱动、修改代码并整理本文。硬件信息、报错和录入验证结果来自这台电脑的实际操作；尚未做过的登录与重启测试会明确写出。

同一台 MateBook D15，前脚刚把[内置扬声器的外放问题](https://xingwangzhe.fun/posts/matebook-d15-bofxx-hwsp0001-no-speaker/)解决，后脚又轮到电源键上的指纹识别。Windows 下有驱动，Ubuntu 的设置里却用不了。我手边还有华为的 Windows 驱动包，最初也想过能不能靠 Wine 把它带起来。

折腾到后来，真正卡住的地方已经不是“系统有没有看到设备”：`fprintd` 能找到指纹传感器，录入却一次次报 `enroll-unknown-error`。图像模式的 ACK 已经到了，后面的图像数据就是等不到。这篇记下我是怎么把超时追到 SPI 中断监听，再把它修好的。

<!--more-->

## 先认准这颗指纹传感器

这台机器是华为 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：

```bash title="确认 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 上游项目](https://github.com/berkekbgz/libfprint-goodix-spi)。上游原本面向 MateBook 16s，我在它的源码上适配这台 D15，并把改动放到公开仓库：[xingwangzhe/libfprint-goodix-spi-d15](https://github.com/xingwangzhe/libfprint-goodix-spi-d15)。我的分支基于上游提交 `010a665f54089a1632b1ab7be588b316ace934e2`；具体差异可以直接看[采图中断修复提交](https://github.com/xingwangzhe/libfprint-goodix-spi-d15/commit/c4ef869)。

把 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`。驱动的关键报错是：

```text title="录入时的采图超时"
image read failed after setmode ACK: gdix51c0: timed out awaiting cmd 0x17 after 1000000 us
```

`setmode 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()`。

```c title="gdix51c0-listener.c：修复前的关键控制流（节选）"
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 的边沿事件已经被取走，等下一次边沿可能要等到超时。

```c title="gdix51c0-listener.c：按 IRQ 电平决定是否读 SPI（节选）"
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`，这一行也要改掉，否则主动唤醒仍然读不到图像。代码在[我的分支的监听线程实现](https://github.com/xingwangzhe/libfprint-goodix-spi-d15/blob/d15-bofxx/drivers/gdix51c0/gdix51c0-listener.c)和[采图流程](https://github.com/xingwangzhe/libfprint-goodix-spi-d15/blob/d15-bofxx/drivers/gdix51c0/gdix51c0.c)里。

### 修复一：解除静默时唤醒，并继续检查 IRQ

`gdix51c0_listener_set_suppress(FALSE)` 原来只是改一个原子变量。现在解除静默时还向 `wake_fd` 写入计数，唤醒 `poll()`；监听线程读掉这个计数后不再 `continue`，让流程走到 `listener_drain_irq_high()`。函数会重新读取 IRQ **当前电平**，不再要求传感器额外制造一次新边沿。

```c title="gdix51c0-listener.c：解除静默时主动唤醒（实际补丁节选）"
void
gdix51c0_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 中，关键就是让处理完唤醒事件的线程继续向下执行：

```diff title="gdix51c0-listener.c：唤醒后继续检查 IRQ"
 (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 放入队列唤醒调用者。

```c title="gdix51c0-listener.c：读到 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，再解除静默。下面是关键调用顺序，省略了错误对象、日志和重试分支，完整代码仍以仓库为准：

```c title="gdix51c0.c：图像模式命令前布防与静默结束（流程节选）"
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)` |

我用的验证命令分别对应三种不同证据：列出设备和已录入手指、执行录入、再用手指实际匹配。别把第一条的“看到了设备”当成第三条的“指纹可用”。

```bash title="本机使用的 fprintd 验证命令"
fprintd-list xingwangzhe
fprintd-enroll -f right-index-finger xingwangzhe
fprintd-verify -f right-index-finger xingwangzhe
```

录入最终出现 `enroll-completed`，验证出现 `verify-match (done)`。这两项结果比 GNOME 页面有没有动画更直接，也给补丁提供了从 SPI 数据读取到匹配的端到端证据。不过它只覆盖当时这一次本机操作，不代表所有手指、长时间运行或睡眠恢复都已测过。

后来我去掉临时诊断日志并重启服务，`fprintd` 仍能列出已录入的手指。再做的一次验证因为当时没有触摸传感器而超时，不能算作第二次成功，也不能据此判断补丁回退了。**目前还没有做重启后的录入检查或 GDM 登录测试**，所以我只把结论写到“本机完成录入，并实际匹配成功一次”。

## 同型号机器怎么参考这份修复

要在同型号机器上尝试，先核对自己的 DMI、`ACPI\GDIX51C0` 和 SPI 设备路径，再读[我的仓库 README](https://github.com/xingwangzhe/libfprint-goodix-spi-d15#readme)与[上游安装说明](https://github.com/berkekbgz/libfprint-goodix-spi#readme)。我本机保留了发行版的 `fprintd`，使用的安装选项是 `--without-fprintd`；仓库默认流程还会安装带持久化匹配学习补丁的 `fprintd`，两种选择的范围不同。不要只复制文章里的 C 片段到系统目录；驱动要随对应的 libfprint 源码构建，并正确配置服务和 SPI 访问。

```bash title="仓库提供的安装与录入入口，执行前先阅读 README"
git clone https://github.com/xingwangzhe/libfprint-goodix-spi-d15.git
cd libfprint-goodix-spi-d15
./install.sh --without-fprintd
fprintd-enroll -f right-index-finger
fprintd-verify
```

这里的命令来自仓库说明，本文**没有**在写作时对另一台机器重新执行。安装器可能配置传感器首次使用所需的 TLS PSK；Windows/Linux 双系统共用传感器时，上游提示两边可能改写彼此的密钥，切换系统后的首次操作也可能变慢。安装前要先看[上游首次运行说明](https://github.com/berkekbgz/libfprint-goodix-spi#first-run)，别把密钥切换和本文的采图超时混为一谈。

这份适配仓库按 **GPL-3.0-only** 发布，保留上游作者信息；[PROVENANCE.md](https://github.com/xingwangzhe/libfprint-goodix-spi-d15/blob/d15-bofxx/PROVENANCE.md) 记录了源码来源和许可证处理。华为的 Windows DLL、指纹图像、模板及密钥都没有放进仓库。

最让我意外的是，最后修掉的不是一整套“Linux 不支持华为指纹”的大问题，而是一个已经拉高的 IRQ 在静默窗口结束后没人再看。`fprintd` 识别到设备只是起点；手指真正能录进去，才算跨过了这道坎。


---

**作者：**xingwangzhe

**本文链接：**[https://xingwangzhe.fun/posts/matebook-d15-goodix-gdix51c0-fingerprint/](https://xingwangzhe.fun/posts/matebook-d15-goodix-gdix51c0-fingerprint/)

本文采用[知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/)进行许可。