15_LCD编程

文章目录[隐藏]

第十五章 LCD编程

15.1 LCD硬件原理

15.2.1 LCD硬件工作原理简介

​ [外链图片转存中…(img-lRChXkF3-1642060379771)]

​ 假设上图是一个LCD屏幕,屏幕中一个一个密密麻麻的黑点称之为像素点,每一行有若干个点,试想下有一个电子枪,电子枪位于某一个像素点的背后,然后向这个像素发射红,绿,蓝三种原色,这三种颜色不同比例的组合成任意一种颜色。电子枪在像素点的背后,一边移动一边发出各种颜色的光,电子枪从左往右移动,到右边边缘之后就跳到下一行的行首,继续从左往右移动,如此往复,一直移动到屏幕右下角的像素点,最后就跳回原点。

​ 问题1:电子枪如何移动?

​ 答: 有一条像素时钟信号线(DCLK),连接屏幕,每来一个像素时钟信号(DCLK),电子枪就移动一个像素。

​ 问题2:电子枪打出的颜色该如何确定?

​ 答:有三组红,绿,蓝信号线(RGB),连接屏幕,由这三组信号线(RGB)确定颜色

​ 问题3:电子枪移动到LCD屏幕右边边缘时,如何得知需要跳到下一行的行首?

​ 答:有一条水平同步信号线(HSYNC),连接屏幕,当接收到水平同步信号(HSYNC),电子枪就跳到下一行的最左边

​ 问题4:电子枪如何得知需要跳到原点?

​ 答:有一条垂直同步信号线(VSYNC),连接屏幕,当接收到垂直同步信号线(VSYNC),电子枪就由屏幕右下脚跳到左上角(原点)

​ 问题5:电子枪如何得知三组信号线(RGB)确定的颜色就是它是需要的呢?

​ 答:有一条RGB数据使能信号线(DE),连接屏幕,当接收到数据使能信号线(DE),电子枪就知道这时由这三组信号线(RGB)确定的颜色是有效的,可以发射到该像素点。

​ 下图是开发板,LCD控制器,LCD屏幕的框图

image-20220112145025755

​ 之前提到的像素时钟(DCLK), 三组红,绿,蓝信号线(RGB),水平同步信号线(HSYNC),垂直同步信号线(VSYNC),RGB数据使能信号线(DE)都是从LCD控制器发出的,只要开发板支持LCD显示,他肯定就会有一个LCD控制器。

​ 问题6:RGB三组信号线上的数据从何而来?

image-20220112145046895

​ 上图是RGB数据来源框图,内存中划出一部分区域,这块区域成为Framebuffer,在这个Framebuffer里面我们会构造好每一个颜色所对应的像素,Framebuffer中的值会被LCD控制器读出来,通过RGB三组线传给电子枪,电子枪再它转换成红绿蓝三种颜色打到屏幕上,在屏幕上的每一个像素,在我们的Frambuffer里面都有一个对应存储空间,里面存有屏幕上对应像素的颜色。

​ 我们的LCD控制器会周而复始的从Framebuffer中取出一个像素的颜色值,发给电子枪,同时需要和DCLK,VSYNC,HSYNC,DE这些信号配合好。

15.2.2 RGB接口的LCD硬件连接信号

​ 本次实验编程的屏幕属于RGB接口的显示屏,RGB接口的显示屏至少具备以下信号:

(1)像素时钟信号(DCLK)

​ 像素时钟信号,用于同步LCD上的DE,VS,HS,RGB信号线。

(2)RGB数据信号(R[0:7] ,G[0:7],B[0:7])

​ 三组信号线组成,分别代表R(红色),G(绿色),B(蓝色),这三组信号中的每一组都会有8根信号,三组共同组成24根线来控制颜色数据。

(3)RGB数据使能信号(DE)

​ RGB接口的 LCD 有两种驱动模式DE 模式和 HV 模式, 在HV模式下,需要用到HS与VS同伴RGB数据,在DE模式下,则只需要DE信号同伴RGB数据,但是一般做LCD显示程序,都会兼容两种模式,所以一般都要将数据使能信号(DE),垂直同步信号(HS),水平同步信号(VS)一起使用。

(4)水平同步信号,

​ 电路中常用HS或HSYNC表示,详细说明下一小节会说明。

(5)垂直同步信号(帧同步或场同步)

​ 电路中常用VS或VSYNC表示,相信说明下一小节会说明。

(6)LCD背光电源控制信号

​ 一般是由普通GPIO控制(利用高低电平控制背光),背光就是在在LCD显示屏的背部一大串的灯珠,用它们来照亮屏幕。

​ 例如100ASK_IMX6ULL开发板的LCD接口定义,就包含了上面所述的几种信号类型:

image-20220112145210913

15.2.3 TFT材质液晶屏接口简介(7寸1024600TN-RGB)

​ 嵌入式一般都采用TFT材质的液晶屏,如遇到别的材质的屏幕,操作方法也是雷同,可能稍微有些差异,针对差异去做修改即可,7寸1024600TN-RGB液晶屏幕接口引脚如下图,一些关键的引脚做了注释。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNh3YKOO-1642060379773)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LCD_program_bare_metal_image-20220112145308136.png)]

15.2.4 LCD关键特性

①信号时序与信号的极性

​ 接下来我们查看下100ASK_7.0寸LCD手册时序图

image-20220112145317135

​ 从最小的像素开始分析,电子枪每次在CLK下降沿,如上图所示,该LCD在像素时钟下降沿采集数据,从数据线上得到数据,发射到显示屏上,然后移动到下一个位置。Dn0-Dn7上的数据来源就是前面介绍的FrameBuffer。就这样从一行的最左边,一直移动到一行的最右边,完成了一行的显示,假设为x。

​ 当打完一行的最后一个数据后,就会收到Hsync行同步信号,如上图可知该LCD的HSD有效脉冲为低脉冲,根据时序图,一个HSD周期可以大致分为五部分组成:thp、thb、thd、thf。thpw称为脉冲宽度,这个时间不能太短,太短电子枪可能识别不到。电子枪正确识别到thpw后,会从最右端移动最左端,这个移动的时间就是thb,称之为移动时间。thfp表示显示完最右像素,再过多久HSD才来,thd为数据有效区,th为打完一行所需要的时间。

image-20220112145335485

​ 同理,当电子枪一行一行的从上面移动到最下面时,VSD垂直同步信号,如上图可知该LCD的VSD有效脉冲为低脉冲。然后就让电子枪移动回最上边。VSD中的tvpw是脉冲宽度,tvb是移动时间,tvfp表示显示完最下一行像素,再过多久VSD才来,tvd为数据有效区,tv为打完一帧所需要的时间。假设一共有y行,则LCD的分辨率就是x*y。

[外链图片转存中…(img-EtHcBoO3-1642060379775)]

​ RGB数据有效信号(DEN),高电平表示数据有效。

根据以上信息大致了解几个关键信号的时序和极性,后面章节会详细介绍。

image-20220112145347564

​ 再根据上图,我们就可以确定像素时钟是51.2Mhz。

②RGB数据的存放形式

​ 前面的LCD硬件接口,R0-R7、G0-G7、B0-B7,每个像素是占据3*8=24位的,即硬件上LCD的BPP是确定的。虽然硬件引脚连接是固定的,但我们使用的时候,可以根据实际情况进行取舍,比如我们的IMX6ULL开发板,可以让他支持不同的像素格式,ARGB888,ARGB555,RGB565等等,

​ 本实验支持ARGB888和ARGB555。

ARGB888:每个像素就占据32位数据,其中最高字节A表示灰度透明度其余RGB数据8+8+8=24BPP。

ARGB555:每个像素就占据16位数据,其中最高位A表示灰度透明度其余RGB数据5+5+5=15BPP。

15.2 IMX6ULL LCD控制器操作及寄存器

15.2.1 LCD控制器模块介绍

​ IMX6ULL的LCD控制器名称为elcdif(增强型LCD接口)主要特性如下:

​ a.支持MPU模式,针对显示屏内部有显存的显示屏;

​ b.支持DOTCLK模式,针对RGB接口使用,本实验就是此模式;

​ c.VSYNC模式,针对高速数据传输(行场信号);

​ d. 8/16/18/24/32 bit 的bpp数据都支持,取决于IO的复用设置及寄存器配置;

​ e.MPU模式,VSYNC模式,DOTCLK模式,都具有配置的时序参数;

image-20220112145424746

​ 上图是IMX6ULL的LCD控制器框图,AXI是一种总线协议,通过此总线将显存中的RGB数据写入到FIFO,经过FIFO过度,到达LCD接口,LCD控制器分两个时钟域,一个是外设总线时钟域,一个是LCD像素时钟域,前者是用于让LCD控制器正常工作的时钟,后者是控制电子枪移动速度的时钟。Read_Data操作工作在MPU模式,我们采用的是DCLK模式,因此不予考虑。

​ 以上只是介绍了部分,如需要更加详细的了解需要查看IMX6ull芯片手册《Chapter 34

​ Enhanced LCD Interface (eLCDIF)》

15.2.2 LCD控制器寄存器简介

[外链图片转存中…(img-Qpqmd64m-1642060379778)]

​ 上图是我们将要使用到的寄存器,接着将会大致讲解下使用到的寄存器,更加详细说明会在后续的LCD控制编程实验中提及。

15.2.2.1 ①LCDIF_CTRL寄存器:

image-20220112145500174

SFTRST:软复位,用于修改像素时钟后,进行复位同步时钟;

BYPASS_COUNT:DOTCLK和DVI modes都需要设置为1;

DOTCLK_MODE:设置为1,进入DOTCLK模式;

LCD_DATABUS_WIDTH:RGB数据总线,跟进数据总线宽度设置;

WORD_LENGTH:输入的RBG数据格式,即多少位表示一个像素;

MASTER:LCD控制器主机模式设置;

DATA_FORMAT_16_BIT:当设置为16BPP的时候需要设置该位

