詳解FrameBuffer 二

(二)一個LCD顯示芯片的驅動實例

    以Skeleton LCD 控制器驅動爲例,在LINUX中存有一個/fb/skeleton.c的skeleton的Framebuffer驅動程序,很簡單,僅僅是填充了 fb_info結構,並且註冊/註銷自己。設備驅動是向用戶程序提供系統調用接口,所以我們需要實現底層硬件操作並且定義file_operations 結構來向系統提供系統調用接口,從而實現更有效的LCD控制器驅動程序。

 

1)在系統內存中分配顯存

在fbmem.c文件中可以看到, file_operations 結構中的open()和release()操作不需底層支持,但read()、write()和 mmap()操作需要函數fb_get_fix()的支持.因此需要重新實現函數fb_get_fix()。另外還需要在系統內存中分配顯存空間,大多數的LCD控制器都沒有自己的顯存空間,被分配的地址空間的起始地址與長度將會被填充到fb_fix_screeninfo 結構的smem_start 和smem_len 的兩個變量中.被分配的空間必須是物理連續的。

 

 

2)實現 fb_ops 中的函數

用戶應用程序通過ioctl()系統調用操作硬件,fb_ops 中的函數就用於支持這些操作。(注: fb_ops結構與file_operations 結構不同,fb_ops是底層操作的抽象,而file_operations是提供給上層系統調用的接口,可以直接調用.

  ioctl()系統調用在文件fbmem.c中實現,通過觀察可以發現ioctl()命令與fb_ops’s 中函數的關係:

FBIOGET_VSCREENINFO fb_get_var

FBIOPUT_VSCREENINFO fb_set_var

FBIOGET_FSCREENINFO fb_get_fix

FBIOPUTCMAP fb_set_cmap

FBIOGETCMAP fb_get_cmap

FBIOPAN_DISPLAY fb_pan_display

 

 

如果我們定義了fb_XXX_XXX 方法,用戶程序就可以使用FBIOXXXX宏的ioctl()操作來操作硬件。

 

文件linux/drivers/video/fbgen.c或者linux/drivers/video目錄下的其它設備驅動是比較好的參考資料。在所有的這些函數中fb_set_var()是最重要的,它用於設定顯示卡的模式和其它屬性,下面是函數fb_set_var()的執行步驟:

 

1)檢測是否必須設定模式

2)設定模式

 

3)設定顏色映射

 

4) 根據以前的設定重新設置LCD控制器的各寄存器。

 

第四步表明了底層操作到底放置在何處。在系統內存中分配顯存後,顯存的起始地址及長度將被設定到 LCD控制器的各寄存器中(一般通過fb_set_var() 函數),顯存中的內容將自動被LCD控制器輸出到屏幕上。另一方面,用戶程序通過函數mmap()將顯存映射到用戶進程地址空間中,然後用戶進程向映射空間發送的所有數據都將會被顯示到LCD顯示器上。

 

三、FrameBuffer的應用

 

(一)、一個使用FrameBuffer的例子

 

FrameBuffer主要是根據VESA標準的實現的,所以只能實現最簡單的功能。

由於涉及內核的問題,FrameBuffer是不允許在系統起來後修改顯示模式等一系列操作。(好象很多人都想要這樣幹,這是不被允許的,當然如果你自己寫驅動的話,是可以實現的).

對FrameBuffer的操作,會直接影響到本機的所有控制檯的輸出,包括XWIN的圖形界面。

在struct fb_info 中的char fontname[40]; /* default font name */默認的字體

就可以實現顯示的中文化----難道 籃點linux就是這樣搞得??

好,現在可以讓我們開始實現直接寫屏:

 

1、打開一個FrameBuffer設備

 

2、通過mmap調用把顯卡的物理內存空間映射到用戶空間

 

3、直接寫內存。

 

/********************************

File name : fbtools.h

*/

 

#ifndef _FBTOOLS_H_

#define _FBTOOLS_H_

#include <linux/fb.h>

//a framebuffer device structure;

