Linux:LCD驱动开发

时间:2024-10-03 07:15:55

目录

1.不同接口的LCD硬件操作原理

应用工程师眼中看到的LCD

1.1像素的颜色怎么表示

​编辑 1.2怎么把颜色发给LCD

 驱动工程师眼中看到的LCD

统一的LCD硬件模型

8080接口

TFTRGB接口

什么是MIPI

Framebuffer驱动程序框架

 怎么编写Framebuffer驱动框架

硬件LCD时序分析

8080接口LCD

 TFTRGB接口LCD

IMX6ULL中的LCD控制器

一、LCD接口概述

 二、external Signal

 三、时钟

四、功能描述

单buffer和双biffer

使用双buffer


1.不同接口的LCD硬件操作原理

应用工程师眼中看到的LCD

LCD由一个一个像素组成:每行由xres个像素,有yres行,他的分辨率是:xres*yres.

只要我们能控制任意一个像素的颜色,就可以在LCD上绘制图片,文字

1.1像素的颜色怎么表示

用红绿蓝三种颜色来表示,可以用24位数据来表示红绿蓝,也可以用16位等等格式。比如:

  • bpp : bits per pixel ,每个像素用多少位来表示
  • 24bpp:实际上会用到32位,其中8位未使用,其余24位中分别用8位来表示红(R),绿(G),蓝(B)
  • 16bpp:有rbg565,rbg555
  1. rbg565:用5位表示红,用6位表示绿,用5位表示蓝
  2. rbg555:16位数据中用5位表示红,5位表示绿,5位表示蓝,浪费一位

 1.2怎么把颜色发给LCD

假设每个像素的颜色用16位来表示,那么一个LCD的所有像素点假设有xres*yres个,

需要的内存为:xres * yres *16/8 ,也就是要设置所有像素的颜色,需要这么大的内存

这块内存就被称为framebuffer:

  • Framebuffer中每块数据对应一个像素
  • 每块数据的大小可能是16位,32位,这跟LCD上的像素的颜色格式有关
  • 设置好LCD硬件后,只需要把颜色数据写入到Framebuffer即可

 驱动工程师眼中看到的LCD

驱动工程师对LCD的理解要深入硬件,必须要回答这几个问题:

  • Framebuffer在哪里
  • 谁把Framebuffer中的数据发给LCD

统一的LCD硬件模型

第一种情况:将显存,LCD控制器,LCD屏幕组成一个模组(LCM)

第二种情况:将LCD控制器,显存与LCD屏幕分隔开,LCD控制器是位于ARM芯片内部的,ARM                       芯片外接内存,从内存中分配出一块空间供显存使用

 第一种情况一般用于单片机MCU(F103):这种接口称为8080

第二种情况一般用于能运行Linux的高性能的芯片MPU:这种接口一般被称为TFTRGB接口

这两种接口是不一样的,但整个模块的原理是一样的

8080接口

用这种接口的单片机一般性能比较弱, 所以外接的模块用内存的接口最好,我们在访问内存时,一般用数据线访问(读信号,写信号,地址线,数据线,片选信号),这样的话需要的IO口太多了,如何进行精简呢?数据线不可少,所以只能精简地址线,可以通过将地址信号转换为数据通过数据线发送,但是怎么分辨发送的是数据还是地址呢,通过引出一个引脚来分辨(data/cmd)

这种显存在屏幕上,显存一般是SRAM,它比较贵,所以一般的 8080屏幕分辨率不高

要使用更高分辨率的屏幕,就可以用以下接口的方式,这种方式内存接口比较便宜

TFTRGB接口

LCD控制器在ARM芯片内部,它会自动获取Framebuffer中的数据,那它怎么将数据发送到LCD屏幕上边呢:

1.移动像素:DCLK

2.从最后跳到下一行:HSYNC(每来一个脉冲就执行一次)

3.跳回一帧的开始(从最后一个像素段跳到第一个像素点):VSYNC 

4.数据来源:RGB三组线(24条线)

5.当数据从最后跳到下一行和从会后一个像素点跳到第一个像素点的时候数据是无效的,所以使用:DE(来决定数据是否有效)

什么是MIPI

 实际上上边这两种接口的实质是一样的,这两种接口都可以归入一个标准:MIPI标准

MIPI表示“Mobile Industry Proc8080essor Interface”,即移动产业处理器接口,是MIPI联盟发起的为移动应用处理器制定的开放标准和一个规范。主要是手机内部的接口(摄像头,显示器接口,射频/基带接口)等标准化,从而减少手机内部接口的复杂程度及增加设计的灵活性

对于LCD,MIPI可以分为3类:

MIPI-DBI(Display Bus Interface)

既可以是Bus(总线),就是既能发送命令,常用的8080接口就是属于DBI接口

MIPI-DPI(Display Pixel Interface)

Pixel(像素),强调的是操作单个像素,在MPU上的LCD控制器就是这种接口 

MIPI-DSI(Display Serial Interface)

Serial,相比于DBI,DPI需要使用很多接口线,DSI需要的接口线大为减少 

Framebuffer驱动程序框架

它再怎么复杂,还是一个字符驱动程序

LCD驱动程序分为

1.上层比较通用性的代码(框架):fbmem.c

2.硬件驱动:对于不同的芯片还有不同的硬件相关的文件:s3c2410fb.c   STM32MP157_fb.c

调用关系:

示例1:
app: open("/dev/fb0", ...)  主设备号:29  , 次设备号: 0
---------------------------------------------------------------------------
kernel:
        fb_open
            int fbidx = iminor(inode);
            struct fb_info *info = = registered_fb[0];




示例2:
app:    read()
----------------------------------------------------------------------------
kernel:
        fb_read
            int fbidx = iminor(inode);
            struct fb_info *info = registered_fb[fbidx];
            if(info -> fbops -> fb_read)
                return info -> fbops -> fb_read(info, buf count ppos);
                
            src = (u32 __inomem *) (info -> screen_base + p);
            dst = buffer;
            *dst++ == fb_readl(src++);
            copy_to_user(buf, buffer, c);

 怎么编写Framebuffer驱动框架

核心:


struct fb_info {
	atomic_t count;
	int node;
	int flags;
	struct mutex lock;		/* Lock for open/release/ioctl funcs */
	struct mutex mm_lock;		/* Lock for fb_mmap and smem_* fields */
	struct fb_var_screeninfo var;	/* Current var */
	struct fb_fix_screeninfo fix;	/* Current fix */
	struct fb_monspecs monspecs;	/* Current Monitor specs */
	struct work_struct queue;	/* Framebuffer event queue */
	struct fb_pixmap pixmap;	/* Image hardware mapper */
	struct fb_pixmap sprite;	/* Cursor hardware mapper */
	struct fb_cmap cmap;		/* Current cmap */
	struct list_head modelist;      /* mode list */
	struct fb_videomode *mode;	/* current mode */

#ifdef CONFIG_FB_BACKLIGHT
	/* assigned backlight device */
	/* set before framebuffer registration, 
	   remove after unregister */
	struct backlight_device *bl_dev;

	/* Backlight level curve */
	struct mutex bl_curve_mutex;	
	u8 bl_curve[FB_BACKLIGHT_LEVELS];
#endif
#ifdef CONFIG_FB_DEFERRED_IO
	struct delayed_work deferred_work;
	struct fb_deferred_io *fbdefio;
#endif

