格物致知:在RK3588Linux平台上增加锂电池充电限制的实践之旅

admin 2026-09-27 04:21:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 这篇文章讲述在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
设备树: &nbsp;ti,charge-current = <0x2625a0> = 2500000uA = 2500mA &nbsp; ✅ 完全吻合

三条独立证据(实测报文、驱动字段表、设备树配置)相互印证, 这才叫”知”。

顺带还格出了几个必须知道的细节:

  1. 1. 字节序:regmap 配置为 REGMAP_ENDIAN_LITTLE,16 位值按 [reg, lo, hi] 传输。
  2. 2. 位域:CHARGE_CURRENT 只占 bit 6..12,必须读-改-写, 直接整寄存器写 0 会破坏其它位。
  3. 3. status 字段会骗人:battery/status 报的 “Charging” 来自芯片 寄存器 0x20 的 IN_FCHRG 位(”快充阶段进行中”),只要插着适配器 就是 1,与我们设置多少电流完全无关。真正要看的指标是 current_now(实际电流)与设定值本身。
  4. 4. 功率路径:SC8886 是 NVDC 架构。插着适配器时,系统由适配器供电, 电池是静止的(既不充也不放)。所以当停充后,电量百分比会 纹丝不动——这不是故障,恰恰是”电池在休息”,正是延寿想要的效果。

把电流设为 0 之后:

