1. 程式人生 > >十二、Linux驅動之LCD驅動

十二、Linux驅動之LCD驅動

1. 基本概念

    LCDLiquid Crystal Display的簡稱,也就是經常所說的液晶顯示器。LCD能夠支援彩色影象的顯示和視訊的播放,是一種非常重要的輸出裝置。如果我們的系統要用GUI(圖形介面介面),比如minigui,MicroWindows。這時LCD裝置驅動程式就應該編寫成frambuffer介面,而不是編寫成僅僅操作底層的LCD控制器介面。
    framebufferLinux系統為顯示裝置提供的一個介面,它將顯示緩衝區抽象,遮蔽影象硬體的底層差異,允許上層應用程式在圖形模式下直接對顯示緩衝區進行操作。framebuffer

又叫幀緩衝,是Linux為操作顯示裝置提供的一個使用者介面。使用者應用程式可以通過framebuffer透明地訪問不同型別的顯示裝置。從這個方面來說,framebuffer是硬體裝置顯示緩衝區的抽象。Linux抽象出framebuffer這個幀緩衝區可以供使用者應用程式直接讀寫,通過更改framebuffer中的內容,就可以立刻顯示在LCD顯示屏上。
    framebuffer是一個標準的字元裝置,主裝置號是29,次裝置號根據緩衝區的數目而定。framebuffer對應/dev/fb%d裝置檔案。根據顯示卡的多少,裝置檔案可能是/dev/fb0、/dev/fb1等。緩衝區裝置也是一種普通的記憶體裝置,可以直接對其進行讀寫。
    對使用者程式而言,它和/dev
下面的其他裝置沒有什麼區別,使用者可以把frameBuffer看成一塊記憶體,既可以寫,又可以讀。顯示器將根據記憶體資料顯示對應的影象介面。這一切都由LCD控制器和響應的驅動程式來完成。

2. 分析核心

2.1 驅動框架

    LCD驅動也是一個字元裝置驅動,那麼核心中是如何實現的呢?框架如下圖:

    接下來我們便通過上圖結構深入分析核心實現LCD驅動的過程。

2.2 fbmem.c(drivers/video中)

    fbmem.cframbuffer驅動的核心,他向上給應用程式提供了系統呼叫介面,向下對特定的硬體提供底層的驅動介面。底層驅動可以通過介面向核心註冊自己。fbmem.c

提供了frambuffer驅動的所有介面程式碼,從而避免了。

2.2.1 入口函式fbmem_init()

    首先我們定位到核心(linux-2.6.22.6)fbmem.c的入口函式fbmem_init(),程式碼如下:

fbmem_init(void)
{
	create_proc_read_entry("fb", 0, NULL, fbmem_read_proc, NULL);

	if (register_chrdev(FB_MAJOR,"fb",&fb_fops))    //建立字元裝置
		printk("unable to get major %d for fb devs\n", FB_MAJOR);

	fb_class = class_create(THIS_MODULE, "graphics");    //建立類
	if (IS_ERR(fb_class)) {
		printk(KERN_WARNING "Unable to create fb class; errno = %ld\n", PTR_ERR(fb_class));
		fb_class = NULL;
	}
	return 0;
}

    在fbmem_init()中建立字元裝置"fb", 註冊file_oprations結構體fb_fops,主裝置FB_MAJOR為29。啟動核心後在開發板上執行“cat /proc/devices”如下:


    可以看到,確實是建立了主裝置號為29的"fb"裝置,而這裡還沒有建立裝置節點,後面會提到,核心將該工作放到註冊lcd驅動的介面函式裡了。

2.2.2 fb_open()

    繼續將程式碼定位到註冊的file_operations結構體裡面的fb_open()函式,部分程式碼如下:

static int fb_open(struct inode *inode, struct file *file)
{
       int fbidx = iminor(inode);      //獲取裝置節點的次裝置號
       struct fb_info *info;           //定義fb_info結構體
       int res = 0;
       ...

if (!(info = registered_fb[fbidx]))    //info= registered_fb[fbidx],獲取此裝置號的lcd驅動資訊
              try_to_load(fbidx);
       ...

       if (info->fbops->fb_open) {          
              res = info->fbops->fb_open(info,1);  //呼叫registered_fb[fbidx]->fbops->fb_open
              if (res)
                     module_put(info->fbops->owner);
       }

       return res;
}

       fb_open()函式間接呼叫registered_fb[fbidx]->fbops->fb_open(),核心搜尋registered_fb發現(include/linux/fb.h中)

extern struct fb_info *registered_fb[FB_MAX];    //#define FB_MAX    32

     可知registered_fb是一個struct fb_info結構體型別全域性陣列,搜尋核心發現在register_framebuffer()函式中被賦值。

