嵌入式Linux驱动笔记(二十六)------framebuffer之使用spi-tft屏幕(下)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Guet_Kite/article/details/90760669

你好!这里是风筝的博客,

欢迎和我一起交流。


上一篇文章,描述的是如何驱动spi的屏幕,嵌入式Linux驱动笔记(二十四)------framebuffer之使用spi-tft屏幕(上)
但是是使用的是在内核里开一个线程来不停的绘制图形,CPU占用率非常高,效率低。
有种较为方便的办法,就是局部刷新,每次只重绘“脏区”即可。
参考了github里几个9225芯片的驱动,对本驱动进行了一些改进,主要是加入fb_deferred_io_init函数,建立延定时工作队列。

标准frambuffer操作api->mkdirty->fbtft_mkdirty()->schedule_delayed_work(&info->deferred_work, fbdefio->delay);->deferred_io()->fbtft_deferred_io()
故只要定期执行deferred_io()即可完成FPS

参考:ili9225.c

ili9225fb.c

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <sound/core.h>
#include <linux/spi/spi.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/vmalloc.h>
#include <linux/fb.h>
#include <linux/delay.h>

#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <asm/mach/map.h>
#include <linux/dma-mapping.h>


//#define USE_HORIZONTAL
//#define __DEBUG__ 1
//#define OPEN_FPS 1


