第017課 LCD原理詳解及裸機程序分析


第001節_LCD硬件原理

先簡單介紹下LCD的操作原理。
如下圖的LCD示意圖,里面的每個點就是一個像素點。
這里寫圖片描述

想象有一個電子槍,一邊移動,一邊發出各種顏色的光。這里有很多細節問題,我們一個一個的梳理。

    1. 電子槍是如何移動的?
      答:有一條CLK時鍾線與LCD相連,每發出一次CLK(高低電平),電子槍就移動一個像素。
    1. 顏色如何確定?
      答:由連接LCD的三組線:R(Red)、G(Green)、B(Blue)確定。
    1. 電子槍如何得知應跳到下一行?
      答:有一條HSYNC信號線與LCD相連,每發出一次脈沖(高低電平),電子槍就跳到下一行。
    1. 電子槍如何得知應跳到原點?
      答:有一條VSYNC信號線與LCD相連,每發出一次脈沖(高低電平),電子槍就跳到原點。
    1. RGB線上的數據從何而來?
      答:內存里面划分一塊顯存(FrameBuffer),里面存放了要顯示的數據,LCD控制器從里面將數據讀出來,通過RGB三組線傳給電子槍,電子槍再依次打到顯示屏上。
    1. 前面的信號由誰發給LCD?
      答:有S3C2440里面的LCD控制器來控制發出信號。

通過JZ2440原理圖對上面進行驗證,下圖的LCD控制器接口圖。
這里寫圖片描述

①是時鍾信號,每來一個CLK,電子槍就移動一個像素;

②是用來傳輸顏色數據;

③是垂直方向同步信號,FRAME(幀);

④是水平方向同步信號,LINE(行);

再來看看LCD的芯片手冊。
這里寫圖片描述

先是VLED+、VLED-背光燈電源。VDD、VDD是LCD電源。

R0-R7、G0-G7、B0-B7是紅綠藍顏色信號。

PCLK是像素時鍾信號。DISP是像素開關。

HSYNC、VSYNC分別是水平方向、垂直方向信號。

DE數據使能。X1、Y1、X2、Y2是觸摸屏信號。

可以看出LCD有很多信號,這些信號要根據時序圖傳輸才能正確顯示。參考JZ2440_4.3寸LCD手冊_AT043TN24的時序如下:
這里寫圖片描述

從最小的像素開始分析,電子槍每次在CLK下降沿(本開發板是下降沿)從數據線Dn0-Dn7上得到數據,發射到顯示屏上,然后移動到下一個位置。Dn0-Dn7上的數據來源就是前面介紹的FrameBuffer。就這樣從一行的最左邊,一直移動到一行的最右邊,完成了一行的顯示,假設為x。

當打完一行的最后一個數據后,就會收到Hsync行同步信號,根據時序圖,一個Hsync周期可以大致分為五部分組成:thp、thb、1/tc、thd、thf。thp稱為脈沖寬度,這個時間不能太短,太短電子槍可能識別不到。電子槍正確識別到thp后,會從最右端移動最左端,這個移動的時間就是thb,稱之為移動時間。thf表示顯示完最右像素,再過多久Hsync才來。

同理,當電子槍一行一行的從上面移動到最下面時,Vsync垂直同步信號就讓電子槍移動回最上邊。Vsync中的tvp是脈沖寬度,tvb是移動時間,tvf表示顯示完最下一行像素,再過多久Vsync才來。
假設一共有y行,則LCD的分辨率就是x*y。

關於顯示原理,可以參考這篇博客:http://www.cnblogs.com/shangdawei/p/4760933.html

里面有一個LCD顯示配置示意圖如下:
這里寫圖片描述

當發出一個HSYNC信號后,電子槍就會從最右邊花費HBP時長移動到最左邊,等到了最右邊后,等待HFP時長HSYNC信號才回來。因此,HBP和HFP分別決定了左邊和右邊的黑框。

同理,當發出一個VSYNC信號后,電子槍就會從最下邊花費VBP時長移動到最上邊,等到了最下邊后,等待VFP時長VSYNC信號才回來。因此,VBP和VFP分別決定了上邊和下邊的黑框。
中間灰色區域才是有效顯示區域。

再來解決最后一個問題:每個像素再FrameBuffer中,占據多少位BPP(Bits Per Pixels)?
前面的LCD引腳功能圖里,R0-R7、G0-G7、B0-B7,每個像素是占據3*8=24位的,即硬件上LCD的BPP是確定的。

雖然LCD上的引腳是固定的,但我們使用的時候,可以根據實際情況進行取舍,比如我們的JZ2440使用的是16BPP,因此LCD只需要R0-R4、G0-G5、B0-B4與SOC相連,5+6+6=16BPP,每個像素就只占據16位數據。

