Linux驱动开发之字符设备驱动

目录

  • 一,字符设备驱动开发框架
  • 二,申请设备号和创建文件结点
    • 1.申请设备号
    • 2.创建设备结点
    • 3.在驱动中实现文件IO的接口,让应用程序可以调用文件IO
  • 三,用户控制驱动和驱动控制硬件方式
    • 1.用户空间和内核空间的数据交互
    • 2.用户操作驱动与控制外设的关系
  • 四,编写字符设备驱动的步骤和规范
    • 1.驱动编写步骤
    • 2.驱动编写规范
  • 五,编写规范LED驱动实例


一,字符设备驱动开发框架


Linux驱动开发之字符设备驱动_第1张图片
开发字符设备驱动的要素:

  • 必须有一个设备号,用于在众多设备驱动中进行区分。
  • 用户必须知道设备驱动对应的设备结点(设备文件,在Linux中所有设备都是文件)。
  • 对设备操作其实就是对文件进行操作,应用空间中操作open(),read()…的时候,实际在驱动代码中有对应的open(),read()…

二,申请设备号和创建文件结点


1.申请设备号

申请设备号函数register_chrdev(unsigned int major, const char * name, const struct file_operations * fops)

头文件包含:#include

参数
参数1:主设备号
设备号(32bit) = 主设备号(高12bit) + 次设备号 (低20bit)

主设备号的给定方式有两种:

 1. 动态分配   ------>     参数1直接给定0
 2. 静态分配   ------->    指定一个整数(通常需要用命令cat /proc/devices 查看被占用的主设备号,避免定义重复的主设备号)

参数2:描述一个设备信息,可自定义

参数3:文件操作对象file_operation指针

返回值:
正确返回0,错误返回负数

注销设备号函数:unregister_chrdev(unsigned int major, const char * name) ------------- 参数同上



2.创建设备结点

  • 手动创建 ------- 缺点:/dev/目录中的文件都是在内存中,断电后这些文件会消失

手动创建文件结点:mknod /dev/设备名 类型 主设备号 次设备号

比如:mknod /dev/chr0 c 250 0 --------可以通过ls /dev/chr0 -l查看

  • 自动创建(通过udev/mdev机制):创建类 + 创建设备

创建类函数struct class *class_create(owner, name)

参数
参数1:THIS_MODULE,类似于C++中的this指针

参数2:类的名字,字符串格式,自定义

返回值
返回一个class指针

销毁类函数void class_destroy(struct class * cls)

创建设备文件

创建设备函数device_create(struct class * cls, struct device * parent, dev_t devt, void * drvdata, const char * fmt,...)

头文件包含#include

参数:
参数1:class结构体,class_create();调用之后的返回值

参数2:表示父类,一般填NULL

参数3:设备号类型,一般由MKDEV(ma,mi)宏来定义,ma为主设备号,mi为次设备号

参数4:私有数据,一般填NULL

参数5和参数6:格式化可变参数,字符串格式,表示设备结点中的名字

销毁设备函数device_destroy(struct class * class, dev_t devt)


3.在驱动中实现文件IO的接口,让应用程序可以调用文件IO

file_operations结构体
文件操作对象,实质就是一些函数指针的集合,需要我们自己去实现。

Linux驱动开发之字符设备驱动_第2张图片
先填充结构体
函数名自定义,要与实现的函数名对应即可。

const struct file_operations my_fops = {
	.open = chr_dev_open,
	.read = chr_dev_read,
	.write = chr_dev_write,
	.release = chr_dev_close,

};

根据需求实现函数
举例:(没有意义,只是举例说明)

int chr_dev_open (struct inode *inode, struct file *filp)
{
	printk("-----%s-----\n",__FUNCTION__);
	return 0;

}

int chr_dev_close (struct inode *inode, struct file *filp)
{
	printk("-----%s-----\n",__FUNCTION__);
	return 0;

}


三,用户控制驱动和驱动控制硬件方式


1.用户空间和内核空间的数据交互

copy_to_user(void __user * to, const void * from, unsigned long n)

将数据从内核空间拷贝到用户空间,一般在驱动中的xxx_read()中用。

参数
参数1:应用空间的buffer

参数2:内核空间的buffer

参数3:个数

返回值
大于0,表示出错,剩下多少个没拷贝成功
等于0,表示正确

copy_from_user(void * to, const void __user * from, unsigned long n)

将数据从用户空间拷贝到内核空间,一般在驱动中的xxx_write()中用。
参数
参数1:内核空间的buffer

参数2:应用空间的buffer

参数3:个数

头文件包含:#include

Linux驱动开发之字符设备驱动_第3张图片

2.用户操作驱动与控制外设的关系

Linux驱动开发之字符设备驱动_第4张图片
内核驱动中是通过虚拟地址转换操作寄存器

void* ioremap(cookie, size);     //映射函数
void iounmap(void __iomen*addr);  //去映射函数

映射函数参数
参数1:物理地址

参数2:长度

返回值:虚拟地址

去映射函数参数:映射成功的虚拟地址

头文件包含:#include


四,编写字符设备驱动的步骤和规范


1.驱动编写步骤

1.1实现模块的加载和卸载入口函数

module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");				// 描述模块的许可证

1.2在模块加载函数的入口函数中实现下面步骤

[ 1 ] 申请主设备号(内核中用于区分和管理不同的字符设备)

register_chrdev(0, "led_dev_test", &my_fops);  //动态分配主设备号,且分配成功返回主设备号

[ 2 ] 创建设备节点(为用户提供一个可操作的文件接口)

struct class* class_create(THIS_MODULE, "dev_class");
struct devices* device_create(led_dev->myclass, NULL, MKDEV(led_dev->dev_major,0), NULL, "led%d",0);

[ 3 ] 硬件的初始化

  • 地址的映射
ioremap(GPJ0CON, GPJ0_SIZE);
  • 中断的申请
  • 实现硬件的寄存器的初始化

[ 4 ] 实现file_operations


const struct file_operations my_fops = {
	.open = led_dev_open,
	.read = led_dev_read,
	.write = led_dev_write,
	.release = led_dev_close,

};

2.驱动编写规范

2.1 面向对象的编程思想
设计一个结构体类型,描述一个设备的所有信息

struct led_desc{
	unsigned int dev_major;        //主设备号
	struct class* myclass;          
	struct device* mydev;         //创建设备文件的类和设备
 	void *reg_virt_base;          //表示进行虚拟映射后寄存器地址的基准值
};       
struct led_desc *led_dev;       //声明全局的设备对象     
        

在模块加载函数static int __init led_dev_init(void) //一般都是申请系统资源中实例化全局的设备对象

	//实例化全局的设备对象----分配空间
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);  //GFP_KERNEL表示当前内存不够用的时候,该函数就会一直阻塞
	if(led_dev == NULL)
	{
		printk(KERN_ERR "malloc error.\n");
		return -ENOMEM;
	}

2.2出错处理
在某个位置出错了,要将之前申请的资源全部释放。

2.3操作寄存器地址的方式

  • 传统方式
volatile unsigned long* gpjocon;
*gpjocon &= ~(0xf << 12);
  • 内核推荐方式readl();/writel();

readl()作用:从给定的地址中读取地址空间的值,即解引用

u32 readl(const volatile void__iomen* addr);

writel()作用:把value的值写到给定的addr地址中

void writel(unsigned long value ,const volatile void__iomen* addr);

五,编写规范LED驱动实例


1.先看原理图和有关寄存器

Linux驱动开发之字符设备驱动_第5张图片

2.驱动代码:

#include 		
#include 			
#include 
#include 
#include 
#include 
#include 

struct led_desc{
	unsigned int dev_major;        //主设备号
	struct class* myclass;          
	struct device* mydev;         //创建设备文件的类和设备
 	void *reg_virt_base;          //表示进行虚拟映射后寄存器地址的基准值
}*led_dev;                        //声明全局的设备对象

#define GPJ0CON 0xE0200240       // GPJ0CON寄存器物理地址
#define GPJ0_SIZE  8

static int kernel_val = 555;     //内核空间定义的一个值,可以看成是一段4字节的空间,模拟和用户空间进行数据交互
ssize_t led_dev_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
	printk("-----%s-----\n",__FUNCTION__);
	int ret;
	ret = copy_to_user(buf,&kernel_val, count);
	if(ret > 0)
	{
		printk("copy_to_user error.\n");
		return -EFAULT;
	}	
	return 0;
}

ssize_t led_dev_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
	printk("-----%s-----\n",__FUNCTION__);
	int ret;
	int value;

	ret = copy_from_user(&value,buf,count);
	if(ret > 0)
	{
		printk("copy_from_user error.\n");
		return -EFAULT;
	}
	if(value)         //根据用户空间传过来的value实现LED的亮灭
	{
		writel(readl(led_dev->reg_virt_base + 4) & ~(1<<3),led_dev->reg_virt_base + 4);     //led亮
	}
	else
	{
		writel(readl(led_dev->reg_virt_base + 4) | (1<<3),led_dev->reg_virt_base + 4);    //led灭
	}
	
	return 0;
}


int led_dev_open (struct inode *inode, struct file *filp)
{
	printk("-----%s-----\n",__FUNCTION__);
	return 0;

}

int led_dev_close (struct inode *inode, struct file *filp)
{
	printk("-----%s-----\n",__FUNCTION__);
	return 0;

}

const struct file_operations my_fops = {
	.open = led_dev_open,
	.read = led_dev_read,
	.write = led_dev_write,
	.release = led_dev_close,

};

static int __init led_dev_init(void)  //一般都是申请系统资源
{	
	int ret;
	
	//0-实例化全局的设备对象----分配空间
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
	if(led_dev == NULL)
	{
		printk(KERN_ERR "malloc error.\n");
		return -ENOMEM;
	}
	
	// 1-申请设备号
	led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);  //动态分配主设备号,且分配成功返回主设备号

	if(led_dev->dev_major < 0)
	{
		printk(KERN_ERR "register_chrdev error.\n");
		ret = -ENODEV;
		goto err_0;
	}

	// 2-创建设备结点
	led_dev->myclass = class_create(THIS_MODULE, "dev_class");
	if(IS_ERR(led_dev->myclass))  //IS_ERR判断指针是否出错
	{
		printk(KERN_ERR "class_create error.\n");
		ret = PTR_ERR(led_dev->myclass);  //PTR_ERR将指针的错误原因转换成错误码
		goto err_1;
	}
	
	led_dev->mydev = device_create(led_dev->myclass, NULL, MKDEV(led_dev->dev_major,0), NULL, "led%d",0);
	if(IS_ERR(led_dev->mydev))  
	{
		printk(KERN_ERR "device_create error.\n");
		ret = PTR_ERR(led_dev->mydev); 
		goto err_2;
	}
	
	// 3-硬件初始化
	//对地址进行映射
	led_dev->reg_virt_base = ioremap(GPJ0CON, GPJ0_SIZE);
	if(led_dev->reg_virt_base == NULL)  
	{
		printk(KERN_ERR "ioremap error.\n");
		ret = -ENOMEM; 
		goto err_3;
	}
	
	//gpio的输出功能的配置
	u32 value = readl(led_dev->reg_virt_base);
	value &= ~(0xf<<12);   //先清零
	value |= (0x1<<12);    
	writel(value,led_dev->reg_virt_base);  //设置GPJ0的[15:12]为输出模式

	return 0;

   //错误处理
	err_3:
		device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
	err_2:
		class_destroy(led_dev->myclass);
	err_1:
		unregister_chrdev(led_dev->dev_major,"led_dev_test");
	err_0:
		kfree(led_dev);
}

static void __exit led_dev_exit(void)
{
	//一般都是释放资源
	iounmap(led_dev->reg_virt_base);
	device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
	class_destroy(led_dev->myclass);
	unregister_chrdev(led_dev->dev_major,"led_dev_test");
	kfree(led_dev);
	printk("--------%s-------\n",__FUNCTION__);
}

module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");				// 描述模块的许可证

3.应用层代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
	int fd;
	int value;

	fd = open("/dev/led0",O_RDWR);    //根据 device_create的最后一个参数确定设备结点的名字
	if(fd < 0)
	{
		perror("open");
		exit(-1);
	}
	read(fd,&value,4);
	printf("---USER----:%d\n",value);
    //应用程序去控制LED的亮灭
    while(1)
    {
        value = 0;
        write(fd,&value,4);
        sleep(1);
        
        value = 1;
        write(fd,&value,4);
        sleep(1);
    }
	close(fd);
	return 0;
}

最终能实现LED的闪烁。

你可能感兴趣的:(#,创客学院Linux驱动开发,嵌入式,字符设备驱动,Linux驱动开发,驱动编写)