IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 【正点原子Linux连载】第五十一章 Linux中断实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0 -> 正文阅读

[嵌入式]【正点原子Linux连载】第五十一章 Linux中断实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子Linux感兴趣的同学可以加群讨论:935446741
4)关注正点原子公众号,获取最新资料更新
在这里插入图片描述

第五十一章 Linux中断实验

不管是裸机实验还是Linux下的驱动实验,中断都是频繁使用的功能,关于I.MX6U的中断原理已经在第十七章做了详细的讲解,在裸机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能IRQ等等。Linux内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。本章我们就来学习一下如何在Linux下使用中断。

51.1 Linux中断简介
51.1.1 Linux中断API函数
先来回顾一下裸机实验里面中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向irqTable数组的指定标号处写入中断服务函数
②、中断发生以后进入IRQ中断服务函数,在IRQ中断服务函数在数组irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
在Linux内核中也提供了大量的中断相关的API函数,我们来看一下这些跟中断有关的API函数:
1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在Linux内核中使用一个int变量表示中断号,关于中断号我们已经在第十七章讲解过了。
2、request_irq函数
在Linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用request_irq函数。request_irq函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq函数原型如下:

int request_irq(unsigned int 		irq, 
			irq_handler_t 		handler, 
			unsigned long 	flags,
	    	const char 		*name, 
			void 			*dev)
函数参数和返回值含义如下:
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。

