一、中断简介

相比 STM32 的 NVIC,IMX6ULL 的中断控制系统更复杂,它的中断管理器使用的是 GIC V2,GIC V2 的实现方式与我们熟知的 NVIC 差别较大。

1.1 GIC

GIC(Generic Interrupt Controller),直译为通用中断控制器,它是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。目前共有 4 个版本 V1~V4,IMX6ULL 使用的是 GIC V2。GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、Cortex-A9、Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。

GIC 最多支持 8 个处理器(processor0~ processor7)。不同处理器的 GIC 功能是相同的,我们只看其中一个即可。GIC 架构主要分为 分发器(Distributor)CPU接口(CPU interface/Virtual CPU interface)

简化后:

1.1.1 分发器

分发器用于 管理CPU所有中断源确定每个中断的优先级管理中断的屏蔽和中断抢占。最终将优先级最高的中断转发到一个或者多个CPU接口

分发器主要工作如下:

  • 全局的开启或关闭 CPU 的中断。
  • 控制任意一个中断请求的开启和关闭。
  • 设置每个中断请求的中断优先级。
  • 指定中断发生时将中断请求发送到哪些 CPU(IMX6ULL 是单核)。
  • 设置每个外部中断的触发方式(边沿触发或者电平触发)。
  • 设置每个中断属于组 0 还是组 1。

CPU的中断源分为三类:

  • SPI(Shared Peripheral Interrupt) 共享中断(标号①),即所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断)。比如我们常用的按键中断、串口中断、DMA中断等等。在图中 SPI 中断编号为(32~1019),这是最大支持的中断数量,实际芯片支持的数量有芯片设计者决定,IMX6ULL 支持 128 个 SPI 中断请求(中断编号为32~159),详细查看《i.MX6UltraLite Applications Processor Reference Manual》Chapter 3Interrupts and DMA Events。

  • PPI(Private Peripheral Interrupt) 私有中断(标号②),GIC 是支持多个 CPU 的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的 CPU 处理,因此这些中断就叫做私有中断。在图中 PPI 有 16 个中断,中断编号为(16~31)。

  • SGI(Software-generated Interrupt) 软件中断(标号③),由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。在图中 SGI 中断共有 16 个中断,中断编号为(0~15)。SGI 一般用于 CPU 之间通信,IMX6ULL 是单核处理器,我们暂时不用过多关心 SGI 中断。

1.1.2 CPU接口

CPU 接口是和 CPU Core 相连接的,就是分发器和 CPU Core 之间的桥梁。与分发器类似它也提供了一些编程接口,我们可以通过CPU接口实现以下功能:

  • 开启或关闭向 CPU Core 发送中断请求信号。
  • 应答中断(acknowledging an interrupt)。
  • 通知中断处理的完成。
  • 设置中断优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
  • 定义 CPU Core 的抢占策略。
  • 当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。

简单来说,CPU 接口可以开启或关闭发往 CPU 的中断请求,CPU 中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到 CPU。 在任何时候 CPU 都可以从其 GICC_Hppir(CPU接口寄存器) 读取当前活动的最高优先级。

1.2 CP15协处理器

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有 16 个 32 位寄存器(编号为C0~C15)。关于 CP15 协处理器和其相关寄存器的详细内容请参考下面两份文档:《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第 1469 页“B3.17 Oranization of the CP15 registers in a VMSA implementation”。《Cortex-A7 Technical ReferenceManua.pdf》第 55 页“Capter 4 System Control”。

1.2.1 CP15协处理器寄存器的访问

在NXP的官方启动文件中有两处用到了CP15协处理寄存器,第一处是系统复位中断服务函数开始处,这里通过CP15修改系统控制寄存器,第二处是获取GIC控制器的基地址。

CP15 寄存器只能使用 MRC/MCR 寄存器进行读、写。

  1. MRC: 将 CP15 协处理器中的寄存器(c0~c15)数据读到 ARM 通用寄存器中(r0~r12)。
mrc {cond} p15, <opc1>, <Rd>, <CRn>, <CRm>, <opc2>
  1. MCR: 将 ARM 通用寄存器(r0~r12)的数据写入到 CP15 协处理器寄存器中(c0~c15)。
mcr {cond} p15, <opc1>, <Rd>, <CRn>, <CRm>, <opc2>

CP15寄存器读、写指令说明如下:

  • cond:指令执行的条件码,忽略则表示无条件执行命令。
  • opc1:协处理器要执行的操作码。
  • Rd:ARM 通用寄存器,当为 mrc 时,用于保存从 CP15 寄存器读取得到的数据。当为 mcr 时,用于保存将要写入 CP15 寄存器的数据。
  • CRn:要读、写的CP15寄存器(c0~c15),对应的CRn选项。
  • CRm:寄存器从编号,对应CRm选项。
  • opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。

二、中断向量表

跟 STM32 一样,Cortex-A7 也有中断向量表,中断向量表也是在代码的最前面。Cortex-A7 内核有 8 个异常中断:

向量地址 中断类型 中断模式 描述
0X00 复位中断(Rest) 特权模式(SVC) 系统上电或者硬件复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等。
0X04 未定义指令中断(Undefined Instruction) 未定义指令中止模式(Undef) 如果CPU检测到无法识别的指令时会进入未定义指令异常中断。这种情况下系统已经无法继续运行,只能通过硬件复位或者看门狗复位系统。
0X08 软中断(Software Interrupt,SWI) 特权模式(SVC) 这种中断用于带 Linux 操作系统的情况,Linux 内核(即驱动程序)运行在 SVC(特权模式),而 Linux 应用程序运行在 usr 模式。应用程序中如果需要调用驱动程序,就需要首先通过系统调用中断切换到 SVC (特权模式),即我们常说的从”用户(应用)空间”切换到”内核空间”。
0X0C 指令预取中止中断(Prefetch Abort) 中止模式 在CPU执行当前指令时会”预取”下一个要执行的指令。如果”取指”失败就会进入该中断。CPU无法获取指令,所以这种情况下可以认为系统”挂了”。
0X10 数据访问中止中断(Data Abort) 中止模式 CPU读取数据终止,就是说系统读数据错误、读不到数据,所以这种中断后系统也是不正常的。
0X14 未使用(Not Used) 未使用
0X18 IRQ 中断(IRQ Interrupt) 外部中断模式(IRQ) 芯片内部的外设中断(串口中断、DMA中断、外部中断等等)都会引起此中断的发生。
0X1C FIQ 中断(FIQ Interrupt) 快速中断模式(FIQ) 如果需要快速处理中断的话就可以使用此中断。

异常向量表并不总是从 0 地址开始,IMX6ULL 可以设置 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

三、共享中断实现

无论是 SPI 中断、PPI 中断、还是 SGI 中断,它们都链接到了 CPU 接口,而 CPU 接口输出到 CPU 的只有两个 FIQ 和 IRQ(VFIQ 和 VIRQ 这里没有用到,暂时忽略)。 中断标号为 0~1019 的任意一个中断发生后 CPU 都会跳转到 FIQ 或 IRQ 中断服务函数去执行。在官方裸机代码中默认关闭了 FIQ,只使用了 IRQ。

3.1 IRQ共享中断实现

NXP 官方裸机共享中断实现代码如下所示, 我们将参照官方代码讲解并最终将官方启动文件移植到我们自己的工程中。

部分代码:

IRQ_Handler:push    {lr}         /* Save return address+4     */push    {r0-r3, r12} /* Push caller save registers           */MRS     r0, spsr    /* Save SPRS to allow interrupt reentry  */push    {r0}MRC     P15, 4, r1, C15, C0, 0   /* Get GIC base address  */ADD     r1, r1, #0x2000          /* r1: GICC base address  */LDR     r0, [r1, #0xC]           /* r0: IAR */push    {r0, r1}CPS  #0x13 /* Change to Supervisor mode to allow interrupt reentry */push    {lr}            /* Save Supervisor lr  */LDR     r2, =SystemIrqHandlerBLX     r2              /* Call SystemIrqHandler with param GCC */POP     {lr}CPS     #0x12           /* Back to IRQ mode */POP     {r0, r1}STR     r0, [r1, #0x10] /* Now IRQ handler finished: write to EOIR */POP     {r0}MSR     spsr_cxsf, r0POP     {r0-r3, r12}POP     {lr}SUBS    pc, lr, #4.size IRQ_Handler, . - IRQ_Handler.align 2.arm.weak FIQ_Handler.type FIQ_Handler, %function

