news 2026/9/15 18:08:30

高通平台第三方充电IC驱动开发:power_supply框架接入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高通平台第三方充电IC驱动开发:power_supply框架接入实战

做高通平台Linux驱动的人,基本上都会遇到同一个尴尬:项目选型时老板拍板用了某颗第三方充电IC,理由是便宜、交期好、原厂支持给力,结果硬件回来后才发现,高通的charger驱动和PMIC内部的充电逻辑是深度绑定的,不是你在dts里改个compatible它就能乖乖跑起来的。我在某个量产的平板上就栽过这跟头,前前后后折腾了两周,从I2C读不到寄存器一路查到电量计跳变,最后把高通power_supply框架和这颗IC的数据手册翻了个底朝天,才算把这件事理顺。

这篇文章不打算给你堆一堆“注册一个power_supply_class设备就算完事”的ABC教程,网上那种文章一大把,跟着抄完你依然跑不起来。我重点写的是:在高通平台这种既有PMIC充电管理、又有复杂充电状态机的环境下,一颗第三方充电IC到底该怎么沿着power_supply框架“寄生”进去,以及那些数据手册里不会写、只有把板子拿在手上才能踩到的坑。内容基于我实际做过的项目,以TI BQ25601和南芯SC8886这两颗常见IC为例子展开,硬件接口、驱动代码、状态机对接、调试手法都会覆盖到。

1. 高通的充电链路与第三方IC的选型边界

1.1 高通原生充电方案的构成

要搞清楚第三方IC怎么加进去,先得知道高通平台原本是怎么管充电的。高通从PMI8998开始把充电管理集成进了PMIC,AP侧对应的驱动是qpnp-smb5(不同平台叫法略有差异,比如PM7250B对应qpnp-smb5的变体,PM8350B又不一样),它的主要工作包括:

  • 检测USB/适配器的插入拔出,通过中断上报
  • 配置输入电流限制(Input Current Limit)、浮充电压(Float Voltage)
  • 控制充电阶段的切换:预充、恒流、恒压、充电完成
  • 与BMS(电量计)联动,读取电池电压/电流/温度来做充电曲线控制
  • 处理Type-C口的方向检测和PR_Swap

这一整套逻辑,在原生方案里PMIC充电器自己就把活干完了,AP侧的驱动只需要把状态翻译给power_supply框架,再往上层抛uevent。

所以很多第一次接触第三方充电IC的工程师,第一反应是“我只要把PMIC的充电关了,让第三方IC自己充就行”。这个思路方向对,但实现起来远没有那么简单,因为高通整个系统里有好几套机制在同时盯着充电这件事,比如BCL(Battery Current Limiter)、JEITA温度策略、充电状态机、USB PD协商,全都在围绕PMIC充电器在运转。你硬生生在中间插一个第三方IC,却不把状态同步给这些机制,轻则电量计数据错乱,重则充电打到一半直接被系统关掉,甚至把电池充坏。

1.2 什么场景下必须上第三方IC

高通PMIC自带充电器不是万能的,现实项目中碰到下面几种情况,基本上就得考虑外挂充电IC:

  • 大电流快充需求:PMIC内部的充电管受限于封装和散热,持续大电流充电(比如6A以上)往往顶不住,这时候需要外挂一颗升降压或电荷泵架构的充电IC,承担主要的充电功率转换。像SC8886这种就是典型的升降压充电IC,支持1-4节电池,最大充电电流能做到6A以上。
  • 多节电池方案:平板、二合一笔记本、便携POS机这类产品经常用2串或更多串的电池,高通的PMIC充电器主要是为单节电池设计的,多串电池必须用独立的充电管理IC。
  • 成本或供应链考虑:PMIC内置充电器在某些低端平台上的饱和电流不够给力,而一颗国产充电IC只要几块钱,配合外部MOS和电感,性能够用,成本却低一截。
  • 特殊充电策略:有些行业设备需要低温充电、脉冲充电、反向放电(OTG反向给外部设备供电),这些功能PMIC不一定都支持,但第三方充电IC往往天生就带。

