嵌入式Linux C应用编程——Framebuffer应用编程 什么是 FrameBufferFrameBuffer帧缓冲 是 Linux 系统中的一种显示驱动接口。它将显示设备如 LCD进行抽象屏蔽了不同显示设备硬件的实现差异对应用层呈现为一块显示内存显存允许上层应用程序直接对显示缓冲区进行读写操作而无需关心物理显存的位置等具体细节这些都由 FrameBuffer 设备驱动来完成。在 Linux 系统中显示设备被称为 FrameBuffer 设备帧缓冲设备LCD 显示屏即属于 FrameBuffer 设备。FrameBuffer 设备对应的设备文件为 /dev/fbXX 为数字如 0、1、2、3 等Linux 最多可支持 32 个 FrameBuffer 设备分别为 /dev/fb0 到 /dev/fb31。开发板出厂系统中/dev/fb0 设备节点便是 LCD 屏。应用程序读写 /dev/fbX 就相当于读写显示设备的显示缓冲区显存。例如若 LCD 分辨率为 800*480每个像素点用 24 位RGB888表示则显存大小为800×480×24/81152000字节执行以下命令可将 LCD 清屏填充为黑色dd if/dev/zero of/dev/fb0 bs1024 count1125该命令将 1125 × 1024 个字节的 0x00 数据全部写入 LCD 显存中。LCD 显示原理LCD 屏幕由按矩阵排列的像素点阵组成每个像素由 R红、G绿、B蓝三个子像素构成。显示的原理大致如下显存FrameBuffer系统在内存中划出一块区域作为显存每个像素的颜色数据按顺序存放在这块内存中。显存中第 (y × 宽度 x) 个位置的数据就对应屏幕上第 y 行第 x 列的像素。LCD 控制器硬件上有一个 LCD 控制器它会以固定的刷新率如 60Hz不断地从显存中读取像素数据。显示驱动控制器将读出的数字信号转换为 LCD 面板需要的模拟电压驱动每个子像素显示对应的亮度组合起来就成了一帧完整的画面。刷新过程控制器从显存起始地址开始逐行扫描读取每个像素的数据并输出到屏幕扫完一帧后立即从头开始下一帧周而复始。所以应用程序往显存中写入数据LCD 控制器自动将其显示到屏幕上——这就是为什么读写 /dev/fbX 就能控制 LCD 显示。为什么需要显存LCD 需要持续刷新LCD 面板必须以固定频率如 60Hz不断刷新否则画面会消失。这意味着显示数据的供给是连续且实时的。CPU 无法独占CPU 还要运行其他任务不可能一直都在送数据给 LCD。显存作为缓冲区在内存中划出一块区域作为显存CPU 只需要在需要更新画面时往里面写数据其余时间可以去做别的事而 LCD 控制器通过 DMA直接存储器访问 方式独立地从显存读取数据并驱动显示完全不需要 CPU 干预。LCD 应用编程介绍操作 /dev/fbX 设备的一般步骤如下打开 /dev/fbX 设备文件。使用 ioctl() 获取当前显示设备的参数信息如分辨率、像素格式据此计算显示缓冲区的大小。通过存储映射 I/Ommap() 将屏幕的显示缓冲区映射到用户空间。映射成功后直接读写显存进行绘图或图片显示等操作。完成显示后调用 munmap() 取消映射并调用 close() 关闭设备文件。19.3.1 使用 ioctl() 获取屏幕参数信息对于 FrameBuffer 设备常用的 ioctl() 请求如下宏定义 功能 数据结构FBIOGET_VSCREENINFO 获取可变参数信息 struct fb_var_screeninfoFBIOPUT_VSCREENINFO 设置可变参数信息 struct fb_var_screeninfoFBIOGET_FSCREENINFO 获取固定参数信息 struct fb_fix_screeninfo这些宏定义及数据结构均定义在 linux/fb.h 头文件中。#define FBIOGET_VSCREENINFO 0x4600#define FBIOPUT_VSCREENINFO 0x4601#define FBIOGET_FSCREENINFO 0x4602struct fb_var_screeninfo 结构体该结构体描述 FrameBuffer 设备的可变参数信息struct fb_var_screeninfo {__u32 xres; /* 可视区域 X 分辨率一行像素点数/__u32 yres; /可视区域 Y 分辨率一列像素点数/__u32 xres_virtual; /虚拟区域 X 分辨率/__u32 yres_virtual; /虚拟区域 Y 分辨率/__u32 xoffset; /虚拟到可见屏幕的行偏移/__u32 yoffset; /虚拟到可见屏幕的列偏移/__u32 bits_per_pixel; /像素深度 bpp/__u32 grayscale; /0 彩色, 1 灰度, 1 FOURCC 颜色 */struct fb_bitfield red; /* 红色分量色域偏移 */ struct fb_bitfield green; /* 绿色分量色域偏移 */ struct fb_bitfield blue; /* 蓝色分量色域偏移 */ struct fb_bitfield transp; /* 透明度分量色域偏移 */ __u32 nonstd; /* 0 标准像素格式, ≠0 非标准 */ __u32 activate; __u32 height; /* 显示高度mm */ __u32 width; /* 显示宽度mm */ __u32 accel_flags; /* 时序参数 */ __u32 pixclock; /* pixel clock in ps */ __u32 left_margin; __u32 right_margin; __u32 upper_margin; __u32 lower_margin; __u32 hsync_len; __u32 vsync_len; __u32 sync; __u32 vmode; __u32 rotate; __u32 colorspace; __u32 reserved[4];};通过 xres、yres 获取屏幕分辨率bits_per_pixel 获取像素深度显存大小计算公式为screen_sizexres×yres×bitsperpixel/8xres_virtual、yres_virtual 表示虚拟分辨率通常与 xres、yres 一致但也可以设置得比实际分辨率大。虚拟区域配合 xoffset、yoffset 可实现两个实用功能滚动Panning虚拟区域大于屏幕时通过修改偏移量让屏幕显示虚拟区域的不同部分实现平滑滚动而无需重新绘制。双缓冲Double Buffering将 yres_virtual 设为 2 * yres显存中就有上下两个页面。一页用于显示的同时往另一页绘图然后切换偏移量避免画面撕裂。red、green、blue 三个 struct fb_bitfield 变量描述 RGB 颜色通道的位偏移和位长度据此可判断 LCD 的像素格式如 RGB888、RGB565、BGR888 等。struct fb_fix_screeninfo 结构体该结构体描述 FrameBuffer 设备的固定参数信息不可修改struct fb_fix_screeninfo {char id[16]; /* 字符串标识符/unsigned long smem_start; /显存起始地址物理地址/__u32 smem_len; /显存长度/__u32 type;__u32 type_aux;__u32 visual;__u16 xpanstep;__u16 ypanstep;__u16 ywrapstep;__u32 line_length; /一行的字节数/unsigned long mmio_start; /MMIO 起始地址物理地址/__u32 mmio_len; /MMIO 长度 */__u32 accel;__u16 capabilities;__u16 reserved[2];};line_length 表示屏幕一行像素点所占的字节数通常可用 line_length * yres 计算显示缓冲区大小。struct fb_var_screeninfo —— 前缀 var 代表 Variable可变描述分辨率、像素格式等可动态调整的参数可通过 FBIOPUT_VSCREENINFO 修改前提是驱动支持。struct fb_fix_screeninfo —— 前缀 fix 代表 Fixed固定描述显存地址、行长等硬件底层固定的参数应用程序只能读取不可修改。struct fb_bitfield 结构体struct fb_bitfield {__u32 offset; /* 偏移量/__u32 length; /长度/__u32 msb_right; /! 0 : 最高有效位在右侧 */};获取屏幕参数示例程序#include stdio.h#include stdlib.h#include sys/types.h#include sys/stat.h#include fcntl.h#include unistd.h#include sys/ioctl.h#include linux/fb.hint main(int argc, char *argv[]){struct fb_fix_screeninfo fb_fix;struct fb_var_screeninfo fb_var;int fd;/* 打开 framebuffer 设备 */ if (0 (fd open(/dev/fb0, O_WRONLY))) { perror(open error); exit(-1); } /* 获取参数信息 */ ioctl(fd, FBIOGET_VSCREENINFO, fb_var); ioctl(fd, FBIOGET_FSCREENINFO, fb_fix); printf(分辨率: %d*%d\n 像素深度 bpp: %d\n 一行的字节数: %d\n 像素格式: R%d %d G%d %d B%d %d\n, fb_var.xres, fb_var.yres, fb_var.bits_per_pixel, fb_fix.line_length, fb_var.red.offset, fb_var.red.length, fb_var.green.offset, fb_var.green.length, fb_var.blue.offset, fb_var.blue.length); /* 关闭设备文件退出程序 */ close(fd); exit(0);}注意ALPHA/Mini I.MX6U 开发板出厂系统中LCD 驱动程序将其实现为 RGB565 格式的显示设备。用户可通过修改设备树使其支持 RGB888或通过 ioctl 修改。不建议通过 FBIOPUT_VSCREENINFO 随意修改可变参数若驱动支持不够完善可能引发问题。使用 mmap() 将显示缓冲区映射到用户空间为什么需要使用存储映射 I/O 而非普通 read()/write()当数据量较大时普通 I/O 方式效率较低。例如一台 1920×1080 分辨率、ARGB8888 像素格式的显示器刷一帧图像的数据量为1920×1080×32/88294400字节约 8MB且显示的图像往往是动态更新的数据量庞大使用普通 I/O 必然导致效率低下因此采用 mmap() 存储映射 I/O 方式。为什么普通 I/O 效率低每次调用 read()/write() 都经历以下过程系统调用用户态 → 内核态切换数据拷贝将数据从用户缓冲区拷贝到内核缓冲区一次额外的内存拷贝写入设备驱动将内核缓冲区的数据写入显存如果要刷新一帧 8MB 的数据每刷一部分就要重复一次上述流程系统调用次数和数据拷贝量都非常大。而 mmap() 的优势在于建立映射后应用程序直接读写映射区的内存相当于直接操作显存——无需系统调用、无需内核态切换、无需数据拷贝。这也是存储映射 I/O 的优势。LCD 应用编程本节实现 LCD 上的画点打点、画线、画矩形、填充等基本操作。关键宏定义#define argb8888_to_rgb565(color) ({unsigned int temp (color);((temp 0xF80000UL) 8) |((temp 0xFC00UL) 5) |((temp 0xF8UL) 3);})该宏将 ARGB8888 格式颜色转换为 RGB565 格式。因为开发板出厂系统的 LCD 是 RGB565 格式每像素 16 位而绘图函数传参用的是 ARGB888832 位整数如 0xFF0000 表示红色写入显存前必须压缩为 16 位 RGB565 值否则直接赋值给 unsigned short 会发生截断导致颜色错误。之所以不直接用 RGB565 传参我猜测应该是主要有两点考虑可读性0xFF0000 一眼就能看出是红色而 RGB565 的红色 0xF800 不够直观。灵活性若后期将 LCD 切换为 RGB888 模式只需修改转换宏所有调用了 lcd_fill(…, 0xFF0000) 的代码无需改动。主要函数函数 功能 说明lcd_draw_point(x, y, color) 打点 在 (x, y) 位置绘制颜色为 color 的像素点lcd_draw_line(x, y, dir, length, color) 画线 从 (x, y) 开始画水平或垂直的线条dir!0 为水平dir0 为垂直lcd_draw_rectangle(s_x, e_x, s_y, e_y, color) 画矩形 绘制指定矩形区域的四条边lcd_fill(s_x, e_x, s_y, e_y, color) 填充 将指定矩形区域填充为指定颜色示例代码lcd_test.c#include stdio.h#include stdlib.h#include sys/types.h#include sys/stat.h#include fcntl.h#include unistd.h#include sys/ioctl.h#include sys/mman.h#include linux/fb.h#define argb8888_to_rgb565(color) ({unsigned int temp (color);((temp 0xF80000UL) 8) |((temp 0xFC00UL) 5) |((temp 0xF8UL) 3);})static int width; // LCD X 分辨率static int height; // LCD Y 分辨率static unsigned short *screen_base NULL; // 映射后的显存基地址static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color){unsigned short rgb565_color argb8888_to_rgb565(color);if (x width) x width - 1; if (y height) y height - 1; screen_base[y * width x] rgb565_color;}static void lcd_draw_line(unsigned int x, unsigned int y, int dir,unsigned int length, unsigned int color){unsigned short rgb565_color argb8888_to_rgb565(color);unsigned int end;unsigned long temp;if (x width) x width - 1; if (y height) y height - 1; temp y * width x; if (dir) { // 水平线 end x length - 1; if (end width) end width - 1; for ( ; x end; x, temp) screen_base[temp] rgb565_color; } else { // 垂直线 end y length - 1; if (end height) end height - 1; for ( ; y end; y, temp width) screen_base[temp] rgb565_color; }}static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,unsigned int start_y, unsigned int end_y,unsigned int color){int x_len end_x - start_x 1;int y_len end_y - start_y - 1;lcd_draw_line(start_x, start_y, 1, x_len, color); // 上边 lcd_draw_line(start_x, end_y, 1, x_len, color); // 下边 lcd_draw_line(start_x, start_y 1, 0, y_len, color); // 左边 lcd_draw_line(end_x, start_y 1, 0, y_len, color); // 右边}static void lcd_fill(unsigned int start_x, unsigned int end_x,unsigned int start_y, unsigned int end_y,unsigned int color){unsigned short rgb565_color argb8888_to_rgb565(color);unsigned long temp;unsigned int x;if (end_x width) end_x width - 1; if (end_y height) end_y height - 1; temp start_y * width; for ( ; start_y end_y; start_y, temp width) { for (x start_x; x end_x; x) screen_base[temp x] rgb565_color; }}int main(int argc, char *argv[]){struct fb_fix_screeninfo fb_fix;struct fb_var_screeninfo fb_var;unsigned int screen_size;int fd;/* 打开 framebuffer 设备 */ if (0 (fd open(/dev/fb0, O_RDWR))) { perror(open error); exit(EXIT_FAILURE); } /* 获取参数信息 */ ioctl(fd, FBIOGET_VSCREENINFO, fb_var); ioctl(fd, FBIOGET_FSCREENINFO, fb_fix); /* 计算显存大小用 line_length * yres 而非 xres * yres * bpp / 8 * 因为 line_length 是驱动实际返回的一行字节数可能包含对齐填充字节 * 用后者算出的值可能偏小导致 mmap 映射区域不足 */ screen_size fb_fix.line_length * fb_var.yres; width fb_var.xres; height fb_var.yres; /* mmap(起始地址, 长度, 读写权限, 映射方式, 文件描述符, 偏移量) PROT_WRITE映射区可写 MAP_SHARED共享映射。对映射区的修改会写回文件 从用户空间映射区写到磁盘设备中。 * 将显存映射到用户空间返回映射区的首地址 */ screen_base mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0); if (MAP_FAILED (void *)screen_base) { perror(mmap error); close(fd); exit(EXIT_FAILURE); } /* 画正方形方块 */ int w height * 0.25; lcd_fill(0, width-1, 0, height-1, 0x0); // 清屏黑色 lcd_fill(0, w, 0, w, 0xFF0000); // 红色方块 lcd_fill(width-w, width-1, 0, w, 0xFF00); // 绿色方块 lcd_fill(0, w, height-w, height-1, 0xFF); // 蓝色方块 lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00); // 黄色方块 /* 画线: 十字交叉线 */ lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF); // 白色水平线 lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF); // 白色垂直线 /* 画矩形 */ unsigned int s_x, s_y, e_x, e_y; s_x 0.25 * width; s_y w; e_x width - s_x; e_y height - s_y; for ( ; (s_x e_x) (s_y e_y); s_x5, s_y5, e_x-5, e_y-5) lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF); /* 退出 */ munmap(screen_base, screen_size); close(fd); exit(EXIT_SUCCESS);}main() 函数执行流程open() 打开 /dev/fb0 获取文件描述符 fd。ioctl() 获取 LCD 的可变参数和固定参数信息计算显存大小和分辨率。mmap() 建立显存映射。调用自定义函数在 LCD 上绘图画方块、画线、画矩形。munmap() 取消映射close() 关闭设备文件退出程序。