Linux regmap 子系统实战:在驱动中 dump PMIC 寄存器定位供电问题


文章目录

  • [Linux regmap 子系统实战:在驱动中 dump PMIC 寄存器定位供电问题](#Linux regmap 子系统实战:在驱动中 dump PMIC 寄存器定位供电问题)
    • [1. regmap 解决的不是"读寄存器",而是"统一读寄存器"](#1. regmap 解决的不是“读寄存器”,而是“统一读寄存器”)
    • [2. 一个 PMIC 的 regmap 是怎么来的](#2. 一个 PMIC 的 regmap 是怎么来的)
    • [3. 业务驱动怎么找到 PMIC 对应的 regmap](#3. 业务驱动怎么找到 PMIC 对应的 regmap)
    • [4. 用 regmap dump 一段 PMIC 寄存器窗口](#4. 用 regmap dump 一段 PMIC 寄存器窗口)
    • [5. 为什么不用 devmem 或自己封装 SPMI 读写](#5. 为什么不用 devmem 或自己封装 SPMI 读写)
    • [6. regmap_config 里哪些字段最容易影响结果](#6. regmap_config 里哪些字段最容易影响结果)
    • [7. 看到异常值后怎么继续排查](#7. 看到异常值后怎么继续排查)
    • [8. 常见坑](#8. 常见坑)
      • [把 bus device 当成 PMIC device](#把 bus device 当成 PMIC device)
      • [把 regmap name 当成稳定 ABI](#把 regmap name 当成稳定 ABI)
      • [忽略 `regmap_read()` 返回值](#忽略 regmap_read() 返回值)
      • [大范围扫 PMIC 寄存器](#大范围扫 PMIC 寄存器)
      • [忽略 cache 和 volatile](#忽略 cache 和 volatile)
      • [在 public blog 里泄露项目路径和芯片信息](#在 public blog 里泄露项目路径和芯片信息)
    • [9. 实战选择:遇到寄存器 dump 需求时怎么选](#9. 实战选择:遇到寄存器 dump 需求时怎么选)
    • 总结

Linux regmap 子系统实战:在驱动中 dump PMIC 寄存器定位供电问题

调驱动时,经常会遇到一种很难只靠日志解释的问题:某一路电源到底有没有打开?

比如板子上有一路供电叫 L6B。从原理图看,它来自某颗 PMIC;从现象看,外设偶尔不工作;从普通日志看,只能看到驱动 probe 了、接口调用了,却看不到 PMIC 内部寄存器到底是什么状态。最后真正能落锤的,往往是把这颗 PMIC 对应的寄存器 dump 出来,对照芯片手册看某个 enable/status bit。

这就是 regmap 很适合出场的地方。

本文不从 API 清单开始背,而是围绕一个真实工作流讲清楚:

  • 为什么 dump PMIC 寄存器要借助 regmap。
  • PMIC 原厂驱动通常怎么把 SPMI 访问封装成 struct regmap
  • 业务驱动怎么从设备树和 device 层级里找到这个 regmap。
  • 拿到 regmap 后,怎么安全地 dump 一段寄存器窗口。
  • 看到 0x800x00 这类值时,应该怎么分析。

1. regmap 解决的不是"读寄存器",而是"统一读寄存器"

先别急着看 regmap_read()。从第一性原理看,驱动访问硬件寄存器至少有三个问题:

问题 如果不用 regmap regmap 做了什么
总线差异 I2C、SPI、SPMI、MMIO 各写一套读写逻辑 用统一 API 隐藏底层 bus 操作
寄存器格式 地址位宽、值位宽、大小端、是否连续读写都要手动处理 struct regmap_config 描述寄存器格式
访问规则 哪些寄存器可读、可写、不可缓存、读了有副作用,要靠驱动自己约束 用 readable/writeable/volatile/precious 表和回调集中约束
性能与电源 慢速总线频繁读写成本高,休眠恢复后状态还要同步 可选 regcache,支持 cache-only、dirty、sync
调试 每个驱动自己做 dump 接口 regmap core/debugfs/trace 能提供统一观察点

所以 regmap 不是某一种总线驱动。它更像"寄存器访问适配层":

text 复制代码
驱动逻辑
  -> regmap API: regmap_read / regmap_write / regmap_update_bits / regmap_bulk_read
    -> regmap core: 锁、访问权限、缓存、格式化、trace/debugfs
      -> bus backend: i2c / spi / spmi / mmio / slimBus / sdw ...
        -> 硬件寄存器

这也是为什么 PMIC、Codec、Sensor、Clock、Pinctrl、PWM、MFD 里经常能看到 regmap。它们都有大量寄存器,而且寄存器访问经常跨模块复用。

2. 一个 PMIC 的 regmap 是怎么来的

在 PMIC 场景里,业务驱动通常不直接知道 SPMI 怎么访问寄存器。芯片原厂或平台侧 PMIC 驱动会先把底层 SPMI 访问封装成 regmap。

一个简化后的 PMIC regmap 初始化大概长这样:

c 复制代码
static const struct regmap_config pmic_regmap_config = {
	.reg_bits     = 16,
	.val_bits     = 8,
	.max_register = 0xffff,
	.fast_io      = true,
};

static int vendor_pmic_probe(struct spmi_device *sdev)
{
	struct regmap *map;
	unsigned int type;
	int ret;

	map = devm_regmap_init_spmi_ext(sdev, &pmic_regmap_config);
	if (IS_ERR(map))
		return PTR_ERR(map);

	ret = regmap_read(map, PMIC_TYPE, &type);
	if (ret)
		return ret;

	/* 后续把 map 挂到 device 上,子模块可以通过 dev_get_regmap() 取到 */
	return 0;
}

这里最关键的不是 regmap_read(),而是 devm_regmap_init_spmi_ext() 这一句。它把 struct spmi_device 背后的 SPMI 通信能力包装成统一的 regmap。

从这以后,上层模块不用关心:

  • SPMI 命令怎么发。
  • 寄存器地址怎么编码。
  • 每次读写走哪个 bus 回调。
  • 是否需要统一加锁。

上层只需要拿到 struct regmap *map,然后按寄存器地址读写。

在本地内核源码里也能看到同样的模式。include/linux/regmap.h 提供了 devm_regmap_init_i2c()devm_regmap_init_spi()devm_regmap_init_mmio()devm_regmap_init_spmi_ext() 等入口;drivers/base/regmap/regmap-i2c.c 里,I2C backend 会根据 reg_bitsval_bits 和 adapter capability 选择 SMBus byte、SMBus word 或普通 I2C transfer。

换句话说,regmap API 看起来统一,真正干活的是后面的 bus backend。

3. 业务驱动怎么找到 PMIC 对应的 regmap

问题来了:PMIC regmap 已经由原厂驱动注册好了,但你的业务驱动不一定直接持有那颗 PMIC 的 struct device。这时要先找到正确的 device,再从 device 上取 regmap。

原始案例里的思路是:

  1. 先明确要查哪路电,比如 L6B
  2. 根据原理图确定这路电来自哪颗 PMIC。
  3. 在设备树里找到这颗 PMIC 挂在哪条 bus 上,比如 spmi_bus 下的某个 USID。
  4. 在自己的驱动节点里引用这个 bus 或 PMIC 相关节点。
  5. 驱动 probe 时解析 phandle,得到起点 device。
  6. 从这个起点往 child device 遍历,找到 regmap name 匹配的那个 device。
  7. 对这个 device 调 dev_get_regmap()

设备树可以设计成下面这种公开可读的形式:

dts 复制代码
&spmi_bus {
	#address-cells = <2>;
	#size-cells = <0>;

	pmic_x: pmic@1 {
		compatible = "vendor,spmi-pmic";
		reg = <0x1 SPMI_USID>;
		#address-cells = <1>;
		#size-cells = <0>;
	};
};

my_driver_node {
	compatible = "example,my-driver";
	pmic-bus = <&spmi_bus>;
};

驱动里先拿到这个 phandle 对应的 device:

c 复制代码
static struct device *get_device_from_phandle(struct device *dev,
					      const char *name)
{
	struct device_node *np;
	struct platform_device *pdev;
	struct device *target;

	np = of_parse_phandle(dev->of_node, name, 0);
	if (!np)
		return NULL;

	pdev = of_find_device_by_node(np);
	of_node_put(np);
	if (!pdev)
		return NULL;

	/*
	 * of_find_device_by_node() returns a referenced platform_device.
	 * Take an explicit device reference for the caller, then drop the
	 * temporary reference. The caller must put_device(target).
	 */
	target = get_device(&pdev->dev);
	put_device(&pdev->dev);

	return target;
}

上面这段代码只表达思路,实际工程里要注意 device 生命周期:拿到的 target 用完后要 put_device(target)。很多时候更推荐直接引用具体 PMIC 子节点,或者使用平台/原厂已经提供的 lookup API。只有在没有更稳定入口时,才退而求其次遍历 device child。

遍历 child 查找 regmap 的逻辑可以写成这样:

c 复制代码
#define TARGET_REGMAP_NAME "0-01"

struct regmap_lookup_ctx {
	struct regmap *map;
};

static int match_child_regmap(struct device *dev, void *data)
{
	struct regmap_lookup_ctx *ctx = data;
	struct regmap *map;
	const char *name;

	map = dev_get_regmap(dev, NULL);
	if (!IS_ERR_OR_NULL(map)) {
		name = regmap_name(map);
		if (name && sysfs_streq(name, TARGET_REGMAP_NAME)) {
			ctx->map = map;
			return 1;
		}
	}

	return device_for_each_child(dev, ctx, match_child_regmap);
}

static struct regmap *find_regmap_under(struct device *parent)
{
	struct regmap_lookup_ctx ctx = { 0 };

	device_for_each_child(parent, &ctx, match_child_regmap);
	return ctx.map;
}

这里的 TARGET_REGMAP_NAME 要按平台实际情况来。案例里 0-01 同时是 device name 和 regmap name,所以可以用它判断是否找到了目标 PMIC 对应的 regmap。但这不是 Linux 统一保证的规则,而是该平台实现里的事实。公开文章里写代码时,最好把它当作"项目匹配条件",不要当作通用 ABI。

4. 用 regmap dump 一段 PMIC 寄存器窗口

拿到 struct regmap *map 后,最直接的 dump 方法就是按地址范围逐个读。

假设芯片手册里定义了几个 regulator 的寄存器窗口:

c 复制代码
#define L13B_ADDR_BASE 0xCD00
#define L12B_ADDR_BASE 0xCC00
#define L6B_ADDR_BASE  0xC600

如果关心 L6B,就从 0xC600 开始 dump 一段窗口:

c 复制代码
static void dump_regmap_window(struct regmap *map,
			       unsigned int base,
			       unsigned int len)
{
	unsigned int vals[0x100];
	unsigned int i;
	int ret;

	if (IS_ERR_OR_NULL(map)) {
		pr_info("regmap is invalid\n");
		return;
	}

	len = min_t(unsigned int, len, ARRAY_SIZE(vals));
	len = round_down(len, 8);
	if (!len)
		return;

	memset(vals, 0, sizeof(vals));

	for (i = 0; i < len; i++) {
		ret = regmap_read(map, base + i, &vals[i]);
		if (ret) {
			pr_info("%s: read %04x failed: %d\n",
				regmap_name(map), base + i, ret);
			vals[i] = 0xff;
		}
	}

	pr_info("%s: addr_base=%04x\n", regmap_name(map), base);

	for (i = 0; i < len; i += 8) {
		pr_info("%s: %04x:%02x %04x:%02x %04x:%02x %04x:%02x "
			"%04x:%02x %04x:%02x %04x:%02x %04x:%02x\n",
			regmap_name(map),
			base + i + 0, vals[i + 0],
			base + i + 1, vals[i + 1],
			base + i + 2, vals[i + 2],
			base + i + 3, vals[i + 3],
			base + i + 4, vals[i + 4],
			base + i + 5, vals[i + 5],
			base + i + 6, vals[i + 6],
			base + i + 7, vals[i + 7]);
	}
}

调用时:

c 复制代码
dump_regmap_window(client->pmic_map, L6B_ADDR_BASE, 0x100);

原始案例里关心的是 base + 0x46 这个寄存器。对 L6B_ADDR_BASE = 0xC600 来说,就是 0xC646

如果手册说明这个寄存器在正常打开时通常是 0x80,而异常时读到 0x00,这就比"驱动调用了 enable"更有说服力:

text 复制代码
0xC646 = 0x80  -> L6B 状态符合预期
0xC646 = 0x00  -> L6B 可能没有真正打开,继续查 regulator enable 路径

这里要注意一点:dump 的输出只是事实,不是结论。结论必须回到芯片手册。PMIC 寄存器常见同名字段很多,有的是 request bit,有的是 enable bit,有的是 status bit,还有的是 fault/ocp/uvlo 状态。不要只看到 0x80 就凭直觉判断。

5. 为什么不用 devmem 或自己封装 SPMI 读写

devmem 很适合快速验证 MMIO 寄存器,但 PMIC 挂在 SPMI/I2C/SPI 这类总线上时,情况完全不同:

  • 你访问的不是 CPU 物理地址,而是总线设备里的寄存器地址。
  • 访问时需要走控制器、协议、设备地址、命令格式。
  • PMIC 往往由 MFD/Regulator/IRQ/GPIO 等多个子模块共享。
  • 有些寄存器有读副作用,不能粗暴全量扫。

直接自己封装 SPMI 读写也可以,但通常不是最短路径。既然原厂 PMIC 驱动已经把通信能力注册成 regmap,再从 device 上拿到 map 并复用它,能少踩很多坑:

做法 适合场景 风险
devmem CPU MMIO 寄存器快速验证 不适合 SPMI/I2C/SPI PMIC 内部寄存器
自己写 bus read/write 平台没有 regmap 或在 bootloader/早期 bring-up 重复造协议层,容易绕过原驱动锁和状态管理
dev_get_regmap() 复用现有 map 原厂/平台驱动已经注册 regmap 需要正确找到 device,注意 regmap name 和访问范围
regmap debugfs 临时调试、已有 debugfs 节点 线上不可依赖,读 precious register 有风险

所以这个案例的关键路径不是"我会调用 regmap_read()",而是:

text 复制代码
原理图确定 PMIC
  -> 设备树确定 bus/device
  -> 从 phandle 找起点 device
  -> 遍历 child 找目标 regmap
  -> 按手册 dump 目标寄存器窗口
  -> 对照具体 bit 判断供电状态

6. regmap_config 里哪些字段最容易影响结果

struct regmap_config 决定了 regmap 怎么解释寄存器地址和值。常见字段可以按"最小必需"和"工程增强"分两类看。

最小必需字段:

c 复制代码
static const struct regmap_config example_regmap_config = {
	.reg_bits     = 16,
	.val_bits     = 8,
	.max_register = 0xffff,
};

这些字段回答三个基本问题:

  • 寄存器地址有多宽,比如 8 位、16 位、32 位。
  • 寄存器值有多宽,比如 8 位、16 位、32 位。
  • 最大合法寄存器地址是多少。

工程增强字段:

c 复制代码
static const struct regmap_config example_regmap_config = {
	.reg_bits          = 16,
	.val_bits          = 8,
	.max_register      = 0xffff,
	.rd_table          = &readable_regs,
	.wr_table          = &writeable_regs,
	.volatile_table    = &volatile_regs,
	.precious_table    = &precious_regs,
	.cache_type        = REGCACHE_RBTREE,
	.reg_format_endian = REGMAP_ENDIAN_BIG,
	.val_format_endian = REGMAP_ENDIAN_BIG,
};

这些字段决定调试可靠性:

字段 作用 调试时的影响
rd_table 限制哪些寄存器可读 dump 读到 -EIO/-EINVAL 时先看是否被访问表拦住
wr_table 限制哪些寄存器可写 防止误写只读/危险寄存器
volatile_table 标记不能缓存的寄存器 状态寄存器、计数器、IRQ 状态一般要 volatile
precious_table 标记不应随便读的寄存器 clear-on-read、FIFO、敏感状态不要全量 dump
cache_type 选择缓存实现 省慢速总线访问,但要处理休眠恢复同步
reg_format_endian 地址编码大小端 配错会导致访问错寄存器
val_format_endian 值编码大小端 配错会导致字节序看起来反了

PMIC dump 最容易踩的坑,是把缓存值当硬件实时值。状态寄存器必须被标成 volatile,或者在调试时明确绕过缓存。否则你看到的是 regcache 里的旧值,而不是 PMIC 当前状态。

7. 看到异常值后怎么继续排查

假设 L6B 对应状态寄存器正常应该是 0x80,现在读到 0x00,下一步不要马上改驱动。先把现象拆成几个可验证问题:

问题 观察点 下一步
PMIC regmap 找对了吗 regmap_name(map)、device name、PMIC USID 如果 name 不对,先修正 device 查找路径
读的是对的 register window 吗 base、offset、手册章节 如果 base 错,dump 另一路 regulator 只会误导
enable 请求是否发出 regulator consumer、PMIC enable register 对比 enable 前后 dump
状态 bit 是否需要延时 上电时序、settling time 在 enable 后延时再读,或者查 status/fault
是否被其他模块关掉 regulator constraints、runtime PM、suspend/resume 对比关键时间点 dump
是否读到了缓存旧值 regcache、volatile 配置 确认该状态寄存器是否 volatile
是否读了有副作用寄存器 clear-on-read、fault latch 缩小 dump 范围,不要扫未知窗口

我习惯把 dump 放在三个时间点:

text 复制代码
probe 后
  -> 外设请求电源前
  -> regulator enable 后
  -> 外设异常发生后

如果只有异常发生后的一次 dump,很容易不知道寄存器是"从来没打开",还是"打开过又被关了"。

8. 常见坑

把 bus device 当成 PMIC device

spmi_bus 是一个很好的遍历起点,但它不一定就是注册 regmap 的 device。原始案例里需要从 bus device 往 child 里找,直到 dev_get_regmap() 成功并且 name 匹配。

把 regmap name 当成稳定 ABI

0-01 这种名字在具体平台上很有用,但不要假设所有内核、所有 PMIC 都这么命名。能用明确 phandle 或平台 API 时,优先用明确入口。

忽略 regmap_read() 返回值

dump 代码里不要只收集 val,要检查返回值。访问表、总线错误、设备未 ready、地址越界都会让读失败。

大范围扫 PMIC 寄存器

PMIC 里可能有 clear-on-read、fault latch、FIFO、一次性状态等寄存器。没有手册确认前,不要把 0x00000xffff 全扫一遍。调试时优先 dump 目标 regulator 的窗口。

忽略 cache 和 volatile

如果状态寄存器没有被标记为 volatile,regmap 可能返回缓存值。电源状态、IRQ 状态、ADC/计数器类寄存器通常不应该缓存。

在 public blog 里泄露项目路径和芯片信息

真实项目里,设备树路径、PMIC 型号、客户项目名、原厂私有 compatible 都可能不适合公开。写文章时保留技术模式,替换成 vendor,spmi-pmicpmic@1L6B 这类匿名名字即可。

9. 实战选择:遇到寄存器 dump 需求时怎么选

场景 推荐路径
单个 MMIO 控制器,寄存器地址明确 先用驱动里的 readl() 或短期 devmem 验证
I2C/SPI/SPMI 外设已有内核驱动 优先找现成 struct regmap,使用 dev_get_regmap() 或子系统 API
PMIC/MFD 被多个子模块共享 不要绕过原厂驱动,复用它注册的 regmap
需要连续读传感器数据 regmap_bulk_read() 或 raw read,注意字节序
需要改寄存器某几个 bit regmap_update_bits(),避免读改写时误伤其他 bit
suspend/resume 后寄存器丢失 使用 regcache:regcache_mark_dirty()regcache_sync()
临时人工查看寄存器 看 debugfs 是否有 /sys/kernel/debug/regmap/.../registers

总结

这个 PMIC dump 案例背后的核心不是"写一个 for 循环读寄存器"。真正的主线是:

text 复制代码
硬件问题:某路供电是否真的打开
  -> 原理图:定位 PMIC 和 regulator
  -> 设备树:定位 SPMI bus / PMIC device
  -> Linux device model:找到注册 regmap 的 device
  -> regmap:用统一接口读取寄存器
  -> PMIC 手册:解释 bit 含义

regmap 把底层总线细节收起来,让驱动可以用统一 API 操作寄存器;但它不会替你判断硬件状态。0x800x00 这类值只有放回芯片手册、供电时序和 driver 调用链里,才有意义。

所以以后看到 PMIC、Codec、Sensor、MFD 里的寄存器问题,可以先问三件事:

  1. 这个设备有没有现成 regmap?
  2. 我能不能从稳定的 device 入口拿到它?
  3. 我要读的寄存器是否安全、实时、可解释?

这三个问题答清楚,dump 寄存器就不再是碰运气,而是一条可复用的调试路径。

相关推荐
计算机安禾3 小时前
【Linux从入门到精通】第35篇:容器化技术预备——Docker安装与基本概念
linux·运维·docker
子木HAPPY阳VIP3 小时前
信创UOS,Docker 完整操作部署(Dockerfile部署方式)&排错整合
linux·运维·redis·nginx·docker·容器·tomcat
瞎折腾啥啊3 小时前
vcpkg与CMake
linux·c++·cmake·cmakelists
AOwhisky3 小时前
Kubernetes调度与服务暴露:从“定时任务”到“服务发现”的完全指南
linux·运维·云原生·容器·kubernetes·服务发现
勤劳的进取家3 小时前
应用层基础
运维·网络·学习
hahaha 1hhh3 小时前
中文乱码 ubuntu autodl
linux·运维·前端
计算机安禾3 小时前
【Linux从入门到精通】第37篇:NFS网络文件系统——无状态的数据共享
linux·网络·php
图码3 小时前
矩阵数据结构入门指南:声明、初始化与基本操作
运维·数据结构·线性代数·算法·矩阵
暴力求解3 小时前
Linux---保存信号
linux·运维·服务器·开发语言·操作系统