typedef struct fbdev{

       int fb;

       unsigned long fb_mem_offset;

       unsigned long fb_mem;

       struct fb_fix_screeninfo fb_fix;

       struct fb_var_screeninfo fb_var;

       char dev[20];

} FBDEV, *PFBDEV;

 

//open & init a frame buffer

//to use this function,

//you must set FBDEV.dev="/dev/fb0"

//or "/dev/fbX"

//it's your frame buffer.

int fb_open(PFBDEV pFbdev);

 

//close a frame buffer

int fb_close(PFBDEV pFbdev);

 

//get display depth

int get_display_depth(PFBDEV pFbdev);

 

//full screen clear

void fb_memset(void *addr, int c, size_t len);

 

#endif

 

/******************

File name : fbtools.c

*/

 

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <unistd.h>

#include <string.h>

#include <sys/ioctl.h>

#include <sys/mman.h>

#include <asm/page.h>

#include "fbtools.h"

#define TRUE        1

#define FALSE       0

#define MAX(x,y)        ((x)>(y)?(x)y))

#define MIN(x,y)        ((x)<(y)?(x)y))

 

//open & init a frame buffer

int fb_open(PFBDEV pFbdev)

{

       pFbdev->fb = open(pFbdev->dev, O_RDWR);

       if(pFbdev->fb < 0)

       {

              printf("Error opening %s: %m. Check kernel config\n", pFbdev->dev);

              return FALSE;

       }

 

       if (-1 == ioctl(pFbdev->fb,FBIOGET_VSCREENINFO,&(pFbdev->fb_var)))

       {

              printf("ioctl FBIOGET_VSCREENINFO\n");

              return FALSE;

       }

 

       if (-1 == ioctl(pFbdev->fb,FBIOGET_FSCREENINFO,&(pFbdev->fb_fix)))

       {

              printf("ioctl FBIOGET_FSCREENINFO\n");

              return FALSE;

       }

 

       //map physics address to virtual address

       pFbdev->fb_mem_offset = (unsigned long)(pFbdev->fb_fix.smem_start) & (~PAGE_MASK);

       pFbdev->fb_mem = (unsigned long int)mmap(NULL, pFbdev->fb_fix.smem_len + pFbdev->fb_mem_offset,              PROT_READ | PROT_WRITE, MAP_SHARED, pFbdev->fb, 0);

 

       if (-1L == (long) pFbdev->fb_mem)

       {

              printf("mmap error! mem:%d offset:%d\n", pFbdev->fb_mem, pFbdev->fb_mem_offset);

              return FALSE;

       }

       return TRUE;

}

 

//close frame buffer

int fb_close(PFBDEV pFbdev)

{

       close(pFbdev->fb);

       pFbdev->fb=-1;

}

 

//get display depth

int get_display_depth(PFBDEV pFbdev);

{

       if(pFbdev->fb<=0)

       {

              printf("fb device not open, open it first\n");

              return FALSE;

       }

       return pFbdev->fb_var.bits_per_pixel;

}

 

//full screen clear

void fb_memset (void *addr, int c, size_t len)

{

    memset(addr, c, len);

}

 

//use by test

#define DEBUG

#ifdef DEBUG

main()

{

       FBDEV fbdev;

       memset(&fbdev, 0, sizeof(FBDEV));

       strcpy(fbdev.dev, "/dev/fb0");

       if(fb_open(&fbdev)==FALSE)

       {

              printf("open frame buffer error\n");

              return;

       }

       fb_memset(fbdev.fb_mem + fbdev.fb_mem_offset, 0, fbdev.fb_fix.smem_len);

              fb_close(&fbdev);

}

 

(二)基於Linux核心的漢字顯示的嘗試

我們以一個簡單的例子來說明字符顯示的過程。我們假設是在虛擬終端1(/dev/tty1)下運行一個如下的簡單程序。

 

main ( )

{

puts("hello, world.\n");

}

 

