上篇文章(【i.MX6ULL】驱动开发3–GPIO寄存器配置原理),介绍了i.MX6ULL芯片的GPIO的工作原理与寄存器配置。
本篇,就要来实际操作一下GPIO,实现板子上LED灯的亮灭控制。
[TOC]
在介绍如何通过寄存器来控制LED之前,需要先来了解一下有关Linux地址映射相关的知识。
1 地址映射
Linux或是STM32,对于硬件的控制,本质都是操作寄存器,在对应的地址进行数据的读写。若是在裸机开发中,可以控制CPU直接操作寄存器的地址,实现相应的功能,其过程是这样的:
linux环境,一般是不会直接访问物理内存,因为如果用户不小心修改了内存中的数据,很有可能造成错误甚至系统崩溃。为了避免这些问题,linux内核便引入了MMU和TLB进行内存地址映射,通过访问虚拟地址实现对实际物理地址的读写:
1.1 MMU介绍
MMU,Memory Manage Unit,即内存管理单元,它提供统一的内存空间抽象,程序通过访问虚拟内存中的地址,MMU将虚拟地址(Virtual Address)翻译成实际的物理地址(Physical Address) ,之后CPU即可操作实际的物理地址。
MMU具有如下功能:
- 保护内存: MMU给一些指定的内存块设置了读、写以及可执行的权限,这些权限存储在页表当中,MMU会检查CPU当前所处的是特权模式还是用户模式,只有和操作系统所设置的权限匹配才可以访问。
- 提供方便统一的内存空间抽象,实现虚拟地址到物理地址的转换:CPU可以运行在虚拟的内存当中,虚拟内存一般要比实际的物理内存大很多,使得CPU可以运行比较大的应用程序。
1.2 TLB介绍
TLB,Translation Lookaside Buffer,即转译后备缓冲器,也称页表缓存,里面存放的是一些页表文件(虚拟地址到物理地址的转换表),又称为快表技术。
当CPU第一次查找一个虚拟地址时,硬件通过3级页表(page table)得到最终的PPN(Physical Page Number),TLB会保存虚拟地址到物理地址的映射关系。这样在下一次访问同一个虚拟地址时,处理器通过查看TLB来直接返回物理地址,而不需要通过page table得到结果,从而提高地址转换的效率。
1.3 I/O映射函数
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。
那在程序编写的时候,如何进行物理内存和虚拟内存之间的转换呢?这就需要用到两个函数:ioremap和iounmap。
ioremap()
ioremap函数用将物理地址映射为虚拟地址。
1 2 3 4 5 6 7 8 9 10 11
| #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)); }
|
iounmap()
iounmap函数的作用是释放掉ioremap函数所做的映射,即反向操作,在卸载驱动的时候需要调用。
1 2 3 4 5
|
void iounmap (volatile void __iomem *addr)
|
1.4 I/O内存访问函数
在使用ioremap函数将物理地址转换成虚拟地址之后,理论上我们便可以直接读写 I/O 内存,但是为了符合驱动的跨平台以及可移植性,我们应该使用 linux 中指定的函数(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等)去读写 I/O 内存,而非直接通过映射后的指向虚拟地址的指针进行访问。读写 I/O 内存的函数如下:
1 2 3 4 5 6 7
| unsigned int ioread8(void __iomem *addr); unsigned int ioread16(void __iomem *addr); unsigned int ioread32(void __iomem *addr);
void iowrite8(u8 b, void __iomem *addr); void iowrite16(u16 b, void __iomem *addr); void iowrite32(u32 b, void __iomem *addr);
|
对于读I/O而言,他们都只有一个 __iomem 类型指针的参数,指向被映射后的地址,返回值为读取到的数据;
对于写I/O而言他们都有两个参数,第一个为要写入的数据,第二个参数为要写入的地址,返回值为空。
与这些函数相似的还有writeb、writew、writel、readb、readw、readl 等
1 2 3 4 5 6 7
| u8 readb(const volatile void __iomem *addr); u16 readw(const volatile void __iomem *addr); u32 readl(const volatile void __iomem *addr); void writeb(u8 value, volatile void __iomem *addr); void writew(u16 value, volatile void __iomem *addr); void writel(u32 value, volatile void __iomem *addr);
|
在 ARM 架构下,writex(readx)函数与 iowritex(ioreadx)有一些区别,writex(readx)不进行端序的检查,而 iowritex(ioreadx)会进行端序的检查。
2 程序编写
2.1 LED驱动程序
led驱动也是属于字符设备驱动的,之前介绍了新旧两种字符驱动的写法,本篇led驱动就按照新字符设置驱动的写法来编写。
关于新字符设备的驱动模块,可参考之前的文章:【i.MX6ULL】驱动开发2–新字符设备开发模板
这里再放一张新字符设备开发的模板框架
2.1.1 字符设备的基本框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| struct newchrled_dev{ dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; }; struct newchrled_dev chrdevled;
static int chrdevled_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t chrdevled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { return 0; } static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { return 0; } static int chrdevled_release(struct inode *inode, struct file *filp) { return 0; }
static struct file_operations chrdevled_fops = { .owner = THIS_MODULE, .open = chrdevled_open, .read = chrdevled_read, .write = chrdevled_write, .release = chrdevled_release, };
static int __init chrdevled_init(void) { return 0; }
static void __exit chrdevled_exit(void) { }
module_init(chrdevled_init); module_exit(chrdevled_exit);
MODULE_LICENSE("GPL"); MODULE_AUTHOR("xxpcb");
|
2.1.2 具体完善
1)GPIO寄存器宏定义
需要配置相关的寄存器,就要对照着LED这个GPIO的硬件按需配置。
有关GPIO的各种寄存器的使用原理介绍,请参考上篇文章的介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define CCM_CCGR1_BASE (0X020C406C) #define SW_MUX_SNVS_TAMPER3_BASE (0X02290014) #define SW_PAD_SNVS_TAMPER3_BASE (0X02290058) #define GPIO5_DR_BASE (0X020AC000) #define GPIO5_GDIR_BASE (0X020AC004)
static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_SNVS_TAMPER3; static void __iomem *SW_PAD_SNVS_TAMPER3; static void __iomem *GPIO5_DR; static void __iomem *GPIO5_GDIR;
|
- CCM 是用来进行时钟的使能,其寄存器包括CCGR0~CCGR6,因为LED用到GPIO属于GPIO5,它对应的时钟配置寄存器就是CCM_CCGR1
- MUX 是用来将IO复用为GPIO
- PAD 是用来配置IO的基本参数(驱动能力、压摆率、上下拉等)
- GPIO5_DR 数据寄存器,当GPIO为输出模式时,用来设置对应的高低电平
- GPIO5_GDIR 方向寄存器,用来设置输入还是输出
以上是先对这些需要使用的寄存器的地址声明宏定义(这些寄存器的地址可通过查阅i.MX6ULL数据手册得到),然后再声明对应的虚拟地址的指针,因为Linux开始MMU后,就不能直接对寄存器的地址直接操作了,需要使用映射后的虚拟地址。
2)GPIO硬件初始化
主要包括以下几步:
- 寄存器地址映射:将需要用的寄存器的物理地址映射为虚拟地址
- 使能GPIO1时钟:就是配置CCM_CCGR1寄存器
- 设置GPIO5_IO03的复用功能:配置MUX和PAD寄存器
- 设置GPIO5_IO03为输出功能:配置GPIO5_GDIR方向寄存器
- 初始默认关闭LED:配置GPIO5_DR数据寄存器
具体配置过程如下,主要这里使用”与”和”或”的位运算操作,来配置寄存器中对应位的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| static void led_hardware_init(void) { u32 val = 0; IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_SNVS_TAMPER3 = ioremap(SW_MUX_SNVS_TAMPER3_BASE, 4); SW_PAD_SNVS_TAMPER3 = ioremap(SW_PAD_SNVS_TAMPER3_BASE, 4); GPIO5_DR = ioremap(GPIO5_DR_BASE, 4); GPIO5_GDIR = ioremap(GPIO5_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); val |= (3 << 26); writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_SNVS_TAMPER3);
writel(0x10B0, SW_PAD_SNVS_TAMPER3);
val = readl(GPIO5_GDIR); val &= ~(1 << 3); val |= (1 << 3); writel(val, GPIO5_GDIR);
val = readl(GPIO5_DR); val |= (1 << 3); writel(val, GPIO5_DR); }
|
3)字符设备初始化
需要定义led字符设备结构体,来管理这个led设备。
1 2 3 4 5 6 7 8 9 10 11
| struct newchrled_dev{ dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; };
struct newchrled_dev chrdevled;
|
具体的led字符设备初始化流程:
- 初始化LED的GPIO(上面刚介绍)
- 创建设备号
- 初始化cdev字符设备
- 添加cdev字符设备
- 创建类
- 创建设备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| static int __init chrdevled_init(void) { led_hardware_init();
if (chrdevled.major) { chrdevled.devid = MKDEV(chrdevled.major, 0); register_chrdev_region(chrdevled.devid, chrdevled_CNT, chrdevled_NAME); } else { alloc_chrdev_region(&chrdevled.devid, 0, chrdevled_CNT, chrdevled_NAME); chrdevled.major = MAJOR(chrdevled.devid); chrdevled.minor = MINOR(chrdevled.devid); } printk("chrdevled major=%d,minor=%d\n",chrdevled.major, chrdevled.minor); chrdevled.cdev.owner = THIS_MODULE; cdev_init(&chrdevled.cdev, &chrdevled_fops); cdev_add(&chrdevled.cdev, chrdevled.devid, chrdevled_CNT);
chrdevled.class = class_create(THIS_MODULE, chrdevled_NAME); if (IS_ERR(chrdevled.class)) { return PTR_ERR(chrdevled.class); }
chrdevled.device = device_create(chrdevled.class, NULL, chrdevled.devid, NULL, chrdevled_NAME); if (IS_ERR(chrdevled.device)) { return PTR_ERR(chrdevled.device); } printk("chrdevled init done!\n"); return 0; }
|
4)LED亮灭控制
驱动程序中,对于LED的控制,可以分为两步。
第一步是接收和解析应用层发来的控制数据(0或1来控制亮灭),将控制参数传递给具体的开关led的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char databuf[1]; unsigned char ledstat; if(0 != copy_from_user(databuf, buf, cnt)) { printk("kernel recevdata failed!\n"); return -EFAULT; } ledstat = databuf[0];
if(ledstat == LEDON) { led_switch(LEDON); printk("led on!\n"); } else if(ledstat == LEDOFF) { led_switch(LEDOFF); printk("led off!\n"); } return 0; }
|
第二步就是根据指令参数,通过控制数据寄存器GPIO5_DR来实现GPIO的高低电平输出,从而实现LED的亮灭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) { val = readl(GPIO5_DR); val &= ~(1 << 3); writel(val, GPIO5_DR); } else if(sta == LEDOFF) { val = readl(GPIO5_DR); val|= (1 << 3); writel(val, GPIO5_DR); } }
|
5)驱动退出
驱动不再使用时,需要注销相关的设备:
首先释放掉这些地址映射:
1 2 3 4 5 6 7 8
| static void led_hardware_exit(void) { iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_SNVS_TAMPER3); iounmap(SW_PAD_SNVS_TAMPER3); iounmap(GPIO5_DR); iounmap(GPIO5_GDIR); }
|
具体的注销过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static void __exit chrdevled_exit(void) { led_hardware_exit(); cdev_del(&chrdevled.cdev); unregister_chrdev_region(chrdevled.devid, chrdevled_CNT);
device_destroy(chrdevled.class, chrdevled.devid); class_destroy(chrdevled.class); printk("chrdevled exit done!\n"); }
|
驱动程序基本就是这些,完整的程序见我的gitee仓库:https://gitee.com/xxpcb/imx6ull
2.2 LED应用程序
写完了驱动程序(BSP),还要写对应的应用程序(APP)。
目前的应用程序比较简短,因为在Linux中,一切皆文件,所以,对于LED的控制,就是通过向文件中写入0或1来实现LED的亮灭。
先来对0和1进行宏定义:
1 2
| #define LEDOFF 0 #define LEDON 1
|
然后就是main函数了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| int main(int argc, char *argv[]) { int fd, retvalue; char *filename; unsigned char databuf[1];
if(argc != 3) { printf("Error Usage!\r\n"); return -1; }
filename = argv[1];
fd = open(filename, O_RDWR); if(fd < 0) { printf("Can't open file %s\r\n", filename); return -1; } databuf[0] = atoi(argv[2]); retvalue = write(fd, databuf, sizeof(databuf)); if(retvalue < 0) { printf("write file %s failed!\r\n", filename); close(fd); return -1; }
retvalue = close(fd); if(retvalue < 0) { printf("Can't close file %s\r\n", filename); return -1; }
return 0; }
|
3 实验测试
3.1 程序编译与下载
再来复习一下基本步骤:
- ubuntu中通过gcc交叉编译器编译出led的驱动程序和应用程序
- 搭建局域网环境(电脑和linux板子连接到同一个路由器下,Linux板子以及烧录了镜像文件,能够正常运行)
- 通过tftp服务将两个文件发送到linux板子的对应目录中(/lib/modules/4.1.15目录)
- 进行字符设备的加载,以及文件读写测试(控制led亮灭)
程序的具体编译过程与之前的类似,这里不再赘述,可参考之前的文章(如这篇:【i.MX6ULL】驱动开发2–新字符设备开发模板)
3.2 实验现象
首先来看一下板子上LED的位置,如下图的电路上的标号D14处:
然后在串口中,按照之前介绍字符设备的加载流程,先加载led字符设备,然后就可以下向应用程序写1或0来控制led的亮灭了。
led点亮的效果如下:
4 总结
本篇主要介绍了如何通过操作寄存器来点亮i.MX6ULL开发板上的led,通过编写LED对应的驱动程序和应用程序,实现程序设计的分层。
因为Linux使用了MMU进行虚拟地址管理,因此在操作寄存器时,要进行地址映射后再操作。最后通过程序的实际测试,验证了led的亮灭功能。
本篇的完整程序见我的gitee仓库:https://gitee.com/xxpcb/imx6ull ,点击阅读原文可直达链接~