1. 程式人生 > >FrameBuffer 原理、實現與應用

FrameBuffer 原理、實現與應用

個人Notes:

What: A framebuffer device is an abstraction for the graphic hardware. It represents (名字源於)the frame buffer of some video  hardware, and allows application software to access the graphic hardware through a well-defined interface, so  that the software doesn't need to know anything about the low-level interface stuff [Taken from Geert  Uytterhoeven's framebuffer.txt in the linux kernel sources]

Why: 方便導向,輕鬆控制導向, 提供原始碼級跨平臺、統一的圖形控制介面。

How: 核心增加抽象層(規範介面)、相關顯示卡驅動“framebuffer規範化”。

Use:

控制方法

    核心:核心原始碼裡寫死(部分引數)、傳遞核心啟動引數。

    shell命令: 系統呼叫/庫函式介面-> fbset(需要顯示卡framebuffer驅動的支援)。

    圖形介面:

Notes:

(1)名字源於顯示卡的幀快取概念,但實則圖形裝置(主要是顯示卡)的完整抽象介面層,不是非要不可,但有了方便很多。=》同一功能通常有多種不同實現策略,按需選擇即可!

(2)所有顯示任務由cpu完成(讀寫framebuffer區),速度不及專用顯示卡驅動。

(3)層次高於顯示卡驅動,相當於顯示卡驅動的外衣,該外衣並非非穿不可,然從linux 2.2.*後通常會穿。

(4)檢視具體的fb:  lsmod|grep fb

一、FrameBuffer的原理
    FrameBuffer 是出現在 2.2.xx 核心當中的一種驅動程式介面。
    Linux是工作在保護模式下,所以使用者態程序是無法象DOS那樣使用顯示卡BIOS裡提供的中斷呼叫來實現直接寫屏,Linux抽象出FrameBuffer這個裝置來供使用者態程序實現直接寫屏。Framebuffer機制模仿顯示卡的功能,將顯示卡硬體結構抽象掉,可以通過Framebuffer的讀寫直接對視訊記憶體進行操作。

使用者可以將Framebuffer看成是顯示記憶體的一個映像,將其對映到程序地址空間之後,就可以直接進行讀寫操作,而寫操作可以立即反應在螢幕上。這種操作是抽象的,統一的。使用者不必關心物理視訊記憶體的位置、換頁機制等等具體細節。這些都是由Framebuffer裝置驅動來完成的。
    但Framebuffer本身不具備任何運算資料的能力,就只好比是一個暫時存放水的水池.CPU將運算後的結果放到這個水池,水池再將結果流到顯示器.中間不會對資料做處理. 應用程式也可以直接讀寫這個水池的內容.在這種機制下,儘管Framebuffer需要真正的顯示卡驅動的支援,但所有顯示任務都有CPU完成,因此CPU負擔很重.
framebuffer的裝置檔案一般是 /dev/fb0、/dev/fb1 等等。
可以用命令: #dd if=/dev/zero of=/dev/fb 清空螢幕.
如果顯示模式是 1024x768-8 位色,用命令:$ dd if=/dev/zero of=/dev/fb0 bs=1024 count=768 清空螢幕;
用命令: #dd if=/dev/fb of=fbfile  可以將fb中的內容儲存下來;
可以重新寫回螢幕: #dd if=fbfile of=/dev/fb;
在使用Framebuffer時,Linux是將顯示卡置於圖形模式下的.

    在應用程式中,一般通過將 FrameBuffer 裝置對映到程序地址空間的方式使用,比如下面的程式就開啟 /dev/fb0 裝置,並通過 mmap 系統呼叫進行地址對映,隨後用 memset 將螢幕清空(這裡假設顯示模式是 1024x768-8 位色模式,線性記憶體模式):