2.2.3 register_framebuffer()

    register_framebuffer()函式部分程式碼如下:

int register_framebuffer(struct fb_info *fb_info)
{
	int i;
        ...
	for (i = 0 ; i < FB_MAX; i++)    //查詢空的registered_fb陣列項
		if (!registered_fb[i])
			break;
	fb_info->node = i;

	fb_info->dev = device_create(fb_class, fb_info->device, MKDEV(FB_MAJOR, i), "fb%d", i);    //建立裝置
        ...
	registered_fb[i] = fb_info;    //填充新的registered_fb陣列項
        ...
	return 0;
}

    register_framebuffer()函式首先從registered_fb陣列中查詢空的陣列項,然後填充fb_info結構體,賦給這個空的陣列項中,在這裡還建立了裝置節點(前面建立字元裝置未完成的工作)。從這裡我們可以看出,register_framebuffer()函式通過註冊各種各樣的fb_info,來讓核心支援多種LCD裝置,並且以/dev/fb*的形式命名。

2.2.4 fb_mmap()

    framebuffer的顯示緩衝區位於Linux的核心態地址空間。而在Linux中,每個應用程式都有自己的虛擬地址空間,在應用程式中是不能直接訪問物理緩衝區的。為此,Linux在檔案操作file_operations結構中提供了mmap()函式,可將檔案的內容對映到使用者空間。對應幀緩衝裝置,則可以通過對映操作,將螢幕緩衝區的實體地址對映到使用者空間的一段虛擬地址中,之後使用者就可以通過讀寫這段虛擬地址訪問螢幕緩衝區,在螢幕上繪圖。

2.2.5 總結

    通過引入fb_info的形式,將硬體相關的部分與fs檔案裝置操作分離開,增加了核心程式碼的穩定性。我們只需呼叫register_framebuffer()函式註冊一個新的fb_info結構體,即可向核心新增一個LCD驅動裝置。

2.3 s3c2410fb.c(drivers/video中)

    接下來我們再來分析核心如何構建fb_info結構體。

2.3.1 入口函式s3c2410fb_init()

    分析驅動,首先從入口函式入手,s3c2410fb_init()程式碼如下:

int __devinit s3c2410fb_init(void)
{
	return platform_driver_register(&s3c2410fb_driver);
}

    該函式註冊一個platform_driver,從上一節十一、Linux驅動之platform匯流排裝置驅動可知,當核心有成員.name名稱相同的platform_device時,會呼叫到platform_driver裡的成員.probe,在這裡就是s3c2410fb_probe()函式。

2.3.2 s3c2410fb_probe()

    s3c2410fb_probe()部分程式碼如下:

static int __init s3c2410fb_probe(struct platform_device *pdev)
{
       struct s3c2410fb_info *info;
       struct fb_info     *fbinfo;
       struct s3c2410fb_hw *mregs;
       int ret;
       int irq;
       int i;
       u32 lcdcon1;
 
       mach_info = pdev->dev.platform_data;     //獲取LCD裝置資訊(長寬、型別等)

       if (mach_info == NULL) {
              dev_err(&pdev->dev,"no platform data for lcd, cannot attach\n");
              return -EINVAL;
       }
       mregs = &mach_info->regs;


       irq = platform_get_irq(pdev, 0);    //得到中斷號
       if (irq < 0) {
              dev_err(&pdev->dev, "no irq for device\n");
              return -ENOENT;
       }

       fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); //分配一個fb_info結構體
       if (!fbinfo) {
              return -ENOMEM;
       }

       /*設定fb_info*/
       info = fbinfo->par;
       info->fb = fbinfo;
       info->dev = &pdev->dev;
       ... ...

       /*硬體相關的操作,設定中斷,LCD時鐘頻率,視訊記憶體地址, 配置引腳... ...*/
       ret = request_irq(irq, s3c2410fb_irq, IRQF_DISABLED, pdev->name, info); //設定中斷
       info->clk = clk_get(NULL, "lcd");                    //獲取時鐘
       clk_enable(info->clk);                               //使能時鐘
       ret = s3c2410fb_map_video_memory(info);              //視訊記憶體地址  
       ret = s3c2410fb_init_registers(info);                //設定暫存器,配置引腳
       ... ...

       ret = register_framebuffer(fbinfo);        //註冊一個fb_info結構體
       if (ret < 0) {
              printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret);
              goto free_video_memory;
       }
       ...
       return ret;
}

    該函式主要工作內容如下:
        (1) 分配一個fb_info結構體
        (2) 設定fb_info
        (3) 與LCD硬體相關的操作
        (4) 註冊fb_info結構體

    接下來仿造s3c2410fb.c編寫LCD驅動程式。

3. 編寫程式碼

