Linux設備驅動之Framebuffer分析

在Linux內核中,Framebuffer(帖緩衝)驅動是顯示驅動的標準,Framebuffer將顯示設備抽象爲帖緩衝區,用戶通過內存映射到進程地址空間之後,就可以直接進行讀寫操作,且寫操作可以立即在屏幕上進行顯示,在Linux內核/linux/drivers/video/下有相關的顯示驅動與接口,其中Frmaebuffer驅動接口爲fbmem.c,此文件提供了LCD驅動的通用文件操作接口,如read 、write、 ioctl等應用程序可能應用到的文件接口,特定平臺的LCD驅動程序可以實現自己的文件操作接口,也可以直接應用fbmem.c中所提供的文件操作接口,在一般情況下,通常應用fbmem.c所提供的文件操作接口,因爲內核所提供的文件操作接口幾乎能滿足我們的需求,如果有特別要求,可以選擇性的重新實現相關的文件操作接口。至於是怎麼進行連接的,我將在後面進行分析。
再進行分析fbmem.c之前,先來看看幾個重要的結構體。所有Freambuffer所用到的結構體都定義在/include/linux/fb.h中,有興趣的讀者可以去看看源碼,這樣也許會有更大的收穫,特別是ioclt所需要應用的宏定義,如果不知道這些宏定義的用途,那麼就不知道怎麼在應用程序中應用ioctl函數來控制LCD以實現相關的功能。
跳過相關的宏定義(對於相關宏定義,請自行查看內核源碼,在此不進行說明)在第136行中定義了struct fb_fix_screeninfo結構體,這個結構體用於描述顯卡自身的屬性,包含識別符、緩存地址、顯示類型等,但是注意的一點是,當LCD系統正常運行後,不能修改此結構體的值。再一個與顯卡相關的結構體是
struct fb_var_screeninfo結構體,此結構用於描述顯卡的一般特性,比如實際分辨率,虛擬分辨率,實際分辨率與虛擬分辨率之間的位移等,可以說,這是一個非重要的結構體,它決定了將進行驅動的LCD的尺寸等特性。
在進行LCD顯示時,通常需要進行相關的顏色設置,struct fb_cmap用於描述設備無關的顏色映射信息。可以通過FBIOGETCMAP 和 FBIOPUTCMAP對應的ioctl 操作設定或獲取顏色映射信息。還有一些比如光標、圖片等信息有關的結構體不進行說明,內核中對每個結構體中的相關域都做了詳細的註釋,相信我親愛的讀者能夠看懂。
Famebuffer驅動有自己特有的文件操作接口fb_ops,該結構體的定義位於fb.h中的第543行,在應用fbmem.c驅動接口來書寫自的LCD驅動時,可以不必完全實現所有的域,但是,有下面的幾個域是必須要進行實現的。
第一:
void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);
該函數是應用顯卡的加速功能進行添充一個矩形,如果顯卡不支持硬件加還功能,可以應用常規操作來代替,但是一定要在自己特定平臺的LCD驅動程序實現這個函數,其實,這個域只是一個函數指針域,也就是不能讓fb_fillrect這個批針爲NULL,就行了。
第二個是:
void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);
用於進行區域的複製。
第三個是:
void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);
用於在屏幕上畫一幅圖片。
第四個是:
int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);
對於這四個函數,如果不想實現的話,可以應用內核給我們提供的函數,讓這四個函數指針分別指向這些函數,這四個函數爲:cfbcopyarea.c中的cfb_copyarea;cfbfillrect.c中的cfb_fillrect,softcursor.c中的soft_cursor, cfbimgbit.c中的cfb_imagedbit,請查看相關的源碼文件。所以,準確來說,這四個函數也不是一定要自己實現,只是必須不要使這四個函數指針爲NULL即可。
最後一個最重要的結構體是struct fb_info,這個結構體用於描述當前顯卡的狀態,fb_info只能在內核中可見,struct fb_ops *fbops這個域指向上述的結構體,用來進行Framebuffer獨有的文件操作。現代顯卡不僅支持單通道顯示,也支持多通道顯示,每個顯示法必須擁有一個自己的獨立的數據區,所以,也就是說,不同顯示方法可以共享顯卡,所以fb_info就是用於區分不同顯示方法的結構,每一個顯示方法必須擁用自己獨立的fb_info,如果支持多顯,則必須定義fb_info數組或者動態內存分配多個fb_info結構體變量。
好了,是時候分析fbmem.c的時候了,在此文件的最開始就定了一個fb_info數組,用於支持不同的顯卡或者顯示方法,如下:
struct fb_info *registered_fb[FB_MAX];共中FB_MAX爲32,在fb.h可見其宏定義,在這裏我們找幾個重要的函數進行一下說明就行了,其它函數請讀者自己進行分析。在此不都進行分析。
char* fb_get_buffer_offset(struct fb_info *info, struct fb_pixmap *buf, u32 size)此函數應用獲取buffer offset,在這個函數中,有一點最爲重要的就是I/O同步顯示,代碼如下所示:
if (buf->flags & FB_PIXMAP_IO) {
        if (info->fbops->fb_sync && (buf->flags & FB_PIXMAP_SYNC))
            info->fbops->fb_sync(info);
        return addr;
    }
