基於framebuffer的驅動分析
framebuffer幀緩衝(簡稱fb)是linux內核中用代碼虛擬出的一個設備,是一個platform類型設備,設備文件位於/dev/fb*
- 在嵌入式系統中一般沒有專門的顯存,而僅僅是從RAM(SDRAM)空間中分配一段顯示緩衝區
- framebuffer的作用是:嚮應用層提供一個統一標準接口的顯示設備。不論最終輸出是通過hdmi還是lcd控制器,可以認爲所有的GUI都是向fb輸出畫面的
- 實際上是frambuffer就是linux內核驅動申請的一片內存空間,然後lcd內有一片sram,cpu內部有個lcd控制器,它有個單獨的dma用來將frambuffer中的數據拷貝到lcd的sram中去,拷貝到lcd的sram中的數據就會顯示在lcd上,具體數據的內容是由應用程序控制的。
- LCD驅動和framebuffer驅動沒有必然的聯繫,它只是驅動LCD正常工作的,比如有信號傳過來,那麼LCD驅動負責把信號轉成顯示屏上的內容,至於什麼內容,怎麼顯示,它根本不關心也不知道。
- 對於現代LCD,有一種“多屏疊加”的機制,即一個LCD設備可以有多個獨立虛擬屏幕,以達到畫面疊加的效果。所以fb與LCD不是一對一的關係,在常見的情況下,一個LCD對應了fb0~fb4。像QT這種GUI會默認把畫面輸出到fb0
1.畫面輸出原理
- 如何輸出畫面?應用程序通過往顯存中寫數據,LCD控制器將自動把顯存中的數據映射到lcd屏幕。映射是全自動的,應用層負責往顯存裏寫數據,而驅動要做的僅僅是配置LCD控制器、創建顯存罷了
- 由於顯存實際是處於內核態的物理內存,所以要把這塊物理內存映射到用戶態,所謂“映射”就可以理解爲建立了一個“符號鏈接”,這樣應用程序就可以直接操作這塊物理內存了
- 關於LCD的硬件原理詳見LCD簡介
- 關於如何在應用層測試fb,詳見framebuffer的使用與測試
2.framebuffer驅動結構
fb的結構和misc極爲類似,由內核中的fb框架實現一部分,然後再由設備驅動本身實現一部分。設備驅動本身就是一個普通的platform總線驅動
雖然每家原廠寫的fb設備驅動可能有些差異,但是基本的套路還是相同的
- 在內核fb框架中,所有的fb設備公用一個主設備號(都是29),它們之間以次設備號互相區分。所以在框架中使用register_chrdev註冊了一個主設備號爲29的設備,而在驅動中device_create創建設備文件主設備號都爲29,次設備號不同
- 由上圖可以看出,內核提供了fb框架,原廠提供了fb的platform設備和驅動;不論有多少LCD屏幕,用的都是這一套platform驅動,它們的操作方式都是固定的,唯一的區別就在platform_data裏的硬件參數。而我們驅動工程師重點關注的就是該硬件參數
3.修改LCD的硬件參數(2.6版本內核)
當我們板子上的LCD需要更換時,驅動中也需要進行相應的修改
- 具體的思路,是去修改platform_device中的platform_data,LCD的硬件參數都在裏面,但是怎麼找到這個platform_data是一門學問,因爲原廠寫的代碼是比較複雜的。。。。具體方法詳見基於platform總線的驅動分析 的文末
- 不難找到設置platform_data的地方,如圖
那麼目前導入的platform_data是哪一個呢?根據menuconfig和xxxxdefconfig,分析可知是ek070tn93_fb_data
:
static struct s3c_platform_fb ek070tn93_fb_data __initdata = {
.hw_ver = 0x62,
.nr_wins = 5,
.default_win = CONFIG_FB_S3C_DEFAULT_WINDOW,
.swap = FB_SWAP_WORD | FB_SWAP_HWORD,
.lcd = &ek070tn93,
.cfg_gpio = ek070tn93_cfg_gpio,
.backlight_on = ek070tn93_backlight_on,
.backlight_onoff = ek070tn93_backlight_off,
.reset_lcd = ek070tn93_reset_lcd,
};
- 裏面最關鍵的是
.lcd
這個成員,即結構體ek070tn93
,查看發現裏面有時序、分辨率等各種參數,這樣我們就可以隨便修改參數了
static struct s3cfb_lcd ek070tn93 = {
.width = S5PV210_LCD_WIDTH,
.height = S5PV210_LCD_HEIGHT,
.bpp = 32,
.freq = 60,
.timing = {
.h_fp = 210,
.h_bp = 38,
.h_sw = 10,
.v_fp = 22,
.v_fpe = 1,
.v_bp = 18,
.v_bpe = 1,
.v_sw = 7,
},
.polarity = {
.rise_vclk = 0,
.inv_hsync = 1,
.inv_vsync = 1,
.inv_vden = 0,
},
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
4.修改LCD的硬件參數(3.0+版本內核)
對於新的內核,platformdata都包含在了dts中,所以需要在dts中修改LCD的硬件參數。有關設備樹詳見設備樹詳解
- 首先進入我們項目的dts以及包含的dtsi,尋找合適的地方安放我們新增的時序,一般某個dtsi裏有一個叫display-timings的節點,裏面會放時序。其實說實話時序放在哪裏根本就無所謂,因爲lcd/ldb節點是通過標號來訪問具體的時序節點的,我們之所以選擇放在display-timings裏,僅僅是爲了規範一點
display-timings {
lq4851lg03:lvds_1280x480_53M{
clock-frequency = <53172000>;
hactive = <1280>;
vactive = <480>;
hback-porch = <268>;
hfront-porch = <70>;
vback-porch = <10>;
vfront-porch = <10>;
hsync-len = <70>;
vsync-len = <25>;
pixelclk-active = <0>;
};
ak070tn93:ttl_1280x480_45M{
clock-frequency = <45000000>;
hactive = <1280>;
vactive = <480>;
hback-porch = <40>;
hfront-porch = <73>;
vback-porch = <20>;
vfront-porch = <23>;
hsync-len = <20>;
vsync-len = <10>;
};
xxxxx:xxxxx{
/*需要添加的參數*/
};
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 那麼我們有兩種方案,第一種是直接在默認的時序上修改,第二種添加一個時序,並在項目的dts中使用該時序。在此,我們選擇第二種更優越的方法
- 首先在已有的時序後面添加一個時序,具體參數照抄datasheet即可,唯一要注意的是某幾個參數的名字可能和datasheet上不同:hsync-len對應datasheet上的Horizontal pulse width;vsync-len 對應datasheet上的Vertical pulse width。那麼上面代碼中的
pixelclk-active
= <0>;
意味着什麼呢?這個元素代表時鐘的極性,如果顯示圖片清晰度不足時,可以嘗試加入該元素 - 打開我們項目的dts,然後可以做如下的修改
&mxcfb1 {
disp_dev = "lcd";
};
&ldb {
status = "disabled";
};
&lcd {
status = "okay";
native-mode = <&xxxx>;
};
- 可以看到有三項,mxcfb1、ldb、lcd。mxcfb1裏面的disp_dev決定了圖像通過什麼方式輸出,顯然這裏有兩種方式,ldb和lcd,即lvds輸出或ttl輸出RGB信號。這裏我們選擇了lcd(即ttl輸出RGB信號),那麼ldb顯然是要被”disabled”了,而lcd顯然是”okay”,並且lcd的native-mode選擇了我們剛剛添加的時序
- 有時,除了時序之外,顯示的模式可能也需要設置,比如某個屏幕需要jeida的data-mapping模式,而我們項目中的dtsi中並未設置過:
ldb: ldb@020e0008 {
#address-cells = <1>;
#size-cells = <0>;
gpr = <&gpr>
status = "disabled"
lvds-channel@0 {
reg = <0>
status = "disabled"
}
- 那麼在我們項目的dts中就要對其進行引用,並設置(類似於重寫)
&ldb {
status = "okay";
lvds-channel@0 {
native-mode = <&SHARP_LQ123B5LW>;
fsl,data-mapping = "jeida";
status = "okay";
};
};
5.修改logo顯示(2.6版本內核)
注意:本段關於logo顯示的ne
當kernel啓動,在probe函數運行時,一般會往fb中輸出一個小企鵝logo(開發板廠商可能會改成其他的)。很多時候產品是不需要這個企鵝logo的,我們要學會去修改它
- 在probe函數中,我們可以發現調用了顯示logo的相關代碼,下面是三星寫的,其他廠家應該也不會有很大的區別:
#if !defined(CONFIG_FRAMEBUFFER_CONSOLE) && defined(CONFIG_LOGO)
if (fb_prepare_logo( fbdev->fb[pdata->default_win], FB_ROTATE_UR)) {
printk("Start display and show logo\n");
fb_set_cmap(&fbdev->fb[pdata->default_win]->cmap, fbdev->fb[pdata->default_win]);
fb_show_logo(fbdev->fb[pdata->default_win], FB_ROTATE_UR);
}
#endif
- 不難發現如果要讓logo消失,只需要讓CONFIG_LOGO不被定義即可,即在menuconfig中配置,或者直接修改xxxdefconfig
- 那麼如果我們想要添加另外的logo呢?linux的啓動logo全部放在drivers/video/logo/下,而且都是以一種專門的格式存在的,稱之爲ppm格式。我們可以使用專門的工具把png格式圖片轉成ppm格式,ubuntu裏也有這個工具,網上教程很多
- 假設添加完了,我們要選擇該logo,怎麼選擇?很簡單,這些logo也是由kconfig管理的,我們只要在drivers/video/logo/下的kconfig和makefile中照葫蘆畫瓢,添加我們的logo,然後就能在menuconfig中選擇了!
- 有時,我們做了一個很小的logo,比屏幕像素小得多,它會被系統放到屏幕左上角。那麼如何把它放到屏幕正中央呢?有兩種思路,第一種方法是把logo的整個畫面做的和屏幕分辨率一樣大小,這樣自然就對齊到中間了;第二種方法是修改顯示logo函數的參數。首先找到probe函數中的
fb_show_logo
,在進去一層,找到fb_show_logo_line
,查看其定義,發現裏面有兩行:
image.dx = 0
image.dy = y
這便是logo圖像的座標偏移量,只需改成恰當的值即可讓logo顯示到中央了
/*********************************************************************************************/
b_fix_screeninfo 和 fb_var_screeninfo
fb_fix_screeninfo 和 fb_var_screeninfo 都和 frame buffer 有關。
結構體的成員變量
-
struct fb_fix_screeninfo {
-
char id[16];
-
unsigned long smem_start;
-
-
__u32 smem_len;
-
__u32 type;
-
__u32 type_aux;
-
__u32 visual;
-
__u16 xpanstep;
-
__u16 ypanstep;
-
__u16 ywrapstep;
-
__u32 line_length;
-
unsigned long mmio_start;
-
-
__u32 mmio_len;
-
__u32 accel;
-
-
__u16 reserved[3];
-
};
結構體的成員變量
-
struct fb_var_screeninfo {
-
__u32 xres;
-
__u32 yres;
-
__u32 xres_virtual;
-
__u32 yres_virtual;
-
__u32 xoffset;
-
__u32 yoffset;
-
-
__u32 bits_per_pixel;
-
__u32 grayscale;
-
-
struct fb_bitfield red;
-
struct fb_bitfield green;
-
struct fb_bitfield blue;
-
struct fb_bitfield transp;
-
-
__u32 nonstd;
-
-
__u32 activate;
-
-
__u32 height;
-
__u32 width;
-
-
__u32 accel_flags;
-
-
-
__u32 pixclock;
-
__u32 left_margin;
-
__u32 right_margin;
-
__u32 upper_margin;
-
__u32 lower_margin;
-
__u32 hsync_len;
-
__u32 vsync_len;
-
__u32 sync;
-
__u32 vmode;
-
__u32 rotate;
-
__u32 reserved[5];
-
};
fb_fix_screeninfo 的 line_length 成員,含義是一行的 size,以字節數表示,就是屏幕的寬度。
結 構fb_var_screeninfo定義了視頻硬件一些可變的特性。這些特性在程序運行期間可以由應用程序動態改變
6.在LCD上劃線
***************************************************************************************************************************************
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <stdio.h>
-
#include <fcntl.h>
-
#include <linux/fb.h>
-
#include <linux/kd.h>
-
#include <sys/mman.h>
-
#include <sys/ioctl.h>
-
#include <sys/time.h>
-
#include <string.h>
-
#include <errno.h>
-
struct fb_var_screeninfo vinfo;
-
struct fb_fix_screeninfo finfo;
-
char *frameBuffer = 0;
-
-
-
void printFixedInfo ()
-
{
-
printf ("Fixed screen info:\n"
-
"\tid: %s\n"
-
"\tsmem_start:0x%lx\n"
-
"\tsmem_len:%d\n"
-
"\ttype:%d\n"
-
"\ttype_aux:%d\n"
-
"\tvisual:%d\n"
-
"\txpanstep:%d\n"
-
"\typanstep:%d\n"
-
"\tywrapstep:%d\n"
-
"\tline_length: %d\n"
-
"\tmmio_start:0x%lx\n"
-
"\tmmio_len:%d\n"
-
"\taccel:%d\n"
-
"\n",
-
finfo.id, finfo.smem_start, finfo.smem_len, finfo.type,
-
finfo.type_aux, finfo.visual, finfo.xpanstep, finfo.ypanstep,
-
finfo.ywrapstep, finfo.line_length, finfo.mmio_start,
-
finfo.mmio_len, finfo.accel);
-
}
-
-
-
void printVariableInfo ()
-
{
-
printf ("Variable screen info:\n"
-
"\txres:%d\n"
-
"\tyres:%d\n"
-
"\txres_virtual:%d\n"
-
"\tyres_virtual:%d\n"
-
"\tyoffset:%d\n"
-
"\txoffset:%d\n"
-
"\tbits_per_pixel:%d\n"
-
"\tgrayscale:%d\n"
-
"\tred: offset:%2d, length: %2d, msb_right: %2d\n"
-
"\tgreen: offset:%2d, length: %2d, msb_right: %2d\n"
-
"\tblue: offset:%2d, length: %2d, msb_right: %2d\n"
-
"\ttransp: offset:%2d, length: %2d, msb_right: %2d\n"
-
"\tnonstd:%d\n"
-
"\tactivate:%d\n"
-
"\theight:%d\n"
-
"\twidth:%d\n"
-
"\taccel_flags:0x%x\n"
-
"\tpixclock:%d\n"
-
"\tleft_margin:%d\n"
-
"\tright_margin: %d\n"
-
"\tupper_margin:%d\n"
-
"\tlower_margin:%d\n"
-
"\thsync_len:%d\n"
-
"\tvsync_len:%d\n"
-
"\tsync:%d\n"
-
"\tvmode:%d\n"
-
"\n",
-
vinfo.xres, vinfo.yres, vinfo.xres_virtual, vinfo.yres_virtual,
-
vinfo.xoffset, vinfo.yoffset, vinfo.bits_per_pixel,
-
vinfo.grayscale, vinfo.red.offset, vinfo.red.length,
-
vinfo.red.msb_right,vinfo.green.offset, vinfo.green.length,
-
vinfo.green.msb_right, vinfo.blue.offset, vinfo.blue.length,
-
vinfo.blue.msb_right, vinfo.transp.offset, vinfo.transp.length,
-
vinfo.transp.msb_right, vinfo.nonstd, vinfo.activate,
-
vinfo.height, vinfo.width, vinfo.accel_flags, vinfo.pixclock,
-
vinfo.left_margin, vinfo.right_margin, vinfo.upper_margin,
-
vinfo.lower_margin, vinfo.hsync_len, vinfo.vsync_len,
-
vinfo.sync, vinfo.vmode);
-
}
下面纔是我們的重點,這個代碼是我自己參考別人畫矩形的代碼改過來的
-
-
void drawline_rgb16 (int x0,int y0, int width,int height, int color,int flag0)
-
{
-
const int bytesPerPixel = 2;
-
const int stride = finfo.line_length / bytesPerPixel;,一行有多少個點
-
const int red = (color & 0xff0000) >> (16 + 3);
-
const int green = (color & 0xff00) >> (8 + 2);
-
const int blue = (color & 0xff) >> 3;
-
const short color16 = blue | (green << 5) | (red << (5 +6));
-
int flag=flag0;
-
-
-
short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
-
-
int x=0,y=0;
-
if(flag==0)
-
{
-
for (x = 0; x < width; ++x)
-
{
-
dest[x] = color16;
-
}
-
}
-
else if(flag==1)
-
{
-
for(y=0;y<height;y++)
-
{
-
dest[x]=color16;
-
-
dest +=stride;
-
}
-
}
-
}
解釋:我的屏的lcd分辨率是480*272,分辨率的意思是一行有480個點,一共有272行,其實屏蔽上都是一個個點組成的,在上面畫線的意思並不是真正意思上的拿一支筆畫線。打個比方來說你你把一行中80-180個點都改成紅色(我們屏蔽不是黑色麼),改完你就可以看見一條紅線了,感覺就是畫了一條紅色的直線對不對?
而且“上色”是從左到右一個點一個點,一行一行“上色”的,屏幕的座標系如下圖所示:
-
short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
上面這一行代碼的具體意思就是定位到(x0,y0)這個座標,也就是我們要畫的其實位置
可以下面這個代碼畫一個矩形。
-
-
void drawRect_rgb16 (int x0, int y0, int width,int height, int color)
-
{
-
const int bytesPerPixel = 2;
-
const int stride = finfo.line_length / bytesPerPixel;
-
const int red = (color & 0xff0000) >> (16 + 3);
-
const int green = (color & 0xff00) >> (8 + 2);
-
const int blue = (color & 0xff) >> 3;
-
const short color16 = blue | (green << 5) | (red << (5 +6));
-
-
short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
-
-
int x, y;
-
for (y = 0; y < height; ++y)
-
{
-
for (x = 0; x < width; ++x)
-
{
-
dest[x] = color16;
-
}
-
dest += stride;
-
}
-
}
下面是main函數:
-
int main (int argc, char **argv)
-
{
-
const char *devfile = "/dev/fb0";
-
long int screensize = 0;
-
int fbFd = 0;
-
-
-
-
-
fbFd = open (devfile, O_RDWR);
-
if (fbFd == -1)
-
{
-
perror ("Error: cannot open framebuffer device");
-
exit (1);
-
}
-
-
-
if (ioctl (fbFd, FBIOGET_FSCREENINFO, &finfo) == -1)
-
{
-
perror ("Error reading fixed information");
-
exit (2);
-
}
-
printFixedInfo ();
-
-
if (ioctl (fbFd, FBIOGET_VSCREENINFO, &vinfo) == -1)
-
{
-
perror ("Error reading variable information");
-
exit (3);
-
}
-
printVariableInfo ();
-
-
-
screensize = finfo.smem_len;
-
-
-
frameBuffer =(char *) mmap (0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED,fbFd, 0);
-
if (frameBuffer == MAP_FAILED)
-
{
-
perror ("Error: Failed to map framebuffer device to memory");
-
exit (4);
-
}
-
-
-
-
drawline_rgb16(50,80,260,0,0xffff0000,0);
-
-
drawline_rgb16(160,10,0,180,0xff00ff00,1);
-
sleep (2);
-
printf (" Done.\n");
-
-
munmap (frameBuffer, screensize);
-
-
close (fbFd);
-
return 0;
-
}
用一個流程圖還說明一下mian函數吧
*****************************************************************************************************************************************************************************************
****************************************************************************************************************************************************************************************
這裏最重要的就是mmap這個函數了
void *mmap(void *addr, size_t len, int prot,
int flags, int fd, off_t offset);
addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。
len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。
prot參數指定共享內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀),PROT_WRITE(可寫),PROT_EXEC(可執行),PROT_NONE(不可訪問)。
flags由以下幾個常值指定:MAP_SHARED, MAP_PRIVATE, MAP_FIXED。其中,MAP_SHARED,MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。
如果指定爲MAP_SHARED,則對映射的內存所做的修改同樣影響到文件。如果是MAP_PRIVATE,則對映射的內存所做的修改僅對該進程可見,對文件沒有影響。
offset參數一般設爲0,表示從文件頭開始映射。
mmap使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以像訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。
再來看看我們代碼中的mmap的實例
frameBuffer =(char *) mmap (0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED,fbFd, 0);
前面我們不是說了frambuffer就是linux內核驅動申請的一片內存空間,lcd驅動將frambuffer的地址通過mmap()將這片內存映射到應用程序空間,這樣我們寫入到fb的數據就寫入到內核驅動裏的frambuffer中去了,而lcd 的dma就將這些數據寫入到lcd的sram中,從而顯示在lcd上.