15.2.2.2 ②LCDIF_CTRL1寄存器:

image-20220112145520234

BYTE_PACKING_FORMAT:用于表示4字节RGB数据中,那几个字节是属于有有效数据,因为其中有一个字节表示A(灰度,透明度)

15.2.2.3 ③LCDIF_TRANSFER_COUNT寄存器:

image-20220112145543927

V_COUNT:表示垂直方向上的像素个数,即分辨率中的x;

H_COUNT:表示水平方向上的像素个数,即分辨率中的y;

15.2.2.4 ④LCDIF_VDCTRL0寄存器

image-20220112145551594

VSYNC_PULSE_WIDTH:垂直同步信号的宽度;

VSYNC_PULSE_WIDTH_UNIT:根据不同模式下的计算时钟方式来决定垂直同步信号宽度;

VSYNC_PERIOD_UNIT:根据不同模式下的计算时钟方式来发垂直同步信号;

ENABLE_POL:DE数据有效信号的极性,即有效电平极性;

DOTCLK_POL:像素时钟信号的极性,即有效电平极性;

HSYNC_POL:水平同步信号的极性,即有效电平极性;

VSYNC_POL:垂直同步信号的极性,即有效电平极性;

ENABLE_PRESENT:在DOTCLK模式下,是否会硬件产生ENABLE使能数据信号;

VSYNC_OEB:VSYNC设置为输出还是输入模式,我们选择输出模式;

15.2.2.5 ⑤LCDIF_VDCTRL1寄存器

image-20220112145558654

VSYNC_PERIOD:两个垂直同步信号之间的总数,即垂直方向同步信号的总周期;

15.2.2.6 ⑥LCDIF_VDCTRL2寄存器

image-20220112145602359

HSYNC_PULSE_WIDTH:水平同步信号脉冲宽度;

HSYNC_PERIOD:两个水平同步信号之间的总数,即水平方向同步信号的总周期

15.2.2.7 ⑦LCDIF_VDCTRL3寄存器

image-20220112145625980

HRIZONTAL_WAIT_CNT:水平方向上的等待像素个数;

VERTICAL_WAIT_CNT:垂直方向上的等待像素个数;

15.2.2.8 ⑧LCDIF_VDCTRL4寄存器

image-20220112145632883

SYNC_SIGNALS_ON:工作在DOTCLK模式下,需要设置为1;

DOTCLK_H_VALID_DATA_CNT:DOTCLK模式下,水平方向上的有效像素点个数,即分辨率的y;

15.2.2.9 ⑨LCDIF_CUR_BUF寄存器

image-20220112145636601

ADDR:通过LCD控制器,发送的当前帧地址;

15.2.2.10 ⑩LCDIF_NEXT_BUF寄存器

image-20220112145651586

ADDR:通过LCD控制器,发送的下一帧地址;

15.3 编程_框架与准备

​ 本节文档对应的视频是《第003节_LCD编程_框架与准备_P》。

15.3.3 功能目的

​ 我们最终的目的是在LCD显示屏上画线、画圆和写字,此外还需要一个测试程序提供操作菜单,调用画线、画圆和写字操作,这些终究其核心是画点,我们需要实现画点才能实现其他功能,但是画点前也要让我们的LCD控制器正常工作起来才能实现它,最终总结:先让LCD控制器正常工作(配置寄存器),再编写画点的函数。

15.3.4 编程框架

​ 接着我们就需要实现画点,在实现画点之前想两个问题:

​ ①有两款尺寸大小的LCD显示屏,如何快速的在两个lcd上切换?

​ ②有两款不同的CPU都需要显示同一款LCD显示屏,如何快速的在两个cpu上切换?

​ 为了让程序更加好扩展,下面介绍“面向对象编程”的概念

​ 我们发现LCD显示屏虽然不同尺寸,参数不同,但是它们终究是LCD显示屏,我们可以把他们归一类,当需要使用某款LCD显示屏的参数时就提供该款的参数,其他的LCD显示屏参数不管他不就可以了吗?

​ 同理不同的CPU虽然LCD控制器地址不同,操作也不同,但是它们终究是LCD控制器,我们可以把他们归一类,当确定使用某个LCD控制器的时候就用这个LCD控制器的操作,其他的LCD控制器不管他。

​ 下图是LCD编程的框架,尽可能的“高内聚低耦合”,即类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低

image-20220112145758887

​ 根据不同的LCD控制器特性,来设置不同的LCD控制器,对于我们开发板,就是imx6ull_con.c,假如希望在其它 开发板上也实现LCD显示,只需添加相应的代码文件xxx_con即可。

​ 根据不同的LCD屏幕特性,来编写不同的LCD屏幕参数,对于我们的开发板,就是lcd_7_0.c,假如希望这个开发板支持别的LCD屏幕,只需添加相应的代码文件lcd_xxx.c即可。

15.4 编程_抽象出重要结构体

​ 本节代码在裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_manager.h) 与**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_controller_manager.h)**头文件中,本节文档对应的视频是《第004节_LCD编程_抽象出重要结构体_P》。

15.4.1 抽象出LCD屏幕的结构体

​ 建立一个lcd_manager.h,将任意LCD都共有的参数(引脚的极性、时序、数据的格式bpp、分辨率等)使用面向对象的思维方式,将这些封装成结构体放在lcd_manager.h中

enum {
		NORMAL = 0,
		INVERT = 1,
	};

	/* NORMAL : 正常
	* INVERT : 取反
	*/
	typedef struct pins_polarity {
		int de;    /* normal: 高电平使能数据 */
		int vclk;  /* normal: 在下降沿获取数据 */
		int hsync; /* normal:高脉冲 */
		int vsync; /* normal:高脉冲  */
	}pins_polarity, *p_pins_polarity;

	typedef struct time_sequence {
		/* 垂直方向 */
		int tvp; /*  vysnc脉冲宽度 */
		int tvb; /*上边黑框 , Vertical Back porch */
		int tvf; /*下边黑框, Vertical Front porch */
	
		/* 水平方向 */
		int thp; /* hsync脉冲宽度 */
		int thb; /* 左边黑框 ,Horizontal Back porch */
		int thf; /* 右边黑框,Horizontal Front porch */

		int vclk;
	}time_sequence, *p_time_sequence;


	typedef struct lcd_params {
	
		char *name;
		
		/*引脚极性参数*/
		pins_polarity pins_pol;
	
		/*时序参数*/
		time_sequence time_seq;
	
		/*分辨率*/
		int xres;
		int yres;
		int bpp;
	
		/*显存*/
		unsigned int fb_base;
	
	}lcd_params, *p_lcd_params;

​ 以后就使用lcd_params结构体来表示lcd参数 ,通过register_lcd函数注册某款LCD屏幕参数到一个lcd_params结构体数组,然后通过select_lcd函数在lcd_params结构体数组中选中指定的LCD屏幕参数保存起来,提供给其他函数用。

15.4.2 抽象出LCD控制器的结构体

​ 建立一个lcd_controller_manager.h,将任意LCD控制器都共有的函数(初始化函数,使能函数等)使用面向对象的思维方式,将这些封装成结构体放在lcd_controller_manager.h中

typedef struct lcd_controller{
	char* name;
	void (*init)(p_lcd_params plcdparams);
	void(*enable)(void);
	void(*disable)(void);
}lcd_controller, *p_lcd_controller;

​ 以后就使用lcd_controller结构体来表示lcd控制器。

​ 建立一个lcd_controller_manager.c,提供一系列的LCD控制器的管理函数,用这些管理函数,通过register_lcd_controller函数注册新的LCD控制器到lcd_controller的结构体数组中,然后通过select_lcd_controller函数在lcd_controller结构体数组中选中指定的LCD控制器,提供给其他函数用,最终用户再调用光宇LCD控制器的操作时,就会通过选中的lcd_controller的结构体访问到对应的某款LCD控制器的函数。

15.5 编程_LCD控制器

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/imx6ull_con.c)**源文件中, 本节文档对应的视频是《第005节_LCD编程_LCD控制器_P》。

15.5.1 LCD控制器相关引脚复用配置

image-20220112145945035

​ 根据前面硬件接口章节15.1.3和上图,我们知道要设置这30个引脚,设置引脚按两步走,第一步设置引脚复用功能,第二部设置引脚的硬件属性。查看相应的寄存器得知,复用功能寄存器均设置为0即可,接着硬件属性根据章节《4-1.3 GPIO操作方法》的内容,均设置为0xB9即可。

15.5.2 LCD控制器像素时钟配置

​ 根据IMX6ULL芯片手册的Chapter 18 Clock Controller Module (CCM),我们就可以设置像素时钟为我们需要的51.2Mhz

15.5.2.1 ①确定PLL

image-20220112150005692

​ 由上图可知LCD控制器的时钟来源是PLL5(video pll)

[外链图片转存中…(img-1hb2DTWp-1642060379786)]

​ 根据上图可得知VIDEO pll的公式,但是为了方便计算,我们不需要后面的小数运算即Video PLL output frequency(PLL5)= Fref * (DIV_SELECT + 0)

15.5.2.2 ②确定PLL后的分频系数

image-20220112150053310

​ 根据上图可知PLL5出来后经过两级分频,即PLL5_MAIN_CLK = PLL5 / POST_DIV_SELECT / VIDEO_DIV

15.5.2.3 ③PLL分频后进入LCDIF控制器前的分频系数

image-20220112150121312

​ 根据上图可知PLL5分频后到LCDIF控制器也有两级分频,即LCDIF1_CLK_ROOT = PLL5_MAIN_CLK /LCDIF1_PRED / LCDIF1_PODF,根据上面三个内容,我们可以采用以下这个配置来达到像素时钟51.2Mhz,DIV_SELECT = 32;NUM = 0;DENOM = 0;POST_DIV_SELECT = 1,VIDEO_DIV = 1LCDIF1_PRED = 3;LCDIF1_PODF = 5

​ 带入时钟公式得24*(32+0)/1/1/3/5 ≈ 51.2Mhz。下面开始编程:

15.5.3 LCD控制器时钟编程

15.5.3.1 ①取消小数分配器

  CCM_ANALOG->PLL_VIDEO_NUM   = 0; 	 
  CCM_ANALOG->PLL_VIDEO_DENOM = 0;

​ 清零表示取消小数分配器

15.5.3.2 ②设置CCM_ANALOG_PLL_VIDEOn寄存器

image-20220112150211473

CCM_ANALOG->PLL_VIDEO =  (2 << 19) | (1 << 13) | (32<< 0);

​ 设置PLL5使能,倍频为32倍,1分频(即不分频),选择外部24M晶振为时钟源,至此PLL5分频后为24 * 32 / 1

15.5.3.3 ③设置CCM_ANALOG_MISC2n

image-20220112150247815

默认就为1分频,所以无需设置,至此PLL5分频后为24 * 32 / 1 / 1 = 768Mhz

15.5.3.4 ④设置CCM_CSCDR2

image-20220112150300024

设置选中对应的时钟源,预分频系数为3,得768 / 3 = 256Mhz

	CCM->CSCDR2 &= ~(7 << 15); 	   
	CCM->CSCDR2 |=  (2 << 15);  
	CCM->CSCDR2 &= ~(7 << 12); 	 
	CCM->CSCDR2 |=  (2 << 12); 						 
 	CCM->CSCDR2 &= ~(7 << 9);

15.5.3.5 ⑤设置CCM_CBCMR

image-20220112150322222

	CCM->CBCMR &= ~(7 << 23);					 
	CCM->CBCMR |=	4 << 23;	/*[25:23] :4 : 表示5分频*/

设置预分频系数为5,最终得到51.2Mhz

15.5.3.6 ⑥重新同步时钟

image-20220112150341565

/* 重新设置时钟后,需要软复位LCD控制器,让LCD控制器像素时钟同步*/
	 LCDIF->CTRL  = 1<<31;   

  /*软复位需要花费好几个时钟周期,这里需要一些时间等待*/
	 delay(100);

  /*同步像素时钟结束*/
	LCDIF->CTRL  = 0<<31; /* 取消复位 */

15.5.4 LCD控制器像素格式配置

15.5.4.1 ①设置LCDIF_CTRLn寄存器

[外链图片转存中…(img-89nWwH5E-1642060379788)]

	LCDIF->CTRL |= (1 << 19) | (1 << 17) |(3 << 10) | (bpp_mode << 8) | (1 << 5) ;

		/* [3]当bpp为16时,数据格式为ARGB555*/
	 	if(plcdparams->bpp == 16)
	 	{
	 		LCDIF->CTRL |= 1<<3;
	 	}

15.5.4.2 ②设置LCDIF_CTRL1n寄存器

image-20220112150501256

if(plcdparams->bpp == 24 || plcdparams->bpp == 32)
	 {		
	 		LCDIF->CTRL1 &= ~(0xf << 16); 
		 	LCDIF->CTRL1 |=  (0x7 << 16); 
	 } 

表示ARGB传输格式模式下,传输24位无压缩数据,A通道不用传输),当我们选用16bpp即ARGB555时,不需要设置此位。

15.5.5 LCD控制器时序配置及极性配置

15.5.5.1 ①设置LCDIF_TRANSFER_COUNT寄存器

image-20220112150610501

    ```c

LCDIF->TRANSFER_COUNT = (plcdparams->yres << 16) | (plcdparams->xres << 0);
```

15.5.5.2 ②设置LCDIF_VDCTRL0n寄存器

[外链图片转存中…(img-CyWxG1LO-1642060379789)]

LCDIF->VDCTRL0 = (1 << 28)|( plcdparams->pins_pol.vsync << 27)
   					|( plcdparams->pins_pol.hsync << 26)
   					|( plcdparams->pins_pol.vclk << 25)
   					|(plcdparams->pins_pol.de << 24)
   					|(1 << 21)|(1 << 20)|( plcdparams->time_seq.tvp << 0);

15.5.5.3 ③配置LCDIF_VDCTRL1寄存器

image-20220112150655412

LCDIF->VDCTRL1 = plcdparams->time_seq.tvb + plcdparams->time_seq.tvp + plcdparams->yres + plcdparams->time_seq.tvf;  

设置垂直方向的总周期:上黑框tvb+垂直同步脉冲tvp+垂直有效高度yres+下黑框tvf

15.5.5.4 ④配置LCDIF_VDCTRL2寄存器

image-20220112150744101

LCDIF->VDCTRL2 = (plcdparams->time_seq.thp << 18) | (plcdparams->time_seq.thb +  plcdparams->time_seq.thp + plcdparams->xres + plcdparams->time_seq.thf);

[18:31] : 水平同步信号脉冲宽度

[17: 0] : 水平方向总周期

设置水平方向的总周期:左黑框thb+水平同步脉冲thp+水平有效高度xres+右黑框thf

15.5.5.5 ⑤配置LCDIF_VDCTRL3寄存器

image-20220112150809660

LCDIF->VDCTRL3 = ((plcdparams->time_seq.thb + plcdparams->time_seq.thp) << 16) | (plcdparams->time_seq.tvb + plcdparams->time_seq.tvp);

设置ELCDIF的VDCTRL3寄存器

[27:16] :水平方向上的等待时钟数 =thb + thp

[15:0] : 垂直方向上的等待时钟数 = tvb + tvp

15.5.5.6 ⑥设置LCDIF_VDCTRL4寄存器

image-20220112150833971

LCDIF->VDCTRL4 = (1<<18) | (plcdparams->xres);

设置ELCDIF的VDCTRL4寄存器

[18] : 使用VSHYNC、HSYNC、DOTCLK模式此为置1

17:0: 水平方向的宽度

15.5.6 设置显存

image-20220112150949262

LCDIF->CUR_BUF  =  plcdparams->fb_base;
LCDIF->NEXT_BUF =  plcdparams->fb_base;

CUR_BUF : 当前显存地址

NEXT_BUF : 下一帧显存地址

方便运算,都设置为同一个显存地址

15.5.7 编程_LCD设置

本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_7_0.c)**源文件中 ,本节文档对应的视频是《第006节_LCD编程_LCD设置_P》

15.5.8 添加LCD屏幕名称

.name = "lcd_7.0",

先给我们本次实验的LCD屏幕参数一个名称,可以根据名称去选择我们需要的LCD屏幕参数。

15.5.9 极性设置

LCDIF_VDCTRL0n寄存器极性设置位

image-20220112151048189

VSYNC_POL与HSYNC_POL:0表示低电平有效,1表示高电平有效

DOTCLK_POL:0表示上升沿有效,1表示下降沿有效

ENABLE_POL:0表示低电平有效,1表示高电平有效

​ 接着根据15-1.4关键特性章节中,我们已经知道像素时钟DCLK是下降沿时获取数据,水平同步信号HSD和垂直同步信号VSD都是低电平有效,数据使能信号DEN是高电平表示数据有效,接着我们只要在lcd_7_0.c文件中设定成对应的标记用来识别即可,1)数据使能信号de设置为1, 2)像素时钟vclk极性设置为1 ,3)水平同步信号HSD和垂直同步信号VSD都设置为0。

enum {
   NORMAL = 0,
   INVERT = 1,
};

根据枚举内容,填入对应的参数即可

.pins_pol = {
	.de    = INVERT,	/* normal: 低电平表示使能输出 */
	.vclk  = INVERT,		/* normal: 在上升降沿获取数据*/
	.hsync = NORMAL,    	/* normal: 低脉冲*/
	.vsync = NORMAL, 	/* normal: 低脉冲*/
},

15.5.10 时序设置

image-20220112151141164

​ 观察上图中红色框框内容,接着我们只需要把上面数值往lcd_7_0.c中定义的lcd_7_0_params中填入对应的值即可

​ 1)Thpw:结构体中的tvb = 20

​ 2)Thb:结构体中的thb = 140

​ 3)Thfp:结构体中的thf = 160

​ 4)THA:结构体中的tvp = 1024

​ 5)Tvpw:结构体中的tvp = 3

​ 6)Tvb:结构体中的thp = 20

​ 7)Tvfp:结构体中的tvf = 12

​ 8)Tvd:结构体中的yres = 600

.time_seq = {
	/* 垂直方向 */
	.tvp=	3, /* vysnc脉冲宽度 */
	.tvb=	20,  /* 上边黑框, Vertical Back porch */
	.tvf=	12,  /* 下边黑框, Vertical Front porch */

	/* 水平方向 */
	.thp=	20, /* hsync脉冲宽度 */
	.thb=	140,  /* 左边黑框, Horizontal Back porch */
	.thf=	160,  /* 右边黑框, Horizontal Front porch */

	.vclk=	51.2,  /* MHz */
},
.xres = 1024,
.yres = 600

15.5.11 显存地址设置

image-20220112151220548

​ 根据上图imx_6ull可以挂接最大2GB的内存,可实际我们使用的100ask_imx_6ull开发板只挂接了512M的内存,因此我们可选的内存地址范围是0x800000000xa0000000。我们的裸机实验的链接地址是0x80100000,我们裸机实验程序不大,最多算它0x900000(实际没有这么多),那么我们显存可以用范围就变成0x810000000xa0000000,再计算我们需要的显存大小10246004=2457600≈2.4MB。

​ 总结:0x81000000~0xa0000000范围内,任意满足2.4MB空间的内存都可以。

​ 本次实验选取的显存是0x99000000。

15.6 编程_简单测试

​ 简单测试编程实现LCD全屏顺序显示红,绿,蓝三种颜色。

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test)**工程文件中,同时本节文档对应的视频是《第007节_LCD编程_简单测试_P》。

15.6.1 初始化LCD

	/*添加LCD屏幕参数*/		
	lcd_7_0_add();
	/*添加LCD控制器*/		
	lcd_contoller_add();

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,添加LCD屏幕参数和LCD控制器到各自的结构体数组中。

	/*选择Imx6ull的LCD控制器*/		
	select_lcd_controller("Imx6ull");	
	/*选择LCD屏幕参数*/		
	select_lcd("lcd_7.0");

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,从各自的结构体数组中选择指定名称的的LCD屏幕参数和LCD控制器。

lcd_controller_init(g_p_lcd_selected);

​ 最后通过lcd_controller_init函数将选择的LCD参数传入到被选中的LCD控制器中,调用该控制器的初始化函数进行初始化。

15.6.2 使能LCD

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,接着调用对应的函数指针即可

	void lcd_controller_enable(void)
	{
		if (g_p_lcd_controller_selected)
		{
			g_p_lcd_controller_selected->enable();
		}
	}

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,现在我们已经成功的选中LCD控制器,接着调用lcd_controller_enable函数,它将负责调用g_p_lcd_controller_selected结构体中的使能函数。即是被选中的LCD控制器使能函数。

static void Imx6ull_lcd_controller_enable(void)
{	
	LCDIF->CTRL |= 1<<0; /* 使能6ULL的LCD控制器 */
}

​ 由于我们选定是imx6ull的LCD控制器,所以g_p_lcd_controller_selected->enable()调用的就是我们使用的Imx6ull_lcd_controller_enable函数。

15.6.3 获取LCD参数

​ 在执行我们的简单测试之前,还需要让我们的测试程序知道现在用的lcd关键参数,实现各种颜色的显示。

	void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp)
	{
		*fb_base = g_p_lcd_selected->fb_base;
		*xres = g_p_lcd_selected->xres;
		*yres = g_p_lcd_selected->yres;
		*bpp = g_p_lcd_selected->bpp;
	}

​ 获取显存地址fb_base,获取分辨率xres和yres,获取bpp即每个像素点所占的位数。

15.6.4 往framebuffer中写数据

​ 为了支持16bpp或32bpp,我们需要分两个分支实现往framebuffer中写数据

		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x7c00;

		/* green */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x3E0;

		/* blue */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x1f;

​ 当获取的lcd参数bpp等于16时走上面分支

​ 当bpp为16时,我们lcd控制设定的颜色格式是ARGB555,也就是ARRRRRGGGGGBBBBB,红绿蓝各占5位,最高一位为灰度。

   	p2 = (unsigned int *)fb_base;
   		for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   		*p2++ = 0xff0000;

   	/*green*/ 
   	p2 = (unsigned int *)fb_base;
   	for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   			*p2++ = 0x00ff00;

   	/*blue*/ 
   	p2 = (unsigned int *)fb_base;
   	for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   			*p2++ = 0x0000ff;

​ 当获取的lcd参数bpp等于32时走上面分支,当bpp为32时,我们lcd控制设定的颜色格式是ARGB888,也就是AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB,红绿蓝各占8位,最高字节表示灰度。

15.7 编程_画点线圆

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/02_dot_line_circle)**工程文件中,同时本节文档对应的视频是《第008节_LCD编程_画点线圆_P》。

15.7.1 实现画点

①一个点(x,y)在FB中的位置如图

image-20220112151751170

可以得出其计算公式:(x,y)像素起始地址=fb_base+(xres*bpp/8)y + xbpp/8

②新建立一个frambuffer.c文件,由于新建立的frambuffer.c没有LCD的资源参数,所以需要编写一个调用

   void fb_get_lcd_params(void)
   {
   	get_lcd_params(&fb_base, &xres, &yres, &bpp);
   }

③在获取LCD资源参数之后,当bpp是32位的时候,可以直接往对应显存位置填颜色值,但是当bpp等于16位时候,我们就需要一些转换,让32位颜色数据变成16位颜色数据,由于16bpp我们使用的是ARG555,因此实际每一个颜色只有5位。我们需要先将32位中的rgb分离出来,再通过移位指令移到ARG555格式对应的红绿蓝位置。

int r = (rgb >> 16)& 0xff;
	int g = (rgb >> 8) & 0xff;
	int b = rgb & 0xff;

	/* argb555 */
	r = r >> 3;
	g = g >> 3;
	b = b >> 3;
	return ((r<<10) | (g<<5) | (b));

④编写画点函数,提供x坐标,y坐标,和颜色,实现往显存对应的位置填写颜色数据即可,对于16PP,每个像素只占据16位(2字节),因此采用unsigned short类型;对于32PP,每个像素只占据32位(4字节),因此采用unsigned int类型;

void fb_put_pixel(int x, int y, unsigned int color)
	{
		unsigned short *pw;  /* 16bpp */
		unsigned int   *pdw; /* 32bpp */
	
		unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;
	
		switch (bpp)
		{
			case 16:
				pw = (unsigned short *) pixel_base;
				*pw = convert32bppto16bpp(color);
			break;
			case 32:
				pdw = (unsigned int *) pixel_base;
				*pdw = color;
				break;
		}
	}

15.7.2 实现画线

​ 画线的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画线”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

​ 新建一个geometry.c,复制博客中代码,替换里面的描点显示函数即可

​ 最后在主函数测试程序里,加上画线的测试代码:

fb_get_lcd_params();
	
	delay(100000);

	draw_line(0, 0, xres - 1, 0, 0xff0000);
	delay(100000);
	draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);
	delay(100000);
	draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
	delay(100000);
	draw_line(0, 0, 0, yres - 1, 0xff00ef);
	delay(100000);
	draw_line(0, 0, xres - 1, yres - 1, 0xff4500);
	delay(100000);
	draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

​ 我们为划线函数提供对应的参数起点坐标,末点坐标,颜色就可以实现。

15.7.3 实现画圆

​ 画圆的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画圆”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

在geometry.c中添加,博客中代码,替换里面的描点显示函数即可

image-20220112151929285

​ 我们只需提供圆心的坐标,圆的半径,和圆的颜色即可实现。最后在主函数测试程序里,加上画圆画线的测试代码:

draw_circle(xres/2, yres/2, yres/4, 0xff00);

15.8 编程_显示文字

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test)**工程文件中,同时本节文档对应的视频是《第009节_LCD编程_显示文字_P》。

15.8.1 获取LCD参数

​ 新建一个font.c,由于新建文件中没有LCD参数,因此也需要获取LCD参数,根据这些LCD参数显示我们需要显示的字符。

void font_init(void)
{
	get_lcd_params(&fb_base, &xres, &yres, &bpp);
}

15.8.2 编写单个字符显示函数

​ 文字也是由点构成的,一个个点组成的点阵,宏观的来看,就是文字。可以参考Linux内核源码中的相关操作,在内核中搜索“font”,打开font_8x16.c,可以看到里面的A字符内容如下:

	/* 65 0x41 'A' */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x10, /* 00010000 */
	0x38, /* 00111000 */
	0x6c, /* 01101100 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xfe, /* 11111110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */

​ 根据这些数据,在一个8*16的区域里,将为1的点显示出来,为0的则不显示,最终将呈现一个字母“A”。根据字母的点阵在LCD上描画文字,需要的步骤如下:

​ a. 根据带显示的字符的ascii码在fontdata_8x16中得到点阵数据

​ b. 根据点阵来设置对应象素的颜色

​ c. 根据点阵的某位决定是否描颜色

void fb_print_char(int x, int y, char c, unsigned int color)
	{
		int i, j;
		
		/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
		const unsigned char *dots = &fontdata_8x16[c * 16];
	
		unsigned char data;
		int bit;
	
		/* 根据点阵来设置对应象素的颜色 */
		for (j = y; j < y+16; j++)
		{
			data = *dots++;
			bit = 7;
			for (i = x; i < x+8; i++)
		
				/* 根据点阵的某位决定是否描颜色 */
				if (data & (1<<bit))
					fb_put_pixel(i, j, color);
				bit--;
			}
		}
	}

​ 在font_8x16.c里面,每个字符占据16位,因此想要根据ascii码找到对应的点阵数据,需要对应的乘16,再取地址,得到该字符的首地址

​ 再根据每个点阵数据每位是否为1,来调用描点函数fb_put_pixel()。这样,依次显示16个点阵数据,获得字符图。

15.8.3 编写实现字符串显示函数

​ 显示字符串,那么就需要在每显示完一个字符后,x方向加8个像素,同时考虑是否超出屏幕(xres)显示范围进行换行处理(y+16)

	void fb_print_string(int x, int y, char* str, unsigned int color)
	{
		int i = 0, j;
		
		while (str[i])
		{
			if (str[i] == '\n')
				y = y+16;
			else if (str[i] == '\r')
				x = 0;
	
			else
			{
				fb_print_char(x, y, str[i], color);
				x = x+8;
				if (x >= xres) /* 换行 */
				{
					x = 0;
					y = y+16;
				}
			}
			i++;
		}
	}

​ 最后在在主函数里,加上显示字符串的函数,传入希望显示的字符串即可。

fb_print_string(10, 10, "www.100ask.net\n\r100ask.taobao.com", 0xff00);

15.8.4 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test) 源码目录进行编译。

15.8.5 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 此时可以看到屏幕先显示RGB三原色 之后显示点 线  和圆,最后显示字体

版权声明:本文为CSDN博主「韦东山」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/thisway_diy/article/details/122475747

第十五章 LCD编程

15.1 LCD硬件原理

15.2.1 LCD硬件工作原理简介

​ [外链图片转存中…(img-lRChXkF3-1642060379771)]

​ 假设上图是一个LCD屏幕,屏幕中一个一个密密麻麻的黑点称之为像素点,每一行有若干个点,试想下有一个电子枪,电子枪位于某一个像素点的背后,然后向这个像素发射红,绿,蓝三种原色,这三种颜色不同比例的组合成任意一种颜色。电子枪在像素点的背后,一边移动一边发出各种颜色的光,电子枪从左往右移动,到右边边缘之后就跳到下一行的行首,继续从左往右移动,如此往复,一直移动到屏幕右下角的像素点,最后就跳回原点。

​ 问题1:电子枪如何移动?

​ 答: 有一条像素时钟信号线(DCLK),连接屏幕,每来一个像素时钟信号(DCLK),电子枪就移动一个像素。

​ 问题2:电子枪打出的颜色该如何确定?

​ 答:有三组红,绿,蓝信号线(RGB),连接屏幕,由这三组信号线(RGB)确定颜色

​ 问题3:电子枪移动到LCD屏幕右边边缘时,如何得知需要跳到下一行的行首?

​ 答:有一条水平同步信号线(HSYNC),连接屏幕,当接收到水平同步信号(HSYNC),电子枪就跳到下一行的最左边

​ 问题4:电子枪如何得知需要跳到原点?

​ 答:有一条垂直同步信号线(VSYNC),连接屏幕,当接收到垂直同步信号线(VSYNC),电子枪就由屏幕右下脚跳到左上角(原点)

​ 问题5:电子枪如何得知三组信号线(RGB)确定的颜色就是它是需要的呢?

​ 答:有一条RGB数据使能信号线(DE),连接屏幕,当接收到数据使能信号线(DE),电子枪就知道这时由这三组信号线(RGB)确定的颜色是有效的,可以发射到该像素点。

​ 下图是开发板,LCD控制器,LCD屏幕的框图

image-20220112145025755

​ 之前提到的像素时钟(DCLK), 三组红,绿,蓝信号线(RGB),水平同步信号线(HSYNC),垂直同步信号线(VSYNC),RGB数据使能信号线(DE)都是从LCD控制器发出的,只要开发板支持LCD显示,他肯定就会有一个LCD控制器。

​ 问题6:RGB三组信号线上的数据从何而来?

image-20220112145046895

​ 上图是RGB数据来源框图,内存中划出一部分区域,这块区域成为Framebuffer,在这个Framebuffer里面我们会构造好每一个颜色所对应的像素,Framebuffer中的值会被LCD控制器读出来,通过RGB三组线传给电子枪,电子枪再它转换成红绿蓝三种颜色打到屏幕上,在屏幕上的每一个像素,在我们的Frambuffer里面都有一个对应存储空间,里面存有屏幕上对应像素的颜色。

​ 我们的LCD控制器会周而复始的从Framebuffer中取出一个像素的颜色值,发给电子枪,同时需要和DCLK,VSYNC,HSYNC,DE这些信号配合好。

15.2.2 RGB接口的LCD硬件连接信号

​ 本次实验编程的屏幕属于RGB接口的显示屏,RGB接口的显示屏至少具备以下信号:

(1)像素时钟信号(DCLK)

​ 像素时钟信号,用于同步LCD上的DE,VS,HS,RGB信号线。

(2)RGB数据信号(R[0:7] ,G[0:7],B[0:7])

​ 三组信号线组成,分别代表R(红色),G(绿色),B(蓝色),这三组信号中的每一组都会有8根信号,三组共同组成24根线来控制颜色数据。

(3)RGB数据使能信号(DE)

​ RGB接口的 LCD 有两种驱动模式DE 模式和 HV 模式, 在HV模式下,需要用到HS与VS同伴RGB数据,在DE模式下,则只需要DE信号同伴RGB数据,但是一般做LCD显示程序,都会兼容两种模式,所以一般都要将数据使能信号(DE),垂直同步信号(HS),水平同步信号(VS)一起使用。

(4)水平同步信号,

​ 电路中常用HS或HSYNC表示,详细说明下一小节会说明。

(5)垂直同步信号(帧同步或场同步)

​ 电路中常用VS或VSYNC表示,相信说明下一小节会说明。

(6)LCD背光电源控制信号

​ 一般是由普通GPIO控制(利用高低电平控制背光),背光就是在在LCD显示屏的背部一大串的灯珠,用它们来照亮屏幕。

​ 例如100ASK_IMX6ULL开发板的LCD接口定义,就包含了上面所述的几种信号类型:

image-20220112145210913

15.2.3 TFT材质液晶屏接口简介(7寸1024600TN-RGB)

​ 嵌入式一般都采用TFT材质的液晶屏,如遇到别的材质的屏幕,操作方法也是雷同,可能稍微有些差异,针对差异去做修改即可,7寸1024600TN-RGB液晶屏幕接口引脚如下图,一些关键的引脚做了注释。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNh3YKOO-1642060379773)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LCD_program_bare_metal_image-20220112145308136.png)]

15.2.4 LCD关键特性

①信号时序与信号的极性

​ 接下来我们查看下100ASK_7.0寸LCD手册时序图

image-20220112145317135

​ 从最小的像素开始分析,电子枪每次在CLK下降沿,如上图所示,该LCD在像素时钟下降沿采集数据,从数据线上得到数据,发射到显示屏上,然后移动到下一个位置。Dn0-Dn7上的数据来源就是前面介绍的FrameBuffer。就这样从一行的最左边,一直移动到一行的最右边,完成了一行的显示,假设为x。

​ 当打完一行的最后一个数据后,就会收到Hsync行同步信号,如上图可知该LCD的HSD有效脉冲为低脉冲,根据时序图,一个HSD周期可以大致分为五部分组成:thp、thb、thd、thf。thpw称为脉冲宽度,这个时间不能太短,太短电子枪可能识别不到。电子枪正确识别到thpw后,会从最右端移动最左端,这个移动的时间就是thb,称之为移动时间。thfp表示显示完最右像素,再过多久HSD才来,thd为数据有效区,th为打完一行所需要的时间。

image-20220112145335485

​ 同理,当电子枪一行一行的从上面移动到最下面时,VSD垂直同步信号,如上图可知该LCD的VSD有效脉冲为低脉冲。然后就让电子枪移动回最上边。VSD中的tvpw是脉冲宽度,tvb是移动时间,tvfp表示显示完最下一行像素,再过多久VSD才来,tvd为数据有效区,tv为打完一帧所需要的时间。假设一共有y行,则LCD的分辨率就是x*y。

[外链图片转存中…(img-EtHcBoO3-1642060379775)]

​ RGB数据有效信号(DEN),高电平表示数据有效。

根据以上信息大致了解几个关键信号的时序和极性,后面章节会详细介绍。

image-20220112145347564

​ 再根据上图,我们就可以确定像素时钟是51.2Mhz。

②RGB数据的存放形式

​ 前面的LCD硬件接口,R0-R7、G0-G7、B0-B7,每个像素是占据3*8=24位的,即硬件上LCD的BPP是确定的。虽然硬件引脚连接是固定的,但我们使用的时候,可以根据实际情况进行取舍,比如我们的IMX6ULL开发板,可以让他支持不同的像素格式,ARGB888,ARGB555,RGB565等等,

​ 本实验支持ARGB888和ARGB555。

ARGB888:每个像素就占据32位数据,其中最高字节A表示灰度透明度其余RGB数据8+8+8=24BPP。

ARGB555:每个像素就占据16位数据,其中最高位A表示灰度透明度其余RGB数据5+5+5=15BPP。

15.2 IMX6ULL LCD控制器操作及寄存器

15.2.1 LCD控制器模块介绍

​ IMX6ULL的LCD控制器名称为elcdif(增强型LCD接口)主要特性如下:

​ a.支持MPU模式,针对显示屏内部有显存的显示屏;

​ b.支持DOTCLK模式,针对RGB接口使用,本实验就是此模式;

​ c.VSYNC模式,针对高速数据传输(行场信号);

​ d. 8/16/18/24/32 bit 的bpp数据都支持,取决于IO的复用设置及寄存器配置;

​ e.MPU模式,VSYNC模式,DOTCLK模式,都具有配置的时序参数;

image-20220112145424746

​ 上图是IMX6ULL的LCD控制器框图,AXI是一种总线协议,通过此总线将显存中的RGB数据写入到FIFO,经过FIFO过度,到达LCD接口,LCD控制器分两个时钟域,一个是外设总线时钟域,一个是LCD像素时钟域,前者是用于让LCD控制器正常工作的时钟,后者是控制电子枪移动速度的时钟。Read_Data操作工作在MPU模式,我们采用的是DCLK模式,因此不予考虑。

​ 以上只是介绍了部分,如需要更加详细的了解需要查看IMX6ull芯片手册《Chapter 34

​ Enhanced LCD Interface (eLCDIF)》

15.2.2 LCD控制器寄存器简介

[外链图片转存中…(img-Qpqmd64m-1642060379778)]

​ 上图是我们将要使用到的寄存器,接着将会大致讲解下使用到的寄存器,更加详细说明会在后续的LCD控制编程实验中提及。

15.2.2.1 ①LCDIF_CTRL寄存器:

image-20220112145500174

SFTRST:软复位,用于修改像素时钟后,进行复位同步时钟;

BYPASS_COUNT:DOTCLK和DVI modes都需要设置为1;

DOTCLK_MODE:设置为1,进入DOTCLK模式;

LCD_DATABUS_WIDTH:RGB数据总线,跟进数据总线宽度设置;

WORD_LENGTH:输入的RBG数据格式,即多少位表示一个像素;

MASTER:LCD控制器主机模式设置;

DATA_FORMAT_16_BIT:当设置为16BPP的时候需要设置该位

15.2.2.2 ②LCDIF_CTRL1寄存器:

image-20220112145520234

BYTE_PACKING_FORMAT:用于表示4字节RGB数据中,那几个字节是属于有有效数据,因为其中有一个字节表示A(灰度,透明度)

15.2.2.3 ③LCDIF_TRANSFER_COUNT寄存器:

image-20220112145543927

V_COUNT:表示垂直方向上的像素个数,即分辨率中的x;

H_COUNT:表示水平方向上的像素个数,即分辨率中的y;

15.2.2.4 ④LCDIF_VDCTRL0寄存器

