本篇内容是在博主在配置外设中断实践过程中不断调试的理解和感悟,内容以库函数开发和寄存器开发为例子,希望能够给在单片机初学阶段的伙伴一些更加通俗的理解中断初始化的运行过程,并给出一些在开发过程中个人感觉比较好的习惯建议,如有错误,欢迎随时联系博主。
一、单片机中断系统(以STM32为例)
1、STM32的中断系统主要有以下几个关键点
1、中断向量表
2、NVIC(内嵌向量中断控制器)
3、中断使能
4、中断服务函数
具体定义我不过多赘述,大家可以自行在网上搜索,都有很详细且官方的解释,我这边只是把我学习过程通俗的理解告诉大家。
中断向量表:“中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址或存放中,断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以 后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。”
中断向量表,是在程序运行前就已经初始化好的
以上是官方的解释,我们可以这样理解,内核相当于是一块巨大的且已经被规划好区块的大陆,其中有一大块面积是我们的中断向量表所在地,而中断向量表里面的各个名称也被划分且永远在各自的土地上不会变动,这一块块不同名称的土地的总和构成的大区域构成了这个还未开发的大土地,我们把他叫做中断向量表。
NVIC(内嵌向量中断控制器):中断系统得有个管理机构,对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫做 NVIC,全称叫做 Nested Vectored Interrupt Controller。
同样的我们让一个更通俗的例子来解释它,NVIC是在内核这片大陆上的一个专门负责管理开发中断向量表土地区域的管理部门,它负责和外设(GPIO、TIM、ADC....)建立合作关系,建不建立当然得看各位写的程序(EXIT_interupt、TIM_interupt.....),如果外设需要用到中断,就需要由NVIC把这些外设的带领到原本为他们准备好的地址上。所写的中断配置函数就建造在这,你就可以在这放入你自己的程序了。(注意,这只是为了便于理解才举例,中断服务函数并不会存放在中断向量表对应中断的地址中,而是放在flash中,cpu会不断从flash中取指译码执行)
中断使能:要使用某个外设的中断,肯定要先使能这个外设的中断。
我们一般会在中断使能之前配置好中断的各项参数(设置优先级分组、定义结构体、在中断向量表中选择中断通道、设置优先级)最后最重要的是使能中断,要不然你配置的这些东西根本不会起作用,就像没有盖章签名的合同一样。中断的使能让这张合同得以生效,而后,我们的中断服务函数才可以大张旗鼓的驶入合同给他分配的领地了。
中断服务函数:我们使用中断的目的就是为了使用中断服务函数,当中断发生以后中断服务函数就会被调 用,我们要处理的工作就可以放到中断服务函数中去完成。
当我们在中断向量表中有了自己的私有领地后,我们就可以在这块土地上建立自己的分公司,分公司的职责和规模以及处理的业务,是由我们用户自己编程的,这个分公司就是我们的中断服务函数了,当总公司(也就是外设)出现了某种突发情况(对应中断事件发生),那么就需要将一个电话打到分公司(中断标志位),这个时候总公司就可以歇了,让分公司来处理事情(程序跳转到中断服务函数运行)。
故事到此,下图是中断发生的过程。
二、中断配置举例说明
STM32库函数中断配置
一般我们刚初学单片时最快的入门方式是库函数开发,以GPIOB_IO14外部中断的配置举例(这里只程序例子,注释很详细,不对硬件过多赘述)。
void EXIT14_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
可以看出,用标准库函数配置(或者是hal库)中断几乎都是用的结构体的方式,标准库官方将不同的外设配置封装成一个个结构体,在配置完结构体之后只需要调用一下初始化函数即可让硬件运行,方便但同时又让底层的运行变得模糊,下面会讲到寄存器开发。
IMX6UL寄存器开发中断配置
IMX6UL中断控制器
即使ST官方为我们提供了很多封装好的API函数,在嵌入式开发地过程中不可避免地就是我们会不断地接触到32以外的各类芯片,而它们大多数是不会给用户提供类似库函数的包的,需要用户自己去根据参考手册编写业务逻辑。
以下是IMX6UL内核中的GIC控制器和ARM内核的关系(类似Cotex-M内核中的NVIC)简图,详细的官方解释和内部结构大家可以参考《IMX6UL参考手册》或者正点原子的《I.MX6UL-linux驱动开发指南》,涉及的概念是很多这里不做解释。
我们这样理解,在内核中我们需要把我们中断向量表中对应已经配置好的中断,通过GIC控制器产生一个对应的中断信号(假设通过IRQ当中的IOMUXC_IRQn)发送到ARM内核中,内核就会根据我们的IRQ中置的位,调用配置注册在IOMUXC_IRQn上的中断服务函数。
GIC控制器的左边的中断0-中断N是什么呢,以我们最常见的外设中断为例,我们配置好外设后,只要使能某个中断,外设在运行中达到这个中断条件后,相应的位被置位,这个信号会通过某条总线(图中总线一般为SPIs,这个可以不做了解,明白传输方式即可)传输到GIC控制器,这个中断就是0-N的其中之一。
知道了中断的运行逻辑,现在只需要对外设进行配置、为对应的中断编写中断服务函数、将中断服务函数注册到中断向量表中、使能外设中断、使能GIC控制器中对应的中断(中断向量表中的对应中断名称),那么他们的顺序又是怎么样的呢,不同的顺序会造成什么样的后果呢?
调试过程举例
这是一段mx6ul芯片中PWM1使能FIFO空中断的核心代码(完整初始化代码放在文章结尾):
以下是一段中断配置过程中的错误代码逻辑:
PWM1->PWMIR |= 1 << 0; /* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 给指定中断注册中断服务函数 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
这段代码逻辑如下:
使能外设中断 > 使能GIC中对应中断 > 注册中断服务函数 > 清空外设的中断标志位
是不是乍一看感觉没啥问题,但是如果这样的话你的程序烧写到单片机内后很大可能中断是不会运行的,更严重的甚至导致程序运行到某个地方出不来,出现卡死现象。
下面几段代码是正确的可以运行的:
第一段:
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 给指定中断注册中断服务函数 */
PWM1->PWMIR |= 1 << 0; /* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
第二段:
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 给指定中断注册中断服务函数 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMIR |= 1 << 0; /* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
第三段:
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 给指定中断注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMIR |= 1 << 0; /* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
错误产生原因
有没有发现一个共性那就是中断注册函数总是位于GIC使能(NVIC的配置函数和使能也是如此)和FIFO空中断使能的前面。具体的原因:
以下是我的代码的中断注册函数,用于将某个中断服务函数注册到对应中断向量表中的对应位置。
/*
* @description : 给指定的中断号注册中断服务函数
* @param - irq : 要注册的中断号
* @param - handler : 要注册的中断处理函数
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}
下面这一段是我的系统中断初始化函数,在main函数运行到while循环之前,我会在系统初始化函数中调用此函数对系统的所有中断进行初始化。
/*
* @description : C语言中断服务函数,irq汇编中断服务函数会
调用此函数,此函数通过在中断服务列表中查
找指定中断号所对应的中断处理函数并执行。
* @param - giccIar : 中断号
* @return : 无
*/
void system_irqhandler(unsigned int giccIar)
{
uint32_t intNum = giccIar & 0x3FFUL;
/* 检查中断号是否符合要求 */
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{
return;
}
irqNesting++; /* 中断嵌套计数器加一 */
/* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/
irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
irqNesting--; /* 中断执行完成,中断嵌套寄存器减一 */
}
而初始化后系统的中断都指向了同一个默认中断服务函数,也就是下面这一段代码。
/*
* @description : 默认中断服务函数
* @param - giccIar : 中断号
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void default_irqhandler(unsigned int giccIar, void *userParam)
{
while(1)
{
}
}
现在我们回到最开始错误的代码逻辑,在注册终端服务函数之前先使能了外设中断和GIC中断控制,这个时候已经把外设中断和默认中断服务函数绑定了,使能之后再重新注册中断服务函数是不被允许的,所以,一旦有任何中断发生,程序都会跳转到默认终端服务函数中执行里面的while(1)死循环,这样就导致程序卡死了。
三、结论
在单片机中断配置的过程中,养成一个良好的先后顺序习惯是很必要的。虽然正确的配置顺序不止一种,但按照:先配置、再使能的原则来编写代码是不会错且便于理解的。中断标志位的清零操作只要保证使能后为0就可以了,摆放是比较随意的。希望我踩过的坑,能对大家有帮助!
四、附加完整代码
PWM1_FIFO空中断初始化代码:
/*
* @description : 初始化背光PWM
* @param : 无
* @return : 无
*/
void backlight_init(void)
{
unsigned char i = 0;
/* 1、背光PWM IO初始化 */
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT, 0); /* 复用为PWM1_OUT */
/* 配置PWM IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 10 100K上拉
*bit [13]: 1 pull功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 010 驱动能力为R0/2
*bit [0]: 0 低转换率
*/
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT, 0XB090);
/* 2、初始化PWM1 */
/*
* 初始化寄存器PWMCR
* bit[27:26] : 01 当FIFO中空余位置大于等于2的时候FIFO空标志值位
* bit[25] :
0 停止模式下PWM不工作
* bit[24] : 0 休眠模式下PWM不工作
* bit[23] : 0 等待模式下PWM不工作
* bit[22] : 0 调试模式下PWM不工作
* bit[21] : 0 关闭字节交换
* bit[20] : 0 关闭半字数据交换
* bit[19:18] : 00 PWM输出引脚在计数器重新计数的时候输出高电平
* 在计数器计数值达到比较值以后输出低电平
* bit[17:16] : 01 PWM时钟源选择IPG CLK = 66MHz
* bit[15:4] : 65 分频系数为65+1=66,PWM时钟源 = 66MHZ/66=1MHz
* bit[3] : 0 PWM不复位
* bit[2:1] : 00 FIFO中的sample数据每个只能使用一次。
* bit[0] : 0 先关闭PWM,后面再使能
*/
PWM1->PWMCR = 0; /* 寄存器先清零 */
PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4);
/* 设置PWM周期为1000,那么PWM频率就是1M/1000 = 1KHz。 */
pwm1_setperiod_value(1000);
/* 设置占空比,默认50%占空比 ,写四次是因为有4个FIFO */
backlight_dev.pwm_duty = 50;
for(i = 0; i < 4; i++)
{
pwm1_setduty(backlight_dev.pwm_duty);
}
/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
PWM1->PWMIR |= 1 << 0;
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
pwm1_enable(); /* 使能PWM1 */
}
对应配置的中断服务函数:
void pwm1_irqhandler(void)
{
if(PWM1->PWMSR & (1 << 3)) /* FIFO为空中断 */
{
/* 将占空比信息写入到FIFO中,其实就是设置占空比 */
pwm1_setduty(backlight_dev.pwm_duty);
PWM1->PWMSR |= (1 << 3); /* 写1清除中断标志位 */
}
}