蜂鸣器简介
有源蜂鸣器只要通电就会叫,所以我们可以做一个供电电路,这个供电电路可以由一个 IO来控制其通断,一般使用三极管来搭建这个电路。为什么我们不能像控制 LED 灯一样,直接将GPIO 接到蜂鸣器的负极,通过 IO 输出高低来控制蜂鸣器的通断。因为蜂鸣器工作的电流比LED 灯要大,直接将蜂鸣器接到 I.MX6U 的 GPIO 上有可能会烧毁 IO,所以我们需要通过一个三极管来间接的控制蜂鸣器的通断,相当于加了一层隔离。
I.MX6U 时钟系统详解
I.MX6U 的系统主频为 528MHz,但是默认情况下内部 bootrom 会将 I.MX6U 的主频设置为 396MHz。
从图可以看出 I.MX6U-ALPHA 开发板的系统时钟来源于两部分:32.768KHz 和24MHz 的晶振,其中32.768KHz 晶振是 I.MX6U 的 RTC 时钟源,24MHz 晶振是 I.MX6U 内核和其它外设的时钟源,也是我们重点要分析的。
I.MX6U 的外设有很多,不同的外设时钟源不同,NXP 将这些外设的时钟源进行了分组,一共有 7 组,这 7 组时钟源都是从 24MHz 晶振 PLL 而来的,因此也叫做 7 组 PLL。I.MX6U 的所有外设时钟源都是从这 7 路 PLL 和有些 PLL 的PFD 而来的。
图中一共有三部分:CLOCK_SWITCHER、CLOCK ROOT GENERATOR 和SYSTEM CLOCKS。其中左边的 CLOCK_SWITCHER 就是那 7 路 PLL 和8 路 PFD,右边的 SYSTEM CLOCKS 就是芯片外设,中间的 CLOCK ROOT GENERATOR 给左边的CLOCK_SWITCHER和右边的SYSTEM CLOCKS进行牵线搭桥。外设时钟源是有多路可以选择的,CLOCK ROOT GENERATOR 就负责从 7 路PLL 和 8 路 PFD 中选择合适的时钟源给外设使用。具体操作肯定是设置相应的寄存器。
时钟设置
以内核时钟为例:
修改步骤如下:
①、 设置寄存器 CCSR 的 STEP_SEL 位,设置 step_clk 的时钟源为 24M 的晶振。
②、设置寄存器 CCSR 的 PLL1_SW_CLK_SEL 位,设置 pll1_sw_clk 的时钟源为step_clk=24MHz,通过这一步我们就将 I.MX6U 的主频先设置为 24MHz,直接来自于外部的24M 晶振。
③、设置寄存器 CCM_ANALOG_PLL_ARMn,将pll1_main_clk(PLL1)设置为 1056MHz。
④、设置寄存器 CCSR 的 PLL1_SW_CLK_SEL 位,重新将 pll1_sw_clk 的时钟源切换回pll1_main_clk,切换回来以后的 pll1_sw_clk 就等于 1056MHz。
⑤、最后设置寄存器 CCM_CACRR 的 ARM_PODF 为 2 分频,I.MX6U 的内核主频就为1056/2=528MHz。
imx6u_clkinit 函数先设置系统主频为 528MHz,然后根据我们上一小节分析的 I.MX6U 时钟系统来设置 8 路 PFD,最后设置 AHB、IPG 和 PERCLK 的时钟频率此外,还有其他的一些根时钟的设置,也都是类似的,主要依靠寄存器进行修改,主要是时钟源选择和分频选择。
中断系统
中断向量表
中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表都是链接到代码的最前面,比如一般 ARM 处理器是从地址 0X00000000 开始执行指令的,那么中断向量表就是从 0X00000000 开始存放的。A7 内核有 8 个异常中断:
简单介绍一下这 7 个中断:
①、复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、IRQ 中断(IRQ Interrupt),外部中断,芯片内部的外设中断都会引起此中断的发生。
⑦、FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
在上面的 7 个中断中,我们常用的就是复位中断和 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理。
我们说 ARM 处理器都是从地址 0X00000000 开始运行的,但是我们学习 STM32 的时候代码是下载到 0X8000000 开始的存储区域中。因此中断向量表是存放到 0X8000000 地址处的,而不是 0X00000000,这样不是就出错了吗?为了解决这个问题,Cortex-M 架构引入了一个新的概念——中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址处,通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可。
GIC 控制器
我们在学32的时候有个NVIC(中断系统管理机构),那么 I.MX6U 所使用的 Cortex-A7 内核是不是也有个中断系统管理机构?答案是肯定的,不过 Cortex-A 内核的中断管理机构不叫做NVIC,而是叫做 GIC。
当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况:VFIQ、VIRQ、FIQ 和 IRQ,他们之间的关系如图所示:
GIC 将众多的中断源分为分为三类:
①、SPI(Shared Peripheral Interrupt),共享中断所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID。
GIC 架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。
Distributor(分发器端):中断事件应该发送到哪个 CPU Interface 上去。
CPU Interface:每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端就是分发器和 CPU Core 之间的桥梁。
CP15 协处理器
分发器端相关寄存器,其相对于 GIC 基地址偏移为 0X1000,因此我们获取到 GIC 基地址以后只需要加上 0X1000 即可访问 GIC 分发器端寄存器。CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000,同样的,获取到 GIC 基地址以后只需要加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。那么GIC 控制器的寄存器基地址在哪里呢?这个就需要用到 Cortex-A 的 CP15 协处理器。CP15 协处理器的访问通过如下另个指令完成:
MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
MCR 指令格式如下:
MCR{cond} p15, , , , ,
cond:指令执行的条件码,如果忽略的话就表示无条件执行。
opc1:协处理器要执行的操作码。
Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
CRn:CP15 协处理器的目标寄存器。
CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测。
opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。
MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 就是目标寄存器,也就是从CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器,也就是要读取的写处理器寄存器。
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:
MRC p15, 0, r0, c0, c0, 0.
CP15 协处理器有 16 个 32 位寄存器,c0~c15
例如c12 寄存器通过不同的配置,其代表的含义也不同,
当 MRC/MCR 指令中的 CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中,比如在前面的例程中,代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下:
ldr r0, =0X87800000 ;
MCR p15, 0, r0, c12, c0, 0 ;
将 r0 里面的数据写入到 c12 中,即 c12=0X87800000
我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR中,我们可以通过如下命令获取到 GIC 基址:
MRC p15, 4, r1, c15, c0, 0 ;
获取 GIC 基础地址,基地址保存在 r1 中。获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了,比如我们可以读取当前中断 ID,当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址相对于 CPU 接口端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下:
MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;
读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器GIC_IAR 的值
其他的,通过 c0 寄存器可以获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。
中断设置
中断使能:包括两部分,一个是 IRQ 或者 FIQ 总中断使能(寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。),另一个就是 ID0~ID1019 这 1020个中断源的使能。GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止。
中断优先级Cortex-A7 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级。抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的。某个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR 来完成,每个中断 ID 配有一个优先级寄存器.
通用中断驱动
中断服务函数 IRQ_Handler 中调用了 C 函数 system_irqhandler 来处理具体的中断。此函数有一个参数,参数是中断号,需要实现函数 system_irqhandler 的具体内容。不同的中断源对应不同的中断处理函数,I.MX6U 有 160 个中断源,所以需要 160 个中断处理函数.将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。当中断发生以后函数 system_irqhandler 根据中断号从中断处理函数数组中找到对应的中断处理函数并执行.
枚举类型 IRQn_Type,此枚举类型就枚举出了 I.MX6U 的所有中断。
总结如下:
对GPIO的初始化工作,实质就是去操作寄存器:
设置输入输出-GDIR(寄存器),设置输出电平-DR
ICR1,ICR2 表示GPIO组0-16的中断模式设置,IMR-中断使能,
ISR-中断标志位,
中断驱动:初始化IO,复用功能,配置电气属性,配置中断模式,使能中断总开关,注册中断处理函数(结束时清除中断标志位),使能单个IO的中断。
初始化中断:初始化GIC,初始化所有中断服务函数,设置中断向量偏移。