還有一段代碼也就用於支持同步的
if (info->fbops->fb_sync && (buf->flags & FB_PIXMAP_SYNC))
            info->fbops->fb_sync(info);
也許細心的讀者發現有這個一句info->fbops->fb_sync(info)這是fb_ops結構體中的一個函數指針域,我在上面的分析中說過,這個域可以爲空,因爲在這個函數中,在操作這個域時,都先進行判斷此域是否爲空,其實,fb_ops中那些域可以爲NULL,那麼不可以爲NULL,看一下fb_mem.c中的源碼就可以知道。
在int fb_show_logo(struct fb_info *info)函數中,需要應用fb_ops結構體中的fb_imageblit域來在屏幕上畫一幅圖象,且是必須的,所以,fb_ops結構中fb_imageblit域不能爲NULL,如果爲NULL,那麼這個函數就沒有辦法完成功能,也就是一個無用的函數,在函數的引用爲fbmem.c中的420行,引用過程如下:
for (x = 0; x < num_online_cpus() * (fb_logo.logo->width + 8) &&\
         x <= info->var.xres-fb_logo.logo->width; x += (fb_logo.logo->width + 8)) {
        image.dx = x;
        info->fbops->fb_imageblit(info, &image);
    }
當系統調用read、write、open、release等函數時,首先調用fbmem.c中實現的文件接口函數,再進行判斷fb_ops中的相關域是否爲NULL,如果不爲NULL,則在適當的地方調用fb_ops中相應的函數來完成相關的功能。但是,關於在應用程序中調用ioctl會得到什麼結果,需要什麼參數,請查看fb_ioctl,進此不進行說明,因爲這個函數太長也太複雜,不是一兩句能分析清楚的。
    不過在此說一下fb_read與fb_write函數中在進行用戶空間與內核空間進行數據交換時,有一個小小的技巧,這個技巧在我們編程過程中是比較有用的,特別在源地址不能直接進行交換數據的情況下。首先我們不能確定我們的數據都爲4個字節的整數倍,所以,我們必須分開來處理,是4個字節的整數倍的部分,我們一次移動4個字節,這對於ARM這樣的32位處理器來說是最好的,因爲這樣尋址最快,餘下的部分就只能一個字節一字節的移動了,很多人也許會想到應用求模來求出不足4字節的部分,這對於ARM這樣的處理器來說,求模與除法有點難度,因爲硬件不直接支特除法,所以,我們只能應用移位的方法來處理,請認真分析源碼,特別if (c & 3)(用於判斷是否爲4的整數倍),for (i = c & 3; i--;)這兩句對我們用於處理緩衝區的時候很有用,比如,我們有一個能容納8個unsigned int型的環形緩衝區,在緩衝下標到達8時,我們需要從0開始,很多人也許會用到下面的語句:
    if (index == 8)
        index = 0;
