imx6u-裸机学习笔记

文章目录


一、Cortex-A7 运行模式

1.九种运行模式

Cortex-A7 处理器有 9 种处理模式
在这里插入图片描述
除了 User(USR)用户模式以外,其它 8 种运行模式都是特权模式。用户模式下是不能访问系统所有资源的,有些资源是受限的,但是用户模式是不能直接进行切换的,用户模式下需要借助异常来完成模式切换。

2.Cortex-A 的内核寄存器组>配置处理模式

Cortex-A 的内核寄存器组,注意不是芯片的外设寄存器。ARM 架构提供了 16 个 32 位的通用寄存器(R0-R15)供软件使用,前 15 个(R0~R14)可以用作通用的数据存储,R15 是程序计数器 PC,用来保存将要执行的指令。ARM 还提供了一个当前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的备份。
在这里插入图片描述
所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。其后五位CPSR[4-0]可以设置处理器模式。

在这里插入图片描述

/* 设置各个模式下的栈指针,
 * 注意: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 */

置 SVC 模式下的 SP 指针=0X80200000,因为 I.MX6U开发板上的DDR3 地 址 范 围 是 X80000000-0XA0000000(512MB) 或 者0X80000000~0X90000000(256MB),不管是 512MB 版本还是 256MB 版本的,其 DDR3 起始地址都是 0X80000000。由于 Cortex-A7 的堆栈是向下增长的,所以将 SP 指针设置为 0X80200000,因此 SVC 模式的栈大小为 0X80200000-0X80000000=0X200000=2MB


二、C语言寄存器定义

自己设置IMX6UL 相关寄存器定义,参考 STM32 寄存器定义方法,但NXP有相应的SDK,可以直接引用。

1.仿stm32编写

①编写外设结构体

先将同属于一个外设的所有寄存器编写到一个结构体里面,如 IO 复用寄存器组的结构体如下

/* 
 * IOMUX寄存器组
 */
typedef struct 
{
	volatile unsigned int BOOT_MODE0;
	volatile unsigned int BOOT_MODE1;
	volatile unsigned int SNVS_TAMPER0;
	volatile unsigned int SNVS_TAMPER1;
	.....
	volatile unsigned int CSI_DATA05;
	volatile unsigned int CSI_DATA06;
	volatile unsigned int CSI_DATA07;
}IOMUX_SW_MUX_Type;

②定义 IO 复用寄存器组的基地址

结构体指针的地址,与它指向的结构体的第一个成员地址是一样的。
根据结构体IOMUX_SW_MUX_Type 的定义,其指针的地址就是第一个成员变量为 BOOT_MODE0,也就是 BOOT_MODE0 这个 IO 的 IO 复用寄存器的地址。
32.5.1 SW_MUX_CTL_PAD_BOOT_MODE0 SW MUX Control Register的地址为0X02290000

#define IOMUX_SW_MUX_BASE (0X02290000)

由于volatile unsigned int定义的元素占4个字节(32bit),故BOOT_MODE1的地址为0X02290004
以此类推,可以定义每个寄存器的地址。

③定义访问指针

#define IOMUX_SW_MUX ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE)

1.(IOMUX_SW_MUX_Type * ):是IOMUX_SW_MUX_Type类型的指针。
2.((IOMUX_SW_MUX_Type * )IOMUX_SW_MUX_BASE):是将IOMUX_SW_MUX_BASE强制转换成一个IOMUX_SW_MUX_Type类型的指针。
3.#define IOMUX_SW_MUX ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE):是将强制类型转换的这个结构体指针重命名为 IOMUX_SW_MUX。

可以看出IOMUX_SW_MUX 是个宏定义指针,是一个指向地址 IOMUX_SW_MUX_BASE的结构体指针,结构体为IOMUX_SW_MUX_Type 。
可以使用该结构体指针访问其内部成员,即内部的寄存器,如

OMUX_SW_MUX->GPIO1_IO03 = 0X5; /* 复用为 GPIO1_IO03 *

2.官方SDK

fsl_iomuxc.h 是NXP官方SDK的IO的寄存器地址定义文档
NXP 的 SDK 库将一个 IO 的所有复用功能都定义了一个宏,比如GPIO1_IO03 就有如下 9 个宏定义:

IOMUXC_GPIO1_IO03_I2C1_SDA
IOMUXC_GPIO1_IO03_GPT1_COMPARE3
IOMUXC_GPIO1_IO03_USB_OTG2_OC
IOMUXC_GPIO1_IO03_USDHC1_CD_B
IOMUXC_GPIO1_IO03_GPIO1_IO03
IOMUXC_GPIO1_IO03_CCM_DI0_EXT_CLK
IOMUXC_GPIO1_IO03_SRC_TESTER_ACK
IOMUXC_GPIO1_IO03_UART1_RX
IOMUXC_GPIO1_IO03_UART1_TX

上面 9 个宏定义分别对应着 GPIO1_IO03 的九种复用功能,比如复用为 GPIO 的宏定义就是:

#define IOMUXC_GPIO1_IO03_GPIO1_IO03 0x020E0068U, 0x5U, 0x00000000U,0x0U, 0x020E02F4U

分别对应:
0x020E0068U: IO的复用寄存器地址
0x5U :IO 复用值
0x00000000U:外设输入 IO 选择寄存器地址
0x0U:外设输入 IO 选择寄存器 inputRegister 的值
0x020E02F4U:所复用的IO 配置寄存器地址


三、CCM_CCGRx 外设时钟使能寄存器

Chapter 18: Clock Controller Module(CCM)
I.MX6U 中,每个外设的时钟都可以独立的使能或禁止, CMM 有CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,这 7 个寄存器控制着 I.MX6U 的所有外设时钟开关。CCM_CCGRx 是个 32 位寄存器,其中每 2 位控制一个外设的时钟.
在这里插入图片描述

ldr r0, =0X020C4068 /* 寄存器 CCGR0 */
ldr r1, =0XFFFFFFFF
str r1, [r0]
/*
 * @description	: 使能I.MX6U所有外设时钟
 * @param 		: 无
 * @return 		: 无
 */
void clk_enable(void)
{
	CCM->CCGR0 = 0XFFFFFFFF;
	CCM->CCGR1 = 0XFFFFFFFF;
	CCM->CCGR2 = 0XFFFFFFFF;
	CCM->CCGR3 = 0XFFFFFFFF;
	CCM->CCGR4 = 0XFFFFFFFF;
	CCM->CCGR5 = 0XFFFFFFFF;
	CCM->CCGR6 = 0XFFFFFFFF;
}

使能I.MX6U所有外设时钟


四、IO配置

前提是对应外设时钟使能
Chapter 18: Clock Controller Module(CCM), CMM 有 CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,这 7 个寄存器控制着 I.MX6U 的所有外设时钟开关

1.初始化 IO 复用

IO名:IOMUXC_SW_MUX_CTL_PAD_XX_XX
找到对应的寄存器:IOMUXC_SW_MUX_CTL_PAD_XX_XX SW MUX Control Register = r0
选择复用模式,寄存器赋值:r0 = 0x%% ,0x%%为MUX_MODE对应的数值

/* 2、设置 GPIO1_IO03 复用为 GPIO1_IO03 */
ldr r0, =0X020E0068 /* 将寄存器 SW_MUX_GPIO1_IO03_BASE 加载到 r0 中 */
ldr r1, =0X5 /* 设置寄存器 SW_MUX_GPIO1_IO03_BASE 的 MUX_MODE 为 5 */
str r1,[r0]

SDK中,IOMUXC_SetPinMux(muxRegister,muxMode,inputRegister,inputDaisy,configRegister,inputOnfield),参数定义如下:

muxRegister   : IO的复用寄存器地址,比如GPIO1_IO03的IO复用寄存器 
                SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。
                
muxMode   	  : IO 复用值,也就是 ALT0-ALT8,对应数字 0~8,比如要将 GPIO1_IO03 设置
			    为 GPIO 功能的话此参数就要设置为 5。
			    
inputRegister :外设输入 IO 选择寄存器地址,有些 IO 在设置为其他的复用功能以后还需
				要设置 IO 输入寄存器,比如 GPIO1_IO03 要复用为 UART1_RX 的话还需要设置寄存器
				UART1_RX_DATA_SELECT_INPUT,此寄存器地址为 0X020E0624。
				
inputDaisy    :寄存器 inputRegister 的值,比如 GPIO1_IO03 要作为 UART1_RX 引脚的话此
				参数就是 1。
				
configRegister:所复用的IO 配置寄存器地址,函数 IOMUXC_SetPinConfig 会使用这个寄存器。

inputOnfield  : IO 软 件 输 入 使 能 ,为1,会强制复用此IO,无论MUX_MODE的选择,
                为0,根据MUX_MODE的值来设置。
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);
    }
}

初始化 IO 复用, 复用为 GPIO1_IO03:

/* 1、初始化 IO 复用, 复用为 GPIO1_IO03 */
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);

2.配置 IO 电气属性

图 8.1.4.2 所示的 GPIO 功能图:
在这里插入图片描述

对照着图 8.1.4.2 来详细看一下寄存器 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 的各个位的含义:

HYS(bit16):对应图 8.1.4.2 中 HYS,用来使能迟滞比较器,当 IO 作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。斯密特触发器是用来整形的,有上下阈值,超过上阈值产生高电平,低于下阈值产生低电平。
·
PUS(bit15:14):对应图 8.1.4.2 中的 PUS,用来设置上下拉电阻的,一共有四种选项可以选择,如表 8.1.4.1 所示:
在这里插入图片描述
·
PUE(bit13):图 8.1.4.2 没有给出来,当 IO 作为输入的时候,这个位用来设置 IO 使用上下拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在IO 作为输入的时候才有用,顾名思义,就是当外部电路断电以后此 IO 口可以保持住以前的状态·
·
PKE(bit12):对应图 8.1.4.2 中的 PKE,此位用来使能或者禁止上下拉/状态保持器功能,为0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
·
ODE(bit11):对应图 8.1.4.2 中的 ODE,当 IO 作为输出的时候,此位用来禁止或者使能开路输出,此位为 0 的时候禁止开路输出,当此位为 1 的时候就使能开路输出功能。
·
SPEED(bit7:6):对应图 8.1.4.2 中的 SPEED,当 IO 用作输出的时候,此位用来设置 IO 速度,设置如表 8.1.4.2 所示:
在这里插入图片描述
·
DSE(bit5:3):对应图 8.1.4.2 中的 DSE,当 IO 用作输出的时候用来设置 IO 的驱动能力,总共有 8 个可选选项,如表 8.1.4.3 所示:
在这里插入图片描述
·
RE(bit0):对应图 8.1.4.2 中的 SRE,设置压摆率,当此位为 0 的时候是低压摆率,当为 1的时候是高压摆率。这里的压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时间,时间越小波形就越陡,说明压摆率越高;反之,时间越多波形就越缓,压摆率就越低。如果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,如果你当前所使用的 IO做高速通信的话就可以使用高压摆率。

IO名:IOMUXC_SW_PAD_CTL_PAD_XX_XX
找到对应的寄存器:IOMUXC_SW_PAD_CTL_PAD_XX_XX SW PAD Control Register = r1
SDK中,IOMUXC_SetPinConfig(muxRegister,muxMode,inputRegister,inputDaisy,configRegister,configValue)
configRegister:所复用的IO 配置寄存器地址
configValue: 就是要写入到寄存器 configRegister 的值,即 配置电气属性的值
其他具体参数含义 1.初始化 IO 复用 已经介绍

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;
    }
}

配置 GPIO1_IO03 的 IO 输出电气属性:

/* 2、配置 GPIO1_IO03 的 IO 属性*/
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0X10B0);

①复用为GPIO, 输出模式下的电气属性

bit 16:0 HYS 关闭
bit [15:14]: 00 默认下拉
bit [13]: 0 kepper 功能
bit [12]: 1 pull/keeper 使能
bit [11]: 0 关闭开路输出
bit [7:6]: 10 速度 100Mhz
bit [5:3]: 110 R0/6 驱动能力
bit [0]: 0 低转换率
寄存器赋值:r1 = 0X10b0

②复用为GPIO, 输入模式下的电气属性

bit 16:0 HYS 关闭
bit [15:14]: 11 默认 22K 上拉
bit [13]: 1 pull 功能
bit [12]: 1 pull/keeper 使能
bit [11]: 0 关闭开路输出
bit [7:6]: 10 速度 100Mhz
bit [5:3]: 000 关闭输出
bit [0]: 0 低转换率
寄存器赋值:r1 = 0Xf080

3.GPIO 寄存器配置

总结一下,要将 I.MX6U 的 IO 作为 GPIO 使用,我们需要一下几步:
1、使能 GPIO 对应的时钟。
2、设置寄存器 IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置 IO 的复用功能,使其复用
为 GPIO 功能。
3、设置寄存器 IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置 IO 的上下拉、速度等等。
4、第2步已经将 IO 复用为了 GPIO 功能,所以需要配置 GPIO,设置输入/输出、是否使
用中断、默认输出电平等。

第4步:当 IO 用作 GPIO 的时候需要设置的寄存器,一共有八个:
DR、GDIR、PSR、ICR1、ICR2、EDGE_SEL、IMR 、 ISR

①28.5.1 GPIO data register (GPIOx_DR) 数据寄存器

此寄存器是 32 位的,一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO。
当 GPIO 被配置为输出功能以后,向指定的位写入数据那么相应的 IO 就会输出相应的高低电平。
比如要设置 GPIO1_IO03 输出高电平,那么就应该设置 GPIO1->DR |=(1 << 3)。
比如要设置 GPIO1_IO03 输出低电平,那么就应该设置 GPIO1->DR &=~(1 << 3)。

当 GPIO 被配置为输入模式以后,此寄存器就保存着对应 IO 的电平值,每个位对对应一个 GPIO。
当 GPIO1_IO00 这个引脚接地的话,那么 GPIO1->DR 的 bit0 就是 0,即((GPIO1->DR) >> pin) & 0x1)

②28.5.2 GPIO direction register (GPIOx_GDIR) 方向寄存器

GDIR 寄存器也是 32 位的,此寄存器用来设置某个 IO 的工作方向,是输入还是输出。同样的,每个 IO 对应一个位,
如果要设置 GPIO 为输入的话就设置相应的位为 0,比如要设置 GPIO1_IO00 为输入,那么 GPIO1.GDIR=0
如果要设置为输出的话就设置为 1。比如设置 GPIO1_IO03 为输出,GPIO1->GDIR |= (1 << 3)

③GPIO pad status register (GPIOx_PSR) GPIO 状态寄存器

同样的 PSR 寄存器也是一个 GPIO 对应一个位,读取相应的位即可获取对应的 GPIO 的状
态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样

④/⑤28.5.4/5 GPIO interrupt configuration register1/2 (GPIOx_ICR1/2) 中断配置寄存器

ICR1 用于配置低 16 个 GPIO,ICR2 用于配置高 16 个 GPIO,每个GPIO使用两位bit来设置中断的触发方式

00 LOW_LEVEL — Interrupt n is low-level sensitive.
01 HIGH_LEVEL — Interrupt n is high-level sensitive.
10 RISING_EDGE — Interrupt n is rising-edge sensitive.
11 FALLING_EDGE — Interrupt n is falling-edge sensitive.

例如,设置GPIO1_017为上升沿触发,
即 ICR2, 17 - 15= 2,从0开始到15.
GPIO1-> ICR2 &= ~(3U << 1 ) 先全部清零
GPIO1-> ICR2 |= (1U << 1 ) 再赋值0x01

