当前位置: 首页 > article >正文

字符设备驱动 — 4 异常与中断

异常与中断

image-20240531195008754

中断属于异常的一种

异常会直接打断 CPU 的运行,而各种中断会被传到中断控制器,由中断控制器来选择优先级最高的中断并通知 CPU

处理流程

arm 对异常(中断)处理流程:

  1. 初始化:

    1. 设置中断源,让它可以产生中断
    2. 设置中断控制器(可以屏蔽中断、优先级)
    3. 设置 CPU 总开关(使能中断)
  2. 执行其他程序:正常程序

  3. 产生中断:比如按下按键 -> 中断控制器 -> CPU

  4. CPU 在每执行完一条指令都会检查 有/无 中断/异常 产生

  5. CPU 发现有 中断/异常 产生,开始处理

    对于不同的异常(中断),跳去不同的地址程序

  6. 这些函数做什么事情:

    1. 保护现场(各种寄存器)
    2. 处理异常(中断);分辨中断源,再调用不同的处理函数
    3. 恢复现场

3、4、5 都是由硬件完成的

异常向量表

异常向量表:异常向量表就是把所有异常类型码及其对应的异常向量(入口函数)按一定的规律存在一个区域内,这个存储区域就叫做异常向量表

中断向量表:把系统中所有的中断类型码(中断号)及其对应的中断向量(中断处理函数)按一定的规律存放在一个区域内,这个存储区域就叫中断向量表

中断向量表包含在异常向量表中

异常向量表中的 “ _irq ” 代表中断

异常向量表:

image-20240601093855130

异常向量表,每一条指令对应一种异常

发生复位时,CPU 就去执行第1条指令:b reset

发生中断时,CPU 就去执行 “ ldr pc,_irq ” 这条指令

这些指令存放的位置是固定的。比如对于 ARM9 芯片中断向量的地址是 0x18(这里指的是偏移位置),当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

image-20240531202100596

查看

查看 /proc/interrupts 可以获取系统中的中断信息

第一列:中断号

第二列:向 CPU 产生中断的次数

再后面就是中断的信息

image-20240601161447658

中断函数

申请中断:

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    	const char *name, void *dev)
  • **irq:**要申请的硬件中断号

  • **handler:**中断处理函数(上半部)

    typedef irqreturn_t (*irq_handler_t)(int, void *);
    
  • **flags:**中断处理的属性

    1. IRQF_DISABLED:表示进入中断处理程序时禁止任何中断,包括其它CPU,默认只是屏蔽当前对应所有CPU的中断线

    2. IRQF_SHARED:表示中断线是共享的,也就是说同一个中断线上会对应多个中断处理程序,在中断处理程序中,通过第5个参数dev来判断到底是哪个发生了中断

  • **name:**中断名

  • **dev:**中断共享时用到,用来判断是哪个中断触发

释放中断:

void free_irq(unsigned int irq, void *dev_id)

irq:硬件中断号

dev_id:如果中断是共享的,则仅删除dev_id所对应的处理程序,直到最后一个中断被删除的时候才禁用此中断线

屏蔽中断:

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

disable_irq 会等待中断处理完成,所以如果在中断的上半部调用,则会造成系统死锁,这种情况下,只能调用disable_irq_nosync

#define local_irq_save(flags) ...
void local_irq_disable(void);

这两个函数或宏将屏蔽 CPU 内所有的中断

与之对应的恢复所有的中断也有两个函数或宏

#define local_irq_restore(flags) ...
void local_irq_enable(void);

Liunx系统对于中断的处理

《中断学习(一) —— Linux中断流程以及处理机制》 - 一个不知道干嘛的小萌新 - 博客园 (cnblogs.com)

ARM程序的运行流程:

首先需要明白,ARM 芯片属于精简指令集计算机(RISC)

​ 特点:1)对于内存只有读、写指令

​ 2)对数据的运算是在 CPU 的内部完成的

