# 小米平板 5（nabu）手写笔无线充电：从“吸上去没反应”到 6.14 内核自编驱动

> 环境：小米平板 5（codename **nabu**，SM8150，三星 UFS `KLUFG8RHGB-B0E1`）· Ubuntu 26.04 ARM64
> 内核：`6.14.11-nabu-tmm`（日常）/ `6.17.0-xiaomi-nabu-tmm-UFS-slowdown+`（备用）
> 时间：2026-11-04
> 授权：文中驱动源码来自 [sm8150-mainline/linux](https://gitlab.com/sm8150-mainline/linux)（GPL-2.0，作者 Nicola Guerrera）

---

## TL;DR

- 平板顶部的磁吸无线充电芯片是 **IDT P9418**（实际读出的 Chip ID 是 `0x9415`），挂在 `i2c-3 / 0x3b`。
- 6.14.11 内核**根本没有编这个驱动**，所以笔吸上去毫无反应——不是硬件坏了。
- 6.17 内核自带驱动，但那个内核对这台机器只有 **5V 慢充**（平板充电几乎不可用），且 UFS 被降频。
- 最终方案：**把 6.17 的 `idtp9418` 驱动移植到 6.14.11**，自己编译一个 out-of-tree 模块，
  让“快充 + 手写笔充电”共存。驱动侧改了两处，另外给上游驱动补了一个 SoC 重试逻辑。
- 顺带打通：手写笔蓝牙配对、GATT 信息读取、桌面小组件与 GNOME 顶栏插件的电量展示。

---

## 1. 背景与硬件

小米平板 5 的 Smart Pen 通过磁吸吸附在平板顶部短边，由平板内的线圈无线供电。Linux 下这条链路涉及三部分：

| 部分 | 说明 |
|---|---|
| 无线充芯片 | **IDT P9418**，i2c-3 地址 `0x3b`，设备树节点 `idtp9418@3b` |
| 霍尔传感器 | 检测笔是否吸附：`gpio101`（active-high）、`gpio134`（active-low） |
| 驱动 | `idtp9418`（`drivers/power/supply/idtp9418.c`），**2025-10-23** 才进 sm8150-mainline |

相关 GPIO（TLMM，`/sys/kernel/debug/gpio` 可看）：

| 功能 | GPIO |
|---|---|
| IRQ | 95 |
| 使能 enable | 3 |
| 升压 boost-enable | 47 |
| 霍尔 high / low | 101 / 134 |
| WPC-DET（旧 dtb 有，驱动没用） | 94 |

驱动暴露的节点：

- `/sys/class/power_supply/idtp9418/` → `online`、`capacity`、`charge_control_end_threshold`
- `/sys/bus/i2c/devices/3-003b/` → `chip_version`、`chip_status`（vout/iin/temp/soc）、`chip_powered`

---

## 2. 第一层：为什么“没反应”

设备树里有节点，不代表有驱动。检查：

```bash
ls /sys/bus/i2c/devices/3-003b/          # 设备存在
readlink /sys/bus/i2c/devices/3-003b/driver   # 悬空 → 没有驱动绑定
modinfo idtp9418                          # 6.14.11 上：Module not found
sudo i2cdetect -y 3                       # 0x3b 无应答（芯片没被使能，正常）
```

结论：6.14.11 的模块目录里**没有** `idtp9418.ko`，内核也没编进去。设备树节点成了“孤儿”。

> 小提示：芯片未上电时 i2c 不 ACK 是正常现象，不能据此判断硬件坏。

---

## 3. 第二层：6.17 上跑通

ESP 里已经装了 6.17 的 UKI（`EFI/ubuntu/6.17.0-UFS-slowdown+.efi`），rEFInd 选它启动，然后：

```bash
sudo modprobe idtp9418
```

笔吸上去（**笔尖朝左**），dmesg 立刻出现：

```
[idtp] Pen attached! Turning on reverse charging...
reverse charging start success
[charge-monitor] pen=1 soc=5
```

`/sys/class/power_supply/idtp9418` 出现，`online=1`，笔从 0% 一路充到 90%（约 18 分钟）。

**为什么 6.14.11 没有、6.17 有？**

- 6.14.11 构建于 **2025-08-14**，驱动首个提交在 **2025-10-23** —— 时间上就不可能有。
- 6.17 构建于 **2026-04-11**，自然包含。
- 更麻烦的是**设备树绑定也换了**：6.14 的 dtb 用小米 BSP 风格属性名
  （`idt,irq` / `hall,int3` / `hall,int4` / `idt,reverse-enable` / `idt,reverse-booset-enable`），
  驱动读的是新绑定（`irq-gpios` / `pen-det-active-high-gpios` / … / `enable-gpios` / `boost-enable-gpios`）。
  所以**直接拷 `.ko` 也 probe 不起来**。

---

## 4. 笔的坑：SoC 0xFF、充电上限、电量口径

### 4.1 SoC 读到 0xFF 就停充

驱动 `charge-monitor` 每 2 秒读一次笔的电量，原逻辑：

```c
if (!attached || soc < 0 || soc >= di->charge_limit)
    /* 停充 */
```

笔在**刚上电的一小段时间内不响应**电量查询（寄存器 `0x3a` 返回 `0xFF`），
于是驱动 2 秒内就判定失败并停充——表现就是“吸上去闪一下就没反应”。

### 4.2 充电上限 85%

`charge_control_end_threshold` 默认 **85**，写值上限 **95**：

```bash
echo 95 | sudo tee /sys/class/power_supply/idtp9418/charge_control_end_threshold
```

笔的电量按 10% 台阶上报（80 → 90），会一步跨过 85，所以在 90 停下是正常的。

### 4.3 停充后 capacity 归 0

驱动的 `POWER_SUPPLY_PROP_CAPACITY` 在 `!is_charging` 时直接返回 0，所以停充后读 `capacity` 是 0，
不是电量真的掉了。

### 4.4 两种电量的口径

| 来源 | 什么时候有 | 特点 |
|---|---|---|
| WPC（`idtp9418/capacity`） | 只在无线充电时 | 实时，但粒度粗、会跳（0→5→10→20→40） |
| BLE（GATT `0x2A19` / UPower） | 只要蓝牙连着 | 细，但充电时可能长时间不刷新 |

实测两者会差 10~20%，充满后（90%）又能对上——是**刷新节奏不同**，不是两套电量计。

---

## 5. 蓝牙：配对与能读到的信息

**为什么 MIUI 吸上自动连，Linux 不会**：MIUI 有小米笔系统服务，霍尔检测到吸附就主动唤醒并配对；
Linux/BlueZ 没有这个服务，笔平时不广播（省电），必须**长按笔上的按键约 5 秒**进入配对模式。

```bash
bluetoothctl --timeout 120 scan on
# 长按笔按键，出现 Xiaomi Smart Pen 后：
bluetoothctl pair C0:2B:3D:70:C6:E9
bluetoothctl trust C0:2B:3D:70:C6:E9
bluetoothctl connect C0:2B:3D:70:C6:E9
```

> 笔的蓝牙地址是**随机地址**，重新配对会变，脚本里别硬编码，按名字匹配。

能读到的 GATT 信息（GATT 读需要 root，BlueZ 的 D-Bus 策略）：

```bash
sudo busctl --system call org.bluez \
  /org/bluez/hci0/dev_XX/serviceXXXX/charXXXX \
  org.bluez.GattCharacteristic1 ReadValue 'a{sv}' 0
```

| 服务/特征 | 内容 | 实测值 |
|---|---|---|
| 0x180F / 0x2A19 | 电量 | 20% |
| 0x180A / 0x2A29 | 厂商 | `Xiaomi` |
| 0x180A / 0x2A24 | 型号 | `wPI07U81PC` |
| 0x180A / 0x2A26 | 固件版本 | `00.00.0703` |
| 0x180A / 0x2A28 | 软件版本 | `00.00.0703` |
| 0x180A / 0x2A50 | PnP ID | vendor `0x1915`（Nordic）/ product `0xEAEA` / ver `0x0001` |
| 0x1812 HID | Report Map | Report 0x10：4 个按键位；0x21：1 字节 feature |
| 0xFE59 | Nordic Secure DFU | 固件升级服务，**别乱刷** |

没有序列号（0x2A25）和硬件版本（0x2A27）；压感/倾角不走蓝牙（走 NVT 触控）。

---

## 6. 第三层：给 6.14.11 编驱动

### 6.1 准备

```bash
# 1) 保存当前 6.14.11 的 config（IKCONFIG）
zcat /proc/config.gz > ~/nabu-build/config-6.14.11

# 2) 下载 6.14.11 源码（约 143MB）
curl -L -o linux-6.14.11.tar.xz https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.11.tar.xz
tar -xJf linux-6.14.11.tar.xz

# 3) 配置 + 准备模块构建（本机 arm64 原生编译）
cd linux-6.14.11
cp ../config-6.14.11 .config
make ARCH=arm64 olddefconfig
make -j8 ARCH=arm64 modules_prepare
make -s kernelrelease     # 6.14.11-nabu-tmm
```

关键点：**`kernelrelease` 必须和运行内核一致**（`6.14.11-nabu-tmm`），否则模块加载时 vermagic 不匹配。
本机 `CONFIG_MODVERSIONS=n`、`CONFIG_MODULE_SIG=n`，所以不需要 `Module.symvers` 的 CRC、也不校验签名。

### 6.2 设备树绑定差异（改驱动，不改 dtb）

6.14 的 dtb 用旧名字，所以把驱动的 GPIO 获取换成“按旧属性名查找”。
6.14 没有 `devm_gpiod_get_from_of_node`，改用 legacy API：

```c
#include <linux/gpio.h>
#include <linux/of_gpio.h>

struct { const char *name; const char *prop; struct gpio_desc **gpio; unsigned long flags; }
gpio_map[] = {
    {"irq",                 "idt,irq",                 &di->gpios.idt_irq,              GPIOF_IN},
    {"enable",              "idt,reverse-enable",      &di->gpios.enable,               GPIOF_OUT_INIT_HIGH},
    {"boost-enable",        "idt,reverse-booset-enable",&di->gpios.boost_enable,         GPIOF_OUT_INIT_HIGH},
    {"pen-det-active-high", "hall,int3",               &di->gpios.pen_det_active_high,  GPIOF_IN},
    {"pen-det-active-low",  "hall,int4",               &di->gpios.pen_det_active_low,   GPIOF_IN},
};

for (int i = 0; i < ARRAY_SIZE(gpio_map); i++) {
    int gpio = of_get_named_gpio(di->dev->of_node, gpio_map[i].prop, 0);
    if (gpio < 0) return gpio;
    ret = devm_gpio_request_one(di->dev, gpio, gpio_map[i].flags, gpio_map[i].name);
    if (ret) return ret;
    *gpio_map[i].gpio = gpio_to_desc(gpio);
}
```

> 注意 `idt,reverse-booset-enable` 里的 **booset** 是 dtb 里就有的拼写，别手滑改掉。

### 6.3 编译与加载

源码树里没有 `Module.symvers` 时，modpost 会把所有符号报成 undefined。
`MODVERSIONS=n` 时这些符号在加载阶段由内核解析，所以可以直接：

```bash
make -C ~/nabu-build/linux-6.14.11 M=$PWD ARCH=arm64 KBUILD_MODPOST_WARN=1 modules
```

产物 `idtp9418.ko` 的 `vermagic` 必须是 `6.14.11-nabu-tmm SMP preempt mod_unload aarch64`。

安装并加载：

```bash
sudo cp idtp9418.ko /lib/modules/6.14.11-nabu-tmm/extra/
sudo depmod -a 6.14.11-nabu-tmm
sudo insmod idtp9418.ko        # 或 modprobe
```

加载后驱动自动绑定到 `3-003b`，`/sys/class/power_supply/idtp9418` 出现。
开机自动加载：`/etc/modules-load.d/charge-stylus.conf` 写一行 `idtp9418`。

### 6.4 重试补丁 v2

解决 4.1 的“0xFF 立刻停充”：

```c
/* 笔刚上电时 SoC 可能暂时读不到（0xFF），先重试 ~30 秒再放弃 */
if (soc >= 0)
    di->soc_retry = 0;
if (!attached || soc >= di->charge_limit || (soc < 0 && ++di->soc_retry > 15)) {
    /* 停充，soc_retry = 0 */
}
```

实测：吸附后第 2 秒笔就报出 `soc=20`，充电继续。

---

## 7. 展示层：终端小组件与顶栏插件

### 7.1 终端小组件

`~/.local/bin/watch_battery.sh`（桌面上有启动器“电池实时电压”）：

- 充电中：`● 充电中 10% 0.85 W 54 °C 上限 85%`
- 未充电：`○ 未充电（蓝牙电量 80%）`
- 没有驱动：`不可用（本内核无 idtp9418 驱动）`

功率 = `vout × iin`（从 `chip_status` 读，raw 值按 mV/mA 解释）。
注意：驱动每读一次 `chip_status` 会 `dev_info` 打日志，所以**每 60 秒才读一次**。

### 7.2 GNOME 顶栏插件

本地扩展 `batteryvitals@local`，在电池数值旁显示 `笔XX%`：

- 充电中：绿色，用 WPC 值
- 未充电但蓝牙连着：灰色，用 BLE 值（`upower`，3 分钟刷新一次）
- 都没有：隐藏（原来的 `笔—` 容易被看成“笔一”，已去掉）

> **大坑**：GNOME Shell 对扩展 JS 有**会话级模块缓存**。
> `gnome-extensions disable/enable`、重装 zip、改 `version` 都**不会**重新加载代码，
> 必须**注销重登**（或重启）才生效。

---

## 8. 两个内核的对比与取舍

| | 6.14.11-nabu-tmm | 6.17.0-UFS-slowdown+ |
|---|---|---|
| 构建时间 | 2025-08-14 | 2026-04-11 |
| 充电栈 | ln8000 电荷泵 + PD 9V/3A（快） | smb5，约 5V（慢，平板充满要 8~10 小时） |
| UFS 时钟 | 300MHz | 201.5MHz（为三星 UFS 稳定性降频） |
| 手写笔驱动 | 无（可自编模块补上） | 自带 |
| 电流符号 | 负 = 充电 | 正 = 充电（相反） |
| 模块数 | 594 | 656（+76 / −14） |

**结论**：6.14.11 自编模块后是“快充 + 笔充电”两全，日常用它；6.17 退居备用。

---

## 9. 踩坑清单 / 给后来者

1. **设备树有节点 ≠ 有驱动**：先看 `/sys/bus/i2c/devices/*/driver` 是否悬空。
2. **先确认内核版本与驱动提交时间的先后**：6.14.11 比驱动早两个月，不可能有。
3. **DT 绑定在新旧树之间会漂移**：BSP 属性名 vs mainline 属性名，移植时要么改驱动、要么重打 dtb/UKI。
4. **`devm_gpiod_get_from_of_node` 在 6.14 已不存在**：用 `of_get_named_gpio + devm_gpio_request_one + gpio_to_desc`。
5. **vermagic 必须逐字匹配**；`CONFIG_MODVERSIONS=n` 时不需要 Module.symvers，`KBUILD_MODPOST_WARN=1` 可绕过 modpost。
6. **驱动可能在笔刚上电时读到 0xFF**：给重试窗口，别第一次失败就放弃。
7. **无线充的 SoC 是粗粒度、会跳**；BLE 电量更细但充电时可能不刷新。两者差 10~20% 正常。
8. **GNOME 扩展改完必须重新登录**，disable/enable 不重载模块。
9. **启动额度**：nabu 的 ABL 每启动一次扣 `slot-retry-count`，7 次上限；频繁切内核有风险，
   能接 PC 时用 `fastboot set_active a` 补满。
10. **6.17 在三星 UFS 上要降频补丁**（201.5MHz），否则可能黑屏/不稳。

---

## 10. 附录

### 10.1 常用命令

```bash
# 笔是否在充
cat /sys/class/power_supply/idtp9418/{online,capacity}

# 芯片遥测（vout/iin/temp/soc，raw）
cat /sys/bus/i2c/devices/3-003b/chip_status

# 芯片版本（OTP/APP 固件版本）
cat /sys/bus/i2c/devices/3-003b/chip_version

# 驱动日志
sudo dmesg | grep -i idtp | tail -20

# 蓝牙电量
upower -e | grep bluetooth
upower -i /org/freedesktop/UPower/devices/bluetooth_generic_dev_XX

# 模块
modinfo idtp9418
lsmod | grep idtp
```

### 10.2 关键文件

| 文件 | 作用 |
|---|---|
| `/etc/modules-load.d/charge-stylus.conf` | 开机自动加载 `idtp9418` |
| `/lib/modules/6.14.11-nabu-tmm/extra/idtp9418.ko` | 自编模块 |
| `~/nabu-build/{idtp9418.c,idtp9418.h}` | 打补丁后的驱动源码 |
| `~/.local/bin/watch_battery.sh` | 终端小组件 |
| `~/.local/share/gnome-shell/extensions/batteryvitals@local/` | 顶栏插件 |

### 10.3 实测数据

| 场景 | 数值 |
|---|---|
| 无线充输出 | 0.85W（6.75V × 128mA）→ 2.3W（9.2V × 248mA） |
| 笔充电 | 0% → 90% 约 18 分钟 |
| 芯片温度 | 48 ~ 57 °C（无线充工作范围） |
| 平板快充（6.14） | 9V/3A PD PPS，电池 −4.2A，约 2%/分钟 |

---

*本文记录于 2026-11-04，所有命令均在小米平板 5（nabu）+ Ubuntu 26.04 上实测。*
*驱动源码来自 sm8150-mainline 社区，遵循 GPL-2.0。*