17 Linux 中断

一、Linux 中断简介

1. Linux 中断 API 函数

① 中断号

  每个中断都有一个中断号,通过中断号可以区分出不同的中断。在 Linux 内核中使用一个 int 变量表示中断号。

② request_irq 函数

  在 Linux 中想要使用某个中断是需要申请的,request_irq 函数就是用来申请中断的,并且 request_irq 函数会激活(使能)中断,但 request_irq 函数会导致睡眠,所以不能在中断上下文或者其他禁止睡眠的代码段中使用  request_irq 函数。

/*
 * @description : 申请内核中断,并使能中断函数
 * @param - irq : 要申请中断的中断号
 * @param - handler : 中断处理函数,当中断发生以后就会执行此中断处理函数
 * @param - flags : 中断标志
 * @param - name : 中断名字
 * @param - dev : 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断。
一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数
 * @return : 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了
 */
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev)
中断标志描述
IRQF_SHARED多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq 函数的 dev 参数就是唯一区分他们的标志。
IRQF_ONESHOT单次中断,中断执行一次就结束。
IRQF_TRIGGER_NONE无触发。
IRQF_TRIGGER_RISING上升沿触发。
IRQF_TRIGGER_FALLING下降沿触发。
IRQF_TRIGGER_HIGH高电平触发。
IRQF_TRIGGER_LOW低电平触发。

③ free_irq 函数 

  使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。 

/*
 * @description : 释放中断
 * @param - irq : 要释放的中断
 * @param - dev : 如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。
                  共享中断只有在释放最后中断处理函数的时候才会被禁止掉
 * @return : 无
 */
void free_irq(unsigned int irq, void *dev);

④ 中断处理函数 

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

irqreturn_t (*irq_handler_t) (int, void *);

/*
 第一个参数是要中断处理函数相应的中断号。
 第二个参数是一个指向 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;


/* 其实一般中断服务函数返回值使用:*/
return IRQ_RETVAL(IRQ_HANDLED)

⑤ 中断使能与禁止函数 

void enable_irq(unsigned int irq);        // 使能指定中断
void disable_irq(unsigned int irq);       // 禁止指定中断

// 其实他们的参数 irq 都是要使能/禁止的中断号
// disable_irq 函数有个缺点是,使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出

  但我们不能确保的情况下,使用这个中断禁止函数(推荐使用):

// 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
void disable_irq_nosync(unsigned int irq);

  当我们需要关闭当前处理器整个中断系统的时候,使用以下函数:

local_irq_enable();        // 使能当前处理器的中断系统
local_irq_disable();       // 禁止当前处理器的中断系统

  这里也有一个缺点是,如果在中断禁止的时候使能中断,这时候可能会任务崩溃,所以使用(推荐使用):

local_irq_save(flags);        // 禁止中断,并且将中断状态保存在 flags 中
local_irq_restore(flags);     // 恢复中断,将中断到 flags 状态

2. 上半部与下半部 

  上半部: 中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。

  下半部: 如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。 

   Linux 内核将中断分为上半部和下半部的目的就是为了实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行。

  关于哪些工作放上半部,哪些放下半部,可以参考:

  ① 如果要处理的内容不希望被其他中断打扰,放在上半部;

  ② 如果要处理的任务对时间敏感,放到上半部;

  ③ 如果要处理的任务与硬件有关,放到上半部;

  ④ 除了以上三点,其他任务都可以放到下半部。

   上半部其实就是编写中断处理函数,下半部 Linux 提供了许多机制:

① 软中断

  软中断常用于处理需要及时响应的事件,优先级较高的任务。它可以根据优先级和中断处理队列的情况来确定哪个软中断被优先处理。

  Linux 内核使用 softirq_action 结构体表示软中断,并且在 kernel/softirq.c 文件中一共定义了 10 个软中断:

static struct softirq_action softirq_vec[NR_SOFTIRQS];
// NR_SOFTIRQS 是枚举类型,定义如下:
enum
{
    HI_SOFTIRQ=0, /* 高优先级软中断 */
    TIMER_SOFTIRQ, /* 定时器软中断 */
    NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ, /* tasklet 软中断 */
    SCHED_SOFTIRQ, /* 调度软中断 */
    HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    RCU_SOFTIRQ, /* RCU 软中断 */
    NR_SOFTIRQS
};

   10 个软中断,所以 NR_SOFTIRQS 元素有 10 个。

  softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组。

  如果要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数:

/*
 * @description : 注册软中断处理函数
 * @param - nr : 要开启的软中断,选择 NR_SOFTIRQS 其中一个元素
 * @param - action : 软中断对应的处理函数
 * @return : 没有返回值
 */
void open_softirq(int nr, void (*action)(struct softirq_action *));

   但是软中断必须必须在编译的时候静态注册(在编译时期将组件与系统进行绑定的配置方式)。内核使用 softirq_init 函数进行初始化软中断:

void __init softirq_init(void) 
{
    int cpu;

    for_each_possible_cpu(cpu) 
    {
        per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

   从 softirq_init函数可以看出,当使用软中断的时候,这个函数会自动打开:

HI_SOFTIRQ, /* 高优先级软中断 */
TASKLET_SOFTIRQ, /* tasklet 软中断 */

② tasklet

  tasklet 适用于低优先级的、需要延迟处理的事件。如果事件需要尽快得到处理并具有不同的优先级,那么软中断更适合;如果事件可以在稍后的时间点进行处理,并且没有特定的优先级要求,那么 tasklet 更适合。但我们已经在下半部分了,所以 tasklet 更适合使用。

  tasklet_struct 结构体如下:

struct tasklet_struct
{
    struct tasklet_struct *next; /* 下一个 tasklet */
    unsigned long state; /* tasklet 状态 */
    atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数 */    // 这里相当于中断处理函数
    unsigned long data; /* 函数 func 的参数 */
};

  如果要使用 tasklet,必须先定义一个 tasklet_struct 变量,然后使用 tasklet_init 函数对其进行初始化:

/*
 * @description :tasklet初始化函数
 * @param - t : 要初始化的 tasklet
 * @param - func : tasklet 的处理函数
 * @param - data : 要传递给 func 函数的参数
 * @return : 没有返回值
 */
void tasklet_init(struct tasklet_struct *t,
                  void (*func)(unsigned long),
                  unsigned long data);

  当然也可以使用宏 DECLARE_TASKLET 一次性来完成 tasklet 的定义和初始化。

/*
 * @description : 定义和初始化tasklet
 * @param - name : 要定义的tasklet名字
 * @param - func : tasklet的处理函数
 * @param - data : 传递给 func 函数的参数
 */
DECLARE_TASKLET(name, func, data);

  除此之外,在上半部分的中断处理函数需要调用 tasklet_schedule 函数,这就可以让 tasklet 在合适的时间运行:

// 这里的形参指针 t:要调度的tasklet
void tasklet_schedule(struct tasklet_struct *t);

   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);
    ......
}

  流程图如下:

③ 工作队列

  工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。 所以如果你要推后的工作可以睡眠的话,那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。 

  在 Linux 内核中,使用 work_struct 结构体表示一个工作,这些工作组成工作队列,工作队列用 workqueue_struct 结构体表示,有了工作队列之后,Linux 内核使用 worker 结构体表示工作者线程,就是管理工作队列的结构体。

  在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用管,因为这两者都是由内核自动管理的。

  创建工作其实就直接定义一个 work_struct 结构体变量:

#define INIT_WORK(_work, _func)
/*
 _work:要初始化的工作
 _func:工作对应的要处理函数
 */

// 也可以用使用 DECLARE_WORK 宏一次性完成工作的创建和初始化
#define DECLARE_WORK(n, f)
/*
 n:要初始化的工作(work_struct)
 f:工作对应的要处理函数
 */

   和 tasklet 一样,工作也是需要调度才能工作,它的调度函数为 schedule_work

/*
 * @description : 调度工作(wrok_struct)的函数
 * @param - work : 要调度的工作
 * @return : 0 成功,其他值 失败
 */
bool schedule_work(struct work_struct *work);

  工作使用示例:

