1. 为什么需要regmap从一次I2C传感器驱动的重复劳动说起做过Linux驱动开发的人大概都有过这样的经历接手一个I2C温度传感器驱动打开芯片手册一看寄存器地址是8位的数据宽度也是8位读写时序就是标准的I2C传输。写完一版能跑测试通过提交代码。然后下一个项目换了一颗SPI接口的传感器寄存器地址变成16位数据宽度有8位也有16位还带页切换机制。你不得不把之前I2C那套读写函数重新写一遍只是把传输层从i2c_transfer换成spi_sync再加上地址拼接的逻辑。再后来遇到一颗通过SPI访问但寄存器布局和I2C版本完全一致的芯片你心里想的是这些代码明明逻辑一样为什么不能复用这就是regmap框架要解决的核心问题。regmap是Linux内核提供的一套寄存器访问抽象层它把“怎么读写寄存器”这件事从具体的总线协议中剥离出来让驱动开发者只需要关心“读哪个寄存器、写什么值”而不需要关心底层是I2C、SPI还是MMIO。你可以把它理解成一个翻译官驱动层说“我要读地址0x10的值”regmap负责根据配置决定是用I2C发一个写地址再读数据还是用SPI发一个带地址的命令帧或者是直接读内存映射的某个偏移。这个框架最早在2011年左右进入内核主线当时的主要动机是音频编解码器驱动。这类芯片通常有几百个寄存器而且同一颗芯片可能同时提供I2C和SPI两种接口封装如果每个驱动都自己实现一套寄存器读写代码量会非常夸张。regmap的出现让驱动代码量大幅缩减同时把缓存、锁、调试接口这些通用逻辑统一收口减少了重复造轮子的情况。适合阅读这篇文章的读者包括正在学习Linux驱动开发、已经写过字符设备驱动但还没接触过regmap的初学者正在维护老式驱动、想迁移到regmap框架的中级开发者以及对内核子系统设计思路感兴趣、想了解抽象层如何落地的工程师。文章会从设计思路讲到实操细节再结合我实际调试中踩过的坑给出可以直接参考的代码片段和排查方法。2. regmap框架的整体设计与核心思路拆解2.1 分层设计把“总线”和“寄存器操作”彻底分开regmap的架构可以分成三层来看。最底层是总线后端负责实际的物理传输比如I2C的i2c_transfer、SPI的spi_sync、MMIO的readl/writel。中间层是regmap核心它维护寄存器缓存、读写锁、格式化配置、访问权限检查等通用逻辑。最上层是驱动接口提供regmap_read、regmap_write、regmap_update_bits这些API给具体驱动调用。这种分层的价值在于当你从I2C切换到SPI时只需要在初始化时把bus_type从regmap_bus_i2c换成regmap_bus_spi上层的读写代码一行都不用改。我试过把一颗音频编解码器的驱动从I2C迁移到SPI整个迁移过程只改了probe函数里regmap_init的那几行其他几百行寄存器操作代码完全复用实测下来很稳。2.2 配置描述用结构体告诉regmap“这颗芯片长什么样”regmap的核心配置放在struct regmap_config里这个结构体有几十个字段但常用的就那么几个。最关键的是reg_bits和val_bits分别表示寄存器地址位宽和数据位宽。比如一颗典型的I2C传感器reg_bits8val_bits8一颗SPI接口的以太网PHYreg_bits16val_bits16有些芯片寄存器地址是16位但数据是8位那就reg_bits16val_bits8。另一个重要字段是max_register它告诉regmap这颗芯片的寄存器地址范围。这个值不是随便填的它直接影响debugfs里寄存器dump的范围也影响缓存分配的大小。如果你填得比实际大很多会浪费内存填小了访问超出范围的寄存器会直接报错。我一般会对照芯片手册的寄存器映射表取最大有效地址再加一。cache_type决定是否启用寄存器缓存。对于读写频繁但值不常变的寄存器启用缓存可以大幅减少总线访问次数。但要注意有些寄存器是“读清零”或“写清零”的这种就不能缓存否则读到的值会是旧值。volatile_reg回调就是用来标记这些特殊寄存器的。2.3 缓存机制为什么它能让音频驱动性能提升明显regmap的缓存分三种模式REGCACHE_NONE、REGCACHE_RBTREE、REGCACHE_FLAT。NONE就是不缓存每次读写都走总线。RBTREE用红黑树存储适合寄存器地址稀疏的场景。FLAT用数组直接索引适合地址连续且范围不大的场景。缓存带来的好处在音频场景特别明显。音频编解码器的寄存器通常有几百个播放时驱动会频繁读取音量、采样率、通道映射等寄存器。如果每次都走I2C总线负载会很高而且I2C本身速率有限。启用缓存后读操作直接命中内存只有写操作才同步到硬件实测下来CPU占用和总线占用都能降一个数量级。但缓存也带来了一致性问题。如果硬件自己会修改某个寄存器的值比如状态寄存器而驱动读的是缓存就会读到过期的数据。解决办法是在volatile_reg回调里返回true告诉regmap这个寄存器不缓存每次都从硬件读。我踩过的坑是一颗电源管理芯片的故障状态寄存器被硬件自动置位但驱动读缓存一直读到0排查了半天才发现是缓存没排除这个寄存器。2.4 锁与并发为什么regmap能保证多线程安全regmap内部有两把锁lock和cache_lock。lock保护总线访问确保同一时刻只有一个传输在进行。cache_lock保护缓存数据结构的完整性。对于SPI这种不支持多主机的总线regmap默认用自旋锁对于I2C可以用互斥锁。如果你的驱动在中断上下文里也要访问寄存器就需要配置fast_io为true这样regmap会用自旋锁而不是可能睡眠的互斥锁。这个设计解决了一个常见问题多个线程同时调用regmap_update_bits修改同一个寄存器的不同位域。如果没有锁读-改-写的过程可能交错导致一个线程的修改被另一个线程覆盖。regmap内部把update_bits实现为“加锁-读-改-写-解锁”的原子操作保证了位域修改的安全性。3. 核心细节解析与实操要点3.1 regmap_config关键字段逐项说明先看一个典型的I2C设备配置static const struct regmap_config sensor_regmap_config { .reg_bits 8, .val_bits 8, .max_register 0x7F, .cache_type REGCACHE_RBTREE, .volatile_reg sensor_volatile_reg, .readable_reg sensor_readable_reg, .writeable_reg sensor_writeable_reg, };reg_bits8表示寄存器地址是8位I2C传输时第一个字节就是地址。val_bits8表示数据是8位。max_register0x7F说明有效地址范围是0x00到0x7F。cache_typeREGCACHE_RBTREE启用红黑树缓存。三个回调函数分别控制哪些寄存器是易变的、可读的、可写的。对于SPI设备配置会有所不同static const struct regmap_config spi_phy_regmap_config { .reg_bits 16, .val_bits 16, .max_register 0x1F, .cache_type REGCACHE_FLAT, .reg_format_endian REGMAP_ENDIAN_BIG, .val_format_endian REGMAP_ENDIAN_BIG, };这里reg_bits16和val_bits16表示地址和数据都是16位。reg_format_endian和val_format_endian指定字节序很多SPI芯片要求大端传输如果配错了读出来的值会字节颠倒。cache_typeREGCACHE_FLAT因为地址范围小且连续用数组缓存效率更高。注意reg_format_endian和val_format_endian的默认值都是REGMAP_ENDIAN_DEFAULT对于I2C通常是little endian对于SPI取决于具体芯片。配错字节序是新手最常见的错误之一现象是读到的值看起来“差一个字节”。3.2 初始化流程从总线设备到regmap句柄I2C设备的初始化通常长这样static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct sensor_data *data; data devm_kzalloc(client-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >data-regmap devm_regmap_init_mmio(pdev-dev, base, mmio_regmap_config);base是通过platform_get_resource和devm_ioremap_resource得到的虚拟地址。MMIO的regmap_config通常不需要指定reg_bits和val_bits因为默认就是32位。3.3 读写APIregmap_read/write/update_bits的正确用法最基本的读操作unsigned int val; int ret; ret regmap_read(data-regmap, SENSOR_REG_CONFIG, val); if (ret) { dev_err(dev, read config failed: %d\n, ret); return ret; } dev_dbg(dev, config 0x%02x\n, val);写操作ret regmap_write(data-regmap, SENSOR_REG_CONFIG, 0x55);位域修改是驱动里用得最多的ret regmap_update_bits(data-regmap, SENSOR_REG_CTRL, SENSOR_CTRL_ENABLE | SENSOR_CTRL_MODE_MASK, SENSOR_CTRL_ENABLE | SENSOR_CTRL_MODE_FAST);update_bits的第一个参数是寄存器地址第二个是掩码哪些位要改第三个是值改成什么。它内部会先读寄存器清除掩码对应的位再或上新的值最后写回。整个过程在锁保护下完成不会和其他线程的修改冲突。批量读写用regmap_bulk_read和regmap_bulk_writeu8 buf[4]; ret regmap_bulk_read(data-regmap, SENSOR_REG_DATA, buf, 4);批量操作对于FIFO读取特别有用一次传输读多个寄存器比循环调用regmap_read效率高得多。提示regmap_update_bits的掩码和值必须匹配如果值里有掩码之外的位那些位会被忽略。我见过有人把掩码写成0xFF但只想改低4位结果高4位也被清零了这种错误在调试时很难发现因为寄存器读出来“看起来正常”。3.4 回调函数volatile/readable/writeable的取舍volatile_reg回调决定一个寄存器是否每次都从硬件读。对于状态寄存器、中断标志寄存器、FIFO数据寄存器必须返回true。对于配置寄存器返回false让缓存生效。static bool sensor_volatile_reg(struct device *dev, unsigned int reg) { switch (reg) { case SENSOR_REG_STATUS: case SENSOR_REG_INT_FLAG: case SENSOR_REG_FIFO_DATA: return true; default: return false; } }readable_reg和writeable_reg用于权限检查。如果驱动试图读一个只写寄存器regmap会直接返回错误而不是发出一个无效的总线传输。这个功能在调试时很有用能快速定位到“为什么读出来是0”的问题。static bool sensor_writeable_reg(struct device *dev, unsigned int reg) { switch (reg) { case SENSOR_REG_CONFIG: case SENSOR_REG_CTRL: return true; default: return false; } }注意如果不实现这些回调regmap默认所有寄存器都可读可写。对于有保留地址的芯片建议至少实现readable_reg和writeable_reg避免访问到未定义区域导致总线错误。4. 实操过程与核心环节实现4.1 从零搭建一个regmap驱动的完整流程假设我们要为一颗I2C接口的三轴加速度计写驱动芯片有8位寄存器地址和8位数据宽度寄存器映射如下地址名称属性说明0x00WHO_AM_I只读设备ID固定0x330x10CTRL_REG1读写控制寄存器10x11CTRL_REG2读写控制寄存器20x20STATUS只读状态寄存器0x28OUT_X_L只读X轴低字节0x29OUT_X_H只读X轴高字节0x2AOUT_Y_L只读Y轴低字节0x2BOUT_Y_H只读Y轴高字节0x2COUT_Z_L只读Z轴低字节0x2DOUT_Z_H只读Z轴高字节第一步定义regmap_configstatic const struct regmap_config accel_regmap_config { .reg_bits 8, .val_bits 8, .max_register 0x2D, .cache_type REGCACHE_RBTREE, .volatile_reg accel_volatile_reg, .readable_reg accel_readable_reg, .writeable_reg accel_writeable_reg, };第二步实现回调static bool accel_volatile_reg(struct device *dev, unsigned int reg) { switch (reg) { case 0x20: /* STATUS */ case 0x28 ... 0x2D: /* OUT_X_L ... OUT_Z_H */ return true; default: return false; } } static bool accel_readable_reg(struct device *dev, unsigned int reg) { if (reg 0x2D) return true; return false; } static bool accel_writeable_reg(struct device *dev, unsigned int reg) { switch (reg) { case 0x10: case 0x11: return true; default: return false; } }第三步probe函数里初始化regmap并验证设备IDstatic int accel_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct accel_data *data; unsigned int val; int ret; data devm_kzalloc(client-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int accel_read_xyz(struct accel_data *data, s16 *x, s16 *y, s16 *z) { u8 buf[6]; int ret; ret regmap_bulk_read(data-regmap, 0x28, buf, 6); if (ret) return ret; *x (s16)((buf[1] 8) | buf[0]); *y (s16)((buf[3] 8) | buf[2]); *z (s16)((buf[5] 8) | buf[4]); return 0; }这里用regmap_bulk_read一次读6个字节比调用6次regmap_read效率高。注意加速度数据是低字节在前所以拼接时要先取buf[1]再取buf[0]。4.2 参数计算max_register和缓存大小的关系max_register不仅影响访问范围还影响缓存分配。对于REGCACHE_FLAT模式regmap会分配(max_register 1) * val_bytes字节的数组。如果max_register0x2Dval_bits8那就是46字节很小。但如果是一颗有65536个寄存器的芯片max_register0xFFFFval_bits16缓存就是128KB这就需要考虑内存占用了。对于REGCACHE_RBTREE模式缓存是动态分配的只存储实际访问过的寄存器内存占用和访问模式有关。如果驱动只访问几十个寄存器RBTREE更省内存。但如果寄存器地址连续且访问频繁FLAT的查找效率更高因为直接数组索引是O(1)而RBTREE是O(log n)。我一般这样选地址范围小于256且连续用FLAT地址范围大或稀疏用RBTREE不需要缓存用NONE。4.3 debugfs不写代码就能查看寄存器状态regmap自带debugfs支持挂载debugfs后可以在/sys/kernel/debug/regmap/下看到每个regmap实例的目录。目录名通常是设备名比如1-0068表示I2C总线1上地址0x68的设备。进入目录后有几个文件registers以十六进制dump所有寄存器的当前值包括缓存和硬件access记录最近一次读写操作nameregmap的名字直接cat registers就能看到寄存器状态cat /sys/kernel/debug/regmap/1-0068/registers输出格式是每行一个寄存器地址: 值。这个功能在调试时非常有用不需要在驱动里加printk就能看到寄存器实际值。我经常用它来验证缓存是否生效先读一次寄存器然后cat registers看缓存值再直接读硬件对比。提示debugfs默认可能没有挂载需要先执行mount -t debugfs none /sys/kernel/debug。另外regmap的debugfs节点需要内核配置CONFIG_DEBUG_FS和CONFIG_REGMAP。4.4 中断上下文中的regmap访问如果驱动在中断处理函数里要读寄存器必须确保regmap的锁不会睡眠。对于I2C默认的锁是互斥锁在中断上下文里用会触发“sleeping function called from invalid context”警告。解决办法是在regmap_config里设置.fast_io true这样regmap会用自旋锁。但要注意fast_io只改变锁的类型不改变总线传输本身。如果I2C传输函数会睡眠大多数情况都会在中断上下文里调用仍然有问题。正确的做法是在中断里只做最小的工作比如记录中断标志然后通过工作队列或线程化中断去读寄存器。static irqreturn_t accel_irq_handler(int irq, void *dev_id) { struct accel_data *data dev_id; /* 只记录中断发生不直接读寄存器 */ >
