ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高通平台外挂第三方充电IC接入power_supply框架全流程解析

高通平台外挂第三方充电IC接入power_supply框架全流程解析 做高通平台的BSP或者内核驱动开发很多朋友早晚会遇到这么一件事项目里不想用高通自带PMIC的那套充电方案转而外挂一颗第三方的充电IC比如TI的BQ25601、BQ25896RICHTEK的RT9467NXP的SC8xxx这些。客户给的理由往往五花八门但归结起来无非是成本、供货、私有快充协议这几个方向。不管哪种原因落到驱动工程师头上就是一句话把这颗IC接到Linux的power_supply框架里让Android上层、HAL层、甚至充电状态机都把它当成一颗标准充电器来用。本文就是我个人在高通平台骁龙6系到8系都调过上接入第三方充电IC的完整思路和实操记录从框架原理、设备树配置、驱动probe、回调实现、中断处理到和高通原生充电功能打架时怎么收场全流程拆开讲。适合正在做类似事情的驱动工程师或者刚接触高通BSP但被分到充电相关bug的新手参考。看完你至少能明白一颗充电IC要进power_supply框架到底需要写什么、注意什么、踩坑点在哪。1. 为什么高通平台要外挂第三方充电IC1.1 高通原生充电方案的限制高通骁龙平台的PMIC比如PM660、PM8150、PM8250内部集成了完整的充电管理功能包括BC1.2检测、USB输入限流、恒压恒流充电、终止电流控制这些。原生方案的好处是软件配套成熟Android的healthd、charger服务、高通的电源管理框架如QPNP、SMBB都是围绕它设计的驱动基本不用动。但实际项目里不用原生方案的情况非常普遍。最常见的原因就是私有快充协议——现在很多手机、平板、移动POS机项目要支持自家私有协议比如低压大电流充电、UFCS融合快充这类高通PMIC原生往往做不到那么灵活的协议适配外挂一颗专用充电IC反而方便芯片原厂直接把协议栈和驱动都给你。其次是成本和供货第三方IC量产后单价低、供应商多、不像高通交期那么紧。最后还有一些设计上的考虑比如把充电电路独立成小板方便复用或者充电功率做得比较大65W往上单独用一颗charge pump IC更安全。1.2 第三方充电IC的接入形态与选型要点接入形态上第三方充电IC几乎都是I2C接口控制配合若干GPIO中断、使能、STAT指示挂在某个空闲I2C总线上。典型的如BQ25601从机地址0x6B使用I2C配置充电电压、电流、终止电流、输入限流通过INT引脚通知插入、拔出、充电完成、故障事件。选型时驱动工程师需要重点关注几个点I2C从机地址和寄存器映射是否清晰有没有read-modify-write的陷阱芯片的中断源是否足够丰富插拔、充电状态跳变、看门狗超时、过温过压是否内置ADC能直接读VBUS、IBUS、V BAT是否带看门狗——这个非常关键很多芯片看门狗一旦开启而驱动没定时喂狗芯片会自动关闭充电导致充电一会就断了的诡异问题。选IC不是硬件的活驱动工程师必须参与评审不然到后面全是驱动在填坑。2. power_supply框架核心设计拆解2.1 框架定位内核里的充电设备抽象层linux的power_supply框架本质上就是一个类设备模型。它把所有与电源相关的东西——电池、充电器、USB、无线充电、电量计——抽象成一个统一的、带属性的设备节点暴露在/sys/class/power_supply/目录下面。上层不管是Android的healthd、charger服务还是你们自研的监控程序统一通过读属性文件来获取状态通过写属性文件来下发指令完全不用关心底层到底是哪颗芯片在干活。这就好比家里用的智能插座——插座下面是什么电器不重要几个标准插孔摆在那谁来都能用。power_supply框架就是给充电设备做的一套标准接口上层只认这套接口驱动负责把芯片的真实行为翻译成接口语义。在高通平台上这个框架还承担着多电源协调的职责。比如系统里同时存在USB检测器Type-C port controller、充电IC、电量计、电池温度监测这些设备通过power_supply的supply关系组成一张供电拓扑上层要查当前是谁在给电池充电时沿着这条拓扑就能找到真正的充电器节点。2.2 核心数据结构与属性回调驱动要做的核心事情就是填充一个power_supply_desc结构体然后调用power_supply_register()把它注册进内核。这个结构体是框架对接驱动的唯一契约static const struct power_supply_desc bq25601_charger_desc { .name bq25601, .type POWER_SUPPLY_TYPE_USB, .properties bq25601_charger_props, .num_properties ARRAY_SIZE(bq25601_charger_props), .get_property bq25601_get_property, .set_property bq25601_set_property, .property_is_writeable bq25601_property_is_writeable, };properties是一个power_supply_property枚举数组它定义了该设备支持哪些属性。get_property和set_property是真正的读写回调框架层在收到上层sysfs访问时会根据属性ID分发到这两个函数里。比如上层读/sys/class/power_supply/bq25601/status内核最终会调用bq25601_get_property()并传入POWER_SUPPLY_PROP_STATUS驱动需要回复一个整数值对应充电状态枚举。这里必须注意不是所有属性都要实现你只declare数组里列出来的那些。如果上层读一个你没实现的属性get_property会返回-EINVAL这是正常的别慌。但一个充电器至少要实现STATUS、HEALTH、ONLINE、PRESENT这四大件否则Android的charger服务会显示异常。2.3 事件上报机制power_supply_changed的秘密驱动在检测到充电状态变化时需要主动调用power_supply_changed(psy)通知内核和上层。这个函数会触发一次uevent事件Android上层接收到后重新读取所有属性刷新状态。注意它不会携带哪个属性变了的信息上层是全部重读的。所以驱动里不要在每个中断里都调用把标志位记下来统一在中断底半部调用一次即可。实际调试中我发现过于频繁地调用power_supply_changed会让上层日志刷屏、CPU占用升高特别是在芯片有持续中断源比如每次ADC转换都触发一次中断的情况下。正确做法是中断里先读寄存器确认到底是什么事件只有充电状态、在线状态、健康状态这些关键信息变化时才上报。3. 驱动开发实操全流程3.1 从芯片手册先抓这些信息拿到一颗充电IC别急着看示例代码先翻手册的这几页I2C从机地址和寄存器映射表确认地址以及哪些寄存器是只读、哪些是R/W、哪些需要read-modify-write。上电默认状态确认芯片上电后充电是否自动开启、看门狗是否默认开启、INT输出极性是什么。充电状态机定义很多IC把未充电、预充电、恒流充电、恒压充电、充电完成编码在状态寄存器的某几个bit里驱动需要把这些bit映射成power_supply的STATUS枚举NOT_CHARGING/CHARGING/FULL。中断寄存器和mask确认有哪些中断源、INT是电平触发还是边沿触发、读哪个寄存器能清除中断。这一步省不得我见过太多人跳过手册直接抄驱动结果连芯片在充电和芯片上报FULL都没分清楚上层永远显示未充电。3.2 设备树配置先把硬件描述清楚设备树是驱动和硬件之间的桥梁。以BQ25601挂在I2C3、中断接在GPIO77为例典型配置如下i2c_3 { status okay; bq256016b { compatible ti,bq25601; reg 0x6b; interrupt-parent tlmm; interrupts 77 IRQ_TYPE_EDGE_FALLING; ti,input-current-limit-microamp 2000000; ti,charge-current-microamp 1024000; ti,charge-voltage-microvolt 4400000; ti,termination-current-microamp 128000; status okay; }; };关于compatible字段芯片原厂提供的驱动通常会自带一个match表比如{ti,bq25601, ...}你要保证设备树和驱动里match的字符串完全一致。reg必须和芯片手册里的从机地址一致。中断触发类型要特别注意BQ25601的INT脚是开漏输出默认低有效一般配IRQ_TYPE_EDGE_FALLING或IRQ_TYPE_LEVEL_LOW都行但要结合硬件上有没有上拉、以及你对边沿丢失的容忍度来定。如果硬件设计有问题导致中断粘连你会看到中断风暴或者干脆没中断。平台相关的还有一件事如果这颗IC要作为系统唯一的charger需要把高通的PMIC充电节点禁用掉。具体节点名要看平台比如pm660_charger或者pmi8998_charger在设备树里加上status disabled;。这一步漏了就会出现两个充电器同时存在的局面上层会随机挑一个去读状态五花八门。3.3 probe流程从I2C探测到注册power_supply驱动probe函数是核心它决定了整个驱动能否正常运行。标准流程我总结为六步第一步通过devm_i2c_new_client_device或者设备树匹配拿到struct i2c_client然后从client里拿struct device和of_node。第二步解析设备树参数把充电限流、目标电压、终止电流等值读出来后面设置寄存器要用。用of_property_read_s32或者of_property_read_u32即可注意单位换算——设备树里我习惯用microamp/microvolt表示寄存器里单位往往不是这个需要按芯片手册换算。第三步分配驱动私有结构体struct bq25601里面至少要有struct i2c_client *client、struct power_supply *psy、struct power_supply_desc psy_desc、struct power_supply_config psy_cfg、int irq、以及用来缓存寄存器值和状态位的字段。第四步对芯片做初始化写入基本配置寄存器、设置充电电流电压、配置中断mask、清掉遗留的中断标志位。如果芯片有看门狗要么关闭它要么配置好喂狗定时器否则芯片会在几十秒后自动关断充电。static int bq25601_hw_init(struct bq25601 *charger) { /* 设置充电电压为4.4V */ bq25601_update_byte(charger, BQ25601_REG_04, 0x8C, 0xFC); /* 高位字节 */ bq25601_update_byte(charger, BQ25601_REG_05, 0x00, 0x03); /* 低位字节 */ /* 设置快速充电电流 */ bq25601_write_byte(charger, BQ25601_REG_02, 0x66); /* 使能充电禁用看门狗 */ bq25601_update_byte(charger, BQ25601_REG_01, 0x00, BIT(4)); return 0; }第五步注册中断。推荐用devm_request_threaded_irq因为充电IC状态查询往往涉及I2C操作I2C通信不能放在中断上下文里必须用线程化中断static int bq25601_irq_init(struct bq25601 *charger) { int ret; charger-irq client-irq; if (!charger-irq) { dev_err(client-dev, no IRQ specified\n); return -EINVAL; } ret devm_request_threaded_irq(client-dev, charger-irq, NULL, bq25601_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, bq25601_irq, charger); if (ret) { dev_err(client-dev, request irq failed: %d\n, ret); return ret; } return 0; }第六步注册power_supply设备charger-psy_desc.name bq25601; charger-psy_desc.type POWER_SUPPLY_TYPE_USB; charger-psy_desc.properties bq25601_charger_props; charger-psy_desc.num_properties ARRAY_SIZE(bq25601_charger_props); charger-psy_desc.get_property bq25601_get_property; charger-psy_desc.set_property bq25601_set_property; charger-psy_desc.property_is_writeable bq25601_property_is_writeable; charger-psy_cfg.drv_data charger; charger-psy_cfg.of_node client-dev.of_node; charger-psy devm_power_supply_register(client-dev, charger-psy_desc, charger-psy_cfg); if (IS_ERR(charger-psy)) { dev_err(client-dev, failed to register power supply\n); return PTR_ERR(charger-psy); }注意devm_power_supply_register是带资源管理的版本驱动unbind时自动注销省心。注册成功之后/sys/class/power_supply/bq25601/就会自动出现。3.4 get_property把芯片寄存器翻译成上层语言get_property是驱动里最繁琐的部分它是上层理解这颗IC当前是什么状态的唯一通道。我会把所有属性用switch语句分发给单独的函数这样可读性和维护性都好。以STATUS属性为例——这是最容易被误判的地方。BQ25601的充电状态在REG08的bit5和bit400未充电01预充电10快速充电11充电完成驱动代码大致这样static int bq25601_get_status(struct bq25601 *charger, int *status) { u8 reg_val; int ret; ret bq25601_read_byte(charger, BQ25601_REG_08, reg_val); if (ret) return ret; switch ((reg_val BQ25601_CHRG_STAT_MASK) 4) { case BQ25601_CHRG_STAT_NOT_CHARGING: *status POWER_SUPPLY_STATUS_NOT_CHARGING; break; case BQ25601_CHRG_STAT_PRE_CHARGING: case BQ25601_CHRG_STAT_FAST_CHARGING: *status POWER_SUPPLY_STATUS_CHARGING; break; case BQ25601_CHRG_STAT_CHARGE_DONE: *status POWER_SUPPLY_STATUS_FULL; break; default: *status POWER_SUPPLY_STATUS_UNKNOWN; break; } return 0; }这个映射千万不要拍脑袋。POWER_SUPPLY_STATUS_FULL表示充满只有芯片明确上报终止完成后才能设置。很多人把连接到适配器但未充电的状态映射成FULL导致上层一直显示100%用户直接懵。其他常用属性也得照顾周全。ONLINE属性表示适配器是否连接HEALTH属性从故障寄存器读出来映射为GOOD、OVER_VOLTAGE、OVER_HEAT等PRESENT属性表示电池是否在位。CURRENT_NOW和VOLTAGE_NOW如果芯片内置ADC可以从ADC寄存器读如果没有可以返回-ENODATA类不会报错只是相关调试工具看不到实时数值。3.5 set_property下发改机指令set_property用于上层主动设置充电参数比如POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT和POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE。上层特别是维修模式、工厂模式调整充电电流时就走这条路。关键点修改寄存器之前先判断当前是否在充电。很多IC在充电过程中修改电流电压寄存器的时机有讲究某些bit需要先关闭充电再改。另外写入参数后要立刻缓存到驱动私有结构体里因为get_property时如果只是读寄存器芯片可能因为状态机的原因还没更新导致读写不对称。property_is_writeable回调也很重要它告诉内核sysfs这个属性文件可写。如果不实现这个回调即使你在set_property里写了逻辑上层写文件时也会被拒。3.6 中断处理把芯片的喊话变成框架的广播中断是充电IC驱动的心脏。充电状态的切换、插拔检测、故障报警全都靠INT引脚通知主控。中断处理函数里要做的事是最小化的static irqreturn_t bq25601_irq_handler(int irq, void *dev_id) { struct bq25601 *charger dev_id; u8 reg_val; /* 读中断状态寄存器既确认事件也顺带清中断 */ bq25601_read_byte(charger, BQ25601_REG_0C, reg_val); bq25601_read_byte(charger, BQ25601_REG_0D, reg_val); /* 读充电状态上报变化 */ power_supply_changed(charger-psy); return IRQ_HANDLED; }注意不要在中断里做任何耗时操作比如循环读多个寄存器、打印寄存器值、调用msleep。用devm_request_threaded_irq注册时handler传NULL、thread_fn传bq25601_irq_handler这样中断处理自动放到内核线程里执行I2C操作就是安全的。清中断标志位这个动作容易被忽略。如果中断标志没有清掉芯片INT脚可能一直拉低触发条件变成电平触发会造成中断风暴。所以每次中断处理读一次中断状态寄存器是必须的既确认事件类型又顺带清标志。4. 与高通平台协同避让、供电关系和电量计4.1 禁用高通原生charger避免两个爸爸抢着报状态高通平台上如果PMIC内部集成了充电管理那它就是内核里的一个power_supply设备名字通常是charger或者main_charger。你外挂第三方IC后如果不做任何处理sysfs下就会同时存在两个充电器节点Android的charger服务会优先选择注册顺序靠前的那个来读状态——到底读谁完全取决于驱动的加载时机这不可控。我吃过一次亏外挂BQ25896后上层时而显示充电时而显示未充电用cat /sys/class/power_supply/*/type一看发现PMIC的charger节点和BQ25896节点同时在而且高通的charger节点先注册占了主位。上层读到的状态是PMIC死的状态完全无视BQ25896。解决办法就是在设备树里把PMIC充电节点禁掉pm660_charger { status disabled; };具体节点名要去你的平台设备树里搜一般搜qcom,qpnp-smb2或者qcom,smb之类的compatible然后把对应节点disable。还有一点禁用PMIC charger后USB插入检测也要确认是否依赖PMIC的extcon如果断了这条路上层就收不到USB插入事件了。这时要么让第三方IC自己检测适配器插入通过中断上报ONLINE属性要么在高通的USB驱动里做绑定后者往往更复杂最好在硬件设计时就把Type-C/CC检测和充电IC打通。4.2 供电关系supplied_to和电源拓扑power_supply框架支持设备之间的供电关系。比如电池是由充电器供电的可以用power_supply_config里的supplied_to指定static char *bq25601_supplied_to[] { battery, }; charger-psy_cfg.supplied_to bq25601_supplied_to; charger-psy_cfg.num_supplicants ARRAY_SIZE(bq25601_supplied_to);这样注册后内核会把bq25601标记为battery的supplier。上层通过power_supply_get_by_name(battery)查询电池的supplier列表时就能找到bq25601从而知道当前是哪颗充电器在给电池充电。在高通平台上电池节点通常由PMIC电量计驱动提供名字就是battery。如果你的硬件方案里电池由第三方电量计IC维护那battery节点会由那个驱动注册名字需要按实际情况调整。4.3 电量计分配charger只管充电容量交给专业选手很多第三方充电IC不集成电量计无法直接给出电池容量百分比。这种情况下我强烈建议把capacity属性的实现单独放给电量计IC充电IC只提供STATUS、ONLINE、HEALTH这些状态属性不要自己去估容量。如果在dts里不引入电量计上层会发现battery的capacity一直是0或者恒定值这是因为没人实现POWER_SUPPLY_PROP_CAPACITY属性。最直接的排查方法cat /sys/class/power_supply/battery/capacity cat /sys/class/power_supply/battery/uevent如果返回为空或者-ENODEV说明battery节点没有正确的容量来源。这时候查一下当前电池节点是谁注册的如果是第三方电量计驱动比如CW2015、BQ27426看看它的驱动有没有正确注册如果压根没有电量计那就得自己写一个小的电池模拟节点用电压查表法估算容量但这个精度很有限不适合量产。4.4 Type-C检测和VBUS状态高通平台现在的USB-C检测通常由独立的CC逻辑芯片如高通自家的WCD9385、FSA4480或者第三方TUSB321、FUSB302负责。第三方充电IC一般只关心适配器插没插它通过VBUS引脚直接判断。我在驱动里通常把ONLINE属性和VBUS在位绑定不依赖USB协议栈。这时候就有一个配合问题上层除了charger服务在轮询充电状态USB驱动也会通过power_supply查询外接电源在线状态。如果充电IC和USB检测器之间的ONLINE上报不一致会出现插上线但系统认为没插电的怪问题。解决思路以USB检测器的插拔事件为总闸当它上报已插入时再使能充电IC的充电路径以充电IC的VBUS状态为辅助两者同时上报才行。简单的做法是在充电IC的ONLINE属性里先读一下USB检测器状态case POWER_SUPPLY_PROP_ONLINE: usb_psy power_supply_get_by_name(usb); online usb_psy ? usb_psy-online : 0; val-intval online bq25601_is_vbus_present(charger); break;虽然多一层依赖但能避免很多单人调试时看不到的系统级矛盾。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因排查与解决sysfs下没有节点probe失败、I2C通信失败、power_supply_register返回错误看dmesg有没有probe错误打印用i2cdetect确认设备在总线上systerm显示一直未充电STATUS属性映射错误、充电路径未使能、看门狗超时关闭充电先读寄存器确认芯片实际在不在充电排查充电使能bit确认看门狗配置充电到一半自动断看门狗未喂、芯片过温保护、电流设置超限检查驱动有没有定时喂狗看HEALTH属性读故障寄存器插入适配器无事件中断没触发、中断mask配置错误、VBUS检测引脚硬件问题用cat /sys/kernel/debug/gpio看中断GPIO电平读芯片中断状态寄存器确认事件有没有发生上层读到重复或错误的charger高通的PMIC charger没禁用设备树里禁用PMIC充电节点或者确认你的第三方IC名字和原有节点不冲突capacity一直0或恒定没有电量计驱动或者battery节点不是由电量计注册的检查/sys/class/power_supply/下的设备列表补电量计驱动中断风暴CPU占用高中断标志未清除、IRQ触发类型不对、共享中断处理不对中断函数里读完状态寄存器确认使用了线程化irq用irq_domain检查中断号I2C读写一直失败从机地址错、I2C总线编号错、硬件上拉缺失、I2C被低功耗状态挂起用i2cdetect扫描地址马上cat /sys/kernel/debug/i2c/3查看总线状态5.2 排查步骤从dmesg到寄存器值遇到问题别急着改代码按顺序来先看dmesg里驱动probe有没有报错再看/sys/class/power_supply/下有哪些设备然后通过sysfs读各属性看有没有异常值最后用i2cget直接读芯片寄存器对照手册验证。这三步能覆盖90%的问题。比如我调BQ25896时遇到过一个非常恶心的bug插上适配器后系统时而识别、时而不识别。用dmesg -w观察发现USB driver和charger driver在抢一个extcon设备。后来看到高通平台里type-c的VBUS检测事件和charger的VBUS检测事件是两个独立线程我在dts里把USB驱动对charger的依赖注释掉后问题就消失了——这属于平台协调问题不是芯片问题所以第一次调试这种问题不要怀疑芯片坏了。5.3 我踩过的几个坑第一个坑是I2C地址。BQ25601的7位地址是0x6B但是i2cdetect扫出来的地址显示是0xD6——因为i2cdetect默认显示8位地址左移一位。很多新手看到0xD6以为芯片地址错了其实没错。这个冤枉路我走过现在看到0x6B的寄存器第一反应就是去手册确认一下芯片是7位还是8位寻址。第二个坑是芯片看门狗。某颗RICHTEK的IC上电默认开启看门狗我一开始没配结果充电5分钟就断一次上层以为是接触不良。查了两天才发现是看门狗超时触发了输出关断。从那以后我写驱动第一步先把看门狗配置好不管默认关没关。第三个坑是寄存器initialization只在probe里做了一次。有时候系统休眠唤醒后IC寄存器会被硬件复位回到默认状态充电参数全丢了。后来我在resume回调里同样调用bq25601_hw_init()并在pm_ops里注册dev_pm_ops才彻底解决。所以记住probe里写的初始化suspend/resume里也要考虑补一遍。第四个坑是中断触发类型。有一次我把BQ25601的中断配成IRQ_TYPE_LEVEL_LOW结果只要芯片中断标志没清就会一直进中断系统负载飙升。后来改成IRQ_TYPE_EDGE_FALLING同时在中断线程里读掉所有中断状态寄存器清了标志才算稳了。实际这是开漏INT脚的经典问题边沿触发要看硬件有没有外部上拉没有上拉的话电平恢复不了边沿就丢了只能配level触发配合mask。最后一个建议调试期间在驱动的probe函数里多打dev_info把读取到的芯片ID、寄存器初始值、识别到的电源状态全打出来。等驱动稳定后再prune掉。这套日志会帮你省下大把抓头发的时间尤其当你的板子是昨天还好好的今天就坏了这种薛定谔状态时全靠这些日志定位是不是硬件配置被改动了。做驱动这些年我越来越觉得充电IC驱动这事儿难的不是怎么适配而是怎么理解和处理这颗IC在高通平台整个电源体系里的角色。你把power_supply框架想清楚了把芯片从手册到寄存器都吃透了剩下就是耐心调试和记录。以上就是我个人的全部实操经验和踩坑记录希望对正在跟充电IC搏斗的同学有帮助。
返回列表