Zephyr RTOS -- Interrupts

Posted 搬砖-工人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr RTOS -- Interrupts相关的知识,希望对你有一定的参考价值。

文章目录

本笔记基于 Zephyr 版本 2.6.0-rc2

 

前言

本人正在学习 Zephyr,一个可移植性较强,可以兼容多种开发板及物联网设备的操作系统,如果你感兴趣,可以点此查看我的 学习笔记总述 进行了解!

 

Interrupts - (中断)

中断服务程序 (ISR) 是响应硬件或软件中断而异步执行的功能。 通常,ISR 会抢先执行当前线程,从而以非常低的开销进行响应。 只有在完成所有 ISR 工作后,线程执行才会恢复。

 

1. Concepts - (概念)

可以定义任意数量的 ISR(仅受可用 RAM 的限制),受底层硬件的约束。

中断 (ISR) 具有以下特性:

  • 触发 ISR 的中断请求信号 (interrupt request (IRQ) signal)
  • 与 ISR 关联的 优先级
  • 用来处理中断的中断回调函数(interrupt handler function)
  • 传递给回调函数的参数值(argument value)

中断描述表 (IDT) 或者向量表用于将给定的中断源和 ISR 相关联。在任意给定的时间只有 1 个 ISR 可以与特定的 IRQ 关联。

多个 ISR 可以使用同样的函数来处理中断,允许一个功能服务一个产生多种中断类型的设备或服务多个设备(通常是同一类型的)。使用传递给 ISR 的函数的参数来确定是哪个中断被置位了。

内核为所有没使用的 IDT 条目提供了默认的 ISR。如果未知的中断置位了,ISR 会产生严重的系统错误。

内核支持中断嵌套,如果有更高优先级的 ISR 置位了,当前被执行的中断会被抢占,当高优先级的 ISR 处理完毕之后,低优先级的 ISR 才恢复执行。

ISR 的中断回调函数在内核的中断上下文中执行。这个上下文拥有其独立的栈区域 (或在某些体系结构上为栈区域)。如果支持中断嵌套,则中断上下文的栈大小必须能够处理多个 ISR 并发的执行。

Note:
许多内核 API 只能由线程使用,而不能由 ISR 使用。 在线程和 ISR 都可以调用的情况下,内核会提供 k_is_in_isr() API,确定代码是否在中断级别运行。这个例程允许调用者根据它是线程还是 ISR 定制它的操作。

 

1.1 Multi-level Interrupt handling - (多级中断处理)

通过使用一个或多个嵌套的中断控制器,硬件平台可以比本地提供的中断线支持更多的中断线。 硬件中断源组合成一行,然后路由到父控制器。

如果中断嵌套控制器被使能了,CONFIG_MULTI_LEVEL_INTERRUPTS 应该被置 1,CONFIG_2ND_LEVEL_INTERRUPTSCONFIG_3RD_LEVEL_INTERRUPTS 同样需要根据硬件结构设置好。

一个唯一的 32bit 中断号码被分配,其中嵌入了信息,用于选择和调用正确的 ISR。每个中断等级都被给予 1 个 32bit 的数字,使用这个架构最多支持 4 个中断级别,如下图所示和解释:

          9             2   0
    _ _ _ _ _ _ _ _ _ _ _ _ _         (LEVEL 1)
  5       |         A   |
_ _ _ _ _ _ _         _ _ _ _ _ _ _   (LEVEL 2)
  |   C                       B
_ _ _ _ _ _ _                         (LEVEL 3)
        D

这展示了 3 个中断等级。

  • ‘-’ 表示中断线(line),从 0(最右边) 开始编号。
  • LEVEL1 有 12 个中断 line,其中 2 个 line (2和9) 连接到嵌套控制器,并且 1 个设备 “A” 在 line4 上。
  • LEVEL2 的 1 个控制器上,中断 line5 连接到 LEVEL3 嵌套控制器,并且 1 个设备 “C” 在 line3 上。
  • 剩余的 LEVEL2 控制器没有嵌套,还只有 1 个设备 “B” 在 line2 上。
  • LEVEL3 的控制器有 1 个设备 “D” 在 line2 上。

