ET6238充电管理芯片C驱动源码拆解与工程调试避坑

发布时间:2026/10/26 10:52:11
ET6238充电管理芯片C驱动源码拆解与工程调试避坑

简介:面向LED显示驱动开发的ET6238 I2C驱动源码,适合嵌入式软件工程师、单片机学习者以及需要驱动16行8列LED矩阵的开发者参考。压缩包共2个文件,包含1个C源文件和1个H头文件,体积约2KB,头文件负责接口声明,源文件承担I2C底层通信与功能实现,代码精简,便于移植到Linux i2c-dev或Arduino Wire等平台。目前已有407人学习,说明其在小型LED段位显示场景中具备一定参考价值。源码围绕I2C从设备寻址、命令与数据写入、段位亮灭控制展开,并涉及PWM亮度调节、超时重试、错误恢复以及多设备总线地址配置等可靠性处理。读者可据此理解ET6238的初始化流程、通信时序和16×8矩阵刷新思路,也可将初始化、写命令、写数据及亮度设置等接口抽取到自有项目中,快速搭建LED显示控制原型,并在总线通信异常时对照源码排查问题、减少重复调试时间。

1. 一颗充电管理芯片的 C 源码,能省掉你反复读手册的时间

板子上电池充不满,充电电流卡在 200mA 上不去,示波器看 VBUS 纹波干干净净,硬件同事拍胸脯说电路没问题,最后翻出来是驱动里配置寄存器写反了一位。这种玄学问题,查三天都不一定有结论。ET6238 这颗电源/充电管理芯片的 C 源码驱动,干的就是把这类寄存器时序、充电状态机、上报接口固化成代码的活。它面向拿这颗片子做产品的嵌入式开发者:在 Linux 下要把充电、电量、故障接进 power_supply 子系统的,可以直接拿内核模块那套改;做裸机工程的,也能从里面挑出寄存器定义和初始化序列塞进自己的工程。下面按源码骨架、核心链路、跑通、避坑、改造这个顺序拆,能看到哪一层就动手改哪一层。

2. 源码骨架与技术栈:一份 C 驱动包拆开看什么

这套代码的技术栈其实不复杂,难的是它把一颗模拟芯片的时序约束翻译成了软件结构。整体是 C 语言写的内核态驱动,跑在 Linux 的 I2C 设备模型上,寄存器访问走 regmap,对外暴露走 power_supply 子系统,中断用 request_threaded_irq 挂线程化处理函数,周期性状态采样用 delayed_work。这套组合是当前内核里充电类驱动的主流写法,好处是用户态不用装任何私有库,/sys/class/power_supply/下面就能读到状态,系统起来以后由上层电源管理策略直接消费。

如果下载包里同时带裸机版本,常见做法是单独抽一个 port 层文件,把 I2C 收发、延时、中断注册做成三个函数指针,核心逻辑再用同一份状态机代码去调。这样移植到 RTOS 或者无系统的 MCU 上,改动量集中在 port 层,不用碰充电参数计算。判断一份驱动源码是不是工程可用的,基本就看它有没有做这层隔离。

2.1 目录骨架:七类文件各管一段

解压以后先别看代码,先把目录树列出来,判断哪部分是你的战场。

et6238_driver/ ├── Makefile # 顶层编译入口,函数一个:把内核构建系统拉起来 ├── Kconfig # 内核配置菜单项,决定编不编进内核树 ├── et6238_reg.h # 寄存器地址与位域,纯宏,无逻辑 ├── et6238_core.c # 主体:probe、参数下发、状态机、电源属性回调 ├── et6238_i2c.c # 总线适配层,regmap 配置与读写封装 ├── et6238_sysfs.c # 自定义 sysfs 属性,调试和产测用的口子 ├── include/et6238.h # 驱动内部共享的结构体与常量 ├── dts/et6238-example.dtsi # 设备树参考节点,板级接入的抄作业模板 ├── tools/et6238_test.sh # 板端自检脚本,读属性、扫总线、灌参数 └── README.md # 编译方式与属性说明

这几类文件里,et6238_reg.h和et6238_core.c是必须逐行读的,其余按需。寄存器头文件决定了你能不能看懂后面所有代码在干什么,因为驱动里几乎所有魔法数字最终都落在这张表上;核心文件决定了充电行为,改板子改参数基本都在这里动。

