并发与竞争
并发
多个"用户"同时访问同一个共享资源。
竞争
并发和竞争的处理方法
处理并发和竞争的机制:原子操作、自旋锁、信号量和互斥体。
1、原子操作
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量 。
2、自旋锁
Linux 内核使用结构体 spinlock_t 表示自旋锁 。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里"转圈圈"的等待锁可用。
注意:中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。
3、信号量
信号量常常用于控制对共享资源的访问。
相比于自旋锁,信号量可以使线程进入休眠状态 。
使用信号量会提高处理器的使用效率。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。
总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。
4、互斥体
在 FreeRTOS 和 UCOS 中也有互斥体,将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体---mutex。
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申
请互斥体。
mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
中断
中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数 ,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
获取中断号 -> 申请中断 -> 中断使能 -> 中断处理函数 -> 释放中断 -> 中断禁止
为实现中断处理函数的快进快出:
**上半部:**上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理 就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?
Linux 内核提供了多种下半部机制:软中断 、tasklet 、工作队列 。
软中断、tasklet和工作队列都是用于处理中断事件的机制,但它们的优先级和执行上下文有所不同。软中断的优先级最高,可以在任何上下文中被调度执行;tasklet的优先级次之,它在软中断上下文中执行,可以睡眠但不能被抢占;工作队列的优先级最低,它在进程上下文中执行,可以睡眠也可以被抢占。根据具体的需求和性能要求,可以选择合适的下半部机制来处理中断事件。
阻塞IO和非阻塞IO
阻塞式 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。
c
// 设备树中添加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。
c
//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 信号了
设备树-无pinctrl子系统
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置
方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。
在根节点"/"下创建一个名为"alphaled"的子节点
c
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子系统
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始
化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl。
c
// 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,节点内容如下所示:
c
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 文件最后面加入如下所示内容:
c
&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。
};
};
platform驱动程序编写
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcnt1.n>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#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 事件处理层,最终给用户空间提供可访问的设备节点 。
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 结构体组织起来。
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcnt1.n>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#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");
Linux系统下板子上的外设分类如下:
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 RS232/485/GPS驱动实验
在 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 节点 。
Linux PWM驱动实验
PWM 驱动就不需要我们再编写了, NXP 已经写好了,在实际使用的时候只需要修改设备树即可。
GPIO1_IO04 可以作为 PWM3 的输出引脚,所以我们需要在设备树里面添加 GPIO1_IO04
的引脚信息以及 PWM3 控制器对应的节点信息。
Regmap 子系统
为了提高代码复用性和驱动一致性,简化驱动开发过程以及减少底层 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设备寄存器的访问方式,简化了设备驱动程序的开发和维护。