Zephyr中断系统-实现

Creative Commons
本作品采用知识共享署名

本文简要介绍Zephyr中断系统是如何实现的。

中断的系统实现和芯片架构紧密相关,分析一个中断系统离不开实际的架构,本文以cortex-m7系列为例分析说明Zephyr如何实现中断系统。

基础概念

对于cortex-m7内核的芯片,当中断发生时会自动从中断向量表中load中断向量到pc中开始执行,中断向量表放置的地方有架构决定,对于cortex-m7的芯片系统上电时默认中断向量表在image最开始的地方,也可以通过relocate重新设置中断向量表的位置,cortex-m7的中断向量表如下图:
vector
其中exception number 1~15是cortex-m7内核的中断向量,16开始的由芯片设计者自行绑定对应外设,例如nxp rt1052就将exception number 20(IRQ4)和UART1绑定,如果我们定义了一个函数uart1_isr, 将其地址放入到IRQ4中,那么当UART1产生中断时,就会执行uart1_isr。

Zephyr中断向量表

下图示例了Zephyr中断向量表的管理:
zephyrint
Zephyr维护硬、软两张中断向量表,_irq_vector_table为硬中断向量表,也就是基础概念中提到的中断向量表,_irq_vector_table的起始地址和exception 16(IRQ0)对齐。通过IRQ_DIRECT_CONNECT安装的ISR会直接保存在该向量表中,当中断发生时直接执行中断向量表中保存的ISR函数。通过IRQ_CONNECT/irq_connect_dynamic安装的ISR会保存在软件中断向量表_sw_isr_table中,当中断发生时,会执行硬中断向量表中默认的_isr_wapper在软中断向量表_sw_isr_table中进行查表,查到对应的ISR函数并执行。如果某个IRQ没有安装任何ISR,硬中断向量表中默认放置_isr_wapper,软中断表中默认z_irq_spurious,当中断发生时会查表到z_irq_spurious执行,在z_irq_spurious进入fault异常。

普通ISR

普通ISR可以分为编译期注册和运行期注册,普通的ISR函数都是放入软件中断表

软件中断表

软件中断表是名为_sw_isr_table的struct _isr_table_entry的结构体数组.
数组中每一条都保存ISR的函数入口和参数

1
2
3
4
struct _isr_table_entry {
void *arg;
void (*isr)(void *);
};

在这里我们先不介绍_sw_isr_table数组的定义位置,因为Zephyr特殊的两段链接过程,为避免混淆后面专门来分析_sw_isr_table。

运行期注册

运行期间,使用irq_connect_dynamic进行注册ISR
include/irq.h

1
2
3
4
5
6
7
8
static inline int
irq_connect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(void *parameter), void *parameter,
u32_t flags)
{
return arch_irq_connect_dynamic(irq, priority, routine, parameter,
flags);
}

不同的架构会有不同的arch_irq_connect_dynamic实现,cortex-m7 32bit系列内核的实现

1
2
3
4
5
6
7
8
int arch_irq_connect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(void *parameter), void *parameter,
u32_t flags)
{
z_isr_install(irq, routine, parameter); //将irq写到中断向量表
z_arm_irq_priority_set(irq, priority, flags); //设置中断优先级
return irq;
}

从下面的代码可以看到动态注册就是改变_sw_isr_table对应条目中保存的ISR函数如何和参数

1
2
3
4
5
6
7
8
9
10
11
12
void z_isr_install(unsigned int irq, void (*routine)(void *), void *param)
{
unsigned int table_idx = irq - CONFIG_GEN_IRQ_START_VECTOR;

__ASSERT(!irq_is_enabled(irq), "IRQ %d is enabled", irq);

/* If dynamic IRQs are enabled, then the _sw_isr_table is in RAM and
* can be modified
*/
_sw_isr_table[table_idx].arg = param;
_sw_isr_table[table_idx].isr = routine;
}

编译期注册

使用宏IRQ_CONNECT进行编译期ISR注册,当所有的参数都已经决定时,采用编译IRQ_CONNECT,在Zephyr中断系统–使用一文中已经说明了IRQ_CONNECT的参数,这里就不再展开说明了,先看一下IRQ_CONNECT如何实现
include/irq.h