文件类型改动频率什么时候需要动它
et6238_reg.h宏定义低手册寄存器表更新、换封装型号
et6238_i2c.c总线层低换 I2C 控制器、加 SPI 变体
et6238_core.c逻辑主体高调充电参数、改状态机、加属性
et6238_sysfs.c调试接口中产测需要读 ADC、需要强制复位
dts/*.dtsi配置每块板都改换地址、换中断脚、换默认电流
tools/*.sh脚本中加自动化测试用例

先说清楚一点:别看et6238_i2c.c只有一两百行,它承担的是把芯片手册里那个「写地址—写寄存器号—写数据—等 ACK」的时序抽象成一次函数调用的职责。封装做得好的代码,会在这里统一处理重试次数、总线错误码转换和地址位宽,上层就不用到处判断-EREMOTEIO。

2.2 编译体系与内核依赖

内核模块的编译入口有两套写法:塞进内核树里编,或者独立 out-of-tree 编。调试阶段强烈建议用后者,改一行编一次,不用每次全量构建内核。

# 源码树内的 Kconfig 片段:加一个可见的配置项,默认不编 config CHARGER_ET6238 tristate "ET6238 power/charger driver" depends on I2C select REGMAP_I2C select POWER_SUPPLY help Say M here to build the ET6238 charger driver as a module.

select REGMAP_I2C和select POWER_SUPPLY这两行不能省,少了就是链接期一堆未定义符号,报错信息还特别绕。我在别的驱动上踩过这个坑,编译器只会告诉你regmap_read找不到定义,不会告诉你是因为 config 没选中。

# 独立编译 Makefile,KDIR 指向你板子对应的内核源码或头文件目录 obj-m += et6238_charger.o et6238_charger-objs := et6238_core.o et6238_i2c.o et6238_sysfs.o KDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

obj-m后面跟的是最终模块名,et6238_charger-objs是参与链接的中间目标文件,两者不能写成同名,否则 make 会抱怨循环依赖。KDIR用?=赋值是为了让命令行可以覆盖,交叉编译时几乎一定要覆盖。

内核版本要求上,带power_supply_get_property这套新回调签名的至少是 4.14 以后,5.x 系列基本都能编过。老内核里power_supply注册用的是power_supply_register加power_supply_desc,如果代码里还在用power_supply_config.of_node的旧写法,移植时得对着改一遍。i2c_driver.probe的签名同样是分水岭,新内核是单参数struct i2c_client *client,老内核多一个const struct i2c_device_id *id,这个差异编译时第一眼就能看到,改起来也就一行。

3. 核心模块拆解:从 I2C 寄存器到 power_supply 上报的完整链路

这一章按数据流走一遍:应用层读一个电量,信号怎么从总线上的 8 位寄存器一路变成 sysfs 里的字符串。看懂这条链路,后面改参数、加属性就有位置感了。

3.1 寄存器定义与 regmap 读写封装

先把手册上的寄存器表翻译成宏,这一步看着枯燥,但它是后面所有代码的地基。

/* et6238_reg.h —— 寄存器地址与位域,严格按手册寄存器表逐条对齐 */ #ifndef __ET6238_REG_H__ #define __ET6238_REG_H__ #define ET6238_REG_CHIP_ID 0x00 /* 只读,用于 probe 阶段确认芯片在位 */ #define ET6238_REG_STATUS 0x01 /* 只读,充电状态 + 故障标志 */ #define ET6238_REG_CTRL0 0x02 /* 全局使能、看门狗喂狗位 */ #define ET6238_REG_CTRL1 0x03 /* 充电使能、终止使能、OTG 使能 */ #define ET6238_REG_CHG_CUR 0x04 /* [5:0] 充电电流档位,步进 100mA */ #define ET6238_REG_CHG_VOL 0x05 /* [5:0] 浮充电压档位,步进 20mV */ #define ET6238_REG_TERM_CUR 0x06 /* [3:0] 终止电流,按满充电流百分比 */ #define ET6238_REG_ADC_VBAT 0x08 /* 电池电压 ADC 高 8 位 */ #define ET6238_REG_ADC_VBUS 0x09 /* 输入电压 ADC 高 8 位 */ #define ET6238_REG_FAULT 0x0A /* 故障锁存,读清 */ /* 芯片 ID 固定值,probe 时用来判断总线上挂的是不是同一颗 */ #define ET6238_CHIP_ID_VAL 0x38 #define ET6238_CHIP_ID_MASK 0xFF /* STATUS 位域 */ #define ET6238_ST_MASK GENMASK(2, 0) #define ET6238_ST_NOT_CHARGING 0x0 #define ET6238_ST_CHARGING 0x1 #define ET6238_ST_FULL 0x2 #define ET6238_ST_FAULT 0x3 #define ET6238_ST_VBUS_OVP BIT(4) /* 输入过压 */ #define ET6238_ST_TSD BIT(5) /* 过温保护触发 */ /* 关键限值,超出范围要在驱动里夹紧,别直接把用户传的值写进寄存器 */ #define ET6238_ICHG_STEP_UA 100000 #define ET6238_ICHG_MAX_STEP 32 #define ET6238_VCHG_STEP_UV 20000 #define ET6238_VCHG_MAX_STEP 48 #endif /* __ET6238_REG_H__ */

