Linux驱动

Linux字符设备驱动框架

  • 字符设备驱动:以字节流方式访问的设备------鼠标/键盘,在对应 /dev 目录下有设备文件
  • 网络设备驱动:用于网络通讯的设备------网卡,使用 ifconfig 进行配置
  • 块设备驱动:以块为单位读写的设备------eMMC、SD卡

最基本框架main

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL v2");
// 驱动入口函数
static int hello_init(void)
{
    return 0;
}
// 驱动出口函数
static void hello_exit(void)
{
    return;
}
module_init(hello_init);   // 告诉Linux,这个驱动入口函数是那个装载入口函数
module_exit(hello_exit);   // 告诉Linux,这个驱动出口函数是那个装载出口函数

linux驱动本质就是向应用层的程序提供设备的操作函数接口

如何让应用层的程序找到底层驱动提供的接口

字符设备会生成设备文件,对于应用层来说,只需要操作设备文件从而操作硬件设备,操作文件需要调open、read、write、close这些函数,应用层操作设备文件调用了这些函数,就需要调用我们写的硬件设备的接口,那怎么找到我们的写的接口,就通过linux提供的框架,它规定好了函数接口,定义函数指针,返回值和参数就写死了,我们就可以把函数指针指向接口,实现回调函数。通过**结构体file_operations规定函数接口,**我们就需要把我们的接口填充到这个结构体中。硬件设备有很多,抽象出一个字符设备通用结构体cdev,保存的是字符设备的通用信息,file_operations也会记录在cdev结构体中。

应用程序访问底层驱动过程

当驱动被加载时,它会向内核注册自己的主设备号和一组操作函数(file_operations)。当用户程序调用 open 打开 /dev 下的对应设备文件时,内核通过文件中的主设备号找到并绑定该驱动,之后用户程序对该文件描述符的读写操作,就会自动被内核转发到驱动注册的对应函数,从而实现对硬件的控制。

mknod创建设备节点然后文件系统会创建对应的struct inode,当打开设备文件时,chrdev_open根据inode中的i_rdev查找字符设备映射表cdev_map,找不到就报错,找到cdev后就用i_cdev记录找到的struct cdev,通过cdev可以找到file_opeartions,再紧接着创建file结构体,同时让file指针的file_ops指向cdev中的file_ops地址,file结构体记录file_operations为了提高效率。

inode记录了cdev,下次打开就不需要去查找了

|-----------------------|------------------------------------|
| cdev_init() | 初始化cdev结构体,绑定file_operations |
| alloc_chrdev_region() | 自动注册设备号 |
| cdev_add() | 将cdev和设备号绑定在cdev_map上 |
| class_create() | 创建设备类/sys/class |
| device_create() | 通过struct class发送uevent事件通知mdev创建设备 |

如果我打开/dev/dev_fifo0进行操作的时候,我怎么知道dev_fifo0所在的内存空间的首地址呢

我们通过open打开/dev/dev_fifo0这个设备文件时,linux内核最终找到了/dev/dev_fifo0对应在内核空间的dev_fifo0这个设备,我们通过container_of获取了内核空间为dev_fifo设备分配的内存首地址,在xxx_open函数中我们把内核为这个设备分配的空间首地址保存在了file->private_data这个变量中,下一次当我们通过read或write调用到驱动层的xxx_read或xxx_write时,VFS层都会把struct file *file传递下来

Linux Platform子系统框架

总线、设备、驱动 => 增加驱动的可扩展性和可移植性

将设备的信息从驱动中分离出来,我们需要在操作系统中,添加设备和驱动两部分。

设备中包含是设备的信息(资源),驱动中包含的是操作设备函数接口。

为了能让驱动最终能操作我们的硬件设备,我们在驱动中必须获取设备的信息(资源)。

总线在操作系统中本质就是两个链表:挂载设备的链表和挂载驱动的链表

