公司动态
SPMI总线调试实战:从spmi.rar解开PMIC寄存器读写之谜
简介本资源是一份面向嵌入式Linux驱动开发者的SPMISystem Power Management Interface接口核心实现代码包聚焦于移动与低功耗设备的电源管理驱动开发场景解决SPMI设备动态注册、通信初始化及硬件协同控制等关键问题。压缩包为RAR格式共含2个文件1个C源文件与1个头文件总大小仅6KB精炼紧凑spmi.c实现设备分配spmi_device_alloc、注册device_add、消息传输及中断处理等核心逻辑spmi.h则定义spmi_device结构体、函数原型与协议常量支撑驱动模块化集成与跨组件调用。目前已有267人学习下载适合具备Linux内核驱动基础的中高级开发者快速理解SPMI框架设计思想掌握从设备实例构建到系统级挂载的完整流程并可直接参考其轻量级实现用于SoC平台电源管理模块开发或教学演示。 做嵌入式底层开发的朋友多半经历过这种场景从老同事手里或者某个共享目录里翻到一个命名特别随意的压缩包。我这次翻出来的就是spmi.rar解压之后的目录名也直接叫spmi。乍一看不起眼但里面装的东西是围绕SPMISystem Power Management Interface系统电源管理接口这套协议的一整套调试工具、内核驱动补丁和平台配置说明。对于长期跟 PMIC、电池、充电这些模块打交道的驱动工程师来说这类包比很多所谓的学习资料都值钱——因为它是可以直接在板子上跑起来、能帮你定位问题的东西。这篇文章我就拿这个spmi.rar说事把它里面的家底拆给大家看。内容会覆盖 SPMI 协议本身是干什么的、压缩包里的代码结构、怎么编译部署到目标板以及我在实际调 PMIC 通信时踩过的几个坑。如果你手里也有一台跑着 Linux 的移动设备或开发板想搞明白PMIC 是怎么被主控读写的那这篇应该对你有用。1. 解压之前SPMI 到底是哪根总线为什么值得单独打包1.1 从 I2C 到 SPMI移动设备电源管理面临的通信瓶颈SPMI 这个缩写全称是 System Power Management Interface由 MIPI 联盟制定专门用于应用处理器AP和电源管理芯片PMIC之间的通信。在老方案里AP 和 PMIC 之间的通信大多走 I2C 或者 SPI但实际用下来问题不少。I2C 的标准模式只有 100kbit/s快速模式 400kbit/s即便走到高速模式 3.4Mbit/s在多设备共享一条总线的情况下也容易被拉垮。而且 I2C 是开漏结构靠上拉电阻把信号拉高上升沿天生就比较慢频率一高信号就糊了。更关键的是I2C 协议本身没有一个专门为电源管理设计的事务模型每次读写都要穿插 start、stop、设备地址、寄存器地址这些组合效率不高。唤醒 PMIC、动态调压这种高频率操作用 I2C 做就是又慢又别扭。SPMI 就是冲着这些问题来的。它同样是两线制一条 CLK、一条 DATA但物理层用的是推挽输出信号质量和上升沿比 I2C 好很多通讯速率可以提到几十 MHz。同时SPMI 协议里定义了大量专用的事务类型包括单寄存器读写、扩展寄存器读写、块读写、长读长写还有 sleep 和 wakeup 命令。这些操作针对寄存器级访问做了优化在电源管理这种高频小数据的场景下效率远高于 I2C。打个比方I2C 就像是小区门口那种公共快递柜所有业主共用一个柜子存取都要排队高峰期还容易堵SPMI 则是专门给电源管理修的一条业务专线通道独立、专用优先而且每种操作都设计好了固定格式不用每次现拼。1.2 哪些设备在依赖 SPMI 工作现在市面上绝大多数手机 SoC 方案AP 和 PMIC 之间都是走 SPMI。不只是 PMIC充电管理芯片、电量计Fuel Gauge、无线充电控制器、USB PD 控制芯片都有不少是通过 SPMI 挂在主控下的。也就是说凡是和电相关的关键器件很多都离不开这条总线。在 Linux 内核里SPMI 被抽象成一组总线驱动代码在drivers/spmi/目录下。内核里有spmi_device、spmi_driver、spmi_controller这些结构体用法和 I2C 子系统非常像。一个完整的 SPMI 系统由三部分组成SPMI 控制器Controller一般集成在 SoC 内部负责物理层时序和总线仲裁比如高通的 PMIC Arbiter。SPMI 总线本身物理上是两根线连接控制器和各个从设备。SPMI 从设备SlavePMIC 等外设每个从设备都有一个唯一的从机地址USIDUser Slave ID。所以你会看到设备树里配置 PMIC 节点的时候reg属性第一个字段就是它的 USID。比如spmi_bus { pmic0 { compatible qcom,pm8150; reg 0x0 SPMI_USID; }; };这里的0x0 SPMI_USID指的就是从设备地址SPMI_USID是一个宏通常表示主 ID。地址对不上后续所有通信都会失败——后面我会专门讲这个坑。2. 拆包体检spmi.rar 的项目结构与核心代码2.1 解压后看到的文件布局我先说下我拿到的这个包解压之后长什么样目录结构大概是这样的spmi/ ├── drivers/ │ └── spmi/ │ ├── spmi.c │ ├── spmi-drv.c │ ├── spmi-pmic-arb.c │ └── Kconfig ├── tools/ │ ├── spmi_cmd.c │ ├── spmi_dump.c │ └── Makefile ├── dts/ │ └── spmi-bus.dtsi ├── docs/ │ ├── SPMI_spec_notes.md │ ├── PMIC寄存器调试手册.md │ └── 调试命令速查.md └── README.md不同来源的包可能略有差异但主体基本就是三块内核驱动、用户态工具、平台配置。其中 README 里一般会写清楚编译方法、加载方式和最常用的命令建议拿到包先看这个文件。我翻到的这份 README 写得很实在直接给了一句话总结如果你只需要做一件事那就是用 spmi_cmd 读写 PMIC 寄存器。2.2 内核总线驱动这一层先看内核驱动部分。spmi.c是 SPMI 子系统的核心相当于 I2C 子系统的i2c-core。它注册了spmi_bus_type提供spmi_register_driver、spmi_device_add、spmi_controller_add这些接口。所有挂在总线上的 SPMI 设备生命周期管理都走这一层。spmi-drv.c是驱动模型的具体封装层。它定义了两个重要的接口方向一边是控制器侧控制器驱动调用spmi_controller_alloc分配一个控制器再通过spmi_controller_add注册到总线上然后提供read/write回调函数。另一边是从设备侧从设备驱动调用spmi_driver_register注册自身等到设备树里的节点和驱动兼容字符串匹配成功就触发 probe。实际读写的时候spmi_regread/spmi_regwrite会走控制器回调最终变成物理总线上的时序。也就是说应用层看到的是一次读寄存器内核底层其实经历了两级spmi-core根据地址找到控制器和从设备然后控制器驱动负责把命令发到 CLK/DATA 两根线上。spmi-pmic-arb.c则是某类 SoC 平台上的控制器驱动很多常见方案用的就是它。它做的事情简单说就是把上层发下来的从机地址、寄存器地址翻译成硬件寄存器操作因为该平台的 SPMI 控制器本质上是内存映射的一组寄存器。2.3 用户态调试工具包里真正让我觉得值钱的其实是tools/下面的两个小工具。spmi_cmd是一个命令行读写工具用法很直观# 读从机地址 0 的 0x0000 寄存器一字节 spmi_cmd r 0x00 0x0000 # 写从机地址 0 的 0x0010 寄存器为 0x5A spmi_cmd w 0x00 0x0010 0x5A # 块读连续读 16 字节 spmi_cmd rd 0x00 0x0000 16spmi_dump更粗暴直接把一段寄存器区整块 dump 出来spmi_dump 0x00 0x0000 0x100它会从指定地址开始连续读 256 字节以十六进制表格式打印。这个工具查 PMIC 初始化状态特别方便一下就能看出哪些寄存器是默认值、哪些被 bootloader 改过。2.4 设备树与平台配置dts/spmi-bus.dtsi里定义的是 SPMI 总线和挂在其下的 PMIC 节点。这里最容易出错的是#address-cells和#size-cells。因为 SPMI 节点的地址由两个字段组成第一个是 slave IDUSID第二个是设备内部基址或预留字段所以通常要写成spmi_bus { #address-cells 0x2; #size-cells 0x0; pmic0 { compatible qcom,pm8150; reg 0x0 SPMI_USID; #address-cells 0x2; #size-cells 0x0; }; pmic1 { compatible qcom,pm8150c; reg 0x1 SPMI_USID; }; };如果你把#address-cells错写成 1总线扫描时地址解析必然出错从设备可能根本扫描不出来。这种问题往往是看起来配置了实际上没生效我在第四部分会展开讲。3. 编译部署让这套工具在目标板上跑起来3.1 交叉编译环境准备要跑这套工具得先准备目标平台的交叉编译工具链。如果你用高通的 SDK 或者某板厂发布的 BSP里面一般自带工具链路径通常在prebuilt/linux-x86/toolchain/下面。如果没有用通用的aarch64-linux-gnu-工具链也能编只要内核版本和 glibc 兼容。编译前先确认两件事一是目标板系统是不是 64 位 ARM二是用户态程序的动态库依赖。如果板子文件系统里缺 libc 的某个版本最省事的做法是静态编译make CROSS_COMPILEaarch64-linux-gnu- LDFLAGS-static我一般习惯直接把工具静态编译出来拷贝到板子上省得和板子的运行库较劲。3.2 内核配置与编译如果内核里还没开 SPMI 支持需要先配置。内核源码目录下执行make menuconfig找到以下路径Device Drivers --- [*] SPMI (System Power Management Interface) --- * SPMI support * SPMI controller driver (for your platform)对应的配置项一般是CONFIG_SPMIy和CONFIG_SPMI_PMIC_ARBy如果只是调试也可以编译成模块m。保存后重新编译内核和内核模块。这里有一个经验如果你只是想用包里的用户态工具内核里的 SPMI 总线支持必须编译进去否则/sys/bus/spmi/目录都不会出现。控制器驱动可以按需选择但如果没有对应的控制器驱动设备树里的控制器节点不会 probe也就无法访问任何从设备。3.3 用户态工具编译进到tools/目录正常情况下直接执行make CROSS_COMPILEaarch64-linux-gnu-就能编出spmi_cmd和spmi_dump。如果包自带的 Makefile 写得比较简陋也可以手动编译aarch64-linux-gnu-gcc -static -o spmi_cmd spmi_cmd.c aarch64-linux-gnu-gcc -static -o spmi_dump spmi_dump.c编完之后通过 adb push 或者 scp 拷贝到目标板上建议放/usr/local/bin/方便直接执行。3.4 上板验证部署好后先看内核里 SPMI 设备有没有被正确识别ls /sys/bus/spmi/devices/正常情况下会看到类似0-00000、1-00000这样的设备目录前缀就是从机 USID 号。如果这里空空如也就是前面的设备树或控制器驱动有问题先去排查配置。确认设备存在后试着读一个安全寄存器比如某个 PMIC 的版本号寄存器spmi_cmd r 0x00 0x0000能读到数据就说明整条链路已经通了。但通了只是开始接下来真正让人头大的问题往往在后面的调试中冒出来。4. 实测踩坑读不到 PMIC 寄存器值的完整排查链路4.1 现象工具返回超时PMIC 没有任何数据我一开始在板子上跑spmi_cmd r 0x00 0x0000结果直接返回spmi read failed: -110-110 是ETIMEDOUT意思是对端没有响应。我当时的第一反应是先怀疑地址写错了检查了几遍没问题后开始认真排查。这个过程比较典型我把完整链路写出来建议大家以后遇到读写不到 PMIC的问题也按这个顺序走一遍。4.2 排查链路的完整过程第一步查 dmesg 里跟 spmi 相关的日志dmesg | grep -i spmi看控制器有没有成功注册设备树节点有没有解析有没有报 interrupt、resource 之类的错误。第二步看/sys/bus/spmi/devices/里有哪些设备。如果设备目录存在说明从设备已经被扫描注册了如果不存在问题大概率出在设备树或控制器驱动上。第三步验证 debugfs 节点。很多平台在/sys/kernel/debug/spmi/下会有一组控制接口可以直接读原始寄存器。用法类似cat /sys/kernel/debug/spmi/spmi-0/0x00/0x0000如果这个节点也超时说明问题不是出在用户态工具而是底层总线通信压根没通。第四步如果方便用示波器量 CLK 和 DATA 两根线。上电后每次发起读操作CLK 上应该能看到一串时钟脉冲DATA 上会有地址和数据信号。如果 CLK 完全没有波形说明控制器就没把时序发出来如果有波形但 DATA 没有响应回读那就要怀疑从机地址或从设备本身的工作状态。4.3 根因之一USID 地址与 PMIC 硬件配置不一致我这次卡住的问题最后定位在 USID 地址不匹配上。SPMI 从设备的地址不是软件随便定的它由 PMIC 芯片的 USID 引脚电平决定。也就是说硬件上 PMIC 的 USID 引脚被拉成什么电平它在总线上的地址就是什么。我手上这块板子PMIC 硬件上 USID 是 1但设备树里却写成了 0。控制器把命令发给地址 0总线上自然没有任何设备应答于是超时。解决办法就是对照硬件原理图或者 PMIC 手册把设备树里的reg改成实际值reg 0x1 SPMI_USID;这里要特别提醒如果系统里挂了两颗 PMIC它们的 USID 通常不同比如主 PMIC 是 0、辅 PMIC 是 1。调试前最好先把 PMIC 型号和对应地址表列出来避免像我一样逐个试。另外一些平台的控制器支持总线扫描可以通过扫描命令把所有在线从设备地址找出来。包里的spmi_cmd如果支持scan子命令可以试试spmi_cmd scan它会尝试访问 0 到 31 的从机地址能应答的地址就是实际存在的设备。这个功能在动态确认地址时非常有用。4.4 第二个坑bank 切换与页寄存器地址对上之后通信算是通了。但随后又出现一个更隐蔽的问题读某些寄存器的值怎么看都不对。比如我要读某颗 PMIC 的某个调压寄存器读回来一个不合理的电压配置但用 PMIC 厂商的官方工具读又是另一个值。最后查了 PMIC 手册才发现这颗 PMIC 内部的寄存器是分 bank / page 的。默认状态下你访问到的是 page 0而关键的调压、充电配置却在 page 1 甚至 page 2。要访问目标寄存器必须先往一个 bank select 寄存器写入目标 bank 号然后再去读实际地址。实际操作时我封装了一个写序列方法。比如某 PMIC 要求先往0x0010写0x01切换到 bank 1然后访问0x4A0spmi_cmd w 0x00 0x0010 0x01 spmi_cmd r 0x00 0x04A0读完之后如果还要回到默认 bank记得再切回去否则后续代码用默认 bank 读其它寄存器就会出错。这类问题最坑的地方在于通信本身完全正常返回的数据也是正确的但偏偏不是你以为的那个寄存器的值。建议在调试脚本或工具里加入 bank 管理的抽象别直接裸读。4.5 第三个坑受保护寄存器和低功耗挂死还有一个让我差点把测试板搞坏的坑有一部分 PMIC 寄存器是受保护的直接写会被硬件拒绝必须先写入一组解锁序列unlock key。不同 PMIC 的解锁序列不一样有的是往特定地址写一串固定值有的是先写一个 magic number 再写目标值。我在调试一个充电电流配置时连续写了几次都没生效最后确认是需要先解锁才能修改。安全起见我的建议是凡是涉及电压、电流、温度保护相关的寄存器操作前第一件事是查手册确认是否有写保护。有保护就老老实实先解锁没保护也别乱写。这类寄存器写错了轻则 PMIC 直接死机重则整块板子烧掉。我在实际工作中见过太多因为随手 write 一个看起来像是配置的寄存器结果板子直接冒烟的案例。低功耗模式下的坑也值得单独说。当系统进入 suspend 时部分 SoC 会关闭 SPMI 控制器的时钟或者 PMIC 进入低功耗状态这时候你从调试终端去读 PMIC往往会发现总线像死了一样命令发出去没有回应甚至会让调用线程卡死。调试这个问题最好在确认系统正常运行、没有 suspend 的状态下进行。如果一定要在 suspend 场景测需要先把相关的 power state 配置调整成允许 SPMI 访问这个优先级低于省电但调试期通常可以接受。5. 把调试工具用得更顺手命令模板、脚本扩展与安全边界5.1 高频命令模板通完这些坑之后一套顺手的 SPMI 调试命令基本就固定下来了。我日常用得最多的是这几种# 读单字节 spmi_cmd r 0x00 0x0000 # 写单字节 spmi_cmd w 0x00 0x0010 0x5A # 连续读多个字节看中断状态或寄存器组 spmi_cmd rd 0x00 0x0800 8 # dump 大段寄存器适合查 PMIC 初始化状态 spmi_dump 0x00 0x0000 0x100如果是看某一位是否翻转可以用 shell 加位运算处理比如读状态寄存器后打印第 3 位的值val$(spmi_cmd r 0x00 0x0800) echo $(( (val 3) 1 ))5.2 自动化方向当要验证某个 PMIC 行为是否稳定时我一般会写一个简单的循环脚本比如反复读写某个寄存器 1000 次确认总线和 PMIC 都没有异常for i in $(seq 1 1000); do val$(spmi_cmd r 0x00 0x0000) if [ $val ! 0x5A ]; then echo iteration $i mismatch: $val break fi done再进一步可以把寄存器表导成 CSV写一个 Python 脚本批量比对 dump 出来的数据跟期望值。这个流程在处理 PMIC 初始化序列异常、排查某个上电时序没生效之类的问题上很省力。5.3 安全边界什么样的寄存器能写什么样的别碰调试越顺利越容易手滑。根据我个人的教训给几条非常硬的安全边界绝对不要凭感觉写电压寄存器。调压寄存器直接控制 PMIC 输出电压写错一个 bit核心电压瞬间可能冲到 2V 以上CPU 或内存当场报废。写之前先读。拿不准的时候先把这个寄存器的当前值记录下来改完再记录一次至少要能回滚。摸清写保护。查手册确认目标寄存器是否需要 unlock key需要就先解密别硬怼。别在低功耗状态下乱敲命令。前面说了总线可能挂死也可能产生异常唤醒干扰其他正在调试的问题。高温保护相关的寄存器别碰。这类寄存器往往关联 PMIC 内部温度监测和降频/关断策略改坏可能导致芯片过热还没有保护。5.4 后续学习路径这份spmi.rar看完用完之后如果想要真正吃透我建议顺着这条路线往下走读一遍 MIPI SPMI 规范原文重点看事务格式、命令类型和状态机。官方文档虽然枯燥但所有总线上诡异行为都能在里面找到答案。仔细读内核drivers/spmi/下的源码特别是spmi.c和平台对应的控制器驱动理解读写回调是怎么从上层一路到物理层的。找一颗常用 PMIC 的 datasheet把它的寄存器映射表、bank/page 划分、写保护机制完整过一遍。知道一颗 PMIC 的内部结构后面的基本都能举一反三。如果平台支持把包里的工具扩展出块读写、扩展寄存器读写、sleep/wakeup 命令的封装做成一个更完整的 PMIC 调试后台。最后说点个人体会。我翻到spmi.rar这个包时第一反应也是随便解压看看但真正静下心把里面的代码和配置过了一遍之后才发现看似不起眼的杂物包里往往藏着最实用的东西。SPMI 这个协议平时不太被提起但它支撑着整个移动设备的电源管理链路。后面我在做低功耗调试时能快速定位是 AP 没发出命令还是 PMIC 没应答还是地址对不上全靠当时把这套包里的工具和代码啃了一遍。如果你手里也有类似的 SPMI 调试包建议别只当工具用多花点时间把总线的工作原理和驱动框架搞清楚。遇到问题的时候至少你会知道该从哪根线、哪个寄存器查起。本文还有配套的精品资源点击获取