宏里的档位步进是从手册的编码表折算出来的,这段最忌讳想当然。ET6238_ICHG_STEP_UA如果是 100mA,那写到寄存器里的值 32 就代表 3.2A,一旦你按 50mA 去算,实际电流就只有设定值的一半,表现出来就是「电流上不去」,而软件层面一切正常。我一般会在头文件旁边留一行注释写明这份表对应的手册版本,换料号的时候能立刻对上。

寄存器访问统一交给 regmap,不要自己写i2c_transfer。

/* et6238_i2c.c —— regmap 配置与带重试的读改写封装 */ static const struct regmap_config et6238_regmap_config = { .reg_bits = 8, /* 寄存器地址 8 位 */ .val_bits = 8, /* 寄存器数据 8 位 */ .max_register = ET6238_REG_FAULT, .cache_type = REGCACHE_NONE, /* 状态类寄存器必须直读,不能吃缓存 */ }; int et6238_update_bits(struct et6238_chip *chip, u8 reg, u8 mask, u8 val) { int ret, i; /* 充电配置寄存器偶发 NACK,重试三次再判定总线故障 */ for (i = 0; i < 3; i++) { ret = regmap_update_bits(chip->regmap, reg, mask, val); if (!ret) return 0; usleep_range(1000, 2000); } dev_err(chip->dev, "update reg 0x%02x failed: %d\n", reg, ret); return ret; }

cache_type = REGCACHE_NONE是这里最容易被忽略的一行。如果开了REGCACHE_RBTREE,读状态寄存器时 regmap 可能直接返回缓存值,你会看到电压、电流、充电状态全都不刷新,然后开始怀疑硬件坏。状态类寄存器一律关缓存,只给纯配置寄存器开缓存才有意义。

重试逻辑放在封装层而不是散落在各调用点,是为了让上层代码干净。三次重试、每次间隔 1 到 2 毫秒,这个经验值来自 I2C 从设备在忙时拉低 SCL 的典型时间,间隔太短重试等于没重试。

3.2 上电探测:芯片 ID 校验与初始化序列

probe 是整份代码的第一个坎,也是「能不能跑起来」的分水岭。