设备和驱动都会注册到总线上,当注册设备的时候,会去寻找同名的驱动,当注册驱动的时候,也会去找同名的设备,相互查找。一旦匹配成功,操作系统就会自动调用驱动提供的probe函数。我们只需要在probe函数中,使用操作系统提供的通用API获取硬件的信息即可。

基于总线写驱动的流程:

(1)根据自己的设备,来确定总线的类型

platform bus / i2c bus /usb bus/....

(2)根据总线的类型, 确定设备在总线上如何描述(结构体)

struct platform_device{

设备名;

设备的资源

通用的设备描述(struct device : platform_data->记录设备私有的信息)

id_entry:当设备和驱动的id_table中某一个成员匹配上的时候,这个成员就会记录id_table中匹配上的成员地址

};

struct resource{

int start;资源的开始

int end;资源的结束

资源的类型:IO资源(寄存器地址) 中断资源(中断号) DMA资源(通道)

};

(3)根据总线的类型, 确定驱动在总线上如何描述(结构体)

struct platform_driver{

probe函数 :设备和驱动匹配的时候,操作系统自动调用

remove函数:设备和驱动分离的时候,操作系统自动调用

通用的驱动描述(struct device_driver : 这里面可以记录驱动的名字)

id_table : 当前驱动支持平台设备(记录支持的平台设备名字)

};

(4)根据总线的类型,确定在总线上如何注册设备

int platform_device_register(struct platform_device *pdev);

(5)根据总线的类型,确定在总线上如何注册驱动

int platform_driver_register(struct platform_driver *pdriver);

(6)根据总线的类型, 确定设备和驱动匹配原则

如果驱动提供了id_table,那就拿设备的名字和id_table中记录的名字匹配

如果驱动没有提供id_table,那就拿设备的名字和驱动的名字进行匹配

(7)一旦设备和驱动匹配后,操作系统就会调用驱动提供的probe函数。

在这个函数中,一般需要做两件事情:

<1>获取匹配的硬件资源

struct resource *platform_get_resource(struct platform_device *dev,unsigned int type,int num);

@dev 平台设备的结构体 @type 资源的类型 @num 同类型资源的编号

<2>注册字符设备(可选)

基于总线写驱动思路:

1.模块化编程

<1>头文件

<2>许可权限

<3>模块的入口和出口

2.在总线上注册驱动

<1>填充结构体

struct platform_driver xxx_driver = {

.probe = xxx_probe,

.remove = xxx_remove,

.id_table = xxx_table,

.driver = {

.owner = THIS_MODULE,

.name = "xxxx",

},

};

<2>注册结构体

在模块的入口函数注册结构体

platform_driver_register

在模块的出口函数注销结构体

platform_driver_unregister

3.总线上的设备和驱动相互匹配后,操作系统自动调用驱动提供的probe函数

实现probe函数

int xxx_probe(struct platform_device *pdev)

{

1.获取资源

platform_get_resource

2.注册字符设备

1初始化cdev结构体 (让cdev结构体记录设备的操作函数接口)

cdev_init

2申请设备号

第一种方式 (静态):

register_chrdev_region

第二种方式 (动态):

alloc_chrdev_region

3添加字符设备

cdev_add

----------------自动创建设备节点------------------------------------------

4创建类

class_create

5创建设备

device_create

}

4.总线上的设备和驱动分离后,操作系统自动调用驱动提供的remove函数

实现remove函数

int xxx_remove(struct platform_device *pdev)

{

核心思想:将probe函数申请的资源,释放掉

<1>删除设备

device_destory

<2>删除类

class_destory

<3>删除字符设备

cdev_del

<4>释放设备号

unregister_chrdev_region

}

设备树