我們寫程序的思路如下:

  1. 查看LCD芯片手冊,查看相關的時間參數、分辨率、引腳極性;

  2. 根據以上信息設置LCD控制器寄存器,讓其發出正確信號;

  3. 在內存里面分配一個FrameBuffer,在里面用若干位表示一個像素,再把首地址告訴LCD控制器;

之后LCD控制器就能周而復始取出FrameBuffer里面的像素數據,配合其它控制信號,發送給電子槍,電子槍再讓在LCD上顯示出來。以后我們想顯示圖像,只需要編寫程序向FrameBuffer填入相應數據即可,硬件會自動的完成顯示操作。

第002節_S3C2440_LCD控制器

LCD控制器主要功能和需要的設置:

    1. 取:從內存(FrameBuffer)取出某個像素的數據;之后需要把FrameBuffer地址、BPP、分辨率告訴LCD控制器;
    1. 發:配合其它信號把FrameBuffer數據發給LCD;需要設置LCD控制器時序、設置引腳極性;

這里主要的難點就是如何配合其它信號,需要我們閱讀LCD芯片手冊,知道其時序要求,然后設置相應的LCD控制器。

先看下S3C2440芯片手冊上的LCD控制器框圖:
這里寫圖片描述

通過設置REGBANK(寄存器組),LCDCDMA會自動(無需CPU參與)把內存上FrameBuffer里的數據,通過VIDPRCS發送到引腳VD[23:0]上,再配合VIDEOMUX引腳的控制信號,正確的顯示出來。

S3C2440芯片手冊介紹了LCD控制器支持TFT和STN兩種LCD,我們常用的都是TFT材質的,因此主要看TFT相關的部分。

調色板的概念:

畫油畫的時候,通常先在調色板里配好想要的顏色,再用畫筆沾到畫布上作畫。LCD控制器里也借用了這個概念,從FrameBuffer獲得數據,這個數據作為索引從調色板獲得對應數據,再發給電子槍顯示出來。
這里寫圖片描述

如圖,假如是16BPP的數據,LCD控制器從FB取出16bit數據,顯示到LCD上。

當如果想節約內存,對顏色要求也沒那么高,就可以采用調色板的方式,調色板里存放了256個16bit的數據,FB只存放每個像素的索引,根據索引去調色板找到對應的數據傳給LCD控制器,再通過電子槍顯示出來。

假設現在想要LCD只顯示一種顏色怎么辦?

如果是16BPP/24BPP需要修改FB里面的數據,填充同一個值。

如果是8BPP可以修改FB為同一種顏色,也可以設置調色板為同一種顏色,對於S3C22440有個臨時調色板的特性,一旦使能了臨時調色板,不管FB里面是什么數據,都只調用臨時調色板的數據。

第003節_編程_框架與准備

本節主要有兩個目的:

a. 講解后續程序的框架;

b. 准備一個支持NAND、NOR啟動的程序;

我們的目的是在LCD顯示屏上畫線、畫圓(geomentry.c)和寫字(font.c)其核心是畫點(farmebuffer.c),這些都屬於純軟件。此外還需要一個lcd_test.c測試程序提供操作菜單,調用畫線、畫圓和寫字操作。

往下操作的是LCD相關的內容,不同的LCD,其配置的參數也會不一樣,通過lcd_3.5.c或lcd_4.3.c來設置。

根據LCD的特性,來設置LCD控制器,對於我們開發板,就是s3c2440_lcd_controller.c,假如希望在其它開發板上也實現LCD顯示,只需添加相應的代碼文件即可。

這就是LCD編程的框架,盡可能的“高內聚低耦合”。
這里寫圖片描述

為了讓程序更加好擴展,下面介紹“面向對象編程”的概念。

假如我們寫好程序后,有兩款尺寸大小的lcd,如何快速的在兩個lcd上切換?

首先我們抽象出lcd_3.5.c和lcd_4.3.c的共同點,比如都有初始化函數init(),我們可以新建一個lcd.c,然后定義一個結構體:

struct lcd_opr{
    void (*init)(void);
};

用戶不接觸lcd_3.5.c和lcd_4.3.c,只需要在lcd.c里通過指針訪問對應的結構體的函數,也就調用了不同init()。
這里寫圖片描述

前面我們的程序大小都沒超過4K,因此無論Nor/Nand啟動,都是正常的,現在的LCD相關代碼比較大,超過4K,因此需要修改啟動部分的代碼。

目前還未講解nand flash,因此直接將19課准備的nand_flash程序部分復制到當前代碼里即可,關於這部分可以參考nand flash講解部分。

第004節_編程_抽象出重要結構體

開始正式編寫程序,根據前面的框架,新建如下文件:

font.c、framebuffer.c、geometry.c、lcd.c、lcd_4.3.c、lcd_controller.c、s3c2440_lcd_controller.c、lcd_test.c

首先編寫lcd_controller.c,它向上要接收不同LCD的參數,向下要使用這些參數設置對應的LCD控制器。

前面我們列舉了LCD的參數,例如引腳的極性、時序、數據的格式bpp、分辨率等,使用面向對象的思維方式,將這些封裝成結構體放在lcd.h中:

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

/* NORMAL : 正常極性
 * INVERT : 反轉極性
 */
typedef struct pins_polarity {
	int vclk;  /* normal: 在下降沿獲取數據 */
	int rgb;   /* normal: 高電平表示1 */
	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 {
	/* 引腳極性 */
	pins_polarity pins_pol;
	
	/* 時序 */
	time_sequence time_seq;
	
	/* 分辨率, bpp */
	int xres;
	int yres;
	int bpp;
	
	/* framebuffer的地址 */
	unsigned int fb_base;
}lcd_params, *p_lcd_params;

以后就使用lcd_params結構體來表示lcd參數。

對於有多個lcd的情況,再定義個一個結構體,包含指針初始化函數和使能函數,放在lcd_controller.h里面:

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

最后在lcd_controller.c里傳入lcd參數,再通過指針函數初始化對應的lcd控制器:

void lcd_controller_init(p_lcd_params plcdparams)
{
	/* 調用2440的LCD控制器的初始化函數 */
	lcd_controller.init(plcdparams);
}

在s3c2440_lcd_controller.c還需構造一個當前soc的lcd控制器結構體:

struct lcd_controller s3c2440_lcd_controller = {
	.init    = xxx,
	.enalbe  = xxx,
	.disable = xxx,
};

第005節_編程_LCD控制器

繼續上一節的代碼,修改s3c2440_lcd_controller.c:

struct lcd_controller s3c2440_lcd_controller = {
	.init    = s3c2440_lcd_controller_init,
	.enalbe  = s3c2440_lcd_controller_enalbe,
	.disable = xs3c2440_lcd_controller_disable,
};

然后對每個函數進行功能實現,首先是s3c2440_lcd_controller_init,依次設置LCD控制器寄存器,先是LCD寄存器1

這里寫圖片描述

[27:18]為只讀數據位,不需要設置;

[17:8]用於設置CLKVAL(像素時鍾頻率),我們使用的是TFT屏,因此采用的公式是VCLK = HCLK / [(CLKVAL+1) x 2],其中HCLK為100M。LCD手冊里面Clock cycle的要求范圍為5-12MHz即可,即假設VCLK=9,根據公式9=100/[(CLKVAL+1)x2],算出CLKVAL≈4.5=5。VCLK為plcdparams->time_seq.vclk,則clkval = HCLK/plcdparams->time_seq.vclk/2-1+0.5;

[7]不用管,默認即可;

[6:5]TFT lcd配置為0b11;

[4:1]設置bpp模式,根據傳入的plcdparams->bpp配置為相應的數值;

[0]LCD輸出使能,先暫時關閉不輸出;

寄存器2

這里寫圖片描述

對比2440LCD部分時序圖和LCD時序圖,得出兩者之間關系,以后就可通過plcdparams傳參數進來設置相關寄存器。

[31:24] : VBPD = tvb - 1

[23:14] : LINEVAL = line - 1

[13:6] : VFPD = tvf - 1

[5:0] : VSPW = tvp - 1

寄存器3

這里寫圖片描述

[25:19] : HBPD = thb - 1

[18:8] : HOZVAL = 列 - 1

[7:0] : HFPD = thf - 1

寄存器4

這里寫圖片描述
[7:0] : HSPW = thp - 1

寄存器5

這里寫圖片描述
用來設置引腳極性, 設置16bpp, 設置內存中象素存放的格式

[12] : BPP24BL

[11] : FRM565, 1-565

[10] : INVVCLK, 0 = The video data is fetched at VCLK falling edge

[9] : HSYNC是否反轉

[8] : VSYNC是否反轉

[7] : INVVD, rgb是否反轉

[6] : INVVDEN

[5] : INVPWREN

[4] : INVLEND

[3] : PWREN, LCD_PWREN output signal enable/disable

[2] : ENLEND

[1] : BSWP

[0] : HWSWP

然后再設置framebuffer地址,先是
LCDSADDR1

這里寫圖片描述

[29:21] : LCDBANK, A[30:22] of fb

[20:0] : LCDBASEU, A[21:1] of fb

即用[29:0]表示起始地址的[30:1]。

LCDSADDR2

這里寫圖片描述

[20:0] : LCDBASEL, A[21:1] of end addr

即framebuffer的結束地址。

最后還要設置相關引腳,包括背光控制引腳、LCD專用引腳、電源控制引腳:

void jz2440_lcd_pin_init(void)
{
	/* 初始化引腳 : 背光引腳 */
	GPBCON &= ~0x3;
	GPBCON |= 0x01;

	/* LCD專用引腳 */
	GPCCON = 0xaaaaaaaa;
	GPDCON = 0xaaaaaaaa;

	/* PWREN */
	GPGCON |= (3<<8);
}

LCD所有寄存器的具體設置如下:

#define HCLK 100

void jz2440_lcd_pin_init(void)
{
	/* 初始化引腳 : 背光引腳 */
	GPBCON &= ~0x3;
	GPBCON |= 0x01;

	/* LCD專用引腳 */
	GPCCON = 0xaaaaaaaa;
	GPDCON = 0xaaaaaaaa;

	/* PWREN */
	GPGCON |= (3<<8);
}


/* 根據傳入的LCD參數設置LCD控制器 */
void s3c2440_lcd_controller_init(p_lcd_params plcdparams)
{
	int pixelplace;
	unsigned int addr;

	jz2440_lcd_pin_init();
	
	/* [17:8]: clkval, vclk = HCLK / [(CLKVAL+1) x 2]
	 *                   9   = 100M /[(CLKVAL+1) x 2], clkval = 4.5 = 5
	 *                 CLKVAL = 100/vclk/2-1
	 * [6:5]: 0b11, tft lcd
	 * [4:1]: bpp mode
	 * [0]  : LCD video output and the logic enable/disable
	 */
	int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
	int bppmode = plcdparams->bpp == 8  ? 0xb :\
				  plcdparams->bpp == 16 ? 0xc :\
				  0xd;  /* 0xd: 24bpp */
	LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;

	/* [31:24] : VBPD    = tvb - 1
	 * [23:14] : LINEVAL = line - 1
	 * [13:6]  : VFPD    = tvf - 1
	 * [5:0]   : VSPW    = tvp - 1
	 */
	LCDCON2 = 	((plcdparams->time_seq.tvb - 1)<<24) | \
	            ((plcdparams->yres - 1)<<14)         | \
				((plcdparams->time_seq.tvf - 1)<<6)  | \
				((plcdparams->time_seq.tvp - 1)<<0);

	/* [25:19] : HBPD	 = thb - 1
	 * [18:8]  : HOZVAL  = 列 - 1
	 * [7:0]   : HFPD	 = thf - 1
	 */
	LCDCON3 =	((plcdparams->time_seq.thb - 1)<<19) | \
				((plcdparams->xres - 1)<<8)		      | \
				((plcdparams->time_seq.thf - 1)<<0);

	/* 
	 * [7:0]   : HSPW	 = thp - 1
	 */
	LCDCON4 =	((plcdparams->time_seq.thp - 1)<<0);

    /* 用來設置引腳極性, 設置16bpp, 設置內存中象素存放的格式
     * [12] : BPP24BL
	 * [11] : FRM565, 1-565
	 * [10] : INVVCLK, 0 = The video data is fetched at VCLK falling edge
	 * [9]  : HSYNC是否反轉
	 * [8]  : VSYNC是否反轉
	 * [7]  : INVVD, rgb是否反轉
	 * [6]  : INVVDEN
	 * [5]  : INVPWREN
	 * [4]  : INVLEND
	 * [3]  : PWREN, LCD_PWREN output signal enable/disable
	 * [2]  : ENLEND
	 * [1]  : BSWP
	 * [0]  : HWSWP
	 */

	pixelplace = plcdparams->bpp == 24 ? (0) : |\
	             plcdparams->bpp == 16 ? (1) : |\
	             (1<<1);  /* 8bpp */
	LCDCON5 = (plcdparams->pins_pol.vclk<<10) |\
	          (plcdparams->pins_pol.rgb<<7)   |\
	          (plcdparams->pins_pol.hsync<<9) |\
	          (plcdparams->pins_pol.vsync<<8) |\
 			  (plcdparams->pins_pol.de<<6)    |\
			  (plcdparams->pins_pol.pwren<<5) |\
			  (1<<11) | pixelplace;

	/* framebuffer地址 */
	/*
	 * [29:21] : LCDBANK, A[30:22] of fb
	 * [20:0]  : LCDBASEU, A[21:1] of fb
	 */
	addr = plcdparams->fb_base & ~(1<<31);
	LCDSADDR1 = (addr >> 1);

	/* 
	 * [20:0] : LCDBASEL, A[21:1] of end addr
	 */
	addr = plcdparams->fb_base + plcdparams->xres*plcdparams->yres*plcdparams->bpp/8;
	addr >>=1;
	addr &= 0x1fffff;
	LCDSADDR2 = addr;//	
}

void s3c2440_lcd_controller_enalbe(void)
{
	/* 背光引腳 : GPB0 */
	GPBDAT |= (1<<0);
	
	/* pwren    : 給LCD提供AVDD  */
	LCDCON5 |= (1<<3);
	
	/* LCDCON1'BIT 0 : 設置LCD控制器是否輸出信號 */
	LCDCON1 |= (1<<0);
}

void s3c2440_lcd_controller_disable(void)
{
	/* 背光引腳 : GPB0 */
	GPBDAT &= ~(1<<0);

	/* pwren	: 給LCD提供AVDD  */
	LCDCON5 &= ~(1<<3);

	/* LCDCON1'BIT 0 : 設置LCD控制器是否輸出信號 */
	LCDCON1 &= ~(1<<0);
}

這就完成了s3c2440_lcd_controller.c的編寫,后面只需要向s3c2440_lcd_controller_init()傳入構造好的參數即可。

第006節_編程_LCD設置

前面編寫了s3c2440_lcd_controller.c,以后我們只需往里面傳入參數即可控制LCD控制器,對於我們的4.3寸LCD,配合LCD手冊時序的介紹,相關的設置如下:

這里寫圖片描述

#define LCD_FB_BASE 0x33c00000

lcd_params lcd_4_3_params = {
	.name = "lcd_4.3"
	.pins_polarity = {
		.de    = NORMAL,	/* normal: 高電平時可以傳輸數據 */
		.pwren = NORMAL,    /* normal: 高電平有效 */
		.vclk  = NORMAL,	/* normal: 在下降沿獲取數據 */
		.rgb   = NORMAL,	/* normal: 高電平表示1 */
		.hsync = INVERT,    /* normal: 高脈沖 */
		.vsync = INVERT, 	/* normal: 高脈沖 */
	},
	.time_sequence = {
		/* 垂直方向 */
		.tvp=	10, /* vysnc脈沖寬度 */
		.tvb=	2,  /* 上邊黑框, Vertical Back porch */
		.tvf=	2,  /* 下邊黑框, Vertical Front porch */

		/* 水平方向 */
		.thp=	41, /* hsync脈沖寬度 */
		.thb=	2,  /* 左邊黑框, Horizontal Back porch */
		.thf=	2,  /* 右邊黑框, Horizontal Front porch */

		.vclk=	9,  /* MHz */
	},
	.xres = 480,
	.yres = 272,
	.bpp  = 16,
	.fb_base = LCD_FB_BASE,
};

完成了lcd控制器和參數的代碼,現在還需要一個管理的中間層將兩者連在一起。

我們用lcd_controller.c管理s3c2440_lcd_controller.c,它向上接受傳入的LCD參數,向下傳給對應的LCD控制器。lcd_controller.c管理下級的控制器的思路如下:

  • a. 用數組保存下面各種lcd_controller;

  • b. 提供register_lcd_controller給下面的代碼設置數組;

  • c. 提供select_lcd_controller(name)給上面的代碼選擇某個lcd_controller;

lcd_controller.c代碼如下:

#define LCD_CONTROLLER_NUM 10

static p_lcd_controller p_array_lcd_controller[LCD_CONTROLLER_NUM];
static p_lcd_controller g_p_lcd_controller_selected;

int register_lcd_controller(p_lcd_controller plcdcon)
{
	int i;
	for (i = 0; i < LCD_CONTROLLER_NUM; i++)
	{
		if (!p_array_lcd_controller[i])
		{
			p_array_lcd_controller[i] = plcdcon;
			return i;
		}
	}
	return -1;		
}

int select_lcd_controller(char *name)
{
	int i;
	for (i = 0; i < LCD_CONTROLLER_NUM; i++)
	{
		if (p_array_lcd_controller[i] && !strcmp(p_array_lcd_controller[i]->name, name))
		{
			g_p_lcd_controller_selected = p_array_lcd_controller[i];
			return i;
		}
	}
	return -1;		
}


/* 向上: 接收不同LCD的參數
 * 向下: 使用這些參數設置對應的LCD控制器
 */

int lcd_controller_init(p_lcd_params plcdparams)
{
	/* 調用所選擇的LCD控制器的初始化函數 */
	if (g_p_lcd_controller_selected)
	{
		g_p_lcd_controller_selected->init(plcdparams);
		return 0;
	}
	return -1;
}

void lcd_contoller_add(void)
{
	s3c2440_lcd_contoller_add();
}


同時,在s3c2440_lcd_controller.c里注冊控制器:

void s3c2440_lcd_contoller_add(void)
{
	register_lcd_controller(&s3c2440_lcd_controller);
}

這樣,s3c2440_lcd_controller.c里的register_lcd_controller()將自己放在p_array_lcd_controller[]這個數組,然后上層的lcd_controller.c調用select_lcd_controller()傳入要選擇的LCD控制器,然后在數組里面找到名字名字匹配的LCD控制器進行相應的初始化。

同理,也通過lcd.c去管理lcd_4.3.c,思路如下:

  • a. 有一個數組存放各類lcd的參數;
  • b. 有一個register_led給下面的lcd程序來設置數組;
  • c. 有一個select_lcd,供上層選擇某款LCD;

參考前面的lcd_controller.c編輯lcd_controller.c如下:

#define LCD_NUM 10

static p_lcd_params p_array_lcd[LCD_NUM];
static p_lcd_params g_p_lcd_selected;

int register_lcd(p_lcd_params plcd)
{
	int i;
	for (i = 0; i < LCD_NUM; i++)
	{
		if (!p_array_lcd[i])
		{
			p_array_lcd[i] = plcd;
			return i;
		}
	}
	return -1;		
}

int select_lcd(char *name)
{
	int i;
	for (i = 0; i < LCD_NUM; i++)
	{
		if (p_array_lcd[i] && !strcmp(p_array_lcd[i]->name, name))
		{
			g_p_lcd_selected = p_array_lcd[i];
			return i;
		}
	}
	return -1;		
}

在lcd_4.3.c里面把lcd參數注冊進去:

void lcd_4_3_add(void)
{
register_lcd(&lcd_4_3_params);
}

以后只需要在lcd.c里面選擇某款lcd和某款lcd控制器即可,底層的只管添加種類即可。

在lcd.c里面添加初始化函數如下:

int lcd_init(void)
{
	/* 注冊LCD */
	lcd_4_3_add();

	/* 注冊LCD控制器 */
	lcd_contoller_add();
	
	/* 選擇某款LCD */
	select_lcd("lcd_4.3");

	/* 選擇某款LCD控制器 */
	select_lcd_controller("s3c2440");

	/* 使用LCD的參數, 初始化LCD控制器 */
	lcd_controller_init(g_p_lcd_selected);
}

第007節_編程_簡單測試

首先向lcd_test.c里面添加一個測試函數lcd_test(),用於向framebuffer寫數據,所需步驟如下:

  • a. 初始化LCD

  • b. 使能LCD

  • c. 獲取LCD參數: fb_base, xres, yres, bpp

  • d. 往framebuffer中寫數據

  • a. 初始化LCD

lcd_init();
  • b. 使能LCD
lcd_enable();

該函數實際調用的是lcd_controller_enable()

  • c. 獲取LCD參數: fb_base, xres, yres, bpp
    只有獲取到LCD的參數信息,才能根據這些信息進行相應顯示。
get_lcd_params(&fb_base, &xres, &yres, &bpp);

該函數是在lcd.c里面實現:

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;
}
  • d. 往framebuffer中寫數據
    假設現在BPP=16,想讓全屏顯示紅色,就需要從framebuffer基地址開始一直填充對應的顏色數據。
    對於16BPP,RGB=565,想顯示紅色,即[15:11]全為1表示紅色,[10:5]全為0表示無綠色,[4:0]全為0表示無藍色,0b1111100000000000=0xF700。

以基地址為起點,分別以xres和yres為邊界,依次填充顏色。

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

編寫好程序后,修改Makefile:

objs = start.o led.o uart.o init.o nand_flash.o main.o exception.o interrupt.o timer.o nor_flash.o my_printf.o string_utils.o lib1funcs.o

objs += lcd/font.o
objs += lcd/framebuffer.o
objs += lcd/geometry.o
objs += lcd/lcd.o
objs += lcd/lcd_4.3.o
objs += lcd/lcd_controller.o
objs += lcd/lcd_test.o
objs += lcd/s3c2440_lcd_controller.o

all: $(objs)
	#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-ld -T sdram.lds $^ -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis
	
%.o : %.c
	arm-linux-gcc -march=armv4 -c -o $@ $<

%.o : %.S
	arm-linux-gcc -march=armv4 -c -o $@ $<

然后把工程文件放到虛擬機上交叉編譯,根據編譯提示結果進行對應修改。

常見問題包括:頭文件未添加、數據類型錯誤等。

同理,假如現在是24BPP,即RGB:888,每個顏色占8位,一共占據24位。

雖然顏色數據只占據24位,但實際中是占據的32位(1字節)方便存儲計算,即[23:0]存放的是數據,[31:24]空閑無數據。

對於32BPP,大多數情況下和24BPP差不多的,即RGB:888,每個顏色占8位,一共占據24位。因此想依次顯示紅綠藍,代碼如下:

		/* 0xRRGGBB */
        /* red */
		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;