flags:中断标志,可以在文件include/linux/interrupt.h里面查看所有的中断标志,这里我们介绍几个常用的中断标志,如表51.1.1.1所示:
标志 描述
IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq函数的dev参数就是唯一区分他们的标志。
IRQF_ONESHOT 单次中断,中断执行一次就结束。
IRQF_TRIGGER_NONE 无触发。
IRQF_TRIGGER_RISING 上升沿触发。
IRQF_TRIGGER_FALLING 下降沿触发。
IRQF_TRIGGER_HIGH 高电平触发。
IRQF_TRIGGER_LOW 低电平触发。
表51.1.1.1 常用的中断标志
比如I.MX6U-ALPHA开发板上的KEY0使用GPIO1_IO18,按下KEY0以后为低电平,因此可以设置为下降沿触发,也就是将flags设置为IRQF_TRIGGER_FALLING。表51.1.1.1中的这些标志可以通过“|”来实现多种组合。
name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。
dev:如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。
3、free_irq函数
使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:
void free_irq(unsigned int irq,
void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
4、中断处理函数
使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:
示例代码51.1.1.1 irqreturn_t结构

10 enum irqreturn {
11  	IRQ_NONE        	= (0 << 0),
12  	IRQ_HANDLED     	= (1 << 0),
13  	IRQ_WAKE_THREAD  	= (1 << 1),
14 };
15 
16 typedef enum irqreturn irqreturn_t;
可以看出irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

return IRQ_RETVAL(IRQ_HANDLED)
5、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq和disable_irq用于使能和禁止指定的中断,irq就是要禁止的中断号。disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习STM32的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
local_irq_enable用于使能当前处理器中断系统,local_irq_disable用于禁止当前处理器中断系统。假如A任务调用local_irq_disable关闭全局中断10S,当关闭了2S的时候B任务开始运行,B任务也调用local_irq_disable关闭全局中断3S,3秒以后B任务调用local_irq_enable函数将全局中断打开了。此时才过去2+3=5秒的时间,然后全局中断就被打开了,此时A任务要关闭10S全局中断的愿望就破灭了,然后A任务就“生气了”,结果很严重,可能系统都要被A任务整崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对,local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。local_irq_restore用于恢复中断,将中断到flags状态。
51.1.2 上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生,SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是我们都知道IIC的速度最高也只有400Kbit/S,所以在中断中通过IIC读取数据就会浪费时间。我们可以将通过IIC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此,Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。
1、软中断
一开始Linux内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代“BH”机制,完全可以使用软中断和tasklet来替代BH,从2.5版本的Linux内核开始BH已经被抛弃了。Linux内核使用结构体softirq_action表示软中断, softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:

示例代码51.1.2.1 softirq_action结构体
433 struct softirq_action
434 {
435     void    (*action)(struct softirq_action *);
436 };
在kernel/softirq.c文件中一共定义了10个软中断,如下所示:

示例代码51.1.2.2 softirq_vec数组
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中,定义如下:

示例代码51.1.2.3 softirq_vec数组
enum
{
    HI_SOFTIRQ=0,           	/* 高优先级软中断 		*/
    TIMER_SOFTIRQ,          	/* 定时器软中断 			*/
    NET_TX_SOFTIRQ,         	/* 网络数据发送软中断 	*/
    NET_RX_SOFTIRQ,         	/* 网络数据接收软中断 	*/
    BLOCK_SOFTIRQ,          
    BLOCK_IOPOLL_SOFTIRQ,   
    TASKLET_SOFTIRQ,        	/* tasklet软中断 		*/
    SCHED_SOFTIRQ,          	/* 调度软中断 			*/
    HRTIMER_SOFTIRQ,        	/* 高精度定时器软中断	*/
    RCU_SOFTIRQ,            	/* RCU软中断			*/

    NR_SOFTIRQS
};
可以看出,一共有10个软中断,因此NR_SOFTIRQS为10,因此数组softirq_vec有10个元素。softirq_action结构体中的action成员变量就是软中断的服务函数,数组softirq_vec是个全局数组,因此所有的CPU(对于SMP系统而言)都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个CPU所执行的软中断服务函数确是相同的,都是数组softirq_vec中定义的action函数。要使用软中断,必须先使用open_softirq函数注册对应的软中断处理函数,open_softirq函数原型如下:

void open_softirq(int nr, void (*action)(struct softirq_action *))
函数参数和返回值含义如下:
nr:要开启的软中断,在示例代码51.1.2.3中选择一个。
action:软中断对应的处理函数。
返回值:没有返回值。
注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
nr:要触发的软中断,在示例代码51.1.2.3中选择一个。
返回值:没有返回值。
软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:

示例代码51.1.2.4 softirq_init函数内容
634 void __init softirq_init(void)
635 {
636     int cpu;
637 
638     for_each_possible_cpu(cpu) {
639         per_cpu(tasklet_vec, cpu).tail =
640             &per_cpu(tasklet_vec, cpu).head;
641         per_cpu(tasklet_hi_vec, cpu).tail =
642             &per_cpu(tasklet_hi_vec, cpu).head;
643     }
644 
645     open_softirq(TASKLET_SOFTIRQ, tasklet_action);
646     open_softirq(HI_SOFTIRQ, tasklet_hi_action);
647 }
从示例代码51.1.2.4可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。
2、tasklet
tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议大家使用tasklet。Linux内核使用tasklet_struct结构体来表示tasklet:
示例代码51.1.2.5 tasklet_struct结构体
484 struct tasklet_struct
485 {
486     struct tasklet_struct *next;    /* 下一个tasklet 			*/
487     unsigned long state;            	/* tasklet状态    			*/
488     atomic_t count;                 	/* 计数器,记录对tasklet的引用数 */
489     void (*func)(unsigned long);   	/* tasklet执行的函数 		*/
490     unsigned long data;             	/* 函数func的参数 			*/
491 };
第489行的func函数就是tasklet要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用tasklet,必须先定义一个tasklet,然后使用tasklet_init函数初始化tasklet,taskled_init函数原型如下:

void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
函数参数和返回值含义如下:
t:要初始化的tasklet
func:tasklet的处理函数。
data:要传递给func函数的参数
返回值:没有返回值。
也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化, DECLARE_TASKLET定义在include/linux/interrupt.h文件中,定义如下:
DECLARE_TASKLET(name, func, data)
其中name为要定义的tasklet名字,这个名字就是一个tasklet_struct类型的时候变量,func就是tasklet的处理函数,data是传递给func函数的参数。
在上半部,也就是中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行,tasklet_schedule函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
t:要调度的tasklet,也就是DECLARE_TASKLET宏里面的name。
返回值:没有返回值。
关于tasklet的参考使用示例如下所示:

示例代码51.1.2.7 tasklet使用示例
/* 定义taselet 			*/
struct tasklet_struct testtasklet;

/* tasklet处理函数 		*/
void testtasklet_func(unsigned long data)
{
    /* tasklet具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度tasklet 		*/
    tasklet_schedule(&testtasklet);
    ......
}

/* 驱动入口函数 				*/
static int __init xxxx_init(void)
{
    ......
    /* 初始化tasklet 		*/
    tasklet_init(&testtasklet, testtasklet_func, data);
    /* 注册中断处理函数 		*/
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}
2、工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet。

Linux内核使用work_struct结构体表示一个工作,内容如下(省略掉条件编译):

示例代码51.1.2.8 work_struct结构体
struct work_struct {
    atomic_long_t data;    
    struct list_head entry; 
    work_func_t func;       	/* 工作队列处理函数 	*/
};
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,内容如下(省略掉条件编译):
示例代码51.1.2.9 workqueue_struct结构体
struct workqueue_struct {
    struct list_head    pwqs;       
    struct list_head    list;       
    struct mutex        mutex;      
    int         work_color; 
    int         flush_color;    
    atomic_t        nr_pwqs_to_flush; 
    struct wq_flusher   *first_flusher; 
    struct list_head    flusher_queue;  
    struct list_head    flusher_overflow;
    struct list_head    maydays;    
    struct worker       *rescuer;   
    int         nr_drainers;    
    int         saved_max_active; 
    struct workqueue_attrs  *unbound_attrs; 
    struct pool_workqueue   *dfl_pwq;   
    char            name[WQ_NAME_LEN]; 
    struct rcu_head     rcu;
    unsigned int        flags ____cacheline_aligned; 
    struct pool_workqueue __percpu *cpu_pwqs; 
    struct pool_workqueue __rcu *numa_pwq_tbl[]; 
};
Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程,worker结构体内容如下:
示例代码51.1.2.10 worker结构体
struct worker {
    union {
        struct list_head    entry;  
        struct hlist_node   hentry; 
    };
    struct work_struct  *current_work;  
    work_func_t     current_func;   
    struct pool_workqueue   *current_pwq; 
    bool            desc_valid; 
    struct list_head    scheduled;  
    struct task_struct  *task;      
    struct worker_pool  *pool;                      
    struct list_head    node;       
    unsigned long       last_active;    
    unsigned int        flags;      
    int         	id;     
    char            desc[WORKER_DESC_LEN];
    struct workqueue_struct *rescue_wq; 
};
从示例代码51.1.2.10可以看出,每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个work_struct结构体变量即可,然后使用INIT_WORK宏来初始化工作,INIT_WORK宏定义如下:

#define INIT_WORK(_work, _func)
_work表示要初始化的工作,_func是工作对应的处理函数。
也可以使用DECLARE_WORK宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n表示定义的工作(work_struct),f表示工作对应的处理函数。
和tasklet一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。
关于工作队列的参考使用示例如下所示:

示例代码51.1.2.11 工作队列使用示例
/* 定义工作(work)           */
struct  work_struct testwork;

/* work处理函数         	*/
void testwork_func_t(struct work_struct *work);
{
    /* work具体处理内容 	*/
}

/* 中断处理函数 				*/
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度work       		*/
    schedule_work(&testwork);
    ......
}