/* et6238_core.c —— probe 主体,先把 ID 读对,再谈别的 */ static int et6238_probe(struct i2c_client *client) { struct et6238_chip *chip; unsigned int id; int ret; chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); if (!chip) return -ENOMEM; chip->dev = &client->dev; i2c_set_clientdata(client, chip); mutex_init(&chip->lock); chip->regmap = devm_regmap_init_i2c(client, &et6238_regmap_config); if (IS_ERR(chip->regmap)) return dev_err_probe(chip->dev, PTR_ERR(chip->regmap), "regmap init failed\n"); /* 第一件事:确认总线上这地址挂的真是这颗芯片 */ ret = regmap_read(chip->regmap, ET6238_REG_CHIP_ID, &id); if (ret) return dev_err_probe(chip->dev, ret, "read chip id failed\n"); if ((id & ET6238_CHIP_ID_MASK) != ET6238_CHIP_ID_VAL) { dev_err(chip->dev, "unexpected chip id 0x%02x\n", id); return -ENODEV; } /* 清看门狗,避免芯片在配置过程中自己复位 */ ret = et6238_update_bits(chip, ET6238_REG_CTRL0, BIT(7), BIT(7)); if (ret) return ret; ret = et6238_parse_dt(chip); /* 从设备树取板级参数 */ if (ret) return ret; ret = et6238_hw_init(chip); /* 下发默认充电参数、清故障 */ if (ret) return ret; ret = et6238_register_power_supply(chip); if (ret) return ret; /* 状态轮询用 delayed_work,1 秒一次,够用又不至于占 CPU */ INIT_DELAYED_WORK(&chip->monitor, et6238_monitor_work); schedule_delayed_work(&chip->monitor, msecs_to_jiffies(1000)); return 0; }

这段的执行顺序有讲究。ID 校验必须排在任何写操作之前,因为如果总线地址配错,你写下去的配置可能落在总线上的另一颗器件里,那才是真的灾难现场。dev_err_probe是 5.9 之后才有的,老内核换成dev_err加return ret即可。

i2c_set_clientdata这行的作用是给中断处理函数和电源属性回调留一个反查入口,没有它,回调里拿不到 chip 指针,只能满世界传全局变量。mutex_init保护的是配置下发和状态读取之间的并发,多个进程同时往同一个属性写参数时,没有这把锁就会出现读改写撕裂,最终写进寄存器的值是两拨参数混合的结果。

et6238_hw_init里做的是读回所有可写寄存器、按设备树参数重算档位、写回去、再读一遍做校验。校验这一步很多人省掉,但芯片的 I2C 写操作失败不会自动重试,读回校验是唯一能在 probe 阶段发现问题的机会。写进去的是 0x12、读回来的是 0x10,说明某一位根本没写成功,这时候不报错,等到用户抱怨充不满电就晚了。

3.3 充电参数下发与状态机上报

参数下发是把用户态或者设备树给的物理量(微安、微伏)折算成寄存器档位。

/* 电流折算:向上取整到最近的档位,避免设定 1.05A 实际只充 1.0A */ static int et6238_set_chg_current(struct et6238_chip *chip, int ua) { int step; if (ua <= 0) { /* 0 表示关闭充电,写 0 档 */ return et6238_update_bits(chip, ET6238_REG_CHG_CUR, ET6238_ICHG_MASK, 0); } step = DIV_ROUND_UP(ua, ET6238_ICHG_STEP_UA); if (step > ET6238_ICHG_MAX_STEP) { dev_warn(chip->dev, "chg current %duA clipped to max\n", ua); step = ET6238_ICHG_MAX_STEP; } return et6238_update_bits(chip, ET6238_REG_CHG_CUR, ET6238_ICHG_MASK, step); } /* 状态上报:把寄存器位域翻译成 power_supply 的状态枚举 */ static int et6238_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct et6238_chip *chip = power_supply_get_drvdata(psy); unsigned int st; int ret; mutex_lock(&chip->lock); ret = regmap_read(chip->regmap, ET6238_REG_STATUS, &st); mutex_unlock(&chip->lock); if (ret) return ret; switch (psp) { case POWER_SUPPLY_PROP_STATUS: switch (st & ET6238_ST_MASK) { case ET6238_ST_CHARGING: val->intval = POWER_SUPPLY_STATUS_CHARGING; break; case ET6238_ST_FULL: val->intval = POWER_SUPPLY_STATUS_FULL; break; case ET6238_ST_FAULT: val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; break; default: val->intval = POWER_SUPPLY_STATUS_DISCHARGING; break; } break; case POWER_SUPPLY_PROP_CHARGE_TYPE: val->intval = (st & ET6238_ST_MASK) == ET6238_ST_CHARGING ? POWER_SUPPLY_CHARGE_TYPE_FAST : POWER_SUPPLY_CHARGE_TYPE_NONE; break; case POWER_SUPPLY_PROP_HEALTH: if (st & ET6238_ST_TSD) val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; else if (st & ET6238_ST_VBUS_OVP) val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; else val->intval = POWER_SUPPLY_HEALTH_GOOD; break; default: return -EINVAL; } return 0; }