之前我們講數據是8BPP的時候,可以通過調色板轉成16BPP,在LCD上顯示出相應顏色。

那前面的24BPP、32BPP是怎樣在 只能接收16BPP(硬件上只有16根數據線)的LCD上顯示的呢?

這是因為在使用24BPP時,發出的8條紅色,8條綠色,8條藍色數據,只用了高5條紅色,高6條綠色,高5條藍色與LCD相連。

第008節_編程_畫點線圓

本節將在LCD上畫點畫圓,無論是何種圖形,都是基於點來構成的,因此我們需要先實現畫點。

在前面第003節_編程_框架與准備所講的框架里,計划的是在farmebuffer.c實現畫點,在geomentry.c實現畫線、畫圓,font.c實現寫字。

我們先在farmebuffer.c實現畫點,一個點(x,y)在FB中的位置如圖:

這里寫圖片描述

可以得出其計算公式:

(x,y)像素起始地址=fb_base+(xres*bpp/8)*y + y*bpp/8

同時利用yres來分辨y方向的邊界。

因此,需要先從LCD中獲取參數:fb_base、xres、yres、bpp;

static unsigned int fb_base;
static int xres, yres, bpp;

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

再實現畫點操作:

void fb_put_pixel(int x, int y, unsigned int color)
{
	unsigned char  *pc;  /* 8bpp */
	unsigned short *pw;  /* 16bpp */
	unsigned int   *pdw; /* 32bpp */

	unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;

	switch (bpp)
	{
		case 8:
			pc = (unsigned char *) pixel_base;
			*pc = color;
			break;
		case 16:
			pw = (unsigned short *) pixel_base;
			*pw = convert32bppto16bpp(color);
			break;
		case 32:
			pdw = (unsigned int *) pixel_base;
			*pdw = color;
			break;
	}
}

