[正点原子]Linux驱动学习笔记--2.LED驱动实验(直接操作寄存器)

作者 by adtxl / 2022-10-22 / 暂无评论 / 56 个足迹

1. 实验目的

上一章我们详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的 chrdevbase 设备为例带领大家完成了第一个字符设备驱动的开发。本章我们就开始编写第一个真正的 Linux 字符设备驱动。在 I.MX6U-ALPHA 开发板上有一个 LED 灯,我们在裸机篇中已经编写过此 LED 灯的裸机驱动,本章我们就来学习一下如何编写 Linux 下的 LED 灯驱动。

2. 知识点

2.1 硬件原理

I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上,因此本章实验的重点就是编写 Linux 下 I.MX6UL 引脚控制驱动。

image.png

从图 8.2.1 可以看出, LED0 接到了 GPIO_3 上, GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭。

我们需要对 GPIO1_IO03 做如下设置:

1、使能 GPIO1 时钟
GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即可。

2、设置 GPIO1_IO03 的复用功能
找到 GPIO1_IO03 的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。

3、配置 GPIO1_IO03
找到 GPIO1_IO03 的配置寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03的地址为0X020E02F4,根据实际使用情况,配置此寄存器。

4、设置 GPIO
我们已经将 GPIO1_IO03 复用为了 GPIO 功能,所以我们需要配置 GPIO。找到 GPIO3 对应的 GPIO 组寄存器地址,在《IMX6ULL 参考手册》的 1357 页,如图 8.3.1 所示:

image1125590bc862e624.png

本实验中 GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出。

5、控制 GPIO 的输出电平
经过前面几步, GPIO1_IO03 已经配置好了,只需要向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平,关闭LED。

2.2 地址映射

Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。比如I.MX6ULL的GPIO1_IO03引脚的复用寄存器
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。
现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。

  1. ioremap函数

ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h 文件中,定义如下:

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
                                            unsigned int mtype)
{
    return arch_ioremap_caller(phys_addr, size, mtype,
                                            __builtin_return_address(0));
}

ioremap 是个宏,有两个参数: cookie 和 size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:

phys_addr:要映射的物理起始地址。
size:要映射的内存空间大小。
mtype: ioremap 的类型,可以选择MT_DEVICEMT_DEVICE_NONSHARED
MT_DEVICE_CACHEDMT_DEVICE_WC, ioremap 函数选择 MT_DEVICE
返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址。

假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应
的虚拟地址,使用如下代码即可:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是映射后的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。
映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。

  1. iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射, iounmap 函数原
型如下:

void iounmap (volatile void __iomem *addr)

iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码即可:

iounmap(SW_MUX_GPIO1_IO03);

2.3 I/O内存访问函数

这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉及到两个概念: I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。
当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议
这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

  1. 读操作函数

读操作函数有如下几个:

1 u8 readb(const volatile void __iomem *addr)
2 u16 readw(const volatile void __iomem *addr)
3 u32 readl(const volatile void __iomem *addr)

readb、 readw 和 readl 这三个函数分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

  1. 写操作函数

写操作函数有如下几个:

1 void writeb(u8 value, volatile void __iomem *addr)
2 void writew(u16 value, volatile void __iomem *addr)
3 void writel(u32 value, volatile void __iomem *addr)

writeb、 writew 和 writel 这三个函数分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。

3. 实验&测试

实验代码:https://github.com/ADTXL/imx_linux/commit/19abf4f8782e436cfe2dc506e5fabaeda8af3686

由于我使用的是出厂代码,所以led为呼吸灯,需要先使用下面的命令关闭呼吸灯模式。

echo none > /sys/class/leds/red/trigger

测试Ok:

image.png

独特见解