Zephyr中断系统-实现

本文简要介绍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的函数入口和参数

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

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

运行期注册 链接到标题

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

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系列内核的实现

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函数如何和参数

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

#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

#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

#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

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最终内容展开就是

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

#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系列内核的实现如下

#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时如下

ARCH_ISR_DIRECT_DECLARE(pwm_isr)
{
	//isr process
}

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

	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没有做什么实际的事情:

static inline void arch_isr_direct_header(void)
{
#ifdef CONFIG_TRACING
	sys_trace_isr_enter();
#endif
}

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

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

#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

#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最终内容展开就是

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生成,只是占位

#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示例如下:

#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

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},然后执行下面过程

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

void z_irq_spurious(void *unused)
{
	ARG_UNUSED(unused);
	z_arm_reserved();
}

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

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都是架构相关的宏

#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

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内


#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 链接到标题

同样也是架构相关函数

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

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

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

#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

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

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

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

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