代码解析:

  • 第3、4行:
    保存当前状态,同函数调用类似,进入中断函数之前要将程序当前的运行状态保存到”栈”中。中断执行完成后能够恢复进入中断之前的状态。
    push {lr} 将lr寄存器”入栈”,当进行函数调用或发生中断时pc(程序计数寄存器,保存当前程序执行位置(Thumb)加4)的值会自动保存到lr寄存器中。lr的值将做为函数会中断返回的地址。
    push {r0-r3, r12} 将r0-r3寄存器以及r12寄存器”入栈”。r0-r3和r12是通用寄存器,在函数中它们可以用于任何用途,但是在函数调用或函数返回时它们用于传入函数参数以及传出返回值等等。中断可能发生在程序的任意时刻,所以进入中断之前也要保存这些信息。
push    {lr}         /* Save return address+4     */
push    {r0-r3, r12} /* Push caller save registers           */
  • 第6、7行:
    保存spsr(备份程序状态寄存器)。SPRS是特殊功能寄存器不能直接访问。
    MRS r0, spsr 用于将spsr寄存器的值保存到r0寄存器。
    push {r0} 将spsr寄存器的值保存到”栈”中。
MRS     r0, spsr    /* Save SPRS to allow interrupt reentry  */
push    {r0}
  • 第9-11行:
    获取GIC基地址以及GICC_IAR寄存器的值。这部分代码使用到了CP15协处理器。
    MRC P15, 4, r1, C15, C0, 0 用于将spsr寄存器的值保存到r0寄存器。
    push {r0} 将spsr寄存器的值保存到”栈”中。
MRC     P15, 4, r1, C15, C0, 0   /* Get GIC base address  */
ADD     r1, r1, #0x2000          /* r1: GICC base address  */
LDR     r0, [r1, #0xC]           /* r0: IAR */
  • 第13行:
    将GICC基地址和GICC_IAR寄存器值入栈。第三部分代码将GICC基地址保存在了r1寄存器,将GICC_IAR寄存器的值保存在了r0寄存器,中断执行完成后我们还要用到这些内容,所以这里将他们”入栈”保存。
push    {r0, r1}
  • 第15行:
    切换到Supervisor模式。
CPS  #0x13 /* Change to Supervisor mode to allow interrupt reentry */
  • 第17-20行:
    跳转到SystemIrqHandler函数执行共享中断对应的中断服务函数。
    push {lr} 保存当前的链接寄存器,即保存程序的返回地址。
    LDR r2, =SystemIrqHandler 用于将函数”SystemIrqHandler”地址保存到r2寄存器中。
    BLX r2 是有返回的跳转,程序将会跳转到”SystemIrqHandler”函数执行。
push    {lr}            /* Save Supervisor lr  */
LDR     r2, =SystemIrqHandler
BLX     r2              /* Call SystemIrqHandler with param GCC */
POP     {lr}

3.2 SystemIrqHandler共享中断处理函数

函数 SystemIrqHandler 保存在 SDK_2.2_MCIM6ULL\devices\MCIMX6Y2\system_MCIMX6Y2.c 文件内。

去掉不必要的条件编译后如下所示。部分代码:

__attribute__((weak)) void SystemIrqHandler(uint32_t giccIar)
{uint32_t intNum = giccIar & 0x3FFUL;/* Spurious interrupt ID or Wrong interrupt number */if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS)){return;}irqNesting++;__enable_irq();      /* Support nesting interrupt *//* Now call the real irq handler for intNum */irqTable[intNum].irqHandler(giccIar, irqTable[intNum].userParam);__disable_irq();irqNesting--;
}

代码解析:

  • 第1行:
    SystemIrqHandler() 函数有一个入口参数 giccIar,它是GICC_IAR寄存器的值。在3.1代码的第四部分代码中,我们将GICC_IAR寄存器的值保存到了R0寄存器,跳转到 SystemIrqHandler() 函数之后R0寄存器的值作为函数参数。
__attribute__((weak)) void SystemIrqHandler(uint32_t giccIar)
{···
}
  • 第3行:
    获取中断的中断编号。中断编号保存在GICC_IAR寄存器的后10位(0~9)。
uint32_t intNum = giccIar & 0x3FFUL;
  • 第6-9行:
    判断中断标号是否有效。如果中断无效,则读取得到的中断号1023。
    NUMBER_OF_INT_VECTORS 是i.MX 6U支持的最大中断号加一,大于等于这个值的中断编号也被认为无效。
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{return;
}
  • 第16行:
    如果中断编号有效,这部分根据中断编号在irqTable中找到对应的中断服务函数。
/* Now call the real irq handler for intNum */
irqTable[intNum].irqHandler(giccIar, irqTable[intNum].userParam);

本地中断向量表定义在system_MCIMX6Y2.h文件:

/*中断数量*/
#define NUMBER_OF_INT_VECTORS 160
/*本地中断向量表*/
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];

本地中断向量表 的作用是当IRQ中断发生后找到对应的处理函数。从上方代码不难看出,本地中断向量表是一个sys_irq_handle_t结构体类型的数组。结构体如下所示:

typedef void (*system_irq_handler_t) (uint32_t giccIar, void *param);
/*** @brief IRQ handle for specific IRQ*/
typedef struct _sys_irq_handle
{//执行完成后IRQ中断并没有结束,这部分代码用于标记中断处理完成并恢复中断前的状态。system_irq_handler_t irqHandler; /**< IRQ handler for specific IRQ */void *userParam;    /**< User param for handler callback */
} sys_irq_handle_t;

sys_irq_handle_t结构体中有一个函数指针和函数参数指针,函数指针用于指定中断的中断处理函数,函数参数指针用于指定中断处理程序的用户参数。初始化中断时我们会根据中断编号初始化对应的数组项(irqTable[])。同样,中断发生后在SystemIrqHandler函数中根据中断标号找到对应的中断处理函数。

四、引脚确定

我使用的是 野火_EBF6ULL S1 Pro 开发板

KEY按键连接至 SNVS_TAMPER1 引脚,用作普通的按键。

板上4个按键的信息及相应GPIO端口引脚号的总结具体如下:

按键 丝印编号 GPIO功能 按键按下时的电平 其它功能
RST复位按键 SW1 不支持 低电平 复位芯片
ON/OFF按键 SW3 不支持 低电平 从低功耗唤醒
MODE按键 SW4 支持 BOOT_MODE[0]与BOOT_MODE[1]相反 选择芯片启动方式
KEY按键 SW2 支持 高电平

五、编程流程

1. 创建工程文件夹
2. 移植官方SDK寄存器定义文件
3. 移植官方SDK引脚复用和引脚属性定义文件
4. 移植官方SDK中断相关文件
5. 移植野火PAD属性配置文件
6. 编写启动文件
7. 编写链接文件
8. 编写makefile文件
9. 编写C语言代码
(1) 添加中断服务函数到“中断向量表”
(2) 开启GPIO时钟
(3) 设置引脚的复用功能以及引脚PAD属性
(4) 设置引脚方向
(5) 设置引脚中断类型
(6) 使能引脚中断

六、创建工程文件夹

  1. 创建一个文件夹 interrupt_init
  2. 创建一个用于存放头文件的文件夹 include
  3. 创建一个用于存放驱动源码的文件 device
  4. 创建一个启动文件 start.S
  5. 创建一个源文件 main.c
  6. 创建一个链接脚本 base.lds

七、移植官方SDK寄存器定义文件

/interrupt_init/include 目录下添加官方SDK寄存器定义文件 MCIMX6Y2.h,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2 目录下。

在官方SDK的头文件 MCIMX6Y2.h 文件多达4万多行,包含了i.MX6U芯片几乎所有的寄存器定义以及中断编号的定义。

这里只列 GPIO1相关寄存器 的部分代码。其他寄存器定义与此类似。 添加这些定义之后我们就可以 直接使用 “GPIO1->DR” 语句操作GPIO1的DR寄存器。操作方法与STM32非常相似。

typedef struct {__IO uint32_t DR;     /**< GPIO data register, offset: 0x0 */__IO uint32_t GDIR;   /**< GPIO direction register, offset: 0x4 */__I  uint32_t PSR;    /**< GPIO pad status register, offset: 0x8 */__IO uint32_t ICR1;   /**< GPIO interrupt configuration register1,*/__IO uint32_t ICR2;   /**< GPIO interrupt configuration register2, */__IO uint32_t IMR;   /**< GPIO interrupt mask register, offset: 0x14 */__IO uint32_t ISR; /**< GPIO interrupt status register, offset: 0x18 */__IO uint32_t EDGE_SEL;/**< GPIO edge select register, offset: 0x1C */
} GPIO_Type;/*********************以下代码省略***************************8*/
/** Peripheral GPIO1 base address */
#define GPIO1_BASE                               (0x209C000u)
/** Peripheral GPIO1 base pointer */
#define GPIO1                                    ((GPIO_Type *)GPIO1_BASE)