/* 驱动入口函数           	*/
static int __init xxxx_init(void)
{
    ......
    /* 初始化work      		*/
    INIT_WORK(&testwork, testwork_func_t);
    /* 注册中断处理函数     	*/
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}

51.1.3 设备树中断信息节点
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开imx6ull.dtsi文件,其中的intc节点就是I.MX6ULL的中断控制器节点,节点内容如下所示:

示例代码51.1.3.1 中断控制器intc节点
1 intc: interrupt-controller@00a01000 {
2     compatible = "arm,cortex-a7-gic";
3     #interrupt-cells = <3>;
4     interrupt-controller;
5     reg = <0x00a01000 0x1000>,
6            <0x00a02000 0x100>;
7 };
第2行,compatible属性值为“arm,cortex-a7-gic”在Linux内核源码中搜索“arm,cortex-a7-gic”即可找到GIC中断控制器驱动文件。
第3行,#interrupt-cells和#address-cells、#size-cells一样。表示此中断控制器下设备的cells大小,对于设备而言,会使用interrupts属性描述中断信息,#interrupt-cells描述了interrupts属性的cells大小,也就是一条信息有几个cells。每个cells都是32位整形值,对于ARM处理的GIC来说,一共有3个cells,这三个cells的含义如下:
第一个cells:中断类型,0表示SPI中断,1表示PPI中断。
第二个cells:中断号,对于SPI中断来说中断号的范围为0~987,对于PPI中断来说中断号的范围为0~15。
第三个cells:标志,bit[3:0]表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit[15:8]为PPI中断的CPU掩码。
第4行,interrupt-controller节点为空,表示当前节点是中断控制器。
对于gpio来说,gpio节点也可以作为中断控制器,比如imx6ull.dtsi文件中的gpio5节点内容如下所示:
示例代码51.1.3.2 gpio5设备节点
1  gpio5: gpio@020ac000 {
2      compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
3      reg = <0x020ac000 0x4000>;
4      interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
5               <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
6      gpio-controller;
7      #gpio-cells = <2>;
8      interrupt-controller;
9      #interrupt-cells = <2>;
10 };
第4行,interrupts描述中断源信息,对于gpio5来说一共有两条信息,中断类型都是SPI,触发电平都是IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是74,一个是75,打开可以打开《IMX6ULL参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表3-1,有如图50.1.3.1所示的内容:

在这里插入图片描述

图50.1.3.1 中断表
从图50.1.3.1可以看出,GPIO5一共用了2个中断号,一个是74,一个是75。其中74对应GPIO5_IO00GPIO5_IO15这低16个IO,75对应GPIO5_IO16GPIOI5_IO31这高16位IO。
第8行,interrupt-controller表明了gpio5节点也是个中断控制器,用于控制gpio5所有IO的中断。
第9行,将#interrupt-cells修改为2。
打开imx6ull-alientek-emmc.dts文件,找到如下所示内容:

示例代码51.1.3.3 fxls8471设备节点
1 fxls8471@1e {
2     compatible = "fsl,fxls8471";
3     reg = <0x1e>;
4     position = <0>;
5     interrupt-parent = <&gpio5>;
6     interrupts = <0 8>;
7 };
fxls8471是NXP官方的6ULL开发板上的一个磁力计芯片,fxls8471有一个中断引脚链接到了I.MX6ULL的SNVS_TAMPER0因脚上,这个引脚可以复用为GPIO5_IO00。
第5行,interrupt-parent属性设置中断控制器,这里使用gpio5作为中断控制器。
第6行,interrupts设置中断信息,0表示GPIO5_IO00,8表示低电平触发。
简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息cells个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。

51.1.4 获取中断号
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过irq_of_parse_and_map函数从interupts属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,
int index)
函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts属性可能包含多条中断信息,通过index指定要获取的信息。
返回值:中断号。
如果使用GPIO的话,可以使用gpio_to_irq函数来获取gpio对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio:要获取的GPIO编号。
返回值:GPIO对应的中断号。
51.2 硬件原理图分析
本章实验硬件原理图参考15.2小节即可。
51.3 实验程序编写
本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 13_irq。
本章实验我们驱动I.MX6U-ALPHA开发板上的KEY0按键,不过我们采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。通过本章我们可以学习到Linux内核中断的使用方法,以及对Linux内核定时器的回顾。
51.3.1 修改设备树文件
本章实验使用到了按键KEY0,按键KEY0使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

示例代码51.3.1.1 key节点信息
1  key {
2      #address-cells = <1>;
3      #size-cells = <1>;
4      compatible = "atkalpha-key";
5      pinctrl-names = "default";
6      pinctrl-0 = <&pinctrl_key>;
7      key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;	/* KEY0 */
8      interrupt-parent = <&gpio1>;
9      interrupts = <18 IRQ_TYPE_EDGE_BOTH>; 	/* FALLING RISING */
10     status = "okay";
11 };
第8行,设置interrupt-parent属性值为“gpio1”,因为KEY0所使用的GPIO为GPIO1_IO18,也就是设置KEY0的GPIO中断控制器为gpio1。
第9行,设置interrupts属性,也就是设置中断源,第一个cells的18表示GPIO1组的18号IO。IRQ_TYPE_EDGE_BOTH定义在文件include/linux/irq.h中,定义如下:
示例代码51.3.1.2 中断线状态
76  enum {
77      IRQ_TYPE_NONE       		= 0x00000000,
78      IRQ_TYPE_EDGE_RISING 		= 0x00000001,
79      IRQ_TYPE_EDGE_FALLING   	= 0x00000002,
80      IRQ_TYPE_EDGE_BOTH        = (IRQ_TYPE_EDGE_FALLING | 
IRQ_TYPE_EDGE_RISING),
81      IRQ_TYPE_LEVEL_HIGH 		= 0x00000004,
82      IRQ_TYPE_LEVEL_LOW  		= 0x00000008,
83      IRQ_TYPE_LEVEL_MASK 		= (IRQ_TYPE_LEVEL_LOW | 
IRQ_TYPE_LEVEL_HIGH),
......
100 };
从示例代码51.3.1.2中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。

设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb文件启动Linux系统。
51.3.2 按键中断驱动程序编写
新建名为“13_irq”的文件夹,然后在13_irq文件夹里面创建vscode工程,工作区命名为“imx6uirq”。工程创建好以后新建imx6uirq.c文件,在imx6uirq.c里面输入如下内容:

示例代码51.3.2.1 imx6uirq.c文件代码
1   #include <linux/types.h>
2   #include <linux/kernel.h>
3   #include <linux/delay.h>
4   #include <linux/ide.h>
5   #include <linux/init.h>
6   #include <linux/module.h>
7   #include <linux/errno.h>
8   #include <linux/gpio.h>
9   #include <linux/cdev.h>
10  #include <linux/device.h>
11  #include <linux/of.h>
12  #include <linux/of_address.h>
13  #include <linux/of_gpio.h>
14  #include <linux/semaphore.h>
15  #include <linux/timer.h>
16  #include <linux/of_irq.h>
17  #include <linux/irq.h>
18  #include <asm/mach/map.h>
19  #include <asm/uaccess.h>
20  #include <asm/io.h>
21  /***************************************************************
22  Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
23  文件名 	: imx6uirq.c
24  作者   	: 左忠凯
25  版本   	: V1.0
26  描述   	: Linux中断驱动实验
27  其他   	: 无
28  论坛   	: www.openedv.com
29  日志   	: 初版V1.0 2019/7/26 左忠凯创建
30  ***************************************************************/
31  #define IMX6UIRQ_CNT     	1           	/* 设备号个数  		*/
32  #define IMX6UIRQ_NAME  	"imx6uirq"	/* 名字       		*/
33  #define KEY0VALUE       	0X01       	/* KEY0按键值 		*/
34  #define INVAKEY          	0XFF       	/* 无效的按键值 		*/
35  #define KEY_NUM          	1           	/* 按键数量     		*/
36  
37  /* 中断IO描述结构体 */
38  struct irq_keydesc {
39      int gpio;                         	/* gpio 			*/
40      int irqnum;                       	/* 中断号     		*/
41      unsigned char value;             	/* 按键对应的键值	*/
42      char name[10];                    	/* 名字 			*/
43      irqreturn_t (*handler)(int, void *); 	/* 中断服务函数 */
44  };
45  
46  /* imx6uirq设备结构体 */
47  struct imx6uirq_dev{
48      dev_t devid;            	/* 设备号     				*/
49      struct cdev cdev;       	/* cdev     				*/
50      struct class *class;    	/* 类      					*/
51      struct device *device; 	/* 设备    					*/
52      int major;              	/* 主设备号   				*/
53      int minor;              	/* 次设备号   				*/
54      struct device_node  *nd;	/* 设备节点 					*/
55      atomic_t keyvalue;      	/* 有效的按键键值 			*/
56      atomic_t releasekey;    	/* 标记是否完成一次完成的按键*/
57      struct timer_list timer;	/* 定义一个定时器*/
58      struct irq_keydesc irqkeydesc[KEY_NUM]; 	/* 按键描述数组 */
59      unsigned char curkeynum;                		/* 当前的按键号 */
60  };
61  
62  struct imx6uirq_dev imx6uirq;   /* irq设备 */
63  
64  /* @description   	: 中断服务函数,开启定时器,延时10ms,
65   *                   	 定时器用于按键消抖。
66   * @param - irq     	: 中断号 
67   * @param - dev_id  	: 设备结构。
68   * @return          	: 中断执行结果
69   */
70  static irqreturn_t key0_handler(int irq, void *dev_id)
71  {
72      struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
73  
74      dev->curkeynum = 0;
75      dev->timer.data = (volatile long)dev_id;
76      mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); 
77      return IRQ_RETVAL(IRQ_HANDLED);
78  }
79  
80  /* @description 	: 定时器服务函数,用于按键消抖,定时器到了以后
81   *                	  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
82   * @param – arg	: 设备结构变量
83   * @return      	: 无
84   */
85  void timer_function(unsigned long arg)
86  {
87      unsigned char value;
88      unsigned char num;
89      struct irq_keydesc *keydesc;
90      struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
91  
92      num = dev->curkeynum;
93      keydesc = &dev->irqkeydesc[num];
94  
95      value = gpio_get_value(keydesc->gpio);  	/* 读取IO值 	*/
96      if(value == 0){                         		/* 按下按键 		*/
97          atomic_set(&dev->keyvalue, keydesc->value);
98      }
99      else{                                   			/* 按键松开 		*/
100         atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
101         atomic_set(&dev->releasekey, 1);    	/* 标记松开按键 */         
102     }   
103 }
104 
105 /*
106  * @description	: 按键IO初始化
107  * @param       	: 无
108  * @return      	: 无
109  */
110 static int keyio_init(void)
111 {
112     unsigned char i = 0;
113    
114     int ret = 0;
115     
116     imx6uirq.nd = of_find_node_by_path("/key");
117     if (imx6uirq.nd== NULL){
118         printk("key node not find!\r\n");
119         return -EINVAL;
120     } 
121 
122     /* 提取GPIO */
123     for (i = 0; i < KEY_NUM; i++) {
124         imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,
"key-gpio", i);
125         if (imx6uirq.irqkeydesc[i].gpio < 0) {
126             printk("can't get key%d\r\n", i);
127         }
128     }
129     
130     /* 初始化key所使用的IO,并且设置成中断模式 */
131     for (i = 0; i < KEY_NUM; i++) {
132         memset(imx6uirq.irqkeydesc[i].name, 0,          sizeof(imx6uirq.irqkeydesc[i].name));  
133         sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);     
134         gpio_request(imx6uirq.irqkeydesc[i].gpio, 
imx6uirq.irqkeydesc[i].name);
135         gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);  
136         imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(
imx6uirq.nd, i);
137 #if 0
138         imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(
imx6uirq.irqkeydesc[i].gpio);
139 #endif
140         printk("key%d:gpio=%d, irqnum=%d\r\n",i, 
imx6uirq.irqkeydesc[i].gpio, 
141                                            imx6uirq.irqkeydesc[i].irqnum);
142     }
143     /* 申请中断 */
144     imx6uirq.irqkeydesc[0].handler = key0_handler;
145     imx6uirq.irqkeydesc[0].value = KEY0VALUE;
146     
147     for (i = 0; i < KEY_NUM; i++) {
148         ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, 
imx6uirq.irqkeydesc[i].handler, 
                                  IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, 
imx6uirq.irqkeydesc[i].name, &imx6uirq);
149         if(ret < 0){
150             printk("irq %d request failed!\r\n", 
imx6uirq.irqkeydesc[i].irqnum);
151             return -EFAULT;
152         }
153     }
154 
155     /* 创建定时器 */
156     init_timer(&imx6uirq.timer);
157     imx6uirq.timer.function = timer_function;
158     return 0;
159 }
160 
161 /*
162  * @description  	: 打开设备
163  * @param – inode	: 传递给驱动的inode
164  * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
165  *                    一般在open的时候将private_data指向设备结构体。
166  * @return        	: 0 成功;其他 失败
167  */
168 static int imx6uirq_open(struct inode *inode, struct file *filp)
169 {
170     filp->private_data = &imx6uirq; /* 设置私有数据 */
171     return 0;
172 }
173 
174  /*
175   * @description 	: 从设备读取数据 
176   * @param – filp	: 要打开的设备文件(文件描述符)
177   * @param – buf	: 返回给用户空间的数据缓冲区
178   * @param - cnt 	: 要读取的数据长度
179   * @param – offt	: 相对于文件首地址的偏移
180   * @return       	: 读取的字节数,如果为负值,表示读取失败
181   */
182 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, 
size_t cnt, loff_t *offt)
183 {
184     int ret = 0;
185     unsigned char keyvalue = 0;
186     unsigned char releasekey = 0;
187     struct imx6uirq_dev *dev = (struct imx6uirq_dev *)
filp->private_data;
188 
189     keyvalue = atomic_read(&dev->keyvalue);
190     releasekey = atomic_read(&dev->releasekey);
191 
192     if (releasekey) { 						/* 有按键按下 	*/ 
193         if (keyvalue & 0x80) {
194             keyvalue &= ~0x80;
195             ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
196         } else {
197             goto data_error;
198         }
199         atomic_set(&dev->releasekey, 0);	/* 按下标志清零 	*/
200     } else {
201         goto data_error;
202     }
203     return 0;
204     
205 data_error:
206     return -EINVAL;
207 }
208 
209 /* 设备操作函数 */
210 static struct file_operations imx6uirq_fops = {
211     .owner = THIS_MODULE,
212     .open = imx6uirq_open,
213     .read = imx6uirq_read,
214 };
215 
216 /*
217  * @description	: 驱动入口函数
218  * @param       	: 无
219  * @return      	: 无
220  */
221 static int __init imx6uirq_init(void)
222 {
223     /* 1、构建设备号 */
224     if (imx6uirq.major) {
225         imx6uirq.devid = MKDEV(imx6uirq.major, 0);
226         register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, 
IMX6UIRQ_NAME);
227     } else {
228         alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, 
IMX6UIRQ_NAME);
229         imx6uirq.major = MAJOR(imx6uirq.devid);
230         imx6uirq.minor = MINOR(imx6uirq.devid);
231     }
232 
233     /* 2、注册字符设备 	*/
234     cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
235     cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
236 
237     /* 3、创建类 		*/
238     imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
239     if (IS_ERR(imx6uirq.class)) {
240         return PTR_ERR(imx6uirq.class);
241     }
242 
243     /* 4、创建设备 		*/
244     imx6uirq.device = device_create(imx6uirq.class, NULL, 
imx6uirq.devid, NULL, IMX6UIRQ_NAME);
245     if (IS_ERR(imx6uirq.device)) {
246         return PTR_ERR(imx6uirq.device);
247     }
248     
249     /* 5、初始化按键 		*/
250     atomic_set(&imx6uirq.keyvalue, INVAKEY);
251     atomic_set(&imx6uirq.releasekey, 0);
252     keyio_init();
253     return 0;
254 }
255 
256 /*
257  * @description	: 驱动出口函数
258  * @param       	: 无
259  * @return      	: 无
260  */
261 static void __exit imx6uirq_exit(void)
262 {
263     unsigned int i = 0;
264     /* 删除定时器 */
265     del_timer_sync(&imx6uirq.timer);   
266         
267     /* 释放中断 */
268     for (i = 0; i < KEY_NUM; i++) {
269         free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
270     }
271     cdev_del(&imx6uirq.cdev);
272     unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
273     device_destroy(imx6uirq.class, imx6uirq.devid);
274     class_destroy(imx6uirq.class);
275 }
276 
277 module_init(imx6uirq_init);
278 module_exit(imx6uirq_exit);
279 MODULE_LICENSE("GPL");
280 MODULE_AUTHOR("zuozhongkai");
第38~43行,结构体irq_keydesc为按键的中断描述结构体,gpio为按键GPIO编号,irqnum为按键IO对应的中断号,value为按键对应的键值,name为按键名字,handler为按键中断服务函数。使用irq_keydesc结构体即可描述一个按键中断。
第47~60行,结构体imx6uirq_dev为本例程设备结构体,第55行的keyvalue保存按键值,第56行的releasekey表示按键是否被释放,如果按键被释放表示发生了一次完整的按键过程。第57行的timer为按键消抖定时器,使用定时器进行按键消抖的原理已经在19.1小节讲解过了。第58行的数组irqkeydesc为按键信息数组,数组元素个数就是开发板上的按键个数,I.MX6U-ALIPHA开发板上只有一个按键,因此irqkeydesc数组只有一个元素。第59行的curkeynum表示当前按键。
第62行,定义设备结构体变量imx6uirq。
第70~78行,key0_handler函数,按键KEY0中断处理函数,参数dev_id为设备结构体,也就是imx6uirq。第74行设置curkeynum=0,表示当前按键为KEY0,第76行使用mod_timer函数启动定时器,定时器周期为10ms。
第85~103,timer_function函数,定时器定时处理函数,参数arg是设备结构体,也就是imx6uirq,在此函数中读取按键值。第95行通过gpio_get_value函数读取按键值。如果为0的话就表示按键被按下去了,按下去的话就设置imx6uirq结构体的keyvalue成员变量为按键的键值,比如KEY0按键的话按键值就是KEY0VALUE=0。如果按键值为1的话表示按键被释放了,按键释放了的话就将imx6uirq结构体的keyvalue成员变量的最高位置1,表示按键值有效,也就是将keyvalue与0x80进行或运算,表示按键松开了,并且设置imx6uirq结构体的releasekey成员变量为1,表示按键释放,一次有效的按键过程发生。
第110~159行,keyio_init函数,按键IO初始化函数,在驱动入口函数里面会调用keyio_init来初始化按键IO。第131~142行轮流初始化所有的按键,包括申请IO、设置IO为输入模式、从设备树中获取IO的中断号等等。第136行通过irq_of_parse_and_map函数从设备树中获取按键IO对应的中断号。也可以使用gpio_to_irq函数将某个IO设置为中断状态,并且返回其中断号。第144和145行设置KEY0按键对应的按键中断处理函数为key0_handler、KEY0的按键值为KEY0VALUE。第147~153行轮流调用request_irq函数申请中断号,设置中断触发模式为IRQF_TRIGGER_FALLING和IRQF_TRIGGER_RISING,也就是上升沿和下降沿都可以触发中断。最后,第156行初始化定时器,并且设置定时器的定时处理函数。
第168~172行,imx6uirq_open函数,对应应用程序的open函数。
第182~207行,imx6uirq_read函数,对应应用程序的read函数。此函数向应用程序返回按键值。首先判断imx6uirq结构体的releasekey成员变量值是否为1,如果为1的话表示有一次有效按键发生,否则的话就直接返回-EINVAL。当有按键事件发生的话就要向应用程序发送按键值,首先判断按键值的最高位是否为1,如果为1的话就表示按键值有效。如果按键值有效的话就将最高位清除,得到真实的按键值,然后通过copy_to_user函数返回给应用程序。向应用程序发送按键值完成以后就将imx6uirq结构体的releasekey成员变量清零,准备下一次按键操作。
第210~214行,按键中断驱动操作函数集imx6uirq_fops。
第221~253行,驱动入口函数,第250和251行分别初始化imx6uirq结构体中的原子变量keyvalue和releasekey,第252行调用keyio_init函数初始化按键所使用的IO。
第261~275行,驱动出口函数,第265行调用del_timer_sync函数删除定时器,第268~270行轮流释放申请的所有按键中断。