/* 定义工作(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);
    ......
}

  这里的 工作 的使用流程其实跟 tasklet 一模一样。但他们区别还是蛮大的,例如 工作 是在进程上下文中执行,tasklet 是在软中断上下文执行等等。

3. 设备树中断信息节点

① GIC 中断控制器

  STM32MP1 有三个与中断有关的控制器: GIC、EXTI 和 NVIC 。因为 NVIC 是 Cortex-M4 内核的中断控制器,暂时不考虑。

  GIC 有 4 个版本:V1~V4,V1 被淘汰,V3 和 V4 是 64 位芯片使用,这次使用的是  GIC V2。

  GIC 中断控制器是用来管理中断的优先级、中断分发、中断控制等。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是 ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ,他们之间的关系如下:

  GIC 接受很多的外部中断,然后对其进行处理,最终通过4个信号报给 ARM 内核:

  VFIQ:虚拟快速 FIQ。

  VIRQ:虚拟快速 IRQ。 

  FIQ:快速中断 IRQ。 

  IRQ:外部中断 IRQ。 

  虚拟 FIQ 是专门虚拟化环境设计的中断机制,与传统的 FIQ 相互独立,VIRQ 也是有虚拟化环境机制。

  FIQ 必须尽快处理,处理结束后离开这个 FIQ。IRQ 可以被 FIQ 中断,但 IRQ 不能中断 FIQ,因此 FIQ 响应更快。GIC V2的逻辑图如下:

  左边就是中断源,中间是 GIC 控制器,右边是中断控制器向处理器内核发送的中断信息。

  重点看中间部分,GIC 将中断源分为三类:

  ① SPI(Shared Peripheral Interrupt),共享中断,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于共享中断 。比如 GPIO 中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。

  ② PPI(Private Peripheral Interrupt),私有中断,GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。

  ③ SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。 

② 中断 ID

  因为有很多中断,为了区分他们必须给他们分配唯一一个 ID 号。这个 ID 号就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,这 1020 个 ID 分配如下:

  D0~ID15:这 16 个 ID 分配给 SGI(软件中断)。

  ID16~ID31:这 16 个 ID 分配给 PPI(私有中断)。 

  D32~ID1019:这 988 个 ID 分配给 SPI(共享中断)。

  STM32MP157 总共分配了 265 个中断 ID,加上 SGI 和 PPI,就有 288 个中断ID。从 ID 32 开始的 SPI 中断:

③ EXTI

  EXTI 称为 外部中断和事件控制器, EXTI 通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对 CPU 事件输入生成事件。 EXTI 唤醒请求可让系统从停止模式唤醒,以及让 CPU 从 CSTOP 和 CSTANDBY 模式唤醒。此外, EXTI 还可以在运行模式下生成中断请求和事件请求。在实际使用中 EXTI 主要是为 STM32 的 GPIO 中断服务的。 

  EXTI 的异步输入事件可以分为 2 组:

  ① 可配置事件(来自能够生成脉冲的 I/O 或外设的信号),这类事件具有以下特性:
  – 可选择的有效触发边沿。
  – 中断挂起状态寄存器位。
  – 单独的中断和事件生成屏蔽。
  – 支持软件触发。

  ② 直接事件(来自其他外设的中断和唤醒源,需要在外设中清除),这类事件具有以下特性:
  – 固定上升沿有效触发。
  – EXTI 中无中断挂起状态寄存器位(中断挂起状态由生成事件的外设提供)。
  – 单独的中断和事件生成屏蔽。
  – 不支持软件触发。

  对于 GPIO 中断来说,就是可配置事件,EXIT 和 GIC 关系如下:

  从上图中可以看出中断方式:

  ① 外设直接产生中断到 GIC,然后 GIC 通知 CPU 内核。

  ② GPIO 或外设产生中断到 EXTI,EXTI 将信号提交给 GIC,最终再将中断信号提交给 CPU。

  ③ GPIO 或外设产生中断到 EXTI,EXTI 直接将中断信号提交给 CPU。

  Linux 系统会用到这三种中断方式,一个外设最多可以有两种中断方式。GPIO 中断是我们最常用的。STM32 每一组 GPIO 最多 16 个 IO,比如 PA0~PA15,因此每组 GPIO 就有 16 个中断,这 16 个 GPIO 事件输入对应 EXTI0~15,其中 PA0、PB0,只要是 PX0 的都是对应的是 EXTI0(其实跟学习STM32裸机的时候一样):

  如果要在 Linux 系统中使用中断,那么就需要在设备树中设置好中断信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。

④ GIC 控制节点

  首先进入 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下,打开 stm32mp151.dtsi 文件。

122     intc: interrupt-controller@a0021000 {
 123         compatible = "arm,cortex-a7-gic";    // compatible属性为"arm,cortex-a7-gic",那么内核就会去找""里的内容,即可找到GIC中断驱动文件
 124         #interrupt-cells = <3>;    // #interrupt-cells 和#address-cells、 #size-cells 一样。
 125         interrupt-controller;    // 表示当前节点为中断控制器,类似于gpio-controller;
 126         reg = <0xa0021000 0x1000>,
 127               <0xa0022000 0x2000>;
 128     };

/*
 详细了解 #interrupt-cells = <3>;
 表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息。
 #interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个cells都是32位整型值。这三个cells含义如下:
 第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。
 第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 32~287(256 个),对于 PPI 中断来说中断号的范围为 16~31,但是该 cell 描述的中断号是从 0 开始。
 第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码。
 */

  首先来看一下 SPI6 如何在设备树节点中描述中断信息的,找到 SPI6 对应的中断号:

  第一列的 Num 是 86 号,但是注意,这里并没有算上前面的 PPI + SGI = 32,所以这里应该是 32 + 86 = 118,就跟第二列的 ID 号所对应。

  打开stm32mp151.dtsi,找到 SPI6 节点内容:

1712         spi6: spi@5c001000 {
1713             #address-cells = <1>;
1714             #size-cells = <0>;
1715             compatible = "st,stm32h7-spi";
1716             reg = <0x5c001000 0x400>;
1717             interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;    
1718             clocks = <&scmi0_clk CK_SCMI0_SPI6>;
1719             resets = <&scmi0_reset RST_SCMI0_SPI6>;
1720             dmas = <&mdma1 34 0x0 0x40008 0x0 0x0 0x0>,
1721                    <&mdma1 35 0x0 0x40002 0x0 0x0 0x0>;
1722             dma-names = "rx", "tx";
1723             power-domains = <&pd_core>;
1724             status = "disabled";

/*
 interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
 第一个表示中断类型,为 GIC_SPI,也就是共享中断。
 第二个表示中断号为86。
 第三个表示中断出发类型,高电平触发
 */

⑤ EXTI 控制节点

  打开 stm32mp151.dtsi,其中的 exti 节点就是 EXTI 中断控制器的节点:

exti: interrupt-controller@5000d000 {
    compatible = "st,stm32mp1-exti", "syscon";
    interrupt-controller;    // 表示exti节点是中断控制器
    #interrupt-cells = <2>;  // 第一个cell表示中断号,第二个cell表示中断标志位
    reg = <0x5000d000 0x400>;
    hwlocks = <&hsem 1 1>;    // 硬件锁,指向hsem节点,数字 "1 1" 是传递给硬件锁节点的参数
};

   在 GPIO 中其实也用到了 EXIT,所以 GPIO 节点里面也有 EXTI 相关内容:

pinctrl: pin-controller@50002000 {
1815             #address-cells = <1>;
1816             #size-cells = <1>;
1817             compatible = "st,stm32mp157-pinctrl";
1818             ranges = <0 0x50002000 0xa400>;
1819             interrupt-parent = <&exti>;    // 指定pinctrl所有子节点的中断父节点为exti,这样就可以将GPIO和EXTI联系起来
1820             st,syscfg = <&exti 0x60 0xff>;
1821             hwlocks = <&hsem 0 1>;
1822             pins-are-numbered;
1823 
1824             gpioa: gpio@50002000 {
1825                 gpio-controller;
1826                 #gpio-cells = <2>;
1827                 interrupt-controller;    // 表示gpioa节点也是中断控制器
1828                 #interrupt-cells = <2>;  // 这里的第一个cell表示某个IO所处组的编号(类似PA0),第二个cell表示中断触发方式,每个#interrupt-cells在EXTI、GPIO和GIC含义都不一样
1829                 reg = <0x0 0x400>;
1830                 clocks = <&rcc GPIOA>;
1831                 st,bank-name = "GPIOA";
                                             // 比如现在要设置PA1引脚为下降沿触发,interrupts=<1 IRQ_TYPE_EDGE_FALLING>
1832                 status = "disabled";
1833             };
    ...
/* 由于GPIOA-GPIOK是连续的,GPIOZ对应的寄存器地址不是连续的,所以单独使用pinctrl_z来描述GPIOZ */
pinctrl_z: pin-controller-z@54004000 {
1947             #address-cells = <1>;
1948             #size-cells = <1>;
1949             compatible = "st,stm32mp157-z-pinctrl";
1950             ranges = <0 0x54004000 0x400>;
1951             pins-are-numbered;
1952             interrupt-parent = <&exti>;
1953             st,syscfg = <&exti 0x60 0xff>;
1954             hwlocks = <&hsem 0 1>;
1955 
1956             gpioz: gpio@54004000 {
1957                 gpio-controller;
1958                 #gpio-cells = <2>;
1959                 interrupt-controller;
1960                 #interrupt-cells = <2>;
1961                 reg = <0 0x400>;
1962                 clocks = <&scmi0_clk CK_SCMI0_GPIOZ>;
1963                 st,bank-name = "GPIOZ";
1964                 st,bank-ioport = <11>;
1965                 status = "disabled";
1966             };
1967         };
1968     };

  来看一个具体应用:

hdmi-transmitter@39 {
    compatible = "sil,sii9022";    // sii9022是ST开发板上的HDMI芯片
    reg = <0x39>;
    iovcc-supply = <&v3v3_hdmi>;
    cvcc12-supply = <&v1v2_hdmi>;
    reset-gpios = <&gpioa 10 GPIO_ACTIVE_LOW>;    
    interrupts = <1 IRQ_TYPE_EDGE_FALLING>;    // 这个芯片是连接到PG1,下降沿触发中断。
    interrupt-parent = <&gpiog>;    // 指定中断节点的父节点为 gpiog
    #sound-dai-cells = <0>;
    status = "okay";
};

// 其实在实际开发过程中,只需要通过interrupts和interrupt-parent就可以指定引脚和触发方式。

   stm32mp157f-ev1-a7-examples.dts 文件,再来看一个应用:

 16     test_keys {
 17         compatible = "gpio-keys";
 18         #address-cells = <1>;
 19         #size-cells = <0>;
 20         autorepeat;
 21         status = "okay";
 22         /* gpio needs vdd core in retention for wakeup */
 23         power-domains = <&pd_core_ret>;
 24 
 25         button@1 {
 26             label = "PA13";
 27             linux,code = <BTN_1>;
 28             interrupts-extended = <&gpioa 13 IRQ_TYPE_EDGE_FALLING>;    // 新出现的interrupts-extended
 29             status = "okay";
 30             wakeup-source;
 31         };
 32     };

/*
 上述代码来描述一个按键,此按键采用中断方式并且使用到PA13引脚。
 直接通过 interrupts-extended 一个属性描述了所有中断信息,如果要用普通方式来描述的话:
 interrupt-parent = <&gpioa>;
 interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
 这种 interrupts-extended 更加简介。
 */

⑥ 获取中断号

  编写驱动的时候就需要中断号,用到的中断号,这个信息都已经写到了设备树里面。

  一个是 interrupt 属性提取对应设备号:

/*
 * @description : 从interrupt属性提取到对应的设备号
 * @param - dev : 设备节点
 * @param - index : 索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息
 * @return : 中断号
 */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

   一个是从 gpio 属性里提取设备号:

/*
 * @description : 从 GPIO 属性提取到对应的设备号
 * @param - gpio : 要获取的GPIO编号
 * @return : GPIO 对应的中断号
 */
int gpio_to_irq(unsigned int gpio);

二、实验程序编写

  这次使用的是案件来触发中断。

  首先修改按键中的设备树,打开 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下的 stm32mp157d-atk.dts 文件,修改 key 节点内容:

52     key {
 53         compatible = "alientek,key";
 54         status = "okay";
 55         key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
 56         interrupts-extended = <&gpiog 3 IRQ_TYPE_EDGE_BOTH>;    // IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于按下KEY0和释放的时候同时有效
 57         // 也可以这样写
 58         // interrupt-parent = <&gpiog>;
 59         // interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
 60     };

   之后编译设备树:

cd
cd linux/atk-mpl/linux/my_linux/linux-5.4.31/
make dtbs

  将编译好的设备树复制:

cd arch/arm/boot/dts/
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f

   在 ~/linux/atk-mpl/Drivers 目录下创建 13_irq,并在里面创建 Vscode 工作区,新建一个 keyirq.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.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*/

/* 定义按键状态 */
enum key_status {
    KEY_PRESS = 0,      /* 按键按下 */ 
    KEY_RELEASE,        /* 按键松开 */ 
    KEY_KEEP,           /* 按键状态保持 */ 
};

/* key设备结构体 */
struct key_dev{
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	int key_gpio;				/* key所使用的GPIO编号		*/
	struct timer_list timer;	/* 按键值 		*/
	int irq_num;				/* 中断号 		*/
	spinlock_t spinlock;		/* 自旋锁		*/
};

static struct key_dev key;          /* 按键设备 */
static int status = KEY_KEEP;   	/* 按键状态 */

/* 中断进入定时器,定时时间是把按键抖动给延时掉 */
static irqreturn_t key_interrupt(int irq, void *dev_id)	// 中断处理函数
{
	/* 按键防抖处理,开启定时器延时15ms */
	mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));	// 为什么需要周期性的定时器,是因为每当检测到按下一次就需要定时器延时
    return IRQ_HANDLED;		// IRQ_HANDLED是一个预定义的常量,表示中断已经得到处理,并且处理程序成功执行了必要的操作
}

