飞道的博客

Linux 驱动开发 / fbdev 双缓存 / 快速入门

512人阅读  评论(0)

哈喽,我是老吴。

我回来啦!

过年回老家,特别充实,大部分时间都在带娃~

女儿快 2 岁了,走起路来像模像样,在屋子里转来转去,有的时候还会小跑,痴迷于外出逛街。

目前我稍微有点理解她了。

2岁的小孩特别倔强,她想做或者不愿意做的事,一般都会跟我死磕到底,很难强迫她。

我发现 2 个比较有效的方法:

1> 转移注意力

例如大半夜她吵着要出去玩,就得先用好吃的水果稳住她.;

2> 顺势而为

例如她一定要玩剪刀,家里就提前准备好一把玩具剪刀,给她玩;

哈哈哈,还有很多好玩的技巧,以后再分享了,下面开始学技术。

一、为何需要 double buffer?

single buffer 会导致:

屏幕撕裂(tearing),即在屏幕上同时看到多帧数据拼接在一起。

点击查看大图

single buffer 为何会造成撕裂:

refresh rate 和 frame rate 不一致。

refresh rate 表示的是 屏幕每秒能更新多少次显示,例如 30hz / 60hz。

点击查看大图

frame rate 表示的是 lcd controller / gpu 每秒能绘制多少帧数据,例如 30fps / 60fps。

点击查看大图

LCD controller / gpu 和 屏幕协作完成一帧图像的显示:

点击查看大图

在 single buffer 的场景下,LCD user 和  LCD controller / gpu 总是在共用同一个 framebuffer,且没有同步机制。

LCD user 是写者,LCD controller / gpu 是读者。

由于存在竞争关系且读写没有同步机制,framebuffer 里必须会发生同时存在frame N 和 frame N-1 的数据,此时 LCD 将 framebuffer 的数据显示出来时,就会看到撕裂的效果:

点击查看大图

可以通过 double buffer+vsync 解决撕裂的问题。

double buffer,顾名思义,就是有 2 个 framebuffer,其工作逻辑如下:

  • LCD controller : draw fb0 to screen

  • LCD user : write data to fb1

  • LCD controller : draw fb1 to screen

  • LCD user : write data to fb0

  • 循环...

vsync 机制则用于确保一帧图像能不被打断地显示在屏幕。

如何支持 double buffer?

需要驱动和应用互相配合:

二、编写支持 double buffer 的 fbdev 驱动

fbdev 框图:

先梳理一下思路:

让驱动支持 double buffer 需要做 3 件事。

1. 申请2 x buffer:


   
  1. size = ( 2 * width * height);
  2. fbi->screen_base = dma_alloc_wc(sfb->dev, size, &map_dma, GFP_KERNEL);

2. 将 buffer 相关的信息保存 struct fb_info-> struct fb_var_screeninfo。


   
  1. struct fb_var_screeninfo {
  2.     __u32 xres;             /* visible resolution        */
  3.     __u32 yres;
  4.     __u32 xres_virtual;         /* virtual resolution        */
  5.     __u32 yres_virtual;
  6.     __u32 xoffset;             /* offset from virtual to visible */
  7.     __u32 yoffset;             /* resolution            */
  8.     ...
  9. }

点击查看大图

xres 和 yres 是真实的 LCD 分辨率的宽和长;

xres_virtual 和 yres_virtual 是显存区域的宽和长;

xoffset 和 yoffset 用于指定当前使用哪一个 Buffer 进行绘制。使用 Buffer0 时 ,xoffset = 0,yoffset=0; 使用 Buffer1 时,xoffset = 0, yoffset = yres * 1;

3. 支持切换 buffer,具体的就是实现 ioctl:FBIOPAN_DISPLAY。

pan 的本意是平移,可以想象成显存上方有一个取景框,平移取景框可以看到不同的显示内容。

实例分析:goldfishfb.c

goldfishfb.c 是虚拟硬件 goldfish 的 fbdev 驱动,我们可以参考这个文件,学习如何实现 double buffer。

1. 分配 2 x buffer:


   
  1. int goldfish_fb_probe()
  2. {
  3.     ...
  4.     framesize = width * height *  2 *  2;
  5.     fb->fb.screen_base = (char __force __iomem *)dma_alloc_coherent(&pdev->dev, framesize, &fbpaddr, GFP_KERNEL);
  6. }

2. 设置 fb_var_screeninfo:


   
  1. int goldfish_fb_probe()
  2. {
  3.     ...
  4.     fb->fb. var.xres  = width;
  5.     fb->fb. var.yres  = height;
  6.     fb->fb. var.xres_virtual = width;
  7.     fb->fb. var.yres_virtual = height *  2;
  8. }

3. 实现 ioctl / FBIOPAN_DISPLAY:


   
  1. static  struct fb_ops goldfish_fb_ops = {
  2.  ...
  3.  .fb_pan_display = goldfish_fb_pan_display,
  4. };

   
  1. int goldfish_fb_pan_display()
  2. {
  3.     ...
  4.      // 将新的显存地址告知 lcd controller
  5.     writel(fb->fb.fix.smem_start + fb->fb. var.xres *  2 *  var->yoffset,
  6.         fb->reg_base + FB_SET_BASE);
  7.      // 等待 LCD controller 的 vsync 信号
  8.     wait_event_timeout(fb->wait,fb->base_update_count != base_update_count, HZ /  15);
  9. }

当LCD controller 将一帧图像完整地显示在 LCD 上后,就会产生一个中断,在中断里就会执行唤醒睡眠在 fb_pan_display 里的进程。

