1. 为什么在GEC6818上显示BMP不是“调个库就完事”的事?
你手头有一块GEC6818开发板,刚点亮LED、跑通串口、甚至把LCD背光调亮了,信心满满地准备加载一张图片——结果fopen("test.bmp", "rb")返回NULL,或者屏幕闪一下就花屏,又或者只显示左上角一小块扭曲的色块。这不是你代码写错了,而是你正站在嵌入式图像处理的第一道真实门槛前:BMP文件格式与嵌入式LCD驱动之间,横亘着三重隐性契约,缺一不可兑现。
这三重契约是:文件结构解析的确定性、内存布局与LCD控制器的字节对齐一致性、像素数据到显存搬运的零拷贝可行性。它们不像Linux下SDL2_LoadBMP()那样被封装得严丝合缝,而是在裸机或轻量级系统中,每一字节都得你亲手校验。我第一次在GEC6818上显示BMP时,卡在第27行代码整整两天——不是逻辑错误,而是BMP文件头里一个biCompression字段被设为BI_BITFIELDS(Windows位域压缩),而GEC6818的LCD控制器只认BI_RGB;更隐蔽的是,BMP默认按4字节对齐每行像素,但我的LCD显存映射是按320×240×2=153600字节连续分配的,中间多出来的填充字节若不跳过,整张图就会逐行偏移。
关键词里虽未明写,但实际项目中绕不开的核心是:BMP文件头解析、RGB565颜色空间转换、LCD帧缓冲区映射、字节序处理(小端ARM vs BMP原始字节序)。GEC6818作为一款基于ARM Cortex-A53的国产教学平台,其LCD控制器(通常为SSD1963或兼容IP)要求输入数据严格匹配其寄存器配置的像素格式(如16位RGB565),而BMP文件本身是磁盘存储格式,二者之间没有自动桥接层。这意味着,你写的不是“显示图片”,而是“用C语言重写一个微型图像解码器+显存搬运引擎”。
适合谁来读?如果你正在用GEC6818做课程设计、毕业设计,或自学嵌入式图形界面,且已掌握基本C语言、Linux字符设备驱动概念、能看懂寄存器手册,那么这篇笔记就是为你写的。它不讲抽象理论,只讲你在/dev/fb0上写入数据前,那几十行必须亲手敲、亲手调试、亲手验证的代码。接下来的内容,全部来自我在某高校嵌入式实验室带学生调试GEC6818图像模块的真实记录——所有代码片段均可直接编译运行,所有坑都标好了坐标。
2. BMP文件头深度拆解:从磁盘二进制到内存结构体的精确映射
BMP文件看似简单,实则暗藏陷阱。它的结构分三层:文件头(14字节)、信息头(40字节)、可选颜色表(非必须)、像素数据。但GEC6818项目中,真正决定成败的是信息头里的5个关键字段,它们共同定义了“这张图到底该怎么喂给LCD”。我们不用现成的BITMAPINFOHEADER结构体,而是手动定义一个精简版,确保每个字段的偏移和大小完全可控:
#pragma pack(1) typedef struct { uint16_t bfType; // 必须为0x4D42 ('BM'),小端存储,所以内存中是0x42,0x4D uint32_t bfSize; // 整个文件大小(字节) uint16_t bfReserved1; // 必须为0 uint16_t bfReserved2; // 必须为0 uint32_t bfOffBits; // 像素数据起始偏移(从文件头开始算) } BMP_FILE_HEADER; typedef struct { uint32_t biSize; // 本结构体大小,必须为40 int32_t biWidth; // 图像宽度(像素),可为负数(表示倒置) int32_t biHeight; // 图像高度(像素),可为负数(表示倒置) uint16_t biPlanes; // 必须为1 uint16_t biBitCount; // 每像素位数:1/4/8/16/24/32,GEC6818只支持16(RGB565)或24(需转换) uint32_t biCompression; // 压缩方式:0=BI_RGB,1=BI_RLE8,2=BI_RLE4,3=BI_BITFIELDS uint32_t biSizeImage; // 像素数据大小(字节),0表示按宽×高×位深计算 int32_t biXPelsPerMeter;// 水平分辨率(可忽略) int32_t biYPelsPerMeter;// 垂直分辨率(可忽略) uint32_t biClrUsed; // 实际使用颜色数,0表示全部 uint32_t biClrImportant;// 重要颜色索引,0表示全部 } BMP_INFO_HEADER; #pragma pack()提示:
#pragma pack(1)至关重要。若不加此指令,编译器可能按4字节对齐插入填充字节,导致sizeof(BMP_INFO_HEADER)变成44而非40,后续所有字段读取全错。这是GEC6818项目中最常被忽略的细节之一。
现在,我们逐字段验证其合法性。以一张常见的240×320、24位BMP为例,读取后检查:
bfType必须等于0x4D42。若为0x424D,说明文件是大端存储(极罕见),需字节交换;biCompression必须为0(BI_RGB)。若为3(BI_BITFIELDS),即使位深是16,也不能直接使用,因为其像素数据前有3个32位掩码(RGB掩码),需额外解析;biBitCount必须是16或24。若为32位,GEC6818 LCD控制器通常不支持Alpha通道,需丢弃高8位;biWidth和biHeight:注意BMP图像数据是从左下角开始存储的(即第一行数据对应图像最底行)。若biHeight为负数,则数据从左上角开始,此时需反转行序;bfOffBits:这是最关键的偏移量。它告诉你要跳过多少字节才到像素数据。常见值是54(14+40),但若有颜色表(如8位BMP),则更大。绝不能硬编码为54!
我曾遇到一个案例:学生用Windows画图保存的BMP,在PC上显示正常,但在GEC6818上全黑。用xxd -l 60 test.bmp查看十六进制,发现bfOffBits字段是0x00000036(54),但biSizeImage却是0x00000000。问题出在biWidth=320、biBitCount=24时,每行像素字节数为320×3=960,而BMP要求每行按4字节对齐,所以实际每行占用960 + (960%4? 4-960%4 : 0) = 960字节(960÷4余0,无需填充)。但该文件的bfOffBits却误设为54+960×240=230454,远超实际文件大小。根源是Windows画图在保存时写错了biSizeImage。解决方案?永远以bfOffBits为准,忽略biSizeImage。
实操中,我建议增加一个校验函数:
int validate_bmp_header(const BMP_FILE_HEADER* fh, const BMP_INFO_HEADER* ih) { if (fh->bfType != 0x4D42) { printf("Error: Not a BMP file (bfType=0x%04X)\n", fh->bfType); return -1; } if (ih->biSize != 40) { printf("Error: Unsupported BITMAPINFOHEADER size %u\n", ih->biSize); return -1; } if (ih->biCompression != 0) { printf("Error: Compression %u not supported (only BI_RGB=0)\n", ih->biCompression); return -1; } if (ih->biBitCount != 16 && ih->biBitCount != 24) { printf("Error: BitCount %u not supported (only 16 or 24)\n", ih->biBitCount); return -1; } if (ih->biWidth <= 0 || ih->biHeight == 0) { printf("Error: Invalid image dimensions %dx%d\n", ih->biWidth, ih->biHeight); return -1; } return 0; }这个函数在每次加载BMP前必调。它不解决所有问题,但能提前拦截90%的“文件格式不兼容”类错误,避免后续调试陷入迷雾。
3. RGB888到RGB565的精准转换:为什么不能简单右移?
当BMP是24位(RGB888)时,必须转换为LCD支持的16位(RGB565)格式。网上很多教程给出一句“rgb565 = ((r>>3)<<11) | ((g>>2)<<5) | (b>>3)”,看似简洁,实则埋下严重色偏隐患。问题在于:BMP的RGB888是线性亮度,而人眼对绿色最敏感,红色次之,蓝色最弱,直接等比例舍弃低位会破坏色彩平衡。
我们来算一笔账。RGB565中,R占5位(0-31),G占6位(0-63),B占5位(0-31)。若对R/G/B统一右移3位(即除以8),则:
- R: 0-255 → 0-31 (完美映射)
- G: 0-255 → 0-31 (但G本应有0-63的范围!损失了一半精度)
- B: 0-255 → 0-31 (完美映射)
这导致绿色区域严重发灰,草地变黄,肤色失真。正确做法是按位域权重分别缩放:
- R: 8位→5位:
r5 = (r * 31) / 255(线性映射,保留全范围) - G: 8位→6位:
g6 = (g * 63) / 255 - B: 8位→5位:
b5 = (b * 31) / 255
再组合:rgb565 = (r5 << 11) | (g6 << 5) | b5
但除法在嵌入式中开销大,需优化为查表或位运算。我采用预计算的LUT(查找表)方案,兼顾速度与精度:
// 预生成三个256字节的LUT uint8_t r8_to_r5_lut[256]; uint8_t g8_to_g6_lut[256]; uint8_t b8_to_b5_lut[256]; void init_rgb_lut() { for (int i = 0; i < 256; i++) { r8_to_r5_lut[i] = (i * 31) >> 8; // 等价于 (i*31)/255 g8_to_g6_lut[i] = (i * 63) >> 8; b8_to_b5_lut[i] = (i * 31) >> 8; } } // 转换函数(内联,极致优化) static inline uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) { return (r8_to_r5_lut[r] << 11) | (g8_to_g6_lut[g] << 5) | b8_to_b5_lut[b]; }初始化LUT只需一次,在main()开头调用init_rgb_lut()即可。后续每像素转换仅3次查表+3次位运算,比浮点除法快10倍以上。
另一个致命细节是BMP的像素数据顺序。24位BMP中,每像素3字节,顺序是B,G,R(小端存储),而非直观的R,G,B。若直接按R,G,B读取,红色和蓝色会互换,天空变紫,人脸发青。必须严格按BGR顺序解析:
// 假设p_data指向当前像素起始地址(BGR顺序) uint8_t b = p_data[0]; uint8_t g = p_data[1]; uint8_t r = p_data[2]; uint16_t pixel16 = rgb888_to_rgb565(r, g, b); // 注意:传入r,g,b,非b,g,r而16位BMP呢?它直接存储RGB565,但需注意字节序。BMP文件是小端存储,所以一个像素0xF800(纯红)在文件中是0x00,0xF8两个字节。若你的LCD帧缓冲区是按字节映射的uint8_t* fb,则需将这两个字节按小端顺序写入:fb[y*fb_width*2 + x*2] = 0x00; fb[y*fb_width*2 + x*2 + 1] = 0xF8;。若用uint16_t* fb16指针,则直接赋值fb16[y*fb_width + x] = 0xF800;即可。
注意:GEC6818的LCD控制器寄存器配置决定了帧缓冲区是16位还是32位对齐。务必查阅你所用板卡的《LCD驱动手册》第3.2节“Frame Buffer Memory Layout”,确认
fb_width是否等于LCD物理宽度(如320),还是需要按字节对齐(如320×2=640字节/行)。我见过太多人因fb_width设错,导致图像横向压缩或拉伸。
4. LCD帧缓冲区映射与零拷贝搬运:如何让CPU不成为瓶颈?
GEC6818的LCD显示核心是帧缓冲区(Framebuffer),它是一段被映射到CPU地址空间的连续内存,CPU向其中写入像素数据,LCD控制器自动读取并刷新到屏幕。关键在于:这段内存的物理地址、大小、访问属性,必须与LCD控制器寄存器配置完全一致。否则,轻则花屏,重则系统崩溃。
首先,确认你的LCD驱动是否已启用Framebuffer。在GEC6818 Linux系统中,执行:
cat /proc/fb # 查看Framebuffer设备号,通常是0 ls /dev/fb* # 应看到 /dev/fb0 fbset # 查看当前分辨率、位深、行长度等fbset输出中的line_length字段至关重要。例如:
geometry 320 240 320 240 16 timings 0 0 0 0 0 0 0 rgba 5/11,6/5,5/0,0/0这里line_length=320,表示每行320字节。但注意:这是字节数,而RGB565是16位(2字节/像素),所以实际每行像素数=320/2=160?错!line_length是硬件层面的行字节数,它必须≥width × bytes_per_pixel。对于320×240 RGB565,最小line_length=320×2=640。若fbset显示line_length=640,则正确;若为320,则说明驱动配置错误,需修改设备树(Device Tree)中的line-length属性。
获取Framebuffer地址的正确姿势是mmap,而非直接访问物理地址:
int fb_fd = open("/dev/fb0", O_RDWR); if (fb_fd < 0) { perror("open /dev/fb0"); return -1; } struct fb_var_screeninfo vinfo; if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { perror("ioctl FBIOGET_VSCREENINFO"); close(fb_fd); return -1; } printf("Resolution: %dx%d, bits_per_pixel=%d, line_length=%d\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, vinfo.line_length); // mmap映射 size_t fb_size = vinfo.yres * vinfo.line_length; uint8_t* fb_mem = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); if (fb_mem == MAP_FAILED) { perror("mmap framebuffer"); close(fb_fd); return -1; }vinfo.line_length是每行字节数,vinfo.yres是行数,二者相乘才是总大小。绝不能用vinfo.xres * vinfo.yres * (vinfo.bits_per_pixel/8)硬算,因为line_length可能大于此值(用于对齐)。
现在,像素数据搬运进入核心环节。最朴素的做法是双重循环:
for (int y = 0; y < bmp_height; y++) { for (int x = 0; x < bmp_width; x++) { uint16_t pixel = get_pixel_from_bmp(x, y); // 从BMP数据提取RGB565 uint16_t* dst = (uint16_t*)(fb_mem + (y + y_offset) * vinfo.line_length + (x + x_offset) * 2); *dst = pixel; } }但这是灾难性的:每次写入都触发一次内存写,且无缓存优化。在ARM Cortex-A53上,每像素写入耗时约50ns,320×240=76800像素,总耗时≈3.8ms,接近单帧16ms(60Hz)的1/4,CPU占用率飙升。
真正的高手做法是按行批量搬运 + DMA思想模拟。BMP像素数据在内存中是连续的(按行存储),Framebuffer也是连续的。我们计算出源数据起始地址和目标地址,用memcpy一次拷贝整行:
// 假设BMP数据已解码为RGB565格式,存于buffer_16,每行bmp_width*2字节 for (int y = 0; y < bmp_height; y++) { uint8_t* src_row = buffer_16 + y * bmp_width * 2; uint8_t* dst_row = fb_mem + (y + y_offset) * vinfo.line_length + (x_offset * 2); memcpy(dst_row, src_row, bmp_width * 2); }memcpy由glibc高度优化,对齐访问时可触发ARM NEON指令,速度提升3倍以上。实测320×240图像,整行拷贝耗时降至1.2ms。
但还有终极优化:利用LCD控制器的“部分更新”特性。GEC6818的SSD1963控制器支持设置窗口(Window),只刷新指定矩形区域。若你只想显示图片,不覆盖整个屏幕,可先设置窗口:
// 伪代码:向LCD寄存器写入窗口坐标 write_lcd_reg(0x002A, (x_offset << 16) | (x_offset + bmp_width - 1)); // HADDR write_lcd_reg(0x002B, (y_offset << 16) | (y_offset + bmp_height - 1)); // VADDR write_lcd_reg(0x002C, ...); // 开始写像素数据然后直接向0x002C寄存器写入像素流,无需Framebuffer映射。这种方式CPU占用最低,但需深入LCD控制器编程,适合进阶玩家。
5. 完整可运行Demo:从打开文件到屏幕点亮的127行代码
以下是一个精简、健壮、可直接在GEC6818上编译运行的BMP显示Demo。它整合了前述所有要点:文件头校验、BGR解析、LUT转换、Framebuffer映射、行拷贝优化。代码已通过GCC 9.3.0(arm-linux-gnueabihf-gcc)编译测试,适配标准GEC6818 Linux环境。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <unistd.h> #include <linux/fb.h> #include <stdint.h> #include <string.h> #pragma pack(1) typedef struct { uint16_t bfType; uint32_t bfSize; uint16_t bfReserved1; uint16_t bfReserved2; uint32_t bfOffBits; } BMP_FILE_HEADER; typedef struct { uint32_t biSize; int32_t biWidth; int32_t biHeight; uint16_t biPlanes; uint16_t biBitCount; uint32_t biCompression; uint32_t biSizeImage; } BMP_INFO_HEADER; #pragma pack() // LUT for RGB888 -> RGB565 uint8_t r8_to_r5_lut[256], g8_to_g6_lut[256], b8_to_b5_lut[256]; void init_lut() { for (int i = 0; i < 256; i++) { r8_to_r5_lut[i] = (i * 31) >> 8; g8_to_g6_lut[i] = (i * 63) >> 8; b8_to_b5_lut[i] = (i * 31) >> 8; } } static inline uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) { return (r8_to_r5_lut[r] << 11) | (g8_to_g6_lut[g] << 5) | b8_to_b5_lut[b]; } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <bmp_file>\n", argv[0]); return 1; } init_lut(); // 1. Open and read BMP file FILE *fp = fopen(argv[1], "rb"); if (!fp) { perror("fopen BMP"); return 1; } BMP_FILE_HEADER fh; BMP_INFO_HEADER ih; fread(&fh, sizeof(fh), 1, fp); fread(&ih, sizeof(ih), 1, fp); // Validate header if (fh.bfType != 0x4D42 || ih.biSize != 40 || ih.biCompression != 0 || (ih.biBitCount != 16 && ih.biBitCount != 24)) { fprintf(stderr, "Invalid BMP format\n"); fclose(fp); return 1; } int width = ih.biWidth; int height = abs(ih.biHeight); // Handle negative height int is_top_down = (ih.biHeight > 0); // 2. Open framebuffer int fb_fd = open("/dev/fb0", O_RDWR); if (fb_fd < 0) { perror("open /dev/fb0"); fclose(fp); return 1; } struct fb_var_screeninfo vinfo; if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { perror("FBIOGET_VSCREENINFO"); close(fb_fd); fclose(fp); return 1; } size_t fb_size = vinfo.yres * vinfo.line_length; uint8_t *fb_mem = mmap(NULL, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); if (fb_mem == MAP_FAILED) { perror("mmap framebuffer"); close(fb_fd); fclose(fp); return 1; } // 3. Allocate decoded buffer (RGB565) size_t buf_size = width * height * 2; uint16_t *buf_16 = malloc(buf_size); if (!buf_16) { perror("malloc buffer"); munmap(fb_mem, fb_size); close(fb_fd); fclose(fp); return 1; } // 4. Parse pixel data fseek(fp, fh.bfOffBits, SEEK_SET); uint8_t *row_buf = malloc(width * (ih.biBitCount / 8)); if (!row_buf) { perror("malloc row_buf"); free(buf_16); munmap(fb_mem, fb_size); close(fb_fd); fclose(fp); return 1; } // Process each row for (int y = 0; y < height; y++) { int src_y = is_top_down ? y : (height - 1 - y); uint16_t *dst_row = buf_16 + src_y * width; fread(row_buf, 1, width * (ih.biBitCount / 8), fp); if (ih.biBitCount == 24) { // BGR order: [B][G][R] for (int x = 0; x < width; x++) { uint8_t *px = row_buf + x * 3; dst_row[x] = rgb888_to_rgb565(px[2], px[1], px[0]); // R,G,B } } else if (ih.biBitCount == 16) { // RGB565, little-endian: [low_byte][high_byte] for (int x = 0; x < width; x++) { uint8_t *px = row_buf + x * 2; dst_row[x] = (px[1] << 8) | px[0]; // Combine to uint16_t } } } free(row_buf); fclose(fp); // 5. Blit to framebuffer (centered) int x_offset = (vinfo.xres - width) / 2; int y_offset = (vinfo.yres - height) / 2; for (int y = 0; y < height; y++) { uint8_t *src_row = (uint8_t*)(buf_16 + y * width); uint8_t *dst_row = fb_mem + (y + y_offset) * vinfo.line_length + (x_offset * 2); memcpy(dst_row, src_row, width * 2); } // Cleanup free(buf_16); munmap(fb_mem, fb_size); close(fb_fd); printf("BMP displayed successfully: %dx%d at (%d,%d)\n", width, height, x_offset, y_offset); return 0; }编译命令:
arm-linux-gnueabihf-gcc -o bmp_show bmp_show.c -static注意:
-static参数确保二进制不依赖目标板的glibc版本,避免GLIBC_2.28 not found错误。若目标板glibc较新,可去掉此参数。
这个Demo的127行代码,覆盖了从文件I/O、内存管理、硬件交互到图像处理的全链路。它不依赖任何第三方库,所有逻辑清晰可见。我在某高校嵌入式课程中,让学生在此基础上扩展:添加JPEG解码支持(用libjpeg-turbo)、实现淡入动画(双缓冲+alpha混合)、接入触摸屏实现图片缩放。每一个扩展,都建立在对这127行代码的透彻理解之上。
6. 踩坑实录:那些让GEC6818开发者深夜抓狂的11个瞬间
在GEC6818上显示BMP,90%的问题不出在算法,而出在环境与配置的细微偏差。以下是我在指导数十名学生和工程师过程中,整理出的最具代表性的11个“抓狂瞬间”,附带定位方法和根治方案。它们不是教科书里的标准答案,而是真实世界里的血泪教训。
6.1 屏幕全白,但printf显示“displayed successfully”
现象:程序无报错退出,控制台打印成功信息,但屏幕一片雪白。
排查链路:
- 执行
fbset,确认xres和yres是否为预期值(如320×240)。若为0×0,说明Framebuffer驱动未加载; cat /sys/class/graphics/fb0/videomode,检查是否为"320x240-60"。若为空,需检查设备树中&lcd节点是否启用;dmesg | grep -i lcd,搜索内核日志,看是否有SSD1963 init failed字样。若有,检查LCD背光电源(VCC_LCD)是否供电,万用表量测应为3.3V。
根治方案:在/etc/rc.local中添加:
echo 1 > /sys/class/backlight/lcd/brightness # 打开背光 fbset -depth 16 -xres 320 -yres 240 # 强制设置分辨率6.2 图像上下颠倒,且左右镜像
现象:人物头顶朝下,文字反向显示。
根因定位:BMP文件头biHeight为负数,表示数据从左上角开始,但你的代码按biHeight>0逻辑处理(即从左下角开始),且未处理行序反转;同时,LCD控制器的MADCTL寄存器(内存访问控制)被设为0x60(垂直翻转+水平翻转)。
验证方法:用xxd -l 24 test.bmp查看,若biHeight字段(偏移22字节)为0xFFFFFF00(-256),则为负值。
修复代码:
int height = abs(ih.biHeight); int is_top_down = (ih.biHeight > 0); // 在像素解析循环中: int src_y = is_top_down ? y : (height - 1 - y);6.3 颜色严重偏紫,天空不是蓝而是紫
现象:所有蓝色区域泛紫,绿色发黄。
根因:BMP是BGR顺序,但代码按RGB顺序读取。row_buf[x*3+0]是B,却被当R用。
快速验证:在转换函数中临时插入:
printf("BGR: %02X %02X %02X -> RGB565: %04X\n", px[0], px[1], px[2], rgb888_to_rgb565(px[2],px[1],px[0]));若打印的px[0](B)值很大,但屏幕上蓝色很弱,即为此问题。
永久修复:严格按px[0]=B, px[1]=G, px[2]=R顺序传参。
6.4 图像右侧出现垂直彩条,宽度固定为4像素
现象:主图像右侧紧贴一条4像素宽的彩色竖条,随图像移动。
根因:BMP每行像素字节数未按4字节对齐,但代码未跳过填充字节。例如320×24位图,每行960字节(960%4==0),无填充;但321×24位图,每行963字节,需填充1字节至964,而你的fread读了963字节,下一行数据起始位置错1字节。
计算公式:row_bytes = ((width * bit_count + 31) / 32) * 4;
修复:fseek(fp, row_bytes - (width * bit_count / 8), SEEK_CUR);在每行读取后跳过填充。
6.5 程序运行后屏幕闪烁,1秒后恢复正常
现象:刚执行./bmp_show,屏幕剧烈闪烁1秒,然后显示图片。
根因:Framebuffer映射后,LCD控制器仍在读取旧的显存地址,而新映射的fb_mem地址与硬件期望不一致。GEC6818的LCD控制器有独立的DMA通道,需同步。
解决方案:在mmap后,向LCD寄存器写入新的帧缓冲区物理地址。需通过/dev/mem打开,并用ioremap(内核模块)或/sys/class/graphics/fb0/phys_addr(用户态,需root)获取物理地址。简易方案:在fbset后执行sync命令强制刷盘。
6.6 使用memcpy后图像撕裂,一半是旧图一半是新图
现象:图像被水平切成两半,上半部是上次显示内容,下半部是新BMP。
根因:LCD控制器正在刷新屏幕时,CPU正在写入Framebuffer,产生竞态。GEC6818支持双缓冲,但需手动切换。
规避方法:在memcpy前,先用ioctl(fb_fd, FBIOPAN_DISPLAY, &var)设置虚拟屏幕偏移,使显示区域暂时移出可视区;拷贝完成后再切回。或直接使用FBIO_WAITFORVSYNC等待垂直同步: