最近要用nRF51822进行项目的多任务调度,其中不可避免的要用到要用到nRF51822的相关中断
首先,nRF51822是基于Cortex-M0架构的MCU,根据nRF51822的软件开发包中的core-m0.h文件夹,可以找到51822的相关中断编号定义(Interrupt Number Definition)
<span style="font-size:18px;">/* ------------------------- Interrupt Number Definition ------------------------ */
typedef enum {
/* ------------------- Cortex-M0 Processor Exceptions Numbers ------------------- */
Reset_IRQn = -15, /*!< 1 Reset Vector, invoked on Power up and warm reset */
NonMaskableInt_IRQn = -14, /*!< 2 Non maskable Interrupt, cannot be stopped or preempted */
HardFault_IRQn = -13, /*!< 3 Hard Fault, all classes of Fault */
SVCall_IRQn = -5, /*!< 11 System Service Call via SVC instruction */
DebugMonitor_IRQn = -4, /*!< 12 Debug Monitor */
PendSV_IRQn = -2, /*!< 14 Pendable request for system service */
SysTick_IRQn = -1, /*!< 15 System Tick Timer */
/* ---------------------- nRF51 Specific Interrupt Numbers ---------------------- */
POWER_CLOCK_IRQn = 0, /*!< 0 POWER_CLOCK */
RADIO_IRQn = 1, /*!< 1 RADIO */
UART0_IRQn = 2, /*!< 2 UART0 */
SPI0_TWI0_IRQn = 3, /*!< 3 SPI0_TWI0 */
SPI1_TWI1_IRQn = 4, /*!< 4 SPI1_TWI1 */
GPIOTE_IRQn = 6, /*!< 6 GPIOTE */
ADC_IRQn = 7, /*!< 7 ADC */
TIMER0_IRQn = 8, /*!< 8 TIMER0 */
TIMER1_IRQn = 9, /*!< 9 TIMER1 */
TIMER2_IRQn = 10, /*!< 10 TIMER2 */
RTC0_IRQn = 11, /*!< 11 RTC0 */
TEMP_IRQn = 12, /*!< 12 TEMP */
RNG_IRQn = 13, /*!< 13 RNG */
ECB_IRQn = 14, /*!< 14 ECB */
CCM_AAR_IRQn = 15, /*!< 15 CCM_AAR */
WDT_IRQn = 16, /*!< 16 WDT */
RTC1_IRQn = 17, /*!< 17 RTC1 */
QDEC_IRQn = 18, /*!< 18 QDEC */
LPCOMP_COMP_IRQn = 19, /*!< 19 LPCOMP_COMP */
SWI0_IRQn = 20, /*!< 20 SWI0 */
SWI1_IRQn = 21, /*!< 21 SWI1 */
SWI2_IRQn = 22, /*!< 22 SWI2 */
SWI3_IRQn = 23, /*!< 23 SWI3 */
SWI4_IRQn = 24, /*!< 24 SWI4 */
SWI5_IRQn = 25 /*!< 25 SWI5 */
} IRQn_Type;
</span>
在进行中断处理时,一般都要按照这个步骤:1、对要用到的中断初始化;2、对用到的中断进行使能;3、对中断进行优先级设置
1、中断初始化
nRF51822的外部I/O(暂时理解的程度以及用过的中断只有I/O中断,其他中断没有实践过)中断是基于任务和事件模式的。
按照nRF51822用户手册,每个GPIOTE通道的以下输入条件可以产生一个事件(在此可将事件理解为一个中断):上升沿、下降沿或者任何变化。因此,在进行中断初始化的时候,首先要将相应的中断输入I/O引脚设置为输入模式。然后通过CONFIG[n]设置GPIOTE通道的模式MODE(任务或者事件)、关联任务OUT[n]或者事件IN[n]的引脚PSEL,触发方式POLARITY。例如下面是将ROCKER_INTERRUPT引脚初始化为事件下降沿触发的事件IN[0]。
<pre name="code" class="objc">NRF_GPIOTE->CONFIG[0]=(GPIOTE_CONFIG_POLARITY_HiToLo<<GPIOTE_CONFIG_POLARITY_Pos)
|(ROCKER_INTERRUPT<<GPIOTE_CONFIG_PSEL_Pos)
|(GPIOTE_CONFIG_MODE_Event<<GPIOTE_CONFIG_MODE_Pos);
在对相关事件的触发方式以及关联引脚初始化完成之后,还需要通过INTENSET寄存器进行使能
NRF_GPIOTE->INTENSET=GPIOTE_INTENSET_IN0_Set<<GPIOTE_INTENSET_IN0_Pos; //ʹÄÜIN0ʼþ
2、中断使能
在中断初始化中仅仅是对事件n的使能,在此要对相应的中断进行使能。
类比于Cortex-M3架构,在MDK内与NVIC相关的寄存器,MDK为其定义了如下的结构体
NVIC(Nested Vectored Interrupt Controller)嵌套中断向量控制器
<span style="font-size:14px;">/** \ingroup CMSIS_core_registerISER[1]:中断使能寄存器。由前边可以看到,nRF51的寄存器编号为0-25,在这里ISER[1]为32位寄存器,总共可以表示32个中断,bit0-bit25分别对应中断0-25(其实后来发现在NVIC_EnableIRQ()函数中是将1左移IRQn位之后又与0x1F,也就是空出了低五位,所以在这里应该是对应的高地址,而把低位给空出来的)。要使能某个中断,必须设置相应的ISER位为1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、I/O口映射等设置才算是一个完整的中断设置)。此处是按照STM32中断设置理解的。
\defgroup CMSIS_NVIC Nested Vectored Interrupt Controller (NVIC)
\brief Type definitions for the NVIC Registers
@{
*/
/** \brief Structure type to access the Nested Vectored Interrupt Controller (NVIC).
*/
typedef struct
{
__IO uint32_t ISER[1]; /*!< Offset: 0x000 (R/W) Interrupt Set Enable Register */
uint32_t RESERVED0[31];
__IO uint32_t ICER[1]; /*!< Offset: 0x080 (R/W) Interrupt Clear Enable Register */
uint32_t RSERVED1[31];
__IO uint32_t ISPR[1]; /*!< Offset: 0x100 (R/W) Interrupt Set Pending Register */
uint32_t RESERVED2[31];
__IO uint32_t ICPR[1]; /*!< Offset: 0x180 (R/W) Interrupt Clear Pending Register */
uint32_t RESERVED3[31];
uint32_t RESERVED4[64];
__IO uint32_t IP[8]; /*!< Offset: 0x300 (R/W) Interrupt Priority Register */
} NVIC_Type;
/*@} end of group CMSIS_NVIC */</span>
ICER[1]:中断除能寄存器,和ISER的作用恰好相反,用来清除某个中断的使能的。专门设置一个ICER来清除中断位,而不是向ISER写0来清除,这是因为NVIC的这些寄存器都是写1有效的,写0无效的。
ISPR[1]:中断挂起控制寄存器。通过置1可以将正在进行的中断挂起,而执行同级或者更高级别的中断。
ICPR[1]:中断解挂控制寄存器。通过置1可以将挂起的中断解挂。
IP[8]:中断优先级控制寄存器组。这个寄存器组相当重要。中断分组与这个寄存器密切相关。
在这里插入一下中断优先级的理解。因为以前都是用的STM32,在这里就按照Cortex-M3架构来理解。Cortex-M3中有两个优先级的概念——抢占优先级和响应优先级,有人把响应优先级称作“亚优先级”和“副优先级”,每个中断源都需要被指定这两种优先级。
具有高抢占优先级的中断可以在具有低抢占优先级的中断处理过程中被响应,即中断嵌套。当两个中断源的抢占优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个(在《原子教你玩STM32》中说在抢占优先级相同的中断,高优先级的响应优先级不可打断低响应优先级的中断,有待考证);如果他们的抢占优先级和响应优先级相等,则根据他们在中断表中的排位顺序来决定先处理哪一个。
在STM32中需要用NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)先对中断优先级进行分组,这个分组在一个程序中只能设定一次,是在中断与复位控制寄存器SCB->AIRCR中设置的,也就是分组之后所有的中断只能按照这个分组方式进行设定抢占优先级和响应优先级。
在对中断优先级进行分组设置之后,要对每个中断的中断源优先级进行设置,也就是设置中断优先级控制寄存器IP,中断源优先级具体设置了该中断源的优先级别。
在此先说一下STM32中断优先级设置的步骤:
系统运行开始的时候设置中断分组,确定组号,也就是确定抢占优先级和子优先级的分配位数,调用函数为NVIC_PriorityGroupConfig();
设置用到的中断的中断优先级别,对每个中断调用函数为NVIC_Init(NVIC_InitTypeDef *NVIC_InitStruct);
而在nRF51822中,我现在还没有碰到中断优先级进行分组的情况,在此就先不写,如果将来用到,再进行相关补充
首先是中断使能,和STM32中在初始化NVIC_InitStruct结构体的同时完成中断使能不同,nRF51822通过专门的函数NVIC_EnableIRQ(IRQn_Type IRQn)来实现响应中断使能,形参变量IRQn为要进行初始化的中断编号,在本博文的开头已经给出,在SDK的core-m0.h中定义,以下为中断使能函数。
/** \brief Enable External Interrupt
The function enables a device-specific interrupt in the NVIC interrupt controller.
\param [in] IRQn External interrupt number. Value cannot be negative.
*/
__STATIC_INLINE void NVIC_EnableIRQ(IRQn_Type IRQn)
{
NVIC->ISER[0] = (1 << ((uint32_t)(IRQn) & 0x1F));
}
/** \brief Disable External Interrupt
The function disables a device-specific interrupt in the NVIC interrupt controller.
\param [in] IRQn External interrupt number. Value cannot be negative.
*/
__STATIC_INLINE void NVIC_DisableIRQ(IRQn_Type IRQn)
{
NVIC->ICER[0] = (1 << ((uint32_t)(IRQn) & 0x1F));
}
例如,要使能GPIOTE中断,则调用
NVIC_EnableIRQ(GPIOTE_IRQn);
3、中断优先级设置
中断优先级设置源函数如下:
/** \brief Set Interrupt Priority
The function sets the priority of an interrupt.
\note The priority cannot be set for every core interrupt.
\param [in] IRQn Interrupt number.
\param [in] priority Priority to set.
*/
__STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if(IRQn < 0) {
SCB->SHP[_SHP_IDX(IRQn)] = (SCB->SHP[_SHP_IDX(IRQn)] & ~(0xFF << _BIT_SHIFT(IRQn))) |
(((priority << (8 - __NVIC_PRIO_BITS)) & 0xFF) << _BIT_SHIFT(IRQn)); }
else {
NVIC->IP[_IP_IDX(IRQn)] = (NVIC->IP[_IP_IDX(IRQn)] & ~(0xFF << _BIT_SHIFT(IRQn))) |
(((priority << (8 - __NVIC_PRIO_BITS)) & 0xFF) << _BIT_SHIFT(IRQn)); }
}
#define __NVIC_PRIO_BITS 2 /*!< Number of Bits used for Priority Levels */
/* Interrupt Priorities are WORD accessible only under ARMv6M */
/* The following MACROS handle generation of the register offset and byte masks */
#define _BIT_SHIFT(IRQn) ( (((uint32_t)(IRQn) ) & 0x03) * 8 )
#define _SHP_IDX(IRQn) ( ((((uint32_t)(IRQn) & 0x0F)-8) >> 2) )
#define _IP_IDX(IRQn) ( ((uint32_t)(IRQn) >> 2) )
先来看_IP_IDX(IRQn)的具体意思,在对应的宏定义中,将中断编号IRQn强制转换为uint32_t类型,在这里我的理解是因为中断优先级控制寄存器是32bit的,将来为了写寄存器以及移位的需要,故将其强制转换为32位。在宏定义中将其右移两位,右移两位是因为中断优先级控制寄存器组IP由8个32位的寄存器组成,每个可屏蔽中断占8位,这样也就是4个中断占用一个中断优先级控制寄存器,将中断编号右移两位之后所得的刚好是中断所在的寄存器的编号。观察下表可以对中断编号右移2位之后产生的作用一目了然。
移动之后对应的寄存器编号 | 中断编号 | 对应的二进制数 | 右移两位所得二进制数 |
0 | 0 | 0000 | 00 |
0 | 1 | 0001 | 00 |
0 | 2 | 0010 | 00 |
0 | 3 | 0011 | 00 |
1 | 4 | 0100 | 01 |
1 | 5 | 0101 | 01 |
1 | 6 | 0110 | 01 |
1 | 7 | 0111 | 01 |
2 | 8 | 1000 | 10 |
2 | 9 | 1001 | 10 |
2 | 10 | 1010 | 10 |
2 | 11 | 1011 | 10 |
3 | 12 | 1100 | 11 |
3 | 13 | 1101 | 11 |
3 | 14 | 1110 | 11 |
3 | 15 | 1111 | 11 |
4 | 16 | 010000 | 0100 |
4 | 17 | 010001 | 0100 |
4 | 18 | 010010 | 0100 |
4 | 19 | 010011 | 0100 |
5 | 20 | 010100 | 0101 |
5 | 21 | 010101 | 0101 |
5 | 22 | 010110 | 0101 |
5 | 23 | 010111 | 0101 |
6 | 24 | 011000 | 0110 |
#define _BIT_SHIFT(IRQn) ( (((uint32_t)(IRQn) ) & 0x03) * 8 )</span>
由上边表格可以看到,中断编号IRQn右移两位余下的部分对应的是中断优先级控制寄存器编号,而移出去的(也就是最低两位)则是对应的中断优先级控制寄存器对应的得位号。每个中断占用8位,也就是[31-24]、[23-16]、[15-8]、[7-0]对应该组中断的3-0。由上表观察可以知道,中断编号IRQn的最低两位代表在某个中断优先控制寄存器(0-8)中对应的位号。因此,利用宏定义_BIT_SHIFT(IRQn)对应要设置的某中断对应的某个中断优先控制寄存器的起始位号。例如:要设置的是外部IO中断,其中断编号GPIOTE被宏定义为6,6的二级制为0110,与0x03与之后,为10(二进制),也就是对应1号中断优先控制寄存器的第23-16位,因此,要对其进行设置中断优先级,要设置的也就是1号中断优先级控制寄存器的第23-16位。在移位操作时,也就是移动2*8位,因此在宏定义中有*8的操作。在设置之前,首先要将其他位进行屏蔽,以免对其他中断编号产生误操作,这也就是NVIC_SetPriority()函数中或号之前的作用,其实也就是屏蔽作用。
在这里将其乘以8即可得到所对应的中断对应的寄存器中的最低位,在将来将设定的中断优先级直接左移_BIT_SHIFT(IRQn)位即可将其写入中断控制寄存器。
<span style="font-size:14px;">#define __NVIC_PRIO_BITS 2 /*!< Number of Bits used for Priority Levels */</span>由_NVIC_PRIO_BITS的宏定义可知,中断等级只有两位,也就是只可以设定四个中断优先级。每个中断的8bit没有全部使用,按照nRF5188官方SDK给出的宏定义可以看出,在此只用了其中2位。8-_NVIC_PRIO_BITS也就确定了将所设定的中断优先级priority左移6位,达到中断所用8bit的高2位,然后与0xFF相与,再左移_BIT_SHIFT(IRQn)即可得出需要写入对应的中断优先级控制寄存器的值。为了将中断对应的位屏蔽,在此通过NVIC->IP[_IP_IDX(IRQn)]&~(0xFF<<_BIT_SHIFT(IRQ))将其他位置1。其中对0xFF<<_BIT_SHIFT(IRQ)取反刚好将其他位置1,与NVIC->IP[_IP_IDX(IRQn)]相与,即可将需要设定的中断寄存器其他位屏蔽。然后二者相或,即可对对应的寄存器完成设定。
在《低功耗蓝牙开发与实战》中,仅仅说是通过下述语句来配置中断优先级为低优先级的,具体如何实现现在不是太清楚,而这个中断优先级设置在青风所给出的教程中根本就没有用到,下步需要查阅Cortex-M3架构的NVIC和中断控制,搞明白NVIC_SetPriority()的作用与具体功能。
NVIC_SetPriority(GPIOTE_IRQn,3);
中断处理函数
进行完这三步设置之后,就可以编写相应的中断处理函数了。
在此完全是自己的理解,有待进行验证。自己感觉中断处理函数命名是由上面配置的中断决定的,例如上面配置的中断是GPIOTE_IRQn,则中断处理函数的名称就是GPIOTE_IRQHandler;如果配置的中断是TIMER1_IRQn,则中断处理函数的名称就是TIMER1_IRQHandler。
进入中断处理函数之后,一般的操作步骤是:1、判断中断类型(是否是所需的中断以及是否使能过);2、对中断标志进行清零,以便程序下次能够正常进入中断;3、进行中断处理
下面为51822的某I/O中断处理函数
void GPIOTE_IRQHandler(void)
{
if((NRF_GPIOTE->EVENTS_IN[0]==1)&&(NRF_GPIOTE->INTENSET&GPIOTE_INTENSET_IN0_Msk))
{
NRF_GPIOTE->EVENTS_IN[0]=0;
rocker_ready_flag=1;
}
}