多个“用户”同时访问同一个共享资源。
处理并发和竞争的机制:原子操作、自旋锁、信号量和互斥体。
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量 。
Linux 内核使用结构体 spinlock_t 表示自旋锁 。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
注意:中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。
信号量常常用于控制对共享资源的访问。
相比于自旋锁,信号量可以使线程进入休眠状态 。
使用信号量会提高处理器的使用效率。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。
总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。
在 FreeRTOS 和 UCOS 中也有互斥体,将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申
请互斥体。
mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
获取中断号 -> 申请中断 -> 中断使能 -> 中断处理函数 -> 释放中断 -> 中断禁止
为实现中断处理函数的快进快出:
**上半部:**上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?
Linux 内核提供了多种下半部机制:软中断 、tasklet 、工作队列 。
软中断、tasklet和工作队列都是用于处理中断事件的机制,但它们的优先级和执行上下文有所不同。软中断的优先级最高,可以在任何上下文中被调度执行;tasklet的优先级次之,它在软中断上下文中执行,可以睡眠但不能被抢占;工作队列的优先级最低,它在进程上下文中执行,可以睡眠也可以被抢占。根据具体的需求和性能要求,可以选择合适的下半部机制来处理中断事件。
阻塞式 IO :会将应用程序对应的线程挂起,放到等待队列当中,直到设备资源可以获取为止。
非阻塞 IO:应用程序对应的线程不会挂起,它会通过poll函数不断的轮询,查看驱动设备资源可以使用。
这两种方式都需要应用程序主动的去查询设备的使用情况。
poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来
查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调
用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动
程序中编写 poll 函数。
当驱动程序可以访问时,驱动可以主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。
整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。
设备节点:根节点“/”下
pinctrl结点: iomuxc 节点下
常规操作:先设置某个 PIN 的复用功能、速度、上下拉等,然后再设置 PIN 所对应的 GPIO。
其实对于大多数的 32 位 SOC 而言,引脚的设置基本都是这两方面,因此 Linux 内核针对 PIN 的配置推出了 pinctrl 子系统,对于 GPIO
的配置推出了 gpio 子系统。
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置
方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl。
// 设备树中添加pinctrl节点模板
// 1、创建对应的节点
// 同一个外设的PIN都放在一个节点里面,在iomuxc节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点
pinctrl_test: testgrp{
/*具体的PIN信息*/
};
// 2、添加“fsl,pins”属性来保存信息
// pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息
pinctrl_test: testgrp{
fsl,pins=<
/*设备所使用的PIN配置信息*/
>;
};
// 3、在“fsl,pins”属性中添加PIN配置信息
pinctrl_test: testgrp{
fsl,pins=<
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config是具体设置值*/
>;
};
如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。 gpio 子系统就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。 gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO, Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
//I.MX6ULL-ALPHA 开发板上的 UART1_RTS_B 做为 SD 卡的检测引脚, UART1_RTS_B 复用为 GPIO1_IO19,通过读取这个 GPIO 的高低电平就可以知道 SD 卡有没有插入。
// 设置pinctrl 节点:将 UART1_RTS_B 这个 PIN 复用为 GPIO1_IO19,并且设置电气属性。
pinctrl_hog_1: hoggrp-1{
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
>
}
//设置gpio 节点:在设备树中 SD 卡节点下添加一个属性来描述 SD 卡的 CD 引脚就行了, SD卡驱动直接读取这个属性值就知道 SD 卡的 CD 引脚使用的是哪个GPIO 了。
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
/* pinctrl-3 = <&pinctrl_hog_1>; *///用来调用名为 "pinctrl_hog_1" 的引脚组配置。
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;//用来指定一个GPIO控制引脚。描述了 SD 卡的 CD 引脚使用的哪个 IO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。
keep-power-in-suspend;
enable-sdio-wakeup;
vmmc-supply = <®_sd1_vmmc>;
status = "okay";
};
// 根据上面这些信息, SD 卡驱动程序就可以使用 GPIO1_IO19 来检测 SD 卡的 CD 信号了
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置
方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。
在根节点“/”下创建一个名为“alphaled”的子节点
alphaled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始
化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl。
// SPI设备节点
&ecspi1{
fsl,spi-num-chipselects = <1>; // 表示只有一个设备
cs-gpios = <&gpio4 9 0>; // 片选信号为GPIO4_IO09
pinctrl-names = "defaults";// SPI设备所使用的IO名字
pinctrl-0 = <&pinctrl_ecspi1>;// 使用的IO对应的pinctrl节点
status = "okay"; // 状态属性
flash: m25p80@0{ // ECSP接口上接了一个m25p80设备,“0”表示 m25p80 的接到了 ECSPI 的通道 0上
#address-cells = <1>;
#size-cells = <1>;
//属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长
(cell),地址长度也占用一个字长(cell)。
compatible = "st, m25p32"; // 用于匹配设备驱动
spi-max-frequency = <20000000>;// 设置SPI控制器的最高频率
reg = <0>; // 设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的“0”一样。
};
};
当我们向Linux内核注册成功spi_driver以后就可以使用SPI核心层提供的API函数来对设备进行读写操作了。
首先在 imx6ull-alientek-emmc.dts 文件中添加 ICM20608 所使用的 IO 信息,在 iomuxc 节点中添加一个新的子节点来描述 ICM20608 所使用的 SPI 引脚,子节点名字为 pinctrl_ecspi3,节点内容如下所示:
pinctrl_ecspi3: icm20608 {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
>;
};
在 imx6ull-alientek-emmc.dts 文件中并没有任何向 ecspi3 节点追加内容的代码,这是因为NXP 官方的 6ULL EVK 开发板上没有连接 SPI 设备。在 imx6ull-alientek-emmc.dts 文件最后面加入如下所示内容:
&ecspi3 {
fsl,spi-num-chipselects = <1>;// 表示只有一个设备
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;// 一定要使用 “cs-gpios”属性来描述片选引脚, SPI 主机驱动就会控制片选引脚。
pinctrl-names = "default";// SPI设备所使用的IO名字
pinctrl-0 = <&pinctrl_ecspi3>;// 设置 IO 要使用的 pinctrl 子节点
status = "okay";
spidev: icm20608@0 { //icm20608 设备子节点,因为 icm20608 连接在 ECSPI3 的第 0 个通道上,因此@后面为 0。
compatible = "alientek,icm20608"; //设置节点属性兼容值
spi-max-frequency = <8000000>;// SPI 最大时钟频率
reg = <0>;// icm20608 连接在通道 0 上,因此 reg 为 0。
};
};
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define LEDDEV_CNT 1 /* 设备号长度 */
#define LEDDEV_NAME "dtsplatled" /* 设备名字 */
#define LEDOFF 0
#define LEDON 1
/*leddev设备结构体*/
struct leddev_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device;/* 设备 */
int major;/* 主设备号 */
struct device_node *node;/* LED 设备节点 */
int led0;/* LED 灯 GPIO 标号 */
};
struct leddev_dev leddev; // 实例化对象
/*LED打开/关闭*/
void led0_switch(u8 sta)
{
if(sta == LEDON)
gpio_set_value(leddev.led0, 0);
else if(sta == LEDOFF)
gpio_set_value(leddev.led0, 1);
}
/*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
{
// 将设备的私有数据指针指向leddev结构体的地址。filp->private_data是文件指针的一个成员,用于存储与该文件相关的私有数据。在这里,将其指向leddev结构体的地址,以便在后续的操作中可以通过filp->private_data访问和操作leddev结构体中的数据。
filp->private_data = &leddev; /*设置私有数据*/
return 0;
}
/*向设备写数据*/
// 从用户空间拷贝数据到内核空间的缓冲区databuf,然后根据拷贝的数据判断LED的状态,并调用相应的函数控制LED的开关状态。返回0表示写操作成功。如果拷贝数据失败,则返回错误码-EFAULT。
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
//filp表示文件指针,buf表示用户空间传递的数据缓冲区指针,cnt表示写入数据的字节数,offt表示文件偏移量。
{
int retvalue; // 用于保存copy_from_user函数的返回值。
unsigned char databuf[2]; // 用于存储从用户空间拷贝的数据。
unsigned char ledstat; // 用于存储LED的状态。
retvalue = copy_from_user(databuf, buf, cnt); // 调用copy_from_user函数将用户空间的数据拷贝到内核空间的databuf数组中。copy_from_user函数返回成功拷贝的字节数,如果返回值小于0,则表示拷贝失败。
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if (ledstat == LEDON) {
led0_switch(LEDON);
} else if (ledstat == LEDOFF) {
led0_switch(LEDOFF);
}
return 0;
}
/*设备操作函数*/
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
}
/*probe函数,当驱动与设备匹配以后,此函数就会执行*/
tatic int led_probe(struct platform_device *dev)
{
printk("led driver and device was matched!\r\n");
/* 1、设置设备号 */
if (leddev.major) {
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
} else {
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
leddev.major = MAJOR(leddev.devid);
}
/* 2、注册设备 */
cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
/* 3、创建类 */
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class)) {
return PTR_ERR(leddev.class);
}
/* 4、创建设备 */
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device)) {
return PTR_ERR(leddev.device);
}
/* 5、初始化IO */
leddev.node = of_find_node_by_path("/gpioled");
if (leddev.node == NULL){
printk("gpioled node nost find!\r\n");
return -EINVAL;
}
leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
if (leddev.led0 < 0) {
printk("can't get led-gpio\r\n");
return -EINVAL;
}
gpio_request(leddev.led0, "led0");
gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平 */
return 0;
}
/*remove函数,移除platform驱动的时候此函数会执行*/
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.led0, 1); /*关闭LED0*/
cdev_del(&leddev.cdev);/*删除cdev*/
unregister_chrdev_region(leddev.devid, LEDDEV_CNT);
device_destory(leddev.class, leddev.devid);
class_destroy(leddev.class);
retrun 0;
}
/*匹配列表:用于匹配设备树中的设备节点和驱动程序。*/
// 通过这样的定义,当系统加载该驱动程序时,会根据匹配列表中的匹配项和设备树中的设备节点进行匹配,如果匹配成功,则会调用probe函数进行初始化操作。当设备被移除时,会调用remove函数进行清理操作。
static const struct of_device_id led_of_match[] = {
{ .compatible = "atkalpha-gpioled"},
{ /*Sentinel*/} // 是一个空的匹配项,用于标记列表的结束。
}
/*platform驱动结构体*/
static struct platform_driver led_driver = {
.driver = {
.name = "imx6ul-led",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
/*驱动模块加载函数,表示设备被插入时要执行的操作。*/
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
/*驱动模块卸载函数,表示设备被移除时要执行的操作。*/
static int __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
input 子系统分为 input 驱动层、 input 核心层、 input 事件处理层,最终给用户空间提供可访问的设备节点 。
input 子系统就是管理输入的子系统,和 pinctrl、 gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。
Linux 内核会使用 input_event 结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于 input_event 结构体。
在按键消抖定时器处理函数中上报输入事件,也就是使用 input_report_key函数上报按键事件以及按键值,最后使用 input_sync 函数上报一个同步事件,这一步一定得做!
使用 input_allocate_device 函数申请 input_dev,然后设置相应的事件以及事件码(也就是 KEY 模拟成那个按键,这里我们设置为 KEY_0)。最后使用 input_register_device函数向 Linux 内核注册 input_dev。
当我们向 Linux 内核成功注册 input_dev 设备以后,会在/dev/input 目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX 就是对应的 input 设备文件。
在input子系统中,每个事件的发生都使用事件(type)->子事件(code)->值(value)
所有的输入设备的主设备号都是13,input-core通过次设备来将输入设备进行分类。
我们读取这个文件就可以获取到输入事件信息,比如按键值什么的。使用 read函数读取输入设备文件,也就是/dev/input/eventX,读取到的数据按照 input_event 结构体组织起来。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define KEYINPUT_CNT 1 // 设备号个数
#define KEYINPUT_NAME "keyinput" // 名字
#define KEY0VALUE 0X01 // KEY0按键值
#define INVAKEY 0XFF // 无效的按键值
#define KEY_NUM 1 // 按键数量
// 中断IO描述结构体
struct irq_keydesc{
int gpio; // gpio
int irqnum; // 中断号
unsigned char value; // 按键对应的键值
char name[10]; // 名字
irqreturn_t (*handler)(int, void *); //中断服务函数
};
//keyinput设备结构体
struct keyinput_dev{
dev_t devid;/* 设备号 */
struct cdev cdev;/* cdev */
struct class *class;/* 类 */
struct device *device;/* 设备 */
struct device_node *nd;/* 设备节点 */
struct timer_list timer;/* 定义一个定时器 */
struct irq_keydesc irqkeydesc[KEY_NUM];/* 按键描述数组 */
unsigned char curkeynum;/* 当前的按键号 */
struct input_dev *inputdev;/* input 结构体 */
};
struct keyinput_dev keyinputdev;/* key input 设备 */
// 中断服务函数
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct keyinput_dev *dev = (struct keyinput_dev *)dev_id;
dev->curkeynum = 0;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
// 定时器服务函数
/*用于按键消抖,定时器到了以后,再次读取按键值,如果按键还是处于按下状态就表示按键有效。*/
void timer_function(unsigned long arg)
{
unsigned char value;
unsigned char num;
struct irq_keydesc *keydesc;
struct keyinput_dev *dev = (struct keyinput_dev *)arg;
num = dev->curkeynum;
keydesc = &dev->irqkeydesc[num];
value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
if(value == 0){ /* 按下按键 */
/* 上报按键值 */
//input_event(dev->inputdev, EV_KEY, keydesc->value, 1);
input_report_key(dev->inputdev, keydesc->value, 1);/*1, 按下*/
input_sync(dev->inputdev);
}
else
{ /* 按键松开 */
//input_event(dev->inputdev, EV_KEY, keydesc->value, 0);
input_report_key(dev->inputdev, keydesc->value, 0);
input_sync(dev->inputdev);
}
}
// 按键IO初始化
static int keyio_init(void)
{
unsigned char i = 0;
char name[10];
int ret = 0;
keyinputdev.nd = of_find_node_by_path("/key");
if (keyinputdev.nd== NULL){
printk("key node not find!\r\n");
return -EINVAL;
}
/* 提取 GPIO */
for (i = 0; i < KEY_NUM; i++) {
keyinputdev.irqkeydesc[i].gpio = of_get_named_gpio(keyinputdev.nd,"key-gpio", i);
if (keyinputdev.irqkeydesc[i].gpio < 0) {
printk("can't get key%d\r\n", i);
}
}
/* 初始化 key 所使用的 IO,并且设置成中断模式 */
for (i = 0; i < KEY_NUM; i++) {
memset(keyinputdev.irqkeydesc[i].name, 0, sizeof(name));
sprintf(keyinputdev.irqkeydesc[i].name, "KEY%d", i);
gpio_request(keyinputdev.irqkeydesc[i].gpio,
// 驱动入口函数
static int _init keyinput_init(void)
{
keyio_init();
return 0;
}
// 驱动出口函数
static void _exit keyinput_init(void)
{
unsigned int i = 0;
/* 删除定时器 */
del_timer_sync(&keyinputdev.timer);
/* 释放中断 */
for(i = 0; i < KEY_NUM; i++){
free_irq(keyinputdev.irqkeydesc[i].irqnum, &keyinputdev);
}
/* 释放IO */
for(i = 0; i < KEY_NUM; i++){
gpio_free(keyinputdev.irqkeydesc[i].gpio);
}
/* 释放input_dev */
input_unregister_device(keyinputdev.inputdev);
input_free_device(keyinputdev.inputdev);
}
module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
1. Graphics(图形)子系统:负责管理显示器、显卡和图形处理器等图形设备。
2. Storage(存储)子系统:负责管理硬盘、固态硬盘、光驱和闪存等存储设备。
3. Network(网络)子系统:负责管理网络接口卡、无线网卡和网络协议栈等网络设备。
4. Sound(声音)子系统:负责管理声卡、扬声器和麦克风等音频设备。
5. USB(通用串行总线)子系统:负责管理USB接口和相关设备。
6. PCI(外设互联总线)子系统:负责管理PCI总线和相关设备。
7. I2C(Inter-Integrated Circuit)子系统:负责管理I2C总线和相关设备。
8. SPI(Serial Peripheral Interface)子系统:负责管理SPI总线和相关设备。
9. UART(通用异步收发传输器)子系统:负责管理串口和相关设备。
10. GPIO(通用输入输出)子系统:负责管理通用输入输出引脚和相关设备。
11. Watchdog(看门狗)子系统:负责管理看门狗定时器和相关设备,用于系统的自动重启。
12. Power Management(电源管理)子系统:负责管理电源和节能功能。
13. Thermal(热管理)子系统:负责管理温度传感器和风扇等热管理设备。
14. RTC(实时时钟)子系统:负责管理实时时钟和相关设备。
15. Camera(摄像头)子系统:负责管理摄像头和相关设备。
16. Touch(触摸)子系统:负责管理触摸屏和触摸板等触摸设备。
17. MISC(杂项)子系统:负责管理一些不属于其他特定子系统的杂项设备。如硬件监控设备、电源管理设备、硬件随机数生成器等
18. Input(输入)子系统:负责管理和处理输入设备,如键盘、鼠标、触摸屏等。
在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。
对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、 RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。
I.MX6U 的 UART 驱动 NXP 已经编写好了,所以不需要我们编写。 我们要做的就是在设备树中添加 UART3 对应的设备节点即可。打开 imx6ull-alientek-emmc.dts文件,在此文件中只有 UART1 对应的 uart1 节点,并没有 UART3 对应的节点,因此我们可以参考 uart1 节点创建 uart3 节点 。
PWM 驱动就不需要我们再编写了, NXP 已经写好了,在实际使用的时候只需要修改设备树即可。
GPIO1_IO04 可以作为 PWM3 的输出引脚,所以我们需要在设备树里面添加 GPIO1_IO04
的引脚信息以及 PWM3 控制器对应的节点信息。
为了提高代码复用性和驱动一致性,简化驱动开发过程以及减少底层 I/O 操作次数,提高访问效率。使用Regmap 。(原因:regmap 在驱动和硬件之间添加了 cache,降低了低速 I/O 的操作次数,提高了访问效率,缺点是实时性会降低。 )
作用:针对 I2C 和 SPI 设备寄存器的操作都是通过相关的 API 函数进行操作的。这样 Linux 内核中就会充斥着大量的重复、冗余代码,再者,代码的复用性也会降低。由于这些操作本质上都是对寄存器进行操作的,因此为了方便内核开发人员统一访问 I2C/SPI 设备,为此引入了 Regmap 子系统。
如果在Linux内核中直接通过相关的API函数进行I2C和SPI设备寄存器的操作,会导致代码中存在大量重复和冗余的代码。这是因为每个设备的寄存器操作都需要编写相应的API函数,而这些函数可能会有很多相似的部分,导致代码的冗余。
此外,直接通过API函数进行设备寄存器操作也会降低代码的复用性。如果每个设备都有自己的API函数,那么在开发其他设备驱动程序时,无法复用已有的代码,需要重新编写相应的API函数。这样会增加开发的工作量,并且可能导致代码的一致性和可维护性的问题。
为了解决这些问题,引入Regmap子系统可以统一访问I2C和SPI设备的寄存器,减少重复和冗余的代码,并提高代码的复用性。Regmap提供了一种通用的方式来读取和写入设备寄存器,可以在不同的设备驱动程序中共享和复用相同的代码,提高开发效率和代码的可维护性。
为了优化针对I2C和SPI设备寄存器的操作,可以使用Regmap子系统来统一访问这些设备。Regmap是Linux内核中的一个子系统,用于管理和访问设备寄存器。它提供了一种通用的方式来读取和写入设备寄存器,减少了重复的代码,并提高了代码的复用性。
使用Regmap子系统的步骤如下:
1. 在设备驱动程序中定义一个Regmap结构体,用于管理设备寄存器。可以使用regmap_init_*函数来初始化Regmap结构体,根据设备的通信接口选择相应的初始化函数,如regmap_init_i2c()用于I2C设备,regmap_init_spi()用于SPI设备。
2. 在设备驱动程序中定义一个Regmap配置结构体,用于配置Regmap的参数,如设备地址、寄存器地址宽度等。可以使用regmap_config_init_*函数来初始化Regmap配置结构体,根据设备的通信接口选择相应的初始化函数,如regmap_config_init_i2c()用于I2C设备,regmap_config_init_spi()用于SPI设备。
3. 在设备驱动程序中使用regmap_register_*函数将Regmap结构体和Regmap配置结构体注册到内核,以便后续使用。
4. 在设备驱动程序中使用regmap_read()和regmap_write()函数来读取和写入设备寄存器。这些函数会根据Regmap结构体和Regmap配置结构体来进行相应的操作,简化了对设备寄存器的访问。
通过使用Regmap子系统,可以减少重复的代码,提高代码的复用性,并且统一了对I2C和SPI设备寄存器的访问方式,简化了设备驱动程序的开发和维护。