一直想花時間來整理一下Linux內核LCD驅動,卻一直都忙着做其他事情去了,這些天特意抽出時間來整理之前落下的筆記,故事就這樣開始了。LCD驅動也是字符設備驅動的一種,框架上相對於字符設備驅動稍微復雜一點點,真的就是一點點,難點在對LCD硬件的配置上。
開發平台:TQ210,S5PV210處理器
內核版本:linux-3.10.46
LCD型號:AT070TN92,7英寸,TFT屏,分辨率800x480x3(RGB),24位真彩色
一、框架分析

上圖說明:①內核裝載LCD驅動模塊:設置並注冊fb_info結構,初始化LCD硬件。②APP打開LCD設備,獲取設備文件,根據設備文件進行讀寫顯存。③在內核中,根據主設備號和次設備號定位一個fb_info結構,如果應用層的系統調用是讀操作則調用fb_ops中對應的操作函數,寫操作也是一樣。
主要數據結構分析:
1 struct fb_info { 2 int node; //用作次設備號索引 3 int flags; 4 struct mutex lock; //用於open/release/ioctl函數的鎖 5 struct fb_var_screeninfo var; //可變參數,重點 6 struct fb_fix_screeninfo fix; //固定參數,重點 7 struct fb_monspecs monspecs; //顯示器標准 8 struct work_struct queue; //幀緩沖區隊列 9 struct fb_pixmap pixmap; //圖像硬件映射 10 struct fb_pixmap sprite; //光標硬件映射 11 struct fb_cmap cmap; //當前顏色表 12 struct list_head modelist; //模式鏈表 13 struct fb_videomode *mode; //當前video模式 14 15 char __iomem *screen_base; //顯存基地址 16 unsigned long screen_size; //顯存大小 17 void *pseudo_palette; //偽16色顏色表 18 #define FBINFO_STATE_RUNNING 0 19 #define FBINFO_STATE_SUSPENDED 1 20 u32 state; //硬件狀態,如掛起 21 void *fbcon_par; //用作私有數據區 22 void *par; //info->par指向了額外多申請內存空間的首地址 23 };
1 struct fb_ops { 2 struct module *owner; 3 int (*fb_open)(struct fb_info *info, int user); 4 int (*fb_release)(struct fb_info *info, int user); 5 ssize_t (*fb_read)(struct fb_info *info, char __user *buf,size_t count, loff_t *ppos); 6 ssize_t (*fb_write)(struct fb_info *info, const char __user *buf,size_t count, loff_t *ppos); 7 8 /* 檢測可變參數,並調整到支持的值 */ 9 int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info); 10 11 /* 根據 info->var 設置 video 模式 */ 12 int (*fb_set_par)(struct fb_info *info); 13 14 /* set color register */ 15 int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green,unsigned blue, unsigned transp, struct fb_info *info); 16 /* set color registers in batch */ 17 int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info); 18 /* blank display */ 19 int (*fb_blank)(int blank, struct fb_info *info); 20 /* pan display */ 21 int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info); 22 23 /* Draws a rectangle */ 24 void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect); 25 /* Copy data from area to another */ 26 void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region); 27 /* Draws a image to the display */ 28 void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image); 29 30 ...... 31 32 /* perform fb specific ioctl (optional) */ 33 int (*fb_ioctl)(struct fb_info *info, unsigned int cmd, 34 unsigned long arg); 35 /* perform fb specific mmap */ 36 int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma); 37 38 ...... 39 };
主要操作代碼分析:
1 fb_open 2 { 3 int fbidx = iminor(inode); //獲取次設備號 4 struct fb_info *info; 5 info = get_fb_info(fbidx); 6 struct fb_info *fb_info; 7 fb_info = registered_fb[fbidx];//根據次設備號從已注冊的fb_info數組中獲取響應的結構 8 return fb_info; 9 ...... 10 /* 11 * 從registered_fb[]數組項里找到fb_info結構體后,將其保存到 12 * struct file結構中的私有信息成員,難道這是為了以后在某些情況方便找到並調用??先放着... 13 * 回過來發現:這樣做是為了驗證在read、write、ioctl等系統調用中獲得的fb_info結構和open獲得的是否一樣 14 */ 15 file->private_data = info; 16 //info->fbops->fb_open無定義,這是值得思考的問題! 17 if (info->fbops->fb_open) { 18 res = info->fbops->fb_open(info,1); 19 if (res) 20 module_put(info->fbops->owner); 21 } 22 ...... 23 }
1 fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) 2 { 3 struct fb_info *info = file_fb_info(file); 4 struct inode *inode = file_inode(file); 5 int fbidx = iminor(inode); 6 //也是根據次設備號來獲取fb_info結構 7 struct fb_info *info = registered_fb[fbidx]; 8 9 if (info != file->private_data) 10 info = NULL; 11 return info; 12 //無定義 13 if (info->fbops->fb_read) 14 return info->fbops->fb_read(info, buf, count, ppos); 15 //獲得顯存的大小 16 total_size = info->screen_size; 17 //如果應用層要讀的數據count比實際最大的顯存還要大,修改count值為最大顯存值 18 if (count >= total_size) 19 count = total_size; 20 //分配顯存,最大只能是一頁PAGE_SIZE=4KB 21 buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,GFP_KERNEL); 22 //要讀的源地址:顯存虛擬基地址+偏移 23 src = (u8 __iomem *) (info->screen_base + p); 24 while (count) { 25 c = (count > PAGE_SIZE) ? PAGE_SIZE : count; 26 //讀的目的地址 27 dst = buffer; 28 //讀操作:拷貝數據 29 fb_memcpy_fromfb(dst, src, c); 30 dst += c; 31 src += c; 32 33 if (copy_to_user(buf, buffer, c)) { 34 err = -EFAULT; 35 break; 36 } 37 *ppos += c; 38 buf += c; 39 cnt += c; 40 count -= c; 41 } 42 kfree(buffer); //釋放buffer,只起到臨時中轉站的作用 43 }
static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; struct fb_info *info = file_fb_info(file); //獲取fb_info結構 /************************************************************ 函數跟進分析: static struct fb_info *file_fb_info(struct file *file) { struct inode *inode = file_inode(file); int fbidx = iminor(inode); //獲取次設備號 struct fb_info *info = registered_fb[fbidx]; //根據次設備號獲取相應的fb_info結構 if (info != file->private_data) info = NULL; return info; //返回fb_info結構 } ************************************************************/ u8 *buffer, *src; u8 __iomem *dst; int c, cnt = 0, err = 0; unsigned long total_size; //獲取fb_info失敗或者fb_info結構中沒有設置顯存基址,返回 if (!info || !info->screen_base) return -ENODEV; if (info->state != FBINFO_STATE_RUNNING) return -EPERM; //如果幀緩沖操作函數結構中有重定義fb_write函數,優先使用!實際上沒有。 if (info->fbops->fb_write) return info->fbops->fb_write(info, buf, count, ppos); //獲取顯存大小 total_size = info->screen_size; if (total_size == 0) total_size = info->fix.smem_len; //如果寫偏移位置p比整個顯存還要大,出錯返回。 if (p > total_size) return -EFBIG; if (count > total_size) { err = -EFBIG; count = total_size; } if (count + p > total_size) { if (!err) err = -ENOSPC; count = total_size - p; } //內核空間分配臨時幀緩沖區 buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,GFP_KERNEL); if (!buffer) return -ENOMEM; //計算寫目的地址(虛擬地址:內核空間中能夠操作的也就是虛擬地址) dst = (u8 __iomem *) (info->screen_base + p); if (info->fbops->fb_sync) info->fbops->fb_sync(info); while (count) { c = (count > PAGE_SIZE) ? PAGE_SIZE : count; //源地址 src = buffer; if (copy_from_user(src, buf, c)) { err = -EFAULT; break; } // 從內存buffer拷貝數據到幀緩沖區 fb_memcpy_tofb(dst, src, c); dst += c; src += c; *ppos += c; buf += c; cnt += c; count -= c; } kfree(buffer); return (cnt) ? cnt : err; }
1 /* 2 * 函數功能:將內核空間分配的物理顯存空間映射到用戶空間中 3 * 用戶空間就能訪問這段內存空間了 4 */ 5 static int fb_mmap(struct file *file, struct vm_area_struct * vma) 6 { 7 struct fb_info *info = file_fb_info(file); 8 struct fb_ops *fb; 9 unsigned long mmio_pgoff; 10 unsigned long start; 11 u32 len; 12 13 if (!info) 14 return -ENODEV; 15 fb = info->fbops; 16 if (!fb) 17 return -ENODEV; 18 mutex_lock(&info->mm_lock); 19 //如果fb_info->fbops->fb_mmap存在就調用該函數,實際中沒有! 20 if (fb->fb_mmap) { 21 int res; 22 res = fb->fb_mmap(info, vma); 23 mutex_unlock(&info->mm_lock); 24 return res; 25 } 26 /* 27 * fb緩沖內存的開始位置(物理地址) 28 * info->fix.smem_start這個地址是在哪里被設置的? 29 * 在驅動程序xxx_lcd_init()函數中: 30 * clb_fbinfo->screen_base = dma_alloc_writecombine(NULL,clb_fbinfo->fix.smem_len, (u32*)&(clb_fbinfo->fix.smem_start), GFP_KERNEL); 31 * dma_alloc_writecombine函數返回的是內核虛擬起始地址,同時第3個參數fix.smem_start會被設置成對應的物理起始地址。 32 * 內核中操作這個分配的空間只能操作虛擬的地址空間!!! 33 * dma_alloc_writecombine函數的調用只是把物理顯存映射到內核空間,並沒有映射到用戶空間,因此用戶在操作物理顯存之前要先把 34 * 物理顯存空間映射到用戶可見的用戶空間中來,這就是該函數的意義所在。 35 */ 36 start = info->fix.smem_start; 37 //幀緩沖長度 38 len = info->fix.smem_len; 39 mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT; 40 if (vma->vm_pgoff >= mmio_pgoff) { 41 if (info->var.accel_flags) { 42 mutex_unlock(&info->mm_lock); 43 return -EINVAL; 44 } 45 46 vma->vm_pgoff -= mmio_pgoff; 47 start = info->fix.mmio_start; 48 len = info->fix.mmio_len; 49 } 50 mutex_unlock(&info->mm_lock); 51 52 vma->vm_page_prot = vm_get_page_prot(vma->vm_flags); 53 fb_pgprotect(file, vma, start); 54 //映射物理內存到用戶空間虛擬地址 55 return vm_iomap_memory(vma, start, len); 56 }
問題思考:
問1.什么叫幀緩沖區,他有哪些特性指標?
答1.對於應用層來說,顯示圖像到LCD設備就相當於往“一塊內存”中寫入數據,獲取LCD設備上的圖像就相當於拷貝“這塊內存”中的數據。因此,LCD就和“一塊內存”一樣,專業一點術語叫幀緩沖區,它和普通的內存不太一樣,除了可以“讀寫”操作之外還可以進行其他操作和功能設置,特性指標就是LCD的特性指標。在內核中,一個LCD顯示器就相當於一個幀緩沖設備,對應一個fb_info結構。
問2.為什么要通過 registered_fb[] 數組來找到對應的 fb_info 結構體?
答2.通過對上邊這幾個函數的剖析發現,不管是fb_read、fb_write、fb_ioctl、fb_mmap系統調用,都是通過次設備號在已注冊的fb_info結構數組中找到匹配的那一個結構之后,判斷其中的fbops結構中的操作函數是否有定義,有的話就優先調用該函數,沒有就使用往下的方案策略。這樣的好處就是多個相同的LCD設備可以使用同一套代碼,減少代碼的重復性,同時對於需要特殊定義的函數又可以方便實現重定義。
問3.這個數組在哪里被注冊?
答3.在register_framebuffer()函數中被注冊 register_framebuffer(struct fb_info *fb_info) ret = do_register_framebuffer(fb_info); ...... registered_fb[i] = fb_info; ......
問4.fb_mmap()函數在什么場合使用?
答4.在用戶空間中通過mmap()函數來進行系統調用,該函數執行成功返回的是指向被映射的幀緩沖區的指針,這樣用戶直接可以通過該指針來讀寫緩沖區。
問5.在用戶程序中調用write函數和直接使用mmap函數返回的fbp指針有什么不一樣?
答5.用戶空間使用fbp指針操作的地址是用戶空間和物理顯存空間直接映射的關系,而使用write是將用戶中的數據拷貝到內核空間,然后再將這些數據寫到內核中已映射的虛擬地址空間中;write是操作整個fb,而fbp只操作一個像素點。
二、驅動代碼編寫
1 #include <linux/module.h> 2 #include <linux/kernel.h> 3 #include <linux/errno.h> 4 #include <linux/string.h> 5 #include <linux/mm.h> 6 #include <linux/slab.h> 7 #include <linux/delay.h> 8 #include <linux/fb.h> 9 #include <linux/init.h> 10 #include <linux/dma-mapping.h> 11 #include <linux/interrupt.h> 12 #include <linux/platform_device.h> 13 #include <linux/clk.h> 14 #include <linux/workqueue.h> 15 16 #include <asm/io.h> 17 #include <asm/div64.h> 18 #include <asm/uaccess.h> 19 20 #include <asm/mach/map.h> 21 #include <mach/regs-gpio.h> 22 #include <linux/fb.h> 23 24 #define VSPW 9 //4 25 #define VBPD 13 //17 26 #define LINEVAL 479 27 #define VFPD 21 //26 28 29 #define HSPW 19 //4 30 #define HBPD 25 //40 31 #define HOZVAL 799 32 #define HFPD 209 //214 33 34 #define LeftTopX 0 35 #define LeftTopY 0 36 #define RightBotX 799 37 #define RightBotY 479 38 39 static struct fb_info *clb_fbinfo; 40 41 /* LCD GPIO Pins */ 42 static long unsigned long *gpf0con; 43 static long unsigned long *gpf1con; 44 static long unsigned long *gpf2con; 45 static long unsigned long *gpf3con; 46 static long unsigned long *gpd0con; 47 static long unsigned long *gpd0dat; 48 static long unsigned long *display_control; 49 50 /* LCD Controler Pins */ 51 struct s5pv210_lcd_regs{ 52 volatile unsigned long vidcon0; 53 volatile unsigned long vidcon1; 54 volatile unsigned long vidcon2; 55 volatile unsigned long vidcon3; 56 57 volatile unsigned long vidtcon0; 58 volatile unsigned long vidtcon1; 59 volatile unsigned long vidtcon2; 60 volatile unsigned long vidtcon3; 61 62 volatile unsigned long wincon0; 63 volatile unsigned long wincon1; 64 volatile unsigned long wincon2; 65 volatile unsigned long wincon3; 66 volatile unsigned long wincon4; 67 68 volatile unsigned long shadowcon; 69 volatile unsigned long reserve1[2]; 70 71 volatile unsigned long vidosd0a; 72 volatile unsigned long vidosd0b; 73 volatile unsigned long vidosd0c; 74 }; 75 76 struct clk *lcd_clk; 77 static struct s5pv210_lcd_regs *lcd_regs; 78 79 static long unsigned long *vidw00add0b0; 80 static long unsigned long *vidw00add1b0; 81 82 static u32 pseudo_palette[16]; 83 84 /* from pxafb.c */ 85 static unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf) 86 { 87 chan &= 0xffff; 88 chan >>= 16 - bf->length; 89 return chan << bf->offset; 90 } 91 92 static int clb210_lcdfb_setcolreg(unsigned regno, 93 unsigned red, unsigned green, unsigned blue, 94 unsigned transp, struct fb_info *info) 95 { 96 unsigned int val; 97 98 if (regno > 16) 99 return 1; 100 101 /* 用red,green,blue三原色構造出val */ 102 val = chan_to_field(red, &info->var.red); 103 val |= chan_to_field(green, &info->var.green); 104 val |= chan_to_field(blue, &info->var.blue); 105 106 pseudo_palette[regno] = val; 107 108 return 0; 109 } 110 111 //幀緩沖操作函數 112 static struct fb_ops clb210_lcdfb_ops = 113 { 114 .owner = THIS_MODULE, 115 .fb_setcolreg = clb210_lcdfb_setcolreg, //設置color寄存器和調色板 116 //下面這3個函數是通用的 117 .fb_fillrect = cfb_fillrect, //畫一個矩形 118 .fb_copyarea = cfb_copyarea, //數據拷貝 119 .fb_imageblit = cfb_imageblit, //圖像填充 120 }; 121 122 static int __init clb210_lcd_init(void) 123 { 124 /* 1.分配一個fb_info */ 125 clb_fbinfo = framebuffer_alloc(0 , NULL); 126 127 /* 2. 設置 */ 128 /* 2.1 設置固定的參數 */ 129 strcpy(clb_fbinfo->fix.id, "clb210_lcd"); 130 clb_fbinfo->fix.smem_len = 800 * 480 * 32/8; 131 clb_fbinfo->fix.type = FB_TYPE_PACKED_PIXELS; 132 clb_fbinfo->fix.visual = FB_VISUAL_TRUECOLOR; 133 clb_fbinfo->fix.line_length = 800 * 32/8; 134 135 /* 2.2 設置可變的參數 */ 136 clb_fbinfo->var.xres = 800; 137 clb_fbinfo->var.yres = 480; 138 clb_fbinfo->var.xres_virtual = 800; 139 clb_fbinfo->var.yres_virtual = 480; 140 clb_fbinfo->var.bits_per_pixel = 32; 141 142 /*RGB:888*/ 143 clb_fbinfo->var.red.offset = 16; 144 clb_fbinfo->var.red.length = 8; 145 146 clb_fbinfo->var.green.offset = 8; 147 clb_fbinfo->var.green.length = 8; 148 149 clb_fbinfo->var.blue.offset = 0; 150 clb_fbinfo->var.blue.length = 8; 151 152 clb_fbinfo->var.activate = FB_ACTIVATE_NOW ; 153 154 /* 2.3 設置操作函數 */ 155 clb_fbinfo->fbops = &clb210_lcdfb_ops; 156 157 /* 2.4 其他的設置 */ 158 /* 2.4.1 設置顯存的大小 */ 159 clb_fbinfo->screen_size = 800 * 480 * 32/8; 160 161 /* 2.4.2 設置調色板 */ 162 clb_fbinfo->pseudo_palette = pseudo_palette; 163 164 /* 2.4.3 設置顯存的虛擬起始地址 */ 165 clb_fbinfo->screen_base = dma_alloc_writecombine(NULL, 166 clb_fbinfo->fix.smem_len, (u32*)&(clb_fbinfo->fix.smem_start), GFP_KERNEL); 167 168 169 /* 3. 硬件相關的操作 */ 170 /* 3.1 獲取lcd時鍾,使能時鍾 */ 171 lcd_clk = clk_get(NULL, "lcd"); 172 if (!lcd_clk || IS_ERR(lcd_clk)) { 173 printk(KERN_INFO "failed to get lcd clock source\n"); 174 } 175 clk_enable(lcd_clk); 176 177 /* 3.2 配置GPIO用於LCD */ 178 gpf0con = ioremap(0xE0200120, 4); 179 gpf1con = ioremap(0xE0200140, 4); 180 gpf2con = ioremap(0xE0200160, 4); 181 gpf3con = ioremap(0xE0200180, 4); 182 gpd0con = ioremap(0xE02000A0, 4); 183 gpd0dat = ioremap(0xE02000A4, 4); 184 185 gpd0con = ioremap(0xE02000A0, 4); 186 gpd0dat = ioremap(0xE02000A4, 4); 187 188 vidcon0 = ioremap(0xF8000000, 4); 189 vidcon1 = ioremap(0xF8000004, 4); 190 vidtcon0 = ioremap(0xF8000010, 4); 191 vidtcon1 = ioremap(0xF8000014, 4); 192 vidtcon2 = ioremap(0xF8000018, 4); 193 wincon0 = ioremap(0xF8000020, 4); 194 vidosd0a = ioremap(0xF8000040, 4); 195 vidosd0b = ioremap(0xF8000044, 4); 196 vidosd0c = ioremap(0xF8000048, 4); 197 vidw00add0b0 = ioremap(0xF80000A0, 4); 198 vidw00add1b0 = ioremap(0xF80000D0, 4); 199 shodowcon = ioremap(0xF8000034, 4); 200 201 display_control = ioremap(0xe0107008, 4); 202 203 /* 設置相關GPIO引腳用於LCD */ 204 *gpf0con = 0x22222222; 205 *gpf1con = 0x22222222; 206 *gpf2con = 0x22222222; 207 *gpf3con = 0x22222222; 208 209 /* 使能LCD本身 */ 210 *gpd0con |= 1<<4; 211 *gpd0dat |= 1<<1; 212 213 /* 顯示路徑的選擇, 0b10: RGB=FIMD I80=FIMD ITU=FIMD */ 214 *display_control = 2<<0; 215 216 /* 3.3 映射LCD控制器對應寄存器 */ 217 lcd_regs = ioremap(0xF8000000, sizeof(struct s5pv210_lcd_regs)); 218 vidw00add0b0 = ioremap(0xF80000A0, 4); 219 vidw00add1b0 = ioremap(0xF80000D0, 4); 220 221 lcd_regs->vidcon0 &= ~((3<<26) | (1<<18) | (0xff<<6) | (1<<2)); 222 lcd_regs->vidcon0 |= ((5<<6) | (1<<4) ); 223 224 lcd_regs->vidcon1 &= ~(1<<7); /* 在vclk的下降沿獲取數據 */ 225 lcd_regs->vidcon1 |= ((1<<6) | (1<<5)); /* HSYNC極性反轉, VSYNC極性反轉 */ 226 227 lcd_regs->vidtcon0 = (VBPD << 16) | (VFPD << 8) | (VSPW << 0); 228 lcd_regs->vidtcon1 = (HBPD << 16) | (HFPD << 8) | (HSPW << 0); 229 lcd_regs->vidtcon2 = (LINEVAL << 11) | (HOZVAL << 0); 230 lcd_regs->wincon0 &= ~(0xf << 2); 231 lcd_regs->wincon0 |= (0xB<<2)|(1<<15); 232 lcd_regs->vidosd0a = (LeftTopX<<11) | (LeftTopY << 0); 233 lcd_regs->vidosd0b = (RightBotX<<11) | (RightBotY << 0); 234 lcd_regs->vidosd0c = (LINEVAL + 1) * (HOZVAL + 1); 235 236 *vidw00add0b0 = clb_fbinfo->fix.smem_start; 237 *vidw00add1b0 = clb_fbinfo->fix.smem_start + clb_fbinfo->fix.smem_len; 238 239 lcd_regs->shadowcon = 0x1; /* 使能通道0 */ 240 lcd_regs->vidcon0 |= 0x3; /* 開啟總控制器 */ 241 lcd_regs->wincon0 |= 1; /* 開啟窗口0 */ 242 243 244 /*4.注冊*/ 245 register_framebuffer(clb_fbinfo); 246 247 return 0; 248 } 249 static void __exit clb210_lcd_exit(void) 250 { 251 unregister_framebuffer(clb_fbinfo); 252 dma_free_writecombine(NULL, clb_fbinfo->fix.smem_len, clb_fbinfo->screen_base, clb_fbinfo->fix.smem_start); 253 iounmap(gpf0con); 254 iounmap(gpf1con); 255 iounmap(gpf2con); 256 iounmap(gpf3con); 257 iounmap(gpd0con); 258 iounmap(gpd0dat); 259 iounmap(display_control); 260 iounmap(lcd_regs); 261 iounmap(vidw00add0b0); 262 iounmap(vidw00add1b0); 263 framebuffer_release(clb_fbinfo); 264 } 265 266 module_init(clb210_lcd_init); 267 module_exit(clb210_lcd_exit); 268 269 MODULE_LICENSE("GPL"); 270 MODULE_AUTHOR("clb"); 271 MODULE_DESCRIPTION("Lcd driver for clb210 board");
這份代碼沒有基於platform設備驅動來編寫,在內核源碼中的demo就是基於platform驅動模型來搭建的,主要內容其實一樣。
