1. 程式人生 > >嵌入式Linux——LCD驅動

嵌入式Linux——LCD驅動

宣告:本文以韋東山老師的視訊為模本進行編寫,開發板為s3c2440,LCD為A043-24-TT-11,此LCD為480*272 的4.3寸螢幕。與老師所講的略有不同。同時本文為複習視訊所學的內容,如有巧合,敬請諒解;
要寫LCD驅動就要先從核心中找到支援LCD的軟體相關的部分,也就是fbmem.c檔案。fbmem.c作為LCD的軟體部分為其提供了程式碼不變的部分,即在入口函式中分配好了主裝置號:29,file_operations結構體和register_chrdev函式,詳細程式碼為:register_chrdev(FB_MAJOR,"fb",&fb_fops)
。而fbmem.c會根據不同的裝置通過register_fb陣列找到不同的設定程式碼進行呼叫。
我們所要編寫的將是硬體相關的部分,就是將裝置的fb_info結構體設定好然後放到register_fb中,讓fbmem.c呼叫,這部分與硬體相關,相對變化較大。而在編寫程式碼之前需要先了解fbmem.c做了什麼工作,而我們自己編寫的驅動又該做什麼樣的工作。首先我們分析入口函式,通過上面的程式碼我們知道了fbmem.c已經為我們將軟體的框架搭好,而假設當我們使用應用程式開啟這個裝置時,我們將呼叫file_operations中的open函式.open = fb_open:

int fbidx = iminor(inode); //獲得次裝置號
struct fb_info *info;      //分配一個fb_info結構體
info = registered_fb[fbidx]; //根據次裝置號從registered_fb中找到相應的fb_info結構體
if (info->fbops->fb_open)      //如果已經在info中定義了fb_open
	res = info->fbops->fb_open(info,1); //呼叫info中的fb_open
	

通過上面的分析我們可以看出,我們是通過register_fb來獲得fb_info結構體的,而register_fb陣列又是由什麼決定那?我們通過從fbmem.c 中查詢發現,在register_framebuffer函式中為register_fb賦值:

for (i = 0 ; i < FB_MAX; i++)   //找到一個沒有佔用的次裝置
	if (!registered_fb[i])
		break;
fb_info->dev = device_create(fb_class, fb_info->device,
	MKDEV(FB_MAJOR, i), "fb%d", i); //使用vdev機制自動生成裝置
registered_fb[i] = fb_info; //將這個fb_info結構體根據獲得次裝置號放入register_fb中

有上面的分析知道我們要寫一個與我們的LCD硬體相關的fb_info結構體,並通過register_framebuffer函式將這個fb_info結構體通過次裝置號放入register_fb中,而fbmem.c 就可以通過次裝置號呼叫register_fb中的fb_info,進而驅動這個硬體。所以我們所要寫的LCD驅動可以分為以下四步:

  1. 分配一個fb_info結構體
  2. 設定這個結構體
  3. 做硬體相關的操作
  4. 通過register_framebuffer函式註冊fb_info

有了上面的步驟,我們按著上面的步驟寫程式:而一些細節的部分我會在程式中說明:
下面我們寫第一步:
既然要分配一個fb_info結構體,我們就應該先定義這個結構體:

static struct fb_info *s3c_lcd;

然後是為其分配:

s3c_lcd =framebuffer_alloc(0,NULL); 

需要說明:

/**
 * framebuffer_alloc 函式就是創造一個新的frame buffer info結構體
 * @size: 是裝置私有資料,可以是0
 * @dev: 指向fb的裝置,這裡可以為 NULL
 * Returns: 返回一個新的fb_info結構體或者NULL(如果出錯).
 */
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)

下面就該進入第二步,設定fb_info結構體:


struct fb_info {
	int node;
	int flags;
	struct fb_var_screeninfo var;	/* Current var */
	struct fb_fix_screeninfo fix;	/* Current fix */
	struct fb_monspecs monspecs;	/* Current Monitor specs */
	struct work_struct queue;	/* Framebuffer event queue */
	struct fb_pixmap pixmap;	/* Image hardware mapper */
	struct fb_pixmap sprite;	/* Cursor hardware mapper */
	struct fb_cmap cmap;		/* Current cmap */
	struct list_head modelist;      /* mode list */
	struct fb_videomode *mode;	/* current mode */
	struct fb_ops *fbops;
	struct device *device;		/* This is the parent */
	struct device *dev;		/* This is this fb device */
	int class_flag;        /* private sysfs flags */
	char __iomem *screen_base;	/* Virtual address */
	unsigned long screen_size;	/* Amount of ioremapped VRAM or 0 */ 
	