DIV_ROUND_UP和DIV_ROUND_CLOSEST的选择不是随手挑的。充电电流宁可略高于设定值也不能低于,因为低于设定值意味着充电时间变长,用户感知明显;而电压必须用DIV_ROUND_CLOSEST,浮充电压高了哪怕 20mV,长期下来对电池循环寿命的影响也是实打实的。这两个宏用反了,表现就是「参数看着对,实际行为不对」。

HEALTH属性把芯片的故障位映射成了标准枚举,这层的价值在于上层不用认识 ET6238 是什么。系统里的热管理策略、充电策略组件读的都是标准属性,你换一颗充电 IC,上层代码一行不用改。电源属性回调里加锁要注意粒度,mutex_lock只包住 regmap 那一次读,不要把整个 switch 包进去,否则开关中断上下文里取属性容易和轮询 work 互相卡住。

4. 跑起来:交叉编译、设备树绑定与板端验证

前面看的都是静态代码,这一章全是能直接敲的命令。我的习惯是先把模块编出来、再挂上设备树、最后用脚本跑一遍自检,三步每一步都有明确的通过标准。

4.1 交叉编译与内核树准备

先确认内核头文件齐不齐,缺Module.symvers是最常见的拦路虎。

# 交叉编译环境,三个变量缺一不可 export ARCH=arm64 export CROSS_COMPILE=aarch64-none-linux-gnu- export KDIR=/path/to/linux-5.10 # 必须是板子实际运行的那份内核源码 # 先编一遍内核模块符号表,没有这一步会报 "no symbol version for module_layout" make -C $KDIR modules_prepare # 进入驱动目录编译 make -C $KDIR M=$(pwd) modules

modules_prepare会生成Module.symvers和必要的头文件链接,不做这一步,模块能编出来但 insmod 时会因为符号版本不匹配被内核拒绝。KDIR一定要和板子上跑的内核完全一致,包括配置,版本号差一个后缀都可能加载失败。

编出来的是一个.ko,用modinfo看一眼就知道对不对。

modinfo et6238_charger.ko # 关注 vermagic 那一行,必须和板端 uname -r 一致

vermagic里带着内核版本、SMP 标志、抢占模型,任何一项不匹配都会加载失败。如果板子上跑的是裁剪过的内核,而你的KDIR是完整的发行版内核,那基本一定会踩这个坑。判断依据很简单,uname -r的输出和modinfo里的 vermagic 逐字符比一遍。

4.2 设备树节点怎么写

设备树节点是把板级差异从代码里赶出去的通道,参数写在这里,同一份驱动能喂给不同板子。

/* dts/et6238-example.dtsi —— 挂在第 2 路 I2C 上的参考节点 */ &i2c2 { status = "okay"; clock-frequency = <400000>; /* 400kHz 快速模式,芯片手册支持 */ et6238: charger@6b { compatible = "vendor,et6238"; reg = <0x6b>; /* 7 位地址,别写成 8 位的 D6 */ interrupt-parent = <&gpio1>; interrupts = <12 IRQ_TYPE_EDGE_FALLING>; /* 板级参数,驱动里用 of_property_read_u32 取 */ input-current-limit-microamp = <2000000>; constant-charge-current-microamp = <1500000>; charge-full-microvolt = <4200000>; termination-current-microamp = <150000>; recharge-threshold-microvolt = <4050000>; status = "okay"; }; };

reg写的是 7 位地址,这是新手最容易翻车的地方之一。逻辑分析仪抓到的地址帧是 0xD6,那是把 7 位地址左移一位再补上读写位得到的结果,设备树里必须写 0x6B。写成 0xD6 的话,probe 阶段读 ID 会直接超时。

中断配置成IRQ_TYPE_EDGE_FALLING的前提是芯片中断脚是开漏输出、默认上拉,低有效。如果你的板子加了反相器或者用了推挽输出,这里要改成IRQ_TYPE_LEVEL_LOW或者高有效,配错了表现就是中断永远不触发,或者触发一次之后再也不来。