3.1 程式碼框架

    3.1.1 在LCD驅動的入口函式中
      1. 分配一個fb_info結構體
      2. 設定fb_info
          2.1 設定固定的引數fb_info-> fix
          2.2 設定可變的引數fb_info-> var
          2.3 設定操作函式fb_info-> fbops
          2.4 設定fb_info 其它的成員
      3. 設定硬體相關的操作   
          3.1 配置LCD引腳
          3.2 根據LCD手冊設定LCD控制器
          3.3 分配視訊記憶體(framebuffer),把地址告訴LCD控制器和fb_info
      4. 開啟LCD,並註冊fb_info: register_framebuffer()
          4.1 直接在init函式中開啟LCD(後面講到電源管理,再來優化)
              4.1.1 控制LCDCON5允許PWREN訊號,
              4.1.2 然後控制LCDCON1輸出PWREN訊號,
              4.1.3 輸出GPB0高電平來開背光,
          4.2 註冊fb_info
    3.1.2 在LCD驅動的出口函式中

      1. 解除安裝核心中的fb_info
      2. 控制LCDCON1關閉PWREN訊號,關背光,iounmap登出地址
      3. 釋放DMA快取地址dma_free_writecombine()
      4. 釋放註冊的fb_info

3.2 編寫程式碼

    驅動程式lcd.c程式碼如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/div64.h>
#include <asm/mach/map.h>
#include <asm/arch/regs-lcd.h>
#include <asm/arch/regs-gpio.h>
#include <asm/arch/fb.h>

static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red,
			     unsigned int green, unsigned int blue,
			     unsigned int transp, struct fb_info *info);

struct lcd_regs {
	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	lpcsel;
};

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,   //繪畫圖形
};

static struct fb_info *s3c_lcd;
static volatile unsigned long *gpbcon;
static volatile unsigned long *gpbdat;
static volatile unsigned long *gpccon;
static volatile unsigned long *gpdcon;
static volatile unsigned long *gpgcon;
static volatile struct lcd_regs* lcd_regs;
static u32 pseudo_palette[16];

/* 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;
}

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;

	/* 用red,green,blue三原色構造出val */
	val  = chan_to_field(red,	&info->var.red);
	val |= chan_to_field(green, &info->var.green);
	val |= chan_to_field(blue,	&info->var.blue);
	
	//((u32 *)(info->pseudo_palette))[regno] = val;
	pseudo_palette[regno] = val;
	return 0;
}

static int lcd_init(void)
{
	/* 1. 分配一個fb_info */
	s3c_lcd = framebuffer_alloc(0, NULL);

	/* 2. 設定 */
	/* 2.1 設定固定的引數 */
	strcpy(s3c_lcd->fix.id, "mylcd");
	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*2;    //每行的長度,以位元組為單位
	
	/* 2.2 設定可變的引數 */
	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.bits_per_pixel = 16;     //每個象素使用多少位

	/* RGB:565 */
	s3c_lcd->var.red.offset     = 11;    //紅色偏移值為11
	s3c_lcd->var.red.length     = 5;     //紅色位長為5
	
	s3c_lcd->var.green.offset   = 5;     //綠色偏移值為5
	s3c_lcd->var.green.length   = 6;     //綠色位長為6

	s3c_lcd->var.blue.offset    = 0;     //藍色偏移值為0
	s3c_lcd->var.blue.length    = 5;     //藍色位長為5

	s3c_lcd->var.activate       = FB_ACTIVATE_NOW;    //使設定的值立即生效
	
	
	/* 2.3 設定操作函式 */
	s3c_lcd->fbops              = &s3c_lcdfb_ops;
	
	/* 2.4 其他的設定 */
	s3c_lcd->pseudo_palette = pseudo_palette;    //存放調色盤所調顏色的陣列
	//s3c_lcd->screen_base  = ;                  //視訊記憶體的虛擬地址,這個在後面設定
	s3c_lcd->screen_size   = 480*272*16/8;       //視訊記憶體的大小

	/* 3. 硬體相關的操作 */
	/* 3.1 配置GPIO用於LCD */
	gpbcon = ioremap(0x56000010, 8);
	gpbdat = gpbcon+1;
	gpccon = ioremap(0x56000020, 4);
	gpdcon = ioremap(0x56000030, 4);
	gpgcon = ioremap(0x56000060, 4);

    *gpccon  = 0xaaaaaaaa;	/* GPIO管腳用於VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */
	*gpdcon  = 0xaaaaaaaa;	/* GPIO管腳用於VD[23:8] */
	
	*gpbcon &= ~(3);  		/* GPB0設定為輸出引腳 */
	*gpbcon |= 1;
	*gpbdat &= ~1;     		/* 輸出低電平 */

	*gpgcon |= (3<<8); 		/* GPG4用作LCD_PWREN */
	
	/* 3.2 根據LCD手冊設定LCD控制器, 比如VCLK的頻率等 */
	lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs));

	lcd_regs->lcdcon1  = (4<<8) | (3<<5) | (0x0c<<1);
	lcd_regs->lcdcon2  = (1<<24) | (271<<14) | (1<<6) | (9);//垂直方向的時間引數
	lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1);			//水平方向的時間引數
	lcd_regs->lcdcon4 = 40;									//水平方向的同步訊號
	lcd_regs->lcdcon5 = (1<<11) | (0<<10) | (1<<9) | (1<<8) | (1<<0);	//訊號的極性 
	
	/* 3.3 分配視訊記憶體(framebuffer), 並把地址告訴LCD控制器 */
	s3c_lcd->screen_base = dma_alloc_writecombine(NULL, s3c_lcd->fix.smem_len, &s3c_lcd->fix.smem_start, GFP_KERNEL);
	
	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);  /* 一行的長度(單位: 2位元組) */	
	
	//s3c_lcd->fix.smem_start = xxx;  /* 視訊記憶體的實體地址 */
	/* 啟動LCD */
	lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD控制器 */
	lcd_regs->lcdcon5 |= (1<<3); /* 使能LCD本身 */
	*gpbdat |= 1;     /* 輸出高電平, 使能背光 */		

	/* 4. 註冊 */
	register_framebuffer(s3c_lcd);
	
	return 0;
}

