一、核心概念: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)
c
int fb_init(void)
{
// 第一步:打开设备
int fd_fb = open("/dev/fb0", O_RDWR);
// 第二步:获取屏幕参数
ioctl(fd_fb, FBIOGET_VSCREENINFO, &info);
// 第三步:映射显存
int len = info.xres_virtual * info.yres_virtual * info.bits_per_pixel / 8;
pfb = (unsigned char *)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 大小计算;② 像素偏移计算。
退出时释放
c
void fb_deinit(void)
{
munmap(pfb, len);
close(fd_fb);
}
三、画点------所有绘图的基础
代码(pro1/framebuffer.c)
c
void draw_point(unsigned short x, unsigned short y, unsigned int col)
{
if ((x > info.xres_virtual) || (y > info.yres_virtual))
return; // 越界保护
unsigned int *p = (unsigned int *)(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)
c
0xFF0000 // 红色
0x00FF00 // 绿色
0x0000FF // 蓝色
0xFFFFFF // 白色
0x000000 // 黑色
0xFFFF00 // 黄色
四、想换背景色怎么做
用 lcd_fill 填充整个屏幕
c
// 把屏幕全部填成黑色(用来清屏)
lcd_fill(0, 0, 799, 599, 0x000000);
// 把屏幕全部填成白色
lcd_fill(0, 0, 799, 599, 0xFFFFFF);
// 函数原型(来自 framebuffer.h)
void lcd_fill(unsigned short x0, unsigned short y0,
unsigned short x1, unsigned short y1,
unsigned int color);
用 draw_bmp 显示图片背景(来自 mouse.c)
c
// 把 bg.bmp 从 (0,0) 开始铺满屏幕
draw_bmp(0, 0, "./bg.bmp");
典型用法:切换背景时先清屏再画
c
// 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)
c
void lcd_show_string(unsigned short x, // 起始 X 坐标
unsigned short y, // 起始 Y 坐标
unsigned short width, // 显示区域宽度(超出自动换行)
unsigned short height, // 显示区域高度(超出截断)
unsigned char size, // 字体大小:12/16/24/32
char *p, // 字符串内容
unsigned int col); // 字体颜色
示例
c
// 在 (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)
c
int draw_bmp(int x0, int y0, const char *bmp_name)
{
int fd = open(bmp_name, O_RDWR);
unsigned char head[54] = {0};
read(fd, head, sizeof(head)); // 跳过 54 字节 BMP 文件头
for (j = 0; j < 120; j++) {
for (i = 0; i < 120; i++) {
unsigned char c[3] = {0};
read(fd, c, sizeof(c));
// BMP 存储顺序是 BGR,转换为 RGB
unsigned int col = (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,自动读取宽高)
c
// 解析 BMP 文件头,自动获取图片宽高
BitMapFileHeader file_head;
BitMapInfoHeader info_head;
read(fd, &file_head, sizeof file_head);
read(fd, &info_head, sizeof info_head);
// info_head.biWidth → 图片宽度
// info_head.biHeight → 图片高度(正数 = 倒序存储)
BMP 文件头结构
c
// 文件头 14 字节
typedef struct {
unsigned char bfType[2]; // "BM" 标识
unsigned int bfSize; // 文件总大小
unsigned short bfReserved1; // 保留,必须为 0
unsigned short bfReserved2; // 保留,必须为 0
unsigned int bfOffBits; // 像素数据相对文件头的偏移量
} BitMapFileHeader; // 共 14 字节(需 #pragma pack(1))
// 信息头 40 字节
typedef struct {
unsigned int biSize; // 信息头大小(40)
int biWidth; // 图片宽度(像素)
int biHeight; // 图片高度,正数=倒序存储,负数=正序
unsigned short biPlanes; // 位面数,恒为 1
unsigned short biBitCount; // 每像素位数:24=RGB,32=ARGB
unsigned int biCompression;// 压缩类型: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)
c
// save_fb:把当前整个显存复制到 p_save(程序开始时调用一次)
int save_fb()
{
unsigned int *pdst = (unsigned int *)p_save;
unsigned int *psrc = (unsigned int *)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(每次移动前调用)
int copy_mem(int x0, int y0, int w, int h)
{
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)
c
// p_save 是额外分配的内存,和显存等大,用来保存背景快照
p_save = malloc(info.xres_virtual * info.yres_virtual * info.bits_per_pixel / 8);
读取鼠标坐标(来自 mouse.c,使用绝对坐标设备)
c
int fd = open("/dev/input/event2", O_RDWR); // 触摸屏/绝对鼠标
struct input_event event;
read(fd, &event, sizeof event);
if (event.type == EV_ABS) {
if (event.code == ABS_X)
x = event.value / 65535.0 * 800; // 原始值 0~65535 → 屏幕 0~800
else if (event.code == ABS_Y)
y = event.value / 65535.0 * 600; // 原始值 0~65535 → 屏幕 0~600
}
读取相对鼠标(来自 fb/main.c,使用 /dev/input/mice)
c
int fd = open("/dev/input/mice", O_RDWR);
char data[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 参数
c
mode = 0; // 非叠加:背景色为黑色(字符区域黑底)
mode = 1; // 叠加:背景透明,只画有笔画的点,不覆盖底色
九、三色条纹测试(最简单的全屏渲染,验证 FB 是否工作)
来自 pro1/main.c 的 DEBUG 版本,验证 Frame Buffer 初始化是否正常:
c
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:蓝色
else if (j < info.yres * 2 / 3)
draw_point(i, j, 0xff00); // 中 1/3:绿色
else
draw_point(i, j, 0xff0000); // 下 1/3:红色
}
}
十、编译运行
bash
# PC 上测试(需要 root 权限,Ctrl+Alt+F2 切到纯文本终端)
gcc fb.c -o fb_test
sudo ./fb_test
# 交叉编译(开发板运行)
arm-linux-gnueabihf-gcc fb.c -o fb_test
# 传到开发板,直接运行(开发板不需要切终端)
./fb_test
# pro1 多文件编译
arm-linux-gnueabihf-gcc main.c framebuffer.c -o main
PC 运行注意 :图形界面(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 恢复背景 |