IMX6UL开发板中断实验(三)
在上一节我们编写完成了中断驱动文件和中断驱动头文件,那么这一讲我们将继续中断实验
下面就是GPIO的中断设置,第一步要设置中断GPIO的触发方式,首先我们先看到寄存器,一共有GPIOx_ICR1和ICR2,
图如上,ICR1或者ICR2可以看出图中都是由两个bit来描述一个IO口,相当于一个寄存器控制16个IO口,两个的话也就是32个IO口
可以看出,bit31-bit30的作用翻译过来就是,此寄存器控制GPIO中断15功能的激活条件,然后像寄存器中写入00,01作用就是,中断的触发模式,分别是低电平,高电平,上升沿,下降沿触发
对于我们需要完成的实验,首先我们看到开发板的原理图,既然是通过中断的方式点亮LED等,那么意思就是当按键按下的时候LED灯亮,那么按键按下是属于哪种形式的触发方式?
这里需要说明的是,各种触发条件的条件以及特性和大致应用场景,在如下图按键按下额时候,如果产生低电平那么就可以触发条件,那么很明显对应的就是,下降沿触发,然后下面的表格对应的是,低电平触发,高电平触发,上升沿触发,下降沿触发
KEY0按键对应的IO是,UART1 CTS,需要讲这个引脚设置为下降沿触发
然后我们看到下一个寄存器,GPIO_IMR 包含了针对每个中断线的屏蔽位(masking bits),具体意思是,用于控制哪些GPIO引脚的中断信号应该被处理器接收和处理,哪些应该被忽略。
GPIO_IMR
寄存器中的每一位都对应一个GPIO引脚的中断屏蔽控制。如果某位被设置为1,则对应的GPIO引脚的中断请求将被屏蔽,即该引脚的中断信号不会被处理器响应;如果某位被设置为0,则对应的GPIO引脚的中断请求是使能的,即该引脚的中断信号可以被处理器接收和处理。
然后这里继续看下一个我们需要用到的寄存器,GPIOx_ISR这个寄存器的作用就是,每个bit位对应一个IO口,一共也就是对应32的IO,如果某个IO口检测到中断产生,则将对应的bit位置1
但是在中断产生过后,这个中断标志位肯定是需要被清0的,但是对于这个寄存器而言,这个寄存器是需要通过写1来清0,并不是直接写0
UART1 CTS 按键这个引脚对应的是GPIO1_IO18,现在我们需要找到这个引脚对应的中断ID号,可以看到GPIO16-31是67这个ID号,但是这并不是他实际的ID号,他的实际ID号还需要加上32,因为PPI(私有中断ID)和SGI(软件中断),以及SPI共享外设中断,这些都是在之前提到过的,这里编号全是SPI共享外设中断,而PPI和SGI加起来一共有32个中断,所以需要在这里67的基础上再加上32,所以GPIO16-31引脚对应的中断是99
然后在我们的代码文件中,也可以看到对应的中断号正好是99
然后下面就开始编写GPIO的驱动函数,在之前的基础上我们加入中断的相关函数,首先我先会展示之前的代码,首先是头文件
#ifndef _BSP_GPIO_H
#define _BSP_GPIO_H
#define _BSP_KEY_H
#include "imx6ul.h"
/* 枚举类型和结构体定义 */
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
} gpio_pin_config_t;
/* 函数声明 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
int gpio_pinread(GPIO_Type *base, int pin);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);
#endif
然后是.c文件
#include "bsp_gpio.h"
/*
* @description : GPIO初始化。
* @param - base : 要初始化的GPIO组。
* @param - pin : 要初始化GPIO在组内的编号。
* @param - config : GPIO配置结构体。
* @return : 无
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
if(config->direction == kGPIO_DigitalInput) /* 输入 */
{
base->GDIR &= ~( 1 << pin);
}
else /* 输出 */
{
base->GDIR |= 1 << pin;
gpio_pinwrite(base,pin, config->outputLogic);/* 设置默认输出电平 */
}
}
/*
* @description : 读取指定GPIO的电平值 。
* @param - base : 要读取的GPIO组。
* @param - pin : 要读取的GPIO脚号。
* @return : 无
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}
/*
* @description : 指定GPIO输出高或者低电平 。
* @param - base : 要输出的的GPIO组。
* @param - pin : 要输出的GPIO脚号。
* @param - value : 要输出的电平,1 输出高电平, 0 输出低低电平
* @return : 无
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if (value == 0U)
{
base->DR &= ~(1U << pin); /* 输出低电平 */
}
else
{
base->DR |= (1U << pin); /* 输出高电平 */
}
}
然后在此基础上加入中断的函数,首先就是使能GPIO中断,前面讲到了GPIO引脚的中断是由引脚GPIOx_IMR寄存器所控制,那么中断使能函数如下
void gpio_enableint(GPIO_Type* base, unsigned int pin)
{
base->IMR |= (1 << pin);
}
有了使能函数,同样就有禁止函数引脚使用中断的函数
void gpio_disableint(GPIO_Type* base, unsigned int pin)
{
base->IMR &= ~(1 << pin);
}
以及对应的清楚中断标志位的函数,这些都是上面所提到的寄存器所控制的
void gpio_clearintflags(GPIO_Type* base, unsigned int pin)
{
base->ISR |= (1 << pin);
}
然后揪下来就要开始编写 设置GPIO的中断配置功能的函数,首先代码如下
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pin_int_mode)
{
volatile uint32_t *icr;
uint32_t icrShift;
icrShift = pin;
base->EDGE_SEL &= ~(1U << pin);
if(pin < 16) /* 低16位 */
{
icr = &(base->ICR1);
}
else /* 高16位 */
{
icr = &(base->ICR2);
icrShift -= 16;
}
switch(pin_int_mode)
{
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
case(kGPIO_IntHighLevel):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (1U << (2 * icrShift));
break;
case(kGPIO_IntRisingEdge):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));
break;
case(kGPIO_IntFallingEdge):
*icr |= (3U << (2 * icrShift));
break;
case(kGPIO_IntRisingOrFallingEdge):
base->EDGE_SEL |= (1U << pin);
break;
default:
break;
}
}
代码看似很长,但其实很好理解,我们分两部分,以swtich语句为分割,首先swtich上面大致原理是这段代码的主要作用是根据pin
号配置相应引脚的中断触发方式,并决定修改哪个中断控制寄存器(ICR1或ICR2)以及在该寄存器中的哪个位位置。它首先禁用当前pin
号引脚的中断触发方式,然后根据pin
号的大小选择合适的ICR寄存器,并计算出在该寄存器中应操作的位位置。
先定义一个指向uint32_t
类型的指针icr,大致作用就是
访问中断控制寄存器。volatile
关键字确保编译器在每次访问icr
时都直接从内存中读取值,而不是使用可能已经缓存在寄存器中的值。
定义一个无符号32位整数icrShift,
用于存储将要操作的位的位置,在ICR寄存器中
base->EDGE_SEL &= ~(1U << pin);就是如果在之后中断采取高电平或者低电平的方式触发的话,就禁用该引脚当前的边沿触发设置。
然后ICR在前面也提高过,分别控制16个IO引脚,那么低16位就是ICR1,不低于16位就是ICR2,这里是对ICR寄存器的一个选择
然后下面看到switch语句的部分,这部分是在完成什么事情呢,也就是选择每个GPIO的中断触发方式,以这个为例,比如我要让某个引脚低电平输出,那么我就将控制触发方式的寄存器(ICR),清零然后写入值,因为低电平触发是00,所以这里就写入00的值,以为着到时候就如果选择模式是低电平,那么就写入值00
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
然后下面的代码都是一样的道理,都是先将这个寄存器的值清零,也就是初始化,然后再根据选择的模式然后写入值进去,唯一有区别的地方就是最后一个,因为在前面ICR寄存器中可以看到,他只有低电平,高点平,上边沿和下边沿四种触发方式
但是在我们的设置当中,却还有一种双边沿触发的方式,那么这个是通过控制哪个寄存器来实现的呢
case(kGPIO_IntRisingOrFallingEdge):
base->EDGE_SEL |= (1U << pin);
break;
它的主要作用是,在GPIO(通用输入输出)接口中,GPIO_EDGE_SEL
是一个特殊的寄存器,它允许开发者对中断的触发方式进行更细致的控制。通常,中断可以由多种条件触发,如电平变化(高电平、低电平)、边缘变化(上升沿、下降沿)等。GPIO_EDGE_SEL
通过其各个位的状态,为GPIO引脚指定了是应该基于上升沿、下降沿还是两者都产生中断。
所以双边沿以及边沿触发的不产生都是需要这个寄存器的控制
然后剩下的函数只需要在头文件在中引用一下,然后GPIO的驱动文件我们就编写完成了。然后就开始创建我们的具体实验代码了,首先需要创建一个文件夹存放两个文件,分别名为exti,创建文件bsp_exti.c和bsp_exti.h,下面我会先放出两个文件的代码
#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"
/*
* @description : 初始化外部中断
* @param : 无
* @return : 无
*/
void exit_init(void)
{
gpio_pin_config_t key_config;
/* 1、设置IO复用 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0); /* 复用为GPIO1_IO18 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
/* 2、初始化GPIO为中断模式 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); /* 使能GIC中对应的中断 */
system_register_irqhandler(GPIO1_Combined_16_31_IRQn, (system_irq_handler_t)gpio1_io18_irqhandler, NULL); /* 注册中断服务函数 */
gpio_enableint(GPIO1, 18); /* 使能GPIO1_IO18的中断功能 */
}
/*
* @description : GPIO1_IO18最终的中断处理函数
* @param : 无
* @return : 无
*/
void gpio1_io18_irqhandler(void)
{
static unsigned char state = 0;
delay(10);
if(gpio_pinread(GPIO1, 18) == 0) /* 按键按下了 */
{
state = !state;
beep_switch(state);
}
gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}
首先将按键的GPIO进行复用,然后先初始化GPIO,最后设置GIC的初始化,包括对于中断功能以及对应中断号的初始化,上面其实我们就是在编写自己的库,然后使用自己的库开始编写具体的外部中断函数,就和以前写STM32的标准库是一个道理
然后下面就是中断处理函数,也就是我们在触发中断后,需要执行的函数,这个就不难理解了,按下 KEY 就会打开蜂鸣器,再次按下就会关闭蜂鸣器,最后再将中断位清除
主函数里面只需要执行灯一直在闪烁的代码就可以了,然后等待之后我们按下按键产生蜂鸣器鸣叫的中断
#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_exit.h"
/*
* @description : main函数
* @param : 无
* @return : 无
*/
int main(void)
{
unsigned char state = OFF;
int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
beep_init(); /* 初始化beep */
key_init(); /* 初始化key */
exit_init(); /* 初始化按键中断 */
while(1)
{
state = !state;
led_switch(LED0, state);
delay(500);
}
return 0;
}
因为添加了文件,最后再修改一下Makefile,确保能找到每个文件的路径
INCDIRS := imx6ul \
bsp/clk \
bsp/led \
bsp/delay \
bsp/beep \
bsp/gpio \
bsp/key \
bsp/exit \
bsp/int
SRCDIRS := project \
bsp/clk \
bsp/led \
bsp/delay \
bsp/beep \
bsp/gpio \
bsp/key \
bsp/exit \
bsp/int
然后最后只需要将代码烧写进去,我们的本次实验就算是完成了,这个中断对于imx6ul确实是一个全新的板块,与STM32的NVIC不同,MPU芯片普遍采取的都是GIC的中断模式,也是一个很复杂的适用于多核的中断模式
博主也是才接触,所以周期有点长,以及在讲解的过程当中可能会有很多地方表述的不够清楚,所以希望各位在阅读的时候保持谅解,如果有问题欢迎讨论或者指针,那么中断的实验到这里就结束了