中断
F103 在内核水平上搭载了一个异常响应系统,支持为数众多的系统异常和外部中断。其中系统异常有8 个(如果把Reset 和HardFault 也算上的话就是10 个),外部中断有60 个。
中断-1 NVIC 结构体定义,来自固件库头文件:core_cm3.h
typedef struct {
__IO uint32_t ISER[8]; // 中断使能寄存器
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; // 中断清除寄存器
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; // 中断使能悬起寄存器
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; // 中断清除悬起寄存器
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; // 中断有效位寄存器
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; // 中断优先级寄存器(8Bit wide)
uint32_t RESERVED5[644];
__O uint32_t STIR; // 软件触发中断寄存器
} NVIC_Type;
在配置中断的时候我们一般只用ISER、ICER 和IP 这三个寄存器,ISER 用来使能中断,ICER 用来失能中断,IP 用来设置中断优先级。
NVIC库函数 | 描述 |
---|---|
void NVIC_ EnableIRQ(IRQn_ Type IRQn) | 使能中断 |
void NVIC_ DisableIRQ(IRQn_ Type IRQn) | 失能中断 |
void NVIC_ SetPendingIRQ(IRQn Type IRQn) | 设置中断悬起位 |
void NVIC_ ClearPendingIRQ(IRQn Type IRQn) | 清除中断悬起位 |
uint32_ t NVIC_ GetPendingIRQ(IRQn_ Type IRQn) | 获取悬起中断编号 |
void NVIC_ SetPriority(IRQn_ Type IRQn, uint32_ _t priority) | 设置中断优先级 |
uint32_ t NVIC_ GetPriority(IRQn _Type IRQn) | 获取中断优先级 |
void NVIC_ SystemReset(void) | 系统复位 |
在NVIC 有一个专门的寄存器:中断优先级寄存器NVIC_IPRx,用来配置外部中断的优先级,IPR宽度为8bit,原则上每个外部中断可配置的优先级为0~255,数值越小,优先级越高。但是绝大多数CM3 芯片都会精简设计,以致实际上支持的优先级数减少,在F103 中,只使用了高4bit,用于表达优先级的这4bit,又被分组成抢占优先级(主优先级)和子优先级。
在配置每个中断的时候一般有3 个编程要点:
- 使能外设某个中断,这个具体由每个外设的相关中断使能位控制。比如串口有发送完成中断,接收完成中断,这两个中断都由串口控制寄存器的相关中断使能位控制。
- 初始化NVIC_InitTypeDef 结构体,配置中断优先级分组,设置抢占优先级和子优先级,使能中断请求。NVIC_InitTypeDef 结构体在固件库头文件misc.h 中定义。
typedef struct {
uint8_t NVIC_IRQChannel; // 中断源
uint8_t NVIC_IRQChannelPreemptionPriority; // 抢占优先级
uint8_t NVIC_IRQChannelSubPriority; // 子优先级
FunctionalState NVIC_IRQChannelCmd; // 中断使能或者失能
} NVIC_InitTypeDef;
- a.NVIC_IROChannel:用来设置中断源(60个外部中断),不同的中断中断源不一样,且不可写错,即使写错了程序也不会报错,只会导致不响应中断。具体的成员配置可参考stm32f10x.h 头文件里面的IRQn_Type结构体定义,这个结构体包含了所有的中断源。
typedef enum IRQn {
//Cortex-M3 处理器异常编号
NonMaskableInt_IRQn = -14,
MemoryManagement_IRQn = -12,
BusFault_IRQn = -11,
UsageFault_IRQn = -10,
SVCall_IRQn = -5,
DebugMonitor_IRQn = -4,
PendSV_IRQn = -2,
SysTick_IRQn = -1,
//STM32 外部中断编号
WWDG_IRQn = 0,
PVD_IRQn = 1,
TAMP_STAMP_IRQn = 2,
// 限于篇幅,中间部分代码省略,具体的可查看库文件stm32f10x.h
DMA2_Channel2_IRQn = 57,
DMA2_Channel3_IRQn = 58,
DMA2_Channel4_5_IRQn = 59
} IRQn_Type;
一共有0-15 16组,分组1:主优先级0个,子优先级16个。分组2:主优先级2个,子优先级8个,总共2*8=16个不变。。。。。。。
2. b. NVIC_IRQChannelPreemptionPriority:抢占优先级, between 0 and ?具体的值要根据优先级分组来确定,具体参考表格优先级分组真值表优先级分组真值表。
c. NVIC_IRQChannelSubPriority:子优先级,具体的值要根据优先级分组来确定,具体参考表格优先级分组真值表优先级分组真值表。
d. NVIC_IRQChannelCmd:中断使能(ENABLE)或者失能(DISABLE)。操作的是VIC_ISER和NVIC_ICER 这两个寄存器。
3. 编写中断服务函数
在启动文件startup_stm32f10x_hd.s 中我们预先为每个中断都写了一个中断服务函数,只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需要我们重新编写,为了方便管理我们把中断服务函数统一写在stm32f10x_it.c 这个库文件中。
关于中断服务函数的函数名必须跟启动文件里面预先设置的一样,如果写错,系统就在中断向量表中找不到中断服务函数的入口,直接跳转到启动文件里面预先写好的空函数,并且在里面无限循环,实现不了中断。
EXTI——外部中断
EXTI(External interrupt/event controller)—外部中断/事件控制器,管理了控制器的20 个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。EXTI 可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。
EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件编号8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器TIM、模拟数字转换器ADC 等等,这样的脉冲信号一般用来触发TIM 或者ADC开始转换。
产生中断线路目的是把输入信号输入到NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
EXTI初始化结构体
typedef struct {
uint32_t EXTI_Line; // 中断/事件线
EXTIMode_TypeDef EXTI_Mode; // EXTI 模式
EXTITrigger_TypeDef EXTI_Trigger; // 触发类型
FunctionalState EXTI_LineCmd; // EXTI 使能
} EXTI_InitTypeDef;
- EXTI_Line:EXTI 中断/事件线选择,可选EXTI0 至EXTI19,可参考表EXTI 中断_ 事件线选择。
- EXTI_Mode:EXTI 模式选择,可选为产生中断(EXTI_Mode_Interrupt) 或者产生事件(EXTI_Mode_Event)。
- EXTI_Trigger:EXTI 边沿触发事件,可选上升沿触发(EXTI_Trigger_Rising)、下降沿触发(EXTI_Trigger_Falling) 或者上升沿和下降沿都触发( EXTI_Trigger_Rising_Falling)。
- EXTI_LineCmd:控制是否使能EXTI 线,可选使能EXTI 线(ENABLE) 或禁用(DISABLE)。
外部中断控制实验-按键中断
编程要点
- 初始化用来产生中断的GPIO(中断源);
- 初始化EXTI;
- 配置NVIC;
- 编写中断服务函数;
在上面的宏定义中,我们除了开GPIO 的端口时钟外,我们还打开了AFIO 的时钟,这是因为
等下配置EXTI 信号源的时候需要用到AFIO 的外部中断控制寄存器AFIO_EXTICRx,
嵌套向量中断控制器NVIC 配置
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 配置NVIC 为优先级组1 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* 配置中断源:按键1 */
NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ;
/* 配置抢占优先级:1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 配置子优先级:1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断通道*/
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 配置中断源:按键2,其他使用上面相关配置*/
NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
/* 开启按键GPIO 口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_INT_GPIO_CLK,ENABLE);
/* 配置NVIC 中断*/
NVIC_Configuration();
/*--------------------------KEY1 配置---------------------*/
/* 选择按键用到的GPIO */
GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN;
/* 配置为浮空输入*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);
/* 选择EXTI 的信号源*/
GPIO_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE, \
KEY1_INT_EXTI_PINSOURCE);
EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE;
/* EXTI 为中断模式*/
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 上升沿中断*/
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
/* 使能中断*/
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/*--------------------------KEY2 配置------------------*/
/* 选择按键用到的GPIO */
GPIO_InitStructure.GPIO_Pin = KEY2_INT_GPIO_PIN;
/* 配置为浮空输入*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);
/* 选择EXTI 的信号源*/
GPIO_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE, \
KEY2_INT_EXTI_PINSOURCE);
EXTI_InitStructure.EXTI_Line = KEY2_INT_EXTI_LINE;
/* EXTI 为中断模式*/
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 下降沿中断*/
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
/* 使能中断*/
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
使用GPIO 之前必须开启GPIO 端口的时钟;用到EXTI 必须开启AFIO 时钟。
作为中断/事件输入线时需把GPIO 配置为输入模式,具体为浮空输入,由外部电路完全决定引脚的状态。
GPIO_EXTILineConfig 函数用来指定中断/事件线的输入源,它实际是设定外部中断配置寄存器的AFIO_EXTICRx 值,该函数接收两个参数,第一个参数指定GPIO 端口源,第二个参数为选择对应GPIO 引脚源编号。
我们的目的是产生中断,执行中断服务函数,EXTI 选择中断模式,按键1 使用上升沿触发方式,并使能EXTI 线。
按键2 基本上采用与按键1 相关参数配置,只是改为下降沿触发方式。
两个按键的电路是一样的,可代码中我们设置按键1 是上升沿中断,按键2 是下降沿中断,有人就会问这是不是设置错了?实际上可以这么理解,按键1 检测的是按键按下的状态,按键2 检测的是按键弹开的状态,那这样就解释的通了。
EXTI中断服务函数
void KEY1_IRQHandler(void)
{
//确保是否产生了EXTI Line 中断
if (EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET)
{
// LED1 取反
LED1_TOGGLE;
//清除中断标志位
EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);
}
}
void KEY2_IRQHandler(void)
{
//确保是否产生了EXTI Line 中断
if (EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET)
{
// LED2 取反
LED2_TOGGLE;
//清除中断标志位
EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);
}
}
当中断发生时,对应的中断服务函数就会被执行,我们可以在中断服务函数实现一些控制。
一般为确保中断确实发生,我们会在中断服务函数中调用中断标志位状态读取函数读取外设中断标志位并判断标志位状态。
EXTI_GetITStatus 函数用来获取EXTI 的中断标志位状态,如果EXTI 线有中断发生函数返回“SET”否则返回“RESET”。实际上,EXTI_GetITStatus 函数是通过读取EXTI_PR 寄存器值来判断EXTI 线状态的。
按键1 的中断服务函数我们让LED1 翻转其状态,按键2 的中断服务函数我们让LED2 翻转其状态。执行任务后需要调用EXTI_ClearITPendingBit 函数清除EXTI 线的中断标志位。