int fb;
unsigned char* fb_mem;
fb = open ("/dev/fb0", O_RDWR);
fb_mem = mmap (NULL, 1024*768, PROT_READ|PROT_WRITE,MAP_SHARED,fb,0);
memset (fb_mem, 0, 1024*768);

    FrameBuffer 裝置還提供了若干 ioctl 命令,通過這些命令,可以獲得顯示裝置的一些固定資訊(比如顯示記憶體大小)、與顯示模式相關的可變資訊(比如解析度、象素結構、每掃描線的位元組寬度),以及偽彩色模式下的調色盤資訊等等。
    通過 FrameBuffer 裝置,還可以獲得當前核心所支援的加速顯示卡的型別(通過固定資訊得到),這種型別通常是和特定顯示晶片相關的。比如目前最新的核心(2.4.9)中,就包含有對 S3、Matrox、nVidia、3Dfx 等等流行顯示晶片的加速支援。在獲得了加速晶片型別之後,應用程式就可以將 PCI 裝置的記憶體I/O(memio)對映到程序的地址空間。這些 memio 一般是用來控制顯示卡的暫存器,通過對這些暫存器的操作,應用程式就可以控制特定顯示卡的加速功能。
    PCI 裝置可以將自己的控制暫存器對映到實體記憶體空間,而後,對這些控制暫存器的訪問,給變成了對實體記憶體的訪問。因此,這些暫存器又被稱為"memio"。一旦被對映到實體記憶體,Linux 的普通程序就可以通過 mmap 將這些記憶體 I/O 對映到程序地址空間,這樣就可以直接訪問這些暫存器了。
    當然,因為不同的顯示晶片具有不同的加速能力,對memio 的使用和定義也各自不同,這時,就需要針對加速晶片的不同型別來編寫實現不同的加速功能。比如大多數晶片都提供了對矩形填充的硬體加速支援,但不同的晶片實現方式不同,這時,就需要針對不同的晶片型別編寫不同的用來完成填充矩形的函式。
    FrameBuffer 只是一個提供顯示記憶體和顯示晶片暫存器從實體記憶體對映到程序地址空間中的裝置。所以,對於應用程式而言,如果希望在 FrameBuffer 之上進行圖形程式設計,還需要自己動手完成其他許多工作。

二、FrameBuffer在Linux中的實現和機制
Framebuffer對應的原始檔在linux/drivers/video/目錄下。總的抽象裝置檔案為fbcon.c,在這個目錄下還有與各種顯示卡驅動相關的原始檔。

(一)、分析Framebuffer裝置驅動
    需要特別提出的是在INTEL平臺上,老式的VESA 1.2 卡,如CGA/EGA卡,是不能支援Framebuffer的,因為Framebuffer要求顯示卡支援線性幀緩衝,即CPU可以訪問顯緩衝中的每一位,但是VESA 1.2 卡只能允許CPU一次訪問64K的地址空間。
FrameBuffer裝置驅動基於如下兩個檔案:
1) linux/include/linux/fb.h
2) linux/drivers/video/fbmem.c

下面分析這兩個檔案。
1、fb.h
   幾乎主要的結構都是在這個中檔案定義的。這些結構包括:
1)fb_var_screeninfo
   這個結構描述了顯示卡的特性:

struct fb_var_screeninfo
{
__u32 xres; /* visible resolution */
__u32 yres;
__u32 xres_virtual; /* virtual resolution */
__u32 yres_virtual;
__u32 xoffset; /* offset from virtual to visible resolution */
__u32 yoffset;

__u32 bits_per_pixel; /* guess what */
__u32 grayscale; /* != 0 Gray levels instead of colors */

struct fb_bitfield red; /* bitfield in fb mem if true color, */
struct fb_bitfield green; /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp; /* transparency */

__u32 nonstd; /* != 0 Non standard pixel format */

__u32 activate; /* see FB_ACTIVATE_* */

__u32 height; /* height of picture in mm */
__u32 width; /* width of picture in mm */

__u32 accel_flags; /* acceleration flags (hints) */

/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 reserved[6]; /* Reserved for future compatibility */
};

2) fb_fix_screeninfon
這個結構在顯示卡被設定模式後建立,它描述顯示卡的屬性,並且系統執行時不能被修改;比如FrameBuffer記憶體的起始地址。它依賴於被設定的模式,當一個模式被設定後,記憶體資訊由顯示卡硬體給出,記憶體的位置等資訊就不可以修改。

struct fb_fix_screeninfo {
char id[16]; /* identification string eg "TT Builtin" */
unsigned long smem_start; /* Start of frame buffer mem */
/* (physical address) */
__u32 smem_len; /* Length of frame buffer mem */
__u32 type; /* see FB_TYPE_* */
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* see FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */
unsigned long mmio_start; /* Start of Memory Mapped I/O */
/* (physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Type of acceleration available */
__u16 reserved[3]; /* Reserved for future compatibility */
};

3) fb_cmap
描述裝置無關的顏色對映資訊。可以通過FBIOGETCMAP 和 FBIOPUTCMAP 對應的ioctl操作設定或獲取顏色對映資訊.

struct fb_cmap {
__u32 start; /* First entry */
__u32 len; /* Number of entries */
__u16 *red; /* Red values */
__u16 *green;
__u16 *blue;
__u16 *transp; /* transparency, can be NULL */
};

4) fb_info
定義當顯示卡的當前狀態;fb_info結構僅在核心中可見,在這個結構中有一個fb_ops指標, 指向驅動裝置工作所需的函式集。

struct fb_info {
char modename[40]; /* default video mode */
kdev_t node;
int flags;
int open; /* Has this been open already ? */
#define FBINFO_FLAG_MODULE 1 /* Low-level driver is a module */
struct fb_var_screeninfo var; /* Current var */
struct fb_fix_screeninfo fix; /* Current fix */
struct fb_monspecs monspecs; /* Current Monitor specs */
struct fb_cmap cmap; /* Current cmap */
struct fb_ops *fbops;
char *screen_base; /* Virtual address */
struct display *disp; /* initial display variable */
struct vc_data *display_fg; /* Console visible on this display */
char fontname[40]; /* default font name */
devfs_handle_t devfs_handle; /* Devfs handle for new name */
devfs_handle_t devfs_lhandle; /* Devfs handle for compat. symlink */
int (*changevar)(int); /* tell console var has changed */
int (*switch_con)(int, struct fb_info*);
/* tell fb to switch consoles */
int (*updatevar)(int, struct fb_info*);
/* tell fb to update the vars */
void (*blank)(int, struct fb_info*); /* tell fb to (un)blank the screen */
/* arg = 0: unblank */
/* arg > 0: VESA level (arg-1) */
void *pseudo_palette; /* Fake palette of 16 colors and
the cursor's color for non
palette mode */
/* From here on everything is device dependent */
void *par;
};

5) struct fb_ops
使用者應用可以使用ioctl()系統呼叫來操作裝置,這個結構就是用一支援ioctl()的這些操作的。

struct fb_ops {
/* open/release and usage marking */
struct module *owner;
int (*fb_open)(struct fb_info *info, int user);
int (*fb_release)(struct fb_info *info, int user);
/* get non settable parameters */
int (*fb_get_fix)(struct fb_fix_screeninfo *fix, int con,
struct fb_info *info);
/* get settable parameters */
int (*fb_get_var)(struct fb_var_screeninfo *var, int con,
struct fb_info *info);
/* set settable parameters */
int (*fb_set_var)(struct fb_var_screeninfo *var, int con,
struct fb_info *info);
/* get colormap */
int (*fb_get_cmap)(struct fb_cmap *cmap, int kspc, int con,
struct fb_info *info);
/* set colormap */
int (*fb_set_cmap)(struct fb_cmap *cmap, int kspc, int con,
struct fb_info *info);
/* pan display (optional) */
int (*fb_pan_display)(struct fb_var_screeninfo *var, int con,
struct fb_info *info);
/* perform fb specific ioctl (optional) */
int (*fb_ioctl)(struct inode *inode, struct file *file, unsigned int cmd,
unsigned long arg, int con, struct fb_info *info);
/* perform fb specific mmap */
int (*fb_mmap)(struct fb_info *info, struct file *file, struct vm_area_struct *vma);
/* switch to/from raster image mode */
int (*fb_rasterimg)(struct fb_info *info, int start);
};

6) structure map
struct fb_info_gen | struct fb_info | fb_var_screeninfo
| | fb_fix_screeninfo
| | fb_cmap
| | modename[40]
| | fb_ops ---|--->ops on var
| | ... | fb_open
| | | fb_release
| | | fb_ioctl
| | | fb_mmap
| struct fbgen_hwswitch -|-> detect
| | encode_fix
| | encode_var
| | decode_fix
| | decode_var
| | get_var
| | set_var
| | getcolreg
| | setcolreg
| | pan_display
| | blank
| | set_disp

[編排有點困難,第一行的第一條豎線和下面的第一列豎線對齊,第一行的第二條豎線和下面的第二列豎線對齊就可以了]
這個結構 fbgen_hwswitch抽象了硬體的操作.雖然它不是必需的,但有時候很有用.

2、 fbmem.c
fbmem.c 處於Framebuffer裝置驅動技術的中心位置.它為上層應用程式提供系統呼叫也為下一層的特定硬體驅動提供介面;那些底層硬體驅動需要用到這兒的介面來向系統核心註冊它們自己. fbmem.c 為所有支援FrameBuffer的裝置驅動提供了通用的介面,避免重複工作.

1) 全域性變數

struct fb_info *registered_fb[FB_MAX];
int num_registered_fb;


這兩變數記錄了所有fb_info 結構的例項,fb_info 結構描述顯示卡的當前狀態,所有裝置對應的fb_info 結構都儲存在這個陣列中,當一個FrameBuffer裝置驅動向系統註冊自己時,其對應的fb_info 結構就會新增到這個結構中,同時num_registered_fb 為自動加1.

static struct {
const char *name;
int (*init)(void);
int (*setup)(void);
} fb_drivers[] __initdata= { ....};