static void lcd_exit(void)
{
	unregister_framebuffer(s3c_lcd);
	lcd_regs->lcdcon1 &= ~(1<<0); /* 關閉LCD本身 */
	*gpbdat &= ~1;     /* 關閉背光 */
	dma_free_writecombine(NULL, s3c_lcd->fix.smem_len, s3c_lcd->screen_base, s3c_lcd->fix.smem_start);
	iounmap(lcd_regs);
	iounmap(gpbcon);
	iounmap(gpccon);
	iounmap(gpdcon);
	iounmap(gpgcon);
	framebuffer_release(s3c_lcd);
}

module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");

Makefile程式碼如下:

KERN_DIR = /work/system/linux-2.6.22.6    //核心目錄

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= lcd.o

4. 測試

核心:linux-2.6.22.6
編譯器:arm-linux-gcc-3.4.5
環境:ubuntu9.10

4.1 配置核心

    1. 重新配置核心,將核心自帶的lcd驅動配置為模組。在linux-2.6.22.6核心目錄下執行:
      make menuconfig
    2. 配置步驟如下:
      Device Drivers  --->
          Graphics support  --->
              <M> S3C2410 LCD framebuffer support
 
   3. 編譯核心與模組
      make uImage
      make modules

4.2 重燒核心

    1. 將linux-2.6.22.6/arch/arm/boot下的uImage燒寫到開發板,網路檔案系統啟動。
    2. 編譯lcd.c檔案,拷貝cfbcopyarea.ko、cfbfillrect.ko、cfbimgblt.ko到網路檔案系統。
      make
      cp linux-2.6.22.6/drivers/video/cfb*.ko /work/nfs_root/first_fs
   
3. 裝載驅動
      insmod cfbcopyarea.ko
      insmod cfbfillrect.ko
      insmod cfbimgblt.ko
      insmod lcd.ko

5.2 測試

    1. 開發板上執行:
      echo hello > /dev/tty1 (此時開發板lcd上有“hello”顯示出來)
      cat lcd.ko > /dev/fb0   (此時開發板lcd花屏)
      vi /etc/inittab

# /etc/inittab
::sysinit:/etc/init.d/rcS
s3c2410_serial0::askfirst:-/bin/sh    //新增這行程式碼,將輸出資訊輸出到lcd上
tty1::askfirst:-/bin/sh       
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

    2. 安裝十、Linux驅動之輸入子系統使用裡的驅動
      Insmod buttons.ko  (此時按下開發板上的按鍵值就能輸出到lcd上了)

5. 相關知識點

分配DMA快取函式函式原型如下:

void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);  //分配DMA快取區給視訊記憶體

引數:   
    dev:指標,填0表示這個申請的緩衝區裡沒有內容。   
    size:分配的地址大小(位元組單位)。   
    handle:申請到的物理起始地址。
    gfp:分配出來的記憶體引數。
   
該函式分配一段DMA快取區,分配出來的記憶體會禁止cache快取(因為DMA傳輸不需要CPU),它和 dma_alloc_coherent ()函式相似,不過 dma_alloc_coherent ()函式是分配出來的記憶體會禁止cache快取以及禁止寫入緩衝區。
對應函式:

dma_free_writecombine(dev,size,cpu_addr,handle);   //釋放快取