image-20220112145551594

VSYNC_PULSE_WIDTH:垂直同步信号的宽度;

VSYNC_PULSE_WIDTH_UNIT:根据不同模式下的计算时钟方式来决定垂直同步信号宽度;

VSYNC_PERIOD_UNIT:根据不同模式下的计算时钟方式来发垂直同步信号;

ENABLE_POL:DE数据有效信号的极性,即有效电平极性;

DOTCLK_POL:像素时钟信号的极性,即有效电平极性;

HSYNC_POL:水平同步信号的极性,即有效电平极性;

VSYNC_POL:垂直同步信号的极性,即有效电平极性;

ENABLE_PRESENT:在DOTCLK模式下,是否会硬件产生ENABLE使能数据信号;

VSYNC_OEB:VSYNC设置为输出还是输入模式,我们选择输出模式;

15.2.2.5 ⑤LCDIF_VDCTRL1寄存器

image-20220112145558654

VSYNC_PERIOD:两个垂直同步信号之间的总数,即垂直方向同步信号的总周期;

15.2.2.6 ⑥LCDIF_VDCTRL2寄存器

image-20220112145602359

HSYNC_PULSE_WIDTH:水平同步信号脉冲宽度;

HSYNC_PERIOD:两个水平同步信号之间的总数,即水平方向同步信号的总周期

15.2.2.7 ⑦LCDIF_VDCTRL3寄存器

image-20220112145625980

HRIZONTAL_WAIT_CNT:水平方向上的等待像素个数;

VERTICAL_WAIT_CNT:垂直方向上的等待像素个数;

15.2.2.8 ⑧LCDIF_VDCTRL4寄存器

image-20220112145632883

SYNC_SIGNALS_ON:工作在DOTCLK模式下,需要设置为1;

DOTCLK_H_VALID_DATA_CNT:DOTCLK模式下,水平方向上的有效像素点个数,即分辨率的y;

15.2.2.9 ⑨LCDIF_CUR_BUF寄存器

image-20220112145636601

ADDR:通过LCD控制器,发送的当前帧地址;

15.2.2.10 ⑩LCDIF_NEXT_BUF寄存器

image-20220112145651586

ADDR:通过LCD控制器,发送的下一帧地址;

15.3 编程_框架与准备

​ 本节文档对应的视频是《第003节_LCD编程_框架与准备_P》。

15.3.3 功能目的

​ 我们最终的目的是在LCD显示屏上画线、画圆和写字,此外还需要一个测试程序提供操作菜单,调用画线、画圆和写字操作,这些终究其核心是画点,我们需要实现画点才能实现其他功能,但是画点前也要让我们的LCD控制器正常工作起来才能实现它,最终总结:先让LCD控制器正常工作(配置寄存器),再编写画点的函数。

15.3.4 编程框架

​ 接着我们就需要实现画点,在实现画点之前想两个问题:

​ ①有两款尺寸大小的LCD显示屏,如何快速的在两个lcd上切换?

​ ②有两款不同的CPU都需要显示同一款LCD显示屏,如何快速的在两个cpu上切换?

​ 为了让程序更加好扩展,下面介绍“面向对象编程”的概念

​ 我们发现LCD显示屏虽然不同尺寸,参数不同,但是它们终究是LCD显示屏,我们可以把他们归一类,当需要使用某款LCD显示屏的参数时就提供该款的参数,其他的LCD显示屏参数不管他不就可以了吗?

​ 同理不同的CPU虽然LCD控制器地址不同,操作也不同,但是它们终究是LCD控制器,我们可以把他们归一类,当确定使用某个LCD控制器的时候就用这个LCD控制器的操作,其他的LCD控制器不管他。

​ 下图是LCD编程的框架,尽可能的“高内聚低耦合”,即类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低

image-20220112145758887

​ 根据不同的LCD控制器特性,来设置不同的LCD控制器,对于我们开发板,就是imx6ull_con.c,假如希望在其它 开发板上也实现LCD显示,只需添加相应的代码文件xxx_con即可。

​ 根据不同的LCD屏幕特性,来编写不同的LCD屏幕参数,对于我们的开发板,就是lcd_7_0.c,假如希望这个开发板支持别的LCD屏幕,只需添加相应的代码文件lcd_xxx.c即可。

15.4 编程_抽象出重要结构体

​ 本节代码在裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_manager.h) 与**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_controller_manager.h)**头文件中,本节文档对应的视频是《第004节_LCD编程_抽象出重要结构体_P》。

15.4.1 抽象出LCD屏幕的结构体

​ 建立一个lcd_manager.h,将任意LCD都共有的参数(引脚的极性、时序、数据的格式bpp、分辨率等)使用面向对象的思维方式,将这些封装成结构体放在lcd_manager.h中

enum {
		NORMAL = 0,
		INVERT = 1,
	};

	/* NORMAL : 正常
	* INVERT : 取反
	*/
	typedef struct pins_polarity {
		int de;    /* normal: 高电平使能数据 */
		int vclk;  /* normal: 在下降沿获取数据 */
		int hsync; /* normal:高脉冲 */
		int vsync; /* normal:高脉冲  */
	}pins_polarity, *p_pins_polarity;

	typedef struct time_sequence {
		/* 垂直方向 */
		int tvp; /*  vysnc脉冲宽度 */
		int tvb; /*上边黑框 , Vertical Back porch */
		int tvf; /*下边黑框, Vertical Front porch */
	
		/* 水平方向 */
		int thp; /* hsync脉冲宽度 */
		int thb; /* 左边黑框 ,Horizontal Back porch */
		int thf; /* 右边黑框,Horizontal Front porch */

		int vclk;
	}time_sequence, *p_time_sequence;


	typedef struct lcd_params {
	
		char *name;
		
		/*引脚极性参数*/
		pins_polarity pins_pol;
	
		/*时序参数*/
		time_sequence time_seq;
	
		/*分辨率*/
		int xres;
		int yres;
		int bpp;
	
		/*显存*/
		unsigned int fb_base;
	
	}lcd_params, *p_lcd_params;

​ 以后就使用lcd_params结构体来表示lcd参数 ,通过register_lcd函数注册某款LCD屏幕参数到一个lcd_params结构体数组,然后通过select_lcd函数在lcd_params结构体数组中选中指定的LCD屏幕参数保存起来,提供给其他函数用。

15.4.2 抽象出LCD控制器的结构体

​ 建立一个lcd_controller_manager.h,将任意LCD控制器都共有的函数(初始化函数,使能函数等)使用面向对象的思维方式,将这些封装成结构体放在lcd_controller_manager.h中

typedef struct lcd_controller{
	char* name;
	void (*init)(p_lcd_params plcdparams);
	void(*enable)(void);
	void(*disable)(void);
}lcd_controller, *p_lcd_controller;

​ 以后就使用lcd_controller结构体来表示lcd控制器。

​ 建立一个lcd_controller_manager.c,提供一系列的LCD控制器的管理函数,用这些管理函数,通过register_lcd_controller函数注册新的LCD控制器到lcd_controller的结构体数组中,然后通过select_lcd_controller函数在lcd_controller结构体数组中选中指定的LCD控制器,提供给其他函数用,最终用户再调用光宇LCD控制器的操作时,就会通过选中的lcd_controller的结构体访问到对应的某款LCD控制器的函数。

15.5 编程_LCD控制器

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/imx6ull_con.c)**源文件中, 本节文档对应的视频是《第005节_LCD编程_LCD控制器_P》。

15.5.1 LCD控制器相关引脚复用配置

image-20220112145945035

​ 根据前面硬件接口章节15.1.3和上图,我们知道要设置这30个引脚,设置引脚按两步走,第一步设置引脚复用功能,第二部设置引脚的硬件属性。查看相应的寄存器得知,复用功能寄存器均设置为0即可,接着硬件属性根据章节《4-1.3 GPIO操作方法》的内容,均设置为0xB9即可。

15.5.2 LCD控制器像素时钟配置

​ 根据IMX6ULL芯片手册的Chapter 18 Clock Controller Module (CCM),我们就可以设置像素时钟为我们需要的51.2Mhz

15.5.2.1 ①确定PLL

image-20220112150005692

​ 由上图可知LCD控制器的时钟来源是PLL5(video pll)

[外链图片转存中…(img-1hb2DTWp-1642060379786)]

​ 根据上图可得知VIDEO pll的公式,但是为了方便计算,我们不需要后面的小数运算即Video PLL output frequency(PLL5)= Fref * (DIV_SELECT + 0)

15.5.2.2 ②确定PLL后的分频系数

image-20220112150053310

​ 根据上图可知PLL5出来后经过两级分频,即PLL5_MAIN_CLK = PLL5 / POST_DIV_SELECT / VIDEO_DIV

15.5.2.3 ③PLL分频后进入LCDIF控制器前的分频系数

image-20220112150121312

​ 根据上图可知PLL5分频后到LCDIF控制器也有两级分频,即LCDIF1_CLK_ROOT = PLL5_MAIN_CLK /LCDIF1_PRED / LCDIF1_PODF,根据上面三个内容,我们可以采用以下这个配置来达到像素时钟51.2Mhz,DIV_SELECT = 32;NUM = 0;DENOM = 0;POST_DIV_SELECT = 1,VIDEO_DIV = 1LCDIF1_PRED = 3;LCDIF1_PODF = 5

​ 带入时钟公式得24*(32+0)/1/1/3/5 ≈ 51.2Mhz。下面开始编程:

15.5.3 LCD控制器时钟编程

15.5.3.1 ①取消小数分配器

  CCM_ANALOG->PLL_VIDEO_NUM   = 0; 	 
  CCM_ANALOG->PLL_VIDEO_DENOM = 0;

​ 清零表示取消小数分配器

15.5.3.2 ②设置CCM_ANALOG_PLL_VIDEOn寄存器

image-20220112150211473

