锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

ARM嵌入式裸机简单使用

时间:2022-09-09 00:30:00 嵌入式电容触摸液晶显示器3rt电容切换专用接触器

基于正点原子 ALPHA开发板,长文预警,建议收藏后检查

文章目录

  • 主频与时钟
    • I.MX6U系统时钟分析
      • 7路PLL
      • 时钟树
      • 外设如何选择时钟?
      • 需要初始化PLL和PFD
    • I.MX6U系统配置
      • 配置系统主频
      • 各个PLL时钟的配置
      • 其它外设时钟源配置
    • C代码
  • 中断
    • Cortex-A7中断系统
      • Cortex-A中断向量表
      • 中断向量偏移
      • GIC中断控制器
    • IMX6U中断号
      • 编写中断服务函数
      • 编制按键中断例程。
      • 修改start.S
      • CP15协处理器
      • 退出中断
    • 中断汇编代码
      • 复位中断函数
      • IRQ中断函数
    • C语言中断编写
      • 具有中断号的中断处理函数
      • irqTable结构体
      • 中断函数服务表赋值函数
      • 初始化函数中断,在main开始调用函数
    • 中断初始化和中断过程
      • 初始化过程中断
      • 中断过程
  • 定时器和延迟
    • EPIT定时器
      • 相关寄存器
      • C程序编写
        • 初始化
        • 中断服务函数
        • 外部调用
    • GPT定时器
      • 相关寄存器
      • C程序编写
        • GPT初始化
        • 延时api
  • 串口
    • 6ULL串口寄存器
    • C程序编写
      • 使能/失能代码
      • 初始化代码
      • 发送代码
      • 接收代码
    • 移植C标准库函数printf
  • DDR3
    • 内存简介
    • DDR3时间参数
    • I.MX6U MMDC控制器
    • DDR3L初始化和测试
      • ddr_stress_tester配置文件
      • 开始测试
  • RGBLCD
    • 显示原理
      • 像素点
      • 分辨率
      • 像素格式
      • LCD屏幕接口
      • LCD时间参数和LCD时序
        • 行时序
        • 帧时序
        • 像素时钟
      • 显存
    • 6ULL的RGB接口
      • LCDIF控制器接口原理
      • LCD设置像素时钟
    • C程序编写
      • LCD编写驱动程序
        • LCD初始化
          • IO复用初始化
          • 时钟初始化
          • LCD控制器的初始化
          • 底层API
      • LCD操作API函数编写
  • RTC
    • RTC原理详解
    • 时间乱码的问题
      • 问题
      • 解决问题的方法
    • C程序编写
      • 相关API以及结构体
      • RTC初始化
  • I2C
    • I2C简要介绍协议
    • 开发板上的I2C
      • AP3216C简介
    • 6ULL I2C接口详解
    • C程序编写
      • I2C模块
        • I2C初始化
        • I2C产生主机时序
        • I2C读写
        • I2C读写封装
      • AP3216C模块
        • 读写
        • 初始化
        • 读取数据
  • SPI
    • SPI协议简单介绍
    • 开发板上的SPI接口
    • 6ULL SPI接口详解
    • C程序编写
      • SPI模块
        • SPI初始化
        • 读写数据
      • 错误修改
  • 多点电容触摸屏
    • 多点电容触摸屏简介
    • FT54x6/FT52x6电容接触芯片
    • C程序编写
      • 芯片的读写
      • 初始化芯片
      • 读取坐标
      • 中服务
  • PWM背光实验
    • 6ULL的PWM
    • C程序编写
      • 初始化
  • 写在最后

主频与时钟

恩智浦在内部ROM中配置为默认的396MHz。但是官方文档显示最高可达800M左右,有些浪费CPU资源。

##硬件原理图分析

​ 1、32.768khz的晶振,共给RTC使用。

​ 2、在6U的T16和T17这两个IO上接了一个24MHz的晶振。

I.MX6U系统时钟分析

7路PLL

​ 为了方便生成时钟,6从24MHz晶振生出来7路PLL。这7路PLL中有的又生出来PFD。

PLL1:ARM PLL供给ARM内核。

PLL2:sysytem PLL,528MHz,528_PLL,此路PLL分出了4路PFD,分别为PLL2_PFD0~PFD3

