前面谈到任务切换时,不管是任务级上下文切换还是中断级任务切换,都是通过悬起PendSV异常,引发一个异常中断实现任务切换的。谈到状态机时也提到状态机是事件驱动的,而这里的事件一般指的是中断触发信号,实际要执行的动作和状态迁移是在事件中断处理函数中进行的。不管是操作系统的任务切换,还是处理器与外设的I/O数据通信,甚至操作系统与用户操作的交互,都离不开中断系统的支持,那么什么是中断呢?
中断从表面意思看就是从中间断开,但程序运行是要可持续且确定的,自然不能出现程序断开后就回不去的情况,所以中断实际指的是一个中断信号打断原来执行的任务A转而去执行与中断信号对应的任务B,但原来的任务A可能还没执行完,所以打断A之前需要保存任务A的上下文信息,执行完任务B后需要借助任务A的上下文信息返回到任务A的折断处继续执行任务A。中断流程的示意图如下:
从上图看,中断处理流程和调子程序有点类似,但二者有个显著的区别:调子程序是主程序主动调用,何时调用对主程序是确定的;中断处理则是中断请求信号触发的被动调用,中断请求何时到来取决于中断源何时发出中断请求信号,不是由主程序决定的。是不是想起了任务调度器的程序流反向控制机制,任务何时切换,主程序不再关心,而是交由子程序任务调度器决定。与此类似,中断何时触发,主程序也控制不了,而是交由中断控制器给出的中断请求信号决定的。
中断系统这种”请求—响应“机制在两个设备进行交互时尤其便捷高效,不管是处理器还是计算机,本身作为一个数据处理设备,少不了跟外界设备进行数据输入输出的交互,比如处理器要及时处理某外设比如串口或网口到来的数据,或者处理器要及时处理人机交互界面被点击的按钮等,处理器当然可以隔一段时间查看一遍是否有数据到来或者按钮被点击,但这种方式浪费了不少处理器的运算资源,而中断这种”请求—响应“的反向控制机制可以很好的解决这个问题。有中断系统辅助,处理器不需要再轮流检查外设是否有数据到达或者点击事件发生,转而专心去处理其他的任务(当然还是要耗费点运算资源去检查是否有中断请求信号到来,但这点耗费显然比轮询方式的耗费小多了),当外设有数据到达或点击事件发生时,通过中断控制器给处理器发出一个中断请求信号,处理器根据优先级再去处理该中断请求信号对应的任务就可以了。
由此可以看出,中断是处理两个系统间涉及到信号输入/输出等不确定性交互的绝佳工具,比如处理器与外设间的交互,系统与使用人间的交互等,外界的信号何时到来具有不确定性,使用中断作为通知机制可以大大提高交互效率。当然中断也可以用于协调系统内的数据处理,最常见的就是定时器/计数器这类时间确定性中断信号,特别是对于操作系统这类比较复杂的系统而言,内部需要有一个确定性的协调信号作为内部资源交互的基准,而定时器可以按照设定的时间间隔,周期性的发出确定性的中断信号可以协调这个系统的有序运行。如果把与外设交互的不确定性中断称为事件中断,则可以把协调内部资源交互的确定性中断(比如周期触发的定时器)称为时间中断,两者共同构成了处理器或计算机的中断系统。
中断系统是根据输入的中断请求信号做出反应的,中断请求信号又是来自于哪里呢?前面谈到中断常用于处理器与外设间的通讯,中断请求信号自然是来自于外设,而外设又分为片内外设与片外外设,片内外设是集成在MCU上并与总线连接的外设(比如定时器、DMA、ADC/DAC、UART等),片外外设则是通过外界I/O引脚输入的中断触发信号(通常由外部某引脚的电平变化引起中断触发)。我们经常把确定性的时间中断(比如定时器)与不确定的事件中断(比如数据通信的外设)信号分开管理,把片外外设称为外部中断,所以中断源可以简单分为下面三大类:
为更方便理解片上外设,下面给出一个STM32 MCU的系统结构图,读者可以看到连接到总线上的外设都有哪些:
跟总线矩阵连接的连接的比如DMA、Ethernet MAC、APB1、APB2等都属于片上外设,可以看出STM32支持的外设总类数量还算丰富。下面给出外部中断信号进入中断控制器的过程框图:
上图中的20指的是该MCU支持20个外部中断/事件请求,这里外部中断电平信号使用边沿触发方式,至于想使用上升沿还是下降沿触发则由开发者配置寄存器确定。上图显示外部中断信号分为了蓝色与红色两条路径:蓝色路径是输入到NVIC(Nested Vectored Interrupt Controller)中断控制器的,最后由CPU处理该中断信号;红色路径通过脉冲发生器最后是输入到特定片内外设的(比如DMA(Direct Memory Access)、DAC(Digital to analog converter)/ADC(Analog-to-Digital Converter)等),并不需要经过CPU处理,直接通过外部中断信号实现外部设备与片内外设的直接通信,减轻了CPU的处理负担提高了MCU的处理效率,但这需要提前配置好相应的寄存器。
既然外部中断是靠外部输入的电平变化实现的,片内外设和定时器也是靠某引脚电平变化来实现中断触发的吗?答案是肯定的,但MCU靠什么获得电平变化的脉冲信号呢?中断需要电平变化触发,MCU内的通信与运算自然也少不了高低电平变化的驱动,所以MCU的运行需要一套提供不同频率脉冲信号的系统支持,这套系统就是时钟树。下面给出STM32时钟树的结构图供参考:
从上图可以看出,不管是前面提到的操作系统心跳SYSTICK、处理器基频SYSCLK,还是DMA、APB1、APB2等通信外设,时钟树都能根据它们各自合适的需求提供相应频率的脉冲信号作为驱动力。时钟树的时钟源可以选择内部时钟也可以选择外部时钟:内部时钟电路简单成本低但时钟精确度也较低,外部时钟需要额外的电路和成本但时钟精度较高。为简化时钟树复杂度,又分别提供了高速时钟与低速时钟,高速时钟主要驱动处理器运算与外设通信,低速时钟主要驱动看门狗与RTC。片内外设的通信和定时器的计数有了时钟树提供的周期脉冲驱动自然可以正常工作了,至于中断信号的触发,则需要开发者配置相应外设或定时器的寄存器,设定好中断触发条件,待条件满足时自然会引起中断触发信号。
从外部中断进入中断控制器的流程框图可以看出,跟外部中断管理相关的寄存器(以STM32为例)主要如下:
直接管理这些寄存器的数据结构及相应寄存器的地址如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\stm32f10x.h
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400)
#define EXTI ((EXTI_TypeDef *) EXTI_BASE)
typedef struct
{
__IO uint32_t IMR;
__IO uint32_t EMR;
__IO uint32_t RTSR;
__IO uint32_t FTSR;
__IO uint32_t SWIER;
__IO uint32_t PR;
} EXTI_TypeDef;
ST为了便于开发,将跟中断相关的寄存器封装为相应的结构体,以结构体的形式管理这些寄存器配置能简化程序开发,当然结构体只提供了最重要的配置选项,还有一部分配置可以通过函数实现,管理外部中断寄存器配置的数据结构及操作函数如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_exti.h
typedef struct
{
uint32_t EXTI_Line; /*!< Specifies the EXTI lines to be enabled or disabled.
This parameter can be any combination of @ref EXTI_Lines */
EXTIMode_TypeDef EXTI_Mode; /*!< Specifies the mode for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
EXTITrigger_TypeDef EXTI_Trigger; /*!< Specifies the trigger signal active edge for the EXTI lines.
This parameter can be a value of @ref EXTIMode_TypeDef */
FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI lines.
This parameter can be set either to ENABLE or DISABLE */
}EXTI_InitTypeDef;
typedef enum
{
EXTI_Mode_Interrupt = 0x00,
EXTI_Mode_Event = 0x04
}EXTIMode_TypeDef;
typedef enum
{
EXTI_Trigger_Rising = 0x08,
EXTI_Trigger_Falling = 0x0C,
EXTI_Trigger_Rising_Falling = 0x10
}EXTITrigger_TypeDef;
typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;
typedef enum {RESET = 0, SET = !RESET} FlagStatus, ITStatus;
void EXTI_DeInit(void);
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
从上面的代码可以看出,管理外部中断最主要的结构体EXTI_InitTypeDef可以配置中断线引脚号、中断信号的边沿触发类型、中断/事件模式、中断使能/屏蔽等寄存器,在使用外部中断前需要先对该结构体进行初始化。除了该结构体,还有几个操作函数(主要是获取/清除标志位和中断悬起状态位操作)用于外部中断的配置,这些操作函数是联结EXTI_InitTypeDef与EXTI_TypeDef的纽带,把面向开发者配置项的EXTI_InitTypeDef结构体的初始值配置到面向底层寄存器的EXTI_TypeDef结构体成员中。
中断源除了外部中断,还有片上外设中断,外设中断可以简单分为两大类:一类是用于数据通信的,中断请求信号何时触发是不确定的,取决于数据什么时候发出或到达,比如USART中断;另一类是用于发出周期性信号的,中断请求信号何时触发是确定的,触发周期是可以配置的,比如Timer定时器中断。下面给出跟高级定时器相关的寄存器(以STM32为例)如下:
直接管理这些寄存器的数据结构及相关寄存器的地址如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\stm32f10x.h
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define TIM1_BASE (APB2PERIPH_BASE + 0x2C00)
#define TIM2_BASE (APB1PERIPH_BASE + 0x0000)
#define TIM1 ((TIM_TypeDef *) TIM1_BASE)
#define TIM2 ((TIM_TypeDef *) TIM2_BASE)
typedef struct
{
__IO uint16_t CR1;
uint16_t RESERVED0;
__IO uint16_t CR2;
uint16_t RESERVED1;
__IO uint16_t SMCR;
uint16_t RESERVED2;
__IO uint16_t DIER;
uint16_t RESERVED3;
__IO uint16_t SR;
uint16_t RESERVED4;
__IO uint16_t EGR;
uint16_t RESERVED5;
__IO uint16_t CCMR1;
uint16_t RESERVED6;
__IO uint16_t CCMR2;
uint16_t RESERVED7;
__IO uint16_t CCER;
uint16_t RESERVED8;
__IO uint16_t CNT;
uint16_t RESERVED9;
__IO uint16_t PSC;
uint16_t RESERVED10;
__IO uint16_t ARR;
uint16_t RESERVED11;
__IO uint16_t RCR;
uint16_t RESERVED12;
__IO uint16_t CCR1;
uint16_t RESERVED13;
__IO uint16_t CCR2;
uint16_t RESERVED14;
__IO uint16_t CCR3;
uint16_t RESERVED15;
__IO uint16_t CCR4;
uint16_t RESERVED16;
__IO uint16_t BDTR;
uint16_t RESERVED17;
__IO uint16_t DCR;
uint16_t RESERVED18;
__IO uint16_t DMAR;
uint16_t RESERVED19;
} TIM_TypeDef;
ST封装后跟中断配置相关的数据结构和操作函数如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_tim.h
/**
* @brief TIM Time Base Init structure definition
* @note This structure is used with all TIMx except for TIM6 and TIM7.
*/
typedef struct
{
uint16_t TIM_Prescaler; /*!< Specifies the prescaler value used to divide the TIM clock.
This parameter can be a number between 0x0000 and 0xFFFF */
uint16_t TIM_CounterMode; /*!< Specifies the counter mode.
This parameter can be a value of @ref TIM_Counter_Mode */
uint16_t TIM_Period; /*!< Specifies the period value to be loaded into the active
Auto-Reload Register at the next update event.
This parameter must be a number between 0x0000 and 0xFFFF. */
uint16_t TIM_ClockDivision; /*!< Specifies the clock division.
This parameter can be a value of @ref TIM_Clock_Division_CKD */
uint8_t TIM_RepetitionCounter; /*!< Specifies the repetition counter value. Each time the RCR downcounter
reaches zero, an update event is generated and counting restarts
from the RCR value (N).
This means in PWM mode that (N+1) corresponds to:
- the number of PWM periods in edge-aligned mode
- the number of half PWM period in center-aligned mode
This parameter must be a number between 0x00 and 0xFF.
@note This parameter is valid only for TIM1 and TIM8. */
} TIM_TimeBaseInitTypeDef;
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
void TIM_GenerateEvent(TIM_TypeDef* TIMx, uint16_t TIM_EventSource);
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
定时器功能相当丰富,包括输入捕获、输出比较等,所以与定时器相关的数据结构和操作函数也挺多,远不止上面列出的这一部分,由于本文的重点是谈论中断系统的,所以上面只列出了跟中断相关的数据结构和操作函数,跟上面外部中断的操作函数对比还是有点类似性的。这些操作函数是联结TIM_TimeBaseInitTypeDef与TIM_TypeDef的纽带,把面向开发者配置项的TIM_TimeBaseInitTypeDef结构体的初始值配置到面向底层寄存器的TIM_TypeDef结构体成员中。
以STM32为例,支持的片上外设还是挺丰富的,这里不能一一列举,实际上定时器也是片上外设的一种,只不过定时器是发出周期性确定的触发信号,其余的大部分都跟通信接口协议相关,属于非周期不确定性触发信号,所以下面再选择一个外设简单介绍下。这里就选择最常用的USART外设作为示例了,跟USART相关的主要寄存器如下:
直接管理这些寄存器的数据结构及相关寄存器地址如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\stm32f10x.h
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
typedef struct
{
__IO uint16_t SR;
uint16_t RESERVED0;
__IO uint16_t DR;
uint16_t RESERVED1;
__IO uint16_t BRR;
uint16_t RESERVED2;
__IO uint16_t CR1;
uint16_t RESERVED3;
__IO uint16_t CR2;
uint16_t RESERVED4;
__IO uint16_t CR3;
uint16_t RESERVED5;
__IO uint16_t GTPR;
uint16_t RESERVED6;
} USART_TypeDef;
ST封装后跟中断配置相关的数据结构和操作函数如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_usart.h
typedef struct
{
uint32_t USART_BaudRate; /*!< This member configures the USART communication baud rate.
The baud rate is computed using the following formula:
- IntegerDivider = ((PCLKx) / (16 * (USART_InitStruct->USART_BaudRate)))
- FractionalDivider = ((IntegerDivider - ((u32) IntegerDivider)) * 16) + 0.5 */
uint16_t USART_WordLength; /*!< Specifies the number of data bits transmitted or received in a frame.
This parameter can be a value of @ref USART_Word_Length */
uint16_t USART_StopBits; /*!< Specifies the number of stop bits transmitted.
This parameter can be a value of @ref USART_Stop_Bits */
uint16_t USART_Parity; /*!< Specifies the parity mode.
This parameter can be a value of @ref USART_Parity
@note When parity is enabled, the computed parity is inserted
at the MSB position of the transmitted data (9th bit when
the word length is set to 9 data bits; 8th bit when the
word length is set to 8 data bits). */
uint16_t USART_Mode; /*!< Specifies wether the Receive or Transmit mode is enabled or disabled.
This parameter can be a value of @ref USART_Mode */
uint16_t USART_HardwareFlowControl; /*!< Specifies wether the hardware flow control mode is enabled
or disabled.
This parameter can be a value of @ref USART_Hardware_Flow_Control */
} USART_InitTypeDef;
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
USART支持的数据结构与操作函数自然也比上面列举出来的丰富得多,其他片上外设的中断配置跟USART或Timer也有很大的相似性,想了解更多更详细的外设中断配置信息,可以查看相关的参考手册和库文件源码。
从上面外部中断进入NVIC中断控制器的过程来看,中断信号经过了边沿选择寄存器、软件中断/事件寄存器、挂起请求寄存器、中断/事件屏蔽寄存器等,中断控制主要是靠配置这些寄存器的值实现的。
对于抢占式实时系统来说,对于触发事件响应的实时性是很重要的,就像任务管理凭借优先级高低判断哪个任务先执行,中断处理也是通过优先级管理的,优先级高的中断能抢占优先级低的中断。在一个中断任务返回退出前,被另一个更高优先级的中断任务抢占,待更高优先级的中断返回后继续执行原来的中断任务,这种有点类似函数嵌套调用的现象称为中断嵌套,但中断嵌套需要更深的主堆栈空间。为了使抢占机能变得更方便管理控制,CM3还引入了优先级分组概念,把8位256级优先级按位分成高低两段,分别是抢占优先级和亚优先级,由NVIC中的一个寄存器(应用程序中断及复位控制寄存器)里的一个位段(10:8优先级分组段)配置优先级分组位置,抢占优先级与亚优先级分组位段及分组位置如下表所示:
CM3支持的位段长度也并不一定全部能获得芯片厂商的支持,比如STM32F103优先级只取了5位,可能的分组如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\misc.h
The table below gives the allowed values of the pre-emption priority and subpriority according
to the Priority Grouping configuration performed by NVIC_PriorityGroupConfig function
============================================================================================================================
NVIC_PriorityGroup | NVIC_IRQChannelPreemptionPriority | NVIC_IRQChannelSubPriority | Description
============================================================================================================================
NVIC_PriorityGroup_0 | 0 | 0-15 | 0 bits for pre-emption priority
| | | 4 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_1 | 0-1 | 0-7 | 1 bits for pre-emption priority
| | | 3 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_2 | 0-3 | 0-3 | 2 bits for pre-emption priority
| | | 2 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_3 | 0-7 | 0-1 | 3 bits for pre-emption priority
| | | 1 bits for subpriority
----------------------------------------------------------------------------------------------------------------------------
NVIC_PriorityGroup_4 | 0-15 | 0 | 4 bits for pre-emption priority
| | | 0 bits for subpriority
============================================================================================================================
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
由于寄存器操作不便,ST将对寄存器的操纵封装为C语言库函数的操作,使用函数void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)即可完成对优先级分组的配置。同样的,在进行NVIC初始化时,要配置中断的抢占优先级与亚优先级,也有与之对应的数据结构管理相应的寄存器配置,参考代码如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\misc.h
typedef struct
{
uint8_t NVIC_IRQChannel; /*!< Specifies the IRQ channel to be enabled or disabled.
This parameter can be a value of @ref IRQn_Type
(For the complete STM32 Devices IRQ Channels list, please
refer to stm32f10x.h file) */
uint8_t NVIC_IRQChannelPreemptionPriority; /*!< Specifies the pre-emption priority for the IRQ channel
specified in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */
uint8_t NVIC_IRQChannelSubPriority; /*!< Specifies the subpriority level for the IRQ channel specified
in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref NVIC_Priority_Table */
FunctionalState NVIC_IRQChannelCmd; /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
will be enabled or disabled.
This parameter can be set either to ENABLE or DISABLE */
} NVIC_InitTypeDef;
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
NVIC_InitTypeDef结构体中的NVIC_IRQChannelPreemptionPriority与NVIC_IRQChannelSubPriority成员变量分别配置该中断的抢占优先级与亚优先级。所以NVIC的优先级配置需要三个要素:优先级分组、抢占优先级、亚优先级。
一个外部中断信号输入到NVIC中断控制器后怎么触发处理器执行中断服务程序呢?输入的中断请求信号只是触发一个中断悬起状态,这里的中断悬起状态可以理解为任务调度中介绍的任务就绪状态,处理器则是选择处于悬起状态优先级最高的中断服务程序执行。中断输入请求信号、中断悬起状态信号、中断活跃状态信号、处理器执行中断服务程序的关系及过程如下图示:
需要指出的是,中断请求信号被外设置位触发相应的中断悬起状态置位,在进入中断服务程序后需要开发者以软件方式清除中断请求标志,否则中断请求信号一直保持,将会导致中断返回后再次进入中断服务程序,不但浪费处理器资源而且影响下一次中断请求信号的输入。中断悬起状态被中断请求信号触发后置位,进入中断服务程序后会被硬件清除,所以开发者不需要以软件方式控制中断悬起状态,当然如果出于特定的目的想以软件方式清除中断悬起状态也是可以的,如果在进入中断服务程序前以软件方式清除中断悬起状态,则后面不会再执行中断服务程序。
如果在中断服务程序返回前又有了更高优先级的中断请求信号,则前一个中断保存状态并切换到更高优先级的中断服务程序执行,待返回后再继续执行前一个中断服务程序,这就是前面谈到的中断嵌套,其处理过程如下图示:
上图中的Handler处理者模式是运行于特权级的,CM3所有的异常或中断服务程序都运行于Handler处理者模式下,处理器的模式可以通过CONTROL控制寄存器配置。
NVIC中断控制器是外设与处理器的中间管理者,主要管理中断优先级、中断向量表、中断使能/掩蔽等,是外设中断信号进入处理器的必经之路,前面已经介绍了中断优先级、中断向量表相关的代码,下面给出中断控制器主要相关的寄存器:
直接管理这些寄存器的数据结构及相关寄存器地址如下:
// Libraries\CMSIS\CM3\CoreSupport\core_cm3.h
#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE + 0x0100) /*!< NVIC Base Address */
#define NVIC ((NVIC_Type *) NVIC_BASE) /*!< NVIC configuration struct */
typedef struct
{
__IO uint32_t ISER[8]; /*!< Offset: 0x000 Interrupt Set Enable Register */
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; /*!< Offset: 0x080 Interrupt Clear Enable Register */
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; /*!< Offset: 0x100 Interrupt Set Pending Register */
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; /*!< Offset: 0x180 Interrupt Clear Pending Register */
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; /*!< Offset: 0x200 Interrupt Active bit Register */
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; /*!< Offset: 0x300 Interrupt Priority Register (8Bit wide) */
uint32_t RESERVED5[644];
__O uint32_t STIR; /*!< Offset: 0xE00 Software Trigger Interrupt Register */
} NVIC_Type;
优先级、优先级分组、向量表偏移量寄存器前面都介绍过了,使能、悬起、活动状态寄存器比较简单,前面中断输入与悬起也介绍过作用,下面仅介绍下异常掩蔽寄存器,CM3除了R0~R15通用寄存器组外,还有几个特殊功能寄存器,其中PRIMASK, FAULTMASK, BASEPRI就属于特殊功能寄存器中的中断屏蔽寄存器,功能描述如下:
NVIC中断控制器的数据结构与操作函数前面已经介绍过了,主要就是NVIC_InitTypeDef结构体和下面这两个操作函数:
// Libraries\STM32F10x_StdPeriph_Driver\inc\misc.h
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
这些操作函数是联结NVIC_InitTypeDef与NVIC_Type的纽带,把面向开发者配置项的NVIC_InitTypeDef结构体的初始值配置到面向底层寄存器的NVIC_Type结构体成员中。
中断请求信号触发中断悬起后,要执行哪个中断服务程序?怎么找到相应中断服务程序的地址?前面提到中断服务程序也是靠优先级管理的,下一个要执行的是处于悬起状态优先级最高的中断服务程序,这很类似于任务调度过程。任务调度是使用任务就绪表找到处于就绪态最高优先级的任务地址的,中断系统则有一个与之类似的中断向量表(Linux系统对应IDT中断描述符表)找到处于悬起状态最高优先级的中断服务程序地址的。下面以STM32为例给出部分中断向量表供参考:
上面只给出了STM32中断向量表的一部分,已经能说明问题了,每个中断都有对应的优先级和中断服务程序的地址,方便处理器进行管理。如果在整个程序运行的生命周期内,都只给每个中断提供固定的中断服务程序(目前MCU开发的绝大多数情况),则可以把向量表放到ROM中,这种情况不需要重建向量表。如果想让自己的设备能随机应变的对付各种复杂情况,常常需要动态改变中断服务程序,更新向量表就是必需了,此时向量表需要转移到RAM中,这就需要向量表的重定位了,NVIC提供了一个”向量偏移量寄存器(VTOR)“支持中断向量的重定位。
需要强调的是中断向量表的起始地址是有要求的,需要先求出系统中共有多少个向量,再把这个数字向上增大到2的整次幂,而起始地址必须对齐到后者的边界上。向量表的重定位相当于重建,在重定位之前需要先把现有的向量表往新的位置复制一份,特别是系统异常的服务例程(上面蓝色背景标识的中断向量表部分)。STM32中断向量表的实现代码如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm\startup_stm32f10x_hd.s
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__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
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line 0
DCD EXTI1_IRQHandler ; EXTI Line 1
DCD EXTI2_IRQHandler ; EXTI Line 2
DCD EXTI3_IRQHandler ; EXTI Line 3
DCD EXTI4_IRQHandler ; EXTI Line 4
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1
DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2
DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3
DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4
DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5
DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6
DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7
DCD ADC1_2_IRQHandler ; ADC1 & ADC2
DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TX
DCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; EXTI Line 9..5
DCD TIM1_BRK_IRQHandler ; TIM1 Break
DCD TIM1_UP_IRQHandler ; TIM1 Update
DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
DCD RTCAlarm_IRQHandler ; RTC Alarm through EXTI Line
DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
DCD TIM8_BRK_IRQHandler ; TIM8 Break
DCD TIM8_UP_IRQHandler ; TIM8 Update
DCD TIM8_TRG_COM_IRQHandler ; TIM8 Trigger and Commutation
DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare
DCD ADC3_IRQHandler ; ADC3
DCD FSMC_IRQHandler ; FSMC
DCD SDIO_IRQHandler ; SDIO
DCD TIM5_IRQHandler ; TIM5
DCD SPI3_IRQHandler ; SPI3
DCD UART4_IRQHandler ; UART4
DCD UART5_IRQHandler ; UART5
DCD TIM6_IRQHandler ; TIM6
DCD TIM7_IRQHandler ; TIM7
DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1
DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2
DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
跟向量表重定向相关的操作函数如下:
// Libraries\STM32F10x_StdPeriph_Driver\inc\misc.h
#define NVIC_VectTab_RAM ((uint32_t)0x20000000)
#define NVIC_VectTab_FLASH ((uint32_t)0x08000000)
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
中断服务程序是中断悬起后,处理器在Handler特权级运行的中断处理任务,该中断服务程序的地址和优先级是如何注册到中断向量表中的呢?还记得前面NVIC的结构体NVIC_InitTypeDef吧,其中的NVIC_IRQChannel成员变量就是用于注册中断服务程序地址的,NVIC_IRQChannelPreemptionPriority与NVIC_IRQChannelSubPriority成员变量用于配置该中断信号或中断服务程序的优先级,NVIC_IRQChannelCmd则用于中断使能/除能配置。
进入中断服务程序后,需要先判断中断状态然后清除中断悬起状态位,以免因中断一直处于悬起状态扰乱中断的正常触发。下面给出一个简单的定时器中断处理程序示例:
// Libraries\STM32F10x_StdPeriph_Driver\src\stm32f10x_tim.c
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
//定时器TIM3初始化
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler =psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断
//中断优先级NVIC配置
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断服务程序函数指针
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //亚优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //中断使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
void TIM3_IRQHandler(void) //TIM3中断服务程序
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断是否发生
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIM3更新中断悬起位
... //要执行的动作
}
}
需要强调的一点是,所有的中断都要靠脉冲信号触发,脉冲信号需要时钟树的驱动,所以几乎所有的外设初始化都需要使能相应的时钟RCC。如果需要配置中断服务,则产生中断信号的外设和NVIC中断控制器都要配置,二者共同服务于整个中断处理过程。
SYSTICK是CM3提供的一个内部异常,也是操作系统常用的滴答定时器,与外部中断和片上外设中断不同的是,SYSTICK是CM3内部提供的私有外设,所以只要是ARM Cortex内核都支持SYSTICK系统滴答定时器。SYSTICK滴答定时器不仅可以用于操作系统任务调度的时基,而且可以用作延时函数,甚至以此为基础创建软件定时器等,下面简单谈下其主要配置与应用。
SYSTICK相关的主要寄存器及功能描述如下:
描述管理SYSTICK寄存器的数据结构与操作函数如下:
// Libraries\CMSIS\CM3\CoreSupport\core_cm3.h
#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define SysTick_BASE (SCS_BASE + 0x0010) /*!< SysTick Base Address */
#define SysTick ((SysTick_Type *) SysTick_BASE) /*!< SysTick configuration struct */
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x00 SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x04 SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x08 SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x0C SysTick Calibration Register */
} SysTick_Type;
#define __NVIC_PRIO_BITS 4 /*!< STM32 uses 4 Bits for the Priority Levels */
#define __Vendor_SysTickConfig 0 /*!< Set to 1 if different SysTick Config is used */
#if (!defined (__Vendor_SysTickConfig)) || (__Vendor_SysTickConfig == 0)
/**
* @brief Initialize and start the SysTick counter and its interrupt.
*
* @param ticks number of ticks between two interrupts
* @return 1 = failed, 0 = successful
*
* Initialise the system tick timer and its interrupt and start the
* system tick timer / counter in free running mode to generate
* periodical interrupts.
*/
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if (ticks > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = (ticks & SysTick_LOAD_RELOAD_Msk) - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Cortex-M0 System Interrupts */
SysTick->VAL = 0; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
#endif
static __INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if(IRQn < 0) {
SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for Cortex-M3 System Interrupts */
else {
NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } /* set Priority for device specific Interrupts */
}
上面的SysTick_Type结构体可以描述SYSTICK寄存器,SysTick_Config可以配置SYSTICK的优先级和初始值等。需要特别提到的一点是,由于SYSTICK是内部异常,所以其异常编号是负的(在IRQn_Type中定义),中断优先级使用SCB系统控制块的SHP[]数组管理,下面给出IRQn_Type的一部分与SCB_Type供参考,这些都是管理相关寄存器组的数据结构及相关寄存器地址:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\stm32f10x.h
typedef enum IRQn
{
/****** Cortex-M3 Processor Exceptions Numbers ***************************************************/
NonMaskableInt_IRQn = -14, /*!< 2 Non Maskable Interrupt */
MemoryManagement_IRQn = -12, /*!< 4 Cortex-M3 Memory Management Interrupt */
BusFault_IRQn = -11, /*!< 5 Cortex-M3 Bus Fault Interrupt */
UsageFault_IRQn = -10, /*!< 6 Cortex-M3 Usage Fault Interrupt */
SVCall_IRQn = -5, /*!< 11 Cortex-M3 SV Call Interrupt */
DebugMonitor_IRQn = -4, /*!< 12 Cortex-M3 Debug Monitor Interrupt */
PendSV_IRQn = -2, /*!< 14 Cortex-M3 Pend SV Interrupt */
SysTick_IRQn = -1, /*!< 15 Cortex-M3 System Tick Interrupt */
/****** STM32 specific Interrupt Numbers *********************************************************/
WWDG_IRQn = 0, /*!< Window WatchDog Interrupt */
PVD_IRQn = 1, /*!< PVD through EXTI Line detection Interrupt */
TAMPER_IRQn = 2, /*!< Tamper Interrupt */
RTC_IRQn = 3, /*!< RTC global Interrupt */
FLASH_IRQn = 4, /*!< FLASH global Interrupt */
RCC_IRQn = 5, /*!< RCC global Interrupt */
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10, /*!< EXTI Line4 Interrupt */
DMA1_Channel1_IRQn = 11, /*!< DMA1 Channel 1 global Interrupt */
DMA1_Channel2_IRQn = 12, /*!< DMA1 Channel 2 global Interrupt */
DMA1_Channel3_IRQn = 13, /*!< DMA1 Channel 3 global Interrupt */
DMA1_Channel4_IRQn = 14, /*!< DMA1 Channel 4 global Interrupt */
DMA1_Channel5_IRQn = 15, /*!< DMA1 Channel 5 global Interrupt */
DMA1_Channel6_IRQn = 16, /*!< DMA1 Channel 6 global Interrupt */
DMA1_Channel7_IRQn = 17, /*!< DMA1 Channel 7 global Interrupt */
...
} IRQn_Type;
#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define SCB_BASE (SCS_BASE + 0x0D00) /*!< System Control Block Base Address */
#define SCB ((SCB_Type *) SCB_BASE) /*!< SCB configuration struct */
typedef struct
{
__I uint32_t CPUID; /*!< Offset: 0x00 CPU ID Base Register */
__IO uint32_t ICSR; /*!< Offset: 0x04 Interrupt Control State Register */
__IO uint32_t VTOR; /*!< Offset: 0x08 Vector Table Offset Register */
__IO uint32_t AIRCR; /*!< Offset: 0x0C Application Interrupt / Reset Control Register */
__IO uint32_t SCR; /*!< Offset: 0x10 System Control Register */
__IO uint32_t CCR; /*!< Offset: 0x14 Configuration Control Register */
__IO uint8_t SHP[12]; /*!< Offset: 0x18 System Handlers Priority Registers (4-7, 8-11, 12-15) */
__IO uint32_t SHCSR; /*!< Offset: 0x24 System Handler Control and State Register */
__IO uint32_t CFSR; /*!< Offset: 0x28 Configurable Fault Status Register */
__IO uint32_t HFSR; /*!< Offset: 0x2C Hard Fault Status Register */
__IO uint32_t DFSR; /*!< Offset: 0x30 Debug Fault Status Register */
__IO uint32_t MMFAR; /*!< Offset: 0x34 Mem Manage Address Register */
__IO uint32_t BFAR; /*!< Offset: 0x38 Bus Fault Address Register */
__IO uint32_t AFSR; /*!< Offset: 0x3C Auxiliary Fault Status Register */
__I uint32_t PFR[2]; /*!< Offset: 0x40 Processor Feature Register */
__I uint32_t DFR; /*!< Offset: 0x48 Debug Feature Register */
__I uint32_t ADR; /*!< Offset: 0x4C Auxiliary Feature Register */
__I uint32_t MMFR[4]; /*!< Offset: 0x50 Memory Model Feature Register */
__I uint32_t ISAR[5]; /*!< Offset: 0x60 ISA Feature Register */
} SCB_Type;
NVIC_Type主要是配置外部中断、片上外设中断包括定时器中断等,片上CM3内核私有的内部设备则离不开SCB_Type相关寄存器的管理,前面提到的优先级分组也是使用SCB_Type中的成员变量设置的。另外需要提醒的一点是,在编写中断服务程序时,中断服务函数的名字是有要求的(详见IRQn_Type结构体),命名不规范则会因找不到相应的中断服务程序而报错。
上面SYSTICK的配置过程了解后,就可以配置SYSTICK的重装载值OS_TICKS_PER_SEC,配置好后SYSTICK就按设定的周期触发异常中断并执行相应的中断服务函数SysTick_Handler,UCOS中相关的配置代码如下:
// Micrium\Software\uCOS-II\Ports\ARM-Cortex-M3\Generic\RealView\os_cpu_c.c
#define OS_TICKS_PER_SEC 100u /* Set the number of ticks in one second */
void SysTick_Init(void)
{
if(SysTick_Config(SystemCoreClock/OS_TICKS_PER_SEC)) //10ms定时器
{
while(1);
}
//SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; //若无法启动则关闭
}
void SysTick_Handler(void)
{
if(delay_osrunning==1) //OS开始运行,才执行正常的调度处理
{
OSIntEnter(); //进入中断
OSTimeTick(); //调用UCOS的时钟服务程序
OSIntExit(); //触发任务切换软中断
}
}
// Micrium\Software\uCOS-II\Source\os_core.c
/*
*********************************************************************************************************
* PROCESS SYSTEM TICK
*
* Description: This function is used to signal to uC/OS-II the occurrence of a 'system tick' (also known
* as a 'clock tick'). This function should be called by the ticker ISR but, can also be
* called by a high priority task.
*
* Arguments : none
*
* Returns : none
*********************************************************************************************************
*/
void OSTimeTick (void)
{
OS_TCB *ptcb;
#if OS_TICK_STEP_EN > 0u
BOOLEAN step;
#endif
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_TIME_TICK_HOOK_EN > 0u
OSTimeTickHook(); /* Call user definable hook */
#endif
#if OS_TIME_GET_SET_EN > 0u
OS_ENTER_CRITICAL(); /* Update the 32-bit tick counter */
OSTime++;
OS_EXIT_CRITICAL();
#endif
if (OSRunning == OS_TRUE) {
ptcb = OSTCBList; /* Point at first TCB in TCB list */
while (ptcb->OSTCBPrio != OS_TASK_IDLE_PRIO) { /* Go through all TCBs in TCB list */
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly != 0u) { /* No, Delayed or waiting for event with TO */
ptcb->OSTCBDly--; /* Decrement nbr of ticks to end of delay */
if (ptcb->OSTCBDly == 0u) { /* Check for timeout */
if ((ptcb->OSTCBStat & OS_STAT_PEND_ANY) != OS_STAT_RDY) {
ptcb->OSTCBStat &= (INT8U)~(INT8U)OS_STAT_PEND_ANY; /* Yes, Clear status flag */
ptcb->OSTCBStatPend = OS_STAT_PEND_TO; /* Indicate PEND timeout */
} else {
ptcb->OSTCBStatPend = OS_STAT_PEND_OK;
}
if ((ptcb->OSTCBStat & OS_STAT_SUSPEND) == OS_STAT_RDY) { /* Is task suspended? */
OSRdyGrp |= ptcb->OSTCBBitY; /* No, Make ready */
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
}
}
}
ptcb = ptcb->OSTCBNext; /* Point at next TCB in TCB list */
OS_EXIT_CRITICAL();
}
}
}
#if (OS_CPU_HOOKS_EN > 0) && (OS_TIME_TICK_HOOK_EN > 0)
void OSTimeTickHook (void)
{
#if OS_APP_HOOKS_EN > 0
App_TimeTickHook();
#endif
#if OS_TMR_EN > 0
OSTmrCtr++;
if (OSTmrCtr >= (OS_TICKS_PER_SEC / OS_TMR_CFG_TICKS_PER_SEC)) {
OSTmrCtr = 0;
OSTmrSignal();
}
#endif
}
#endif
#if OS_TMR_EN > 0u
INT8U OSTmrSignal (void)
{
INT8U err;
err = OSSemPost(OSTmrSemSignal);
return (err);
}
#endif
上面的SysTick_Init()程序在部分程序示例中并没有出现,比如正点原子的例程把SYSTICK同样用于延时函数delay_us(u32 nus)与delay_ms(u16 nms)中,于是就把SYSTICK的配置工作放到了delay_init()中进行,用delay_init()取代了SysTick_Init()的工作。在SYSTICK中断服务函数SysTick_Handler(void)主要调用了三个函数,前面和后面的函数主要用于中断的嵌套管理,其中OSIntExit()还是任务调度器进行任务切换的重要驱动力(触发PendSV异常),中间函数OSTimeTick ()则是系统时钟服务程序,完成的任务较多,主要如下表所示:
OSTimeTick ()主要操作变量 | 基于该变量的扩展功能支持 |
---|---|
系统时间的递增 OSTime++ |
设置系统时间void OSTimeSet (INT32U ticks); 获取系统时间INT32U OSTimeGet (void); |
任务延时等待时间的递减 ptcb->OSTCBDly– |
当OSTCBDly减至零时相应任务进入就绪状态; 设置任务等待延时void OSTimeDly (INT32U ticks); 以时分秒方式设置任务等待延时INT8U OSTimeDlyHMSM (INT8U hours,INT8U minutes,INT8U seconds,INT16U ms); 将任务从等待延时中恢复至就绪态INT8U OSTimeDlyResume (INT8U prio); |
软件定时器的管理 OSTimeTickHook() OSTmrSignal() |
OSTimeTickHook()函数中定时器计数器达到要求会调用OSTmrSignal()发出一个定时器信号量,该信号量作为更新软件定时器的信号,详见static void OSTmr_Task (void *p_arg)函数; |
定时器其实就是一个递减计数器,当计数器递减到0的时候就会触发一个动作,这个动作就是回调函数,当定时器计时完成时就会自动的调用这个回调函数。因此我们可以使用这个回调函数来完成一些设计。比如协议栈处理、I/O设备定时轮询等,在回调函数中应避免任何可以阻塞或者删除定时任务的函数。
软件定时器同样由OSTimTick提供时钟(相当于由SYSTICK提供时钟),但是软件定时器的时钟还受OS_TMR_CFG_TICKS_PER_SEC设置的控制(详见OSTimeTickHook() 函数代码),也就是在UCOSII的时钟节拍上面再做了一次“分频”,软件定时器的最快时钟节拍就等于UCOSII的系统时钟节拍,这也决定了软件定时器的精度。
软件定时器的管理同任务管理和事件管理类似,也需要一个相应的控制块来管理一个定时器,任务管理是靠任务控制块TCB,事件管理是靠事件控制块ECB,定时器管理也有一个控制块OS_TMR,其数据结构如下:
// Micrium\Software\uCOS-II\Source\ucos_ii.h
#if OS_TMR_EN > 0u
typedef void (*OS_TMR_CALLBACK)(void *ptmr, void *parg);
typedef struct os_tmr {
INT8U OSTmrType; /* Should be set to OS_TMR_TYPE */
OS_TMR_CALLBACK OSTmrCallback; /* Function to call when timer expires */
void *OSTmrCallbackArg; /* Argument to pass to function when timer expires */
void *OSTmrNext; /* Double link list pointers */
void *OSTmrPrev;
INT32U OSTmrMatch; /* Timer expires when OSTmrTime == OSTmrMatch */
INT32U OSTmrDly; /* Delay time before periodic update starts */
INT32U OSTmrPeriod; /* Period to repeat timer */
#if OS_TMR_CFG_NAME_EN > 0u
INT8U *OSTmrName; /* Name to give the timer */
#endif
INT8U OSTmrOpt; /* Options (see OS_TMR_OPT_xxx) */
INT8U OSTmrState; /* Indicates the state of the timer: */
/* OS_TMR_STATE_UNUSED */
/* OS_TMR_STATE_RUNNING */
/* OS_TMR_STATE_STOPPED */
} OS_TMR;
#endif
单个定时器管理有了数据结构,多个定时器的调度应该也需要一个数据结构来管理吧?多任务调度靠任务就绪表,单个事件在多任务间通信靠任务等待表,多个有效定时器的调度则靠定时器轮OS_TMR_WHEEL,其数据结构与图示如下:
// Micrium\Software\uCOS-II\Source\ucos_ii.h
#if OS_TMR_EN > 0u
typedef struct os_tmr_wheel {
OS_TMR *OSTmrFirst; /* Pointer to first timer in linked list */
INT16U OSTmrEntries;
} OS_TMR_WHEEL;
#endif
其中OSTmrTbl[OS_TMR_CFG_MAX]保存了已建立的定时器控制块,单个定时器OS_TMR则是以双向链表的形式组织起来的,OSTmrFreeList则指向空闲定时器控制块链表首地址,OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]保存已启动的定时器分组,每个元素保存一个定时器轮,也即一个处于运行状态的定时器链表。
有了管理已建立和在运行的定时器的数据结构,下面简单介绍下UCOS提供的定时器操作函数如下:
函数 | 功能描述 |
---|---|
OS_TMR *OSTmrCreate (INT32U dly, INT32U period,INT8U opt, OS_TMR_CALLBACK callback, void *callback_arg, INT8U *pname, INT8U *perr) |
创建一个定时器并按照传入的参数初始化定时器控制块,并返回该定时器控制块指针; 其中opt支持两种选项: OS_TMR_OPT_ONE_SHOT单次定时器, OS_TMR_OPT_PERIODIC周期定时器; |
BOOLEAN OSTmrDel (OS_TMR *ptmr, INT8U *perr) |
删除该定时器控制块,并释放相应的资源; |
INT32U OSTmrRemainGet (OS_TMR *ptmr, INT8U *perr) |
获取该定时器的剩余时间; |
INT8U OSTmrStateGet (OS_TMR *ptmr, INT8U *perr) |
获取该定时器当前所处的状态,定时器主要有如下状态: OS_TMR_STATE_UNUSED, OS_TMR_STATE_STOPPED, OS_TMR_COMPLETED, OS_TMR_RUNNING; |
BOOLEAN OSTmrStart (OS_TMR *ptmr, INT8U *perr) |
启动该定时器,并将其插入到相应的定时器轮中; |
BOOLEAN OSTmrStop (OS_TMR *ptmr, INT8U opt, void *callback_arg, INT8U *perr) |
停止该定时器,并将其从相应的定时器轮中移除; 其中opt支持三种选项: OS_TMR_OPT_NONE停止定时器后什么都不做; OS_TMR_OPT_CALLBACK停止定时器后执行回调函数,回调参数使用创建该定时器时赋予的参数; OS_TMR_OPT_CALLBACK_ARG停止定时器后执行回调函数,回调参数使用该函数传入的参数; |
INT8U OSTmrSignal (void) | 发送一个信号量去更新软件定时器,由OSTimeTickHook()调用; |
接下来介绍下软件定时器的工作过程,前面也提到了,软件定时器主要是靠OSTimTick提供的时钟驱动的,定时器计数器达到设定值后会发送一个信号量去更新所有的软件定时器,主要驱动过程如下图示:
OSTmr_Task是UCOS初始化时创建的一个任务,具体调用过程为:主函数中调用OSInit()—>OSTmr_Init()—>OSTmr_InitTask()—>OSTaskCreate(OSTmr_Task,(void *)0,&OSTmrTaskStk[OS_TASK_TMR_STK_SIZE - 1u],OS_TASK_TMR_PRIO)—>OSTmr_Task (void *p_arg),这些都是在系统启动时自动执行的,也就是系统启动起来后OSTmr_Task任务已经创建好了,正处于等待定时器信号量状态,待执行完SysTick_Handler()—>OSTimeTick()—>OSTimeTickHook()—>OSTmrSignal()流程后会释放一个信号量完成定时器状态的更新。下面给出OSTmr_Task函数代码如下:
// Micrium\Software\uCOS-II\Source\os_tmr.c
/*
************************************************************************************************************************
* TIMER MANAGEMENT TASK
*
* Description: This task is created by OSTmrInit().
*
* Arguments : none
*
* Returns : none
************************************************************************************************************************
*/
#if OS_TMR_EN > 0u
static void OSTmr_Task (void *p_arg)
{
INT8U err;
OS_TMR *ptmr;
OS_TMR *ptmr_next;
OS_TMR_CALLBACK pfnct;
OS_TMR_WHEEL *pspoke;
INT16U spoke;
p_arg = p_arg; /* Prevent compiler warning for not using 'p_arg' */
for (;;) {
OSSemPend(OSTmrSemSignal, 0u, &err); /* Wait for signal indicating time to update timers */
OSSchedLock();
OSTmrTime++; /* Increment the current time */
spoke = (INT16U)(OSTmrTime % OS_TMR_CFG_WHEEL_SIZE); /* Position on current timer wheel entry */
pspoke = &OSTmrWheelTbl[spoke];
ptmr = pspoke->OSTmrFirst;
while (ptmr != (OS_TMR *)0) {
ptmr_next = (OS_TMR *)ptmr->OSTmrNext; /* Point to next timer to update because current ... */
/* ... timer could get unlinked from the wheel. */
if (OSTmrTime == ptmr->OSTmrMatch) { /* Process each timer that expires */
OSTmr_Unlink(ptmr); /* Remove from current wheel spoke */
if (ptmr->OSTmrOpt == OS_TMR_OPT_PERIODIC) {
OSTmr_Link(ptmr, OS_TMR_LINK_PERIODIC); /* Recalculate new position of timer in wheel */
} else {
ptmr->OSTmrState = OS_TMR_STATE_COMPLETED; /* Indicate that the timer has completed */
}
pfnct = ptmr->OSTmrCallback; /* Execute callback function if available */
if (pfnct != (OS_TMR_CALLBACK)0) {
(*pfnct)((void *)ptmr, ptmr->OSTmrCallbackArg);
}
}
ptmr = ptmr_next;
}
OSSchedUnlock();
}
}
#endif
定时器信号量更新处于运行状态的定时器(也即定时器轮中的定时器),若哪个定时器满足设定条件(定时器更新后的时间与设定时间一致),则将该定时器从定时器轮中移除(如果是周期定时器则需要重新插入相应的定时器轮开始下一周期的定时),并开始执行创建定时器时注册的用户回调函数。所以,软件定时器的关键作用还是完成用于在回调函数中要执行的任务,该任务是一次性的还是周期性执行的看开发者创建了什么类型的定时器,所有这些软件定时器都由SYSTICK提供的滴答定时器驱动。