1
2
#define IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p)

不同的架构会有不同的ARCH_IRQ_CONNECT实现,cortex-m7 32bit系列内核的实现如下
include/arch/arm/aarch32/irq.h

1
2
3
4
5
6
#define ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
({ \
Z_ISR_DECLARE(irq_p, 0, isr_p, isr_param_p); \
z_arm_irq_priority_set(irq_p, priority_p, flags_p); \
irq_p; \
})

以上三局分别是生成中断向量表内容,设置中断向量优先级,和返回中断号,我们关注的是生成中断向量表内容,展开Z_ISR_DECLARE

1
2
3
4
#define Z_ISR_DECLARE(irq, flags, func, param) \
static Z_DECL_ALIGN(struct _isr_list) Z_GENERIC_SECTION(.intList) \
__used _MK_ISR_NAME(func, __COUNTER__) = \
{irq, flags, &func, (void *)param}

上面的代码就是生成一个struct _isr_list结构体变量放入.intList段中,结构体变量中保持了中断号irq, 标志flags, 中断函数func和传入中断函数的参数param

1
2
3
4
5
6
7
8
9
10
struct _isr_list {
/** IRQ line number */
s32_t irq;
/** Flags for this IRQ, see ISR_FLAG_* definitions */
s32_t flags;
/** ISR to call */
void *func;
/** Parameter for non-direct IRQs */
void *param;
};

如果传入的fun是uart_isr, 调用IRQ_CONNECT的次数是第5次,那么IRQ_CONNECT最终内容展开就是

1
2
3
4
5
6
7
static  __aligned(__alignof(struct _ist_list))struct _ist_list  __attribute__((section(".intList")))
__used __isr_uart_isr_irq_5 = {
irq,
0,
uart_isr,
param
}

结构体变量__isr_uart_isr_irq_5将被放入.initList。我们这里先记住IRQ_CONNECT注册的信息是保存在中断.initList Section中,后面有专门一节介绍如何initList如何进入软件中断向量表

直接ISR

声明

和普通ISR不一样,直接ISR的函数需要先声明
include/irq.h

1
2
3
4
#define ISR_DIRECT_DECLARE(name) ARCH_ISR_DIRECT_DECLARE(name)
#define ISR_DIRECT_HEADER() ARCH_ISR_DIRECT_HEADER()
#define ISR_DIRECT_FOOTER(check_reschedule) \
ARCH_ISR_DIRECT_FOOTER(check_reschedule)

cortex-m7 32bit系列内核的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#define ARCH_ISR_DIRECT_HEADER() arch_isr_direct_header()
#define ARCH_ISR_DIRECT_FOOTER(swap) arch_isr_direct_footer(swap)

#define ARCH_ISR_DIRECT_DECLARE(name) \
static inline int name##_body(void); \
__attribute__ ((interrupt ("IRQ"))) void name(void) \
{ \
int check_reschedule; \
ISR_DIRECT_HEADER(); \
check_reschedule = name##_body(); \
ISR_DIRECT_FOOTER(check_reschedule); \
} \
static inline int name##_body(void)

当声明一个pwm_isr时如下

1
2
3
4
ARCH_ISR_DIRECT_DECLARE(pwm_isr)
{
//isr process
}

将其展开如下,相当于是产生了一个子函数pwm_isr_body,pwm_isr_body是真正的isr处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline int pwm_isr_body(void); \
__attribute__ ((interrupt ("IRQ"))) void pwm_isr_body(void) \
{ \
int check_reschedule; \
arch_isr_direct_header(); \
check_reschedule = pwm_isr_body(); \
arch_isr_direct_footer(check_reschedule); \
} \
static inline int pwm_isr_body(void)
{
//isr process

ISR_DIRECT_PM();

return 1;
}

在pwm_isr_body的前后分别调用header和footer函数, 对于cortex-m7架构arch_isr_direct_header没有做什么实际的事情:

1
2
3
4
5
6
static inline void arch_isr_direct_header(void)
{
#ifdef CONFIG_TRACING
sys_trace_isr_enter();
#endif
}

pwm_isr_body的返回值决定在退出中断处理时是否要重新调度,将返回值传递给arch_isr_direct_footer由其判断是否进行调度

1
2
3
4
5
6
7
8
9
static inline void arch_isr_direct_footer(int maybe_swap)
{
#ifdef CONFIG_TRACING
sys_trace_isr_exit();
#endif
if (maybe_swap) {
z_arm_int_exit(); //这里是pwm_isr_body的返回值判断,为ture表示要重新调度
}
}

在pwm_isr_body中调用ISR_DIRECT_PM();在电源管理开启的情况下表示中断退出时要退出电源idle状态,调用顺序如下:
ISR_DIRECT_PM->ARCH_ISR_DIRECT_PM->_arch_isr_direct_pm->z_sys_power_save_idle_exit

注册

直接ISR通过IRQ_DIRECT_CONNECT注册
include/irq.h

1
2
#define IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p) \
ARCH_IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p)

不同的架构会有不同的ARCH_IRQ_DIRECT_CONNECT实现,cortex-m7 32bit系列内核的实现如下
include/arch/arm/aarch32/irq.h

1
2
3
4
5
6
#define ARCH_IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p) \
({ \
Z_ISR_DECLARE(irq_p, ISR_FLAG_DIRECT, isr_p, NULL); \
z_arm_irq_priority_set(irq_p, priority_p, flags_p); \
irq_p; \
})

看一下是不是和普通的ISR很像,只有一点差异就是flag = ISR_FLAG_DIRECT, 也就是说对于直接ISR最后还是产生一个struct _isr_list结构体变量放入.intList中。
如果传入的fun是dma_isr, 调用IRQ_CONNECT的次数是第6次,那么IRQ_DIRECT_CONNECT最终内容展开就是

1
2
3
4
5
6
7
static  __aligned(__alignof(struct _ist_list))struct _ist_list  __attribute__((section(".intList")))
__used __isr_dma_isr_irq_6 = {
irq,
ISR_FLAG_DIRECT,
uart_isr,
param
}

前面有提到过直接ISR注册的ISR是放到硬件中断向量表中,这里怎么就放到.intList中了呢,接下来我们来分析如何生成中断向量表

生成中断向量表

从前面的分析可以看到动态注册普通isr是在运行时改变软件向量表,这里再详细分析如何将保存在.initList内的ISR信息放入中断向量表。下图说明的整个装换过程:
zephyrvector
Zephyr采用了两阶段链接,第一阶段链接生成zephyr_prebuild.elf,该elf中中断向量由zephyr/arch/common/isr_tables.c生成,只是占位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef CONFIG_GEN_IRQ_VECTOR_TABLE
u32_t __irq_vector_table _irq_vector_table[IRQ_TABLE_SIZE] = {
[0 ...(IRQ_TABLE_SIZE - 1)] = (u32_t)&_isr_wrapper,
};
#endif

/* If there are no interrupts at all, or all interrupts are of the 'direct'
* type and bypass the _sw_isr_table, then do not generate one.
*/
#ifdef CONFIG_GEN_SW_ISR_TABLE
struct _isr_table_entry __sw_isr_table _sw_isr_table[IRQ_TABLE_SIZE] = {
[0 ...(IRQ_TABLE_SIZE - 1)] = {(void *)0x42, (void *)&z_irq_spurious},
};
#endif

在第一阶段ISR_CONNECT和ISR_DERICT_CONNECT注册的中断向量都放在zephyr_prebuild.elf的.intList段中,第二阶段链接时,会通过objcopy从zephyr_prebuild.elf中dump出.intList,然后由gen_isr_table.py解析生成新的isr_table.c,生成过程就是将direct isr放入新的硬件中断向量表_irq_vector_table,普通的isr放入新的软件中断向量表,生成新的isr_table.c示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define ISR_WRAPPER ((u32_t)&_isr_wrapper)

