在Linux字符设备驱动的框架下,想要驱动一个LED灯是比较简单的。从总体结构来说,驱动程序可拆解为3个部分,即硬件操作、Linux字符设备驱动框架和I/O内存映射。硬件操作,无非就是对开发板外设寄存器的读写,对LED灯来讲就是操作与其连接的GPIO相关的寄存器,设置复用、上下拉、方向、电平等等,这个需要依赖开发板核心芯片的参考手册。字符设备驱动框架是Linux内核提供的一套固有的框架,简单的字符设备驱动非常成套路,其代码结构几乎是固定的,开发者可直接套用现成的模板,它的核心内容就是实现开关设备、读写设备等内核接口以实现对硬件的控制,为应用层接口的调用提供服务。I/O内存映射是利用MMU将I/O口的寄存器物理地址映射到内核的虚拟地址空间中,这样对内核驱动而言可以很方便地操作映射后的虚拟地址,从而完成对真实寄存器的设置、读写等。我们直接看下代码,这里照搬了正点原子提供的适配IMX6ULL开发板的LED灯驱动代码,可以稍加分析。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 200 // 主设备号
#define LED_NAME "led" // 设备名称
#define LEDOFF 0 // 关灯指令
#define LEDON 1 // 开灯指令
/* GPIO1_IO03的寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C) // 时钟
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) // 复用
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) // 物理参数
#define GPIO1_DR_BASE (0X0209C000) // 数据
#define GPIO1_GDIR_BASE (0X0209C004) // 方向
/* I/O内存映射后的寄存器虚拟地址 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/* 根据命令控制LED灯的开、关 */
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR); // 读GPIO1_DR寄存器原值
val &= ~(1 << 3); // GPIO1_DR寄存器的位3清零,即GPIO1_IO03输出低电平,灯亮
writel(val, GPIO1_DR); // 寄存器写值
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3); // GPIO1_DR寄存器的位3置位,即GPIO1_IO03输出高电平,灯灭
writel(val, GPIO1_DR);
}
}
/* 打开设备 */
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/* 读设备数据 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/* 向设备写数据 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt); // 实现数据从用户空间传到内核
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if(ledstat == LEDON) {
led_switch(LEDON);
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
/* 关闭设备 */
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备接口 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/* LED初始化 */
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* 寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); // 这几个寄存器都是32位
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/* 使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
/* GPIO1_IO03的复用功能 */
writel(5, SW_MUX_GPIO1_IO03); // 复用为GPIO
/* 设置IO属性 */
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 设置GPIO1_IO03为输出 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
/* 默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 注册字符设备驱动 */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
static void __exit led_exit(void)
{
/* 取消I/O内存映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 注销字符设备驱动 */
unregister_chrdev(LED_MAJOR, LED_NAME);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XXX");
以上代码是一个很简单的字符设备驱动框架示例,开发者需要做的就是根据框架填充file_operations结构体给出的主要接口。由于仅仅是控制LED亮灭,这里只实现了write接口就足矣,当然也可以通过自定义命令参数实现ioctl接口来控制,相对write来说麻烦一点。按照习惯,在driver初始化函数中,进行LED的初始化,也就是GPIO1_IO03的初始化,这个就按照时钟配置、复用配置(一个I/O可复用为多个功能引脚)、IO属性设置(电气物理参数)、方向(输入?输出?)以及电平值大小。这些寄存器怎么配置?各个位代表什么意思?直接在IMX6ULL的官方参考手册中查询即可,非常详细。这里需要注意的就是,把GPIO1_IO03的这几个寄存器作了I/O内存映射,这个过程调用了ioremap,本质是由MMU完成的,如此一来,在内核空间操作映射后的寄存器虚拟地址,就相当于操作硬件寄存器了。当然,在设备注销时要记得取消初始化时建立的I/O内存映射关系。