设备的信息是针对于特定平台的,如果我们在Linux内核中包含太多设备信息,则Linux内核移植性就会变差。引入设备树之后,设备的信息的描述不再是以代码的形式存在于 Linux内核源代码中,这种做法实际上是将设备的信息,从Linux 内核中独立出来,单独描述(用设备树语法规则来描述设备的信息)。

  • compatible:驱动通过of_match_table中的compatible与设备树匹配
  • reg = <寄存器的起始地址 地址的长度>
  • status:disable不注册,okay注册
  • 设备名-gpios = <&gpio控制器的标签名 管脚编号 标志>
  • interrupt-parent = <&中断控制器的标签名>
  • interrupts = < 1个或多个uint32的数字描述中断的信息 >,<...>;// #interrupt-cells

|---------------------------|---------------------|
| of_get_named_gpio_flags() | 从设备树获取gpio信息 |
| devm_gpio_request() | 申请占用gpio资源 |
| gpio_direction_output() | 设置gpio输出 |
| devm_request_irq() | 注册中断号、触发方式和注册中断回调函数 |

Linux 中断子系统框架

**中断控制器(GIC )**负责对每个中断源进行编号、使能、屏蔽和优先级仲裁,并根据配置将中断请求分类为普通中断(IRQ)或快速中断(FIQ),最终分发给ARM 核心(ARM Core)。ARM 核接收到中断信号后,会立即保存当前程序的执行上下文(现场),然后跳转到异常处理向量表,执行对应的中断服务例程(ISR),处理完后再恢复现场并返回被打断的程序继续执行。

外部中断由外接的设备通过管脚产生,管脚由gpio控制器控制,gpio控制器向GIC提出中断请求

在linux系统中实现中断功能需要调用request_irq函数进行中断注册

1.中断注册(当中断产生的时候,操作系统会自动调用我们中断处理函数)

typedef irqreturn_t (*irq_handler_t)(int, void *);

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)

参数:

@irq 中断号

@handler 中断处理函数的入口地址

@flags 标志

@name 名字:/proc/interrupts

@dev 传递给中断处理函数的参数

返回值:

成功返回0,失败返回错误码

2.注销中断

void free_irq(unsigned int irq, void *dev)

参数:

@irq 中断号

@dev 注册中断的时候,传递给中断处理函数的参数

注意:
devm_request_irq申请的中断号,在驱动与设备分离的时候,自动释放中断资源

3.共享中断
多个驱动程序申请的是同一个中断号(多个设备请求的中断是同一个中断的时候)
此时中断产生的时候,操作系统是不区分那个设备产生了中断,操作系统做法是将这个中断号关联
所有中断处理函数全部调用一次,所以此时中断处理函数必须能判别是否是自己的设备产生的中断(通过读状态寄存器),如果不是,立即返回,如果是,则做中断处理。
<1>注册中断的时候,需要IRQF_SHARED标志
<2>给中断处理函数传递的参数必须是唯一的,不能是NULL

中断上半部和下半部

将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。

中断处理函数中完成的事情 ,是上半部(屏蔽外面的中断)。

而另外一个函数中完成的事情,是下半部(不屏蔽外面的中断)。

  • 如果一个任务时间十分敏感,将其放在上半部
  • 如果一个任务和硬件有关,将其放在上半部
  • 如果一个任务要保证不被其他中断打断,将其放在上半部
  • 其他所有任务,考虑放在下半部

下半部的实现机制

1.软中断

2.tasklet

tasklet 是基于软中断实现的

软中断不能直接使用 ,Linux 内核为了方便开发者,在软中断之上封装了 tasklet 接口,使用更简单。所以,除非对性能要求特别高,否则都应使用 tasklet

一个tasklet被tasklet_schedule调用过,它的state状态就被设置为TASKLET_STATE_SCHED,接下来等待被执行。tasklet被执行完后,它的状态才会被清除。如果重复调用一个tasklet,它如果没有被执行过,是不会重复执行的。

|--------------------|---------------|
| tasklet_init() | 注册tasklet回调函数 |
| tasklet_schedule() | 调用软中断 |

3.workqueue

