文章总结: 这篇文章讲述在RK3588Linux平台上通过修改SC8886充电芯片寄存器实现锂电池充电上限80%的实践。作者通过kprobe观测I2C报文发现驱动实际写寄存器0x02而非数据手册所述0x01,纠正了错误认知,实现了充电控制工具,并总结了格物方法论与过度操作导致内核崩溃的教训。
综合评分: 85
文章分类: 实战经验,其他
格物致知:在RK3588 Linux平台上增加锂电池充电限制的实践之旅
原创
SteveLsh
SteveLsh
格友
2026年9月25日 16:52
上海
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
【编者按】
今天是中秋佳节,魔都艳阳高照,气温超过了30度,有点像回到了夏天。但与夏天不同的是空气很通透,而且经常有清新的风吹来,不是夏天那般闷热。
上午,Steve发来一篇文章,当时没来得及细看。午觉醒来,坐到案头,打开细读,让我吃了一惊。这不是一篇普通的技术文章,这是一次“方法论”的实践。
在过去的十余年时间里,我在这个公众号里说了无数次“格物”,但真的不知道读者中有多少人看懂了我的意思。
稍微缩短一些,在过去5年左右的时间里,我在盛格塾平台上,开了很多门课,从Windows讲到Linux,从X86讲到ARM,从A核讲到M核,从基础的计算机系统讲到专业的AMBA和USB总线,从UEFI讲到U-Boot……但无论讲什么,我都会提到“格物”。每次讲到格物,我都热情洋溢,但打心里说,我真不知道有多少听者是愿意听的,更不敢想有多少人能听懂我想传达的深意。
看到Steve的文章,我倍感欣慰。我和Steve没有见过面,但是我们认识很久了,他是这个公众号的老读者,而且从读者变成了作者。Steve也听了我讲的很多课。现在我知道,我讲格物没有白讲,至少Steve听懂了,而且到了一个很高的高度。真是念念不忘,必有回响。
闲话打住,请大家与Steve一起开始格物之旅。
#
设备:RK3588(YourLand XiuFeng1,Ubuntu 23.04 / Linux 5.10.110)
话题:如何把充电上限限制在 80%,以及在这个过程中”格”出了什么
一、缘起:为什么要限制充电
锂电池的寿命与”充得多满”高度相关。长期维持在 100% 满电状态,会加速正极
材料的老化与电解液分解;把上限压在 80% 左右,通常能把循环寿命延长一倍以上。
笔记本厂商(Lenovo、Dell、ThinkPad)早就用 EC 或 ACPI 提供”充电阈值”,
但在 ARM 开发板上,这往往要自己动手。
目标很朴素:电量到 80% 就停充,掉到 75% 再恢复。
但这个朴素的目标,让我开启了一次完整的”格物”旅程。
二、格物:先看清硬件到底是什么
《大学》讲”致知在格物”,朱熹解为”即物而穷其理”——要到事物本身去穷究其理。
第一步不是写代码,而是把”物”看清楚。
在设备上追溯:
ls /sys/class/power_supply/
# bq25700-charger cw2017-battery gpio-charger
ls -la /sys/class/power_supply/bq25700-charger/device
# → ../../../../platform/feaa0000.i2c/i2c-2/2-006b
cat /sys/bus/i2c/devices/2-006b/name
# → sc8886
cat .../of_node/compatible
# → southchip,sc8886
# ti,bq25703
由此得到一幅清晰的图:
USB-C ──▶ FUSB302 (TCPC/PD) ──▶ SC8886 (充电 IC) ──▶ 2S 锂电池
│ I2C-2 @ 0x6B
│
CW2017 电量计 @ 0x63
│
RK3588 ── rk3x-i2c (0xfeaa0000)
- • SC8886:南芯(Southchip)充电管理芯片,寄存器兼容 TI bq25703
- • CW2017:电量计(fuel gauge),负责报百分比、电压、电流
- • FUSB302:USB-C PD 协议协商
这就是”物”的本来面目。注意 SC8886 是通过 “ti,bq25703” 这个 compatible
字符串让内核驱动认出它的——它是”行为兼容”的国产替代,而不是 TI 原厂芯片。
这处细节,后来成了整件事的关键伏笔。
三、一次错误的”致知”:数据手册的陷阱
要停充,最直接的办法是把充电电流设为 0。那么,ChargeCurrent 是哪个寄存器?
当时手上只有 TI bq25703A 数据手册,上面白纸黑字写着:
Register 0x01 — ChargeCurrent()
Bits 14:6 充电电流, 64mA/LSB
于是写下:
REG_CHARGE_CURRENT = 0x01 # 依据:TI 数据手册
i2c_write(0x01, 0x0000) # 停充
看起来”生效”了——写入后充电器状态确实变成了 Discharging。
但这里埋着一个方法论的错误:数据手册是”二手间接之知”,不是”即物之知”。
SC8886 与 bq25703 只是行为兼容,寄存器映射完全可能不同。
更糟的是,那次”生效”的观察本身是不可靠的:写寄存器之前,我们
unbind(解绑)了内核驱动——而解绑这个动作本身就会改变充电器状态。
把”解绑的副作用”误当成了”写寄存器的效果”,这是典型的
因果混淆。
而且,当时的读回校验也用错了地方:sysfs 的 constant_charge_current
读的是实测电流(OUTPUT_CHG_CUR),根本不是我们写的那个设定值寄存器,
所以它永远无法验证我们的写操作。
未格其物,先致其知 —— 于是”知”是假的。
四、格物之法:让内核自己说出真相
要破除臆测,只有一条路:直接观测”物”的行为。
“物”的核心是内核驱动。它每次操作 I2C 设备,都会经过 i2c_transfer()。
于是写一个 kprobe 模块,挂在 i2c_transfer 上,解码每一个 i2c_msg:
struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* I2C_M_RD? */
__u16 len; /* 长度 */
__u8 *buf; /* 数据 */
};
结果一目了然:
i2c_spy: addr=0x63 W len=1 data=04 ← CW2017 电量计轮询
i2c_spy: addr=0x63 W len=1 data=02
i2c_spy: addr=0x6b W len=3 data=02 c0 09 ← 充电芯片,写寄存器 0x02 !
i2c_spy: addr=0x6b W len=3 data=0e 00 1e
i2c_spy: addr=0x6b W len=3 data=0a 80 1c
关键发现:驱动从来不写寄存器 0x01。 它写的是 0x02、0x0e、0x0a。
再把调用栈打出来(只关心充电配置路径,用 per-task 标志过滤):
i2c_transfer
regmap_i2c_write
_regmap_raw_write_impl
_regmap_write
_regmap_update_bits ← 读-改-写(位域!)
regmap_update_bits_base
regmap_field_update_bits_base
bq2570x_pd_notifier_call+0x28c/0x318 ★ 真正的调用者
← power_supply_changed_work
Workqueue: events power_supply_changed_work
插拔 USB-C 时的写入路径,根本不是我们以为的 bq25700_enable_charger(),
而是电量上报驱动的 notifier —— bq2570x_pd_notifier_call()。
至此,”物”开口说话了。
五、真相:回到驱动源码
有了实测线索,再回去读驱动源码 bq25700_charger.c,
字段表赫然在目:
/* bq25703_reg_fields[] —— 本驱动真正的映射 */
[CHARGE_CURRENT] = REG_FIELD(0x02, 6, 12); /* 寄存器 0x02, bit 6..12 */
[MAX_CHARGE_VOLTAGE] = REG_FIELD(0x04, 4, 14);
[INPUT_VOLTAGE] = REG_FIELD(0x0a, 6, 13);
[INPUT_CURRENT] = REG_FIELD(0x0e, 8, 14);
寄存器 0x01 从头到尾是个幻影。
而且交叉验证严丝合缝:
实测: reg 0x02 = 0x09c0
字段: (0x09c0 >> 6) & 0x7f = 39
换算: 39 × 64mA = 2496mA ≈ 2500mA
设备树: ti,charge-current = <0x2625a0> = 2500000uA = 2500mA ✅ 完全吻合
三条独立证据(实测报文、驱动字段表、设备树配置)相互印证,
这才叫”知”。
顺带还格出了几个必须知道的细节:
- 1. 字节序:regmap 配置为
REGMAP_ENDIAN_LITTLE,16 位值按
[reg, lo, hi]传输。 - 2. 位域:CHARGE_CURRENT 只占 bit 6..12,必须读-改-写,
直接整寄存器写 0 会破坏其它位。 - 3. status 字段会骗人:
battery/status报的 “Charging” 来自芯片
寄存器 0x20 的IN_FCHRG位(”快充阶段进行中”),只要插着适配器
就是 1,与我们设置多少电流完全无关。真正要看的指标是
current_now(实际电流)与设定值本身。 - 4. 功率路径:SC8886 是 NVDC 架构。插着适配器时,系统由适配器供电,
电池是静止的(既不充也不放)。所以当停充后,电量百分比会
纹丝不动——这不是故障,恰恰是”电池在休息”,正是延寿想要的效果。
把电流设为 0 之后:
reg 0x02 = 0x0000 → 设定值 0 mA (我们写的,已读回校验)
current_now = 0 uA (实际电流为零 = 确实没在充)
capacity = 84% 且稳定 (电池静止,符合 NVDC 功率路径)
charger/status = "Charging" (假象,见上文第 3 点)
六、知行合一:让”知”变成可验证的”行”
王阳明讲”知行合一”——知而不行,只是未知。工程上,这就是
每一步都要可验证。
最终的工具 /home/geduer/charge_ctl.py 把上述认知固化下来:
| 要点 | 实现 |
| — | — |
| 正确寄存器 | 0x02 的 bit 6..12(而非 0x01) |
| 读-改-写 | 保留 bit0..5 / bit13..15,只改目标位域 |
| 小端传输 | [reg, lo, hi] |
| 写后校验 | 写完立即读回,字段不符即报 MISMATCH |
| 不解绑驱动 | I2C_SLAVE_FORCE 可直接访问设备,无需 unbind |
| 判断依据 | 用设定值/实际电流,不用会骗人的 status |
sudo python3 charge_ctl.py status # 含权威判定 VERDICT
sudo python3 charge_ctl.py read # 寄存器全量 dump
sudo python3 charge_ctl.py stop # 充电电流 → 0
sudo python3 charge_ctl.py set 1000 # 设定 1000 mA
sudo python3 charge_ctl.py monitor 80 # 守护:80% 停、75% 恢复
七、代价:一次”过度之格”的教训
格物也要有分寸。为了获得 I2C 设备的独占访问,早期方案采用了
unbind → 写寄存器 → rebind 的办法。这个动作看起来无伤大雅,
实则闯了祸:
5 × notifier_chain_register 崩溃 ← 同一个 notifier_block 被重复注册
4 × kernel Oops, 其中:
pc : regmap_field_read+0x28/0x88
lr : bq25700_power_supply_get_property+0x188/0x280
→ 之后读取 /sys/class/power_supply/bq25700-charger/* 会让读取进程 SIGSEGV
原因:该驱动的 remove() 不做清理(会打印 “duplicate filename” 告警),
每次 bind 重新 probe() 都会重复注册 notifier、并留下悬空的
power_supply 对象;此后读取它的属性就是访问已释放内存 → 内核 Oops。
教训很直白:
- • “够用即止”:实测证明
I2C_SLAVE_FORCE无需解绑即可读写,
那个 unbind 本就不必要。 - • 对副作用要保持警惕:一个”看起来只是临时释放”的操作,
可能污染整个内核状态。 - • 内核态的错误只能靠重启清除——代价不小。
八、结语:格物的次序
回顾全程,方法论其实很清晰:
| 阶段 | 动作 | 结果 |
| — | — | — |
| ① 格物 | 查 sysfs / 设备树,画清硬件拓扑 | 知道 SC8886 ≈ bq25703 |
| ② 臆测之误 | 只凭数据手册断定 0x01 | ❌ 错误且被”因果混淆”掩盖 |
| ③ 即物 | kprobe 观测真实 I2C 报文与调用栈 | 发现驱动只写 0x02/0x0e/0x0a |
| ④ 穷理 | 回读驱动字段表 + 数据手册 + 设备树三方印证 | ✅ 0x02 bit6..12,2496mA 吻合 |
| ⑤ 知行合一 | 写成可验证的工具,读回校验、不解绑 | ✅ 停充成功且指标自洽 |
| ⑥ 知止 | 移除不必要的 unbind,承认并记录代价 | ✅ 避免再次污染内核 |
“格物”不是读资料,而是让事物本身说话。
数据手册会误导,经验直觉会混淆因果,只有让内核打印出它真实的行为、
再用多条独立证据交叉印证,才算真正”致知”。
而”知”若不能落成”行”(可验证的工具、可复现的判据),
便不是真知。
所谓致知在格物者,言欲致吾之知,在即物而穷其理也。
—— 朱熹《大学章句》
附1:关键数据速查
| 项目 | 值 |
| — | — |
| 充电芯片 | SC8886(southchip,sc8886 / ti,bq25703),I2C-2 @ 0x6B |
| 电量计 | CW2017(cellwise,cw2017),I2C-2 @ 0x63 |
| PD 控制器 | FUSB302 @ 0x22 |
| I2C 控制器 | rk3x-i2c,物理 0xfeaa0000 |
| CHARGE_CURRENT | 寄存器 0x02,bit 6..12,64mA/LSB |
| MAX_CHARGE_VOLTAGE | 寄存器 0x04,bit 4..14 |
| INPUT_VOLTAGE | 寄存器 0x0a,bit 6..13 |
| INPUT_CURRENT | 寄存器 0x0e,bit 8..14 |
| 字节序 | 小端:[reg, lo, hi] |
| 设备树默认电流 | 2500mA(字段值 39) |
| 插拔时的写入者 | bq2570x_pd_notifier_call() (不是enable_charger()) |
| 权威判据 | reg 0x02 字段值 + cw2017/current_now |
| 不可信指标 | charger/status 、battery/status(来自 reg0x20 的 IN_FCHRG 位) |
| 禁止操作 | 对 bq25700-charger 做 unbind/bind(会污染内核 notifier 与 power_supply) |
附2:文章中提到的代码
// SPDX-License-Identifier: GPL-2.0/* * i2c_spy.c — 监控"充电配置路径"里的 I2C 传输并打印调用栈 * * 背景 (本板实测): * 插拔 USB-C 时, 充电寄存器写入【不是】走的 bq25700_enable_charger(), * 而是走 power_supply notifier: * charger-detect → power_supply notifier → bq2570x_pd_notifier_call() * → bq25700_field_write() * → regmap → i2c_transfer * 所以必须 hook bq2570x_pd_notifier_call (以及其它相关函数)。 * * 机制: * 对 watch_syms[] 里每个函数注册 kprobe(入口)+kretprobe(返回), * 维护 per-task 深度计数; i2c_transfer 的 pre_handler 只有在 * "当前任务正处于被监控函数调用链内" 时才打印 msg + dump_stack()。 * * 编译: make * 加载: sudo insmod i2c_spy.ko * sudo insmod i2c_spy.ko verbose=1 # 打印所有传输(诊断用) * sudo insmod i2c_spy.ko addr=-1 # 不过滤地址 * sudo insmod i2c_spy.ko stack=0 # 不打印调用栈 * 查看: sudo dmesg | grep -A 20 "i2c_spy: \[watch" * 卸载: sudo rmmod i2c_spy */#include <linux/module.h>#include <linux/kernel.h>#include <linux/init.h>#include <linux/kprobes.h>#include <linux/i2c.h>#include <linux/version.h>#include <generated/utsrelease.h>
static int addr = 0x6b;module_param(addr, int, 0444);MODULE_PARM_DESC(addr, "filter by i2c slave addr (-1 = all)");
static bool stack = true;module_param(stack, bool, 0444);MODULE_PARM_DESC(stack, "dump_stack() on watched paths");
static bool verbose;module_param(verbose, bool, 0444);MODULE_PARM_DESC(verbose, "log ALL transfers, not just watched paths");
/* ---- 要监控的"调用者"函数 ---- * 按本板实测的充电路径排列; 注册失败(符号不存在/不可 probe)的会被跳过。 */static const char *watch_syms[] = {"bq2570x_pd_notifier_call", /* ★ 实测插拔时走这里 */"bq25700_charger_evt_handel","bq25700_charger_usb_bc_handel","bq25700_enable_charger","bq25700_disable_charge",};
#define NWATCH ARRAY_SIZE(watch_syms)
static struct kprobe kps[NWATCH];static struct kretprobe krps[NWATCH];static int nregistered;
/* ---- per-task 跟踪 ---- */static struct task_struct *watch_task;static int watch_depth;static const char *watch_name; /* 当前命中的被监控函数名 */
static unsigned long total_seen;static unsigned long hit_count;
#define I2C_M_RD_LOCAL 0x0001
/* ============ 被监控函数: 入口 ============ */static int watch_entry(struct kprobe *p, struct pt_regs *regs){ watch_task = current; watch_name = p->symbol_name; watch_depth++;return 0;}
/* ============ 被监控函数: 返回 ============ */static int watch_return(struct kretprobe_instance *ri, struct pt_regs *regs){if (current == watch_task) { watch_depth--;if (watch_depth <= 0) { watch_depth = 0; watch_task = NULL; watch_name = NULL; } }return 0;}
/* ============ i2c_transfer ============ */static int i2c_xfer_pre(struct kprobe *p, struct pt_regs *regs){struct i2c_msg *msgs;int num, i;int from_watch;
from_watch = (watch_task == current && watch_depth > 0);
if (!verbose && !from_watch)return 0;
msgs = (struct i2c_msg *)regs->regs[1]; num = (int)regs->regs[2];
if (!msgs || num <= 0 || num > 64)return 0;
for (i = 0; i < num; i++) {struct i2c_msg *m = &msgs[i]; u16 maddr, mflags, mlen; u8 buf[8];int j, n;
maddr = m->addr; mflags = m->flags; mlen = m->len;
if (addr >= 0 && maddr != (u16)addr)continue;
n = mlen < sizeof(buf) ? mlen : sizeof(buf);if (m->buf)memcpy(buf, m->buf, n);
total_seen++;if (from_watch) hit_count++;
if (from_watch)pr_info("i2c_spy: [watch:%s] addr=0x%02x %s len=%u data=", watch_name ? watch_name : "?", maddr, (mflags & I2C_M_RD_LOCAL) ? "R" : "W", mlen);elsepr_info("i2c_spy: [other] addr=0x%02x %s len=%u data=", maddr, (mflags & I2C_M_RD_LOCAL) ? "R" : "W", mlen);
for (j = 0; j < n; j++)pr_cont("%02x ", buf[j]);if (mlen > n)pr_cont("...");pr_cont("\n");
if (stack && from_watch)dump_stack(); }return 0;}
static struct kprobe kp_xfer = { .symbol_name = "i2c_transfer", .pre_handler = i2c_xfer_pre,};
static int __init i2c_spy_init(void){int ret, i;
ret = register_kprobe(&kp_xfer);if (ret < 0) {pr_err("i2c_spy: register i2c_transfer failed: %d\n", ret);return ret; }
for (i = 0; i < NWATCH; i++) {struct kprobe *kp = &kps[nregistered];struct kretprobe *krp = &krps[nregistered];
memset(kp, 0, sizeof(*kp)); kp->symbol_name = watch_syms[i]; kp->pre_handler = watch_entry;
ret = register_kprobe(kp);if (ret < 0) {pr_info("i2c_spy: skip '%s' (not probeable, ret=%d)\n", watch_syms[i], ret);continue; }
memset(krp, 0, sizeof(*krp)); krp->kp.symbol_name = watch_syms[i]; krp->handler = watch_return; krp->maxactive = 16;
if (register_kretprobe(krp) < 0) {pr_warn("i2c_spy: kretprobe '%s' failed (entry-only)\n", watch_syms[i]);/* 入口探针仍有效 */ }
pr_info("i2c_spy: watching '%s' @ %px\n", watch_syms[i], kp->addr); nregistered++; }
if (!nregistered) {pr_err("i2c_spy: no watch function registered\n");unregister_kprobe(&kp_xfer);return -ENOENT; }
pr_info("i2c_spy: ready — %d watch fn, filter addr=0x%x (-1=all), stack=%d, verbose=%d\n", nregistered, addr, stack, verbose);return 0;}
static void __exit i2c_spy_exit(void){int i;
for (i = 0; i < nregistered; i++) {unregister_kretprobe(&krps[i]);unregister_kprobe(&kps[i]); }unregister_kprobe(&kp_xfer);
pr_info("i2c_spy: unloaded (seen=%lu, watch hits=%lu)\n", total_seen, hit_count);}
module_init(i2c_spy_init);module_exit(i2c_spy_exit);
MODULE_LICENSE("GPL");MODULE_AUTHOR("geduer");MODULE_DESCRIPTION("Trace i2c_transfer from charger-config paths with call stack");MODULE_VERSION("3.0");
#!/usr/bin/env python3"""charge_ctl.py — RK3588 (SC8886 / bq25700-charger) 充电电流控制★ 重要更正 (2026-09-23) 旧版本写的是"寄存器 0x01 = ChargeCurrent", 那是错的。 权威依据是驱动源码的字段表 bq25703_reg_fields[] (bq25700_charger.c): [CHARGE_CURRENT] = REG_FIELD(0x02, 6, 12); # reg 0x02, bit 6..12, 64mA/LSB [MAX_CHARGE_VOLTAGE] = REG_FIELD(0x04, 4, 14); [INPUT_VOLTAGE] = REG_FIELD(0x0a, 6, 13); [INPUT_CURRENT] = REG_FIELD(0x0e, 8, 14); 0x01 来自 TI bq25703A 数据手册, 与该厂商驱动映射不一致 (SC8886 是行为兼容, 非寄存器完全一致的克隆)。 另外两点要点: 1) regmap 配置为 REGMAP_ENDIAN_LITTLE → 16 位值按 [reg, lo, hi] 字节序传输 2) CHARGE_CURRENT 是【位域】(bit 6..12), 必须【读-改-写】, 不能整寄存器直接写 0 (会破坏 bit0..5 / bit13..15 的其它设置) 复位后的 DTS 默认: 2500mA → 字段值 39 (= 2500/64 ≈ 39)用法: sudo ./charge_ctl.py status # 显示状态 + 当前电流字段值 sudo ./charge_ctl.py read # dump 充电芯片所有寄存器 sudo ./charge_ctl.py stop # 停止充电 (电流字段置 0) sudo ./charge_ctl.py start # 恢复充电 (回到 DTS 默认 2500mA) sudo ./charge_ctl.py set 1000 # 设为指定电流 (mA) sudo ./charge_ctl.py monitor 80 # 守护: 到 80% 停充, 掉到 75% 恢复注意: - 需要 root (访问 /dev/i2c-2) - ★ 不使用 unbind/bind: I2C_SLAVE_FORCE 可直接访问设备; 反复解绑/重绑会把驱动的内核状态搞坏 (notifier 重复注册 + 悬空 power_supply) → 读它的 sysfs 会 kernel Oops。 历史版本用过 unbind, 已废弃并改为禁用桩函数。"""import osimport sysimport timeimport fcntl# ---------------------------------------------------------------- 常量I2C_BUS = 2I2C_ADDR = 0x6BDEVICE_ID = "2-006b"I2C_DRIVER = "bq25700-charger"I2C_SLAVE_FORCE = 0x0706# CHARGE_CURRENT 字段: 寄存器 0x02, bit 6..12 (7 bit 宽), 步进 64mAREG_CHARGE_CURRENT = 0x02FIELD_LSB = 6FIELD_MSB = 12FIELD_MASK = ((1 << (FIELD_MSB - FIELD_LSB + 1)) - 1) # 0x7FFIELD_SHIFT = FIELD_LSBCURRENT_STEP_MA = 64# DTS (yourland.dts): ti,charge-current = <0x2625a0> = 2500000 uA = 2500 mADEFAULT_CURRENT_MA = 2500# 只读参考字段REG_MAX_CHARGE_VOLTAGE = 0x04REG_INPUT_VOLTAGE = 0x0AREG_INPUT_CURRENT = 0x0EREG_DEVICE_ID = 0x2FSYS_CHARGER = "/sys/class/power_supply/bq25700-charger"SYS_BATTERY = "/sys/class/power_supply/cw2017-battery"DRV_BIND = f"/sys/bus/i2c/drivers/{I2C_DRIVER}"# ---------------------------------------------------------------- 工具def read_file(path, default="N/A"): try: with open(path) as f: return f.read().strip() except Exception: return defaultdef ma_to_field(ma): """mA → 7bit 字段值 (四舍五入), 并做范围钳制""" idx = int(round(ma / CURRENT_STEP_MA)) return max(0, min(FIELD_MASK, idx))def field_to_ma(idx): return idx * CURRENT_STEP_MA# ---------------------------------------------------------------- I2C 底层def i2c_read16(fd, reg): """读 16 位寄存器 (LE: 返回 b0 | b1<<8)""" os.write(fd, bytes([reg])) data = os.read(fd, 2) if len(data) != 2: raise IOError(f"short read for reg 0x{reg:02x}") return data[0] | (data[1] << 8)def i2c_write16(fd, reg, val): """写 16 位寄存器 (LE: 发送 [reg, lo, hi])""" lo = val & 0xFF hi = (val >> 8) & 0xFF os.write(fd, bytes([reg, lo, hi]))def _open_i2c(): fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR) fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR) return fd# ---------------------------------------------------------------- 驱动绑定## ★★★ 严重警告 (2026-09-24): 不要再使用 unbind/bind ★★★## 实测后果: 反复解绑/重绑 bq25700-charger 驱动会把内核状态搞坏——# 5 次 notifier_chain_register 崩溃 (同一 notifier_block 被重复注册)# 4 次 kernel Oops, 其中:# pc : regmap_field_read+0x28/0x88# lr : bq25700_power_supply_get_property+0x188/0x280# → 读取 /sys/class/power_supply/bq25700-charger/* 会让读取进程 SIGSEGV## 原因: 该驱动的 remove() 不做清理 (会打印 "duplicate filename" 告警),# 每次 bind 重新 probe() 都会重复注册 notifier、并留下悬空的# power_supply 对象 → 之后读它的属性就访问已释放内存 → Oops。## 结论: **完全不需要 unbind** —— I2C_SLAVE_FORCE 可以直接读写设备,# 实测读 reg 0x02 无需解绑即可得到 0x09c0 (字段 39 = 2496mA)。# 下面两个函数保留仅为留档, 请勿调用。def unbind(): raise RuntimeError("DISABLED: unbind/bind corrupts driver state " "(notifier double-register + dangling power_supply). " "Not needed — I2C_SLAVE_FORCE works with the driver bound.")def rebind(): raise RuntimeError("DISABLED: see unbind()")# ---------------------------------------------------------------- 高层操作def get_charge_current_ma(): """读回 CHARGE_CURRENT 字段 (自身设置值, 非测量值) 读操作无需解绑驱动 —— I2C_SLAVE_FORCE 可直接读。 实测 reg 0x02 读回 0x09c0 (字段 39 = 2496mA = DTS 2500mA)。 """ fd = _open_i2c() try: raw = i2c_read16(fd, REG_CHARGE_CURRENT) finally: os.close(fd) idx = (raw >> FIELD_SHIFT) & FIELD_MASK return raw, idx, field_to_ma(idx)def set_charge_current_ma(ma): """读-改-写 CHARGE_CURRENT 位域 (不解绑驱动!) 不再使用 unbind/bind —— I2C_SLAVE_FORCE 可直接访问设备, 而 unbind/bind 会破坏驱动内核状态 (见上方警告)。 """ idx = ma_to_field(ma) fd = _open_i2c() try: old = i2c_read16(fd, REG_CHARGE_CURRENT) new = (old & ~(FIELD_MASK << FIELD_SHIFT)) | (idx << FIELD_SHIFT) i2c_write16(fd, REG_CHARGE_CURRENT, new) time.sleep(0.05) chk = i2c_read16(fd, REG_CHARGE_CURRENT) # 读回校验 finally: os.close(fd) chk_idx = (chk >> FIELD_SHIFT) & FIELD_MASK ok = (chk_idx == idx) print(f" reg 0x{REG_CHARGE_CURRENT:02x}: 0x{old:04x} -> 0x{new:04x} " f"(读回 0x{chk:04x})") print(f" CHARGE_CURRENT 字段 = {chk_idx} -> {field_to_ma(chk_idx)} mA " f"{'✅ verified' if ok else '❌ MISMATCH'}") return okdef dump_regs(): """dump 充电芯片寄存器 (只读, 不解绑驱动)""" fd = _open_i2c() try: print(" reg : value (little-endian 16-bit)") print(" ----:-------") for reg in list(range(0x00, 0x20)) + [0x2F]: try: v = i2c_read16(fd, reg) except Exception: v = None tag = "" if reg == REG_CHARGE_CURRENT: i = (v >> FIELD_SHIFT) & FIELD_MASK if v is not None else 0 tag = f" <- CHARGE_CURRENT = {i} ({field_to_ma(i)} mA)" elif reg == REG_MAX_CHARGE_VOLTAGE: tag = " <- MAX_CHARGE_VOLTAGE" elif reg == REG_INPUT_VOLTAGE: tag = " <- INPUT_VOLTAGE" elif reg == REG_INPUT_CURRENT: tag = " <- INPUT_CURRENT" elif reg == REG_DEVICE_ID: tag = " <- DEVICE_ID" print(f" 0x{reg:02x} : " + (f"0x{v:04x}" if v is not None else " n/a ") + tag) finally: os.close(fd)def status(): print("=== charger (bq25700-charger) ===") for k in ("status", "online", "health", "constant_charge_current", "constant_charge_current_max", "input_current_limit"): print(f" {k:28s} = {read_file(f'{SYS_CHARGER}/{k}')}") print("=== battery (cw2017) ===") for k in ("capacity", "status", "current_now", "voltage_now", "temp", "charge_full", "charge_full_design"): print(f" {k:28s} = {read_file(f'{SYS_BATTERY}/{k}')}") print("=== CHARGE_CURRENT setpoint (reg 0x02 bit6..12) ===") raw = idx = ma = None try: raw, idx, ma = get_charge_current_ma() print(f" reg 0x02 = 0x{raw:04x}") print(f" field = {idx}") print(f" setpoint = {ma} mA") except Exception as e: print(f" (read failed: {e})") # ★ status 字段不可信: 插着电源时芯片状态机恒报 "Charging", # 与我们把电流设成 0 无关。真正要看的是【实际电流】与【setpoint】。 cur = read_file(f"{SYS_BATTERY}/current_now", "0") try: cur_ua = int(cur) except ValueError: cur_ua = 0 print("=== VERDICT (authoritative) ===") if idx == 0: print(" setpoint = 0 → charging DISABLED by this tool ✅") else: print(f" setpoint = {ma} mA → charging ENABLED") print(f" battery current_now = {cur_ua} uA " f"({'no current flowing' if cur_ua == 0 else 'current flowing'})") print(" note: charger/status == 'Charging' is NOT a故障 —— 它来自芯片") print(" reg0x20 的 IN_FCHRG 位, 与电流设置无关。")def monitor(limit=80, hyst=5, interval=30): """到 limit% 停充; 掉到 (limit-hyst)% 恢复 ★ 不能用 charger/status 判断! 插着电源时 status 恒为 "Charging" (它来自芯片 reg0x20 的 IN_FCHRG/IN_PCHRG 位, 与我们设置的电流无关)。 改为以【实际电流字段值】为准: setpoint > 0 表示允许充电; setpoint == 0 表示已停充。 """ print(f"monitor: stop at {limit}%, resume at {limit - hyst}%, " f"poll {interval}s (Ctrl-C to stop)") while True: try: cap = int(read_file(f"{SYS_BATTERY}/capacity", "-1")) if cap < 0: time.sleep(interval) continue _, idx, setpoint_ma = get_charge_current_ma() stopped = (idx == 0) if cap >= limit and not stopped: print(f"[{time.strftime('%H:%M:%S')}] {cap}% >= {limit}% " f"(setpoint {setpoint_ma}mA) → stop charging") set_charge_current_ma(0) elif cap <= (limit - hyst) and stopped: print(f"[{time.strftime('%H:%M:%S')}] {cap}% <= " f"{limit - hyst}% → resume {DEFAULT_CURRENT_MA}mA") set_charge_current_ma(DEFAULT_CURRENT_MA) else: state = "stopped" if stopped else f"charging {setpoint_ma}mA" print(f"[{time.strftime('%H:%M:%S')}] {cap}% ({state}) — no action") time.sleep(interval) except KeyboardInterrupt: print("\nmonitor: stopped by user") break except Exception as e: print(f"monitor: error {e}", file=sys.stderr) time.sleep(interval)# ---------------------------------------------------------------- maindef main(): if os.geteuid() != 0: print("error: must run as root (needs /dev/i2c-2 access)", file=sys.stderr) return 1 cmd = sys.argv[1] if len(sys.argv) > 1 else "status" if cmd == "status": status() elif cmd == "read": dump_regs() elif cmd == "stop": print(f"stopping charge (CHARGE_CURRENT → 0 mA):") return 0 if set_charge_current_ma(0) else 1 elif cmd == "start": print(f"resuming charge (CHARGE_CURRENT → {DEFAULT_CURRENT_MA} mA):") return 0 if set_charge_current_ma(DEFAULT_CURRENT_MA) else 1 elif cmd == "set": if len(sys.argv) < 3: print("usage: charge_ctl.py set <mA>", file=sys.stderr) return 1 ma = int(sys.argv[2]) print(f"setting CHARGE_CURRENT → {ma} mA:") return 0 if set_charge_current_ma(ma) else 1 elif cmd == "monitor": limit = int(sys.argv[2]) if len(sys.argv) > 2 else 80 monitor(limit) else: print(__doc__) return 1 return 0if __name__ == "__main__": sys.exit(main())
最初的错误脚本:
#!/usr/bin/env python3"""Battery charge limiter for RK3588 (SC8886/bq25703 charger).Controls charging via I2C register writes (unbind → write → rebind).
Safe: unbinds the kernel driver only long enough for one register write,then immediately rebinds it. Total window without driver: ~50ms.
Usage: sudo ./charge_ctl.py status - Show charging status sudo ./charge_ctl.py stop - Stop charging immediately sudo ./charge_ctl.py start - Resume charging sudo ./charge_ctl.py monitor - Daemon: limit at 80% (or custom %)"""import os, sys, time, fcntl
# ConfigurationI2C_BUS = 2I2C_ADDR = 0x6bDEVICE_ID = "2-006b"I2C_DRIVER = "bq25700-charger"REG_CHARGE_CURRENT = 0x01I2C_SLAVE_FORCE = 0x0706DEFAULT_CHARGE_CURRENT = 0x1E00 # ~3A
def read_sysfs(path): try: with open(path) as f: return f.read().strip() except: return "N/A"
def unbind(): """Unbind the kernel I2C driver. Returns True if successful.""" try: with open(f"/sys/bus/i2c/drivers/{I2C_DRIVER}/unbind", "w") as f: f.write(DEVICE_ID) time.sleep(0.1) return True except Exception as e: return False
def rebind(): """Rebind the kernel I2C driver.""" try: with open(f"/sys/bus/i2c/drivers/{I2C_DRIVER}/bind", "w") as f: f.write(DEVICE_ID) time.sleep(0.1) return True except: return False
def i2c_write(reg, value): """Write 16-bit big-endian value to I2C register.""" try: fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR) fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR) data = bytes([reg, (value >> 8) & 0xff, value & 0xff]) os.write(fd, data) os.close(fd) return True except Exception as e: return False
def i2c_read(reg): """Read 16-bit value from I2C register.""" try: fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR) fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR) os.write(fd, bytes([reg])) data = os.read(fd, 2) os.close(fd) if len(data) == 2: return (data[0] << 8) | data[1] return None except: return None
def set_charge_current(value): """Unbind → write register → rebind. Returns True on success.""" unbound = unbind() if not unbound: return False ok = i2c_write(REG_CHARGE_CURRENT, value) rebind() return ok
def status(): print(f"Charger: {read_sysfs('/sys/class/power_supply/bq25700-charger/status')}") print(f"Battery: {read_sysfs('/sys/class/power_supply/cw2017-battery/capacity')}%") print(f"Bat status: {read_sysfs('/sys/class/power_supply/cw2017-battery/status')}") # Read I2C register unbound = unbind() if unbound: val = i2c_read(REG_CHARGE_CURRENT) rebind() if val is not None: ma = ((val >> 6) & 0x3ff) * 64 print(f"ChargeCurrent: 0x{val:04x} ({ma} mA)") else: print("ChargeCurrent: N/A") else: print("ChargeCurrent: driver busy")
def stop(): if set_charge_current(0x0000): print("Charging stopped ✅") return True print("Failed ❌") return False
def start(): if set_charge_current(DEFAULT_CHARGE_CURRENT): print("Charging resumed ✅") return True print("Failed ❌") return False
def monitor(limit=80, hyst=5): """Monitor battery, stop charging at limit%, resume at (limit-hyst)%""" stopped = False while True: try: cap = int(read_sysfs("/sys/class/power_supply/cw2017-battery/capacity")) st = read_sysfs("/sys/class/power_supply/bq25700-charger/status")
if st == "Charging" and cap >= limit and not stopped: if stop(): stopped = True elif st in ("Discharging", "Not charging") and cap <= (limit - hyst) and stopped: if start(): stopped = False
time.sleep(30) except KeyboardInterrupt: break except: time.sleep(30)
if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: sudo charge_ctl.py [status|stop|start|monitor]") sys.exit(1) a = sys.argv[1] if a == "status": status() elif a == "stop": stop() elif a == "start": start() elif a == "monitor": monitor(int(sys.argv[2]) if len(sys.argv) > 2 else 80)
***
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:格友 SteveLsh
SteveLsh《格物致知:在RK3588 Linux平台上增加锂电池充电限制的实践之旅》