	void *pseudo_palette;		/* Fake palette of 16 colors */ 
#define FBINFO_STATE_RUNNING	0
#define FBINFO_STATE_SUSPENDED	1
	u32 state;			/* Hardware state i.e suspend */
	void *fbcon_par;                /* fbcon use-only private area */
	/* From here on everything is device dependent */
	void *par;	
};

fb_info中有很多設定的選項,我們只設置與我們LCD相關的選項, 而其中重要的設定又分為四部分:

  • 設定fb_info的固定引數:struct fb_fix_screeninfo fix;
  • 設定fb_info的可變引數:struct fb_var_screeninfo var;
  • 設定fb_info操作函式:struct fb_ops *fbops;
  • fb_info的其他設定:char __iomem *screen_base;
    unsigned long screen_size;
    void *pseudo_palette;

下面我們先設定fb_info的固定引數(固定引數多為硬體相關而不會變化的,如螢幕的尺寸,視訊記憶體實體地址,和螢幕型別等):

strcpy(s3c_lcd->fix.id,"mylcd");
//s3c_lcd->fix.smem_start   //LCD視訊記憶體的實體地址,在3.3中設定
s3c_lcd->fix.smem_len    = 480*272*16/8;     //視訊記憶體的長度
s3c_lcd->fix.type        = FB_TYPE_PACKED_PIXELS;
s3c_lcd->fix.visual      = FB_VISUAL_TRUECOLOR; //TFT屏為真彩色
s3c_lcd->fix.line_length = 480*16/8; //一行的長度(單位:type)

再設定fb_info的可變引數(而可變的引數是可以根據不同情況而進行不同設定的,如:x,y方向虛擬解析度,多少位元組每畫素,以及RGB所佔有的比例等):

	s3c_lcd->var.xres          = 480;        //x方向真實的解析度
	s3c_lcd->var.yres          = 272;        //y方向真實的解析度
	s3c_lcd->var.xres_virtual  = 480;        //x方向虛擬的解析度
	s3c_lcd->var.yres_virtual  = 272;        //y方向虛擬的解析度
	s3c_lcd->var.xoffset       = 0;          //x方向真實解析度與虛擬解析度的差值
	s3c_lcd->var.yoffset       = 0;          //y方向真實解析度與虛擬解析度的差值

	s3c_lcd->var.bits_per_pixel = 16;        //設定16個位元組每畫素

	/*RGB:565*/	
	s3c_lcd->var.red.offset   = 11;
	s3c_lcd->var.red.length   = 5;
	s3c_lcd->var.green.offset = 5;
	s3c_lcd->var.green.length = 6;
	s3c_lcd->var.blue.offset  = 0;
	s3c_lcd->var.blue.length  = 5;

	s3c_lcd->var.activate     = FB_ACTIVATE_NOW;

之後設定操作函式:

s3c_lcd->fbops = &s3c_lcdfb_ops;
static struct fb_ops s3c_lcdfb_ops = {
	.owner		= THIS_MODULE,
	.fb_setcolreg	= s3c_lcdfb_setcolreg,   /* 調色盤 */
	.fb_fillrect	= cfb_fillrect,
	.fb_copyarea	= cfb_copyarea,
	.fb_imageblit	= cfb_imageblit,
};

最後就是對fb_info的其他設定:

//s3c_lcd->screen_base             //視訊記憶體的虛擬地址
s3c_lcd->screen_size = 480*272*2;     //螢幕的尺寸
s3c_lcd->pseudo_palette = pseudo_palette;  //調色盤

上面對fb_info設定完後就該到第三步:硬體相關的設定 ,而硬體首先要配置的就是用於LCD的GPIO介面,GPIO的圖為:
這裡寫圖片描述這裡寫圖片描述
這裡寫圖片描述
而相應的程式碼為:

gpbcon = ioremap(0x56000010, 8);
gpbdat = gpbcon + 1;
gpccon = ioremap(0x56000020, 4);  
gpdcon = ioremap(0x56000030, 4); 
gpgcon = ioremap(0x56000060, 4); 

/* 設定背光燈 */
*gpbcon &= ~(3);   //清零
*gpbcon |= (1);    //設定輸出模式
*gpbdat &= ~(1);   //設定低電平

/* 設定RGB資料介面 */
*gpccon = 0xaaaaaaaa;
*gpdcon = 0xaaaaaaaa;

/* 設定PWREN */
*gpgcon &= ~(3<<4);   //清零
*gpgcon |= (3<<4);    //設定LCD模式

