天天看点

Linux 中断子系统

文章目录

  • ​​中断子系统​​
  • ​​一、中断执行流程​​
  • ​​1、ARM中断​​
  • ​​2、Linux中断​​
  • ​​2.1、中断号​​
  • ​​2.2、irq_desc​​
  • ​​2.3、irq_domain​​
  • ​​2.4、irqaction​​
  • ​​二、中断配置​​

中断子系统

linux的中断子系统,可以分为两个部分:

  • 中断处理流程:中断函数的设置、执行流程
  • 中断配置:中断开关、触发类型等的设置

linux使用中断描述符来表示一个具体的外设中断,irq_desc,这个中断描述符既包含中断处理流程所需的数据,也包含中断配置的数据。

//一个虚拟中断号对应一个irq_desc
struct irq_desc {
  struct irq_data    irq_data;  //中断配置
  unsigned int __percpu  *kstat_irqs;  //中断状态
  irq_flow_handler_t  handle_irq;  //高级中断处理
  struct irqaction  *action;  /* 具体的业务的中断处理函数,即中断处理流程 */
  unsigned int    status_use_accessors;
  unsigned int    core_internal_state__do_not_mess_with_it;
  unsigned int    depth;    /* nested irq disables */
  unsigned int    wake_depth;  /* nested wake enables */
  unsigned int    irq_count;  /* For detecting broken IRQs */
  unsigned long    last_unhandled;  /* Aging timer for unhandled count */
  unsigned int    irqs_unhandled;
  atomic_t    threads_handled;
  int      threads_handled_last;
  raw_spinlock_t    lock;
  struct cpumask    *percpu_enabled;
  int      parent_irq;
  struct module    *owner;
  const char    *name;
} ____cacheline_internodealigned_in_smp;      

其中的irq_data 负责 实现中断的配置,irqaction 包含外设的中断处理程序。下面就从这两个部分分析中断子系统。

一、中断执行流程

1、ARM中断

中断信号一般由这样的过程构成:

  1. 中断源,比如按键
  2. 外设控制器,比如GPIO控制器
  3. GIC,通用中断控制器
  4. CPU

而软件上的中断需要经过这样的处理:

  1. CPU产生IRQ中断,跳转到IRQ_Handler
  2. IRQ_Handler中读取GIC状态寄存器,判断是哪个具体的外设产生的中断,跳转到该外设的handler
  3. 某外设的handler:读取该外设控制器的寄存器,判断是否产生中断,并进行处理

软件的中断处理就是硬件中断信号的逆过程。软件需要通过一层层地读取中断控制器的寄存器来确定具体的中断源。

2、Linux中断

首先这里补充一个中断号是什么。

2.1、中断号

假设有一个GIC,他能处理120个中断,那么他就会把中断编号成0-119,假设系统有另一个相同的GIC,他也有一样的中断编号0-119,但是所对应的硬件显然是不一样的。这对于软件开发而言是很麻烦的,我们需要去区别这个中断编号是哪个GIC下的。

对于软件开发来说,最理想的情况就是,一个中断源对应一个中断号,一个中断号有一个中断处理函数。而实际的硬件中断编号是无法满足我们的要求的。

2.2、irq_desc

linux为了开发方便,定义了虚拟的中断号,一般写成irq,并且用irq_desc结构体来表示这一个虚拟中断号所对应的外设中断。

当我们需要使用一个外设的中断时候,就需要在linux中为他分配一个虚拟中断号。通过创建irq_desc,我们就能获取到虚拟中断号。

/*
 申请描述符
 irq:一般为-1,不指定
 from:从from开始查找符合条件的irq_desc
 cnt:申请的desc的数量
*/
int __irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node,struct module *owner);

//申请描述符并进行映射
static inline unsigned int irq_of_parse_and_map(struct device_node *dev,int index)      

申请完成虚拟中断号后,还需要能把hw_irq与irq一一对应起来,如何做到?

2.3、irq_domain

linux提出了domain的结构体来应对这个情况。domain的作用就是把硬件中断号翻译成一个虚拟的中断号。一般来说,一个中断控制器就拥有一个domain,在GIC1的domain1下,有0-119中断号,在GIC2的domain2下,有0-119中断号.domain1和domain2把这些hw_irq翻译成唯一的irq,这样linux就只需要关注irq。

我们开发了一个有中断控制器功能的外设,其一般就会有若干个hw_irq,我们需要提供一个irq_domain,将hw_irq翻译成irq。

irq_domain 负责将硬件中断号翻译成虚拟中断号,具体的实现方法由irq_domain_ops 中的map函数实现。

//将硬件中断号翻译成虚拟中断号
struct irq_domain {
  struct list_head link;  //全局irq_domain
  const char *name;
  const struct irq_domain_ops *ops;  //irq_domain的方法
  void *host_data;  //驱动开发者的私有数据
  unsigned int flags;
};      

irq_domain_ops :不一定要全部实现

