嵌入式Linux中的LED驱动控制(续)

嵌入式Linux中的LED驱动控制”一文实现了在野火i.MX6ULL开发板上对三个LED灯的控制,这里来讨论一下该驱动程序具体实现的原理。由于实例使用的是NXP的i.MX6ULL这款芯片,所以先来看一下与该芯片端口操作相关的寄存器。

先看软件多路复用控制寄存器SW MUX Control Register,这样的寄存器有很多,基本上每个IO引脚都配置了这样一个寄存器,其结构都是一样的,区别仅在于复用的功能不同。下表给出的是GPIO1_IO04引脚的该寄存器位结构,其名称为SW_MUX_CTL_PAD_GPIO1_IO04,字节地址为0x20E006C。

无标题

从上表中可以看出,要使用引脚的GPIO功能,该寄存器的低4位值应该为0101(即ALT5),一般这类寄存器的ALT5都是GPIO功能。另外,第4位(SION)是软件输入使能位,它用于强制导通引脚 GPIO1_IO04 的输入路径,本例作为输出功能,设置为0即可。在点亮LED的例子中,还涉及其他两个类似的寄存器,一个是SW_MUX_CTL_PAD_CSI_HSYNC,另一个是SW_MUX_CTL_PAD_CSI_VSYNC,设置上都一样。

接下来看软件引脚控制寄存器SW PAD Control Register,这样的寄存器也有很多,基本上每个IO引脚都配置了这样一个寄存器,其结构都是一样的。下表给出的是GPIO1_IO04引脚的该寄存器位结构,其名称为SW_PAD_CTL_PAD_GPIO1_IO04,字节地址为0x20E02F8。

未标题-3

第0位(SRE)是压摆率设置位,设置为0时是慢压摆,1时是快压摆。压摆主要用于控制方波边沿的陡峭度,类似于运放的转速率。

第3-5位(DSE)用于引脚驱动强度选择,当值为001时,R0即为在不同电压下的导通电阻,决定引脚灌电流的能力大小,其后的值都是R0的分数倍(即电阻更小,电流更大)。一般对于点对点、短走线(< 10 cm)、低频(< 10 MHz)的情况,可以使用中等强度(如 R0/3 ~ R0/4,即 DSE=011 或 100);对于长走线、大电容负载(如多个设备、长排线)的情况,则需要较强驱动(如 R0/2 ~ R0/1,即 DSE=010 或 001);对于需要精确匹配的传输线(如以太网 RMII、SDIO)等情况,应接近走线特性阻抗(例如 50Ω 走线,使用 R0/5 ~ R0/6,即 260/5=52Ω 左右)。另外,如果已经选了快压摆率(SRE=1),可以先尝试中等驱动强度,避免过冲。如果选了慢压摆率,可能需要适当增大驱动强度来补偿边沿变缓。

第6-7位(SPEED)用于选择IO引脚的速度,本例使用50MHz足够了。

第11位(ODE)用于选择是否启用开漏模式,本例仅驱动一个LED,不涉及“线与”,所以可以禁用开漏输出。

第12位(PKE)为上拉/保持功能使能位,第13位(PUE)为上拉/保持功能选择位,这两位需要配合使用,共同决定引脚在输入状态下的弱上拉/下拉或保持功能。具体可参考下表。

未标题-1

第14-15位(PUS)为上/下拉配置位,可以选择上拉的强度。

第16位(HYS)为迟滞使能位,主要用于输入时对信号的整形,本例不使用。

综上所述,在点亮LED的例子中,该寄存器的值可以设置为1111100000111000(即0xF838),即引脚配置为:慢压摆、强驱动、低速度、无开漏、强上拉、无迟滞等模式。在例子中还涉及其他两个类似的寄存器,一个是SW_PAD_CTL_PAD_CSI_HSYNC,另一个是SW_PAD_CTL_PAD_CSI_VSYNC,设置上都一样。

接下来看GPIO方向寄存器GPIO Direction Register ,该寄存器按组来分,一般把它们命名为GPIOx_GDIR(x的值为1~4),每个寄存器可以配置32个引脚,其结构都是一样的。下表给出了其全部位结构。

未标题-1

从上表中可以看出,每1位对应1个引脚的配置,当值为0时是输入方向,为1时是输出方向,本例要配置为输出方向。

然后看GPIO数据寄存器GPIO Data Register,该寄存器也按组来分,一般把它们命名为GPIOx_DR(x的值为1~4),每个寄存器可以控制32个引脚,其结构都是一样的。下表给出了其全部位结构。

未标题-1

