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系统配置
系统主频的配置
- 设置ARM内核主频为528MHz,设置CACRR寄存器的ARM_PODF位为2分频,然后设置PLL1=1056MHz即可。CACRR的bit3~0为ARM_PODF位,可设置0~7,分别对应1~8分频。应该设置CACRR寄存器的ARM_PODF=1。
- 设置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。
- 设置系统时钟的时候需要给6ULL一个临时的时钟,也就是step_clk。在修改PLL1的时候需要将pll1_sw_clk切换到step_clk上。
- 设置step_clk。Step_clk也有两路来源,由CCSR的step_sel位(bit8)来设置,为0的时候设置step_clk为osc=24MHz。
- 时钟切换成功以后就可以修改PLL1的值。
- 通过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 - 设置CCM_ANALOG_PLL_ARM寄存器的ENABLE位(bit13)为1,也就是使能输出。
- 在切换回PLL1之前,设置置CACRR寄存器的ARM_PODF=1!!切记。
各个PLL时钟的配置
-
PLL2和PLL3。PLL2固定为528MHz,PLL3固定为480MHz。
- 初始化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。
- 初始化PLL3_PFD0~PFD3
其他外设时钟源配置
AHB_CLK_ROOT、PERCLK_CLK_ROOT以及IPG_CLK_ROOT。
-
因为PERCLK_CLK_ROOT和IPG_CLK_ROOT要用到AHB_CLK_ROOT,所以我们要初始化AHB_CLK_ROOT。
- AHB_CLK_ROOT的初始化。 AHB_CLK_ROOT=132MHz。 设置CBCMR寄存器的PRE_PERIPH_CLK_SEL位,设置CBCDR寄存器的PERIPH_CLK_SEL位0。设置CBCDR寄存器的AHB_PODF位为2,也就是3分频,因此396/3=132MHz。
- IPG_CLK_ROOT初始化 设置CBCDR寄存器IPG_PODF=1,也就是2分频。
- PERCLK_CLK_ROOT初始化 设置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提供了开关中断,设置中断优先级。
-
GIC 将众多的中断源分为分为三类:
- SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
- PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
- 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(快速中断)未定义中断 */
-
编写复位中断服务函数,内容如下:
- 关闭I,D Cache和MMU。(内部ROM已关)
- 设置处理器9中工作模式下对应的SP指针(每种工作模式都有自己独有的SP指针)。要使用中断那么必须设置IRQ模式下的SP指针。索性直接设置所有模式下的SP指针。
- 清除bss段。
- 跳到main函数
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。
下图
所以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_UFCR、UART_UBIR和UART_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)
{
/* 1、初始化IO复用
* UART1_RXD -> UART1_TX_DATA
* UART1_TXD -> UART1_RX_DATA
*/
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX,0); /* 复用为UART1_TX */
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX,0); /* 复用为UART1_RX */
/* 2、配置UART1_TX_DATA、UART1_RX_DATA的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]: 0 低转换率
*/
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0x10B0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX,0x10B0);
}
/**
* @brief : 初始化串口1,波特率为115200
* @param : 无
* @return : 无
*/
void uart_init(void)
{
/* 设置UART时钟源频率为80M */
CCM->CSCDR1 &= ~(1 << 6); /* UART时钟源为pll3_80m */
CCM->CSCDR1 &= ~0X3F; /* UART时钟1分频 */
/* 1、初始化串口IO */
uart_io_init();
/* 2、初始化UART1 */
uart_disable(UART1); /* 先关闭UART1 */
uart_softreset(UART1); /* 软件复位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;
/* 使能串口 */
uart_enable(UART1);
}
发送代码
/**
* @brief : 发送一个字符
* @param - c : 要发送的字符
* @return : 无
*/
void putc(unsigned char c)
{
while(((UART1->USR2 >> 3) &0X01) == 0);/* 等待上一次发送完成 */
UART1->UTXD = c & 0XFF; /* 发送数据 */
}
/**
* @brief : 发送一个字符串
* @param - str : 要发送的字符串
* @return : 无
*/
void puts(char *str)
{
char *p = str;
while(*p)
putc(*p++);
}
makefile 修改:
putc和puts编译的时候会提示吧报错,要在Makefile中添加 -fno-builtin
$(SOBJS) : obj/%.o : %.S
$(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
接收代码
未使用中断死等的代码
/**
* @brief : 接收一个字符
* @param : 无
* @return : 接收到的字符
*/
unsigned char getc(void)
{
while((UART1->USR2 & 0x1) == 0);/* 等待接收完成 */
return UART1->URXD; /* 返回接收到的数据 */
}
移植C标准库函数printf
和STM32类似,printf打印数据最后会调用putc函数。
移植stdio时,直接找到格式化函数源码,移植到工程中即可
移植的printf不支持浮点计算和输出!!!!!
DDR3
内存简介
RAM:
随机存储器,可以随时进行读写操作,速度很快,掉电以后数据会丢失。比如内存条、SRAM、SDRAM、DDR 等都是 RAM。RAM 一般用来保存程序数据、中间结果
SRAM:在上电之后就会一直保存数据,成本较高,但是足够快速,可用作Cache。
SDRAM:功耗低,成本低,适合做大容量存储。SDRAM需要时钟线,常见的频率就是100MHz,133MHz,166Mhz,200MHz
DDR3:是 Double Data Rate 3 SDRAM,容量更大,成本更低
DDR3时间参数
- 时间参数
- 传输速率 速率不同,时序不同
- tRCD 行地址、列地址之间的延时
- CL 列地址选通潜伏期
- tRC 两个 ACTIVE 命令,或者 ACTIVE 命令到 REFRESH 命令之间的周期
- tRAS ACTIVE 命令到 PRECHARGE 命令之间的最小时间
I.MX6U MMDC控制器
I.MX6U通过MMDC控制器来与DDR3进行连接通讯
1、多模支持DDR3/DDR3L LPDDR2 x16位
2、MMDC最高支持DDR3频率是400MHz,800MT/S
3、MMDC提供的DDR3连接信号。6ULL给DDR提供了专用的IO,
DDR时钟配置
DDR使用的时钟源为MMDC_CLK_ROOT=PLL2_PFD2=396MHz。
CBCMR寄存器的PRE_PERIPH2_CLK_SE位来选择,也就是bit22:21,设置pre_periph2时钟源,设置为01,也就是PLL2_PFD2作为pre_periph2时钟源。
CBCDR寄存器的PERIPH2_CLK_SEL位,也就是bit26,设置为0,PLL2作为MMDC时钟源,396MHz。
CBCDR寄存器的FABRIC_MMDC_PODF位,bit5:3,设置0,也就是1分频。最终MMDC_CLK_ROOT=396MHz。
DDR3L初始化与测试
ddr_stress_tester配置文件
NXP 提供了一个非常好用的 DDR 初始化工具,叫做 ddr_stress_tester
①、此工具通过 USB OTG 接口与开发板相连接,也就是通过 USB OTG 口进行 DDR 的初始化与测试。
②、此工具有一个默认的配置文件,为 excel 表,通过此表可以设置板子的 DDR 信息,最后生成一个.inc 结尾的 DDR 初始化脚本文件。这个.inc 文件就包含了 DDR 的初始化信息,一般都是寄存器地址和对应的寄存器值。
③、此工具会加载.inc 表里面的 DDR 初始化信息,然后通过 USB OTG 接口向板子下载DDR 相关的测试代码,包括初始化代码。
④、对此工具进行简单的设置,即可开始 DDR 测试,一般要先做校准,因为不同的 PCB其结构肯定不同,必须要做一次校准,校准完成以后会得到两个寄存器对应的校准值,我们需要用这个新的校准值来重新初始化 DDR。
⑤、此工具可以测试板子的 DDR 超频性能,一般认为 DDR 能够以超过标准工作频率10%~20%稳定工作的话就认定此硬件 DDR 走线正常。
⑥、此工具也可以对 DDR 进行 12 小时的压力测试。
进行测试需要弹出SD卡,修改启动方式!!!连接USB
开始测试
首先根据选择的DDR型号来对excel进行配置。
正点原子使用的DDR3型号只需要修改以下红框中的数据,生成inc结尾的文件。
ddr_stress_tester界面如下:
- 选择生成的inc文件
- 选择芯片以及DDR的大小
- ARM的速度选择
- 这里千万不要选,否则会报错
- 下载等待即可
- 校准频率以及开始校准,校准时间较长可能花费10+分钟
- 校准结果,将此结果对应的寄存器以及寄存器值修改到inc中,不存在的寄存器可忽略
- 超频测试,修改校准结果后重启,进行超频测试,可以最大达到多少频率,此处测试时间也较长。当超频到不能使用的频率之后会报错,此时可以找到最大的超频频率,我测试我的为561MHz
RGBLCD
显示原理
LCD 全称是 Liquid Crystal Display,也就是液晶显示器
像素点
不管是液晶屏,还是手机,平板,RGBLCD屏幕他都是有由一个个的彩色小灯构成的。彩色点阵屏每个像素点有三个小灯,红色、绿色和蓝色,也叫做RGB。RGB就是光的三原色。通过调整RGB三种颜色的比例,就可以变成彩色。
分辨率
要想显示文字,图片,视频等等就需要很多个像素点,分辨率说的就是像素点的个数,1080P、720P、2K、4K,8K。1080P=19201080,表示一行有1920个像素点,一列有1080个。显示器有尺寸!24寸,27寸、55寸。尺寸不变的情况下,分辨率越高,显示效果越精细。4K=38402160相当于4个1080P
正点原子的RGB屏幕有:4.3寸480272,800480,7寸的800480和1024600,10.1寸的1280*800。
Iphone4屏幕尺寸是3.5寸,960*640分辨率,PPI=327。PPI 为分辨率除以尺寸
像素格式
将RGB三种颜色进行量化,每种颜色用8bit表示,RGB就需要8-8-8共24bit。可以描述出2^24种颜色16777216=1677万种颜色。现在流行10bit,HDR10,支持HDR效果的10bit面板,RGB10 10 10。
在RGB888的基础上在加上8bit的ALPHA通道,也就是透明通道,ARGB8888=32位。
LCD屏幕接口
RGB格式的屏幕,一般叫做RGB接口屏。
屏幕接口有:MIPI、LVDS、MCU、RGB接口。
正点原子屏幕ID:使用ID可以识别出不同的屏幕,在RGBLCD屏幕上对R7,G7,B7焊接上拉或下拉电阻实现不同的ID。,
正点原子的ALPHA地板RGB屏幕接口用了3个3157模拟开关。原因是防止LCD屏幕上的ID电阻影响到6ULL的启动。
LCD时间参数和LCD时序
行时序
水平:
HSYNC:水平同步信号,行同步信号,当出现HSYNC信号的时候表示新的一行开始显示
1、产生HSYNC信号,表示新的一行开始显示,HSYNC信号得维持一段时间,这个时间叫做HSPW。
2、HSYNC信号完成以后,需要一段时间延时,这段时间叫做HBP。
3、显示1024个像素点的数据,1024个clk。
3、一行像素显示完成以后,到HSYNC下一行信号的产生之间有个延时,叫做HFP。
因此真正显示一行所需的时间计时HSPW+HBP+WIDTH(屏幕水平像素点个数,比如1024)+HFP=20+140+1024+160=1344CLK.
帧时序
垂直:
VSYNC:垂直同步信号,帧同步信号,当出现VSYNC信号的时候表示新的一帧开始显示。
1、VSYNC信号,持续一段时间,位VSPW。
2、VSYNC信号完成以后,需要一段时间,叫做VBP
3、VBP信号结束以后,就是要显示的行数,比如600行,
4、所有的行显示完成以后,一段VFP延时,
DE持续时间为有效数据。
我使用的是正点原子的7寸屏幕(1024*600):
像素时钟
像素时钟就是 RGB LCD 的时钟信号,以 1024*600这款屏幕为例,显示一帧图像所需要的时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
= (3 + 20 + 600 + 12) * (20 + 140 + 1024 + 160)
= 635 * 1344
= 853440。
显示一帧图像需要853440个时钟数,那么显示60帧就是:853440 * 60 = 51206400≈51.2M,
所以像素时钟就是 51.2MHz。
显存
显存:显示存储空间,采用ARGB8888=32bit=4B。这4个字节的数据表示一个像素点的信息,必须得存起来。1024*600=2.5MB。因此需要流出2.5MB的内存给LCD用。
6ULL的RGB接口
LCDIF控制器
LCDIF控制器接口原理
- 使用DOTCLK接口,也就是VSYNC、HSYNC、ENABLE(DE)和DOTCLK(PCLK)
- LCDIF_CTRL寄存器,bit0必须置1,bit1设置数据格式24位全部有效,设置为0。Bit5设置LCDIF接口工作在主机模式下,要置1。Bit9:8设置输入像素格式位24bit,写3。Bit11:10,设置数据传输宽度位24bit,写3。Bit13:12设置数据交换,我们不需要交换设置位0。Bit15:14设置输入数据交换,不交换设置位0。Bit17置1,LCDIF工作在DOTCLK模式下。Bit19必须置1,因为工作在DOTCL模式。Bit31是复位功能必须置0.
- LCDIF_CTRL1寄存器的bit19:16设置位0X7。24位的格式
- LCDIF_TRANSFER_COUNT寄存器的bit15:0是LCD一行的像素数,1024。Bit31:16是LCD一共有多少行,600行
- LCDIF_VDCTRL0寄存器,bit17:0为vspw参数。Bit20设置vsync信号的宽度单位 ,设置为1。Bit21设置为1。Bit24设置ENABLE信号极性,为0的时候是低电平有效,为1是高电平,我们设置为1。Bit25设置CLK信号极性,设置为0.。bit26设置HSYNC信号极性,设置0,低电平有效,bit27设置VSYNC信号极性,设置为0,低电平有效。Bit28设置1,开始ENABLE信号。Bit29设置为0,VSYNC输出
- LCDIF_VDCTRL1寄存器为两个VSYNC信号之间的长度,那就是VSPW+ VBPD+HEIGHT+VFP
- LCDIF_VDCTRL2寄存器bit17:0是两个HSYNC信号之间的长度,那就是hspw+hbp+width+hfp。Bit31:18为hspw
- LCDIF_VDCTRL3寄存器,bit15:0是vbp+vspw。Bit27:16是hbp+hspw。
- LCDIF_VDCTRL4寄存器,bit17:0是一行有多少个像素点,1024
- LCDIF_CUR_BUF,LCD当前缓存,显存首地址。
- LCDIF_NEXT_BUF,LCD下一帧数据首地址。
- LCD IO初始化。
LCD像素时钟的设置
LCD需要一个CLK信号,这个时钟信号是6ULL的CLK引脚发送给RGB LCD的。比如7寸1024*600的屏幕需要51.2MHz的CLK。
LCDIF_CLK_ROOT就是6ULL的像素时钟。PLL5(video PLL)为LCD的时钟源,请查看第一章主频与时钟
PLL5CLK=Fref*(DIV_SELECT + NUM/DENOM)
CCM_ANALOG_PLL_VIDEO的bit6:0,也就是DIV_SELEC位,可选范围27-54。设置PLL_VIDEO寄存器的bit20:19为2,表示1分频。设置CCM_ANALOG_MISC2寄存器的bit31:30为0,也就是VIDOE_DIV为0,为1分频。不使用小数分频器,因此CCM_ANALOG_PLL_VIDEO_NUM=0,再设置CCM_ALALOG_PLL_VIDEO_DENOM=0。
CCM_CSCDR2寄存器的bit17:15,设置LCDIF_PRE_CLK_SEL,选择LCDIF_CLK_ROOT的时钟源,设置为0x2。表示LCDIF时钟源为PLL5。Bit14:12为LCDIF_PRED位,设置前级分频,可以设置07,分别对应18分频。
CCM_CBCMR寄存器,bit25:23为LCDIF_PODF,设置第二级分频,可以设置为07,分别对应18分频。
CCM_CSCDR2寄存器的bit11:9为LCDIF_CLK_SEL,选择LCD CLK的最终时钟源,设置为0,LCDIF的最终时钟源来源于pre-muxed LCDIF clock。
C程序编写
LCD驱动程序编写
LCD初始化
如果使用的正点原子的开发板和RGB屏幕,那么在驱动LCD之前,要先读取屏幕ID。
MMP,这要是自己写,真的累死了,
我觉得了解下流程即可,我也不知道我吧代码贴出来是为了什么
IO复用初始化
/**
* IO引脚: LCD_DATA00 -> LCD_B0
* LCD_DATA01 -> LCD_B1
* LCD_DATA02 -> LCD_B2
* LCD_DATA03 -> LCD_B3
* LCD_DATA04 -> LCD_B4
* LCD_DATA05 -> LCD_B5
* LCD_DATA06 -> LCD_B6
* LCD_DATA07 -> LCD_B7
*
* LCD_DATA08 -> LCD_G0
* LCD_DATA09 -> LCD_G1
* LCD_DATA010 -> LCD_G2
* LCD_DATA011 -> LCD_G3
* LCD_DATA012 -> LCD_G4
* LCD_DATA012 -> LCD_G4
* LCD_DATA013 -> LCD_G5
* LCD_DATA014 -> LCD_G6
* LCD_DATA015 -> LCD_G7
*
* LCD_DATA016 -> LCD_R0
* LCD_DATA017 -> LCD_R1
* LCD_DATA018 -> LCD_R2
* LCD_DATA019 -> LCD_R3
* LCD_DATA020 -> LCD_R4
* LCD_DATA021 -> LCD_R5
* LCD_DATA022 -> LCD_R6
* LCD_DATA023 -> LCD_R7
*
* LCD_CLK -> LCD_CLK
* LCD_VSYNC -> LCD_VSYNC
* LCD_HSYNC -> LCD_HSYNC
* LCD_DE -> LCD_DE
* LCD_BL -> GPIO1_IO08
*/
/**
* @brief : LCD GPIO初始化
* @param : 无
* @return : 无
*/
void lcdgpio_init(void)
{
gpio_pin_config_t gpio_config;
/* 1、IO初始化复用功能 */
IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22,0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23,0);
IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK,0);
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE,0);
IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC,0);
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC,0);
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_GPIO1_IO08,0); /* 背光BL引脚 */
/* 2、配置LCD IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 0 默认22K上拉
*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);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC,0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC,0xB9);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_GPIO1_IO08,0xB9); /* 背光BL引脚 */
/* GPIO初始化 */
gpio_config.direction = kGPIO_DigitalOutput; /* 输出 */
gpio_config.outputLogic = 1; /* 默认关闭背光 */
gpio_init(GPIO1, 8, &gpio_config); /* 背光默认打开 */
gpio_pinwrite(GPIO1, 8, 1); /* 打开背光 */
}
时钟初始化
/**
* @brief : LCD时钟初始化, LCD时钟计算公式如下:
* LCD CLK = 24 * loopDiv / prediv / div
* @param - loopDiv : loopDivider值
* @param - loopDiv : lcdifprediv值
* @param - div : lcdifdiv值
* @return : 无
*/
void lcdclk_init(unsigned char loopDiv, unsigned char prediv, unsigned char div)
{
/* 先初始化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;
/* 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时钟 */
}
LCD控制器的初始化
/**
* @brief : 复位ELCDIF接口
* @param : 无
* @return : 无
*/
void lcd_reset(void)
{
LCDIF->CTRL = 1<<31; /* 强制复位 */
}
/**
* @brief : 结束复位ELCDIF接口
* @param : 无
* @return : 无
*/
void lcd_noreset(void)
{
LCDIF->CTRL = 0<<31; /* 取消强制复位 */
}
/**
* @brief : 使能ELCDIF接口
* @param : 无
* @return : 无
*/
void lcd_enable(void)
{
LCDIF->CTRL |= 1<<0; /* 使能ELCDIF */
}
/**
* @brief : 清屏
* @param - color : 颜色值
* @return : 读取到的指定点的颜色值
*/
void lcd_clear(unsigned int color)
{
unsigned int num;
unsigned int i = 0;
unsigned int *startaddr=(unsigned int*)tftlcd_dev.framebuffer; //指向帧缓存首地址
num=(unsigned int)tftlcd_dev.width * tftlcd_dev.height; //缓冲区总长度
for(i = 0; i < num; i++)
{
startaddr[i] = color;
}
}
/* LCD控制参数结构体 */
typedef struct tftlcd_typedef{
unsigned short height; /* LCD屏幕高度 */
unsigned short width; /* LCD屏幕宽度 */
unsigned char pixsize; /* LCD每个像素所占字节大小 */
unsigned short vspw;
unsigned short vbpd;
unsigned short vfpd;
unsigned short hspw;
unsigned short hbpd;
unsigned short hfpd;
unsigned int framebuffer; /* LCD显存首地址 */
unsigned int forecolor; /* 前景色 */
unsigned int backcolor; /* 背景色 */
}tftlcd_typedef_t;
tftlcd_typedef_t tftlcd_dev={0};
#define LCD_FRAMEBUF_ADDR (0x89000000)
#define LCD_BLACK 0x00000000
#define LCD_WHITE 0x00FFFFFF
/**
* @brief : 始化RGBLCD
* @param : 无
* @return : 无
*/
void lcd_init(void)
{
lcdgpio_init(); /* 初始化IO */
lcdclk_init(32, 3, 5); /* 初始化LCD时钟 */
lcd_reset(); /* 复位LCD */
delayms(10); /* 延时10ms */
lcd_noreset(); /* 结束复位 */
/* TFTLCD参数结构体初始化 */
tftlcd_dev.height = 600;
tftlcd_dev.width = 1024;
tftlcd_dev.pixsize = 4; /* ARGB8888模式,每个像素4字节 */
tftlcd_dev.vspw = 3;
tftlcd_dev.vbpd = 20;
tftlcd_dev.vfpd = 12;
tftlcd_dev.hspw = 20;
tftlcd_dev.hbpd = 140;
tftlcd_dev.hfpd = 160;
tftlcd_dev.framebuffer = LCD_FRAMEBUF_ADDR;
tftlcd_dev.backcolor = LCD_WHITE; /* 背景色为白色 */
tftlcd_dev.forecolor = LCD_BLACK; /* 前景色为黑色 */
/* 初始化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; //先清零
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;
lcd_enable(); /* 使能LCD */
delayms(10);
lcd_clear(LCD_WHITE); /* 清屏 */
}
底层API
只不过是数组内中对应偏移赋值
/**
* @brief : 画点函数
* @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;
}
/**
* @brief : 读取指定点的颜色值
* @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));
}
/**
* @brief : 清屏
* @param - color : 颜色值
* @return : 读取到的指定点的颜色值
*/
void lcd_clear(unsigned int color)
{
unsigned int num;
unsigned int i = 0;
unsigned int *startaddr=(unsigned int*)tftlcd_dev.framebuffer; //指向帧缓存首地址
num=(unsigned int)tftlcd_dev.width * tftlcd_dev.height; //缓冲区总长度
for(i = 0; i < num; i++)
{
startaddr[i] = color;
}
}
/**
* @brief : 以指定的颜色填充一块矩形
* @param - x0 : 矩形起始点坐标X轴
* @param - y0 : 矩形起始点坐标Y轴
* @param - x1 : 矩形终止点坐标X轴
* @param - y1 : 矩形终止点坐标Y轴
* @param - color : 要填充的颜色
* @return : 读取到的指定点的颜色值
*/
void lcd_fill(unsigned short x0, unsigned short y0,
unsigned short x1, unsigned short y1, unsigned int color)
{
unsigned short x, y;
if(x0 < 0) x0 = 0;
if(y0 < 0) y0 = 0;
if(x1 >= tftlcd_dev.width) x1 = tftlcd_dev.width - 1;
if(y1 >= tftlcd_dev.height) y1 = tftlcd_dev.height - 1;
for(y = y0; y <= y1; y++)
{
for(x = x0; x <= x1; x++)
lcd_drawpoint(x, y, color);
}
}
LCD操作API函数编写
API函数在此 只做声明因为实在太多了,内部只不过for循环加条件判断然后调用上个小节底层API的画点函数lcd_drawpoint
void lcd_drawline(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2);
void lcd_draw_rectangle(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2);
void lcd_draw_Circle(unsigned short x0,unsigned short y0,unsigned char r);
//以下API需要有点阵字库
void lcd_showchar(unsigned short x,unsigned short y,unsigned char num,unsigned char size, unsigned char mode);
void lcd_shownum(unsigned short x, unsigned short y, unsigned int num, unsigned char len,unsigned char size);
void lcd_showxnum(unsigned short x, unsigned short y, unsigned int num, unsigned char len, unsigned char size, unsigned char mode);
void lcd_show_string(unsigned short x,unsigned short y,unsigned short width, unsigned short height, unsigned char size,char *p);
RTC
RTC原理详解
6U内部自带到了一个RTC外设,确切的说是SRTC。6U和6ULL的RTC内容在SNVS章节。6U的RTC分为LP和HP。LP叫做SRTC,HP是RTC,但是HP的RTC掉电以后数据就丢失了,即使用了纽扣电池也没用。所以必须要使用LP,也就是SRTC。
SNVS章节有些是跟加密有关的,需要与NXP签订NDA协议才可以拿到。
RTC分为SNVS_LP和SNVS_HP,
如果做产品,建议使用外置RTC芯片,PCF8563。
RTC很类似定时器,外接32.768KHz的晶振,然后就开始计时,RTC使用两个寄存器来保存计数值。
RTC使用很简单,打开RTC,然后RTC就开始工作,我们要做的就是不断地读取RTC计数寄存器,获取时间值,或者向RTC计数器写入时间值,也就是调整时间。
SNVS_HPCOMR的bit31置1,表示所有的软件都可以访问SNVS所有寄存器。Bit8也是和安全有关的,我们置1,也可以不置1.
SNVS_LPCR寄存器,bit0置1,开始SRTC功能。
SNVS_LPSRTCMR的bit14:0为RTC计数寄存器的高15位
SNVS_LPSRTCLR是低32为RTC计数器,与LPSRTCMR共同组成了SRTC计数器,,每1秒数据加1。
6U的RTC模式从1970年1月1日0时0点0分0秒开始计时。(32位秒计时逃不过的2038年)
时间乱码的问题
问题
当我们按照6U的参考手册编写代码,读取SRTC的LPSRTCMR和LPSRTCLR获取时间值的时候,发现按照手册的说法,时间是错误的。我淦
手册上写的:LPSRTCMR是SRTC的高15bit。LPSRTCLR寄存器是SRTC的低32位。RTC计数器是47bit。
问题解决方法
LPSRTCMR作为SRTC计数器的高15位,但是LPSRTCLR寄存器bit31:15作为SRTC计数器的低17位。相当于SRTC的计数器是个32位的。不是47位!
C程序编写
相关API以及结构体
/* 时间日期结构体 */
typedef struct rtc_datetime
{
unsigned short year; /* 范围为:1970 ~ 2099 */
unsigned char month; /* 范围为:1 ~ 12 */
unsigned char day; /* 范围为:1 ~ 31 (不同的月,天数不同).*/
unsigned char hour; /* 范围为:0 ~ 23 */
unsigned char minute; /* 范围为:0 ~ 59 */
unsigned char second; /* 范围为:0 ~ 59 */
}rtc_datetime_t;
/* 相关宏定义 */
#define SECONDS_IN_A_DAY (86400) /* 一天86400秒 */
#define SECONDS_IN_A_HOUR (3600) /* 一个小时3600秒 */
#define SECONDS_IN_A_MINUTE (60) /* 一分钟60秒 */
#define DAYS_IN_A_YEAR (365) /* 一年365天 */
#define YEAR_RANGE_START (1970) /* 开始年份1970年 */
#define YEAR_RANGE_END (2099) /* 结束年份2099年 */
/**
* @brief : 开启RTC
*/
void rtc_enable(void)
{
/*
* LPCR寄存器bit0置1,使能RTC
*/
SNVS->LPCR |= 1 << 0;
while(!(SNVS->LPCR & 0X01));//等待使能完成
}
/**
* @brief : 关闭RTC
*/
void rtc_disable(void)
{
/*
* LPCR寄存器bit0置0,关闭RTC
*/
SNVS->LPCR &= ~(1 << 0);
while(SNVS->LPCR & 0X01);//等待关闭完成
}
/**
* @brief : 判断指定年份是否为闰年,闰年条件如下:
* @param - year: 要判断的年份
* @return : 1 是闰年,0 不是闰年
*/
unsigned char rtc_isleapyear(unsigned short year)
{
unsigned char value=0;
if(year % 400 == 0)
value = 1;
else
{
if((year % 4 == 0) && (year % 100 != 0))
value = 1;
else
value = 0;
}
return value;
}
/**
* @brief : 将时间转换为秒数
* @param - datetime: 要转换日期和时间。
* @return : 转换后的秒数
*/
unsigned int rtc_coverdate_to_seconds(struct rtc_datetime *datetime)
{
unsigned short i = 0;
unsigned int seconds = 0;
unsigned int days = 0;
unsigned short monthdays[] = {0U, 0U, 31U, 59U, 90U, 120U, 151U, 181U, 212U, 243U, 273U, 304U, 334U};
for(i = 1970; i < datetime->year; i++)
{
days += DAYS_IN_A_YEAR; /* 平年,每年365天 */
if(rtc_isleapyear(i)) days += 1;/* 闰年多加一天 */
}
days += monthdays[datetime->month];
if(rtc_isleapyear(i) && (datetime->month >= 3)) days += 1;/* 闰年,并且当前月份大于等于3月的话加一天 */
days += datetime->day - 1;
seconds = days * SECONDS_IN_A_DAY +
datetime->hour * SECONDS_IN_A_HOUR +
datetime->minute * SECONDS_IN_A_MINUTE +
datetime->second;
return seconds;
}
/**
* @brief : 设置时间和日期
* @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); /* 设置高17位 */
SNVS->LPSRTCLR = (unsigned int)(seconds << 15); /* 设置低15位 */
/* 如果此前RTC是打开的在设置完RTC时间以后需要重新打开RTC */
if (tmp & 0x1)
rtc_enable();
}
RTC初始化
/**
* @brief : 初始化RTC
*/
void rtc_init(void)
{
/*
* 设置HPCOMR寄存器
* bit[31] 1 : 允许访问SNVS寄存器,一定要置1
* bit[8] 1 : 此位置1,需要签署NDA协议才能看到此位的详细说明,
* 这里不置1也没问题
*/
SNVS->HPCOMR |= (1 << 31) | (1 << 8);
#if 0
rtc_datetime rtcdate;
rtcdate.year = 2021U;
rtcdate.month = 2U;
rtcdate.day = 25U;
rtcdate.hour = 23U;
rtcdate.minute = 49;
rtcdate.second = 0;
rtc_setDatetime(&rtcdate); //初始化时间和日期,我也要写成我现在的时间
#endif
rtc_enable(); //使能RTC
}
黑人问号脸?就两句?就这?就这?就这?
I2C
I2C协议简单介绍
I2C是一个比较简单且运用非常广泛的协议,如不了解可百度了解,这里不做介绍
1、I2C分为SCL和SDA,两个必须要接上拉电阻到VCC,比如3.3V,一般是4.7K上拉电阻。
2、I2C总线支持多从机,通过从机地址来区分从机。
3、I2C频率标准模式100kbit/S,快速模式400Kbit/S
开发板上的I2C
开发板上有个AP3216C,这是一个IIC接口的器件,这是一个环境光传感器。AP3216C连接到了I2C1上:
I2C1_SCL: 使用的是UART4_TXD这个IO,复用位ALT2
I2C1_SDA: 使用的是UART4_RXD这个IO。复用为ALT2
AP3216C简介
1、AP3216C是一个三合一的环境光传感器,ALS+PS+IRLED,ALS是环境光,PS是接近传感器,IR是红外LED灯。I2C接口,最高400Kbit/S的频率。环境光,ALS是16位输出。接近传感器PS是10bit输出。IR传感器也是10bit
2、AP3216C的从机地址位0X1E。
3、0X0A 是IR Ddata low。Bit7为0的时候表示IR和PS数据有效,为1的时候IR和PS数据无效。Bit1:0是IR的低2位。
4、0X0B 是IR Data high,big7:0是高字节。与0X0A一起组成需要使用的数据。
5、0X0C、0X0D 分别位ALS的低8位和高8位。
6、0X0E 的bit3:0是低4位数据,0X0F的bit5:0是高6位数据。加起来就是10位
7、0X00 是系统配置寄存器,bit2:0设置AP3216C开始那些传感器,我们需要设置位011,也就是0x3,表示开始ALS+PS+IR。
6ULL I2C接口详解
1、时钟源选择perclk_clk_root=ipg_clk_root=66MHz
2、IFDR寄存器设置I2C频率,bit5:0设置频分值,假如我们现在需要100kbit的速率,那么66000000/100000=660。经过查找IC位设置位0X38或0X15的时候,为640分频,66000000/640=103.125Kbit.
3、I2CR寄存器,bit7为I2C使能位,置1使能I2C。bit5为主从模式选择位,为0表示从机,为1表示主机。Bit4为发送/接收设置位,为0的时候是接收,为1的时候是发送
4、I2SR寄存器,bit7为传输完成位,为0表示正在发送,为1表示发送完成。Bit5是I2C忙闲位,为0的时候I2C总线空闲,为1的时候I2C总线忙。Bit0是读确认位,也就是ACK信号
5、I2DR寄存器,数据寄存器。
C程序编写
I2C模块
I2C初始化
/**
* @brief : 初始化I2C,波特率100KHZ
* @param - base : 要初始化的IIC设置
* @return : 无
*/
void i2c_init(I2C_Type *base)
{
/* 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);
}
I2C主机时序产生
/**
* @brief : 发送开始信号
* @param - base : 要使用的IIC
* @param - addrss : 设备地址
* @param - direction : 方向
* @return : 0 正常 其他值 出错
*/
unsigned char i2c_master_start(I2C_Type *base, unsigned char address, enum i2c_direction direction)
{
if(base->I2SR & (1 << 5)) /* I2C忙 */
return 1;
/*
* 设置寄存器I2CR
* bit[5]: 1 主模式
* bit[4]: 1 发送
*/
base->I2CR |= (1 << 5) | (1 << 4);
/*
* 设置寄存器I2DR
* bit[7:0] : 要发送的数据,这里写入从设备地址
* 参考资料:IMX6UL参考手册P1249
*/
base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);
return 0;
}
/**
* @brief : 发送重新开始信号
* @param - base : 要使用的IIC
* @param - addrss : 设备地址
* @param - direction : 方向
* @return : 0 正常 其他值 出错
*/
unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address, enum i2c_direction direction)
{
/* I2C忙并且工作在从模式,跳出 */
if(base->I2SR & (1 << 5) && (((base->I2CR) & (1 << 5)) == 0))
return 1;
/*
* 设置寄存器I2CR
* bit[4]: 1 发送
* bit[2]: 1 产生重新开始信号
*/
base->I2CR |= (1 << 4) | (1 << 2);
/*
* 设置寄存器I2DR
* bit[7:0] : 要发送的数据,这里写入从设备地址
* 参考资料:IMX6UL参考手册P1249
*/
base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);
return 0;
}
/**
* @brief : 停止信号
* @param - base : 要使用的IIC
* @param : 无
* @return : 状态结果
*/
unsigned char i2c_master_stop(I2C_Type *base)
{
unsigned short timeout = 0xffff;
/*
* 清除I2CR的bit[5:3]这三位
*/
base->I2CR &= ~((1 << 5) | (1 << 4) | (1 << 3));
/* 等待忙结束 */
while((base->I2SR & (1 << 5)))
{
timeout--;
if(timeout == 0) /* 超时跳出 */
return I2C_STATUS_TIMEOUT;
}
return I2C_STATUS_OK;
}
/**
* @brief : 检查并清除错误
* @param - base : 要使用的IIC
* @param - status : 状态
* @return : 状态结果
*/
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status)
{
/* 检查是否发生仲裁丢失错误 */
if(status & (1<<4))
{
base->I2SR &= ~(1<<4); /* 清除仲裁丢失错误位 */
base->I2CR &= ~(1 << 7); /* 先关闭I2C */
base->I2CR |= (1 << 7); /* 重新打开I2C */
return I2C_STATUS_ARBITRATIONLOST;
}
else if(status & (1 << 0)) /* 没有接收到从机的应答信号 */
{
return I2C_STATUS_NAK; /* 返回NAK(No acknowledge) */
}
return I2C_STATUS_OK;
}
I2C读写
/**
* @brief : 发送数据
* @param - base : 要使用的IIC
* @param - buf : 要发送的数据
* @param - size : 要发送的数据大小
* @param - flags : 标志
* @return : 无
*/
void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size)
{
/* 等待传输完成 */
while(!(base->I2SR & (1 << 7)));
base->I2SR &= ~(1 << 1); /* 清除标志位 */
base->I2CR |= 1 << 4; /* 发送数据 */
while(size--)
{
base->I2DR = *buf++; /* 将buf中的数据写入到I2DR寄存器 */
while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */
base->I2SR &= ~(1 << 1); /* 清除标志位 */
/* 检查ACK */
if(i2c_check_and_clear_error(base, base->I2SR))
break;
}
base->I2SR &= ~(1 << 1);
i2c_master_stop(base); /* 发送停止信号 */
}
/**
* @brief : 读取数据
* @param - base : 要使用的IIC
* @param - buf : 读取到数据
* @param - size : 要读取的数据大小
* @return : 无
*/
void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size)
{
volatile uint8_t dummy = 0;
dummy++; /* 防止编译报错 */
/* 等待传输完成 */
while(!(base->I2SR & (1 << 7)));
base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */
base->I2CR &= ~((1 << 4) | (1 << 3)); /* 接收数据 */
/* 如果只接收一个字节数据的话发送NACK信号 */
if(size == 1)
base->I2CR |= (1 << 3);
dummy = base->I2DR; /* 假读 */
while(size--)
{
while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */
base->I2SR &= ~(1 << 1); /* 清除标志位 */
if(size == 0)
{
i2c_master_stop(base); /* 发送停止信号 */
}
if(size == 1)
{
base->I2CR |= (1 << 3);
}
*buf++ = base->I2DR;
}
}
I2C读写封装
正点原子在这个模块通过结构体对I2C读写函数的封装,代码如下
/**
* @brief :I2C方向枚举类型,确定I2C的方向
*/
enum i2c_direction
{
kI2C_Write = 0x0, /* 主机向从机写数据 */
kI2C_Read = 0x1, /* 主机从从机读数据 */
} ;
/**
* @brief :主机传输结构体、确定I2C的数据传输规则
*/
struct i2c_transfer
{
unsigned char slaveAddress; /* 7位从机地址 */
enum i2c_direction direction; /* 传输方向 */
unsigned int subaddress; /* 寄存器地址 */
unsigned char subaddressSize; /* 寄存器地址长度 */
unsigned char *volatile data; /* 数据缓冲区 */
volatile unsigned int dataSize; /* 数据缓冲区长度 */
};
/*对外部读写只有一个接口,使用起来较为简单*/
/**
* @brief : I2C数据传输,包括读和写
* @param - base: 要使用的IIC
* @param - xfer: 传输结构体
* @return : 传输结果,0 成功,其他值 失败;
*/
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{
unsigned char ret = 0;
enum i2c_direction direction = xfer->direction;
base->I2SR &= ~((1 << 1) | (1 << 4)); /* 清除标志位 */
/* 等待传输完成 */
while(!((base->I2SR >> 7) & 0X1)){};
/* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */
if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read))
{
direction = kI2C_Write;
}
ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */
if(ret)
{
return ret;
}
while(!(base->I2SR & (1 << 1))){}; /* 等待传输完成 */
ret = i2c_check_and_clear_error(base, base->I2SR); /* 检查是否出现传输错误 */
if(ret)
{
i2c_master_stop(base); /* 发送出错,发送停止信号 */
return ret;
}
/* 发送寄存器地址 */
if(xfer->subaddressSize)
{
do
{
base->I2SR &= ~(1 << 1); /* 清除标志位 */
xfer->subaddressSize--; /* 地址长度减一 */
base->I2DR = ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址
while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */
/* 检查是否有错误发生 */
ret = i2c_check_and_clear_error(base, base->I2SR);
if(ret)
{
i2c_master_stop(base); /* 发送停止信号 */
return ret;
}
} while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));
if(xfer->direction == kI2C_Read) /* 读取数据 */
{
base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */
i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */
while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 */
/* 检查是否有错误发生 */
ret = i2c_check_and_clear_error(base, base->I2SR);
if(ret)
{
ret = I2C_STATUS_ADDRNAK;
i2c_master_stop(base); /* 发送停止信号 */
return ret;
}
}
}
/* 发送数据 */
if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0))
{
i2c_master_write(base, xfer->data, xfer->dataSize);
}
/* 读取数据 */
if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0))
{
i2c_master_read(base, xfer->data, xfer->dataSize);
}
return 0;
}
6ULL这个I2C对于新手是不太友好的,需要自己操作起始信号和停止信号,但是从机地址却不需要自己去主动发送,总觉得是有些别扭的。调试起来也会比较麻烦。需要自己阅读datasheet进行调试。
AP3216C模块
读写
/**
* @brief : 向AP3216C写入数据
* @param - addr: 设备地址
* @param - reg : 要写入的寄存器
* @param - data: 要写入的数据
* @return : 操作结果
*/
unsigned char ap3216c_writeonebyte(unsigned char addr,unsigned char reg, unsigned char data)
{
unsigned char status=0;
unsigned char writedata=data;
struct i2c_transfer masterXfer;
/* 配置I2C xfer结构体 */
masterXfer.slaveAddress = addr; /* 设备地址 */
masterXfer.direction = kI2C_Write; /* 写入数据 */
masterXfer.subaddress = reg; /* 要写入的寄存器地址 */
masterXfer.subaddressSize = 1; /* 地址长度一个字节 */
masterXfer.data = &writedata; /* 要写入的数据 */
masterXfer.dataSize = 1; /* 写入数据长度1个字节 */
if(i2c_master_transfer(I2C1, &masterXfer))
status=1;
return status;
}
/**
* @brief : 从AP3216C读取一个字节的数据
* @param - addr: 设备地址
* @param - reg : 要读取的寄存器
* @return : 读取到的数据。
*/
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg)
{
unsigned char val=0;
struct i2c_transfer masterXfer;
masterXfer.slaveAddress = addr; /* 设备地址 */
masterXfer.direction = kI2C_Read; /* 读取数据 */
masterXfer.subaddress = reg; /* 要读取的寄存器地址 */
masterXfer.subaddressSize = 1; /* 地址长度一个字节 */
masterXfer.data = &val; /* 接收数据缓冲区 */
masterXfer.dataSize = 1; /* 读取数据长度1个字节 */
i2c_master_transfer(I2C1, &masterXfer);
return val;
}
初始化
/**
* @brief : 初始化AP3216C
* @param : 无
* @return : 0 成功,其他值 错误代码
*/
unsigned char ap3216c_init(void)
{
unsigned char data = 0;
/* 1、IO初始化,配置I2C IO属性
* I2C1_SCL -> UART4_TXD
* I2C1_SDA -> UART4_RXD
*/
IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1);
IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_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);
i2c_init(I2C1); /* 初始化I2C1 */
/* 2、初始化AP3216C */
ap3216c_writeonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X04); /* 复位AP3216C */
delayms(50); /* AP33216C复位至少10ms */
ap3216c_writeonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X03); /* 开启ALS、PS+IR */
data = ap3216c_readonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG); /* 读取刚刚写进去的0X03 */
if(data == 0X03)
return 0; /* AP3216C正常 */
else
return 1; /* AP3216C失败 */
}
读取数据
/**
* @brief : 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
* : 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
* @param - ir : ir数据
* @param - ps : ps数据
* @param - ps : als数据
* @return : 无。
*/
void ap3216c_readdata(unsigned short *ir, unsigned short *ps, unsigned short *als)
{
unsigned char buf[6];
unsigned char i;
/* 循环读取所有传感器数据 */
for(i = 0; i < 6; i++)
{
buf[i] = ap3216c_readonebyte(AP3216C_ADDR, AP3216C_IRDATALOW + i);
}
if(buf[0] & 0X80) /* IR_OF位为1,则数据无效 */
*ir = 0;
else /* 读取IR传感器的数据 */
*ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);
*als = ((unsigned short)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */
if(buf[4] & 0x40) /* IR_OF位为1,则数据无效 */
*ps = 0;
else /* 读取PS传感器的数据 */
*ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}
SPI
SPI协议简单介绍
SPI是一个比较简单且运用非常广泛的协议,如不了解可百度了解,这里不做介绍
1、SPI相比I2C最大的优势有两点:一个是速度快,最高可以大几十M,甚至上百MHz,第二个就是SPI是个全双工。
2、SPI接口和I2C一样,一个SPI接口可以连接多个SPI外设,SPI通过CS引脚/数据线,片选引脚来选择和哪个SPI外设通信。SPI通信前先将指定的SPI外设对应的CS引脚拉低来选中此设备。
3、根据CPOL和CPHA可以设置四种工作模式,一般使用CPOL=0、CPHA=0。
开发板上的SPI接口
通过ECSPI3接口连接了一个6轴传感器,引脚如下:
ECSPI3_SCLK : UART2_RX
ECSPI3_MOSI:UART2_CTS
ECSPI3_SS0:UART2_TXD
ECSPI3_MISO: UART2_RTS
6ULL一个SPI主接口有4个硬件片选,分别为SS0~SS3。
6ULL SPI接口详解
1、6ULL的SPI接口叫做ECSPI,支持全双工、主丛可配置。
2、4个硬件片选信号,可以使用软件片选,这样一个SPI接口所能连接的外设就无限制了。
1、RXDATA寄存器为接收到的数据。
2、TXDATA寄存器为发送数据寄存器。
3、CONREG寄存器为配置寄存器,bit0置1,使能SPI。Bit3置1,表示当向TXFIFO写入数据以后马上开启SPI突发访问,也就是发送数据。Bit7:4设置SPI通道主从模式,bit7为通道3,bit4为通道0,我们使用到了SS0,也就是通道0,因此需要设置bit4为1。Bit19:18设置为00,我们使用到SS0,也就是通道0。Bit31:30设置突发访问长度,我们设置为7,也就是8bit突发长度,一个字节。
4、CONFIGREG寄存器的bit0为PHA,设置为0,表示 串行时钟的第一个跳变沿开始采集数据。设置bit4为PO,设置为0,表示SCLK空闲的时候为低电平。Bit8设置0。Bit12设置 为0。Bit16设置为0,表示空闲的时候数据线为高。Bit20设置为0,表示SCLK空闲的时候为低。
5、STATREG寄存器,bit0表示TXFIFO为空,我们在发送数据之前要等待TXFIFO为空,也就是等待bit0为1。Bit3表示RXFIFO是否有数据,为1的时候示RXFIFO至少有1个字的数据,我们在接收数据的时候要等到bit3为1。
6、PERIODREG寄存器,bit14:0设置wait states时间,我们设置为0X2000。Bit15设置wait states的时钟源为SPI CLK,将此位设置0。Bit21:16表示片选信号的延时,可设置0-63,这里设置为0.
7、SPI时钟设置!
SPI时钟源最终来源于pll3_sw_clk=480MHz/8=60MHz,设置CSCDR2寄存器的bit18为0,也就是ECSPI时钟源为60MHz。bit24:19设置为0,表示1分频,因此最终进入到SPI外设的时钟源为60MHz
ECSPI模块还需要对时钟进行两级分频,由ECSPI_CONREG寄存器设置。Bit15:12设置前级分频,可以设置00xf,表示116分频。Bit11:8设置2级分频,设置2^n分频,n=0~15.
C程序编写
SPI模块
SPI初始化
/**
* @brief : 初始化SPI
* @param - base : 要初始化的SPI
* @return : 无
*/
void spi_init(ECSPI_Type *base)
{
/* 配置CONREG寄存器
* bit0 : 1 使能ECSPI
* bit3 : 1 当向TXFIFO写入数据以后立即开启SPI突发。
* bit[7:4] : 0001 SPI通道0主模式,根据实际情况选择,
* 开发板上的ICM-20608接在SS0上,所以设置通道0为主模式
* bit[19:18]: 00 选中通道0(其实不需要,因为片选信号我们我们自己控制)
* bit[31:20]: 0x7 突发长度为8个bit。
*/
base->CONREG = 0; /* 先清除控制寄存器 */
base->CONREG |= (1 << 0) | (1 << 3) | (1 << 4) | (7 << 20); /* 配置CONREG寄存器 */
/*
* ECSPI通道0设置,即设置CONFIGREG寄存器
* bit0: 0 通道0 PHA为0
* bit4: 0 通道0 SCLK高电平有效
* bit8: 0 通道0片选信号 当SMC为1的时候此位无效
* bit12: 0 通道0 POL为0
* bit16: 0 通道0 数据线空闲时高电平
* bit20: 0 通道0 时钟线空闲时低电平
*/
base->CONFIGREG = 0; /* 设置通道寄存器 */
/*
* ECSPI通道0设置,设置采样周期
* bit[14:0] : 0X2000 采样等待周期,比如当SPI时钟为10MHz的时候
* 0X2000就等于1/10000 * 0X2000 = 0.8192ms,也就是连续
* 读取数据的时候每次之间间隔0.8ms
* bit15 : 0 采样时钟源为SPI CLK
* bit[21:16]: 0 片选延时,可设置为0~63
*/
base->PERIODREG = 0X2000; /* 设置采样周期寄存器 */
/*
* ECSPI的SPI时钟配置,SPI的时钟源来源于pll3_sw_clk/8=480/8=60MHz
* 通过设置CONREG寄存器的PER_DIVIDER(bit[11:8])和POST_DIVEDER(bit[15:12])来
* 对SPI时钟源分频,获取到我们想要的SPI时钟:
* SPI CLK = (SourceCLK / PER_DIVIDER) / (2^POST_DIVEDER)
* 比如我们现在要设置SPI时钟为6MHz,那么PER_DIVEIDER和POST_DEIVIDER设置如下:
* PER_DIVIDER = 0X9。
* POST_DIVIDER = 0X0。
* SPI CLK = 60000000/(0X9 + 1) = 60000000=6MHz
*/
base->CONREG &= ~((0XF << 12) | (0XF << 8)); /* 清除PER_DIVDER和POST_DIVEDER以前的设置 */
base->CONREG |= (0X9 << 12); /* 设置SPI CLK = 6MHz */
}
读写数据
因为是全双工,在发送的时候也在读取数据,所有代码较为简单
/**
* @brief : SPI通道0发送/接收一个字节的数据
* @param - base : 要使用的SPI
* @param - txdata : 要发送的数据
* @return : 无
*/
unsigned char spich0_readwrite_byte(ECSPI_Type *base, unsigned char txdata)
{
uint32_t spirxdata = 0;
uint32_t spitxdata = txdata;
/* 选择通道0 */
base->CONREG &= ~(3 << 18);
base->CONREG |= (0 << 18);
while((base->STATREG & (1 << 0)) == 0){} /* 等待发送FIFO为空 */
base->TXDATA = spitxdata;
while((base->STATREG & (1 << 3)) == 0){} /* 等待接收FIFO有数据 */
spirxdata = base->RXDATA;
return spirxdata;
}
错误修改
使用浮点计算的时候程序卡死了,因为没有开始6UL的硬件浮点运算。在编译的时候没有使用浮点。解决此问题需要两点:
①、开启6UL的硬件浮点单元
/**
* @brief : 使能I.MX6U的硬件NEON和FPU(浮点运算单元)
* @param : 无
* @return : 无
*/
void imx6ul_hardfpu_enable(void)
{
uint32_t cpacr;
uint32_t fpexc;
/* 使能NEON和FPU */
cpacr = __get_CPACR();
cpacr = (cpacr & ~(CPACR_ASEDIS_Msk | CPACR_D32DIS_Msk))
| (3UL << CPACR_cp10_Pos) | (3UL << CPACR_cp11_Pos);
__set_CPACR(cpacr);
fpexc = __get_FPEXC();
fpexc |= 0x40000000UL;
__set_FPEXC(fpexc);
}
②、编译指定硬件浮点。
#-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard
$(COBJS) : obj/%.o : %.c
$(CC) -Wall -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
多点电容触摸屏
多点电容触摸屏简介
1、多点触摸,不需要按下。
2、电容触摸屏需要一个IC驱动控制的,一般是I2C接口,多点触摸屏驱动最终就是一个I2C外设驱动。
触摸屏IP:
CT_INT,触摸中断线,连接到了GPIO1_IO09
I2C2_SCL:连接到了UART5_TXD
I2C2_SDA:连接到了UART5_RXD
RESET:连接到了SNVS_TAMPER9
电容触摸芯片输出的触摸点坐标信息为对应的屏幕像素点信息,因此不需要校准。电阻屏需要校准。
FT54x6/FT52x6电容触摸芯片
1、IIC接口,最大400KHz,因此此模块需要用到I2C中的I2C模块
2、正点原子7寸屏幕FT5426的IIC地址为0X38.
3、需要用到的寄存器
DEVICE_MODE 0X00,需要设置为0X0,表示正常运行模式。
ID_G_LIB_VERSION_H以及ID_G_LIB_VERSION_L 0XA1和0XA2。表示固件版本号。
ID_G_MODE 0XA4,设置为1,表示采用中断方式上报触摸信息。
TD_STATUS 0X02,当前触摸点的个数,1~5。
TOUCH1_XH 0X03开始记录着触摸屏的触摸点坐标信息,一个触摸点6个寄存器,一共需要5*6=30个寄存器。一直读取到0X20
一个触摸点坐标信息用12bit表示,其中H的bit3:0这4个bit为高4位,L寄存器的bit7:0为低8位。
对于XH寄存器,bit7:6表示事件,很重要。
YH寄存器的bit7:4表示触摸ID,
当有触摸时,产生中断信号到6ULL
C程序编写
芯片的读写
/**
* @brief : 向FT5429写入数据
* @param - addr: 设备地址
* @param - reg : 要写入的寄存器
* @param - data: 要写入的数据
* @return : 操作结果
*/
unsigned char ft5426_write_byte(unsigned char addr,unsigned char reg, unsigned char data)
{
unsigned char status=0;
unsigned char writedata=data;
struct i2c_transfer masterXfer;
/* 配置I2C xfer结构体 */
masterXfer.slaveAddress = addr; /* 设备地址 */
masterXfer.direction = kI2C_Write; /* 写入数据 */
masterXfer.subaddress = reg; /* 要写入的寄存器地址 */
masterXfer.subaddressSize = 1; /* 地址长度一个字节 */
masterXfer.data = &writedata; /* 要写入的数据 */
masterXfer.dataSize = 1; /* 写入数据长度1个字节 */
if(i2c_master_transfer(I2C2, &masterXfer))
status=1;
return status;
}
/**
* @brief : 从FT5426读取一个字节的数据
* @param - addr: 设备地址
* @param - reg : 要读取的寄存器
* @return : 读取到的数据。
*/
unsigned char ft5426_read_byte(unsigned char addr,unsigned char reg)
{
unsigned char val=0;
struct i2c_transfer masterXfer;
masterXfer.slaveAddress = addr; /* 设备地址 */
masterXfer.direction = kI2C_Read; /* 读取数据 */
masterXfer.subaddress = reg; /* 要读取的寄存器地址 */
masterXfer.subaddressSize = 1; /* 地址长度一个字节 */
masterXfer.data = &val; /* 接收数据缓冲区 */
masterXfer.dataSize = 1; /* 读取数据长度1个字节 */
i2c_master_transfer(I2C2, &masterXfer);
return val;
}
/**
* @brief : 从FT5429读取多个字节的数据
* @param - addr: 设备地址
* @param - reg : 要读取的开始寄存器地址
* @param - len : 要读取的数据长度.
* @param - buf : 读取到的数据缓冲区
* @return : 无
*/
void ft5426_read_len(unsigned char addr,unsigned char reg,unsigned char len,unsigned char *buf)
{
struct i2c_transfer masterXfer;
masterXfer.slaveAddress = addr; /* 设备地址 */
masterXfer.direction = kI2C_Read; /* 读取数据 */
masterXfer.subaddress = reg; /* 要读取的寄存器地址 */
masterXfer.subaddressSize = 1; /* 地址长度一个字节 */
masterXfer.data = buf; /* 接收数据缓冲区 */
masterXfer.dataSize = len; /* 读取数据长度1个字节 */
i2c_master_transfer(I2C2, &masterXfer);
}
/**
* @brief : 读取当前触摸点个数
* @param : 无
* @return : 无
*/
void ft5426_read_tpnum(void)
{
ft5426_dev.point_num = ft5426_read_byte(FT5426_ADDR, FT5426_TD_STATUS);
}
初始化芯片
/* 触摸屏结构体 */
struct ft5426_dev_struc
{
unsigned char initfalg; /* 触摸屏初始化状态 */
unsigned char intflag; /* 标记中断有没有发生 */
unsigned char point_num; /* 触摸点 */
unsigned short x[5]; /* X轴坐标 */
unsigned short y[5]; /* Y轴坐标 */
};
struct ft5426_dev_struc ft5426_dev;
/**
* @brief : 初始化触摸屏,其实就是初始化FT5426
* @param : 无
* @return : 无
*/
void ft5426_init(void)
{
unsigned char reg_value[2];
ft5426_dev.initfalg = FT5426_INIT_NOTFINISHED;
/* 1、初始化IIC2 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);
/* 配置I2C2 IO属性
*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);
/* 2、初始化触摸屏中断IO和复位IO */
gpio_pin_config_t ctintpin_config;
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO09_GPIO1_IO09,0); /* 复用为GPIO1_IO9 */
IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09,0);/* 复用为GPIO5_IO9 */
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO09_GPIO1_IO09,0xF080);
IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09,0X10B0);
/* 中断IO初始化 */
ctintpin_config.direction = kGPIO_DigitalInput;
ctintpin_config.interruptMode = kGPIO_IntRisingOrFallingEdge;
gpio_init(GPIO1, 9, &ctintpin_config);
GIC_EnableIRQ(GPIO1_Combined_0_15_IRQn); /* 使能GIC中对应的中断 */
system_register_irqhandler(GPIO1_Combined_0_15_IRQn, (system_irq_handler_t)gpio1_io9_irqhandler, NULL); /* 注册中断服务函数 */
gpio_enableint(GPIO1, 9); /* 使能GPIO1_IO18的中断功能 */
/* 复位IO初始化 */
ctintpin_config.direction=kGPIO_DigitalOutput;
ctintpin_config.interruptMode=kGPIO_NoIntmode;
ctintpin_config.outputLogic=1;
gpio_init(GPIO5, 9, &ctintpin_config);
/* 3、初始化I2C 与I2C模块相同*/
i2c_init(I2C2);
/* 4、初始化FT5426 */
gpio_pinwrite(GPIO5, 9, 0); /* 复位FT5426 */
delayms(20);
gpio_pinwrite(GPIO5, 9, 1); /* 停止复位FT5426 */
delayms(20);
ft5426_write_byte(FT5426_ADDR, FT5426_DEVICE_MODE, 0); /* 进入正常模式 */
ft5426_write_byte(FT5426_ADDR, FT5426_IDG_MODE, 1); /* FT5426中断模式 */
ft5426_read_len(FT5426_ADDR, FT5426_IDGLIB_VERSION, 2, reg_value);
printf("Touch Frimware Version:%#X\r\n", ((unsigned short)reg_value[0] << 8) + reg_value[1]);
ft5426_dev.initfalg = FT5426_INIT_FINISHED; /* 标记FT5426初始化完成 */
ft5426_dev.intflag = 0;
}
读取坐标
/**
* @brief : 读取当前所有触摸点的坐标
* @param : 无
* @return : 无
*/
void ft5426_read_tpcoord(void)
{
unsigned char i = 0;
unsigned char type = 0;
//unsigned char id = 0;
unsigned char pointbuf[FT5426_XYCOORDREG_NUM];
ft5426_dev.point_num = ft5426_read_byte(FT5426_ADDR, FT5426_TD_STATUS);
/*
* 从寄存器FT5426_TOUCH1_XH开始,连续读取30个寄存器的值,这30个寄存器
* 保存着5个点的触摸值,每个点占用6个寄存器。
*/
ft5426_read_len(FT5426_ADDR, FT5426_TOUCH1_XH, FT5426_XYCOORDREG_NUM, pointbuf);
for(i = 0; i < ft5426_dev.point_num ; i++)
{
unsigned char *buf = &pointbuf[i * 6];
/* 以第一个触摸点为例,寄存器TOUCH1_XH(地址0X03),各位描述如下:
* bit7:6 Event flag 0:按下 1:释放 2:接触 3:没有事件
* bit5:4 保留
* bit3:0 X轴触摸点的11~8位。
*/
ft5426_dev.x[i] = ((buf[2] << 8) | buf[3]) & 0x0fff;
ft5426_dev.y[i] = ((buf[0] << 8) | buf[1]) & 0x0fff;
type = buf[0] >> 6; /* 获取触摸类型 */
/* 以第一个触摸点为例,寄存器TOUCH1_YH(地址0X05),各位描述如下:
* bit7:4 Touch ID 触摸ID,表示是哪个触摸点
* bit3:0 Y轴触摸点的11~8位。
*/
//id = (buf[2] >> 4) & 0x0f;
if(type == FT5426_TOUCH_EVENT_DOWN || type == FT5426_TOUCH_EVENT_ON )/* 按下 */
{
} else { /* 释放 */
}
}
}
中断服务
/**
* @brief : GPIO1_IO9最终的中断处理函数
* @param : 无
* @return : 无
*/
void gpio1_io9_irqhandler(void)
{
if(ft5426_dev.initfalg == FT5426_INIT_FINISHED)
{
//ft5426_dev.intflag = 1;
ft5426_read_tpcoord();//读取坐标并记录
}
gpio_clearintflags(GPIO1, 9); /* 清除中断标志位 */
}
PWM背光实验
通过PWM来实现屏幕亮度的调节,占空比越高,屏幕越亮
6ULL的PWM
1、6ULL的PWM是16位计数器,
2、有4个16位的FIFO。
3、一个12位的分频器
4、正点原子LCD屏幕的背光IO连接到了GPIO1_IO08上。GPIO1_IO08可以复用位PWM1_OUT信号。
PWM计数器从0X0000开始计数,当计数器的值等于PWMPR+1的时候定时器就会重新开始下一个周期的运行,因此PWMPR寄存器控制着PWM频率。
FIFO保存着采样值,当我们向PWMSAR寄存器写采样值的时候会写到FIFO里面,每当读取一次PWMSAR寄存器,FIFO里面的数据都会减一,或者每产生一个PWM信号,FIFO的数据也会减一。直到FIFO为空,那么就无法再产生PWM信号。FIFO为空的时候会产生中断,我们可以在中断中向FIFO写入采样数据,也就是向PWMSAR写数据。
PWMCR寄存器,bit0是PWM使能信号,bit2:1设置为0,每个周期使用FIFO里面的一个数据。Bit15:4,PWM分频设置,可以设置04095,对应14096分频。Bit17:16设置PWM时钟源,设置为1,表示使用ipg_clk=66MHz。bit19:18,设置为0。Bit27:26,设置为01,表示当FIFO里面空余位置大于2的时候FIFO为空。
PWMIR寄存器,bit0设置为1,开启FIFO空中断。
C程序编写
初始化
/* 背光PWM结构体 */
struct backlight_dev_struc
{
unsigned char pwm_duty; /* 占空比 */
};
/* 背光设备 */
struct backlight_dev_struc backlight_dev;
/**
* @brief : 设置PWM周期,就是设置寄存器PWMPR,PWM周期公式如下
* PWM_FRE = PWM_CLK / (PERIOD + 2), 比如当前PWM_CLK=1MHz
* 要产生1KHz的PWM,那么PERIOD = 1000000/1K - 2 = 998
* @param - value : 周期值,范围0~0XFFFF
* @return : 无
*/
void pwm1_setperiod_value(unsigned int value)
{
unsigned int regvalue = 0;
if(value < 2)
regvalue = 2;
else
regvalue = value - 2;
PWM1->PWMPR = (regvalue & 0XFFFF);
}
/**
* @brief : 设置Sample寄存器,Sample数据会写入到FIFO中,
* 所谓的Sample寄存器,就相当于比较寄存器,假如PWMCR中的POUTC
* 设置为00的时候。当PWM计数器中的计数值小于Sample的时候
* 就会输出高电平,当PWM计数器值大于Sample的时候输出底电平,
* 因此可以通过设置Sample寄存器来设置占空比
* @param - value : 寄存器值,范围0~0XFFFF
* @return : 无
*/
void pwm1_setsample_value(unsigned int value)
{
PWM1->PWMSAR = (value & 0XFFFF);
}
/**
* @brief : 使能PWM
* @param : 无
* @return : 无
*/
void pwm1_enable(void)
{
PWM1->PWMCR |= 1 << 0;
}
/**
* @brief : 设置PWM占空比
* @param - value : 占空比0~100,对应0%~100%
* @return : 无
*/
void pwm1_setduty(unsigned char duty)
{
unsigned short preiod;
unsigned short sample;
backlight_dev.pwm_duty = duty;
preiod = PWM1->PWMPR + 2;
sample = preiod * backlight_dev.pwm_duty / 100;
pwm1_setsample_value(sample);
}
/**
* @brief : 初始化背光PWM
* @param : 无
* @return : 无
*/
void backlight_init(void)
{
unsigned char i = 0;
/* 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);
/* 2、初始化PWM1 */
/*
* 初始化寄存器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);
/* 设置PWM周期为1000,那么PWM频率就是1M/1000 = 1KHz。 */
pwm1_setperiod_value(1000);
/* 设置占空比,默认50%占空比 ,写四次是因为有4个FIFO */
backlight_dev.pwm_duty = 50;
for(i = 0; i < 4; i++)
{
pwm1_setduty(backlight_dev.pwm_duty);
}
/* 使能FIFO空中断,设置寄存器PWMIR寄存器的bit0为1 */
PWM1->PWMIR |= 1 << 0;
system_register_irqhandler(PWM1_IRQn, (system_irq_handler_t)pwm1_irqhandler, NULL); /* 注册中断服务函数 */
GIC_EnableIRQ(PWM1_IRQn); /* 使能GIC中对应的中断 */
PWM1->PWMSR = 0; /* PWM中断状态寄存器清零 */
pwm1_enable(); /* 使能PWM1 */
}
写在最后
到这里,正点原子的裸机视频已经全部结束了,视频很多,但是有很多视频个人觉得不用全部看完,比如一些寄存器的讲解以及编写。因为这些寄存器之类的只适用于当前芯片,换一个芯片就不能用了,然而在实际工作过程中,选择该款芯片作为产品芯片的概率也不会太高。所以我在学习的过程中,把这些视频掠过,先看了原子哥在讲解过程中编写的笔记,然后阅读了一下代码的大概流程,有个系统性的认知。大概会有哪些东西,以便于在使用其他芯片时能够更好的找到问题并解决问题!
历经十五天,终于学完了C盘。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!