从0编写linux framebuff驱动、测试用例

在从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这个设备了。

猜你喜欢

转载自blog.csdn.net/huang_165/article/details/80424072