
Linux内核基本知识Linux 驱动的分类字符设备驱动— 鼠标/键盘在对应/dev目录下有设备文件网络设备驱动— 网卡使用ifconfig进行配置块设备驱动— eMMC、SD卡最基本框架#include linux/init.h #include linux/module.h // 驱动入口函数 static int hello_init(void) { return 0; } // 驱动出口函数 static void hello_exit(void) { return; } module_init(hello_init); // 告诉Linux这个驱动入口函数是那个装载入口函数 module_exit(hello_exit); // 告诉Linux这个驱动出口函数是那个装载出口函数source insight 只是代码阅读工具驱动代码从下向上读调试硬件之前学硬件开发功能开发 功能和你业务代码是杂糅在一起 比如你要开灯 串口通信 led的控制 自定义协议 解析协议实现对应的 led的控制单片机裸机 寄存器操作hal库操作 cubmx hal库函数freeRTOS实时操作系统linux驱动开发1、只给应用层提供对应操作硬件设备的接口具体怎么实现什么功能怎么使用应用层管理 美颜相机只给你提供相机接口打开 读取 关闭Linux一切皆文件 /对应设备文件/dev 去操作对应的硬件设备2、Linux可以是开发 多任务去访问设备Samba远程链接虚拟机特点虚拟机共享文件夹SambaVSCode Remote-SSH依赖依赖虚拟机软件VMware Tools依赖 Samba 服务依赖 SSH 服务传输方式虚拟机专用通道网络协议SMB网络协议SSH主要用途临时传文件文件共享类 NAS代码开发、调试权限控制较弱精细基于 SSH 用户权限访问设备仅限宿主机局域网任何设备仅限发起 SSH 连接的设备实时性需要手动刷新需要手动刷新实时同步即时生效开发体验差需手动拷贝一般需手动拷贝极佳本地化体验内核编译由于驱动的代码是在内核层运行的但是gcc是在应用层运行的所以gcc不能编译驱动代码所以要用linux源码的编译系统Makefile编译内核编译思想Linux 内核源码的编译系统可以编译我们编写的模块代码。两种方式第一种产品发布阶段把自己写的驱动源码拷贝到Linux内核源码树下进行对应的配置编译编译进内核。通过编写Makefile和Kconfig文件并执行make menuconfig配置界面来选择是否将驱动编译进内核。第二种产品研发阶段自己编译Makefile然后利用Linux内核的编译系统编译自己的模块代码。Makefile告诉内核编译系统怎么编译你的.c源码内核编译系统根据内核源码树里的规则和配置最终生成一个可以在ARM开发板上加载的.ko驱动模块文件。模块的内核编译makefile文件讲解在linux编译系统Makefile里面KERNELRELEASE第一次调用的时候不赋值第二次调用的时候才赋值;ifeq ($(KERNELRELEASE),)用于判断当前是否处于内核构建系统的第二次调用如果KERNELRELEASE为空说明是第一次被用户手动调用此时执行主机端配置如指定交叉编译工具链和目标平台若不为空则说明已被内核构建系统调用此时执行模块编译obj-m。KERNELRELEASE当前正在编译的内核版本号字符串如5.15.0-91-generic用于标识模块编译目标的内核源码树版本。我们写的makefile会被两次调用第一次我们在终端执行make的时候makefile被第一次调用第二次调用是linux编译系统调用我们写的makefile第二次调用是由make -C $(BUILD_SYSTEM) M$(MODULE_PATH) modules这条命令触发的它会进入内核源码树的Makefile而内核的Makefile会递归调用当前目录的Makefile来编译具体的模块目标obj-m所以第二次是内核构建系统主动、自动地重新读取并执行你的 Makefile。所以通过KERNELRELEASE来判断是第一次还是第二次调用自己编译的Makefile如下ifeq ($(KERNELRELEASE),) ARM_BUILD_SYSTEM/home/hqyj/fs4412/linux.3.14/linux-3.14 X86_BUILD_SYSTEM/lib/modules/$(shell uname -r)/build MODULE_PATH$(shell pwd) arm_module: make -C $(ARM_BUILD_SYSTEM) M$(MODULE_PATH) ARCHarm CROSS_COMPILEarm-linux- modules x86_module: make -C $(X86_BUILD_SYSTEM) M$(MODULE_PATH) modules clean: make -C $(ARM_BUILD_SYSTEM) M$(MODULE_PATH) clean make -C $(X86_BUILD_SYSTEM) M$(MODULE_PATH) clean install: cp ./Hello-driver.ko ~/fs4412/rootfs/ else obj-m Hello-driver.o //新的驱动只需要改这里 endif虚拟机也是linux系统的内核源码的编译系统在/lib/modules/$(uname -r)/build目录下它是一个指向内核源码树的符号链接。uname -r来获取当前内核版本号modules是 Makefile 里的一个编译目标执行它告诉编译系统只生成内核模块.ko文件而不是编译整个内核。驱动模块操作指令sudo insmod hello-driver.ko安装驱动文件dmesg查看驱动sudo rmmod hello-driver卸载驱动文件lsmod列出当前有哪些驱动modinfo hello-driver.ko 查看驱动的信息编译内核操作系统make ARCHarm exynos_defconfig //导入配置make ARCHarm CROSS_COMPILEarm-linux- uImage驱动传参作用1.设置驱动相关参数比如设置缓冲区大小2.设置完全校验防止驱动被盗用方法1.传递普通的参数module_param(name, type, perm); name: 传递的进去的名称 type: 类型 perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)static int a 0; static char *my_char a; //对于字符来说只能传字符指针 static int count 0; static int number[10]; module_param(a, int, 0644); module_param(my_char, charp, 0644);insmod hello.ko a12.传递数组module_param_array(name, type, nump, perm); name: 传递的进去的名称 type: 类型 nump: 实际传入进去的参数个数 perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)module_param_array(number, int, count, 0644);字符设备驱动框架字符设备和块设备会生成设备文件应用层操作设备文件从而操作硬件设备通过linux提供的框架将open和硬件接口联系对于应用层和接口函数接口通过函数指针来规定返回值和参数规定死上层代码只认这个底层通过注册回调函数来填充实现调用时通过指针间接执行从而实现接口标准化、逻辑解耦和硬件抽象。// 1. 规定接口函数指针类型 typedef int (*key_operation_t)(int key_code, int value); // 2. 上层框架只认这个接口 struct key_driver { key_operation_t report_key; // 函数指针作为接口 }; // 3. 底层驱动实现具体功能回调函数 int my_key_report(int key_code, int value) { printk(Key %d %d\n, key_code, value); return 0; } // 4. 注册把实现绑定到接口 struct key_driver drv; drv.report_key my_key_report; // 函数指针指向具体实现 // 5. 调用硬件触发时通过函数指针调用 drv.report_key(1, 1); // 实际执行的是 my_key_report对硬件来说只写操作设备的接口由于对于上层来说只有设备文件所以要设备文件和接口关联通过linux设备框架来关联函数接口通过结构体file_operations规定;里面存放了很多函数高类聚低耦合把相关的紧密放一起高内聚不相关的别扯上关系低耦合内核open设备文件的驱动查找流程应用层范围设备号就藏在设备文件中创建设备文件mknod 设备文件名 设备文件类型 主设备号 次设备号 mknod /dev/led c 77 0内核用inode和file这两个结构体来记录信息为了方便查找提高效率。inode这个结构体第一次打开这个设备时对应的i_cdev是没有值的发现对应的指针为空它就会通过设备号去查找去找对应的底下的cdev找到之后会把对应的cdev的地址放到i_cdev中第二次open打开这个设备文件的时候对应的i_cdev已经记录了就可以通过i_cdev找到。我们对应打开文件之后一般都是读写那对应的read和write第一个参数都是文件描述符你通过文件描述符找到的就是file结构体从而找到file_operations如果没有file这个结构体需要重新找一遍大大提高了对应的效率。当驱动被加载时它会向内核注册自己的主设备号和一组操作函数file_operations。当用户程序调用open打开/dev下的对应设备文件时内核通过文件中的主设备号找到并绑定该驱动之后用户程序对该文件描述符的读写操作就会自动被内核转发到驱动注册的对应函数从而实现对硬件的控制。cdev把写好的接口函数写入cdev里面去cdev结构体:描述通用的字符设备结构体不然一个设备一个结构体太占内存了几个设备提供几个函数接口然后把函数接口放到cdev结构体中struct cdev { struct kobject kobj; /* 内嵌内核对象用于设备模型管理sysfs、引用计数 */ struct module *owner; /* 指向拥有此设备的模块通常为 THIS_MODULE防止模块被卸载 */ const struct file_operations *ops; /* 文件操作函数集open/read/write/ioctl 等驱动核心接口 */ struct list_head list; /* 链表节点用于将设备挂入内核全局链表或哈希表 */ dev_t dev; /* 设备号主设备号 次设备号唯一标识设备 */ unsigned int count; /* 次设备号数量表示该设备占用的连续次设备号范围大小 */ }; //主要填充dev和opskmalloc和malloc对比malloc分配的空间不连续虚拟地址连续物理地址可能不连续kmalloc分配的空间连续虚拟地址连续物理地址也连续┌─────────────────────────────────────┐ 高地址 │ 内核空间 │ ← 内核代码、数据、kmalloc分配在这里 │ (1GB on 32-bit Linux) │ ├─────────────────────────────────────┤ │ 栈区 (Stack) │ ← 局部变量、函数调用 ├─────────────────────────────────────┤ │ │ │ (内存映射区) │ ← mmap、共享库 │ │ ├─────────────────────────────────────┤ │ 堆区 (Heap) │ ← malloc分配在这里用户空间 ├─────────────────────────────────────┤ │ BSS段 │ ← 未初始化全局/静态变量 ├─────────────────────────────────────┤ │ 数据段 (Data) │ ← 已初始化全局/静态变量 ├─────────────────────────────────────┤ │ 代码段 (Text) │ ← 程序指令、只读 └─────────────────────────────────────┘ 低地址设备号设备号是内核查找cdev的唯一索引主设备号标识设备类型次设备号区分同类型的不同设备。驱动的标识设备号12bit主设备号 20bit次设备号 32bit主设备号标识一类设备次设备号为了区分同类设备的不同设备特性ls -l /devcat /proc/devices查看对象/dev目录下的具体设备文件节点内核中已注册的设备驱动程序由主设备号标识展示内容主设备号和次设备号仅主设备号以及对应的驱动名称信息维度个体信息每个设备文件是一个独立的个体拥有自己完整的设备号 (major, minor)类别信息展示有哪些驱动类别主设备号已被内核识别不关心每个类别下的具体个体次设备号适用场景当你想知道某一个特定设备文件如/dev/sda1的设备号时当你想知道系统支持哪些类型的设备或验证一个驱动程序是否已成功加载时驱动核心思想向应用层的程序提供设备的操作函数接口如何让应用层的程序找到底层驱动提供的接口linux应用程序和驱动程序相互关系底层写驱动的对应的本质为上层提供接口函数接口硬件的函数接口。向上层应用工程师提供函数接口的方法函数怎么写怎么规定写法在c语言中函数指针来限制函数的写法在c有语法可以对子类函数接口来进行限制多态的场合写的纯虚函数父类定义纯虚函数相当于规定“必须有这个功能”总结我们写驱动就是给别人提供一种函数接口给上层的工程师调用但是上层工程师不知道我们今天给提供了什么接口函数不知道函数名调用不了但是他必须要调用我们函数这个时候能想到的就是函数指针。底层驱动把具体的函数地址赋值给这个指针这样就能调用函数内核发展字符驱动框架2.4 linux kernel字符驱动框架LED驱动缺点可移植性差原因驱动中包含了特定平台的硬件信息华清板子 ccynos4412芯片如果是其他平台硬件信息会有差异所以驱动无法直接使用总线、设备、驱动框架2.6 linux kernel总线、设备、驱动框架总线、设备、驱动 增加驱动的可扩展性和可移植性将设备的信息从驱动中分离出来我们需要在操作系统中添加设备和驱动两部分。设备中包含设备的信息资源驱动中包含的是操作设备函数接口。为了让驱动最终能操作我们的硬件设备我们在驱动中必须获取设备的信息资源。设备驱动设备驱动驱动中不再直接包含设备的信息设备和驱动是分离的它们都会向内核总线如platform总线注册。当任何一方无论是设备先来还是驱动先来注册时内核都会触发一次总线上的匹配扫描寻找名称或ID匹配的对方。一旦匹配成功操作系统就会自动调用驱动中实现的probe函数。在probe函数里开发者只需要使用操作系统提供的通用API如platform_get_resource、devm_clk_get等就能从匹配到的设备信息中获取硬件资源地址、中断、GPIO等而无需关心这些信息是如何被记录的。设备与驱动匹配成功后二者通过指针相互绑定如pdev-driver指向驱动drv-devices链接设备形成“你中有我我中有你”的关联。在Linux内核中总线本质上是两个用于管理和匹配的链表分别挂载着该总线上的设备和驱动当设备或驱动注册时内核通过总线遍历这两个链表进行匹配匹配成功则调用驱动的probe函数。设备树早期没有设备树只有镜像文件包含我们上面写的配置信息设备的信息是针对于特定平台的如果我们在Linux内核中包含太多设备信息则Linux内核移植性就会变差。引入设备树之后设备的信息的描述不在是以代码的形式存在于Linux内核源代码中这种做法实际上是将设备的信息从Linux内核中独立出来单独描述Linux内核在启动的时候要求把设备树文件传递给它。它拿到设备树之后会解析设备树文件从而识别设备信息。// 后面通常跟设备基地址 fs4412-led11000c40{ compatible fs4412-led; reg 0x11000c40 8; };Linux 内核platform总线上设备与驱动的匹配规则1设备树中的compatible属性与驱动中指定的of_match_table中的compatible进行匹配如果没有匹配成功:2如果驱动中有id_table,则拿id_table中记录的名字与设备的名字匹配如果驱动中没有id_table,则拿驱动的名字与设备的名字platform_device结构体中name字段匹配of 是 Open Firmware开放固件设备树设备树后缀三个设备文件.dtb二进制文件给操作系统用的可执行文件.dts.c文件设备信息源文件给工程师看的.dtsi.h文件设备树的头文件写设备树需要看驱动写设备树步骤1.拷贝设备树通过当前设备树的头文件找出所有设备树文件然后拷贝出来到电脑的共享文件夹中因为在内核linux里面查会有很多设备树文件不方便看2.找厂家的GPIO控制器节点每个GPIO控制器必包含gpio-controller属性# 在设备树目录下递归搜索 gpio-controller 属性 cd arch/arm/boot/dts/ grep gpio-controller * -nR部分含义grepLinux 下的文本搜索工具Global Regular Expression Printgpio-controller设备树中声明某个硬件节点为 GPIO 控制器的标签告诉内核“我能管理 GPIO 引脚其他设备可以通过我申请使用具体引脚”。*表示当前目录下的所有文件作为搜索范围-n显示行号在匹配结果中显示该行在文件中的行号-R递归搜索Recursive进入所有子目录继续搜索知道哪个是GPIO控制节点然后根据自己的设备电路图找到对应的节点//gpx2:gpx2前面的是标签名方便其他节点引用;后面的是节点名 gp2: gp2 { gpio-controller; //#xxx-cells 数字 //含义xxx 这个信息需要使用多少个 32-bit 数字u32来表示。 #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };芯片厂商的gp2定义了一个“管家”GPIO 控制器并规定谁想用我的服务必须给我2 个参数引脚号 标志。然后在自己的设备树中linux-3.14/arch/arm/boot/dts引用该节点 exynos4412-fs4412.dts实在不知道怎么写在源码/Documentation/devicetree/bindings有各个设备树的解释grep gpio-cells * -nR | grep 厂家名字查找到的信息如下所以我们的GPIO配置如下按照芯片的GPIO控制器规定去找管家申请服务gpx2 7 0——“管家我要用你的第 7 号引脚标志是 0”。到这一步我们代码就不需要寄存器操作了我们创建了多个设备文件那么驱动怎么知道操作哪个设备文件获取不同的设备信息全局的pled只能一次记录一个设备的信息其他的就丢了此时用链表of_gpio_led不论应用层open的是哪一个LED设备的设备文件在LED的驱动中调用的都是同一个led_open函数那么如何在驱动的led_open函数中区分不同的LED设备进行操作回答在led_open函数中通过file参数中的f_inode指针找到当前打开的设备号dev_t然后根据设备号在驱动中查找对应的私有数据结构体如led_dev从而区分操作不同的 LED。中断InterruptCPU处理事情1.轮询方式不断的查询是否要处理事情如果需要则处理。由于很多时候条件不满足CPU不需要处理这样浪费了CPU的时间。2.中断方式当需要CPU处理的时候产生一个信号打断CPU正在做的事情。让CPU处理当前的事情当CPU处理完处理完当前事件之后回到打断之前的地方接着执行。中断处理需要注意的地方1.中断打断其他程序的执行所以中断处理的时候需要尽可能的快不能在中断处理过程中做耗时很长的事情。2.中断打断的当前的程序执行所以在中断处理的时候需要先保存现场CPU的状态和CPU内部寄存器的值压栈保存在中断处理结束的时候则恢复现场。中断的一些概念1.中断源产生中断的源头2.中断号是SOC芯片厂家对SOC芯片内部中断源的编号3.中断处理函数中断产生之后需要调用执行的函数4.中断控制器控制中断的优先级、中断是否被允许产生5.内部中断和外部中断内部中断SOC芯片内部控制器产生的中断外部中断SOC芯片外部管脚通过电平触发产生的中断高电平触发、低电平触发、上升沿触发、下降沿触发、双边沿触发Linux 中断分为三类软件中断内核代码主动触发用于异步任务处理、私有中断PPI仅绑定的特定 CPU 核心接收如 CPU 本地定时器和共享中断SPI中断信号可以发给多个 CPU 核心由系统决定由哪个核心处理。可被多个外设共用但同一中断号上多个设备需共享一个 ISR由驱动自己判断谁触发了中断。当 GPIO、定时器Timer、ADC 等外设模块产生中断信号后这些信号会先经过各自内部的子中断控制器如 Combiner进行初步合并或管理随后统一汇总到中断控制器GICGeneric Interrupt Controller。GIC 作为中断枢纽负责对每个中断源进行编号、使能、屏蔽和优先级仲裁并根据配置将中断请求分类为普通中断IRQ或快速中断FIQ最终分发给ARM 核心ARM Core。ARM 核接收到中断信号后会立即保存当前程序的执行上下文现场然后跳转到异常处理向量表执行对应的中断服务例程ISR处理完后再恢复现场并返回被打断的程序继续执行。默认高电平按键按下低电平查找怎么写设备树先在自己设备树目录下查找看别人怎么写的grep interrupt-parent * -nR然后在linux-share/learn-driver/dts自己的设备树下查找看看芯片厂家怎么写的grep interrupt-controller * -nRcombiner要看芯片手册支不支持就近原则谁直接控制我们中断就写谁我们选图三实在不知道怎么找在芯片的参考文档linux-3.14/Documentation/devicetree/bindings中找grep #interrupt-cells * -nR | grep exynos先找板子再找厂家从范围小的找起所以写出来的设备树是参数值来源第一个参数中断号1硬件原理图按键连接在 GPX1_1 引脚对应中断编号 1第二个参数触发方式8芯片手册/文档低电平触发写程序先调用platform总线设备结构体然后注册和注销设备注册设备还可以用以下代码/* * 功能将 platform_driver 结构体注册到内核平台总线 *module_platform_driver() 是一个宏它会在预处理阶段自动展开成一段完整的代码 *同时包含了注册module_init和注销module_exit两部分。 * 等价于在 module_init 中调用 platform_driver_register() * 并在 module_exit 中调用 platform_driver_unregister() */ module_platform_driver(key_driver); /* * MODULE_AUTHOR声明驱动作者信息 * 可通过 modinfo 命令查看 */ MODULE_AUTHOR(HQYJ); /* * MODULE_LICENSE声明模块许可证内核强制要求 */ MODULE_LICENSE(GPTL);接着就是设备树匹配表/* 设备树匹配表用于定义该驱动支持哪些硬件设备 */ static const struct of_device_id key_of_match[] { { .compatible fs4412-key }, { /* Sentinel */ } // 空哨兵表示表结束 }; /* 将设备树匹配表导出为模块设备表供内核在加载时识别 */ MODULE_DEVICE_TABLE(of, key_of_match);代码一句话概括{ .compatible snps,dw-apb-uart }“我能修这种设备”写在驱动里运行时匹配MODULE_DEVICE_TABLE(of, key_of_match)“把我的名片贴在外面让大家知道我能修这种设备”写在模块元数据供外部工具识别在设备树中你只能看到compatible snps,dw-apb-uart这个节点属性而MODULE_DEVICE_TABLE()的产物在设备树中看不到只能在模块文件.ko的元数据中看到modinfo key_driver.ko然后就是驱动匹配和分离函数其中驱动匹配函数需要获取资源//第一种获取中断资源 res platform_get_resource(pdev, IORESOURCE_IRQ, 0); if(!res) { printk(Fail to platform_get_resource:IRQ\n); return -ENODEV; } printk(IRQ number:%d\n,res-start); printk(interrupt-name:%s\n,res-name); printk(interrupt flags:%#x\n,(unsigned int)res-flags); //第二种自己解析设备树节点获取中断号 irq_number irq_of_parse_and_map(np,0); if(!irq_number) { printk(Fail to irq_of_parse_and_map\n); return -ENODEV; } printk(irq number:%d\n,irq_number);共享中断多个驱动程序申请的是同一个中断号多个设备请求的中断是同一个中断的时候此时中断产生的时候操作系统是不区分那个设备产生了中断操作系统做法是将这个中断号关联所有中断处理函数全部调用一次所以此时中断处理函数必须能判别是否是自己的设备产生的中断如果不是立即返回如果是则做中断处理。1注册中断的时候需要IRQF_SHARED标志2给中断处理函数传递的参数必须是唯一的不能是NULL4.中断上半部和下半部将中断处理函数中需要做的事情分成两部分在不同的函数中完成。中断处理函数中完成的事情 是上半部(屏蔽外面的中断)。而另外一个函数中完成的事情是下半部(不屏蔽外面的中断)。打游戏(进程)----------(中断)吃饭:重要(上半部)吃虾:耗时时间长(下半部:进程上下文或中断上下文)----------女友电话(中断)上半部:中断处理函数不可以被打断下半部:可以被打断可以在中断上下文或进程上下文执行问:上半部什么时候执行?答:中断产生,就执行中断处理函数(上半部)问:下半部什么时候执行?答:在合适的时间点执行(一般是在上半部结束的时候开始触发下半部)问:什么事情放在上半部什么事情放在下半部?答:紧急的事情耗费时间不多我们放在上半部耗时的时间长或需要休眠我们放在下半部网卡(轮询或中断) 中断----- 网卡收到数据 (以太网头|ip头|tcp/udp头|用户数据)5.下半部的实现机制软中断(理解原理) tasklet(掌握) workqueue(掌握)注意:1软中断相关的函数接口Linux 内核并没有导出,所以在驱动中不可以直接使用2tasklet是基于软中断实现3workqueue它是进程上下文的下半部机制软中断分析到这里要告诉大家一个不幸的消息Linux 内核并没有导出open_softirq、raise_softirq函数的符号也就说我们无法以动态加载模块的方式使用软中断。如果大家需要使用软中断可以修改内核代码导出这两个函数的符号然后重新编译内核。查看是否导出EXPORT_SYMBOL下半部机制上下文复杂度执行性能顺序执行保障软中断中断高(需要自己确保软中断的执行顺序及锁机制)好(全部自己实现便于调优)没有tasklet中断中(提供了简单的接口来使用软中断)中同类型不能同时执行工作队列进程低(在进程上下文中运行与写用户程序差不多)差没有(和进程上下文一样被调度)Input子系统主要是对外部事件的感知相当于外部事件的输入向上层应用提供接口主要组成部分包括1 、 输入核心层 I nput core 2 、 输入事件层 I nput Events 3 、 设备驱动程序层 I nput Device Driver Linux 内核中注册了多个事件处理 handler 驱动模块分别是evdev 通用输入事件接口 将不同设备的输入 ( 如键盘、 鼠标、 触摸屏 ) 转换为统一的事件格式。 用户 可以通过设备文件 /dev/eventX 或 /dev/input/eventX 读取事件。 evdev 次设设备号范围 64-95 和 256-1024 如果设备数量不超过 32 使用静态保留区范围 64-95 如 果超过 32 个 则在 256-1024 之间动态分配。主设备号是一定的mousedev 一个早期的鼠标事件接口 支持 PS/2 接口的鼠标 现在鼠标基本上都是 USB 或蓝牙接口 的 他们使用了 HID 接口 HID 后文会单独介绍。joydev 专为游戏手柄、 摇杆设计 处理多轴、 按键事件 生成 /dev/input/jsX 设备节点。 提供标准游 戏设备接口 支持校准和力反馈功能 方便游戏或输入工具直接访问。input_leds 管理输入设备的 LED 状态 如键盘的 Caps Lock 、 Num Lock 。 通过 sysfs 如 /sys/class/leds 或 i octl 控制 LED 开关 是硬件驱动与用户空间控制的中继。Input 子系统框架input_dev 是硬件驱动层 代表 一个 i nput 设备。input_handler 是事件处理层 代表一个事件处理器。input_handle 属于核心层 代表 一个配对的 i nput 设备与 i nput 事件处 理器。Linux 内核中维护了一个名为 i nput_handler_list 的链表 每个 i nput_handler 都会被注册到这个链表 上 而链表是通过类型为 list_head 的成员 node 串联起来的。设备匹配流程匹配成功后输入设备结构体struct input_dev { unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; // 设备属性 unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 事件类型表 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 按键事件码表 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相对坐标事件码表 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 绝对坐标事件码表 unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 混杂事件码表 unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // led灯事件码表 unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // 声音事件码表 unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 力反馈事件码表 unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 开关事件码表 }在注册 i nput_dev 前需要初始化一些必要的数据包括 i nput_dev 的 evbit 、 keybit 、 absbit 和 relbit 等几个重要成员 evbit 代表当前设备支持哪些事件类型 其他几个表示当前设备支持 那些事件码。 比如一个按键驱动需要初始化 evbit 和 keybit evbit 的 EV_KEY 位要置位 keybit 的键值位要置位。使用 set_bit 这个函数进行操作void set_bit(unsigned int nr, volatile unsigned long *p); //示例 set_bit(EV_KEY, input_dev-evbit); // 支持按键事件 set_bit(KEY_1, input_dev-keybit); // 支持数字1键 set_bit(KEY_2, input_dev-keybit); // 支持数字2键input能支持哪些类型事件struct input_event { __kernel_ulong_t __sec; // 上报时间秒 __kernel_ulong_t __usec; // 上报时间微秒 __u16 type; // 上报事件类型 __u16 code; // 上报事件编码 __s32 value; // 上报事件值 }Input 子系统常用接口/* 手动分配 input_dev 结构体 */ struct input_dev *input_allocate_device(void); /* 释放 input_dev 结构体 */ void input_free_device(struct input_dev *dev); /* 自动管理 input_dev 结构体当 dev 被释放时 input_dev 结构体自动释放 */ struct input_dev *devm_input_allocate_device(struct device *dev); /* 注册 input_dev 到系统中的 input 核心层 */ int input_register_device(struct input_dev *dev); /* 在 input 核心层注销 input_dev */ void input_unregister_device(struct input_dev *dev);具体的事件还需要下面的函数/* 上报一个按键事件 */ void input_report_key(struct input_dev *dev, unsigned int code, int value); /* 上报一个相对坐标事件 */ void input_report_rel(struct input_dev *dev, unsigned int code, int value); /* 上报一个绝对坐标事件 */ void input_report_abs(struct input_dev *dev, unsigned int code, int value); /* 标记一组输入事件的结束 */ void input_sync(struct input_dev *dev); /* 上报触摸点信息 */ void input_mt_slot(struct input_dev *dev, int slot); /* 添加新触摸点 */ bool input_mt_report_slot_state(struct input_dev *dev, unsigned int tool_type, bool active); /* 同步触摸事件 */ void input_mt_sync(struct input_dev *dev);在中断章节里我们已经讲解了按键驱动的编写这里只需要在其基础上增加输入设备相关代码即可调整部分包括1、定义input_dev结构体2、probe函数中的修改通过input_allocate_device为input_dev分配内存并初始化设置input_dev的evbit成员和keybit的成员通过input_register_device将input_dev注册到系统中3、remove函数的修改通过input_unregister_device将input_dev从系统中注销通过input_free_device将input_dev释放掉4、中断处理函数的修改判断具体按键通过input_report_key将按键事件提交给上层一次按键需要同步两个事件包括按下和抬起调用input_sync同步事件硬件