如果你想多了解一些,可以阅读 DRM 框架里的 fbdev 兼容代码,此代码也是支持 double buffer的:

  • linux/drivers/gpu/drm/*/*_drm_fbdev.c

  • linux/drivers/gpu/drm/drm_fb_helper.c

三、编写支持 double buffer 的 fbdev 应用

驱动支持 double buffer 后,还得在应用程序里将其使用起来。

先梳理一下思路:

  1. 检查是否支持 double buffer;

  2. 使能 double buffer:FBIOPUT_VSCREENINFO;

  3. 更新 buffer 里数据;

  4. 通知驱动切换 buffer:FBIOPAN_DISPLAY;

  5. 等待切换完成:FBIO_WAITFORVSYNC;

实例分析:show_color.c


   
  1. static  int fd_fb;
  2. static  struct fb_fix_screeninfo fix;     /* Current fix */
  3. static  struct fb_var_screeninfo  var;     /* Current var */
  4. static  int screen_size;
  5. static unsigned char *fb_base;
  6. static unsigned  int line_width;
  7. static unsigned  int pixel_width;
  8. int main( int argc, char **argv)
  9. {
  10.      int i;
  11.      int ret;
  12.      int buffer_num;
  13.      int buf_idx =  1;
  14.     char *buf_next;
  15.     unsigned  int colors[] = { 0x00FF00000x0000FF000x000000FF00x00FFFFFF};   /* 0x00RRGGBB */
  16.      struct timespec time;
  17.     ...
  18.     
  19.     fd_fb = open( "/dev/fb0", O_RDWR);
  20.     ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix);
  21.     ioctl(fd_fb, FBIOGET_VSCREENINFO, & var);
  22.     line_width  =  var.xres *  var.bits_per_pixel /  8;
  23.     pixel_width =  var.bits_per_pixel /  8;
  24.     screen_size =  var.xres *  var.yres *  var.bits_per_pixel /  8;
  25.      // 1. 获得 buffer 个数
  26.     buffer_num = fix.smem_len / screen_size;
  27.     printf( "buffer_num = %d\n", buffer_num);
  28.     
  29.     fb_base = (unsigned char *)mmap(NULL , fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb,  0);
  30.      if (fb_base == (unsigned char *) -1) {
  31.         printf( "can't mmap\n");
  32.          return  -1;
  33.     }
  34.      if ((argv[ 1][ 0] ==  's') || (buffer_num ==  1)) {
  35.         printf( "single buffer:\n");
  36.         while ( 1) {
  37.              for (i =  0; i < sizeof(colors)/sizeof(colors[ 0]); i++) {
  38.                 lcd_draw_screen(fb_base, colors[i]);
  39.                 nanosleep(&time, NULL);
  40.             }
  41.         }
  42.     }  else {
  43.         printf( "double buffer:\n");
  44.          // 2. 使能多 buffer
  45.          var.yres_virtual = buffer_num *  var.yres;
  46.         ioctl(fd_fb, FBIOPUT_VSCREENINFO, & var);
  47.         while ( 1) {
  48.              for (i =  0; i < sizeof(colors)/sizeof(colors[ 0]); i++) {
  49.                  // 3. 更新 buffer 里的数据
  50.                 buf_next =  fb_base + buf_idx * screen_size;
  51.                 lcd_draw_screen(buf_next, colors[i]);
  52.                  // 4. 通知驱动切换 buffer
  53.                  var.yoffset = buf_idx *  var.yres;
  54.                 ret = ioctl(fd_fb, FBIOPAN_DISPLAY, & var);
  55.                  if (ret <  0) {
  56.                     perror( "ioctl() / FBIOPAN_DISPLAY");
  57.                 }
  58.                  // 5. 等待帧同步完成
  59.                 ret =  0;
  60.                 ioctl(fd_fb, FBIO_WAITFORVSYNC, &ret);
  61.                  if (ret <  0) {
  62.                     perror( "ioctl() / FBIO_WAITFORVSYNC");
  63.                 }
  64.                 
  65.                 buf_idx = !buf_idx;
  66.                 nanosleep(&time, NULL);
  67.             }
  68.         }
  69.         
  70.     }
  71.     
  72.     munmap(fb_base , screen_size);
  73.      close(fd_fb);
  74.     
  75.      return  0;   
  76. }

运行:


   
  1. $ ./show_color single
  2. buffer_num =  1
  3. single buffer:
  4. $ ./show_color double
  5. buffer_num =  2
  6. double buffer:

该程序会在屏幕上循环的显示不同的颜色。

当传入 "single" 参数时,使用单 buffer,可见撕裂。

当传入 "double" 参数时,使用双 buffer,不再撕裂。

代码不是很复杂,我就不再详细分析了。

如果你想多了解一些,可以阅读开源软件 SDL-1.2 里的 sdl_fbvideo.c,此代码也支持了 double buffer。

另外,现在越来越多的显示设备走的是 DRM 框架,该框架自然是支持多 buffer的。感兴趣的小伙伴,自行查看下面的代码:

https://github.com/dvdhrm/docs/blob/master/drm-howto/modeset-double-buffered.c

四、相关参考

百问网 / 韦东山驱动大全教学视频:https://www.100ask.net/detail/p_5ff2c46ce4b0c4f2bc4fa16d/8

维基百科:https://en.wikipedia.org/wiki/Screen_tearing

思考技术,也思考人生

要学习技术,更要学习如何生活

最近在看的书:

《你的2岁孩子》

点击查看大图

收获了什么?

  • 有了一张关于 2 岁儿童成长的参考地图;

  • 稍微能理解 2 岁儿童面临的处境和困难;

  • 父母应该让自己不断地变得更专业一些;

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。

觉得文章对你有价值,不妨点个 在看和赞


转载:https://blog.csdn.net/wuweidonggmail/article/details/113931763
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场