Linux驱动开发之Fbdev双缓存快速入门

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

[[382813]]

本文转载自微信公众号「嵌入式Hacker」,可以通过以下二维码关注。转载本文请联系嵌入式Hacker公众号。

一、为何需要 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 和 屏幕协作完成一帧图像的显示:

[[382817]]

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

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

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

[[382818]]

可以通过 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.     ... 

[[382819]]

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.     framesize = width * height * 2 * 2; 
  4.  
  5.     fb->fb.screen_base = (char __force __iomem *)dma_alloc_coherent(&pdev->dev, framesize, &fbpaddr, GFP_KERNEL); 

2. 设置 fb_var_screeninfo:

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

3. 实现 ioctl / FBIOPAN_DISPLAY:

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

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

运行:

  1. $ ./show_color single 
  2. buffer_num = 1 
  3. single buffer: 
  4.  
  5. $ ./show_color double 
  6. buffer_num = 2 
  7. 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

 

责任编辑:武晓燕 来源: 嵌入式Hacker
相关推荐

2023-02-13 09:01:29

Linux驱动实例

2011-09-02 15:18:49

Sencha Touc

2010-07-19 10:05:52

ibmdwLinux

2011-07-11 09:29:32

PHP面向对象编程

2009-06-01 15:32:30

EclipseJPA入门

2009-12-09 10:50:53

嵌入式Linux

2012-02-29 00:49:06

Linux学习

2009-10-21 12:45:07

linux程序开发基础入门

2021-03-11 12:23:13

Linux驱动开发

2011-11-08 10:36:42

Java

2009-12-17 15:59:26

Linux驱动开发

2012-05-30 15:15:42

ibmdw

2011-09-02 15:42:55

Sencha Touc布局

2011-09-02 15:58:38

Sencha Touc布局

2020-09-04 15:38:19

Web前端开发项目

2012-03-01 22:37:02

Linux入门

2016-12-15 14:55:31

Linux定时延时

2009-12-10 09:33:30

Linux驱动开发

2022-09-26 00:00:01

java代码开发

2020-04-15 08:33:43

Netty网络通信
点赞
收藏

51CTO技术栈公众号