本文以 SGM41528_9 充电芯片驱动为例,完整说明一个 Linux I2C 寄存器型设备的常见读写分层:
●●●业务接口
-> 寄存器字段接口
-> 读改写接口
-> 带锁寄存器接口
-> SMBus 单字节接口
-> I2C 控制器
-> 芯片硬件寄存器读完本文后,应能够回答以下问题:
1. i2c_smbus_read_byte_data() 和 i2c_smbus_write_byte_data() 分别做什么。 2. 为什么底层函数使用 s32 返回值,而输出数据使用 u8。
3. 为什么普通寄存器访问需要互斥锁。
4. 为什么修改一个字段不能直接覆盖整个寄存器。
5. reg、mask、shift 如何描述一个寄存器字段。 6. field_read() 和 field_write() 如何隐藏寄存器位运算细节。
7. PG_STAT 如何变成 Linux 的 POWER_SUPPLY_PROP_ONLINE。
8. 上层如何通过 Power Supply 接口修改 EN_CHG。
9. I2C 错误如何逐层返回给调用者。
10. 当前实现有哪些值得保留和需要改进的地方。
本文所有“当前代码行为”均以真实源码为准。芯片内部 Power Good 阈值、充电状态编码等硬件细节,如果源码不能证明,则明确标记为“需查数据手册”。
寄存器地址用于指定要访问芯片内部的哪个寄存器,例如:
●●●0x06:包含 EN_CHG 等控制字段
0x0B:包含 CHRG_STAT 等状态字段
0x0C:包含 PG_STAT 等输入状态字段字段表确认 EN_CHG 位于 0x06,CHRG_STAT 位于 0x0B,PG_STAT 位于 0x0C。
SGM 驱动使用单字节寄存器访问,因此一次底层读取获得一个 u8,即完整的 8 位寄存器值:
●●●bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0一个寄存器可能同时承载多个字段,所以读取出来的一个字节不一定只表示一种功能。
逻辑字段值是从完整寄存器中提取出的某一组位。例如 EN_CHG 只有一位,逻辑值是 0/1;CHRG_STAT 占三位,逻辑值范围理论上是 0~7。
驱动通过 mask 选择字段,通过 shift 把字段移动到 bit 0,形成上层容易使用的普通整数。

图中业务入口由 Power Supply 描述符注册,读取回调为 sgm41528_9_get_property(),写入回调为 sgm41528_9_set_property()。
字段层位于 field_read() 和 field_write(),带锁的完整寄存器读取由 read_reg() 完成,字段写入则通过 update_bits() 完成读改写。
最底层通过 Linux SMBus Byte Data API 与 I2C 设备通信。

SGM 驱动把 i2c_client、设备对象、Power Supply 对象和 I2C 互斥锁放在同一个私有结构体中:
●●●struct sgm41528_9_device {
struct i2c_client *client;
struct device *dev;
struct power_supply *charger;
struct mutex lock;
struct mutex i2c_rw_lock;
/* 其他成员省略 */
};其中:
client | |
dev | |
charger | |
i2c_rw_lock |
probe() 建立关联I2C 核心调用驱动的 probe() 时传入 struct i2c_client *client。驱动把它保存到 sgm->client,并初始化 I2C 互斥锁:
●●●sgm->client = client;
sgm->dev = dev;
mutex_init(&sgm->lock);
mutex_init(&sgm->i2c_rw_lock);
i2c_set_clientdata(client, sgm);之后任何拿到 sgm 的函数,都可以通过 sgm->client 访问同一颗芯片,而不需要重复传递 I2C 总线号和从设备地址。
●●●staticint __sgm41528_9_read_byte(struct sgm41528_9_device *sgm,
u8 reg, u8 *data)
{
s32 ret;
ret = i2c_smbus_read_byte_data(sgm->client, reg);
if (ret < 0) {
pr_err("i2c read fail: can't read from reg 0x%02X\n", reg);
return ret;
}
*data = (u8)ret;
return0;
}●●●sgm :当前芯片实例
reg :需要读取的 8 位寄存器地址
data :输出参数,保存读取到的一个字节函数返回值和输出值分开设计:
●●●返回 0 :读取成功,数据写入 *data
返回负数 :读取失败,返回 Linux I2C 错误码ret 使用 s32i2c_smbus_read_byte_data() 成功时返回 0~255,失败时返回负错误码,因此不能直接用 u8 保存返回值。代码先用有符号 s32 接收,确认 ret >= 0 后再转换为 u8。
读取失败时,*data 不会被更新。调用者必须先判断返回值,再使用输出数据。
从协议角度看,i2c_smbus_read_byte_data(client, reg) 表示“以 reg 为命令码读取一个数据字节”。典型时序为:
●●●S
-> Slave Address + Write
-> ACK
-> Command/Register Address
-> ACK
-> Repeated START
-> Slave Address + Read
-> ACK
-> Data Byte
-> NACK
-> P符号含义:
●●●S :START
P :STOP
ACK :接收方应答
NACK :主机读取最后一个字节后结束传输Linux I2C 核心和具体 I2C Adapter 驱动负责把 SMBus 请求转换成控制器传输;SGM 驱动只提供 client 和寄存器地址。
●●●staticint __sgm41528_9_write_byte(struct sgm41528_9_device *sgm,
int reg, u8 val)
{
s32 ret;
ret = i2c_smbus_write_byte_data(sgm->client, reg, val);
if (ret < 0) {
pr_err("i2c write fail: can't write 0x%02X "
"to reg 0x%02X: %d\n", val, reg, ret);
return ret;
}
return0;
}●●●sgm :当前芯片实例
reg :目标寄存器地址
val :需要写入的完整 8 位值成功时返回 0,失败时返回底层负错误码。与读取不同,写操作没有额外输出参数。
典型写入时序为:
●●●S
-> Slave Address + Write
-> ACK
-> Command/Register Address
-> ACK
-> Data Byte
-> ACK
-> P这个接口写入的是一个完整字节。如果目标寄存器包含多个字段,而业务只想修改其中一位,则不应该直接调用该接口覆盖整个寄存器,应使用后面的 update_bits() 或 field_write()。
__sgm41528_9_read_byte() 和 __sgm41528_9_write_byte() 中的双下划线不是 C 语言语法要求,而是本驱动采用的命名约定。
在当前实现中,这两个底层函数自身不获取 i2c_rw_lock。上层 read_reg() 和 update_bits() 会在调用它们之前加锁,因此双下划线可以理解为:
●●●内部底层版本;调用者负责满足加锁或无并发前提这只是代码约定,编译器不会自动阻止其他函数直接调用双下划线版本。
●●●staticint sgm41528_9_read_reg(struct sgm41528_9_device *sgm,
u8 reg, u8 *data)
{
int ret;
mutex_lock(&sgm->i2c_rw_lock);
ret = __sgm41528_9_read_byte(sgm, reg, data);
mutex_unlock(&sgm->i2c_rw_lock);
return ret;
}i2c_rw_lock 让当前驱动中的访问者按顺序执行 I2C 寄存器操作,避免同一设备的不同线程同时进入底层传输。

这里使用 mutex,意味着调用路径必须处于允许睡眠的上下文。I2C 传输本身通常也可能睡眠,因此普通工作线程、probe()、sysfs 和 Power Supply 回调可以使用,硬中断上下文不能直接使用。
该锁只能约束遵守它的当前驱动代码。如果某个路径直接调用 __sgm41528_9_write_byte(),就会绕过这把锁。它也不能自动协调其他驱动或用户空间通过其他接口访问同一硬件。