51.3.3 编写测试APP
测试APP要实现的内容很简单,通过不断的读取/dev/imx6uirq文件来获取按键值,当按键按下以后就会将获取到的按键值输出在终端上,新建名为imx6uirqApp.c的文件,然后输入如下所示内容:

示例代码51.3.3.1 imx6uirqApp.c文件代码
1  #include "stdio.h"
2  #include "unistd.h"
3  #include "sys/types.h"
4  #include "sys/stat.h"
5  #include "fcntl.h"
6  #include "stdlib.h"
7  #include "string.h"
8  #include "linux/ioctl.h"
9  /***************************************************************
10 Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
11 文件名  	: imx6uirqApp.c
12 作者   	: 左忠凯
13 版本   	: V1.0
14 描述   	: 定时器测试应用程序
15 其他   	: 无
16 使用方法 	:./imx6uirqApp /dev/imx6uirq 打开测试App
17 论坛   	: www.openedv.com
18 日志    	 初版V1.0 2019/7/26 左忠凯创建
19 ***************************************************************/
20 
21 /*
22  * @description	: main主程序
23  * @param - argc  	: argv数组元素个数
24  * @param - argv  	: 具体参数
25  * @return         	: 0 成功;其他 失败
26  */
27 int main(int argc, char *argv[])
28 {
29  	int fd;
30  	int ret = 0;
31  	char *filename;
32 		unsigned char data;
33  	if (argc != 2) {
34      	printf("Error Usage!\r\n");
35      	return -1;
36  	}
37 
38  	filename = argv[1];
39  	fd = open(filename, O_RDWR);
40  	if (fd < 0) {
41      	printf("Can't open file %s\r\n", filename);
42      	return -1;
43  	}
44 
45  	while (1) {
46      	ret = read(fd, &data, sizeof(data));
47      	if (ret < 0) {  	/* 数据读取错误或者无效 	*/
48          
49      	} else {        	/* 数据读取正确 			*/
50          		if (data)   	/* 读取到数据 			*/
51              		printf("key value = %#X\r\n", data);
52      	}
53  }
54  close(fd);
55  return ret;
56 }
第45~53行的while循环用于不断的读取按键值,如果读取到有效的按键值就将其输出到终端上。

51.4 运行测试
51.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为imx6uirq.o,Makefile内容如下所示:

示例代码51.4.1.1 Makefile文件
1  KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
...... 
4  obj-m := imx6uirq.o
......
11 clean:
12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,设置obj-m变量的值为imx6uirq.o。
输入如下命令编译出驱动模块文件:

make -j32
编译成功以后就会生成一个名为“imx6uirq.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试imx6uirqApp.c这个测试程序:
arm-linux-gnueabihf-gcc imx6uirqApp.c -o imx6uirqApp
编译成功以后就会生成imx6uirqApp这个应用程序。
51.4.2 运行测试
将上一小节编译出来imx6uirq.ko和imx6uirqApp这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中,输入如下命令加载imx6uirq.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe imx6uirq.ko //加载驱动
驱动加载成功以后可以通过查看/proc/interrupts文件来检查一下对应的中断有没有被注册上,输入如下命令:
cat /proc/interrupts
结果如图51.4.2.1所示:
在这里插入图片描述

图51.4.2.1 proc/interrupts文件内容
从图51.4.2.1可以看出imx6uirq.c驱动文件里面的KEY0中断已经存在了,触发方式为跳边沿(Edge),中断号为49。
接下来使用如下命令来测试中断:
./imx6uirqApp /dev/imx6uirq
按下开发板上的KEY0键,终端就会输出按键值,如图51.4.2.2所示:
在这里插入图片描述

图51.4.2.2 读取到的按键值
从图51.4.2.2可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:
rmmod imx6uirq.ko

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-09-08 10:53:52  更:2021-09-08 10:56:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 9:05:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计