struct irq_domain_ops {
  int (*match)(struct irq_domain *d, struct device_node *node);  //匹配一个中断控制器到irq_domain
  int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);  //创建或更新硬件中断号和虚拟中断号的映射
  void (*unmap)(struct irq_domain *d, unsigned int virq);  //取消映射
  //解码出设备树中的硬件中断号和中断类型
  int (*xlate)(struct irq_domain *d, struct device_node *node,  
         const u32 *intspec, unsigned int intsize,
         unsigned long *out_hwirq, unsigned int *out_type);
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
  /* extended V2 interfaces to support hierarchy irq_domains */
  int (*alloc)(struct irq_domain *d, unsigned int virq,
         unsigned int nr_irqs, void *arg);
  void (*free)(struct irq_domain *d, unsigned int virq,
         unsigned int nr_irqs);
  void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
  void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
#endif
};

//读取interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>; 的前两位
int irq_domain_xlate_onetwocell(struct irq_domain *d,
        struct device_node *ctrlr,
        const u32 *intspec, unsigned int intsize,
        unsigned long *out_hwirq, unsigned int *out_type)
{
  if (WARN_ON(intsize < 1))
    return -EINVAL;
  *out_hwirq = intspec[0];  //读取硬件中断号
  *out_type = (intsize > 1) ? intspec[1] : IRQ_TYPE_NONE;  //读取中断类型
  return 0;
}      

完成后使用 irq_domain_add_legacy() 建立domain并添加到系统中。该函数中,first_hwirq是我们从设备树获取的,first_irq是虚拟中断号,需要向linux申请,有多少个hw_irq就申请多少个irq_number。

struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
           unsigned int size,   //domain含有的中断号数量
           unsigned int first_irq,  //first 虚拟中断号
           irq_hw_number_t first_hwirq, 
           const struct irq_domain_ops *ops,
           void *host_data);      

一般来说,一个irq_number对应一个irq_desc,所以我们申请irq_desc的时候,就会返回对应的irq_number,这些irq_number就能用来加入domain.

/*
 irq:一般是-1,即动态分配
 from:Start the search from this irq number 从from开始搜索空闲的描述符
 cnt:Number of consecutive irqs to allocate 描述符数量
*/
irq_alloc_descs(irq, from, cnt, node)      
2.4、irqaction