其實,這對代碼效率的優化有一定的影響,當然也不太大,但是,在一個工程中,大的影響都是從小的影響開始的,所以,我們最好應用這樣一句語句來代替它,這樣一樣的能完成任務,而完成得更好:index &= 0x7;能看得出怎麼做到的吧,如果在7的範圍內,index的值不會做任何的改變,但是一但是8,也就是0x8,寫出其二進制一看就知道了,0x8的二進制爲(1000),而0x7的二進制爲(0111),所以進行按位與當然又回到了0,但是,應用這個技巧是有一個條件的,那就是緩衝區的的大小必須爲2的次方,才能正確應用這個技巧,好了,對於這個技巧不再進行說明了,但是,請注意一點,每一個小小的優化對整個工程都是一個很大的優化。對於對代碼優化感興趣的讀者,可以參考一下《ARM嵌入式系統開發-軟件設計與優化》這本書,這是我的老師給我推薦的。fb_read進行數據處理的部分代碼如下所示:
while (count) {
        c  = (count > PAGE_SIZE) ? PAGE_SIZE : count;
        dst = buffer;
        for (i = c >> 2; i--; )
            *dst++ = fb_readl(src++);
        if (c & 3) {
            u8 *dst8 = (u8 *) dst;
            u8 __iomem *src8 = (u8 __iomem *) src;

            for (i = c & 3; i--;)
                *dst8++ = fb_readb(src8++);

            src = (u32 __iomem *) src8;
        }

        if (copy_to_user(buf, buffer, c)) {
            err = -EFAULT;
            break;
        }
        *ppos += c;
        buf += c;
        cnt += c;
        count -= c;
    }