PLL3: USB1 PLL,480MHz 480_PLL,此路PLL分出了4路PFD,分别为PLL3_PFD0~PFD3。

PLL4: Audio PLL,主供音频使用。

PLL5: Video PLL,主供视频外设,比如RGB LCD接口,和图像处理有关的外设。

PLL6:ENET PLL,主供网络外设。

PLL7: USB2_PLL ,480MHz,无PFD。

时钟树

太难了,原图分为了两截,我重新用ps拼接了一下

时钟树

外设如何选择时钟

​ 比如ESAI时钟源选择:

​ PLL4、PLL3_PFD2、PLL5、PLL3。

需要初始化的PLL和PFD

​ PLL1,

​ PLL2,以及PLL2_PFD0~PFD3.

​ PLL3以及PLL3_PFD0~PFD3.

​ 一般按照时钟树里面的值进行设置。

I.MX6U系统配置

系统主频的配置

  1. 设置ARM内核主频为528MHz,设置CACRR寄存器的ARM_PODF位为2分频,然后设置PLL1=1056MHz即可。CACRR的bit3~0为ARM_PODF位,可设置0~7,分别对应1~8分频。应该设置CACRR寄存器的ARM_PODF=1。
  2. 设置PLL1=1056MHz。PLL1=pll1_sw_clk。pll1_sw_clk有两路可以选择,分别为pll1_main_clk,和step_clk,通过CCSR寄存器的pll1_sw_clk_sel位(bit2)来选择。为0的时候选择pll1_main_clk,为1的时候选step_clk。
  3. 设置系统时钟的时候需要给6ULL一个临时的时钟,也就是step_clk。在修改PLL1的时候需要将pll1_sw_clk切换到step_clk上。
  4. 设置step_clk。Step_clk也有两路来源,由CCSR的step_sel位(bit8)来设置,为0的时候设置step_clk为osc=24MHz。
  5. 时钟切换成功以后就可以修改PLL1的值。
  6. 通过CCM_ANALOG_PLL_ARM寄存器的DIV_SELECT位(bit6~0)来设置PLL1的频率,公式为:Output = fref*DIV_SEL/2
    因此 1056=24*DIV_SEL/2 -> DIEV_SEL=88。
    设置CCM_ANALOG_PLL_ARM寄存器的DIV_SELECT位=88即可。PLL1=1056MHz
  7. 设置CCM_ANALOG_PLL_ARM寄存器的ENABLE位(bit13)为1,也就是使能输出。
  8. 在切换回PLL1之前,设置置CACRR寄存器的ARM_PODF=1!!切记。

各个PLL时钟的配置

  1. PLL2和PLL3。PLL2固定为528MHz,PLL3固定为480MHz。
  2. 初始化PLL2_PFD0~PFD3。寄存器CCM_ANALOG_PFD_528用于设置4路PFD的时钟。比如PFD0= 528*18/PFD0_FRAC。设置PFD0_FRAC位即可。比如PLL2_PFD0=352M=528*18/PFD0_FRAC,因此FPD0_FRAC=27。
  3. 初始化PLL3_PFD0~PFD3

其他外设时钟源配置

​ AHB_CLK_ROOT、PERCLK_CLK_ROOT以及IPG_CLK_ROOT。

  1. 因为PERCLK_CLK_ROOT和IPG_CLK_ROOT要用到AHB_CLK_ROOT,所以我们要初始化AHB_CLK_ROOT。
  2. AHB_CLK_ROOT的初始化。
  3. AHB_CLK_ROOT=132MHz。 设置 CBCMR寄存器的PRE_PERIPH_CLK_SEL位,设置CBCDR寄存器的PERIPH_CLK_SEL位0。设置 CBCDR寄存器的AHB_PODF位为2,也就是3分频,因此396/3=132MHz。
  4. IPG_CLK_ROOT初始化
  5. 设置 CBCDR寄存器IPG_PODF=1,也就是2分频。
  6. PERCLK_CLK_ROOT初始化
  7. 设置 CSCMR1寄存器的PERCLK_CLK_SEL位为0,表示PERCLK的时钟源为IPG。

C代码

