修一台 Kindle Paperwhite 3 的「电池无效 -22」# 修一台 Kindle Paperwhite 3 的「电池无效 -22」——从误判硬件到自编译内核
记录时间:2026-10-24 ~ 10-27
设备:Kindle Paperwhite 3(7 代,2015),板型 Muscat WFO,i.MX6SoloLite
固件:原 5.16.2.1.1(已被刷为非原厂内核,is_production=0)
结果:成功启动,问题根治
一、问题是什么
机器长按电源后:
- 指示灯先绿、后橙色闪烁
- 屏幕刷新一次,背光点亮
- 随后停在
Your Kindle needs repair. Repair Code 0+ 底部一行BATTERY INVALID: -22 - 无限循环重启
串口抓到的关键日志:
[ 5.925502] INFO:BATTERY INVALID: -22
[ 6.159269] === BATTERY ID IS INVALID. STOPPING BOOT ===
-22 不是「电量低」,而是 Linux 的 EINVAL(Invalid argument)。它发生在内核 initramfs 阶段——比挂载主系统还早。
二、先搞懂机理,否则全是瞎猜
2.1 校验逻辑
Kindle 的 PMIC 是 MAX77696。内核 max77696-battery.c 里有个函数:
static int wario_check_battery(void)
{
int temp_delta = 0;
temp_delta = abs(wario_battery_temp_c - display_temp_c);
if ((temp_delta < BATT_ID_CHECK_TEMP_DELTA) || /* 15℃ */
(strncmp(system_bootmode, "diags", 5) == 0) ||
(lab126_board_is(BOARD_ID_WARIO) || lab126_board_is(BOARD_ID_WOODY)) ||
wario_battery_check_disabled) {
return 1;
}
return 0;
}
放行条件是一个「或」链:温差 < 15℃、或进 diags、或特定板型、或校验被禁用。
return 0 之后,内核做两件事:
if (!wario_battery_valid) {
LLOG_DEVICE_METRIC(...);
/* Turn OFF charging */
rc = max77696_charger_set_mode(MAX77696_CHARGER_MODE_OFF); /* 关充电! */
}
这就是为什么很多人报 -22 后「充不进电」——不是充电坏了,是内核主动关的。
2.2 两个温度从哪来
wario_battery_temp_c:电量计 NTC 测的电池温度,约 27~28℃display_temp_c:屏幕温度,由max77696-adc.c采 PMIC 的 ADC 得到
如果屏幕温度读不到,内核会硬写一个默认值:
ADC value out of thermistor range! setting display temp to 75 deg C
75℃ − 28℃ = 47℃ > 15℃ → 校验失败 → -22。
2.3 屏幕 NTC 到底在哪
PW3 显示总成后面有 4 条排线:
| 排线 | 作用 | 含 NTC |
|---|---|---|
| display cable | EPD 面板本体 | ✅ 是 |
| touch cable | 电容触摸(I²C) | ❌ |
| backlit cable | 前光 LED | ❌ |
| button key cable | 按键 | ❌ |
结论:NTC 在显示排线上,不在触摸、不在背光。
依据:E Ink 把 NTC 定义为面板本体的一部分;触摸层和背光层都是后贴的独立层;固件变量名就叫 display_temp_c,用途是补偿墨水在不同温度下的驱动波形(波形文件名 ..._TC.wrf.gz 里的 TC = Temperature Compensation)。
三、第一轮:我的判断全错了
当时我根据论坛帖子,给出的概率分布是:
| 嫌疑 | 我估的概率 |
|---|---|
| 屏幕热敏电阻 | 55~65% |
| 电池侧 NTC / 座子 | 25~30% |
| 主板 PMIC | 10~15% |
然后实测把这些全推翻了。
3.1 实测:全部正常
| 测点 | 结果 |
|---|---|
| 主板电池触点各引脚对地反向压降 | 正常 |
| 屏幕排线上 NTC 阻值 | 约 10 kΩ,正常 |
| NTC 与排线座联通 | 正常 |
| 排线座相关引脚对地反向压降 | 正常 |
| 电池电压 | 3991 mV(约 70~75%,不是过放) |
四条实测把「屏侧」和「电池侧」全都排除了。
⚠️ 这里有个方法论教训:「反向压降正常」只能证明保护二极管/ESD 结构和对地通路没问题,不能证明信号到达了 PMIC 的 ADC 输入端。 这两件事在万用表层面区分不出来。
3.2 换了个思路:diags 也失败
进 diags 模式后:
BOOTMODE OVERRIDE: DIAGS
[ 0.331530] max77696-adc max77696-adc.0: adc conversion timed out
[ 0.422881] max77696-adc max77696-adc.0: adc conversion timed out
3.0.35-lab126 #1 PREEMPT Mon Jun 1 23:54:30 PDT 2015
diags 下照样超时。
这一点很关键:wario_battery_valid 是靠内核变量计分的,diags 只影响判决,不影响 ADC 驱动本身。之前所有日志都来自 2023 自制内核(builder@eink-builds),存在「是不是自制内核搞坏了驱动」的合理怀疑。现在换了个 2015-06-01 的内核,依然超时。
⇒ 排除内核 bug,坐实是硬件问题。
四、深入 PMIC:一个被「回读残留」坑掉的过程
4.1 MAX77696 是四从地址的复合芯片
从 GPL 源码 drivers/mfd/max77696-i2c.c 找到:
/* MAX77696 I2C Slave Addresses
* Real-Time Clock / Energy Harvester W 0xD0 R 0xD1
* USB Interface Circuit W 0x6A R 0x6B
* Fuel Gauge W 0x68 R 0x69
* Power Management / GPIO/ other W 0x78 R 0x79
*/
换算成 7-bit:
| 模块 | 7-bit 地址 | 实测 probe |
|---|---|---|
| RTC | 0x68 |
✅ |
| UIC | 0x35 |
✅ |
| Fuel Gauge | 0x34 |
✅ |
| Power Management | 0x3C |
✅ |
在 u-boot 的 bist 里跑 i2c probe,返回 Valid chip addresses: 00 34 35 3C 68 —— 四个子模块全部 ACK,芯片是活的。
4.2 寄存器速查(来自官方源码)
include/max77696_registers.h:
| 寄存器 | 地址 | 关键位 |
|---|---|---|
| ADCCNTL | 0x26 |
bit4 ADCCONV(转换完成自动清零)、bit3:2 ADCAVG、bit1 ADCREFEN、bit0 ADCEN |
| ADCDLY | 0x27 |
转换延迟,每步 500 µs |
| ADCSEL0/1 | 0x28/0x29 |
通道选择 |
| ADCCHSEL | 0x2A |
读结果前选通道 |
| ADCDATAL/H | 0x2B/0x2C |
结果 |
| ADCICNFG | 0x30 |
bit3:2 IADCMUX、bit1:0 IADC(恒流源档位) |
IADC 档位编码:
| 代码 | 电流 |
|---|---|
0x03 |
≥50 µA |
0x02 |
≥10 µA |
0x01 |
≥5 µA |
0x00 |
<5 µA |
4.3 屏幕温度读的是 AIN0,AIN2 是接地噪声参考
adc_disp_gettemp_work() 原文:
max77696_adc_read(MAX77696_ADC_CH_AIN0, &data); /* 真正的屏幕温度 */
if (lab126_board_is(BOARD_ID_MUSCAT_WFO) || ...) {
u16 noise = 0x0;
max77696_adc_read(MAX77696_ADC_CH_AIN2, &noise); /* AIN2 = 接地噪声参考 */
data = abs(data - noise); /* 两者相减 */
}
display_temp_c = lkup_temp_data(data);
这解释了两件事:
- 为什么
adc conversion timed out总是成对出现、间隔约 20 ms —— 一次采样要读 AIN0 和 AIN2 各一次,两次都超时就是两条; - 有两条模拟通道参与,都要查。
4.4 踩坑:pmic rd 的读数根本不可信
在 u-boot 里读寄存器,得到一组诡异数据:
pmic rd 3c 26 → 0x80
pmic rd 3c 27 → 0x80
pmic rd 3c 2a → 0x80
pmic rd 3c 2b → 0x80
pmic rd 3c 2c → 0x80
五个功能完全不同的寄存器,读出同一个值。 真实寄存器中 0x27(转换延迟)、0x2A(通道号)、0x2B/2C(数据)取值范围完全不同,不可能全等。
于是做了个决定性的对照实验:
pmic wr 3c 2a 05 ← 写入 0x05
pmic rd 3c 2a ← 读回 0x05 ✅
pmic rd 3c 26 ← 读回 0x05 ⚠️
pmic rd 3c 27 ← 读回 0x05 ⚠️
pmic rd 3c 2b ← 读回 0x05 ⚠️
pmic rd 3c 2c ← 读回 0x05 ⚠️
全部返回最后写入的值。这是「回读残留」——pmic rd 在 0x3C 上返回的是 I²C 总线上的残留值,不是寄存器真实内容。
教训:u-boot 的 pmic rd 在这个从地址上不可信,基于它得出的所有结论都要作废。 后来改用 i2c md 34 20 10 验证(Fuel Gauge 能读出 14 个不同值),证明块读路径是可靠的,只有 0x3C 的单读路径有问题。
五、突破:恒流源是好的,坏的是内核写寄存器的方式
5.1 决定性实验
在 u-boot 里整字节写入:
pmic wr 3c 30 03 ← ADCICNFG:IADC=0x03(50µA), IADCMUX=0(AIN0)
NTC 两端立刻出现 0.38 V。
算一下:0.38 V ÷ 50 µA = 7600 Ω。用 NTC 的 B 值公式(R25=10k,B=3435)反推,7600 Ω 对应 约 32℃ —— 和室温完全吻合。
这证明了:PMIC 的 50 µA 恒流源完全正常,NTC、排线、座子全部完好。
5.2 但重启后又变回 0 V
现象很明确:bist 里写就有电压,一启动内核就没了。
5.3 根因:读-改-写遇到坏读取
max77696_write_masked() 的实现(include/linux/mfd/max77696.h):
static __always_inline int max77696_write_masked(struct max77696_i2c *i2c,
u8 addr, u8 mask, u8 val)
{
rc = max77696_read(i2c, addr, &buf); /* ← 先读当前值 */
if (unlikely(rc)) return rc;
buf = ((buf & (~mask)) | (val & mask)); /* ← 只改目标位 */
return max77696_write(i2c, addr, buf); /* ← 整个字节写回 */
}
而 max77696_adc_cfgadc_write() 用的是 reg_set_bit,走的就是这条路径:
rc = max77696_adc_reg_set_bit(me, ADCICNFG, IADC, tmp); /* 读-改-写 */
因果链:
内核 probe → 读 ADCICNFG(读取不可靠)
→ 读到脏值
→ 读-改-写把整个字节写坏(IADC 变 0,或 IADCMUX 切错)
→ 恒流源关闭 / 电流走错通道
→ NTC 上 0 V
→ ADC 读到 0 → 查表得 75℃
→ 温差 47℃ > 15℃
→ wario_battery_valid = 0
→ STOPPING BOOT
而 ADCCNTL 的位分布注定这个缺陷致命:
bit4 = ADCCONV bit3:2 = ADCAVG bit1 = ADCREFEN bit0 = ADCEN
adcavg_write() 的目标掩码只覆盖 bit3:2,ADCEN 和 ADCREFEN 完全依赖读回来的值来保持。读到脏值 → 写回时 ADCEN 变 0 → ADC 被关掉。
5.4 这解释了全部现象
| 观察 | 解释 |
|---|---|
u-boot 写 0x30=0x03 立刻出电压 |
u-boot 用整字节直写,不读-改-写 |
| 内核一启动恒流源消失 | 内核走读-改-写,读错就写坏 |
pmic rd 五个寄存器同值 |
同一批 I²C 读取不可靠 |
| 电量计、充电、背光、LED 全正常 | 它们不依赖这一路 ADC |
六、软件方案的两条路
6.1 路线 A:逆向 recovery-util(最后放弃了)
-22 的报错文本在 initramfs 里的 recovery-util(一个 1,021,768 字节的独立静态 ELF,32-bit ARM)。
反汇编后定位到判定代码:
0x8394: bl 0xca74 ; gasgauge_battery_id_valid()
0x8398: subs r6, r0, #0
0x839c: beq 0x8408 ; ★ r0==0 才继续启动
0x83a0: mov r0, #3
0x83a4: bl 0xbe98
0x83a8: mov r1, r6
0x83ac: ldr r0, [pc] → "BATTERY INVALID: %d\n"
0x83b0: bl 0xc464 ; printf
0x83c4: ldr r0, [pc] → "=== BATTERY ID IS INVALID. STOPPING BOOT ===\n"
并确认了返回值约定(0xca74 内部):
cb2c: ldr r3, [sp, #4] ; 读出的 battery_id_valid
cb30: cmp r3, #0
cb34: bne 0xca90 ; 非 0 → 有效
ca90: mov r0, #0 ; ★ return 0 = 有效
...
cb38: mvn r0, #21 ; ★ return -22 = 无效
-22 正是返回值,被 printf("%d") 打出来。
于是补丁就是一个字节:
| 指令 | 机器码(小端) | |
|---|---|---|
| 原 | beq 0x8408 |
19 00 00 **0A** |
| 改 | b 0x8408 |
19 00 00 **EA** |
文件偏移 0x39F,0x0A → 0xEA。
6.2 两次失败,因为压缩器用错了
第一次重建后,刷入内核:
Starting kernel ...
(之后无任何输出)
内核自解压阶段就挂了。
排查发现 zImage 尾部有 52 字节的绝对偏移索引表:
a4e33600 = 0x0036E3A4
2c460000 = 0x0000462C ← gzip 起点
98e33600 = 0x0036E398 ← zImage 总长
重压缩后文件变短了 11,971 字节,偏移表全部失效。
第二次改用「填充到等长」策略,还是失败。直到从官方源码包里翻到答案:
scripts/Makefile.lib 第 200 行:
cmd_gzip = (cat (filter-out FORCE,^) | gzip -n -f -9 > $@)
在编译机上验证:
gzip -n -9 -c img.raw > gz_repro.bin
cmp gz_repro.bin gz_orig.bin
# *** 完全相同 ***
用 gzip -n -9 可以逐字节复现原厂 gzip 流。 而 Python 的 gzip.compress(level=9) 生成的是 3,567,221 字节,比原厂少 11,971——根本不是同一种压缩。
| 压缩方式 | 结果长度 |
|---|---|
| 原厂 | 3,579,192 |
| Python gzip level=9 | 3,567,221(差 -11,971) |
GNU gzip -n -9 |
3,579,192(完全一致) ✅ |
改用正确的压缩器后,新流只比原流小 1 字节,填充 1 个 0x00 即可。
但刷上去还是 Starting kernel 后无输出。
这次放弃了逆向路线。 根本障碍:改 initramfs 一个字节必须重压缩,而 zImage 自解压器对压缩流有自己的校验逻辑,我无法完全掌握。
七、转向编译内核
7.1 源码从哪来
Kindle 内核是 Linux(GPLv2),Amazon 必须开源。官方 GPL 包:
Kindle_src_5.15.1_3889320042.tar.gz 322 MB
└── gplrelease/linux-3.0.35.tar.gz 116 MB ← 内核源码
包里还有 BUILD_HOW-TO.txt,明确写了:
Kindle Paperwhite (6th & 7th generation)、Kindle Voyage (7th generation)、Kindle (7th generation):
imx60_wario_defconfig
PW3 正是 7th generation。
7.2 编译环境搭建
在 Ubuntu 18.04(12 核、11G、420G 空闲)上:
sudo apt install -y bison flex libncurses-dev libelf-dev gawk \
git diffstat texinfo chrpath socat libssl-dev u-boot-tools
sudo apt install -y gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi
工具链踩坑记录:
| 尝试 | 结果 |
|---|---|
| koxtoolchain 2025.05 预编译 | ❌ GLIBC_2.32/2.33/2.34 not found(18.04 只有 2.27) |
| koxtoolchain 2021.12 | ❌ 下载失败 |
apt 的 gcc-arm-linux-gnueabi 7.5.0 |
✅ 可用 |
apt 版本的前缀 arm-linux-gnueabi- 正好和 Amazon 官方要求一致。
7.3 三个编译错误
① compiler-gcc7.h: No such file or directory
3.0.35 内核只带 compiler-gcc3.h 和 compiler-gcc4.h,而 gcc 7.5 需要 gcc7 的头:
cd include/linux
for v in 5 6 7 8 9 10 11 12 13 14; do
ln -sf compiler-gcc4.h compiler-gcc${v}.h
done
② timeconst.pl 的 defined(@array)
Can't use 'defined(@array)' (Maybe you should just omit the defined()?)
at kernel/timeconst.pl line 373.
新版 Perl 禁用了 defined(@array),改掉:
- if (!defined(@val)) {
+ if (!@val) {
③ 编译成功,但产物只有 2.28 MB
原厂 uImage 是 3.43 MB。差的那 1.15 MB 是 initramfs。
CONFIG_INITRAMFS_SOURCE=""
空的。 Amazon 的 GPL 包只给代码,不给 initramfs 里的二进制(recovery-util、kinit、.ko 都是闭源产物)。
7.4 initramfs 怎么补——两次试错
第一次:直接指向解包目录
CONFIG_INITRAMFS_SOURCE="/tmp/imx/root"
产物 3.42 MB,结构正确,刷入后:
[0.408721] max77696-adc max77696-adc.0: adc conversion timed out
(然后循环重启)
第二次:用源码包的 initramfs.list
生成清单指向解包目录,产物 3.37 MB,刷入后:
[0.551309] FAT-fs (loop1): bogus number of reserved sectors
[0.573498] ERROR:/dev/loop1:Invalid argument(22)
[0.576901] ERROR:find_file: Failed to open /mnt/wfm/waveform_to_use: error 2
[0.576915] ERROR:insmod(/lib/modules/mxc_epdc_eink.ko): open(2) error: No such file or directory
[0.577494] INFO:Rebooting ...
找到根因:mxc_epdc_eink.ko 没有被带进去。
而 Amazon 自带的 initramfs.list 本身就不完整——它只列了 mxc_epdc_fb.ko,漏了 mxc_epdc_eink.ko(11,924 字节,在原厂 cpio 里是有的):
# eink driver
file /lib/modules/mxc_epdc_fb.ko /initrd/initramfs/lib/mxc_epdc_fb.ko 0755 0 0
7.5 正确的做法:整份原厂 cpio 嵌进去
CONFIG_INITRAMFS_SOURCE="/tmp/orig_initramfs.cpio"
104 个条目一个不少。
结果:framework:started:time=80640 —— 系统起来了。
八、最后一个坑:补丁放错位置
v3 能启动,但日志里有:
[ 29.702230] max77696-adc max77696-adc.0: adc conversion timed out
[ 60.661781] ...
[ 60.691811] ...
[ 120.821775] ...
(每 60 秒一轮,持续不断)
原因:我最早把补丁加在了 wario_check_battery() 里:
static int wario_check_battery(void)
{
int temp_delta = 0;
display_temp_c = wario_battery_temp_c; /* ← 我的补丁在这里 */
temp_delta = abs(wario_battery_temp_c - display_temp_c);
但这个函数只在启动时调用一次。 而 display_temp_c 每 60 秒会被 ADC 驱动覆盖:
adc_disp_gettemp_work() ← DISP_ADC_TEMP_THRESHOLD = 60000 ms
display_temp_c = lkup_temp_data(data); ← ADC 坏 → data=0 → 返回 75℃
所以:启动时补丁生效(能过校验),运行 60 秒后温度又变回 75℃,波形温补按 75℃ 走。
修正:补丁移到数据源头
static void adc_disp_gettemp_work(struct work_struct *work)
{
/* PATCH: 屏幕 NTC 通路故障,ADC 每次转换都超时。
* 原逻辑: 读 AIN0/AIN2 -> lkup_temp_data(0) -> 恒返回 75C
* 改法 : 直接采用电池温度作为屏幕温度近似值
*/
extern int wario_battery_temp_c;
display_temp_c = wario_battery_temp_c;
schedule_delayed_work(&adc_disp_temp_work, msecs_to_jiffies(DISP_ADC_TEMP_THRESHOLD));
return;
}
同时把 wario_check_battery() 恢复为原厂实现——理由是:
display_temp_c现在由 ADC 驱动持续维护为合理值,源头已正确;wario_check_battery()只是消费者,自然通过校验:abs(27−28) = 1 < 15;- 如果在消费者端也改,反而会掩盖问题:将来 ADC 修好,
display_temp_c会变成真实屏幕温度,
而那个赋值会让它永远等同电池温度,修正就白做了。
九、最终结果对比
| 项目 | 修之前 | v3(错误位置) | v5(最终) |
|---|---|---|---|
BATTERY INVALID: -22 |
❌ 阻断启动 | ✅ 消失 | ✅ 消失 |
out of thermistor range |
❌ 75℃ | ❌ 仍有 | ✅ 消失 |
Unknown waveform mode → MODE_07 |
— | ❌ 有 | ✅ 消失 |
Failed to load fw! (err -2) |
— | ❌ 有 | ✅ 消失 |
FAT-fs (loop1): bogus reserved sectors |
— | ❌ 出现 2 次 | ✅ 消失 |
| 显示就绪时间 | — | 27.4 秒 | ✅ 12.5 秒 |
| 系统启动完成 | 永不 | 80.6 秒 | ✅ 60.9 秒 |
| ADC timeout 日志 | 每 60 秒 | 每 60 秒 ×6 | ⚠️ 仅 1 次(无害) |
一个意外的连带修复
「未知波形模式」确实是温度引起的 —— 这个推断被证实了。
温度输入从 75℃ 修正到约 28℃ 后,波形加载、模式识别、FAT 挂载全部自愈。这说明错误的温度值会级联影响整条波形处理链路。
十、这一路学到的东西
1. 错误码不是错误原因
-22 是 EINVAL,但真正的原因是温差校验失败。看到错误码就去搜「EINVAL 怎么修」会走偏。
2. 万用表能力有边界
「反向压降正常」只证明保护结构和对地通路没问题,不能证明信号到达了芯片内部。这两件事在万用表层面无法区分。
3. 工具的读数可能是假的
pmic rd 返回「回读残留」——五个不同寄存器读出同一个值(最后写入的值)。
识别方法:读一个内容必然不同的寄存器做对照。 如果它也返回同值,说明读取不可信。
4. 「整字节写」和「读-改-写」是两种完全不同的操作
一个在坏读取下正常工作,另一个会把整字节写坏。在 I²C 不可靠的环境里,任何 read-modify-write 都是定时炸弹。
5. 压缩参数必须精确匹配
Python 的 gzip 和 GNU gzip -9 不是同一种压缩。前者 3,567,221,后者 3,579,192,差 11,971 字节。
在需要精确重建的场合(如内核镜像),必须用原始工具和原始参数。
6. 官方源码包可能故意不完整
Amazon 的 GPL 包只给代码,不给二进制。而它自带的 initramfs.list 也是不完整的(漏了 mxc_epdc_eink.ko)。
最可靠的做法:从自己设备的原厂镜像里提取完整 initramfs,整个嵌回去。
7. 补丁要打在数据源头,不是消费者
wario_check_battery() 是消费者,adc_disp_gettemp_work() 是生产者。
改消费者只能骗过一次校验;改生产者才能让整个系统都拿到正确值。
8. 上下文决定结论
我一开始基于论坛帖子估出「PMIC 概率 10~15%」,结果实测把屏侧和电池侧全部排除,概率反转成 85%。
论坛数据是别人的机器,自己的实测才是证据。
附:最终交付物
| 文件 | 说明 |
|---|---|
uImage_v5 |
最终内核(3.41 MB),含完整原厂 initramfs + 温度源头补丁 |
uImage.orig |
原厂内核,回滚用 |
| 编译环境 | Ubuntu 18.04 + gcc-arm-linux-gnueabi 7.5.0 + 官方 GPL 5.15.1 源码 |
刷入
fastboot flash kernel uImage_v5
验证要点
日志里应当看到:
- ❌ 没有
BATTERY INVALID - ❌ 没有
out of thermistor range - ❌ 没有
Unknown waveform mode - ✅
display:started在 15 秒内 - ✅
framework:started在 70 秒内
后记
这台机器的 ADC 硬件问题没有修——max77696-adc 那一路仍然超时,日志里偶尔还会出现 adc conversion timed out。
但它的影响被完全隔离了:屏幕温度不再从 ADC 读取,改由电池温度近似。
代价是:波形温度补偿用的是「电池温度」而非「真实屏幕温度」。充电时电池可能比屏幕高 10℃,在 TRT 温度分档的粒度内通常可接受,但不是零误差。
要完全正确,只能换 PMIC。 而那是 BGA 封装,DIY 性价比为零。
对一个已经报废边缘的 2015 年设备来说,「能读、显示正常、不报错」已经是好结局了。
文章评论