u32_t __irq_vector_table _irq_vector_table[160] = {
ISR_WRAPPER,
ISR_WRAPPER,
ISR_WRAPPER,
ISR_WRAPPER,
test_direct_isr,
...
ISR_WRAPPER
}


struct _isr_table_entry __sw_isr_table [160] = {
{(void *)0x0, (void *)&z_irq_spurious},
{(void *)0x0, (void *)&z_irq_spurious},
{(void *)0x0, (void *)&z_irq_spurious},
{(void *)0x800029c8, (void *)0x6000ae61},
{(void *)0x0, (void *)&z_irq_spurious},
{(void *)0x80002a04, (void *)0x6000adb7},
{(void *)0x80002a04, (void *)0x6000adb7},
{(void *)0x0, (void *)&z_irq_spurious},
{(void *)0x0, (void *)&z_irq_spurious},
...
}

然后新的isr_table.c重新和zephyr的lib链接生成zephyr.elf,再该过程中就会替换掉之前的中断向量表。
目前并不清楚为何Zephyr为何要用该种方式来生成中断向量表,猜测有两种原因:1.兼容其它架构的中断向量,2. userspace开启后会重新插入object会影响软件中断向量表的定址。

ISR执行过程

直接ISR

以前面的中断向量表示例为例当IRQ4发生中断时,访问中断入口函数test_direct_isr,直接执行

普通ISR

以前面的中断向量表示例为例当IRQ3发生中断时, 访问中断入口函数ISR_WRAPPER,也就是_isr_wrapper,列出主要代码
zephyr/arch/arm/core/aarch32/isr_wrapper.S

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTION_FUNC(TEXT, _isr_wrapper)
mrs r0, IPSR /* 获取exception号*/
sub r0, r0, #16 /* 计算出IRQ号,也就是3,放入r0*/
lsl r0, r0, #3 /* table is 8-byte wide */
ldr r1, =_sw_isr_table
add r1, r1, r0 /* 查表,获取ISR地址 */

ldm r1!,{r0,r3} /* 获取ISR参数地址 */
blx r3 /* 执行ISR*/

pop {r0, lr}
ldr r1, =z_arm_int_exit /*执行完ISR后重新调度*/
bx r1

以上过程相当于是查表获得了 {(void )0x800029c8, (void )0x6000ae61},然后执行下面过程

1
2
3
4
5
6
7
8
9
struct _isr_table_entry {
void *arg;
void (*isr)(void *);
};

void (*isr)(void *) = _sw_isr_table[3].isr; //0x6000ae61
void *arg = _sw_isr_table[3].arg; //0x800029c8

isr(arg);

如果没有注册ISR则最后会查表到z_irq_spurious, 最后会调用z_arm_fault,大致流程如下
zephyr/arch/arm/core/aarch32/irq_manage.c

1
2
3
4
5
void z_irq_spurious(void *unused)
{
ARG_UNUSED(unused);
z_arm_reserved();
}

zephyr/arch/arm/core/aarch32/fault_s.S

1
2
3
4
SECTION_SUBSEC_FUNC(TEXT,__fault,z_arm_reserved)
...
bl z_arm_fault //zephyr/arch/arm/core/aarch32/cortex_m/fault.c
...

控制操作

lock/unlock

lock/unlock都是架构相关的宏

1
2
#define irq_lock() arch_irq_lock()
#define irq_unlock(key) arch_irq_unlock(key)

在cortex-m7的情况下是通过操作BASEPRI寄存器完成,该寄存器设置一个优先级,低于该优先级的IRQ都不会响应
zephyr/include/arch/arm/aarch32/asm_inline_gcc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static ALWAYS_INLINE unsigned int arch_irq_lock(void)
{
unsigned int key;
unsigned int tmp;

__asm__ volatile(
"mov %1, %2;" //tmp = _EXC_IRQ_DEFAULT_PRIO, 该值一般为0
"mrs %0, BASEPRI;" //读BASEPRI放到key
"msr BASEPRI, %1;" //将_EXC_IRQ_DEFAULT_PRIO写入到BASEPRI
"isb;"
: "=r"(key), "=r"(tmp)
: "i"(_EXC_IRQ_DEFAULT_PRIO)
: "memory");

return key; //返回原本BASEPRI内的值为key值
}