上面的 4 个设备 (A,B,C,D) 中断号是怎么生成的呢?

LEVEL 2 和之后的位位置偏移 1,因为 0 意味着该级别不存在中断号。例如,LEVEL3 的控制器有设备 “D” 在 line2 上,连接到 LEVEL2 控制器的 line5 上,又连接到 LEVEL1 控制器的 line9 上 (2->5->9)。因为 LEVEL2 和前面 LEVEL 的编码偏移,所以设备 “D” 分配的号码是 0x00030609。(这个地方可以理解为,当需要向前面一级的 LEVEL 去寻找嵌套链路,如果前一级存在,就需要在本级别的 line 数上加 1。)

 

1.2 Preventing Interruptions - (阻止中断)

在某些情况下,当前的线程可能有必要在执行时间敏感或关键部分操作时阻止 ISR 执行。

这种情况下可以使用 IRQ 锁 临时阻止系统中的所有 IRQ 处理。即使该锁已经生效了,也依然可以应用该锁,所以程序可以在不需要知道它是否已经生效的时候使用它。在线程运行时,中断可以被内核再次处理前,线程必须解锁其 IRQ 锁,并且解锁的次数要与锁定的次数相同。

Important:
IRQ 锁是线程特定的。如果线程 A 锁定中断,然后执行使自身进入睡眠状态的操作 (例如,睡眠 N 毫秒),一旦线程 A 被换出并且下一个就绪线程 B 开始运行,则线程的 IRQ 锁不再适用。

这意味着可以在线程 B 运行时处理中断,除非线程 B 也使用自己的 IRQ 锁锁定了中断。

当线程 A 最终再次成为当前线程时,内核会重新建立线程 A 的 IRQ 锁。这确保线程 A 在解锁其 IRQ 锁之前不会被中断。

如果线程 A 没有休眠但确实使更高优先级的线程 B 准备就绪,则 IRQ 锁将禁止任何否则会发生的抢占。线程 B 将不会运行,直到在释放 IRQ 锁后到达下一个重新调度点(reschedule points)

或者,线程可以临时禁用指定的 IRQ,因此当该 IRQ 被发送信号时,其关联的 ISR 不会执行。随后必须启用 IRQ 以允许执行 ISR。

禁用 IRQ 可以防止系统中的所有线程被相关的 ISR 抢占,而不仅仅是禁用 IRQ 的线程。

Zero Latency Interrupts - (零延时中断)

通过应用 IRQ 锁来防止中断可能会增加观察到的中断等待时间。 但是,对于某些低延迟的用例,中断延迟高可能是不可接受的。

内核通过允许具有关键延迟约束的中断 (设置无法被中断锁阻止的优先级) 执行来解决此类用例。这些中断被定义 为零延迟中断。需要 CONFIG_ZERO_LATENCY_IRQS 启用对零延迟中断的 支持。除此之外,IRQ_ZERO_LATENCY 必须将该标志传递给 IRQ_CONNECTIRQ_DIRECT_CONNECT 宏以配置具有零延迟的特定中断。

零延迟中断被期望用于直接管理硬件事件,而不是与内核代码进行互操作。他们应该将所有内核 API 视为未定义的行为(例如,在零延迟中断上下文中使用 API 的应用程序负责直接验证正确的行为)。零延迟中断可能不会修改从普通 Zephyr 上下文调用的内核 API 所检查的任何数据,也不会产生需要同步处理的异常(例如内核恐慌)。

 

1.3 Offloading ISR Work - (转移/分配中断工作)

ISR 应快速执行以确保可预测的系统操作。如果需要耗时的处理,ISR 应该将部分或全部处理转移(Offload)到一个线程,从而恢复内核响应其他中断的能力。