input-current-limit-microamp这个参数决定了适配器能力的上限,和充电电流是两码事。很多人只调充电电流不调输入限流,结果充电电流设到 2A,输入还在 500mA,芯片会自动降流,你看到的现象就是「设了但没生效」。

4.3 板端验证三步走

模块挂上去之后,按顺序做三件事,每件事都有一个明确的通过标准。

# 第一步:确认总线通信正常,地址能看到 i2cdetect -y 2 # 期望输出里 0x6b 位置显示 6b,显示 UU 说明已被驱动占用,也正常 # 第二步:手动读一次芯片 ID,绕过驱动确认硬件通路 i2cget -y 2 0x6b 0x00 # 期望输出 0x38,读出来 0xff 或报错就是通信有问题 # 第三步:插入模块,看 probe 是否成功、属性是否出现 insmod et6238_charger.ko dmesg | tail -20 cat /sys/class/power_supply/et6238/uevent

uevent那一步是最终的通过标准,正常输出里应该能看到POWER_SUPPLY_NAME=et6238、POWER_SUPPLY_STATUS=Charging、POWER_SUPPLY_HEALTH=Good这几行。看不到属性说明 power_supply 注册失败,看 dmesg 里的报错;属性在但状态一直是 Discharging,说明充电使能位没写进去,回去查CTRL1的配置。

再往下就是灌参数验证真实行为。

# 设定充电电流 1.2A,观察 ADC 读数是否跟随 echo 1200000 > /sys/class/power_supply/et6238/constant_charge_current sleep 3 cat /sys/class/power_supply/et6238/current_now # 扫描寄存器全表,和手册默认值对一遍 bash tools/et6238_test.sh dump

current_now返回的是实际充电电流的微安值,和设定值偏差在 10% 以内算正常,超过就要查输入限流和适配器能力。dump子命令是把 0x00 到 0x0A 全部读一遍打印,和手册的复位默认值逐条比对,能快速定位哪一位写歪了。这个脚本在产测阶段很值钱,一批板子跑一遍,配置不对的直接挑出来。

5. 避坑与排查:调这颗片子最容易翻车的五个地方

这些不是从手册上抄的,是实际调试里一次次栽出来的。每条我都按「看到什么、为什么、怎么办」写,遇到了直接对着排。

5.1 上电与总线类问题

现象一:i2cdetect能看到 0x6b,但insmod之后 probe 失败,dmesg 报unexpected chip id 0xff。

原因通常不在代码,而在芯片根本没进工作状态。很多电源芯片有个 EN 或者 CE 引脚,悬空时芯片处于关断态,I2C 从机接口是活的能应答地址,但内部寄存器全是 0xFF。另一种可能是 SDA/SCL 上拉电阻阻值选大了,400kHz 下上升沿太缓,ACK 位采样出错。

解决办法是先量 EN 脚电平,确认被拉高;再量总线上拉,400kHz 下一般用 2.2k 到 4.7k,10k 在长走线上会出问题。确认硬件之后,在 probe 里加一段上电延时再读 ID,有些芯片从上电到 I2C 就绪需要几十毫秒,读太早就是 0xFF。

现象二:模块加载成功,属性也出来了,但读出来的电压电流全是 0。

原因是 ADC 通道没使能。这颗片子的 ADC 是独立使能的,为了省电默认关闭,寄存器表里那个使能位不写,读回来就是 0。翻代码会发现hw_init里只配了充电参数,忘了配 ADC。

解决办法是在hw_init里补上 ADC 使能位,并且使能之后延时 10 毫秒再读,转换需要时间。顺手在get_property里加一个判断,读到 0 的时候打一条 warn,下次就不会再被这个坑绊住。

5.2 充电行为与状态上报类问题

现象三:充电电流设定 2A,实测只有 800mA,反复改寄存器无效。

原因是输入限流没放开。芯片会在 VBUS 电流达到输入限流阈值时自动降充电电流,这个降流动作是硬件做的,软件读CHG_CUR寄存器还是你设的值,所以你会觉得「配置明明写进去了却没生效」。

解决办法是同时把input-current-limit-microamp提到适配器能力之上,并且接一个能真正输出 2A 的电源做测试。用 PC 的 USB 口测大电流是典型的误判来源,那个口本身可能就只有 900mA 能力。