static ALWAYS_INLINE void arch_irq_unlock(unsigned int key)
{

__asm__ volatile(
"cpsie i;"
"isb"
: : : "memory");

__asm__ volatile(
"msr BASEPRI, %0;"
"isb;"
: : "r"(key) : "memory"); //将Key值恢复到BASEPRI内

}

对于cortex-m7来说值越低,优先级越高,因此lock所有IRQ会把BASEPRI内的优先级设置为0。在Zephyr中断系统–使用一文中曾经提到过,一些情况下我们不原因中断被延迟,因此引入了0延迟中断的概念,所以在配置了0延迟中断时,我们就会将0这个优先级留出来给0延迟中断用,而1会被写入到BASEPRI内

1
2
3
4
5
6
7
8
9
10
11
12
13

#define _EXCEPTION_RESERVED_PRIO 0

#ifdef CONFIG_ZERO_LATENCY_IRQS //配置了0言辞中断
#define _EXC_ZERO_LATENCY_IRQS_PRIO 0
#define _EXC_SVC_PRIO 1
#define _IRQ_PRIO_OFFSET (_EXCEPTION_RESERVED_PRIO + 1) //将lock的最高优先级降一级
#else
#define _EXC_SVC_PRIO 0
#define _IRQ_PRIO_OFFSET (_EXCEPTION_RESERVED_PRIO)
#endif

#define _EXC_IRQ_DEFAULT_PRIO Z_EXC_PRIO(_IRQ_PRIO_OFFSET)

enable/disable

同样也是架构相关函数

1
2
#define irq_enable(irq) arch_irq_enable(irq)
#define irq_disable(irq) arch_irq_disable(irq)

在cortex-m7的情况下是通过操作NVIC完成,不再做详细分析了

1
2
3
4
5
6
7
8
9
void arch_irq_enable(unsigned int irq)
{
NVIC_EnableIRQ((IRQn_Type)irq);
}

void arch_irq_disable(unsigned int irq)
{
NVIC_DisableIRQ((IRQn_Type)irq);
}

状态获取

架构相关

判断当前irq是否enable中, 通过NVIC的寄存器判读
zephyr/arch/arm/core/aarch32/irq_manage.c

1
2
3
4
5
6
#define irq_is_enabled(irq) arch_irq_is_enabled(irq)

int arch_irq_is_enabled(unsigned int irq)
{
return NVIC->ISER[REG_FROM_IRQ(irq)] & BIT(BIT_FROM_IRQ(irq));
}

判断当前代码是否在isr中,读取ipsr寄存器
zephyr/arch/arm/include/aarch32/cortex_m/exc.h

1
2
3
4
5
6
7
8
9
bool k_is_in_isr(void)
{
return arch_is_in_isr();
}

static ALWAYS_INLINE bool arch_is_in_isr(void)
{
return (__get_IPSR()) ? (true) : (false);
}

判断代码代码是否在isr中或者在协成中,读取ipsr寄存器和检查kernel preempt状态
zephyr/kernel/sched.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int z_impl_k_is_preempt_thread(void)
{
return !arch_is_in_isr() && is_preempt(_current);
}

static inline int is_preempt(struct k_thread *thread)
{
#ifdef CONFIG_PREEMPT_ENABLED
/* explanation in kernel_struct.h */
return thread->base.preempt <= _PREEMPT_THRESHOLD;
#else
return 0;
#endif
}

架构无关

判断当前代码是否在post kernel之后,判断z_sys_post_kernel标志即可
zephyr/include/kernel.h

1
2
3
4
5
6
static inline bool k_is_pre_kernel(void)
{
extern bool z_sys_post_kernel; /* in init.c */

return !z_sys_post_kernel;
}

z_sys_post_kernel标记在bg_thread_main中post kernel 初始化前设置为true的
zephyr/kernel/init.c

1
2
3
4
5
6
7
8
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
...
z_sys_post_kernel = true;

z_sys_device_do_config_level(_SYS_INIT_LEVEL_POST_KERNEL);
...
}

参考

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