對於8PP,每個像素只占據8位(1字節),因此采用unsigned char類型;

對於16PP,每個像素只占據16位(2字節),因此采用unsigned short類型;

對於32PP,每個像素只占據32位(4字節),因此采用unsigned int類型;

再根據傳入的x,y坐標,計算出對應的顯存位置。

根據BPP的不同,修改相應位的顯存數據。

傳入的顏色數據一般都是32bit的,即格式為:0x00RRGGBB,

對於8PP,通過的是調色板索引實現的,這個后續再講解,直接*pc = color即可。

對於16PP,需要進行顏色轉換。

對於32PP,大小剛好對應,直接*pc = color即可。

使用convert32bppto16bpp()函數進行顏色數據轉換:

unsigned short convert32bppto16bpp(unsigned int rgb)
{
	int r = (rgb >> 16)& 0xff;
	int g = (rgb >> 8) & 0xff;
	int b = rgb & 0xff;

	/* rgb565 */
	r = r >> 3;
	g = g >> 2;
	b = b >> 3;

	return ((r<<11) | (g<<5) | (b));
}

先分別取出RGB,再相應的清除低位數據,實現將RGB888變為RGB565,最后再組成unsigned short類型數據返回。

畫圓畫線的具體原理不是我們的主要內容,我們直接百度“C語言 LCD 畫圓”可以得到相關的實現代碼,比如這篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