reg 0x02 &nbsp; &nbsp; = 0x0000 &nbsp; → 设定值 0 mA &nbsp; &nbsp; &nbsp;(我们写的,已读回校验)
current_now &nbsp;= 0 uA &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(实际电流为零 = 确实没在充)
capacity &nbsp; &nbsp; = 84% 且稳定 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(电池静止,符合 NVDC 功率路径)
charger/status = "Charging" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(假象,见上文第 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&nbsp;python3 charge_ctl.py status&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 含权威判定 VERDICT
sudo&nbsp;python3 charge_ctl.py&nbsp;read&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 寄存器全量 dump
sudo&nbsp;python3 charge_ctl.py stop&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 充电电流 → 0
sudo&nbsp;python3 charge_ctl.py&nbsp;set&nbsp;1000&nbsp; &nbsp; &nbsp; &nbsp; # 设定 1000 mA
sudo&nbsp;python3 charge_ctl.py monitor 80&nbsp; &nbsp; &nbsp; # 守护:80% 停、75% 恢复

七、代价:一次”过度之格”的教训

格物也要有分寸。为了获得 I2C 设备的独占访问,早期方案采用了 unbind → 写寄存器 → rebind 的办法。这个动作看起来无伤大雅, 实则闯了祸:

5 × notifier_chain_register 崩溃 &nbsp; ← 同一个 notifier_block 被重复注册
4 × kernel Oops, 其中:
&nbsp; &nbsp; pc : regmap_field_read+0x28/0x88
&nbsp; &nbsp; 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/*&nbsp;* i2c_spy.c — 监控"充电配置路径"里的 I2C 传输并打印调用栈&nbsp;*&nbsp;* 背景 (本板实测):&nbsp;* &nbsp; 插拔 USB-C 时, 充电寄存器写入【不是】走的 bq25700_enable_charger(),&nbsp;* &nbsp; 而是走 power_supply notifier:&nbsp;* &nbsp; &nbsp; &nbsp; charger-detect → power_supply notifier → bq2570x_pd_notifier_call()&nbsp;* &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;→ bq25700_field_write()&nbsp;* &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;→ regmap → i2c_transfer&nbsp;* &nbsp; 所以必须 hook bq2570x_pd_notifier_call (以及其它相关函数)。&nbsp;*&nbsp;* 机制:&nbsp;* &nbsp; 对 watch_syms[] 里每个函数注册 kprobe(入口)+kretprobe(返回),&nbsp;* &nbsp; 维护 per-task 深度计数; i2c_transfer 的 pre_handler 只有在&nbsp;* &nbsp; "当前任务正处于被监控函数调用链内" 时才打印 msg + dump_stack()。&nbsp;*&nbsp;* 编译: make&nbsp;* 加载: sudo insmod i2c_spy.ko&nbsp;* &nbsp; &nbsp; &nbsp; sudo insmod i2c_spy.ko verbose=1 &nbsp; &nbsp; # 打印所有传输(诊断用)&nbsp;* &nbsp; &nbsp; &nbsp; sudo insmod i2c_spy.ko addr=-1 &nbsp; &nbsp; &nbsp; # 不过滤地址&nbsp;* &nbsp; &nbsp; &nbsp; sudo insmod i2c_spy.ko stack=0 &nbsp; &nbsp; &nbsp; # 不打印调用栈&nbsp;* 查看: sudo dmesg | grep -A 20 "i2c_spy: \[watch"&nbsp;* 卸载: sudo rmmod i2c_spy&nbsp;*/#include&nbsp;<linux/module.h>#include&nbsp;<linux/kernel.h>#include&nbsp;<linux/init.h>#include&nbsp;<linux/kprobes.h>#include&nbsp;<linux/i2c.h>#include&nbsp;<linux/version.h>#include&nbsp;<generated/utsrelease.h>
static&nbsp;int&nbsp;addr =&nbsp;0x6b;module_param(addr,&nbsp;int,&nbsp;0444);MODULE_PARM_DESC(addr,&nbsp;"filter by i2c slave addr (-1 = all)");
static&nbsp;bool&nbsp;stack =&nbsp;true;module_param(stack,&nbsp;bool,&nbsp;0444);MODULE_PARM_DESC(stack,&nbsp;"dump_stack() on watched paths");
static&nbsp;bool&nbsp;verbose;module_param(verbose,&nbsp;bool,&nbsp;0444);MODULE_PARM_DESC(verbose,&nbsp;"log ALL transfers, not just watched paths");
/* ---- 要监控的"调用者"函数 ----&nbsp;* 按本板实测的充电路径排列; 注册失败(符号不存在/不可 probe)的会被跳过。&nbsp;*/static&nbsp;const&nbsp;char&nbsp;*watch_syms[] = {"bq2570x_pd_notifier_call", &nbsp; &nbsp; &nbsp;&nbsp;/* ★ 实测插拔时走这里 */"bq25700_charger_evt_handel","bq25700_charger_usb_bc_handel","bq25700_enable_charger","bq25700_disable_charge",};
#define&nbsp;NWATCH ARRAY_SIZE(watch_syms)
static&nbsp;struct&nbsp;kprobe&nbsp;kps[NWATCH];static&nbsp;struct&nbsp;kretprobe&nbsp;krps[NWATCH];static&nbsp;int&nbsp;nregistered;
/* ---- per-task 跟踪 ---- */static&nbsp;struct&nbsp;task_struct&nbsp;*watch_task;static&nbsp;int&nbsp;watch_depth;static&nbsp;const&nbsp;char&nbsp;*watch_name; &nbsp; &nbsp; &nbsp;/* 当前命中的被监控函数名 */
static&nbsp;unsigned&nbsp;long&nbsp;total_seen;static&nbsp;unsigned&nbsp;long&nbsp;hit_count;
#define&nbsp;I2C_M_RD_LOCAL 0x0001
/* ============ 被监控函数: 入口 ============ */static&nbsp;int&nbsp;watch_entry(struct&nbsp;kprobe *p,&nbsp;struct&nbsp;pt_regs *regs){    watch_task = current;   watch_name = p->symbol_name;    watch_depth++;return&nbsp;0;}
/* ============ 被监控函数: 返回 ============ */static&nbsp;int&nbsp;watch_return(struct&nbsp;kretprobe_instance *ri,&nbsp;struct&nbsp;pt_regs *regs){if&nbsp;(current == watch_task) {        watch_depth--;if&nbsp;(watch_depth <=&nbsp;0) {         watch_depth =&nbsp;0;           watch_task =&nbsp;NULL;         watch_name =&nbsp;NULL;     }   }return&nbsp;0;}
/* ============ i2c_transfer ============ */static&nbsp;int&nbsp;i2c_xfer_pre(struct&nbsp;kprobe *p,&nbsp;struct&nbsp;pt_regs *regs){struct&nbsp;i2c_msg&nbsp;*msgs;int&nbsp;num, i;int&nbsp;from_watch;
    from_watch = (watch_task == current && watch_depth >&nbsp;0);
if&nbsp;(!verbose && !from_watch)return&nbsp;0;
    msgs = (struct&nbsp;i2c_msg *)regs->regs[1];    num &nbsp;= (int)regs->regs[2];
if&nbsp;(!msgs || num <=&nbsp;0&nbsp;|| num >&nbsp;64)return&nbsp;0;
for&nbsp;(i =&nbsp;0; i < num; i++) {struct&nbsp;i2c_msg&nbsp;*m = &msgs[i];        u16 maddr, mflags, mlen;        u8 buf[8];int&nbsp;j, n;
        maddr &nbsp;= m->addr;      mflags = m->flags;      mlen &nbsp; = m->len;
if&nbsp;(addr >=&nbsp;0&nbsp;&& maddr != (u16)addr)continue;
        n = mlen <&nbsp;sizeof(buf) ? mlen :&nbsp;sizeof(buf);if&nbsp;(m->buf)memcpy(buf, m->buf, n);
        total_seen++;if&nbsp;(from_watch)           hit_count++;
if&nbsp;(from_watch)pr_info("i2c_spy: [watch:%s] addr=0x%02x %s len=%u data=",                watch_name ? watch_name :&nbsp;"?",             maddr, (mflags & I2C_M_RD_LOCAL) ?&nbsp;"R"&nbsp;:&nbsp;"W", mlen);elsepr_info("i2c_spy: [other] addr=0x%02x %s len=%u data=",              maddr, (mflags & I2C_M_RD_LOCAL) ?&nbsp;"R"&nbsp;:&nbsp;"W", mlen);
for&nbsp;(j =&nbsp;0; j < n; j++)pr_cont("%02x ", buf[j]);if&nbsp;(mlen > n)pr_cont("...");pr_cont("\n");
if&nbsp;(stack && from_watch)dump_stack();    }return&nbsp;0;}
static&nbsp;struct&nbsp;kprobe&nbsp;kp_xfer = {    .symbol_name =&nbsp;"i2c_transfer", .pre_handler = i2c_xfer_pre,};
static&nbsp;int&nbsp;__init&nbsp;i2c_spy_init(void){int&nbsp;ret, i;
    ret =&nbsp;register_kprobe(&kp_xfer);if&nbsp;(ret <&nbsp;0) {pr_err("i2c_spy: register i2c_transfer failed: %d\n", ret);return&nbsp;ret;    }
for&nbsp;(i =&nbsp;0; i < NWATCH; i++) {struct&nbsp;kprobe&nbsp;*kp = &kps[nregistered];struct&nbsp;kretprobe&nbsp;*krp = &krps[nregistered];
memset(kp,&nbsp;0,&nbsp;sizeof(*kp));        kp->symbol_name = watch_syms[i];        kp->pre_handler = watch_entry;
        ret =&nbsp;register_kprobe(kp);if&nbsp;(ret <&nbsp;0) {pr_info("i2c_spy: skip '%s' (not probeable, ret=%d)\n",              watch_syms[i], ret);continue;       }
memset(krp,&nbsp;0,&nbsp;sizeof(*krp));        krp->kp.symbol_name = watch_syms[i];        krp->handler = watch_return;        krp->maxactive =&nbsp;16;
if&nbsp;(register_kretprobe(krp) <&nbsp;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&nbsp;(!nregistered) {pr_err("i2c_spy: no watch function registered\n");unregister_kprobe(&kp_xfer);return&nbsp;-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&nbsp;0;}
static&nbsp;void&nbsp;__exit&nbsp;i2c_spy_exit(void){int&nbsp;i;
for&nbsp;(i =&nbsp;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)&nbsp; 旧版本写的是"寄存器 0x01 = ChargeCurrent", 那是错的。&nbsp; 权威依据是驱动源码的字段表 bq25703_reg_fields[] (bq25700_charger.c):&nbsp; &nbsp; &nbsp; [CHARGE_CURRENT] &nbsp; &nbsp; = REG_FIELD(0x02, 6, 12); &nbsp; # reg 0x02, bit 6..12, 64mA/LSB&nbsp; &nbsp; &nbsp; [MAX_CHARGE_VOLTAGE] = REG_FIELD(0x04, 4, 14);&nbsp; &nbsp; &nbsp; [INPUT_VOLTAGE] &nbsp; &nbsp; &nbsp;= REG_FIELD(0x0a, 6, 13);&nbsp; &nbsp; &nbsp; [INPUT_CURRENT] &nbsp; &nbsp; &nbsp;= REG_FIELD(0x0e, 8, 14);&nbsp; 0x01 来自 TI bq25703A 数据手册, 与该厂商驱动映射不一致&nbsp; (SC8886 是行为兼容, 非寄存器完全一致的克隆)。&nbsp; 另外两点要点:&nbsp; &nbsp; 1) regmap 配置为 REGMAP_ENDIAN_LITTLE → 16 位值按 [reg, lo, hi] 字节序传输&nbsp; &nbsp; 2) CHARGE_CURRENT 是【位域】(bit 6..12), 必须【读-改-写】,&nbsp; &nbsp; &nbsp; &nbsp;不能整寄存器直接写 0 (会破坏 bit0..5 / bit13..15 的其它设置)&nbsp; 复位后的 DTS 默认: 2500mA → 字段值 39 (= 2500/64 ≈ 39)用法:&nbsp; sudo ./charge_ctl.py status &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 显示状态 + 当前电流字段值&nbsp; sudo ./charge_ctl.py read &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # dump 充电芯片所有寄存器&nbsp; sudo ./charge_ctl.py stop &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 停止充电 (电流字段置 0)&nbsp; sudo ./charge_ctl.py start &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 恢复充电 (回到 DTS 默认 2500mA)&nbsp; sudo ./charge_ctl.py set 1000 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 设为指定电流 (mA)&nbsp; sudo ./charge_ctl.py monitor 80 &nbsp; &nbsp; &nbsp; &nbsp; # 守护: 到 80% 停充, 掉到 75% 恢复注意:&nbsp; - 需要 root (访问 /dev/i2c-2)&nbsp; - ★ 不使用 unbind/bind: I2C_SLAVE_FORCE 可直接访问设备;&nbsp; &nbsp; 反复解绑/重绑会把驱动的内核状态搞坏 (notifier 重复注册 +&nbsp; &nbsp; 悬空 power_supply) → 读它的 sysfs 会 kernel Oops。&nbsp; &nbsp; 历史版本用过 unbind, 已废弃并改为禁用桩函数。"""import&nbsp;osimport&nbsp;sysimport&nbsp;timeimport&nbsp;fcntl# ---------------------------------------------------------------- 常量I2C_BUS =&nbsp;2I2C_ADDR =&nbsp;0x6BDEVICE_ID =&nbsp;"2-006b"I2C_DRIVER =&nbsp;"bq25700-charger"I2C_SLAVE_FORCE =&nbsp;0x0706# CHARGE_CURRENT 字段: 寄存器 0x02, bit 6..12 (7 bit 宽), 步进 64mAREG_CHARGE_CURRENT =&nbsp;0x02FIELD_LSB =&nbsp;6FIELD_MSB =&nbsp;12FIELD_MASK = ((1&nbsp;<< (FIELD_MSB - FIELD_LSB +&nbsp;1)) -&nbsp;1) &nbsp;&nbsp;# 0x7FFIELD_SHIFT = FIELD_LSBCURRENT_STEP_MA =&nbsp;64# DTS (yourland.dts): ti,charge-current = <0x2625a0> = 2500000 uA = 2500 mADEFAULT_CURRENT_MA =&nbsp;2500# 只读参考字段REG_MAX_CHARGE_VOLTAGE =&nbsp;0x04REG_INPUT_VOLTAGE =&nbsp;0x0AREG_INPUT_CURRENT =&nbsp;0x0EREG_DEVICE_ID =&nbsp;0x2FSYS_CHARGER =&nbsp;"/sys/class/power_supply/bq25700-charger"SYS_BATTERY =&nbsp;"/sys/class/power_supply/cw2017-battery"DRV_BIND =&nbsp;f"/sys/bus/i2c/drivers/{I2C_DRIVER}"# ---------------------------------------------------------------- 工具def&nbsp;read_file(path, default="N/A"):&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;open(path)&nbsp;as&nbsp;f:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;f.read().strip()&nbsp; &nbsp;&nbsp;except&nbsp;Exception:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;defaultdef&nbsp;ma_to_field(ma):&nbsp; &nbsp;&nbsp;"""mA → 7bit 字段值 (四舍五入), 并做范围钳制"""&nbsp; &nbsp; idx =&nbsp;int(round(ma / CURRENT_STEP_MA))&nbsp; &nbsp;&nbsp;return&nbsp;max(0,&nbsp;min(FIELD_MASK, idx))def&nbsp;field_to_ma(idx):&nbsp; &nbsp;&nbsp;return&nbsp;idx * CURRENT_STEP_MA# ---------------------------------------------------------------- I2C 底层def&nbsp;i2c_read16(fd, reg):&nbsp; &nbsp;&nbsp;"""读 16 位寄存器 (LE: 返回 b0 | b1<<8)"""&nbsp; &nbsp; os.write(fd,&nbsp;bytes([reg]))&nbsp; &nbsp; data = os.read(fd,&nbsp;2)&nbsp; &nbsp;&nbsp;if&nbsp;len(data) !=&nbsp;2:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;IOError(f"short read for reg 0x{reg:02x}")&nbsp; &nbsp;&nbsp;return&nbsp;data[0] | (data[1] <<&nbsp;8)def&nbsp;i2c_write16(fd, reg, val):&nbsp; &nbsp;&nbsp;"""写 16 位寄存器 (LE: 发送 [reg, lo, hi])"""&nbsp; &nbsp; lo = val &&nbsp;0xFF&nbsp; &nbsp; hi = (val >>&nbsp;8) &&nbsp;0xFF&nbsp; &nbsp; os.write(fd,&nbsp;bytes([reg, lo, hi]))def&nbsp;_open_i2c():&nbsp; &nbsp; fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR)&nbsp; &nbsp; fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR)&nbsp; &nbsp;&nbsp;return&nbsp;fd# ---------------------------------------------------------------- 驱动绑定## ★★★ 严重警告 (2026-09-24): 不要再使用 unbind/bind ★★★## 实测后果: 反复解绑/重绑 bq25700-charger 驱动会把内核状态搞坏——# &nbsp; 5 次 notifier_chain_register 崩溃 (同一 notifier_block 被重复注册)# &nbsp; 4 次 kernel Oops, 其中:# &nbsp; &nbsp; &nbsp; pc : regmap_field_read+0x28/0x88# &nbsp; &nbsp; &nbsp; lr : bq25700_power_supply_get_property+0x188/0x280# &nbsp; → 读取 /sys/class/power_supply/bq25700-charger/* 会让读取进程 SIGSEGV## 原因: 该驱动的 remove() 不做清理 (会打印 "duplicate filename" 告警),# &nbsp; &nbsp; &nbsp; 每次 bind 重新 probe() 都会重复注册 notifier、并留下悬空的# &nbsp; &nbsp; &nbsp; power_supply 对象 → 之后读它的属性就访问已释放内存 → Oops。## 结论: **完全不需要 unbind** —— I2C_SLAVE_FORCE 可以直接读写设备,# &nbsp; &nbsp; &nbsp; 实测读 reg 0x02 无需解绑即可得到 0x09c0 (字段 39 = 2496mA)。# &nbsp; &nbsp; &nbsp; 下面两个函数保留仅为留档, 请勿调用。def&nbsp;unbind():&nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError("DISABLED: unbind/bind corrupts driver state "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"(notifier double-register + dangling power_supply). "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"Not needed — I2C_SLAVE_FORCE works with the driver bound.")def&nbsp;rebind():&nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError("DISABLED: see unbind()")# ---------------------------------------------------------------- 高层操作def&nbsp;get_charge_current_ma():&nbsp; &nbsp;&nbsp;"""读回 CHARGE_CURRENT 字段 (自身设置值, 非测量值)&nbsp; &nbsp; 读操作无需解绑驱动 —— I2C_SLAVE_FORCE 可直接读。&nbsp; &nbsp; 实测 reg 0x02 读回 0x09c0 (字段 39 = 2496mA = DTS 2500mA)。&nbsp; &nbsp; """&nbsp; &nbsp; fd = _open_i2c()&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; raw = i2c_read16(fd, REG_CHARGE_CURRENT)&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; os.close(fd)&nbsp; &nbsp; idx = (raw >> FIELD_SHIFT) & FIELD_MASK&nbsp; &nbsp;&nbsp;return&nbsp;raw, idx, field_to_ma(idx)def&nbsp;set_charge_current_ma(ma):&nbsp; &nbsp;&nbsp;"""读-改-写 CHARGE_CURRENT 位域 (不解绑驱动!)&nbsp; &nbsp; 不再使用 unbind/bind —— I2C_SLAVE_FORCE 可直接访问设备,&nbsp; &nbsp; 而 unbind/bind 会破坏驱动内核状态 (见上方警告)。&nbsp; &nbsp; """&nbsp; &nbsp; idx = ma_to_field(ma)&nbsp; &nbsp; fd = _open_i2c()&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; old = i2c_read16(fd, REG_CHARGE_CURRENT)&nbsp; &nbsp; &nbsp; &nbsp; new = (old & ~(FIELD_MASK << FIELD_SHIFT)) | (idx << FIELD_SHIFT)&nbsp; &nbsp; &nbsp; &nbsp; i2c_write16(fd, REG_CHARGE_CURRENT, new)&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.05)&nbsp; &nbsp; &nbsp; &nbsp; chk = i2c_read16(fd, REG_CHARGE_CURRENT) &nbsp;&nbsp;# 读回校验&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; os.close(fd)&nbsp; &nbsp; chk_idx = (chk >> FIELD_SHIFT) & FIELD_MASK&nbsp; &nbsp; ok = (chk_idx == idx)&nbsp; &nbsp;&nbsp;print(f" &nbsp;reg 0x{REG_CHARGE_CURRENT:02x}: 0x{old:04x}&nbsp;-> 0x{new:04x}&nbsp;"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"(读回 0x{chk:04x})")&nbsp; &nbsp;&nbsp;print(f" &nbsp;CHARGE_CURRENT 字段 =&nbsp;{chk_idx}&nbsp;->&nbsp;{field_to_ma(chk_idx)}&nbsp;mA "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"{'✅ verified'&nbsp;if&nbsp;ok&nbsp;else&nbsp;'❌ MISMATCH'}")&nbsp; &nbsp;&nbsp;return&nbsp;okdef&nbsp;dump_regs():&nbsp; &nbsp;&nbsp;"""dump 充电芯片寄存器 (只读, 不解绑驱动)"""&nbsp; &nbsp; fd = _open_i2c()&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp;reg : value &nbsp; (little-endian 16-bit)")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp;----:-------")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;reg&nbsp;in&nbsp;list(range(0x00,&nbsp;0x20)) + [0x2F]:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v = i2c_read16(fd, reg)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v =&nbsp;None&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;""&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;reg == REG_CHARGE_CURRENT:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i = (v >> FIELD_SHIFT) & FIELD_MASK&nbsp;if&nbsp;v&nbsp;is&nbsp;not&nbsp;None&nbsp;else&nbsp;0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;f" &nbsp; <- CHARGE_CURRENT =&nbsp;{i}&nbsp;({field_to_ma(i)}&nbsp;mA)"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;reg == REG_MAX_CHARGE_VOLTAGE:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;" &nbsp; <- MAX_CHARGE_VOLTAGE"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;reg == REG_INPUT_VOLTAGE:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;" &nbsp; <- INPUT_VOLTAGE"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;reg == REG_INPUT_CURRENT:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;" &nbsp; <- INPUT_CURRENT"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;reg == REG_DEVICE_ID:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tag =&nbsp;" &nbsp; <- DEVICE_ID"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;0x{reg:02x}&nbsp;: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; + (f"0x{v:04x}"&nbsp;if&nbsp;v&nbsp;is&nbsp;not&nbsp;None&nbsp;else&nbsp;" &nbsp;n/a ") + tag)&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; os.close(fd)def&nbsp;status():&nbsp; &nbsp;&nbsp;print("=== charger (bq25700-charger) ===")&nbsp; &nbsp;&nbsp;for&nbsp;k&nbsp;in&nbsp;("status",&nbsp;"online",&nbsp;"health",&nbsp;"constant_charge_current",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"constant_charge_current_max",&nbsp;"input_current_limit"):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;{k:28s}&nbsp;=&nbsp;{read_file(f'{SYS_CHARGER}/{k}')}")&nbsp; &nbsp;&nbsp;print("=== battery (cw2017) ===")&nbsp; &nbsp;&nbsp;for&nbsp;k&nbsp;in&nbsp;("capacity",&nbsp;"status",&nbsp;"current_now",&nbsp;"voltage_now",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"temp",&nbsp;"charge_full",&nbsp;"charge_full_design"):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;{k:28s}&nbsp;=&nbsp;{read_file(f'{SYS_BATTERY}/{k}')}")&nbsp; &nbsp;&nbsp;print("=== CHARGE_CURRENT setpoint (reg 0x02 bit6..12) ===")&nbsp; &nbsp; raw = idx = ma =&nbsp;None&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; raw, idx, ma = get_charge_current_ma()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;reg 0x02 &nbsp; &nbsp; &nbsp;= 0x{raw:04x}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;field &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;{idx}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;setpoint &nbsp; &nbsp; &nbsp;=&nbsp;{ma}&nbsp;mA")&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;(read failed:&nbsp;{e})")&nbsp; &nbsp;&nbsp;# ★ status 字段不可信: 插着电源时芯片状态机恒报 "Charging",&nbsp; &nbsp;&nbsp;# &nbsp; 与我们把电流设成 0 无关。真正要看的是【实际电流】与【setpoint】。&nbsp; &nbsp; cur = read_file(f"{SYS_BATTERY}/current_now",&nbsp;"0")&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; cur_ua =&nbsp;int(cur)&nbsp; &nbsp;&nbsp;except&nbsp;ValueError:&nbsp; &nbsp; &nbsp; &nbsp; cur_ua =&nbsp;0&nbsp; &nbsp;&nbsp;print("=== VERDICT (authoritative) ===")&nbsp; &nbsp;&nbsp;if&nbsp;idx ==&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(" &nbsp;setpoint = 0 &nbsp;→ charging DISABLED by this tool ✅")&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;setpoint =&nbsp;{ma}&nbsp;mA → charging ENABLED")&nbsp; &nbsp;&nbsp;print(f" &nbsp;battery current_now =&nbsp;{cur_ua}&nbsp;uA "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"({'no current flowing'&nbsp;if&nbsp;cur_ua ==&nbsp;0&nbsp;else&nbsp;'current flowing'})")&nbsp; &nbsp;&nbsp;print(" &nbsp;note: charger/status == 'Charging' is NOT a故障 —— 它来自芯片")&nbsp; &nbsp;&nbsp;print(" &nbsp; &nbsp; &nbsp; &nbsp;reg0x20 的 IN_FCHRG 位, 与电流设置无关。")def&nbsp;monitor(limit=80, hyst=5, interval=30):&nbsp; &nbsp;&nbsp;"""到 limit% 停充; 掉到 (limit-hyst)% 恢复&nbsp; &nbsp; ★ 不能用 charger/status 判断! 插着电源时 status 恒为 "Charging"&nbsp; &nbsp; &nbsp; (它来自芯片 reg0x20 的 IN_FCHRG/IN_PCHRG 位, 与我们设置的电流无关)。&nbsp; &nbsp; &nbsp; 改为以【实际电流字段值】为准:&nbsp; &nbsp; &nbsp; &nbsp; setpoint > 0 表示允许充电; setpoint == 0 表示已停充。&nbsp; &nbsp; """&nbsp; &nbsp;&nbsp;print(f"monitor: stop at&nbsp;{limit}%, resume at&nbsp;{limit - hyst}%, "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"poll&nbsp;{interval}s &nbsp;(Ctrl-C to stop)")&nbsp; &nbsp;&nbsp;while&nbsp;True:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cap =&nbsp;int(read_file(f"{SYS_BATTERY}/capacity",&nbsp;"-1"))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cap <&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; time.sleep(interval)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _, idx, setpoint_ma = get_charge_current_ma()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stopped = (idx ==&nbsp;0)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cap >= limit&nbsp;and&nbsp;not&nbsp;stopped:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[{time.strftime('%H:%M:%S')}]&nbsp;{cap}% >=&nbsp;{limit}% "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"(setpoint&nbsp;{setpoint_ma}mA) → stop charging")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_charge_current_ma(0)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;cap <= (limit - hyst)&nbsp;and&nbsp;stopped:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[{time.strftime('%H:%M:%S')}]&nbsp;{cap}% <= "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"{limit - hyst}% → resume&nbsp;{DEFAULT_CURRENT_MA}mA")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_charge_current_ma(DEFAULT_CURRENT_MA)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state =&nbsp;"stopped"&nbsp;if&nbsp;stopped&nbsp;else&nbsp;f"charging&nbsp;{setpoint_ma}mA"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[{time.strftime('%H:%M:%S')}]&nbsp;{cap}% ({state}) — no action")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; time.sleep(interval)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;KeyboardInterrupt:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("\nmonitor: stopped by user")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"monitor: error&nbsp;{e}", file=sys.stderr)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; time.sleep(interval)# ---------------------------------------------------------------- maindef&nbsp;main():&nbsp; &nbsp;&nbsp;if&nbsp;os.geteuid() !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("error: must run as root (needs /dev/i2c-2 access)",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; file=sys.stderr)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1&nbsp; &nbsp; cmd = sys.argv[1]&nbsp;if&nbsp;len(sys.argv) >&nbsp;1&nbsp;else&nbsp;"status"&nbsp; &nbsp;&nbsp;if&nbsp;cmd ==&nbsp;"status":&nbsp; &nbsp; &nbsp; &nbsp; status()&nbsp; &nbsp;&nbsp;elif&nbsp;cmd ==&nbsp;"read":&nbsp; &nbsp; &nbsp; &nbsp; dump_regs()&nbsp; &nbsp;&nbsp;elif&nbsp;cmd ==&nbsp;"stop":&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"stopping charge (CHARGE_CURRENT → 0 mA):")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0&nbsp;if&nbsp;set_charge_current_ma(0)&nbsp;else&nbsp;1&nbsp; &nbsp;&nbsp;elif&nbsp;cmd ==&nbsp;"start":&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"resuming charge (CHARGE_CURRENT →&nbsp;{DEFAULT_CURRENT_MA}&nbsp;mA):")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0&nbsp;if&nbsp;set_charge_current_ma(DEFAULT_CURRENT_MA)&nbsp;else&nbsp;1&nbsp; &nbsp;&nbsp;elif&nbsp;cmd ==&nbsp;"set":&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;len(sys.argv) <&nbsp;3:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("usage: charge_ctl.py set <mA>", file=sys.stderr)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1&nbsp; &nbsp; &nbsp; &nbsp; ma =&nbsp;int(sys.argv[2])&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"setting CHARGE_CURRENT →&nbsp;{ma}&nbsp;mA:")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0&nbsp;if&nbsp;set_charge_current_ma(ma)&nbsp;else&nbsp;1&nbsp; &nbsp;&nbsp;elif&nbsp;cmd ==&nbsp;"monitor":&nbsp; &nbsp; &nbsp; &nbsp; limit =&nbsp;int(sys.argv[2])&nbsp;if&nbsp;len(sys.argv) >&nbsp;2&nbsp;else&nbsp;80&nbsp; &nbsp; &nbsp; &nbsp; monitor(limit)&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(__doc__)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1&nbsp; &nbsp;&nbsp;return&nbsp;0if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp; 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:&nbsp; sudo ./charge_ctl.py status &nbsp; &nbsp; &nbsp; - Show charging status&nbsp; sudo ./charge_ctl.py stop &nbsp; &nbsp; &nbsp; &nbsp; - Stop charging immediately&nbsp; sudo ./charge_ctl.py start &nbsp; &nbsp; &nbsp; &nbsp;- Resume charging&nbsp; sudo ./charge_ctl.py monitor &nbsp; &nbsp; &nbsp;- Daemon: limit at 80% (or custom %)"""import&nbsp;os, sys, time, fcntl
# ConfigurationI2C_BUS =&nbsp;2I2C_ADDR =&nbsp;0x6bDEVICE_ID =&nbsp;"2-006b"I2C_DRIVER =&nbsp;"bq25700-charger"REG_CHARGE_CURRENT =&nbsp;0x01I2C_SLAVE_FORCE =&nbsp;0x0706DEFAULT_CHARGE_CURRENT =&nbsp;0x1E00&nbsp;&nbsp;# ~3A
def&nbsp;read_sysfs(path):&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;open(path)&nbsp;as&nbsp;f:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;f.read().strip()&nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;"N/A"
def&nbsp;unbind():&nbsp; &nbsp;&nbsp;"""Unbind the kernel I2C driver. Returns True if successful."""&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;open(f"/sys/bus/i2c/drivers/{I2C_DRIVER}/unbind",&nbsp;"w")&nbsp;as&nbsp;f:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; f.write(DEVICE_ID)&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.1)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
def&nbsp;rebind():&nbsp; &nbsp;&nbsp;"""Rebind the kernel I2C driver."""&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;open(f"/sys/bus/i2c/drivers/{I2C_DRIVER}/bind",&nbsp;"w")&nbsp;as&nbsp;f:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; f.write(DEVICE_ID)&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.1)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
def&nbsp;i2c_write(reg, value):&nbsp; &nbsp;&nbsp;"""Write 16-bit big-endian value to I2C register."""&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR)&nbsp; &nbsp; &nbsp; &nbsp; fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR)&nbsp; &nbsp; &nbsp; &nbsp; data =&nbsp;bytes([reg, (value >>&nbsp;8) &&nbsp;0xff, value &&nbsp;0xff])&nbsp; &nbsp; &nbsp; &nbsp; os.write(fd, data)&nbsp; &nbsp; &nbsp; &nbsp; os.close(fd)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
def&nbsp;i2c_read(reg):&nbsp; &nbsp;&nbsp;"""Read 16-bit value from I2C register."""&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; fd = os.open(f"/dev/i2c-{I2C_BUS}", os.O_RDWR)&nbsp; &nbsp; &nbsp; &nbsp; fcntl.ioctl(fd, I2C_SLAVE_FORCE, I2C_ADDR)&nbsp; &nbsp; &nbsp; &nbsp; os.write(fd,&nbsp;bytes([reg]))&nbsp; &nbsp; &nbsp; &nbsp; data = os.read(fd,&nbsp;2)&nbsp; &nbsp; &nbsp; &nbsp; os.close(fd)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;len(data) ==&nbsp;2:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(data[0] <<&nbsp;8) | data[1]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None&nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None
def&nbsp;set_charge_current(value):&nbsp; &nbsp;&nbsp;"""Unbind → write register → rebind. Returns True on success."""&nbsp; &nbsp; unbound = unbind()&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;unbound:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False&nbsp; &nbsp; ok = i2c_write(REG_CHARGE_CURRENT, value)&nbsp; &nbsp; rebind()&nbsp; &nbsp;&nbsp;return&nbsp;ok
def&nbsp;status():&nbsp; &nbsp;&nbsp;print(f"Charger: &nbsp; &nbsp; &nbsp;&nbsp;{read_sysfs('/sys/class/power_supply/bq25700-charger/status')}")&nbsp; &nbsp;&nbsp;print(f"Battery: &nbsp; &nbsp; &nbsp;&nbsp;{read_sysfs('/sys/class/power_supply/cw2017-battery/capacity')}%")&nbsp; &nbsp;&nbsp;print(f"Bat status: &nbsp; &nbsp;{read_sysfs('/sys/class/power_supply/cw2017-battery/status')}")&nbsp; &nbsp;&nbsp;# Read I2C register&nbsp; &nbsp; unbound = unbind()&nbsp; &nbsp;&nbsp;if&nbsp;unbound:&nbsp; &nbsp; &nbsp; &nbsp; val = i2c_read(REG_CHARGE_CURRENT)&nbsp; &nbsp; &nbsp; &nbsp; rebind()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;val&nbsp;is&nbsp;not&nbsp;None:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ma = ((val >>&nbsp;6) &&nbsp;0x3ff) *&nbsp;64&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"ChargeCurrent: 0x{val:04x}&nbsp;({ma}&nbsp;mA)")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("ChargeCurrent: N/A")&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("ChargeCurrent: driver busy")
def&nbsp;stop():&nbsp; &nbsp;&nbsp;if&nbsp;set_charge_current(0x0000):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("Charging stopped ✅")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;print("Failed ❌")&nbsp; &nbsp;&nbsp;return&nbsp;False
def&nbsp;start():&nbsp; &nbsp;&nbsp;if&nbsp;set_charge_current(DEFAULT_CHARGE_CURRENT):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("Charging resumed ✅")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp;&nbsp;print("Failed ❌")&nbsp; &nbsp;&nbsp;return&nbsp;False
def&nbsp;monitor(limit=80, hyst=5):&nbsp; &nbsp;&nbsp;"""Monitor battery, stop charging at limit%, resume at (limit-hyst)%"""&nbsp; &nbsp; stopped =&nbsp;False&nbsp; &nbsp;&nbsp;while&nbsp;True:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cap =&nbsp;int(read_sysfs("/sys/class/power_supply/cw2017-battery/capacity"))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; st &nbsp;= read_sysfs("/sys/class/power_supply/bq25700-charger/status")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;st ==&nbsp;"Charging"&nbsp;and&nbsp;cap >= limit&nbsp;and&nbsp;not&nbsp;stopped:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;stop():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stopped =&nbsp;True&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;st&nbsp;in&nbsp;("Discharging",&nbsp;"Not charging")&nbsp;and&nbsp;cap <= (limit - hyst)&nbsp;and&nbsp;stopped:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;start():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stopped =&nbsp;False
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; time.sleep(30)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;KeyboardInterrupt:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; time.sleep(30)
if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp;&nbsp;if&nbsp;len(sys.argv) <&nbsp;2:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("Usage: sudo charge_ctl.py [status|stop|start|monitor]")&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)&nbsp; &nbsp; a = sys.argv[1]&nbsp; &nbsp;&nbsp;if&nbsp;a ==&nbsp;"status": status()&nbsp; &nbsp;&nbsp;elif&nbsp;a ==&nbsp;"stop": stop()&nbsp; &nbsp;&nbsp;elif&nbsp;a ==&nbsp;"start": start()&nbsp; &nbsp;&nbsp;elif&nbsp;a ==&nbsp;"monitor": monitor(int(sys.argv[2])&nbsp;if&nbsp;len(sys.argv) >&nbsp;2&nbsp;else&nbsp;80)

***

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

也欢迎关注格友公众号


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:格友 SteveLsh SteveLsh《格物致知:在RK3588 Linux平台上增加锂电池充电限制的实践之旅》

评论:0   参与:  0