[ARM Linux 驱动开发] LED 驱动开发(ioremap)
文章 [ARM Linux 驱动开发] 第一个字符设备驱动 已经介绍了字符设备驱动的基本框架,本篇文章在其基础上进行 LED 驱动的开发。
本篇文章的“核心”点就是使用 ioremap 函数。现在已经进入操作系统的范畴了,涉及到了内存管理的方面,不能像裸机开发那样直接操作寄存器。而 ioremap 函数的作用就是把我们需要操作的 I/O 地址映射为虚拟地址,接着我们就可以针对这个映射的虚拟地址就行操作了。摘录 ioremap 函数的说明:
ioremap
ioremap 函数(宏)是体系结构相关的,其函数原型基本上等同于:
- void __iomem* ioremap(unsigned long phys_addr, size_t size);
此处的 __iomem 的作用只是提醒调用者返回的是 io 类型的地址,如同 __user、__percpu 一样,某些工具软件有可能会利用这些定义符作一些诸如代码质量等方面的检查。
ioremap 函数及其变种用来将 vmalloc 区的某段虚拟内存块映射到 I/O 空间,其实现原理与 vmalloc 函数基本上一样,都是通过在 vmalloc 区分配虚拟地址块,然后修改内核页表的方式将其映射到设备的内存区,也就是设备的 I/O 地址空间。与 vmalloc 函数不同的是,ioremap 并不需要通过伙伴系统去分配物理页,因为 ioremap 要映射的目标地址是 I/O 空间,不是物理内存。
因为 I/O 空间在不同的体系架构上有不同的解释,比如 IA32 架构上有独立于内存访问指令之外的 I/O 命令,ARM 的架构上则没有,所以在函数返回地址的使用上,有些要注意的地方。假设返回的地址是 pVaddr,对于有专门 I/O 指令的体系,比如 IA32,不能直接用内存访问的方式使用该地址,*pVaddr = 0x1234 是错误的,应该使用 readw(pVaddr),后者实际上使用了 inw 指令,这是 IA32 架构上专门的 I/O 指令;而在 ARM 处理器上,*pVaddr = 0x1234 则是完全正确的。因此,为了简化不同的架构平台代码移植工作,对于 ioremap 返回的地址,应该统一使用 readb/writeb、readw/writew 这样的宏,这些宏在不同的平台上会展开成架构相关的代码。
实际代码中 ioremap 还有一些相关的变体,包括 ioremap_nocache、ioremap_cached 等,这些变体的主要功能是通过加入一些映射标志位来影响相关内核页表项的设置,比如设备驱动程序中最常用的 ioremap_nocache,就是通过清除页表项中的 C(ache) 标志,使得处理器在访问这段地址时不会被 cache,这对外设空间的地址是非常重要的。
如果被映射的 I/O 空间不再使用,应该使用 iounmap 函数来做相关的清除工作,iounmap 函数要完成的工作包括将 vmalloc 区中分配的虚拟内存块返还给 vmalloc 区,清除对应的页表目录项等。
1. 裸机实验回顾
我们来温习一下文章 [ARM裸机开发] 汇编语言LED灯实验,它使用汇编代码进行 LED 操作。涉及到的寄存器以及功能包括:
1. GPIO 时钟寄存器。需要使能 GPIO 时钟,相关的模块才能工作。
2. IO 复用寄存器。IO 口可以复用为多种“角色”,这边需要把连接 LED 灯的 IO1_3 口复用为 GPIO 口。
3. IO 电气属性寄存器。设置 IO 的所需要的电气属性,比如上拉电阻、IO 速率等。
4. GPIO 方向寄存器。设置 GPIO 口是输入还是输出。
5. GPIO 数据寄存器。当 GPIO 口是输出时,向此寄存器写可控制输出高低电平;当 GPIO 口是输入时,此寄存器保存对应的电平值。
相关寄存器的地址可以参照 [ARM裸机开发] 汇编语言LED灯实验 文中编写的汇编代码,这边就不再赘述。将这些地址通过 ioremap 函数映射好后,操作就和裸机实验一样了。
2. 驱动编写
复习好 LED 的裸机实验之后,就可以开始编写驱动了。总体框架和 [ARM Linux 驱动开发] 第一个字符设备驱动 一样:
最末尾的 131 至 134 行声明了驱动的加载和卸载函数,以及作者信息和版权信息。
重点在驱动注册的 led_init 函数,功能包括设备注册和 LED 相关寄存器初始化。设备注册指明了设备号和设备名称,以及一组文件操作函数,这已经在之前的文章中介绍过了。文件操作函数后续再说,这边先看 LED 的初始化操作。第 97 至 101 行将第一节中说到的寄存器都进行了映射,将映射好的虚拟地址进行保存,用于后续操作。第 103 至 106 行使用映射的 CCM_CCGR1 地址使能 GPIO1 时钟;同理,第 109 和 111 行分别设置 IO 复用和电气属性;第 114 至 116 行将 GPIO 设置为输出;第 118 至 120 行输出高电平,即当驱动加载后 LED 默认是关闭的。读写使用 readl/writel 的原因,在文章开头介绍中已经说明,是为了统一便于移植。
驱动卸载时别忘了 iounmap 之前分配的虚拟地址(第 83 至 87 行)。并将设备注销(第 89 行)。
现在来看文件操作函数,此例中只需要打开、关闭和写函数。文件打开和关闭函数不需要额外的操作,基本就是空函数。LED 灯的亮灭逻辑在写函数 led_write 中实现:通过 copy_from_user 读取写的内容(第 57 行);判断写的内容,0 为关灯,1 为开灯。开灯(turn_on_led)和关灯(turn_off_led)函数就是操作 GPIO 的数据寄存器。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <asm/io.h>
- #include <asm/uaccess.h>
- /* 寄存器物理地址 */
- #define CCM_CCGR1_BASE (0x020c406c)
- #define SW_MUX_GPIO1_IO03_BASE (0x020e0068)
- #define SW_PAD_GPIO1_IO03_BASE (0x020e02f4)
- #define GPIO1_GDIR_BASE (0x0209c004)
- #define GPIO1_DR_BASE (0x0209c000)
- /* 映射后的虚拟地址 */
- void __iomem* CCM_CCGR1;
- void __iomem* SW_MUX_GPIO1_IO03;
- void __iomem* SW_PAD_GPIO1_IO03;
- void __iomem* GPIO1_GDIR;
- void __iomem* GPIO1_DR;
- #define LED_MAJOR 200
- #define LED_NAME "led"
- #define LEDOFF 0
- #define LEDON 1
- int led_open(struct inode* inode, struct file* filp)
- {
- printk("led_open\n");
- return 0;
- }
- int led_release(struct inode *inode, struct file *filp)
- {
- printk("led_release\n");
- return 0;
- }
- void turn_on_led(void)
- {
- int val = readl(GPIO1_DR);
- val &= ~(1 << 3);
- writel(val, GPIO1_DR);
- }
- void turn_off_led(void)
- {
- int val = readl(GPIO1_DR);
- val |= (1 << 3);
- writel(val, GPIO1_DR);
- }
- ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int res;
- unsigned char writeBuf[1], status;
- res = copy_from_user(writeBuf, buf, sizeof(writeBuf));
- if (res < 0)
- {
- printk("copy_from_user() failed\n");
- return -EFAULT;
- }
- status = writeBuf[0];
- if (status == LEDON)
- turn_on_led();
- else
- turn_off_led();
- return 0;
- }
- const struct file_operations led_fops =
- {
- .owner = THIS_MODULE,
- .open = led_open,
- .release = led_release,
- .write = led_write,
- };
- static void __exit led_exit(void)
- {
- iounmap(CCM_CCGR1);
- iounmap(SW_MUX_GPIO1_IO03);
- iounmap(SW_PAD_GPIO1_IO03);
- iounmap(GPIO1_GDIR);
- iounmap(GPIO1_DR);
- unregister_chrdev(LED_MAJOR, LED_NAME);
- }
- static int __init led_init(void)
- {
- int val;
- /* 寄存器映射 */
- CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
- SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
- SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
- GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
- GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
- /* 使能 GPIO1 时钟*/
- val = readl(CCM_CCGR1);
- val |= (3 << 26);
- writel(val, CCM_CCGR1);
- /* 复用为 GPIO */
- writel(5, SW_MUX_GPIO1_IO03);
- /* IO 属性 */
- writel(0x10b0, SW_PAD_GPIO1_IO03);
- /* GPIO 设置为输出 */
- val = readl(GPIO1_GDIR);
- val |= (1 << 3);
- writel(val, GPIO1_GDIR);
- /* 默认关闭 */
- val = readl(GPIO1_DR);
- val |= (1 << 3);
- writel(val, GPIO1_DR);
- val = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
- if (val < 0)
- {
- printk("register_chrdev() failed\n");
- return -EIO;
- }
- return 0;
- }
- module_init(led_init);
- module_exit(led_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("hanhanblog mrchen.love");
3. 实验验证
驱动的 Makefile 可以复用,编译驱动:
- > make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
开发板上加载驱动模块,并创建设备结点文件:
- > modprobe led.ko
- > mknod /dev/led c 200 0
这边测试的应用程序可以不用编写,直接使用 echo 命令进行简单的测试:
- > echo -ne "\x1" > /dev/led
- > echo -ne "\x0" > /dev/led
可以看到 LED 灯按预期亮灭。
总结
我感觉这个实验还是相当有意义的,虽然实际的驱动中很少有这么写的,但是它把裸机和驱动联系在了一起,让人能意识到“本质”还是寄存器的操作。后续的驱动在此基础上肯定会越来越抽象,让我们“拭目以待”!