puts 函數向缺省輸出文件(/dev/tty1)發出寫的系統調用write(2)。系統調用到linux核心裏面對應的核心函數是console.c中的 con_write(),con_write()最終會調用do_con_write( )。在do_con_write( )中負責把"hello, world.\n"這個字符串放到tty1對應的緩衝區中去。

do_con_write( )還負責處理控制字符和光標的位置。讓我們來看一下do_con_write()這個函數的聲明。

static int do_con_write(struct tty_struct * tty, int from_user, const unsigned char *buf, int count)

    其中tty是指向tty_struct結構的指針,這個結構裏面存放着關於這個tty的所有信息(請參照 linux/include/linux/tty.h)。Tty_struct結構中定義了通用(或高層)tty的屬性(例如寬度和高度等)。在 do_con_write( )函數中用到了tty_struct結構中的driver_data變量。driver_data是一個vt_struct指針。在vt_struct結構中包含這個tty的序列號(我們正使用tty1,所以這個序號爲1)。Vt_struct結構中有一個vc結構的數組vc_cons,這個數組就是各虛擬終端的私有數據。

 

static int do_con_write(struct tty_struct * tty, int from_user,const unsigned char *buf, int count)

{

struct vt_struct *vt = (struct vt_struct *)tty->driver_data;//我們用到了driver_data變量

. . . . .

currcons = vt->vc_num; file://我們在這裏的vc_nums就是1

. . . . .

}

 

    要訪問虛擬終端的私有數據,需使用vc_cons〔currcons〕.d指針。這個指針指向的結構含有當前虛擬終端上光標的位置、緩衝區的起始地址、緩衝區大小等等。

    "hello, world.\n"中的每一個字符都要經過conv_uni_to_pc( )這個函數轉換成8位的顯示字符。這要做的主要目的是使不同語言的國家能把16位的UniCode碼映射到8位的顯示字符集上,目前還是主要針對歐洲國家的語言,映射結果爲8位,不包含對雙字節(double byte)的範圍。

    這種UNICODE到顯示字符的映射關係可以由用戶自行定義。在缺省的映射表上,會把中文的字符映射到其他的字符上,這是我們不希望看到也是不需要的。所以我們有兩個選擇∶

 

不進行conv_uni_to_pc( )的轉換。

加載符合雙字節處理的映射關係,即對非控制字符進行1對1的不變映射。我們自己定製的符合這種映射關係的UNICODE碼錶是direct.uni。要想查看/裝載當前系統的unicode映射表,可使外部命令loadunimap。

經過conv_uni_to_pc( )轉換之後,"hello, world.\n"中的字符被一個一個地填寫到tty1的緩衝區中。然後do_con_write( )調用下層的驅動,把緩衝區中的內容輸出到顯示器上(也就相當於把緩衝區的內容拷貝到VGA顯存中去)。

 

sw->con_putcs(vc_cons〔currcons〕.d, (u16 *)draw_from, (u16*)draw_to-(u16 *)draw_from, y, draw_x);

 

之所以要調用底層驅動,是因爲存在不同的顯示設備,其對應VGA顯存的存取方式也不一樣。

上面的Sw->con_putcs( )就會調用到fbcon.c中的fbcon_putcs()函數(con_putcs是一個函數的指針,在Framebuffer模式下指向 fbcon_putcs()函數)。也就是說在do_con_write( )函數中是直接調用了fbcon_putcs()函數來進行字符的繪製。比如說在256色模式下,真正負責輸出的函數是void fbcon_cfb8_putcs(struct vc_data *conp, struct display *p,const unsigned short *s, int count, int yy, int xx)

 

顯示中文

    比如說我們試圖輸出一句中文∶putcs(你好\n );(你好的內碼爲0xc4,0xe3,0xba,0xc3)。這時候會怎麼樣呢,有一點可以肯定,"你好"肯定不會出現在屏幕上,國爲核心中沒有漢字字庫,中文顯示就是無米之炊了.

    1 在負責字符顯示的void fbcon_cfb8_putcs( )函數中,原有操作如下∶對於每個要顯示的字符,依次從虛擬終端緩衝區中以WORD爲單位讀取(低位字節是ASCII碼,高8位是字符的屬性),由於漢字是雙字節編碼方式,所以這種操作是不可能顯示出漢字的,只能顯示出xxxx_putcs()是一個一個VGA字符.

 

