十二、Linux驅動之LCD驅動
1. 基本概念
LCD是Liquid Crystal Display的簡稱,也就是經常所說的液晶顯示器。LCD能夠支援彩色影象的顯示和視訊的播放,是一種非常重要的輸出裝置。如果我們的系統要用GUI(圖形介面介面),比如minigui,MicroWindows。這時LCD裝置驅動程式就應該編寫成frambuffer介面,而不是編寫成僅僅操作底層的LCD控制器介面。
framebuffer是Linux系統為顯示裝置提供的一個介面,它將顯示緩衝區抽象,遮蔽影象硬體的底層差異,允許上層應用程式在圖形模式下直接對顯示緩衝區進行操作。framebuffer
framebuffer是一個標準的字元裝置,主裝置號是29,次裝置號根據緩衝區的數目而定。framebuffer對應/dev/fb%d裝置檔案。根據顯示卡的多少,裝置檔案可能是/dev/fb0、/dev/fb1等。緩衝區裝置也是一種普通的記憶體裝置,可以直接對其進行讀寫。
對使用者程式而言,它和/dev
2. 分析核心
2.1 驅動框架
LCD驅動也是一個字元裝置驅動,那麼核心中是如何實現的呢?框架如下圖:
接下來我們便通過上圖結構深入分析核心實現LCD驅動的過程。
2.2 fbmem.c(drivers/video中)
fbmem.c是frambuffer驅動的核心,他向上給應用程式提供了系統呼叫介面,向下對特定的硬體提供底層的驅動介面。底層驅動可以通過介面向核心註冊自己。fbmem.c
2.2.1 入口函式fbmem_init()
首先我們定位到核心(linux-2.6.22.6)的fbmem.c的入口函式fbmem_init(),程式碼如下:
fbmem_init(void)
{
create_proc_read_entry("fb", 0, NULL, fbmem_read_proc, NULL);
if (register_chrdev(FB_MAJOR,"fb",&fb_fops)) //建立字元裝置
printk("unable to get major %d for fb devs\n", FB_MAJOR);
fb_class = class_create(THIS_MODULE, "graphics"); //建立類
if (IS_ERR(fb_class)) {
printk(KERN_WARNING "Unable to create fb class; errno = %ld\n", PTR_ERR(fb_class));
fb_class = NULL;
}
return 0;
}
在fbmem_init()中建立字元裝置"fb", 註冊file_oprations結構體fb_fops,主裝置FB_MAJOR為29。啟動核心後在開發板上執行“cat /proc/devices”如下:
可以看到,確實是建立了主裝置號為29的"fb"裝置,而這裡還沒有建立裝置節點,後面會提到,核心將該工作放到註冊lcd驅動的介面函式裡了。
2.2.2 fb_open()
繼續將程式碼定位到註冊的file_operations結構體裡面的fb_open()函式,部分程式碼如下:
static int fb_open(struct inode *inode, struct file *file)
{
int fbidx = iminor(inode); //獲取裝置節點的次裝置號
struct fb_info *info; //定義fb_info結構體
int res = 0;
...
if (!(info = registered_fb[fbidx])) //info= registered_fb[fbidx],獲取此裝置號的lcd驅動資訊
try_to_load(fbidx);
...
if (info->fbops->fb_open) {
res = info->fbops->fb_open(info,1); //呼叫registered_fb[fbidx]->fbops->fb_open
if (res)
module_put(info->fbops->owner);
}
return res;
}
fb_open()函式間接呼叫registered_fb[fbidx]->fbops->fb_open(),核心搜尋registered_fb發現(include/linux/fb.h中):
extern struct fb_info *registered_fb[FB_MAX]; //#define FB_MAX 32
可知registered_fb是一個struct fb_info結構體型別全域性陣列,搜尋核心發現在register_framebuffer()函式中被賦值。
2.2.3 register_framebuffer()
register_framebuffer()函式部分程式碼如下:
int register_framebuffer(struct fb_info *fb_info)
{
int i;
...
for (i = 0 ; i < FB_MAX; i++) //查詢空的registered_fb陣列項
if (!registered_fb[i])
break;
fb_info->node = i;
fb_info->dev = device_create(fb_class, fb_info->device, MKDEV(FB_MAJOR, i), "fb%d", i); //建立裝置
...
registered_fb[i] = fb_info; //填充新的registered_fb陣列項
...
return 0;
}
register_framebuffer()函式首先從registered_fb陣列中查詢空的陣列項,然後填充fb_info結構體,賦給這個空的陣列項中,在這裡還建立了裝置節點(前面建立字元裝置未完成的工作)。從這裡我們可以看出,register_framebuffer()函式通過註冊各種各樣的fb_info,來讓核心支援多種LCD裝置,並且以/dev/fb*的形式命名。
2.2.4 fb_mmap()
framebuffer的顯示緩衝區位於Linux的核心態地址空間。而在Linux中,每個應用程式都有自己的虛擬地址空間,在應用程式中是不能直接訪問物理緩衝區的。為此,Linux在檔案操作file_operations結構中提供了mmap()函式,可將檔案的內容對映到使用者空間。對應幀緩衝裝置,則可以通過對映操作,將螢幕緩衝區的實體地址對映到使用者空間的一段虛擬地址中,之後使用者就可以通過讀寫這段虛擬地址訪問螢幕緩衝區,在螢幕上繪圖。
2.2.5 總結
通過引入fb_info的形式,將硬體相關的部分與fs檔案裝置操作分離開,增加了核心程式碼的穩定性。我們只需呼叫register_framebuffer()函式註冊一個新的fb_info結構體,即可向核心新增一個LCD驅動裝置。
2.3 s3c2410fb.c(drivers/video中)
接下來我們再來分析核心如何構建fb_info結構體。
2.3.1 入口函式s3c2410fb_init()
分析驅動,首先從入口函式入手,s3c2410fb_init()程式碼如下:
int __devinit s3c2410fb_init(void)
{
return platform_driver_register(&s3c2410fb_driver);
}
該函式註冊一個platform_driver,從上一節十一、Linux驅動之platform匯流排裝置驅動可知,當核心有成員.name名稱相同的platform_device時,會呼叫到platform_driver裡的成員.probe,在這裡就是s3c2410fb_probe()函式。
2.3.2 s3c2410fb_probe()
s3c2410fb_probe()部分程式碼如下:
static int __init s3c2410fb_probe(struct platform_device *pdev)
{
struct s3c2410fb_info *info;
struct fb_info *fbinfo;
struct s3c2410fb_hw *mregs;
int ret;
int irq;
int i;
u32 lcdcon1;
mach_info = pdev->dev.platform_data; //獲取LCD裝置資訊(長寬、型別等)
if (mach_info == NULL) {
dev_err(&pdev->dev,"no platform data for lcd, cannot attach\n");
return -EINVAL;
}
mregs = &mach_info->regs;
irq = platform_get_irq(pdev, 0); //得到中斷號
if (irq < 0) {
dev_err(&pdev->dev, "no irq for device\n");
return -ENOENT;
}
fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); //分配一個fb_info結構體
if (!fbinfo) {
return -ENOMEM;
}
/*設定fb_info*/
info = fbinfo->par;
info->fb = fbinfo;
info->dev = &pdev->dev;
... ...
/*硬體相關的操作,設定中斷,LCD時鐘頻率,視訊記憶體地址, 配置引腳... ...*/
ret = request_irq(irq, s3c2410fb_irq, IRQF_DISABLED, pdev->name, info); //設定中斷
info->clk = clk_get(NULL, "lcd"); //獲取時鐘
clk_enable(info->clk); //使能時鐘
ret = s3c2410fb_map_video_memory(info); //視訊記憶體地址
ret = s3c2410fb_init_registers(info); //設定暫存器,配置引腳
... ...
ret = register_framebuffer(fbinfo); //註冊一個fb_info結構體
if (ret < 0) {
printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret);
goto free_video_memory;
}
...
return ret;
}
該函式主要工作內容如下:
(1) 分配一個fb_info結構體
(2) 設定fb_info
(3) 與LCD硬體相關的操作
(4) 註冊fb_info結構體
接下來仿造s3c2410fb.c編寫LCD驅動程式。
3. 編寫程式碼
3.1 程式碼框架
3.1.1 在LCD驅動的入口函式中
1. 分配一個fb_info結構體
2. 設定fb_info
2.1 設定固定的引數fb_info-> fix
2.2 設定可變的引數fb_info-> var
2.3 設定操作函式fb_info-> fbops
2.4 設定fb_info 其它的成員
3. 設定硬體相關的操作
3.1 配置LCD引腳
3.2 根據LCD手冊設定LCD控制器
3.3 分配視訊記憶體(framebuffer),把地址告訴LCD控制器和fb_info
4. 開啟LCD,並註冊fb_info: register_framebuffer()
4.1 直接在init函式中開啟LCD(後面講到電源管理,再來優化)
4.1.1 控制LCDCON5允許PWREN訊號,
4.1.2 然後控制LCDCON1輸出PWREN訊號,
4.1.3 輸出GPB0高電平來開背光,
4.2 註冊fb_info
3.1.2 在LCD驅動的出口函式中
1. 解除安裝核心中的fb_info
2. 控制LCDCON1關閉PWREN訊號,關背光,iounmap登出地址
3. 釋放DMA快取地址dma_free_writecombine()
4. 釋放註冊的fb_info
3.2 編寫程式碼
驅動程式lcd.c程式碼如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/div64.h>
#include <asm/mach/map.h>
#include <asm/arch/regs-lcd.h>
#include <asm/arch/regs-gpio.h>
#include <asm/arch/fb.h>
static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red,
unsigned int green, unsigned int blue,
unsigned int transp, struct fb_info *info);
struct lcd_regs {
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 lpcsel;
};
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, //繪畫圖形
};
static struct fb_info *s3c_lcd;
static volatile unsigned long *gpbcon;
static volatile unsigned long *gpbdat;
static volatile unsigned long *gpccon;
static volatile unsigned long *gpdcon;
static volatile unsigned long *gpgcon;
static volatile struct lcd_regs* lcd_regs;
static u32 pseudo_palette[16];
/* 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;
}
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;
/* 用red,green,blue三原色構造出val */
val = chan_to_field(red, &info->var.red);
val |= chan_to_field(green, &info->var.green);
val |= chan_to_field(blue, &info->var.blue);
//((u32 *)(info->pseudo_palette))[regno] = val;
pseudo_palette[regno] = val;
return 0;
}
static int lcd_init(void)
{
/* 1. 分配一個fb_info */
s3c_lcd = framebuffer_alloc(0, NULL);
/* 2. 設定 */
/* 2.1 設定固定的引數 */
strcpy(s3c_lcd->fix.id, "mylcd");
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*2; //每行的長度,以位元組為單位
/* 2.2 設定可變的引數 */
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.bits_per_pixel = 16; //每個象素使用多少位
/* RGB:565 */
s3c_lcd->var.red.offset = 11; //紅色偏移值為11
s3c_lcd->var.red.length = 5; //紅色位長為5
s3c_lcd->var.green.offset = 5; //綠色偏移值為5
s3c_lcd->var.green.length = 6; //綠色位長為6
s3c_lcd->var.blue.offset = 0; //藍色偏移值為0
s3c_lcd->var.blue.length = 5; //藍色位長為5
s3c_lcd->var.activate = FB_ACTIVATE_NOW; //使設定的值立即生效
/* 2.3 設定操作函式 */
s3c_lcd->fbops = &s3c_lcdfb_ops;
/* 2.4 其他的設定 */
s3c_lcd->pseudo_palette = pseudo_palette; //存放調色盤所調顏色的陣列
//s3c_lcd->screen_base = ; //視訊記憶體的虛擬地址,這個在後面設定
s3c_lcd->screen_size = 480*272*16/8; //視訊記憶體的大小
/* 3. 硬體相關的操作 */
/* 3.1 配置GPIO用於LCD */
gpbcon = ioremap(0x56000010, 8);
gpbdat = gpbcon+1;
gpccon = ioremap(0x56000020, 4);
gpdcon = ioremap(0x56000030, 4);
gpgcon = ioremap(0x56000060, 4);
*gpccon = 0xaaaaaaaa; /* GPIO管腳用於VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */
*gpdcon = 0xaaaaaaaa; /* GPIO管腳用於VD[23:8] */
*gpbcon &= ~(3); /* GPB0設定為輸出引腳 */
*gpbcon |= 1;
*gpbdat &= ~1; /* 輸出低電平 */
*gpgcon |= (3<<8); /* GPG4用作LCD_PWREN */
/* 3.2 根據LCD手冊設定LCD控制器, 比如VCLK的頻率等 */
lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs));
lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0x0c<<1);
lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9);//垂直方向的時間引數
lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1); //水平方向的時間引數
lcd_regs->lcdcon4 = 40; //水平方向的同步訊號
lcd_regs->lcdcon5 = (1<<11) | (0<<10) | (1<<9) | (1<<8) | (1<<0); //訊號的極性
/* 3.3 分配視訊記憶體(framebuffer), 並把地址告訴LCD控制器 */
s3c_lcd->screen_base = dma_alloc_writecombine(NULL, s3c_lcd->fix.smem_len, &s3c_lcd->fix.smem_start, GFP_KERNEL);
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); /* 一行的長度(單位: 2位元組) */
//s3c_lcd->fix.smem_start = xxx; /* 視訊記憶體的實體地址 */
/* 啟動LCD */
lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD控制器 */
lcd_regs->lcdcon5 |= (1<<3); /* 使能LCD本身 */
*gpbdat |= 1; /* 輸出高電平, 使能背光 */
/* 4. 註冊 */
register_framebuffer(s3c_lcd);
return 0;
}
static void lcd_exit(void)
{
unregister_framebuffer(s3c_lcd);
lcd_regs->lcdcon1 &= ~(1<<0); /* 關閉LCD本身 */
*gpbdat &= ~1; /* 關閉背光 */
dma_free_writecombine(NULL, s3c_lcd->fix.smem_len, s3c_lcd->screen_base, s3c_lcd->fix.smem_start);
iounmap(lcd_regs);
iounmap(gpbcon);
iounmap(gpccon);
iounmap(gpdcon);
iounmap(gpgcon);
framebuffer_release(s3c_lcd);
}
module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");
Makefile程式碼如下:
KERN_DIR = /work/system/linux-2.6.22.6 //核心目錄
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += lcd.o
4. 測試
核心:linux-2.6.22.6
編譯器:arm-linux-gcc-3.4.5
環境:ubuntu9.10
4.1 配置核心
1. 重新配置核心,將核心自帶的lcd驅動配置為模組。在linux-2.6.22.6核心目錄下執行:
make menuconfig
2. 配置步驟如下:
Device Drivers --->
Graphics support --->
<M> S3C2410 LCD framebuffer support
3. 編譯核心與模組
make uImage
make modules
4.2 重燒核心
1. 將linux-2.6.22.6/arch/arm/boot下的uImage燒寫到開發板,網路檔案系統啟動。
2. 編譯lcd.c檔案,拷貝cfbcopyarea.ko、cfbfillrect.ko、cfbimgblt.ko到網路檔案系統。
make
cp linux-2.6.22.6/drivers/video/cfb*.ko /work/nfs_root/first_fs
3. 裝載驅動
insmod cfbcopyarea.ko
insmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod lcd.ko
5.2 測試
1. 開發板上執行:
echo hello > /dev/tty1 (此時開發板lcd上有“hello”顯示出來)
cat lcd.ko > /dev/fb0 (此時開發板lcd花屏)
vi /etc/inittab
# /etc/inittab
::sysinit:/etc/init.d/rcS
s3c2410_serial0::askfirst:-/bin/sh //新增這行程式碼,將輸出資訊輸出到lcd上
tty1::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
2. 安裝十、Linux驅動之輸入子系統使用裡的驅動
Insmod buttons.ko (此時按下開發板上的按鍵值就能輸出到lcd上了)
5. 相關知識點
分配DMA快取函式函式原型如下:
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA快取區給視訊記憶體
引數:
dev:指標,填0表示這個申請的緩衝區裡沒有內容。
size:分配的地址大小(位元組單位)。
handle:申請到的物理起始地址。
gfp:分配出來的記憶體引數。
該函式分配一段DMA快取區,分配出來的記憶體會禁止cache快取(因為DMA傳輸不需要CPU),它和 dma_alloc_coherent ()函式相似,不過 dma_alloc_coherent ()函式是分配出來的記憶體會禁止cache快取以及禁止寫入緩衝區。
對應函式:
dma_free_writecombine(dev,size,cpu_addr,handle); //釋放快取