现象四:电量在 99% 和 100% 之间来回跳,或者满了之后一直不进充电。

原因是终止电流阈值和再充电阈值没配成一对。终止电流设得太小,电池永远到不了终止条件;再充电阈值设得太接近满充电压,一充就满、一放就掉,形成振荡。

解决办法是终止电流取满充电流的 10% 左右,再充电阈值比满充电压低 100 到 150mV,形成足够的迟滞。这两个值我一般放在设备树里而不是写死在代码里,不同容量的电池配比不一样。

现象五:设备休眠唤醒之后,充电配置全部恢复默认,电流又变回出厂值。

原因是这颗芯片带了看门狗,一段时间没有喂狗位就自己复位寄存器,而驱动里没有 suspend/resume 的处理。表现很有迷惑性,因为运行中一切正常,只有休眠之后才出问题,容易误判成电源管理子系统的问题。

解决办法是实现dev_pm_ops的 resume 回调,重新下发一遍配置;或者在 monitor work 里周期性检查关键寄存器,发现被复位就重配。前者干净,后者更抗造,我一般两个都做,resume 保证快速恢复,轮询兜底。

6. 二次开发:把硬编码参数变成板级可配置

改板子是迟早的事。这份源码里最常见的改造点,是把hw_init里那几个写死的电流电压值挪到设备树里,让同一份驱动覆盖多个 SKU。

/* et6238_parse_dt —— 板级参数解析,缺省值给一个安全的小电流 */ static int et6238_parse_dt(struct et6238_chip *chip) { struct device_node *np = chip->dev->of_node; u32 val; int ret; /* 充电电流,默认 500mA,宁可慢也别过冲 */ ret = of_property_read_u32(np, "constant-charge-current-microamp", &val); chip->chg_current_ua = ret ? 500000 : val; /* 浮充电压,默认 4.2V,这是最普遍的锂电规格 */ ret = of_property_read_u32(np, "charge-full-microvolt", &val); chip->chg_voltage_uv = ret ? 4200000 : val; /* 输入限流,默认 500mA,对接标准 USB 口的安全值 */ ret = of_property_read_u32(np, "input-current-limit-microamp", &val); chip->input_limit_ua = ret ? 500000 : val; dev_info(chip->dev, "dt: ichg=%duA vchg=%duV ilim=%duA\n", chip->chg_current_ua, chip->chg_voltage_uv, chip->input_limit_ua); return 0; }

这段的关键不在解析,而在缺省值。设备树里漏写一个属性,驱动不能挂,得用安全值继续跑,同时在 dmesg 里喊一声。我见过不少驱动漏了属性就直接-EINVAL,结果整块板子起不来,排查半天发现是 dts 里少写一行。

加一个只读调试节点也很实用。

/* et6238_sysfs.c —— 产测用的寄存器快照节点 */ static ssize_t regs_show(struct device *dev, struct device_attribute *attr, char *buf) { struct et6238_chip *chip = dev_get_drvdata(dev); ssize_t len = 0; unsigned int val; int i, ret; for (i = 0; i <= ET6238_REG_FAULT; i++) { ret = regmap_read(chip->regmap, i, &val); if (ret) return len; len += scnprintf(buf + len, PAGE_SIZE - len, "0x%02x: 0x%02x\n", i, val); } return len; } static DEVICE_ATTR_RO(regs);

scnprintf这里不能换成sprintf,PAGE_SIZE 边界不检查就是内核里最典型的溢出。DEVICE_ATTR_RO省掉了手写权限位,只读节点用这个宏最省事。这个节点在产测时把整张寄存器表打出来,和黄金样机对比,能一把定位配置偏差。

改造完怎么验证,我的习惯是三个层次都过一遍:编译过不算完,得 insmod 之后dmesg没有 error 级日志;参数下发之后读回寄存器,值必须对;最后接上真实电池跑一个完整充电循环,看从预充、恒流、恒压到终止这条曲线是不是平滑。这三步少一步,问题就会流到后面。

从那以后我每次改完驱动,哪怕只动了一个默认值,也强制走一遍「编译—加载—读回寄存器—跑一轮真实充电」这个闭环,再也不敢只看编译通过就交付。希望帮到你。

本文还有配套的精品资源,点击获取