	struct fb_ops *fbops;
	struct device *device;		/* This is the parent */
	struct device *dev;		/* This is this fb device */
	int class_flag;                    /* private sysfs flags */
#ifdef CONFIG_FB_TILEBLITTING
	struct fb_tile_ops *tileops;    /* Tile Blitting */
#endif
	union {
		char __iomem *screen_base;	/* Virtual address */
		char *screen_buffer;
	};
	unsigned long screen_size;	/* Amount of ioremapped VRAM or 0 */ 
	void *pseudo_palette;		/* Fake palette of 16 colors */ 
#define FBINFO_STATE_RUNNING	0
#define FBINFO_STATE_SUSPENDED	1
	u32 state;			/* Hardware state i.e suspend */
	void *fbcon_par;                /* fbcon use-only private area */
	/* From here on everything is device dependent */
	void *par;
	/* we need the PCI or similar aperture base/size not
	   smem_start/size as smem_start may just be an object
	   allocated inside the aperture so may not actually overlap */
	struct apertures_struct {
		unsigned int count;
		struct aperture {
			resource_size_t base;
			resource_size_t size;
		} ranges[0];
	} *apertures;

	bool skip_vt_switch; /* no VT switch on suspend/resume required */
};

1.分配fb_info:framebuffer_alloc

2.设置fb_info:var   ,    fbops       ,硬件相关操作

3.注册fb_info:register_framebuffer

4.进行硬件相关的操作

代码如下:

#include <linux/module.h>

#include <linux/compat.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/mman.h>
#include <linux/vt.h>
#include <linux/init.h>
#include <linux/linux_logo.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/console.h>
#include <linux/kmod.h>
#include <linux/err.h>
#include <linux/device.h>
#include <linux/efi.h>
#include <linux/fb.h>

#include <asm/fb.h>

static strcut fb_info *myfb_info;

static struct fb_ops myfb_ops = {
	.owner = THIS_MODULE,
	.fb_fillrect = cfb_fillrect,
	.fb_copyarea = cfb_copyarea,
	.fb_imageblit = cfb_imageblit,
};

/*入口函数*/
static int __init lcd_drv_init(void)
{
	dma_addr_t phy_addr;
	
	/*分配fb_info*/
	myfb_info = framebuffer_alloc(0,NULL);
	
	/*设置fb_info*/
	/*1.var: LCD的分辨率,颜色格式等*/
	myfb_info -> var.xres = 1024;
	myfb_info -> var.yres = 600;
	myfb_info -> var.bits_per_pixel = 16;
	
	myfb_info -> var.red.offset = 11;
	myfb_info -> var.red.length = 5;
	
	myfb_info -> var.green.offset = 5;
	myfb_info -> var.green.length = 6;

	myfb_info -> var.blue.offset = 0;
	myfb_info -> var.blue.length = 5;
	/*2.fix*/
	myfb_info -> fiX.smem_len = myfb_info -> var.xres * myfb_info -> var.yres * 4;
	if(myfb_info ->var.bits_per_pixel == 24)
	{
		myfb_inco -> fix.smem_len = myfb_info -> var.xres * myfb_info -> var.yres * myfb_info -> var.bits_per_pixel / 8
	}
	//framebuffer的虚拟地址
	myfb_info -> screen_base = dma_alloc_wc(NULL,myfb_info -> fix.smem_len, &phy_addr, GFP_KERNEL);
	myfb_info -> fix.smem_start = phy_addr;  //framebuffer的物理地址
	
	myfb_info -> fix.type = FB_TYPE_PACKED_PIXELS;
	myfb_info -> fix.visual = FB_VISUAL_TRUECOLOR;
	
	/*3.fbops*/
	myfb_info->fbops = &myfb_ops;
	
	/*注册fb_info*/
	register_framebuffer(myfb_info);

	/*硬件操作*/
	
	
	return 0;
}

/*出口函数*/
static void __exit lcd_drv_exit(void)
{
	/*反过来操作*/
	
	/*反注册fb_info*/
	unregister_framebuffer(myfb_info);

	/*释放fb_info*/
	framebuffer_release(myfb_info);
	
}


module_init(lcd_drv_init);
module_exit(lcd_drv_exit);
MODULE_AUTHOR("zt");
MODULE_DESCRIPTION("Framebuffer driver for the linux");
MODULE_LICENSE("GPL");

对于硬件相关的操作:

这里使用QEMU中简单的四个寄存器

struct lcd_regs{
    volatile unsigned int fb_base_phys;   //这个寄存器存放的是显存的物理地址
    volatile unsigned int fb_xres;             //这个寄存器存放的是X像素
    volatile unsigned int fb_yres;             //这个寄存器存放的是Y像素
    volatile unsigned int fb_bpp;              //这个寄存器存放的是颜色的位数
};

static struct lcd_regs *mylcd_regs;

/*硬件操作*/

    //因为驱动不能直接操作物理地址所以需要将物理地址映射为虚拟地址
    mylcd_regs = ioremap(0x021C8000,sizeof(struct lcd_regs));
    //地址寄存器存放的是之前myfb_info分配的物理地址(也就是显存的物理地址)
    mylcd_regs->fb_base_phys = phy_addr;
    mylcd_regs->fb_xres = 500;
    mylcd_regs->fb_yres = 300;
    mylcd_regs->fb_bpp  = 16;

硬件LCD时序分析

8080接口LCD

8080接口其实就是一般的内存接口

显存,LCD控制器,LCD屏幕放在是一块的(LCM)

1.接口原理图

数据线,读信号,写信号,地址/数据引脚(当这个引脚为高电平时,传输的为数据,为低电平时,传输的是地址),片选引脚(在Linux中内存空间都是分开的,当使用其中一段内存空间的时候,对应的片选引脚会自动有效)

 当位于第一块内存区间时,片选7自动有效,当位于第二块内存区间时,片选6自动有效

其引脚图如下:

 TFTRGB接口LCD

IMX6ULL中的LCD控制器

下面只是简单介绍一下,具体请看这篇文章:LCD控制器

 它是TFTRGB的接口,显存和LCD控制器在IMX6Ull中所以主要就是LCD控制器与LCD屏幕进行互动

一、LCD接口概述

IMX6ULL的控制器名称为eLCDIF(增强型LCD接口)主要特性如下:

1.支持MPU模式:有些显示屏自带显存,只需要把命令、数据发送给显示屏就可以

2.支持DOTCLK模式:RGB接口,本板子就是此模式

3.VSYNC模式:针对高速数据传输(行场信号)

4.支持ITU-R BT.656接口,可以把4:2:2YcbCr格式的数据转换为模拟电视信号

5.8/16/18/24/32 bit的bpp数据都支持,取决于IO的复用设置及寄存器配置

6.MPU模式,VSYNC模式,DOTCLK模式,都可以配置时序参数

 二、external Signal

 三、时钟

 LCD控制器有两个时钟域:外设总线时钟域,LCD像素时钟域。前者是用来让LCD控制器来正常工作,后者是用来控制电子枪移动

四、功能描述

 我们在内存中划出一块内存,称之为显存,软件把数据写入到显存中

设置好LCD控制器之后,它会通过AXI总线协议从显存把RGB数据读入FIFO,再到达LCD接口(LCD Interface)。上图的Read_Data操作,在MPU模式下才用到。

单buffer和双biffer

首先要知道为什么要区分出单Buffer和双buffer

因为在IMX6ULL中,显存和LCD控制器都是在屏幕外的,当使用单buffer时,因为应用层和LCD控制器都是操作的一块显存(framebuffer),如果应用层写入数据时过快,LCD控制器还没有将显存中的数据写到屏幕中,显存有刷新了,此时就会出现问题。

使用双buffer甚至是多buffer可以有效的解决这个问题,因为应用层和LCD控制器操作的不是同一块显存,设有显存1和显存2(这里是在驱动层直接分配了两个显存大小的内存空间{实际上在IMX6ULL中是直接分配了32Mb的内存空间,相当于13个显存,是非常豪横的!!!}),刚开始应用层先写显存1,LCD控制器读显存2的数据,再一次LCD控制器读显存1的数据,应用层向显存2写数据,这样往复操作,就不会出现单buffer时的情况

使用双buffer

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>

static int fd_fb;
static struct fb_fix_screeninfo fix;	/* Current fix */
static struct fb_var_screeninfo var;	/* Current var */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;

/**********************************************************************
 * 函数名称: lcd_put_pixel
 * 功能描述: 在LCD指定位置上输出指定颜色(描点),一个像素一个像素的放
 * 输入参数: x坐标,y坐标,颜色
 * 输出参数: 无
 * 返 回 值: 会
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 * 2020/05/12	     V1.0	  zh(angenao)	      创建
 ***********************************************************************/ 
void lcd_put_pixel(void *fb_base, int x, int y, unsigned int color)
{
	//
	unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (var.bits_per_pixel)
	{
		case 8:
		{
			*pen_8 = color;
			break;
		}
		case 16:
		{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:
		{
			*pen_32 = color;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}

void lcd_draw_screen(void *fb_base, unsigned int color)
{
	int x, y;
	for (x = 0; x < var.xres; x++)
		for (y = 0; y < var.yres; y++)
			lcd_put_pixel(fb_base, x, y, color);
}


/* ./multi_framebuffer_test single
 * ./multi_framebuffer_test double
 */
int main(int argc, char **argv)
{
	int i;
	int ret;
	//这个参数是记录的
	int nBuffers;
	int nNextBuffer = 1;
	char *pNextBuffer;
	unsigned int colors[] = {0x00FF0000, 0x0000FF00, 0x000000FF, 0, 0x00FFFFFF};  /* 0x00RRGGBB */
	struct timespec time;

	time.tv_sec  = 0;
	time.tv_nsec = 100000000;

	if (argc != 2)
	{
		printf("Usage : %s <single|double>\n", argv[0]);
		return -1;
	}
	
	fd_fb = open("/dev/fb0", O_RDWR);
	if (fd_fb < 0)
	{
		printf("can't open /dev/fb0\n");
		return -1;
	}

	if (ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix))
	{
		printf("can't get fix\n");
		return -1;
	}
	
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
	{
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8;
	pixel_width = var.bits_per_pixel / 8;
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;

	nBuffers = fix.smem_len / screen_size;
	printf("nBuffers = %d\n", nBuffers);
	
	fb_base = (unsigned char *)mmap(NULL , fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1)
	{
		printf("can't mmap\n");
		return -1;
	}

	if ((argv[1][0] == 's') || (nBuffers == 1))
	{
		while (1)
		{
			/* use single buffer */
			for (i = 0; i < sizeof(colors)/sizeof(colors[0]); i++)
			{
				lcd_draw_screen(fb_base, colors[i]);
				nanosleep(&time, NULL);
			}
		}
	}
	else
	{
		/* use double buffer */
		/* a. enable use multi buffers */
		var.yres_virtual = nBuffers * var.yres;
		ioctl(fd_fb, FBIOPUT_VSCREENINFO, &var);

		while (1)
		{
			for (i = 0; i < sizeof(colors)/sizeof(colors[0]); i++)
			{
				/* get buffer */
				pNextBuffer =  fb_base + nNextBuffer * screen_size;

				/* set buffer */
				lcd_draw_screen(pNextBuffer, colors[i]);

				/* switch buffer */
				var.yoffset = nNextBuffer * var.yres;
				ioctl(fd_fb, FBIOPAN_DISPLAY, &var);

				ret = 0;
				ioctl(fd_fb, FBIO_WAITFORVSYNC, &ret);
				
				nNextBuffer = !nNextBuffer;
				nanosleep(&time, NULL);
			}
		}
		
	}
	
	munmap(fb_base , screen_size);
	close(fd_fb);
	
	return 0;	
}


当输入single时使用单buffer,当输入double时使用双buffer,经过对比会发现明显的差异,使用双buffer的刷屏会更加流畅,并且不会出现颜色覆盖的情况。