嵌入式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驅動可以分為以下四步:
- 分配一個fb_info結構體
- 設定這個結構體
- 做硬體相關的操作
- 通過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驅動程式就寫完了。
下面就是測試了:測試的步驟為:
- 進入核心目錄:make menuconfig ,去掉原來的LCD驅動程式
- make uImage :生成沒有LCD驅動的核心
- cp arch/arm/boot/uImage /work/nfs_root/uImage_nolcds
- make modules :得到cfd_fillrect,cfb_copyarea,cfb_imageblit函式對應的模組
- 使用新的uImage_nolcds 啟動:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
- 編譯自己寫的LCD驅動程式,並將其考到根檔案系統
- 在開發板上使用安裝驅動:insmod cfbcopyarea.ko insmod cfdfillrect.ko insmod cfbimageblit.ko insmod lcd.ko
- echo hollo >/dev/tty1 ,可以在LCD上可以看到hello
- cat lcd.ko >/dev/fb0 ,花屏
- 修改/etc/inittab,加一行tty1::askfirst:-/bin/sh
- 使用新的uImage_nolcds 重新啟動:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
- insmod input.ko
- 可以按鍵s2,s3,s4,螢幕會顯示ls,和ls命令後的內容
我的文章中可能有些概念或道理講的不詳細,下面是我看到的幾篇我認為比較好的文章我在這裡轉載: