直接写显存:用 framebuffer 构建高性能嵌入式显示系统
你有没有遇到过这样的场景?
一块 Cortex-A7 的开发板,配上 480×272 的小屏幕,跑个 Qt 界面却卡得像幻灯片;或者医疗设备要求“上电即显”,结果等了十几秒才跳出第一个画面。问题出在哪?不是硬件不行,而是图形路径太绕了。
在这些对性能和启动速度敏感的嵌入式项目中,我们其实可以绕开 X11、Wayland、Qt 这些庞然大物,直接动手改显存——这就是framebuffer的用武之地。
为什么还要用 framebuffer?
你说现在都 2025 年了,不是都在推 DRM/KMS 和 Weston 吗?没错,但现实是:很多工业设备、自助终端、车载仪表盘仍然跑着 Linux + framebuffer 的组合。为什么?
因为简单、快、稳。
传统 GUI 栈像一条高速公路,要经过收费站(X Server)、服务区(合成器)、导航系统(窗口管理器)才能到达目的地。而 framebuffer 是一条乡间小道,虽然没路灯也没护栏,但它直通屏幕,没有中间商赚时间差。
谁在用 framebuffer?
- 医疗监护仪:开机 2 秒内必须出波形图
- 工业 HMI:按钮点击后响应延迟不能超过 50ms
- 自助售货机:低功耗待机 + 快速唤醒显示
- 车载倒车影像:视频叠加与 UI 共屏,实时性优先
这些场景不需要多窗口、不需要透明特效,它们只关心一件事:我改了一个像素,它什么时候能出现在屏幕上?
答案是:下一帧。
framebuffer 到底是什么?
你可以把它理解为一块“镜像内存”——内核为显示控制器划出一段连续的物理内存,里面存的就是你要显示的每一个像素。只要你能往这块内存里写数据,屏幕就会跟着变。
Linux 把这块区域抽象成一个字符设备:/dev/fb0。
对,就是文件。你可以open()它,ioctl()查询参数,甚至用mmap()映射到你的进程空间,然后像操作数组一样画画。
char *fbp = mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);这行代码之后,fbp就是指向显存的指针。你想画红点?好,计算偏移,写进去:
*(uint16_t*)(fbp + location) = 0xF800; // RGB565 红色就这么直接。没有回调,没有事件循环,也没有上下文切换开销。
关键参数从哪来?别猜,去问内核!
很多人第一次写 framebuffer 程序时都会犯一个错误:硬编码分辨率或色深。结果程序换台设备就花屏,颜色发绿,甚至段错误。
正确做法是:一切参数都通过ioctl向内核查询。
有两个结构体你需要熟记:
struct fb_var_screeninfo—— 可变信息
| 字段 | 含义 |
|---|---|
xres,yres | 实际分辨率 |
bits_per_pixel | 每像素位数(16/32 最常见) |
red.offset,blue.length | RGB 分量在像素中的位置(决定格式) |
示例:如果
bits_per_pixel=16,red.offset=11,green.offset=5,那基本就是 RGB565。
struct fb_fix_screeninfo—— 固定信息
| 字段 | 含义 |
|---|---|
line_length | 每行字节数(注意不一定等于 xres × bpp) |
smem_start | 显存起始物理地址(调试用) |
smem_len | 总显存大小 |
有了这两个结构体,你就能动态适配任何支持 fbdev 的平台。
内存映射怎么算?别让偏移搞崩你的画面
最核心的公式是:
location = (x + xoffset) * bytes_per_pixel + (y + yoffset) * line_length;其中:
-xoffset/yoffset是虚拟显示偏移(用于双缓冲)
-line_length很关键!有些驱动为了对齐会补零,所以不能用xres * bpp替代
举个例子,假设:
- 分辨率:800×480
- 格式:RGB565(2 字节/像素)
-line_length = 1600(刚好 800×2)
那么第 (100, 50) 像素的位置就是:
loc = 100 * 2 + 50 * 1600 = 200 + 80000 = 80200直接往fbp + loc写两个字节就行。
⚠️ 注意陷阱:
- 如果你误用了xres * bpp而不是line_length,在某些 Allwinner 或 Rockchip 平台上会越界访问。
- 多字节写入记得考虑 CPU 大小端问题(ARM 通常是小端,放心)。
如何实现无撕裂刷新?试试双缓冲
如果你直接往前台缓冲写数据,用户可能会看到“半幅旧图 + 半幅新图”的撕裂现象。怎么办?
老办法:双缓冲 + pan_display。
原理很简单:
1. 显存分配两倍高度(比如 yres_virtual = 2 * yres)
2. 前台显示前一半,后台在后一半绘图
3. 画完后调用VIA_PAN_DISPLAY把yoffset改成yres,瞬间切换
代码片段如下:
// 设置双缓冲模式 vinfo.yres_virtual = vinfo.yres * 2; ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo); // 提交变更 // 绘图时使用后半区 int buffer_offset = vinfo.yres * finfo.line_length; draw_to_buffer(fbp + buffer_offset); // 切换显示 vinfo.yoffset = vinfo.yres; ioctl(fbfd, FBIOPAN_DISPLAY, &vinfo);这样就能做到视觉上“瞬切”,避免闪烁和撕裂。
⚠️ 并非所有驱动都支持
FBIOPAN_DISPLAY,尤其是基于 DRM 的兼容层。上线前务必实测。
实战:画个矩形有多快?
来看一段极简的绘图代码,目标是在屏幕上画一个红色方块:
#include <stdio.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <linux/fb.h> int main() { int fd = open("/dev/fb0", O_RDWR); if (fd < 0) { perror("open"); return -1; } struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); ioctl(fd, FBIOGET_FSCREENINFO, &finfo); printf("Res: %dx%d, BPP: %d, Line: %d\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, finfo.line_length); long size = (long)vinfo.yres_virtual * finfo.line_length; void *fbp = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); int bpp = vinfo.bits_per_pixel / 8; uint16_t red = 0xF800; // RGB565 for (int y = 100; y < 200; y++) { for (int x = 100; x < 300; x++) { long loc = x*bpp + y*finfo.line_length; *(uint16_t*)(fbp + loc) = red; } } munmap(fbp, size); close(fd); return 0; }编译运行后,你会立刻看到一个红框出现在屏幕上——整个过程不依赖任何图形库,连 libcairo 都不需要链接。
这种“裸奔式”绘图,在资源受限设备上极具优势。
工程实践中的坑与秘籍
🔹 坑一:颜色不对,屏幕发绿?
可能是 RGB 和 BGR 顺序搞反了。查fb_var_screeninfo中red.offset和blue.offset的值。如果是 BGR 格式,你就得把颜色反转一下:
// BGR565:蓝在低5位,红在高5位 color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);🔹 坑二:程序退出后屏幕花屏?
记得清理现场!要么清屏,要么至少还原原始yoffset。否则下一个应用读到的是你残留的数据。
🔹 坑三:多个程序抢/dev/fb0?
Linux 不阻止多进程打开同一个 fb 设备。后果就是互相覆盖。解决方案:
- 使用互斥锁文件(如/tmp/.fb_lock)
- 或由主进程统一管理 framebuffer,其他通过 IPC 发送绘图指令
🔹 坑四:CPU 缓存导致更新看不到?
某些 SoC(如 TI AM335x)有 write-back 缓存策略。你在用户空间写了内存,但还没刷到物理 RAM。解决方法:
- 使用O_SYNC标志打开设备
- 或调用__builtin___clear_cache()(GCC 扩展)
- 更推荐:确保 D-cache 一致性策略配置正确(通常内核已处理)
适合用 framebuffer 的三大典型场景
✅ 场景一:静态 UI + 局部刷新
比如设置菜单、状态面板。大部分区域不变,只需重绘变化部分。配合“脏矩形”机制,效率极高。
✅ 场景二:实时数据可视化
波形图、仪表盘、进度条。每帧只更新少量扫描线或指针位置,完全可控。
✅ 场景三:快速启动出图
只要内核加载完 fbdev 驱动,应用就能立即绘图。配合 initramfs,可实现“内核启动完毕 → 1 秒内出 Logo”。
它会被淘汰吗?未来在哪里?
官方确实在推动 DRM/KMS 替代 fbdev,后者已被标记为“legacy”。但在实际项目中,framebuffer 仍有不可替代的优势:
- 兼容性极强:几乎所有嵌入式 Linux 内核都默认启用
CONFIG_FB - 文档丰富,社区案例多
- 不依赖复杂的 mode-setting 流程
- 在 RTOS 或轻量级系统中更容易移植
而且,DRM 本身也可以模拟 framebuffer 行为(viadumb buffer),说明这种“直接映射+简单绘图”的模型依然有价值。
未来趋势可能是:
- 新项目用 DRM dumb buffer + mmap
- 老项目继续维护 fbdev
- 开发者用 Rust/C++ 封装安全的 framebuffer 库(避免裸指针越界)
但无论接口如何演进,“贴近硬件、去除冗余”的设计哲学不会过时。
结语:掌握底层,才能掌控体验
当你不再依赖层层封装的图形框架,而是亲手把一个个像素写进显存时,你会重新理解什么叫“控制力”。
framebuffer 不是最先进的技术,但它足够简单、足够快、足够可靠。对于那些不能容忍卡顿、不能浪费内存、不能等待服务初始化的系统来说,它是真正的利器。
下次你在树莓派上跑不动 Qt,不妨试试关掉桌面环境,直接操作/dev/fb0。也许你会发现,最快的图形库,是你自己写的那一行*(fbp + loc) = color;。
如果你正在做嵌入式显示开发,欢迎留言交流实战经验。你是坚持用 fbdev,还是已经转向 DRM?遇到了哪些坑?一起聊聊。