以我最近接触的某个手持终端项目为例,硬件就是一颗SC8886做升降压充电,PMIC的充电功能完全不用,只保留它的BMS采样功能(采电池电压、温度)。换句话说,充电的“执行者”变了,但“监督者”依然是高通PMIC。这个思路要贯穿整个驱动设计过程。

1.3 选型时要确认的四个硬件接口

决定外挂充电IC之后,硬件选型阶段就得把下面几个接口确认清楚,否则驱动写一半发现管子被复用或者中断冲突,哭都来不及:

  1. 通信接口:绝大多数充电IC都是I2C接口,地址一般是0x6B、0x6A之类。要确认这个地址和同总线上的其他器件不冲突。SC8886的I2C地址可以通过ADR引脚配置成0x6B或0x6C。
  2. 中断引脚:充电IC会通过INT引脚向AP上报插入拔出、充电完成、故障等事件,这个GPIO必须有,且最好选一个支持中断唤醒的引脚。
  3. 充电使能引脚:CE引脚控制充电器是否实际输出电流,有的IC还带独立的EN引脚。这个脚一般由AP GPIO控制,驱动里要能在必要时强制停止充电(比如温度过高、系统进入低功耗模式)。
  4. 电流采样电阻:BQ25601这类NVDC架构的IC需要外接10mΩ或20mΩ的采样电阻,阻值会直接影响驱动里电流读数的换算系数。驱动里的R_SENSE值必须和硬件一一对应,否则读出来的电流全是错的。

2. power_supply框架替你干了哪些活

2.1 从sysfs看框架的外在表现

先说个基础概念,Linux内核里power_supply框架的核心作用,是给所有“电源类设备”提供一套统一的上报接口。注册成功的每一个电源设备,都会在/sys/class/power_supply/下生成一个目录。你在手机上通过adb shell cat /sys/class/power_supply/battery/uevent能看到的那些信息,包括电压、电流、电量、温度、充电状态,全是各个驱动通过这个框架一层层抛上来的。

一台典型的高通设备,/sys/class/power_supply/下面一般会看到这么几个节点:

节点名类型对应设备
batteryBATTERY电池电量计(BMS)
usbUSBUSB口充电状态
main_chargerCHARGER主充电器
wirelessWIRELESS无线充电(如果有)
bmsBATTERY高通的BMS驱动(采集电压/电流/温度)
parallel_chargerCHARGER并联充电器(高通原生并行充电方案)

你要外挂的第三方充电IC,最典型的做法就是注册成一个type为CHARGER或者USB的power_supply设备。它在sysfs里的表现,就是新增一个目录,并且在uevent里抛出一堆充电状态属性。

2.2 内核侧的核心结构体与回调机制

驱动代码里你打交道最多的是struct power_supply_descstruct power_supply_config这两个结构体。前者描述“我是什么电源设备”,后者描述“我应该和谁打交道”。

power_supply_desc里最关键的是这五个字段:

  • name:设备名,对应sysfs目录名
  • type:设备类型,充电IC一般用POWER_SUPPLY_TYPE_USBPOWER_SUPPLY_TYPE_CHARGER
  • properties:一个属性数组,列出这个设备支持上报哪些属性
  • num_properties:属性个数
  • get_property:回调函数,用户空间读某个属性时触发

举个例子,BQ25601的驱动会注册一个bq25601_charger设备,properties数组里会有POWER_SUPPLY_PROP_STATUS(充电状态)、POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT(恒流充电电流设置)、POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT(输入电流限制)等。当你在终端里执行cat /sys/class/power_supply/bq25601_charger/status时,内核就会调用驱动里的get_property回调,然后你在回调里通过I2C去读充电IC的寄存器,把充电状态解析出来返回给上层。

