Linux 驱动编写
因为公司业务需要,很早之前写过一个电池驱动,可以兼容TI三款电池电量计芯片(BQ28Z610、BQ40Z50、BQ27542)。一直没整理,本文记录了一个驱动开发过程。
芯片概述
驱动需要同时兼容以下三款芯片:
| 芯片型号 |
适用场景 |
关键特性 |
| BQ28Z610 |
2节锂电池 |
支持阻抗跟踪,标准 SBS 命令集 |
| BQ40Z50 |
多节锂电池(1-4节) |
支持 SHA-1 认证,MAC 命令扩展 |
| BQ27542 |
单节锂电池 |
简化版,低成本方案 |
三款芯片虽然功能差异较大,但通信总线统一为SMBus(兼容标准I2C),因此可以通过寄存器映射表的方式实现一个驱动兼容多款芯片。
主要思路:定义统一功能寄存器索引,为每颗芯片独立配置寄存器映射表,业务层读写全部调用统一索引接口,上层Power Supply子系统无感硬件差异。
I2C驱动注册与设备树匹配
驱动匹配机制
Linux I2C子系统支持两种匹配方式:
- ACPI / 设备树匹配:通过
compatible 字符串匹配(现代嵌入式主流方式)
- I2C ID Table 匹配:通过设备名称匹配(传统方式,兼容旧内核)
在驱动中,两种方式都进行了注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static const struct of_device_id bq_fg_match_table[] = { { .compatible = "ti,bq28z610" }, { .compatible = "ti,bq40z50" }, { .compatible = "ti,bq27542" }, {}, }; MODULE_DEVICE_TABLE(of, bq_fg_match_table);
static const struct i2c_device_id bq_fg_id[] = { { "bq28z610", BQ28Z610 }, { "bq40z50", BQ40Z50 }, { "bq27542", BQ27542 }, {}, }; MODULE_DEVICE_TABLE(i2c, bq_fg_id);
|
宏作用解析:MODULE_DEVICE_TABLE 将匹配结构体导出至模块段,内核加载驱动、modprobe动态加载时,可自动遍历总线设备完成绑定。
I2C 驱动结构体注册
1 2 3 4 5 6 7 8 9 10 11 12 13
| static struct i2c_driver bq_fg_driver = { .driver = { .name = "bq_fg", .owner = THIS_MODULE, .of_match_table = bq_fg_match_table, }, .id_table = bq_fg_id, .probe = bq_fg_probe, .remove = bq_fg_remove, .shutdown = bq_fg_shutdown, };
module_i2c_driver(bq_fg_driver);
|
module_i2c_driver() 是一个便捷宏,它会自动生成 module_init 和 module_exit 函数,分别调用 i2c_add_driver() 和 i2c_del_driver()。
设备树配置示例
同一I2C总线挂载同地址不同芯片,通过status字段按需启停,硬件底板兼容焊接三款芯片,软件按需开启节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| &i2c3{ status = "okay"; i2c-scl-rising-time-ns = <345>; i2c-scl-falling-time-ns = <11>; clock-frequency = <100000>;
battery0: bq28z610@0b{ status = "okay"; compatible ="ti,bq28z610"; reg = <0x0b>;
}; battery1: bq40z50@0b{ status = "disabled"; compatible ="ti,bq40z50"; reg = <0x0b>;
}; };
|
芯片枚举与寄存器索引
三款芯片的寄存器地址各不相同,但功能语义一致。我们定义统一的寄存器索引枚举,再为每款芯片配置具体的地址映射表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| enum bq_fg_reg_idx { BQ_FG_REG_MAC = 0, BQ_FG_REG_TEMP, BQ_FG_REG_VOLT, BQ_FG_REG_AI, BQ_FG_REG_BATT_STATUS, BQ_FG_REG_TTE, BQ_FG_REG_TTF, BQ_FG_REG_FCC, BQ_FG_REG_RM, BQ_FG_REG_CC, BQ_FG_REG_SOC, BQ_FG_REG_SOH, BQ_FG_REG_DC, BQ_FG_REG_MBA, BQ_FG_REG_MAC_CHKSUM, NUM_REGS, };
enum bq_fg_device { BQ28Z610, BQ40Z50, BQ27542 };
|
寄存器映射表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #define INVALID_REG_ADDR 0xFF
static u8 bq28z610_regs[NUM_REGS] = { INVALID_REG_ADDR, 0x08, 0x09, 0x0b, 0x16, 0x12, 0x13, 0x10, 0x0f, 0x17, 0x0d, INVALID_REG_ADDR, 0x18, INVALID_REG_ADDR, INVALID_REG_ADDR, };
static u8 bq40z50_regs[NUM_REGS] = { INVALID_REG_ADDR, 0x08, 0x09, 0x0B, 0x16, 0x12, 0x13, 0x10, 0x0f, 0x17, 0x0d, INVALID_REG_ADDR, 0x18, INVALID_REG_ADDR, INVALID_REG_ADDR, };
static u8 bq27542_regs[NUM_REGS] = { INVALID_REG_ADDR, 0x06, 0x08, 0x14, 0x0a, 0x16, INVALID_REG_ADDR, 0x12, 0x10, 0x2a, 0x2c, INVALID_REG_ADDR, INVALID_REG_ADDR, INVALID_REG_ADDR, INVALID_REG_ADDR, };
|
按照功能索引一一绑定每颗芯片原生I2C寄存器地址,不支持功能统一填INVALID_REG_ADDR,i2c_probe 阶段读取芯片类型,拷贝对应映射表至私有结构体 bq->regs[],所有业务读写,只传入功能索引,底层自动适配物理寄存器地址.
SMBus 读写
BQ 系列芯片遵循 SMBus(System Management Bus)协议,Linux 内核提供了 i2c_smbus_* 系列函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| static int __fg_read_word(struct i2c_client *client, u8 reg, u16 *val) { s32 ret; ret = i2c_smbus_read_word_data(client, reg); if (ret < 0) { bq_err("i2c read word fail: can't read from reg 0x%02X\n", reg); return ret; } *val = (u16)ret; return 0; }
static int __fg_write_word(struct i2c_client *client, u8 reg, u16 val) { s32 ret; ret = i2c_smbus_write_word_data(client, reg, val); if (ret < 0) { bq_err("i2c write word fail: can't write 0x%02X to reg 0x%02X\n", val, reg); return ret; } return 0; }
|
读取接口示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static void fg_read_fw_version(struct bq_fg_chip *bq) { int ret; u8 buf[36];
ret = fg_write_word(bq, bq->regs[BQ_FG_REG_MBA], FG_MAC_CMD_FW_VER); if (ret < 0) { bq_err("Failed to send firmware version subcommand:%d\n", ret); return; }
mdelay(2);
ret = fg_mac_read_block(bq, bq->regs[BQ_FG_REG_MBA], buf, 11); if (ret < 0) { bq_err("Failed to read firmware version:%d\n", ret); return; }
bq_log("FW Ver:%04X, Build:%04X\n", buf[2] << 8 | buf[3], buf[4] << 8 | buf[5]); bq_log("Ztrack Ver:%04X\n", buf[7] << 8 | buf[8]); }
|
Power Supply框架概述
Linux 的 power_supply 子系统是电池/电源管理的标准框架,它将电池抽象为一个 “power supply” 设备,通过统一的 sysfs 接口向用户空间暴露属性。
用户空间可以通过以下路径访问:
/sys/class/power_supply/BAT/ — 电池属性目录
cat /sys/class/power_supply/BAT/capacity — 读取电量百分比
cat /sys/class/power_supply/BAT/voltage_now — 读取电压
属性注册
1 2 3 4 5 6 7 8 9 10 11
| static enum power_supply_property fg_props[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT, POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_CURRENT_NOW, POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_CAPACITY_LEVEL, POWER_SUPPLY_PROP_TEMP, POWER_SUPPLY_PROP_CHARGE_FULL, POWER_SUPPLY_PROP_TECHNOLOGY, };
|
属性读取回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| static int fg_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct bq_fg_chip *bq = power_supply_get_drvdata(psy); int ret;
switch (psp) { case POWER_SUPPLY_PROP_STATUS: val->intval = fg_get_batt_status(bq); break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW: ret = fg_read_volt(bq); mutex_lock(&bq->data_lock); if (ret >= 0) bq->batt_volt = ret; val->intval = bq->batt_volt * 1000; mutex_unlock(&bq->data_lock); break;
case POWER_SUPPLY_PROP_CAPACITY: if (bq->fake_soc >= 0) { val->intval = bq->fake_soc; break; } ret = fg_read_rsoc(bq); mutex_lock(&bq->data_lock); if (ret >= 0) bq->batt_soc = ret; else bq->batt_soc = 0; val->intval = bq->batt_soc; mutex_unlock(&bq->data_lock); break;
case POWER_SUPPLY_PROP_TEMP: if (bq->fake_temp != -EINVAL) { val->intval = bq->fake_temp; break; } ret = fg_read_temperature(bq); mutex_lock(&bq->data_lock); if (ret > 0) bq->batt_temp = ret; else bq->batt_temp = 0; val->intval = bq->batt_temp; mutex_unlock(&bq->data_lock); break;
default: return -EINVAL; } return 0; }
|
Power Supply 注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static int fg_psy_register(struct bq_fg_chip *bq) { struct power_supply_config fg_psy_cfg = {};
bq->fg_psy_d.name = "BAT"; bq->fg_psy_d.type = POWER_SUPPLY_TYPE_BATTERY; bq->fg_psy_d.properties = fg_props; bq->fg_psy_d.num_properties = ARRAY_SIZE(fg_props); bq->fg_psy_d.get_property = fg_get_property; bq->fg_psy_d.set_property = fg_set_property; bq->fg_psy_d.property_is_writeable = fg_prop_is_writeable;
fg_psy_cfg.drv_data = bq; fg_psy_cfg.num_supplicants = 0;
bq->fg_psy = devm_power_supply_register(bq->dev, &bq->fg_psy_d, &fg_psy_cfg); if (IS_ERR(bq->fg_psy)) { bq_err("Failed to register fg_psy"); return PTR_ERR(bq->fg_psy); } return 0; }
|
中断与状态监控
ALERT 中断处理
BQ 芯片的ALERT 引脚在电池状态变化时触发(如低电量、过温、充满等)。驱动中注册线程化中断处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| static irqreturn_t fg_btp_irq_thread(int irq, void *dev_id) { struct bq_fg_chip *bq = dev_id;
mutex_lock(&bq->irq_complete); bq->irq_waiting = true;
if (!bq->resume_completed) { pr_info("IRQ triggered before device resume\n"); if (!bq->irq_disabled) { disable_irq_nosync(irq); bq->irq_disabled = true; } mutex_unlock(&bq->irq_complete); return IRQ_HANDLED; } bq->irq_waiting = false;
fg_read_status(bq); fg_update_status(bq);
power_supply_changed(bq->fg_psy);
mutex_unlock(&bq->irq_complete); return IRQ_HANDLED; }
|
定时轮询工作队列
除了中断驱动,还启用了延时工作队列进行定时轮询(5 秒间隔),确保数据及时更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static void fg_monitor_workfunc(struct work_struct *work) { struct bq_fg_chip *bq = container_of(work, struct bq_fg_chip, monitor_work.work);
fg_update_status(bq); fg_dump_registers(bq);
power_supply_changed(bq->fg_psy);
schedule_delayed_work(&bq->monitor_work, 5 * HZ); }
|
在 probe() 中初始化:
1 2
| INIT_DELAYED_WORK(&bq->monitor_work, fg_monitor_workfunc); schedule_delayed_work(&bq->monitor_work, 5 * HZ);
|
在 remove() 中取消:
1
| cancel_delayed_work_sync(&bq->monitor_work);
|
一些优势
- 多芯片兼容,通过寄存器映射表映射,支持一个驱动兼容多个电池芯片
- 数据主动上报,避免系统更新电池延迟