一、核心概念: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_init)
intfb_init(void){// 第一步:打开设备intfd_fb=open("/dev/fb0",O_RDWR);// 第二步:获取屏幕参数ioctl(fd_fb,FBIOGET_VSCREENINFO,&info);// 第三步:映射显存intlen=info.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 / 600 |
xres_virtual/yres_virtual | 显存虚拟分辨率(通常更大) | 1024 / 600 |
bits_per_pixel | 每个像素占多少位 | 32 |
为什么要用
xres_virtual而不是xres?
显存实际宽度是xres_virtual,不是屏幕可见宽度xres。用错了偏移计算就会错位。
两个地方必须用xres_virtual:① mmap 大小计算;② 像素偏移计算。
退出时释放
voidfb_deinit(void){munmap(pfb,len);close(fd_fb);}三、画点——所有绘图的基础
代码(pro1/framebuffer.c)
voiddraw_point(unsignedshortx,unsignedshorty,unsignedintcol){if((x>info.xres_virtual)||(y>info.yres_virtual))return;// 越界保护unsignedint*p=(unsignedint*)(pfb+(y*info.xres_virtual+x)*info.bits_per_pixel/8);*p=col;}偏移公式推导
显存是一维内存,按行存储: 第 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位 ARGB)
0xFF0000// 红色0x00FF00// 绿色0x0000FF// 蓝色0xFFFFFF// 白色0x000000// 黑色0xFFFF00// 黄色四、想换背景色怎么做
用 lcd_fill 填充整个屏幕
// 把屏幕全部填成黑色(用来清屏)lcd_fill(0,0,799,599,0x000000);// 把屏幕全部填成白色lcd_fill(0,0,799,599,0xFFFFFF);// 函数原型(来自 framebuffer.h)voidlcd_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.h)
voidlcd_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 | 字宽 | 字高 | 适用场景 |
|---|---|---|---|
| 12 | 6px | 12px | 小字,密集信息 |
| 16 | 8px | 16px | 普通正文 |
| 24 | 12px | 24px | 标题 |
| 32 | 16px | 32px | 大标题 |
注意:字符堆叠问题
连续显示不同字符串时,如果不清屏,新字符串会和旧字符串叠在一起。原因是lcd_showchar里只画"有笔画"的点,不画背景。
解决方法:显示前先用lcd_fill清屏(或用copy_mem局部恢复背景,见第七节)。
六、想显示图片怎么做
版本一:简单版(fb/fb.c,硬编码尺寸 120×120)
intdraw_bmp(intx0,inty0,constchar*bmp_name){intfd=open(bmp_name,O_RDWR);unsignedcharhead[54]={0};read(fd,head,sizeof(head));// 跳过 54 字节 BMP 文件头for(j=0;j<120;j++){for(i=0;i<120;i++){unsignedcharc[3]={0};read(fd,c,sizeof(c));// BMP 存储顺序是 BGR,转换为 RGBunsignedintcol=(c[2]<<16)|(c[1]<<8)|c[0];draw_point(i+x0,120-j-1+y0,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;// 信息头大小(40)intbiWidth;// 图片宽度(像素)intbiHeight;// 图片高度,正数=倒序存储,负数=正序unsignedshortbiPlanes;// 位面数,恒为 1unsignedshortbiBitCount;// 每像素位数:24=RGB,32=ARGBunsignedintbiCompression;// 压缩类型: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(j=0;j<info.yres_virtual;j++)for(i=0;i<info.xres_virtual;i++)*pdst++=*psrc++;}// copy_mem:从 p_save 把指定矩形区域恢复到 p_mem(每次移动前调用)intcopy_mem(intx0,inty0,intw,inth){for(j=y0;j<y0+h;j++)for(i=x0;i<x0+w;i++)*(pdst+j*xres_virtual+i)=*(psrc+j*xres_virtual+i);}内存分配(fb_init 里额外 malloc)
// p_save 是额外分配的内存,和显存等大,用来保存背景快照p_save=malloc(info.xres_virtual*info.yres_virtual*info.bits_per_pixel/8);读取鼠标坐标(来自 mouse.c,使用绝对坐标设备)
intfd=open("/dev/input/event2",O_RDWR);// 触摸屏/绝对鼠标structinput_eventevent;read(fd,&event,sizeofevent);if(event.type==EV_ABS){if(event.code==ABS_X)x=event.value/65535.0*800;// 原始值 0~65535 → 屏幕 0~800elseif(event.code==ABS_Y)y=event.value/65535.0*600;// 原始值 0~65535 → 屏幕 0~600}读取相对鼠标(来自 fb/main.c,使用 /dev/input/mice)
intfd=open("/dev/input/mice",O_RDWR);chardata[3]={0};read(fd,data,3);// data[0]:按键状态(9=左键按下,10=右键按下,8=左键松开)// data[1]:X 轴位移(有符号,负=向左)// data[2]:Y 轴位移(有符号,负=向上)x+=data[1];// 累加位移y+=data[2];if(x<0)x=0;// 边界保护if(x>799)x=799;注意: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 参数
mode=0;// 非叠加:背景色为黑色(字符区域黑底)mode=1;// 叠加:背景透明,只画有笔画的点,不覆盖底色九、三色条纹测试(最简单的全屏渲染,验证 FB 是否工作)
来自 pro1/main.c 的 DEBUG 版本,验证 Frame Buffer 初始化是否正常:
for(j=0;j<info.yres;j++){for(i=0;i<info.xres;i++){if(j<info.yres/3)draw_point(i,j,0xff);// 上 1/3:蓝色elseif(j<info.yres*2/3)draw_point(i,j,0xff00);// 中 1/3:绿色elsedraw_point(i,j,0xff0000);// 下 1/3:红色}}十、编译运行
# PC 上测试(需要 root 权限,Ctrl+Alt+F2 切到纯文本终端)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)独占显存,必须先
Ctrl+Alt+F2切换到 TTY 纯文本终端后再运行,否则没有效果或权限报错。
十一、常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 运行没有任何显示 | 在图形界面下运行 | Ctrl+Alt+F2切换到 TTY |
| 颜色显示不对 | bpp 不是 32,颜色格式不同 | 用ioctl查看bits_per_pixel,RGB565 需要位操作转换 |
| 图片显示上下颠倒 | 忘记翻转 y 坐标 | 用height - j - 1 + y0代替j + y0 |
| 图片颜色偏 | BMP 是 BGR 存储,直接用成了 RGB | 用 `(c[2]<<16) |
| 字符显示堆叠 | 没清屏就切换内容 | 显示前先调用lcd_fill清屏 |
| 鼠标移动有残影 | 没有恢复旧位置背景 | 移动前先调用copy_mem恢复背景 |