新建一個geometry.c,復制博客中代碼,替換里面的描點顯示函數即可。

最后在主函數測試程序里,加上畫圓畫線的測試代碼:

    /* 畫線 */
	draw_line(0, 0, xres - 1, 0, 0xff0000);
	draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);
	draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
	draw_line(0, 0, 0, yres - 1, 0xff00ef);
	draw_line(0, 0, xres - 1, yres - 1, 0xff4500);
	draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

	delay(1000000);

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

希望在LCD上顯示如下圖形,由6條線和一個圓組成。

將線的起始坐標作為參數傳入畫線函數。

將圓心和半徑作為參數傳入畫圓函數。
這里寫圖片描述

第009節_編程_顯示文字

文字也是由點構成的,一個個點組成的點陣,宏觀的來看,就是文字。

可以參考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”。

新建一個font.c,根據字母的點陣在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中得到點陣數據 */
	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個點陣數據,獲得字符圖形。

同樣的,在顯示之前,還需要獲取LCD參數:

extern const unsigned char fontdata_8x16[];
/* 獲得LCD參數 */
static unsigned int fb_base;
static int xres, yres, bpp;

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

如果想顯示字符串,那就在每顯示完一個字符后,x軸加8即可,同時考慮是否超出屏幕顯示范圍進行換行處理:

/* "abc\n\r123" */
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++;
	}
}

最后在在主函數里,加上顯示字符串的函數,傳入希望顯示的字符串。

第010節_編程_添加除法

在s3c2440_lcd_controller.c里,以前我們使用如下除法:

int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;

編譯的時候會提示出錯:
這里寫圖片描述

我們的lib1funcs.S里面是有除法的,但除法的功能不夠強,將前面除法計算代碼中的double改成精度更小的float還是不行。

