一、Cortex-M中断介绍
1、什么是中断?
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后 CPU 就会中断当前 的流程转而去处理中断服务。Cortex-M 内核的MCU 提供了一个用于中断管理的嵌套向量中断 控制器(NVIC)。OneOS中配置的Cotex-M的NVIC 最多支持256个中断请求(16个内核中断+240个外部中断)、1个不可屏蔽中断(NMI)、1个 Systick(滴答定时器)定时器中断和多个系统异常。同中断相关的概念和特点可见下表:
特性/概念 | 描述 |
异步性 | 中断是异步发生的,可以在任何时刻打断CPU的正常执行流程。 |
通知机制 | 中断用于通知CPU发生了需要立即处理的事件。 |
优先级 | 中断具有不同的优先级,操作系统根据优先级决定处理顺序。 |
处理程序 | 每个中断都有一个中断处理程序,专门用于响应和处理中断。 |
上下文保存 | 中断发生时,CPU需要保存当前执行任务的上下文。 |
中断响应 | 操作系统负责响应中断,包括保存上下文和执行处理程序。 |
硬件支持 | 中断机制需要硬件支持,例如中断控制器或IRQ线路。 |
软件实现 | 操作系统软件实现对中断的处理,包括接收、排序和调用处理程序。 |
多任务处理 | 中断机制允许系统在多任务环境中及时响应外部事件。 |
系统性能 | 合理使用中断可以提升系统性能,但也可能引入性能问题。 |
应用场景 | 中断适用于多种场景,如用户输入、设备状态变化、定时事件等。 |
2、NVIC工作原理
NVIC是ARM Cortex-M处理器中的一个硬件模块,负责管理中断和异常。它提供了一种机制,允许微控制器响应外部和内部事件。NVIC结构如下图:
①②③④⑤⑥⑦⑧⑨AIRCR(Application Interrupt and Reset Control Register,应用中断和复位控制寄存器): 用于全局中断使能/失能和复位控制。
① SHPR(System Handlers Priority Registers,系统异常优先级寄存器阵列): 4 个相临的优先级寄存器拼成一个32 位寄存器,用于设置每个中断的优先级。
② ISER(Interrupt Set-Enable Register,中断使能设置寄存器):用于使能特定的中断源。
③ IPR(Interrupt Priority Registers,中断优先级寄存器): 每个外部中断都一个对应优先级寄存器,每个寄存器占用 8 位,4 个相临的优先级寄存器拼成一个 32 位寄存器。
④ ICER(Interrupt Clear-Enable Register,中断使能清除寄存器):用于清除(失能)特定的中断源。
运行原理:
① 中断使能: 开发者可以通过写入ISER寄存器来使能一个或多个中断源。每个位对应一个中断源,1表示使能,0表示失能。
② 中断优先级: 每个中断源都有一个与之关联的优先级,通过SHPR寄存器设置。优先级数值越小,优先级越高。
③ 中断处理: 当一个中断事件发生时,如果该中断源被使能(在ISER中对应的位为1),并且具有最高的优先级(在SHPR中设置的数值最小),CPU将响应中断,执行相应的中断服务例程(ISR)。
④ 中断响应: 中断响应过程中,CPU会保存当前任务的上下文,包括程序计数器和寄存器状态,然后跳转到中断服务例程的入口地址。
⑤ 中断服务例程执行: 中断服务例程执行完成后,CPU会恢复之前保存的任务上下文,并继续执行被中断的任务。
⑥ 中断清除: 如果需要,开发者可以通过写入ICER寄存器来清除(失能)一个或多个中断源。
⑦ 分组控制: NVIC的控制寄存器(如AIRCR)可能包含位字段,用于控制8种不同的中断分组,允许对中断进行更细粒度的管理。
⑧ 优先级配置: 优先级配置确保了在多个中断同时发生时,NVIC能够根据优先级顺序处理它们。
NVIC的设计允许Cortex-M处理器有效地管理中断,通过使能/失能控制和优先级配置,确保了对外部和内部事件的快速响应。这种机制对于实时系统和多任务处理至关重要,因为它可以保证高优先级的中断得到及时处理,同时保持系统稳定性和响应性。
3、优先级分组定义
当多个中断来临的时候处理器应该响应哪一个中断是由中断的优先级来决定的,高优先级的中断(优先级编号小)肯定是首先得到响应,而且高优先级的中断可以抢占低优先级的中断,这个就是中断嵌套。Cortex-M 处理器的有些中断是具有固定的优先级的,比如复位、NMI、HardFault,这些中断的优先级都是负数,优先级也是最高的。
Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级,最多有 128 个抢占等级,但是实际的优先级数量是由芯片厂商来决定的。但是,绝大多数的芯片都会精简设计的,以致实际上支持的优先级数会更少,如 8 级、16 级、32 级等,比如 STM32 就只有 16 级优先级。在设计芯片的时候会裁掉表达优先级的几个低端有效位,以减少优先级数,所以不管用多少位来表达优先级,都是 MSB 对齐的,如下图就是使用三位来表达优先级。
在上图中,Bit0~Bit4 没有实现,所以读它们总是返回零,写如它们的话则会忽略写入的值。因此,对于3个位的情况,可是使用的优先级就是8个:0X00(最高优先级)、0X20、0X40、0X60、0X80、0XA0、0XC0 和 0XE0。注意,这个是芯片厂商来决定的!不是我们能决定的,比如STM32就选择了4位作为优先级!
请注意,优先级配置寄存器是8位宽的,为什么却只有128个抢占等级?8位不应该是256个抢占等级吗?为了使抢占机能变得更可控,Cortex-M 处理器还把256个优先级按位分为高低两段:抢占优先级(分组优先级)和亚优先级(子优先级),NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器(AIRCR)”,AIRCR 寄存器里面有个位段名为“优先级组”,如下图所示:
上图中PRIGROUP就是优先级分组,它把优先级分为两个位段:MSB 所在的位段(左边的)对应抢占优先级,LSB 所在的位段(右边的)对应亚优先级,如下表所示。
分组位置 | 表达抢占优先级的位段 | 表达亚优先级的位段 |
0(默认) | [7:1] | [0:0] |
1 | [7:2] | [0:1] |
2 | [7:3] | [0:2] |
3 | [7:4] | [0:3] |
4 | [7:5] | [0:4] |
5 | [7:6] | [0:5] |
6 | [7:7] | [0:6] |
7 | 无 | [0:7] |
4、Cortex-M 优先级设置
每个外部中断都有一个对应的优先级寄存器,每个寄存器占 8 位,因此最大宽度是 8 位, 但是最小为 3 位。4 个相临的优先级寄存器拼成一个 32 位寄存器。如前所述,根据优先级组的 设置,优先级又可以分为高、低两个位段,分别抢占优先级和亚优先级。OneOS 默认设置为组 4,所以就只有抢占优先级了。
4.1、系统异常优先级阵列
4个相临的寄存器可以拼成一个32位的寄存器,因此地址0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为 0xE000_ED20 的 32 位寄存器。这一点很重要!因为 OneOS 在设置 PendSV 和 SysTick 的中断优先级的时候都是直接操作的地址 0xE000_ED20。
4.2、Cortex-M 用于中断屏蔽的特殊寄存器
中断屏蔽寄存器(通常称为中断屏蔽位或中断禁用寄存器)是一个重要的硬件特性,它用于控制中断的响应。主要作用有四点。
- 临界区保护:在访问共享资源或执行关键操作时,需要保护代码的临界区,以防止被中断打断。通过屏蔽中断,可以确保这些区域的代码能够连续、无干扰地执行。
- 优先级控制:在某些情况下,可能需要临时提高代码的执行优先级,以确保它在没有其他中断干扰的情况下运行。中断屏蔽可以防止低优先级的中断抢占CPU。
- 系统状态管理:在系统启动、关闭或复位过程中,可能需要屏蔽所有中断,以确保系统状态的一致性和稳定性。
- 中断处理中的嵌套:在某些复杂的中断处理中,可能需要嵌套处理多个中断源。中断屏蔽寄存器可以用来控制中断的嵌套级别。
中断屏蔽特殊寄存器 | 描述 |
PRIMASK (OneOS常用) | 禁止除 NMI 和 HardFalut 外的所有异常和中断 |
FAULTMASK | 禁止除 NMI 外的所有异常和中断 |
BASEPRI(FreeRTOS常用) | 只屏蔽优先级低于某一个阈值的中断 |
注:因为任务调度或任务切换需要使用pendsv,但是pendsv的优先级最低,为防止任务在切换或者调度中途被打断,所以需要使用中断屏蔽。pendsv执行的时候,开启中断屏蔽,任务或者pendsv完成之后再撤销屏蔽。
5、裸机和OneOS在中断方面的区别?
(1)中断设置有何变化?
裸机怎么玩,OneOS内核里面就怎么玩
(2)中断处理有何变化?
① 任何任务执行过程中都被中断打断
② 避免中断处理程序切换任务
③ 中断可以嵌套,不影响任务正常执行
(3)中断返回有何变化?
找出就绪列表中优先级最高的任务,直接切换过去执行,强行改变中断程序的正常返回路径
二、Cortex中断管理实验
1. 实验目的
学习 os_irq_lock ()和 os_irq_unlock ()这两个函数的使用。
2. 实验设置
本实验设计两个任务: task1_task 和 task2_task ,这两个任务的任务功能如下:
定义一个共享资源区os_uint8_t share_resource[30]
task1_task:①向共享资源区拷贝数据,
②串口输出共享资源区数据。
task2_task:①向共享资源区拷贝数据,
②串口输出共享资源区数据
任务函数详解:
#include <os_types.h> /* 包含基本数据类型定义 */
#include <os_task.h> /* 包含任务相关函数声明 */
#include <board.h> /* 包含硬件平台相关配置 */
#define TASK1_PRIO 3 /* 定义任务1的优先级 */
#define TASK1_STK 512 /* 定义任务1的堆栈大小 */
os_task_dummy_t *task_cb1; /* 任务1的控制块指针 */
void task1_task(void *parameter); /* 任务1的函数声明 */
static os_task_id task1 = 0; /* 任务1的ID */
#define TASK2_PRIO 3 /* 定义任务2的优先级 */
#define TASK2_STK 512 /* 定义任务2的堆栈大小 */
os_task_dummy_t *task_cb2; /* 任务2的控制块指针 */
void task2_task(void *parameter); /* 任务2的函数声明 */
static os_task_id task2 = 0; /* 任务2的ID */
char share_resource[] = {"hello world"}; /* 定义一个共享资源区 */
void task1_task(void *parameter) /*任务1的任务函数*/
{
parameter = parameter; /* 避免未使用参数的编译器警告 */
int task1_num = 0; /* 任务1的计数器 */
os_base_t level1 = 0; /* 用于存储中断锁状态的变量 */
char task1_str[] = {"task1 run"}; /* 任务1要复制到共享资源区的字符串 */
while(1) /* 任务1的无限循环 */
{
task1_num++; /* 任务1计数器递增 */
level1 = os_irq_lock(); /* 获取中断锁,保存当前中断状态到level1 */
memcpy(share_resource, task1_str, sizeof(task1_str)); /* 复制任务1的字符串到共享资源区 */
os_kprintf("%s \r\n", share_resource); /* 打印共享资源区的内容到串口 */
os_irq_unlock(level1); /* 根据保存的状态恢复中断 */
os_task_msleep(1000); /* 任务1休眠1000毫秒 */
}
}
void task2_task(void *parameter) /*任务2的任务函数*/
{
parameter = parameter; /* 避免未使用参数的编译器警告 */
int task2_num = 0; /* 任务2的计数器 */
char task2_str[] = {"task2 run"}; /* 任务2要复制到共享资源区的字符串 */
os_base_t level2 = 0; /* 用于存储中断锁状态的变量 */
while(1) /* 任务2的无限循环 */
{
task2_num++; /* 任务2计数器递增 */
level2 = os_irq_lock(); /* 获取中断锁,保存当前中断状态到level2 */
memcpy(share_resource, task2_str, sizeof(task2_str)); /* 复制任务2的字符串到共享资源区 */
os_kprintf("%s \r\n", share_resource); /* 打印共享资源区的内容到串口 */
os_irq_unlock(level2); /* 根据保存的状态恢复中断 */
os_task_msleep(1000); /* 任务2休眠1000毫秒 */
}
}
int main(void)
{
/* 创建任务1 */
task1 = os_task_create(
task_cb1, /* 任务控制块 */
OS_NULL, /* 未使用的参数 */
TASK1_STK, /* 任务1的堆栈大小 */
"task1", /* 任务1的名称 */
task1_task, /* 任务1的函数 */
OS_NULL, /* 任务1的参数 */
TASK1_PRIO /* 任务1的优先级 */
);
os_task_set_time_slice(task1, 80); /* 设置任务1的时间片 */
OS_ASSERT(task1); /* 断言任务1创建成功 */
os_task_startup(task1); /* 启动任务1 */
/* 创建任务2 */
task2 = os_task_create(
task_cb2, /* 任务控制块 */
OS_NULL, /* 未使用的参数 */
TASK2_STK, /* 任务2的堆栈大小 */
"task2", /* 任务2的名称 */
task2_task, /* 任务2的函数 */
OS_NULL, /* 任务2的参数 */
TASK2_PRIO /* 任务2的优先级 */
);
os_task_set_time_slice(task2, 50); /* 设置任务2的时间片 */
OS_ASSERT(task2); /* 断言任务2创建成功 */
os_task_startup(task2); /* 启动任务2 */
return 0; /* 主函数返回 */
}
实验结果和原因分析
由于任务1和任务2具有相同的优先级,它们将通过时间片轮转调度。每个任务在运行时都会尝试修改共享资源 share_resource 的内容,并打印出来。通过使用中断锁,即使在中断发生的情况下,共享资源的修改也是安全的,不会出现竞态条件。
输出结果将是任务1和任务2交替打印它们各自的字符串,每次打印之间有1000毫秒的间隔。由于使用了中断锁,即使任务在打印过程中被抢占,共享资源的内容也不会被破坏。
图示情况符合预期,任务1和任务2交替输出资源区的内容。