/** * @description : 初始化系统时钟,设置系统时钟为528Mhz,并且设置PLL2和PLL3各个 PFD时钟,所有的时钟频率均按照I.MX6U官方手册推荐的值. * @param : 无 * @return : 无 */
void imx6u_clkinit(void)
{ 
        
	unsigned int reg = 0;
	/* 1、设置ARM内核时钟为528MHz */
	/* 1.1、判断当前ARM内核是使用的那个时钟源启动的,正常情况下ARM内核是由pll1_sw_clk驱动的,而 * pll1_sw_clk有两个来源:pll1_main_clk和tep_clk(参考手册648页)。 * 如果我们要让ARM内核跑到528M的话那必须选择pll1_main_clk作为pll1的时钟源。 * 如果我们要修改pll1_main_clk时钟的话就必须先将pll1_sw_clk从pll1_main_clk切换到step_clk, * 当修改完pll1_main_clk以后在将pll1_sw_clk切换回pll1_main_clk。而step_clk的时钟源可以选择 * 板子上的24MHz晶振。 */
	
	if((((CCM->CCSR) >> 2) & 0x1 ) == 0) 	/* 当前pll1_sw_clk使用的pll1_main_clk*/
	{ 
        	
		CCM->CCSR &= ~(1 << 8);				/* 配置step_clk时钟源为24MH OSC */	
		CCM->CCSR |= (1 << 2);				/* 配置pll1_sw_clk时钟源为step_clk */
	}

	/* 1.2、设置pll1_main_clk为1056MHz,也就是528*2=1056MHZ, * 因为pll1_sw_clk进ARM内核的时候会被二分频! * 配置CCM_ANLOG->PLL_ARM寄存器 * bit13: 1 使能时钟输出 * bit[6:0]: 88, 由公式:Fout = Fin * div_select / 2.0,1056=24*div_select/2.0, * 得出:div_select= 88 */
	CCM_ANALOG->PLL_ARM = (1 << 13) | ((88 << 0) & 0X7F); 	/* 配置pll1_main_clk=1056MHz */
	CCM->CCSR &= ~(1 << 2);									/* 将pll_sw_clk时钟重新切换回pll1_main_clk */
	CCM->CACRR = 1;											/* ARM内核时钟为pll1_sw_clk/2=1056/2=528Mhz */

	/* 2、设置PLL2(SYS PLL)各个PFD */
	reg = CCM_ANALOG->PFD_528;
	reg &= ~(0X3F3F3F3F);		/* 清除原来的设置 */
	reg |= 32<<24;				/* PLL2_PFD3=528*18/32=297Mhz */
	reg |= 24<<16;				/* PLL2_PFD2=528*18/24=396Mhz(DDR使用的时钟,最大400Mhz) */
	reg |= 16<<8;				/* PLL2_PFD1=528*18/16=594Mhz */
	reg |= 27<<0;				/* PLL2_PFD0=528*18/27=352Mhz */
	CCM_ANALOG->PFD_528=reg;	/* 设置PLL2_PFD0~3 */

	/* 3、设置PLL3(USB1)各个PFD */
	reg = 0;					/* 清零 */
	reg = CCM_ANALOG->PFD_480;
	reg &= ~(0X3F3F3F3F);		/* 清除原来的设置 */
	reg |= 19<<24;				/* PLL3_PFD3=480*18/19=454.74Mhz */
	reg |= 17<<16;				/* PLL3_PFD2=480*18/17=508.24Mhz */
	reg |= 16<<8;				/* PLL3_PFD1=480*18/16=540Mhz */
	reg |= 12<<0;				/* PLL3_PFD0=480*18/12=720Mhz */
	CCM_ANALOG->PFD_480=reg;	/* 设置PLL3_PFD0~3 */	

	/* 4、设置AHB时钟 最小6Mhz, 最大132Mhz (boot rom自动设置好了可以不用设置)*/
	CCM->CBCMR &= ~(3 << 18); 	/* 清除设置*/ 
	CCM->CBCMR |= (1 << 18);	/* pre_periph_clk=PLL2_PFD2=396MHz */
	CCM->CBCDR &= ~(1 << 25);	/* periph_clk=pre_periph_clk=396MHz */
	while(CCM->CDHIPR & (1 << 5));/* 等待握手完成 */
		
	/* 修改AHB_PODF位的时候需要先禁止AHB_CLK_ROOT的输出,但是 * 我没有找到关闭AHB_CLK_ROOT输出的的寄存器,所以就没法设置。 * 下面设置AHB_PODF的代码仅供学习参考不能直接拿来使用!! * 内部boot rom将AHB_PODF设置为了3分频,即使我们不设置AHB_PODF, * AHB_ROOT_CLK也依旧等于396/3=132Mhz。 * 淦,就这?就这?就这? */
#if 0
	/* 要先关闭AHB_ROOT_CLK输出,否则时钟设置会出错 */
	CCM->CBCDR &= ~(7 << 10);	/* CBCDR的AHB_PODF清零 */
	CCM->CBCDR |= 2 << 10;		/* AHB_PODF 3分频,AHB_CLK_ROOT=132MHz */
	while(CCM->CDHIPR & (1 << 1));/* 等待握手完成 */
#endif
	
	/* 5、设置IPG_CLK_ROOT最小3Mhz,最大66Mhz (boot rom自动设置好了可以不用设置)*/
	CCM->CBCDR &= ~(3 << 8);	/* CBCDR的IPG_PODF清零 */
	CCM->CBCDR |= 1 << 8;		/* IPG_PODF 2分频,IPG_CLK_ROOT=66MHz */
	
	/* 6、设置PERCLK_CLK_ROOT时钟 */
	CCM->CSCMR1 &= ~(1 << 6);	/* PERCLK_CLK_ROOT时钟源为IPG */
	CCM->CSCMR1 &= ~(7 << 0);	/* PERCLK_PODF位清零,即1分频 */
}