CCM_ANALOG->PLL_VIDEO =  (2 << 19) | (1 << 13) | (32<< 0);

​ 设置PLL5使能,倍频为32倍,1分频(即不分频),选择外部24M晶振为时钟源,至此PLL5分频后为24 * 32 / 1

15.5.3.3 ③设置CCM_ANALOG_MISC2n

image-20220112150247815

默认就为1分频,所以无需设置,至此PLL5分频后为24 * 32 / 1 / 1 = 768Mhz

15.5.3.4 ④设置CCM_CSCDR2

image-20220112150300024

设置选中对应的时钟源,预分频系数为3,得768 / 3 = 256Mhz

	CCM->CSCDR2 &= ~(7 << 15); 	   
	CCM->CSCDR2 |=  (2 << 15);  
	CCM->CSCDR2 &= ~(7 << 12); 	 
	CCM->CSCDR2 |=  (2 << 12); 						 
 	CCM->CSCDR2 &= ~(7 << 9);

15.5.3.5 ⑤设置CCM_CBCMR

image-20220112150322222

	CCM->CBCMR &= ~(7 << 23);					 
	CCM->CBCMR |=	4 << 23;	/*[25:23] :4 : 表示5分频*/

设置预分频系数为5,最终得到51.2Mhz

15.5.3.6 ⑥重新同步时钟

image-20220112150341565

/* 重新设置时钟后,需要软复位LCD控制器,让LCD控制器像素时钟同步*/
	 LCDIF->CTRL  = 1<<31;   

  /*软复位需要花费好几个时钟周期,这里需要一些时间等待*/
	 delay(100);

  /*同步像素时钟结束*/
	LCDIF->CTRL  = 0<<31; /* 取消复位 */

15.5.4 LCD控制器像素格式配置

15.5.4.1 ①设置LCDIF_CTRLn寄存器

[外链图片转存中…(img-89nWwH5E-1642060379788)]

	LCDIF->CTRL |= (1 << 19) | (1 << 17) |(3 << 10) | (bpp_mode << 8) | (1 << 5) ;

		/* [3]当bpp为16时,数据格式为ARGB555*/
	 	if(plcdparams->bpp == 16)
	 	{
	 		LCDIF->CTRL |= 1<<3;
	 	}

15.5.4.2 ②设置LCDIF_CTRL1n寄存器

image-20220112150501256

if(plcdparams->bpp == 24 || plcdparams->bpp == 32)
	 {		
	 		LCDIF->CTRL1 &= ~(0xf << 16); 
		 	LCDIF->CTRL1 |=  (0x7 << 16); 
	 } 

表示ARGB传输格式模式下,传输24位无压缩数据,A通道不用传输),当我们选用16bpp即ARGB555时,不需要设置此位。

15.5.5 LCD控制器时序配置及极性配置

15.5.5.1 ①设置LCDIF_TRANSFER_COUNT寄存器

image-20220112150610501

    ```c

LCDIF->TRANSFER_COUNT = (plcdparams->yres << 16) | (plcdparams->xres << 0);
```

15.5.5.2 ②设置LCDIF_VDCTRL0n寄存器

[外链图片转存中…(img-CyWxG1LO-1642060379789)]

LCDIF->VDCTRL0 = (1 << 28)|( plcdparams->pins_pol.vsync << 27)
   					|( plcdparams->pins_pol.hsync << 26)
   					|( plcdparams->pins_pol.vclk << 25)
   					|(plcdparams->pins_pol.de << 24)
   					|(1 << 21)|(1 << 20)|( plcdparams->time_seq.tvp << 0);

15.5.5.3 ③配置LCDIF_VDCTRL1寄存器

image-20220112150655412

LCDIF->VDCTRL1 = plcdparams->time_seq.tvb + plcdparams->time_seq.tvp + plcdparams->yres + plcdparams->time_seq.tvf;  

设置垂直方向的总周期:上黑框tvb+垂直同步脉冲tvp+垂直有效高度yres+下黑框tvf

15.5.5.4 ④配置LCDIF_VDCTRL2寄存器

image-20220112150744101

LCDIF->VDCTRL2 = (plcdparams->time_seq.thp << 18) | (plcdparams->time_seq.thb +  plcdparams->time_seq.thp + plcdparams->xres + plcdparams->time_seq.thf);

[18:31] : 水平同步信号脉冲宽度

[17: 0] : 水平方向总周期

设置水平方向的总周期:左黑框thb+水平同步脉冲thp+水平有效高度xres+右黑框thf

15.5.5.5 ⑤配置LCDIF_VDCTRL3寄存器

image-20220112150809660

LCDIF->VDCTRL3 = ((plcdparams->time_seq.thb + plcdparams->time_seq.thp) << 16) | (plcdparams->time_seq.tvb + plcdparams->time_seq.tvp);

设置ELCDIF的VDCTRL3寄存器

[27:16] :水平方向上的等待时钟数 =thb + thp

[15:0] : 垂直方向上的等待时钟数 = tvb + tvp

15.5.5.6 ⑥设置LCDIF_VDCTRL4寄存器

image-20220112150833971

LCDIF->VDCTRL4 = (1<<18) | (plcdparams->xres);

设置ELCDIF的VDCTRL4寄存器

[18] : 使用VSHYNC、HSYNC、DOTCLK模式此为置1

17:0: 水平方向的宽度

15.5.6 设置显存

image-20220112150949262

LCDIF->CUR_BUF  =  plcdparams->fb_base;
LCDIF->NEXT_BUF =  plcdparams->fb_base;

CUR_BUF : 当前显存地址

NEXT_BUF : 下一帧显存地址

方便运算,都设置为同一个显存地址

15.5.7 编程_LCD设置

本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_7_0.c)**源文件中 ,本节文档对应的视频是《第006节_LCD编程_LCD设置_P》

15.5.8 添加LCD屏幕名称

.name = "lcd_7.0",

先给我们本次实验的LCD屏幕参数一个名称,可以根据名称去选择我们需要的LCD屏幕参数。

15.5.9 极性设置

LCDIF_VDCTRL0n寄存器极性设置位

image-20220112151048189

VSYNC_POL与HSYNC_POL:0表示低电平有效,1表示高电平有效

DOTCLK_POL:0表示上升沿有效,1表示下降沿有效

ENABLE_POL:0表示低电平有效,1表示高电平有效

​ 接着根据15-1.4关键特性章节中,我们已经知道像素时钟DCLK是下降沿时获取数据,水平同步信号HSD和垂直同步信号VSD都是低电平有效,数据使能信号DEN是高电平表示数据有效,接着我们只要在lcd_7_0.c文件中设定成对应的标记用来识别即可,1)数据使能信号de设置为1, 2)像素时钟vclk极性设置为1 ,3)水平同步信号HSD和垂直同步信号VSD都设置为0。

enum {
   NORMAL = 0,
   INVERT = 1,
};

根据枚举内容,填入对应的参数即可

.pins_pol = {
	.de    = INVERT,	/* normal: 低电平表示使能输出 */
	.vclk  = INVERT,		/* normal: 在上升降沿获取数据*/
	.hsync = NORMAL,    	/* normal: 低脉冲*/
	.vsync = NORMAL, 	/* normal: 低脉冲*/
},

15.5.10 时序设置

image-20220112151141164

​ 观察上图中红色框框内容,接着我们只需要把上面数值往lcd_7_0.c中定义的lcd_7_0_params中填入对应的值即可

​ 1)Thpw:结构体中的tvb = 20

​ 2)Thb:结构体中的thb = 140

​ 3)Thfp:结构体中的thf = 160

​ 4)THA:结构体中的tvp = 1024

​ 5)Tvpw:结构体中的tvp = 3

​ 6)Tvb:结构体中的thp = 20

​ 7)Tvfp:结构体中的tvf = 12

​ 8)Tvd:结构体中的yres = 600

.time_seq = {
	/* 垂直方向 */
	.tvp=	3, /* vysnc脉冲宽度 */
	.tvb=	20,  /* 上边黑框, Vertical Back porch */
	.tvf=	12,  /* 下边黑框, Vertical Front porch */

	/* 水平方向 */
	.thp=	20, /* hsync脉冲宽度 */
	.thb=	140,  /* 左边黑框, Horizontal Back porch */
	.thf=	160,  /* 右边黑框, Horizontal Front porch */

	.vclk=	51.2,  /* MHz */
},
.xres = 1024,
.yres = 600

15.5.11 显存地址设置

image-20220112151220548

​ 根据上图imx_6ull可以挂接最大2GB的内存,可实际我们使用的100ask_imx_6ull开发板只挂接了512M的内存,因此我们可选的内存地址范围是0x800000000xa0000000。我们的裸机实验的链接地址是0x80100000,我们裸机实验程序不大,最多算它0x900000(实际没有这么多),那么我们显存可以用范围就变成0x810000000xa0000000,再计算我们需要的显存大小10246004=2457600≈2.4MB。

​ 总结:0x81000000~0xa0000000范围内,任意满足2.4MB空间的内存都可以。

​ 本次实验选取的显存是0x99000000。

15.6 编程_简单测试

​ 简单测试编程实现LCD全屏顺序显示红,绿,蓝三种颜色。

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test)**工程文件中,同时本节文档对应的视频是《第007节_LCD编程_简单测试_P》。

15.6.1 初始化LCD

	/*添加LCD屏幕参数*/		
	lcd_7_0_add();
	/*添加LCD控制器*/		
	lcd_contoller_add();

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,添加LCD屏幕参数和LCD控制器到各自的结构体数组中。

	/*选择Imx6ull的LCD控制器*/		
	select_lcd_controller("Imx6ull");	
	/*选择LCD屏幕参数*/		
	select_lcd("lcd_7.0");

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,从各自的结构体数组中选择指定名称的的LCD屏幕参数和LCD控制器。

lcd_controller_init(g_p_lcd_selected);

​ 最后通过lcd_controller_init函数将选择的LCD参数传入到被选中的LCD控制器中,调用该控制器的初始化函数进行初始化。