内核支持几种将中断相关的处理转移到线程的机制。

  • ISR 可以使用一个内核对象 (如FIFO、LIFO或信号量) 来通知辅助线程进行与中断相关的处理。
  • ISR可以指示系统工作队列线程执行一个工作项。(参见 Workqueue Threads)。

当 ISR 将工作转移给一个线程时,当 ISR 完成时,通常会有一个到该线程的上下文切换,允许中断相关的处理立即处理。然而,根据处理转移的线程的优先级,当前正在执行的协作线程或其他高优先级线程可能会在调度处理转移的线程之前执行。

 

2. Implementation - (实现)

2.1 Defining a regular ISR - (定义一个常规中断)

ISR 在运行时通过调用 IRQ_CONNECT 来定义。然后必须通过调用 irq_enable() 启用它。

Important:
IRQ_CONNECT() 不是一个 C 函数,它在后台执行一些内联汇编功能。它的所有参数必须在构建时已知。有多个实例的驱动程序可能需要定义每个实例的配置函数来配置中断的每个实例。

以下示例代码定义并使能一个 ISR:

#define MY_DEV_IRQ  24       /* device uses IRQ 24 */
#define MY_DEV_PRIO  2       /* device uses interrupt priority 2 */
/* argument passed to my_isr(), in this case a pointer to the device */
#define MY_ISR_ARG  DEVICE_GET(my_device)
#define MY_IRQ_FLAGS 0       /* IRQ flags */

void my_isr(void *arg)

   ... /* ISR code */


