Linux驱动入门必须get的知识点-02.点亮世界的那盏灯—LED驱动的实现

0.知识点速览

LED驱动属于字符设备驱动,所谓字符设备驱动就是通过字节流进行读写的驱动,Linux下包含三大类驱动,分别是字符设备驱动,块设备驱动和网络设备驱动,字符设备驱动是最常用的驱动。
一个完整的字符设备编写流程如下:
实例化设备对象、申请设备号、创建设备文件、实现应用层的接口、完善异常处理。

  • 实例化设备对象
    内核中大量使用了面向对象的思想,所以我们在设计驱动时,也要遵循此原则。将设备参数封装到一个结构体中,然后定义一个全局指针,在模块入口中将其进行实例化。

  • 申请设备号
    所谓设备号可以理解为内核用来标识设备的编号。
    该编号大小为 32bit,其中高 12bit 叫做主设备号,低 20bit 叫做次设备号组成。
    主设备号 用来表示一类设备的编号,比如摄像头、LED、按键等
    次设备号 用来区别该类设备中所属的具体编号,比如区别前置摄像头和后置摄像头。

    设置主设备号有两种方法:动态申请和静态申请,通过 register_chrdev 函数进行申请。
    动态申请register_chrdev 第一个参数填 0,由内核动态进行分配设备号
    静态申请register_chrdev 第一个参数填人为指定的设备号,该设备号必须是一个自然数且不能和已有设备号发生冲突

    申请的设备号通过 unregister_chrdev 函数进行释放
    完整的设备号可以通过 MKDEV 这个宏获得,宏内部是通过将主设备号左移20位然后或上次设备号来得到完整的设备号。

  • 创建设备文件
    创建设备文件分为手动创建和自动创建两种
    手动创建
    mknod /dev/设备名 类型 主设备号 次设备号
    例如:mknod /dev/led c 280 0
    这里的设备号是正是我们上一阶段指定或得到。
    因为/dev目录里的文件是存储在内存中的,所以掉电后/dev里手动创建的文件会丢失
    自动创建:通过udev/mdev机制
    首先通过 class_create 函数创建一个类,然后通过 device_create 函数创建一个设备节点

    创建好的设备文件通过 class_destroy 和 device_destroy 进行销毁

  • 实现应用层的接口
    即实现 file_operations 结构体中的与所需操作对应的函数指针
    例如 open、release(close)、read、write等,应用层的调用接口的本质就是通过函数指针间接调用这些函数
    内核空间作为独立空间在于应用空间进行数据交互时应尽可能使用内核提供的安全接口。例如:

    • copy_to_user
      内核传递数据给应用,对应应用层的read接口
    • copy_from_user
      应用传递数据给内核,对应应用层的write接口
    • ioremap
      内核通过mmu将物理地址映射为更安全的虚拟地址,因此偏移的虚拟地址也会对应到同样偏移的物理地址上。
    • iounmap
      与ioremap对应,进行虚拟地址的销毁动作
    • IS_ERR
      该宏用来判断一个指针是否非法,比手动判断指针是否为 NULL 要更为安全
    • PTR_ERR
      该宏可以返回一个非法指针的错误号,这样可以具体确定出错的原因,性质上与应用层的errno类似。
    • readx
      从地址读数据,x代表读取的数据类型,例如 l 表示 long 类型,readl 就是读一个32bit的数回来
    • writex
      与 readx 对应,往地址写入数据。read write 接口内都进行了地址的非法判断,因此使用接口操作要比我们直接操作地址要安全的多
  • 完善异常处理
    驱动编写是一项非常严谨的工作,它作为被调用的底层操作,容错处理必须要强,所以在进行以上操作时出错时,应尽可能的通过内核提供的接口来锁定出错原因,向用户打印对应的提示信息并进行对应的容错处理。

1.LED驱动代码

//头文件不需要刻意去记忆,在使用某个API时,直接看该API所处文件,该文件一般就是我们所需的头文件
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 

//这里我们没有使用设备树,设备树将从下一节开始使用,这里演示没有设备树的情况下的操作
//LED对应GPIO的模式寄存器的地址,模式寄存器与输出寄存器相连,所以这里我们记录首地址即可
#define LED_ADDR_BASE 0x11000c40
#define LED_ADDR_SIZE 8

typedef struct
{
	unsigned int id;		//记录主设备号
	struct class *cls;		//类指针
	struct device *dev;		//设备文件指针
	void *reg_addr_base;	//存放led对应gpio的寄存器映射后的地址
	unsigned int val;		//记录led操作的状态,如果要确保数据的真实性,最好还是直接读对应寄存器的值
} led_desc_t;

//创建设备对象的全局变量指针,在入口函数中进行实例化,通过这种方法来体现内核面向对象的思想
static led_desc_t *led2;

//简化操作,因为基地址的void *类型的指针,所以这里的便宜需要加上4
#define LED2_CON (led2->reg_addr_base)
#define LED2_BIT (led2->reg_addr_base + 4)

//open操作,默认灯为熄灭状态,具体参数的含义后期用到的时候再慢慢展开详细说明
int led_drv_open(struct inode *inode, struct file *fp)
{
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);

	writel(readl(LED2_BIT) & ~(0x01<<7), LED2_BIT);
	
	return 0;
}
//close操作
int led_dev_close(struct inode *inode, struct file *fp)
{
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);
	
	return 0;
}
//read操作, ssize_t的源类型是一个32位有符号整数,前面三个参数对饮应用层的read函数的三个参数,这里直接将led状态返回到应用空间
ssize_t led_drv_read(struct file *fp, char __user *buf, size_t count, loff_t *fops)
{
	int ret;
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);

	if ((ret = copy_to_user(buf,&led2->val, count)) > 0){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		return -EFAULT;	//出错返回一个故障的错误码
	}

	return 0;
}
//write操作,参数同应用层write接口,这里实现用户传值控制led状态
ssize_t led_drv_write(struct file *fp, const char __user *buf, size_t count, loff_t *fops)
{
	int ret;
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);

	if ((ret = copy_from_user(&led2->val, buf, count)) > 0){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		return -EFAULT;
	}

	if (led2->val){
		writel(readl(LED2_BIT) | (0x01<<7), LED2_BIT);
	}
	else{
		writel(readl(LED2_BIT) & ~(0x01<<7), LED2_BIT);
	}

	return 0;
}

//与用户交互的接口,最常用是以下四种操作
const struct file_operations led_fops = {
	.open = led_drv_open,
	.release = led_dev_close,
	.read = led_drv_read,
	.write = led_drv_write
};

static int __init led_dev_init(void)
{
	int ret;
	//printk是独立于内核调度的函数,用于打印调试信息,KERN_DEBUG为打印等级,可以通过打印等级进行筛选打印信息
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);
	
	//与应用层得的malloc类型,还有kzalloc等函数,GFP_KERNEL表示动态申请内存。这里是将LED类实例化
	if (IS_ERR(led2 = kmalloc(sizeof(led_desc_t), GFP_KERNEL))){
		printk(KERN_ERR "%s\n", __FUNCTION__);	//打印的等级为KERN_ERR,比KERN_DEBUF要高
		return -ENOMEM;	//出错返回一个内存空间不足的错误码
	}
	//动态申请设备号,设备号标识名称为led,可通过cat /proc/devices进行查看,与用户交互的fops为led_fops
	if ((led2->id = register_chrdev(0, "led", &led_fops)) < 0){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		ret = -ENODEV; //出错返回一个设备号申请失败的错误码
		goto err_0;	//通过goto完成前面动作的卸载操作
	}
	//创建类,THIS_MODULE 类似于面向对象中的 this 指针, led 是类的标识名
	if (IS_ERR(led2->cls = class_create(THIS_MODULE, "led"))){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		ret = PTR_ERR(led2->cls);	//返回错误码,通过 PTR_ERR 获取空指针出错的真正原因
		goto err_1;
	}
	//第一个参数是前面申请类, 第二参数是父节点,这里我们没有就填NULL, 第三个是设备号,第四个是私有数据,也就是传入的参数,这里我们也没有也填一个NULL, 第五个是设备文件的标识名,通过ls /dev/led 可以查看得到
	if (IS_ERR(led2->dev = device_create(led2->cls, NULL, MKDEV(led2->id, 0), NULL, "led"))){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		ret = PTR_ERR(led2->dev);
		goto err_2;
	}
	//地址的重映射,连续映射8个字节的地址,包括gpio模式寄存器地址和输出寄存器地址
	if ((led2->reg_addr_base = ioremap(LED_ADDR_BASE, LED_ADDR_SIZE)) < 0){
		printk(KERN_ERR "%s\n", __FUNCTION__);
		ret = PTR_ERR(led2->reg_addr_base);
		goto err_3;
	}
	
	//配置模式寄存器为输出模式
	writel((readl(LED2_CON) & ~(0x01<<28)) | (0x01<<28), LED2_CON);

	printk(KERN_DEBUG "led driver init end......\n");
	return 0;
	
//出错判断出口,一一将出错前的动作都卸载掉
err_3:
	device_destroy(led2->cls, MKDEV(led2->id, 0));
err_2:
	class_destroy(led2->cls);
err_1:
	unregister_chrdev(MKDEV(led2->id, 0), "led");
err_0:
	kfree(led2);

	return ret;
}

static void __exit led_dev_exit(void)
{
	printk(KERN_DEBUG "_____%s_____\n", __FUNCTION__);

	//按与申请时相反的顺序进行释放
	iounmap(led2->reg_addr_base);
	device_destroy(led2->cls, MKDEV(led2->id, 0));
	class_destroy(led2->cls);
	unregister_chrdev(MKDEV(led2->id, 0), "led");
	kfree(led2);

	printk(KERN_DEBUG "led driver exit end......\n");
}

module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

你可能感兴趣的:(#,linux驱动)