​ 3)CPU 复杂度较小,易于设计

eg:a = a + b,在 ARM 中分为4步来实现:

  1. 把内存 a 的值读入 CPU 寄存器 R0
  2. 把内存 b 的值读入 CPU 寄存器 R1
  3. 把 R0、R1 的值相加,存入 R0
  4. 把 R0 的值写入内存 a

进程与线程的切换(保护现场):

进程、线程的核心是栈,栈是一种串列形状的数据结构,这种数据结构的特点是后入先出

程序被中断,如何保存现场:

  1. 程序A正常运行中,突然中断触发。程序A被打断这个时候会暂停程序A运行,将程序A中所有的寄存器保存下来。这也就保存现场(保存现场并不局限于中断,也包括程序之间的切换:函数调用)
  2. 这些寄存器会被保存在内存中,而这块内存就被称为栈
  3. 然后ARM去执行中断程序
  4. 等中断程序执行完了,从栈中恢复刚被保存的寄存器值

进程与线程之间的切换也大致如此

中断的拓展

Linux中有硬件中断和软件中断

对于硬件中断的两个原则:

  1. 不能嵌套(防止过多的保存现场导致栈溢出)(早期的 Linux 支持中断嵌套)
  2. 越快越好(上半部、下半部,上半部简单的接收数据(中断触发会被屏蔽,硬件自动屏蔽),将耗时的处理放在下半部(可以被其他硬件中断打断))(在中断处理的过程中是不能进行进程调度的,进程调度要靠定时器中断来实现,所以中断处理需要越快越好)

硬件中断

对于按键等硬件产生的中断称为 “ 硬件中断 ”,每个硬件中断都有自己的处理函数

当硬件触发中断后,会自动跳转到异常向量表的 irq 中,然后判断中断源,再跳转到对应的中断服务函数中,这些都是由硬件完成的

软件中断

人为制造的中断

软件中断何时产生:

由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断

软件中断何时处理:

Linux系统中,各种硬件中断发生的很频繁,至少定时器中断每 10ms 发生1次(心跳),所以在处理完硬件中断后,再去处理软件中断

image-20240601103250695

  1. 硬件中断 A 处理过程中,没有其他中断产生:

    1. 中断 A 发生,count ++ 等于 1
    2. 硬件屏蔽中断,中断 A 上半部运行处理,处理完成,count – 等于 0
    3. count ++ 等于 1,硬件开启接收硬件中断,执行软件中断也就是所谓的中断下半部
    4. count – 等于 0,然后退出
  2. 硬件中断 A 处理过程中,又发生了中断 A:

    1. 第一次中断A发生,执行到第 ⑥ 时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表,这时 count 等于 1,并且中断A下半部的代码并未执行
    2. CPU 又从 ① 开始再次执行中断 A 的上半部代码,在 ① 中 count++ 等于2,到 ③ 后 count-- 等于1
    3. 在第 ④ 步发现 count 等于 1,所以直接结束当前第 2 次中断的处理
    4. 重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理,当第 2 次中断处理完毕, CPU会继续去执行第 ⑦ 步(但是只会处理一次下半部)

    可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只执行了一次

  3. 硬件中断 A 处理过程中,又发生了中断 B:

    1. 第一次中断A发生,执行到第 ⑥ 时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表,这时 count 等于 1,并且中断下半部的代码并未执行
    2. CPU 又从 ① 开始执行中断 B 的上半部代码,在 ① 中 count++ 等于2,到 ③ 后 count-- 等于1
    3. 在第 ④ 步发现 count 等于 1,所以直接结束当前第 2 次中断的处理
    4. 重点来了,第 2 次中断发生后,打断了第一次中断的第 ⑦ 步处理。当第 2 次中断处理完毕, CPU会继续去执行第 ⑦ 步

    在第 ⑦ 步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。 所以,多个中断的下半部,是汇集在一起处理的

中断不能休眠原因:

如果在中断的上半部睡眠,会发生什么:

在中断上半部执行的时候,硬件是屏蔽了中断接收的。所以操作系统接收不到任何中断(包括时钟中断),系统就会瘫痪(调度器依赖的时钟节拍也会没有)

如果在中断的下半部睡眠,会发生什么:

不能睡眠的关键是因为上下文
操作系统以进程调度为单位,进程的运行在进程的上下文中,以进程描述符作为管理的数据结构,进程可以睡眠的原因是操作系统可以切换不同进程的上下文,进行调度操作,这些操作都以进程描述符为支持,中断运行在中断上下文,没有一个所谓的中断描述符来描述它,它不是操作系统调度的单位,一旦在中断上下文中睡眠,首先无法切换上下文(因为没有中断描述符,当前上下文的状态得不到保存),其次,没有人来唤醒它,因为它不是操作系统的调度单位,此外,中断的发生是非常非常频繁的,在一个中断睡眠期间,其它中断发生并睡眠了,那很容易就造成中断栈溢出导致系统崩溃

注意:如果中断下半部使用的是工作队列机制,它是可以休眠的

执行中断下半部的三种机制

软中断

参考:

Linux中断子系统之软中断、tasklet和工作队列_linux 软中断-CSDN博客

初始化

内核中有一个软件中断数组

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

softirq_action 这个结构体用于描述一个软件中断,默认情况下,系统上只能使用 32 个软中断

struct softirq_action
{
    void (*action)(struct softirq_action *);
};

action是一个指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程

软件中断必须先注册,内核才能执行软中断,open_softirq 函数即用于该目的,它在 softirq_vec 表中指定的位置写入新的软中断

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

软中断的类型是通过枚举静态定义的,当前有10种软中断类型,从0到9,0的优先级最高,当系统调度到软中断时会最先执行

enum
{
    HI_SOFTIRQ=0,        /* 高优先级tasklet软中断 */
    TIMER_SOFTIRQ,       /* 定时器软中断 */
    NET_TX_SOFTIRQ,      /* 发送网络包软中断 */
    NET_RX_SOFTIRQ,      /* 接收网络包软中断 */
    BLOCK_SOFTIRQ,       /* 用于处理块设备「block device」的软中断 */
    IRQ_POLL_SOFTIRQ,    /* 用于执行IOPOLL的回调函数 */
    TASKLET_SOFTIRQ,     /* tasklet软中断 */
    SCHED_SOFTIRQ,       /* 用于进程调度与负载均衡的软中断 */
    HRTIMER_SOFTIRQ,     /* 用于高精度定时器的软中断 */
    RCU_SOFTIRQ,         /* 用于RCU服务的软中断 */

    NR_SOFTIRQS
};

在 start_kernel 函数会调用 softirq_init(); 函数来初始化软件中断:

void __init softirq_init(void)
{
    ...
    /* 注册tasklet相关的两个软中断 */
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
触发

系统通过一个 irq_cpustat_t 的数据结构来记录软中断触发与否,可以将其理解为软中断状态寄存器,该寄存器实际是一个 unsigned int 类型的 _softirq_pending 变量,32 bit 可以对应32个软中断,当然目前只使用了 bit0 到 bit9 这10个

typedef struct {
    unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

触发软中断其实就是在 _softirq_pending 相应的bit位上写1,触发软中断的接口有两个 raise_softirq 和 raise_softirq_irqodd,前者在接口内部会关本地中断,而后者需要保证调用时已经处于关中断状态

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

    if (!in_interrupt())
        wakeup_softirqd();
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

void __raise_softirq_irqoff(unsigned int nr)
{
    lockdep_assert_irqs_disabled();
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}
执行

因为每个cpu都有一个软中断状态寄存器__softirq_pending,因此每个CPU可以并行执行自己的软中断队列,软中断的执行时机有3个:

  1. 触发软中断后(执行raise_softirqraise_softirq_irqoff时);
  2. 硬中断退出时(irq_exit时);
  3. 任何调用local_bh_enable打开本地软中断的时机image-20240603094224628

有几种方法可开启软中断处理,但这些都归结为调用 do_softirq 函数

asmlinkage __visible void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())
        return;

    local_irq_save(flags);     /* 保存IF标志的状态值,并禁用本地CPU上的中断 */

    pending = local_softirq_pending();

    if (pending && !ksoftirqd_running(pending))
        do_softirq_own_stack();

    local_irq_restore(flags);  /* 恢复保存的IF标志的状态值并返回 */
}

in_interrupt() 该函数确认当前不处于中断上下文中(当然,即不涉及硬件中断),如果处于中断上下文,则立即结束,因为软中断用于执行 ISR 中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用

通过 local_softirq_pending,确定当前 CPU 软中断位图中所有置位的比特位。如果有软中断等待处理,则调用 __do_softirq

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;    /* 循环计数器的值初始化为10 */
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;

    /*
     * Mask out PF_MEMALLOC as the current task context is borrowedfor the
     * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
     * again if the socket is related to swapping.
     */
    current->flags &= ~PF_MEMALLOC;
    /* 将本CPU的__softirq_pending变量复制到局部变量中 */
    pending = local_softirq_pending();
    account_irq_enter_time(current);
    /* 关软中断,防止再次进入软中断执行逻辑 */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

restart:
    /* 将__softirq_pending清零,以便可以激活新的软中断 */
    set_softirq_pending(0);
    /* 开启硬件中断 */
    local_irq_enable();

    h = softirq_vec;

    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;

        h += softirq_bit - 1;

        vec_nr = h - softirq_vec;
        prev_count = preempt_count();
        /* 对softirq的执行次数计数,这是/proc/softirqs的数据来源 */
        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        /* 某软中断对应的action回调 */
        h->action(h);
        trace_softirq_exit(vec_nr);
        if (unlikely(prev_count != preempt_count())) {
            pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                   vec_nr, softirq_to_name[vec_nr], h->action,
                   prev_count, preempt_count());
            preempt_count_set(prev_count);
        }
        h++;
        pending >>= softirq_bit;
    }

    if (__this_cpu_read(ksoftirqd) == current)
        rcu_softirq_qs();
    /* 处理完一波软中断后,关闭硬件中断 */
    local_irq_disable();
    /* 读一下当前的__softirq_pending,看这段时间有没有触发新的软中断 */
    pending = local_softirq_pending();
    /* 有新的软中断需要处理:
     * 如果时间没超过2ms && 当前没有进程需要调度 && 这种循环没有超过10次,
     * 此时重新跳到while循环处依次执行软中断的action回调;
     * 否则唤醒ksoftirqd线程执行软中断处理。
     */
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        wakeup_softirqd();
    }

    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    /* 离开软中断上下文时,开启软中断 */
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    current_restore_flags(old_flags, PF_MEMALLOC);
}

__do_softirq函数只做固定时间或固定次数的循环,然后就返回了,避免出现延迟用户进程的执行,如果还有其余挂起的软中断,内核线程ksoftirqd将会在预期时间里处理它们

内核线程 ksoftirqd

系统中的每个处理器都分配了自身的守护进程,名为 ksoftirqd

static void wakeup_softirqd(void)
{
    /* Interrupts are disabled: no need to stop preemption */
    struct task_struct *tsk = __this_cpu_read(ksoftirqd);

    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
}

wake_up_process唤醒进程后运行run_ksoftirqd()函数

static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        /*
         * We can safely run softirq on inline stack, as we are not deep
         * in the task stack here.
         */
        __do_softirq();
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();
}

tasklet(小任务)机制

tasklet 机制主要用在下半部要做的事耗时不是很长

tasklet 是基于软件中断来实现的,上面软件中断处理的流程图使用的就是 tasket 机制

image-20240601155015348

tasklet 的使用非常的简单,我们只需要定义 tasklet 及其处理函数并将它们将关联

