023 嵌入式Linux应用开发——文件 IO之Framebuffer 应用编程实战(下)

张开发
2026/4/13 3:15:51 15 分钟阅读

分享文章

023 嵌入式Linux应用开发——文件 IO之Framebuffer 应用编程实战(下)
00 Framebuffer 核心原理先把根搞懂1 核心概念Framebuffer帧缓冲是 Linux 内核提供给应用层的LCD 显示设备统一抽象接口本质是一块和LCD 屏幕的像素点一一对应显存内存区域。内核驱动把 LCD 硬件抽象成/dev/fb0这个字符设备文件应用层通过文件 IO 操作它完全不用管底层硬件时序、寄存器Framebuffer 里存储着一帧图像的所有像素值修改这块内存LCD 就会实时显示对应的内容和 MCU 里操作 LCD 显存的逻辑完全一致核心哲学操作 LCD 操作/dev/fb0设备文件 操作映射后的内存往这块内存里写入什么颜色值屏幕对应位置就会显示什么颜色完全不用关心底层是 RGB 屏、LVDS 屏还是 MIPI 屏应用层操作逻辑完全通用。和 MCU 的区别MCU 里你直接操作 LCD 的物理 GRAMLinux 里用户空间不能直接访问内核显存必须通过映射的方式间接操作。2. 核心参数与像素地址计算基础中的基础1核心参数分辨率xres水平像素数、yres垂直像素数比如 1024*768像素深度 BPPbits per pixel每个像素用多少位表示嵌入式常用 3 种BPP格式说明嵌入式常用场景32BPPARGB888832 位高 8 位透明通道低 24 位 RGB各 8 位高分辨率屏GUI 开发24BPPRGB88824 位纯 RGB无透明通道Framebuffer 中用 32 位存储通用彩色屏16BPPRGB56516 位R (5) G (6) B (5)最省内存嵌入式低功耗小屏最常用显存总大小分辨率宽 * 分辨率高 * BPP / 8比如 1024*768 32BPP 的屏显存大小就是1024*768*32/8 3145728字节和教材里的计算一致。秋招考点16BPP RGB565 格式的像素值拼接// 从 32 位 color 中提取 R/G/B拼接成 16 位 RGB565 uint16_t rgb565 ((color 19) 0x1F) 11 | // R(5位) 11 ((color 10) 0x3F) 5 | // G(6位) 5 ((color 3) 0x1F); // B(5位)2像素地址计算公式画点的核心要在 LCD 坐标(x, y)处画点必须先算出该像素在 Framebuffer 中的内存地址公式如下// fb_basemmap 得到的 Framebuffer 基地址 // xresLCD 水平分辨率屏幕宽度 // bpp每像素位数bits per pixel如 16/24/32 像素坐标 (x, y) 显存地址 fb_base (y * xres x) * (bpp / 8)示例1024×768 分辨率32BPP(100, 50) 坐标的地址 fb_base (50*1024 100) * 43. LCD 操作的总流程驱动层LCD 驱动初始化 LCD 控制器根据屏幕分辨率、BPP每像素位数分配 Framebuffer 显存应用层获取参数APP 通过ioctl向驱动获取 LCD 的分辨率、BPP、RGB 格式等参数内存映射APP 通过mmap把/dev/fb0对应的内核显存映射到用户态的虚拟内存地址像素操作APP 直接读写映射后的内存修改像素颜色LCD 实时刷新显示资源释放程序退出时munmap解除映射close关闭设备文件二、核心 API 详解Framebuffer 开发的 4 个核心函数你已经学过文件 IO这几个函数都是基于文件 IO 的重点记它们在 Framebuffer 场景下的专属用法、坑点和秋招考点。1. open 函数打开 Framebuffer 设备int fd open(/dev/fb0, O_RDWR); // 必须用读写权限因为要写显存设备节点嵌入式里固定是/dev/fb0第一个显示设备返回值成功返回非负的文件描述符fd失败返回-1必做操作必须判断返回值perror(open /dev/fb0 failed)漏了错误处理秋招笔试直接扣分秋招考点Framebuffer 属于字符设备Linux 一切皆文件通过设备文件访问为什么用O_RDWR因为既要读显存如截图又要写显存显示内容2. ioctl 函数获取 LCD 设备参数这是应用层和驱动交互的核心用来拿屏幕的分辨率、BPP 等参数绝对不能在代码里写死参数不然换屏就炸。#include sys/ioctl.h #include linux/fb.h // 核心头文件定义了 fb_var_screeninfo 等结构体 struct fb_var_screeninfo var; int ret ioctl(fd, FBIOGET_VSCREENINFO, var);关键结构体fb_var_screeninfo必须掌握的成员struct fb_var_screeninfo { __u32 xres; // 水平分辨率宽度 __u32 yres; // 垂直分辨率高度 __u32 bits_per_pixel;// BPP每像素位数16/24/32 __u32 offset; // 帧缓冲偏移 // RGB 位偏移16BPP 时 __u32 red.offset; // R 分量在像素中的起始位 __u32 red.length; // R 分量位数5 __u32 green.offset; // G 分量起始位 __u32 green.length; // G 分量位数6 __u32 blue.offset; // B 分量起始位 __u32 blue.length; // B 分量位数5 // ... 其他成员 };Framebuffer 场景下 2 个必背命令秋招 100% 会考命令宏作用核心获取的参数FBIOGET_VSCREENINFO获取 LCD 可变参数xres(宽)、yres(高)、bits_per_pixel(BPP 像素深度)FBIOGET_FSCREENINFO获取 LCD 固定参数smem_len(显存总大小)、smem_start(显存物理地址)秋招考点两个FBIOGET_*宏的作用struct fb_var_screeninfo的核心成员ioctl和read/write的区别(read/write做通用数据读写ioctl做设备专属控制获取参数、配置硬件是字符设备驱动的专属接口)3. mmap 函数显存映射这是和 MCU 操作最大的区别也是 Framebuffer 高效的核心。Linux 用户空间不能直接访问内核空间的内存必须通过 mmap 把内核的显存物理地址映射成用户空间的虚拟地址让应用层直接操作内核显存不用每次都做系统调用效率极高#include sys/mman.h // 计算 Framebuffer 总大小 size_t screen_size var.xres * var.yres * var.bits_per_pixel / 8; // 映射把内核显存映射到用户态虚拟地址 uint8_t *fb_base mmap( NULL, // 让系统自动分配映射地址 screen_size, // 映射长度显存总大小 PROT_READ | PROT_WRITE, // 映射区域可读可写 MAP_SHARED, // 共享映射修改内存会同步到文件显存必须用这个 fd, // /dev/fb0 的文件描述符 0 // 从文件起始位置映射 );核心参数说明面试必问PROT_READ | PROT_WRITE必须同时加才能读写显存MAP_SHARED绝对不能用MAP_PRIVATE私有映射不会把修改同步到显存LCD 不会显示返回值成功返回映射后的基地址fb_base失败返回MAP_FAILED(void *)-1,不是 NULL笔试选择题高频坑点必做操作必须判断返回值是否为MAP_FAILED秋招考点重灾区全背下来mmap的原理和read/write相比的优势(原理把内核空间的文件 / 显存映射到用户态虚拟地址空间用户态直接读写内存减少系统调用次数优势大文件 / 显存操作效率高适合 LCD 这种高频刷新的场景)MAP_SHARED和MAP_PRIVATE的区别为什么 Framebuffer 必须用MAP_SHAREDmmap 失败返回MAP_FAILED不是 NULL面试必问为什么用 mmap不用 read/write 操作/dev/fb0标准答案mmap 是零拷贝直接把内核显存映射到用户空间操作内存无需经过系统调用拷贝效率极高刷屏无卡顿read/write 每次都要在用户和内核空间之间拷贝数据高分辨率屏刷新一次要拷贝数兆数据效率极低完全无法满足显示需求。4. 收尾函数munmap close// 解除映射 munmap(fb_base, screen_size); // 关闭设备文件 close(fd);munmap必须手动释放 mmap 的映射否则会造成内存泄漏、文件描述符泄漏嵌入式长期运行程序会崩溃原型int munmap(void *addr, size_t length);用法munmap(fb_base, screen_size);地址和长度必须和 mmap 的参数完全一致close关闭打开的文件描述符用法close(fd);注意必须先 munmap再 close顺序不能反秋招考点不释放 mmap 映射会导致内存泄漏选择题常考三、核心功能实现画点函数所有显示的基础笔试常考所有的显示功能清屏、画线、画图形、显示图片都是基于画点函数实现的必须吃透。函数作用给指定 (x,y) 坐标设置指定的颜色适配不同 BPP 的屏幕。标准实现秋招笔试直接用// 全局变量初始化时赋值 unsigned char *fb_base; // mmap映射后的显存基地址 struct fb_var_screeninfo var; // 屏幕参数 void lcd_put_pixel(int x, int y, unsigned int color) { // 【面试加分项】边界判断防止内存越界段错误 if (x 0 || x var.xres || y 0 || y var.yres) return; // 计算像素对应的内存地址 unsigned int offset (y * var.xres x) * (var.bits_per_pixel / 8); unsigned char *pixel_addr fb_base offset; // 根据BPP写入颜色值 switch (var.bits_per_pixel) { case 16: // RGB565 { unsigned short rgb565_color; // 32位ARGB8888转16位RGB565 unsigned short r (color 16) 0xFF; unsigned short g (color 8) 0xFF; unsigned short b color 0xFF; rgb565_color ((r 3) 11) | ((g 2) 5) | (b 3); *(unsigned short *)pixel_addr rgb565_color; break; } case 24: // RGB888 { pixel_addr[0] color 0xFF; // B pixel_addr[1] (color 8) 0xFF; // G pixel_addr[2] (color 16) 0xFF;// R break; } case 32: // ARGB8888 { *(unsigned int *)pixel_addr color; break; } default: printf(不支持的BPP格式\n); break; } }秋招考点笔试常让你写适配 RGB565 的画点函数必须记住 RGB565 的位分配R 高 5 位、G 中间 6 位、B 低 5 位颜色格式转换的位运算32 位 ARGB8888 转 16 位 RGB565 的实现边界判断的作用防止内存越界触发段错误面试会问优化点四、完整的 Framebuffer 应用开发标准流程一步都不能少错误处理必须全面试官就看这个。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include sys/mman.h #include linux/fb.h #include string.h // 全局变量 unsigned char *fb_base; struct fb_var_screeninfo var; unsigned int screen_size; // 上面写的画点函数 void lcd_put_pixel(int x, int y, unsigned int color) { // 实现同上省略 } int main(int argc, char *argv[]) { // 步骤1打开Framebuffer设备 int fd open(/dev/fb0, O_RDWR); if (fd 0) { perror(open /dev/fb0 failed); return -1; } // 步骤2获取LCD可变参数 if (ioctl(fd, FBIOGET_VSCREENINFO, var) 0) { perror(ioctl FBIOGET_VSCREENINFO failed); close(fd); return -1; } printf(屏幕参数%d*%d, BPP:%d\n, var.xres, var.yres, var.bits_per_pixel); // 步骤3计算显存大小mmap映射 screen_size var.xres * var.yres * var.bits_per_pixel / 8; fb_base mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb_base MAP_FAILED) { perror(mmap failed); close(fd); return -1; } // 步骤4功能实现比如清屏成红色 memset(fb_base, 0, screen_size); // 先清屏 for (int y 0; y var.yres; y) { for (int x 0; x var.xres; x) { lcd_put_pixel(x, y, 0xFFFF0000); // 32位ARGB红色 } } // 停留5秒看效果 sleep(5); // 步骤5释放资源收尾 munmap(fb_base, screen_size); close(fd); return 0; }完整版备用/* 请手写实现LCD画点函数 lcd_put_pixel ·功能:在坐标(x,y)绘制指定颜色支持16BPP(RGB565)、32BPP格式 参数:int x,int y,uint32_t color 要求:正确计算像素内存地址完成RGB565颜色拼接 全局变量可直接使用:fb_base(显存基地址)、var(LCD参数结构体) */ #include fcntl.h #include stdio.h #include unistd.h #include sys/mman.h #include sys/ioctl.h #include linux/fb.h #include string.h #include errno.h #include stdint.h unsigned char *fb_base; unsigned long request; struct fb_var_screeninfo var; // 屏幕参数 void lcd_put_pixel(int x,int y,uint32_t color) { // 越界判断 // 更规范、更易读的是先x后y if (x 0 || y 0 || x var.xres || y var.yres) { return ; } // 像素偏移 unsigned int offset (x y * var.xres) * (var.bits_per_pixel / 8); unsigned char *pixel_addr fb_base offset; switch(var.bits_per_pixel) { case 32: { *(uint32_t *)pixel_addr color; break; } case 24: { pixel_addr[2] (color 16) 0xff; pixel_addr[1] (color 8) 0xff; pixel_addr[0] color 0xff; break; } case 16: { uint16_t r (color 19) 0x1f; uint16_t g (color 10) 0x3f; uint16_t b (color 3) 0x1f; uint16_t rgb565 (r 11) | (g 5) | b ; *(uint16_t *)pixel_addr rgb565; break; } default : fprintf(stderr, unsupported bpp: %d\n, var.bits_per_pixel); break; } } /********************************************************************** * 函数名lcd_draw_rect * 功能画实心矩形 * 参数x1,y1 左上角坐标x2,y2 右下角坐标color 颜色 * 返回无 **********************************************************************/ void lcd_draw_rect(int x1, int y1, int x2, int y2, uint32_t color) { // 坐标校正保证x1x2, y1y2 if (x1 x2) { int tmp x1; x1 x2; x2 tmp; } if (y1 y2) { int tmp y1; y1 y2; y2 tmp; } // 遍历矩形内所有点循环画点 for (int y y1; y y2; y) for (int x x1; x x2; x) lcd_put_pixel(x, y, color); } /********************************************************************** * 函数名lcd_draw_line * 功能画直线简化版秋招够用不用Bresenham * 参数x1,y1 起点x2,y2 终点color 颜色 * 返回无 **********************************************************************/ void lcd_draw_line(int x1, int y1, int x2, int y2, uint32_t color) { int dx x2 - x1; int dy y2 - y1; // 斜率绝对值 1以x为步长 if (abs(dx) abs(dy)) { if (x1 x2) { int tmp x1; x1 x2; x2 tmp; tmp y1; y1 y2; y2 tmp; } for (int x x1; x x2; x) { int y y1 (long long)(x - x1) * dy / dx; // 用long long防止溢出 lcd_put_pixel(x, y, color); } } // 斜率绝对值 1以y为步长 else { if (y1 y2) { int tmp x1; x1 x2; x2 tmp; tmp y1; y1 y2; y2 tmp; } for (int y y1; y y2; y) { int x x1 (long long)(y - y1) * dx / dy; lcd_put_pixel(x, y, color); } } } /********************************************************************** * 函数名lcd_draw_circle * 功能画实心圆简化版 * 参数x0,y0 圆心r 半径color 颜色 * 返回无 **********************************************************************/ void lcd_draw_circle(int x0, int y0, int r, uint32_t color) { // 遍历圆的外接矩形内的所有点 for (int y y0 - r; y y0 r; y) for (int x x0 - r; x x0 r; x) { // 圆的方程(x-x0)^2 (y-y0)^2 r^2 int dx x - x0; int dy y - y0; if (dx*dx dy*dy r*r) lcd_put_pixel(x, y, color); } } int main(int argc, char *argv[]) { int fd_fb; // 打开文件 fd_fb open(/dev/fb0,O_RDWR); if (fd_fb 0) { perror(open /dev/fb0 failed); return -1; } // 获取屏幕参数 if (ioctl(fd_fb, FBIOGET_VSCREENINFO, var)) { close(fd_fb); return -1; } // 内存映射 size_t screen_size var.xres * var.yres * var.bits_per_pixel / 8; fb_base mmap(NULL, screen_size, PROT_WRITE | PROT_READ ,MAP_SHARED,fd_fb,0 ); if (fb_base MAP_FAILED) { perror(mmap failed); close(fd_fb); return -1; } memset(fb_base, 0xff, screen_size); // 画图 lcd_draw_rect(100, 100, 300, 200, 0xFF0000); // 红色矩形 lcd_draw_line(0, 0, var.xres, var.yres, 0x00FF00); // 绿色对角线 lcd_draw_circle(var.xres/2, var.yres/2, 100, 0x0000FF); // 蓝色圆心圆 munmap(fb_base,screen_size); close(fd_fb); return 0; }五、嵌入式 Linux Framebuffer 秋招考点笔记核心概念必背・一句话速记FramebufferLinux 内核提供的 LCD 显示抽象接口本质是显存对应/dev/fb0字符设备应用层通过open/ioctl/mmap操作无需管底层硬件。编程 5 步流程打开/dev/fb0→ioctl获取参数 →mmap映射显存 → 读写内存画点 →munmapclose释放资源。MAP_SHARED vs MAP_PRIVATESHARED共享映射修改同步到显存LCD 可显示必用。PRIVATE私有映射修改不同步LCD 无显示。ioctl vs read/writeioctl字符设备专属控制获取 / 设置设备参数。read/write通用数据读写无设备控制能力。RGB56516 位格式R5 位 G6 位 B5 位公式(R11)|(G5)|B。笔试选择题高频考点速记Framebuffer 设备节点/dev/fb0属于字符设备。打开设备 flagO_RDWR读写。获取可变参数 ioctl 命令FBIOGET_VSCREENINFO。mmap 必用标志MAP_SHARED。mmap 失败返回MAP_FAILED非 NULL。RGB565 位数R5、G6、B5。映射内存释放必须调用munmap。笔试编程题高频考点手写必练Framebuffer 完整初始化流程带错误处理。兼容 16/24/32BPP 的画点函数。指定颜色清屏函数全黑 / 全白。基于画点的画线、画矩形函数。mmap 完整参数及含义。面试问答题高频考点口述满分什么是 Framebuffer作用是什么答内核提供的 LCD 抽象接口将显存封装为字符设备/dev/fb0应用层通过标准文件 IO 操作屏幕简化显示开发。为什么用 mmap 不用 read/write 操作 LCD答mmap 将内核显存映射到用户空间直接读写内存无频繁系统调用效率远高于 read/write适合大屏高频刷新。为何必须用 MAP_SHARED答共享映射可将用户态内存修改同步到内核显存LCD 才能实时显示PRIVATE 不同步无法显示。应用层操作 Framebuffer 完整流程答open 打开设备→ioctl 获取分辨率 / BPP→mmap 映射显存→操作内存画点 / 图→munmap 解除映射→close 关闭设备。fb_var_screeninfo vs fb_fix_screeninfo答var可变参数分辨率、BPP、RGB 位偏移。fix固定参数显存物理地址、行长度、显存总大小。画点为何要做边界判断不做会怎样答防止坐标超出屏幕范围越界写内存会导致显存数据错乱、程序段错误。如何实现汉字显示答用 freetype 字库获取汉字点阵遍历点阵在对应坐标画点。如何避免画面闪烁答双缓冲机制 —— 后台内存绘图绘制完成后一次性拷贝到 Framebuffer 显存减少刷新次数。Framebuffer 与 MCU 裸机 LCD 驱动区别答MCU 需手动操作寄存器、时序Linux 由内核驱动完成底层应用层只操作文件和内存开发快、移植性强。五、师傅给你的学习建议MCU 转型专属抓住本质和 MCU 知识联动Framebuffer 本质就是「Linux 帮你写好了 LCD 驱动你只需要操作显存」和你之前写的 MCU 屏驱逻辑完全一致只是不用管寄存器了把这个思维打通学起来飞快先跑通代码再抠细节先把上面的完整代码跑通在 Ubuntu 虚拟控制台看到画线效果再去研究 BPP 格式、地址计算这些细节不要一开始就死磕原理秋招优先级先把流程、核心 API、画点代码吃透保证笔试手写不丢分再去研究双缓冲、汉字显示这些进阶内容给面试加分为后续章节铺垫这章是第 6 章「文字显示freetype」、GUI 开发如 Qt/ LVGL的基础把 Framebuffer 吃透后面学 GUI 会非常轻松六、后续学习规划下一步基于这个画点函数实现画线、画矩形、清屏、截图等基础功能做一个简单的 LCD 绘图工具再下一步学习第 6 章 freetype 字体显示用 Framebuffer 实现汉字、英文的行显示做一个简单的文本显示程序秋招前把 Framebuffer 原理、流程、代码背熟面试能完整说清笔试能手写画点函数小伙子这章是你 Linux 应用层硬件操作的关键一步把它吃透后面的输入系统、网络、I2C 都是一样的「文件 IO 驱动抽象」逻辑一通百通有任何代码问题、原理问题随时来问我。要不要我给你补充一份Framebuffer 常用工具函数画线、画矩形、清屏、截图的完整代码直接可以用到项目里

更多文章