十二、Linux驱动之LCD驱动

1. 基本概念

    LCDLiquid Crystal Display的简称,也就是经常所说的液晶显示器。LCD能够支持彩色图像的显示和视频的播放,是一种非常重要的输出设备。如果我们的系统要用GUI(图形界面接口),比如minigui,MicroWindows。这时LCD设备驱动程序就应该编写成frambuffer接口,而不是编写成仅仅操作底层的LCD控制器接口。
    framebufferLinux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作。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.cframbuffer驱动的核心,他向上给应用程序提供了系统调用接口,向下对特定的硬件提供底层的驱动接口。底层驱动可以通过接口向内核注册自己。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_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);   //释放缓存

猜你喜欢

转载自blog.csdn.net/qq_36576792/article/details/84580767