嵌入式Linux驱动开发pinctrl篇(1)——从寄存器到子系统:驱动演进之路
嵌入式Linux驱动开发pinctrl篇1——从寄存器到子系统驱动演进之路仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里或者一起来尝试跑7.0的Linux欢迎各位大佬观摩喜欢的话点个⭐仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/前言我们为什么要折腾这个说实话在上一章我们通过ioremap()writel()的方式点亮 LED 的时候我真的感觉很有成就感。你看一行writel(val, GPIO1_DR)就能让 LED 亮起来这种直接操控硬件的感觉真的很爽。但后来我发现一个问题这种方式在简单的 demo 里确实没问题可一旦项目复杂起来代码会变得非常难以维护。你想想如果你的驱动里到处都是writel(0x0209C000, ...)这样的硬编码地址过两个月再回头看你自己都得翻手册才能搞清楚这行代码在干什么。更糟糕的是这种直接操作寄存器的方式有个致命问题它把硬件细节和驱动逻辑混在一起了。如果你明天换了块板子GPIO 引脚重新分配了整个驱动代码得大改。这就像是你家里装修电工把所有电线都裸露在外面每次想改动个开关位置都得重新布线。这时候我就在想有没有一种办法能让驱动开发者不需要知道底层寄存器地址也不需要手动配置引脚复用只要说我要控制 GPIO1_IO03就能用答案就是pinctrl gpio 子系统。从直接操作寄存器说起让我们先回顾一下上一章我们是怎么点灯的。如果你还记得我们做了这么几件事// 1. 映射寄存器地址IMX6U_CCM_CCGR1ioremap(0x020C406C,4);MUX_CTL_GPIO1_IO03ioremap(0x020E0068,4);PAD_CTL_GPIO1_IO03ioremap(0x020E02F4,4);GPIO1_GDIRioremap(0x0209C004,4);GPIO1_DRioremap(0x0209C000,4);// 2. 使能时钟writel(readl(IMX6U_CCM_CCGR1)|(326),IMX6U_CCM_CCGR1);// 3. 配置引脚复用writel(0x5,MUX_CTL_GPIO1_IO03);// 4. 配置电气特性writel(0x10B0,PAD_CTL_GPIO1_IO03);// 5. 设置方向为输出writel(readl(GPIO1_GDIR)|(13),GPIO1_GDIR);// 6. 点灯writel(readl(GPIO1_DR)~(13),GPIO1_DR);这里有什么问题呢让我数一数硬编码的物理地址到处都是。每个寄存器地址都是硬编码的换个芯片就得全部改掉。配置步骤冗长且容易出错。你得记住时钟→复用→电气特性→方向→数据少一步都不行。而且这些步骤的顺序还有讲究搞错了就可能起不来。代码没法复用。每个驱动都得重复这套流程没法共享。没有冲突检测。如果两个驱动都想用同一个引脚没人提醒你最后就是两个驱动打起来。和设备树脱节。我们前面花了那么多功夫学设备树结果驱动里还是用硬编码地址设备树的引脚配置信息根本没用到。说实话这种写法在嵌入式开发的早期确实很常见。那时候芯片简单引脚少驱动也不多这么写还能接受。但现在都 2026 年了我们的系统越来越复杂如果还用这种方式写驱动真的会把自己逼疯。驱动分离与分层的哲学现在让我们来聊聊 Linux 内核是怎么解决这个问题的。核心思想就四个字分离与分层。什么叫分离就是把硬件相关的操作和设备驱动的逻辑分开。硬件相关的操作——配置引脚复用、设置电气特性、使能时钟——这些事情应该由专门的子系统来处理而不是每个驱动都自己写一套。什么叫分层就是在驱动和硬件之间插入一层抽象这层抽象负责屏蔽硬件差异给上层提供统一的接口。你可以把它理解成搬家。传统的方式是你每个房间的东西都自己搬这很累。新的方式是你请了一支专业的搬家队子系统你只需要告诉他们把这台电视搬到新家剩下的打包、运输、拆包、摆放都由他们搞定。你不需要知道搬家车怎么开也不需要知道电视怎么打包你只需要知道我要搬电视这个意图。在我们的 LED 驱动里“搬家队就是 pinctrl 子系统和 gpio 子系统。我们只需要告诉它们我要用 GPIO1_IO03设置为输出”剩下的引脚配置、时钟使能都由它们搞定。这里有个很重要的设计理念驱动不应该知道硬件的细节。驱动只需要知道我要控制哪个 GPIO至于这个 GPIO 的寄存器地址是多少、需要配置哪些位、时钟要不要使能这些都不是驱动该关心的事情。pinctrl gpio 子系统全景图现在让我们来看看这两个子系统是如何协同工作的。我先给你画个全景图有个整体印象┌─────────────────────────────────────────────────────────────────┐ │ 用户空间程序 │ │ open(/dev/AES_LED) │ └─────────────────────────────┬───────────────────────────────────┘ │ 系统调用 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ LED 驱动 (我们的代码) │ │ of_get_named_gpio(led-gpio) │ │ gpio_set_value(gpio, 1) │ └─────────────────────────────┬───────────────────────────────────┘ │ GPIO API ▼ ┌─────────────────────────────────────────────────────────────────┐ │ GPIO 子系统核心层 │ │ (gpiolib.c - 提供统一接口) │ └───────────────────┬─────────────────────────────┬───────────────┘ │ │ ▼ ▼ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │ GPIO 控制器驱动 (mxc) │ │ GPIO 控制器驱动 (其他) │ │ (gpio-mxc.c) │ │ │ └───────────────┬───────────────┘ └───────────────────────────────┘ │ 寄存器操作 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ pinctrl 子系统 │ │ (配置引脚复用和电气特性) │ └─────────────────────────────────────────────────────────────────┘ │ 设备树解析 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 硬件寄存器 │ │ IOMUXC, GPIO_DR, GPIO_GDIR, CCM_CCGR, ... │ └─────────────────────────────────────────────────────────────────┘从这个图里你可以看到几个关键点我们的驱动只需要调用 GPIO API。像gpio_set_value()这样的函数我们不需要知道底层寄存器在哪也不需要知道怎么操作。GPIO 子系统负责管理所有 GPIO 控制器。无论是 i.MX 的 GPIO 控制器还是其他厂商的都通过统一的接口向上提供能力。pinctrl 子系统负责引脚配置。在我们使用某个 GPIO 之前pinctrl 子系统已经根据设备树配置好了引脚的复用功能和电气特性。设备树是配置的中心。所有的硬件配置信息——引脚复用、电气特性、GPIO 编号——都写在设备树里驱动通过设备树获取这些信息。为什么要用两个子系统你可能会问为什么不能一个子系统搞定非要分成 pinctrl 和 gpio 两个这里有个很重要的设计理念职责分离。pinctrl 子系统负责的事情是这个引脚要配置成什么功能GPIOUARTSPI它的电气特性是什么驱动强度上下拉。gpio 子系统负责的事情是这个 GPIO 引脚的值是 0 还是 1它是输入还是输出。你可以这么理解pinctrl 是装修队进场之前把房间的基础设施搞好gpio 是开关装修好了之后你来控制灯的开关。如果装修没搞好比如引脚没配置成 GPIO 功能你按开关也没用。这种分离的设计还有一个好处可移植性。你的驱动代码只需要调用 gpio 子系统的接口至于底层是什么芯片、引脚怎么配置完全不影响你的代码。换芯片的时候只需要修改设备树和 pinctrl/gpio 控制器驱动你的驱动代码可以完全不动。现在的 LED 驱动是什么样的让我们先看看最终效果有个直观的感受。这是使用子系统之后的 LED 驱动代码硬件抽象层部分// 从设备树获取 GPIO 编号led.gpio_sub_sys_nrof_get_named_gpio(led.device_tree_node,led-gpio,0);// 设置为输出模式初始值为 1gpio_direction_output(led.gpio_sub_sys_nr,1);// 设置 GPIO 值gpio_set_value(led.gpio_sub_sys_nr,0);// 点亮就这么简单没有物理地址没有ioremap没有寄存器操作。我们只需要告诉子系统我要用这个 GPIO剩下的都由子系统搞定。那么子系统是怎么知道这个 GPIO 需要配置什么引脚复用、什么电气特性的呢答案在设备树里iomuxc { pinctrl_aes_led: led_grp { fsl,pins MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 ; }; }; / { imx_aes_led { compatible imxaes_led; pinctrl-names default; pinctrl-0 pinctrl_aes_led; led-gpio gpio1 3 GPIO_ACTIVE_LOW; status okay; }; };这里有两个关键信息pinctrl-0 pinctrl_aes_led告诉 pinctrl 子系统这个设备需要用哪个引脚配置。led-gpio gpio1 3 GPIO_ACTIVE_LOW告诉 GPIO 子系统这个设备用的是 gpio1 控制器的第 3 号引脚而且它是低电平有效的。子系统会在设备加载的时候自动读取这些配置把引脚配置好。等我们的驱动代码执行的时候引脚已经配置完毕我们可以直接使用了。接下来我们要做什么现在我们对子系统有了整体印象接下来的章节会深入分析每个部分。我们的学习路径是这样的硬件基础先搞清楚 i.MX 6ULL 的引脚复用和 GPIO 模块是怎么工作的这是理解子系统的前提。pinctrl 子系统深入源码分析 pinctrl 子系统是如何工作的它和设备树是如何交互的。gpio 子系统同样深入源码看看 gpio 子系统是如何管理 GPIO 的它的 API 是怎么实现的。设备树配置学习如何在设备树里正确配置 pinctrl 和 gpio。驱动实现编写一个完整的 LED 驱动使用新 API从设备树读取配置控制 LED。编译测试上板验证看看我们的驱动是否能正常工作。内核对比对比主线内核和 imx 内核的差异看看这两个内核在子系统实现上有什么不同。在正式开始之前我需要提醒你一点子系统的源码量很大pinctrl-imx.c 就有两万多行gpio-mxc.c 也有七百多行。我们不可能逐行分析每一段代码那样会迷失在细节里。我们的策略是抓住主线理解核心流程遇到细节再看。另外我会用主线内核third_party/linux_mainline和 imx 内核third_party/linux-imx进行对比让你看看这两个内核在实现上的差异。这对于你以后做内核移植或者驱动兼容会很有帮助。准备好了吗让我们先从硬件基础开始搞清楚我们在操作什么。下一步阅读 02_hardware_foundation.md 了解 i.MX 6ULL 的引脚复用和 GPIO 硬件原理。相关阅读嵌入式Linux嵌入式Linux驱动开发设备树驱动改造——从硬编码到设备树的实战之旅 - 相似度 100%04. OF API 基础与验证——从 DTS 到代码的桥梁 - 相似度 82%嵌入式Linux嵌入式Linux驱动开发板级DTS实操与完整实战演练——从修改设备树到点亮LED的完整闭环 - 相似度 82%
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2627821.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!