目录
title: lcd驱动框架
tags: linux date: 2018/12/3 15:43:23 toc: true ---lcd驱动框架
参考文档 鱼树笔记 韦老师2期视频
框图
LCD设备驱动程序应该编写成frambuffer
接口, frambuffer
设备层是对图像设备的一种抽象,它代表了视频硬件的帧缓存,使得应用程序通过定义好的接口就可以访问硬件。应用程序不需要考虑底层的(寄存器级)的操作。
这里的lcd
驱动框架,也可以理解是fb
总线下面挂接了lcd
设备,默认的是一种总线-平台-设备模型.
完整的程序流程图在
程序分析
入口
函数入口在drivers/video/fbmem.c
中的fbmem_init
static int __initfbmem_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;}
create_proc_read_entry
在/proc
下也会有fb
文件# cat /proc/fb0 s3c2410fb
register_chrdev
注册驱动告知内核,主设备号是FB_MAJOR=29
class_create
注册了一个类graphics
,具体的设备文件并不在这里创建# ls /sys/class/graphics/fb0 fbcon # 这个并不是在这里创建的
打开open
这里注册的是字符设备驱动,结构是默认的file_operations=fb_fops
,从open=fb_open
入手分析.
- 可以看到这里有一个新的结构
fb_info
,这个结构存储在以次设备号为索引的数组registered_fb[]
中,这里次设备号最大为32
,应该就是支持最多32个fb
设备了,这里的fb_info
应该就是管理结构了. - 也就是根据次设备号在
registered_fb
中寻找对应的fb_info
中的fb_ops
中的open
- 注册了一个字符设备驱动
fb_fops
结构,open
=fb_fops.open > registered_fb[次设备号].fb_ops.open
static intfb_open(struct inode *inode, struct file *file){ int fbidx = iminor(inode); struct fb_info *info; //这个是fb信息结构 int res = 0; if (fbidx >= FB_MAX) return -ENODEV; if (!(info = registered_fb[fbidx])) return -ENODEV; if (!try_module_get(info->fbops->owner)) return -ENODEV; file->private_data = info; 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;}
读read
已经抽象出read
的算法部分,根据lcd
的参数读取具体的frambuf
static ssize_tfb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; // 从全局数组中按照次设备号获取具体的 fb_info 结构 struct inode *inode = file->f_path.dentry->d_inode; int fbidx = iminor(inode); //调用 registered_fb[次设备号].fb_read struct fb_info *info = registered_fb[fbidx]; u32 *buffer, *dst; u32 __iomem *src; int c, i, cnt = 0, err = 0; unsigned long total_size; if (!info || ! info->screen_base) return -ENODEV; if (info->state != FBINFO_STATE_RUNNING) return -EPERM; // 如果自定义了驱动层的read,则调用自定义的read,否则执行默认的 if (info->fbops->fb_read) return info->fbops->fb_read(info, buf, count, ppos); //total_size = 屏幕大小 total_size = info->screen_size; if (total_size == 0) total_size = info->fix.smem_len; if (p >= total_size) return 0; if (count >= total_size) count = total_size; if (count + p > total_size) count = total_size - p; //分配大小,如果读取的大小大于页面大小则读取页面大小,否则读取指定大小,也就是从 页面大小和指定大小中取小值 buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count, GFP_KERNEL); if (!buffer) return -ENOMEM; //获得需要读取的地址=基地址+offset src = (u32 __iomem *) (info->screen_base + p); //这个应该用于等待同步如果有自定义的话 if (info->fbops->fb_sync) info->fbops->fb_sync(info); //读取数据到buf while (count) { c = (count > PAGE_SIZE) ? PAGE_SIZE : count; dst = buffer; for (i = c >> 2; i--; ) *dst++ = fb_readl(src++); if (c & 3) { u8 *dst8 = (u8 *) dst; u8 __iomem *src8 = (u8 __iomem *) src; for (i = c & 3; i--;) *dst8++ = fb_readb(src8++); src = (u32 __iomem *) src8; } if (copy_to_user(buf, buffer, c)) { err = -EFAULT; break; } *ppos += c; buf += c; cnt += c; count -= c; } kfree(buffer); return (err) ? err : cnt;}
初始化registered_fb
从上述可得知,具体的操作实际上是在registered_fb
这个全局数组中的,肯定有函数来注册初始化它.si
中搜索register
很快找到,这里会寻找到空的数组元素,并使用device_create
创建类下的设备
intregister_framebuffer(struct fb_info *fb_info){ int i; struct fb_event event; struct fb_videomode mode; // 寻找一个空的registered_fb[] ,可以发现fb_info->node 也就是索引也是设备号 if (num_registered_fb == FB_MAX) return -ENXIO; num_registered_fb++; for (i = 0 ; i < FB_MAX; i++) if (!registered_fb[i]) break; fb_info->node = i; // 在类下创建设备文件,名字为fb次设备号 fb_info->dev = device_create(fb_class, fb_info->device, MKDEV(FB_MAJOR, i), "fb%d", i); if (IS_ERR(fb_info->dev)) { /* Not fatal */ printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n", i, PTR_ERR(fb_info->dev)); fb_info->dev = NULL; } else fb_init_device(fb_info); if (fb_info->pixmap.addr == NULL) { fb_info->pixmap.addr = kmalloc(FBPIXMAPSIZE, GFP_KERNEL); if (fb_info->pixmap.addr) { fb_info->pixmap.size = FBPIXMAPSIZE; fb_info->pixmap.buf_align = 1; fb_info->pixmap.scan_align = 1; fb_info->pixmap.access_align = 32; fb_info->pixmap.flags = FB_PIXMAP_DEFAULT; } } fb_info->pixmap.offset = 0; if (!fb_info->pixmap.blit_x) fb_info->pixmap.blit_x = ~(u32)0; if (!fb_info->pixmap.blit_y) fb_info->pixmap.blit_y = ~(u32)0; if (!fb_info->modelist.prev || !fb_info->modelist.next) INIT_LIST_HEAD(&fb_info->modelist); fb_var_to_videomode(&mode, &fb_info->var); fb_add_videomode(&mode, &fb_info->modelist); registered_fb[i] = fb_info; event.info = fb_info; fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event); return 0;}
注册
搜索这个注册函数register_framebuffer
,可以看到在s3c2410fb_probe
中调用,再搜索则有如下,.看到了类型是platform_driver
,这就是平台platform
框架程序了,可以搜索"s3c2410-lcd"
来查找它的设备文件也就是来查看资源.s3c24xx_fb_set_platdata
可以为这个资源文件再分配私有数据s3c2410fb_mach_info
//drivers\video\s3c2410fb.cstatic struct platform_driver s3c2410fb_driver = { .probe = s3c2410fb_probe, .remove = s3c2410fb_remove, .suspend = s3c2410fb_suspend, .resume = s3c2410fb_resume, .driver = { .name = "s3c2410-lcd", .owner = THIS_MODULE, },};//arch\arm\plat-s3c24xx\devs.cstruct platform_device s3c_device_lcd = { .name = "s3c2410-lcd", .id = -1, .num_resources = ARRAY_SIZE(s3c_lcd_resource), .resource = s3c_lcd_resource, .dev = { .dma_mask = &s3c_device_lcd_dmamask, .coherent_dma_mask = 0xffffffffUL }};void __init s3c24xx_fb_set_platdata(struct s3c2410fb_mach_info *pd){ struct s3c2410fb_mach_info *npd; npd = kmalloc(sizeof(*npd), GFP_KERNEL); if (npd) { memcpy(npd, pd, sizeof(*npd)); s3c_device_lcd.dev.platform_data = npd; } else { printk(KERN_ERR "no memory for LCD platform data\n"); }}
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); //1.分配一个fb_info结构体 if (!fbinfo) { return -ENOMEM; } /*2.设置fb_info*/ info = fbinfo->par; info->fb = fbinfo; info->dev = &pdev->dev; ... ... /*3.硬件相关的操作,设置中断,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); //4.注册一个fb_info结构体 if (ret < 0) { printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret); goto free_video_memory; }... ...return ret;}
小结
也就是说,fbmem.c
已经抽象出读写的操作,能够根据提供的基地址,页面大小来读写内部ram
类型frambuf
,我们需要写驱动来操作硬件,并告知其具体lcd的信息
程序设计
参照drivers\video\s3c2410fb.c
来设计这个fb
总线下的platform
平台驱动,我们这里不使用platform
设计,而是直接写驱动.参考s3c2410fb_probe
来进行初始化设置
- 入口
- 分配一个
fb_info
,使用framebuffer_alloc
,具体的参数设置可以参考s3c2410fb_probe
,其中mach_info
在bast_init>s3c24xx_fb_set_platdata(&bast_lcd_info)
- 设置固定的参数f
b_info-> fix
- 设置可变的参数
fb_info-> var
- 设置具体的文件操作指针
fb_info->fbops
- 设置固定的参数f
设置
GPIO
引脚分配显存
fb_info>screen_base
,这里使用dma_alloc_writecombine
,这里注意函数返回值是虚拟地址,有个参数*handle
返回实际物理地址,这个物理地址需要设置到lcd
的寄存器void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA缓存区给显存//返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,则需要使用dma_free_writecombine()释放内存,避免内存泄漏//参数如下: //*dev:指针,这里填0,表示这个申请的缓冲区里没有内容//size:分配的地址大小(字节单位)//*handle:申请到的物理起始地址//gfp:分配出来的内存参数,标志定义在
,常用标志如下: //GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠. //GFP_KERNEL 内核内存的正常分配. 可能睡眠. //GFP_USER 用来为用户空间页来分配内存; 它可能睡眠. 注册
fb_info
结构体,register_framebuffer
- 出口
- 卸载内核的
fb_info
,unregister_framebuffer
- 释放申请的显存
dma_free_writecombine
- 寄存器操作以及
ioremap
- 释放
fb_info
的内存framebuffer_release
fb_ops
结构注册,这里需要我们实现设置调色板的功能static struct fb_ops my_lcdfb_ops = { .owner = THIS_MODULE, .fb_setcolreg = my_lcdfb_setcolreg,//调用my_lcdfb_setcolreg()函数,来设置调色板fb_info-> pseudo_palette .fb_fillrect = cfb_fillrect, //填充矩形 .fb_copyarea = cfb_copyarea, //复制数据 .fb_imageblit = cfb_imageblit, //绘画图形,};//这里颜色表示为565 red green blue my_lcd->var.red.offset = 11; //红色的最低bit my_lcd->var.red.length = 5; //红色的长度 my_lcd->var.green.offset = 5; my_lcd->var.green.length = 6; my_lcd->var.blue.offset = 0; my_lcd->var.blue.length = 5;//填充颜色到16位数据中static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf){/*内核中的单色都是16位,默认从左到右排列,比如G颜色[0x1f],那么chan就等于0XF800*/ chan &= 0xffff; chan >>= 16 - bf->length; //右移,将数据靠到位0上 return chan << bf->offset; //左移一定偏移值,放入16色数据中对应的位置}static int my_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) //调色板数组不能大于15 return 1; /* 用red,green,blue三个颜色值构造出16色数据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; //放到调色板数组中 return 0;}
测试
去掉内核的
2410lcd
模块Device Drivers>Graphics support
编译为模块(M
选项),为了使用以下三个static struct fb_ops my_lcdfb_ops = {... .fb_fillrect = cfb_fillrect, //填充矩形 .fb_copyarea = cfb_copyarea, //复制数据 .fb_imageblit = cfb_imageblit, //绘画图形,};
make uImage
make modules
得到需要的模块/drivers/videoinsmod cfbcopyarea.ko && insmod cfbfillrect.ko && insmod cfbimgblt.koinsmod cfbfillrect.koinsmod cfbimgblt.ko
uboot
使用nfs
烧写指定内核nfs 0x30008000 192.168.137.222:/work/nfs_root/u-boot.bin
启动
bootm 30000000
方式一操作fb0
使用cat lcd.ko>/dev/fb0
这个直接写设备文件,可以看到lcd花屏
方式二操作tty
显示文件,这里第一次测试需要使用自带的那个qt
的文件系统,但是我做完方式三后发现精简的文件系统也可以,后来重试了也可以,没明白为什么
echo 123> /dev/tty1 cat Makefile>/dev/tty1
方式三操作终端
使用
lcd
显示sh
,修改/etc/inittab
,添加tty1::askfirst:-/bin/sh
,重启后加载lcd
驱动,当加载lcd.ko
就能发现 LCD 上有提示输入回车激活终端mount -o nolock,rsize=1024,wsize=1024 172.16.45.222:/home/book/stu /mnt insmod cfbcopyarea.ko && insmod cfbfillrect.ko && insmod cfbimgblt.ko insmod lcd.ko
再使用输入子系统中的按键驱动
insmod button.ko
,按下按键ent11
也就是代表enter
会激活 LCD 的终端这个时候可以使用按键输入
ls
,激活命令在同时在串口中输入
ps
可以看到有两个sh
767 0 3096 S -sh 768 0 3096 S -sh
查看
sh
对应的文件shell # ls /proc/768/fd -l lrwx------ 1 0 0 64 Jan 1 00:48 0 -> /dev/tty1 lrwx------ 1 0 0 64 Jan 1 00:48 1 -> /dev/tty1 lrwx------ 1 0 0 64 Jan 1 00:48 10 -> /dev/tty lrwx------ 1 0 0 64 Jan 1 00:48 2 -> /dev/tty1
完整程序
#include#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*LCD : 480*272 */#define LCD_xres 480 //LCD 行分辨率#define LCD_yres 272 //LCD列分辨率 /* GPIO prot */static unsigned long *GPBcon;static unsigned long *GPCcon;static unsigned long *GPDcon;static unsigned long *GPGcon; //GPG4:控制LCD信号 static unsigned long *GPBdat; //GPB0: 控制背光/* LCD control */ struct lcd_reg{ 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 tconsel; };static struct lcd_reg *lcd_reg; static struct fb_info *my_lcd; //定义一个全局变量static u32 pseudo_palette[16]; //调色板数组,被fb_info->pseudo_palette调用static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf){/*内核中的单色都是16位,默认从左到右排列,比如G颜色[0x1f],那么chan就等于0XF800*/ chan &= 0xffff; chan >>= 16 - bf->length; //右移,将数据靠到位0上 return chan << bf->offset; //左移一定偏移值,放入16色数据中对应的位置}static int my_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) //调色板数组不能大于15 return 1; /* 用red,green,blue三个颜色值构造出16色数据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; //放到调色板数组中 return 0;}static struct fb_ops my_lcdfb_ops = { .owner = THIS_MODULE, .fb_setcolreg = my_lcdfb_setcolreg,//调用my_lcdfb_setcolreg()函数,来设置调色板fb_info-> pseudo_palette .fb_fillrect = cfb_fillrect, //填充矩形 .fb_copyarea = cfb_copyarea, //复制数据 .fb_imageblit = cfb_imageblit, //绘画图形,};static int lcd_init(void){ /*1.申请一个fb_info结构体*/ my_lcd= framebuffer_alloc(0,0); /*2.设置fb_info*/ /* 2.1设置固定的参数fb_info-> fix */ /*my_lcd->fix.smem_start 物理地址后面注册MDA缓存区设置*/ strcpy(my_lcd->fix.id, "mylcd"); //名字 my_lcd->fix.smem_len =LCD_xres*LCD_yres*2; //地址长 my_lcd->fix.type =FB_TYPE_PACKED_PIXELS; my_lcd->fix.visual =FB_VISUAL_TRUECOLOR; //真彩色 my_lcd->fix.line_length =LCD_xres*2; //LCD 一行的字节 /* 2.2 设置可变的参数fb_info-> var */ my_lcd->var.xres =LCD_xres; //可见屏X 分辨率 my_lcd->var.yres =LCD_yres; //可见屏y 分辨率 my_lcd->var.xres_virtual =LCD_xres; //虚拟屏x分辨率 my_lcd->var.yres_virtual =LCD_yres; //虚拟屏y分辨率 my_lcd->var.xoffset = 0; //虚拟到可见屏幕之间的行偏移 my_lcd->var.yoffset =0; //虚拟到可见屏幕之间的行偏移 my_lcd->var.bits_per_pixel=16; //像素为16BPP my_lcd->var.grayscale = 0; //灰色比例 my_lcd->var.red.offset = 11; my_lcd->var.red.length = 5; my_lcd->var.green.offset = 5; my_lcd->var.green.length = 6; my_lcd->var.blue.offset = 0; my_lcd->var.blue.length = 5;/* 2.3 设置操作函数fb_info-> fbops */ my_lcd->fbops = &my_lcdfb_ops; /* 2.4 设置fb_info 其它的成员 */ /*my_lcd->screen_base 虚拟地址在后面注册MDA缓存区设置*/ my_lcd->pseudo_palette =pseudo_palette; //保存调色板数组 my_lcd->screen_size =LCD_xres * LCD_yres *2; //虚拟地址长 /*3 设置硬件相关的操作*/ /*3.1 配置LCD引脚*/ GPBcon = ioremap(0x56000010, 8); GPBdat = GPBcon+1; GPCcon = ioremap(0x56000020, 4); GPDcon = ioremap(0x56000030, 4); GPGcon = ioremap(0x56000060, 4); *GPBcon &=~(0x03<<(0*2)); *GPBcon |= (0x01<<(0*2)); //PGB0背光 *GPBdat &=~(0X1<<0); //关背光 *GPCcon =0xaaaaaaaa; *GPDcon =0xaaaaaaaa; *GPGcon |=(0x03<<(4*2)); //GPG4:LCD信号 /*3.2 根据LCD手册设置LCD控制器,参考之前的裸机驱动*/ lcd_reg=ioremap(0X4D000000, sizeof( lcd_reg) ); /*HCLK:100Mhz */ lcd_reg->lcdcon1 = (4<<8) | (0X3<<5) | (0x0C<<1) ; lcd_reg->lcdcon2 = ((3)<<24) | (271<<14) | ((1)<<6) |((0)<<0); lcd_reg->lcdcon3 = ((16)<<19) | (479<<8) | ((10)); lcd_reg->lcdcon4 = (4); lcd_reg->lcdcon5 = (1<<11) | (1<<9) | (1<<8) |(1<<0); lcd_reg->lcdcon1 &=~(1<<0); // 关闭PWREN信号输出 lcd_reg->lcdcon5 &=~(1<<3); //禁止PWREN信号 /* 3.3 分配显存(framebuffer),把地址告诉LCD控制器和fb_info*/ my_lcd->screen_base=dma_alloc_writecombine(0,my_lcd->fix.smem_len, &my_lcd->fix.smem_start, GFP_KERNEL); /*lcd控制器的地址必须是物理地址*/ lcd_reg->lcdsaddr1 =(my_lcd->fix.smem_start>>1)&0X3FFFFFFF; //保存缓冲起始地址A[30:1] lcd_reg->lcdsaddr2 =((my_lcd->fix.smem_start+my_lcd->screen_size)>>1)&0X1FFFFF; //保存存缓冲结束地址A[21:1] lcd_reg->lcdsaddr3 =LCD_xres& 0x3ff; //OFFSIZE[21:11]:保存LCD上一行结尾和下一行开头的地址之间的差 //PAGEWIDTH [10:0]:保存LCD一行占的宽度(半字数为单位) /*4开启LCD,并注册fb_info: register_framebuffer()*/ /*4.1 直接在init函数中开启LCD(后面讲到电源管理,再来优化)*/ lcd_reg->lcdcon1 |=1<<0; //输出PWREN信号 lcd_reg->lcdcon5 |=1<<3; //允许PWREN信号 *GPBdat |=(0X1<<0); //开背光 /*4.2 注册fb_info*/ register_framebuffer(my_lcd); return 0;}static int lcd_exit(void){ /* 1卸载内核中的fb_info*/ unregister_framebuffer(my_lcd); /*2 控制LCDCON1关闭PWREN信号,关背光,iounmap注销地址*/ lcd_reg->lcdcon1 &=~(1<<0); // 关闭PWREN信号输出 lcd_reg->lcdcon5 &=~(1<<3); //禁止PWREN信号 *GPBdat &=~(0X1<<4); //关背光 iounmap(GPBcon); iounmap(GPCcon); iounmap(GPDcon); iounmap(GPGcon); /*3.释放DMA缓存地址dma_free_writecombine()*/ dma_free_writecombine(0,my_lcd->screen_size,my_lcd->screen_base,my_lcd->fix.smem_start); /*4.释放注册的fb_info*/ framebuffer_release(my_lcd); return 0;}module_init(lcd_init);module_exit(lcd_exit);MODULE_LICENSE("GPL");