在从0编写framebuff期间搜了很多博客,他们都对framebuff进行很详细的解析,不过他们并没有从0–>1地给出编写framebuff的方案。不过从众多博客中我总结出编写framebuff的步骤及其注意事项。
下面,我将结合我工作经验来阐述如何从0编写framebuff驱动、测试用例。
在我看来framebuff是
framebuff = framebuff子系统 + LCD显存 + 虚拟页存 + 应用bind、write framebuff。
“虚拟页存”常用与内存的倒腾。因为上层写buff速度慢、LCD显示快。所以做一个中间内存用做缓冲。
“LCD显存”是一块主控中LCD控制器给出的一块连续存储空间。它用来存放像素点、主动将显存中的像素点显示在panel中。所以,上层开发看来显存的位置对应着panel的位置,操作显存相当于操作panel。
- 对于拥有LCD控制器的主控芯片来说,它们有显存(framebuff)。那对于用不到LCD控制器的显示器如spi LCD它由怎么实现framebuff机制呢?
所以:接下来讨论的是如何在没有LCD控制器的情况下编写一个framebuff。
那么,现在的framebuff变为
framebuff = framebuff子系统 + 虚拟页存 + 应用bind、write framebuff,少掉LCD显存。
在开始讲编写代码前,先介绍下linux下framebuff系统架构。
可以看到,framebuff被linux内部抽象成一个帧缓冲设备。其实这也很好理解,因为对于上层应用来说他们open这个设备后将像素点write进去就可以实现panel的显示。这样的设备就是一个拥有帧缓冲功能的设备。
对于一个精简的framebuff它只需要:驱动做初始化fb_info这种类型的设备、注册framebuff、应用mmap、应用write。这四步,下面将以源码讲解形式剖析framebuff。
我们先来看看应用层需要做的功能:
bmp2lcd(demo_QR, FB, &vinfo, rinfo);
char * init_lcd(struct fb_var_screeninfo *vinfo)
{
int frame_fd= open("/dev/fb0", O_RDWR);
if (frame_fd == -1){
printf("open frame buffer device error\n");
return NULL;
}
bzero(vinfo, sizeof(struct fb_var_screeninfo));
// 获取lcd 信息
if(ioctl(frame_fd, FBIOGET_VSCREENINFO, vinfo) !=0)
printf("ioctl FBIOGET_VSCREENINFO fail \n");
char *FB = mmap(NULL, 50*4096, //frame buff page=4096 320*320*2 =200k
PROT_READ|PROT_WRITE,
MAP_SHARED, frame_fd, 0);
return FB;
}
// 将cmp加载到一块buff中 成功:返回指向buff指针 失败:返回:NULL
char * load_bmp(const char *Imagefile, struct image_info *minfo)
{
int fd = open(Imagefile, O_RDONLY);
if(fd == -1)
{
fprintf(stderr, "opening \"%s\" failed: %s\n",
Imagefile, strerror(errno));
return NULL;
}
// 获得文件大小,并分配内存
struct stat fileinfo;
fstat(fd, &fileinfo);
int rgb_size = fileinfo.st_size;
char *rgb_buf = calloc(1, rgb_size); // 申请一块将用于存放bmp的buff
if(rgb_buf == NULL)
{
fprintf(stderr, "calloc rgb_buf failed: %s\n",
strerror(errno));
return NULL;
}
// 读取BMP内容到内存中
struct bitmap_header header;
struct bitmap_info info;
struct rgb_quad quad;
read(fd, &header, sizeof(header));
read(fd, &info, sizeof(info));
if(info.compression != 0)
{
read(fd, &quad, sizeof(quad));
fprintf(stderr, "read quad! \n");
}
read(fd, rgb_buf, rgb_size);
minfo->width = info.width;
minfo->height= info.height;
minfo->pixel_size = info.bit_count/8;
#undef DEBUG
#ifdef DEBUG
printf("width: %d\n", minfo->width);
printf("height: %d\n", minfo->height);
printf("pixel_size: %d\n", minfo->pixel_size);
#endif
close(fd);
return rgb_buf;
}
// 成功:返回0 失败:返回负数码
int bmp2lcd(char *Imagefile, char *FB,struct fb_var_screeninfo *vinfo, struct lcd_rinfo *rinfo)
{
//x y为lcd屏幕的偏移 i j为bmp图片的偏移
int y,x,ret;
int i,j;
short color;
// 获取bmp图片数据
struct image_info *minfo = calloc(1, sizeof(struct image_info));
char *rgb_buf = load_bmp(Imagefile, minfo);
if(rgb_buf == NULL)
return -1;
char *tmp = rgb_buf; // 标记bmp buff首地址
// 从最后一行开始显示BMP图像
int pad = ((4-( minfo->width * minfo->pixel_size ) % 4)) % 4; // 0-3
rgb_buf += (minfo->width * minfo->pixel_size + pad) * (minfo->height-1);//bmp最后一行的首地址
unsigned long lcd_offset,bmp_offset;
/*
printf("vinfo->bits_per_pixel:%d\n", vinfo->bits_per_pixel);
printf("minfo->pixel_size:%d\n", minfo->pixel_size);
*/
for (i=0,y = rinfo->y_s; y < rinfo->y_e&& y < minfo->height+rinfo->y_s; y++,i++)
{
for (j=0,x = rinfo->x_s; x < rinfo->x_e && x < minfo->width+rinfo->x_s; x++,j++)
{
bmp_offset = (minfo->width*i + j) * 3; // 当前bmp buffer中 偏移的位置
lcd_offset = (vinfo->xres*y + x) * 2; // 当前frame buffer中 偏移的位置
//从bmp中逐个取出一个像素点(24位宽) rgb8888 --> rgb565
color = (( rgb_buf[bmp_offset + 0]) >> 3); //蓝色
color |= (( rgb_buf[bmp_offset + 1]) >>2)<<5; //绿色
color |= (( rgb_buf[bmp_offset + 2]) >>3)<<11; //红色
memcpy(FB + lcd_offset, &color, 2);
}
rgb_buf += pad; //4字节对齐 所以要跳过补齐的字节数
//从最后一行往上扫描. *1是跳过自身行到达上一行行尾,*2即到达上一行行首.
rgb_buf -= (minfo->width * minfo->pixel_size + pad) * 2;
}
ret = ioctl(fd_lcd, BUFF2LCD, rinfo);
free(tmp);
// if(ret == 0)
// printf("[bmp2lcd]: bmp2lcd succeed\n");
return ret;
}
上面提供了mmap 实现进程的虚拟地址绑定底层分配的虚拟页存、24bits像素点转成16bits像素点并memcpy到framebuff中,又由于这里没有LCD控制器,所以应用层需要做一个ioctl动作将刷图的信息传给底层,让底层代劳刷panel。
所以,应用层只需要调用bmp2lcd传入bmp图像地址、LCD info 、bmp info就可以实现刷图了。
完成应用层工作,现在考虑下驱动层怎么支持。
static int 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;
}
/*
*framebuff子系统规定:
*如果没有指定.fb_mmap就使用系统提供的mmap。
*/
static int lcd_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long pfn = (virt_to_phys(lcd->screen_base) >> PAGE_SHIFT); //求出虚拟页所映射的物理页
if (remap_pfn_range(vma, vma->vm_start, pfn, //利用应用层传下来的虚拟地址bind刚求出的物理页!
vma->vm_end - vma->vm_start,
vma->vm_page_prot))
return -EAGAIN;
return 0;
}
static struct fb_ops lcdfb_ops = { //这个ops 供给frame buff子系统使用
.owner = THIS_MODULE,
.fb_setcolreg = lcdfb_setcolreg, //设置调色板
// .fb_fillrect = cfb_fillrect, //画一个矩形
// .fb_copyarea = cfb_copyarea, //Copy data from area to another 可用于终端 cp /dev/fbX myfdX 抓图
// .fb_imageblit = cfb_imageblit, //Draws a image to the display
.fb_mmap = lcd_mmap,
};
/********************* frame buff **********************************************/
/* 1. 分配一个fb_info */
lcd = framebuffer_alloc(0, NULL); //不需要私有空间、暂时不要给device成员赋值
/* 2. 设置 */
/* 2.1 设置固定的参数 */
strcpy(lcd->fix.id, "lcd");
lcd->fix.smem_len = horizontal*vertical*pixel;
lcd->fix.type = FB_TYPE_PACKED_PIXELS;
lcd->fix.visual = FB_VISUAL_TRUECOLOR; /* TFT */
lcd->fix.line_length = horizontal*pixel;
/* 2.2 设置可变的参数 */
lcd->var.xres = horizontal;
lcd->var.yres = vertical;
lcd->var.xres_virtual = horizontal;
lcd->var.yres_virtual = vertical;
lcd->var.bits_per_pixel = pixel*8;
/* RGB:565 */
lcd->var.red.offset = 11;
lcd->var.red.length = 5;
lcd->var.green.offset = 5;
lcd->var.green.length = 6;
lcd->var.blue.offset = 0;
lcd->var.blue.length = 5;
lcd->var.activate = FB_ACTIVATE_NOW; //使设置的值立即生效
/* 2.3 设置操作函数 */
lcd->fbops = &lcdfb_ops;
/* 2.4 其他的设置 */
lcd->pseudo_palette = pseudo_palette; //存放调色板所调颜色的数组
lcd->screen_base = (char __iomem *)__get_free_pages(GFP_KERNEL, 6); /* 分配256k显存(framebuffer) 返回显存的虚拟地址 */
lcd->screen_size = horizontal*vertical*pixel;
/*******************************************************************************/
ret = register_framebuffer(lcd);
if (0 != ret){
printk("Kernel: register register_framebuffer device failed!\n");
return -1;
}
当调用register_framebuffer时就像系统申请了一个fbn,应用就可以通过文件系统传入fbn设备节点调用驱动的mmap,实现应用进程虚拟空间和物理空间绑定。调用memcpy那个虚拟地址就可以将像素点写入物理地址。
insmod驱动后做 mknod /dev/fb0 c 29 0就可以使用fb0这个设备了。