假设一个寄存器布局如下:
●●●bit 7 bit 6 bit 5:4 bit 3 bit 2:0
EN_OTG AUTO_DET TREG EN_CHG OTHER如果只想修改 EN_CHG,直接写一个固定字节可能同时覆盖 EN_OTG、AUTO_DET、TREG 和 OTHER。正确方式是:
●●●读取旧寄存器值
-> 清除 EN_CHG 对应位
-> 合并新的 EN_CHG 值
-> 写回完整寄存器●●●staticint sgm41528_9_update_bits(struct sgm41528_9_device *sgm,
u8 reg, u8 mask, u8 val)
{
int ret;
u8 tmp;
mutex_lock(&sgm->i2c_rw_lock);
ret = __sgm41528_9_read_byte(sgm, reg, &tmp);
if (ret) {
pr_err("Failed: reg=%02X, ret=%d\n", reg, ret);
goto out;
}
tmp &= ~mask;
tmp |= val & mask;
ret = __sgm41528_9_write_byte(sgm, reg, tmp);
if (ret)
pr_err("Failed: reg=%02X, ret=%d\n", reg, ret);
out:
mutex_unlock(&sgm->i2c_rw_lock);
return ret;
}两行位运算:
●●●tmp &= ~mask;
tmp |= val & mask;等价于:
●●●new_value = (old_value & ~mask) | (val & mask);含义分别是:
●●●old_value & ~mask :清除目标字段,保留其他位
val & mask :只保留新值中属于目标字段的位
按位或 :把新字段合并到旧寄存器假设:
●●●old = 0xAC = 10101100
mask = 0x0C = 00001100
val = 0x04 = 00000100清除目标字段:
●●●~mask = 11110011
10101100
& 11110011
-----------
10100000合并新值:
●●●val & mask = 00000100
10100000
| 00000100
-----------
10100100 = 0xA4最终只有 bit 3:2 被修改,其他位保持不变。
错误的锁范围可能产生丢失更新:
●●●线程 A 读取 0x10
线程 B 修改并写回 0x20
线程 A 仍根据旧值 0x10 计算并写回
线程 B 的修改被覆盖当前实现从读取前一直持锁到写回后,使“读、改、写”成为当前驱动内部不可交叉的事务。
out读取失败后 tmp 无效,不能继续计算和写回。使用 goto out 可以统一释放互斥锁。如果直接在错误分支 return ret,会遗漏 mutex_unlock(),导致后续访问阻塞。
●●●struct sgm41528_9_fields {
u8 reg;
u8 shift;
u8 mask;
};成员含义:
reg | EN_CHG0x06 | |
shift | EN_CHG | |
mask | 0x08 |
驱动先用枚举为字段命名:
●●●enum SGM41528_9_BIT_OPTION {
/* ... */
F_BIT_EN_CHG,
/* ... */
F_BIT_CHRG_STAT,
F_BIT_PG_STAT,
/* ... */
F_BIT_MAX,
};再通过指定初始化器建立映射:
●●●staticconststruct sgm41528_9_fields
sgm41528_9_regs[F_BIT_MAX] = {
[F_BIT_ICHG] = {0x01, 0, GENMASK(5, 0)},
[F_BIT_WD] = {0x05, 4, GENMASK(5, 4)},
[F_BIT_EN_CHG] = {0x06, 3, GENMASK(3, 3)},
[F_BIT_CHRG_STAT] = {0x0B, 0, GENMASK(2, 0)},
[F_BIT_PG_STAT] = {0x0C, 7, GENMASK(7, 7)},
};GENMASK()GENMASK(high, low) 生成从 bit high 到 bit low 连续为 1 的掩码:
●●●GENMASK(3, 3) = 00001000 = 0x08
GENMASK(5, 4) = 00110000 = 0x30
GENMASK(5, 0) = 00111111 = 0x3F
GENMASK(7, 7) = 10000000 = 0x80shift 的本质shift 是字段最低有效位的 bit 编号,也就是 mask 中最右侧那个 1 所在的位置。
0000 0001 | ||
0000 1000 | ||
0000 1100 | ||
0011 0000 | ||
1100 0000 |
shift 负责在两种表示之间转换:
●●●寄存器对齐值 --右移 shift--> 右对齐逻辑值
右对齐逻辑值 --左移 shift--> 寄存器对齐值field_read()●●●staticint sgm41528_9_field_read(struct sgm41528_9_device *sgm,
enum SGM41528_9_BIT_OPTION field_id)
{
int ret;
u8 val;
ret = sgm41528_9_read_reg(
sgm,
sgm41528_9_regs[field_id].reg,
&val);
if (ret < 0)
return ret;
val &= sgm41528_9_regs[field_id].mask;
val >>= sgm41528_9_regs[field_id].shift;
return val;
}●●●field_value = (register_value & mask) >> shift;流程为:
●●●field_id
-> 查表得到 reg/mask/shift
-> 读取完整寄存器
-> mask 清除无关位
-> 右移 shift
-> 返回逻辑字段值int字段本身只有 u8,但函数需要同时返回字段值和负错误码:
●●●>= 0:字段值
< 0:I2C 读取错误所以上层必须先执行:
●●●ret = sgm41528_9_field_read(...);
if (ret < 0)
return ret;再把 ret 当作字段值。
field_write()●●●staticint sgm41528_9_field_write(struct sgm41528_9_device *sgm,
enum SGM41528_9_BIT_OPTION field_id,
u8 val)
{
return sgm41528_9_update_bits(
sgm,
sgm41528_9_regs[field_id].reg,
sgm41528_9_regs[field_id].mask,
val << sgm41528_9_regs[field_id].shift);
}●●●aligned_value = logical_value << shift;
new_register = (old_register & ~mask) |
(aligned_value & mask);上层传入的是右对齐逻辑值。例如 EN_CHG 位于 bit 3,上层传 1,field_write() 自动计算:
●●●1 << 3 = 0x08然后 update_bits() 把 0x08 写入 bit 3,不影响其他位。

