IMX6ULL开发板驱动LED灯的代码分析

在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内存映射关系。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值