power_supply_config里有一个对充电IC来说极其关键的字段:supplied_to。这是char *[]类型的数组,里面填的是“我这个充电器要给谁供电”。手机场景下,它指向的就是battery,意思是充电IC和电池之间是供电关系。这个字段的直接作用是:充电IC的online状态发生变化时,power_supply框架会自动通知supplied_to里指定的那些设备,让它们重新读取状态。如果没有配这个字段,你插上了充电器,电量计那边可能还傻傻不知道,自然不会更新充电状态。

2.3 状态变更的通路:power_supply_changed和uevent

充电IC驱动里,中断处理函数中做的最重要的一件事,就是调用power_supply_changed(psy)。这个函数会触发power_supply框架向用户空间发送uevent,上层hal层或者应用层就能实时感知到“充电器插入了”、“充电完成了”、“温度越界了”这些事件。

这条链路本身不复杂,但要注意一个细节:power_supply_changed不是同步阻塞的,它只是把事件挂到workqueue里异步去发。所以在中断里调用它没问题。但是如果你在中断里通过I2C读寄存器,I2C控制器本身可能处于休眠状态,直接读会卡死,所以常见做法是把状态读取放到work_struct或者threaded irq里做,中断只负责置标志位。

3. 驱动实现的三个核心模块

开始写代码之前,我建议你先做一件事:读完充电IC的完整数据手册,把下面这张表整理出来。我做驱动时每次都先建这个表,后面写代码只需要对着填就行了。

功能寄存器地址位域读写属性
充电使能REG00BIT4RW
充电状态指示REG08BIT3:0R
输入电流限制REG00BIT2:0RW
恒流充电电流REG04BIT5:1RW
恒压充电电压REG06BIT7:2RW
中断状态REG0BBIT0RC
芯片IDREG0CBIT7:0R

有了这张表,写驱动就是在做翻译工作:把power_supply框架的标准化属性,翻译成这颗IC的寄存器操作。

3.1 设备树节点与probe流程

设备树是第一步。以BQ25601为例,硬件挂在I2C总线上,设备树节点长这样:

&i2c_3 { status = "okay"; clock-frequency = <400000>; bq25601: bq25601@6b { compatible = "ti,bq25601"; reg = <0x6b>; interrupt-parent = <&tlmm>; interrupts = <62 IRQ_TYPE_EDGE_FALLING>; ti,ce-gpio = <&tlmm 63 GPIO_ACTIVE_LOW>; ti,sense-resistor-mohm = <10>; ti,charge-current-ma = <2000>; ti,input-current-limit-ma = <1500>; ti,charge-voltage-mv = <4400>; status = "okay"; }; };

注意几点:

  • reg = <0x6b>必须和硬件实际接线一致,否则I2C探测阶段就失败。
  • interrupts里我用了IRQ_TYPE_EDGE_FALLING,充电IC的中断输出一般是低有效,插入/拔出事件会拉低中断脚,这个边沿触发类型要跟IC的中断输出极性匹配。
  • CE引脚用的GPIO,我在dts里用ti,ce-gpio带出去了,因为不是所有平台都默认把CE脚拉高,驱动里probe时必须主动设置GPIO的方向和初始电平。

probe函数的标准写法如下:

static int bq25601_probe(struct i2c_client *client) { struct device *dev = &client->dev; struct power_supply_config psy_cfg = {}; struct bq25601 *charger; int ret; charger = devm_kzalloc(dev, sizeof(*charger), GFP_KERNEL); if (!charger) return -ENOMEM; charger->client = client; i2c_set_clientdata(client, charger); mutex_init(&charger->reg_lock); /* 读取芯片ID,确认I2C通信正常 */ ret = bq25601_read_byte(charger, BQ25601_REG_IC_ID, &val); if (ret < 0) { dev_err(dev, "Failed to read chip ID: %d\n", ret); return ret; } if (val != BQ25601_IC_ID_EXPECTED) { dev_err(dev, "Unexpected chip ID: 0x%02x\n", val); return -ENODEV; } /* CE引脚初始化为高电平,允许充电 */ charger->ce_gpio = devm_gpiod_get(dev, "ti,ce", GPIOD_OUT_HIGH); if (IS_ERR(charger->ce_gpio)) return PTR_ERR(charger->ce_gpio); /* 中断申请:使用threaded irq,把I2C访问放到线程上下文 */ ret = devm_request_threaded_irq(dev, client->irq, NULL, bq25601_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "bq25601", charger); if (ret) { dev_err(dev, "Failed to request IRQ: %d\n", ret); return ret; } /* 注册power_supply设备 */ charger->psy_desc.name = "bq25601_charger"; charger->psy_desc.type = POWER_SUPPLY_TYPE_USB; charger->psy_desc.properties = bq25601_props; charger->psy_desc.num_properties = ARRAY_SIZE(bq25601_props); charger->psy_desc.get_property = bq25601_get_property; psy_cfg.drv_data = charger; psy_cfg.of_node = dev->of_node; psy_cfg.supplied_to = bq25601_supplied_to; psy_cfg.num_supplicants = ARRAY_SIZE(bq25601_supplied_to); charger->psy = devm_power_supply_register(dev, &charger->psy_desc, &psy_cfg); if (IS_ERR(charger->psy)) { dev_err(dev, "Failed to register power_supply: %ld\n", PTR_ERR(charger->psy)); return PTR_ERR(charger->psy); } return 0; }

我在实际项目里踩过一个坑:一开始没有配置psy_cfg.supplied_to,结果驱动注册完全正常,sysfs节点也出来了,但插入USB后battery节点的status始终不切到“Charging”。查了两天才发现是供电关系没建立,framework没往电量计那边推状态。这个字段一定要配。

3.2 I2C寄存器读写封装

充电IC的I2C读写是驱动的地基,这一层封装不牢固,上层所有功能都会出幺蛾子。三星、TI、南芯这些厂家的数据手册里寄存器基本都是8位地址、8位数据,用普通的i2c_smbus_read_byte_datai2c_smbus_write_byte_data就够了。

但要注意I2C通信异常的情况。充电IC挂在系统I2C总线上,总线上还有其他设备,如果I2C控制器时钟异常或者总线被拉死,一次读写会卡住整个系统的I2C总线,直接导致系统hang住。在处理中建议加个访问保护:

static int bq25601_read_byte(struct bq25601 *charger, u8 reg, u8 *data) { int ret; mutex_lock(&charger->reg_lock); ret = i2c_smbus_read_byte_data(charger->client, reg); mutex_unlock(&charger->reg_lock); if (ret < 0) return ret; *data = (u8)ret; return 0; }

我加这个互斥锁的原因特别简单:充电状态读取和中断处理可能发生在不同上下文,如果两个线程同时发起I2C操作,虽然I2C核心层本身有锁,但中间夹着寄存器缓存的一些运算操作就不安全了。另外,有一个非常重要的小细节:充电IC的寄存器,写入前最好先读一下当前的原始值,改完位域后再写回。因为很多寄存器一个地址里塞了好几个功能位,贸然整体写入会把它不相关的配置冲掉。我在一个项目中就是直接写寄存器把OTG配置位给覆盖了,结果Type-C口反向供电功能神秘失效。

3.3 get_property属性集与充电状态上报

这是power_supply驱动的重头戏。用户在sysfs里读你的充电器状态,最终都会走到这个回调里来。你需要根据充电IC的数据手册,把常用的属性翻译成寄存器读取操作。

以前我做BQ25601时报的属性集包括:

static enum power_supply_property bq25601_props[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_ONLINE, POWER_SUPPLY_PROP_CHARGE_TYPE, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, POWER_SUPPLY_PROP_HEALTH, POWER_SUPPLY_PROP_PRESENT, };

get_property回调里的处理逻辑,核心就是在一堆属性里做switch-case:

static int bq25601_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct bq25601 *charger = power_supply_get_drvdata(psy); int ret = 0; switch (psp) { case POWER_SUPPLY_PROP_STATUS: ret = bq25601_get_status(charger, &val->intval); break; case POWER_SUPPLY_PROP_ONLINE: ret = bq25601_get_online(charger, &val->intval); break; case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: ret = bq25601_get_charge_current(charger, &val->intval); break; default: return -EINVAL; } return ret; }