八、移植官方SDK引脚复用和引脚属性定义文件

/interrupt_init/include 目录下添加官方SDK引脚复用和引脚属性定义文件 fsl_iomuxc.h,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2/drivers 目录下。

使用每一个引脚之前我们都要选择引脚的复用功能以及引脚的pad属性。在官方SDK的 fsl_iomuxc.h 中定义了所有可用引脚以及这些引脚的所有复用功能,我们需要哪种复用功能只需要选择即可,并且官方SDK中提供了初始化函数。

  • 定义引脚的复用功能
    这里只列出了“GPIO1_IO00”引脚的复用功能,其他引脚类似。每个引脚对应多个宏定义代表引脚的不同的复用功能,以宏“IOMUXC_GPIO1_IO00_I2C2_SCL”为例,它表示“GPIO1_IO00”引脚复用为“I2C2”的“SCL”引脚。这些宏定义将会用作某些函数的入口参数。
#define IOMUXC_GPIO1_IO00_I2C2_SCL \0x020E005CU, 0x0U, 0x020E05ACU, 0x1U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_GPT1_CAPTURE1L \0x020E005CU, 0x1U, 0x020E058CU, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ANATOP_OTG1_IDL   \0x020E005CU, 0x2U, 0x020E04B8U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ENET1_REF_CLK1L  \0x020E005CU, 0x3U, 0x020E0574U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_MQS_RIGHTL  \0x020E005CU, 0x4U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_GPIO1_IO00L  \0x020E005CU, 0x5U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ENET1_1588_EVENT0_INL \0x020E005CU, 0x6U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_SRC_SYSTEM_RESETL  \0x020E005CU, 0x7U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_WDOG3_WDOG_BL   \0x020E005CU, 0x8U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO01_I2C2_SDAL    \0x020E0060U, 0x0U, 0x020E05B0U, 0x1U, 0x020E02ECU
#define IOMUXC_GPIO1_IO01_GPT1_COMPARE1L  \0x020E0060U, 0x1U, 0x00000000U, 0x0U, 0x020E02ECU
#define IOMUXC_GPIO1_IO01_USB_OTG1_OCL    \0x020E0060U, 0x2U, 0x020E0664U, 0x0U, 0x020E02ECU
  • 引脚复用功能设置函数
    IOMUXC_SetPinMux() 拥有6个入口参数, 但是前五个是通过上面的宏定义自动完成设置的。而第6个入口参数“inputOnfiled”用于设置是否开启读回引脚电平功能。
static inline void IOMUXC_SetPinMux(uint32_t muxRegister,uint32_t muxMode,uint32_t inputRegister,uint32_t inputDaisy,uint32_t configRegister,uint32_t inputOnfield)
{*((volatile uint32_t *)muxRegister) =IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(muxMode) |\IOMUXC_SW_MUX_CTL_PAD_SION(inputOnfield);if (inputRegister){*((volatile uint32_t *)inputRegister) = \IOMUXC_SELECT_INPUT_DAISY(inputDaisy);}
}
  • 引脚PAD属性设置函数
    IOMUXC_SetPinConfig() 函数共有6个入口参数,其中前五个是通过上面的宏定义自动完成设置的。而第6个参数用于设置PAD属性,根据每个引脚拥有一个32位PAD属性寄存器。第六个参数就是设置要填入PAD属性寄存器的值。
static inline void IOMUXC_SetPinConfig(uint32_t muxRegister,uint32_t muxMode,uint32_t inputRegister,uint32_t inputDaisy,uint32_t configRegister,uint32_t configValue)
{if (configRegister){*((volatile uint32_t *)configRegister) = configValue;}
}

代码屏蔽 #include "fsl_common.h"

九、移植官方SDK中断相关文件

9.1 移植头文件

/interrupt_init/include 目录下共添加了5个中断相关的头文件。

  • ① 处是 内核相关头文件以及符合CMSIS标准的头文件,位于/SDK_2.2_MCIM6ULL/CMSIS/IncludeSDK_2.2_MCIM6ULL/CORTEXA/Include目录下,没有对应的.c文件,这些头文件提供了系统控制函数以及特殊寄存器操作函数,我们直接添加到我们工程即可,几乎不用修改。

  • ② 处是 系统初始化头文件,位于/SDK_2.2_MCIM6ULL/devices/MCIMX6Y2目录下,这里包含我们需要的中断初始化代码以及MMU、时钟等等初始化代码。

9.2 移植源文件

/interrupt_init/device 目录下添加 系统初始化源文件

位于/SDK_2.2_MCIM6ULL/devices/MCIMX6Y2目录下,这里包含我们需要的中断初始化代码以及MMU、时钟等等初始化代码。

代码修改成如下:

/*!* @file MCIMX6Y2* @version 3.0* @date 2017-02-28* @brief Device specific configuration file for MCIMX6Y2 (implementation file)** Provides a system configuration function and a global variable that contains* the system frequency. It configures the device and initializes the oscillator* (PLL) that is part of the microcontroller device.*/#include <stdint.h>
#include "led.h"
#include "system_MCIMX6Y2.h"uint32_t __VECTOR_TABLE = 0x80002000;/* Local irq table and nesting level value */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
static uint32_t irqNesting;/* ------------------------------------------------------------------------------ Core clock---------------------------------------------------------------------------- */uint32_t SystemCoreClock = DEFAULT_SYSTEM_CLOCK;/* ------------------------------------------------------------------------------ SystemInit()---------------------------------------------------------------------------- */void SystemInit(void)
{uint32_t sctlr;uint32_t actlr;#if ((__FPU_PRESENT == 1) && (__FPU_USED == 1))uint32_t cpacr;uint32_t fpexc;#endifL1C_InvalidateInstructionCacheAll();L1C_InvalidateDataCacheAll();actlr = __get_ACTLR();actlr = (actlr | ACTLR_SMP_Msk); /* Change to SMP mode before enable DCache */__set_ACTLR(actlr);sctlr = __get_SCTLR();sctlr = (sctlr & ~(SCTLR_V_Msk | /* Use low vector */SCTLR_A_Msk | /* Disable alignment fault checking */SCTLR_M_Msk)) /* Disable MMU */| (SCTLR_I_Msk |         /* Enable ICache */SCTLR_Z_Msk |         /* Enable Prediction */SCTLR_CP15BEN_Msk |   /* Enable CP15 barrier operations */SCTLR_C_Msk);         /* Enable DCache */__set_SCTLR(sctlr);/* Set vector base address */GIC_Init();__set_VBAR((uint32_t)__VECTOR_TABLE);// rgb_led_init();// blue_led_on;#if ((__FPU_PRESENT == 1) && (__FPU_USED == 1))cpacr = __get_CPACR();/* Enable NEON and FPU */cpacr = (cpacr & ~(CPACR_ASEDIS_Msk | CPACR_D32DIS_Msk)) | (3UL << CPACR_cp10_Pos) | (3UL << CPACR_cp11_Pos);__set_CPACR(cpacr);fpexc = __get_FPEXC();fpexc |= 0x40000000UL; /* Enable NEON and FPU */__set_FPEXC(fpexc);#endif /* ((__FPU_PRESENT == 1) && (__FPU_USED == 1)) */
}// /* ----------------------------------------------------------------------------
//    -- SystemCoreClockUpdate()
//    ---------------------------------------------------------------------------- */// void SystemCoreClockUpdate (void) {
//   /* i.MX6ULL systemCoreClockUpdate */
//   uint32_t PLL1SWClock;
//   uint32_t PLL2MainClock;
//   if (CCM->CCSR & CCM_CCSR_PLL1_SW_CLK_SEL_MASK)
//   {
//     if (CCM->CCSR & CCM_CCSR_STEP_SEL_MASK)
//     {
//         /* Get SYS PLL clock*/
//         if (CCM_ANALOG->PLL_SYS & CCM_ANALOG_PLL_SYS_DIV_SELECT_MASK)
//         {
//           PLL2MainClock = (24000000UL * 22UL + (uint64_t)(24000000UL) * (uint64_t)(CCM_ANALOG->PLL_SYS_NUM) / (uint64_t)(CCM_ANALOG->PLL_SYS_DENOM));
//         }
//         else
//         {
//           PLL2MainClock = (24000000UL * 20UL + (uint64_t)(24000000UL) * (uint64_t)(CCM_ANALOG->PLL_SYS_NUM) / (uint64_t)(CCM_ANALOG->PLL_SYS_DENOM));
//         }//         if (CCM->CCSR & CCM_CCSR_SECONDARY_CLK_SEL_MASK)
//         {
//             /* PLL2 ---> Secondary_clk ---> Step Clock ---> CPU Clock */
//             PLL1SWClock = PLL2MainClock;
//         }
//         else
//         {
//             /* PLL2 PFD2 ---> Secondary_clk ---> Step Clock ---> CPU Clock */
//             PLL1SWClock = ((uint64_t)PLL2MainClock * 18) / ((CCM_ANALOG->PFD_528 & CCM_ANALOG_PFD_528_PFD2_FRAC_MASK) >> CCM_ANALOG_PFD_528_PFD2_FRAC_SHIFT);
//         }
//     }
//     else
//     {
//       /* Osc_clk (24M) ---> Step Clock ---> CPU Clock */
//       PLL1SWClock = 24000000UL;
//     }
//   }
//   else
//   {
//     /* ARM PLL ---> CPU Clock */
//     PLL1SWClock = 24000000UL;
//     PLL1SWClock = ( PLL1SWClock * (CCM_ANALOG->PLL_ARM & CCM_ANALOG_PLL_ARM_DIV_SELECT_MASK) >> CCM_ANALOG_PLL_ARM_DIV_SELECT_SHIFT) >> 1UL;
//    }//   SystemCoreClock = PLL1SWClock / (((CCM->CACRR & CCM_CACRR_ARM_PODF_MASK) >> CCM_CACRR_ARM_PODF_SHIFT) + 1UL);
// }/* ------------------------------------------------------------------------------ SystemInstallIrqHandler()---------------------------------------------------------------------------- */void SystemInstallIrqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{irqTable[irq].irqHandler = handler;irqTable[irq].userParam = userParam;
}/* ------------------------------------------------------------------------------ SystemIrqHandler()---------------------------------------------------------------------------- */__attribute__((weak)) void SystemIrqHandler(uint32_t giccIar)
{uint32_t intNum = giccIar & 0x3FFUL;// rgb_led_init();// blue_led_on;/* Spurious interrupt ID or Wrong interrupt number */if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS)){return;}irqNesting++;// __enable_irq();      /* Support nesting interrupt *//* Now call the real irq handler for intNum */irqTable[intNum].irqHandler(giccIar, irqTable[intNum].userParam);// __disable_irq();irqNesting--;
}// uint32_t SystemGetIRQNestingLevel(void)
// {
//   return irqNesting;
// }/* Leverage GPT1 to provide Systick */
// void SystemSetupSystick(uint32_t tickRateHz, void *tickHandler, uint32_t intPriority)
// {
//     uint32_t clockFreq;
//     uint32_t spllTmp;//     /* Install IRQ handler for GPT1 */
//     SystemInstallIrqHandler(GPT1_IRQn, (system_irq_handler_t)(uint32_t)tickHandler, NULL);//     /* Enable Systick all the time */
//     CCM->CCGR1 |= CCM_CCGR1_CG10_MASK | CCM_CCGR1_CG11_MASK;//     GPT1->CR = GPT_CR_SWR_MASK;
//     /* Wait reset finished. */
//     while (GPT1->CR == GPT_CR_SWR_MASK)
//     {
//     }
//     /* Use peripheral clock source IPG */
//     GPT1->CR = GPT_CR_WAITEN_MASK | GPT_CR_STOPEN_MASK | GPT_CR_DOZEEN_MASK |
//                GPT_CR_DBGEN_MASK | GPT_CR_ENMOD_MASK | GPT_CR_CLKSRC(1UL);
//     /* Set clock divider to 1 */
//     GPT1->PR = 0;//     /* Get IPG clock*/
//     /* Periph_clk2_clk ---> Periph_clk */
//     if (CCM->CBCDR & CCM_CBCDR_PERIPH_CLK_SEL_MASK)
//     {
//         switch (CCM->CBCMR & CCM_CBCMR_PERIPH_CLK2_SEL_MASK)
//         {
//             /* Pll3_sw_clk ---> Periph_clk2_clk ---> Periph_clk */
//             case CCM_CBCMR_PERIPH_CLK2_SEL(0U):
//                 clockFreq = (24000000UL * ((CCM_ANALOG->PLL_USB1 & CCM_ANALOG_PLL_USB1_DIV_SELECT_MASK) ? 22U : 20U));
//                 break;//             /* Osc_clk ---> Periph_clk2_clk ---> Periph_clk */
//             case CCM_CBCMR_PERIPH_CLK2_SEL(1U):
//                 clockFreq = 24000000UL;
//                 break;//             case CCM_CBCMR_PERIPH_CLK2_SEL(2U):
//             case CCM_CBCMR_PERIPH_CLK2_SEL(3U):
//             default:
//                 clockFreq = 0U;
//                 break;
//         }//         clockFreq /= (((CCM->CBCDR & CCM_CBCDR_PERIPH_CLK2_PODF_MASK) >> CCM_CBCDR_PERIPH_CLK2_PODF_SHIFT) + 1U);
//     }
//     /* Pll2_main_clk ---> Periph_clk */
//     else
//     {
//         /* Get SYS PLL clock*/
//         if (CCM_ANALOG->PLL_SYS & CCM_ANALOG_PLL_SYS_DIV_SELECT_MASK)
//         {
//           spllTmp = (24000000UL * 22UL + (uint64_t)(24000000UL) * (uint64_t)(CCM_ANALOG->PLL_SYS_NUM) / (uint64_t)(CCM_ANALOG->PLL_SYS_DENOM));
//         }
//         else
//         {
//           spllTmp = (24000000UL * 20UL + (uint64_t)(24000000UL) * (uint64_t)(CCM_ANALOG->PLL_SYS_NUM) / (uint64_t)(CCM_ANALOG->PLL_SYS_DENOM));
//         }//         switch (CCM->CBCMR & CCM_CBCMR_PRE_PERIPH_CLK_SEL_MASK)
//         {
//             /* PLL2 ---> Pll2_main_clk ---> Periph_clk */
//             case CCM_CBCMR_PRE_PERIPH_CLK_SEL(0U):
//                 clockFreq = spllTmp;
//                 break;//             /* PLL2 PFD2 ---> Pll2_main_clk ---> Periph_clk */
//             case CCM_CBCMR_PRE_PERIPH_CLK_SEL(1U):
//                 clockFreq = ((uint64_t)spllTmp * 18) / ((CCM_ANALOG->PFD_528 & CCM_ANALOG_PFD_528_PFD2_FRAC_MASK) >> CCM_ANALOG_PFD_528_PFD2_FRAC_SHIFT);
//                 break;//             /* PLL2 PFD0 ---> Pll2_main_clk ---> Periph_clk */
//             case CCM_CBCMR_PRE_PERIPH_CLK_SEL(2U):
//                 clockFreq = ((uint64_t)spllTmp * 18) / ((CCM_ANALOG->PFD_528 & CCM_ANALOG_PFD_528_PFD0_FRAC_MASK) >> CCM_ANALOG_PFD_528_PFD0_FRAC_SHIFT);
//                 break;//             /* PLL2 PFD2 divided(/2) ---> Pll2_main_clk ---> Periph_clk */
//             case CCM_CBCMR_PRE_PERIPH_CLK_SEL(3U):
//                 clockFreq = ((((uint64_t)spllTmp * 18) / ((CCM_ANALOG->PFD_528 & CCM_ANALOG_PFD_528_PFD2_FRAC_MASK) >> CCM_ANALOG_PFD_528_PFD2_FRAC_SHIFT)) >> 1U);
//                 break;//             default:
//                 clockFreq = 0U;
//                 break;
//         }
//     }
//     clockFreq /= (((CCM->CBCDR & CCM_CBCDR_AHB_PODF_MASK) >> CCM_CBCDR_AHB_PODF_SHIFT) + 1U);
//     clockFreq /= (((CCM->CBCDR & CCM_CBCDR_IPG_PODF_MASK) >> CCM_CBCDR_IPG_PODF_SHIFT) + 1U);//     /* Set timeout value and enable interrupt */
//     GPT1->OCR[0] = clockFreq / tickRateHz - 1UL;
//     GPT1->IR = GPT_IR_OF1IE_MASK;//     /* Set interrupt priority */
//     GIC_SetPriority(GPT1_IRQn, intPriority);
//     /* Enable IRQ */
//     GIC_EnableIRQ(GPT1_IRQn);//     /* Start GPT counter */
//     GPT1->CR |= GPT_CR_EN_MASK;
// }// void SystemClearSystickFlag(void)
// {
//     GPT1->SR = GPT_SR_OF1_MASK;
// }