// tasklet 结构体
struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};
void my_tasklet_func(unsigned long); // 定义一个处理函数

DECLARE_TASKLET(my_tasklet, my_tasklet_fun, data); 
 // 代码 DECLARE_TASKLET 实现了定义名称为	my_tasklet 的 tasklet 并将其与 my_tasklet_func 这个函数绑定,而传入这个函数的参数为 data
 
tasklet_schedule(&my_tasklet) // 使系统在适当的时候进行调度运行

使用模板:

根据上面软件中断的那个流程图可以写出以下的使用模板

void xxx_do_tasklet(unsigned long);

struct tasklet_struct xxx_tasklet;

DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);
 
void xxx_do_tasklet(unsigned long)
{
	  ……
}
 
// 中断服务程序
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{ 
      ……
      // 使系统在合适的时候调用驱动的下半部
      tasklet_schedule(&xxx_tasklet); 
      ……
}

int _init xxx_init(void)
{ 
      …… 
      // 申请中断,中断的上半部
      result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLED,”xxx”,NULL);  
      ……
      return IRQ_HANDLED;
}

void _exit xxx_exit(void)
{
      ……
      free_irq(xxx_irq,xxx_irq_interrupt);
      ……
}

工作队列机制

tasklet 机制在中断下半部执行的过程中,虽然是开中断的,期间可以处理各类中断,但是毕竟整个中断的处理还没有走完,这期间 APP 是无法运行的

如果中断要做的事情太过于耗时,就不能使用 tasket 机制(软件中断)来做,而应该用内核线程来做:在中断的上半部唤醒内核线程。这样内核线程和 APP 都一样竞争执行,APP 有机会执行,系统不会卡顿

上述的那个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:

image-20240602192915011

kworker 线程要去 “ 工作队列(work queue)”上取出一个一个 “ 工作(work)”,来执行它里面的函数

使用步骤:

  1. 创建 work:

    void my_func();
    DECLARE_WORK(my_work, my_func, &data); 
    // DECLARE_WORK(my_work, my_func);    // 第三个参数可以不要也可以写为空
    static DECLARE_WORK(aer_recover_work, aer_recover_work_func);
    // 在编译时创建函数,并将函数入口地址和参数地址赋给它
    

    如果不想要在编译时就用DECLARE_WORK ( ) 创建并初始化工作结构体变量,也可以在程序运行时再用INIT_WORK ( ) 创建

    void my_func();
    struct work_struct my_work;
    INIT_WORK(&my_work, my_func, &data); 
    
  2. 将 work 提交给 work queue

    schedule_work(&my_work); // 一般是中断的上半部调用
    

谁来调用 work:

不用我们管,schedule_work 函数不仅仅是把 work 放入队列,还会把 kworker 线程唤醒,此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数

使用模板:

struct work_struct xxx_wq;

// 中断下半部
void xxx_do_work(unsigned long) 
{
      ……
}
 
// 中断上半部
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
      …… 
      schedule_work(&xxx_wq); // 将 work 放入工作队列,并且唤醒内核的 kworker 线程
      …… 
}
 
int init(void)
{
      ……
      result = request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,“xxx”,NULL); 
      ……
      INIT_WORK(&xxx_wq,(void(*)(void*))xxx_do_work,NULL);
      …….
}
 
void xxx_exit(void)
{
      ……
      free_irq(xxx_irq,xxx_interrupt);
      ……
}
新技术:threaded_irq

image-20240602203438962

你可以只提供 thread_fn,系统会为这个函数创建一个内核线程,发生中断时,内核线程就会执行这个函数

之前的 work 都是由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着,但是在多个 CPU 的系统中,就会浪费资源

新技术 threaded_irq,为每一个中断创建一个内核线程,多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率

重要结构体

image-20240603095835751

irq_desc

image-20240603100113187

理解:

image-20240603100235670

