雪落的小屋

展讯平台安卓15电池健康实现分析

2025-05-13学习笔记Android编程小坑LK

好像是说欧盟新规定,手机必须要有电池健康的查询,所以客户提了这个需求。

查询文档

参阅展讯文档 《Kernel 5.15充电模块配置指导手册V1.3》

以总电量值为基础,累计充电电量达到总电量值时记为一次完整的充电次数。此功能可以用来统计用户使用过程对电池进行充电、放电的循环次数。可以根据需求配置是否启用此功能,在 pmic fgu 节点中增加如下字段,即可开启此功能。

&pmic_fgu {
 ...
 sprd,capacity-charge-cycle;
 ...

分析代码

在源代码中查询关键字:

./kernel5.15/kernel5.15/drivers/power/supply/sc27xx_fuel_gauge.c:4692:          device_property_read_bool(&pdev->dev, "sprd,capacity-charge-cycle");

定位到代码:

static int sc27xx_fgu_probe(struct platform_device *pdev) {
 ...
 data->support_charge_cycle =
  device_property_read_bool(&pdev->dev, "sprd,capacity-charge-cycle");
 if (!data->support_charge_cycle)
  dev_info(&pdev->dev, "Do not support charge cycle function\n");
    ...
}
 
 
static void sc27xx_fgu_parse_cmdline(struct sc27xx_fgu_data *data)
{
    ...
 /* parse charge cycle */
 if (data->support_charge_cycle)
  sc27xx_fgu_parse_charge_cycle(data);
    ...
}
 
static void sc27xx_fgu_parse_charge_cycle(struct sc27xx_fgu_data *data)
{
 char result[32] = {};
 int ret;
 char *str;
 
 str = "charge.charge_cycle=";
 data->charge_cycle = -1;
 ret = sc27xx_fgu_parse_cmdline_match(data, str, result, sizeof(result));
 if (!ret) {
  ret = kstrtoint(result, 10, &data->charge_cycle);
  if (ret) {
   data->charge_cycle = -1;
   dev_err(data->dev, "Covert charge_cycle fail, ret = %d, result = %s\n",
    ret, result);
  }
 }
}
 
 
static void sc27xx_fgu_calc_charge_cycle(struct sc27xx_fgu_data *data, int cap, int *fgu_cap)
{
 int delta_cap;
 
 if (!data->support_charge_cycle)
  return;
 
 if (*fgu_cap == SC27XX_FGU_MAGIC_NUMBER)
  *fgu_cap = cap;
 
 delta_cap = cap - *fgu_cap;
 
 if (data->support_debug_log)
  dev_info(data->dev, "%s: delta_cap = %d, fgu_cap = %d, cap = %d\n",
    __func__, delta_cap, *fgu_cap, cap);
 
 *fgu_cap = cap;
 
 /*
  * Formula:
  * charge_cycle(0.001 cycle) = accumulate_cap  * 1000 /  SC27XX_FGU_FCC_PERCENT
  */
 if (delta_cap > 0)
  data->charge_cycle += delta_cap * 1000 / SC27XX_FGU_FCC_PERCENT;
}

继续查找数据存储位置:

// VND\bsp\bootloader\lk\platform\sprd_shared\driver\battery\sprd_battery.c
int fdt_fixup_charger_parameters(u8 *fdt)
{
    ...
 int charge_cycle = -1;
 ...
 /* Read charge cycle */
 if (MISCDATA_CHARGE_CYCLE_OFFSET != 0 && MISCDATA_CHARGE_CYCLE_SIZE != 0) {
  ret = sprdbat_read_miscdata_data((char *)(&charge_cycle),
       MISCDATA_CHARGE_CYCLE_OFFSET,
       MISCDATA_CHARGE_CYCLE_SIZE);
  if (ret) {
   errorf("sprd_chg: sprdbat Fail to read charge_cycle\n");
   charge_cycle = -1;
  }
 }
}
 

发现是放在 miscdata 中,查看数据偏移

./VND/bsp/bootloader/lk/project/ums9230_6h10.mk

# miscdata
GLOBAL_DEFINES += \
        MISCDATA_ADDRESS_BASE=10496 \
        MISCDATA_MAGIC_OFFSET=0 \
        MISCDATA_MAGIC_SIZE=4 \
        MISCDATA_RTC_TIME_OFFSET=4 \
        MISCDATA_RTC_TIME_SIZE=8 \
        MISCDATA_CHARGE_CYCLE_OFFSET=12 \
        MISCDATA_CHARGE_CYCLE_SIZE=4 \
        MISCDATA_BASP_OFFSET=16 \
        MISCDATA_BASP_SIZE=4 \
        MISCDATA_CAPACITY_OFFSET=20 \
        MISCDATA_CAPACITY_SIZE=4 \
        MISCDATA_CAPACITY_CHECK_OFFSET=24 \
        MISCDATA_CAPACITY_CHECK_SIZE=4 \
        MISCDATA_SHUTDOWN_CHARGE_FLAG_OFFSET=28 \
        MISCDATA_SHUTDOWN_CHARGE_FLAG_SIZE=4 \
        MISCDATA_AUTO_POWERON_FLAG_OFFSET=32 \
        MISCDATA_AUTO_POWERON_FLAG_SIZE=1

既然知道数据放哪里,后续写 Miscdata 就可以了

小插曲

最开始是起了一个 SystemServer 去实现这个需求

尝试1:读写 miscdata 以文件流读写,补权限会遇到 neverallow

尝试2:参考工模的 PhaseCheck,使用 LocalSocket 连接到 phasecheckServer ,发现也有权限报错,SystemServer 不能连接其他的 Socket,也是 neverallow ,大无语。

尝试3:土法发广播给工模的 PhaseCheckReceiver 这下就好了。

其他

测试用 adb 手动写日期。

printf '\x2C\xFF\x34\x01\x90\xFF\x34\x01' | dd of=/dev/block/by-name/miscdata bs=1 seek=10536 conv=notrunc

设置里面界面默认没开,要打开

// packages\apps\Settings\src\com\android\settings\fuelgauge\BatterySettingsFeatureProviderImpl.java
/** Feature provider implementation for battery settings usage. */
public class BatterySettingsFeatureProviderImpl implements BatterySettingsFeatureProvider {
 
    @Override
    public boolean isManufactureDateAvailable(Context context, long manufactureDateMs) {
        return false;
    }
 
    @Override
    public boolean isFirstUseDateAvailable(Context context, long firstUseDateMs) {
        return false;
    }
 
    @Override
    public boolean isBatteryInfoEnabled(Context context) {
        return false;
    }