#ifdef __DEBUG__
#define DEBUG(format,...) \
        printk("DEBUG::"format,  ##__VA_ARGS__)
#else
#define DEBUG(format,...)
#endif


#define LCD_X_SIZE          176
#define LCD_Y_SIZE          220

#ifdef USE_HORIZONTAL//如果定义了横屏
#define X_MAX_PIXEL         LCD_Y_SIZE
#define Y_MAX_PIXEL         LCD_X_SIZE
#else//竖屏
#define X_MAX_PIXEL         LCD_X_SIZE
#define Y_MAX_PIXEL         LCD_Y_SIZE
#endif

static struct timer_list frame_timer;

static int tft_lcdfb_setcolreg(unsigned int regno, unsigned int red,
			     unsigned int green, unsigned int blue,
			     unsigned int transp, struct fb_info *info);

struct tft_lcd{
    struct gpio_desc *reset_gpio;   
	struct gpio_desc *rs_gpio;
};

struct fb_page {
	unsigned short x;
	unsigned short y;
	u32 *buffer;//unsigned short
	unsigned short len;
};

struct tft_lcd_fb{
	struct spi_device *spi; //记录fb_info对象对应的spi设备对象
	struct fb_info *fbi;
	unsigned int pages_count;
	struct fb_page *pages;
	u32 *last_buffer_start;//unsigned short
	struct task_struct *thread; //记录线程对象的地址,此线程专用于把显存数据发送到屏的驱动ic
};

static struct fb_ops fops = {
	.owner		= THIS_MODULE,
	.fb_setcolreg	= tft_lcdfb_setcolreg,	/*设置颜色寄存器*/
	.fb_fillrect	= cfb_fillrect,			/*用像素行填充矩形框,通用库函数*/
	.fb_copyarea	= cfb_copyarea,			/*将屏幕的一个矩形区域复制到另一个区域,通用库函数*/
	.fb_imageblit	= cfb_imageblit,		/*显示一副图像,通用库函数*/
};

struct regdata_t{
        u8  reg;
        u16 data;
        int delay_ms;
}regdatas[] = {
    {0x10, 0x0000, 0}, {0x11, 0x0000, 0}, {0x12, 0x0000, 0},
    {0x13, 0x0000, 0}, {0x14, 0x0000, 40},

    {0x11, 0x0018, 0}, {0x12, 0x1121, 0}, {0x13, 0x0063, 0},
    {0x14, 0x3961, 0}, {0x10, 0x0800, 10}, {0x11, 0x1038, 30},

    {0x02, 0x0100, 0}, 
#ifdef USE_HORIZONTAL//如果定义了横屏
	{0x01, 0x001c, 0}, {0x03, 0x1038, 0},
#else//竖屏
	{0x01, 0x011c, 0}, {0x03, 0x1030, 0},
#endif

    {0x07, 0x0000, 0}, {0x08, 0x0808, 0}, {0x0b, 0x1100, 0},
    {0x0c, 0x0000, 0}, {0x0f, 0x0501, 0}, {0x15, 0x0020, 0},
    {0x20, 0x0000, 0}, {0x21, 0x0000, 0},

    {0x30, 0x0000}, {0x31, 0x00db}, {0x32, 0x0000}, {0x33, 0x0000},
    {0x34, 0x00db}, {0x35, 0x0000}, {0x36, 0x00af}, {0x37, 0x0000},
    {0x38, 0x00db}, {0x39, 0x0000},

    {0x50, 0x0603}, {0x51, 0x080d}, {0x52, 0x0d0c}, {0x53, 0x0205},
    {0x54, 0x040a}, {0x55, 0x0703}, {0x56, 0x0300}, {0x57, 0x0400},
    {0x58, 0x0b00}, {0x59, 0x0017},

    {0x0f, 0x0701}, {0x07, 0x0012, 50}, {0x07, 0x1017},
}; 

int frames = 0;
#ifdef 	OPEN_FPS
static void frames_timer_function(unsigned long data)
{
	printk("frames is %d fps\n",frames);
	frames = 0;
	//mod_timer(&frame_timer, jiffies+HZ/1000);
	mod_timer(&frame_timer, jiffies + (1*HZ));
}
#endif

static void Lcd_WriteIndex(struct spi_device *spi, u8 Index)
{
	struct tft_lcd *pdata = spi_get_drvdata(spi);

	gpiod_set_value(pdata->rs_gpio, 0); //高电平
	spi_write(spi, &Index, 1);
}

static void Lcd_WriteData_16Bit(struct spi_device *spi, u16 Data)
{   
	u8 buf[2];
	struct tft_lcd *pdata = spi_get_drvdata(spi);

	buf[0] = ((u8)(Data>>8));
	buf[1] = ((u8)(Data&0x00ff));

	gpiod_set_value(pdata->rs_gpio, 1); //高电平
	spi_write(spi, &buf[0], 1);
	spi_write(spi, &buf[1], 1);   
}

static void LCD_WriteReg(struct spi_device *spi, u8 Index, u16 Data)
{
	int addr;
	addr = Index;
    Lcd_WriteIndex(spi, addr);
    Lcd_WriteData_16Bit(spi, Data);
}

static void Lcd_SetRegion(struct spi_device *spi, u8 xStar, u8 yStar,u8 xEnd,u8 yEnd)
{
#ifdef USE_HORIZONTAL//如果定义了横屏 
		LCD_WriteReg(spi,0x38,xEnd);
		LCD_WriteReg(spi,0x39,xStar);
		LCD_WriteReg(spi,0x36,yEnd);
		LCD_WriteReg(spi,0x37,yStar);
		LCD_WriteReg(spi,0x21,xStar);
		LCD_WriteReg(spi,0x20,yStar);
#else//竖屏   
		LCD_WriteReg(spi,0x36,xEnd);
		LCD_WriteReg(spi,0x37,xStar);
		LCD_WriteReg(spi,0x38,yEnd);
		LCD_WriteReg(spi,0x39,yStar);
		LCD_WriteReg(spi,0x20,xStar);
		LCD_WriteReg(spi,0x21,yStar);
#endif
		Lcd_WriteIndex(spi,0x22);	

}

static int lcd_dt_parse(struct spi_device *spi, struct tft_lcd *lcd_data)
{

    lcd_data->reset_gpio = devm_gpiod_get(&spi->dev, "rest", GPIOD_OUT_HIGH);
    if (IS_ERR(lcd_data->reset_gpio))
        goto err0;
	gpio_direction_output(desc_to_gpio(lcd_data->reset_gpio), 1);

	lcd_data->rs_gpio = devm_gpiod_get(&spi->dev, "rs", GPIOD_OUT_HIGH);
    if (IS_ERR(lcd_data->rs_gpio))
        goto err1;
	gpio_direction_output(desc_to_gpio(lcd_data->rs_gpio), 1);
	
	return 0;

err1:
	devm_gpiod_put(&spi->dev, lcd_data->reset_gpio);
err0:
	DEBUG("[%s]:failed\n", __FUNCTION__);
	return -1;
}

static void lcd_init(struct spi_device *spi, struct tft_lcd *pdata)
{
	int i =0;
	gpiod_set_value(pdata->reset_gpio, 0); //设低电平
    msleep(100);
    gpiod_set_value(pdata->reset_gpio, 1); //设高电平
    msleep(50);

	for (i = 0; i < ARRAY_SIZE(regdatas); i++)
    {
        LCD_WriteReg(spi, regdatas[i].reg, regdatas[i].data);
        if (regdatas[i].delay_ms)
            msleep(regdatas[i].delay_ms);
    }

}

void show_fb(struct fb_info *fbi, struct spi_device *spi)
{
    int x, y;
    u32 k;
    u32 *p = (u32 *)(fbi->screen_base);
    u16 c;
    u8 *pp;

    //从屏的0,0坐标开始刷
    Lcd_SetRegion(spi, 0,0,X_MAX_PIXEL-1,Y_MAX_PIXEL-1);
 	DEBUG("[%s] \n",__FUNCTION__);
    for (y = 0; y < fbi->var.yres; y++)
    {
        for (x = 0; x < fbi->var.xres; x++)
        {
            k = p[y*fbi->var.xres+x];//取出一个像素点的32位数据
            // rgb8888 --> rgb565       
            pp = (u8 *)&k;
            c = pp[0] >> 3; //蓝色
            c |= (pp[1]>>2)<<5; //绿色
            c |= (pp[2]>>3)<<11; //红色

            //发出像素数据的rgb565
            //write_data(spi, c >> 8);
            //write_data(spi, c & 0xff);
			Lcd_WriteData_16Bit(spi, c);
        }
    }

}
static void fb_update(struct fb_info *fbi, struct list_head *pagelist);

int thread_func_fb(void *data)
{
    struct fb_info *fbi = (struct fb_info *)data;
    struct tft_lcd_fb *lcd_fb = fbi->par;

    while (1)
    {
		if (kthread_should_stop())
			break;
        show_fb(fbi, lcd_fb->spi);
        //fb_update(fbi, &fbi->fbdefio->pagelist);
        frames++;
    }

    return 0;
}

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 u32 pseudo_palette[16];
static int tft_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)
	{
		DEBUG("[%s] the regno is %d !!\n",__FUNCTION__, regno);
		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 void fb_copy(struct tft_lcd_fb *item, unsigned int index, struct spi_device *spi)
{
	unsigned short x;
	unsigned short y;
	u32 *buffer;
	unsigned int len;
	int i = 0;
	u32 k;
	u16 c;
    u8 *pp;
	//  unsigned int count;
	//unsigned int diff;
				 
	x = item->pages[index].x;
	y = item->pages[index].y;		 
	buffer = item->pages[index].buffer;
	len = item->pages[index].len;
				 
	// check if we should change the chip framebuffer pointer
	if (item->last_buffer_start!=buffer)
	{
		/*
		if (x!=0)	 // The ssd1963 needs to start from X=0 to keep drawing in next lines 
		{
			diff = x*(item->info->var.bits_per_pixel / 16);
			buffer-=diff;
			len+=diff;
		}
		*/
		//dev_dbg(item->dev, "%s: page[%u]: x=%3hu y=%3hu buffer=0x%p len=%3hu diff=%3hu\n",
		//		__func__, index, x, y, buffer, len,diff);

		Lcd_SetRegion(spi, x,y,X_MAX_PIXEL-1,Y_MAX_PIXEL-1);
	}
	
	for (i = 0; i < len; i++)
    {
		k = buffer[i];//取出一个像素点的32位数据
        // rgb8888 --> rgb565       
		pp = (u8 *)&k;
		c = pp[0] >> 3; //蓝色
		c |= (pp[1]>>2)<<5; //绿色
		c |= (pp[2]>>3)<<11; //红色

        //发出像素数据的rgb565
		Lcd_WriteData_16Bit(spi, c);
	}
	item->last_buffer_start=&buffer[len];
}

static void fb_update(struct fb_info *fbi, struct list_head *pagelist)
{
	struct tft_lcd_fb *lcd_fb = (struct tft_lcd_fb *)fbi->par;
	struct page *page;

	DEBUG("[%s]:fb_update\n", __FUNCTION__);
	list_for_each_entry(page, pagelist, lru) {
		DEBUG("[%s]:the pages x is %d, y is %d \n",__func__, lcd_fb->pages[page->index].x, lcd_fb->pages[page->index].y);
		fb_copy(lcd_fb, page->index, lcd_fb->spi);
	}
	frames++;
}

static struct fb_deferred_io fb_defio = {
	.delay 		 = HZ/20,
	.deferred_io	 = &fb_update,
};

static struct fb_var_screeninfo fb_var __initdata = {
    .xres 			= LCD_X_SIZE,
    .yres 			= LCD_Y_SIZE,
    .xres_virtual 	= LCD_X_SIZE,
    .yres_virtual 	= LCD_Y_SIZE,
    .bits_per_pixel = 32, // 屏是rgb565, 但QT程序只能支持32位.还需要在刷图时把32位的像素数据转换成rgb565
    .red.offset 	= 16,
    .red.length 	= 8,
    .green.offset 	= 8,
    .green.length 	= 8,
    .blue.offset 	= 0,
    .blue.length 	= 8,
	.activate       = FB_ACTIVATE_NOW,
	.vmode			= FB_VMODE_NONINTERLACED,
};

static struct fb_fix_screeninfo fb_fix __initdata= {
	.type 			= FB_TYPE_PACKED_PIXELS,
    .visual 		= FB_VISUAL_TRUECOLOR,
    .line_length 	= LCD_X_SIZE*4,//32 bit = 4 byte
	.accel 			= FB_ACCEL_NONE,
	.id				= "myfb",
};

static int fb_video_alloc(struct tft_lcd_fb *item)
{
	unsigned int frame_size;
				 
	frame_size = item->fbi->fix.line_length * item->fbi->var.yres;
	item->pages_count = frame_size / PAGE_SIZE;
	if ((item->pages_count * PAGE_SIZE) < frame_size) {
		item->pages_count++;
	}
	
	item->fbi->fix.smem_len = item->pages_count * PAGE_SIZE;			 
	item->fbi->fix.smem_start =
						 (u32)vmalloc(item->fbi->fix.smem_len);	
	if (!item->fbi->fix.smem_start) {
		dev_err(&item->spi->dev, "[%s]: unable to vmalloc\n", __func__);
		return -ENOMEM;
	}
	memset((void *)item->fbi->fix.smem_start, 0, item->fbi->fix.smem_len);
				 
	return 0;
}

static int fb_pages_alloc(struct tft_lcd_fb *item)
{
	u32 pixels_per_page;
	u32 yoffset_per_page;
	u32 xoffset_per_page;
	u32 index;
	unsigned short x = 0;
	unsigned short y = 0;
	u32 *buffer;
	unsigned int len;

	item->pages = kmalloc(item->pages_count * sizeof(struct fb_page), GFP_KERNEL);
	if (!item->pages) {
		dev_err(&item->spi->dev, "[%s]: unable to kmalloc for fb_page\n", __func__);
		return -ENOMEM;
	}

	pixels_per_page = PAGE_SIZE / (item->fbi->var.bits_per_pixel / 8);
	yoffset_per_page = pixels_per_page / item->fbi->var.xres;
	xoffset_per_page = pixels_per_page -
	    (yoffset_per_page * item->fbi->var.xres);
	
	DEBUG("[%s]: pixels_per_page=%hu "
		"yoffset_per_page=%hu xoffset_per_page=%hu\n",
		__func__, pixels_per_page, yoffset_per_page, xoffset_per_page);//5 144

	buffer = (u32 *)item->fbi->fix.smem_start;
	for (index = 0; index < item->pages_count; index++) {
		len = (item->fbi->var.xres * item->fbi->var.yres) -
		    (index * pixels_per_page);
		if (len > pixels_per_page) {
			len = pixels_per_page;
		}

		printk("%s: page[%d]: x=%3hu y=%3hu buffer=0x%p len=%3hu\n",
			__func__, index, x, y, buffer, len);
		item->pages[index].x = x;
		item->pages[index].y = y;
		item->pages[index].buffer = buffer;
		item->pages[index].len = len;

		x += xoffset_per_page;
		if (x >= item->fbi->var.xres) {
			y++;
			x -= item->fbi->var.xres;
		}
		y += yoffset_per_page;
		buffer += pixels_per_page;
	}

	/* tell the copy routines that the LCD controller is ready to receive data
 	  from the start of the buffer X,Y = (0,0) */
	item->last_buffer_start=(u32 *)item->fbi->fix.smem_start;

	return 0;
}

int tft_lcd_fb_register(struct spi_device *spi) //此函数在probe函数里被调用
{
	struct fb_info *fbi;
    //u8 *v_addr;
    //u32 p_addr;
	struct tft_lcd_fb *lcd_fb;
	int ret = 0;

	//v_addr = dma_alloc_coherent(NULL, LCD_X_SIZE*LCD_Y_SIZE*4, &p_addr, GFP_KERNEL);

	//额外分配tft_lcd_fb类型空间
    fbi = framebuffer_alloc(sizeof(struct tft_lcd_fb), &spi->dev);
	if(fbi == NULL){
		dev_err(&spi->dev, "[%s]: framebuffer_alloc failed\n", __func__);
		goto out;
	}
    lcd_fb = fbi->par; //data指针指向额外分配的空间
	lcd_fb->spi = spi;
	lcd_fb->fbi = fbi;

	fbi->pseudo_palette = pseudo_palette;
	fbi->var    = fb_var;
	fbi->fix = fb_fix;
	fbi->fbops = &fops;
	fbi->flags = FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB;

    //fbi->fix.smem_start = p_addr; //显存的物理地址
    //fbi->fix.smem_len = LCD_X_SIZE*LCD_Y_SIZE*4; 
	
    //fbi->screen_base = v_addr; //显存虚拟地址
    //fbi->screen_base = dma_alloc_writecombine(NULL, fbi->fix.smem_len, &fbi->fix.smem_start, GFP_KERNEL);
    //fbi->screen_size = LCD_X_SIZE*LCD_Y_SIZE*4; //显存大小

	ret = fb_video_alloc(lcd_fb);
	if (ret) {
		dev_err(&spi->dev, "[%s]: unable to fb_video_alloc\n", __func__);
		goto out_info;
	}
	fbi->screen_base = (char __iomem *)lcd_fb->fbi->fix.smem_start;

	ret = fb_pages_alloc(lcd_fb);
	if (ret) {
		dev_err(&spi->dev, "[%s]: unable to fb_pages_init\n", __func__);
		goto out_info;
	}

	fbi->fbdefio = &fb_defio;
	fb_deferred_io_init(fbi);
	
    ret = register_framebuffer(fbi);
	if (ret < 0) {
		dev_err(&spi->dev, "[%s]: unable to register_frambuffer\n", __func__);
		goto out_info;
	}
	
    //lcd_fb->thread = kthread_run(thread_func_fb, fbi, spi->modalias);
	
    return 0; 

out_info:
	framebuffer_release(fbi);
out:
	return ret;

}

static void tft_fb_test(struct spi_device *spi)
{
	int i,j;
	u16 color = 0x001f; /* rgb565,  蓝色 */
	
	Lcd_SetRegion(spi, 0,0,X_MAX_PIXEL-1,Y_MAX_PIXEL-1); //设置从屏哪个坐标开始显示,到哪个坐标结束

	#define rgb(r, g, b)  ((((r>>3)&0x1f)<<11) | (((g>>2)&0x3f)<<5) | ((b>>3)&0x1f))
    for(i=0 ; i<Y_MAX_PIXEL/2 ; i++)
    {
		
        color = rgb(255, 0, 255); 
        for(j=0; j<X_MAX_PIXEL/2; j++)
            Lcd_WriteData_16Bit(spi, color);//(u8 *)&
        color = rgb(255, 255, 0);
        for(j=X_MAX_PIXEL/2; j<X_MAX_PIXEL; j++)
            Lcd_WriteData_16Bit(spi, color);
    }
    for(i=Y_MAX_PIXEL/2 ; i<Y_MAX_PIXEL; i++)
    {
        color = rgb(0, 255, 255);
        for(j=0; j<X_MAX_PIXEL/2; j++)
            Lcd_WriteData_16Bit(spi, color);

        color = rgb(255, 0,0);
        for(j=X_MAX_PIXEL/2; j<X_MAX_PIXEL; j++)
            Lcd_WriteData_16Bit(spi, color);
    }
}

static int tft_lcd_probe(struct spi_device *spi)
{
	int ret;
	
	struct tft_lcd *lcd_data = devm_kzalloc(&spi->dev, sizeof(struct tft_lcd), GFP_KERNEL);

	ret = lcd_dt_parse(spi, lcd_data);
	if(ret !=0)
		goto err0;

	DEBUG("[%s]:success\n", __FUNCTION__);
	spi_set_drvdata(spi, lcd_data);
	lcd_init(spi, lcd_data);

	tft_fb_test(spi);

	ret = tft_lcd_fb_register(spi); //fb设备初始化
	
#ifdef 	OPEN_FPS
	init_timer(&frame_timer);
	frame_timer.expires = jiffies+(1*HZ);
	frame_timer.function = frames_timer_function;
	add_timer(&frame_timer);
#endif

    return 0;

err0:
	devm_gpiod_put(&spi->dev, lcd_data->rs_gpio);
	dev_err(&spi->dev, "[%s]:failed\n", __func__);
	return ret;
	
}

int tft_lcd_remove(struct spi_device *spi)
{
    struct tft_lcd *pdata = spi_get_drvdata(spi);

    DEBUG("[%s]:success\n", __FUNCTION__);
	devm_gpiod_put(&spi->dev, pdata->rs_gpio);
    devm_gpiod_put(&spi->dev, pdata->reset_gpio);

	//kthread_stop(data->thread); //让刷图线程退出container_of(&temp.j,struct test,j);
    //unregister_framebuffer(fbi);
    //dma_free_coherent(NULL, fbi->screen_size, fbi->screen_base, fbi->fix.smem_start);

    return 0;
}


struct of_device_id tft_lcd_ids[] = {
    {.compatible = "nanopi,tft_lcd_spi"},
    {},
};

struct spi_driver tft_lcd_drv = {
        .probe	= tft_lcd_probe,
        .remove = tft_lcd_remove,

        .driver = {
            .owner = THIS_MODULE,
            .name = "tft_lcd_drv",
            .of_match_table = tft_lcd_ids,
        },
};

module_spi_driver(tft_lcd_drv);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TFT LCD SPI driver");

这样即可做到局部刷新,哪里改变就刷新它的page即可。
其实把,最好还是使用内核里的fbtft驱动,如果内核没有fbtft驱动,可以去github下载fbtft的驱动,地址:https://github.com/notro/fbtft
Linux内核里也是有这个fbtft这个驱动的,比如我的Linux4.14就在drivers/staging/fbtft/目录下。
在make menuconfig里选择对应的芯片驱动为m,作为模块,最后:
make ARCH=arm CROSS_COMPILE=arm-linux- modules SUBDIRS=/work/linux/drivers/staging/fbtft/
即可编译出ko文件,insmod即可使用。

测试用例可以使用fbtft里的应用测试一下:https://github.com/notro/fbtft/wiki/Testing

或者网上找一个参考试一试:一个简单的framebuffer的显示使用例子
当然这个例子我没使用过,网上随便找的。

最后,给个framebuffer驱动分析

展开阅读全文

没有更多推荐了,返回首页