拿最关键的POWER_SUPPLY_PROP_STATUS来说,充电IC的寄存器里一般会有一个充电状态指示位,指示当前是“未充电”、“预充”、“恒流充电”、“恒压充电”还是“充电完成”。你在驱动里要把这个状态映射成power_supply框架的标准枚举值:

充电IC寄存器状态power_supply标准枚举
未充电/待机POWER_SUPPLY_STATUS_NOT_CHARGING
预充/恒流/恒压中POWER_SUPPLY_STATUS_CHARGING
充电完成POWER_SUPPLY_STATUS_FULL
故障状态POWER_SUPPLY_STATUS_DISCHARGING

BQ25601的寄存器0x08的bit 3:0能直接读出这几种状态,读取后简单switch映射一下就行。

4. 让第三方IC接入高通充电状态机

4.1 充电状态同步:别让高通PMIC“以为”自己还在充

这是整篇最核心、最容易被忽略的一点。

很多工程师把第三方充电IC的驱动写完,sysfs节点都能读到电压电流,以为就完事了。但你在高通平台上跑一下就会发现:插入充电器,第三方IC确实在充电,但系统上层显示“未充电”,电量还在往下掉。

原因是高通整个电量管理架构里,healthdBatteryService看的不是你的第三方充电IC,而是battery设备(也就是BMS电量计)。BMS设备自己在算充电状态的时候,依据的是它从上端充电器那里接收到的online状态和电压电流数据。第三方充电IC如果不通过supplied_to机制把状态同步给BMS,BMS就一直认为外部没有电源接入,自然继续按放电逻辑计算。

所以设备树和驱动里有一个环节不能省:不仅要注册充电IC自己的power_supply设备,还要在supplied_to里明确指向battery,让框架自动完成状态推送

static char *bq25601_supplied_to[] = { "battery", };

插拔事件产生时,power_supply_changed会遍历supplied_to里的设备,调用它们的external_power_changed回调。高通BMS驱动会在这个回调里主动执行一次状态刷新,接着上层就能看到正确的充电状态了。

4.2 插入/拔出事件与中断处理

刚才提到中断只置标志位,不直接做I2C访问。具体实现如下:

static irqreturn_t bq25601_irq_handler(int irq, void *data) { struct bq25601 *charger = data; /* 中断线程里读取INT寄存器,把事件源搞清楚 */ bq25601_read_byte(charger, BQ25601_REG_INT0, &int0); bq25601_read_byte(charger, BQ25601_REG_INT1, &int1); if (int0 & BQ25601_INT0_VBUS_INSERT_MASK) power_supply_changed(charger->psy); if (int0 & BQ25601_INT0_CHG_DONE_MASK) power_supply_changed(charger->psy); if (int1 & BQ25601_INT1_FAULT_MASK) { /* 读取具体故障寄存器,驱动里记录下来 */ bq25601_get_health(charger, &health); power_supply_changed(charger->psy); /* 必要时通过GPIO强制关断充电 */ if (health != POWER_SUPPLY_HEALTH_GOOD) gpiod_set_value(charger->ce_gpio, 0); } /* 清中断标志位 */ bq25601_write_byte(charger, BQ25601_REG_INT0, 0xFF); bq25601_write_byte(charger, BQ25601_REG_INT1, 0xFF); return IRQ_HANDLED; }

这里要注意中断标志的清除顺序:一定要先读中断状态、处理完业务逻辑,最后再清标志。有些IC的中断寄存器是“读即清”或者“写1清”,如果是“读即清”,你在开头一读,中断标志就已经没了,后面再做什么都晚了,会漏事件。

另外,高通对充电相关的中断有一些底层要求。如果这个充电IC在休眠时要保持中断唤醒能力(也就是插入充电器能把系统从suspend状态唤醒来充电),驱动在probe阶段需要把中断设置成wakeup source:

device_init_wakeup(dev, true); enable_irq_wake(client->irq);

如果这一步没做,板子休眠后插充电器,充电IC自己的硬件确实开始充了,但AP还睡着,上层状态不会更新,等你按电源键唤醒之后,才看到一瞬间跳变的电量。

4.3 与BMS电量计的联动细节

在高通平台,电量计一般走的是高通的qpnp-bms或者qpnp-fg驱动。它读取电池电压、电流、温度,积分算SOC。第三方充电IC的接入,对BMS最大的影响是充电电流的采样路径。

举一个我实际遇到过的问题:用了第三方充电IC之后,BMS上报的充电电流是负的(也就是还在放电),但万用表实测充电IC确实在往电池灌电流。排查到最后发现,BMS的电流采样是由PMIC完成的,PMIC的充电管被禁用后,它自带的电流采样通路里“电阻分压逻辑”失效了,导致BMS认为没有电流流入。

这种问题的根因不是充电IC的驱动,而是硬件方案设计上的冲突。解决思路通常有两个:

  1. 充电IC内部集成的电流采样(比如SC8886本身就有电流ADC),把它通过I2C读出来,由充电IC驱动直接更新给battery节点。这个方案需要你自己扩展battery节点的属性逻辑,比较麻烦。
  2. 硬件上保留PMIC的电池电流采样通路,让PMIC的充电器禁能但BMS功能继续工作。这是最省事的方案,但前提是硬件设计时PMIC和电池之间的采样电阻必须保持连接。

如果你是用SC8886这类升降压IC,它经常配一套自己的电量计方案(比如SC8886配套的BMS固件),这时候更彻底的做法是围绕这颗IC重新梳理整个电量路径,直接把高通的BMS禁用,用IC自带的方案。做之前一定和硬件工程师确认电流采样到底走的哪条通路。

5. 设备树与内核配置的完整参考

5.1 charger节点配置

前面已经给了BQ25601的设备树节点,再补一个SC8886的参考。SC8886是升降压架构,I2C地址一般是0x6B,中断脚和CE脚配置方式类似:

&i2c_7 { status = "okay"; clock-frequency = <400000>; sc8886: sc8886@6b { compatible = "southchip,sc8886"; reg = <0x6b>; interrupt-parent = <&tlmm>; interrupts = <28 IRQ_TYPE_EDGE_FALLING>; pinctrl-names = "default"; pinctrl-0 = <&charger_irq_default>; sc8886,ce-gpio = <&tlmm 29 GPIO_ACTIVE_HIGH>; sc8886,en-gpio = <&tlmm 30 GPIO_ACTIVE_HIGH>; sc8886,sense-resistor-mohm = <5>; sc8886,charge-current-ma = <6000>; sc8886,input-current-limit-ma = <3000>; sc8886,charge-voltage-mv = <8800>; /* 2串电池场景 */ sc8886,otg-voltage-mv = <5000>; sc8886,otg-current-ma = <1500>; status = "okay"; }; };

注意SC8886因为是升降压,输出电压可以高于输入电压,这也是它适合做多串电池快充的原因。charge-voltage-mv这里就要和硬件协商好,2串电池和单串电池的值完全不同,写错直接导致充电电压过低充不进去或者过高损坏电池。

5.2 defconfig与内核配置

设备树搞定之后,需要在内核配置里把对应的驱动编进去。高通平台一般在arch/arm64/configs/vendor/<platform>-_defconfig里加:

CONFIG_CHARGER_BQ25601=y CONFIG_CHARGER_SC8886=y

如果你的驱动是通过Kconfig挂到drivers/power/supply/下的,别忘了在drivers/power/supply/Kconfigdrivers/power/supply/Makefile里加上对应项。高通平台编译内核时,vendor defconfig和普通defconfig经常是叠加关系,改完defconfig后最好搜索确认一下有没有被另一个配置文件覆盖掉。

