Linux Kernel Driver 设备驱动 之 字符设备

设备驱动的作用

操作和管理硬件设备

给用户提供访问操作硬件的接口

linux内核设备驱动的分类

字符设备驱动

特性:设备的访问采用字节流形式

包含的设备:串口(蓝牙,GPS,GPRS,zigbee等),

按键,鼠标,触摸屏,LCD,声卡,各种传感器

块设备驱动

特性:设备的访问采用数据块形式,比如512byte,1K,4K

包含的设备:硬盘,U盘,SD卡,TF卡,Nand,Nor,EMMC等

网络设备驱动

特性:设备的访问结合TCP/IP协议栈

包含的设备:有线和无线

字符设备驱动

字符设备文件本身就是字符设备硬件,存放于 linux 系统的  /dev/ 目录下

设备号

设备号包含主设备号与次设备号

设备号的数据类型:dev_t(本质上是unsigned int)

设备号的高12位保存主设备号信息

设备号的低20位保存次设备号信息

设备号相关操作宏:

设备号=MKDEV(主设备号,次设备号);

主设备号=MAJOR(设备号);

次设备号=MINOR(设备号);

主设备号作用:应用程序根据设备文件的主设备号在茫茫的内核驱动中找到适合自己的设备驱动

注意:一个设备驱动仅仅根一个主设备号进行绑定关联,以串口为例,主设备号都是204,说明四个硬件串口共享一个设备驱动

次设备号作用:如果多个硬件设备共享一个驱动,那么驱动再根据次设备号来分区具体的硬件设备个体

编程步骤

设备号对于内核来说是一种宝贵的资源,所以,如果使用,驱动层需要向内核进行申请。

这个函数的功能,就是向内核设备申请一个设备号。

参数

dev: 出参,待申请的设备号,内存空间,去hold申请成功的设备号。

baseminor: 希望的起始次设备号,期望值,可能有用,可能没用。一般给0即可。

count: 次设备号的个数。如果次设备号从0开始,个数为3个,那么次设备号便分别为0,1,2

name: 设备名称,这个设备名称不是设备文件名(设备文件名就是在/dev/目录下的对应的文件),这个 name 将出现在 /proc/devices 中。

返回值

成功返回0,失败,返回非零,一般都是返回 <0 的值。

释放

dev: 申请的设备号

count: 次设备号的个数

申请完成之后,需要注意,我们如果获取设备号信息呢?

1. 编程日志记录信息  MAJOR(),  MINOR()

2. 系统查询     cat /proc/devices

两列,左边一列为主设备号,右边一列为设备名称,即alloc时传入的name。

这个很有用,因为,驱动写完了,并不代表就完事,设备就可以使用了,其实不是,后面还有一个操作,要用到设备号信息。

以上仅仅是设备号的处理,设备号仅仅是第一步,后面还有其他的事儿要做。

既然是设备文件,作为“文件”,用户操作自然是通过文件的那一套操作系统API,操作设备。也即 open,write,read,close 等等。我们要实现设备的这些功能,至少要open close两个操作,否则,驱动便失去了存在的意义。我们要把硬件的操作对接上系统,我们要怎么做呢?

数据结构

1. cdev

2. 文件操作

这个结构套结构,对象套对象,还挺复杂,不过我们不要care 太多。

看看这些函数名字,也都很熟悉,我们要挂载的callback 主要比如 open,release(注意不是close),read,write等等。

底层的操作对象式 struct file,到了应用层,便是抽象成一个fd了。这个数据结构,跟那个net device的数据结构一样,确实是个big mistake。所有的东西都揉在了一起,字符设备,块设备,都没有区分开,一堆这个用的那个用不到的操作,都杂糅一起......

所以,我们要定义一个 cdev 结构,然后定义一个 file_operations 结构,然后把这两个结构关联到一起。当然,还要把这个cdev 关联到 dev 设备号上。

file_operations,我们先不考虑太多别的操作,先 open close,也就是 open realse,但是的,试验阶段,没得开发版,手册之类的,那就空实现,打印一句信息即可。

关联

看参数,就知道是谁关联谁了,再看函数名,init add,就知道关联的顺序了。

还有就是注册了之后,要卸载,接口:

试验代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>       // struct file_operations
#include <linux/cdev.h>     // struct cdev


MODULE_DESCRIPTION("Frocheng: Driver for DEMO!");
MODULE_AUTHOR("Frodo Cheng");
MODULE_LICENSE("GPL");
MODULE_VERSION("V0.0.1");

static char* name = "hello";

// 应用程序调用关系:open->软中断->sys_open->led_open
static int hello_open(struct inode *inode,
                        struct file *file)
{
    printk("===[frocheng]===[%s]===[%s]===[%d]===\n",__FILE__, __func__, __LINE__);
    return 0; // 执行成功返回0,失败返回负值
}

// 应用程序调用关系:close->软中断->sys_close->led_close
static int hello_close(struct inode *inode,
                        struct file *file)
{
    printk("===[frocheng]===[%s]===[%s]===[%d]===\n",__FILE__, __func__, __LINE__);
    return 0; //执行成功返回0,失败返回负值
}

// 定义初始化LED的硬件操作对象
// open,release一旦加载内存中,静静等待着应用程序来调用
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,     // 打开设备
    .release = hello_close  // 关闭设备
};

// 定义字符设备对象
static struct cdev hello_cdev;

// 定义设备号对象
static dev_t dev;

static int __init hello_init(void)
{
	int rc = -1;
    printk("===[frocheng]===[%s]===[%s]===[%d]===[Hello !]===\n",__FILE__, __func__, __LINE__);

    // 申请设备号
    rc = alloc_chrdev_region(&dev, 0, 1, name);
	if (rc != 0)
	{
    	printk("===[frocheng]===[%s]===[%s]===[alloc_chrdev_region error with %d]===\n",
    	    __FILE__, __func__, rc);
		// hello 驱动模块加载失败,因为并没有申请到字符神资源,驱动加载失败。
		return -1;
	}

    printk("===[frocheng]===[%s]===[%s]===[name=%s, major=%u, minor=%u]===\n",
        __FILE__, __func__, name, MAJOR(dev), MINOR(dev));
    
    // 初始化字符设备对象
    cdev_init(&hello_cdev, &hello_fops);

    // 注册字符设备对象
    cdev_add(&hello_cdev, dev, 1);
   
    return 0;
}

static void __exit hello_exit(void)
{
    // 卸载字符设备对象
    cdev_del(&hello_cdev);
    
    // 释放设备号
    unregister_chrdev_region(dev, 1);
    printk("===[frocheng]===[%s]===[%s]===[%d]===[Bye bye...]===\n",__FILE__, __func__, __LINE__);
}

module_init(hello_init);
module_exit(hello_exit);

这个只是驱动,驱动的加载,并不是结束,而是刚刚开始,如果用这个驱动呢?

我们实现了open 以及 close。前文也介绍过系统调用的过程,

应用层 open 到 GLIBC 中 触发软中断,取寄存器中断号,跳转异常向量表,取对应的系统调用函数指针,跳转到内核空间,一层层往下,直到驱动。

测试程序

我们这边要使用这个驱动,还是要写一个测试程序,如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

	// open->软中断->sys_open->hello_open
	fd = open("/dev/hello", O_RDWR);
	if (fd < 0)
	{
		perror("open");
		return -1;
	}

	sleep(3);

	// close->软中断->sys_close->hello_close
	close(fd);
    return 0;
}

test.c按照正常文件编译 gcc test.c -o hello_test 即可。

加载 hello.ko,然后运行 hello_test 发现失败了,open失败。为什么呢? /dev/hello 没有啊... 

所以说,我们还缺少了重要的一步,那就是创建设备文件。

创建设备文件

创建设备文件语法:

mknod /dev/设备文件名 c 主设备号  次设备号

说明:

第一. 设备文件名,跟设备名,也就是代码里面的name不是一回事。

第二. 主次设备号,哪儿来呢?cat /proc/devices  |grep 设备名name,也就是 hello。

注意,查询之前,必须先加载 hello.ko,否则是没有的。

我们拿到主设备号即可,次设备号,用0,然后手动创建节点。

整体运行结果展示:

后续说明

各种名称

insmod xxx.ko 以及 rmmod xxx  : xxx 表示的是驱动模块名称。

设备名称: 就是 name参数以及 cat /proc/devices 中的,这是一起绑定的。

设备文件名称: /dev/ 目录下的文件的文件名。

以上三个名字都是相互独立的,设备文件的文件名,跟设备名称是mknod 中的设备号进行关联的。

设备名称跟该设备的驱动模块名,没什么直接的联系,可以一样,可以不一样,就看是否加载成功即可。

另外,测试程序,是个进程,是可以多个同时运行的,但是 设备驱动层面,没有做互斥,都可以同时多个去打开,多个去同时处理,这个可能对于某些字符设备而言,并不是期望的。我们的实验并没有做处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值