中断

Cortex-A7中断系统

Cortex-A中断向量表

​ Cortex-A中断向量表有8个中断,其中重点关注IRQ。

外部中断均进入IRQ中断,根据中断号进行区分。

Cortex-A的中断向量表需要用户自己定义。

中断向量偏移

​ 裸机例程都是从0X87800000开始的,因此要设置中断向量偏移。

GIC中断控制器

​ 同NVIC一样,GIC用于管理Cortex-A的中断。GIC提供了开关中断,设置中断优先级。

  1. GIC 将众多的中断源分为分为三类:
  2. SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
  3. PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
  4. SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信

IMX6U中断号

​ 为了区分不同的中断,引入了中断号。

​ ID0~ID15是给SGI。

​ ID16~ID31是给PPI。

​ ID32~ID1019给SPI,也就是按键中断、串口中断。。。。

​ 6ULL支持128个中断。

中断服务函数的编写

​ IRQ中断服务函数的编写。

​ 在IRQ中断服务函数里面去查找并运行的具体的外设中断服务函数。

编写按键中断例程。

​ KEY0使用UART1_CTS这个IO。编写UART1_CTS的中断代码。

修改start.S

​ 添加中断向量表,编写复位中断服务函数和IRQ中断服务函数。

/**
 * 描述:	_start函数,首先是中断向量表的创建
 * 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
 * 		 	ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
 * _start 最开始加入以下代码
 */
_start:
	ldr pc, =Reset_Handler		/* 复位中断 					*/	
	ldr pc, =Undefined_Handler	/* 未定义中断 					*/
	ldr pc, =SVC_Handler		/* SVC(Supervisor)中断 		*/
	ldr pc, =PrefAbort_Handler	/* 预取终止中断 					*/
	ldr pc, =DataAbort_Handler	/* 数据终止中断 					*/
	ldr	pc, =NotUsed_Handler	/* 未使用中断					*/
	ldr pc, =IRQ_Handler		/* IRQ中断 					*/
	ldr pc, =FIQ_Handler		/* FIQ(快速中断)未定义中断 			*/
  1. 编写复位中断服务函数,内容如下:
  2. 关闭I,D Cache和MMU。(内部ROM已关)
  3. 设置处理器9中工作模式下对应的SP指针(每种工作模式都有自己独有的SP指针)。要使用中断那么必须设置IRQ模式下的SP指针。索性直接设置所有模式下的SP指针。
  4. 清除bss段。
  5. 跳到main函数

MMU负责地址映射,将CPU中虚拟地址VA映射到物理地址PA

i-cache(instruction cache):指令高速缓冲存储器

dcache(data cache):数据高速缓冲存储器

CP15协处理器

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有

16 个 32 位寄存器。CP15 协处理器的访问通过如下另个指令完成

MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。

MCR :将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中:

@MCR{cond} p15, , , , , 

@各个参数的意义
@cond:指令执行的条件码,如果忽略的话就表示无条件执行。
@opc1:协处理器要执行的操作码。
@Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
@CRn:CP15 协处理器的目标寄存器。
@CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将
@CRm 设置为 C0,否则结果不可预测。
@opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。

SCTLR寄存器。也就是系统控制寄存器,此寄存器bit0用于打开和关闭MMU,bit1控制对齐,bit2控制D Cache的打开和关闭。Bit11用于控制分支预测。Bit12用于控制I Cache。

下图

所以SCTLR的读取方式如下

mrc     p15, 0, r0, c1, c0, 0     /* 读取SCTLR寄存器到R0中*/
mcr     p15, 0, r0, c1, c0, 0     /* 将r0中的值写入到SCTLR寄存器中	*/

中断向量偏移设置

​ 将新的中断向量表首地址写入到CP15协处理器的VBAR寄存器。

同理,可得VBAR寄存器的访问方法如下:

@mcr{cond} p15, , , , , 
mrc p15,0,r0,c12,c0,0 	
mcr p15,0,r0,c12,c0,0

IRQ中断服务函数

mrc p15, 4, r1, c15, c0, 0 

读取CBAR寄存器。CBAR寄存器保存了GIC控制器的寄存器组首地址。GIC寄存器组偏移0x10000x1fff为GIC的分发器。0x20000x3fff为CPU接口端。

​ 代码中,R1寄存器保存着GIC控制器的CPU接口端基地址。读取CPU接口段的GICC_IAR寄存器的值保存到R0寄存器里面。

​ GICC_IAR的bit9~0存放着中断ID号,然后跳转到对应的中断处理函数。

​ system_irqhandler就是具体的中断处理函数,此函数有一个参数,为GICC_IAR寄存器的值。

​ system_irqhandler处理完具体的中断以后,需要将对应的中断ID值写入到GICC_EOIR寄存器里面。

退出中断

subs pc, lr, #4				/* 将lr-4赋给pc */

中断处理完成以后就要重新返回到曾经被中断打断的地方运行,这里为什么要将lr-4 然后赋给 pc 呢?而不是直接将 lr 赋值给 pc?ARM 的指令是三级流水线:取指、译指、执行,pc 指向的是正在取值的地址,这就是很多书上说的 pc=当前执行指令地址+8。

比如下面代码示例:

0X2000 MOV R1, R0 ;执行

0X2004 MOV R2, R3 ;译指

0X2008 MOV R4, R5 ;取值 PC

上面示例代码中,左侧一列是地址,中间是指令,最右边是流水线。当前正在执行 0X2000地址处的指令“MOV R1, R0”,但是 PC 里面已经保存了 0X2008 地址处的指令“MOV R4, R5”。假设此时发生了中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008。当中断处理完成以后肯定需要回到被中断点接着执行,如果直接跳转到 lr 里面保存的地址处(0X2008)开始运行,那么就有一个指令没有执行,那就是地址 0X2004 处的指令“MOV R2, R3”,显然这是一个很严重的错误!所以就需要将 lr-4 赋值给 pc,也就是 pc=0X2004,从指令“MOV R2,R3”开始执行。

中断汇编代码

复位中断函数

复位中断函数会在上电时执行

/* 复位中断 */	
Reset_Handler:
	cpsid i						/* 关闭全局中断 */

	/* 关闭I,DCache和MMU 
	 * 采取读-改-写的方式。
	 */
	mrc     p15, 0, r0, c1, c0, 0     /* 读取CP15的C1寄存器到R0中       		        	*/
    bic     r0,  r0, #(0x1 << 12)     /* 清除C1寄存器的bit12位(I位),关闭I Cache            	*/
    bic     r0,  r0, #(0x1 <<  2)     /* 清除C1寄存器的bit2(C位),关闭D Cache    				*/
    bic     r0,  r0, #0x2             /* 清除C1寄存器的bit1(A位),关闭对齐						*/
    bic     r0,  r0, #(0x1 << 11)     /* 清除C1寄存器的bit11(Z位),关闭分支预测					*/
    bic     r0,  r0, #0x1             /* 清除C1寄存器的bit0(M位),关闭MMU				       	*/
    mcr     p15, 0, r0, c1, c0, 0     /* 将r0寄存器中的值写入到CP15的C1寄存器中	 				*/