void my_isr_installer(void)

   ...
   IRQ_CONNECT(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_ISR_ARG, MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...

由于IRQ_CONNECT 宏要求在构建时知道其所有参数,因此在某些情况下这可能是不可接受的。也可以在运行时使用 irq_connect_dynamic() 安装中断。它的使用方式与 IRQ_CONNECT 完全相同:

void my_isr_installer(void)

   ...
   irq_connect_dynamic(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_ISR_ARG,
                       MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...

动态中断需要启用 CONFIG_DYNAMIC_INTERRUPTS 选项。当前不支持删除或重新配置动态中断。

 

2.2 Defining a ‘direct’ ISR - (定义一个‘direct’中断)

常规的 Zephyr 中断会带来一些开销,这对于一些低延迟的用例来说可能是不可接受的。具体地说:

  • ISR 的参数被检索和传递给 ISR。
  • 如果启用电源管理并且系统处于空闲状态,则在执行 ISR 之前所有硬件将从低功耗状态恢复,这可能非常耗时
  • 虽然有些架构会在硬件中做到这一点,但其他架构需要在代码中切换到中断堆栈
  • 在中断得到服务后,操作系统会执行一些逻辑来潜在地做出调度决策。

Zephyr 支持所谓的 “direct” 中断,通过 IRQ_DIRECT_CONNECT 安装。这些直接中断有一些特殊的实现需求和一个简化的特性集;详细信息请参见 IRQ_DIRECT_CONNECT 的定义。

下面的代码演示了一个 “direct” ISR:

#define MY_DEV_IRQ  24       /* device uses IRQ 24 */
#define MY_DEV_PRIO  2       /* device uses interrupt priority 2 */
/* argument passed to my_isr(), in this case a pointer to the device */
#define MY_IRQ_FLAGS 0       /* IRQ flags */

ISR_DIRECT_DECLARE(my_isr)

   do_stuff();
   ISR_DIRECT_PM(); /* PM done after servicing interrupt for best latency */
   return 1; /* We should check if scheduling decision should be made */


void my_isr_installer(void)

   ...
   IRQ_DIRECT_CONNECT(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...

 

2.3 Implementation Details - (实施细则)

中断表在构建的时候使用一些特殊的构建工具建立。详细的放在下面,它应用于除了 x86 之外的全部架构。

任何调用 IRQ_CONNECT 都会声明一个 struct _isr_list 的实例,它被放置在一个特殊的 .intList 部分:

struct _isr_list 
    /** IRQ line number */
    int32_t irq;
    /** Flags for this IRQ, see ISR_FLAG_* definitions */
    int32_t flags;
    /** ISR to call */
    void *func;
    /** Parameter for non-direct IRQs */
    void *param;
;

Zephyr 分两个阶段构建;构建的第一阶段生成 $ZEPHYR_PREBUILT_EXECUTABLE.elf,其中包含 .intList 部分中的所有条目,并开头以:

struct 
    void *spurious_irq_handler;
    void *sw_irq_handler;
    uint32_t num_isrs;
    uint32_t num_vectors;
    struct _isr_list isrs[];  <- of size num_isrs
;

这个数据由 $ZEPHYR_PREBUILT_EXECUTABLE.elf 中的 struct _isr_list 的头文件和实例组成。然后被 gen_isr_tables.py 脚本用来生成一个 C 文件,它定义了一个向量表和软件 ISR 表,然后编译并链接到最终的应用程序中。

任何中断的优先级级别都不会被编码在这些表中,相反 IRQ_CONNECT 也有一个运行时组件,它为中断控制器编写所需的中断优先级级别。有些体系结构不支持中断优先级的概念,在这种情况下,priority 参数将被忽略。

 

2.3.1 Vector Table - (向量表)

启用 CONFIG_GEN_IRQ_VECTOR_TABLE 时会生成向量表。这个数据结构由 CPU 本地使用,它只是一个函数指针数组,其中每个元素 n 对应于 IRQ 行 n 的 IRQ 处理程序,函数指针是:

  • 对于用 IRQ_DIRECT_CONNECT声明的 “direct” 中断,处理函数将放置在这里。
  • 对于用 IRQ_CONNECT 声明的常规中断,通用软件 IRQ 处理程序的地址放在这里。这段代码执行普通内核中断记录,并从软件 ISR 表中查找 ISR 和参数。
  • 对于根本没有配置的中断线,虚假 IRQ 处理程序的地址将放置在这里。如果遇到虚假 IRQ 处理程序,则会导致系统致命错误。

有些体系结构 (比如 Nios II 内部中断控制器) 对所有中断都有一个公共入口点,并且不支持向量表,在这种情况下,应该禁用 CONFIG_GEN_IRQ_VECTOR_TABLE 选项。

有些体系结构可能会为系统异常保留一些初始向量,并在其他表中声明这些向量,在这种情况下,需要设置CONFIG_GEN_IRQ_START_VECTOR 以适当地偏移表中的索引。

 

2.3.2 SW ISR Table - (软件中断表)

这是一个 struct _isr_table_entry 的数组:

struct _isr_table_entry 
    void *arg;
    void (*isr)(void *);
;

通用软件 IRQ 处理程序使用它来查找 ISR 及其参数并执行它。在中断控制器寄存器中查找活跃的 IRQ 线并用于索引该表。

 

3. Suggested Uses - (建议用途)

使用 regular 的或 direct 的 ISR 来执行需要快速响应的中断处理,并且可以在不阻塞的情况下快速完成。

Note:
耗费时间或涉及阻塞的中断处理应该交给一个线程。请参阅 Offloading ISR Work,了解可以在应用程序中使用的各种技术的描述。

 

4. Configuration Options - (配置选项)

相关配置选项:

还存在其他特定于体系结构和特定于设备的配置选项。

 

参考链接

https://docs.zephyrproject.org/latest/reference/kernel/other/interrupts.html#

以上是关于Zephyr RTOS -- Interrupts的主要内容,如果未能解决你的问题,请参考以下文章

Zephyr RTOS -- Stacks

Zephyr RTOS -- Stacks

Zephyr RTOS -- Stacks

Zephyr RTOS -- Polling API

Zephyr RTOS -- Polling API

Zephyr RTOS -- Message Queues