工作队列可以把工作推后,交由一个内核线程去执行,这个下半部总是会在进程上下文执行,由于是内核线程,其不能访问用户空间,工作队列允许重新调度甚至是睡眠。

  1. 如果推后执行的任务需要睡眠,那么只能选工作队列
  2. 如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时
  3. 如果推后执行的任务需要在一个tick之内处理,则需要软中断或tasklet,因为其可以抢占普通进程和内核线程
  4. 如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务

|------------------|-----------------------------------|
| INIT_WORK | 初始化 work_struct 结构体,并绑定工作队列回调函数 |
| schedule_work | 将工作项放入系统默认工作队列的链表中,并唤醒工作线程执行 |
| cancel_work_sync | 取消已提交但未执行的工作项,并同步等待正在执行的工作项完成 |

如果一个工作没有被执行,后面重复提交的工作将不会被加入工作队列中

下半部机制的使用总结

|---------|-----|-----|------|-----------|
| 下半部机制 | 上下文 | 复杂度 | 执行性能 | 顺序执行保障 |
| 软中断 | 中断 | 高 | 好 | 没有 |
| tasklet | 中断 | 中 | 中 | 同类型不能同时执行 |
| 工作队列 | 进程 | 低 | 差 | 没有 |

Linux Input子系统框架

Input 子系统用于管理各类输入设备(如按键、触摸屏、鼠标等),负责对外部事件的感知与上报。它将硬件层产生的原始输入事件,经过设备驱动层和核心层处理后,通过统一的接口,当 input_register_device() 执行时,内核会自动创建(如 /dev/input/eventX)提供给用户空间应用程序使用。

linux内核注册了多个handler驱动模块,并通过input_handler_list链表维护

Linux内核中维护了一个名为input_handler_list的链表,每个input_handler都会被注册到这个链表 上,而链表是通过类型为list_head的成员node串联起来的。

input_handler和input_dev匹配成功后会创建**input_handle ,会分配新的次设备号、注册字符设备(提供 evdev_fops)并创建设备节点(/dev/input/eventX)。**

|----------------------------|------------------------------------|
| devm_input_allocate_device | 申请input_dev资源并在remove时自动释放资源 |
| input_register_device | 注册input_dev设备,并创建一个input设备节点给应用层访问 |
| input_report_key | 向核心层上报按键值,存入事件缓存 |
| input_sync | 发送同步事件,表示应用层可读取一组输入事件 |

在我们自己设备树下查找

然后看看别人是怎么写的

我们选谁来当我们的中断控制器节点呢,就近原则,谁直接控制我们的中断源就选谁,这里是gpio控制我们的中断源,所以选gpio,即这里的gpx1(通过原理图知道K2的gpio标签名为gpx1)

在芯片的参考文档linux-3.14/Documentation/devicetree/bindings中找

最后写出我们的设备树节点是

实验结果

相关推荐
tjjingpan1 小时前
HCIP-Datacom Core Technology V1.0_20 IPv6概述
linux·服务器·网络
云泽8081 小时前
Linux基础开发工具(三):Vim高级配置、Swap恢复机制与Sudo权限白名单详解
linux·运维·vim
qetfw2 小时前
CentOS 7 vsftpd.conf 配置文件详解:监听、用户、权限、被动模式与 TLS
linux·运维·centos·ftp·vsftpd
风曦Kisaki2 小时前
Kubernetes(K8s)笔记Day04:控制器(ReplicaSet 与Deployment),滚动更新及回滚,滚动更新策略,Pod 的 DNS 策略
linux·运维·笔记·docker·容器·kubernetes
code_whiter2 小时前
初阶linux2环境基础开发工具完整教程
linux
AmazingEgg2 小时前
RHCSA 第三天学习笔记
linux
数聚天成DeepSData2 小时前
外贸海关进出口数据去哪免费下载?从统计到明细的查找指南
linux·服务器·开发语言·前端·网络·人工智能·自然语言处理
Huangjin007_2 小时前
【Linux 系统篇(十)】基础开发工具(五) —— 第一个系统程序 - 进度条
linux·运维·服务器
时空无限3 小时前
linux mellanox 网卡队列 irq rps 相关
linux·运维