中断函数究竟是如何执行的?首先从request_irq()函数分析:

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
       irq_handler_t thread_fn, unsigned long irqflags,
       const char *devname, void *dev_id)
{
  struct irqaction *action;
  struct irq_desc *desc;
  int retval;
  //检查
  if (((irqflags & IRQF_SHARED) && !dev_id) ||
      (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
      ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
    return -EINVAL;
  //获取对应描述符
  desc = irq_to_desc(irq);
  //检查
  if (!irq_settings_can_request(desc) ||
      WARN_ON(irq_settings_is_per_cpu_devid(desc)))
    return -EINVAL;
  //设置默认handler
  if (!handler) {
    if (!thread_fn)
      return -EINVAL;
    handler = irq_default_primary_handler;
  }
  //申请action
  action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
  if (!action)
    return -ENOMEM;
  //设置action成员
  action->handler = handler;
  action->thread_fn = thread_fn;
  action->flags = irqflags;
  action->name = devname;
  action->dev_id = dev_id;

  chip_bus_lock(desc);
  //__setup_irq :注册一个irqaction,添加到irq_desc
  retval = __setup_irq(irq, desc, action);
  chip_bus_sync_unlock(desc);
  return retval;
}
EXPORT_SYMBOL(request_threaded_irq);      

可以看到,该函数就是创建一个irqaction对象并添加到irq_desc中,而且中断服务函数、参数等信息都被保存到irqaction中。

struct irqaction {
  irq_handler_t    handler;  //对应中断上半部
  void      *dev_id;  //用于共享中断中,判断设备驱动
  void __percpu    *percpu_dev_id;
  struct irqaction  *next;  //当是共享中断时,irqaction是一个链表,对应多个驱动中断
  irq_handler_t    thread_fn;  //中断线程化的线程处理函数
  struct task_struct  *thread;  //中断线程化的线程
  unsigned int    irq;  //虚拟中断号
  unsigned int    flags;  //request_irq 传入的flag
  unsigned long    thread_flags;
  unsigned long    thread_mask;
  const char    *name;
  struct proc_dir_entry  *dir;
} ____cacheline_internodealigned_in_smp;      

可以推测在中断产生时,该irqaction就会被调用。而如何找到irqaction然后执行呢?

大概是这样的:中断产生后,中断服务函数会读取产生中断的hw_irq,并通过个domain,把hw_irq翻译成irq_number,通过 ​

​irq_to_desc()​

​ 获取到irq_number对应的irq_desc,执行irq_desc中的irqaction。

二、中断配置

中断的配置本质上就是去读写中断相关的寄存器,linux中把拥有中断功能的外设控制器,抽象出来一个中断芯片:irq_chip。irq_chip结构体包含中断相关的寄存器,以及中断开关、应答、模式选择等函数。

这些函数不是要全部实现,根据硬件支持来实现。

struct irq_chip {
    const char    *name;
    //中断使能开关
    unsigned int    (*irq_startup)(struct irq_data *data);
    void        (*irq_shutdown)(struct irq_data *data);
    void        (*irq_enable)(struct irq_data *data);
    void        (*irq_disable)(struct irq_data *data);
    
    void        (*irq_ack)(struct irq_data *data);  //应答中断
    void        (*irq_mask)(struct irq_data *data);  //mask一个外设中断,masked表示使能
    void        (*irq_mask_ack)(struct irq_data *data);
    void        (*irq_unmask)(struct irq_data *data);
    void        (*irq_eoi)(struct irq_data *data);
    
    int        (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
    int        (*irq_retrigger)(struct irq_data *data);
    int        (*irq_set_type)(struct irq_data *data, unsigned int flow_type);  //设置中断类型
    int        (*irq_set_wake)(struct irq_data *data, unsigned int on);  //设置中断唤醒
    ......

    unsigned long    flags;
};      

可以发现,这些函数的第一个参数都是 irq_data ,这个结构体用于描述 中断的配置信息:

struct irq_data {
  u32      mask;  //某一个中断的掩码
  unsigned int    irq;  //虚拟中断号
  unsigned long    hwirq;  //硬件中断号
  unsigned int    node;
  unsigned int    state_use_accessors;
  struct irq_chip    *chip;  //对应的中断控制器芯片
  struct irq_domain  *domain;  //中断控制器对应的域
  void      *handler_data;
  void      *chip_data;
  struct msi_desc    *msi_desc;
  cpumask_var_t    affinity;
};      

主要是mask成员,例如 当需要打开某个中断时,就将该中断的mask置1,然后传递给irq_mask(),例如:

//irq_mask = irq_gc_mask_clr_bit,
void irq_gc_mask_clr_bit(struct irq_data *d)
{
  struct irq_chip_generic *gc = irq_data_get_irq_chip_data(d);
  struct irq_chip_type *ct = irq_data_get_chip_type(d);
  u32 mask = d->mask;  //获取掩码

  irq_gc_lock(gc);
  *ct->mask_cache &= ~mask;  //将寄存器上对应的bit清零
  irq_reg_writel(gc, *ct->mask_cache, ct->regs.mask);  //写入硬件寄存器
  irq_gc_unlock(gc);
}      

假如我们要给一个具有中断功能的外设开发中断的功能,就需要实现上述的irq_chip和irq_data。

linux提供了 irq_chip_generic 来更加完整的描述一个中断控制器,驱动开发者要实现的就是这个结构体:

主要需要填充 寄存器地址、chip_types这两个成员。

//描述一个硬件的中断控制器
struct irq_chip_generic {
  raw_spinlock_t    lock;
  void __iomem    *reg_base;  //中断控制器基地址
  u32      (*reg_readl)(void __iomem *addr);  //读写寄存器的函数(defaults to readl if NULL)
  void      (*reg_writel)(u32 val, void __iomem *addr);
  unsigned int    irq_base;  //中断控制器硬件中断号起始
  unsigned int    irq_cnt;  //硬件中断号数量
  u32      mask_cache;  //mask register 的缓存
  u32      type_cache;  //type register 的缓存
  u32      polarity_cache;  //polarity register 的缓存
  u32      wake_enabled;
  u32      wake_active;
  unsigned int    num_ct;  //chip_types的大小
  void      *private;
  unsigned long    installed;
  unsigned long    unused;
  struct irq_domain  *domain;  //中断控制器对应的domain
  struct list_head  list;  //连接到全局的 irq_chip_generic 链表
  struct irq_chip_type  chip_types[0];  //类似于irq_chip,用于操作中断控制器的寄存器
};

//中断流控
struct irq_chip_type {
  struct irq_chip    chip;  //提供操作中断寄存器的方法
  struct irq_chip_regs  regs;  //中断寄存器的偏移
  irq_flow_handler_t  handler;  //handle_level_irq - Level type irq handler
  u32      type;
  u32      mask_cache_priv;
  u32      *mask_cache;
};
//中断控制器的寄存器偏移
struct irq_chip_regs {
  unsigned long    enable;
  unsigned long    disable;
  unsigned long    mask;
  unsigned long    ack;
  unsigned long    eoi;
  unsigned long    type;
  unsigned long    polarity;
};      

所需要调用的接口函数有:

分配一个irq_chip_generic 结构体,设置一些成员:

struct irq_chip_generic *
irq_alloc_generic_chip(const char *name, int nr_ct, unsigned int irq_base,
           void __iomem *reg_base, irq_flow_handler_t handler);      
void irq_setup_generic_chip(struct irq_chip_generic *gc, u32 msk,
          enum irq_gc_flags flags, unsigned int clr,
          unsigned int set);      

继续阅读