现代 SoC 上,一个外设能跑起来光有寄存器远远不够:它的时钟得开着,引脚得复用成正确功能,电源域得供电,不用时还得能断电省电.这四件事分别归四个框架管:clock,pinctrl,regulator,runtime PM.这篇不写新驱动,直接把板上四个框架的实景全扒出来看,顺便理解 DT 在里面扮演的角色 — 它就是这些框架的配置数据库.
这篇回答几件事:
- 时钟框架的引用计数怎么运作,enable 和 prepare 差在哪
- pinctrl 怎么管理一个物理引脚的"多重人格"
- regulator 为什么值得为电源建一套对象系统
- runtime PM 的 suspended 是怎么发生的,哪些设备管不了
先看时钟,实景在 /sys/kernel/debug/clk/clk_summary.这张表是整个 SoC 的时钟树,缩进表示父子.抓控制台串口那两行:
clk_uart2 1 1 0 24000000 ... Y ff1a0000.serial baudclk
pclk_uart2 1 1 0 50000000 ... Y ff1a0000.serial apb_pclk
信息密度很高.串口驱动声明要两个时钟,波特率时钟 24MHz 和总线时钟 50MHz,名字 baudclk 和 apb_pclk — 这正是 3.2 那篇设备树节点里 clock-names 写的那对名字,框架按名字解析,驱动代码里 devm_clk_get(dev, "baudclk") 拿到的就是它.两列计数 enable count 和 prepare count 都是 1,含义是"有一个使用者".时钟框架的核心就是这套引用计数:驱动 probe 时 clk_prepare_enable,计数加一,remove 时减一,减到零时钟才真的关.两个设备共用一个时钟,谁先谁后都不出事,这是"裸写时钟寄存器"时代不敢想的.
enable 和 prepare 为什么要分开:内核规定 prepare 可能睡眠(比如等锁相环锁定),只能进程上下文调;enable 必须能原子执行(就拨一个门控位),中断上下文也允许.两个计数分开记,对应两段 API.读表格还能看到 clk_uart2_div,800MHz,是 24MHz 的父时钟,分频比随 rate 自动算 — 驱动要改波特率时钟,调 clk_set_rate,框架自己找分频组合,不必手搓寄存器.
pinctrl 管的是引脚的多重人格.一个物理引脚,今天是 GPIO,明天是 UART 的 TX,后天是 I2C 的 SCL,谁说了算?pinctrl 框架按"功能/组"组织:SoC 的 pinctrl 驱动(rockchip 的是 pinctrl-rockchip)登记一张大表,设备树里设备节点写 pinctrl-0 = <&uart2c_xfer>,probe 时框架自动把这几个引脚切到 uart2c 功能.实景:
pin 147 (gpio4-19): ff1a0000.serial (GPIO UNCLAIMED) function uart2c group uart2c-xfer
pin 148 (gpio4-20): ff1a0000.serial (GPIO UNCLAIMED) function uart2c group uart2c-xfer
147 和 148 就是控制台串口的 TX/RX,mux_owner 是串口驱动,复用功能 uart2c.后面那个 GPIO UNCLAIMED 说明没有 GPIO 客户端占用 — 同一个引脚如果 GPIO 子系统也来申请,pinctrl 会当仲裁人,先到先得,后来者吃 -EBUSY.排查"引脚状态不对"的万能第一步,就是来这个文件看 mux_owner 到底是谁.
regulator 是电源的对象化.板上的电源是一棵树:12V 进来,各种 DCDC 和 LDO 降出 5V,3.3V,1.8V,外设各自挂在不同枝上.sysfs 里每路电一个对象:
regulator.0: regulator-dummy
regulator.1: dc_12v
regulator.10: vcc5v0_typec
regulator.11: vcc3v0_sd
名字来自设备树 — RK3399 板的 PMIC(RK808)驱动按 DT 枚举出所有电源路.对象化之后能做的事:引用计数开关(regulator_enable 谁用谁开),电压切换(调频时内核联动调核电压,cpufreq 和 regulator 在这里握手),状态查询(microvolts,state).这解释了一个新手困惑:为什么 devm_regulator_get 拿到的电源"总是开着的" — 可能真开着,也可能是别的使用者的计数撑着,你关了它未必断电,这是保护,不是 bug.
runtime PM 是省电的最后一公里.设备没人用时自动挂起,要用时零延迟唤醒 — 这件事没有框架不可能做对,因为"设备空闲"的判断分散在所有驱动里.板上的实况:
/sys/devices/platform/fe380000.usb/usb8/power/runtime_status: suspended
/sys/devices/platform/ff100000.saradc/iio:device0/power/runtime_status: unsupported
USB 口没人插,autosuspend 到期后进 suspended,硬件时钟甚至电源直接关;下一位主人来访问时框架先 resume 再放行,驱动只管在 probe 里 pm_runtime_enable 加 Autosuspend 延时.那个 unsupported 是对照组:驱动压根没实现 runtime PM,框架如实报告.顺带一提 cpufreq 也在隔壁干活,cpu4 空闲时实测掉到 600MHz,governor 是 ondemand — 频率和电源这套,板上没有一个子系统在偷懒.
四个框架串起来有个共同的设计气味,值得点破:驱动写的从来不是"把寄存器 X 设成 Y",而是"我要名字叫 baudclk 的时钟/叫 vcc3v0_sd 的电/uart2c 这组引脚".名字和资源在设备树里绑定,框架负责翻译和仲裁.好处换板子只换 DT,驱动一行不改;坏处是出了问题你要知道去哪层查 — clk_summary 查时钟,pinmux-pins 查引脚,regulator 目录查电,power/runtime_status 查挂起,这一篇就是一张查询地图.
最后留几个容易忽略的细节:
- clk_summary 的 prepare 计数为零但 enable 非零的状态不存在,enable 隐含 prepare,逆序 API 成对用,别 enable 了忘 disable.
- pinctrl 的"hog"是指启动时 pinctrl 驱动自己占用的引脚(比如某些默认拉低),pinmux-pins 里会标出来,别当成设备申请的.
- regulator_get 拿到 dummy 不报错(那行 regulator-dummy),配错名字时静默给你一个假电源,一切照旧就是不管电,这种静默降级排查极难,DT 名字要较对.
- pm_runtime 的 autosuspend 是"延迟到点才挂",防抖设计,频繁短暂访问的设备把延时调大反而更省.
- clk_set_rate 可能连累别人:父时钟只有一个,两个设备要求冲突的频率,框架尽量找公倍数,找不到就有设备拿到近似值,精度敏感的外设(波特率!)要检查返回的实际 rate.
- 这些 debugfs 接口都能写(echo 1 > clk/.../clk_enable_count 之类别乱试),生产系统 debugfs 挂载要收敛,不是摆设.