9.3 GIC相关API操作函数

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 获取指定中断的优先级。

十、移植野火PAD属性配置文件

/interrupt_init/device 目录下添加 pad_config.h

通常情况下一个引脚要设置8种PAD属性,而这些属性只能通过数字指定。为简化PAD属性设置野火编写了一个PAD属性配置文件 pad_config.h (embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】,这里使用宏定义了引脚可选的PAD属性值,并且通过宏定义的名字很容易知道宏代表的属性值:

/* SPEED 带宽配置 */
#define SPEED_0_LOW_50MHz       IOMUXC_SW_PAD_CTL_PAD_SPEED(0)
#define SPEED_1_MEDIUM_100MHz   IOMUXC_SW_PAD_CTL_PAD_SPEED(1)
#define SPEED_2_MEDIUM_100MHz   IOMUXC_SW_PAD_CTL_PAD_SPEED(2)
#define SPEED_3_MAX_200MHz      IOMUXC_SW_PAD_CTL_PAD_SPEED(3)/* PUE 选择使用保持器还是上下拉 */
#define PUE_0_KEEPER_SELECTED       IOMUXC_SW_PAD_CTL_PAD_PUE(0)
#define PUE_1_PULL_SELECTED         IOMUXC_SW_PAD_CTL_PAD_PUE(1)/* PUS 上下拉配置 */
#define PUS_0_100K_OHM_PULL_DOWN  IOMUXC_SW_PAD_CTL_PAD_PUS(0)
#define PUS_1_47K_OHM_PULL_UP     IOMUXC_SW_PAD_CTL_PAD_PUS(1)
#define PUS_2_100K_OHM_PULL_UP    IOMUXC_SW_PAD_CTL_PAD_PUS(2)
#define PUS_3_22K_OHM_PULL_UP     IOMUXC_SW_PAD_CTL_PAD_PUS(3)

完整的代码请阅读源文件,这里只列出了文件“pad_config.h”部分代码(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】。

十一、编写启动文件

启动文件start.S参照官方GCC版本启动文件startup_MCIMX6Y2.S修改,位于/SDK_2.2_MCIM6ULL/devices/MCIMX6Y2/gcc

11.1 完整代码

/*DDR的前8K字节保留, [0x80000000-0x80001FFF] 保留为ROM区域 *//*定义内存起始地址和大小*/
#define m_DDR_start             0x80000000
#define m_DDR_size              0x20000000/*定义主代码区域,m_text_start将会作为中断向量表的起始地址,链接脚本中
*将该地址用作起始链接地址。
*/
#define  m_text_start           0x80002000/*定义Supervisor工作模式的栈起始地址和大小
*野火开发板标配512M字节的DDR, Supervisor工作模式的栈和IRQ工作模式的栈
*位于DDR的后2M地址,大小均为1M。
*/
#define   SUP_model_stack_start     0x9FE00000
#define   SUP_model_stack_size      0x00100000/*定义IRQ工作模式的栈起始地址和大小,大小为1M*/
#define   IRQ_model_stack_start     0x9FF00000
#define   IRQ_model_stack_size      0x00100000.globl light_led.text
.align 2         //设置字节对齐
.global _start
_start:ldr     pc, =Reset_Handler           /* Reset                  */ldr     pc, =Undefined_Handler       /* Undefined instructions */ldr     pc, =SVC_Handler             /* Supervisor Call        */ldr     pc, =PrefAbort_Handler       /* Prefetch abort         */ldr     pc, =DataAbort_Handler       /* Data abort             */.word   0                            /* RESERVED               */ldr     pc, =IRQ_Handler             /* IRQ interrupt          */ldr     pc, =FIQ_Handler             /* FIQ interrupt          */Reset_Handler:cpsid   i                         /* 全局关闭中断 *//* 关闭 I,DCache 和 MMU * 采取读-改-写的方式。*/mrc     p15, 0, r0, c1, c0, 0     /*读取CP15系统控制寄存器   */bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)关闭分支预测   */bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */mcr     p15, 0, r0, c1, c0, 0     /*  将修改后的值写回CP15寄存器   *//* 设置各个模式下的栈指针,* 注意:IMX6UL 的堆栈是向下增长的!* 堆栈指针地址一定要是 4 字节地址对齐的!!!* DDR 范围:0X80000000~0X9FFFFFFF 或者 0X8FFFFFFF*//* 定义IRQ工作模式的栈起始地址 */cps     #0x12                ldr     sp, =IRQ_model_stack_start    /*定义User工作模式的栈起始地址,与Supervisor相同*/cps     #0x1F               ldr     sp, =SUP_model_stack_start    /*定义Supervisor工作模式的栈起始地址,与User相同 */cps     #0x13                ldr     sp, =SUP_model_stack_start   /*跳转到系统初始化函数,初始化GIC、CACHE-L1、mmu等等*/ldr     r2, =SystemInit      blx     r2  /*开启全局中断*/cpsie   i                   /*跳转到到 main 函数执行,*/b main                b .        /*死循环*/Undefined_Handler:b Undefined_Handler.size Undefined_Handler, . - Undefined_Handler.align 2.arm.weak SVC_Handler.type SVC_Handler, %function
SVC_Handler:ldr   r0,=SVC_Handlerbx    r0.size SVC_Handler, . - SVC_Handler.align 2.arm.weak PrefAbort_Handler.type PrefAbort_Handler, %function
PrefAbort_Handler:ldr   r0,=PrefAbort_Handlerbx    r0.size PrefAbort_Handler, . - PrefAbort_Handler.align 2.arm.weak DataAbort_Handler.type DataAbort_Handler, %function
DataAbort_Handler:ldr   r0,=DataAbort_Handlerbx    r0.size DataAbort_Handler, . - DataAbort_Handler.align 2.arm.weak IRQ_Handler.type IRQ_Handler, %function
IRQ_Handler:push    {lr}                         /* Save return address+4                                */push    {r0-r3, r12}                 /* Push caller save registers                           */MRS     r0, spsr                     /* Save SPRS to allow interrupt reentry                 */push    {r0}MRC     P15, 4, r1, C15, C0, 0       /* Get GIC base address  */ADD     r1, r1, #0x2000              /* r1: GICC base address  */LDR     r0, [r1, #0xC]               /* r0: IAR  */push    {r0, r1}CPS     #0x13                        /* Change to Supervisor mode to allow interrupt reentry */push    {lr}                         /* Save Supervisor lr  */ldr     r2, =SystemIrqHandlerblx     r2POP     {lr}CPS     #0x12                        /* Back to IRQ mode                                     */POP     {r0, r1}STR     r0, [r1, #0x10]              /* Now IRQ handler finished: write to EOIR              */POP     {r0}MSR     spsr_cxsf, r0POP     {r0-r3, r12}POP     {lr}SUBS    pc, lr, #4.size IRQ_Handler, . - IRQ_Handler.align 2.arm.weak FIQ_Handler.type FIQ_Handler, %functionFIQ_Handler:ldr   r0,=FIQ_Handlerbx    r0.size FIQ_Handler, . - FIQ_Handler.end

11.2 分析代码

  • 内存相关的宏定义
    通过宏记录内存的大小、起始地址、链接起始地址以及栈起始地址和大小,便于后面代码中调用。
/*DDR的前8K字节保留, [0x80000000-0x80001FFF] 保留为ROM区域 *//*定义内存起始地址和大小*/
#define m_DDR_start             0x80000000
#define m_DDR_size              0x20000000/*定义主代码区域,m_text_start将会作为中断向量表的起始地址,链接脚本中
*将该地址用作起始链接地址。
*/
#define  m_text_start           0x80002000/*定义Supervisor工作模式的栈起始地址和大小
*野火开发板标配512M字节的DDR, Supervisor工作模式的栈和IRQ工作模式的栈
*位于DDR的后2M地址,大小均为1M。
*/
#define   SUP_model_stack_start     0x9FE00000
#define   SUP_model_stack_size      0x00100000/*定义IRQ工作模式的栈起始地址和大小,大小为1M*/
#define   IRQ_model_stack_start     0x9FF00000
#define   IRQ_model_stack_size      0x00100000
  • 定义中断向量表
    这些代码与官方启动文件完全相同。
.text            //代码段
.align 2         //设置2字节对齐
.global _start   //定义一个全局标号
_start:          //程序的开始ldr     pc, =Reset_Handler           /* Reset                  */ldr     pc, =Undefined_Handler       /* Undefined instructions */ldr     pc, =SVC_Handler             /* Supervisor Call        */ldr     pc, =PrefAbort_Handler       /* Prefetch abort         */ldr     pc, =DataAbort_Handler       /* Data abort             */.word   0                            /* RESERVED               */ldr     pc, =IRQ_Handler             /* IRQ interrupt          */ldr     pc, =FIQ_Handler             /* FIQ interrupt          */
  • 复位中断服务函数
    执行Reset_Handler时,CPU处于IRQ模式,用的是IRQ模式下的栈,需要先在Reset_Handler里设置好IRQ模式的栈,这样在中断模式里才可以使用栈,才能调用C函数。
    这部分做了两件事,第一,使用cpsid i指令关闭全局中断,防止中断干扰初始化过程。第二,剩余代码修改修改系统控制寄存器关闭我们暂时用不到的功能,例如cache、mmu等等。
Reset_Handler:cpsid   i                         /* 全局关闭中断 */mrc     p15, 0, r0, c1, c0, 0     /*读取CP15系统控制寄存器   */bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */mcr     p15, 0, r0, c1, c0, 0     /*  将修改后的值写回CP15寄存器   */
  • 设置栈地址
    本程序将栈地址设置在DDR的末尾处。IRQ工作模式的栈位于0x9FF00000地址处(DDR最后1M地址空间)。User模式和Supervisor模式使用相同的栈空间,位于0x9FE00000起始地址处,大小为1M。
/* 定义IRQ工作模式的栈起始地址 */
cps     #0x12
ldr     sp, =IRQ_model_stack_start/*定义User工作模式的栈起始地址,与Supervisor相同*/
cps     #0x1F
ldr     sp, =SUP_model_stack_start/*定义Supervisor工作模式的栈起始地址,与User相同 */
cps     #0x13
ldr     sp, =SUP_model_stack_start
  • 调用SystemInit()执行系统初始化
    上一步设置了”栈”所以这里就可以直接调用C函数了。
/*跳转到系统初始化函数,初始化GIC、CACHE-L1、mmu等等*/
ldr     r2, =SystemInit
blx     r2
  • 开启全局中断并跳转到main()函数开始执行
/*开启全局中断*/
cpsie   i/*跳转到到 main 函数执行,*/
b main

SystemInit()函数执行完成后,系统已经可以接受中断了。SystemInit()函数如下所示:

void SystemInit(void)
{uint32_t sctlr;uint32_t actlr;
/*FPU 相关代码省略*/L1C_InvalidateInstructionCacheAll();L1C_InvalidateDataCacheAll();actlr = __get_ACTLR();actlr = (actlr | ACTLR_SMP_Msk); /* Change to SMP mode before enable DCache */__set_ACTLR(actlr);sctlr = __get_SCTLR();sctlr = (sctlr & ~(SCTLR_V_Msk | /* Use low vector */SCTLR_A_Msk | /* Disable alignment fault checking */SCTLR_M_Msk)) /* Disable MMU */| (SCTLR_I_Msk |         /* Enable ICache */SCTLR_Z_Msk |         /* Enable Prediction */SCTLR_CP15BEN_Msk |   /* Enable CP15 barrier operations */SCTLR_C_Msk);         /* Enable DCache */__set_SCTLR(sctlr);/* Set vector base address */GIC_Init();__set_VBAR((uint32_t)__VECTOR_TABLE);/*FPU 相关代码省略*/
}

第7、8行: 无效化Icache和Dcache。虽然我们前面已经关闭了cache这里再次无效化cache可能出于安全考虑,我们不深究,参考官方的写即可。
第10-11行: 修改辅助控制寄存器ACTLR,使能SMP模式。
第14-22行: 修改系统控制寄存器,开启我们需要的的功能,例如默认开启了Cahe,关闭了MMU。
第25行: 初始化GIC。在GIC_Init初始化函数可直接使用NXP官方函数,无需修改。
第27行: 这部分非常重要,它用于设置中断向量表起始地址。在程序中,中断向量表可以放到任意的位置,但是必须将中断向量表的起始地址写入VBAR寄存器。这样中断发生后CPU才能找到中断向量表。

十二、编写链接脚本

写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言,汇编是将汇编文件转化为机器码。

使用 arm-none-eabi-gcc -g -c led.S -o led.o 命令完成源码的编译、汇编工作,生成了 .o文件。编译和汇编是针对单个源文件,也就编译完成后一个源文件(.c.S.s)对应一个 .o 文件。程序链接阶段就会将这些 .o 链接成一个文件。

链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。

/interrupt_init 下创建 base.lds

12.1 完整代码

ENTRY(_start)
SECTIONS {. = 0x80002000;. = ALIGN(4);.text :{start.o (.text)*(.text)}. = ALIGN(4);.data : {*(.data)}. = ALIGN(4);.bss : {*(.bss) }
}

12.2 分析代码

  • 指定程序的入口
    ENTRY(_start) 用于指定程序的入口,ENTRY() 是设置入口地址的命令, “_start” 是程序的入口,led程序的入口地址位于 start.S“_start” 标号处。
 ENTRY(_start)
  • 定义SECTIONS
    SECTIONS 可以理解为是一块区域,我们在这块区域排布我们的代码,链接时链接器就会按照这里的指示链接我们的代码。
 SECTIONS {
···
···
}
  • 定义链接起始地址
    “.” 运算符代表当前位置。 我们在SECTION的最开始使用 “.= 0x80002000” 就是将链接起始地址设置为0x80002000。DDR的前8K字节保留, [0x80000000-0x80001FFF] 保留为ROM区域。
. = 0x80002000;
  • 设置字节对齐
    “. = ALIGN(4);” 它表示从当前位置开始执行四字节对齐。假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。

  • 设置代码段
    “.text :” 用于定义代码段,固定的语法要求,我们按照要求写即可。在“{}”中指定那些内容放在代码段。
    start.o 中的代码放到代码段的最前面。start.S是启动代码应当首先被执行,所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可,通配符 “*” 在这里表示其他剩余所有的 .o文件。

   . = ALIGN(4);.text :{start.o (.text)*(.text)}
  • 设置数据段
    同设置代码段类似,首先设置字节对齐,然后定义代码段。在数据段里使用 “*” 通配符, 将所有源文件中的代码添加到这个数据段中。
   . = ALIGN(4);.data :{*(.data)}
  • 设置BSS段
    设置方法与设置数据段完全相同。
. = ALIGN(4);.bss :{*(.bss)}

十三、编写makefile文件

程序编写完成后需要依次输入编译、链接、格式转换命令才能最终生成二进制文件。这种编译方式效率低、容易出错。

使用makefile只需要在所在文件夹下执行make命令,makefile工具便会自动完成程序的编译、链接、格式转换等工作。正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin文件。

修改makefile主要包括两部分

  • 第一部分,在“device”文件夹下添加并编写子makefile。
  • 第二部分,修改主makefile。

13.1 编写子makefile

/interrupt_init/device 下创建 makefile

子makefile: 用于将“device”文件夹下的驱动源文件编译为一个“.o”文件

all : button.o  led.o system_MCIMX6Y2.oarm-none-eabi-ld -r $^  -o device.o%.o : %.carm-none-eabi-gcc ${header_file} -c $^%.o : %.Sarm-none-eabi-gcc ${header_file} -c $^clean:-rm -f *.o *.bak
  • 添加最终目标以及依赖文件
    生成最终目标“device.o”。如果程序中新增了某个外设驱动程序,只需要将对应的“.o”文件填入“依赖”处即可。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
all : button.o  led.o system_MCIMX6Y2.oarm-none-eabi-ld -r $^  -o device.o
  • 添加编译C文件的命令
    编译“device”文件夹下的所有“.c”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.carm-none-eabi-gcc ${header_file} -c $^
  • 添加汇编文件编译命令
    编译“device”文件夹下的所有“.S”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.Sarm-none-eabi-gcc ${header_file} -c $^
  • 添加清理命令
    “clean” 为目标用于删除make生成的文件。
clean:-rm -f *.o *.bak

13.2 修改主makefile

主makefile的改动主要有两点:

  1. 在编译命令中指明头文件位置。
  2. 使用命令调用子makefile,生成依赖文件。
#定义变量,用于保存编译选项和头文件保存路径
header_file := -fno-builtin -I$(shell pwd)/include
export header_fileall : start.o main.o device/device.o arm-none-eabi-ld -Tbase.lds $^ -o base.elf arm-none-eabi-objcopy -O binary -S -g base.elf base.bin%.o : %.Sarm-none-eabi-gcc -g -c $^
%.o : %.carm-none-eabi-gcc $(header_file) -c $^     #调用其他文件的makefile
device/device.o :make -C device all.PHONY: copy
copy:cp ./base.bin  /home/pan/download/embedfire#定义清理伪目标
.PHONY: clean
clean:make -C device clean-rm -f *.o *.elf *.bin
  • 添加编译选项和头文件保存路径
    定义变量 “header_file”。在makefile中“变量”更像C原因中的宏定义。
    “-fno-builtin” 是一个编译选项,用于解决库函数与自己编写函数同名问题。
    “-I$(shell pwd)/include” 用于指定头文件路径。
    “export header_file” 声明后可以在其他makefile中调用。
header_file := -fno-builtin -I$(shell pwd)/include
export header_file
  • 添加最终目标以及依赖文件
all : start.o main.o device/device.o
  • 添加链接命令
    “-Tbase.lds” 表示使用base.lds链接脚本链接程序。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
arm-none-eabi-ld -Tbase.lds $^ -o base.elf
  • 添加格式转换命令
    “-O binary” 指定输出二进制文件。
    “-S” 不从源文件中复制重定位信息和符号信息。
    “-g” 不从源文件中复制可调试信息。
arm-none-eabi-objcopy -O binary -S -g base.elf base.bin
  • 添加汇编文件编译命令
    “$^” 替代要编译的源文件。
%.o : %.Sarm-none-eabi-gcc -g -c $^
  • 添加编译C文件的命令
    “$^” 替代要编译的源文件。
%.o : %.carm-none-eabi-gcc $(header_file) -c $^
  • 添加调用其他文件的makefile
    定义生成“device/device.o”的命令,“device.o”文件由子makefile生成,所以这里只需要调用子makefile即可。
device/device.o :make -C device all
  • 添加清理命令
    在清理命令中不但要清理主makefile所在文件夹的内容还要调用子makefile的清理命令以清理子makefile所在文件夹的内容。
    “.PHONY” 定义了伪目标“clean”。伪目标一般没有依赖,并且 “clean” 伪目标一般放在Makefile文件的末尾。
    “clean” 为目标用于删除make生成的文件。
.PHONY: clean
clean:make -C device clean-rm -f *.o *.elf *.bin

十四、编写C语言代码

14.1 添加按键中断初始化和中断服务函数代码

14.1.1 button.h

/interrupt_init/include 下创建 button.h

#ifndef button_h
#define button_h#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
#include "system_MCIMX6Y2.h"/*按键2 GPIO端口、引脚号及IOMUXC复用宏定义*/
#define button2_GPIO               GPIO5
#define button2_GPIO_PIN           (1U)
#define button2_IOMUXC             IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01/* 按键PAD配置 */
#define button_PAD_CONFIG_DATA            (SRE_0_SLOW_SLEW_RATE|\DSE_6_R0_6|\SPEED_2_MEDIUM_100MHz|\ODE_0_OPEN_DRAIN_DISABLED|\PKE_0_PULL_KEEPER_DISABLED|\PUE_0_KEEPER_SELECTED|\PUS_0_100K_OHM_PULL_DOWN|\HYS_1_HYSTERESIS_ENABLED)/* 配置说明 : *//* 转换速率: 转换速率慢驱动强度: R0/6 带宽配置 : medium(100MHz)开漏配置: 关闭 拉/保持器配置: 关闭拉/保持器选择: 保持器(上面已关闭,配置无效)上拉/下拉选择: 100K欧姆下拉(上面已关闭,配置无效)滞回器配置: 开启 */ /*函数*/
void button2_init(void);
int get_button2_status(void);
void interrupt_button2_init(void);
void EXAMPLE_GPIO_IRQHandler(void);#endif

14.1.2 button.c

/interrupt_init/device 下创建 button.c

为了简化程序,我们并没有使用GIC接口寄存器配置每个中断的中断优先级、中断分组等等。中断优先级和中断优先级分组保持函数SystemInit()设定的默认值。

/*说明:* 按键对应Pro 开发板 button2 ,button2输入引脚接有下拉电阻,默认低电平,按键按下后变为高电平。* 按键初始化函数有两个,函数button2_init 仅将引脚设置为输入,通过轮询检测按键状态。函数interrupt_button2_init* 初始化了GPIO中断,上升沿触发.
*/
#include "button.h"/*简单延时函数*/
void delay_button(uint32_t count)
{volatile uint32_t i = 0;for (i = 0; i < count; ++i){__asm("NOP"); /* 调用nop空指令 */}
}/*按键初始化函数*/
void button2_init(void)
{/*按键初始化*/CCM_CCGR1_CG15(0x3);  //开启GPIO5的时钟/*设置 绿灯 引脚的复用功能以及PAD属性*/IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01,0);     IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, button_PAD_CONFIG_DATA); GPIO5->GDIR &= ~(1<<1);  //设置GPIO5_01为输入模式
}extern uint8_t button_status;  //按键状态标记 0,按键未按下。1,按键按下
/*按键初始化函数*/
void interrupt_button2_init(void)
{volatile uint32_t *icr;  //用于保存 GPIO-ICR寄存器的地址,与 icrShift 变量配合使用uint32_t icrShift;       //引脚号大于16时会用到,icrShift = button2_GPIO_PIN;  //保存button2引脚对应的 GPIO 号/*添加中断服务函数到  "中断向量表"*/SystemInstallIrqHandler(GPIO5_Combined_0_15_IRQn, (system_irq_handler_t)EXAMPLE_GPIO_IRQHandler, NULL);GIC_EnableIRQ(GPIO5_Combined_0_15_IRQn);                 //开启中断CCM_CCGR1_CG15(0x3);  //开启GPIO5的时钟/*设置 按键引脚的PAD属性*/IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01,0);     IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, button_PAD_CONFIG_DATA); /*设置GPIO方向(输入或输出)*/GPIO5->IMR &= ~(1 << button2_GPIO_PIN);  //寄存器重置为默认值GPIO5->GDIR &= ~(1<<1);                  //设置GPIO5_01为输入模式/*设置GPIO引脚中断类型*/GPIO5->EDGE_SEL &= ~(1U << button2_GPIO_PIN);//寄存器重置为默认值if(button2_GPIO_PIN < 16){icr = &(GPIO5->ICR1);}else{icr = &(GPIO5->ICR2);icrShift -= 16;}/*按键引脚默认低电平,设置为上升沿触发中断*/*icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));button2_GPIO->IMR |= (1 << button2_GPIO_PIN); //使能GPIO引脚中断
}/*按键状态输出函数*/
int get_button2_status(void)
{if((GPIO5->DR)&(1<<1)){delay_button(0xFF);if((GPIO5->DR)&(1<<1)){return 1;}}return 0;
}/*按键中断处理函数*/
void EXAMPLE_GPIO_IRQHandler(void)
{/*按键引脚中断服务函数*/button2_GPIO->ISR = 1U << button2_GPIO_PIN;  //清除GIIP中断标志位if(button_status > 0){button_status = 0;}else{button_status = 1;}
}
  • 第44、45行:
    添加中断服务函数到”本地中断向量表”。在STM32程序中,中断服务函数在启动文件中已经定义好了,我们只需要实现即可。但这里需要手动添加中断服务函数到”本地向量表中” SystemInstallIrqHandler函数原型如下所示。从以上代码不难看出,该函数就是根据中断号填充全局结构体数组”irqTable”。SystemInstallIrqHandler函数共有三个参数,
    irq指定中断对应的中断号,
    handler指定中断服务函数,
    userParam指定中断服务函数的用户参数,如果不同设置为”NULL”即可。
/*添加中断服务函数到  "中断向量表"*/
SystemInstallIrqHandler(GPIO5_Combined_0_15_IRQn, \(system_irq_handler_t)EXAMPLE_GPIO_IRQHandler, NULL);
void SystemInstallIrqHandler(IRQn_Type irq, \system_irq_handler_t handler,\void *userParam)
{irqTable[irq].irqHandler = handler;irqTable[irq].userParam = userParam;
}
  • 第46行:
    开启中断。GIC相关操作函数定义在core_ca.h文件。感兴趣可以使用这些函数实现更完善的中断。
GIC_EnableIRQ(GPIO5_Combined_0_15_IRQn);                 //开启中断
  • 第49行:
    GPIO相关初始化。这部分开启GPIO5的时钟并设置对应GPIO引脚属性,这和LED灯引脚初始化相同。
CCM->CCGR1 |= CCM_CCGR1_CG15(0x3);  //开启GPIO5的时钟
  • 第56、57行:
    设置引脚为输入模式
/*设置GPIO方向(输入或输出)*/
GPIO5->IMR &= ~(1 << button2_GPIO_PIN);  //寄存器重置为默认值
GPIO5->GDIR &= ~(1<<1);                  //设置GPIO5_01为输入模式
  • 第60-70行:
    设置引脚触发方式
/*设置GPIO引脚中断类型*/
GPIO5->EDGE_SEL &= ~(1U << button2_GPIO_PIN);//寄存器重置为默认值if(button2_GPIO_PIN < 16)
{icr = &(GPIO5->ICR1);
}
else
{icr = &(GPIO5->ICR2);icrShift -= 16;
}
  • 第75行:
    使能GPIO引脚中断
button2_GPIO->IMR |= (1 << button2_GPIO_PIN); //使能GPIO引脚中断

14.2 main.c

/interrupt_init 下创建 main.c

GPIO引脚中断初始化完成后,验证代码就比较简单了。我们定义一个全局变量,在按键中断服务函数中循环切换0和1。 在main函数循环检测全局变量的值,大于0则亮红灯,否则亮绿灯。

#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
#include "system_MCIMX6Y2.h"#include "button.h"
#include "led.h"uint8_t button_status = 0;
/*简单延时函数*/
void delay(uint32_t count)
{volatile uint32_t i = 0;for (i = 0; i < count; ++i){__asm("NOP"); /* 调用nop空指令 */}
}int main()
{int i = 0;rgb_led_init();                          //初始化 RGB 灯,初始化后 默认所有灯都不亮。interrupt_button2_init();                //初始化引脚,和引脚的中断方式以及开启引脚中断。while (1){if(button_status > 0){/*绿灯亮*/red_led_off;green_led_on;}else{/*红灯亮*/green_led_off;red_led_on;}}return 0;
}

十五、编译下载验证

15.1 编译代码

make

执行make命令,生成base.bin文件。

15.2 代码烧写

编译成功后会在当前文件夹下生成.bin文件,这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。

为二进制文件添加头部信息并烧写到SD卡。查看 IMX6ULL学习笔记(12)——通过SD卡启动官方SDK程序

进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径> 命令,例如要烧写的 base.bin 位于 home 目录下,则烧写命令为 ./mkimage.sh /home/button.bin

执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。

15.3 实验现象

烧写完成,首先将开发板启动方式设置为SD卡启动,将SD卡插入开发板卡槽。 接通电源后循环按下sw2(KEY)按键,正常情况下可以看到RGB灯交替亮红、绿色。


• 由 Leung 写于 2023 年 3 月 15 日

• 参考:10. 中断

IMX6ULL学习笔记(18)——GPIO中断相关推荐

  1. IMX6ULL学习笔记(19)——时钟系统

    一.时钟系统简介 I.MX6U 的系统主频为 528MHz,有些型号可以跑到 696MHz,但是默认情况下内部 boot rom 会将 I.MX6U 的主频设置为 396MHz.我们在使用 I.MX6 ...

  2. IMX6ULL学习笔记(20)——UART串口使用

    一.UART简介 i.MX6U 芯片具有多达 8 个 UART 外设用于串口通讯,UART 是在 USART 基础上裁剪掉了同步通信功能,只支持异步通信.简单区分同步和异步就是看通信时需不需要对外提供 ...

  3. IMX6ULL学习笔记(9)——通过SD卡启动Linux内核

    一.搭建环境 通过以下方式烧写一个镜像: IMX6ULL学习笔记(2)--通过SD卡烧录镜像 二.设置为SD卡启动模式 开发板插入烧录好U-Boot的SD卡. 根据以下BOOT拨码开关启动配置表,调整 ...

  4. IMX6ULL学习笔记(四) —— uboot 启动流程

    IMX6ULL 学习笔记 version : v1.0 「2023.4.27」 author: Y.Z.T. 摘要: 随记, 记录 I.MX6ULL 系列 SOC 的uboot 启动流程 ⭐️ 目录 ...

  5. Hadoop学习笔记—18.Sqoop框架学习

    Hadoop学习笔记-18.Sqoop框架学习 一.Sqoop基础:连接关系型数据库与Hadoop的桥梁 1.1 Sqoop的基本概念 Hadoop正成为企业用于大数据分析的最热门选择,但想将你的数据 ...

  6. Ext.Net学习笔记18:Ext.Net 可编辑的GridPanel

    Ext.Net GridPanel 有两种编辑模式:编辑单元格和编辑行. 单元格编辑: 行编辑: 可以看出,单元格编辑的时候,只有单元格会进入编辑模式,而行编辑模式中则对编辑行的所有可编辑字段统一进行 ...

  7. 华为HCIA-datacom 学习笔记18——SDN与NFV概述

    华为HCIA-datacom 学习笔记18--SDN与NFV概述 SDN与NFV概述 1.计算机时代的演进 1.1大型机 专门的硬件 专门的操作系统 专门的应用.(稳定性能好,但封闭) 1.2小型机 ...

  8. 2020-4-12 深度学习笔记18 - 直面配分函数 5 ( 去噪得分匹配,噪声对比估计NCE--绕开配分函数,估计配分函数)

    第十八章 直面配分函数 Confronting the Partition Function 中文 英文 2020-4-8 深度学习笔记18 - 直面配分函数 1 ( 配分函数概念,对数似然梯度) 2 ...

  9. Python学习笔记18:实操案例十五(记录用户登录日志,模拟淘宝客服自动回复)

    Python学习笔记18:实操案例十五(记录用户登录日志,模拟淘宝客服自动回复) 网课传送门:https://www.bilibili.com/video/BV1Sw411Z779?p=168& ...

最新文章

  1. jquery入门 修改网页背景颜色
  2. wordpress网站后台打开速度很卡很慢解决方法?
  3. c语言如何使四种运算符同级,二 如何学习C语言的运算符和运算顺序
  4. java comet demo,[转]comet4j的简单应用
  5. [BUUCTF-pwn]——picoctf_2018_are you root
  6. sql replace 双引号变单引号_sql-汇总、排序以及分析思路
  7. observable_Java Observable setChanged()方法与示例
  8. 动手学深度学习(PyTorch实现)(二)--softmax与分类模型
  9. java 账户和密码 3次_模拟登录,给三次机会,并提示还有几次。Java实现
  10. 2017-2018-1 20179202《Linux内核原理与分析》第四周作业
  11. matlab对一个数组进行补零,matlab 输出 整数 补0
  12. EnableQ,细腻让其与众不同
  13. k8s 集群部署springcloud 多应用
  14. TeamViewer突破地域限制,解决办公难题
  15. 青少年软件编程(Python)等级考试试卷一级(判断题)2021-9
  16. CNN基础——如何设置BatchSize
  17. 微信公众平台iPhone版开始内测了
  18. Oracle 查询库文件信息
  19. python十六进制转为二进制数_python进制转换(二进制、十进制和十六进制)及注意事项...
  20. 我们上语文英语音乐计算机和美术英语,语文、英语、数学、是什么意思

热门文章

  1. 记录下在线扩容服务器遇到的问题 NOCHANGE: partition 1 is size 419428319. it cannot be grown
  2. vue项目模拟后台数据
  3. HTML 行间距的设置方法与问题
  4. 李德毅院士:大数据认知
  5. 【苏宁消费金融对接苏宁联盟链 打造金融区块链应用案例】
  6. python输入n个数、输出最小的数字_程序查找最少的斐波纳契数以在Python中加到n?...
  7. 数字计算机模拟人脑,人造突触问世计算机模拟人脑不是梦
  8. ballerina 学习二十八 快速grpc 服务开发
  9. 云时代编程语言Ballerina发布,TIOBE9月排行榜PHP排名在边缘飘摇(2019/09/16)
  10. 基于Cesium的火箭发射演示