ARM裸机开发:输入中断
一、硬件平台:
正点原子I.MX6U阿尔法开发板
二、原理图分析
输入中断是配置GPIO作为输入IO口,检测按键引脚电平,当目标电平来到时产生中断,进入中断服务函数处理程序,I.MX6U的按键引脚如下:
可以看到按键引脚接到 GPIO1_IO18 口,按键的原理就是默认接一个上拉电阻,按键按下接地,可以有效控制 IO 电平
三、程序编写
程序编写前先复制上一节按键输入的工程作为本小节的开始工程
3.1 移植相关文件
在 NXP 提供的 SDK 包内 core_ca7.h 有相关的定义文件,为了节省开发时间,我们将其移植到本地工程目录;注意该文件要做一些修改,删除一些不必要的内容,不然会保存,这里我直接复制正点原子修改后的文件到工程目录下:
该文件下面我们只需要注意 10 个API函数,函数如下:
函数 | 描述 |
---|
GIC_Init | 初始化 GIC | GIC_EnableIRQ | 使能指定的外设中断 | GIC_DisableIRQ | 关闭指定的外设中断 | GIC_AcknowledgeIRQ | 返回中断号 | GIC_DeactivateIRQ | 无效化指定中断 | GIC_GetRunningPriority | 获取当前正在运行的中断优先级 | GIC_SetPriorityGrouping | 设置抢占优先级位数 | GIC_GetPriorityGrouping | 获取抢占优先级位数 | GIC_SetPriority | 设置指定中断的优先级 | GIC_GetPriority | 获取指定中断的优先级 |
文件添加后使用如下头文件调用
#include "core_ca7.h"
3.2 编写启动文件
SDK 添加完成之后就是修改启动文件,定义系统中断服务函数,修改 IRQ 中断,判断中断类型,进入不同的中断服务函数,启动文件编写如下:
首先编写全局标号,进入 _start 函数,在里面创建中断向量表
.global _start /* 全局标号 */
/*
* 描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
编写对应的中断服务函数,这里除了 Reset_Handler 和 IRQ_Handler 我们需要关注一下,其他的都暂时先编写为死循环:
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中断 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0
/* FIQ中断 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
这些中断服务函数是可以编写一些处理代码,方便用户判断错误的来源的,暂时先不研究
下面编写复位中断服务函数:
/* 复位中断 */
Reset_Handler:
/* 关闭全局中断 */
cpsid i
/* 关闭I、DCache和MMU 采取读-改-写的方式*/
/* 读取CP15的C1寄存器到R0中*/
mrc p15, 0, r0, c1, c0, 0
/* 清除C1寄存器的bit12位(I位),关闭I Cache*/
bic r0, r0, #(0x1 << 12)
/* 清除C1寄存器的bit2(C位),关闭D Cache*/
bic r0, r0, #(0x1 << 2)
/* 清除C1寄存器的bit1(A位),关闭对齐*/
bic r0, r0, #0x2
/* 清除C1寄存器的bit11(Z位),关闭分支预测*/
bic r0, r0, #(0x1 << 11)
/* 清除C1寄存器的bit0(M位),关闭MMU*/
bic r0, r0, #0x1
/* 将r0寄存器中的值写入到CP15的C1寄存器中*/
mcr p15, 0, r0, c1, c0, 0
#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000
dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif
/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endif
b main /* 跳转到main函数 */
IRQ 中断服务函数,进入中断服务函数后,先进行现场保护,然后获取 GIC 的基地址,偏移后操作其寄存器,获取当前中断号,保存到寄存器 r0 和 r1,接着调用一个c语言中断处理函数,将参数从 r0-r3 四个寄存器传入函数
汇编调用 C 函数的时候建议形参不要超过 4 个,形参可以由 r0~r3 这四个寄存器来传递,如果形参大于 4 个,那么大于 4 个的部分要使用堆栈进行传递。
所以 r0 寄存器写入中断号就可以了传入到函数 system_irqhandler;接着该函数进行对应中断的调用和处理,处理完成后向 GICC_EOIR 寄存器写入其中断号表示中断处理完成;
/* IRQ中断!重点!!!!! */
IRQ_Handler:
# 现场保护
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */
mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,*/
/* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据*/
/* 这个中断号来绝对调用哪个中断服务函数*/
push {r0, r1} /* 保存r0,r1 */
cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */
pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
# 向 GICC_EOIR 寄存器写入刚刚处理完成的中断号,
# 当一个中断处理完成以后必须向 GICC_EOIR 寄存器
# 写入其中断号表示中断处理完成
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */
pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */
pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */
之后就是进行现场恢复,返回到中断位置!注意,此处恢复现场传递的是 lr - 4 的寄存器值,而不是pc,因为 ARM 的指令是三级流水线:取指、译指、执 行,pc 指向的是正在取值的地址,比如下面一段代码
0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC
当前正在执行 0X2000 地址处的指令 “MOV R1, R0” ,但 PC 里面已经保存了 0X2008 地址处的指令“MOV R4, R5”。若发生中断,中断发生的时候保存在 lr 中的是 pc 的值,即地址 0X2008。当中断处理完成如果直接跳转到 lr 里面保存的地址处(0X2008) 开始运行,那么就有一个指令没有执行,所以就需要将 lr-4 赋值给 pc,即 pc=0X2004,从第二级正在译指的指令 “MOV R2, R3” 开始执行
3.3 中断处理程序
我们在中断服务函数 IRQ_Handler 中调用了 C 函数 system_irqhandler 来处理具体的中断,该函数的具体细节需要我们自己实现,所以要编写中断处理程序来实现,同时因为中断数量较多,所以我们引入一些其他的数据结构单元辅助管理中断服务函数,编写如下:
新建一个新的模块文件
头文件插入如下代码
#ifndef __BSP_INT_H
#define __BSP_INT_H
#include "imx6ul.h"
typedef void (* system_irq_handler_t) (unsigned int giccIar,void *param);
typedef struct _sys_irq_handle
{
system_irq_handler_t irqHandler;
void *userParam;
} sys_irq_handle_t;
void int_init(void);
void system_irqtable_init(void);
void system_register_irqhandler(IRQn_Type irq,
system_irq_handler_t handler,
void *userParam);
void system_irqhandler(unsigned int giccIar);
void default_irqhandler(unsigned int giccIar,void *userParam);
#endif
代码解释:
typedef void (* system_irq_handler_t) (unsigned int giccIar,void *param);
创建一个函数指针,用 typedef 定义修饰别名为 system_irq_handler_t
typedef struct _sys_irq_handle
{
system_irq_handler_t irqHandler;
void *userParam;
} sys_irq_handle_t;
创建一个结构体,其有两个参数,一个是函数指针的入口指针,另外一个则是一个用户参数,创建这个结构体用于保存中断的信息,保存其中断处理函数入口因为有160个中断源,所以我们在.c文件中可以定义一个结构体数组用于存储所有中断的信息
其他的就是一些函数声明了:
void int_init(void);
void system_irqtable_init(void);
void system_register_irqhandler(IRQn_Type irq,
system_irq_handler_t handler,
void *userParam);
void system_irqhandler(unsigned int giccIar);
void default_irqhandler(unsigned int giccIar,void *userParam);
.c 模块文件代码如下,具体功能注释写在代码中:
#include "bsp_int.h"
static unsigned int irqNesting;
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
void int_init(void)
{
GIC_Init();
system_irqtable_init();
__set_VBAR((uint32_t)0x87800000);
}
void system_irqtable_init(void)
{
unsigned int i = 0;
irqNesting = 0;
for(i = 0; i < NUMBER_OF_INT_VECTORS; i++)
{
system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);
}
}
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}
void system_irqhandler(unsigned int giccIar)
{
uint32_t intNum = giccIar & 0x3FFUL;
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{
return;
}
irqNesting++;
irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
irqNesting--;
}
void default_irqhandler(unsigned int giccIar, void *userParam)
{
while(1) ;
}
3.4 开启输入中断
这里 GPIO 配置代码直接使用正点原子的驱动方案,有关的注释我写在代码内
bsp_gpio.h
#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 enum _gpio_interrupt_mode
{
kGPIO_NoIntmode = 0U,
kGPIO_IntLowLevel = 1U,
kGPIO_IntHighLevel = 2U,
kGPIO_IntRisingEdge = 3U,
kGPIO_IntFallingEdge = 4U,
kGPIO_IntRisingOrFallingEdge = 5U,
} gpio_interrupt_mode_t;
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction;
uint8_t outputLogic;
gpio_interrupt_mode_t interruptMode;
} 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);
void gpio_intconfig(GPIO_Type* base, unsigned int pin,
gpio_interrupt_mode_t pinInterruptMode);
void gpio_enableint(GPIO_Type* base, unsigned int pin);
void gpio_disableint(GPIO_Type* base, unsigned int pin);
void gpio_clearintflags(GPIO_Type* base, unsigned int pin);
#endif
bsp_gpio.c
#include "bsp_gpio.h"
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
base->IMR &= ~(1U << pin);
if(config->direction == kGPIO_DigitalInput)
{
base->GDIR &= ~( 1 << pin);
}
else
{
base->GDIR |= 1 << pin;
gpio_pinwrite(base,pin, config->outputLogic);
}
gpio_intconfig(base, pin, config->interruptMode);
}
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if (value == 0U)
{
base->DR &= ~(1U << pin);
}
else
{
base->DR |= (1U << pin);
}
}
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)
{
icr = &(base->ICR1);
}
else
{
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;
}
}
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);
}
3.5 按键中断编写
有了 GPIO 驱动代码后,我们就可以新建一个新的模块代码,用于配置外部触发中断,新建模块如下:
bsp_exit.h 代码:
#ifndef __BSP_EXIT_H
#define __BSP_EXIT_H
#include "imx6ul.h"
void exit_init(void);
void gpio1_io18_irqhandler(void);
#endif
bsp_exit.c 代码如下:
#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"
void exit_init(void)
{
gpio_pin_config_t key_config;
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
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);
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
(system_irq_handler_t)gpio1_io18_irqhandler,
NULL);
gpio_enableint(GPIO1, 18);
}
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);
}
以上代码准备完成后,我们在 main.c 中分别调用代码进行初始化
3.6 编写Makefile脚本
在 Makefile 里面添加上对应文件的文件夹就可以完成编译,添加位置如下:
编译一下,成功通过:
四、实验现象
按下按键 LED 的灯光效果切换
|