/*
 * @description	: 初始化按键IO,open函数打开驱动的时候
 * 				  初始化按键所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int key_parse_dt(void)
{
	int ret;
	const char *str;
	
	/* 设置LED所使用的GPIO */
	/* 1、获取设备节点:key */
	key.nd = of_find_node_by_path("/key");
	if(key.nd == NULL) {
		printk("key node not find!\r\n");
		return -EINVAL;
	}

	/* 2.读取status属性 */
	ret = of_property_read_string(key.nd, "status", &str);
	if(ret < 0) 
	    return -EINVAL;

	if (strcmp(str, "okay"))
        return -EINVAL;
    
	/* 3、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(key.nd, "compatible", &str);
	if(ret < 0) {
		printk("key: Failed to get compatible property\n");
		return -EINVAL;
	}

    if (strcmp(str, "alientek,key")) {
        printk("key: Compatible match failed\n");
        return -EINVAL;
    }

	/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */
	key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
	if(key.key_gpio < 0) {
		printk("can't get key-gpio");
		return -EINVAL;
	}

    /* 5 、获取GPIO对应的中断号 */
    key.irq_num = irq_of_parse_and_map(key.nd, 0);
    if(!key.irq_num){
        return -EINVAL;
    }

	printk("key-gpio num = %d\r\n", key.key_gpio);
	return 0;
}