對於未實現的函數:

  • a. 去uboot中查找
  • b. 去內核源碼中查找
  • c. 去庫函數中查找(一般來說編譯器自帶有很多庫)

在庫函數中查找步驟:

  • a. 輸入命令arm-linux-gcc -v,查看當前使用的交叉編譯工具鏈;
  • b. 輸入命令echo $PATH,在環境變量中找到當前使用的交叉編譯工具鏈所在的路徑;
  • c. 進入交叉編譯工具鏈所在目錄搜索相關函數,例如grep "__floatsisf" * -nR;
  • d. 提取出其中的靜態庫(.a后綴文件),復制文件到代碼文件;
  • e. 修改Makefile,依次嘗試加入的每個靜態庫,直至編譯成功;

注意:如果你更換了編譯器,需要自己去編譯器目錄里找出對應的libgcc.a;

有可能有多個libgcc.a,逐個嘗試;

第011節_編程_使用調色板#

前面我們寫的程序都是采用的16BPP或者24BPP(也就是32BPP),假如我們要使用8PP,就得使用調色板。

如圖所示8PP工作原理示意圖,在FB只存放8bit得每個像素索引,根據這個索引,在去去調色板找到對應的數據傳給LCD控制器,再通過電子槍顯示出來。

調色板里面有2^8(256)個顏色數據,每個顏色數據為16bit,表示一種顏色。
這里寫圖片描述

在硬件上,我們要初始化這個調色板,才能通過索引得到顏色。

根據第三節的軟件框架,調色板的初始化應該放在s3c2440_lcd_controller.c里面。

在lcd_controller結構體里添加調色板初始化函數:

struct lcd_controller s3c2440_lcd_controller = {
	.name    = "s3c2440",
	.init    = s3c2440_lcd_controller_init,
	.enable  = s3c2440_lcd_controller_enalbe,
	.disable = s3c2440_lcd_controller_disable,
	.init_palette = s3c2440_lcd_controller_init_palette,
};

調色板對應一塊內存,我們需要找到他的位置和格式。

這里寫圖片描述

從芯片手冊了解到,其起始地址為0x4D000400,且設置調色板前,需要關閉LCD控制器。

void s3c2440_lcd_controller_init_palette(void)
{
	volatile unsigned int *palette_base =  (volatile unsigned int *)0x4D000400;
	int i;

	int bit = LCDCON1 & (1<<0);

	/* LCDCON1'BIT 0 : 設置LCD控制器是否輸出信號 */
	if (bit)
		LCDCON1 &= ~(1<<0);

	for (i = 0; i < 256; i++)
	{
		/* 低16位 : rgb565 */	
		*palette_base++ = i;
	}

	if (bit)
		LCDCON1 |= (1<<0);
}

設置調色板前,先判斷LCD控制器是否打開,如果打開了就先關閉,且設置完成后再打開。

再網上搜索了一下,沒有找到調色板數據數組,這里作為實驗,就隨便設置,讓其為i。

我們讓調色板數據等於i,0-255,只占據8位,最后的顏色范圍為B5G3R0,因此會比較偏藍。

修改lcd_4.3.c,將BPP改為8,

再修改lcd_controller.c的初始化函數,加入調色板初始化函數。

int lcd_controller_init(p_lcd_params plcdparams)
{
	/* 調用所選擇的LCD控制器的初始化函數 */
	if (g_p_lcd_controller_selected)
	{
		g_p_lcd_controller_selected->init(plcdparams);
		g_p_lcd_controller_selected->init_palette();
		return 0;
	}
	return -1;
}

再修改lcd_test.c,加入bpp=8的情況:

	if (bpp == 8)
	{
		/* 讓LCD輸出整屏的紅色 */

		/* bpp: palette[12] */

		p0 = (unsigned char *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p0++ = 12;

		/* palette[47] */
		p0 = (unsigned char *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p0++ = 47;

		/* palette[88] */
		p0 = (unsigned char *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p0++ = 88;

		/* palette[0] */
		p0 = (unsigned char *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p0++ = 0;
			
	}

隨便讓其全屏顯示某個顏色。

最后在畫線畫圓,顯示文字的函數里,修改下顏色:

	/* 畫線 */
	draw_line(0, 0, xres - 1, 0, 0x23ff77);
	draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff);
	draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
	draw_line(0, 0, 0, yres - 1, 0xff00ef);
	draw_line(0, 0, xres - 1, yres - 1, 0xff45);
	draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

	delay(1000000);

	/* 畫圓 */
	draw_circle(xres/2, yres/2, yres/4, 0xff);

	/* 輸出文字 */
	fb_print_string(10, 10, "www.100ask.net\n\r100ask.taobao.com", 0xff);

上面的顏色數據,其實只有低兩位有效,因為前面的調色板映射范圍是0-255。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM