I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

[一、Linux 中断简介](#一、Linux 中断简介)

[1.1 核心中断 API 函数](#1.1 核心中断 API 函数)

[1.1.1 中断号](#1.1.1 中断号)

[1.1.2 中断申请与释放](#1.1.2 中断申请与释放)

[1.1.3 中断处理函数](#1.1.3 中断处理函数)

[1.1.4 中断使能 / 禁止(入门必备工具)](#1.1.4 中断使能 / 禁止(入门必备工具))

[1.2 中断上半部与下半部(核心优化思路,零基础也能懂)](#1.2 中断上半部与下半部(核心优化思路,零基础也能懂))

1.2.1、上半部

1.2.2、下半部

软中断(静态注册的轻量级下半部)

tasklet(推荐优先使用的下半部)

工作队列(支持睡眠的下半部)

[二、设备树中断配置(KEY0 为例,零基础手把手教学)](#二、设备树中断配置(KEY0 为例,零基础手把手教学))

[2.1 中断控制器节点(GIC 控制器示例)](#2.1 中断控制器节点(GIC 控制器示例))

[2.2 GPIO 作为中断控制器(GPIO5 节点示例)](#2.2 GPIO 作为中断控制器(GPIO5 节点示例))

[2.3 设备中断节点(以 fxls8471 磁力计为例)](#2.3 设备中断节点(以 fxls8471 磁力计为例))

2.4、设备树中断属性总结

三、获取中断号(驱动开发必备)

[3.1 从设备树解析中断号](#3.1 从设备树解析中断号)

[3.2 从 GPIO 获取中断号](#3.2 从 GPIO 获取中断号)

[四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)](#四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战))

4.1、硬件原理图

4.2、程序编写

4.2.1、修改设备树文件

4.2.2、驱动代码编写(imx6uirq.c)

4.2.3、驱动代码分段分析

[结构体定义(33-58 行)](#结构体定义(33-58 行))

[中断处理函数(66-74 行)](#中断处理函数(66-74 行))

[定时器消抖函数(81-99 行)](#定时器消抖函数(81-99 行))

[按键 IO 初始化函数(106-155 行)](#按键 IO 初始化函数(106-155 行))

[字符设备操作函数(164-210 行)](#字符设备操作函数(164-210 行))

[驱动入口 / 出口函数(217-272 行)](#驱动入口 / 出口函数(217-272 行))

[4.2.3、测试 APP 代码](#4.2.3、测试 APP 代码)

五、运行测试

[5.1、编译驱动程序和测试 APP](#5.1、编译驱动程序和测试 APP)

5.2、运行测试

总结


前言

裸机开发中使用中断需要手动配置寄存器、映射中断向量表,步骤繁琐;而 Linux 内核提供了完善的中断框架,开发者只需申请中断 + 注册中断处理函数,无需关注底层寄存器操作,极大简化了开发流程。


一、Linux****中断简介

先来回顾一下裸机实验里面中断的处理方法:

①、使能中断,初始化相应的寄存器。

②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数。

②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

在 Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的API 函数。

1.1 核心中断 API 函数

1.1.1 中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。

1.1.2 中断申请与释放

在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断

函数 作用 关键参数说明
request_irq(irq, handler, flags, name, dev) 申请并使能中断 irq:中断号 handler:中断处理函数。当中断发生以后就会执行此中断处理函数。 flags:中断标志(触发方式) name:中断名(/proc/interrupts 可查) dev:一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
free_irq(irq, dev) 释放中断 共享中断需通过dev区分不同设备

常用中断标准如下所示:

1.1.3 中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数

复制代码
irqreturn_t (*irq_handler_t) (int irq, void *dev_id)
// 返回值:IRQ_RETVAL(IRQ_HANDLED) 表示中断已处理

第一个参数是要中断处理函数要相应的中断号。

第二个参数是一个指向 void 的指针,也就是个通用指针。

需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
中断处理函数的返回值为 irqreturn_t 类型。

复制代码
enum irqreturn {
    IRQ_NONE        = (0 << 0),  // 中断不是本设备触发
    IRQ_HANDLED     = (1 << 0),  // 中断已被本设备处理
    IRQ_WAKE_THREAD = (1 << 1),  // 唤醒中断线程(线程化中断场景)
};
typedef enum irqreturn irqreturn_t;

1.1.4 中断使能 / 禁止(入门必备工具)

Linux 内核提供了两类中断控制函数:

单中断控制:enable_irq() / disable_irq() / disable_irq_nosync(),使能/禁止指定的单个中断;

全局中断控制:local_irq_enable() / local_irq_disable(),用于开启 / 关闭当前处理器的整个中断系统。

注意:⚠️ 全局中断的坑(零基础一定要看!)

直接使用local_irq_disable+local_irq_enable会引发严重问题:

假设 A 任务调用local_irq_disable关闭全局中断 10 秒,执行到第 2 秒时 B 任务开始运行,B 也调用local_irq_disable关闭 3 秒。3 秒后 B 调用local_irq_enable直接打开全局中断,此时仅过去 2+3=5 秒,A 任务要关闭 10 秒的愿望就破灭了,可能导致系统崩溃!

为了避免这种 "互相干扰" 的问题,必须使用状态保存 + 恢复的成对函数:

复制代码
local_irq_save(flags)   // 禁止中断,并将当前中断状态保存到flags中
local_irq_restore(flags) // 恢复中断到flags保存的状态

local_irq_save:不仅关闭中断,还会把当前 CPU 的中断状态(是否已关闭)存到flags变量里;

local_irq_restore:不是简单打开中断,而是把中断状态恢复到flags记录的样子 ------ 如果之前是关闭的,就保持关闭;如果之前是打开的,才打开。


✅ 最佳实践:永远用 local_irq_save / local_irq_restore 替代 local_irq_disable / local_irq_enable ,保证多任务环境下中断状态的安全,零基础也能写出稳定代码!
总结:

函数 作用 适用场景
enable_irq/ disable_irq 使能 / 禁止指定中断 irq 就是要禁止的中断号 单个中断控制
local_irq_enable/ local_irq_disable 开启 / 关闭全局中断 临界区保护
local_irq_save/ local_irq_restore 保存 / 恢复中断状态 避免全局中断被误修改

1.2 中断上半部与下半部(核心优化思路,零基础也能懂)

中断处理的核心要求是快进快出,因此 Linux 将中断分为两部分:

上半部:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);

下半部:处理耗时操作(如数据解析、硬件交互),内核提供 3 种实现方式:

软中断:静态注册,适用于高频场景(如网络、定时器);

tasklet:基于软中断实现,推荐优先使用(接口简单);

工作队列:运行在进程上下文,支持睡眠(如 IIC/SPI 数据读取)。

1.2.1、上半部

主要为:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);

1.2.2、下半部

软中断(静态注册的轻量级下半部)

早期 Linux 内核使用 "bottom half(BH)" 实现下半部,后被软中断和 tasklet 替代(2.5 版本后 BH 已被废弃)。

Linux 内核用softirq_action结构体表示软中断:

复制代码
struct softirq_action {
    void (*action)(struct softirq_action *); // 软中断服务函数
};

内核中定义了 10 个全局软中断(softirq_vec[NR_SOFTIRQS]),NR_SOFTIRQS是枚举类型:

复制代码
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
};

使用软中断的核心步骤:

注册软中断:open_softirq(nr, action),指定软中断类型和处理函数;

触发软中断:raise_softirq(nr),在中断上半部中调用;

内核初始化:softirq_init()会默认打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

注意:软中断必须编译时静态注册 ,适合高频、轻量的耗时操作。

tasklet(推荐优先使用的下半部)

tasklet 是基于软中断实现的下半部机制,接口更简单,零基础推荐优先使用

Linux 内核用tasklet_struct结构体表示 tasklet:

cpp 复制代码
struct tasklet_struct {
    struct tasklet_struct *next;  // 下一个tasklet
    unsigned long state;          // tasklet状态
    atomic_t count;               // 引用计数
    void (*func)(unsigned long);  // tasklet执行函数
    unsigned long data;           // 传递给func的参数
};

使用步骤:

定义 + 初始化 tasklet:

方式 1:先定义struct tasklet_struct,再用tasklet_init()初始化;

方式 2:用宏DECLARE_TASKLET(name, func, data)一次性完成定义和初始化;

调度 tasklet:在中断上半部调用tasklet_schedule(&tasklet),让内核在合适时间执行;

示例代码

cpp 复制代码
// 定义tasklet
struct tasklet_struct testtasklet;
// tasklet处理函数
void testtasklet_func(unsigned long data) {
    /* 耗时操作放在这里,比如数据解析、IIC读取 */
}
// 中断处理函数(上半部)
irqreturn_t test_handler(int irq, void *dev_id) {
    tasklet_schedule(&testtasklet); // 触发下半部
    return IRQ_RETVAL(IRQ_HANDLED);
}
// 驱动入口初始化
static int __init xxxx_init(void) {
    tasklet_init(&testtasklet, testtasklet_func, data);
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    return 0;
}
工作队列(支持睡眠的下半部)

工作队列是唯一运行在进程上下文的下半部机制,允许睡眠 / 重调度,适合需要阻塞的耗时操作(如 IIC/SPI 慢速设备读取)。

Linux 内核用work_struct表示一个工作:

cpp 复制代码
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func; // 工作队列处理函数
};

使用步骤:

定义 + 初始化工作:

方式 1:先定义struct work_struct,再用INIT_WORK(work, func)初始化;

方式 2:用宏DECLARE_WORK(n, f)一次性完成创建和初始化;

调度工作:调用schedule_work(&work),让内核线程处理;

示例代码

cpp 复制代码
// 定义工作
struct work_struct testwork;
// work处理函数
void testwork_func_t(struct work_struct *work) {
    /* 可睡眠的耗时操作,比如SPI读取Flash数据 */
}
// 中断处理函数(上半部)
irqreturn_t test_handler(int irq, void *dev_id) {
    /* 调度 work */
    schedule_work(&testwork); // 触发下半部
    return IRQ_RETVAL(IRQ_HANDLED);
}
// 驱动入口初始化
static int __init xxxx_init(void) {
    /* 初始化 work */
    INIT_WORK(&testwork, testwork_func_t);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);  
    return 0;
}

二、设备树中断配置(KEY0 为例,零基础手把手教学)

使用设备树时,需要在设备树中配置中断相关属性,Linux 内核会自动读取并配置中断。其中的 intc 节点就是 I.MX6ULL 的中断控制器节点。

2.1 中断控制器节点(GIC 控制器示例)

I.MX6ULL 的 GIC 中断控制器节点(imx6ull.dtsi中的intc节点)定义了中断解析规则:

cpp 复制代码
intc: interrupt-controller@00a01000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;  // 每个中断源用3个cells描述
    interrupt-controller;    // 表示当前节点是中断控制器
    reg = <0x00a01000 0x1000>,
          <0x00a02000 0x100>;
};

compatible = "arm,cortex-a7-gic":匹配 GIC 中断控制器驱动;

#interrupt-cells = <3>:表示此控制器下的中断源需要 3 个 cell 来描述:

第 1 个 cell:中断类型(0=SPI,1=PPI);

第 2 个 cell:中断号(SPI:0~987;PPI:0~15);

第 3 个 cell:触发类型 + PPI CPU 掩码(bit 3:0:1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平);

interrupt-controller:空属性,标记当前节点为中断控制器。

2.2 GPIO 作为中断控制器(GPIO5 节点示例)

GPIO 节点也可作为中断控制器,以imx6ull.dtsi中的gpio5节点为例:

cpp 复制代码
gpio5: gpio@020ac000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x020ac000 0x4000>;
    interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
                 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

interrupts:包含两条中断信息,均为 SPI 类型、高电平触发,中断号分别为 74、75;

74 对应 GPIO5_IO00~GPIO5_IO15;

75 对应 GPIO5_IO16~GPIO5_IO31;

interrupt-controller:标记gpio5为中断控制器,管理其所有 IO 的中断;

#interrupt-cells = <2>:表示此控制器下的中断源用 2 个 cell 描述(GPIO 编号 + 触发类型)。

2.3 设备中断节点(以 fxls8471 磁力计为例)

以 NXP 官方开发板的磁力计芯片fxls8471为例,其设备节点配置:

cpp 复制代码
fxls8471@1e {
    compatible = "fsl,fxls8471";
    reg = <0x1e>;
    position = <0>;
    interrupt-parent = <&gpio5>;  // 指定父中断控制器为gpio5
    interrupts = <0 8>;           // 0=GPIO5_IO00,8=低电平触发
};

关键属性解析:

interrupt-parent:指定中断控制器(这里是gpio5);

interrupts:中断信息,0对应 GPIO5_IO00,8对应低电平触发(IRQ_TYPE_LEVEL_LOW)。

2.4、设备树中断属性总结

属性名 含义
#interrupt-cells 指定中断源的信息 cell 个数
interrupt-controller 标记当前节点为中断控制器
interrupts 指定中断号、触发方式等信息
interrupt-parent 指定父中断(即中断控制器)

三、获取中断号(驱动开发必备)

中断信息已写入设备树,驱动中可通过以下函数获取中断号:

3.1 从设备树解析中断号

irq_of_parse_and_map():从设备节点的interrupts属性中提取中断号:

cpp 复制代码
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

dev:设备节点指针;

index:interrupts属性可能包含多条中断信息,用index指定要获取的条目;

返回值:对应的中断号。

3.2 从 GPIO 获取中断号

gpio_to_irq():将 GPIO 编号转换为对应的中断号(GPIO 作为中断时使用):

cpp 复制代码
int gpio_to_irq(unsigned int gpio)

gpio:要获取的 GPIO 编号;

返回值:GPIO 对应的中断号。

四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)

4.1、硬件原理图

按键 KEY0 的原理图如下:

图中可以看出,按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的,KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。

4.2、程序编写

本章实验我们驱动 I.MX6U-ALPHA 开发板上的 KEY0 按键,不过我们采用中断的方式, 并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。

4.2.1、修改设备树文件

实验使用到了按键 KEY0 ,按键 KEY0 使用中断模式,因此需要在" key "节点下添加 中断相关属性,添加完成以后的"key "节点内容如下所示:

cpp 复制代码
key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
    interrupt-parent = <&gpio1>;           /* 中断控制器为gpio1 */
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>;  /* 上升沿+下降沿触发 */
    status = "okay";
};

key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>:指定 KEY0 为 GPIO1_IO18,低电平有效;

interrupt-parent = <&gpio1>:KEY0 的中断控制器为gpio1;

interrupts = <18 IRQ_TYPE_EDGE_BOTH>:

18:GPIO1 组的 18 号 IO(即 GPIO1_IO18);

IRQ_TYPE_EDGE_BOTH:上升沿和下降沿同时触发(KEY0 按下和释放都会触发中断)。

注意:设置好key这个节点,要把系统自带的按键节点 gpio-keys禁用,加一行如下代码:

cpp 复制代码
 status = "disabled";  // 禁用系统自带按键,解决冲突!

最终系统自带的按键节点如下所示:

cpp 复制代码
/* zuozhongkai dts keys linux自带的key驱动设备树 */
gpio-keys {
    status = "disabled";  // 禁用系统自带按键,解决冲突!
    compatible = "gpio-keys";
    #address-cells = <1>;
    #size-cells = <0>;
    autorepeat;
    key0 {
        label = "GPIO Key Enter";
        linux,code = <KEY_ENTER>;
        gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
    };
};

修改完之后把设备树文件重新编译一下,使用如下代码重新编译:

cpp 复制代码
make dtbs

编译完之后,把生成的设备树.dtb文件更新到tftp目录里,再重启开发板。

4.2.2、驱动代码编写(imx6uirq.c)

cpp 复制代码
#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.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: imx6uirq.c
描述	   	: Linux中断驱动实验
其他	   	: 无
***************************************************************/
#define IMX6UIRQ_CNT		1			/* 设备号个数 	*/
#define IMX6UIRQ_NAME		"imx6uirq"	/* 名字 		*/
#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 *);	/* 中断服务函数 */
};

/* imx6uirq设备结构体 */
struct imx6uirq_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	atomic_t keyvalue;		/* 有效的按键键值 */
	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
	struct timer_list timer;/* 定义一个定时器*/
	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键描述数组 */
	unsigned char curkeynum;				/* 当前的按键号 */
};

struct imx6uirq_dev imx6uirq;	/* irq设备 */

/* @description		: 中断服务函数,开启定时器,延时10ms,
 *				  	  定时器用于按键消抖。
 * @param - irq 	: 中断号 
 * @param - dev_id	: 设备结构。
 * @return 			: 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
	}	
}

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	unsigned char i = 0;
	int ret = 0;
	
	imx6uirq.nd = of_find_node_by_path("/key");
	if (imx6uirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} 

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
		if (imx6uirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name));	/* 缓冲区清零 */
		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
#if 0
		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, 
                                         imx6uirq.irqkeydesc[i].irqnum);
	}
	/* 申请中断 */
	imx6uirq.irqkeydesc[0].handler = key0_handler;
	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&imx6uirq.timer);
	imx6uirq.timer.function = timer_function;
	return 0;
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &imx6uirq;	/* 设置私有数据 */
	return 0;
}

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char releasekey = 0;
	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		} else {
			goto data_error;
		}
		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
	} else {
		goto data_error;
	}
	return 0;
	
data_error:
	return -EINVAL;
}

/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
	.owner = THIS_MODULE,
	.open = imx6uirq_open,
	.read = imx6uirq_read,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init imx6uirq_init(void)
{
	/* 1、构建设备号 */
	if (imx6uirq.major) {
		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
	} else {
		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
		imx6uirq.major = MAJOR(imx6uirq.devid);
		imx6uirq.minor = MINOR(imx6uirq.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

	/* 3、创建类 */
	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.class)) {
		return PTR_ERR(imx6uirq.class);
	}

	/* 4、创建设备 */
	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
	if (IS_ERR(imx6uirq.device)) {
		return PTR_ERR(imx6uirq.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&imx6uirq.keyvalue, INVAKEY);
	atomic_set(&imx6uirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit imx6uirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&imx6uirq.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
		gpio_free(imx6uirq.irqkeydesc[i].gpio);
	}
	cdev_del(&imx6uirq.cdev);
	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
	device_destroy(imx6uirq.class, imx6uirq.devid);
	class_destroy(imx6uirq.class);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("duan");

4.2.3、驱动代码分段分析

结构体定义(33-58 行)
cpp 复制代码
33 /* 中断IO描述结构体 */
34 struct irq_keydesc {
35 	int gpio;								/* gpio */
36 	int irqnum;								/* 中断号     */
37 	unsigned char value;					/* 按键对应的键值 */
38 	char name[10];							/* 名字 */
39 	irqreturn_t (*handler)(int, void *);	/* 中断服务函数 */
40 };

42 /* imx6uirq设备结构体 */
43 struct imx6uirq_dev{
44 	dev_t devid;			/* 设备号 	 */
45 	struct cdev cdev;		/* cdev 	*/
46 	struct class *class;	/* 类 		*/
47 	struct device *device;	/* 设备 	 */
48 	int major;				/* 主设备号	  */
49 	int minor;				/* 次设备号   */
50 	struct device_node	*nd; /* 设备节点 */
51 	atomic_t keyvalue;		/* 有效的按键键值 */
52 	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
53 	struct timer_list timer;/* 定义一个定时器*/
54 	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键描述数组 */
55 	unsigned char curkeynum;				/* 当前的按键号 */
56 };

58 struct imx6uirq_dev imx6uirq;	/* irq设备 */

irq_keydesc 结构体(34 行):

封装单个按键的所有关键信息,包括 GPIO 号、中断号、键值、中断处理函数;

便于扩展多按键(只需增加数组元素,无需修改核心逻辑)。

imx6uirq_dev 结构体(43 行):

字符设备驱动标准成员(44-49 行):设备号、cdev、类 / 设备节点;

设备树成员(50 行):指向设备树中 key 节点的指针;

原子变量(51-52 行):keyvalue存储按键值,releasekey标记完整按键过程(按下 + 释放),原子变量避免多线程竞态;

定时器成员(53 行):用于按键消抖的定时器;

按键描述数组(54 行):支持多按键扩展;

全局设备实例(58 行):整个驱动的核心数据载体。

中断处理函数(66-74 行)
cpp 复制代码
66 static irqreturn_t key0_handler(int irq, void *dev_id)
67 {
68 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
69 
70 	dev->curkeynum = 0;
71 	dev->timer.data = (volatile long)dev_id;
72 	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
73 	return IRQ_RETVAL(IRQ_HANDLED);
74 }

函数作用:KEY0 中断触发后执行的上半部处理函数,遵循 "快进快出" 原则;

关键逻辑:

68 行:将dev_id转换为设备结构体指针,获取全局设备实例;

70 行:标记当前触发中断的是第 0 个按键(KEY0);

71 行:设置定时器私有数据为设备结构体指针;

72 行:启动 10ms 定时器(msecs_to_jiffies将毫秒转换为内核节拍数),用于按键消抖;

73 行:标准中断返回值,告知内核中断已处理完成。

定时器消抖函数(81-99 行)
cpp 复制代码
81 void timer_function(unsigned long arg)
82 {
83 	unsigned char value;
84 	unsigned char num;
85 	struct irq_keydesc *keydesc;
86 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
87 
88 	num = dev->curkeynum;
89 	keydesc = &dev->irqkeydesc[num];
90 
91 	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
92 	if(value == 0){ 						/* 按下按键 */
93 		atomic_set(&dev->keyvalue, keydesc->value);
94 	}
95 	else{ 									/* 按键松开 */
96 		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
97 		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
98 	}	
99 }

函数作用:10ms 定时到期后执行,读取 GPIO 电平确认按键真实状态(消抖核心);

关键逻辑:

86 行:将定时器参数转换为设备结构体指针;

91 行:读取 GPIO 电平(0 = 按下,1 = 松开);

92-94 行:按键按下时,设置keyvalue为 KEY0VALUE(0x01);

95-98 行:按键松开时,keyvalue最高位置 1(0x81),并设置releasekey为 1,标记一次完整按键过程;

原子操作:atomic_set保证多线程下按键状态的原子性,避免竞态。

按键 IO 初始化函数(106-155 行)
cpp 复制代码
106 static int keyio_init(void)
107 {
108 	unsigned char i = 0;
109 	int ret = 0;
110 	
111 	imx6uirq.nd = of_find_node_by_path("/key");
112 	if (imx6uirq.nd== NULL){
113 		printk("key node not find!\r\n");
114 		return -EINVAL;
115 	} 
116 
117 	/* 提取GPIO */
118 	for (i = 0; i < KEY_NUM; i++) {
119 		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
120 		if (imx6uirq.irqkeydesc[i].gpio < 0) {
121 			printk("can't get key%d\r\n", i);
122 		}
123 	}
124 	
125 	/* 初始化key所使用的IO,并且设置成中断模式 */
126 	for (i = 0; i < KEY_NUM; i++) {
127 		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name));	/* 缓冲区清零 */
128 		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
129 		gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
130 		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
131 		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
132 #if 0
133 		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
134 #endif
135 		printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, 
136                                          imx6uirq.irqkeydesc[i].irqnum);
137 	}
138 	/* 申请中断 */
139 	imx6uirq.irqkeydesc[0].handler = key0_handler;
140 	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
141 	
142 	for (i = 0; i < KEY_NUM; i++) {
143 		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
144 		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
145 		if(ret < 0){
146 			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
147 			return -EFAULT;
148 		}
149 	}
150 
151 	/* 创建定时器 */
152 	init_timer(&imx6uirq.timer);
153 	imx6uirq.timer.function = timer_function;
154 	return 0;
155 }

核心解析

设备树节点查找(111 行):

of_find_node_by_path("/key"):查找设备树中路径为/key的节点;

失败则返回-EINVAL,驱动初始化失败。

GPIO 提取(118-123 行):

of_get_named_gpio:从key节点的key-gpio属性中提取 GPIO 编号;

支持多按键扩展(只需修改KEY_NUM和设备树)。

GPIO 初始化(126-137 行):

129 行:申请 GPIO 资源,避免资源冲突;

130 行:设置 GPIO 为输入模式(按键检测);

131 行:从设备树解析中断号(推荐方式);

133 行:备选方案(GPIO 转中断号),适合无设备树场景。

中断申请(142-149 行):

request_irq参数说明:

参数 1:中断号;

参数 2:中断处理函数(key0_handler);

参数 3:触发方式(上升沿 + 下降沿);

参数 4:中断名(/proc/interrupts 可查);

参数 5:传递给中断处理函数的私有数据(设备结构体);

触发方式与设备树中IRQ_TYPE_EDGE_BOTH对应。

定时器初始化(152-153 行):

init_timer:初始化定时器;

设置定时器处理函数为timer_function(消抖核心)。

字符设备操作函数(164-210 行)
cpp 复制代码
164 static int imx6uirq_open(struct inode *inode, struct file *filp)
165 {
166 	filp->private_data = &imx6uirq;	/* 设置私有数据 */
167 	return 0;
168 }

178 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
179 {
180 	int ret = 0;
181 	unsigned char keyvalue = 0;
182 	unsigned char releasekey = 0;
183 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
184 
185 	keyvalue = atomic_read(&dev->keyvalue);
186 	releasekey = atomic_read(&dev->releasekey);
187 
188 	if (releasekey) { /* 有按键按下 */	
189 		if (keyvalue & 0x80) {
190 			keyvalue &= ~0x80;
191 			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
192 		} else {
193 			goto data_error;
194 		}
195 		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
196 	} else {
197 		goto data_error;
198 	}
199 	return 0;
200 	
201 data_error:
202 	return -EINVAL;
203 }

205 /* 设备操作函数 */
206 static struct file_operations imx6uirq_fops = {
207 	.owner = THIS_MODULE,
208 	.open = imx6uirq_open,
209 	.read = imx6uirq_read,
210 };

open 函数(164 行):

将设备结构体指针赋值给filp->private_data,便于后续 read 函数获取设备实例;

字符设备驱动标准操作,无额外逻辑。

read 函数(178 行):

185-186 行:读取原子变量,获取按键值和按键完成标记;

188 行:仅当releasekey=1(完成一次按键过程)时,才向用户空间返回数据;

189-191 行:清除keyvalue最高位(松开标记),通过copy_to_user将按键值拷贝到用户空间;

195 行:清零releasekey,准备下一次按键检测;

核心注意:copy_to_user是内核空间向用户空间拷贝数据的标准函数,不能直接赋值(内存隔离)。

驱动入口 / 出口函数(217-272 行)
cpp 复制代码
217 static int __init imx6uirq_init(void)
218 {
219 	/* 1、构建设备号 */
220 	if (imx6uirq.major) {
221 		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
222 		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
223 	} else {
224 		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
225 		imx6uirq.major = MAJOR(imx6uirq.devid);
226 		imx6uirq.minor = MINOR(imx6uirq.devid);
227 	}
228 
229 	/* 2、注册字符设备 */
230 	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
231 	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
232 
233 	/* 3、创建类 */
234 	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
235 	if (IS_ERR(imx6uirq.class)) {
236 		return PTR_ERR(imx6uirq.class);
237 	}
238 
239 	/* 4、创建设备 */
240 	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
241 	if (IS_ERR(imx6uirq.device)) {
242 		return PTR_ERR(imx6uirq.device);
243 	}
244 	
245 	/* 5、初始化按键 */
246 	atomic_set(&imx6uirq.keyvalue, INVAKEY);
247 	atomic_set(&imx6uirq.releasekey, 0);
248 	keyio_init();
249 	return 0;
250 }

257 static void __exit imx6uirq_exit(void)
258 {
259 	unsigned int i = 0;
260 	/* 删除定时器 */
261 	del_timer_sync(&imx6uirq.timer);	/* 删除定时器 */
262 		
263 	/* 释放中断 */
264 	for (i = 0; i < KEY_NUM; i++) {
265 		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
266 		gpio_free(imx6uirq.irqkeydesc[i].gpio);
267 	}
268 	cdev_del(&imx6uirq.cdev);
269 	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
270 	device_destroy(imx6uirq.class, imx6uirq.devid);
271 	class_destroy(imx6uirq.class);
272 }

274 module_init(imx6uirq_init);
275 module_exit(imx6uirq_exit);
276 MODULE_LICENSE("GPL");
277 MODULE_AUTHOR("duan");

入口函数(imx6uirq_init)

设备号处理(220-227 行):

静态指定主设备号:若major不为 0,使用register_chrdev_region注册;

动态分配设备号:若major为 0,使用alloc_chrdev_region分配,自动获取主 / 次设备号;

动态分配是推荐方式,避免设备号冲突。

字符设备注册(230-231 行):

cdev_init:初始化 cdev 结构体,绑定设备操作集;

cdev_add:将 cdev 添加到内核,完成字符设备注册。

类 / 设备创建(234-243 行):

class_create:创建类,用于自动生成设备节点;

device_create:创建设备节点(/dev/imx6uirq);

IS_ERR:检查创建是否失败,避免空指针操作。

按键初始化(246-248 行):

初始化原子变量为默认值;

调用keyio_init完成 GPIO / 中断 / 定时器初始化。

出口函数(imx6uirq_exit)

资源释放顺序:

先释放定时器(261 行):del_timer_sync等待定时器完成,避免竞态;

再释放中断 / GPIO(264-267 行):free_irq释放中断,gpio_free释放 GPIO 资源;

最后释放字符设备资源(268-271 行):注销 cdev、设备号、销毁设备 / 类;

核心原则:与初始化顺序相反,避免资源泄漏。

4.2.3、测试 APP 代码

cpp 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"

int main(int argc, char *argv[])
{
    int fd;
    int ret = 0;
    char *filename;
    unsigned char data;

    if (argc != 2) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    while (1) {
        ret = read(fd, &data, sizeof(data));
        if (ret >= 0 && data) { /* 读取到有效按键值 */
            printf("key value = %#X\r\n", data);
        }
    }

    close(fd);
    return ret;
}

第 45~53 行的 while 循环用于不断的读取按键值,如果读取到有效的按键值就将其输出到终端上。

五、运行测试

5.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为imx6uirq.o,Makefile 内容如下所示:

cpp 复制代码
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
CURRENT_PATH := $(shell pwd)

obj-m :=imx6uirq.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
	

第 4 行,设置 obj-m 变量的值为imx6uirq.o。

输入如下命令编译出驱动模块文件:

cpp 复制代码
make -j32

编译成功以后就会生成一个名为"imx6uirq.ko"的驱动模块文件。

编译测试 APP

输入如下命令编译测试试imx6uirqApp.c这个测试程序:

cpp 复制代码
arm-linux-gnueabihf-gcc imx6uirqApp.c -o  imx6uirqApp

编译成功以后就会生成 keyApp 这个应用程序。

5.2、运行测试

将上一小节编译出来的imx6uirq.ko 和 imx6uirqApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

cpp 复制代码
sudo cp imx6uirq.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
cpp 复制代码
sudo cp  imx6uirqApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载imx6uirq.ko 驱动模块:

cpp 复制代码
depmod //第一次加载驱动的时候需要运行此命令
modprobe imx6uirq.ko //加载驱动

驱动加载成功可通过查看/proc/interrupts 来检查一下对应的中断有没有被注册上,输入如下命令:

cpp 复制代码
cat /proc/interrupts

如下图所示:

接下来使用如下命令来测试中断:

cpp 复制代码
./imx6uirqApp /dev/imx6uirq

可以看出 imx6uirq.c 驱动文件里面的 KEY0 中断已经存在了。

按下开发板上的 KEY0 键,终端就会输出按键值,如下图所示:


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

cpp 复制代码
rmmod imx6uirq.ko

总结

完成了I.MX6U Linux 驱动中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)。

相关推荐
seven_stars_2 小时前
CVE-2018-19518PHP漏洞
linux·浏览器·kali
浅止菌3 小时前
嵌入式设备ping不通?tcpdump 5分钟找到凶手,比加printf快10倍
linux
巴啦啦小魔仙变身3 小时前
SIGINT = Signal Interrupte
linux·服务器·python
RisunJan3 小时前
Linux命令-route(查看/修改 IP 路由表)
linux·运维
凉、介4 小时前
Linux 设备驱动匹配机制
linux·笔记·单片机·学习·操作系统·嵌入式
卵男(章鱼)4 小时前
第4章:软件架构与SOA
linux·车载系统·汽车
是摆烂第一名呀4 小时前
【RK3506】RGB屏幕与背光调试记录
linux·arm开发·驱动开发·嵌入式硬件
qetfw4 小时前
CentOS 7 搭建 DNS 主从服务
linux·运维·centos·dns·bind