PG_STAT 到 ONLINEPG_STAT 字段定义●●●[F_BIT_PG_STAT] = {0x0C, 7, GENMASK(7, 7)};对应:
●●●reg = 0x0C
shift = 7
mask = 0x80Power Supply getter 读取:
●●●online = sgm41528_9_field_read(sgm, F_BIT_PG_STAT);
if (online < 0) {
dev_err(sgm->dev,
"%s: read PG_STAT failed, ret=%d\n",
__func__, online);
return online;
}POWER_SUPPLY_PROP_ONLINE 分支把非零值规范化成 1:
●●●case POWER_SUPPLY_PROP_ONLINE:
val->intval = !!online;
break;
上层充电管理器通过名称取得真实充电器 Power Supply,并读取 POWER_SUPPLY_PROP_ONLINE。
假设芯片 REG0C 返回:
●●●REG0C = 0xB0 = 10110000
mask = 0x80 = 10000000
shift = 7计算:
●●●(10110000 & 10000000) >> 7
= 10000000 >> 7
= 1因此 Linux ONLINE 返回 1。
PG_STAT 如何知道输入在线源码确认的是:驱动读取 REG0C.bit7 并把它作为 online。驱动本身没有根据电压做软件计算。
硬件机制推断:SGM41528_9 内部监测 VBUS 输入,在满足芯片 Power Good 条件时置位 PG_STAT,条件失效时清零。具体输入阈值、去抖时间和异常条件必须查 SGM41528_9 数据手册,当前仓库没有提供这些定义。
ONLINE 不等于“正在充电”PG_STAT 描述输入侧是否存在有效电源,CHRG_STAT 描述充电状态机当前阶段,两者语义不同。
●●●ONLINE :外部输入电源是否有效
CHRG_STAT :芯片当前是否未充电、涓流、预充、快充或终止即使 ONLINE=1,也可能由于软件关闭 EN_CHG、电池已满或保护条件而没有实际充电。
当前 STATUS 逻辑先判断 online,再解释 chg_stat:
●●●case POWER_SUPPLY_PROP_STATUS:
if (!online)
val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
elseif (chg_stat == 0)
val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
elseif (chg_stat == 1 || chg_stat == 2)
val->intval = POWER_SUPPLY_STATUS_CHARGING;
elseif (chg_stat == 3)
val->intval = POWER_SUPPLY_STATUS_FULL;
else
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
break;这说明“输入在线”和“充电阶段”必须分别读取,然后组合成上层状态。
EN_CHGEN_CHG 字段定义●●●[F_BIT_EN_CHG] = {0x06, 3, GENMASK(3, 3)};对应:
●●●reg = 0x06
shift = 3
mask = 0x08●●●staticint sgm41528_9_set_property(
struct power_supply *psy,
enum power_supply_property prop,
constunion power_supply_propval *val)
{
struct sgm41528_9_device *sgm =
power_supply_get_drvdata(psy);
int ret = -EINVAL;
switch (prop) {
case POWER_SUPPLY_PROP_CHARGING_ENABLED:
case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT:
ret = sgm41528_9_chg_enable_set(
sgm, !!val->intval);
break;
default:
return -EINVAL;
}
return ret;
}●●●ret = sgm41528_9_field_write(
sgm,
F_BIT_EN_CHG,
enable ? 1 : 0);
上层 tp_chg_manager 通过 POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT 请求开启或关闭真实充电器。
假设寄存器 0x06 原值是:
●●●old = 0x34 = 00110100开启充电:
●●●logical value = 1
aligned = 1 << 3 = 0x08
mask = 0x08
new = (0x34 & ~0x08) | (0x08 & 0x08)
= 0x3C关闭充电:
●●●logical value = 0
aligned = 0 << 3 = 0x00
new = (0x3C & ~0x08) | (0x00 & 0x08)
= 0x34整个过程只改变 bit 3。
当前驱动提供寄存器调试属性,可以从设备对应的 I2C sysfs 目录读取或写入寄存器。
读取路径使用带锁的 sgm41528_9_read_reg():
●●●sgm41528_9_read_reg(sgm, reg, &val);写入路径当前直接调用底层未加锁函数:
●●●__sgm41528_9_write_byte(sgm, (int)reg, val);设备路径通常可通过以下命令查找:
●●●find /sys -name sgm41529_all_regs 2>/dev/null
find /sys -name sgm41529_rw_reg 2>/dev/nullI2C 设备目录通常类似:
●●●/sys/bus/i2c/devices/1-006b/具体总线号和地址以设备实际枚举结果为准。
下面把 SGM 的思路提炼成更完整的通用模板。示例代码用于讲解设计,不代表已经修改当前驱动。
●●●staticint __chip_read_byte(struct chip_device *chip,
u8 reg, u8 *data)
{
s32 ret;
ret = i2c_smbus_read_byte_data(chip->client, reg);
if (ret < 0)
return ret;
*data = (u8)ret;
return0;
}
staticint __chip_write_byte(struct chip_device *chip,
u8 reg, u8 val)
{
return i2c_smbus_write_byte_data(chip->client, reg, val);
}●●●staticint chip_read_reg(struct chip_device *chip,
u8 reg, u8 *data)
{
int ret;
mutex_lock(&chip->io_lock);
ret = __chip_read_byte(chip, reg, data);
mutex_unlock(&chip->io_lock);
return ret;
}
staticint chip_write_reg(struct chip_device *chip,
u8 reg, u8 val)
{
int ret;
mutex_lock(&chip->io_lock);
ret = __chip_write_byte(chip, reg, val);
mutex_unlock(&chip->io_lock);
return ret;
}●●●staticint chip_update_bits(struct chip_device *chip,
u8 reg, u8 mask, u8 val)
{
int ret;
u8 old, new;
mutex_lock(&chip->io_lock);
ret = __chip_read_byte(chip, reg, &old);
if (ret < 0)
goto out;
new = (old & ~mask) | (val & mask);
if (new == old) {
ret = 0;
goto out;
}
ret = __chip_write_byte(chip, reg, new);
out:
mutex_unlock(&chip->io_lock);
return ret;
}这里增加 new == old 判断,可以在字段值没有变化时跳过 I2C 写入。是否需要该优化取决于芯片行为和项目要求。
●●●struct chip_field {
u8 reg;
u8 shift;
u8 mask;
};
staticint chip_field_read(struct chip_device *chip,
unsignedint field_id)
{
conststruct chip_field *field;
int ret;
u8 value;
if (field_id >= FIELD_MAX)
return -EINVAL;
field = &chip_fields[field_id];
ret = chip_read_reg(chip, field->reg, &value);
if (ret < 0)
return ret;
return (value & field->mask) >> field->shift;
}
staticint chip_field_write(struct chip_device *chip,
unsignedint field_id,
u8 value)
{
conststruct chip_field *field;
u8 max;
if (field_id >= FIELD_MAX)
return -EINVAL;
field = &chip_fields[field_id];
max = field->mask >> field->shift;
if (value > max)
return -EINVAL;
return chip_update_bits(
chip,
field->reg,
field->mask,
value << field->shift);
}read_reg() | ||
write_reg() | ||
update_bits() | ||
field_read() | ||
field_write() | ||
__read_byte()__write_byte() |
一般业务代码应优先使用字段接口;寄存器调试和芯片初始化可以使用完整寄存器接口;双下划线底层接口应限制在驱动内部受控路径。
SGM41528_9 的寄存器读写路径体现了 Linux I2C 寄存器型驱动的典型分层:
●●●最底层:i2c_smbus_read_byte_data / write_byte_data
-> 完成单字节总线访问
寄存器层:read_reg / update_bits
-> 提供互斥保护和读改写
字段层:field_read / field_write
-> 使用 reg、mask、shift 操作具体功能字段
业务层:get_property / set_property
-> 把硬件字段转换成 ONLINE、STATUS、EN_CHG 等系统语义
上层管理:tp_chg_manager
-> 周期读取状态并控制充电策略最重要的设计原则是:
1. 底层总线返回值和寄存器数据必须分开处理。
2. 运行期 I2C 访问需要统一的并发保护策略。
3. 读改写必须在同一锁范围内完成。
4. 修改单个字段时必须保留寄存器其他位。
5. 用字段表集中管理寄存器地址、掩码和位移。
6. 上层优先使用字段接口,而不是重复硬编码寄存器魔数。
7. 所有 I2C 错误必须逐层返回,不能只打印日志后假装成功。
8. 芯片状态编码和特殊位语义必须以数据手册为最终依据。
【往期推荐】