15.6.2 使能LCD

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,接着调用对应的函数指针即可

	void lcd_controller_enable(void)
	{
		if (g_p_lcd_controller_selected)
		{
			g_p_lcd_controller_selected->enable();
		}
	}

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,现在我们已经成功的选中LCD控制器,接着调用lcd_controller_enable函数,它将负责调用g_p_lcd_controller_selected结构体中的使能函数。即是被选中的LCD控制器使能函数。

static void Imx6ull_lcd_controller_enable(void)
{	
	LCDIF->CTRL |= 1<<0; /* 使能6ULL的LCD控制器 */
}

​ 由于我们选定是imx6ull的LCD控制器,所以g_p_lcd_controller_selected->enable()调用的就是我们使用的Imx6ull_lcd_controller_enable函数。

15.6.3 获取LCD参数

​ 在执行我们的简单测试之前,还需要让我们的测试程序知道现在用的lcd关键参数,实现各种颜色的显示。

	void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp)
	{
		*fb_base = g_p_lcd_selected->fb_base;
		*xres = g_p_lcd_selected->xres;
		*yres = g_p_lcd_selected->yres;
		*bpp = g_p_lcd_selected->bpp;
	}

​ 获取显存地址fb_base,获取分辨率xres和yres,获取bpp即每个像素点所占的位数。

15.6.4 往framebuffer中写数据

​ 为了支持16bpp或32bpp,我们需要分两个分支实现往framebuffer中写数据

		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x7c00;

		/* green */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x3E0;

		/* blue */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x1f;

​ 当获取的lcd参数bpp等于16时走上面分支

​ 当bpp为16时,我们lcd控制设定的颜色格式是ARGB555,也就是ARRRRRGGGGGBBBBB,红绿蓝各占5位,最高一位为灰度。

   	p2 = (unsigned int *)fb_base;
   		for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   		*p2++ = 0xff0000;

   	/*green*/ 
   	p2 = (unsigned int *)fb_base;
   	for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   			*p2++ = 0x00ff00;

   	/*blue*/ 
   	p2 = (unsigned int *)fb_base;
   	for (x = 0; x < xres; x++)
   		for (y = 0; y < yres; y++)
   			*p2++ = 0x0000ff;

​ 当获取的lcd参数bpp等于32时走上面分支,当bpp为32时,我们lcd控制设定的颜色格式是ARGB888,也就是AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB,红绿蓝各占8位,最高字节表示灰度。

15.7 编程_画点线圆

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/02_dot_line_circle)**工程文件中,同时本节文档对应的视频是《第008节_LCD编程_画点线圆_P》。

15.7.1 实现画点

①一个点(x,y)在FB中的位置如图

image-20220112151751170

可以得出其计算公式:(x,y)像素起始地址=fb_base+(xres*bpp/8)y + xbpp/8

②新建立一个frambuffer.c文件,由于新建立的frambuffer.c没有LCD的资源参数,所以需要编写一个调用

   void fb_get_lcd_params(void)
   {
   	get_lcd_params(&fb_base, &xres, &yres, &bpp);
   }

③在获取LCD资源参数之后,当bpp是32位的时候,可以直接往对应显存位置填颜色值,但是当bpp等于16位时候,我们就需要一些转换,让32位颜色数据变成16位颜色数据,由于16bpp我们使用的是ARG555,因此实际每一个颜色只有5位。我们需要先将32位中的rgb分离出来,再通过移位指令移到ARG555格式对应的红绿蓝位置。

int r = (rgb >> 16)& 0xff;
	int g = (rgb >> 8) & 0xff;
	int b = rgb & 0xff;

	/* argb555 */
	r = r >> 3;
	g = g >> 3;
	b = b >> 3;
	return ((r<<10) | (g<<5) | (b));

④编写画点函数,提供x坐标,y坐标,和颜色,实现往显存对应的位置填写颜色数据即可,对于16PP,每个像素只占据16位(2字节),因此采用unsigned short类型;对于32PP,每个像素只占据32位(4字节),因此采用unsigned int类型;

void fb_put_pixel(int x, int y, unsigned int color)
	{
		unsigned short *pw;  /* 16bpp */
		unsigned int   *pdw; /* 32bpp */
	
		unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;
	
		switch (bpp)
		{
			case 16:
				pw = (unsigned short *) pixel_base;
				*pw = convert32bppto16bpp(color);
			break;
			case 32:
				pdw = (unsigned int *) pixel_base;
				*pdw = color;
				break;
		}
	}

15.7.2 实现画线

​ 画线的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画线”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

​ 新建一个geometry.c,复制博客中代码,替换里面的描点显示函数即可

​ 最后在主函数测试程序里,加上画线的测试代码:

fb_get_lcd_params();
	
	delay(100000);

	draw_line(0, 0, xres - 1, 0, 0xff0000);
	delay(100000);
	draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);
	delay(100000);
	draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
	delay(100000);
	draw_line(0, 0, 0, yres - 1, 0xff00ef);
	delay(100000);
	draw_line(0, 0, xres - 1, yres - 1, 0xff4500);
	delay(100000);
	draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

​ 我们为划线函数提供对应的参数起点坐标,末点坐标,颜色就可以实现。

15.7.3 实现画圆

​ 画圆的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画圆”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

在geometry.c中添加,博客中代码,替换里面的描点显示函数即可

image-20220112151929285

​ 我们只需提供圆心的坐标,圆的半径,和圆的颜色即可实现。最后在主函数测试程序里,加上画圆画线的测试代码:

draw_circle(xres/2, yres/2, yres/4, 0xff00);

15.8 编程_显示文字

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test)**工程文件中,同时本节文档对应的视频是《第009节_LCD编程_显示文字_P》。

15.8.1 获取LCD参数

​ 新建一个font.c,由于新建文件中没有LCD参数,因此也需要获取LCD参数,根据这些LCD参数显示我们需要显示的字符。

void font_init(void)
{
	get_lcd_params(&fb_base, &xres, &yres, &bpp);
}

15.8.2 编写单个字符显示函数

​ 文字也是由点构成的,一个个点组成的点阵,宏观的来看,就是文字。可以参考Linux内核源码中的相关操作,在内核中搜索“font”,打开font_8x16.c,可以看到里面的A字符内容如下:

	/* 65 0x41 'A' */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x10, /* 00010000 */
	0x38, /* 00111000 */
	0x6c, /* 01101100 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xfe, /* 11111110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */

​ 根据这些数据,在一个8*16的区域里,将为1的点显示出来,为0的则不显示,最终将呈现一个字母“A”。根据字母的点阵在LCD上描画文字,需要的步骤如下:

​ a. 根据带显示的字符的ascii码在fontdata_8x16中得到点阵数据

​ b. 根据点阵来设置对应象素的颜色

​ c. 根据点阵的某位决定是否描颜色

void fb_print_char(int x, int y, char c, unsigned int color)
	{
		int i, j;
		
		/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
		const unsigned char *dots = &fontdata_8x16[c * 16];
	
		unsigned char data;
		int bit;
	
		/* 根据点阵来设置对应象素的颜色 */
		for (j = y; j < y+16; j++)
		{
			data = *dots++;
			bit = 7;
			for (i = x; i < x+8; i++)
		
				/* 根据点阵的某位决定是否描颜色 */
				if (data & (1<<bit))
					fb_put_pixel(i, j, color);
				bit--;
			}
		}
	}

​ 在font_8x16.c里面,每个字符占据16位,因此想要根据ascii码找到对应的点阵数据,需要对应的乘16,再取地址,得到该字符的首地址

​ 再根据每个点阵数据每位是否为1,来调用描点函数fb_put_pixel()。这样,依次显示16个点阵数据,获得字符图。

15.8.3 编写实现字符串显示函数

​ 显示字符串,那么就需要在每显示完一个字符后,x方向加8个像素,同时考虑是否超出屏幕(xres)显示范围进行换行处理(y+16)

	void fb_print_string(int x, int y, char* str, unsigned int color)
	{
		int i = 0, j;
		
		while (str[i])
		{
			if (str[i] == '\n')
				y = y+16;
			else if (str[i] == '\r')
				x = 0;
	
			else
			{
				fb_print_char(x, y, str[i], color);
				x = x+8;
				if (x >= xres) /* 换行 */
				{
					x = 0;
					y = y+16;
				}
			}
			i++;
		}
	}

​ 最后在在主函数里,加上显示字符串的函数,传入希望显示的字符串即可。

fb_print_string(10, 10, "www.100ask.net\n\r100ask.taobao.com", 0xff00);

15.8.4 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test) 源码目录进行编译。

15.8.5 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 此时可以看到屏幕先显示RGB三原色 之后显示点 线  和圆,最后显示字体

版权声明:本文为CSDN博主「韦东山」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/thisway_diy/article/details/122475747

生成海报
点赞 0

韦东山

我还没有学会写个人说明!

暂无评论

相关推荐

7_时钟体系

第七章 时钟体系 ​ 时钟信号是数字时序电路的“脉搏”,电路每接收到一个周期的时钟信号,就做一个相应的动作。因此,在允许的范围内,时钟信号的快慢直接决定着电路性能的好坏。在片上系统&#x

4_LED程序

第四章 LED程序 4.1 硬件知识_LED原理图 ​ 当我们学习C语言的时候,我们会写个Hello程序。 ​ 那当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。

15_LCD编程

第十五章 LCD编程 15.1 LCD硬件原理 15.2.1 LCD硬件工作原理简介 ​ [外链图片转存中…(img-lRChXkF3-1642060379771)] ​ 假设上图是一个LCD屏幕,屏幕中一个一个密密麻麻的

18_SPI编程

第十八章 SPI编程(有误) 18.1 SPI接口简介 ​ SPI(Serial Peripheral Interface)接口是全双工的同步串行通讯总线,支持通过多个不同的片选信号来连接多个外设。