从上表中可以看出,每1位对应1个引脚的电平,当值为0时是输出低电平,为1时是输出高电平。读取时也一样,引脚为低电平时读到0,为高电平时读到1。

最后来看时钟门控寄存器CCM Clock Gating Register,该寄存器按组来分,一般把它们命名为CCM_CCGRx(x的值为0~6),下表给出的是CCM_CCGR1的全部位结构。

未标题-2

从上表中可以看出,要使能GPIO1的端口时钟,需要配置第26、27位。时钟由两位来共同确定,具体如下表所示。

无标题

从上表中可以看出,值为00时所有模式下都关闭外设时钟,值为01时只有在运行模式下打开外设时钟,值为11时除了停止模式以外,该外设时钟全程使能。

在本例中,可以把CCM_CCGR1的26、27位设置为11。另外,本例还需要配置CCM_CCGR3寄存器的第12、13位为11,以使能GPIO4的端口时钟。

以上对操作GPIO端口所用到的寄存器进行了介绍。在Linux系统中,并不能直接使用物理地址对寄存器进行操作,必须先把寄存器的物理地址映射到操作系统中来形成虚拟地址,然后才能进行相应地操作。实现映射功能的函数名为ioremap,其原型为:void __iomem *ioremap(phys_addr_t paddr, unsigned long size)。第一个参数为物理地址,第二个参数为长度,返回值是一个指向__iomem类型的指针。__iomem是一个宏,它表示返回的地址是一个IO存储空间的有效地址。相应的解除映射函数iounmap,其原型为:void iounmap(void *addr)。只有一个参数,为前面映射时的返回指针,该函数没有返回值。

下面以映射CCM_CCGR1寄存器为例进行说明,先对CCM_CCGR1寄存器的物理地址进行一个宏定义,定义采取基址+偏移量的方式,如下。

#define CCM_BASE_ADDR         (0x20C4000)
#define CCM_CCGR1_ADDR        (CCM_BASE_ADDR + 0x6C)

然后定义一个用于接收映射的返回值的指针变量,如下。

static void __iomem *CCM_CCGR1;

然后就可以进行地址映射了,如下。

CCM_CCGR1 = ioremap(CCM_CCGR1_ADDR, 4);

映射成功后,就可以使用CCM_CCGR1来进行赋值了,如下。

tmp = ioread32(CCM_CCGR1);
tmp |= 0xC000000;
iowrite32(tmp, CCM_CCGR1);

上面的赋值采用了“读——改——写”的方式,所以只改变寄存器的第26、27两位,其余位不变。经过赋值后,就使能了GPIO1的时钟。

在Linux中,对端口的读写有专用的函数,一般不推荐直接对指针进行操作。ioread32函数用于读取一个32位的值,iowrite32函数用于写入一个32位的值(早期可能会使用readl和writel函数,目前不推荐使用)。使用这些专用函数来操作芯片的IO端口,在定义端口寄存器时,就不用再加入关键字volatile了,因为在这些函数内容已经考虑了更新寄存器值的问题,使用起来更为方便。

本例把寄存器的地址映射放在了入口函数(led_init)中,同时也把端口配置放在了入口中。把解除映射函数放在出口函数(led_exit)中进行。对芯片寄存器的读、写等操作放在了文件操作接口里面(file_operations结构体成员函数中)。在使用设备时,应用程序会打开设备节点,并通过设备节点的inode结构体、file结构体最终找到file_operations结构体,然后从file_operations结构体中得到操作设备的具体方法。 这部分的具体内容可参见“嵌入式Linux中字符型驱动程序的基本框架”一文。在函数alloc_chrdev_region(&led.devid, 0, 1, "led")执行后,字符串led会出现在/proc/devices文件中。函数class_create(THIS_MODULE,  "led_dev")执行后,字符串led_dev会出现在/sys/class目录下。函数device_create(led.class, NULL, led.devid, NULL, "led")执行后,字符串led会出现在/dev目录下。可在开发板上自行查看。

在实例中,把设备号、字符型结构体、类结构体和设备结构体又封装在了一个名为led_dev的结构体中,并定义了一个名为led的该类结构体,后面的程序来引用该led的成员。这样做的好处是逻辑性强,容易区别不同的设备,当然,也可以不用结构体,直接声明变量,看自己的喜好而定。此外,在该实例中,把打开端口时钟的操作放在了open函数中,然后在release函数中关闭端口时钟。其意义在于,当应用程序执行完成后,端口时钟处于关闭状态,这样可以节约一点功耗。(一般的做法是在移除驱动模块后才关闭端口时钟)

posted @ 2026-06-12 14:52  fxzq  阅读(33)  评论(0)    收藏  举报