外部中断1和外部中断n共享 GPIO 的B号中断,而多个 GPIO 又汇聚到 GIC 的A号中断,最后由GIC中断 CPU,那么软件的处理就是反过来的,先读取 GIC 得知A号中断,再细分出 GPIO 的B号中断,最后在判断出具体是哪一个外部设备

所以中断处理函数的来源有三:

  1. GIC 的处理函数(GIC:中断控制器)

    irq_desc[A].handle_irq 细分出中断 后B ,调用对应的 irq_desc[B].handle_irq

    显然中断 A 是 CPU 感受到的顶层的中断,GIC 中断 CPU 时,CPU 读取 GIC 状态得到中断 A

  2. GPIO 的处理函数

    比如对于GPIO模块向 GIC 发出的中断B ,它的处理函数是 irq_desc[B].handle_irq

    判断中断具体有哪一个外设产生

  3. 外部设备提供的处理函数

    判断设备是否发生了中断,如何处理中断

    对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数,所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数,一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行(并不判断是哪一个外部设备而是循环执行所有的处理函数,这些处理函数会自行判断自己是否产生了中断),这个链表就是 action 链表

image-20240603102029523

irq_action

image-20240603102122776

当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体,里面保存 name、dev_id 等,最重要的是 handler、thread_fn、thread

**handler:**中断处理的上半部

thread_fn: 对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程,在内核线程里,会调用 thread_fn 函数

dev_id:

  1. 中断处理函数执行时,可以使用

  2. 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项

    所以在共享中断中必须提供 dev_id,非共享中断可以不提供

irq_data

image-20240603103614731

中转站,里面有 irq_chip 指针、irq_domain 指针、都是指向别的结构体

irq:软件中断号

hwirq:硬件中断号

GPIO 中断B是软件中断号,GPIO里的第X号中断,这是硬件中断号

irq_domain 会把本地的 hwirq 映射为全局的 irq,比如GPIO控制器里有第1号中断,UART模块里也有第1号中断,这两个 “ 第1号中断 ”是不一样的,它们属于不同的 “ 域 ” —— irq_domain

irq_domain

image-20240603104547632

如何在设备树中指定中断,设备树的中断如何被转换为 irq 时,irq_domain 将会起到极大的作用

eg:

在设备树中会有这样的属性:

interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

它表示要使用 gpio1 里的第 5 号中断,hwirq 就是 5

但是我们在驱动中会使用 request_irq(irq,handler)这样的函数来注册中断,irq 是软件中断号

gpio1 对应的 irq_domain 会将 hwirq 转换为 irq

irq_domian 结构体中有一个 irq_domain_ops 结构体,里面有各种的操作函数

**xlate:**用来解析设备树的中断属性,提取出 hwirq、type 等信息

**map:**把 hwirq 转换为 irq

irq_chip

image-20240603105826107

我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断

我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数

但是对于外部设备相关的清中断操作,还是需要我们自己做的

就像上面图里的 “ 外部设备 1 “、“ 外部设备 n ”,外设备千变万化,内核里可没有对应的清除中断操作

设备树指定中断

概述

image-20240603110249327

在硬件上,“ 中断控制器 ” 只有 GIC 这一个,但是在软件上也可以把 “ GPIO ” 称为 “ 中断控制器 ”

在设备树中,中断控制器必须有一个属性:interrupt-controller,表明它是 “ 中断控制器 ”

还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个 cell

image-20240603144354783

如果中断控制器有级联关系 ,下级的中断控制器还需要表明它的 “ interrupt-parent ” 是谁

image-20240603144433586

interrupts 的第2个参数取值:(这个参数可以写为宏,也可以写为具体的值)

#define IRQ_TYPE_NONE           0
#define IRQ_TYPE_EDGE_RISING    1
#define IRQ_TYPE_EDGE_FALLING   2
#define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH     4
#define IRQ_TYPE_LEVEL_LOW      8

新写法:interrupts-extended

