中断作为单片机开发必须掌握的内容,它能够在不搭载操作系统的情况下让我们体验多任务处理的快感,保证了高优先级任务的实时性,同时系统中断也能够提供给用户在核心发生错误之后进行处理的机会。STM32F103系列单片机中断非常强大,每个外设都可以产生中断,F103 在内核基础上搭载了一个中断响应系统, 支持为数众多的系统中断和外部中断。
本篇文章介绍了在STM32平台实现摁键中断控制LED亮灭以及定时器中断控制LED灯周期亮灭的功能。文章首先系统的介绍了中断有关的概念,然后通过摁键以及定时器两个实例带领读者直观的了解中断的作用。
中断概念
下面介绍一些中断的概念,这些概念依托于STM32平台,不同的芯片平台会有出入。
- 中断:程序执行过程中CPU会遇到一些特殊情况,正在执行的程序被打断,cpu中止原来正在执行的程序,保存现场(保存上下文),转到处理异常情况或特殊事件的程序去执行,结束后恢复现场(被打断程序上下文),继续执行。
- 异常:异常是中断的一种类型,其中断源是芯片内部,中断原因为比如执行了未定义指令、算术溢出、除零运算等发生在CPU内部的意外事件,这些异常的发生,会引起CPU运行相应的异常处理程序。下面是Stm32异常中断。
- Reset:处理器在工作时, 突然收到复位信号, 就会触发该异常。
- NMI(Non Maskable Interrupt):不可屏蔽中断,产生这个中断的时候,表示系统发生了致命的错误。
- HardFault:硬件错误中断,数组越界,野指针,任务堆栈溢出,未初始化硬件却开始操作,或无中断服务函数等,都会导致这个中断产生。(总线地址不可访问等),Memory Management Fault(在只读写的区域尝试执行代码),Usage Fault(除零异常等)等异常都会导致硬件错误中断。
- MemManage:访问了内存管理单元(MPU)定义的不合法的内存区域,比如向只读区域写入数据。
- BusFault:在fetch指令、数据读写、fetch中断向量或中断时存储恢复寄存器栈情况下,检测到内存访问错误则产生。
- UsageFault:检测到未定义指令或在存取内存时有未对齐,检测到除数为0也产生该异常。
- SVC:系统服务调用,SVC异常由SVC指令触发,在很多系统中SVC机制用于实现应用任务访问系统资源。
- DebugMon:调试监视器(断电, 数据观察点, 或外部调试请求),大部分debug的时候就是调试的时候遇到。
- PendSV:可挂起的系统调用,由于PendSV在系统中被设置为最低优先级,因此只有当没有其他异常或者中断在执行时才会被执行。在一个操作系统环境中,当没有其他异常正在执行时,可以使用PendSV来进行上下文的切换。
- SysTick:系统定时中断,系统定时器中断 一般用在操作系统的延时 。
- 保存现场:保存现场是当发生异常/中断时,CPU跳转之前,就需要把r0-r3, r12, lr, psr这几个寄存器的值保存到栈中。
- 恢复现场:STM实现了恢复现场的机制:CPU进入异常/中断服务程序时,LR寄存器保存的并不是中断前下一条指令的地址,而是会保存一个特殊的数值,被称为EXC_RETURN。异常/中断返回时,LR寄存器赋值给PC的值是一个被称为EXC_RETURN的值,而一旦CPU识别到PC的值等于EXC_RETURN的话,那么就会触发异常/中断返回机制,这个机制会帮我们把保存在栈中r0-r3, r12, lr, psr的寄存器的值恢复回去。
- 中断抢占优先级和子优先级(又称响应优先级):所谓抢占式优先级和响应优先级,具有高抢占式优先级的中断可以在低抢占式优先级中断处理过程中被响应,即中断嵌套。当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个;如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断表中的排位顺序决定先处理哪一个。每一个中断源都必须定义2个优先级。
- 中断优先级分组:一般情况下,系统代码执行过程中,只设置一次中断优先级分组,比如分组2,设置好分组之后一般不会再改变分组。随意改变分组会导致中断管理混乱,程序出现意想不到的执行结果。不同分组方式会影响中断抢占优先级和子优先级范围。
实现原理
摁键检测
EXTI(External interrupt/event controller):外部中断/事件控制器,管理了控制器的 20个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI 可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。
实现流程大体分为以下三个部分:
- 配置GPIO相应引脚:通过按键产生一个电平信号,然后经EXTI处理传入NVIC产生中断的,所以要配置连接按键的GPIO引脚,主要是设置相应的引脚模式为浮空输入 。
- 配置EXTI并映射GPIO引脚:打开相关的时钟,使用GPIO_EXTILineConfig()函数映射中断IO口,然后配置EXIT,包括中断源,触发类型等。
- 编写中断服务函数:stm32f10x_it.c中提前根据中断向量表生成了空的中断函数,我们只需要找到对应的中断函数编写中断服务内容即可。
定时器
定时器(Timer)最基本的功能就是定时了,本例我们使用的是通用定时器,它的时钟源经过以下路径计算得到:SYSCLK经过PLL的得到为72M,AHB时钟 =SYSCLK,APB1时钟=72M/2=36M,最终CK_INT的时钟频率倍频两倍为72M. 计数器的最终的频率还需要经过PSC预分频计算才能得到,基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR),这三个寄存器都是 16 位有效数字,即可设置值为 0至 65535。
定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR两个寄存器值决定,这个也就是定时器的周期。比如我们需要一个 1s周期的定时器,具体这两个寄存器值该如何设置? 假设,我们先设置 TIMx_ARR寄存器值为 9999,即当 TIMx_CNT从 0开始计算,刚好等于 9999时生成事件,总共计数 10000次,那么如果此时时钟源周期为 100us即可得到刚好 1s的定时周期。 接下来问题就是设置 TIMx_PSC寄存器值使得 CK_CNT 输出为 100us 周期(10000Hz)的时钟。预分频器的输入时钟 CK_PSC为 90MHz,所以设置预分频器值为(9000-1)即可满足。
嵌入式程序
摁键检测实现LED灯亮灭
- 中断通用配置
static void NVIC_Config(void) /* 主要是配置中断源的优先级与打开使能中断通道 */ { NVIC_InitTypeDef NVIC_InitStruct ; /* 配置中断优先级分组(设置抢占优先级和子优先级的分配),在函数在misc.c */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1) ; /* 配置初始化结构体 在misc.h中 */ /* 配置中断源 在stm32f10x.h中 */ NVIC_InitStruct.NVIC_IRQChannel = KEY1_EXTI_IRQN ; /* 配置抢占优先级 */ NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1 ; /* 配置子优先级 */ NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0 ; /* 使能中断通道 */ NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ; /* 调用初始化函数 */ NVIC_Init(&NVIC_InitStruct) ; /* 对key2执行相同操作 */ NVIC_InitStruct.NVIC_IRQChannel = KEY2_EXTI_IRQN ; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1 ; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1 ; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ; NVIC_Init(&NVIC_InitStruct) ; }
- 外部中断配置
void EXTI_Config() /* 主要是连接EXTI与GPIO */ { GPIO_InitTypeDef GPIO_InitStruct ; EXTI_InitTypeDef EXTI_InitStruct ; NVIC_Config(); /* 初始化要与EXTI连接的GPIO */ /* 开启GPIOA与GPIOC的时钟 */ RCC_APB2PeriphClockCmd(KEY1_EXTI_GPIO_CLK | KEY2_EXTI_GPIO_CLK, ENABLE) ; GPIO_InitStruct.GPIO_Pin = KEY1_EXTI_GPIO_PIN ; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING ; GPIO_Init(KEY1_EXTI_GPIO_PORT , &GPIO_InitStruct) ; GPIO_InitStruct.GPIO_Pin = KEY2_EXTI_GPIO_PIN ; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING ; GPIO_Init(KEY2_EXTI_GPIO_PORT , &GPIO_InitStruct) ; /* 初始化EXTI外设 */ /* EXTI的时钟要设置AFIO寄存器 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE) ; /* 选择作为EXTI线的GPIO引脚 */ GPIO_EXTILineConfig( KEY1_GPIO_PORTSOURCE , KEY1_GPIO_PINSOURCE) ; /* 配置中断or事件线 */ EXTI_InitStruct.EXTI_Line = KEY1_EXTI_LINE ; /* 使能EXTI线 */ EXTI_InitStruct.EXTI_LineCmd = ENABLE ; /* 配置模式:中断or事件 */ EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt ; /* 配置边沿触发 上升or下降 */ EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising ; EXTI_Init(&EXTI_InitStruct) ; GPIO_EXTILineConfig( KEY2_GPIO_PORTSOURCE , KEY2_GPIO_PINSOURCE) ; EXTI_InitStruct.EXTI_Line = KEY2_EXTI_LINE ; EXTI_InitStruct.EXTI_LineCmd = ENABLE ; EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt ; EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling ; EXTI_Init(&EXTI_InitStruct); }
- 中断服务函数与主函数
void EXTI0_IRQHandler(void) { if( EXTI_GetITStatus(KEY1_EXTI_LINE)!=RESET) { LED1_TOGGLE; //LED1的亮灭状态反转 } EXTI_ClearITPendingBit(KEY1_EXTI_LINE); } void EXTI15_10_IRQHandler(void) { if( EXTI_GetITStatus(KEY2_EXTI_LINE)!=RESET) { LED2_TOGGLE; //LED2的亮灭状态反转 } EXTI_ClearITPendingBit(KEY2_EXTI_LINE); } / #include "stm32f10x.h" #include "bsp_led.h" #include "bsp_key.h" int main(void) { LED_GPIO_Config(); EXTI_Config(); while(1) { } }
定时器实现LED灯闪烁
代码主要包含以下三个部分:
- 定时器初始化:TIM3时钟使能,设置TIM3_ARR和TIM3_PSC的值,设置TIM3_DIER允许更新中断,允许TIM3工作,TIM3中断分组设置。
- 编写中断服务函数:实现LED灯亮灭。
- 主函数:调用相关接口,完成IO口和定时器启动。
//定时器初始化
void TIM3_Int_Init()
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
TIM_TimeBaseStructure.TIM_Prescaler =7199; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_Period = 4999; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_ITConfig(TIM3, TIM_IT_Update,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIMx外设
}
//定时器中断服务函数
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx的中断待处理位:TIM 中断源
LED1=!LED1;
}
}
//主函数
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
LED_Init(); //初始化与LED连接的硬件接口
TIM3_Int_Init(); //10Khz的计数频率,计数到5000为500ms
while(1)
{
LED0=!LED0;
delay_ms(200);
}
}
十六宿舍 原创作品,转载必须标注原文链接。
©2023 Yang Li. All rights reserved.
欢迎关注 『十六宿舍』,大家喜欢的话,给个👍,更多关于嵌入式相关技术的内容持续更新中。