本文包含源代码、原理图、PCB、封装库、中英文PDF等资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
前言
本文学习RT-Thread的中断管理,这里主要包括异常与中断的基本概念、RT-Thread 中断工作机制,更多关于RT-Thread的中断管理请看《RT-Thread编程指南》 ,后面通过使用STM32来进行实验。
一、异常与中断的基本概念
异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性瘫痪。所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环,对于实时系统更是如此。异常是指任何打断处理器正常执行,并且迫使处理器进入一个由有特权的特殊指令执行的事件。异常通常可以分成两类:同步异常和异步异常。
1、异步异常与同步异常:异步异常主要是指由于外部异常源产生的异常,是一个由外部硬件装置产生的事件引起的异步异常。。同步异常不同于异步异常的地方是事件的来源,同步异常事件是由于执行某些指令而从处理器内部产生的,而异步异常事件的来源是外部硬件装置。例如按下设备某个按钮产生的事件。同步异常与异步异常的区别还在于,同步异常触发后,系统必须立刻进行处理而不能够依然执行原有的程序指令步骤;而异步异常则可以延缓处理甚至是忽略,例如按键中断异常,虽然中断异常触发了,但是系统可以忽略它继续运行(同样也忽略了相应的按键事件)。
2、中断:中断属于异步异常。所谓中断是指中央处理器 CPU 正在处理某件事的时候,外部发生了某一事件,请求 CPU迅速处理,CPU 暂时中断当前的工作,转入处理所发生的事件,处理完后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。中断能打断线程的运行,无论该线程具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简单处理,例如标记该事件,在使用 RT-Thread 系统时,一般建议使用信号量、消息或事件标志组等标志中断的发生,将这些内核对象发布给处理线程,处理线程再做具体处理。
二、RT-Thread 中断工作机制
1、中断向量表
(1)中断向量表是所有中断处理程序的入口,如下图所示是 Cortex-M 系列的中断处理过程:把一个函数(用户中断服务程序)同一个虚拟中断向量表中的中断向量联系在一起。当中断向量对应中断发生的时候,被挂接的用户中断服务程序就会被调用执行。
中断处理过程(来源RT-Thread编程指南)
(2)在 Cortex-M 内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,每个中断服务程序必须排列在一起放在统一的地址上(这个地址必须要设置到NVIC 的中断向量偏移寄存器中)。中断向量表一般由一个数组定义或在起始代码中给出,默认采用起始代码给出,打开startup_stm32xxxxxx.s文件:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
......
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
......
复制代码
请注意代码后面的 [WEAK] 标识,它是符号弱化标识,在 [WEAK] 前面的符号 (如 NMI_Handler、HardFault_Handler)将被执行弱化处理,如果整个代码在链接时遇到了名称相同的符号(例如与NMI_Handler 相同名称的函数),那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),而与弱化符号相关的代码将被自动丢弃。以 SysTick 中断为例,在系统启动代码中,需要填上 SysTick_Handler 中断入口函数,然后实现该函数即可对 SysTick 中断进行响应,中断处理函数示例程序如下所示:
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
复制代码
2、中断处理过程
RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分,如下图:
中断处理程序的3部分(来源RT-Thread编程指南)
(1)中断前导程序
中断前导程序主要工作如下:
(A)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
(B)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest加 1,用它来记录中断嵌套的层数,代码如下所示。
void rt_interrupt_enter(void)
{
rt_base_t level;
RT_DEBUG_LOG(RT_DEBUG_IRQ, ("irq coming..., irq nest:%d\n",
rt_interrupt_nest));
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
RT_OBJECT_HOOK_CALL(rt_interrupt_enter_hook,());
rt_hw_interrupt_enable(level);
}
复制代码
(2)用户中断服务程序
(A)在用户中断服务程序(ISR)中,分为两种情况,第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。
(B)另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt()函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
(C)在 Cortex-M 架构中,rt_hw_context_switch_interrupt() 的函数实现流程如下图所示,它将设置需要切换的线程 rt_interrupt_to_thread 变量,然后触发 PendSV 异常(PendSV 异常是专门用来辅助上下文切换的,且被初始化为最低优先级的异常)。PendSV 异常被触发后,不会立即进行 PendSV 异常中断处理程序,因为此时还在中断处理中,只有当中断后续程序运行完毕,真正退出中断处理后,才进入 PendSV异常中断处理程序。
rt_hw_context_switch_interrupt() 函数实现流程(来源RT-Thread编程指南)
(3)中断后续程序
中断后续程序主要完成的工作是:
(A) 通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。
void rt_interrupt_leave(void)
{
rt_base_t level;
RT_DEBUG_LOG(RT_DEBUG_IRQ, ("irq leave, irq nest:%d\n",
rt_interrupt_nest));
level = rt_hw_interrupt_disable();
rt_interrupt_nest --;
RT_OBJECT_HOOK_CALL(rt_interrupt_leave_hook,());
rt_hw_interrupt_enable(level);
}
复制代码
(B)恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程的 CPU上下文,如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文。这部分实现跟 CPU 架构相关,不同 CPU 架构的实现方式有差异,在 Cortex-M 架构中实现流程如下图所示。
rt_hw_context_switch_interrupt() 函数实现流程(来源RT-Thread编程指南)
3、中断嵌套
在允许中断嵌套的情况下,在执行中断服务程序的过程中,如果出现高优先级的中断,当前中断服务程序的执行将被打断,以执行高优先级中断的中断服务程序,当高优先级中断的处理完成后,被打断的中断服务程序才又得到继续执行,如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生,如下图所示。
中断中的线程切换(来源RT-Thread编程指南)
4、中断栈
(1)在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。
(2)中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。
(3)RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。
(4)在 Cortex-M 处理器内核里有两个堆栈指针,一个是主堆栈指针(MSP),是默认的堆栈指针,在运行第一个线程之前和在中断和异常服务程序里使用;另一个是线程堆栈指针(PSP),在线程里使用。在中断和异常服务程序退出时,修改 LR 寄存器的第 2 位的值为 1,线程的 SP 就由 MSP 切换到 PSP。
5、中断的底半处理
当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如 CPU 时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分(Top Half)和底半部分(Bottom Half)。在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),然
后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称之为底半处理。
三、基于STM32的RT-Thread中断管理实验
光说不练都是假把式,那么接下来我们进行实际操作,在STM32平台上实验,使用RTT&正点原子联合出品潘多拉开发板。创建一个消息队列、一个二值信号量和两个线程,通过两个按键KEY1和KEY2来发送消息和发送信号量,线程1用于读取消息队列的消息,读取到就打印处理,线程2用于获取二值信号量,获取到就打印获取到了信号量相关信息,按键KEY1和KEY2用于下降沿触发中断。
1、实验代码:
(1)中断实现代码:
#include "exti.h"
#include "rtthread.h"
extern rt_mq_t msgqueue;
extern rt_sem_t binary_sem;
/**************************************************************
函数名称 : exti_init
函数功能 : 外部中断初始化函数
输入参数 : 无
返回值 : 无
备注 : 无
**************************************************************/
void exti_init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
GPIO_Initure.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
GPIO_Initure.Mode = GPIO_MODE_IT_FALLING;
GPIO_Initure.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOD, &GPIO_Initure);
HAL_NVIC_SetPriority(EXTI9_5_IRQn,2,0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
#if 1
/**************************************************************
函数名称 : EXTI9_5_IRQHandler
函数功能 : EXTI9_5中断服务函数
输入参数 : 无
返回值 : 无
备注 : 无
**************************************************************/
void EXTI9_5_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);/* 调用中断处理公用函数 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_9);/* 调用中断处理公用函数 */
}
/**************************************************************
函数名称 : HAL_GPIO_EXTI_Callback
函数功能 : 外部中回调函数
输入参数 : GPIO_Pin:中断引脚号
返回值 : 无
备注 : 无
**************************************************************/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 进入中断 */
rt_interrupt_enter();
if(GPIO_Pin == GPIO_PIN_8)
{
rt_mq_send(msgqueue, "RT-Thread interrupt send msgqueue test", sizeof("RT-Thread interrupt send msgqueue test"));
}
else if(GPIO_Pin == GPIO_PIN_9)
{
rt_sem_release(binary_sem);
}
/* 离开中断 */
rt_interrupt_leave();
}
#endif
复制代码
(2)main.c代码:
#include "main.h"
#include "board.h"
#include "rtthread.h"
#include "data_typedef.h"
/* 线程句柄 */
rt_thread_t thread1 = RT_NULL;
rt_thread_t thread2 = RT_NULL;
/* 消息队列句柄 */
rt_mq_t msgqueue = RT_NULL;
/* 二值信号量句柄 */
rt_sem_t binary_sem = RT_NULL;
void thread1_recv_msgqueue_entry(void *parameter);
void thread2_recv_semaphore_entry(void *parameter);
int main(void)
{
/* 创建消息队列 */
msgqueue = rt_mq_create("msgqueue",
64,
10,
RT_IPC_FLAG_FIFO);/* FIFO模式 */
if(msgqueue != RT_NULL)
{
rt_kprintf("RT-Thread create msgqueue successful\r\n");
}
else
{
rt_kprintf("RT-Thread create msgqueue failed\r\n");
return 0;
}
/* 创建信号量 */
binary_sem = rt_sem_create("binary_sem",
0,
RT_IPC_FLAG_FIFO);
if(RT_NULL != binary_sem)
{
rt_kprintf("RT-Thread create semaphore successful\r\n");
}
else
{
rt_kprintf("RT-Thread create semaphore failed\r\n");
return 0;
}
/* 创建线程 */
thread1 = rt_thread_create("thread1",
thread1_recv_msgqueue_entry,
NULL,
512,
3,
20);
if(thread1 != RT_NULL)
{
rt_thread_startup(thread1);
}
else
{
rt_kprintf("create thread1 failed\r\n");
return 0;
}
/* 创建线程 */
thread2 = rt_thread_create("thread2",
thread2_recv_semaphore_entry,
NULL,
512,
2,
20);
if(thread2 != RT_NULL)
{
rt_thread_startup(thread2);
}
else
{
rt_kprintf("create thread2 failed\r\n");
return 0;
}
return 0;
}
/**************************************************************
函数名称 : thread1_recv_msgqueue_entry
函数功能 : 线程1入口函数,接收消息
输入参数 : parameter:入口参数
返回值 : 无
备注 : 无
**************************************************************/
void thread1_recv_msgqueue_entry(void *parameter)
{
char buf[64];
while(1)
{
if(rt_mq_recv(msgqueue, buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("%s\r\n", buf);
}
rt_thread_mdelay(1);
}
}
/**************************************************************
函数名称 : thread2_recv_semaphore_entry
函数功能 : 线程2入口函数,接收信号量
输入参数 : parameter:入口参数
返回值 : 无
备注 : 无
**************************************************************/
void thread2_recv_semaphore_entry(void *parameter)
{
while(1)
{
if(rt_sem_take(binary_sem, RT_WAITING_FOREVER) == RT_EOK)
{
rt_kprintf("RT-Thread interrupt send semaphore test\r\n");
}
rt_thread_mdelay(1);
}
}
复制代码
2、观察FinSH:
(1)按下KEY1,触发中断,发送信号量,线程2获取到信号量,打印如下信息:
(2)按下KEY2,触发中断,发送消息,线程1获取到消息,打印如下消息:
参考文献:
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》
2、《RT-THREAD 编程指南》