简介面向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 没选中。# 独立编译 MakefileKDIR 指向你板子对应的内核源码或头文件目录 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) cleanobj-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_work1 秒一次够用又不至于占 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 ARCHarm64 export CROSS_COMPILEaarch64-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) modulesmodules_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: charger6b { 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/ueventuevent那一步是最终的通过标准正常输出里应该能看到POWER_SUPPLY_NAMEet6238、POWER_SUPPLY_STATUSCharging、POWER_SUPPLY_HEALTHGood这几行。看不到属性说明 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 dumpcurrent_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.7k10k 在长走线上会出问题。确认硬件之后在 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这里不能换成sprintfPAGE_SIZE 边界不检查就是内核里最典型的溢出。DEVICE_ATTR_RO省掉了手写权限位只读节点用这个宏最省事。这个节点在产测时把整张寄存器表打出来和黄金样机对比能一把定位配置偏差。改造完怎么验证我的习惯是三个层次都过一遍编译过不算完得 insmod 之后dmesg没有 error 级日志参数下发之后读回寄存器值必须对最后接上真实电池跑一个完整充电循环看从预充、恒流、恒压到终止这条曲线是不是平滑。这三步少一步问题就会流到后面。从那以后我每次改完驱动哪怕只动了一个默认值也强制走一遍「编译—加载—读回寄存器—跑一轮真实充电」这个闭环再也不敢只看编译通过就交付。希望帮到你。本文还有配套的精品资源点击获取