下面將是對LCD控制器的設定,以使其可以支援相應的LCD,在這之前我們先構造一個lcd_reg的結構體用於存放LCD控制器的各種暫存器:

/* s3c2440 lcd registers */
struct lcd_reg{
	unsigned long lcdcon1;
	unsigned long lcdcon2;
	unsigned long lcdcon3;
	unsigned long lcdcon4;
	unsigned long lcdcon5;
	unsigned long lcdsaddr1;
	unsigned long lcdsaddr2;
	unsigned long lcdsaddr3;
	unsigned long REDLUT;
	unsigned long GREENLUT;
	unsigned long BLUELUT;
	unsigned long reserved[9];
	unsigned long DITHMODE;
	unsigned long TPAL;
	unsigned long LCDINTPND;
	unsigned long LCDSRCPND;
	unsigned long LCDINTMSK;
	unsigned long TCONSEL;
};

由於我開發板上的LCD與視訊中的LCD不是同一種類型的,所以這部分程式碼我做了相應的改動所以從lcdcon1到lcdcon4,我要另做說明:


/*
*LCDCON1
*bit[17:8] CLKVAL :TFT: VCLK = HCLK / [(CLKVAL+1) * 2] ( CLKVAL> 0 )
*					LCD手冊:VCLK =9MHZ ,而HCLK =100MHZ
*					所以 CLKVAL=5
*bit[6:5] PNRMODE :0b11 = TFT LCD panel
*bit[4:1] BPPMODE :0b1100 = 16 bpp for TFT
*bit [0]  ENVID   :0 = Disable the video output and the LCD control signal.
*/
lcd_regs->lcdcon1 = (5<<8) | (3<<5) | (12<<1) | (0<<0);

而垂直方向的時間引數發生了變化:
這裡寫圖片描述
所以程式碼為:

/*
*LCDCON2 :垂直方向時間引數
*bit[31:24] VBPD    : VSYNC 後再過多長時間才能發出第一個資料
*					  =1
*bit[23:14] LINEVAL : 多少行
*					  =271
*bit [13:6] VFPD    : 最後一行資料後再過多久發VSYNC訊號
*					  =1
*bit [5:0]  VSPW    : VSYNC脈衝寬度
*					  =9
*/
lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0);

水平方向的時間引數:這裡寫圖片描述
所以程式碼為:

/*
*水平方向時間引數
*LCDCON3:
*bit[25:19] HBPD    : HSYNC 後再過多長時間才能發出第一個資料
*					  =2
*bit[18:8] HOZVAL   : 多少列
*					  =479
*bit[7:0] HFPD      : 一行中發出最後一個畫素資料後再過多久發HSYNC訊號
*					  =2
*
*LCDCON4 :
*bit[7:0] HSPW     : HSYNC脈衝寬度
*					  =40
*/
lcd_regs->lcdcon3 = (2<<19) | (479<<8) | (2<<0);
lcd_regs->lcdcon4 = (40<<0);

訊號的極性並沒有發生變化,所以程式碼為:

/*
*訊號極性
*LCDCON5:
*
*bit[11] FRM565  : 16bpp輸出形式
*				   1 = 5:6:5 Format
*bit[10] INVVCLK : 表示是上升沿讀取資料還是下降沿讀取資料
*				   0 = 下降沿讀取資料
*bit[9] INVVLINE : 水平方向同步訊號是否反轉
*				   1 = 反轉
*bit[8] INVVFRAME: 垂直方向同步訊號是否反轉
*				   1 = 反轉
*bit[7] INVVD    : 資料脈衝是否反轉
*				   0 = Normal(不反轉)
*bit[6] INVVDEN  : 資料使能位是否反轉
*				   0 = normal
*bit[5] INVPWREN : PWREN(LCD電源)位是否反轉
*				   0 = normal
*bit[3] PWREN    : PWREN(LCD電源)位是否使能
*				   0 = Disable PWREN signal(不使能)
*bit[1] BSWP     :位元組轉換位
*				   0 = Swap Disable
*bit[0] HWSWP    : 半字轉換控制位
*				   1 = Swap Enable(轉換)
*/

lcd_regs->lcdcon5 = (1<<11) | (1<<9) | (1<<8) | (1<<0);

上面的工作做完我們的對LCD的設定就基本完成了,下面就是對視訊記憶體的設定了,