⑥28.5.6 GPIO interrupt mask register (GPIOx_IMR) 中断屏蔽寄存器

IMR 寄存器也是一个 GPIO 对应一个位,IMR 寄存器用来控制 GPIO 的中断禁止和使能,如果使能某个 GPIO 的中断,那么设置相应的位为 1 即可,反之,如果要禁止中断,那么就设置相应的位为 0 即可
例如,要使能 GPIO1_IO00 的中断,那么就可以设置 GPIO1.MIR=1, 即 GPIO1->IMR |= (1 << 0);
反之,关闭,即 GPIO1->IMR &= ~(1 << 0);

⑦28.5.7 GPIO interrupt status register (GPIOx_ISR) 中断状态寄存器

ISR 寄存器也是 32 位寄存器,一个 GPIO 对应一个位,只要某个 GPIO 的中断发生,那么ISR 中相应的位就会被置 1。所以,我们可以通过读取 ISR 寄存器来判断 GPIO 中断是否发生,相当于 ISR 中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除方法就是向 ISR 中相应的位写 1,也就是写 1 清零。
例如,GPIO1_03处理完中断以后,清除中断标志位 GPIO1->ISR |= (1 << 3);

⑧28.5.8 GPIO edge select register (GPIOx_EDGE_SEL) 边沿选择寄存器

EDGE_SEL 寄存器用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一个 GPIO 对应一个位。如果相应的位被置 1,那么就相当与设置了对应的 GPIO 是上升沿和下降沿(双边沿)触发。例如,我们设置 GPIO1.EDGE_SEL=1,那么就表示 GPIO1_IO01 是双边沿触发中断,无论 GFPIO1_CR1 的设置为多少,都是双边沿触发。

⑨案例>涉及到IO就要配置IO

9.1 GPIO输出>0X10B0
/* 2、设置GPIO1_IO03复用为GPIO1_IO03 */
	ldr r0, =0X020E0068	/* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
	ldr r1, =0X5		/* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
	str r1,[r0]

    ldr r0, =0X020E02F4	/*寄存器SW_PAD_GPIO1_IO03_BASE */
    ldr r1, =0X10B0
    str r1,[r0]

	/* 4、设置GPIO1_IO03为输出 */
    ldr r0, =0X0209C004	/*寄存器GPIO1_GDIR */
    ldr r1, =0X0000008		
    str r1,[r0]

	/* 5、打开LED0,设置GPIO1_IO03输出低电平 */
	ldr r0, =0X0209C000	/*寄存器GPIO1_DR */
   ldr r1, =0		
   str r1,[r0]
void led_init(void){
    /* 1、初始化 IO 复用, 复用为 GPIO1_IO03 */
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
    
   	/* 2、配置 GPIO1_IO03 的 IO 属性
		*bit 16:0 HYS 关闭
		*bit [15:14]: 00 默认下拉
		*bit [13]: 0 kepper 功能
		*bit [12]: 1 pull/keeper 使能
		*bit [11]: 0 关闭开路输出
		*bit [7:6]: 10 速度 100Mhz
		*bit [5:3]: 110 R0/6 驱动能力
		*bit [0]: 0 低转换率 */
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0X10B0);

    /* 3、初始化 GPIO, GPIO1_IO03 设置为输出 */
    GPIO1->GDIR = 0x8;

    /* 4、设置 GPIO1_IO03 输出低电平,打开 LED0 */
    GPIO1->DR = 0x0;
}
9.2 GPIO输入>0xF080
gpio_pin_config_t key_config;

/* 1、初始化IO复用, 复用为GPIO1_IO18 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);

/* 2、、配置 UART1_CTS_B 的 IO 属性
*bit 16:0 HYS 关闭
*bit [15:14]: 11 默认 22K 上拉
*bit [13]: 1 pull 功能
*bit [12]: 1 pull/keeper 使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度 100Mhz
*bit [5:3]: 000 关闭输出
*bit [0]: 0 低转换率
 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);

/* 3、初始化GPIO */
#if 1
key_config.direction = kGPIO_DigitalInput; /* GPIO1_IO18设置为输入 */	
gpio_init(GPIO1,18, &key_config); //GPIO1->GDIR &= ~(1 << 18);	
#else
GPIO1->GDIR &= ~( 1 << 18);
#endif


9.3 UART1>TXD.TRD通信 0x10b0
/*
 * @description : 初始化串口1所使用的IO引脚
 * @param		: 无
 * @return		: 无
 */
void uart_io_init(){

    IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
    IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
    
/*      *bit 16:0 HYS 关闭
		*bit [15:14]: 00 默认下拉
		*bit [13]: 0 kepper 功能
		*bit [12]: 1 pull/keeper 使能
		*bit [11]: 0 关闭开路输出
		*bit [7:6]: 10 速度 100Mhz
		*bit [5:3]: 110 R0/6 驱动能力
		*bit [0]: 0 低转换率 */
    IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0x10b0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX,0x10b0);

}
9.4 LCD引脚>RBGLCD 0xB9
/* 1、IO初始化复用功能 */
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00,0);
	......
/* 2、配置LCD IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 0 默认100K下拉
	 *bit [13]: 0 pull功能
	 *bit [12]: 0 pull/keeper使能 
	 *bit [11]: 0 关闭开路输出
	 *bit [7:6]: 10 速度100Mhz
	 *bit [5:3]: 111 驱动能力为R0/7
	 *bit [0]: 1 高转换率
	 */
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00,0xB9);
	......
9.5 I2C引脚>I2C2 0x70B0
/* 1、IO初始化,配置I2C IO属性	
     * I2C2_SCL -> UART5_TXD
     * I2C2_SDA -> UART5_RXD
     */
	IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL, 1);
	IOMUXC_SetPinMux(IOMUXC_UART5_RX_DATA_I2C2_SDA, 1);

	/* 
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 1 默认47K上拉
	 *bit [13]: 1 pull功能
	 *bit [12]: 1 pull/keeper使能 
	 *bit [11]: 0 关闭开路输出
	 *bit [7:6]: 10 速度100Mhz
	 *bit [5:3]: 110 驱动能力为R0/6
	 *bit [0]: 1 高转换率
	 */
	IOMUXC_SetPinConfig(IOMUXC_UART5_TX_DATA_I2C2_SCL, 0x70B0);
	IOMUXC_SetPinConfig(IOMUXC_UART5_RX_DATA_I2C2_SDA, 0X70B0);
9.6 SPI通信 0x10B1
	/* 1、ESPI3 IO初始化 
 	 * ECSPI3_SCLK 	-> UART2_RXD
 	 * ECSPI3_MISO 	-> UART2_RTS
 	 * ECSPI3_MOSI	-> UART2_CTS
 	 */
	IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
	IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
	IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
	
	/* 配置SPI   SCLK MISO MOSI IO属性	
	 *bit 16: 0 HYS关闭
	 *bit [15:14]: 00 默认100K下拉
	 *bit [13]: 0 keeper功能
	 *bit [12]: 1 pull/keeper使能 
	 *bit [11]: 0 关闭开路输出
 	 *bit [7:6]: 10 速度100Mhz
 	 *bit [5:3]: 110 驱动能力为R0/6
	 *bit [0]: 1 高转换率
 	 */
	IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
	IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
	IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);

	/* 初始化片选引脚 */
	IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
	IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0X10B0);

这里的软件片选,因为原本就是IOMUXC_UART2_TX_DATA_ECSPI3_SS0,也就是UART2_TX这个IO复用为ECSPI3_SS0,IO还是这个IO,但是选择了IOMUXC_UART2_TX_DATA_GPIO1_IO20,即复用功能不同而已。

9.7 pwm引脚 0XB090
/* 1、背光PWM IO初始化 */
	IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT, 0); /* 复用为PWM1_OUT */

	/* 配置PWM IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 10 100K上拉
	 *bit [13]: 1 pull功能
	 *bit [12]: 1 pull/keeper使能
	 *bit [11]: 0 关闭开路输出
	 *bit [7:6]: 10 速度100Mhz
	 *bit [5:3]: 010 驱动能力为R0/2
	 *bit [0]: 0 低转换率
	 */
	IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT, 0XB090);
	

五、BSP工程管理

Board Support Package ???
一个项目文件中有bsp、imx6ul、obj 和 project 这 4 个文件夹。
其中 ,
bsp 用来存放驱动文件,如bsp_led,bsp_gpio,驱动文件中又有搭配 .c .h文件;
imx6ul 用来存放跟芯片有关的文件,比如 NXP 官方的 SDK库文件;
obj 用来存放编译生成的.o 文件;
project 存放 start.S 和 main.c 文件,也就是应用文件;


六、主频和时钟配置

1.基本介绍

默认配置下 I.MX6U 工作频率为 396MHz。
RTC(Real_Time Clock) 实时时钟
PLL 锁相环路,简称锁相环,是一种输出一定频率信号的振电路,倍频器???
PFD Phase Fractional Dividers相位分数分频器
OSC通常表示振荡器(Oscillator),晶振s

Uses the available clock sources to generate clock roots to various parts of the chip:

• PLL1 also referenced as ARM PLL
• PLL2 also referenced as System PLL
• PLL3 also referenced as USB1 PLL
• PLL4 also referenced as Audio PLL
• PLL5 also referenced as Video PLL
• PLL6 also referenced as ENET PLL
• PLL7 also referenced as USB2 PLL (This PLL is only used by the USB UTM interface through a direct connection.)

32.768KHz 晶振是 I.MX6U 的 RTC 时钟源,24MHz 晶振OSC是 I.MX6U 内核和其它外设的时钟源。I.MX6U 的外设有很多,不同的外设时钟源不同,NXP 将这些外设的时钟源进行了分组,一共有 7 组,这 7 组时钟源都是从 24MHz 晶振 PLL 而来的,因此也叫做 7 组 PLL,一些PLL能够分出不同的PFD。

I.MX6U 的所有外设时钟源都是从这 7 路 PLL 和有些 PLL 的PFD 而来的,Chapter 18 Clock Controller Module (CCM)的时钟树描述了各外设时钟源的来源,即如何配置的。时钟树可以划分为三部分:CLOCK_SWITCHER、CLOCK ROOT GENERATOR 和SYSTEM CLOCKS。

CLOCK_SWITCHER:7 路 PLL 和8 路 PFD可供选择
CLOCK ROOT GENERATOR:描写时钟源的配置关系,负责从 7 路PLL 和 8 路 PFD 中选择合适的时钟源给外设使用
SYSTEM CLOCKS:连接芯片外设

Table 18-4. System Clock Frequency Values,系统时钟的取值范围如下:
在这里插入图片描述

2.如何根据时钟树进行配置

18.3 CCM Clock Tree, 以ESAI 这个外设为例
在这里插入图片描述
①、此部分是时钟源选择器,ESAI 有 4 个可选的时钟源,是 由 寄 存 器 CCM->CSCMR2 的ESAI_CLK_SEL 位来决定的
②、此部分是 ESAI 时钟的前级分频,分频值由寄存器 CCM_CS1CDR 的 ESAI_CLK_PRED来确定的
③、分 频 器 , 对 ② 中 输 出 的 时 钟 进 一 步 分 频 , 分 频 值 由 寄 存 器CCM_CS1CDR 的 ESAI_CLK_PODF 来决定

根据各寄存器的设置位,可以配置相应的外设频率。

3.设置PLL及外设时钟

目标:将主频设置为528MHZ

①从时钟树查看外设时钟配置路线

在这里插入图片描述
①、内核时钟源来自于 PLL1
②、通过寄存器 CCM_CACRR 的 ARM_PODF 位对 PLL1 进行分频,1-8分频
③、此处没有进行 2 分频
公式:ARM_CLK_ROOT = PLL1 / CCM_CACRR ->ARM_PODF
要想ARM_CLK_ROOT = 528MHZ ,首先得知道PLL1的值,如果CCM_CACRR ->ARM_PODF为2分频,那么PLL1 就得为1056MHZ

CCM->CACRR = 1;											/* ARM内核时钟为pll1_sw_clk/2=1056/2=528Mhz */

具体代码见总结

②PLLx的时钟来源>更改PLL1内的时钟源选择

在修改 PLL1 时钟频率的时候我们需要先将内核时钟源改为其他的时钟源。

18.5.1.5.1 Clock Switcher,描述了各PLLx的时钟来源,PLL1 可选择的时钟源如图 16.1.4.4 所示:
在这里插入图片描述
①、pll1_sw_clk 也就是 PLL1 的最终输出频率。
② 、 此 处 是 一 个 选 择 器 , 选 择 pll1_sw_clk 的 时 钟 源 , 由 寄 存 器 CCM_CCSR 的PLL1_SW_CLK_SEL 位决定 pll1_sw_clk 是选择 pll1_main_clk 还是 step_clk。正常情况下应该选择 pll1_main_clk,但是如果要对 pll1_main_clk(PLL1)的频率进行调整的话,比如我们要设置PLL1=1056MHz,此时就要先将 pll1_sw_clk 切换到 step_clk 上。等 pll1_main_clk 调整完成以后再切换回来。
③、此处也是一个选择器,选择 step_clk 的时钟源,由寄存器 CCM_CCSR 的 STEP_SEL 位来决定 step_clk 是选择 osc_clk 还是 secondary_clk。

CCM->CCSR &= ~(1 << 8);				/* 配置step_clk时钟源为24MH OSC */	
CCM->CCSR |= (1 << 2);				/* 配置pll1_sw_clk时钟源为step_clk */

具体代码见总结

③设置PLL1内的时钟

18.5.1.3.1 ARM PLL 指出,PLL output frequency = Fref * DIV_SEL/2
其中,PLL output frequency = PLL1 ,Fref = OSC , DIV_SEL = CCM_ANALOG_PLL_ARM[DIV_SELECT].

18.7.1 Analog ARM PLL control Register (CCM_ANALOG_PLL_ARMn)
DIV_SELECT,占7bit,This field controls the PLL loop divider. Valid range for divider value: 54-108. Fout = Fin * div_select/2.0
Fout = Fin * div_select/2.0 =》 PLL output frequency = Fref * DIV_SEL/2
Fin=24MHz,如果 PLL1 要输出 1056MHz 的话,div_select 就要设置为 88,54 < 88 < 108,使能 PLL1 输出。

CCM_ANALOG->PLL_ARM = (1 << 13) | ((88 << 0) & 0X7F); 	/* 配置pll1_main_clk=1056MHZ */

具体代码见总结

④步骤总结及代码详解

①、 设置寄存器 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。

代码如下:

①②

/*18.6.4 CCM Clock Switcher Register (CCM_CCSR)
查看当前pll1_sw_clk使用的是pll1_main_clk(0)还是tep_clk(1)*/
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 */
	}

③④⑤

  /* bit13: 1 使能时钟输出 */
	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/1=1056/1=528Mhz */

⑤其他外设时钟设置与MMDC 握手

其他外设时钟的设置也可以根据上面的步骤来,从时钟树找起,也可以找其他的电路图,如18.5.1.5.4 Clock Root Generator
要注意的是,在修改如下时钟选择器或者分频器的时候会引起与 MMDC 的握手发生:

①、mmdc_podf
②、periph_clk_sel
③、periph2_clk_sel
④、arm_podf
⑤、ahb_podf

手册的description中会有提示:

NOTE: Any change of this mux select will involve handshake with the MMDC. Refer to the CCDR and CDHIPR registers for the handshake bypass and busy bits.

发生握手信号以后需要等待握手完成,寄存器 CCM_CDHIPR 中保存着握手信号是否完成,发生握手信号以后需要等待握手完
成,寄存器 CCM_CDHIPR 中保存着握手信号是否完成,如果相应的位为 1 的话就表示握手没有完成,如果为 0 的话就表示握手完成。
例如,18.6.6 CCM Bus Clock Divider Register (CCM_CBCDR)的PERIPH_CLK_SEL位,在18.6.17 CCM Divider Handshake In-Process Register (CCM_CDHIPR)的第五位PERIPH_CLK_SEL_BUSY上显示,等待握手如下:

while(CCM->CDHIPR & (1 << 5));/* 等待握手完成 */

⑥寄存器设置规范

如果涉及到寄存器的多位赋值时,可以一步一步的与或,当直接对寄存器进行操作时,如果清零,0 也可能表示一种配置,特别是一些分频器的配置是有范围的,直接操作寄存器可能会导致出错,故建议先取值,在运算,最后赋值
例如:用32位的reg进行过渡,避免直接操作寄存器。

如果不想如此麻烦,也一定要先清零再赋值,因为只要清零了,后面赋值(与、非运算)一定不会出错,清零是一种保障。

/* 2、设置PLL2(SYS PLL)各个PFD */
	unsigned int reg = 0;
	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 		 		*/

4.设置PFD时钟

设置 PLL2 和 PLL3 的各自 4 路 PFD,NXP 推荐的这 8 路 PFD 频率如表 16.1.5.1 所示:
在这里插入图片描述

18.7.16 528MHz Clock (PLL2) Phase Fractional Divider Control Register (CCM_ANALOG_PFD_528n)
置 PLL2 的 4 路 PFD 频率,用到寄存器是 CCM_ANALOG_PFD_528n,

PFD0对应的寄存器位如下:
PFD0_FRAC: PLL2_PFD0 的分频数,PLL2_PFD0 的计算公式为 528*18/PFD0_FRAC,此位可设置的范围为12~35
PFD0_STABLE: 此位为只读位,可以通过读取此位判断 PLL2_PFD0 是否稳定。
PFD0_CLKGATE: PLL2_PFD0 输出使能位,为 1 的时候关闭 PLL2_PFD0 的输出,为 0 的时候使能输出.

如 果 PLL2_PFD0 的 频 率 要 设 置 为 352MHz 的 话,PFD0_FRAC=528*18/352=27。其他PLLx_PFDx依次类推。


七、中断

1.中断向量表>判断触发何种中断类型

Cortex-A7 有中断向量表,中断向量表是在代码的最前面。Cortex-A7 内核有 8 个异常中断,这 8 个异常中断的中断向量表如表 17.1.2.1 所示:
在这里插入图片描述
在表 17.1.2.1 中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出
相应的处理。

在表 17.1.2.1 中一共有 7 个中断,简单介绍一下这 7 个中断:

①、复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中断的发生。
⑦、FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。

首先我们要根据表 17.1.2.1
的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的 start.S 文件最前面,中断向量表如下:

.global _start
_start:
    @程序计数器 R15(PC)总是指向当前正在执行的指令地址再加上 2 条指令的地址
	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(快速中断)未定义中断 	*/

/* 复位中断 */	
Reset_Handler:
/* 复位中断具体处理过程 */

/* 未定义中断 */
Undefined_Handler:
	ldr r0, =Undefined_Handler
	bx r0

/* SVC中断 */
SVC_Handler:
	ldr r0, =SVC_Handler
	bx r0

/* 预取终止中断 */
PrefAbort_Handler:
	ldr r0, =PrefAbort_Handler	
	bx r0

/* 数据终止中断 */
DataAbort_Handler:
	ldr r0, =DataAbort_Handler
	bx r0

/* 未使用的中断 */
NotUsed_Handler:
	ldr r0, =NotUsed_Handler
	bx r0

/* IRQ中断!重点!!!!! */
IRQ_Handler:
	/* 复位中断具体处理过程 */

/* FIQ中断 */
FIQ_Handler:
	ldr r0, =FIQ_Handler	
	bx r0	

中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数IRQ_Handler,其它的中断本教程没有用到,所以都是死循环.

2.中断ID>判断具体是何种中断

中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。1020 个 ID 分配如下:

ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 ,至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。

I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160个,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 Cortex A7 interrupts”小节。
前面移植了 NXP 官方 SDK中的文件 MCIMX6Y2C.h,在此文件中定义了一个枚举类型 IRQn_Type,此枚举类型就枚举出
了 I.MX6U 的所有中断,代码如下所示:

#define NUMBER_OF_INT_VECTORS 160 /* 中断源 160 个,SGI+PPI+SPI*/
typedef enum IRQn {
  /* Auxiliary constants */
  NotAvail_IRQn                = -128,             /**< Not available device specific interrupt */
  /* Core interrupts */
  Software0_IRQn               = 0,                /**< Cortex-A7 Software Generated Interrupt 0 */
  Software1_IRQn               = 1,                /**< Cortex-A7 Software Generated Interrupt 1 */
  Software2_IRQn               = 2,                /**< Cortex-A7 Software Generated Interrupt 2 */
  ......
  Reserved158_IRQn             = 158,              /**< Reserved */
  PMU_IRQ2_IRQn                = 159        /**< Brown-out event on either core, gpu or soc regulators. */
} IRQn_Type;

当一个中断处理完成以后必须向 GICC_EOIR 寄存器写入其中断号表示中断处理完成

3.GIC 中断控制器>通过GIC获得中断ID并使能

I.MX6U(Cortex-A)的中断控制器叫做 Generic Interrupt Controller GIC,关于 GIC 的详细内容请参考开发板光盘中的文档《ARM Generic Interrupt Controller(ARM GIC 控制器)V2.0.pdf》。

GIC V2 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况:VFIQ、VIRQ、FIQ 和 IRQ,他们之间的关系如图 17.1.3.1 所示:
在这里插入图片描述

在图 17.1.3.1 中,GIC 接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给 ARM 内核,这四个信号的含义如下:

VFIQ:虚拟快速 FIQ。
VIRQ:虚拟外部 IRQ。
FIQ:快速中断 FRQ。
IRQ:外部中断 IRQ。

这里主要关注 IRQ 。

GIC 将众多的中断源分为分为三类:

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

GIC 架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。
Distributor(分发器端):此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去。
CPU Interface(CPU 接口端):CPU 接口端听名字就知道是和 CPU Core 相连接的,因此每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。
文件 core_ca7.h 定义了 GIC 结构体,此结构体里面的寄存器分为了分发器端和 CPU 接口端,寄存器定义如下所示:

/*******************************************************************************
 *                 GIC相关内容
 *有关GIC的内容,参考:ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf
 ******************************************************************************/

/*
 * GIC寄存器描述结构体,
 * GIC分为分发器端和CPU接口端
 */
typedef struct
{
/* 分发器端寄存器 */
  uint32_t RESERVED0[1024];
  __IOM uint32_t D_CTLR;                 /*!< Offset: 0x1000 (R/W) Distributor Control Register */
  __IM  uint32_t D_TYPER;                /*!< Offset: 0x1004 (R/ )  Interrupt Controller Type Register */
  ......
  __IM  uint32_t D_CIDR2;                /*!< Offset: 0x1FF8 (R/ ) Component ID2 Register */
  __IM  uint32_t D_CIDR3;                /*!< Offset: 0x1FFC (R/ ) Component ID3 Register */
/* CPU 接口端寄存器 */
  __IOM uint32_t C_CTLR;                 /*!< Offset: 0x2000 (R/W) CPU Interface Control Register */
  __IOM uint32_t C_PMR;                  /*!< Offset: 0x2004 (R/W) Interrupt Priority Mask Register */
  __IOM uint32_t C_BPR;                  /*!< Offset: 0x2008 (R/W) Binary Point Register */
  ......
  __IM  uint32_t C_IIDR;                 /*!< Offset: 0x20FC (R/ ) CPU Interface Identification Register */
        uint32_t RESERVED18[960];
  __OM  uint32_t C_DIR;                  /*!< Offset: 0x3000 ( /W) Deactivate Interrupt Register */
} GIC_Type;

D_CTLR:GIC 的分发器端相关寄存器,其相对于 GIC 基地址偏移为 0X1000,因此我们获取到 GIC 基地址以后只需要加上 0X1000 即可访问 GIC 分发器端寄存器。
C_CTLR:GIC 的 CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000,同样的,获取到 GIC 基地址以后只需要加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,可获得中断ID。
Cortex-A 的 CP15 协处理器可以获得GIC 控制器的寄存器基地址,CP15见④。

使能GIC中对应的中断 :
例如使能GPIO1_IO18中断,在IRQn_Type 中断ID中可以找到GPIO1_Combined_16_31_IRQn,其代表GPIO1_IO16到GPIO1_IO31这些IO的中断。
使能:GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn)

4.CP15协处理器>获得GIC基地址

关 于 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”。

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

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

CP15 协处理器的访问指令格式:MCR / MRC {cond} p15, < opc1>, < Rt>, < CRn>, < CRm>, < opc2>

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

Cortex-A7 Technical ReferenceManua.pdf》第55 页“Capter 4 System Control”给出了每个寄存器的配置情况,通过配置这16个寄存器可以获得其他各类寄存器的值,或配置其它寄存器。c15 寄存器也可以通过不同的配置得到不同的含义,如下:
在这里插入图片描述
我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR中,我们可以通过如下命令获取到 GIC 基地址:

MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中。

如我们可以读取当前中断 ID,当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址
相对于 CPU 接口端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下:

mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C15寄存器内的值到R1寄存器中,  c15 寄存器可以获取 GIC 基地址, p441
								* 参考文档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寄存器,
                               Cortex-A7 Technical ReferenceManua 的 8:Generic Interrupt Controller,p188 
							 * GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
							 * 这个中断号来绝对调用哪个中断服务函数
							 */

总结一下,通过 c0 寄存器可以获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。

5.中断使能>总开关和子开关使能

中断使能包括两部分,一个是 IRQ、 FIQ 总中断使能(总开关),另一个就是 ID0~ID1019 这 1020个中断源使能(子开关)。
总开关打开才能使用子开关。

5.1 总开关>IRQ 和 FIQ 总中断使能

在“6.3.2 程序状态寄存器”小节已经讲过了,寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。我们还有更简单的指令来完成 IRQ 或者 FIQ 的使能和禁止,图表 17.1.5.1 所示:
在这里插入图片描述

5.2 D0~ID1019 中断使能和禁止

ARMGeneric Interrupt Controller Architecture Specification的
4.3.5 Interrupt Set-Enable Registers, GICD_ISENABLERn。
4.3.6 Interrupt Clear-Enable Registers, GICD_ICENABLERn

GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个GICD_ICENABLER 寄存器来完成中断的禁止。其中 GICD_ISENABLER0 的 bit[15:0]对应ID15-0 的 SGI 中断,GICD_ISENABLER0 的 bit[31:16]对应 ID31-16 的 PPI 中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。

为什么要由两个寄存器控制使能与否???

6.中断优先级

Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高!Cortex-A7 选择了 32 个优先级。占优先级(Preemption Priority)/ group priority 和子优先级(Subpriority),这两个优先级共同决定了中断的响应顺序。

抢占优先级决定了中断之间的嵌套关系。当一个高抢占优先级的中断发生时,如果当前正在处理一个低抢占优先级的中断,那么高优先级的中断会抢占低优先级中断的执行,直到高优先级中断处理完成。这种机制确保了关键任务能够及时得到处理。

子优先级则在具有相同抢占优先级的中断中起作用。如果两个中断具有相同的抢占优先级,那么子优先级高的中断会先被处理。子优先级的存在使得系统能够在多个同等重要的中断中做出选择,优先处理某些中断。

6.1 优先级数配置>选择 32 个优先级

ARM Generic Interrupt Controller Architecture Specification 4.4.2 Interrupt Priority Mask Register, GICC_PMR

在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级,GICC_PMR 寄存器只有低 8 位有效,这 8 位最多可以设置 256 个优先级,其他优先级数设置如表 17.1.6.1 所示:
在这里插入图片描述

在这里插入图片描述
I.MX6U 是 Cortex-A7 内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000。

6.2 抢占优先级和子优先级位数设置

ARM Generic Interrupt Controller Architecture Specification 4.4.3 Binary Point Register, GICC_BPR

抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的,寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如表 17.1.6.2 所示:
在这里插入图片描述
在这里插入图片描述
为了简单起见,一般将所有的中断优先级位都配置为抢占优先级,比如 I.MX6U 的优先级位数为 5(32 个优先级,Priority Mask Register, GICC_PMR = 0b11111000),所以可以设置 Binary point 为 2,表示 [7:3] 这5 个优先级位全部为抢占优先级。

6.4 优先级设置

前面已经设置好了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。具体要使用某个中断的时候 就可以设置 其优先级为 0~31。某 个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR 来完成,前面说了 Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:3 (32 个优先级,Priority Mask Register, GICC_PMR = 0b11111000) 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5,示例代码如下:

GICD_IPRIORITYR[40] = 5 << 3;

6.5 优先级配置总结

有关优先级设置的内容就讲解到这里,优先级设置主要有三部分:

①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。
②、设置抢占优先级和子优先级位数,一般为了简单起见,会将所有的位数都设置为抢占优先级。
③、设置指定中断 ID 的优先级,也就是设置外设优先级。

7.具体中断设置步骤

7.1 复位中断和IRQ中断汇编>编写start.S

复位中断:

/* 复位中断 */	
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
	/* 汇编版本设置中断向量表偏移 */
	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中断!重点!!!!! */
IRQ_Handler:
	push {lr}					/* 保存lr地址 */
	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 */
	

7.2 中断初始化>GIC与中断表初始化和中断向量表偏移

中断初始化包括:GIC初始化,中断表初始化,中断向量表偏移,其余不同的中断类型各写一个bsp文件即可。

/*
 * @description	: 中断初始化函数
 * @param		: 无
 * @return 		: 无
 */
void int_init(void)
{
	GIC_Init(); 						/* 初始化GIC 							*/
	system_irqtable_init();				/* 初始化中断表 							*/
	__set_VBAR((uint32_t)0x80100000); 	/* 中断向量表偏移,偏移到起始地址   		0X87800000*/
}

7.3 定义中断BSP文件>为外部中断定义BSP文件

步骤:

①初始化外设
②初始化外设中断
③使能GIC中对应的中断
④注册中断三步走函数(判断是否触发中断 -> 中断触发后的处理 -> 清楚中断标志位 )
⑤使能外设的中断功能

7.3.1 GPIO中断配置

步骤:

①初始化外设(IO)
②初始化外设(GPIO)中断
③使能GIC中对应的中断
④注册中断服务函数
⑤使能外设(GPIO1_IO18)的中断功能

/*
 * @description			: 初始化外部中断
 * @param				: 无
 * @return 				: 无
 */
void exit_init(void)
{
	gpio_pin_config_t key_config;

	/* 1、设置IO复用 */
	IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);			/* 复用为GPIO1_IO18 */
	IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);

	/* 2、初始化GPIO为中断模式 */
	key_config.direction = kGPIO_DigitalInput;
	key_config.interruptMode = kGPIO_IntFallingEdge;
	// key_config.outputLogic = 1;
	gpio_init(GPIO1, 18, &key_config);

	GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);				/* 使能GIC中对应的中断 */
	system_register_irqhandler(GPIO1_Combined_16_31_IRQn, 
							(system_irq_handler_t)gpio1_io18_irqhandler, NULL);	/* 注册中断服务函数 */
	gpio_enableint(GPIO1, 18);								/* 使能GPIO1_IO18的中断功能 */
}