/* 主要进行GPIO和中断的初始化 */
static int key_gpio_init(void)
{
	int ret;
    unsigned long irq_flags;
	
	/* 使用GPIO就要申请GPIO使用权 */
	ret = gpio_request(key.key_gpio, "KEY0");
    if (ret) {
        printk(KERN_ERR "key: Failed to request key-gpio\n");
        return ret;
	}	
	
	/* 将GPIO设置为输入模式 */
    gpio_direction_input(key.key_gpio);

   /* 获取设备树中指定的中断触发类型 */
	irq_flags = irq_get_trigger_type(key.irq_num);		// 获得定义的中断触发类型
	if (IRQF_TRIGGER_NONE == irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
		
	/* 申请中断(使用中断必须申请中断) */
	ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);	// request_irq会默认使能中断,所以不需要enable_irq使能中断
	if (ret) {
        gpio_free(key.key_gpio);
        return ret;
    }
	// 建议申请成功后先用disbale_irq函数禁止中断,等所有工作完成之后再来使能中断

	return 0;
}

/* 定时器处理函数 */
static void key_timer_function(struct timer_list *arg)
{
    static int last_val = 1;	// 保存按键上一次读取到的值
    unsigned long flags;
    int current_val;		// 存放当前按键读取的值

    /* 自旋锁上锁 */
    spin_lock_irqsave(&key.spinlock, flags);

    /* 读取按键值并判断按键当前状态 */
    current_val = gpio_get_value(key.key_gpio);
    if (0 == current_val && last_val)       /* 按下 */ 	// 读取的值为0,上次的值为1,则是按下
        status = KEY_PRESS;
    else if (1 == current_val && !last_val)
        status = KEY_RELEASE;  	 			/* 松开 */ 
    else
        status = KEY_KEEP;              	/* 状态保持 */ 

    last_val = current_val;

    /* 自旋锁解锁 */
    spin_unlock_irqrestore(&key.spinlock, flags);
}

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

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

    /* 自旋锁上锁 */
    spin_lock_irqsave(&key.spinlock, flags);

    /* 将按键状态信息发送给应用程序 */
    ret = copy_to_user(buf, &status, sizeof(int));	// 当前的status保存了按键当前的状态

    /* 状态重置 */
    status = KEY_KEEP;

    /* 自旋锁解锁 */
    spin_unlock_irqrestore(&key.spinlock, flags);

    return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = 	key_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init mykey_init(void)
{
	int ret;
	
	/* 初始化自旋锁 */
	spin_lock_init(&key.spinlock);
	
	/* 设备树解析 */
	ret = key_parse_dt();
	if(ret)
		return ret;
		
	/* GPIO 中断初始化 */
	ret = key_gpio_init();
	if(ret)
		return ret;
		
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
	if(ret < 0) {
		pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);
		goto free_gpio;
	}
	
	/* 2、初始化cdev */
	key.cdev.owner = THIS_MODULE;
	cdev_init(&key.cdev, &key_fops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
	if(ret < 0)
		goto del_unregister;
		
	/* 4、创建类 */
	key.class = class_create(THIS_MODULE, KEY_NAME);
	if (IS_ERR(key.class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);
	if (IS_ERR(key.device)) {
		goto destroy_class;
	}
	
	/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
	timer_setup(&key.timer, key_timer_function, 0);
	
	return 0;

destroy_class:
	class_destroy(key.class);
del_cdev:
	cdev_del(&key.cdev);
del_unregister:
	unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:
	free_irq(key.irq_num, NULL);
	gpio_free(key.key_gpio);
	return -EIO;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit mykey_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&key.cdev);/*  删除cdev */
	unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */
	del_timer_sync(&key.timer);		/* 删除timer */
	device_destroy(key.class, key.devid);/*注销设备 */
	class_destroy(key.class); 		/* 注销类 */
	free_irq(key.irq_num, NULL);	/* 释放中断 */
	gpio_free(key.key_gpio);		/* 释放IO */
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

  新建一个 keyirqApp 测试文件, 通过不断的读取/dev/key 设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有 3 个值,分别为 0、 1、 2; 0 表示按键按下时的这个状态, 1 表示按键松开时对应的状态,而 2 表示按键一直被按住或者松开。编写测试 APP:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*
 * @description		: main主程序
 * @param – argc		: argv数组元素个数
 * @param – argv		: 具体参数
 * @return			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;
    int key_val;

    /* 判断传参个数是否正确 */
    if(2 != argc) {
        printf("Usage:\n"
             "\t./keyApp /dev/key\n"
            );
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDONLY);
    if(0 > fd) {
        printf("ERROR: %s file open failed!\n", argv[1]);
        return -1;
    }

    /* 循环读取按键数据 */
    while(1) {

        read(fd, &key_val, sizeof(int));
        if (0 == key_val)
            printf("Key Press\n");
        else if (1 == key_val)
            printf("Key Release\n");
    }

    /* 关闭设备 */
    close(fd);
    return 0;
}

  编写 Makefile 文件:

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)

obj-m := keyirq.o

build: kernel_modules

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

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

  编译 keyirq.c 和 keyirqApp.c 文件:

make -j32
arm-none-linux-gnueabihf-gcc keyirqApp.c -o keyirqApp

  将编译好的 keyirqApp 和 keyirq.ko 文件复制:

sudo cp keyirqApp keyirq.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/

  开启开发板,输入以下命令:

cd lib/modules/modules/5.4.31/
depmod     # 第一次加载驱动需要运行此命令
modprobe keyirq.ko     # 加载驱动

  可以查看 /proc/interrputs 文件来检查对应的中断是否注册上了:

cd
cat /proc/interrupts

  从上图可以看出,keyirq.c 驱动文件里面的 KYE0 中断已经存在,触发方式为跳边沿(Edge)。

  接下来测试中断:

cd lib/modules/5.4.31/
./keyirqApp /dev/key

  按键值成功获取,并且不会有抖动的误判发生,说明消抖工作正常。

  卸载驱动:

rmmod keyirq.ko

总结

  概念:

  首先,我们学习了 Linux 中断号,并且了解了中断是如何开启的。每当使用到了中断,必须去申请中断(request_irq),在驱动出口再释放中断(free_irq),如果使用了 request_irq 函数,那么就不用使用使能中断 (enable_irq)。建议在申请成功后先用 disbale_irq 函数禁止中断,等所有工作完成之后再来使能中断。

  其次,学习了上半部和下半部,上半部其实就是对哪些时间敏感、执行速度快的操作放在中断处理函数中,也就是上半部,其他的就放在下半部。下半部里面我们学习了三个东西:

  ① 软中断:它是处理需要及时响应的事件,一般这个了解即可。

  ② tasklet:是利用软中断来实现,这个是适用于低优先级,需要延迟的事件。这个需要掌握概念和使用方法,定义->处理函数->中断处理函数里写调度->驱动入口函数里写初始化和注册中断处理函数。

  ③ 工作队列:工作队列在进程上下文执行,如果你的工作可以睡眠,那么选择工作队列。掌握概念及使用方法,使用方法和 tasklet 极为相似。

  最后,学习了设备树的中断信息节点,这里面有 GIC 中断控制(重点了解SPI(共享中断))、中断ID(需要查手册)和 EXTI。后面又了解到了 GIC 控制节点和 EXTI 控制节点,这两者 compatible 里面的元素和 #interrupt-cells 信息稍许不一样外,其他类似。并且在设备树里加入中断信息的方式有两种:一种是 interrupts-extended、另一种是 interrupt-parent 和 interrupts,前者是后者的结合体。

  程序:

  ① 在初始化阶段分开了设备树信息设置和 GPIO 初始化设置;

  ② 在中断处理函数中增减定时器消除按键抖动;

  ③ 定时器处理函数(也就是回调函数)中去判断按键的值,并打印出按键的值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/125945.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【python海洋专题四十四】海洋指数画法--多色渐变柱状图

【python海洋专题四十四】海洋指数画法–多色渐变柱状图

winform开发小技巧

如果我们不知道怎么在代码中new 一个控件&#xff0c;我们可以先在窗体中拉一个然后看Form1.Designer.cs 里面生成的代码就是我们要的 我们会在下面看到 还有泛型的使用&#xff0c;马上更新

Termius for Mac:掌控您的云端世界,安全高效的SSH客户端

你是否曾经在Mac上苦苦寻找一个好用的SSH客户端&#xff0c;让你能够远程连接到Linux服务器&#xff0c;轻松管理你的云端世界&#xff1f;现在&#xff0c;我们向你介绍一款强大而高效的SSH客户端——Termius。 Termius是一款专为Mac用户设计的SSH客户端&#xff0c;它提供了…

JavaScript从入门到精通系列第三十二篇:详解正则表达式语法(一)

文章目录 一&#xff1a;正则表达式 1&#xff1a;量词设置次数 2&#xff1a;检查字符串以什么开头 3&#xff1a;检查字符串以什么结尾 4&#xff1a; 同时使用开头结尾 5&#xff1a;同值开头同值结尾 二&#xff1a;练习 1&#xff1a;检查是否是一个手机号 大神链…

『MySQL快速上手』-⑤-数据类型

文章目录 1.数据类型有哪些2.数值类型2.1 tinyint 类型2.2 bit 类型2.3 小数类型2.3.1 float2.3.2 decimal3.字符串类型3.1 char3.2 varchar3.2 char 和 varchar 比较4.日期和时间类型5.enum和set1.数据类型有哪些 MySQL支持多种数据类型,这些数据类型可用于定义表中的列,以…

Selenium关于内容信息的获取读取

在进行自然语言处理、文本分类聚类、推荐系统、舆情分析等研究中,通常需要使用新浪微博的数据作为语料,这篇文章主要介绍如果使用Python和Selenium爬取自定义新浪微博语料。因为网上完整的语料比较少,而使用Selenium方法有点简单、速度也比较慢,但方法可行,同时能够输入验…

【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果

前言 【Unity ShaderGraph】| 如何快速制作一个炫酷的 全息投影效果一、效果展示二、 全息投影效果 前言 本文将使用ShaderGraph制作一个 炫酷的 全息投影效果 &#xff0c;可以直接拿到项目中使用。对ShaderGraph还不了解的小伙伴可以参考这篇文章&#xff1a;【Unity Shader…

