十二、Linux驱动之LCD驱动
1. 基本概念
LCD是Liquid Crystal Display的简称,也就是经常所说的液晶显示器。LCD能够支持彩色图像的显示和视频的播放,是一种非常重要的输出设备。如果我们的系统要用GUI(图形界面接口),比如minigui,MicroWindows。这时LCD设备驱动程序就应该编写成frambuffer接口,而不是编写成仅仅操作底层的LCD控制器接口。
framebuffer是Linux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作。framebuffer又叫帧缓冲,是Linux为操作显示设备提供的一个用户接口。用户应用程序可以通过framebuffer透明地访问不同类型的显示设备。从这个方面来说,framebuffer是硬件设备显示缓冲区的抽象。Linux抽象出framebuffer这个帧缓冲区可以供用户应用程序直接读写,通过更改framebuffer中的内容,就可以立刻显示在LCD显示屏上。
framebuffer是一个标准的字符设备,主设备号是29,次设备号根据缓冲区的数目而定。framebuffer对应/dev/fb%d设备文件。根据显卡的多少,设备文件可能是/dev/fb0、/dev/fb1等。缓冲区设备也是一种普通的内存设备,可以直接对其进行读写。
对用户程序而言,它和/dev下面的其他设备没有什么区别,用户可以把frameBuffer看成一块内存,既可以写,又可以读。显示器将根据内存数据显示对应的图像界面。这一切都由LCD控制器和响应的驱动程序来完成。
2. 分析内核
2.1 驱动框架
LCD驱动也是一个字符设备驱动,那么内核中是如何实现的呢?框架如下图:
接下来我们便通过上图结构深入分析内核实现LCD驱动的过程。
2.2 fbmem.c(drivers/video中)
fbmem.c是frambuffer驱动的核心,他向上给应用程序提供了系统调用接口,向下对特定的硬件提供底层的驱动接口。底层驱动可以通过接口向内核注册自己。fbmem.c提供了frambuffer驱动的所有接口代码,从而避免了。
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_openif (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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include 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; //红色偏移值为11s3c_lcd->var.red.length = 5; //红色位长为5s3c_lcd->var.green.offset = 5; //绿色偏移值为5s3c_lcd->var.green.length = 6; //绿色位长为6s3c_lcd->var.blue.offset = 0; //蓝色偏移值为0s3c_lcd->var.blue.length = 5; //蓝色位长为5s3c_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 cleanrm -rf modules.orderobj-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 --->
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
4.3 测试
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); //释放缓存
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