如果FrameBuffer裝置被靜態連結到核心,其對應的入口就會新增到這個表中;如果是動態載入的,即使用insmod/rmmod,就不需要關心這個表。

static struct file_operations fb_ops ={
owner: THIS_MODULE,
read: fb_read,
write: fb_write,
ioctl: fb_ioctl,
mmap: fb_mmap,
open: fb_open,
release: fb_release
};
這是一個提供給應用程式的介面.

2)fbmem.c 實現瞭如下函式.

register_framebuffer(struct fb_info *fb_info);
unregister_framebuffer(struct fb_info *fb_info);

這兩個是提供給下層FrameBuffer裝置驅動的介面,裝置驅動通過這兩函式向系統註冊或登出自己。幾乎底層裝置驅動所要做的所有事情就是填充fb_info結構然後向系統註冊或登出它。

(二)一個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的例子

  1. FrameBuffer主要是根據VESA標準的實現的,所以只能實現最簡單的功能。
  2. 由於涉及核心的問題,FrameBuffer是不允許在系統起來後修改顯示模式等一系列操作。 (好象很多人都想要這樣幹,這是不被允許的,當然如果你自己寫驅動的話,是可以實現的) .
  3. 對FrameBuffer的操作,會直接影響到本機的所有控制檯的輸出,包括XWIN的圖形介面。

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

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到顯示字元的對映關係可以由使用者自行定義。在預設的對映表上,會把中文的字元對映到其他的字元上,這是我們不希望看到也是不需要的。所以我們有兩個選擇∶

  1. 不進行conv_uni_to_pc( )的轉換。
  2. 載入符合雙位元組處理的對映關係,即對非控制字元進行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字元來顯示。

試驗的結果表明∶

  1. 能夠輸出漢字,但仍有許多不理想的地方,比如說,輸出以半個漢字開始的一串漢字,則這半個漢字後面的漢字都會是亂碼。這是半個漢字的問題。
  2. 游標移動會破壞漢字的顯示。表現為,游標移動過的漢字會變成亂碼。這是因為游標的更新是通過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字型檔的指標,呼叫底層的顯示驅動程式。這裡還有幾個需要注意的地方∶

  1. 由於螢幕重繪等原因,呼叫底層驅動xxxx_putc( )和xxxx_putcs()的地方有多處。我們作了兩個函式分別包裝這兩個呼叫,完成替換字型檔、呼叫xxxx_putcs( )或xxxx_putc( )、恢復字型檔等功能。
  2. 為了實現向上滾屏(shift + pageup)時也能看到漢字,我們需要作另外的修改。
        Linux 在設計虛擬終端的時候,提供了回顧被卷出螢幕以外的資訊的功能,這就是用熱鍵來向上滾屏(shift + pageup)。當前被使用的虛擬終端擁有一個公共的緩衝區(soft back),用來存放被滾出螢幕以外的資訊。當切換虛擬終端的時候,公共緩衝區的內容會被清除而被新的虛擬終端使用。向上滾屏的時候,顯示的是公共緩衝區中的內容。因此,如果我們想在向上滾屏的時候看到漢字,公共緩衝區也必須加倍,以確保沒有資訊丟失。當滾出螢幕的資訊向公共緩衝區填寫的時候,必須把相應的附加資訊也填寫進公共緩衝區的附加區域。這就要求fbcon.c必須懂得利用公共緩衝區的附加資訊。
        當然,有另外一種偷懶的方法,那就是不允許使用者向上滾屏,從而避免對公區緩衝區的處理。
  3. 把不同的編碼方式(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) { } 程式碼去掉。

Framebuffer HOWTO  :   http://tldp.org/HOWTO/pdf/Framebuffer-HOWTO.pdf

其他優秀資訊蒐集

專用顯示卡更快!

framebuffer就是把顯示螢幕用記憶體的一塊空間來虛擬螢幕而已。然後具體的底層硬體驅動再負責將這一塊記憶體的內容寫道顯示RAM。此時就可以使用諸如DMA等技術來加快資料的傳輸速度。當然,如果硬體直接支援,也可以直接將framebuffer對映到顯示RAM,從而直接完成寫螢幕,不需要再經過中間的傳輸過程。
所謂專業的驅動程式,無非就是完成一些更快速的操作,比如你要將螢幕上一塊區域反白,如果直接用程式操作,就得先度回這寫區域的內容,然後和0xff異或,再寫到螢幕上,如此修改一個點的資料就需要三個步驟。但是如果顯示硬體可以直接支援這種特性的話,你就可以只需要設定需要操作的區域,然後設定操作的模式(比如異或),顯示的硬體就可以幫助你完成剩餘的工作,所以速度會成倍的增加。有此可見顯示驅動的一斑。