一、TFT-LCD简介
TFT-LCD(Thin Film Transistor-Liquid Crystal Display) 即薄膜晶体管液晶显示器。TFT-LCD 与无源 TN-LCD、 STN-LCD 的简单矩阵不同,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。 TFT-LCD 也被叫做真彩液晶显示器。
虽然TFT-LCD被统称为LCD,不过它是种主动式矩阵LCD,被应用在电视、平面显示器及投影机上。
1.1 液晶控制原理
这个完整的显示屏由液晶显示面板、电容触摸面板以及PCB底板构成。图中的触摸面板带有触摸控制芯片,该芯片处理触摸信号并通过引出的信号线与外部器件通讯面板中间是透明的,它贴在液晶面板上面,一起构成屏幕的主体,触摸面板与液晶面板引出的排线连接到PCB底板上,根据实际需要,PCB底板上可能会带有”液晶控制器芯片”。因为控制液晶面板需要比较多的资源,所以大部分低级微控制器都不能直接控制液晶面板,需要额外配套一个专用液晶控制器来处理显示过程,外部微控制器只要把它希望显示的数据直接交给液晶控制器即可。例如STM32F103系列的微控制器不带液晶控制器功能,所以必须要有额外液晶控制器芯片进行处理。而不带液晶控制器的PCB底板 ,只有小部分的电源管理电路,液晶面板的信号线与外部微控制器相连,直接控制。RT1052、STM32F429、IMX6U系列的芯片不需要额外的液晶控制器,也就是说它把专用液晶控制器的功能集成到RT1052、STM32F429、IMX6U芯片内部了,节约了额外的控制器成本。
1.2 像素时钟
像素时钟就是 LCD 的时钟信号,以 ATK7016 这款屏幕为例,显示一帧图像所需要的时钟数就是:
= (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。
I.MX6U 的 eLCDIF 接口时钟图如图:
- ①部分是一个选择器,用于选择哪个 PLL 可以作为 LCDIF 时钟源,由寄存器
CCM_CSCDR2
的位LCDIF1_PRE_CLK_SEL(bit17:15)
来决定,LCDIF1_PRE_CLK_SEL
选择设置下表所示:
值 | 时钟源 |
---|---|
0 | PLL2 作为 LCDIF 的时钟源。 |
1 | PLL3_PFD3 作为 LCDIF 的时钟源。 |
2 | PLL5 作为 LCDIF 的时钟源。 |
3 | PLL2_PFD0 作为 LCDIF 的时钟源。 |
4 | PLL2_PFD1 作为 LCDIF 的时钟源。 |
5 | PLL3_PFD1 作为 LCDIF 的时钟源。 |
有个专用的 PLL5 给 VIDEO 使用,所以 LCDIF1_PRE_CLK_SEL
设置为 2。
- ②部分是 LCDIF 时钟的预分频器,由寄存器
CCM_CSCDR2
的位LCDIF1_PRED
来决定预分频值。可设置值为 0~7,分别对应 1~8 分频。 - ③部分进一步分频,由寄存器
CBCMR
的位LCDIF1_PODF
来决定分频值。可设置值为 0~7,分别对应 1~8 分频。 - ④部分是一个选择器,选择 LCDIF 最终的根时钟,由寄存器
CSCDR2
的位
LCDIF1_CLK_SEL
决定,LCDIF1_CLK_SEL
选择设置如下表所示:
值 | 时钟源
0 | 前面复用器出来的时钟,也就是前面 PLL5 出来的时钟作为 LCDIF 的根时钟。
1 | ipp_di0_clk 作为 LCDIF 的根时钟。
2 | ipp_di1_clk 作为 LCDIF 的根时钟。
3 | ldb_di0_clk 作为 LCDIF 的根时钟。
4 | ldb_di1_clk 作为 LCDIF 的根时钟。
这里肯定选择 PLL5 出来的那一路时钟作为 LCDIF 的根时钟,因此 LCDIF1_CLK_SEL
设置为 0。LCDIF 既然选择了 PLL5 作为时钟源,那么还需要初始化 PLL5,LCDIF 的时钟是由 PLL5 和图中的②、③这两个分频值决定的,所以需要对这三个进行合理的设置以搭配出所需的时钟值,我们就以 ATK7016 屏幕所需的 51.2MHz 为例,看看如何进行配置。
PLL5 频率设置涉及到四个寄存器:CCM_PLL_VIDEO
、CCM_PLL_VIDEO_NUM
、CCM_PLL_VIDEO_DENOM
、CCM_MISC2
。其中 CCM_PLL_VIDEO_NUM
和 CCM_PLL_VIDEO_DENOM
这两个寄存器是用于小数分频的,我们这里为了简单不使用小数分频,因此这两个寄存器设置为 0。
PLL5 的时钟计算公式如下:
PLL5_CLK = OSC24M * (loopDivider + (denominator / numerator)) / postDivider
不使用小数分频的话 PLL5 时钟计算公式就可以简化为 :
PLL5_CLK = OSC24M * loopDivider / postDivider
OSC24M 就是 24MHz 的有源晶振,现在的问题就是设置 loopDivider 和 postDivider。先来看一下寄存器 CCM_PLL_VIDEO
。
1.3 显存
液晶屏中的每个像素点都是数据,在实际应用中需要把每个像素点的数据缓存起来,再传输给液晶屏,这种存储显示数据的存储器被称为显存。显存一般至少要能存储液晶屏的一帧显示数据,如分辨率为800x480的液晶屏, 使用 RGB888
格式显示,它的一帧显示数据大小为:3x800x480=1152000 字节;若使用 RGB565
格式显示,一帧显示数据大小为:2x800x480=768000 字节。但是 LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 LCD 屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。
二、eLCDIF简介
eLCDIF(Enhanced LCD Interface),是 IMX6U 系列芯片内部自带一个增强型液晶接口外设。配合使用 DDR 作为显存,可直接控制液晶面板,
无需额外增加液晶控制器芯片
。IMX6U 的 eLCDIF 液晶控制器最高支持 1366x768 分辨率的屏幕;可支持多种颜色格式,包括 RGB888、RGB565、ARGB8888 等(其中的“A”是指透明像素)。还可配合像素渲染流水线 PXP(Pixel Pipeline)进行复杂的图像处理,如格式转换、缩放、翻转以及图层混合等操作,使 IMX6U 有非常出色的图形显示性能。
下图是 eLCDIF 控制器的结构框图,它主要包含信号线、配置寄存器、缓冲区以及与系统总线、控制总线的关系。
2.1 通讯引脚
eLCDIF 的通讯引脚与液晶显示面板控制信号一一对应, 包含有 HSYNC、VSYNC、DE、CLK 以及 RGB 数据线各 8 根。设计硬件时把液晶面板与 IMX6U 对应的这些引脚连接起来即可。
eLCDIF外设信号 | 引脚号 | 复用模式 |
---|---|---|
BUSY | LCD_VSYNC | ALT1 |
CLK | LCD_CLK | ALT0 |
CS | LCD_RESET | ALT1 |
DATA0 | LCD_DATA00 | ALT0 |
DATA1 | LCD_DATA01 | ALT0 |
DATA2 | LCD_DATA02 | ALT0 |
DATA3 | LCD_DATA03 | ALT0 |
DATA4 | LCD_DATA04 | ALT0 |
DATA5 | LCD_DATA05 | ALT0 |
DATA6 | LCD_DATA06 | ALT0 |
DATA7 | LCD_DATA07 | ALT0 |
DATA8 | LCD_DATA08 | ALT0 |
DATA9 | LCD_DATA09 | ALT0 |
DATA10 | LCD_DATA10 | ALT0 |
DATA11 | LCD_DATA11 | ALT0 |
DATA12 | LCD_DATA12 | ALT0 |
DATA13 | LCD_DATA13 | ALT0 |
DATA14 | LCD_DATA14 | ALT0 |
DATA15 | LCD_DATA15 | ALT0 |
DATA16 | LCD_DATA16 | ALT0 |
DATA17 | LCD_DATA17 | ALT0 |
DATA18 | LCD_DATA18 | ALT0 |
DATA19 | LCD_DATA19 | ALT0 |
DATA20 | LCD_DATA20 | ALT0 |
DATA21 | LCD_DATA21 | ALT0 |
DATA22 | LCD_DATA22 | ALT0 |
DATA23 | LCD_DATA23 | ALT0 |
ENABLE | LCD_ENABLE | ALT0 |
HSYNC | LCD_HSYNC | ALT0 |
RD_E | LCD_ENABLE | ALT1 |
RESET | LCD_RESET | ALT0 |
RS | LCD_HSYNC | ALT1 |
VSYNC | LCD_VSYNC | ALT0 |
WR_RWN | LCD_CLK | ALT1 |
2.2 总线接口
eLCDIF 的液晶接口有两个总线接口:
- System Bus 系统总线,用于向 eLCDIF 液晶接口的 FIFO 中写入数据。
- Control Bus 控制总线,用于设置 eLCDIF 用于读、写控制寄存器以及 DMA、数据寄存器等等。
2.3 液晶接口(LCD Interface)
液晶接口(LCD Interface),它是 eLCDIF 外设的主要功能部件,受控制总线(Control Bus)的寄存器控制,从系统总线(System Bus)获得输入像素数据,经过一系列转换后通过 eLCDIF 的通讯引脚发送至外接的液晶面板。
其中控制总线的寄存器可以配置显存地址、输入像素数据的格式、输出的数据信号线宽度、各个控制信号的有效极性以及控制时序中的 VSW、VBP 等参数,还能配置使用 DMA 传输。
使用寄存器初始化好 eLCDIF 的后,它会从 “LFIFO” 和 “TXFIFO” 中获取数据进行转换处理(格式转换、移位等操作)并传输出去。当 FIFO 中的数据量低于一定程度时,它会向系统总线(SystemBus)发起请求,系统总线会把显存地址的数据搬运至 FIFO 中。 FIFO 还可以配置阈值,低于该阈值时系统总线会提高获取数据的优先级。
eLCDIF正常运行后,数据从显存到液晶屏全程不需要内核的干预,程序控制时我们只要把像素数据写入到显存即可。
2.4 驱动时钟
eLCDIF 模块包含两个时钟信号,分别是 BUS CLOCK(apb_clk) 和 DISPLAY CLOCK(pix_clk)。
2.4.1 BUS CLOCK(apb_clk)
BUS CLOCK(apb_clk) 就是指 eLCDIF 外设的根时钟 LCDIF_CLK_ROOT
,它给 eLCDIF 提供驱动的时钟源,在时钟树中的结构具体如下图所示。
LCDIF_CLK_ROOT
根时钟可以选择多种输入时钟源,首先是时钟源预选择器(Pre-multiplexer)支持使用如下时钟:
- PLL2: System PLL,该时钟频率通常为 528MHz。
- PLL2 PFD0: 该时钟常规配置为 352MHz。
- PLL2 PFD1: 该时钟常规配置为 594MHz。
- PLL3 PFD3: 该时钟常规配置为 454.74MHz。
- PLL3 PFD1: 该时钟常规配置为 664.62MHz。
- PLL5: Video PLL,该时钟常规配置为 649.52MHz。
预选择器得到的时钟,可根据需要进行分频配置,分频后输入到时钟源选择器(multiplexer)作为 LCDIF_CLK_ROOT
默认的时钟源,除此之外,图中的时钟的选择器还包含其它可选的输入时钟:ipp_di0_clk、ipp_di1_clk、ldb_di0_clk、ldb_di1_clk,不过关于这些时钟在参考手册中并没有介绍,而且在寄存器中并没有这些时钟源的分频、选择的配置,也许该选择器是兼容其它设备而保留的内容,所以使用时我们直接选择预选择器得到的时钟作为 LCDIF_CLK_ROOT
的输入时钟源即可。
2.4.2 DISPLAY CLOCK(pix_clk)
DISPLAY CLOCK(pix_clk) 是指 eLCDIF 与液晶面板接口的像素时钟 LCDIF_pix_clk
,它的时钟频率与根时钟 LCDIF_CLK_ROOT
一致,不过它们的时钟开关是分开的,其中LCDIF_CLK_ROOT
使用寄存器位 CCM_CCGR2[CG14] 控制,而 LCDIF_pix_clk
使用寄存器位 CCM_CCGR3[CG5] 控制。
三、引脚确定
四、编程流程
1. 创建工程文件夹
2. 移植官方SDK寄存器定义文件
3. 移植野火PAD属性配置文件
4. 编写启动文件
5. 编写链接文件
6. 编写makefile文件
7. 编写C语言代码
(1) 初始化LCD所使用的IO
(2) 设置LCD的像素时钟
(3) 复位LCD
(4) 配置eLCDIF接口(寄存器CTRL、CTRL1、TRANSFER_COUNT、VDCTRL0~4、CUR_BUF和NEXT_BUF)
(5) 开启中断
(6) 编写API函数(显示内容,比如画点、画线、画圆函数,字符串显示函数)
五、创建工程文件夹
- 创建一个文件夹
elcdif
- 创建一个用于存放头文件的文件夹
include
- 创建一个用于存放驱动源码的文件
device
- 创建一个启动文件
start.S
- 创建一个源文件
main.c
- 创建一个链接脚本
base.lds
六、移植官方SDK寄存器定义文件
在 /elcdif/include
目录下添加官方SDK寄存器定义文件 MCIMX6Y2.h
,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2
目录下。
在官方SDK的头文件
MCIMX6Y2.h
文件多达4万多行,包含了i.MX6U芯片几乎所有的寄存器定义以及中断编号的定义。
这里只列 GPIO1相关寄存器 的部分代码。其他寄存器定义与此类似。 添加这些定义之后我们就可以 直接使用 “GPIO1->DR”
语句操作GPIO1的DR寄存器。操作方法与STM32非常相似。
typedef struct {
__IO uint32_t DR; /**< GPIO data register, offset: 0x0 */
__IO uint32_t GDIR; /**< GPIO direction register, offset: 0x4 */
__I uint32_t PSR; /**< GPIO pad status register, offset: 0x8 */
__IO uint32_t ICR1; /**< GPIO interrupt configuration register1,*/
__IO uint32_t ICR2; /**< GPIO interrupt configuration register2, */
__IO uint32_t IMR; /**< GPIO interrupt mask register, offset: 0x14 */
__IO uint32_t ISR; /**< GPIO interrupt status register, offset: 0x18 */
__IO uint32_t EDGE_SEL;/**< GPIO edge select register, offset: 0x1C */
} GPIO_Type;
/*********************以下代码省略***************************8*/
/** Peripheral GPIO1 base address */
#define GPIO1_BASE (0x209C000u)
/** Peripheral GPIO1 base pointer */
#define GPIO1 ((GPIO_Type *)GPIO1_BASE)
七、移植野火PAD属性配置文件
在 /elcdif/device
目录下添加 pad_config.h
。
通常情况下一个引脚要设置8种PAD属性,而这些属性只能通过数字指定。为简化PAD属性设置野火编写了一个PAD属性配置文件 pad_config.h
(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】,这里使用宏定义了引脚可选的PAD属性值,并且通过宏定义的名字很容易知道宏代表的属性值:
/* SPEED 带宽配置 */
#define SPEED_0_LOW_50MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(0)
#define SPEED_1_MEDIUM_100MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(1)
#define SPEED_2_MEDIUM_100MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(2)
#define SPEED_3_MAX_200MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(3)
/* PUE 选择使用保持器还是上下拉 */
#define PUE_0_KEEPER_SELECTED IOMUXC_SW_PAD_CTL_PAD_PUE(0)
#define PUE_1_PULL_SELECTED IOMUXC_SW_PAD_CTL_PAD_PUE(1)
/* PUS 上下拉配置 */
#define PUS_0_100K_OHM_PULL_DOWN IOMUXC_SW_PAD_CTL_PAD_PUS(0)
#define PUS_1_47K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(1)
#define PUS_2_100K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(2)
#define PUS_3_22K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(3)
完整的代码请阅读源文件,这里只列出了文件“pad_config.h”部分代码(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】。
八、编写启动文件
在 /elcdif
下创建 start.S
文件用于编写启动文件。
在汇编文件中设置“栈地址”
并执行跳转命令跳转到main函数
执行C代码。
8.1 完整代码
/***********************第一部分*********************/
.text //代码段
.align 2 //设置2字节对齐
.global _start //定义一个全局标号
/*************************第二部分*************************/
_start: //程序的开始
b reset //跳转到reset标号处
/*************************第三部分*************************/
reset:
mrc p15, 0, r0, c1, c0, 0 /* 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中 */
bic r0, r0, #(0x1 << 12) /* 清除第12位(I位)禁用 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除第 2位(C位)禁用 D Cache */
bic r0, r0, #0x2 /* 清除第 1位(A位)禁止严格对齐 */
bic r0, r0, #(0x1 << 11) /* 清除第11位(Z位)分支预测 */
bic r0, r0, #0x1 /* 清除第 0位(M位)禁用 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中 */
/***********************第四部分*********************/
ldr sp, =0x84000000 //设置栈地址64M
b main //跳转到main函数
/***********************第五部分*******************/
/*进入死循环*/
loop:
b loop
8.2 分析代码
- 第一部分
.text
定义代码段。
.align 2
设置字节对齐。
.global _start
生命全局标号_start。
/*************************第一部分*************************/
.text //代码段
.align 2 //设置2字节对齐
.global _start //定义一个全局标号
- 第二部分
_start:
定义标号_start: ,它位于汇编的最前面,说以会首先被执行。
b reset
使用b指令将程序跳转到reset标号处。
/*************************第二部分*************************/
_start: //程序的开始
b reset //跳转到reset标号处
- 第三部分
通过修改CP15寄存器(系统控制寄存器)
关闭 I Cache 、D Cache、MMU 等等。
我们暂时用不到的功能,如果开启可能会影响我们裸机运行,为避免不必要的麻烦暂时关闭这些功能。
/*************************第三部分*************************/
reset:
mrc p15, 0, r0, c1, c0, 0 /* 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中 */
bic r0, r0, #(0x1 << 12) /* 清除第12位(I位)禁用 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除第 2位(C位)禁用 D Cache */
bic r0, r0, #0x2 /* 清除第 1位(A位)禁止严格对齐 */
bic r0, r0, #(0x1 << 11) /* 清除第11位(Z位)分支预测 */
bic r0, r0, #0x1 /* 清除第 0位(M位)禁用 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中 */
- 第四部分
ldr sp, =0x84000000
用于设置栈指针。野火i.MX6ULL开发板标配512M的DDR内存,裸机开发用不了这么多。程序中我们将栈地址设置到DDR的64M地址处。 这个值也可以根据需要自行定义。
b main
只用跳转指令跳转到main函数中执行。
/***********************第四部分*********************/
ldr sp, =0x84000000 //设置栈地址64M
b main //跳转到main函数
- 第五部分
b loop
是“无返回”的跳转指令。正常情况下,不会执行第五部分代码。
/***********************第五部分*******************/
/*进入死循环*/
loop:
b loop
九、编写链接脚本
写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言,汇编是将汇编文件转化为机器码。
使用
arm-none-eabi-gcc -g -c led.S -o led.o
命令完成源码的编译、汇编工作,生成了.o
文件。编译和汇编是针对单个源文件,也就编译完成后一个源文件(.c
,.S
或.s
)对应一个.o
文件。程序链接阶段就会将这些.o
链接成一个文件。链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。
在 /elcdif
下创建 base.lds
。
9.1 完整代码
ENTRY(_start)
SECTIONS {
. = 0x80000000;
. = ALIGN(4);
.text :
{
start.o (.text)
*(.text)
}
. = ALIGN(4);
.data :
{
*(.data)
}
. = ALIGN(4);
.bss :
{
*(.bss)
}
}
9.2 分析代码
- 指定程序的入口
ENTRY(_start)
用于指定程序的入口,ENTRY()
是设置入口地址的命令,“_start”
是程序的入口,led程序的入口地址位于start.S
的“_start”
标号处。
ENTRY(_start)
- 定义SECTIONS
SECTIONS
可以理解为是一块区域,我们在这块区域排布我们的代码,链接时链接器就会按照这里的指示链接我们的代码。
SECTIONS {
···
···
}
- 定义链接起始地址
“.”
运算符代表当前位置。 我们在SECTION的最开始使用“.= 0x80000000”
就是将链接起始地址设置为0x80000000。
. = 0x80000000;
-
设置字节对齐
“. = ALIGN(4);”
它表示从当前位置开始执行四字节对齐。假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。 -
设置代码段
“.text :”
用于定义代码段,固定的语法要求,我们按照要求写即可。在“{}”中指定那些内容放在代码段。
将start.o
中的代码放到代码段的最前面。start.S
是启动代码应当首先被执行,所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可,通配符“*”
在这里表示其他剩余所有的.o
文件。
. = ALIGN(4);
.text :
{
start.o (.text)
*(.text)
}
- 设置数据段
同设置代码段类似,首先设置字节对齐,然后定义代码段。在数据段里使用“*”
通配符, 将所有源文件中的代码添加到这个数据段中。
. = ALIGN(4);
.data :
{
*(.data)
}
- 设置BSS段
设置方法与设置数据段完全相同。
. = ALIGN(4);
.bss :
{
*(.bss)
}
十、编写makefile文件
程序编写完成后需要依次输入编译、链接、格式转换命令才能最终生成二进制文件。这种编译方式效率低、容易出错。
使用
makefile
只需要在所在文件夹下执行make
命令,makefile工具便会自动完成程序的编译、链接、格式转换等工作。正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin
文件。
修改makefile主要包括两部分
- 第一部分,在“device”文件夹下添加并编写子makefile。
- 第二部分,修改主makefile。
10.1 编写子makefile
在 /elcdif/device
下创建 makefile
。
子makefile: 用于将“device”文件夹下的驱动源文件编译为一个“.o”文件
all : system_MCIMX6Y2.o elcdif.o
arm-none-eabi-ld -r $^ -o device.o
%.o : %.c
arm-none-eabi-gcc ${header_file} -c $^
%.o : %.S
arm-none-eabi-gcc ${header_file} -c $^
clean:
-rm -f *.o *.bak
- 添加最终目标以及依赖文件
生成最终目标“device.o”。如果程序中新增了某个外设驱动程序,只需要将对应的“.o”文件填入“依赖”处即可。
“$^”
代表所有的依赖文件。
“-o”
指定输出文件名。
all : system_MCIMX6Y2.o elcdif.o
arm-none-eabi-ld -r $^ -o device.o
- 添加编译C文件的命令
编译“device”文件夹下的所有“.c”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
“$^”
替代要编译的源文件。
%.o : %.c
arm-none-eabi-gcc ${header_file} -c $^
- 添加汇编文件编译命令
编译“device”文件夹下的所有“.S”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
“$^”
替代要编译的源文件。
%.o : %.S
arm-none-eabi-gcc ${header_file} -c $^
- 添加清理命令
“clean”
为目标用于删除make生成的文件。
clean:
-rm -f *.o *.bak
10.2 修改主makefile
主makefile的改动主要有两点:
- 在编译命令中指明头文件位置。
- 使用命令调用子makefile,生成依赖文件。
#定义变量,用于保存编译选项和头文件保存路径
header_file := -fno-builtin -I$(shell pwd)/include
export header_file
all : start.o main.o device/device.o
arm-none-eabi-ld -Tbase.lds $^ -o base.elf
arm-none-eabi-objcopy -O binary -S -g base.elf base.bin
%.o : %.S
arm-none-eabi-gcc -g -c $^
%.o : %.c
arm-none-eabi-gcc $(header_file) -c $^
#调用其他文件的makefile
device/device.o :
make -C device all
.PHONY: copy
copy:
cp ./base.bin /home/pan/download/embedfire
#定义清理伪目标
.PHONY: clean
clean:
make -C device clean
-rm -f *.o *.elf *.bin
- 添加编译选项和头文件保存路径
定义变量 “header_file”。在makefile中“变量”更像C原因中的宏定义。
“-fno-builtin”
是一个编译选项,用于解决库函数与自己编写函数同名问题。
“-I$(shell pwd)/include”
用于指定头文件路径。
“export header_file”
声明后可以在其他makefile中调用。
header_file := -fno-builtin -I$(shell pwd)/include
export header_file
- 添加最终目标以及依赖文件
all : start.o main.o device/device.o
- 添加链接命令
“-Tbase.lds”
表示使用base.lds链接脚本链接程序。
“$^”
代表所有的依赖文件。
“-o”
指定输出文件名。
arm-none-eabi-ld -Tbase.lds $^ -o base.elf
- 添加格式转换命令
“-O binary”
指定输出二进制文件。
“-S”
不从源文件中复制重定位信息和符号信息。
“-g”
不从源文件中复制可调试信息。
arm-none-eabi-objcopy -O binary -S -g base.elf base.bin
- 添加汇编文件编译命令
“$^”
替代要编译的源文件。
%.o : %.S
arm-none-eabi-gcc -g -c $^
- 添加编译C文件的命令
“$^”
替代要编译的源文件。
%.o : %.c
arm-none-eabi-gcc $(header_file) -c $^
- 添加调用其他文件的makefile
定义生成“device/device.o”的命令,“device.o”文件由子makefile生成,所以这里只需要调用子makefile即可。
device/device.o :
make -C device all
- 添加清理命令
在清理命令中不但要清理主makefile所在文件夹的内容还要调用子makefile的清理命令以清理子makefile所在文件夹的内容。
“.PHONY”
定义了伪目标“clean”。伪目标一般没有依赖,并且“clean”
伪目标一般放在Makefile文件的末尾。
“clean”
为目标用于删除make生成的文件。
.PHONY: clean
clean:
make -C device clean
-rm -f *.o *.elf *.bin
十一、编写C语言代码
11.1 添加eLCDIF初始化和复位LCD并开启背光代码
11.1.1 elcdif.h
在 /elcdif/include
下创建 elcdif.h
。
#ifndef elcdif_h
#define elcdif_h
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
/*定义 显示屏信息 */
#define APP_IMG_HEIGHT 480
#define APP_IMG_WIDTH 800
#define APP_HSW 41
#define APP_HFP 4
#define APP_HBP 8
#define APP_VSW 10
#define APP_VFP 4
#define APP_VBP 2
/* 定义 elcdf 显示控制引脚*/
#define LCD_DISP_GPIO GPIO5
#define LCD_DISP_GPIO_PIN 9
/* 定义 elcdf 背光控制引脚 */
#define LCD_BL_GPIO GPIO1
#define LCD_BL_GPIO_PIN 8
#define false 0
#define true 1
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
/* 所有引脚均使用同样的PAD配置 */
#define LCD_PAD_CONFIG_DATA (SRE_1_FAST_SLEW_RATE | \
DSE_6_R0_6 | \
SPEED_3_MAX_200MHz | \
ODE_0_OPEN_DRAIN_DISABLED | \
PKE_1_PULL_KEEPER_ENABLED | \
PUE_0_KEEPER_SELECTED | \
PUS_0_100K_OHM_PULL_DOWN | \
HYS_0_HYSTERESIS_DISABLED)
/* 配置说明 : */
/* 转换速率: 转换速率快
驱动强度: R0/6
带宽配置 : max(200MHz)
开漏配置: 关闭
拉/保持器配置: 使能
拉/保持器选择: 保持器
上拉/下拉选择: 100K欧姆下拉(选择了保持器此配置无效)
滞回器配置: 禁止 */
void lcdif_pin_config(void);
void CLOCK_InitVideoPll(void);
void lcdif_clock_init(void);
void BOARD_InitLcd(void);
void APP_ELCDIF_Init(void);
void ELCDIF_RgbModeInit(void);
void APP_FillFrameBuffer(uint32_t frameBuffer[APP_IMG_HEIGHT][APP_IMG_WIDTH]);
#endif
11.1.2 elcdif.c
在 /elcdif/device
下创建 elcdif.c
。
#include "elcdif.h"
#include "MCIMX6Y2.h"
/*定义 elcdf 缓冲区*/
uint32_t s_frameBuffer[2][APP_IMG_HEIGHT][APP_IMG_WIDTH];
/* elcdif 显示接口外部引脚初始化
*
*/
void lcdif_pin_config(void)
{
IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, LCD_PAD_CONFIG_DATA);
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0U);
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, LCD_PAD_CONFIG_DATA);
}
/*初始化 elcdf 的时钟
*/
void lcdif_clock_init(void)
{
/*设置 PLL5 的输出时钟*/
CCM_ANALOG->PLL_VIDEO_NUM &= (0x3 << 30); //清零PLL 分数分频的分子寄存器
CCM_ANALOG->PLL_VIDEO_DENOM &= (0x3 << 30); //清零PLL 分数分频的分母寄存器
/*
* 设置时钟分频
*
* ------------------------------------------------------------------------
* | 分频数 | PLL_VIDEO[POST_DIV_SELECT] | MISC2[VIDEO_DIV] |
* ------------------------------------------------------------------------
* | 1 | 2 | 0 |
* ------------------------------------------------------------------------
* | 2 | 1 | 0 |
* ------------------------------------------------------------------------
* | 4 | 2 | 3 |
* ------------------------------------------------------------------------
* | 8 | 1 | 3 |
* ------------------------------------------------------------------------
* | 16 | 0 | 3 |
* ------------------------------------------------------------------------
*/
CCM_ANALOG->PLL_VIDEO = 0;
CCM_ANALOG->PLL_VIDEO &= ~(0x3 << 19); // 清零PLL_VIDEO[POST_DIV_SELECT]
CCM_ANALOG->PLL_VIDEO |= (0x01 << 19); //设置分频系数为2
CCM_ANALOG->MISC2 &= ~(0xC0000000); //清零VIDEO_DIV位
CCM_ANALOG->MISC2 |= (0x3 << 30);// 配合CCM_ANALOG->PLL_VIDEO寄存器设置时钟分频
CCM_ANALOG->PLL_VIDEO &= ~(0x7F); // 清零时钟分频
CCM_ANALOG->PLL_VIDEO |= (0x1F); //设置时钟分频为 31(十进制)
CCM_ANALOG->PLL_VIDEO |= 1 << 13; //使能PLL5时钟输出
/*等待设置生效*/
while ((CCM_ANALOG->PLL_VIDEO & CCM_ANALOG_PLL_VIDEO_LOCK_MASK) == 0)
{
}
/*设置从PLL5 到 elcdf 根时钟所经过的时钟选择和时钟分频寄存器*/
CCM->CSCDR2 &= ~(0x07 << 15); //清零
CCM->CSCDR2 |= (0x02 << 15); //设置CSCDR2[LCDIF1_PRE_CLK_SEL] 选择 PLL5 输出时钟
CCM->CSCDR2 &= ~(0x07 << 12); //清零
CCM->CSCDR2 |= (0x01 << 12); //设置 CSCDR2[LCDIF1_PRED]时钟分频值
CCM->CBCMR &= ~(0x07 << 23); //清零CBCMR[LCDIF1_PODF] 时钟分频值
CCM->CBCMR |= (0x01 << 23);
CCM->CSCDR2 &= ~(0x07 << 9); //清零
CCM->CSCDR2 |= (0x00 << 9); //选择 CSCDR2[LCDIF1_CLK_SEL] 选择 PLL5 输出时钟
}
/* 初始化 LCD_DISP,点亮 显示屏 */
void BOARD_InitLcd(void)
{
volatile uint32_t i = 0x100U; //大致设定elcdf复位信号的持续时间
GPIO5->GDIR |= (1 << 9); //设置GPIO5_09为输出模式
GPIO5->DR &= ~(1 << 9); //设置GPIO5_09输出电平为低电平
/*等待一段时间,elcdf复位信号保持一段时间*/
while (i--)
{
}
GPIO5->DR |= (1 << 9); //设置GPIO5_09输出电平为高电平。 完成elcdf复位
/* 设置 LCD D的背光控制引脚为高电平 */
CCM_CCGR1_CG13(0x3); //开启GPIO1的时钟
GPIO1->GDIR |= (1 << 8); //设置GPIO1_08为输出模式
GPIO1->DR |= (1 << 8); //设置GPIO1_08输出电平为高电平
}
/*复位 elcdf*/
void ELCDIF_Reset(void)
{
volatile uint32_t i = 0x100;
/* Disable the clock gate. */
LCDIF->CTRL_CLR = LCDIF_CTRL_CLKGATE_MASK;
/* Confirm the clock gate is disabled. */
while (LCDIF->CTRL & LCDIF_CTRL_CLKGATE_MASK)
{
}
/* Reset the block. */
LCDIF->CTRL_SET = LCDIF_CTRL_SFTRST_MASK;
/* Confirm the reset bit is set. */
while (!(LCDIF->CTRL & LCDIF_CTRL_SFTRST_MASK))
{
}
/* Delay for the reset. */
while (i--)
{
}
/* Bring the module out of reset. */
LCDIF->CTRL_CLR = LCDIF_CTRL_SFTRST_MASK;
/* Disable the clock gate. */
LCDIF->CTRL_CLR = LCDIF_CTRL_CLKGATE_MASK;
}
/*将 elcdf 初始化为 rgb 888 模式
*/
void ELCDIF_RgbModeInit(void)
{
CCM_CCGR3_CG5(0x3); //开启GPIO5的时钟
/* Reset. */
ELCDIF_Reset();
LCDIF->CTRL &= ~(0x300); //根据颜色格式设置 CTRL 寄存器 颜色个事为RGB888
LCDIF->CTRL |= (0x3 << 8);
LCDIF->CTRL &= ~(0xC00); //设置数据宽度为24位宽
LCDIF->CTRL |= (0x3 << 10);
LCDIF->CTRL |= (0x20000); // 选择 RGB 模式
LCDIF->CTRL |= (0x80000); // 选择 RGB 模式 开启显示
LCDIF->CTRL |= (0x20); //设置elcdf接口为主模式
LCDIF->CTRL1 &= ~(0xF0000); //清零32位数据有效位
LCDIF->CTRL1 |= (0x07 << 16); // 设置32位有效位的低24位有效。
// LCDIF->TRANSFER_COUNT = 0;//清零分辨率设置寄存器
LCDIF->TRANSFER_COUNT |= APP_IMG_HEIGHT << 16; //设置一列 像素数 480
LCDIF->TRANSFER_COUNT |= APP_IMG_WIDTH << 0; //设置一行 像素数 800
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_ENABLE_PRESENT_MASK; //生成使能信号
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_VSYNC_PERIOD_UNIT_MASK; //设置VSYNC周期 的单位为显示时钟的时钟周期
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH_UNIT_MASK; //设置VSYNC 脉冲宽度的单位为显示时钟的时钟周期
LCDIF->VDCTRL0 |= (1 << 24); //设置 数据使能信号的有效电平为高电平
LCDIF->VDCTRL0 &= ~(0x8000000); //设置 VSYNC 有效电平为低电平
LCDIF->VDCTRL0 &= ~(0x4000000); //设置HSYNC有效电平为低电平
LCDIF->VDCTRL0 |= (0x2000000); // 设置在时钟的下降沿输出数据,在时钟的上升沿捕获数据。
LCDIF->VDCTRL0 |= APP_VSW;
// 以显示时钟为单位的周期。
LCDIF->VDCTRL1 = APP_VSW + APP_IMG_HEIGHT + APP_VFP + APP_VBP; //设置VSYNC 信号周期
LCDIF->VDCTRL2 |= (APP_HSW << 18); //HSYNC 信号有效电平长度
LCDIF->VDCTRL2 |= (APP_HFP + APP_HBP + APP_IMG_WIDTH + APP_HSW); //HSYNC 信号周期
LCDIF->VDCTRL3 |= (APP_HBP + APP_HSW) << 16;
LCDIF->VDCTRL3 |= (APP_VBP + APP_VSW);
LCDIF->VDCTRL4 |= (0x40000);
LCDIF->VDCTRL4 |= (APP_IMG_WIDTH << 0);
LCDIF->CUR_BUF = (uint32_t)s_frameBuffer[0];
LCDIF->NEXT_BUF = (uint32_t)s_frameBuffer[0];
}
// void APP_ELCDIF_Init(void)
// {
// lcdif_pin_config();
// ELCDIF_RgbModeInit(APP_ELCDIF);
// }
void APP_FillFrameBuffer(uint32_t frameBuffer[APP_IMG_HEIGHT][APP_IMG_WIDTH])
{
/* Background color. */
static const uint32_t bgColor = 0U;
/* Foreground color. */
static uint8_t fgColorIndex = 0U;
static const uint32_t fgColorTable[] = {0x000000FFU, 0x0000FF00U, 0x0000FFFFU, 0x00FF0000U,
0x00FF00FFU, 0x00FFFF00U, 0x00FFFFFFU};
uint32_t fgColor = fgColorTable[fgColorIndex];
/* Position of the foreground rectangle. */
static uint16_t upperLeftX = 0U;
static uint16_t upperLeftY = 0U;
static uint16_t lowerRightX = (APP_IMG_WIDTH - 1U) / 2U;
static uint16_t lowerRightY = (APP_IMG_HEIGHT - 1U) / 2U;
static int8_t incX = 1;
static int8_t incY = 1;
/* Change color in next forame or not. */
static unsigned char changeColor = false;
uint32_t i, j;
/* Background color. */
for (i = 0; i < APP_IMG_HEIGHT; i++)
{
for (j = 0; j < APP_IMG_WIDTH; j++)
{
frameBuffer[i][j] = bgColor;
}
}
/* Foreground color. */
for (i = upperLeftY; i < lowerRightY; i++)
{
for (j = upperLeftX; j < lowerRightX; j++)
{
frameBuffer[i][j] = fgColor;
}
}
/* Update the format: color and rectangle position. */
upperLeftX += incX;
upperLeftY += incY;
lowerRightX += incX;
lowerRightY += incY;
changeColor = false;
if (0U == upperLeftX)
{
incX = 1;
changeColor = true;
}
else if (APP_IMG_WIDTH - 1 == lowerRightX)
{
incX = -1;
changeColor = true;
}
if (0U == upperLeftY)
{
incY = 1;
changeColor = true;
}
else if (APP_IMG_HEIGHT - 1 == lowerRightY)
{
incY = -1;
changeColor = true;
}
if (changeColor)
{
fgColorIndex++;
if (ARRAY_SIZE(fgColorTable) == fgColorIndex)
{
fgColorIndex = 0U;
}
}
}
- 第一部分:初始化 PLL5(Video PLL)的时钟输出
设置 PLL5 我们需要设置 5 个寄存器的值(控制寄存器除外)。
- PLL5 分数分频分子寄存器
CCM_ANALOG_PLL_VIDEO_NUM
,为精确的控制输出时钟。记为 numerator。
CCM_ANALOG->PLL_VIDEO_NUM &= (0x3 << 30); //清零PLL 分数分频的分子寄存器
- PLL5 分数分频分母寄存器
CCM_ANALOG_PLL_VIDEO_DENOM
,它与分数分频分子寄存器作用相同,都是为使 PLL5 输出时钟更灵活、精确。记为 denominator。
CCM_ANALOG->PLL_VIDEO_DENOM &= (0x3 << 30); //清零PLL 分数分频的分母寄存器
- PLL5 时钟分频寄存器
CCM_ANALOG_PLL_VIDEO_DENOM[DIV_SELECT]
,时钟的名字是根据《IMX6ULRM》(参考手册)翻译得到,准确的说这个寄存器是用于设置 PLL 的倍频系数。记为 DIV_SELECT。
CCM_ANALOG->PLL_VIDEO = 0; // 清零PLL_VIDEO[POST_DIV_SELECT]
- PLL5 POST 分频寄存器
CCM_ANALOG_PLL_VIDEO_DENOM [POST_DIV_SELECT]
,这个寄存器才是真正用于分频的。PLL5 时钟产生之后经过该寄存器分频才能输出到外部。实际的分频值还要参考CCM_ANALOG_MISC2[VIDEO_DIV]
寄存器。
CCM_ANALOG->PLL_VIDEO &= ~(0x3 << 19); // 清零PLL_VIDEO[POST_DIV_SELECT]
CCM_ANALOG->PLL_VIDEO |= (0x01 << 19); //设置分频系数为2
- 杂项寄存器
CCM_ANALOG_MISC2[VIDEO_DIV]
,它与 PLL5 POST 分频寄存器CCM_ANALOG_PLL_VIDEO_DENOM [POST_DIV_SELECT]
,配合使用,用于设置实际的 PLL 分频值,如下表所示。
CCM_ANALOG_PLL_VIDEO_DENOM寄存器值 | CCM_ANALOG_MISC2寄存器值 | PLL5 输出前的分频值 |
---|---|---|
2 | 0 | 1 |
1 | 0 | 2 |
2 | 3 | 4 |
1 | 3 | 8 |
0 | 3 | 16 |
从上表中可以看出 CCM_ANALOG_PLL_VIDEO_DENOM [POST_DIV_SELECT]
寄存器和 CCM_ANALOG_PLL_VIDEO_DENOM [POST_DIV_SELECT]
寄存器共同决定了 PLL 输出前的最后一次分频的分频系数。方便书写这里记为:
PLL5 的输出频率计算公式为 PLL5 = OSC24M * (DIV_SELECT + (denominator / numerator)) / postDivider
当 numerator = 0、numerator = 0、CCM_ANALOG_PLL_VIDEO_DENOM [POST_DIV_SELECT] = 2、 CCM_ANALOG_MISC2[VIDEO_DIV] = 3、DIV_SELECT = 0x1F,最终得到的 PLL5 输出频率为 PLL5 = 24 * (0x1F + 0) / 8 = 93MHz。
- 第二部分:设置从 PLL5 输出到 elcdf 根时钟所经过的时钟选择寄存器和时钟分频寄存器
PLL5 输出的时钟要经过过两个时钟选择寄存器和两个时钟分频寄存器,其中时钟选择寄存器的设置是固定的(对于本例程要选择 PLL5),而时钟分频根据自己需要即可。
/*设置从PLL5 到 eLCIF 根时钟所经过的时钟选择和时钟分频寄存器*/
CCM->CSCDR2 &= ~(0x07 << 15); //清零
CCM->CSCDR2 |= (0x02 << 15); //设置CSCDR2[LCDIF1_PRE_CLK_SEL] 选择 PLL5 输出时钟
CCM->CSCDR2 &= ~(0x07 << 12); //清零
CCM->CSCDR2 |= (0x01 << 12); //设置 CSCDR2[LCDIF1_PRED]时钟分频值
CCM->CBCMR &= ~(0x07 << 23); //清零CBCMR[LCDIF1_PODF] 时钟分频值
CCM->CBCMR |= (0x01 << 23);
CCM->CSCDR2 &= ~(0x07 << 9); //清零
CCM->CSCDR2 |= (0x00 << 9); //选择 CSCDR2[LCDIF1_CLK_SEL] 选择 PLL5 输出时钟
- 第三部分:复位 LCD 并开启背光
LCD 有一个复位引脚,正常情况下为高电平,低电平将复位 LCD。这部分代码就是在初始化 LCD 之前向 LCD 发送一个复位信号,并开启 LCD 的背光。
/* 初始化 LCD_DISP,点亮 显示屏 */
void BOARD_InitLcd(void)
{
volatile uint32_t i = 0x100U; //大致设定elcdf复位信号的持续时间
GPIO5->GDIR |= (1 << 9); //设置GPIO5_09为输出模式
GPIO5->DR &= ~(1 << 9); //设置GPIO5_09输出电平为低电平
/*等待一段时间,elcdf复位信号保持一段时间*/
while (i--)
{
}
GPIO5->DR |= (1 << 9); //设置GPIO5_09输出电平为高电平。 完成elcdf复位
/* 设置 LCD D的背光控制引脚为高电平 */
CCM_CCGR1_CG13(0x3); //开启GPIO1的时钟
GPIO1->GDIR |= (1 << 8); //设置GPIO1_08为输出模式
GPIO1->DR |= (1 << 8); //设置GPIO1_08输出电平为高电平
}
- 第四部分:eLCDIF 接口初始化
主要是将 LCD 显示屏的一些参数填入到 eLCDIF 相关的配置寄存器。大致分为了五部分:
- 开启 eLCDIF 外设的时钟,并复位 eLCDIF,之后修改 CTRL 控制寄存器配置 eLCDIF 基本配置信息。从上到下依次设置颜色格式为 RGB888、数据宽度为 24 位、选择 RGB 模式、选择有效的数据位。
CCM_CCGR3_CG5(0x3); //开启GPIO5的时钟
ELCDIF_Reset(); //复位LCD
LCDIF->CTRL &= ~(0x300); //根据颜色格式设置 CTRL 寄存器 颜色个事为RGB888
LCDIF->CTRL |= (0x3 << 8);
LCDIF->CTRL &= ~(0xC00); //设置数据宽度为24位宽
LCDIF->CTRL |= (0x3 << 10);
LCDIF->CTRL |= (0x20000); // 选择 RGB 模式
LCDIF->CTRL |= (0x80000); // 选择 RGB 模式 开启显示
LCDIF->CTRL |= (0x20); //设置elcdf接口为主模式
LCDIF->CTRL1 &= ~(0xF0000); //清零32位数据有效位
LCDIF->CTRL1 |= (0x07 << 16); // 设置32位有效位的低24位有效。
- 设置与显示屏相关的配置参数。设置显示屏的分辨率。修改时最高修改分辨率对应的宏定义,因为其他地方还会用到分辨率。使能 RGB 模式并设置 VSYNC 信号周期的单位(行同步信号数或 pix_clk 时钟周期数)以及 VSYNC 信号脉宽的单位(同上),RGB 接口应当设置单位为“行同步信号周期数”。设置VSYNC有效电平以及数据采集的边缘。实际应用中我们很少修改这部分内容。
LCDIF->TRANSFER_COUNT |= APP_IMG_HEIGHT << 16; //设置一列 像素数 480
LCDIF->TRANSFER_COUNT |= APP_IMG_WIDTH << 0; //设置一行 像素数 800
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_ENABLE_PRESENT_MASK; //生成使能信号
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_VSYNC_PERIOD_UNIT_MASK; //设置VSYNC周期 的单位为显示时钟的时钟周期
LCDIF->VDCTRL0 |= LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH_UNIT_MASK; //设置VSYNC 脉冲宽度的单位为显示时钟的时钟周期
LCDIF->VDCTRL0 |= (1 << 24); //设置 数据使能信号的有效电平为高电平
LCDIF->VDCTRL0 &= ~(0x8000000); //设置 VSYNC 有效电平为低电平
LCDIF->VDCTRL0 &= ~(0x4000000); //设置HSYNC有效电平为低电平
LCDIF->VDCTRL0 |= (0x2000000); // 设置在时钟的下降沿输出数据,在时钟的上升沿捕获数据。
LCDIF->VDCTRL0 |= APP_VSW;
- 设置显示屏相关配置参数设置传输一帧所用的的时间单位是传输“一行”像素所用的时间,或者说是行同步信号周期。设置 VSYNC 信号宽度,设置 HSYNC(行同步信号周期),单位为同步时钟 CLK 的个数。设置行同步信号之后等待的时间(单位为同步时钟 CLK 的个数) 、列同步信号之后等待时间(单位为行同步信号周期)。
// 以显示时钟为单位的周期。
LCDIF->VDCTRL1 = APP_VSW + APP_IMG_HEIGHT + APP_VFP + APP_VBP; //设置VSYNC 信号周期
LCDIF->VDCTRL2 |= (APP_HSW << 18); //HSYNC 信号有效电平长度
LCDIF->VDCTRL2 |= (APP_HFP + APP_HBP + APP_IMG_WIDTH + APP_HSW); //HSYNC 信号周期
LCDIF->VDCTRL3 |= (APP_HBP + APP_HSW) << 16;
LCDIF->VDCTRL3 |= (APP_VBP + APP_VSW);
LCDIF->VDCTRL4 |= (0x40000);
LCDIF->VDCTRL4 |= (APP_IMG_WIDTH << 0);
- 设置当前显存地址和下一个缓存地址。我们把它设置为同一个,如果有需要在后面的程序中再修改。
LCDIF->CUR_BUF = (uint32_t)s_frameBuffer[0];
LCDIF->NEXT_BUF = (uint32_t)s_frameBuffer[0];
11.2 main.c
在 /elcdif
下创建 main.c
。
初始化完成后我们只需要填充显存并启动显示即可,程序中我们开启帧传输完成中断,在中断服务函数中更新传输状态。
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
#include "system_MCIMX6Y2.h"
#include "clock.h"
#include "elcdif.h"
static volatile unsigned char s_frameDone = false; // elcdf 帧传输状态
extern uint32_t s_frameBuffer[2][APP_IMG_HEIGHT][APP_IMG_WIDTH]; // elcdif 显存
/*简单延时函数*/
void delay(uint32_t count)
{
volatile uint32_t i = 0;
for (i = 0; i < count; ++i)
{
__asm("NOP"); /* 调用nop空指令 */
}
}
/*
* elcdf 帧传输完成中断
*/
void APP_LCDIF_IRQHandler(void)
{
uint32_t intStatus = 0;
/*获取传输完成中断的状态,*/
intStatus = ((LCDIF->CTRL1) & (1 <<9));
/*清除 1 帧传输完成中断标志位*/
LCDIF->CTRL1_CLR = (1 << 9);
if (intStatus)
{
s_frameDone = true;
}
}
int main()
{
uint32_t frameBufferIndex = 0;
system_clock_init();
lcdif_pin_config(); //初始 lcdif 引脚
lcdif_clock_init(); //初始化时钟
BOARD_InitLcd(); //复位LCD
SystemInstallIrqHandler(LCDIF_IRQn, (system_irq_handler_t)(uint32_t)APP_LCDIF_IRQHandler, NULL); // 设置中断服务函数
ELCDIF_RgbModeInit(); // 初始化 elcdf 位 RGB 888 模式
GIC_EnableIRQ(LCDIF_IRQn); //开启中断
APP_FillFrameBuffer(s_frameBuffer[frameBufferIndex]);
LCDIF->CTRL1_SET |= (0x2000); // 使能 elcdf 一帧传输完成中断
LCDIF->CTRL_SET |= 0x1;//开启 elcdf 开始显示
LCDIF->CTRL_SET |= (1 << 17);
while (1)
{
frameBufferIndex ^= 1U;
APP_FillFrameBuffer(s_frameBuffer[frameBufferIndex]);
LCDIF->NEXT_BUF = (uint32_t)s_frameBuffer[frameBufferIndex];
s_frameDone = false;
/* Wait for previous frame complete. */
while (!s_frameDone)
{
}
}
return 0;
}
- 第一部分:定义帧传输状态变量和显存
static volatile unsigned char s_frameDone = false; // elcdf 帧传输状态
extern uint32_t s_frameBuffer[2][APP_IMG_HEIGHT][APP_IMG_WIDTH]; // elcdif 显存
- 第二部分:定义帧传输完成中断的中断服务函数
和 STM32 相似,在中断服务函数中检测中断状态并清除中断标志,如果是帧传输完成中断则设置 s_frameDone 变量为真。
/*
* elcdf 帧传输完成中断
*/
void APP_LCDIF_IRQHandler(void)
{
uint32_t intStatus = 0;
/*获取传输完成中断的状态,*/
intStatus = ((LCDIF->CTRL1) & (1 <<9));
/*清除 1 帧传输完成中断标志位*/
LCDIF->CTRL1_CLR = (1 << 9);
if (intStatus)
{
s_frameDone = true;
}
}
- 第三部分:系统时钟初始化
system_clock_init();
- 第四部分:初始化eLCDIF
初始化引脚、时钟、以及 eLCDIF 的配置参数。初始化 eLCDIF 帧传输完成中断,使能中断。
lcdif_pin_config(); //初始 lcdif 引脚
lcdif_clock_init(); //初始化时钟
BOARD_InitLcd(); //复位LCD
SystemInstallIrqHandler(LCDIF_IRQn, (system_irq_handler_t)(uint32_t)APP_LCDIF_IRQHandler, NULL); // 设置中断服务函数
ELCDIF_RgbModeInit(); // 初始化 elcdf 位 RGB 888 模式
GIC_EnableIRQ(LCDIF_IRQn); //开启中断
- 第五部分:使用 APP_FillFrameBuffer 函数填充显存,之后开启传输完成中断
第四部分设置的是系统中断,表示某一个中断号对应的中断被使能。这里设置的是 eLCDIF 可以产生中断请求。一切准备就绪设置 CTRL_SET 控制寄存器开启 eLCDIF 显示。
APP_FillFrameBuffer(s_frameBuffer[frameBufferIndex]);
LCDIF->CTRL1_SET |= (0x2000); // 使能 elcdf 一帧传输完成中断
LCDIF->CTRL_SET |= 0x1; //开启 elcdf 开始显示
LCDIF->CTRL_SET |= (1 << 17);
- 第六部分:在 while(1) 中循环改变显存内容,不断更新显示
while (1)
{
frameBufferIndex ^= 1U;
APP_FillFrameBuffer(s_frameBuffer[frameBufferIndex]);
LCDIF->NEXT_BUF = (uint32_t)s_frameBuffer[frameBufferIndex];
s_frameDone = false;
/* Wait for previous frame complete. */
while (!s_frameDone)
{
}
}
十二、编译下载验证
12.1 编译代码
make
执行make命令,生成base.bin文件。
12.2 代码烧写
编译成功后会在当前文件夹下生成.bin文件,这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。
为二进制文件添加头部信息并烧写到SD卡。查看 IMX6ULL学习笔记(12)——通过SD卡启动官方SDK程序
进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径>
命令,例如要烧写的 base.bin 位于 home 目录下,则烧写命令为 ./mkimage.sh /home/button.bin
。
执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。
12.3 实验现象
上电后显示屏右下角会显示一个蓝色方块,并不断向左上角移动。如果显示不正常显示请断电后重新插拔显示屏连接线,确保接触良好。
• 由 Leung 写于 2023 年 4 月 15 日
• 参考:14. eLCDIF—液晶显示