一个“interrupts-extended”属性就可以既指定 “ interrupt-parent ”,也指定 “ interrupts ”

interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;

示例

image-20240603151550216

在代码中获取中断

对于platform_device

这类节点可以转换为 platform_device 结构体

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type,
										unsigned int num);
// 第2个参数:获取资源的类型    IORESOURCE_IRQ:中断资源
// 第3个参数:这类资源中的哪一个

对于I2C、SPI设备

一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里

image-20240603152319275

一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里

image-20240603152403882

调用 of_irq_get 获得中断号

如果你的设备节点既不能转换为 platform_device,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号

对于GPIO

eg

gpio-keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    user {
        label = "User Button";
        gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
        gpio-key,wakeup;
        linux,code = <KEY_1>;
    };
};
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);

irq = gpiod_to_irq(bdata->gpiod);

按键中断驱动示例

设备树

gpio_keys_100ask {
    compatible = "100ask,gpio_key";
	gpios = <&gpio5 1 GPIO_ACTIVE_LOW
	&gpio4 14 GPIO_ACTIVE_LOW>;
		
    pinctrl-names = "default";
    pinctrl-0 = <&key1_100ask &key2_100ask>;
    };

image-20240603161822601

image-20240603161901910

驱动代码

/* 1. 从platform_device获得GPIO
 * 2. gpio=>irq
 * 3. request_irq
 */
static int gpio_key_probe(struct platform_device *pdev)
{
	int err;
	struct device_node *node = pdev->dev.of_node;
	int count;
	int i;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
		
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	count = of_gpio_count(node); // 计算 gpio 的引脚数目
	if (!count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	for (i = 0; i < count; i++)
	{
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag); // 获取 gpio 里的第i个引脚
		if (gpio_keys_100ask[i].gpio < 0)
		{
			printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
			return -1;
		}
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;

		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;

		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

		
		gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}

	for (i = 0; i < count; i++)
	{
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
	}
        
    return 0;
}

http://www.kler.cn/news/311094.html

相关文章:

  • 【Elasticsearch系列七】索引 crud
  • 【Java】网络编程-地址管理-IP协议后序-NAT机制-以太网MAC机制
  • 爬虫逆向学习(六):补环境过某数四代
  • C++初阶学习第六弹------标准库中的string类
  • 每日刷题(算法)
  • 开发一个电商API接口的步骤!!!
  • microchip中使用printf给AVR单片机串口重定向
  • Redis实现发布/订阅功能(实战篇)
  • uniapp中实现<text>文本内容点击可复制或拨打电话
  • tronado websocket
  • Java基础:Api 文档注释,字符串种类,String字符串创建,特点及常用方法
  • 【洛谷】P1546 [USACO3.1] 最短网络 Agri-Net 的题解
  • SqlServer自定义类型的使用
  • 【数据结构-一维差分】力扣1893. 检查是否区域内所有整数都被覆盖
  • 无人机滑环的核心特点及其应用分析
  • [论文笔记] LLM端侧小模型篇——1、剪枝量化的latency
  • MySQl篇(基本介绍)(持续更新迭代)
  • Leetcode—删除有序数组的重复项
  • 408算法题leetcode--第七天
  • Llama 3.1 大模型指令微调提升中文能力
  • 【系统架构设计师-2019年真题】案例分析-答案及详解
  • Scikit-learn 学习笔记
  • 尚品汇-秒杀商品存入缓存、Redis发布订阅实现状态位(五十一)
  • 全球首个!复旦大学冯建峰团队开发数字孪生脑平台,具备 860 亿神经元规模
  • 旷视轻量化网络shufflenet算法解读
  • MySQL——数据库的高级操作(二)用户管理(3)删除普通用户
  • 机器学习1--概述
  • Linux创建虚拟磁盘并分区格式化
  • 「Netmarble 小镇」活动来了:踏上穿越标志性世界的旅程!
  • OpenHarmony鸿蒙( Beta5.0)智能门铃开发实践