10. LCD驅動程序 ——框架分析
引言:
由LCD的硬件原理及操作(可參看韋哥博客:第017課 LCD原理詳解及裸機程序分析)
我們知道只要LCD控制器的相關寄存器正確配置好,就可以在LCD面板上顯示framebuffer中的內容。
若應用程序需要在LCD屏幕上顯示文字或圖像時,只需要把相應的顯示內容以正確的格式寫到Framebuffer中即可。
(Framebuffer,中文名字是幀緩沖,這個幀也就是一副圖像所需要的數據。因此,幀緩沖其實就是LCD設備的驅動程序)
一.LCD驅動程序框架
根據上述思路,Linux LCD 驅動程分為兩個層次,如下圖所示
類似於Platform 平臺驅動框架,也將驅動程序分為相對穩定的算法驅動,即fb總線驅動,與易變的設備驅動,即fb設備驅動
1.底層為LCD硬件驅動層
負責對LCD硬件相關寄存器進行初始化;
2.上層為幀緩沖區層
主要用來為應用程序提供操作LCD屏的接口,應用程序要在LCD上顯示時,只需把內容寫到幀緩沖區中即可。
在幀緩沖區層,主要把內核空間的一塊內存虛擬為一個字符設備,並實現文件接口操作函數(open/read/write)
然後把幀緩沖註冊為一個字符設備,這樣在應用層就可以像訪問普通字符設備一樣來訪問幀緩沖,從而實現顯示。
二、驅動源碼分析
以s3c2440 CPU為例:
FrameBuffer設備驅動基於如下兩個文件:
1) linux/include/linux/fb.h
2) linux/drivers/video/fbmem.c
下面分析這兩個文件。
一)fb.h
幀緩沖主要的數據結構幾乎都是在這個中文件定義的。這些結構包括:
1)fb_var_screeninfo
2) fb_fix_screeninfon
3) fb_cmap
4) fb_info
5) struct fb_ops
6) structure map
詳見:【Linux開發】全面的framebuffer詳解
二).fbmem.c
fbmem.c 處於Framebuffer設備驅動技術的中心位置.它為上層應用程序提供系統調用,
也為下一層的特定硬件驅動提供接口;那些底層硬件驅動需要用到這兒的接口來向系統內核註冊它們自己.
fbmem.c 為所有支持FrameBuffer的設備驅動提供了通用的接口,避免重復工作.
內核中的frambuffer在drivers/video/fbmem.c
1. 進入fbmem.c找到它的入口函數:
1 fbmem_init(void) 2 { 3 create_proc_read_entry("fb", 0, NULL, fbmem_read_proc, NULL); 4 5 if (register_chrdev(FB_MAJOR,"fb",&fb_fops)) //創建字符設備 6 printk("unable to get major %d for fb devs\n", FB_MAJOR); 7 8 fb_class = class_create(THIS_MODULE, "graphics"); //創建類 9 if (IS_ERR(fb_class)) { 10 printk(KERN_WARNING "Unable to create fb class; errno = %ld\n", PTR_ERR(fb_class)); 11 fb_class = NULL; 12 } 13 return 0; 14 }
(1)create_proc_read_entry在/proc下也會有fb文件
(2)創建字符設備"fb", FB_MAJOR=29,主設備號為29,我們cat /proc/devices 也能找到這個字符設備:
與之前的驅動程序一樣,但是沒有使用創建設備節點,為什麽?
因為需要註冊了LCD驅動後,才會有設備節點
(3)class_create 註冊了一個類 graphics, 具體的設備文件不在此處創建
2.進入結構體fb_fops
此處註冊的是字符設備驅動,結構為默認的file_operations = fb_fops, 從 open = fb_open 開始分析
分析一下應用層是如何打開驅動、讀取驅動數據
2.1 fb_open函數如下:
1 static int fb_open(struct inode *inode, struct file *file) 2 { 3 int fbidx = iminor(inode); //獲取設備節點的次設備號 4 struct fb_info *info; //定義fb_info結構體,其中包含幀緩沖相關信息 5 int res = 0; 6 ... ... 7 8 if (!(info = registered_fb[fbidx])) //(1) info= registered_fb[fbidx],獲取此設備號的lcd驅動信息 9 try_to_load(fbidx); 10 ... ... 11 12 if (info->fbops->fb_open) { ////判斷此結構體中是否有fb_open 13 res = info->fbops->fb_open(info,1); //調用registered_fb[fbidx]->fbops->fb_open 14 if (res) 15 module_put(info->fbops->owner); 16 } 17 18 return res; 19 }
1).registered_fb[fbidx] 這個數組也是fb_info結構體,其中fbidx等於次設備號id,顯然這個數組就是保存我們各個lcd驅動的信息。
2).根據次設備號在 registered_fb 中尋找對應的 fb_info
中的 fb_ops
中的open,有就調用,無則返回
#define FB_MAX 32 //次設備號最大為32
,最多支持32個fb
設備
extern struct fb_info *registered_fb[FB_MAX];
2.1 fb_read函數如下:
1 static ssize_t fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) 2 { 3 unsigned long p = *ppos; 4 struct inode *inode = file->f_path.dentry->d_inode; 5 int fbidx = iminor(inode); //獲取次設備號 6 struct fb_info *info = registered_fb[fbidx]; //獲取次設備號的lcd驅動的信息 7 u32 *buffer, *dst; 8 u32 __iomem *src; 9 int c, i, cnt = 0, err = 0; 10 unsigned long total_size; 11 ... ... 12 if (info->fbops->fb_read) //如果自定義了驅動層的read,則調用自定義的,否則調用默認的 13 return info->fbops->fb_read(info, buf, count, ppos); 14 15 total_size = info->screen_size; //獲取屏幕長度 16 17 ... ... 18 19 buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,GFP_KERNEL); //分配緩沖區 20 if (!buffer) 21 return -ENOMEM; 24 25 src = (u32 __iomem *) (info->screen_base + p); //獲取顯存物理基地址 26 if (info->fbops->fb_sync) 27 info->fbops->fb_sync(info); 28 29 while (count) { 30 c = (count > PAGE_SIZE) ? PAGE_SIZE : count; //獲取頁地址 31 dst = buffer; 32 33 /*因為src是32位,一個src等於4個字節,所以頁地址c >> 2*/ 34 for (i = c >> 2; i--; ) 35 *dst++ = fb_readl(src++); //讀取顯存每個像素點數據,放到dst地址上 36 37 if (c & 3) { 38 u8 *dst8 = (u8 *) dst; 39 u8 __iomem *src8 = (u8 __iomem *) src; 40 for (i = c & 3; i--;) 41 *dst8++ = fb_readb(src8++); 42 src = (u32 __iomem *) src8; 43 } 44 if (copy_to_user(buf, buffer, c)) { //上傳數據,長度等於頁地址大小 45 err = -EFAULT; 46 break; 47 } 48 *ppos += c; 49 buf += c; 50 cnt += c; 51 count -= c; 52 } 53 kfree(buffer); 54 return (err) ? err : cnt; 55 }
從.open和.write函數中可以發現,都依賴於fb_info幀緩沖信息結構體,它從registered_fb[fbidx]數組中得到,這個數組保存我們各個lcd驅動的信息
.3.registered_fb[fbidx]數組在哪裏被註冊,位於register_framebuffer():
1 int register_framebuffer(struct fb_info *fb_info) 2 { 3 ... ... 4 for (i = 0 ; i < FB_MAX; i++) //查找空的數組 5 if (!registered_fb[i]) 6 break; 7 8 fb_info->node = i; 9 ... ... 10 11 /*創建設備節點,名稱為fdi,主設備號為29,次設備號為i */ 12 fb_info->dev = device_create(fb_class, fb_info->device,MKDEV(FB_MAJOR, i), "fb%d", i); 13 ... ... 14 15 registered_fb[i] = fb_info; 16 ... ... 17 }
register_framebuffer()除了註冊fb_info,還創建了設備節點,底層驅動程序即設備驅動,通過調用register_framebuffer來設置硬件
所以要註冊驅動時就調用這個,如下圖所示:
4.再來看看/drivers/video/s3c2410fb.c 中又是怎麽實現驅動的
4.1先找到入口出口函數:
1 int __devinit s3c2410fb_init(void) 2 { 3 return platform_driver_register(&s3c2410fb_driver); 4 } 5 6 static void __exit s3c2410fb_cleanup(void) 7 { 8 platform_driver_unregister(&s3c2410fb_driver); 9 }
入口函數中,註冊LCD平臺設備驅動的數據結構體到平臺總線上。出口函數則卸載。
4.2 來看看平臺設備驅動 s3c2410fb_driver 如何定義的
1 static struct platform_driver s3c2410fb_driver = { 2 .probe = s3c2410fb_probe, //檢測函數,註冊設備 3 .remove = s3c2410fb_remove, //刪除設備 4 .suspend = s3c2410fb_suspend, //休眠 5 .resume = s3c2410fb_resume, //喚醒 6 .driver = { 7 .name = "s3c2410-lcd", //drv名字 8 .owner = THIS_MODULE, 9 }, 10 };
當有對應的設備註冊到平臺總線上時,就會根據設備名(s3c2410-lcd)或ID,找到相應的設備驅動,調用probe函數來探測設備。
4.2 再進入probe函數看看它的處理
先看看函數傳入的參數 s3c2410fb_probe (struct platform_device *pdev)
1 //arch\arm\plat-s3c24xx\devs.c 2 struct platform_device s3c_device_lcd = { 3 .name = "s3c2410-lcd", //設備名稱 4 .id = -1, //設備ID 5 .num_resources = ARRAY_SIZE(s3c_lcd_resource), 6 .resource = s3c_lcd_resource, 7 .dev = { 8 .dma_mask = &s3c_device_lcd_dmamask, 9 .coherent_dma_mask = 0xffffffffUL 10 } 11 };
1 static int __init s3c2410fb_probe(struct platform_device *pdev) 2 { 3 struct s3c2410fb_info *info; //定義指向s3c2410fb_info的結構體指針 4 struct fb_info *fbinfo; //定義指向fb_info的結構體指針 5 struct s3c2410fb_hw *mregs; 6 int ret; 7 int irq; 8 int i; 9 u32 lcdcon1; 10 11 mach_info = pdev->dev.platform_data; //獲取LCD設備信息(長寬、類型等) 12 13 if (mach_info == NULL) { 14 dev_err(&pdev->dev,"no platform data for lcd, cannot attach\n"); 15 return -EINVAL; 16 } 17 mregs = &mach_info->regs; 18 19 20 irq = platform_get_irq(pdev, 0); 21 if (irq < 0) { 22 dev_err(&pdev->dev, "no irq for device\n"); 23 return -ENOENT; 24 } 25 27 /* 1. 分配一個s3c2410fb_info結構體給fbinfo*/ 28 fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); 29 if (!fbinfo) { 30 return -ENOMEM; 31 } 34 35 /*2.設置fb_info*/ 36 info = fbinfo->par; //par成員用來存放幀緩沖的私有數據,此處為LCD控制器 37 info->fb = fbinfo; 38 info->dev = &pdev->dev; 39 ... ... 40 41 /*3.硬件相關的操作,設置中斷,LCD時鐘頻率,顯存地址, 配置引腳... ...*/ 42 ret = request_irq(irq, s3c2410fb_irq, IRQF_DISABLED, pdev->name, info); //設置中斷 43 info->clk = clk_get(NULL, "lcd"); //獲取時鐘 44 clk_enable(info->clk); //使能時鐘 45 ret = s3c2410fb_map_video_memory(info); //顯存地址 46 ret = s3c2410fb_init_registers(info); //設置寄存器,配置引腳 47 ... ... 48
/* 4.註冊一個fb_info結構體,裏面包含幀緩沖的相關信息 */ 49 ret = register_framebuffer(fbinfo); 50 if (ret < 0) { 51 printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret); 52 goto free_video_memory; 53 } 54 ... ... 55 return ret; 56 }
完成了幀緩沖變量struct s3c2410fb_info初始化之後,調用fbmem.c的接口,即第49行,通過register_framebuffer註冊fb_info結構體後,
會根據次設備號將fb_info存入registered_fb[fbidx]數組中,
這樣操作函數就可以通過次設備號找到數組中對應的設備信息,進行操作。
參考一下框圖
總結
參照drivers\video\s3c2410fb.c
來設計這個fb
總線下的platform
平臺驅動,我們這裏不使用platform
設計,
而是直接寫驅動.參考s3c2410fb_probe
來進行初始化設置
由上可知要寫個LCD驅動程序,需要以下4步:
1) 分配一個fb_info結構體: framebuffer_alloc();
2) 設置fb_info
3) 硬件相關的操作(設置中斷,LCD時鐘頻率,顯存地址, 配置引腳... ...)
4 註冊fb_info: register_framebuffer()
下一節寫LCD驅動程序
參考:
lcd驅動框架
Linux的幀緩沖設備
【Linux開發】全面的framebuffer詳解
深入理解嵌入式Linux設備驅動程序
10. LCD驅動程序 ——框架分析