三国志14信息查询小程序(历史武将信息一览)制作更新过程06-复现小程序

0&#xff0c;所需文件 所需全部文件下载 文件总览&#xff1a; 代码&#xff1a; 数据库&#xff1a; 1&#xff0c;前期准备 假定你已经看过前面的文章&#xff0c;并完成了下列准备&#xff1a; &#xff08;1&#xff09;一台有公网IP的云服务器&#xff0c;服务器上…

Oracle 三种分页方法(rownum、offset和fetch、row_number() over())

Oracle的三种分页指的是在进行分页查询时&#xff0c;使用三种不同的方式来实现分页效果&#xff0c;分别是使用rownum、使用offset和fetch、使用row_number() over() 1、使用rownum rownum是oracle中一个伪劣&#xff0c;它用于表示返回的行的序号。使用rownum进行分页查询的方…

数据结构之单链表基本操作

&#x1f937;‍♀️&#x1f937;‍♀️&#x1f937;‍♀️ 今天给大家分享的是单链表的基本操作。 清风的个人主页 &#x1f389;欢迎&#x1f44d;点赞✍评论❤️收藏 &#x1f61b;&#x1f61b;&#x1f61b;希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位…

大数据毕业设计选题推荐-农作物观测站综合监控平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

计算当月工作日时间进度

目录 1.按一个月平均算 2.除去星期六星期天算 3.自定义节假日算 1.按一个月平均算 // 获取当前时间 const now new Date(); // 获取当前年份和月份 const currentYear now.getFullYear(); const currentMonth now.getMonth() 1; // 计算当月天数 const daysInMonth ne…

【Docker】iptables基本原理

在当今数字化时代&#xff0c;网络安全问题变得越来越重要。为了保护我们的网络免受恶意攻击和未经授权的访问&#xff0c;我们需要使用一些工具来加强网络的安全性。其中&#xff0c;iptables是一个强大而受欢迎的防火墙工具&#xff0c;它可以帮助我们控制网络流量并保护网络…

【剑指offer|图解|双指针】训练计划 I + 删除有序数组中的重复项

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️训练计划 I二. ⛳️查找总价格为目标值的两个商品三. ⛳️删除有序数组中的…

【JAVA学习笔记】67 - 坦克大战1.5 - 1.6,防止重叠,记录成绩,选择是否开新游戏或上局游戏,播放游戏音乐

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter20/src 增加功能 1.防止敌人坦克重叠运动 2.记录玩家的成绩&#xff0c;存盘退出 3.记录当时的敌人坦克坐标&#xff0c;存盘退出 4.玩游戏时&#xff0c;可以选择是开新游戏还是继续上局…

尚硅谷大数据项目《在线教育之实时数仓》笔记007

视频地址&#xff1a;尚硅谷大数据项目《在线教育之实时数仓》_哔哩哔哩_bilibili 目录 第9章 数仓开发之DWD层 P053 P054 P055 P056 P057 P058 P059 P060 P061 P062 P063 P064 P065 第9章 数仓开发之DWD层 P053 9.6 用户域用户注册事务事实表 9.6.1 主要任务 读…

Facebook主页评分的优化建议

Facebook是全球最大的社交媒体平台之一&#xff0c;它拥有着超10亿的用户&#xff0c;那么在这个竞争激烈的平台上维护和优化你的Facebook主页评分对于增加曝光度以及吸引更多的粉丝和提升品牌形象是非常重要的&#xff0c;下面小编将讲讲Facebook主页评分的优化建议。 1、清楚…

《国产服务器操作系统发展报告(2023)》重磅发布

11月1日&#xff0c;《国产服务器操作系统发展报告&#xff08;2023&#xff09;》&#xff08;以下简称“报告”&#xff09;在 2023 云栖大会上正式发布&#xff0c;开放原子开源基金会理事长孙文龙、中国信息通信研究院副总工程师石友康、阿里云基础软件部副总裁马涛、浪潮信…

MathWorks Matlab R2023b ARM Mac报错 License Manager Error -8

MathWorks Matlab R2023b 23.2.0.2365128 ARM 版本安装激活后出现报错&#xff1a; License Manager Error -8 License checkout failed. License Manager Error -8 Make sure the HostID of the license file matches this machine, and that the HostID on the SERVER line m…

k8s存储卷 PV和PVC

目录 emptyDir存储卷 hostPath存储卷 nfs共享存储卷 PVC 和 PV 生命周期 一个PV从创建到销毁的具体流程如下&#xff1a; 静态pvc 动态pvc 3、定义PVC 4、测试访问 搭建 StorageClass NFS&#xff0c;实现 NFS 的动态 PV 创建 1、在stor01节点上安装nfs&#xff0…