ARM嵌入式裸机简单使用

基于正点原子 ALPHA开发板,长文预警,建议收藏用到之后再查看

文章目录

主频与时钟

恩智浦在内部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, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>

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

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

下图

cp15读取寄存器方式

所以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, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
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);
	/**
     * GPT的PR寄存器,GPT的分频设置
     * bit11:0  设置分频值,设置为0表示1分频,
     *          以此类推,最大可以设置为0XFFF,也就是最大4096分频
	 */
	GPT1->PR = 65;	/* 设置为65,即66分频,因此GPT1时钟为66M/(65+1)=1MHz */
	 /**
      * GPT的OCR1寄存器,GPT的输出比较1比较计数值,
      *	GPT的时钟为1Mz,那么计数器每计一个值就是就是1us。
      * 为了实现较大的计数,我们将比较值设置为最大的0XFFFFFFFF,
      * 这样一次计满就是:0XFFFFFFFFus = 4294967296us = 4295s = 71.5min
      * 也就是说一次计满最多71.5分钟,存在溢出
	  */
	GPT1->OCR[0] = 0XFFFFFFFF;

	GPT1->CR |= 1<<0;			//使能GPT1
}
延时api
/**
 * @brief   		: 微秒(us)级延时
 * @param - value	: 需要延时的us数,最大延时0XFFFFFFFFus
 * @return 			: 无
 */
void delayus(unsigned    int usdelay)
{
   
	unsigned long oldcnt,newcnt;
	unsigned long tcntvalue = 0;	/* 走过的总时间  */

	oldcnt = GPT1->CNT;
	while(1)
	{
   
		newcnt = GPT1->CNT;
		if(newcnt != oldcnt)
		{
   
			if(newcnt > oldcnt)		/* GPT是向上计数器,并且没有溢出 */
				tcntvalue += newcnt - oldcnt;
			else  					/* 发生溢出    */
				tcntvalue += 0XFFFFFFFF-oldcnt + newcnt;//0xFFFFFFFF为初始化的比较值
			oldcnt = newcnt;
			if(tcntvalue >= usdelay)/* 延时时间到了 */
			break;			 		
		}
	}
}

/**
 * @brief   		: 毫秒(ms)级延时
 * @param - msdelay	: 需要延时的ms数
 * @return 			: 无
 */
void delayms(unsigned	 int msdelay)
{
   
	int i = 0;
	for(i=0; i<msdelay; i++)
	{
   
		delayus(1000);
	}
}

串口

串口协议这种东西,不用在这里写了吧。

6ULL的串口寄存器

​ 6ULL的UART_URXD寄存器保存这串口接收到的数据。

UART_UTXD寄存器为发送数据寄存器,如果需要通过串口发送数据,只需要将数据写入到UART_UTXD寄存器里面。

UART_UCR1~UCR4都是串口的控制寄存器。UART_UCR1的bit0是UART的使能位,为1的时候使能UART。Bit14为自动检测波特率使能位,为1的时候使能波特率自动检测。

UART_UCR2的bit0为软件复位位。为0的时候复位UART。Bit1使能UART的接收,我们要配置为1。Bit2为发送使能,要设置为1。Bit5设置数据位,0的话表示7位数据位,1的话表示8位数据位。Bit6设置停止位,0的话表示1位停止位,1的话表示2位。Bit7奇偶校验位,为0的时候是偶校验,为1的时候是计校验。Bit8校验使能位,为0的时候关闭校验。

UART_UCR3的bit2必须为1!!!

UART_UFCR寄存器的bit9~7设置分频值,UART的时钟源=PLL3/6=480/6=80MHz。CSCDR1寄存器的UART_CLK_SEL位设置UART的时钟源,为0的时候UART时钟源为80MHz,为1的时候UART时钟源为24M晶振。CSCDR1寄存器的UART_CLK_PODF位控制分频,一般设置为1分频,因此UART_CLK_ROOT=80MHZ

UART_UFCRUART_UBIRUART_UBMR这三个寄存器决定了串口波特率。

公式

波特率计算公式

UART_USR2寄存器的bit0为1的时候表示有数据可以读取。Bit3为1的时候表示数据发送完成。

C程序编写

使能/失能代码

/**
 * @brief       : 关闭指定的UART
 * @param - base: 要关闭的UART
 * @return		: 无
 */
void uart_disable(UART_Type *base)
{
   
	base->UCR1 &= ~(1<<0);	
}

/**
 * @brief       : 打开指定的UART
 * @param - base: 要打开的UART
 * @return		: 无
 */
void uart_enable(UART_Type *base)
{
   
	base->UCR1 |= (1<<0);	
}

/**
 * @brief       : 复位指定的UART
 * @param - base: 要复位的UART
 * @return		: 无
 */
void uart_softreset(UART_Type *base)
{
   
	base->UCR2 &= ~(1<<0); 			/* UCR2的bit0为0,复位UART  	  	*/
	while((base->UCR2 & 0x1) == 0); /* 等待复位完成 					*/
}

初始化代码

/**
 * @brief       : 初始化串口1所使用的IO引脚
 * @param		: 无
 * @return		: 无
 */
void uart_io_init(void
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值