4. Regmap读写操作:regmap_read/regmap_write,单次读写的高效用法

说实话,很多刚接触Regmap的工程师,上来就盯着批量读写、缓存同步这些高级功能。但我个人觉得,单次读写才是Regmap的基石。你想想看,一个驱动里90%以上的操作,其实都是单次读写。把这部分用好了,整个驱动的效率就稳了。

4.1 最基础的两个API

Regmap提供了两个最核心的单次读写接口:

// 读操作
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);

// 写操作
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);

这两个函数,参数结构非常清晰:

  • map:你初始化好的regmap句柄
  • reg:寄存器地址
  • val:读回来的值,或者要写入的值

返回值是0表示成功,负数表示出错。嗯,这里要注意,千万别忽略返回值检查。我在项目中遇到过好几次,因为没检查返回值,导致读回来的数据是错的,排查了半天才发现是I2C总线偶尔丢了一个ACK。

4.2 单次读写的内部机制

你可能好奇,这两个函数底层到底干了什么?我画了一张图,帮你理清流程:

regmap_read/regmap_write 内部调用流程 驱动调用 API 格式转换 (format) 总线传输 (I2C/SPI/MMIO) 硬件寄存器 关键点 • 自动处理字节序 • 自动处理位宽 • 自动加锁保护 • 支持缓存(可选) • 支持重试机制 • 错误统一处理 你只需要关心: reg 和 val 其他交给Regmap

说白了,Regmap帮你把底层的脏活累活全包了。你只需要告诉它「读哪个地址」或者「写哪个地址、什么值」,剩下的字节序转换、总线协议、错误重试,它都帮你处理好了。

4.3 高效使用的几个习惯

我做了这么多年驱动,总结了几条单次读写的高效用法:

✅ 习惯一:批量读取用循环,但别太频繁

如果你要连续读多个寄存器,很多人会这么写:

for (i = 0; i < 10; i++) {
    regmap_read(map, base_reg + i, &val[i]);
}

这样写没问题,但效率不高。每次读都要加锁、格式转换、总线传输。我建议:如果寄存器地址连续,用regmap_bulk_read代替。但如果地址不连续,或者只有两三个寄存器,单次读反而更清晰。

💡 习惯二:写操作尽量合并

我曾经调试一个音频驱动,发现每次设置音量都要写三个寄存器。一开始我分开写:

regmap_write(map, REG_VOL_L, vol_l);
regmap_write(map, REG_VOL_R, vol_r);
regmap_write(map, REG_VOL_UPDATE, 1);

后来发现这样会导致中间状态被硬件读到,产生pop噪声。改成一次批量写就解决了。所以,如果多个寄存器需要原子更新,考虑用regmap_multi_reg_write。

⚠️ 习惯三:读回验证是个好习惯

写操作之后,要不要立刻读回来验证?我的经验是:关键寄存器一定要验证。比如配置时钟分频、电源模式这些,写错了系统可能直接挂掉。

regmap_write(map, REG_CLK_DIV, 0x05);
regmap_read(map, REG_CLK_DIV, &val);
if (val != 0x05) {
    dev_err(dev, "时钟分频配置失败!\n");
    return -EIO;
}

但普通的数据寄存器,比如FIFO数据、状态寄存器,就没必要每次都验证了,浪费性能。

4.4 实际项目中的代码模板

给你看一个我实际项目里用的代码片段,一个温度传感器的驱动:

static int temp_sensor_read_temp(struct device *dev, int *temp)
{
    struct regmap *map = dev_get_regmap(dev, NULL);
    unsigned int raw;
    int ret;

    // 第一步:触发转换
    ret = regmap_write(map, REG_CTRL, 0x01);
    if (ret) {
        dev_err(dev, "触发转换失败: %d\n", ret);
        return ret;
    }

    // 第二步:等待转换完成(实际项目用中断或轮询)
    msleep(10);

    // 第三步:读取温度值
    ret = regmap_read(map, REG_TEMP, &raw);
    if (ret) {
        dev_err(dev, "读取温度失败: %d\n", ret);
        return ret;
    }

    // 第四步:格式转换
    *temp = (int)(raw * 0.0625);
    return 0;
}

你看,整个流程非常清晰。每个regmap_read/regmap_write都检查了返回值,出错了立刻打印日志。这就是我强调的防御性编程。

4.5 性能对比:单次读写 vs 批量读写

为了让你有个直观感受,我整理了一个对比表:

操作类型 适用场景 性能 代码可读性
regmap_read/write 单次、非连续地址 中等(每次加锁) ⭐⭐⭐⭐⭐
regmap_bulk_read/write 连续地址、大量数据 高(一次加锁) ⭐⭐⭐⭐
regmap_multi_reg_write 多个非连续地址、原子操作 高(一次加锁) ⭐⭐⭐

说白了,单次读写是万金油,什么场景都能用。但如果你追求极致性能,或者需要原子操作,那就得用批量接口。我个人习惯是:先写单次读写让功能跑通,性能瓶颈时再优化成批量。别一开始就搞复杂了。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 寄存器地址别搞错:我曾经把16位地址写成了8位,结果读回来的数据全是错的。检查了三天才发现是regmap配置里reg_bits设错了。
  • 注意volatile寄存器:有些寄存器读一次就会清掉状态位。如果你连续读两次,第二次读到的就是0。这种情况要小心。
  • 不要在中断上下文用regmap_read/write:除非你配置了IRQ-safe的regmap。否则可能会死锁。我见过有人这么干,结果系统随机卡死。
  • val的类型是unsigned int:别传int*进去,虽然编译能过,但符号扩展会坑死你。

嗯,单次读写就讲这么多。记住一句话:简单的事情做到极致,就是高手。regmap_read和regmap_write虽然简单,但用好了,你的驱动就会既稳定又高效。


可可精品资料库