/*分配視訊記憶體:
//s3c_lcd->fix.smem_start
//s3c_lcd->fix.smem_len    = 480*272*16/8;
//s3c_lcd->screen_base
*/
s3c_lcd->screen_base = dma_alloc_writecombine(NULL,272*480*2,&(s3c_lcd->fix.smem_start),GFP_KERNEL);

/*
*把地址告訴LCD控制器:
*LCDSADDR1:
*bit[29:21] LCDBANK   :A[30:22] of the bank location for the video buffer in the system memory
*bit[20:0]  LCDBASEU  :A[21:1] of the start address of the LCD frame buffer
*
*LCDSADDR2:
*bit[20:0] LCDBASEL   :A[21:1] of the end address of the LCD frame buffer
*                       LCDBASEL = ((the frame end address) >>1) + 1
*									= LCDBASEU + (PAGEWIDTH+OFFSIZE) x (LINEVAL+1)
*
*LCDSADDR3:
*bit[10:0] PAGEWIDTH  : Virtual screen page width (the number of half words).
*
*/
lcd_regs->lcdsaddr1 = (s3c_lcd->fix.smem_start >> 1) & ~(3<<30);
lcd_regs->lcdsaddr2 = ((s3c_lcd->fix.smem_start + s3c_lcd->fix.smem_len) >>1) & 0x1fffff;
lcd_regs->lcdsaddr3 = 480*16/16;

而在這裡我需要講一下dma_alloc_writecombine函式:

void *
dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)

此函式為分配視訊記憶體的函式,而且這一視訊記憶體的實體地址為連續的地址, 其中第一個引數dev為裝置,第二個引數size表示要分配的地址的大小,第三個引數handle為實體地址,而第四個引數為標記。而此函式的返回值為分配記憶體的虛擬地址。

上面的工作做完後,我們就基本完成入口函式的程式,只差最後一步將fb_info結構體註冊了:

register_framebuffer(s3c_lcd);

然後我們完善程式,如調色盤:

static u32 pseudo_palette[16];
s3c_lcd->pseudo_palette = pseudo_palette;
//在操作函式中
.fb_setcolreg	= s3c_lcdfb_setcolreg,

static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red,
			     unsigned int green, unsigned int blue,
			     unsigned int transp, struct fb_info *info)
{
	unsigned int val;
	if (regno > 16) {
		return 1;
	}
	u32 *pseudo_palette = info->pseudo_palette;

	val  = chan_to_field(red, &info->var.red);
	val |= chan_to_field(green, &info->var.green);
	val |= chan_to_field(blue, &info->var.blue);

	pseudo_palette[regno] = val;

	return 0;
}
/* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
	chan &= 0xffff;
	chan >>= 16 - bf->length;
	return chan << bf->offset;
}

出口函式:

unregister_framebuffer(s3c_lcd);

lcd_regs->lcdcon1 &= ~1;         //關LCD控制器
lcd_regs->lcdcon5 &= ~(1<<3);    //關LCD本身,給LCD斷電
*gpbdat           &= ~1;         //關背光燈

dma_free_writecombine(NULL,s3c_lcd->fix.smem_len,s3c_lcd->screen_base,s3c_lcd->fix.smem_start);

iounmap(gpbcon);
iounmap(gpccon);  
iounmap(gpdcon); 
iounmap(gpgcon); 
iounmap(lcd_regs);

framebuffer_release(s3c_lcd);

寫完出口函式,我們的LCD驅動程式就寫完了。
下面就是測試了:測試的步驟為:

  1. 進入核心目錄:make menuconfig ,去掉原來的LCD驅動程式
  2. make uImage :生成沒有LCD驅動的核心
  3. cp arch/arm/boot/uImage /work/nfs_root/uImage_nolcds
  4. make modules :得到cfd_fillrect,cfb_copyarea,cfb_imageblit函式對應的模組
  5. 使用新的uImage_nolcds 啟動:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
  6. 編譯自己寫的LCD驅動程式,並將其考到根檔案系統
  7. 在開發板上使用安裝驅動:insmod cfbcopyarea.ko insmod cfdfillrect.ko insmod cfbimageblit.ko insmod lcd.ko
  8. echo hollo >/dev/tty1 ,可以在LCD上可以看到hello
  9. cat lcd.ko >/dev/fb0 ,花屏
  10. 修改/etc/inittab,加一行tty1::askfirst:-/bin/sh
  11. 使用新的uImage_nolcds 重新啟動:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
  12. insmod input.ko
  13. 可以按鍵s2,s3,s4,螢幕會顯示ls,和ls命令後的內容

我的文章中可能有些概念或道理講的不詳細,下面是我看到的幾篇我認為比較好的文章我在這裡轉載: