疑问
file_operation
中每个操作函数的形参中inode的作用
设备树中compatible
属性中厂商和型号如何填写
file_operation
定义了Linux内核驱动的所有的操作函数,每个操作函数与一个系统调用对应,对于字符设备来说,常用的函数有:llseek
、read
、write
、pool
等等,这些操作函数都要需要完成实现。
为实现“高内聚,低耦合”的软件设计理念,Linux驱动采用了内核加载的方式,将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块,用“rmmod”命令卸载具体驱动模块,相应地需要注册模块加载函数和模块卸载函数。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号
wangchenxiao@sdc-uvdise057:~$ cat /proc/devices
Character devices:
1 mem
····
Block devices:
2 fd
····
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备,设备号由dev_t
数据类型描述,是一个32位的数据类型,中高 12 位为主设备号,其范围为 0~4095,所低 20 位为次设备号
Linux中每个设备号是独一无二的,申请新的设备号不能与已有的设备号重复,为避免冲突,使用alloc_chrdev_region
和d unregister_chrdev_region
动态申请和注销设备号
1.使用insmod
或modprobe
加载驱动模块。可以通过lsmod
查看当前系统中存在的模块,模块加载后会注册设备,然后可以通过cat /proc/devices
查看当前系统中存在的设备。
2.使用mknod
创建设备对应的设备节点文件。通过ls /dev
查看当前系统中的设备节点文件。设备节点文件是设备在用户空间中的实现,在应用程序中可以通过设备节点文件操作该设备。
3.不再使用该设备后,通过rmmod
卸载设备。
MMU(内存管理单元)完成虚拟内存到物理内存的映射和内存保护的功能。可以通过ioremap
完成物理内存地址到虚拟内存地址的映射,相应在卸载内存时需要通过unremap
释放虚拟内存的映射,在获得指向虚拟内存地址的指针后,可以通过readb
、readw
、readl
和writeb
、writew
、writel
对内存进行读写操作
pinctrl和gpio子系统
疑问:
用途:在裸机驱动开发中,设备的引脚和gpio通过设置相应的寄存器进行配置,在Linux驱动开发中引入了pinctrl和gpio子系统,通过在设备树中添加节点并完成引脚复用,电气等相应属性的配置,即可在驱动程序中通过提供的API函数完成引脚和gpio驱动的开发工作。
在设备树的外设节点下创建pinctrl节点,例如
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
>;
};
fsl,pins属性通过宏定义MX6UL_PAD_GPIO1_IO03__GPIO1_IO03
将GPIO1_IO03复用为GPIO1_IO03,并设置电气属性为0X10B0。
再设备树的根节点下创建gpio对应的外设节点
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
}
pinctrl-0属性设置了该节点对应的pintrcl节点,led-gpio属性设置了该设备对应的gpio节点信息,通过该属性可以获得GPIO编号
在驱动文件中
在设备结构体中定义GPIO编号
在初始化函数中根据of_find_node_by_path获得设备节点,通过of_get_named_gpio获得设备gpio编号,设置gpio输出输入属性
在写入函数中,通过gpio_set_value设置gpio的输出值。
Linux并发与竞争
原子变量:该变量的赋值和读取为原子操作
自旋锁:加锁后其他线程无法访问临界区,会处于忙等状态,因此适用于临界区耗时不长的场景。
信号量/互斥体:其他线程无法访问临界区时会进入休眠状态,进行上下文切换,适用于临界区较为耗时的场景。
同时只允许一个进程打开设备
通过在设备结构体中设置原子变量,初始化时设置该设备的原子变量为1,打开设备时原子变量减一,并判断是否小于零,若小于零则打开失败,关闭设备时原子变量加一。
在设备结构体中定义整形变量表示该设备是否使用,并定义自旋锁保护该变量,在初始化函数中初始化自旋锁,在打开设备时变量加一,如果变量大于零打开失败,关闭设备时变量减一,注意在对变量进行读写时需要通过自旋锁进行保护。
在设备结构体中定义信号量,在初始化函数中初始化信号量为1,在打开函数中获取信号量,若此时信号量为0则打开失败,在关闭函数中释放信号量。
在设备结构体中定义互斥体,在设备初始化函数中初始化互斥体,在打开设备函数中获取互斥体,如果获取互斥体失败则打开文件失败,在关闭函数中释放互斥体。
Linux内核定时器
Linux通过硬件定时器湖区时钟源,该时钟源的频率被称为系统频率或节拍率,该频率可以在编译LInux内核的时候设置。Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0
在设备结构体中定义timer_list定时器结构体,在初始化函数中初始化定时器,添加超时时间和处理函数,并注册定时器,开始定时功能,在关闭函数中删除定时器。
Linux中断
裸机中断系统?
设备树中通过interrupt-parent指定相应的gpio中断控制器,通过interrupts设置引脚号和中断出发标志。gpio中断控制器节点中,通过interrupts设置该gpio中断源信息
在驱动文件的设备结构体中定义中断IO描述结构体,其中包含gpio、终端号、中断服务函数,名字等信息。通过of_get_named_gpio提取gpio号,然后根据irq_of_parse_and_map从设备树中获取中断号,完成中断处理函数后,通过request_irq完成中断的申请。在关闭函数中,释放中断号
阻塞和非阻塞IO
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃,设备驱动文件的默认读取方式就是阻塞式的,若要通过非阻塞方式打开,则需要再open函数中传入O_NONBLOCK
通过等待队列实现阻塞IO
在设备结构体中添加等待队列头结构体
平台设备驱动
在Linux内核中包含了大量驱动代码,因此必须提高驱动的可重用性。
驱动的分离:采用驱动、总线、设备信息模型,将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息,后根据获取到的设备信息来初始化设备,即于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。
驱动的分层:分层的目的也是为了在不同的层处理不同的内容。
定义platform_driver
结构体,在其中设置驱动名称name
,表示/sys/bus/platform/drivers/
目录下的驱动名称,设备树匹配表of_match_table
,用于和设备树中的compatible
属性匹配,probe
函数用于设备和驱动完成匹配时的执行,remove
函数。
在初始化函数中通过platform_driver_register注册platform_driver
结构体,在卸载函数中通过platform_driver_unregister
卸载platform_driver
结构体,
在probe函数中完成初始化,在remove函数中完成卸载。
misc驱动
misc为杂项驱动,主设备号为10。misc_register
会完成申请设备号、初始化cdev、添加cdev、创建类、创建设备等操作,misc_registe会完成删除cdev、注销设备号、删除设备、删除类等操作
input子系统
Linux为输入设备创建的框架,统分为 input 驱动层、input 核心层、input 事件处理层。
首先在设备结构体定义一个input_dev指针,然后通过 input_allocate_device
函数申请一个input_dev
结构体,然后设置该结构体的名称,事件类型和事件值,然后通过input_register_device
向Linux内核注册结构体,卸载驱动模块时需要通过input_unregister_device
和input_free_device
注销和删除设备。然后向input_event
Linux上报事件
LCD
驱动文件基本无需更改,可以在设备树中修改相应的参数