第一次接触Linux电源管理子系统时,我完全被各种专业术语搞晕了。后来在实际项目中摸爬滚打几年后才发现,这套机制其实设计得非常巧妙。简单来说,它就是Linux内核专门为电源设备(比如电池、充电器)设计的一套"翻译官"系统。
想象一下你的手机电池:它只会用"电压下降5mV"、"温度升高2℃"这样的硬件语言说话。而Android系统需要知道的却是"电量还剩30%"、"正在快速充电"这样的用户语言。Power Supply子系统就是负责在这两者之间架起桥梁。
这个框架主要由三部分组成:
我见过很多新手容易混淆的概念是:Power Supply Class不是直接管理硬件的,它只是制定了一套标准接口。真正的硬件操作还是由具体的驱动来完成,比如高通平台的qpnp-smb5驱动或者TI的bq系列芯片驱动。
让我们用最常见的电池驱动为例。首先需要定义驱动的基本信息:
c复制static struct power_supply_desc battery_desc = {
.name = "battery",
.type = POWER_SUPPLY_TYPE_BATTERY,
.properties = battery_props,
.num_properties = ARRAY_SIZE(battery_props),
.get_property = battery_get_property,
.set_property = battery_set_property,
};
这里有几个关键点需要注意:
name字段会在/sys/class/power_supply/下创建对应目录type要准确匹配设备类型(电池/USB/无线充等)properties数组定义了该设备支持的属性我在第一次实现时犯过的错误是忘记初始化num_properties,结果导致sysfs节点创建不全。建议用ARRAY_SIZE宏来避免这种低级错误。
属性获取函数是驱动的核心,典型的实现如下:
c复制static int battery_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct battery_data *data = power_supply_get_drvdata(psy);
switch (psp) {
case POWER_SUPPLY_PROP_STATUS:
val->intval = get_battery_status(data);
break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = get_battery_capacity(data);
break;
case POWER_SUPPLY_PROP_TEMP:
val->intval = get_battery_temp(data);
break;
default:
return -EINVAL;
}
return 0;
}
实测中发现一个性能优化技巧:对于需要通过I2C读取的传感器数据,可以考虑添加缓存机制,但要注意及时更新缓存值。
驱动注册成功后,你会在/sys/class/power_supply/battery/目录下看到各种属性文件。比如:
capacity:当前电量百分比status:充电状态temp:电池温度这些文件的读写权限由驱动决定。比如只读属性在驱动中不实现set_property回调。我常用的调试命令是:
bash复制# 查看所有属性
find /sys/class/power_supply/battery/ -type f | xargs -I{} sh -c 'echo -n "{}: "; cat {}'
# 监控实时变化
watch -n 1 cat /sys/class/power_supply/battery/capacity
当电池状态变化时,驱动需要调用:
c复制power_supply_changed(battery_psy);
这会触发以下连锁反应:
在实际项目中,我遇到过uevent丢失的问题。后来发现是因为在中断上下文中调用了power_supply_changed。正确的做法是使用工作队列来异步处理。
让我们看一个真实的TI BQ25601驱动实现片段:
c复制/* 定义支持的属性 */
static enum power_supply_property bq25601_battery_props[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_HEALTH,
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
POWER_SUPPLY_PROP_CURRENT_NOW,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_TEMP,
POWER_SUPPLY_PROP_CHARGE_FULL,
POWER_SUPPLY_PROP_CHARGE_NOW,
};
/* 驱动初始化 */
static int bq25601_probe(struct i2c_client *client)
{
struct bq25601_device *bq;
bq = devm_kzalloc(&client->dev, sizeof(*bq), GFP_KERNEL);
/* 初始化power_supply_desc */
bq->bat_desc.name = "bq25601-battery";
bq->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY;
bq->bat_desc.properties = bq25601_battery_props;
bq->bat_desc.num_properties = ARRAY_SIZE(bq25601_battery_props);
bq->bat_desc.get_property = bq25601_battery_get_property;
/* 注册power_supply */
bq->bat_psy = devm_power_supply_register(&client->dev,
&bq->bat_desc, &psy_cfg);
/* 设置定时器轮询状态 */
INIT_DELAYED_WORK(&bq->status_work, bq25601_status_worker);
schedule_delayed_work(&bq->status_work, STATUS_CHECK_INTERVAL);
}
这个驱动有几个值得注意的实现细节:
在开发过程中,我总结了一些典型问题的解决方法:
问题1:sysfs节点没有生成
num_properties是否正确get_property回调已实现问题2:属性值显示不正确
问题3:uevent事件未触发
udevadm monitor监听事件记得有一次调试时,电池电量显示总是卡在50%,后来发现是硬件工程师把检测电阻焊反了。所以当软件查不出问题时,不妨也检查下硬件连接。
在支持快充的设备中,通常会有多个电源设备:
c复制static enum power_supply_property usb_props[] = {
POWER_SUPPLY_PROP_ONLINE,
POWER_SUPPLY_PROP_VOLTAGE_MAX,
POWER_SUPPLY_PROP_CURRENT_MAX,
};
static enum power_supply_property dc_props[] = {
POWER_SUPPLY_PROP_ONLINE,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
};
通过supplied_to字段可以建立它们之间的关系:
c复制static char *battery_supplied_to[] = { "system" };
static char *usb_supplied_to[] = { "battery" };
bq->usb_desc.supplied_to = usb_supplied_to;
bq->usb_desc.num_supplicants = ARRAY_SIZE(usb_supplied_to);
一个健壮的驱动应该实现完整的状态机:
c复制enum charger_state {
STATE_DISCONNECTED,
STATE_CHARGING,
STATE_FULL,
STATE_FAULT,
};
static void update_charger_state(struct bq25601_device *bq)
{
int ret;
u8 reg;
ret = i2c_smbus_read_byte_data(bq->client, REG_STATUS);
if (ret < 0) {
dev_err(&bq->client->dev, "read status failed\n");
return;
}
reg = ret;
if (!(reg & STATUS_PG)) {
bq->state = STATE_DISCONNECTED;
} else if (reg & STATUS_CHRG) {
bq->state = STATE_CHARGING;
} else {
bq->state = STATE_FULL;
}
if (reg & STATUS_FAULT) {
bq->state = STATE_FAULT;
}
}
在嵌入式设备上,电源管理的效率至关重要。以下是几个实测有效的优化方法:
中断替代轮询:
c复制/* 在probe函数中 */
irq = gpio_to_irq(bq->irq_gpio);
ret = request_threaded_irq(irq, NULL, bq25601_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"bq25601-irq", bq);
属性变化聚合:
c复制static void bq25601_status_worker(struct work_struct *work)
{
bool changed = false;
if (check_voltage_changed())
changed = true;
if (check_temp_changed())
changed = true;
if (changed)
power_supply_changed(bq->bat_psy);
}
合理设置轮询间隔:
c复制/* 快充时更频繁检查 */
if (bq->state == STATE_CHARGING) {
interval = FAST_CHECK_INTERVAL;
} else {
interval = SLOW_CHECK_INTERVAL;
}
schedule_delayed_work(&bq->status_work, interval);
记得在某个项目中,通过将轮询间隔从1秒调整为5秒,整机待机电流降低了0.3mA。虽然看似微小,但对穿戴设备来说非常宝贵。
工欲善其事,必先利其器。这些工具是我日常调试的得力助手:
sysfs接口:
bash复制# 查看所有电源设备
ls /sys/class/power_supply/
# 查看具体属性
cat /sys/class/power_supply/battery/status
内核日志:
bash复制dmesg | grep power_supply
i2c工具:
bash复制# 安装工具
sudo apt install i2c-tools
# 扫描I2C设备
i2cdetect -y 1
# 读取寄存器
i2cget -y 1 0x6b 0x0
电源监测:
bash复制# 查看电源事件
upower --monitor-detail
遇到过一个棘手的案例:电池电量在80%时突然跳到100%。最终通过逻辑分析仪抓取I2C波形,发现是硬件上拉电阻值不匹配导致通信错误。