#if 0
	/* 汇编版本设置中断向量表偏移 该部分在c语言的中断编写种也有写到,因此注释*/
	ldr r0, =0X87800000

	dsb
	isb
	mcr p15, 0, r0, c12, c0, 0
	dsb  @数据同步指令,有该指定确保之前的数据全部写入或者读取成功
	isb  @指令同步指令,有该指定确保之前的指令全部写入或者读取成功
#endif
    
	/* 设置各个模式下的栈指针,
	 * 注意:IMX6UL的堆栈是向下增长的!
	 * 堆栈指针地址一定要是4字节地址对齐的!!!
	 * DDR范围:0X80000000~0X9FFFFFFF
	 */
	/* 进入IRQ模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x12 	/* r0或上0x13,表示使用IRQ模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0x80600000	/* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */

	/* 进入SYS模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x1f 	/* r0或上0x13,表示使用SYS模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0x80400000	/* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */

	/* 进入SVC模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x13 	/* r0或上0x13,表示使用SVC模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0X80200000	/* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */

	cpsie i				/* 打开全局中断 */
#if 0
	/* 使能IRQ中断 */
	mrs r0, cpsr		/* 读取cpsr寄存器值到r0中 			*/
	bic r0, r0, #0x80	/* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
	msr cpsr, r0		/* 将r0重新写入到cpsr中 			*/
#endif
	b main				/* 跳转到main函数 			 	*/

IRQ中断函数

IRQ_Handler:
	push {lr}					/* 保存lr地址 ,lr为当前pc的值*/
	push {r0-r3, r12}			/* 保存r0-r3,r12寄存器 */

	mrs r0, spsr				/* 读取spsr寄存器 */
	push {r0}					/* 保存spsr寄存器 */

	mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
								* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
								* Cortex-A7 Technical ReferenceManua.pdf P68 P138
								*/							
	add r1, r1, #0X2000			/* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
	ldr r0, [r1, #0XC]			@ GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
								@ GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
								@ 这个中断号来绝对调用哪个中断服务函数
								@
	push {r0, r1}				/* 保存r0,r1 */
	
	cps #0x13					/* 进入SVC模式,允许其他中断再次进去 */
	
	push {lr}					/* 保存SVC模式的lr寄存器 */
	ldr r2, =system_irqhandler	/* 加载C语言中断处理函数到r2寄存器中*/
	blx r2						/* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

	pop {lr}					/* 执行完C语言中断服务函数,lr出栈 */
	cps #0x12					/* 进入IRQ模式 */
	pop {r0, r1}				
	str r0, [r1, #0X10]			/* 中断执行完成,写EOIR */

	pop {r0}						
	msr spsr_cxsf, r0			/* 恢复spsr */

	pop {r0-r3, r12}			/* r0-r3,r12出栈 */
	pop {lr}					/* lr出栈 */
	subs pc, lr, #4				/* 将lr-4赋给pc */

C语言中断编写

带有中断号的中断处理函数

/** * @brief : C语言中断服务函数,irq汇编中断服务函数会 调用此函数,此函数通过在中断服务列表中查 找指定中断号所对应的中断处理函数并执行。 * @param - giccIar : 中断号 * @return : 无 */
void system_irqhandler(unsigned int giccIar) 
{ 
        

   uint32_t intNum = giccIar & 0x3FFUL;
   
   /* 检查中断号是否符合要求 */
   if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
   { 
        
	 	return;
   }
 
   irqNesting++;	/* 中断嵌套计数器加一 */

   /* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/
   irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
 
   irqNesting--;	/* 中断执行完成,中断嵌套寄存器减一 */

}

irqTable结构体

/* 中断服务函数形式 */ 
typedef void (*system_irq_handler_t) (unsigned int giccIar, void *param);

 
/* 中断服务函数结构体*/
typedef struct _sys_irq_handle
{ 
        
    system_irq_handler_t irqHandler; /* 中断服务函数 */
    void *userParam;                 /* 中断服务函数参数 */
} sys_irq_handle_t;

/* 中断嵌套计数器 */
static unsigned int irqNesting;

/* 中断服务函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];

中断函数服务表赋值函数

void system_irqtable_init(void)
{ 
        
	unsigned int i = 0;
	irqNesting = 0;
	
	/* 先将所有的中断服务函数设置为默认值 */
	for(i = 0; i < NUMBER_OF_INT_VECTORS; i++)
	{ 
        
		system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);
	}
}

/** * @brief : 给指定的中断号注册中断服务函数 * @param - irq : 要注册的中断号 * @param - handler : 要注册的中断处理函数 * @param - usrParam : 中断服务处理函数参数 * @return : 无 */
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam) 
{ 
        
	irqTable[irq].irqHandler = handler;
  	irqTable[irq].userParam = userParam;
}

/** * @brief : 默认中断服务函数 * @param - giccIar : 中断号 * @param - usrParam : 中断服务处理函数参数 * @return : 无 */
void default_irqhandler(unsigned int giccIar, void *userParam) 
{ 
        
	while(1) 
  	{ 
        
   	}
}

/** * IRQn_Type 为core_ca7中的关于终端号的定义 * 这两个函数实现了IRQ中断服务函数的赋值 * 此处所有的中断服务函数均使用了默认的中断服务函数以作示例,在具体项目中,需要对其具体定义 */

中断初始化函数,在main函数最开始进行调用中

/** * @brief : 中断初始化函数 * @param : 无 * @return : 无 */
void int_init(void)
{ 
        
	GIC_Init(); 						/* 初始化GIC 此函数声明在core_ca7.h */
	system_irqtable_init();				/* 初始化中断表 */
	__set_VBAR((uint32_t)0x87800000); 	/* 中断向量表偏移,偏移到起始地址 */
}

中断初始化以及中断流程

中断初始化流程

上电执行汇编语言中的 _start函数, 在函数中定义了a7内核支持的八种中断服务函数(其实是7个,有一个未使用)。

执行复位函数,因为初始化了中断服务函数,因此在之后执行复位中断函数,先关闭全局中断以及保存在通用寄存器的中断服务函数指针、I cache、D cache以及MMU也需要关闭。之后定义9种运行状态下的sp指针(栈指针),跳转到c语言中的main函数

main函数执行int_init函数,初始化所有的中断服务函数。

中断过程

当中断发生时,进入到IRQ_Handler函数,保存现场

IRQ_Handler判断中断ID号,进入中断ID对应的中断服务函数

执行用户指定的中断服务内容

恢复现场

定时器与延时

EPIT定时器

实现周期性的中断以及定时功能。

  • EPIT是32位的一个向下计数器。
  • EPIT的时钟源可以选择,例程选择ipg_clk=66MHz。
  • 可以对时钟源进行分频,12位的分频器,0~4095分别代表1~4096分频。
  • 开启定时器以后,计数寄存器会每个时钟减1,如果和比较寄存器里面的值相等的话就会触发中断。
  • EPIT有两种工作模式:Set-add-forget,一个是free-runing
  • 5、6ULL有两个EPIT定时器。

EPIT_CR寄存器用于配置EPIT。

相关寄存器

EPIT_CR bit0为1,设置EPIT使能,bit1为1,设置计数器的初始值为记载寄存器的值。Bit2为1使能比较中断,bit3为1设置定时器工作在set-and-forget模式下。Bit15~bit4设置分频值。Bit25:24设置时钟源的选择,我们设置为1,那么EPIT的时钟源就为ipg_clock=66MHz

EPIT_SR寄存器,只有bit0有效,表示中断状态,写1清零。当OCIF位为1的时候表示中断发生,为0的时候表示中断未发生。我们处理完定时器中断以后一定要清除中断标志位。

EPIT_LR寄存器设置计数器的加载值。计数器每次计时到0以后就会读取LR寄存器的值重新开始计时。

CMPR比较计数器,当计数器的值和CMPR相等以后就会产生比较中断。

​ 使用EPIT实现500ms周期的定时器。500ms进入一次中断。

C程序编写

初始化

/** * @brief : 初始化EPIT定时器. * EPIT定时器是32位向下计数器,时钟源使用ipg=66Mhz * @param - frac : 分频值,范围为0~4095,分别对应1~4096分频。 * @param - value : 倒计数值。 * @return : 无 */
void epit1_init(unsigned int frac, unsigned int value)
{ 
        
	if(frac > 0XFFF)
		frac = 0XFFF;
		
	EPIT1->CR = 0;	/* 先清零CR寄存器 */
	
	/* * CR寄存器: * bit25:24 01 时钟源选择Peripheral clock=66MHz * bit15:4 frac 分频值 * bit3: 1 当计数器到0的话从LR重新加载数值 * bit2: 1 比较中断使能 * bit1: 1 初始计数值来源于LR寄存器值 * bit0: 0 先关闭EPIT1 */
	EPIT1->CR = (1<<24 | frac << 4 | 1<<3 | 1<<2 | 1<<1);
	
	EPIT1->LR = value;	/* 倒计数值 */
	EPIT1->CMPR	= 0;	/* 比较寄存器,当计数器值和此寄存器值相等的话就会产生中断 */

	/* 使能GIC中对应的中断 */
	GIC_EnableIRQ(EPIT1_IRQn);

	/* 注册中断服务函数 */
	system_register_irqhandler(EPIT1_IRQn, (system_irq_handler_t)epit1_irqhandler, NULL);	

	EPIT1->CR |= 1<<0;	/* 使能EPIT1 */ 
}

中断服务函数

/** * @brief : EPIT中断处理函数 */
void epit1_irqhandler(void)
{ 
         
	if(EPIT1->SR & (1<<0)) 			/* 判断比较事件发生 */
	{ 
        
		/* User code begin*/
        /* User code end */
	}
	EPIT1->SR |= 1<<0; 				/* 清除中断标志位 */
}

外部调用

epit1_init(0, 66000000/2);	/* 初始化EPIT1定时器,1分频计数值为:66000000/2,也就是定时周期为500ms。*/

GPT定时器

正点原子使用该定时器实现高精度精准阻塞延时

  • GPT定时器是32位向上计数器。
  • GPT定时器有捕获的功能。
  • GPT定时器支持比较输出或中断功能。
  • GPT定时器有一个12位的分频器。
  • GPT时钟源可以选择,这里我们使用ipg_clk=66M作为GPT的时钟源。
  • GPT定时器有两种工作模式:restart和free-run。

Restart模式下:定时器计数值和比较寄存器OCR的值相等的话定时器就会重新从0开始计时。注意!只有比较通道1才有此功能。

Free-run模式:所有三个输出比较通道都适用。从0开始一直加到0xffffffff,然后重新从0开始,周而复始。

相关寄存器

GPT_CR寄存器,bit0为GPT使能位,为0的时候关闭GPT,为1的时候使能GPT。Bit1确定GPT定时器计数器的初始值,为0的时候表示GPT定时器计数值默认为上次关闭的时候遗留的值,为1的话计数值为0。Bit8~6为时钟源的选择,设置为1,表示GPT时钟源为ipg_clk=66MHz。bit9设置GPT定时器工作模式,为0的时候工作在restart模式,为1的时候工作在free-run模式。Bit15软件复位。

GPT_PR寄存器的bit110为分频值,可设置0-4095,表示14096分频。

GPT_SR寄存器,bit5表示溢出发生,bit4和bit3分别为输入通道2和1的捕获中断标志位。Bit20,也就是OF3OF1为比较中断。

GPT_IR寄存器,也就是中断使能寄存器

C程序编写

GPT初始化

/** * @brief : 延时有关硬件初始化,主要是GPT定时器 GPT定时器时钟源选择ipg_clk=66Mhz * @param : 无 * @return : 无 */
void delay_init(void)
{ 
        
	GPT1->CR = 0; 					/* 清零,bit0也为0,即停止GPT */

	GPT1->CR = 1 << 15;				/* bit15置1进入软复位 */
	while((GPT1->CR >> 15) & 0x01);	/*等待复位完成 */
	/** * GPT的CR寄存器,GPT通用设置 * bit22:20 000 输出比较1的输出功能关闭,也就是对应的引脚没反应 * bit9: 0 Restart模式,当CNT等于OCR1的时候就产生中断 * bit8:6 001 GPT时钟源选择ipg_clk=66Mhz * bit */
	GPT1->CR = (1<<6);
	元器件数据手册IC替代型号,打造电子元器件IC百科大全!
          

相关文章