要解決的問題∶

確保在do_con_write( )時uni_pc轉換不會改變原有編碼。一個很直接的實現方式就是加載一個我們自己定製的UNICODE映射表,loadunimapdirect.uni,或者直接把direct.uni置爲核心的缺省映射表。

 

針對如上問題,我們要做的第一個嘗試方案是如下。

首先需要在覈心中加載漢字字庫,然後修改fbcon_cfb8_putcs()函數,在 fbcon_cfb8_putcs( )中一次讀兩個WORD,檢查這兩個WORD的低位字節是否能拼成一個漢字,如果發現能拼成一個漢字,就算出這個漢字在漢字字庫中的偏移,然後把它當成一個16 x 16的VGA字符來顯示。

 

試驗的結果表明∶

 

能夠輸出漢字,但仍有許多不理想的地方,比如說,輸出以半個漢字開始的一串漢字,則這半個漢字後面的漢字都會是亂碼。這是半個漢字的問題。

光標移動會破壞漢字的顯示。表現爲,光標移動過的漢字會變成亂碼。這是因爲光標的更新是通過xxxx_putc( )函數來完成的。

xxxx_putc( )函數與xxxx_putcs( )函數實現的功能類似,但是xxxx_putc()函數只刷新一個字符而不是一個字符串,因而xxxx_putc()的輸入參數是一個整數,而不是一個字符串的地址。Xxxx_putc( )函數的聲明如下∶void fbcon_cfb8_putc(struct vc_data *conp, struct display *p, int c, int yy, int xx)

 

    下一個嘗試方案就是同時修改xxxx_putcs( )函數和xxxx_putc()函數。爲了解決半個漢字的問題,每一次輸出之前,都從屏幕當前行的起始位置開始掃描,以確定要輸出的字符是否落在半個漢字的位置上。如果是半個漢字的位置,則進行相應的調整,即從向前移動一個字節的位置開始輸出。

    這個方案有一個困難,即xxxx_putc( )函數不用緩衝區的地址,而是用一個整數作爲參數。所以xxxx_putc( )無法直接利用相鄰的字符來判別該定符是否是漢字。

    解決方案是,利用xxxx_putc( )的光標位置參數(yy, xx),可以逆推出該字符在緩衝區中的位置。但仍有一些小麻煩,在Linux的虛擬終端下,用戶可能會上卷該屏幕(shift + pageup),導致光標的y座標和相應字符在緩衝區的行數不一致。相應的解決方案是,在逆推的過程中,考慮卷屏的參量。

    這樣一來,我們就又進了一步,得到了一個相對更好的版本。但仍有問題沒有解決。敲入turbonetcfg,會發現菜單的邊框字符也被當成漢字顯示。這是因爲,這種邊框字符是擴展字符,也使用了字符的第8位,因而被當作漢字來顯示。例如,單線一的製表符內碼爲0xC4,當連成一條長線就是由一連串0xC4 組成,而0xC4C4正是漢字哪。於是水平的製表符被一連串的哪字替代了。要解決這個問題就非常不容易了,因爲製表符的種類比較多,而且垂直製表符與其後面字符的組合型式又多種多樣,因而很難判斷出相應位置的字符是不是製表符,從理論上說,無論採取什麼樣的排除算法,都必然存在誤判的情況,因爲總存在二義性,沒有充足的條件來推斷出當前字符究竟是製表符還是漢字。

    我們一方面尋找更好的排除組合算法,一方面試圖尋找其它的解決方案。要想從根本上解決定個問題,必須利用其它的輔助信息,僅僅從緩衝區的字符來判斷是不夠的。

    經過一番努力,我們發現,在UNIX中使用擴展字符時,都要先輸出字符轉義序列(Escape sequence)來切換當前字符集。字符轉義序列是以控制字符Esc爲首的控制命令,在UNIX的虛擬終端中完成終端控制命令,這種命令包括,移動光標座標、卷屏、刪除、切換字符集等等。也就是說在輸出代表製表符的字符串之前,通常是要先輸出特定的字符轉義序列。在console.c裏,有根據字符轉義序列命令來記錄字符狀態的變量。結合該變量提供的信息,就可以非常乾淨地把製表符與漢字區別開來。

 

    在如上思路的指引下,我們又產生了新的解決方案。經過改動得到了另一各版本.

    在這個新版本上,turbonetcfg在初次繪製的時候,製表符與漢字被清晰地區分開來,結果是非常正確的。但還有新的問題存在 ∶turbonetcfg 在重繪的時候(如切換虛擬終端或是移動鼠標光標的時候),製表符還是變成了漢字,因爲重繪完全依賴於緩衝區,而這時用來記錄字符集狀態的變量並不反映當前字符集狀態。問題還是沒有最終解決。我們又回到了起點。∶( 看來問題的最終解決手段必須是把字符集的狀態伴隨每一個字符存在緩衝區中。讓我們來研究一下緩衝區的結構。每一個字符佔用16bit的緩衝區,低8位是 ASCII值,完全被利用,高8位包含前景顏色和背景顏色的屬性,也沒有多餘的空間可以利用。因而只能另外開闢新的緩衝區。爲了保持一致性,我們決定在原來的緩衝區後面添加相同大小的緩衝區,用來存放是否是漢字的信息。

 

    也許有讀者會問,我們只需要爲每個字符添加一bit的信息來標誌是否是漢字就足夠了,爲什麼還要開闢與原緩衝區大小相同的雙倍緩衝區,是不是太浪費呢?我們先放下這個問題,稍後再作回答。

    其實,如果再添加一bit來標誌是當前字符是漢字的左半邊還是右半邊的話,就會省去掃描屏幕上當前整行字符串的工作,這樣一來,編程會更簡單。但是有讀者會問,即使是這樣,使用8bit總夠用了吧?爲什麼還要使用16bit呢?

    我們的作法是∶用低8位來存放漢字另外一半的內碼,用高8位中的2 bit來存放上面所講的輔助信息,高8位的剩餘6位可以用來存放漢字或其它編碼方式(如BIG5或日文、韓文)的信息,從而使我們可以實現同屏顯示多種雙字節語言的字符而不會有相互干擾。另外,在編程時,雙倍緩衝也比較容易計算。這樣我們就回答瞭如上的兩個問題。

    迄今爲止,我們有了一套徹底解決漢字和製表符相互干擾、半個漢字的刷新、重繪等問題的方案。剩下的就是具體編程實現的問題了。

    但是,由於Framebuffer的驅動很多,修改每一個驅動的xxxx_putc()函數和xxxx_putcs( )函數會是一項不小的工作,而且,改動驅動程序後,每種驅動的測試也是很麻煩的,尤其是對於有硬件加速的顯卡,修改和測試會更不容易。那麼,存不存在一種不需要修改顯卡驅動程序的方法呢?

    經過努力,我們發現,可以在調用xxxx_putcs( )或xxxx_putc()函數輸出漢字之前,修改vga字庫的指針使其指向所需顯示的漢字在漢字字庫中的位置,即把一個漢字當成兩個vga ASCII字符輸出。也就是說,在內核中存在兩個字庫,一個是原有的vga字符字庫,另一個是漢字字庫,當我們需要輸出漢字的時候,就把vga字庫的指針指向漢字字庫的相應位置,漢字輸出完之後,再把該指針指向vga字庫的原有位置。

   這樣一來,我們只需要修改fbcon.c和console.c,其中console.c負責維護雙倍緩衝區,把每一個字符的信息存入附加的緩衝區;而 fbcon.c負責利用雙倍緩衝區中附加的信息,調整vga字庫的指針,調用底層的顯示驅動程序。這裏還有幾個需要注意的地方∶

 

由於屏幕重繪等原因,調用底層驅動xxxx_putc( )和xxxx_putcs()的地方有多處。我們作了兩個函數分別包裝這兩個調用,完成替換字庫、調用xxxx_putcs( )或xxxx_putc( )、恢復字庫等功能。

爲了實現向上滾屏(shift + pageup)時也能看到漢字,我們需要作另外的修改。

    Linux 在設計虛擬終端的時候,提供了回顧被卷出屏幕以外的信息的功能,這就是用熱鍵來向上滾屏(shift + pageup)。當前被使用的虛擬終端擁有一個公共的緩衝區(soft back),用來存放被滾出屏幕以外的信息。當切換虛擬終端的時候,公共緩衝區的內容會被清除而被新的虛擬終端使用。向上滾屏的時候,顯示的是公共緩衝區中的內容。因此,如果我們想在向上滾屏的時候看到漢字,公共緩衝區也必須加倍,以確保沒有信息丟失。當滾出屏幕的信息向公共緩衝區填寫的時候,必須把相應的附加信息也填寫進公共緩衝區的附加區域。這就要求fbcon.c必須懂得利用公共緩衝區的附加信息。

    當然,有另外一種偷懶的方法,那就是不允許用戶向上滾屏,從而避免對公區緩衝區的處理。

把不同的編碼方式(GB、BIG5、日文和韓文)寫成不同的module,以實現動態加載,從而使得擴展新的編碼方式不需要重新編譯核心。

測試

 

本文實現的Kernel Patch文件(patch.kernel.chinese)可以從http://www.turbolinux.com.cn下載。Cd /usr/src/(該目錄下應有Linux核心源程序所在的目錄linux/) patch -p0 -b < patch.kernel.chinese make menuconfig 請選擇Console drivers選項中的

 

〔*〕 Double Byte Character Display Support(EXPERIMENTAL)

〔*〕 Double Byte GB encode (module only)

〔*〕 VESA VGA graphics console

<*> Virtual Frame Buffer support (ONLY FOR TESTING!)

<*> 8 bpp packed pixels support

<*> 16 bpp packed pixels support

<*> VGA characters/attributes support

〔*〕 Select compiled-in fonts

〔*〕VGA 8x8 font

〔*〕VGA 8x16 font

 

make dep

make bzImage

make modules

make install

make modules_install

 

然後用新的核心啓動。

 

Insmod encode-gb.o

 

四、其它

 

(一)   設置FrameBuffer

    FrameBuffer,可以譯作"幀緩衝",有時簡稱爲 fbdrv,基於fbdrv的console也被稱之爲fbcon。這是一種獨立於硬件的抽象圖形設備。FrameBuffer的優點在於其高度的可移植性、易使用性、穩定性。使用Linux內核的 FrameBuffer驅動(vesafb),可以輕鬆支持到1024X768X32bpp以上的分辯率。而且目前可得到的絕大多數linux版本所發行的內核中,已經預編譯了FrameBuffer支持,通常不需要重新編譯內核就可以使用。所以FrameBuffer也是zhcon推薦使用的驅動方式。

 

進入FrameBuffer可以簡單地在系統啓動時向kernel傳送vga=mode-number的參數來激活FrameBuffer設備,如:

lilo:linux vga=305

將會啓動1024x768x8bpp模式。

 

           640x480    800x600    1024x768    1280x1024

  8 bpp      769          771       773        775

  16 bpp     785          788       791        794

  32 bpp     786          789       792        795

 

(二)   要使linux缺省進入FrameBuffer,可以修改/etc/lilo.conf,加入一下語句:

vga=0x303

退出編輯,執行:

lilo -v

重新啓動linux,可以使其進入800x600的256色模式。

grub也是一樣,在grub.conf中的kernel行後面寫上vga=xxx就行了,也可以用vga=ask,讓系統啓動的時候詢問你用多大的分辨率

 

(三)我編譯內核時,選擇framebuffer模式,啓動時屏幕上有一企鵝圖片,不知這是如何造成的這個圖片可以去掉或改動嗎?

可以將drivers/video/fbcon.c: fbcon_setup()中if (logo) { } 代碼去掉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章