/*
 * @description			: GPIO1_IO18最终的中断处理函数
 * @param				: 无
 * @return 				: 无
 */
void gpio1_io18_irqhandler(void)
{ 
	static u8 state = 0;

	/*
	 *采用延时消抖,中断服务函数中禁止使用延时函数!因为中断服务需要
	 *快进快出!!这里为了演示所以采用了延时函数进行消抖,后面我们会讲解
	 *定时器中断消抖法!!!
 	 */

	delay_ms(10);
	if(gpio_pinread(GPIO1, 18) == 0)	/* 按键按下了  */
	{
		state = !state;
		beep_switch(state);
	}
	
	gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}

7.3.2 EPIT定时器中断配置

步骤:

①初始化外设(EPIT1)
②初始化外设(EPIT1)中断, 这里的①②没分的太清
③使能GIC中对应的中断
④注册中断服务函数
⑤使能外设(EPIT1)的中断功能

/*
 * @description		: 初始化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;

	/*
     * CR寄存器:
     * bit25:24 01 时钟源选择Peripheral clock=66MHz
     * bit15:4  frac 分频值
     * bit3:	1  当计数器到0的话从LR重新加载数值
     * bit2:	1  比较中断使能
     * bit1:    1  初始计数值来源于LR寄存器值
     * bit0:    0  先关闭EPIT1
     */

    uint32_t reg = 0;  /*不要这个reg也行*/
	// EPIT1->CR = 0;	/* 先清零CR寄存器 */
	reg = (1<<24 | frac << 4 | 1<<3 | 1<<2 | 1<<1);
	EPIT1->CR = reg;
	
	EPIT1->LR = value;	/* 倒计数值 */
	EPIT1->CMPR	= 0;	/* 比较寄存器,当计数器值和此寄存器值相等的话就会产生中断 */

    /* 使能GIC中对应的中断 			*/
    GIC_EnableIRQ(EPIT1_IRQn);
    /* 注册中断服务函数 			*/
    system_register_irqhandler(EPIT1_IRQn, epit1_irqhandler, NULL) ;

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

/*
 * @description			: EPIT中断处理函数
 * @param				: 无
 * @return 				: 无
 */ 
void epit1_irqhandler(u32 gicciar, void *param){
    static u8 state = 0;
    state = !state;

    if (EPIT1->SR & (1<<0)){      /* 判断比较事件发生 */
        led_switch(LED0, state);  /* 定时器周期到,反转LED */
    }
    EPIT1->SR |= 1<<0; 				/* 清除中断标志位 */
}

八、EPIT定时器

EPIT 的全称是:Enhanced Periodic Interrupt Timer 增强型周期中断定时器,它主要是完成周期性中断定时的。学过 STM32 的话应该知道,STM32 里面的定时器还有很多其它的功能,比如输入捕获、PWM 输出等等。但是 I.MX6U 的 EPIT 定时器只是完成周期性中断定时的,仅此一项功能!至于输入捕获、PWM 输出等这些功能,I.MX6U 由其它的外设来完成。

1.两种工作模式

EPIT 定时器有两种工作模式:set-and-forget 和 free-running,这两个工作模式的区别如下:

set-and-forget 模式:EPITx_CR(x=1,2)寄存器的 RLD 位置 1 的时候 EPIT 工作在此模式下,在此模式下 EPIT 的计数器从加载寄存器 EPITx_LR 中获取初始值,不能直接向计数器寄存器写入数据。不管什么时候,只要计数器计数到 0,那么就会从加载寄存器 EPITx_LR 中重新加载数据到计数器中,周而复始。
.
free-running 模式:EPITx_CR 寄存器的 RLD 位清零的时候 EPIT 工作在此模式下,当计数器计数到 0 以后会重新从 0XFFFFFFFF 开始计数,并不是从加载寄存器 EPITx_LR 中获取数据。

2.EPIT配置相关寄存器

2.1 Control register (EPITx_CR)>类似于初始化

imx6ull参考手册 24.6.1 Control register (EPITx_CR)

CLKSRC(bit25:24):EPIT 时钟源选择位,为 0 的时候关闭时钟源,1 的时候选择选择Peripheral 时钟(ipg_clk),为 2 的时候选择 High-frequency 参考时钟(ipg_clk_highfreq),为 3 的时候选择 Low-frequency 参考时钟(ipg_clk_32k)。在本例程中,我们设置为 1,也就是选择 ipg_clk作为 EPIT 的时钟源,ipg_clk=66MHz。
.
PRESCALAR(bit15:4):EPIT 时钟源分频值,可设置范围 0~4095,分别对应 1~4096 分频。
.
RLD(bit3):EPIT 工作模式,为 0 的时候工作在 free-running 模式,为 1 的时候工作在 set-and-forget 模式。本章例程设置为 1,也就是工作在 set-and-forget 模式。
.
OCIEN(bit2):比较中断使能位,为 0 的时候关闭比较中断,为 1 的时候使能比较中断,本章试验要使能比较中断。
.
ENMOD(bit1):设置计数器初始值,为 0 时计数器初始值等于上次关闭 EPIT 定时器以后计数器里面的值,为 1 的时候来源于加载寄存器。
.
EN(bit0):EPIT 使能位,为 0 的时候关闭 EPIT,为 1 的时候使能 EPIT。

2.2 Status register (EPITx_SR)

24.6.2 Status register (EPITx_SR)
寄存器 EPITx_SR 只有一个位有效,那就是 OCIF(bit0),这个位是比较中断标志位,为 0 的时候表示没有比较事件发生,为 1 的时候表示有比较事件发生。当比较中断发生以后需要手动清除此位,此位是写 1 清零的。

2.3 Load register (EPITx_LR)

24.6.3 Load register (EPITx_LR)
32位的LR寄存器存放加载值,在set-and-forget 模式下,每个计数周期开始时加载到计数器中的值。

2.4 Compare register (EPITx_CMPR)

24.6.4 Compare register (EPITx_CMPR)
32位的CMPR寄存器存放比较值,当计数器值等于此位字段值时,将生成比较事件。

2.5 Counter register (EPITx_CNR)

24.6.5 Counter register (EPITx_CNR)
32位的CNR寄存器存放计数器值,表示计数器的当前值。

3.EPIT中断配置步骤>分频值和加载值的计算

具体代码可以看 七、中断的7.3.2 EPIT定时器中断

1、设置 EPIT1 的时钟源
设置寄存器 EPIT1_CR 寄存器的 CLKSRC(bit25:24)位,选择 EPIT1 的时钟源。
2、设置分频值
设置寄存器 EPIT1_CR 寄存器的 PRESCALAR(bit15:4)位,设置分频值。
3、设置工作模式
设置寄存器 EPIT1_CR 的 RLD(bit3)位,设置 EPTI1 的工作模式。
4、设置计数器的初始值来源
设置寄存器 EPIT1_CR 的 ENMOD(bit1)位,设置计数器的初始值来源。
5、使能比较中断
我们要使用到比较中断,因此需要设置寄存器 EPIT1_CR 的 OCIEN(bit2)位,使能比较中断。
6、设置加载值和比较值
设置寄存器 EPIT1_LR 中的加载值和寄存器 EPIT1_CMPR 中的比较值,通过这两个寄存器就可以决定定时器的中断周期。
7、EPIT1 中断设置和中断服务函数编写
使能 GIC 中对应的 EPIT1 中断,注册中断服务函数,如果需要的话还可以设置中断优先级。最后编写中断服务函数。
8、使能 EPIT1 定时器
配置好 EPIT1 以后就可以使能 EPIT1 了,通过寄存器 EPIT1_CR 的 EN(bit0)位来设置。


epit1_init 有两个参数 frac 和 value,其中 frac 是分频值,value 是加载值。

如果设置了 EPIT1 工作模式为 set-and-forget,并且时钟源为 ipg_clk=66MHz。假如我们现在要设置 EPIT1 中断周期为500ms,可以设置分频值为 0,也就是 1 分频,这样进入 EPIT1的 时 钟 就 是 66MHz ,也就是 1s 计 66M 个数, 如 果 要 实 现 500ms (0.5s)的 中 断 周 期 , EPIT1 的 加 载 寄 存 器 就 应 该 为value = 66000000/2=33000000,即计 33M 个数就触发一次中断。1s = 66M -> 0.5s = 33M


九、按键消抖

用到按键就要处理因为机械结构带来的按键抖动问题,也就是按键消抖。直接用延时函数来实现消抖会浪费 CPU 性能,因为在延时函数里面 CPU 什么都做不了,就是空跑。如果按键使用中断的话更不能在中断里面使用延时函数,因为中断服务函数要快进快出!

1.EPIT定时器消抖原理

EPIT 定时器设置好定时时间,然后 CPU 就可以做其他事情去了,定时时间到了以后就会触发中断,然后在中断中做相应的处理即可。因此,我们可以借助定时器来实现消抖,按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为 10ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。定时器按键消抖如图 19.1.1 所示:
在这里插入图片描述
在图 19.1.1 中 t1-t3 这一段时间就是按键抖动,是需要消除的。设置按键为下降沿触发,因此会在 t1、t2 和 t3 这三个时刻会触发按键中断,每次进入中断处理函数都会重新开器定时器中断,所以会在 t1、t2 和 t3 这三个时刻开器定时器中断。但是 t1~t2 和 t2~t3 这两个时间段是小于我们设置的定时器中断周期(也就是消抖时间,比如 10ms),所以虽然 t1 开启了定时器,但是定时器定时时间还没到呢 t2 时刻就重置了定时器,最终只有 t3 时刻开启的定时器能完整的完成整个定时周期并触发中断,我们就可以在中断处理函数里面做按键处理了,这就是定时器实现按键防抖的原理.

2.EPIT1 配合按键 KEY实现消抖步骤

2.1 配置按键 IO 中断

配置按键所使用的 IO,因为要使用到中断驱动按键,所以要配置 IO 的中断模式。

gpio_pin_config_t key_config;

/* 1、设置IO复用 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);			/* 复用为GPIO1_IO18 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);

/* 2、初始化GPIO为中断模式 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);

GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);				/* 使能GIC中对应的中断 */
system_register_irqhandler(GPIO1_Combined_16_31_IRQn, 
          (system_irq_handler_t)gpio1_16_31_irqhandler, NULL);	/* 注册中断服务函数 */
gpio_enableint(GPIO1, 18);								/* 使能GPIO1_IO18的中断功能 */

2.2 初始化消抖用的定时器

消抖要用定时器来完成,所以需要初始化一个定时器,这里使用 EPIT1 定时器,定时器的定时周期为 10ms,也可根据实际情况调整定时周期。

 epit1_init(0, 660000); 
 EPIT1_stop(); 	/*EPIT1->CR &= ~(1<<0) 关闭EPIT1定时器,当触发按键中断才开启 */ 

2.3 编写GPIO和EPIT中断处理函数

需要编写两个中断处理函数:按键对应的 GPIO 中断处理函数和 EPIT1 定时器的中断处理函数。在按键的中断处理函数中主要用于开启 EPIT1 定时器,EPIT1 的中断处理函数判断是否KEY是否按下,通过反转beep来体现。

void gpio1_16_31_irqhandler(void)
{ 
    EPIT1_restart(660000);
	gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}

void epit1_irqhandler2(u32 gicciar, void *param){
    static u8 state = 0;

    if (EPIT1->SR & (1<<0)){      /* 判断比较事件发生 */
        EPIT1_stop();               /* 关闭定时器 				*/
        if(gpio_pinread(GPIO1, 18) == 0)	/* 按键按下了  */
        {
            state = !state;
            beep_switch(state);
        }
    }
    EPIT1->SR |= 1<<0; 				/* 清除中断标志位 */
}

void EPIT1_stop(void){
    EPIT1->CR &= ~(1 << 0);  /* 关闭定时器 */
}

void EPIT1_restart(u32 value){
    EPIT1->CR &= ~(1<<0);	/* 先关闭定时器 */
	EPIT1->LR = value;		/* 计数值 			*/
	EPIT1->CR |= (1<<0);	/* 打开定时器 		*/
}

epit1_init(0, 660000) 中注册中断函数epit1_irqhandler2(),但没有使能EPIT1,即还没有开启EPIT1定时器。按键中断触发后开启EPIT1定时器,经过EPIT1_restart后,最后计时10ms,触发epit1_irqhandler2()函数,判断按键是否按下。
注意:当进入EPIT1 的 epit1_irqhandler2()函数后,判断按键按下了,就要立即关键EPIT1定时器,否则每10ms会进入一次epit1_irqhandler2(),因为此时按键还可能还在按着,这时beep会以10ms的频率不断反转。


十、GPT定时器

延时函数是很常用的 API 函数,使用循环来实现的延时函数不准确,如果ARM主频变动了,那么循环的数据也要变动,不方便,且误差会很大。在使用 STM32 的时候可以使用 SYSTICK 定时器来实现高精度延时, I.MX6U 有EPIT、 GPT 定时器来实现高精度延时。

1. GPT 通用定时器介绍

General Purpose Timer GPT 通用定时器, 参考手册Chapter 30 General Purpose Timer (GPT)

GPT 定时器是一个 32 位向上定时器(也就是从 0X00000000 开始向上递增计数),GPT 定时器也可以跟一个值进行比较,当计数器值和这个值相等的话就发生比较事件,产生比较中断。GPT 定时器有一个 12 位的分频器,可以对 GPT 定时器的时钟源进行分频,GPT 定时器特性如下:

①、一个可选时钟源的 32 位向上计数器。
②、两个输入捕获通道,可以设置触发方式。
③、三个输出比较通道,可以设置输出模式。
④、可以生成捕获中断、比较中断和溢出中断。
⑤、计数器可以运行在重新启动(restart)或(自由运行)free-run 模式。

2.两种工作模式

GPT 定时器有两种工作模式:重新启动(restart)模式和自由运行(free-run)模式,这两个工作模式的区别如下:

重新启动(restart)模式:当 GPTx_CR(x=1,2)寄存器的 FRR 位清零的时候 GPT 工作在此模式。在此模式下,当计数值和比较寄存器中的值相等的话计数值就会清零,然后重新从0X00000000 开始向上计数,只有比较通道 1 才有此模式!向比较通道 1 的比较寄存器写入任何数据都会复位 GPT 计数器。对于其他两路比较通道(通道 2 和 3),当发生比较事件以后不会复位计数器。
·
自由运行(free-run)模式:当 GPTx_CR(x=1,2)寄存器的 FRR 位置 1 时候 GPT 工作在此模式下,此模式适用于所有三个比较通道,当比较事件发生以后并不会复位计数器,而是继续计数,直到计数值为 0XFFFFFFFF,然后重新回滚到 0X00000000。

3.GPT配置相关寄存器

3.1 30.6.1 GPT Control Register (GPTx_CR)>初试化

寄存器 GPTx_CR 我们用到的重要位如下:

SWR(bit15):复位 GPT 定时器,向此位写 1 就可以复位 GPT 定时器,当 GPT 复位完成以后此为会自动清零。
·
FRR(bit9):运行模式选择,当此位为 0 的时候比较通道 1 工作在重新启动(restart)模式。当此位为 1 的时候所有的三个比较通道均工作在自由运行模式(free-run)。
·
CLKSRC(bit8:6):GPT 定时器时钟源选择位,为 0 的时候关闭时钟源;为 1 的时候选择ipg_clk 作为时钟源;为 2 的时候选择 ipg_clk_highfreq 为时钟源;为 3 的时候选择外部时钟为时钟源;为 4 的时候选择 ipg_clk_32k 为时钟源;为 5 的时候选择 ip_clk_24M 为时钟源。本章例程选择 ipg_clk 66M 作为 GPT 定时器的时钟源,因此此位设置位 1(0b001)。
·
ENMOD(bit1):GPT 使能模式,此位为 0 的时候如果关闭 GPT 定时器,计数器寄存器保存定时器关闭时候的计数值。此位为 1 的时候如果关闭 GPT 定时器,计数器寄存器就会清零。
·
EN(bit):GPT 使能位,为 1 的时候使能 GPT 定时器,为 0 的时候关闭 GPT 定时器。

3.2 30.6.2 GPT Prescaler Register (GPTx_PR)

PRESCALER(bit11:0),这就是 12 位分频值,可设置 0~4095,分别对应 1~4096 分频。
PRESCALER24M(15-12),也是设置分频值的,但是已经选择了ipg_clk,从图30-2来看,无需设置此位.
在这里插入图片描述

3.3 30.6.3 GPT Status Register (GPTx_SR)

寄存器 GPTx_SR 重要的位如下:

ROV(bit5):回滚标志位,当计数值从 0XFFFFFFFF 回滚到 0X00000000 的时候此位置 1。
·
IF2~IF1(bit4:3):输入捕获标志位,当输入捕获事件发生以后此位置 1,一共有两路输入捕获通道。如果使用输入捕获中断的话需要在中断处理函数中清除此位。
·
OF3~OF1(bit2:0):输出比较中断标志位,当输出比较事件发生以后此位置 1,一共有三路输出比较通道。如果使用输出比较中断的话需要在中断处理函数中清除此位。

3.4 30.6.4 GPT Interrupt Register (GPTx_IR)

控制回滚、输入捕获、输出比较的使能
bit2-0:对应输出比较3、2、1的使能。

3.5 GPT Output Compare Register 123 (GPTx_OCR123)

一个 GPT 定时器有三个 OCR 寄存器,每个输出比较通道对应一个输出比较寄存器,当计数器值和寄存器 GPTx_OCR1 中的值相等就会产生比较事件,如果使能了比较中断的话就会触发相应的中断。

3.6 GPT Input Capture Register 12 (GPTx_ICR12)

一个 GPT 定时器有两个 ICR 寄存器,在输入捕获通道 1 上发生捕获事件后,计数器的当前值将加载到 GPT 输入捕获寄存器 1 中。

3.7 30.6.10 GPT Counter Register (GPTx_CNT)

显示当前计数器的值

4.GPT定时器使用

4.1 GPT1输出比较中断设置

中断设置步骤:

①初始化外设
②初始化外设中断
③使能GIC中对应的中断
④注册中断三步走函数(判断是否触发中断 -> 中断触发后的处理 -> 清楚中断标志位 )
⑤使能外设的中断功能

仿写EPIT的中断设置,注意:GPTbit15是软件复位,需要等待

void delay_init(u32 fra, u32 value){
    GPT1->CR = 0; 					/* 清零,bit0也为0,即停止GPT */
    
    GPT1->CR |= (1<<15);           /* bit15置1进入软复位 				*/
    while((GPT1->CR >> 15) & 0x01);	/*等待复位完成 						*/
/*
   	 * GPT的CR寄存器,GPT通用设置
     * bit6-8:    001 Peripheral Clock (ipg_clk)
     * bit1:1 GPT counter value is reset to 0 when it is disabled.
     * bit9: 0 restart mode
  	 */
    GPT1->CR |= (1<<1) |(1<<6);

    GPT1->PR = (fra<<0); // 分频值

    GPT1->IR |= (1<<0);  //中断使能

    GPT1->OCR[0] = value; //比较值

    GIC_EnableIRQ(GPT1_IRQn);
    system_register_irqhandler(GPT1_IRQn, GPT1_irqhandler, NULL);

    GPT1->CR |= (1<<0);
}

void GPT1_irqhandler(u32 gicciar, void *param){
    static u8 state = 0;
    state = !state;

    if (GPT1->SR & (1<<0)){      /* 判断比较事件发生 */
        led_switch(LED0, state);  /* 定时器周期到,反转LED */
    }
    GPT1->SR |= 1<<0; 				/* 清除中断标志位 */
}

4.2 高精度延迟函数设置

设置好GPT频率后,就知道计数器每加一个数所耗费的时间,获取CPT->CNT计数器的值,在while中不断取值累加,直到到达目标值。

void delay_init(void){
    GPT1->CR = 0; 					/* 清零,bit0也为0,即停止GPT */

    GPT1->CR |= (1<<15);           /* bit15置1进入软复位 				*/
    while((GPT1->CR >> 15) & 0x01);	/*等待复位完成 						*/
	/*
   	 * GPT的CR寄存器,GPT通用设置
     * bit6-8:    001 Peripheral Clock (ipg_clk)
     * bit1:1 GPT counter value is reset to 0 when it is disabled.
     * bit9: 0 restart mode
  	 */
    GPT1->CR |= (1<<1) |(1<<6);

    GPT1->PR = 65;// 分频值
 
    /*  
      * GPT的OCR1寄存器,GPT的输出比较1比较计数值,
      *	GPT的时钟为1Mz,那么计数器每计一个值就是就是1us。
      * 为了实现较大的计数,我们将比较值设置为最大的0XFFFFFFFF,
      * 这样一次计满就是:0XFFFFFFFFus = 4294967296us = 4295s = 71.5min
      * 也就是说一次计满最多71.5分钟,存在溢出
	  */
    GPT1->OCR[0] = 0xffffffff; //比较值
   
    GPT1->CR |= (1<<0);//使能GPT1
}

/*
 * @description		: 微秒(us)级延时
 * @param - value	: 需要延时的us数,最大延时0XFFFFFFFFus
 * @return 			: 无
 */
void delay_us(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;
			oldcnt = newcnt;
			if(tcntvalue >= usdelay)/* 延时时间到了 */
			break;			 		/*  跳出 */
		}
	}
}


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

十一、UART串口通信

1.UART/USART及通信格式介绍

UART 全称是 Universal Asynchronous Receiver/Trasmitter,也就是异步串行收发器。
串口全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。

USART 的 全 称 是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/异步串行收发器。
相比 UART 多了一个同步的功能,在硬件上体现出来的就是多了一条时钟线。

UART 作为串口的一种,其工作原理也是将数据一位一位的进行传输,发送和接收各用一条线,因此通过 UART 接口与外界相连最少只需要三条线:TXD(发送)、RXD(接收)和 GND(地线)。

在这里插入图片描述
图 21.1.1.1 中各位的含义如下:

空闲位:数据线在空闲状态的时候为逻辑“1”状态,也就是高电平,表示没有数据线空闲,没有数据传输。
·
起始位:当要传输数据的时候先传输一个逻辑“0”,也就是将数据线拉低,表示开始数据传输。
·
数据位:数据位就是实际要传输的数据,数据位数可选择 5~8 位,我们一般都是按照字节传输数据的,一个字节 8 位,因此数据位通常是 8 位的。低位在前,先传输,高位最后传输。
·
奇偶校验位:这是对数据中“1”的位数进行奇偶校验用的,可以不使用奇偶校验功能。
·
停止位:数据传输完成标志位,停止位的位数可以选择 1 位、1.5 位或 2 位高电平,一般都选择 1 位停止位。
·
波特率:波特率就是 UART 数据传输的速率,也就是每秒传输的数据位数,一般选择 9600、19200、115200 等。

2.UART电平标准>TTL/RS-232

UART 一般的接口电平有 TTL 和 RS-232

TTL 电平:低电平表示逻辑 0,高电平表示逻辑 1,如一般开发板上的 TXD 和 RXD 这样的引脚。
RS-232: 采用差分线,-3~-15V 表示逻辑 1,+3~+15V 表示逻辑 0。

现在的电脑都有 USB 接口,所以就催生出了很多 USB转串口 TTL 芯片,比如 CH340、PL2303 等。

3.UART时钟源配置

时钟树如下:
在这里插入图片描述
粉色的时钟线是PLL3 480MHZ,经过6分频后为pll3_80m 80MHZ,UART 的时钟源是由寄存器 CCM_CSCDR1 的 UART_CLK_SEL(bit)位来选择的,当为 0 的时候 UART 的时钟源为 pll3_80m(80MHz),如果为 1 的时候 UART 的时钟源为 osc_clk(24M),一般选择 pll3_80m 作为 UART 的时钟源。寄存器 CCM_CSCDR1 的 UART_CLK_PODF(bit5:0)
位是 UART 的时钟分频值,可设置 0~63,分别对应 1~64 分频,一般设置为 1 分频,因此最终进入 UART 的时钟为 80MHz。

CCM_CSCDR1[UART_CLK_SEL] = 0; //选择 pll3_80m
CCM_CSCDR1[UART_CLK_PODF] = 0; // 1 分频

4.UART寄存器>只涉及TXD、RXD相关寄存器

4.1 55.15.3 UART Control Register 1 (UARTx_UCR1)

寄存器 UARTx_UCR1 我们用到的重要位如下:

ADBR(bit14):自动波特率检测使能位,为 0 的时候关闭自动波特率检测,为 1 的时候使能自动波特率检测。
`
UARTEN(bit0):UART 使能位,为 0 的时候关闭 UART,为 1 的时候使能 UART。

4.2 55.15.4 UART Control Register 2 (UARTx_UCR2)

寄存器 UARTx_UCR2 用到的重要位如下:

IRTS(bit14):为 0 的时候使用 RTS 引脚功能,为 1 的时候忽略 RTS 引脚。
·
PREN(bit8):奇偶校验使能位,为 0 的时候关闭奇偶校验,为 1 的时候使能奇偶校验。
·
PROE(bit7):奇偶校验模式选择位,开启奇偶校验以后此位如果为 0 的话就使用偶校验,此位为 1 的话就使能奇校验。
·
STOP(bit6):停止位数量,为 0 的话 1 位停止位,为 1 的话 2 位停止位。
·
WS(bit5):数据位长度,为 0 的时候选择 7 位数据位,为 1 的时候选择 8 位数据位。
·
TXEN(bit2):发送使能位,为 0 的时候关闭 UART 的发送功能,为 1 的时候打开 UART的发送功能。
·
RXEN(bit1):接收使能位,为 0 的时候关闭 UART 的接收功能,为 1 的时候打开 UART的接收功能。
·
SRST(bit0):软件复位,为 0 的是时候软件复位 UART,为 1 的时候表示复位完成。复位完成以后此位会自动置 1,表示复位完成。此位只能写 0,写 1 会被忽略掉。

4.3 55.15.5 UART Control Register 3 (UARTx_UCR3)

寄存器 UARTx_UCR3 中的位 RXDMUXSEL(bit2),这个位应该始终为 1。

RXD Muxed Input Selected. Selects proper input pins for serial and Infrared input signal.
NOTE: In this chip, UARTs are used in MUXED mode, so that this bit should always be set.
`
RXD 多路复用输入已选中。为串行和红外输入信号选择适当的输入引脚。
注意:在此芯片中,UART 在 MUXED 模式下使用,因此应始终设置此位。

4.4 55.15.9 UART Status Register 2 (UARTx_USR2)

寄存器 UARTx_USR2 用到的重要位如下:

TXDC(bit3):发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向 TxFIFO 写入数据此位就会自动清零。
`
RDR(bit0) : 数 据 接 收 标 志 位 , 为 1 的 时 候 表 明 至 少 接 收 到 一 个 数 据 , 从 寄 存 器UARTx_URXD 读取数据接收到的数据以后此位会自动清零。

4.5 Baud Rate 配置>UARTx_UFCR &UARTx_UBIR & UARTx_UBMR寄存器

55.5 Binary Rate Multiplier (BRM)
55.5 Binary Rate Multiplier (BRM)

Ref Freq:UARTx_UFCR 中我们要用到的是位 RFDIV,用来设置参考时钟分频,Ref Freq为经过分频以后进入 UART 的最终时钟频率。
UBMR:寄存器 UARTx_UBMR 中的值。
UBIR:寄存器 UARTx_UBIR 中的值。

比如现在要设置 UART 波特率为 115200,那么可以设置 RFDIV 为5(0b101),也就是 1 分频,因此 Ref Freq=80MHz。设置 UBIR=71,UBMR=3124。

4.6 UARTx_URXD & UARTx_UTXD

UARTx_URXD 和 UARTx_UTXD,这两个寄存器分别为 UART 的接收和发送数据寄存器,这两个寄存器的低八位为接收到的和要发送的数据。
读取寄存器UARTx_URXD 即可获取到接收到的数据,如果要通过 UART 发送数据,直接将数据写入到寄存器 UARTx_UTXD 即可。

5.UART串口通信>TXD/RXD配置

UART1 的配置步骤如下:
1、设置 UART1 的时钟源
设置 UART 的时钟源为 pll3_80m,设置寄存器 CCM_CSCDR1 的 UART_CLK_SEL 位为 0即可。

CCM->CSCDR1 &= ~(1 << 6); //UART_CLK_ROOT
CCM->CSCDR1 &= ~(0x3f);   //都是置0,但默认好像就是0

2、初始化 UART1
初始化 UART1 所使用 IO,设置 UART1 的寄存器 UART1_UCR1~UART1_UCR3,设置内容包括波特率,奇偶校验、停止位、数据位等等。

/* 1、初始化串口IO 			*/
    uart_io_init();

     /* 2、初始化UART1  			*/
    uart_disable(UART1);
    uart_softreset(UART1);

    UART1->UCR1 = 0;		/* 先清除UCR1寄存器 */
	
	/*
     * 设置UART的UCR1寄存器,关闭自动波特率
     * bit14: 0 关闭自动波特率检测,我们自己设置波特率
	 */
	UART1->UCR1 &= ~(1<<14);
	
	/*
     * 设置UART的UCR2寄存器,设置内容包括字长,停止位,校验模式,关闭RTS硬件流控
     * bit14: 1 忽略RTS引脚
	 * bit8: 0 关闭奇偶校验
     * bit6: 0 1位停止位
 	 * bit5: 1 8位数据位
 	 * bit2: 1 打开发送
 	 * bit1: 1 打开接收
	 */
	UART1->UCR2 |= (1<<14) | (1<<5) | (1<<2) | (1<<1);

	/*
     * UART1的UCR3寄存器
     * bit2: 1 必须设置为1!参考IMX6ULL参考手册3624页
	 */
	UART1->UCR3 |= 1<<2; 
	
	/*
	 * 设置波特率
	 * 波特率计算公式:Baud Rate = Ref Freq / (16 * (UBMR + 1)/(UBIR+1)) 
	 * 如果要设置波特率为115200,那么可以使用如下参数:
	 * Ref Freq = 80M 也就是寄存器UFCR的bit9:7=101, 表示1分频
	 * UBMR = 3124
 	 * UBIR =  71
 	 * 因此波特率= 80000000/(16 * (3124+1)/(71+1))=
 	 * 80000000/(16 * 3125/72) = (80000000*72) / (16*3125) = 115200
	 */
	UART1->UFCR = 5<<7; //ref freq等于ipg_clk/1=80Mhz
	UART1->UBIR = 71;
	UART1->UBMR = 3124;
/*
 * @description : 关闭指定的UART
 * @param - base: 要关闭的UART
 * @return		: 无
 */
void uart_disable(UART_Type *base)
{
	base->UCR1 &= ~(1<<0);	
}

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

/*
 * @description : 复位指定的UART
 * @param - base: 要复位的UART
 * @return		: 无
 */
void uart_softreset(UART_Type *base)
{
    /*55.10.2 Software reset 正确步骤如下:
    Programmer can follow the following software reset sequence:
    1. Clear the SRST_B bit (UCR2[0])
    2. Wait for software reset complete: poll SOFTRST bit (UTS[0]) until it is 0.
    3. Re-program baud rate registers: Re-write UBIR and UBMR.
    */
	base->UCR2 &= ~(1<<0); 			/* UCR2的bit0为0,复位UART  	  	*/
	while((base->UCR2 & 0x1) == 0); /* 等待复位完成 					*/
}


/*
 * @description : 初始化串口1所使用的IO引脚
 * @param		: 无
 * @return		: 无
 */
void uart_io_init(){

    IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
    IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);

    IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0x10b0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX,0x10b0);

}

4、使能 UART1
UART1 初始化完成以后就可以使能 UART1 了,设置寄存器 UART1_UCR1 的位 UARTEN为 1。

/* 使能串口 */
uart_enable(UART1);
void uart_enable(UART_Type *base)
{
	base->UCR1 |= (1<<0);	
}

5、编写 UART1 数据收发函数
编写两个函数用于 UART1 的数据收发操作。

/*
 * @description : 发送一个字符
 * @param - c	: 要发送的字符
 * @return		: 无
 */
void putc(u8 c){ 
    while(((UART1->USR2 >>3) & 0x01) == 0);/* 等待上一次发送完成 */
    UART1->UTXD = c & 0XFF; 				/* 发送数据 */
}



/*
 * @description : 发送一个字符串
 * @param - str	: 要发送的字符串
 * @return		: 无
 */
void puts(char *str)
{
	char *p = str;

	while(*p)
		putc(*p++);
}

/*
 * @description : 接收一个字符
 * @param 		: 无
 * @return		: 接收到的字符
 */
unsigned char getc(void)
{
	while((UART1->USR2 & 0x1) == 0);/* 等待接收完成 */
	return UART1->URXD;				/* 返回接收到的数据 */
}

6.串口格式化函数>printf & scanf

移植stdio文件到工程文件中,stdio 里面的文件其实是从 uboot 里面移植过来的,stdio 中并没有实现完全版的格式化函数,比如 printf 函数并不支持浮点数


十二、DDR3

I.MX6U-ALPHA 开发板上带有一个 256MB/512MB 的 DDR3 内存芯片,一般 Cortex-A 芯片自带的 RAM 很小,比如 I.MX6U 只有 128KB 的 OCRAM。如果要运行 Linux 的话完全不够用的,所以必须要外接一片 RAM 芯片,I.MX6U 支持 LPDDR2、LPDDR3/DDR3,I.MX6U-ALPHA开发板上选择的是 DDR3。
在这里插入图片描述
参考:http://t.csdnimg.cn/Jqffo

1.RAM 系列

1.1 RAM(随机存储器)

早期的RAM是一种易失性存储器,它可以随机读取和写入数据。在这个阶段,RAM是一种相对易失的存储,通常在电源断开时会失去存储的数据。

现代RAM仍然是易失性存储器,但有了更快的速度和更高的密度。在计算机系统中,RAM用于临时存储正在运行的程序和数据。

1.2 SRAM(静态随机存储器)

SRAM是一种高速缓存存储器,以其快速的读写速度和相对较低的功耗而闻名。它由触发器电路构成,能够在不断电的情况下保持存储数据。

SRAM芯片通常用于高速缓存(如CPU和GPU中的L1/L2缓存),因为它们具有快速读写能力和不需要刷新的特性。由于每个SRAM存储单元需要6个晶体管,因此其集成度较低,存储容量有限,通常在几MB到几十MB之间。

1.3 DRAM(动态随机存储器)

DRAM是一种常见的内存类型,以其高密度和相对低廉的价格而受欢迎。它的存储单元是由一个电容和一个晶体管组成,需要定期刷新以保持数据。虽然DRAM的读写速度较慢,但在系统内存方面表现出色。

1.4 SDRAM(同步动态随机访问存储器)

SDRAM是同步性存储器,它的操作是与系统时钟同步的。相对于早期的DRAM(动态随机访问存储器)来说,提供了更高的数据传输速度。它通常具有较低的延迟,适用于需要快速读写的应用场景。

1.5 DDR SDRAM(双倍数据速率同步动态随机访问存储器)

DDR 全称是 Double Data Rate SDRAM
DDR SDRAM是SDRAM的一种升级版本,数据传输速率是SDRAM的两倍,提高内存带宽。 DDR SDRAM有不同的版本,如DDR2、DDR3、DDR4等,每个版本都提供了更高的频率和更好的性能。

DDR1:SDRAM 在一个 CLK 周期传输一次数据,DDR 在一个 CLK 周期传输两次数据,也就是在上升沿和下降沿各传输一次数据,这个概念叫做预取(prefetch),相当于 DDR 的预取为 2bit,因此DDR 的 速度直接加倍 。

DDR2: 在 DDR 基础上进一步增加预取(prefetch),增加到了 4bit,相当于比 DDR 多读取一倍的数据。

DDR3 :在 DDR2 的基础上将预取(prefetch)提高到 8bit,因此又获得了比 DDR2 高一倍的传输速率。

2.ROM 系列

2.1 ROM(只读存储器)

最初,ROM是一种非易失性存储器,一旦数据被写入,通常就不能被随意擦除或修改。

随着技术的进步,现代计算机系统中的“ROM”通常指的是非易失性存储器,但不再严格限制为只读。闪存技术,如NAND Flash和NOR Flash,允许多次擦除和重写数据。因此,现代的“ROM”更灵活,可以用于存储可更新的固件和操作系统。

2.2 Mask ROM(掩模只读存储器)

Mask ROM是一种固化数据的只读存储器,其内容在制造时由芯片制造商预设。由于其固定性,无法被用户修改,通常用于存储固件和基本的系统软件。

2.3 PROM(可编程只读存储器)

PROM允许用户一次性编程,通过烧录数据来定制存储内容。一旦编程完成,数据将永久存储在其中。PROM在一些应用中提供了更大的灵活性。

2.4 EPROM(可擦写可编程只读存储器)

EPROM具有擦写功能,擦除操作需要使用紫外线,然后重新编程。尽管这种过程有一定的繁琐性,但EPOM在一些特殊应用中仍然有其独特的价值。

EPROM常用于嵌入式系统中,用于存储固件或引导程序。

2.5 EEPROM(电可擦写可编程只读存储器)

EEPROM不需要紫外线,通过电信号就能实现擦写操作。这使得EEPROM更加灵活,可在系统运行时进行修改。它常用于存储配置信息和小规模的数据。

2.6 Flash存储器>NAND Flash & NOR Flash

Flash存储器结合了高密度和可擦写的优势,广泛应用于移动设备、存储卡和固态硬盘等领域。它以块的形式擦除,相对于EEPROM而言,Flash存储器的擦写速度更快。Flash又分为NAND Flash和NOR Flash

NAND Flash

NAND Flash以块(Block)的形式组织数据,每个块包含多个页面(Page),而每个页面包含多个字节。数据是以页为单位进行读写和擦除。NAND Flash的寿命较长,但其擦写次数有限,因此适用于需要大容量、高速度、相对较低擦写次数的应用场景

常用于大容量、高性能的存储需求,例如固态硬盘(SSD)、USB驱动器、SD卡、eMMC等。

NOR Flash

NOR Flash以字节为单位进行寻址,具有直接访问任意字节的能力,不需要通过块擦除。这使得它更适用于随机读取。NOR Flash的寿命通常较长,适用于需要频繁擦写和相对较低容量的应用。

常用于嵌入式系统、固件存储、引导代码等场景。

3.MMDC控制器

Chapter 35 Multi Mode DDR Controller (MMDC)
MMDC 是 I.MX6U的内存控制器,MMDC 是一个多模的 DDR 控制器,可以连接 16 位宽的 DDR3/DDR3L、16 位宽的 LPDDR2,MMDC 是一个可配置、高性能的 DDR 控制器。
MMDC 外设包含一个内核(MMDC_CORE)和 PHY(MMDC_PHY),内核和 PHY 的功能如下:

MMDC 内核:内核负责通过 AXI 接口与系统进行通信、DDR 命令生成、DDR 命令优化、
读/写数据路径。
·
MMDC PHY:PHY 负责时序调整和校准,使用特殊的校准机制以保障数据能够在 400MHz
被准确捕获。

因为DDR 对于硬件要求非常严格,因此 DDR 的引脚都是独立的,一般没有复用功能,只做为 DDR引脚使用因此就不存在所谓的 DDR 引脚复用配置,只需要设置 DDR 引脚的电气属性即可,注意,DDR 引脚的电气属性寄存器和普通的外设引脚电气
属性寄存器不同!

I.MX6U 的 DDR 或者 MDDC 的时钟频率为 400MHz,

在这里插入图片描述①、pre_periph2 时钟选择器,也就是 periph2_clkd 的前级选择器,CBCMR 的PRE_PERIPH2_CLK_SEL 位(bit22:21)来控制。当 PRE_PERIPH2_CLK_SEL 为 0x1 的时候选中 PLL2_PFD2 396MHz(约等于 400MHz) 为pre_periph2 时钟源。

②、periph2_clk 时钟选择器,由 CBCDR 寄存器的 PERIPH2_CLK_SEL 位(bit26)来控制。将 PERIPH2_CLK_SEL 设 置为 0 ,也 就是 选择pll2_main_clk 作为 periph2_clk 的时钟源,因此 periph2_clk=PLL2_PFD0=396MHz。

③、最后就是分频器,由 CBCDR 寄存器的 FABRIC_MMDC_PODF 位(bit5:3)设置分频值,可设置 0~7,分别对应 1~8 分频,要配置 MMDC 的时钟源为 396MHz,那么此处就要设置为 1分频,因此 FABRIC_MMDC_PODF=0。

4.DDR3L初始化与测试

4.1 初始化

通过excel 表可以设置板子的 DDR 信息,生成一个.inc 结尾的 DDR 初始化脚本文件。这个.inc 文件就包含了 DDR 的初始化信息。ddr_stress_tester工具会加载.inc 表里面的 DDR 初始化信息,然后通过 USB OTG 接口向板子下载DDR 相关的测试代码,包括初始化代码。

4.2 校准

对此工具进行简单的设置,即可开始 DDR 测试,一般要先做校准,因为不同的 PCB其结构肯定不同,必须要做一次校准,校准完成以后会得到两个寄存器对应的校准值,我们需要用这个新的校准值来重新初始化 DDR。经过校准一会 DDR3L 就会工作到最佳状态。


十三、RGBLCD显示屏

LCD 全称是 Liquid Crystal Display,也就是液晶显示器。

1.分辨率

提起 LCD 显示器,我们都会听到 720P、1080P、2K 或 4K 这样的字眼,这个就是 LCD 显示器分辨率。LCD 显示器都是由一个一个的像素点组成,1080P 的意思就是一个 LCD 屏幕上的像素数量是
1920*1080 个,也就是这个屏幕一列 1080 个像素点,一共 1920 列。

2K 就是 25601440 个像素点,4K 是38402160 个像素点。很明显,在 LCD 尺寸不变的情况下,分辨率越高越清晰。同样的,分辨率不变的情况下,LCD 尺寸越小越清晰。

2.像素格式

一个像素点就相当于一个 RGB 小灯,通过控制 R、G、B 这三种颜色的亮度就可以显示出各种各样的色彩。一般一个 R、G、B 这三部分分别使用 8bit 的数据,那么一个像素点就是 8bit*3=24bit,也就是说一个像素点3 个字节,这种像素格式称为 RGB888。如果再加入 8bit 的 Alpha(透明)通道的话一个像素点就 32bit,也就是 4 个字节,这种像素格式称为 ARGB8888,其中 bit31~bit24 是 Alpha 通道,bit23~bit16 是RED 通道,bit15~bit14 是 GREEN 通道,bit7~bit0 是 BLUE 通道。

3.LCD 时间参数与时序

如果将 LCD 显示一帧图像的过程想象成绘画,那么在显示的过程中就是用一根“笔”在不同的像素点画上不同的颜色。这根笔按照从左至右、从上到下的顺序扫描每个像素点,并且在像素画上对应的颜色,当画到最后一个像素点的时候一幅图像就绘制好了。假如一个 LCD 的分辨率为 1024*600,那么其扫描如图 24.1.1.5 所示:
在这里插入图片描述

3.1 行显示时序

在这里插入图片描述图 24.1.1.6 就是 RGB LCD 的行显示时序,我们来分析一下其中重要的几个参数:

HSYNC:行同步信号,当产生此信号的话就表示开始显示新的一行了,所以此信号都是在图 24.1.1.5 的最左边。
·
HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC
信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
·
HBP:有些地方叫做 thb,缓冲延时时间,术语叫做行同步信号后肩,单位是 CLK。
·
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。
·
HFP:有些地方叫做 thf,缓冲延时时间,术语叫做行同步信号前肩,单位是 CLK。

当 HSYNC 信号发出以后,需要等待 HSPW+HBP 个 CLK 时间才会接收到真正有效的像素数据。当显示完一行数据以后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以显示一行所需要的时间就是:HSPW + HBP + HOZVAL + HFP。

3.2 帧显示时序

在这里插入图片描述图 24.1.1.7 就是 RGB LCD 的帧显示时序,我们来分析一下其中重要的几个参数:

VSYNC:帧同步信号,产生此信号的话就表示开始显示新的一帧图像了,所以此信号在图 24.1.1.5 的左上角。
·
VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间。
·
VBP:有些地方叫做 tvb,缓冲延时时间,术语叫做帧同步信号后肩,单位为 1 行的时间。
·
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。
·
VFP:有些地方叫做 tvf,缓冲延时时间,术语叫做帧同步信号前肩,单位为 1 行的时间。

显示一帧所需要的时间就是:VSPW+VBP+LINE+VFP 个行时间,最终的计算公式:
T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
注意:因为帧显示时序中的信号单位都是th,即一行所需要的时间,故两者之间是乘法

4.LCD时钟>像素时钟配置>PLL5配置

正点原子的不同屏幕参数不同,相应的配置也不同,像素时钟自然也不同。
像素时钟就是 RGB LCD 的时钟信号,以 ATK4384 这款屏幕为例,显示一帧图像所需要的时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
=(3+32+480+13)*(48+88+800+40)
= 528 * 97
= 515328

显示一帧图像需要 515328个时钟数,那么显示 60 帧就是:515328* 60 = 30,919,680≈31M,所以像素时钟就是 31MHz。

在这里插入图片描述PLL5 also referenced as Video PLL
PLL5 专给 VIDEO 使用,根据时钟树可以将LCDIF1_CLK_ROOT连接到PLL5,但还需要配置PLL5本身。
18.5.1.3.4 Audio / Video PLL可知,
PLL output frequency = Fref * (DIV_SELECT + NUM/DENOM)
PLL5 频 率 设 置 涉 及 到 四 个 寄 存 器 : CCM_PLL_VIDEO 、 CCM_PLL_VIDEO_NUM 、CCM_PLL_VIDEO_DENOM 、 CCM_MISC2 。 其 中 CCM_PLL_VIDEO_NUM 和CCM_PLL_VIDEO_DENOM 这两个寄存器是用于小数分频的,我们这里为了简单不使用小数分频,因此这两个寄存器设置为 0。
LL5 的时钟计算公式如下:
PLL5_CLK = OSC24M * (loopDivider + (denominator / numerator)) / postDivider/ videoDiv
官方公式里面没有postDivider和videoDiv,因为postDivider,videoDiv就是后面的分频器。

不使用小数分频的话 PLL5 时钟计算公式就可以简化为 :
PLL5_CLK = OSC24M * loopDivider / postDivider/ videoDiv

loopDivider :CCM_ANALOG_PLL_VIDEOn 的DIV_SELECT
postDivider:CCM_ANALOG_PLL_VIDEOn 的POST_DIV_SELECT
denominator :CCM_PLL_VIDEO_DENOM
numerator:CCM_PLL_VIDEO_NUM
videoDiv:CCM_ANALOG_MISC2的VIDEO_DIV

代码中注释了每个寄存器控制那些参数,默认postDivider = videoDiv = 1,只需要设置loopDivider 的值。

/* 先初始化video pll 
   * VIDEO PLL = OSC24M * (loopDivider + (denominator / numerator)) / postDivider
	 *不使用小数分频器,因此denominator和numerator设置为0
	 */
CCM_ANALOG->PLL_VIDEO_NUM = 0;		/* 不使用小数分频器 */
CCM_ANALOG->PLL_VIDEO_DENOM = 0;	

/*
    * PLL_VIDEO寄存器设置
    * bit[13]:    1   使能VIDEO PLL时钟
    * bit[20:19]  2  设置postDivider为1分频
    * bit[6:0] : 32  设置loopDivider寄存器
 */
CCM_ANALOG->PLL_VIDEO =  (2 << 19) | (1 << 13) | (loopDiv << 0); 

/*
    * MISC2寄存器设置
    * bit[31:30]: 0  VIDEO的post-div设置,时钟源来源于postDivider,1分频
 */
CCM_ANALOG->MISC2 &= ~(3 << 30);
CCM_ANALOG->MISC2 = 0 << 30;

在这里插入图片描述

设置好PLL5后,通过相应的寄存器控制位将LCDIF1_CLK_ROOT连接到PLL5,还有两个分频器LCDIF_PRE分频和LCDIF分频需要配置。

/* LCD时钟源来源与PLL5,也就是VIDEO           PLL  */
	CCM->CSCDR2 &= ~(7 << 15);  	
	CCM->CSCDR2 |= (2 << 15);			/* 设置LCDIF_PRE_CLK使用PLL5 */

	/* 设置LCDIF_PRE分频 */
	CCM->CSCDR2 &= ~(7 << 12);		
	CCM->CSCDR2 |= (prediv - 1) << 12;	/* 设置分频  */

	/* 设置LCDIF分频 */
	CCM->CBCMR &= ~(7 << 23);					
	CCM->CBCMR |= (div - 1) << 23;				

	/* 设置LCD时钟源为LCDIF_PRE时钟 */
	CCM->CSCDR2 &= ~(7 << 9);					/* 清除原来的设置		 	*/
	CCM->CSCDR2 |= (0 << 9);					/* LCDIF_PRE时钟源选择LCDIF_PRE时钟 */

5.显存

采用 ARGB8888 格式,一个像素需要 4 个字节的内存来存放像素数据,那么 1024600 分辨率就需要 1024600*4=2457600B≈2.4MB 内存。但是 RGB LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 RGBLCD 屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。

6.eLCDIF 接口与RGBLCD相关寄存器

相关寄存器直接看正点原子的开发指南。因为太多太杂了。

eLCDIF 是 I.MX6U 自带的液晶屏幕接口,用于连接 RGB LCD 接口的屏幕。
eLCDIF 支持三种接口:MPU 接口、VSYNC 接口和 DOTCLK 接口。
DOTCLK 接口就是用来连接 RGB LCD 接口屏幕的, 它包括 VSYNC、HSYNC、DOTCLK和 ENABLE(可选的)这四个信号,这样的接口通常被称为 RGB 接口.

RGBLCD寄存器初始化

/* 初始化ELCDIF的CTRL寄存器
     * bit [31] 0 : 停止复位
     * bit [19] 1 : 旁路计数器模式
     * bit [17] 1 : LCD工作在dotclk模式
     * bit [15:14] 00 : 输入数据不交换
     * bit [13:12] 00 : CSC不交换
     * bit [11:10] 11 : 24位总线宽度
     * bit [9:8]   11 : 24位数据宽度,也就是RGB888
     * bit [5]     1  : elcdif工作在主模式
     * bit [1]     0  : 所有的24位均有效
	 */
	 LCDIF->CTRL |= (1 << 19) | (1 << 17) | (0 << 14) | (0 << 12) |
	 				(3 << 10) | (3 << 8) | (1 << 5) | (0 << 1);
	/*
     * 初始化ELCDIF的寄存器CTRL1
     * bit [19:16]  : 0X7 ARGB模式下,传输24位数据,A通道不用传输
	 */	
	 LCDIF->CTRL1 = 0X7 << 16; 

	 /*
      * 初始化ELCDIF的寄存器TRANSFER_COUNT寄存器
      * bit [31:16]  : 高度
      * bit [15:0]   : 宽度
	  */
	LCDIF->TRANSFER_COUNT  = (tftlcd_dev.height << 16) | (tftlcd_dev.width << 0);

	/*
     * 初始化ELCDIF的VDCTRL0寄存器
     * bit [29] 0 : VSYNC输出
     * bit [28] 1 : 使能ENABLE输出
     * bit [27] 0 : VSYNC低电平有效
     * bit [26] 0 : HSYNC低电平有效
     * bit [25] 0 : DOTCLK上升沿有效
     * bit [24] 1 : ENABLE信号高电平有效
     * bit [21] 1 : DOTCLK模式下设置为1
     * bit [20] 1 : DOTCLK模式下设置为1
     * bit [17:0] : vsw参数
	 */
	LCDIF->VDCTRL0 = 0;	//先清零
	if(lcdid == ATKVGA) {   //VGA需要特殊处理
		LCDIF->VDCTRL0 = (0 << 29) | (1 << 28) | (0 << 27) |
					 (0 << 26) | (1 << 25) | (0 << 24) |
					 (1 << 21) | (1 << 20) | (tftlcd_dev.vspw << 0);
	} else {
		LCDIF->VDCTRL0 = (0 << 29) | (1 << 28) | (0 << 27) |
					 (0 << 26) | (0 << 25) | (1 << 24) |
					 (1 << 21) | (1 << 20) | (tftlcd_dev.vspw << 0);
	}

	/*
	 * 初始化ELCDIF的VDCTRL1寄存器
	 * 设置VSYNC总周期
	 */  
	LCDIF->VDCTRL1 = tftlcd_dev.height + tftlcd_dev.vspw + 
	                tftlcd_dev.vfpd + tftlcd_dev.vbpd;  //VSYNC周期
	 
	 /*
	  * 初始化ELCDIF的VDCTRL2寄存器
	  * 设置HSYNC周期
	  * bit[31:18] :hsw
	  * bit[17:0]  : HSYNC总周期
	  */ 
	LCDIF->VDCTRL2 = (tftlcd_dev.hspw << 18) | 	
								(tftlcd_dev.width + tftlcd_dev.hspw + tftlcd_dev.hfpd + tftlcd_dev.hbpd);

	/*
	 * 初始化ELCDIF的VDCTRL3寄存器
	 * 设置HSYNC周期
	 * bit[27:16] :水平等待时钟数
	 * bit[15:0]  : 垂直等待时钟数
	 */ 
	LCDIF->VDCTRL3 = ((tftlcd_dev.hbpd + tftlcd_dev.hspw) << 16) | 
								(tftlcd_dev.vbpd + tftlcd_dev.vspw);

	/*
	 * 初始化ELCDIF的VDCTRL4寄存器
	 * 设置HSYNC周期
	 * bit[18] 1 : 当使用VSHYNC、HSYNC、DOTCLK的话此为置1
	 * bit[17:0]  : 宽度
	 */ 
	
	LCDIF->VDCTRL4 = (1<<18) | (tftlcd_dev.width);

	/*
     * 初始化ELCDIF的CUR_BUF和NEXT_BUF寄存器
     * 设置当前显存地址和下一帧的显存地址
	 */
	LCDIF->CUR_BUF = (unsigned int)tftlcd_dev.framebuffer;
	LCDIF->NEXT_BUF = (unsigned int)tftlcd_dev.framebuffer;

7.LCD显示图案案例>设置显存

显存内的每个unsigned int都表示一个ARGB数值,也就是一个lcd灯的颜色, 通过设置显存元素的大小,直接可以控制每个lcd的颜色。

直接赋值给显存,LCD就更新???类似触发操作吗???

/*
 * @description		: 画点函数 
 * @param - x		: x轴坐标
 * @param - y		: y轴坐标
 * @param - color	: 颜色值
 * @return 			: 无
 */
inline void lcd_drawpoint(unsigned short x,unsigned short y,unsigned int color)
{ 
  	*(unsigned int*)((unsigned int)tftlcd_dev.framebuffer + 
		             tftlcd_dev.pixsize * (tftlcd_dev.width * y+x))=color;
}


/*
 * @description		: 读取指定点的颜色值
 * @param - x		: x轴坐标
 * @param - y		: y轴坐标
 * @return 			: 读取到的指定点的颜色值
 */
inline unsigned int lcd_readpoint(unsigned short x,unsigned short y)
{ 
	return *(unsigned int*)((unsigned int)tftlcd_dev.framebuffer + 
		   tftlcd_dev.pixsize * (tftlcd_dev.width * y + x));
}


十四、RTC>SNVS

1.相关介绍

I.MX6U 系列的 RTC 是在 SNVS 里面,也就是《I.MX6UL 参考手册》的第46 章“Chapter 46 Secure Non-Volatile Storage(SNVS)”。

NVS 直译过来就是安全的非易性存储,SNVS 里面主要是一些低功耗的外设,包括一个安全的实时计数器(RTC)、一个单调计数器(monotonic counter)和一些通用的寄存器,本章我们肯定只使用实时计数器(RTC)。SNVS 里面的外设在芯片掉电以后由电池供电继续运行.

SNVS 有两部分:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域(SNVS_LP)系统主电源断电以后 SNVS_HP 也会断电,但是在后备电源支持下,SNVS_LP 是不会断电的,而且 SNVS_LP是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着。

其实不管是 SNVS_HP 里面的 RTC,还是 SNVS_LP 里面的 SRTC,其本质就是一个定时器,和 EPIT 定时器一样,只要给它提供时钟,它就会一直运行。SRTC 需要外界提供一个 32.768KHz 的时钟。寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR 保存着秒数,直接读取这两个寄存器的值就知道过了多长时间了。一般以 1970 年 1 月 1 日为起点,加上经过的秒数即可得到现在的时间和日期.

SRTC 也是带有闹钟功能的,可以在寄存器 SNVS_LPAR 中写入闹钟时间值,当时钟值和闹钟值匹配的时候就会产生闹钟中断,要使用时钟功能的话还需要进行一些设置.

2.RTC寄存器>只包括LP下读取操作

46.7.3 SNVS_HP Command (HPCOMR)
46.7.16 SNVS_LP Control (LPCR)
46.7.22 SNVS_LP Secure Real Time Counter MSB (LPSRTCMR)
46.7.23 SNVS_LP Secure Real Time Counter LSB (LPSRTCLR)
正点原子文档与下面的解释不同

SNVS_HPCOMR 寄存器
这个寄存器我们只用到了位:NPSWA_EN(bit31),这个位是非特权软件访问控制位,如果非特权软件要访问 SNVS 的话此位必须为 1。
·
SNVS_LPCR 寄存器
此寄存器也只用到了一个位:SRTC_ENV(bit0),此位为 1 的话就使能 STC 计数器
·
SNVS_SRTCMR寄存器
SNVS_SRTCMR的 bit14:0 这 15 位是 SRTC 计数器的高 15 位,SRTC 计数器为47 位。
·
SNVS_SRTCLR 寄存器
SNVS_SRTCLR 的 bit31:0这 17 位是 SRTC 计数器的低 32位,SRTC 计数器为47 位。

注意:SNVS_SRTCMR 和 SNVS_SRTCLR这两个counter 存放的是时钟数, 32.768KHz 外部时钟每跳动一次,时钟数就加1,即counter内每32768个时钟数代表 1 秒,32768= 1000 0000 0000 0000 十转二进制,也就是SNVS_SRTCLR 需要右移15位才是 秒数,SRTC 计数器为47 位,右移15位就剩32位了 ,故可以用unsigned int 来存放秒。

读取 SNVS_SRTCMR 和 SNVS_SRTCLR 这两个寄存器就可以得到正确的时间,如果要调整时间的话也是向这两个寄存器写入要设置的时间值对应的时钟数(不是秒数)就可以了,但是要修改这两个寄存器的话要先关闭 SRTC。

3.RTC读取案例

①RCT初始化

/* 
 * 描述:初始化RTC
 */
void rtc_init(void)
{

	/*
     * 设置HPCOMR寄存器
     * bit[31] 1 : 允许访问SNVS寄存器,一定要置1
     * bit[8]  1 : 此位置1,需要签署NDA协议才能看到此位的详细说明,
     *             这里不置1也没问题
	 */
	SNVS->HPCOMR |= (1 << 31) | (1 << 8);
    rtc_enable();	//使能RTC
}

/*
 * 描述: 开启RTC
 */
void rtc_enable(){
	/*
	 * LPCR寄存器bit0置1,使能RTC
 	 */
    SNVS->LPCR |= (1<<0);
    while((SNVS->LPCR & 0x01) == 0);
}

②读取RTC时钟值

/*
 * @description	: 获取RTC当前秒数
 * @param 		: 无
 * @return 		: 当前秒数 
 */
unsigned int rtc_getseconds(void)
{
	unsigned int seconds = 0;
	seconds = (SNVS->LPSRTCMR << 17) | (SNVS->LPSRTCLR >> 15);
	return seconds;
}

③向RCT counter写入值

/*
 * @description		: 设置时间和日期
 * @param - datetime: 要设置的日期和时间
 * @return 			: 无
 */
void rtc_setdatetime(struct rtc_datetime *datetime)
{
	
	unsigned int seconds = 0;
	unsigned int tmp = SNVS->LPCR; 
	
	rtc_disable();	/* 设置寄存器HPRTCMR和HPRTCLR的时候一定要先关闭RTC */

	
	/* 先将时间转换为秒         */
	seconds = rtc_coverdate_to_seconds(datetime);
	
	SNVS->LPSRTCMR = (unsigned int)(seconds >> 17); /* 设置高15位 */
	SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 设置地17位 */

	/* 如果此前RTC是打开的在设置完RTC时间以后需要重新打开RTC */
	if (tmp & 0x1)
		rtc_enable();
}

十五、I2C通信

1.I2C介绍

2C 是很常见的一种总线协议,I2C 是 NXP 公司设计的,I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据线需要接上拉电阻,一般是 4.7K,总线空闲的时候 SCL 和 SDA 处于高电平。I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。

2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C设备了,一个 I2C 总线连接多个 I2C 设备如图 26.1.1.1 所示:

在这里插入图片描述

2.通信时序

2.1 起始位

顾名思义,也就是 I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始进行 I2C 通信了。在 SCL 为高电平的时候,SDA 出现下降沿就表示为起始位,如图 26.1.1.2 所示:
在这里插入图片描述

2.2 停止位

停止位就是停止 I2C 通信的标志位,和起始位的功能相反。在 SCL 位高电平的时候,SDA出现上升沿就表示为停止位,如图 26.1.1.3 所示

在这里插入图片描述

2.3 数据传输

I2C 总线在数据传输的时候要保证在 SCL 高电平期间,SDA 上的数据稳定,因此 SDA 上的数据变化只能在 SCL 低电平期间发生,如图 26.1.1.4 所示:
在这里插入图片描述

2.4 应答信号

当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是等到 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。

2.5 I2C 写时序

主机通过 I2C 总线与从机之间进行通信不外乎两个操作:写和读,I2C 总线单字节写时序如图 26.1.1.5 所示:
在这里插入图片描述图 26.1.1.5 就是 I2C 写时序,我们来看一下写时序的具体步骤:
1)、开始信号。
2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。
3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。
4)、从机发送的 ACK 应答信号。
5)、重新发送开始信号。
6)、发送要写写入数据的寄存器地址。
7)、从机发送的 ACK 应答信号。
8)、发送要写入寄存器的数据。
9)、从机发送的 ACK 应答信号。
10)、停止信号。

2.6 I2C 读时序

I2C 总线单字节读时序如图 26.1.1.6 所示:
在这里插入图片描述
I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要读取的寄存器值,我们具体来看一下这几步。
1)、主机发送起始信号。
2)、主机发送要读取的 I2C 从设备地址。
3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。
4)、从机发送的 ACK 应答信号。
5)、重新发送 START 信号。
6)、主机发送要读取的寄存器地址。
7)、从机发送的 ACK 应答信号。
8)、重新发送 START 信号。
9)、重新发送要读取的 I2C 从设备地址。
10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。
11)、从机发送的 ACK 应答信号。
12)、从 I2C 器件里面读取到的数据。
13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。
14)、主机发出 STOP 信号,停止 I2C 通信。

3.I2C相关寄存器

I2Cx_IADR :31.7.1 I2C Address Register (I2Cx_IADR)

寄存器 I2Cx_IADR 只有 ADR(bit7:1)位有效,用来保存 I2C 从设备地址数据。当我们要访问某个 I2C 从设备的时候就需要将其设备地址写入到 ADR 里面。

I2Cx_IFDR :31.7.2 I2C Frequency Divider Register (I2Cx_IFDR)

寄存器 I2Cx_IFDR 也只有 IC(bit5:0)这个位,用来设置 I2C 的波特率,I2C 的时钟源可以选择 IPG_CLK_ROOT=66MHz,通过设置 IC 位既可以得到想要的 I2C 波特率。不像其他外设的分频设置一样可以随意设置,比如现在 I2C 的时钟源为 66MHz,我们要设置 I2C 的波特率为 100KHz,那么 IC 就可以设置为 0X15,也就是 640 分频。66000000/640=103.125KHz≈100KHz。

寄存器 I2Cx_I2CR 的各位含义如下:31.7.3 I2C Control Register (I2Cx_I2CR)

IEN(bit7):I2C 使能位,为 1 的时候使能 I2C,为 0 的时候关闭 I2C。
IIEN(bit6):I2C 中断使能位,为 1 的时候使能 I2C 中断,为 0 的时候关闭 I2C 中断。
MSTA(bit5):主从模式选择位,设置 IIC 工作在主模式还是从模式,为 1 的时候工作在主模式,为 0 的时候工作在从模式。
MTX(bit4):传输方向选择位,用来设置是进行发送还是接收,为 0 的时候是接收,为 1 的时候是发送。
TXAK(bit3):传输应答位使能,为 0 的话发送 ACK 信号,为 1 的话发送 NO ACK 信号。
RSTA(bit2):重复开始信号,为 1 的话产生一个重新开始信号。

寄存器 I2Cx_I2SR 的各位含义如下:31.7.4 I2C Status Register (I2Cx_I2SR)

ICF(bit7):数据传输状态位,为 0 的时候表示数据正在传输,为 1 的时候表示数据传输完成。
IAAS(bit6):当为 1 的时候表示 I2C 地址,也就是 I2Cx_IADR 寄存器中的地址是从设备地址。
IBB(bit5):I2C 总线忙标志位,当为 0 的时候表示 I2C 总线空闲,为 1 的时候表示 I2C 总线忙。
IAL(bit4):仲裁丢失位,为 1 的时候表示发生仲裁丢失。
SRW(bit2):从机读写状态位,当 I2C 作为从机的时候使用,此位用来表明主机发送给从机的是读还是写命令。为 0 的时候表示主机要向从机写数据,为 1 的时候表示主机要从从机读取数据。
IIF(bit1):I2C 中断挂起标志位,当为 1 的时候表示有中断挂起,此位需要软件清零。
RXAK(bit0):应答信号标志位,为 0 的时候表示接收到 ACK 应答信号,为 1 的话表示检测到 NO ACK 信号。

I2Cx_I2DR: 31.7.5 I2C Data I/O Register (I2Cx_I2DR)

寄存器I2Cx_I2DR,这是 I2C 的数据寄存器,此寄存器只有低 8 位有效,当要发送数据的时候将要发送的数据写入到此寄存器,如果要接收数据的话直接读取此寄存器即可得到接收到的数据。

4.I2C使用案例

步骤:

1、初始化相应的 IO
初始化 I2C1 相应的 IO,设置其复用功能,如果要使用 mlx90614中断功能的话,还需要设置 mlx90614的中断 IO。
·
2、初始化 I2C1
初始化 I2C1 接口,设置波特率。
·
3、初始化 mlx90614
初始化 mlx90614,读取 mlx90614的数据。

4.1 初始化相应的 IO

初始化I2C2_SCL和I2C2_SDA

31.2 External Signals中写到:
Inputs of I2Cn_SCL and I2Cn_SDA also need to be manually enabled by setting the SION bit in the IOMUX after the corresponding PADs are selected as I2C function.
故,IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL, 1),设置为1.

/* 1、IO初始化,配置I2C IO属性	
     * I2C1_SCL -> UART4_TXD
     * I2C1_SDA -> UART4_RXD
     */
	IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL, 1);
	IOMUXC_SetPinMux(IOMUXC_UART5_RX_DATA_I2C2_SDA, 1);

	/* 
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 1 默认47K上拉
	 *bit [13]: 1 pull功能
	 *bit [12]: 1 pull/keeper使能 
	 *bit [11]: 0 关闭开路输出
	 *bit [7:6]: 10 速度100Mhz
	 *bit [5:3]: 110 驱动能力为R0/6
	 *bit [0]: 1 高转换率
	 */
	IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x70B0);
	IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0X70B0);

4.2 初始化I2Cx

/* 1、配置I2C */
	base->I2CR &= ~(1 << 7); /* 要访问I2C的寄存器,首先需要先关闭I2C */

    /* 设置波特率为100K
     * I2C的时钟源来源于IPG_CLK_ROOT=66Mhz
 	 * IC2 时钟 = PERCLK_ROOT/dividison(IFDR寄存器)
	 * 设置寄存器IFDR,IFDR寄存器参考IMX6UL参考手册P1260页,表29-3,
	 * 根据表29-3里面的值,挑选出一个还是的分频数,比如本例程我们
	 * 设置I2C的波特率为100K, 因此当分频值=66000000/100000=660.
	 * 在表29-3里面查找,没有660这个值,但是有640,因此就用640,
	 * 即寄存器IFDR的IC位设置为0X15
	 */
	base->IFDR = 0X15 << 0;

    /*
     * 设置寄存器I2CR,开启I2C
     * bit[7] : 1 使能I2C,I2CR寄存器其他位其作用之前,此位必须最先置1
	 */
	base->I2CR |= (1<<7);

/*
 *函数声明
 */
void i2c_init(I2C_Type *base);
unsigned char i2c_master_start(I2C_Type *base, unsigned char address, enum i2c_direction direction);
unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address,  enum i2c_direction direction);
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status);
unsigned char i2c_master_stop(I2C_Type *base);
void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size);
void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size);
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer);

十六、SPI通信

SPI,SPI 全称是 Serial Perripheral Interface,也就是串行外围设备接口,是一种高速、全双工的同步通信总线,SPI 时钟频率相比 I2C 要高很多,最高可以工作在上百 MHz。SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要4 根线,但是也可以使用三根线(单向传输)。这四根线如下:

①、CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI 主机不需要发送从机设备,直接将相应的从机设备片选信号拉低即可。
②、SCK,Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。
③、MOSI/SDO,Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线只能用于主机向从机发送数据,也就是主机输出,从机输入。
④、MISO/SDI,Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只能用户从机向主机发送数据,也就是主机输入,从机输出。

SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从设备的结构如图 27.1.1.1 所示:
在这里插入图片描述IO配置见IO章节


十七、多点电容触摸屏

ATK-4384这款屏幕其实是由 TFT LCD+触摸屏组合起来的。底下是 LCD 面板,上面是触摸面板,将两个封装到一起就成了带有触摸屏的 LCD 屏幕。电容触摸屏也是需要一个驱动 IC的,驱动 IC 一般会提供一个 I2C 接口给主控制器,主控制器可以通过 I2C 接口来读取驱动 IC里面的触摸坐标数据。

电容触摸屏部分有 4 个 IO 用于连接主控制器:SCL、SDA、RST 和 INT,SCL 和 SDA 是 I2C 引脚,RST 是复位引脚,INT 是中断引脚。一般通过 INT 引脚来通知主控制器有触摸点按下,然后在 INT 中断服务函数中读取触摸数据。也可以不使用中断功能,采用轮询的方式不断查询是否有触摸点按下.

1.相关寄存器配置见正点原子

用的是GT1151Q,但可以用gt9147的代码,但是有一个问题,需要将 函数 gt9147_init(void) 中的一段代码取消,否则会卡在这里。

// printf("Default Ver:%#x\r\n",(((unsigned short)temp[5]<<8) | temp[6]));   /* 打印固件版本 */

有代码和文档,但是两者结合就无法对应的上。

IO配置见IO章节,主要是IIC 和 输出。

十八、PWM>pwm调节lcd背光

1.imx6u pwm 控制原理

在这里插入图片描述
图 29.1.2 中的各部分功能如下:

①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk、ipg_clk_highfreq 和 ipg_clk_32k。
②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。
③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。
④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。
⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。
⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就会产生中断。
⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出,I.MX6U-ALPHA 开发板的 LCD 背光控制引脚连接在 I.MX6U 的 GPIO1_IO8 上,GPIO1_IO8 可以复用为 PWM1_OUT。

在一个周期内,PWM 从 0X0000 开始计数的时候,PWM 引脚先输出高电平(默认情况下,可以通过配置输出低电平)。采样 FIFO 中保存的采样值会在每个时钟和计数器值进行比较,当采样值和计数器相等的话 PWM 引脚就会改为输出低电平(默认情况下,同样可以通过配置输出高电平)。计数器会持续计数,直到和周期寄存器 PWMx_PWMPR(x=1~8) + 1 的值相等,这样一个周期就完成了.

2.寄存器设置

正点原子的参考手册很详细。

2.1 40.7.1 PWM Control Register (PWMx_PWMCR)

寄存器 PWM1_PWMCR 用到的重要位如下:

FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。设置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空;设置为 1 的时候表示 FIFO 空余位置大于等于 2 的时候 FIFO 为空;设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候FIFO 为空;设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。
STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下PWM 继续工作,为 1 的话表示停止模式下关闭 PWM。
DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下PWM 继续工作,为 1 的话表示休眠模式下关闭 PWM。
WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下PWM 继续工作,为 1 的话表示等待模式下关闭 PWM。
DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下PWM 继续工作,为 1 的话表示调试模式下关闭 PWM。
BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时候不进行字节交换,为 1 的时候进行字节交换。
HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写入采样寄存器的低 16 位中。
POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式,为 0 的时候表示PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。为 1 的时候相反,当为 2 或者 3 的时候 PWM 信号不输出。本章我们设置为 0,也就是一开始输出高电平,当计数器值和采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。
CLKSRC(bit17:16):PWM 时钟源选择,为 0 的话关闭;为 1 的话选择 ipg_clk 为时钟源;为 2 的话选择 ipg_clk_highfreq 为时钟源;为 3 的话选择 ipg_clk_32k 为时钟源。本章我们设置为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。
PRESCALER(bit15:4):分频值,可设置为 0~4095,对应着 1~4096 分频。SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位会自动清零。
REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。可设置 0~3,分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一次。
EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。

/*
   	 * 初始化寄存器PWMCR
   	 * bit[27:26]	: 01  当FIFO中空余位置大于等于2的时候FIFO空标志值位
   	 * bit[25]		: 0  停止模式下PWM不工作
   	 * bit[24]		: 0	  休眠模式下PWM不工作
   	 * bit[23]		: 0   等待模式下PWM不工作
   	 * bit[22]		: 0   调试模式下PWM不工作
   	 * bit[21]		: 0   关闭字节交换
   	 * bit[20]		: 0	  关闭半字数据交换
   	 * bit[19:18]	: 00  PWM输出引脚在计数器重新计数的时候输出高电平
   	 *					  在计数器计数值达到比较值以后输出低电平
   	 * bit[17:16]	: 01  PWM时钟源选择IPG CLK = 66MHz
   	 * bit[15:4]	: 65  分频系数为65+1=66,PWM时钟源 = 66MHZ/66=1MHz
   	 * bit[3]		: 0	  PWM不复位
   	 * bit[2:1]		: 00  FIFO中的sample数据每个只能使用一次。
   	 * bit[0]		: 0   先关闭PWM,后面再使能
	 */
	PWM1->PWMCR = 0;	/* 寄存器先清零 */
	PWM1->PWMCR |= (1 << 26) | (1 << 16) | (65 << 4);

从采样寄存器 PWMx_PWMSAR 读取一次数据,FIFO 里面的数据就会减一,每产生一个周期的 PWM 信号,FIFO 里面的数据就会减一,相当于被用掉了。PWM 有个 FIFO 空中断,当FIFO 为空的时候就会触发此中断,可以在此中断处理函数中向 FIFO 写入数据。所以有 bit[27:26] 和bit[2:1] 的设置。

2.2 40.7.2 PWM Status Register (PWMx_PWMSR)

FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。
CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。
ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。
FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。
FIFOAV(bit2:1):此位记录 FIFO 中的有效数据个数,有效值为 0~4,分别表示 FIFO 中有0~4 个有效数据。

需要判断FIFO为空,故需要bit3.

if(PWM1->PWMSR & (1 << 3)) 	/* FIFO为空中断 */

2.3 40.7.3 PWM Interrupt Register (PWMx_PWMIR)

CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。

使能FIEbit0 即可。

2.4 40.7.4 PWM Sample Register (PWMx_PWMSAR)

采样 FIFO 控制着占空比,而采样 FIFO 里面的值来源于采样寄存器PWMx_PWMSAR,因此相当于 PWMx_PWMSAR 控制着占空比。

此寄存器也是只有低 16 位有效,为采样值。假如我们要设置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为pwm周期/ 2 = 1000 / 2=500。

2.5 40.7.5 PWM Period Register (PWMx_PWMPR)

寄存器 PWM1_PWMPR 只有低 16 位有效,当 PWM 计数器的值等于 PERIOD+1 的时候就会从 0X0000 重新开始计数,开启另一个周期。PWM 的频率计算公式如下:
PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
其中 PCLK 是最终进入 PWM 的时钟频率,假如 PCLK 的频率为 1MHz,现在我们要产生一个频率为 1KHz 的 PWM 信号,那么就可以设置 PERIOD = 1000000 / 1000 – 2 = 998。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值