嵌入式Linux驱动开发——开始pinctrl之前,快速回顾i.MX 6ULL 引脚复用与 GPIO 硬件原理
嵌入式Linux驱动开发——开始pinctrl之前快速回顾i.MX 6ULL 引脚复用与 GPIO 硬件原理仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里或者一起来尝试跑7.0的Linux欢迎各位大佬观摩喜欢的话点个⭐仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/前言先搞清楚我们在操作什么说实话在深入子系统源码之前我觉得非常有必要先搞清楚硬件是怎么工作的。很多教程一上来就讲 pinctrl 驱动、讲 gpio_chip结果读者对硬件完全没有概念看着那些抽象的数据结构完全不知道它们在映射什么硬件功能。这就像你学开车教练直接给你讲发动机原理、悬挂系统但你连方向盘和油门在哪都不知道。所以我们的策略是先搞清楚硬件长什么样再来看软件是怎么抽象它的。引脚复用一个物理引脚多种功能让我们先来回答一个问题为什么需要引脚复用i.MX 6ULL 这个芯片有很强大的功能——UART、SPI、I2C、PWM、Ethernet 等等。但如果每个功能都配独立的物理引脚芯片的封装会变得非常大成本也会飙升。所以芯片厂商做了一个很聪明的决定让一个物理引脚可以被多个功能模块共享。这就是**引脚复用Pin Multiplexing**的概念。你想象一下你家的客厅有多种用途平时是客厅有客人来了可以当客房过年的时候还能当餐厅。同一个物理空间在不同的场景下有不同的功能。芯片的引脚也是这样GPIO1_IO03 这个引脚可以被配置成 GPIO也可以被配置成 I2C1_SDA还可以配置成 UART1_DCE_RX等等。具体选哪个功能由我们通过软件来配置。配置的入口就是 IOMUXCI/O Multiplexer Controller控制器。IOMUXC 控制器IOMUXC 是 i.MX 系列芯片里专门负责引脚复用的硬件模块。你可以把它理解成一个巨大的多路选择器每个引脚都有一个开关决定这个引脚的信号连接到哪个内部功能模块。在芯片手册里你会看到每个引脚都有一个 MUX 寄存器。这个寄存器决定了引脚的功能模式。比如 GPIO1_IO03 的 MUX 寄存器SW_MUX_CTL_PAD_GPIO1_IO03位 [2:0] - MUX_MODE 000 ALT0 - 这个引脚作为某个功能模块的信号 001 ALT1 - 这个引脚作为另一个功能模块的信号 ... 101 ALT5 GPIO - 这个引脚作为 GPIO 使用当我们写writel(0x5, MUX_CTL_GPIO1_IO03)的时候实际上是在告诉 IOMUXC把 GPIO1_IO03 这个引脚连接到 GPIO 模块而不是连接到 UART、I2C 或者其他模块。引脚的多种功能让我给你看一个真实的例子。GPIO1_IO03 这个引脚在 i.MX 6ULL 里有 9 种功能模式MUX_MODE 0: I2C1_SDA (I2C1 的数据线) MUX_MODE 1: GPT1_COMPARE3 (定时器比较输出) MUX_MODE 2: USB_OTG2_OC (USB 过流检测) MUX_MODE 3: OSC32K_32K_OUT (32kHz 时钟输出) MUX_MODE 4: USDHC1_CD_B (SD 卡检测) MUX_MODE 5: GPIO1_IO03 (GPIO 模式) ← 我们要用的 MUX_MODE 6: CCM_DI0_EXT_CLK (外部时钟输入) MUX_MODE 7: SRC_TESTER_ACK (测试信号) MUX_MODE 8: UART1_DCE_RX (UART1 接收)这些信息都定义在设备树的 pinfunc.h 文件里#defineMX6UL_PAD_GPIO1_IO03__I2C1_SDA0x00680x02f40x05a801#defineMX6UL_PAD_GPIO1_IO03__GPT1_COMPARE30x00680x02f40x000010#defineMX6UL_PAD_GPIO1_IO03__USB_OTG2_OC0x00680x02f40x066020#defineMX6UL_PAD_GPIO1_IO03__OSC32K_32K_OUT0x00680x02f40x000030#defineMX6UL_PAD_GPIO1_IO03__USDHC1_CD_B0x00680x02f40x066840#defineMX6UL_PAD_GPIO1_IO03__GPIO1_IO030x00680x02f40x000050← 这一行#defineMX6UL_PAD_GPIO1_IO03__CCM_DI0_EXT_CLK0x00680x02f40x000060#defineMX6UL_PAD_GPIO1_IO03__SRC_TESTER_ACK0x00680x02f40x000070#defineMX6UL_PAD_GPIO1_IO03__UART1_DCE_RX0x00680x02f40x062481这里每个宏定义有 5 个参数后面会详细解释第 4 个参数就是 MUX_MODE 的值。你可以看到MX6UL_PAD_GPIO1_IO03__GPIO1_IO03的第 4 个参数是 5对应的就是 ALT5 模式。PAD 配置不只是选择功能选择了功能之后还没完引脚的电气特性也需要配置。这就像是装修房子你决定了客厅的用途还得决定铺什么地板、装什么灯、墙面刷什么颜色。这些电气特性包括驱动强度、上下拉电阻、迟滞、速率等等。它们会影响信号的质量和抗干扰能力。PAD 寄存器每个引脚除了有 MUX 寄存器还有一个 PAD 寄存器SW_PAD_CTL_PAD_GPIO1_IO03。这个寄存器配置引脚的电气特性。i.MX 6ULL 的 PAD 寄存器是 32 位的每个位都有特定的含义位 [16] - HYS (迟滞使能) 位 [15:14] - PUS (上下拉选择) 00 100K 下拉 01 47K 上拉 10 100K 上拉 11 22K 上拉 位 [13] - PUE (上下拉使能) 0 保留 / 100K 下拉 1 上拉/下拉使能 位 [12] - PKE (保持使能) 0 禁用保持器 1 使能保持器 位 [11] - ODE (开漏使能) 0 禁止开漏 1 使能开漏 位 [10:6] - SPEED (速率选择) 000 低速 001 中速 010 高速 100 超高速 位 [5:3] - DSE (驱动强度选择) 000 关闭驱动 001 R0(260 欧姆) 010 R0/2 011 R0/3 100 R0/4 101 R0/5 110 R0/6 111 R0/7 位 [1:0] - SRE (快速 slew rate) 0 慢速 slew rate 1 快速 slew rate这些参数的具体含义取决于你的应用场景。比如驱动强度DSE如果你的引脚要驱动长线或者多个负载就需要更大的驱动强度。上下拉PUS/PUE/PKE如果引脚在空闲时可能悬空就需要加上拉或下拉电阻来避免不确定状态。迟滞HYS对于输入引脚使能迟滞可以提高抗干扰能力。速率SPEED/SRE对于高速信号如 UART、SPI需要配置更快的速率。我们的 LED 驱动使用的配置值是0x10B0让我们来分解一下0x10B0 0b0001 0000 1011 0000 位 [16] HYS 0 (不使能迟滞) 位 [15:14] PUS 10 (100K 上拉) 位 [13] PUE 1 (使能上拉) 位 [12] PKE 1 (使能保持器) 位 [11] ODE 0 (禁止开漏) 位 [10:6] SPEED 00010 (中速) 位 [5:3] DSE 011 (R0/3) 位 [1:0] SRE 0 (慢速 slew rate)这个配置对于 LED 控制来说完全足够。LED 不需要高速信号也不需要很强的驱动能力所以用了中速和中等驱动强度。GPIO 模块点亮 LED 的最后一步当我们把引脚配置成 GPIO 功能之后还需要配置 GPIO 模块本身才能控制 LED。i.MX 6ULL 有多个 GPIO 模块GPIO1~GPIO5每个模块最多控制 32 个 GPIO。GPIO1_IO03 表示这是 GPIO1 模块的第 3 号引脚。GPIO 寄存器GPIO 模块有一组寄存器最常用的有几个DR (Data Register) - 数据寄存器读写 GPIO 的值 GDIR (Direction Register) - 方向寄存器设置 GPIO 是输入还是输出 PSR (Pad Status Register) - 状态寄存器读取 GPIO 的实际电平 ICR1/ICR2 (Interrupt Control) - 中断控制寄存器 IMR (Interrupt Mask) - 中断屏蔽寄存器 ISR (Interrupt Status) - 中断状态寄存器 EDGE_SEL (Edge Select) - 边沿选择寄存器对于我们的 LED 控制只需要关注 DR 和 GDIR 这两个寄存器。GDIR方向寄存器在使用 GPIO 之前必须先设置它的方向是输入还是输出。// 设置 GPIO1_IO03 为输出writel(readl(GPIO1_GDIR)|(13),GPIO1_GDIR);GDIR 寄存器的每一位对应一个 GPIO。位 3 对应 GPIO1_IO03写 1 表示输出写 0 表示输入。DR数据寄存器对于输出引脚写 DR 寄存器可以设置引脚的电平。// 点亮 LED写 0writel(readl(GPIO1_DR)~(13),GPIO1_DR);// 熄灭 LED写 1writel(readl(GPIO1_DR)|(13),GPIO1_DR);对于输入引脚读 DR 寄存器可以获取引脚的电平。时钟控制别忘了给 GPIO 模块供电这里有个很容易被忽略的细节GPIO 模块也需要时钟如果时钟没使能你操作 GPIO 寄存器不会有任何效果。i.MX 6ULL 的时钟由 CCMClock Controller Module控制。每个外设模块都有对应的时钟门控寄存器需要手动使能。// 使能 GPIO1 的时钟// CCM_CCGR1 的位 [27:26] 控制 GPIO1writel(readl(IMX6U_CCM_CCGR1)|(326),IMX6U_CCM_CCGR1);CCM_CCGRx 寄存器每个模块占 2 位00 时钟关闭低功耗模式01 时钟在运行模式下开启10 保留11 时钟始终开启⚠️注意这一步真的很容易忘如果你配置了引脚、设置了方向但 LED 就是不亮大概率是时钟没使能。完整的初始化流程现在让我们把所有步骤串起来看看完整的初始化流程是什么样子的1. 使能 GPIO1 模块的时钟 writel(readl(CCM_CCGR1) | (3 26), CCM_CCGR1); 2. 配置 GPIO1_IO03 的引脚复用为 GPIO 功能 writel(0x5, MUX_CTL_PAD_GPIO1_IO03); 3. 配置 GPIO1_IO03 的电气特性 writel(0x10B0, PAD_CTL_PAD_GPIO1_IO03); 4. 设置 GPIO1_IO03 为输出模式 writel(readl(GPIO1_GDIR) | (1 3), GPIO1_GDIR); 5. 控制 GPIO1_IO03 的电平 writel(readl(GPIO1_DR) ~(1 3), GPIO1_DR); // 点亮每一步都必须按顺序来不能跳过。而且每一步都有对应的寄存器地址你需要从芯片手册里查到这些地址。硬件连接低电平有效最后让我们看看硬件连接。我们的 LED 是连接在 GPIO1_IO03 上的但有一个细节这个 LED 是低电平有效的。什么叫低电平有效意思是当 GPIO 输出低电平0V的时候LED 点亮当 GPIO 输出高电平3.3V的时候LED 熄灭。这是因为 LED 的接法。常见的有两种接法高电平有效 3.3V → [限流电阻] → [LED] → GPIO 低电平有效 3.3V → [限流电阻] → [LED] → GPIO ↓ (实际上 LED 的负极接 GPIO)低电平有效的接法更常见因为很多芯片的 GPIO 灌电流能力sink capability比拉电流能力source capability更强。所以在我们的驱动代码里你会发现// LED 初始化时设置为 1熄灭gpio_direction_output(led.gpio_sub_sys_nr,1);// 点亮 LED 时写 0gpio_set_value(led.gpio_sub_sys_nr,0);// 熄灭 LED 时写 1gpio_set_value(led.gpio_sub_sys_nr,1);这个逻辑看起来是反的但配合硬件连接就是对的。如果你在设备树里看到GPIO_ACTIVE_LOW就是在告诉内核这个 GPIO 是低电平有效的。内核会自动处理反转你就可以用正常的逻辑1 表示开0 表示关来编程了。寄存器地址一览为了方便你查阅我把涉及到的寄存器地址列出来// 时钟控制 CCM_CCGR1 0x020C406C // GPIO1 模块 GPIO1_DR 0x0209C000 // 数据寄存器 GPIO1_GDIR 0x0209C004 // 方向寄存器 GPIO1_PSR 0x0209C008 // 状态寄存器 // GPIO1_IO03 的引脚控制 MUX_CTL_PAD_GPIO1_IO03 0x020E0068 // 引脚复用控制 PAD_CTL_PAD_GPIO1_IO03 0x020E02F4 // 电气特性配置这些地址可以从 i.MX 6ULL 的参考手册Reference Manual里查到。手册有几千页但你需要关注的只是 GPIO 章节和 IOMUXC 章节。下一章软件登场现在我们对硬件有了完整的理解。我们知道了引脚可以通过 IOMUXC 配置成不同的功能引脚的电气特性可以通过 PAD 寄存器配置GPIO 模块需要时钟使能才能工作GPIO 的方向和数据需要通过 GDIR 和 DR 寄存器控制接下来就是有趣的部分了Linux 内核是怎么把这些硬件操作抽象成子系统的pinctrl 子系统和 gpio 子系统是如何协同工作的