一、核心概念Frame Buffer 是什么裸机 vs Linux 的显示方式方式做法裸机直接向 LCD 控制器物理地址如0x56000000写像素数据Linux通过/dev/fb0设备用mmap映射显存到用户空间再写虚拟地址Linux 的内存保护机制禁止应用层直接访问物理地址Frame Buffer 就是内核提供的合法通道。mmap 的本质open(/dev/fb0) ↓ mmap() → 用户空间拿到虚拟地址指针 p_mem ↓ 写 p_mem → 内核页表 → 真实显存物理地址 → LCD 自动刷新用mmap之后写内存 写屏幕不需要任何驱动调用效率极高。二、初始化流程三步固定写法代码来自 pro1/framebuffer.c 的 fb_initintfb_init(void){// 第一步打开设备intfd_fbopen(/dev/fb0,O_RDWR);// 第二步获取屏幕参数ioctl(fd_fb,FBIOGET_VSCREENINFO,info);// 第三步映射显存intleninfo.xres_virtual*info.yres_virtual*info.bits_per_pixel/8;pfb(unsignedchar*)mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd_fb,0);}关键参数说明fb_var_screeninfo 结构体字段含义例子xres/yres屏幕实际可见分辨率800 / 600xres_virtual/yres_virtual显存虚拟分辨率通常更大1024 / 600bits_per_pixel每个像素占多少位32为什么要用xres_virtual而不是xres显存实际宽度是xres_virtual不是屏幕可见宽度xres。用错了偏移计算就会错位。两个地方必须用xres_virtual① mmap 大小计算② 像素偏移计算。退出时释放voidfb_deinit(void){munmap(pfb,len);close(fd_fb);}三、画点——所有绘图的基础代码pro1/framebuffer.cvoiddraw_point(unsignedshortx,unsignedshorty,unsignedintcol){if((xinfo.xres_virtual)||(yinfo.yres_virtual))return;// 越界保护unsignedint*p(unsignedint*)(pfb(y*info.xres_virtualx)*info.bits_per_pixel/8);*pcol;}偏移公式推导显存是一维内存按行存储 第 0 行像素(0,0) (1,0) (2,0) ... (xres_virtual-1, 0) 第 1 行像素(0,1) (1,1) ... 像素(x, y) 的字节偏移 (y × xres_virtual x) × (bits_per_pixel / 8) ↑ 把位转成字节32位色 4字节颜色格式32位 ARGB0xFF0000// 红色0x00FF00// 绿色0x0000FF// 蓝色0xFFFFFF// 白色0x000000// 黑色0xFFFF00// 黄色四、想换背景色怎么做用 lcd_fill 填充整个屏幕// 把屏幕全部填成黑色用来清屏lcd_fill(0,0,799,599,0x000000);// 把屏幕全部填成白色lcd_fill(0,0,799,599,0xFFFFFF);// 函数原型来自 framebuffer.hvoidlcd_fill(unsignedshortx0,unsignedshorty0,unsignedshortx1,unsignedshorty1,unsignedintcolor);用 draw_bmp 显示图片背景来自 mouse.c// 把 bg.bmp 从 (0,0) 开始铺满屏幕draw_bmp(0,0,./bg.bmp);典型用法切换背景时先清屏再画// main.c 里的写法lcd_fill(0,0,799,599,0xffff);// 先清屏lcd_show_string(100,200,200,50,32,hello,0xffffff);// 再显示文字sleep(3);lcd_fill(0,0,799,599,0xffff);// 清屏换背景lcd_show_string(100,200,200,50,32,abcde,0xffffff);// 显示新内容五、想显示字符串怎么做函数原型framebuffer.hvoidlcd_show_string(unsignedshortx,// 起始 X 坐标unsignedshorty,// 起始 Y 坐标unsignedshortwidth,// 显示区域宽度超出自动换行unsignedshortheight,// 显示区域高度超出截断unsignedcharsize,// 字体大小12/16/24/32char*p,// 字符串内容unsignedintcol);// 字体颜色示例// 在 (100, 100) 位置显示 hello字体 16白色lcd_show_string(100,100,200,50,16,hello,0xffffff);// 在 (100, 200) 位置显示 world字体 32红色lcd_show_string(100,200,200,50,32,world,0xff0000);字体大小对应像素size字宽字高适用场景126px12px小字密集信息168px16px普通正文2412px24px标题3216px32px大标题注意字符堆叠问题连续显示不同字符串时如果不清屏新字符串会和旧字符串叠在一起。原因是lcd_showchar里只画有笔画的点不画背景。解决方法显示前先用lcd_fill清屏或用copy_mem局部恢复背景见第七节。六、想显示图片怎么做版本一简单版fb/fb.c硬编码尺寸 120×120intdraw_bmp(intx0,inty0,constchar*bmp_name){intfdopen(bmp_name,O_RDWR);unsignedcharhead[54]{0};read(fd,head,sizeof(head));// 跳过 54 字节 BMP 文件头for(j0;j120;j){for(i0;i120;i){unsignedcharc[3]{0};read(fd,c,sizeof(c));// BMP 存储顺序是 BGR转换为 RGBunsignedintcol(c[2]16)|(c[1]8)|c[0];draw_point(ix0,120-j-1y0,col);// BMP 是倒序需要翻转}}}// 调用把图片显示在 (679, 0)draw_bmp(679,0,./123.bmp);版本二通用版mouse.c自动读取宽高// 解析 BMP 文件头自动获取图片宽高BitMapFileHeader file_head;BitMapInfoHeader info_head;read(fd,file_head,sizeoffile_head);read(fd,info_head,sizeofinfo_head);// info_head.biWidth → 图片宽度// info_head.biHeight → 图片高度正数 倒序存储BMP 文件头结构// 文件头 14 字节typedefstruct{unsignedcharbfType[2];// BM 标识unsignedintbfSize;// 文件总大小unsignedshortbfReserved1;// 保留必须为 0unsignedshortbfReserved2;// 保留必须为 0unsignedintbfOffBits;// 像素数据相对文件头的偏移量}BitMapFileHeader;// 共 14 字节需 #pragma pack(1)// 信息头 40 字节typedefstruct{unsignedintbiSize;// 信息头大小40intbiWidth;// 图片宽度像素intbiHeight;// 图片高度正数倒序存储负数正序unsignedshortbiPlanes;// 位面数恒为 1unsignedshortbiBitCount;// 每像素位数24RGB32ARGBunsignedintbiCompression;// 压缩类型0不压缩...}BitMapInfoHeader;为什么 BMP 是上下颠倒的BMP 标准规定图像数据从底部向上存储最后一行数据在文件最前面所以显示时需要height - j - 1翻转 y 坐标。七、想显示鼠标并让鼠标移动怎么做核心思路移动前先恢复背景鼠标移动 在新位置画鼠标图片但旧位置的鼠标覆盖了背景需要先恢复。每次移动 ① copy_mem(旧x, 旧y, 16, 16) ← 从 p_save 恢复旧位置的背景 ② draw_bmp(新x, 新y, mouse.bmp) ← 在新位置画鼠标save_fb 和 copy_mem来自 mouse.c// save_fb把当前整个显存复制到 p_save程序开始时调用一次intsave_fb(){unsignedint*pdst(unsignedint*)p_save;unsignedint*psrc(unsignedint*)p_mem;for(j0;jinfo.yres_virtual;j)for(i0;iinfo.xres_virtual;i)*pdst*psrc;}// copy_mem从 p_save 把指定矩形区域恢复到 p_mem每次移动前调用intcopy_mem(intx0,inty0,intw,inth){for(jy0;jy0h;j)for(ix0;ix0w;i)*(pdstj*xres_virtuali)*(psrcj*xres_virtuali);}内存分配fb_init 里额外 malloc// p_save 是额外分配的内存和显存等大用来保存背景快照p_savemalloc(info.xres_virtual*info.yres_virtual*info.bits_per_pixel/8);读取鼠标坐标来自 mouse.c使用绝对坐标设备intfdopen(/dev/input/event2,O_RDWR);// 触摸屏/绝对鼠标structinput_eventevent;read(fd,event,sizeofevent);if(event.typeEV_ABS){if(event.codeABS_X)xevent.value/65535.0*800;// 原始值 0~65535 → 屏幕 0~800elseif(event.codeABS_Y)yevent.value/65535.0*600;// 原始值 0~65535 → 屏幕 0~600}读取相对鼠标来自 fb/main.c使用 /dev/input/miceintfdopen(/dev/input/mice,O_RDWR);chardata[3]{0};read(fd,data,3);// data[0]按键状态9左键按下10右键按下8左键松开// data[1]X 轴位移有符号负向左// data[2]Y 轴位移有符号负向上xdata[1];// 累加位移ydata[2];if(x0)x0;// 边界保护if(x799)x799;注意save_fb 的局限性save_fb只保存调用那一刻的画面。如果之后又画了字符串再copy_mem会把字符串也擦掉因为快照里没有这些字符串。全屏重绘清屏 重画所有内容是最简单但效率最低的解决方案。八、基本图形绘制pro1/framebuffer.c函数速查表函数用途参数lcd_fill(x0,y0,x1,y1,col)填充矩形区域左上角、右下角坐标、颜色lcd_drawline(x1,y1,x2,y2,col)画直线起点、终点坐标、颜色lcd_draw_rectangle(x1,y1,x2,y2,col)画矩形边框左上角、右下角坐标、颜色lcd_draw_Circle(x0,y0,r,col)画圆形边框圆心坐标、半径、颜色lcd_showchar(x,y,ch,size,mode,col)显示单个字符坐标、字符、大小、叠加模式、颜色lcd_show_string(x,y,w,h,size,str,col)显示字符串坐标、区域宽高、大小、内容、颜色lcd_shownum(x,y,num,len,size,col)显示整数高位0不显示坐标、数值、位数、大小、颜色lcd_showxnum(x,y,num,len,size,mode,col)显示整数可控制高位0同上模式lcd_showchar 的 mode 参数mode0;// 非叠加背景色为黑色字符区域黑底mode1;// 叠加背景透明只画有笔画的点不覆盖底色九、三色条纹测试最简单的全屏渲染验证 FB 是否工作来自 pro1/main.c 的 DEBUG 版本验证 Frame Buffer 初始化是否正常for(j0;jinfo.yres;j){for(i0;iinfo.xres;i){if(jinfo.yres/3)draw_point(i,j,0xff);// 上 1/3蓝色elseif(jinfo.yres*2/3)draw_point(i,j,0xff00);// 中 1/3绿色elsedraw_point(i,j,0xff0000);// 下 1/3红色}}十、编译运行# PC 上测试需要 root 权限CtrlAltF2 切到纯文本终端gcc fb.c-ofb_testsudo./fb_test# 交叉编译开发板运行arm-linux-gnueabihf-gcc fb.c-ofb_test# 传到开发板直接运行开发板不需要切终端./fb_test# pro1 多文件编译arm-linux-gnueabihf-gcc main.c framebuffer.c-omainPC 运行注意图形界面X11/Wayland独占显存必须先CtrlAltF2切换到 TTY 纯文本终端后再运行否则没有效果或权限报错。十一、常见问题问题原因解决运行没有任何显示在图形界面下运行CtrlAltF2切换到 TTY颜色显示不对bpp 不是 32颜色格式不同用ioctl查看bits_per_pixelRGB565 需要位操作转换图片显示上下颠倒忘记翻转 y 坐标用height - j - 1 y0代替j y0图片颜色偏BMP 是 BGR 存储直接用成了 RGB用 (c[2]16)字符显示堆叠没清屏就切换内容显示前先调用lcd_fill清屏鼠标移动有残影没有恢复旧位置背景移动前先调用copy_mem恢复背景