很經典的一小段代碼,請慢慢的消化,看Linux內核源碼的其中一個最大的收穫就是學到很多經典的算法,這些算法在實際編程中很有用,有時候對某一個工程來言也許起到關鍵性的因素。
另一個,也是LCD驅動中最重要的功能函數之一就是fb_mmap函數,看過ARM中LCD控制器的讀者都知道,在一般顯卡中都有顯存的存在,但是對於嵌入式系統而言,幾乎沒有誰會在CPU上集成顯卡也是不可能的事,所以,一般應用內存映射爲LCD顯存地址,應用DMA或者其它方式把此緩衝區的內容在不經過CPU的情況下進行顯示,因爲圖片、視頻等數據非常大,如果應用CPU直接處理,那麼CPU的工作會非常之重同時顯示也許不盡人意,同樣,在Linux內核與應用程序之間,爲了減少內核把應用程序中的數據先複製到內核空間的時間(請注意用戶空間與內核空間的不同),也需要進行內存的映射,目的是爲了使用戶空間與內核空間都可以直接應用此內存,將Linxu內核空間與用戶空間內存和應用內存爲LCD顯存結合起來,就可以知道這個函數的用途有多大,首先,它完成用戶空間與內核空間可以直接使用內一塊內存的功能,在此同時,將此內存映射爲LCD顯存,應用程序可以直接讀寫該內存,在LCD屏幕上立即得到顯示。比如將一張照片寫入該內存,那麼在LCD上立即顯示此照片,本人在最後將給大家一個例子,也就是一個顯示照片的例子。關於這個函數是怎麼實現的,請查看源碼;應用程序是怎麼應用mmap函數的,請查看相關的資料。在此不進行詳細說明了。
在這個文件中還提供了兩個函數給我們進行註冊與註銷我們特定平臺的LCD驅動程序。int register_framebuffer(struct fb_info *fb_info),這個函數應用完成特定平臺LCD驅動的註冊;int unregister_framebuffer(struct fb_info *fb_info)這個函數用於註銷特定平臺的LCD驅動。關於此文件的其實函數請讀者自行查看源碼並進行分析,在此不進行相關的說明。
在video目錄下,還有一個skeletonfb.c文件,該文件是一個框架,主要是說明怎樣應用fb_mem.c所提供的register_framebuffer與unregister_framebuffer這兩個函數來實現特定平臺LCD驅動的開發過程,有詳細的說明,如果能把這個文件看懂,那麼在這個文件的基礎上進行添充相關的實現,就可以開發出一個特定平臺的LCD驅動程序,在此目錄下,還有很多其它的LCD驅動程序,比如sa1100fb.c,這是一個StrongARM 1100 LCD的驅動程序;還有一個vfb.c,這一個虛擬LCD驅動程序,其實,Linux內核中的很多源碼是很有用的,我們可以直接應用或者修改之而用之,問題在於你必須明確知道你的需要與內核源碼中的相關驅動的硬件特性與你所需要的差別,修改這些差別也就爲你所用,但是,這也就是困難之處,不像其它程序,如果C不夠好,同時對整個內核驅動構架不瞭解(或者某一子系統不瞭解),看懂內核源碼很困難,想修改爲之所用更加困難。
對於應用S3C2440或者S3C2410等的朋友,可以直接應用內核提供的s3c2410fb.c這個文件,這是一個對於S3C2440和S3C2410都實用的LCD驅動,如果已經編譯進內核中,一般目錄爲/dev/fb/0,當然也有可能不是這個,請查看相關的資料,也就是說,我們在應用程序中可以直接應用open函數打開此設備文件,這樣就可以直接操作LCD顯示了。s3c2410fb.c是作者參考skeletonfb.c, sa1100fb.c等相關驅動書寫的,還是一句話,看懂,修改並用之,這也許是Linux設備驅動開發做得最多的事,也許也是Linux在嵌入式受喜歡的一個原因爲,正如上面所說的,看懂,很難。關於s3c2410fb.c的實現過程,大體框架與skeletonfb.c差不多,請讀者自己查看skeletonfb.c與S3C2410或者S3C2440手冊中LCD控制器一節。
在s3c2410fb.c中需要說明的是static void s3c2410fb_set_lcdaddr函數,這個函數真正的完成了把用戶空間與內核空間共的內存映射爲LCD顯存,所以在此做一下詳細說明,在S3C2440手冊LCD控制器一節中,可以看到LCSADDR1寄存器,此寄存器主要是用於存儲LCD顯存的起始地址,不過我們只用到1到30,所以,我們需要把內存的起始地址右移一位。關於LCDSADDR1的相關說明如下:
LCDBANK[29:21]:These bits indicate A[30:22] of the bank location for the video buffer in the system memory. LCDBANK value cannot be changed even when moving the view port. LCD frame buffer should be within aligned 4MB region, which ensures that LCDBANK value will not be changed when moving the view port. So, care should be taken to use the malloc()function.
LCDBASEU [20:0]:For dual-scan LCD : These bits indicate A[21:1] of the start address of the upper address counter, which is for the upper frame memory of dual scan LCD or the frame memory of single scan LCD.
For single-scan LCD : These bits indicate A[21:1] of the start address of the LCD frame buffer.
所以在此函數中把內存地址右移了一位,源碼爲:
saddr1  = fbi->fb.fix.smem_start >> 1;
對於LCDSADDR2寄存器的處理要相當的煩瑣一些,我們還是從CPU手冊開始,在CPU手冊中,LCDSADDR2的描述如下:
LCDBASEL [20:0]:For dual-scan LCD: These bits indicate A[21:1] of the start address of the lower address counter, which is used for the lower frame memory
of dual scan LCD.
For single scan LCD: These bits indicate 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)
查看源碼可知,我們應用的是單掃描方式,所以,A[21:1]應該爲LCD 顯存的結束地址,此函數中處理如下:
saddr2  = fbi->fb.fix.smem_start;                    /* 起始地址 */
saddr2 += (var->xres * var->yres * var->bits_per_pixel)/8; /* 緩衝區大小 */
saddr2>>= 1;                     /*執行((the frame end address) >>1)*/
上面的代碼請注意爲什麼沒有執行加1操作。
LCDSADDR3的處理相對來說較爲簡單多了,這個寄存器只處理相關的OFFSIZE各PAGEWIDTH,我們還是從CPU手冊着手吧。其描述如下:
OFFSIZE [21:11] Virtual screen offset size (the number of half words).This value defines the difference between the address of the last half word displayed on the previous LCD line and the address of the first half word to be displayed in the new LCD line.
PAGEWIDTH [10:0] Virtual screen page width (the number of half words).This value defines the width of the view port in the frame.
所以,函數中只是調用相關的宏來進行處理,源碼如下:
saddr3 =  S3C2410_OFFSIZE(0) | S3C2410_PAGEWIDTH(var->xres);
把這三個寄存器的內所需的值求出以後,最後寫入到相關的寄存器就可以了,寫入的源碼如下:
writel(saddr1, S3C2410_LCDSADDR1);
writel(saddr2, S3C2410_LCDSADDR2);      
writel(saddr3, S3C2410_LCDSADDR3);
到此,這個函數分析完了,在此也將就說明一下,看內核相關的源碼時,請查看相關的芯片手冊,如果對芯片的功能不瞭解,主要是芯片所需的時序,相關寄存器的值的規定,功能等不瞭解。你看懂的只是一串字符串,不知道其含義與驅動的功能。
對於顯卡的處理,也就是顏色的處理方式問題上,因爲s3c2410fb所用的顏色模式爲16BPP模式,我們可以查看S3C2440或者S3C2440 CPU手冊,其對16BPP顏色模的描述如下:
16 BPP color mode
16 bits (5 bits of red, 6 bits of green, 5 bits of blue) of video data correspond to 1 pixel. But, stn controller will use only 12 bit color data. It means that only upper 4bit each color data will be used as pixel data (R[15:12], G[10:7], B[4:1]). The following table shows color data format in words:
所以,對應的var結構體變量相應的域也需要進與此相對應,在s3c2410fb_check_var中做了如下的處理:
if (var->bits_per_pixel == 16) {
    var->red.offset        = 11;
    var->green.offset    = 5;
    var->blue.offset    = 0;
    var->red.length        = 5;
    var->green.length    = 6;
    var->blue.length    = 5;
    var->transp.length    = 0;
} else {
    var->red.length        = 8;
    var->red.offset        = 0;
    var->green.length    = 0;
    var->green.offset    = 8;
    var->blue.length    = 8;
    var->blue.offset    = 0;
    var->transp.length    = 0;
}
關於以紅、綠、蘭這三種顏色爲基色,它們在1 word中的表示順序與規則請自行查看相關的資料。關於其它函數請參照相關的芯片手冊自行分析。在s3c2410fb.c中,我個人認爲也就上述兩點有一定的難度,因爲它們不那麼直觀。需要相應的轉化才能看懂。
關於應用程序,必須定義相關的結構體變量來實現ioctl的應用,如果有需要的話,但是,必須定義fb_var_screeninfo與fb_fix_screeninfo結構體變量,因爲我們需要獲取驅動的相關信息,比如顯示區域的大小,像素等重要信息,這在進行內存映射時必須用到的,因爲如果不知道像素,顯示區域的大小,我們不能準確的映射內存,那麼對此內映的讀寫將沒有太多的意義。本人在寫應用測試程序時,能正確顯示圖片,但是在加入ASCII與中文字庫後,想進行簡單的顯示ASCII與中文的操作,可是,沒有成功,不知道是什麼原因,裸機的也不行了,就是能正常顯示圖片,不能顯示字符,以前做過的裸機LCD驅動實驗現在只能看圖片了,這同時也打亂了我的計劃,因爲本人想設計一個基於Linux操作系統的時鐘系統(應用相關的GUI在LCD上畫一個時鐘表,並顯示星期,同時也要求顯示實時溫度),做爲嵌入式系統課程的課程設計。也許是LCD有什麼問題吧,正在尋求答案中,如果那位朋友也見過這樣的情況,並且給解決了,請告訴一下本人,本人急需知道問題所在。
好了,我該停筆了,花了整整一個晚上的時間,但是我很樂意寫,因爲我習慣記錄學習每一個知識點的理解,設備驅動文檔,時間長了,可以做爲複習資料。同時,也可以提高個人文檔書寫能力。特別是在嵌入式領域,驅動設計者很多時候不寫相關的應用程序,這就得要求說明白驅動給應用程序提供的接口,比如應用ioctl需要宏定義,功能;讀、寫需要傳入多大的內存地址,需要什麼特別的數據結構、讀出什麼值,這些值需不需要進行進一步的變換,如果需要變換,應該怎麼變換。比如,一個DS18B20的驅動,以其讀溫度值爲例說明。由於用戶空與內核空間只能傳送char型數據,所以,一個16位的溫度值,需要分開爲兩個值讀取,這就要求說明,在讀取的這兩個值中,那一個是低8位,那一個是高8位,否則別人怎麼進行數據的組合。
說了很多,還是覺到沒有說明白,這也許是本人的水平有限,不過,上面所說的都是本人現在水平所能及的了,如果有什麼錯誤,請給予指正,讓本人也有學習改正的機會,Linux操作系本是開源的。
    同時,若想研究控制檯相關驅動的讀者,有興趣我們一起學習,Framebuffer後也許會學習之。在video目錄下有一個子目錄,console目錄,此目錄下都是一些關於控制檯的相關驅動,特別是,在此目錄下有大量的字庫,當然沒有中文的,關於中文字庫在Mini2440開發板提供的UCOS操作系統實驗中有一個16 * 16的中文字庫,網上也有很多其它類型的中文字庫,不過關鍵是要有使用說明,
若有需要本人所做實驗源碼和與本人討論問題的朋友,歡迎與我聯繫,本人很想找幾個朋友一起學習,因爲我們學校我不知道還有誰在學習我學習的這個方向,我所認識的範圍內也就只有我一個人。一個人學習真的很沒有味道,同時進步也很慢,因爲有什麼問題總是自己想,自己找資料,有時候別人一句話就可以解決的問題,我一個人也許需要幾個小時或者幾天的時間來解決。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章