Linux驱动相关的基础知识

1. 什么是Linux驱动?
在各种书上都有对于驱动程序的介绍,大致的意思都为
处于内核与硬件之间的,由操作系统管理的,控制硬件设备的程序;是硬件和内核之间的桥梁
在这里插入图片描述
我们这里所说的驱动都是软件驱动的概念,是用来驱动硬件,或是提供硬件驱动方法的程序;
记住:是内核和硬件之间的桥梁

2. Linux内核的分类:
主要分为三大类:字符设备、块设备、网络设备
三类设备编写驱动的方式不太一样,目前只接触了字符设备和网络设备的内容;
字符设备: IO传输过程中以 字节为单位 进行传输;用户对于字符设备发出读写请求时,实际硬件读写操作一般紧接着发生(同步性
块设备:数据传输以块为单位(根据文件系统不同,一个块的大小有所不同,大多为4K);异步
前两个在linux中都通过
对应的设备文件进行访问
(字符设备文件、块设备文件)

**网络设备:**通过socket接口函数进行访问;

3. 字符设备驱动
开发阶段一般使用 驱动模块 来完成驱动功能的调试;
这涉及到:模块的编写、模块的编译、模块的使用、模块的卸载等问题;下面的456对应;
发布阶段一般将驱动程序编译进内核,这涉及到以下几个内容的修改,在修改后重新编译内核既能完成将驱动与内核融为一体;

4. 模块的编写
模块的编写是我们实现功能的过程,当然在这个过程中需要对于linux的驱动框架以及很多相关知识比如设备树、子系统等内容。
模块的框架请参考我的其他文章;

5. 模块的编译
编译工具的选择:这取决于我们的驱动所应用的平台,如果是x86的ubuntu,那么就使用对应的gcc即可;如果是嵌入式arm平台,就要选择对应的arm-linux-gcc编译器进行编译;这样驱动程序才能够在对应的平台下使用;

对于编译存在几个概念需要我们理清楚:
内部编译: 将模块的源文件.c文件,放到内核源码中进行编译;需要对内核文件:Kconfig、Makefile、make menuconfig进行修改;
外部编译:将内核模块源文件放到内核源码外进行编译
静态编译:将内核模块编译到内核镜像uimage / zimage中
动态编译:将内核模块编译成.ko文件

对应我们之前:开发阶段一般是 《外部编译 + 动态编译》;发布阶段一般是《内部编译 + 静态编译》

6. 模块的加载和卸载
请看另一篇文章,很简单。
3.第一个字符设备驱动(虚拟设备)框架搭建、驱动模块加载、驱动函数实现、应用程序编写;
常用的这里简单罗列一下:
加载:insmod led.ko || modprobe led.ko
展示当前加载的模块:lsmod
查看最近的内核日志信息:dmesg(一般情况下在内核中pirintk,会打印到内核日志中)
清除最近的内核日志信息:dmesg -c
卸载模块:rmmod led

7. 字符设备的相关结构体
描述所有字符设备驱动的结构体:cdev;内核也是通过这个结构体保存着各个字符设备的信息的;
我们目前只需要注意其中一部分就ok

struct cdev {
	struct kobject kobj;		// 与设备模型相关的
	struct module *owner;		// 与设备模型相关:一般赋值为当前模块:THIS_MODULE
	const struct file_operations *ops;	// 文件操作集(设备操作集):其中都是各种方法的函数指针
	struct list_head list;		// 链表:内核在管理字符设备的时候是通过链式管理的;
	dev_t dev;					// 内核标识的唯一的:设备号
	unsigned int count;			// 设备个数:一个驱动管理多个设备的时候个数增加
};

这里我重点描述俩东西:dev 和 fops
1)dev_t dev是内核唯一标识当前设备的:设备号(uint32);
它由两部分组成,高12bit:主设备号;低20bit:次设备号
使用MAJOR(dev_t) 和 MINOR(dev_t)函数可以获取到设备的高12位和低20位分别是什么(int);
2)file_operations结构体的内容如下:在这里插入图片描述
看起来非常复杂,其实里面都是函数指针,因此这是一个操作方法集
上面我们说过,整个计算机分为3层:①应用层 ②内核层 ③硬件层
我们在①中使用open/close/read/等等上图中出现的系统调用时,会通过系统调用进入到②中,内核通过驱动管理
找到对应的操作的设备文件的驱动,从而调用这个驱动中的操作方法集中的函数;进而完成对③的控制;

8. 驱动中的字符设备注册
在驱动中,我们期望向内核注册一个cdev结构体,让内核来进行统一的管理,为了完成这个目的,我们需要以下几个步骤;
(从1)开始看,0)的讲解在后面)


0)分配设备号,注册设备号

  1. 自动分配
/**
* 功能: 系统自动分配设备号
* 参数: @dev	: dev_t类型定义的变量,取址&传入
* 		@basemonor: 次设备号起始数值	:0
* 		@count:	设备个数
* 		@name:		设备名字
* 返回值:成功返回0,失败返回错误码
*/
int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);
  1. 手动分配:自己指定设备号注册
/**
* 功能: 指定设备号注册
* 参数: @from : 设备号(MKDEV(major,minor)
* 		@count:	设备个数
* 		@name:		设备名字
* 返回值:成功返回0,失败返回错误码
*/
int register_chrdev_region(dev_t from, unsigned baseminor, unsigned count, const char* name);
  1. 注销设备号
/**
* 功能: 指定设备号注册
* 参数: @from : 	设备号
* 		@count:	设备个数
* 返回值:void
*/
void unregister_chrdev_region(dev_t from, unsigned count);

1) cdev结构体分配内存空间(内核有写好的函数)

/**
* 功能: 为cdev结构体分配空间
* 参数: @void
* 返回值:成功返回分配的结构体地址,失败返回NULL
*/
struct cdev *cdev_alloc(void);

2)初始化cdev结构体(内核有写好的函数)

/**
* 功能: 初始化cdev结构体
* 参数: @cdev:cdev结构体指针        
* 		@fops:操作方法集指针
* 返回值:void
*/
struct cdev *cdev_init(struct cdev* cdev, const struct file_operations* fops);

3)向内核注册cdev结构体

/**
* 功能: 添加(注册)字符设备到内核中
* 参数: @p:cdev结构体指针        
* 		@dev:设备号
* 		@count:设备个数
* 返回值:成功返回0, 失败返回错误码(<0)
*/
struct cdev *cdev_add(struct cdev* p, dev_t dev, unsigned count);

4)删除cdev结构体并释放空间

/**
* 功能: 从内核中删除cdev结构体并回收空间
* 参数: @p:cdev结构体指针        
* 返回值:void
*/
void cdev_del(struct cdev* p);

前三个函数一般用在驱动模块的入口(注册)函数中,最后的删除函数一般用在模块的出口(注销)函数中。

至此,我们完成了cdev在内核中的注册,但是,这其中还有一个问题:dev_t dev的来源;
设备号是由内核管理的,对于一个设备来说是唯一的标识,因此设备号是内核严格管控的 内核资源;我们需要使用内核提供的函数来申请设备号;这部分代码应该放到我们cdev_add之前,一般我们放在0)的位置

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值