我干过一件蠢事:改完defconfig直接编译,结果烧进去之后sysfs里根本没有charger节点,回头一查,发现我用的编译脚本参数里指定的是vendor/<platform>-perf_defconfig,而我只改了vendor/<platform>-defconfig,两个文件长得几乎一样,字符差别不大,但加载的配置天差地别。

5.3 常见编译和链接问题

驱动文件写完后,编译阶段最常见的就三类错误:

  • 头文件路径缺失#include <linux/power_supply.h>找不到,检查内核源码里对应目录有没有编译进去。
  • 回调函数签名不匹配:新版内核4.19以上get_property的prototype变成了int (*get_property)(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val),老驱动代码如果还按struct power_supply *psystruct power_supply_config *cfg写,直接编译报错。内核版本升级时这个API变过好几次,网上随手抄的代码经常在这里翻车。
  • I2C ID表没写:驱动里必须有static const struct i2c_device_id bq25601_i2c_id[],否则设备树compatible匹配到了,但modprobe方式加载时找不到设备ID表对应的条目。

6. 调试手段与高频故障排查

6.1 sysfs节点逐级排查法

驱动加载后,第一件事就是先看sysfs里节点有没有生成:

adb shell ls -l /sys/class/power_supply/

正常情况下应该能看到bq25601_charger或者sc8886_charger的目录。如果没有,按这个顺序排查:

  1. 设备树节点有没有生效:adb shell ls /proc/device-tree/i2c@*/,看看有没有对应的compatible字段。
  2. 驱动有没有probe成功:adb shell dmesg | grep bq25601,有报错信息一般就直接定位。
  3. I2C通信正不正常:用i2c-tools直接读芯片ID寄存器:
adb shell i2cget -f -y 3 0x6b 0x0c

如果这一步返回的芯片ID不是预期值,那就是硬件问题,查I2C上拉电阻、地址引脚、焊接——大概率是硬件问题,尤其是首板。

6.2 状态数据实测验证

节点成功出现后,直接读各个属性值:

adb shell cat /sys/class/power_supply/bq25601_charger/uevent adb shell cat /sys/class/power_supply/battery/status adb shell cat /sys/class/power_supply/battery/voltage_now

插入充电器后,对比下面的预期结果:

观察点插入充电器前插入充电器后
bq25601_charger/online01
bq25601_charger/statusDischargingCharging
battery/statusDischargingCharging
battery/charge_typeN/AFast
battery/current_now负值(放电)正值或接近0(充电,视符号约定)

如果online变成1但status始终是Not charging,优先怀疑supplied_to没配对,或者BMS的external_power_changed回调没有正确执行。

如果status是充电但current_now一直是负值,大概率是电流采样路径问题,结合4.3的思路去查。

6.3 几个我实际踩过的高频坑

坑一:中断风暴导致系统卡死

有次用SC8886,probe之后插上充电器,CPU占用率直接飙到100%,卡到触摸都没反应。查日志发现中断触发了几万次。原因是充电IC的中断标志是“写1清”,驱动里却用了写0清,导致中断脚一直处于低电平,中断线一直被拉低,形成中断风暴。

修法很简单,把清中断的寄存器写入值从0改成0xFF。但这个坑的隐蔽之处在于,有些IC的手册写着“write 1 to clear”,实际行为却是“写1清对应位,写0无影响”,那写成0xFF也是安全的,而写成0就一点作用都没有。

坑二:拔掉充电器后battery状态卡在Charging

这个是中断触发条件没配全。BQ25601的VBUS拔出事件和充电完成事件走的是不同的中断源,我只处理了其中一个,导致拔出时没有调用power_supply_changed,上层不知道充电器已经没了。所以中断处理时,把充电IC所有中断源都读一遍、都处理一遍,尤其是拔出事件。

坑三:休眠后插入充电器无法唤醒

前文提到需要enable_irq_wake,但这个设置在高通平台可能还不够。高通TLMM引脚默认在休眠时会把它配置成输入下拉,中断触发条件和唤醒能力都可能丢失。需要在设备树里通过pinctrl配置好休眠时的GPIO状态:

charger_irq_default: charger_irq_default { mux { pins = "gpio62"; function = "gpio"; }; config { pins = "gpio62"; bias-pull-up; input-enable; }; }; charger_irq_suspend: charger_irq_suspend { mux { pins = "gpio62"; function = "gpio"; }; config { pins = "gpio62"; bias-pull-up; input-enable; input-debounce = <1000>; }; };

然后在dts节点里引用休眠状态的pinctrl,确保睡眠时中断引脚还保持着正确的上拉和输入属性。

坑四:I2C速率导致寄存器读写偶发失败

充电IC初始化时我按400kHz的总线频率去读,有时候能读到有时候读不到,后来用示波器抓波形,发现上升沿太缓,总线上拉电阻选大了。降到100kHz就稳了。如果你碰到I2C读写偶发失败,先别急着改驱动,排查一下总线频率和上拉阻值。

6.4 整体调试流程参考

最后给一个我自己项目里梳理的调试顺序,跟着走基本能解决90%的问题:

  1. dmesg查驱动probe有没有成功
  2. sysfs确认节点生成
  3. i2cget读芯片ID确认通信
  4. 插充电器,确认online状态
  5. 确认battery状态跟着变
  6. 实测充电电流是否符合预期
  7. 拔掉充电器,确认状态切换回Discharging
  8. 休眠测试,确认插入充电器能唤醒
  9. 充电全程监测温度、电压、电流曲线

走到第6步如果电流不对,回头查寄存器设置和采样电阻配置。走到第7步不对,查中断处理逻辑。第8步不对,查pinctrl和wakeup配置。

我在接触高通平台之前,一直以为power_supply就是个简单的“注册一下设备、实现几个回调”的框架,真正上手才发现它背后牵扯着电量计、充电状态机、中断唤醒、上层BatteryService这一整条链路。第三方充电IC能不能在高通平台上“无缝”工作,关键不在驱动本身,而在于你愿不愿意花时间去弄清它和高通原生机制之间那些看不见的握手关系。希望这篇对正在填这个坑的人有帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 18:06:44

Loop macOS 窗口管理工具使用指南:径向菜单与一键窗口布局

Loop macOS 窗口管理工具使用指南&#xff1a;径向菜单与一键窗口布局 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop Loop 是一款免费开源的 macOS 窗口管理工具&#xff0c;解决系统缺少内建窗口分屏…

作者头像 李华
网站建设 2026/9/15 18:05:16

MOS登录页改版背后:Oracle统一身份认证迁移深度解析与排障指南

1. 登录页变更的前因后果&#xff1a;它不是临时改版&#xff0c;而是身份体系的一次底层切换先说结论&#xff1a;你看到的新页面不是简单的换皮&#xff0c;也不是Oracle偷偷升级了界面主题&#xff0c;而是MOS&#xff08;My Oracle Support&#xff09;把登录这件事&#x…

作者头像 李华
网站建设 2026/9/15 18:03:55

MATLAB图像处理实战:从算法验证到FPGA部署

简介&#xff1a;本资源是一套完整的MATLAB图像处理教学与实践项目&#xff0c;面向数字图像处理课程学习者、课程设计学生及GUI开发初学者&#xff0c;聚焦超分辨率重建这一核心任务&#xff0c;提供从界面交互到算法实现的端到端解决方案。压缩包共20个文件&#xff0c;含15幅…

作者头像 李华
网站建设 2026/9/15 18:03:32

Zemax显微镜光学设计全流程:从物镜优化到杂散光分析

做光学设计这些年&#xff0c;Zemax几乎就是案头的常驻工具。最近一个项目是整套显微镜系统的光路设计&#xff0c;从物镜初始结构、目镜匹配到照明系统建模&#xff0c;再到后续用非序列模式做分光棱镜的杂散光分析&#xff0c;前前后后折腾了一个多月。这中间踩了不少坑&…

作者头像 李华