IMX6ULL学习笔记(18)——GPIO中断

2023-05-16

一、中断简介

相比 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)未使用
0X18IRQ 中断(IRQ Interrupt)外部中断模式(IRQ)芯片内部的外设中断(串口中断、DMA中断、外部中断等等)都会引起此中断的发生。
0X1CFIQ 中断(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, =SystemIrqHandler
    BLX     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, r0

    POP     {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;
 #endif

  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);
  // 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_Handler
    bx    r0
    .size SVC_Handler, . - SVC_Handler

    .align 2
    .arm
    .weak PrefAbort_Handler
    .type PrefAbort_Handler, %function
PrefAbort_Handler:
    ldr   r0,=PrefAbort_Handler
    bx    r0
    .size PrefAbort_Handler, . - PrefAbort_Handler

    .align 2
    .arm
    .weak DataAbort_Handler
    .type DataAbort_Handler, %function
DataAbort_Handler:
    ldr   r0,=DataAbort_Handler
    bx    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, =SystemIrqHandler
    blx     r2
                           
    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, r0

    POP     {r0-r3, r12}
    POP     {lr}
    SUBS    pc, lr, #4
    .size IRQ_Handler, . - IRQ_Handler

    .align 2
    .arm
    .weak FIQ_Handler
    .type FIQ_Handler, %function


FIQ_Handler:
    ldr   r0,=FIQ_Handler
    bx    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.o
	arm-none-eabi-ld -r $^  -o device.o
	
%.o : %.c
	arm-none-eabi-gcc ${header_file} -c $^
	
%.o : %.S
	arm-none-eabi-gcc ${header_file} -c $^

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


all : 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 : %.S
	arm-none-eabi-gcc -g -c $^ 
%.o : %.c
	arm-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 : %.S
  arm-none-eabi-gcc -g -c $^
  • 添加编译C文件的命令
    “$^” 替代要编译的源文件。
%.o : %.c
  arm-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. 中断

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

IMX6ULL学习笔记(18)——GPIO中断 的相关文章

  • 机器学习 小工具

    python美化打印的标准库 xff1a pprint
  • python数据类型

    python数据类型 数字 xff08 整型 xff0c 浮点型 xff09 字符串列表 xff1a 元组 xff1a 字典 xff1a 列表 元组以及字典的区别 xff1f 列表 元祖以及字典都是容器型数据类型 xff0c 可以对列表中的
  • 实用机器学习(hw1/hw4)

    实用机器学习 hw1 hw4 文章目录 实用机器学习 hw1 hw4 1 环境安装2 baseline 代码分析3 提升精度代码4 机器学习模型 1 环境安装 autogluon 2 baseline 代码分析 span class tok
  • 没有与这些操作数匹配的运算符

    没有与这些操作数匹配的 lt lt 运算符 include与 include lt string h gt 的区别 lt string h gt 的区别 是C 43 43 特化的字符容器 xff0c 内含string类 lt string
  • gazebo模型下载以及配置

    最近在学习ROS xff0c 主要是为了结合SLAM仿真使用 启动gazebo命令 roscore 在另一个终端执行 gazebo 就可以进入清爽的gazebo界面 xff08 如果屏幕出现黑屏并不是安装错误可以稍微等待一会 xff09 x
  • SLAM中常用数据集下载链接(TUM KITTI DSO Mono EuRoC)

    TUM 链接 xff1a https pan baidu com s 1nwXtGqH 密码 xff1a lsgr KITTI 链接 xff1a https pan baidu com s 1htFmXDE 密码 xff1a uu20 KI
  •  windows docker 更改镜像安装目录

    目录 1 问题 1 1 版本信息 2 修改Docker盘位操作 2 1 停止docker 2 2 备份已有的数据 2 3 删除旧数据 数据未备份前请谨慎操作 2 4 导入数据到新盘 2 5 启动Docker START 1 问题 Windo
  • gnssins代码阅读

    这个代码是GNSS和INS紧组合的 xff1a https github com marcoamm gnssins xff0c 实现了ppp和ins紧组合 改变数据需要改代码的地方 xff1a imu tactical 61 fopen 3
  • 深度解析FUTABA的SBUS协议(/天地飞遥控器的WBUS协议/Robomaster接收机的DBUS协议)到底是啥?

    写在前面 xff1a 无论是SBUS xff08 日本FUTABA xff0c 所以航模 xff0c 车模爱好者都知道的公司 xff0c 一个好点遥控器近万了 xff09 xff0c 还是WBUS xff08 天地飞遥控器接收机用 xff0
  • 贝塞尔曲线动画C++简单实践

    目录 贝塞尔曲线简介一阶贝塞尔二阶贝塞尔三阶贝塞尔N阶贝塞尔曲线 贝塞尔曲线在动画中的应用实践求曲线散点坐标将曲线应用到动画动画框架cmd动画窗口动画 完整代码示例代码核心类代码BezierCurve Animator Console 参考
  • package.xml文件介绍

    package xml文件介绍 在ROS中创建功能包时 xff0c 会自动生成package xml文件 xff0c pacakge xml 包含了package的名称 版本号 内容描述 维护人员 软件许可 编译构建工具 编译依赖 运行依赖
  • ubuntu误修改了bashrc文件的解决办法

    在安装Pycharm的过程中配置JAVA的JDK环境变量时 xff0c 将bashrc内的内容不小心修改了 xff0c 导致命令窗口中的很多命令不能执行 xff0c 并且su及sudo这些权限的命令也用不了 xff0c 问题信息如下图所示
  • docker安装canal1.1.5监控mysql的binlog日志并配置rocketmq进行数据同步到elasticsearch(超级大干货)

    直接来 xff0c 不逼逼 xff08 canal官网说的很明白 xff0c 伪从节点请求dump 然后这个那个的 xff0c 自行查阅资料 xff09 1 直接拉取canal镜像 docker pull canal canal serve
  • 相机定位、相机重定位和视觉里程计的概念定义

    相机定位 相机重定位和视觉里程计的概念定义 什么是相机定位 xff1f 什么是相机重定位 xff1f 什么是视觉里程计 xff1f 相机定位 相机定位 xff08 Camera Localization xff09 是求解基于基本坐标系下的
  • vscode使用restClient实现各种http请求

    vscode使用restClient实现各种http请求 一 xff0c 安装插件 首先 xff0c 我们要在vscode的扩展中 xff0c 搜索rest Client xff0c 然后安装它 xff0c 这里我已经安装过了 安装后 xf
  • akka-2 利用模式匹配,实现worker节点向master报告本机配置信息

    1 创建本机配置信息类 WorkInfo Worker的本机信息类 id 主机名称 momery 内存大小 cores CPU核数 直接使用默认构造创建消息类 Worker的本机信息类 id 主机名称 momery 内存大小 cores C
  • 多路归并排序-Python实现大文件排序,合并排序

    使用python实现多 K 路归并外部排序 xff0c 解决小内存排序大文件问题 上一篇中 xff0c 我们实现了一般的归并排序 归并排序递归与非递归 Python实现 在实际工作中 xff0c 多个有序数列合并成一个 xff0c 大文件或
  • GINAV使用

    在配置文件中 xff0c 绝对不能 psd gyro 61 8 46e 14 等号后面必须要有空格才能区分开 psd gyro 61 8 46e 14 运行谷歌数据直接是空的图像
  • ZED双目相机(c++程序实现)

    1 前提条件 SDK最新版 xff08 从官网直接下载安装 xff0c 默认安装路径 xff09 CUDA xff08 安装对应版本 xff0c 此步需要电脑GPU支持 xff08 此步劝退一部人 xff09 xff09 下载zed示例包
  • fork()使用详解

    其他参考 xff1a linux中fork xff08 xff09 函数详解 一 fork入门知识 进程的定义 xff1a 进程是一个执行中的程序的实例 xff0c 是系统进行资源分配和调度的一个独立单位 PCB是进程存在的唯一标识 PCB

随机推荐