title: FrameBuffer 与 LCD 应用编程 tags: [
嵌入式Linux,
Linux应用编程,
FrameBuffer,
LCD,
fb0,
mmap,
RGB565,
libjpeg,
libpng,
FreeType,
竖屏,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[03-外设与高级IO编程/03-输入设备与tslib]]、[[03-外设与高级IO编程/01-高级IO]];延伸阅读:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]]
在 Linux 里,“显示”这件事被抽象成了一块内存:FrameBuffer(帧缓冲)。应用层不接触 LCD 控制器、时序、寄存器,只要拿到 /dev/fb0、用 ioctl 问清楚屏幕参数、再把这块内存 mmap 到用户空间,就能像写普通数组一样在屏幕上打点、画线、贴图片、显示文字。
本篇把《I.MX6U 嵌入式 Linux C 应用编程指南》第五篇(第十九~二十三章)从头到尾串起来:裸 FrameBuffer 编程 → 显示 BMP → libjpeg 显示 JPEG → libpng 显示 PNG → 横屏转竖屏 → FreeType 显示字符,所有例程完整收录,可直接交叉编译运行。
Frame = 帧,buffer = 缓冲,Framebuffer = 帧缓冲 = 保存着一帧图像的一块内存。/dev/fbX(X 为数字)。Linux 最多支持 32 个,即 /dev/fb0 ~ /dev/fb31;正点原子出厂系统里 /dev/fb0 就是 LCD 屏。显存大小由分辨率与像素深度决定。例如 800×480、RGB888(24 位/像素):
800 × 480 × 24 / 8 = 1152000 字节
因此可以用 dd 直接把显存清成黑色(假设设备节点 /dev/fb0、分辨率 800×480、RGB888):
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125
该命令把 1125×1024 个字节日均写入 LCD 显存,内容都是 0x0。
flowchart LR
App["应用程序"] -->|"open / dev/fb0"| FBDev["FrameBuffer 设备"]
App -->|"ioctl 取参数"| FBDev
App -->|"mmap 映射"| Mem["显存<br/>显示缓冲区"]
FBDev -.->|"驱动管理"| Mem
Mem -->|"LCD 控制器循环扫描"| Panel["LCD 液晶面板"]
App -->|"munmap / close"| FBDev
/dev/fbX 设备文件,得到文件描述符 fd。ioctl() 获取当前显示设备的参数信息(分辨率、像素格式),据此计算显存大小。mmap)把屏幕显存映射到用户空间。munmap() 取消映射,close() 关闭设备文件。普通 I/O(read/write)也能操作显存,但数据量大时效率低。举例:1920×1080、ARGB8888,刷一帧就是
1920 × 1080 × 32 / 8 = 8294400 字节 ≈ 8MB
显示画面还在动态更新,数据量庞大,普通 I/O 必然效率低下,所以采用存储映射 I/O:映射一次,之后就是内存访问。
FrameBuffer 应用编程绕不开 <linux/fb.h> 里的两个结构体:可变参数 struct fb_var_screeninfo 与固定参数 struct fb_fix_screeninfo。
| request 宏 | 值 | 作用 | 第三参数 |
|---|---|---|---|
FBIOGET_VSCREENINFO |
0x4600 |
获取可变参数信息 | struct fb_var_screeninfo * |
FBIOPUT_VSCREENINFO |
0x4601 |
设置可变参数信息(驱动支持时才能改) | struct fb_var_screeninfo * |
FBIOGET_FSCREENINFO |
0x4602 |
获取固定参数信息(应用不可改) | struct fb_fix_screeninfo * |
struct fb_var_screeninfo fb_var;
struct fb_fix_screeninfo fb_fix;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
struct fb_var_screeninfo {
__u32 xres; /* 可视区域,一行有多少个像素点,X 分辨率 */
__u32 yres; /* 可视区域,一列有多少个像素点,Y 分辨率 */
__u32 xres_virtual; /* 虚拟区域,一行有多少个像素点 */
__u32 yres_virtual; /* 虚拟区域,一列有多少个像素点 */
__u32 xoffset; /* 虚拟到可见屏幕之间的行偏移 */
__u32 yoffset; /* 虚拟到可见屏幕之间的列偏移 */
__u32 bits_per_pixel; /* 每个像素点使用多少个 bit 来描述,即像素深度 bpp */
__u32 grayscale; /* =0 彩色, =1 灰度, >1 FOURCC 颜色 */
/* 描述 R、G、B 三分量各用多少位及偏移量 */
struct fb_bitfield red; /* Red 颜色分量色域偏移 */
struct fb_bitfield green; /* Green 颜色分量色域偏移 */
struct fb_bitfield blue; /* Blue 颜色分量色域偏移 */
struct fb_bitfield transp; /* 透明度分量色域偏移 */
__u32 nonstd; /* 0 表示标准像素格式;非 0 表示非标准像素格式 */
__u32 activate;
__u32 height; /* LCD 显示图像的高度(毫米) */
__u32 width; /* LCD 显示图像的宽度(毫米) */
__u32 accel_flags;
/* 以下表示时序参数 */
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 rotate; /* angle we rotate counter clockwise */
__u32 colorspace; /* colorspace for FOURCC-based modes */
__u32 reserved[4]; /* Reserved for future compatibility */
};
关键字段理解:
xres / yres:屏幕水平/垂直分辨率(可视区域)。xres * yres * bits_per_pixel / 8 即整个显示缓冲区大小。xres_virtual / yres_virtual:虚拟分辨率,是显存里实际一行/一列能容纳的像素数,可以大于可视区域。它配合 xoffset / yoffset 实现平移(panning):虚拟分辨率更大时显存像一块“大画布”,可视区域是从 (xoffset, yoffset) 开始的一个窗口。显存布局与一行字节数由固定参数 line_length 描述,所以绘图定位用 line_length 比用 xres 更稳妥(两者在无虚拟扩展时通常相等)。bits_per_pixel:像素深度 bpp,每个像素用多少 bit 描述颜色。red / green / blue:三个颜色通道各占多少 bit、偏移多少,用来判断 RGB888 / RGB565 / BGR888 / BGR565 等。height / width:LCD 物理尺寸(毫米),与分辨率无关。struct fb_bitfield {
__u32 offset; /* 偏移量 */
__u32 length; /* 长度 */
__u32 msb_right; /* != 0 : Most significant bit is right */
};
打印 red.offset/length、green.offset/length、blue.offset/length 就能确定像素格式。例如正点原子 7 寸 800×480 屏打印出 R<11 5> G<5 6> B<0 5>,表示:
16bit 颜色值:高 5 位 = R,中间 6 位 = G,低 5 位 = B → RGB565
struct fb_fix_screeninfo {
char id[16]; /* 字符串形式的标识符 */
unsigned long smem_start; /* 显存的起始地址(物理地址) */
__u32 smem_len; /* 显存的长度 */
__u32 type;
__u32 type_aux;
__u32 visual;
__u16 xpanstep;
__u16 ypanstep;
__u16 ywrapstep;
__u32 line_length; /* 一行的字节数 */
unsigned long mmio_start; /* Start of Memory Mapped I/O(physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which specific chip/card we have */
__u16 capabilities;
__u16 reserved[2];
};
smem_start:显存起始物理地址,应用层无法直接使用,需靠 mmap。smem_len:显存长度,不一定等于 LCD 实际显存大小。line_length:屏幕一行像素点占用的字节数。通常用 line_length * yres 得到显示缓冲区大小。| 结构体 | 字段 | 含义 | 典型用途 |
|---|---|---|---|
| fb_var | xres / yres |
可视分辨率 | 计算 width、height |
| fb_var | xres_virtual / yres_virtual |
虚拟分辨率 | 双缓冲 / panning |
| fb_var | xoffset / yoffset |
可视窗口在虚拟显存中的偏移 | 平移显示 |
| fb_var | bits_per_pixel |
像素深度 bpp | 一行有效字节数 = width * bpp / 8 |
| fb_var | red/green/blue |
通道位域 | 判定 RGB565 / RGB888 |
| fb_fix | line_length |
一行字节数 | 显存大小 = line_length * yres |
| fb_fix | smem_start / smem_len |
显存物理地址/长度 | 物理信息,应用不可直接用 |
一个像素点的字节数 = bpp / 8。RGB888 用 3 字节,RGB565 用 2 字节。开发板出厂系统把 LCD 实现为 RGB565 显示设备,所以例程里普遍使用 unsigned short 指针访问显存。
RGB565 的位域掩码为 0xF800、0x07E0、0x001F。从 ARGB8888 的颜色值转 RGB565 的宏:
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
对应例程:19_lcd/lcd_info.c。打开 /dev/fb0,用两个 ioctl 取参数并打印。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
int fd;
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_WRONLY))) {
perror("open error");
exit(-1);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
printf("分辨率: %d*%d\n"
"像素深度 bpp: %d\n"
"一行的字节数: %d\n"
"像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
fb_fix.line_length,
fb_var.red.offset, fb_var.red.length,
fb_var.green.offset, fb_var.green.length,
fb_var.blue.offset, fb_var.blue.length);
/* 关闭设备文件退出程序 */
close(fd);
exit(0);
}
逐段说明:
open("/dev/fb0", O_WRONLY):只读参数用 O_WRONLY 即可;后面要写显存时用 O_RDWR。FBIOGET_VSCREENINFO 得到可变参数,FBIOGET_FSCREENINFO 得到固定参数。red/green/blue 的 offset 与 length,就能按第 2.3 节的方法判断像素格式。7 寸 800×480 屏实测:分辨率 800*480、bpp 16、一行 1600 字节、像素格式 R<11 5> G<5 6> B<0 5>,即 RGB565。800*16/8 = 1600,与 line_length 一致。
Tips:正点原子的 RGB LCD(4.3 寸 800×480、4.3 寸 480×272、7 寸 800×480、7 寸 1024×600、10.1 寸 1280×800)硬件上均支持 RGB888,但 ALPHA/Mini I.MX6U 出厂系统的 LCD 驱动把它实现为 RGB565 设备;可改设备树支持 RGB888,或通过
ioctl修改。不建议随意用FBIOPUT_VSCREENINFO改参数,驱动支持不完善时可能出问题。
对应例程:19_lcd/lcd_test.c。这是 FrameBuffer 编程的核心范例:open → ioctl → mmap → 直接写显存。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int width; /* LCD X 分辨率 */
static int height; /* LCD Y 分辨率 */
static unsigned short *screen_base = NULL; /* 映射后的显存基地址 */
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
screen_base[y * width + x] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
unsigned int end;
unsigned long temp;
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
temp = y * width + x; /* 定位到起点 */
if (dir) { /* 水平线 */
end = x + length - 1;
if (end >= width)
end = width - 1;
for ( ; x <= end; x++, temp++)
screen_base[temp] = rgb565_color;
}
else { /* 垂直线 */
end = y + length - 1;
if (end >= height)
end = height - 1;
for ( ; y <= end; y++, temp += width)
screen_base[temp] = rgb565_color;
}
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color); /* 上边 */
lcd_draw_line(start_x, end_y, 1, x_len, color); /* 下边 */
lcd_draw_line(start_x, start_y + 1, 0, y_len, color); /* 左边 */
lcd_draw_line(end_x, start_y + 1, 0, y_len, color); /* 右边 */
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
unsigned long temp;
unsigned int x;
/* 对传入参数的校验 */
if (end_x >= width)
end_x = width - 1;
if (end_y >= height)
end_y = height - 1;
/* 填充颜色 */
temp = start_y * width; /* 定位到起点行首 */
for ( ; start_y <= end_y; start_y++, temp += width) {
for (x = start_x; x <= end_x; x++)
screen_base[temp + x] = rgb565_color;
}
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块 */
int w = height * 0.25; /* 方块的宽度为 1/4 屏幕高度 */
lcd_fill(0, width-1, 0, height-1, 0x0); /* 清屏(屏幕显示黑色) */
lcd_fill(0, w, 0, w, 0xFF0000); /* 红色方块 */
lcd_fill(width-w, width-1, 0, w, 0xFF00); /* 绿色方块 */
lcd_fill(0, w, height-w, height-1, 0xFF); /* 蓝色方块 */
lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);/* 黄色方块 */
/* 画线: 十字交叉线 */
lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF); /* 白色线 */
lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF); /* 白色线 */
/* 画矩形 */
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * width;
s_y = w;
e_x = width - s_x;
e_y = height - s_y;
for ( ; (s_x <= e_x) && (s_y <= e_y);
s_x+=5, s_y+=5, e_x-=5, e_y-=5)
lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
/* 退出 */
munmap(screen_base, screen_size); /* 取消映射 */
close(fd); /* 关闭文件 */
exit(EXIT_SUCCESS); /* 退出进程 */
}
以显存基地址 screen_base(unsigned short *,RGB565)为例,像素 (x, y) 对应:
screen_base[y * width + x]
若用字节指针 (unsigned char *)base,则:
base + (y * width + x) * pix_bytes
其中 pix_bytes 是一个像素占用的字节数。一行跨度为 width 个 unsigned short,所以换行时指针 +width 即可。
| 函数 | 作用 | 关键参数 |
|---|---|---|
lcd_draw_point |
打点 | x, y, color |
lcd_draw_line |
画水平/垂直线(不支持斜线) | dir != 0 水平,dir == 0 垂直;length 像素长度 |
lcd_draw_rectangle |
画矩形边框(4 条线拼成) | 左上 (start_x,start_y)、右下 (end_x,end_y) |
lcd_fill |
矩形区域填充 | 同上 |
lcd_draw_rectangle 的内边长度 y_len = end_y - start_y - 1,左右两条竖线各画 y_len 个点;lcd_fill 用双层循环逐行填充。
open 打开 LCD,得到 fd。ioctl 取可变/固定参数,算出 screen_size = line_length * yres、width = xres、height = yres。mmap 建立映射。注意 MAP_FAILED 判断要先把指针转成 void * 再比较。screen_base 画方块、十字线、同心矩形。munmap + close 退出。本组函数只支持水平/垂直线。画斜线需要 Bresenham 之类的算法,不属于本章要点。
对应例程:19_lcd/bmp_show.c。BMP 未压缩、解析简单,是理解“图像文件 → 显存”的最好入口。
| 数据段 | 大小(Byte) | 说明 |
|---|---|---|
| BMP 文件头(bmp file header) | 14 | 文件格式、大小、到位图数据的偏移量 |
| 位图信息头(bitmap information) | 通常 40 或 56 | 头大小、图像尺寸、图像大小、位平面数、压缩方式、颜色索引 |
| 调色板(color palette) | 由颜色索引数决定 | 可选;索引色图像才有 |
| 位图数据(bitmap data) | 由图像尺寸决定 | 图像数据本体 |
16 位(R5/G6/B5)、24 位(R8/G8/B8)真彩色图像不需要调色板,位图信息头后紧跟位图数据。
| 变量名 | 地址偏移 | 大小 | 作用 |
|---|---|---|---|
bfType |
00H | 2 bytes | 文件类型,BM 表示 Windows 位图 |
bfSize |
02H | 4 bytes | 文件大小 |
bfReserved1 |
06H | 2 bytes | 保留,必须为 0 |
bfReserved2 |
08H | 2 bytes | 保留,必须为 0 |
bfOffBits |
0AH | 4 bytes | 从文件头到图像数据的字节偏移量,用它快速定位图像数据 |
| 变量名 | 地址偏移 | 大小 | 作用 |
|---|---|---|---|
biSize |
0EH | 4 bytes | 位图信息头大小 |
biWidth |
12H | 4 bytes | 图像宽度(像素) |
biHeight |
16H | 4 bytes | 图像高度(像素);正数 = 倒向位图,负数 = 正向位图 |
biPlanes |
1AH | 2 bytes | 色彩平面数,总为 1 |
biBitCount |
1CH | 2 bytes | 像素深度,可为 1、4、8、16、24、32 |
biCompression |
1EH | 4 bytes | 压缩方式:0=RGB,1=8bpp RLE,2=4bpp RLE,3=Bit-fields,4/5=打印机 |
biSizeImage |
22H | 4 bytes | 图像数据大小(BI_RGB 时可设为 0) |
biXPelsPerMeter |
26H | 4 bytes | 水平分辨率(像素/米) |
biYPelsPerMeter |
2AH | 4 bytes | 垂直分辨率(像素/米) |
biClrUsed |
2EH | 4 bytes | 实际使用的调色板颜色索引数 |
biClrImportant |
32H | 4 bytes | 重要颜色索引数,0 表示都重要 |
只有压缩方式为 Bit-fields(0x3)时,位图信息头才是 56 字节,否则为 40 字节;多出的 16 字节是 R、G、B、A 四个 32bit 位域掩码。RGB565 的位域掩码是 0xF800、0x07E0、0x001F。
biHeight < 0):从左上角到右下角排列,水平从左到右、垂直从上到下。biHeight > 0):从左下角到右上角排列,水平从左到右、垂直从下到上。一般 BMP 都是倒向位图。#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
/**** BMP 文件头数据结构 ****/
typedef struct {
unsigned char type[2]; /* 文件类型 */
unsigned int size; /* 文件大小 */
unsigned short reserved1; /* 保留字段 1 */
unsigned short reserved2; /* 保留字段 2 */
unsigned int offset; /* 到位图数据的偏移量 */
} __attribute__ ((packed)) bmp_file_header;
/**** 位图信息头数据结构 ****/
typedef struct {
unsigned int size; /* 位图信息头大小 */
int width; /* 图像宽度 */
int height; /* 图像高度 */
unsigned short planes; /* 位面数 */
unsigned short bpp; /* 像素深度 */
unsigned int compression; /* 压缩方式 */
unsigned int image_size; /* 图像大小 */
int x_pels_per_meter; /* 像素/米 */
int y_pels_per_meter; /* 像素/米 */
unsigned int clr_used;
unsigned int clr_omportant;
} __attribute__ ((packed)) bmp_info_header;
/**** 静态全局变量 ****/
static int width; /* LCD X 分辨率 */
static int height; /* LCD Y 分辨率 */
static unsigned short *screen_base = NULL; /* 映射后的显存基地址 */
static unsigned long line_length; /* LCD 一行的长度(字节为单位) */
/********************************************************************
* 函数名称: show_bmp_image
* 功能描述: 在 LCD 上显示指定的 BMP 图片
* 输入参数: 文件路径
* 返 回 值: 成功返回 0, 失败返回 -1
********************************************************************/
static int show_bmp_image(const char *path)
{
bmp_file_header file_h;
bmp_info_header info_h;
unsigned short *line_buf = NULL; /* 行缓冲区 */
unsigned long line_bytes; /* BMP 图像一行的字节大小 */
unsigned int min_h, min_bytes;
int fd = -1;
int j;
/* 打开文件 */
if (0 > (fd = open(path, O_RDONLY))) {
perror("open error");
return -1;
}
/* 读取 BMP 文件头 */
if (sizeof(bmp_file_header) !=
read(fd, &file_h, sizeof(bmp_file_header))) {
perror("read error");
close(fd);
return -1;
}
if (0 != memcmp(file_h.type, "BM", 2)) {
fprintf(stderr, "it's not a BMP file\n");
close(fd);
return -1;
}
/* 读取位图信息头 */
if (sizeof(bmp_info_header) !=
read(fd, &info_h, sizeof(bmp_info_header))) {
perror("read error");
close(fd);
return -1;
}
/* 打印信息 */
printf("文件大小: %d\n"
"位图数据的偏移量: %d\n"
"位图信息头大小: %d\n"
"图像分辨率: %d*%d\n"
"像素深度: %d\n", file_h.size, file_h.offset,
info_h.size, info_h.width, info_h.height,
info_h.bpp);
/* 将文件读写位置移动到图像数据开始处 */
if (-1 == lseek(fd, file_h.offset, SEEK_SET)) {
perror("lseek error");
close(fd);
return -1;
}
/* 申请一个 buf、暂存 bmp 图像的一行数据 */
line_bytes = info_h.width * info_h.bpp / 8;
line_buf = malloc(line_bytes);
if (NULL == line_buf) {
fprintf(stderr, "malloc error\n");
close(fd);
return -1;
}
if (line_length > line_bytes)
min_bytes = line_bytes;
else
min_bytes = line_length;
/**** 读取图像数据显示到 LCD ****/
/* 本示例默认传入的 bmp 图像是 RGB565 格式 */
if (0 < info_h.height) { /* 倒向位图 */
if (info_h.height > height) {
min_h = height;
lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
screen_base += width * (height - 1); /* 定位到屏幕左下角位置 */
}
else {
min_h = info_h.height;
screen_base += width * (info_h.height - 1);
}
for (j = min_h; j > 0; screen_base -= width, j--) {
read(fd, line_buf, line_bytes); /* 读取出图像数据 */
memcpy(screen_base, line_buf, min_bytes);/* 刷入 LCD 显存 */
}
}
else { /* 正向位图 */
int temp = 0 - info_h.height; /* 负数转成正数 */
if (temp > height)
min_h = height;
else
min_h = temp;
for (j = 0; j < min_h; j++, screen_base += width) {
read(fd, line_buf, line_bytes);
memcpy(screen_base, line_buf, min_bytes);
}
}
/* 关闭文件、函数返回 */
close(fd);
free(line_buf);
return 0;
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验 */
if (2 != argc) {
fprintf(stderr, "usage: %s <bmp_file>\n", argv[0]);
exit(-1);
}
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
line_length = fb_fix.line_length;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示 BMP 图片 */
memset(screen_base, 0xFF, screen_size);
show_bmp_image(argv[1]);
/* 退出 */
munmap(screen_base, screen_size); /* 取消映射 */
close(fd); /* 关闭文件 */
exit(EXIT_SUCCESS); /* 退出进程 */
}
__attribute__ ((packed)),保证与文件中的字节布局严格对齐。memcmp(file_h.type, "BM", 2) 校验文件类型;再用 lseek(fd, file_h.offset, SEEK_SET) 跳到图像数据。screen_base 先定位到 width * (height - 1),每刷一行 screen_base -= width。screen_base += width。对应例程:20_libjpeg/show_jpeg_image.c。
.jpg / .jpeg。flowchart TD
A["jpeg_create_decompress<br/>创建解码对象"] --> B["jpeg_stdio_src<br/>指定数据源"]
B --> C["jpeg_read_header<br/>读取头信息"]
C --> D["设置解码参数<br/>out_color_space / scale"]
D --> E["jpeg_start_decompress<br/>开始解码"]
E --> F{"jpeg_read_scanlines<br/>逐行读取,每次 1 行"}
F -->|"output_scanline < min_h"| F
F -->|"读取完成"| G["jpeg_finish_decompress<br/>结束解码"]
G --> H["jpeg_destroy_decompress<br/>销毁对象"]
| 结构体 | 说明 |
|---|---|
struct jpeg_decompress_struct cinfo |
解码对象,记录 JPEG 详细信息与解码输出信息 |
struct jpeg_error_mgr jerr |
错误处理对象 |
调用 jpeg_read_header() 后可读到的信息:
cinfo.image_width /* jpeg 图像宽度 */
cinfo.image_height /* jpeg 图像高度 */
cinfo.num_components /* 颜色通道数 */
cinfo.jpeg_color_space /* jpeg 图像的颜色空间 */
jpeg_start_decompress() 后填充的输出信息:
cinfo.output_width /* 输出图像宽度 */
cinfo.output_height /* 输出图像高度 */
cinfo.output_components /* 每像素颜色通道数(灰度 1,RGB888 为 3) */
cinfo.output_scanline /* 接下来要读取的行索引 */
颜色空间枚举 J_COLOR_SPACE:JCS_UNKNOWN、JCS_GRAYSCALE、JCS_RGB、JCS_YCbCr、JCS_CMYK、JCS_YCCK、JCS_BG_RGB、JCS_BG_YCC。
| 函数 | 作用 |
|---|---|
jpeg_std_error(&jerr) |
绑定 libjpeg 默认错误处理 |
jpeg_create_decompress(&cinfo) |
创建解码对象 |
jpeg_stdio_src(&cinfo, fp) |
以标准 I/O 文件流作为数据源 |
jpeg_read_header(&cinfo, TRUE) |
读取 JPEG 头信息(约定必须调用) |
jpeg_calc_output_dimensions(&cinfo) |
在 start 前提前计算输出尺寸 |
jpeg_start_decompress(&cinfo) |
开始解码 |
jpeg_read_scanlines(&cinfo, buf, 1) |
逐行读取解码数据(当前一次只支持 1 行) |
jpeg_finish_decompress(&cinfo) |
完成解码 |
jpeg_destroy_decompress(&cinfo) |
销毁解码对象、释放资源 |
struct jpeg_error_mgr 中的 error_exit 是错误处理函数指针。jpeg_std_error() 将错误处理设为默认方式:内存不足、文件格式不对等错误发生时,默认处理函数会调用 exit() 结束整个进程。
/* 初始化错误处理对象、并将其与解压对象绑定 */
cinfo.err = jpeg_std_error(&jerr);
也可以注册自定义错误处理:
void my_error_exit(struct jpeg_decompress_struct *cinfo)
{
/* ... */
}
cinfo.err.error_exit = my_error_exit;
jpegsrc.v9b.tar.gz 为例,解压得 jpeg-9b。初始化交叉编译环境:
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
配置、编译、安装:
./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/jpeg/
make
make install
--host 指定目标平台(通常取交叉编译器名称前缀),--prefix 指定安装目录。安装目录含 bin(测试工具)、include(头文件,应用只需包含 jpeglib.h)、lib(动态库,libjpeg.so → libjpeg.so.9.2.0)。
移植到开发板:bin 下工具拷到 /usr/bin,lib 下库文件拷到 /usr/lib,注意保持符号链接(可先打包再解压)。移植前删除出厂旧库:
rm -rf /usr/lib/libjpeg.*
验证:执行 djpeg --help 能打印帮助即成功。
Tips:删除出厂
libjpeg后,出厂 Qt GUI 程序对 jpeg 图片解码会出问题(原图位置变空白)。新库版本不同也救不回来,知道即可。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <jpeglib.h>
typedef struct bgr888_color {
unsigned char red;
unsigned char green;
unsigned char blue;
} __attribute__ ((packed)) bgr888_t;
static int width; /* LCD X 分辨率 */
static int height; /* LCD Y 分辨率 */
static unsigned short *screen_base = NULL; /* 映射后的显存基地址 */
static unsigned long line_length; /* LCD 一行的长度(字节为单位) */
static unsigned int bpp; /* 像素深度 bpp */
static int show_jpeg_image(const char *path)
{
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *jpeg_file = NULL;
bgr888_t *jpeg_line_buf = NULL; /* 行缓冲区:存储从 jpeg 解压出来的一行图像数据 */
unsigned short *fb_line_buf = NULL; /* 行缓冲区:存储写入到 LCD 显存的一行数据 */
unsigned int min_h, min_w;
unsigned int valid_bytes;
int i;
/* 绑定默认错误处理函数 */
cinfo.err = jpeg_std_error(&jerr);
/* 打开.jpeg/.jpg 图像文件 */
jpeg_file = fopen(path, "r"); /* 只读方式打开 */
if (NULL == jpeg_file) {
perror("fopen error");
return -1;
}
/* 创建 JPEG 解码对象 */
jpeg_create_decompress(&cinfo);
/* 指定图像文件 */
jpeg_stdio_src(&cinfo, jpeg_file);
/* 读取图像信息 */
jpeg_read_header(&cinfo, TRUE);
printf("jpeg 图像大小: %d*%d\n", cinfo.image_width, cinfo.image_height);
/* 设置解码参数 */
cinfo.out_color_space = JCS_RGB; /* 默认就是 JCS_RGB */
/* cinfo.scale_num = 1; */
/* cinfo.scale_denom = 2; */
/* 开始解码图像 */
jpeg_start_decompress(&cinfo);
/* 为缓冲区分配内存空间 */
jpeg_line_buf = malloc(cinfo.output_components * cinfo.output_width);
fb_line_buf = malloc(line_length);
/* 判断图像和 LCD 屏哪个的分辨率更低 */
if (cinfo.output_width > width)
min_w = width;
else
min_w = cinfo.output_width;
if (cinfo.output_height > height)
min_h = height;
else
min_h = cinfo.output_height;
/* 读取数据 */
valid_bytes = min_w * bpp / 8; /* 真正写入到 LCD 显存的一行数据大小 */
while (cinfo.output_scanline < min_h) {
jpeg_read_scanlines(&cinfo, (unsigned char **)&jpeg_line_buf, 1);/* 每次读取一行 */
/* 将读取到的 BGR888 数据转为 RGB565 */
for (i = 0; i < min_w; i++)
fb_line_buf[i] = ((jpeg_line_buf[i].red & 0xF8) << 8) |
((jpeg_line_buf[i].green & 0xFC) << 3) |
((jpeg_line_buf[i].blue & 0xF8) >> 3);
memcpy(screen_base, fb_line_buf, valid_bytes);
screen_base += width; /* +width 定位到 LCD 下一行显存起点 */
}
/* 解码完成 */
jpeg_finish_decompress(&cinfo); /* 完成解码 */
jpeg_destroy_decompress(&cinfo); /* 销毁 JPEG 解码对象、释放资源 */
/* 关闭文件、释放内存 */
fclose(jpeg_file);
free(fb_line_buf);
free(jpeg_line_buf);
return 0;
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验 */
if (2 != argc) {
fprintf(stderr, "usage: %s <jpeg_file>\n", argv[0]);
exit(-1);
}
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
line_length = fb_fix.line_length;
bpp = fb_var.bits_per_pixel;
screen_size = line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示图片 */
memset(screen_base, 0xFF, screen_size);
show_jpeg_image(argv[1]);
/* 退出 */
munmap(screen_base, screen_size); /* 取消映射 */
close(fd); /* 关闭文件 */
exit(EXIT_SUCCESS); /* 退出进程 */
}
bgr888_t 的成员顺序是 red/green/blue,但读取时示例按该顺序访问,形成 BGR888 排布)。示例中 jpeg_line_buf[i].red/green/blue 分别对应转换需要的分量。jpeg_read_scanlines 参数:第二个参数是 unsigned char ** 类型,示例用 (unsigned char **)&jpeg_line_buf 强转。libjpeg 当前只支持一次读 1 行。cinfo.output_scanline < min_h,每读一行 output_scanline 自增 1。cinfo.scale_num / cinfo.scale_denom 可缩放输出,解出图像大小为 scale_num/scale_denom;JPEG 仅支持 1/1、1/2、1/4、1/8。默认 1/1。示例中注释掉,需要时打开。min_w/min_h 取图像与屏幕分辨率的较小值,超过屏幕的部分不显示。((R & 0xF8) << 8) | ((G & 0xFC) << 3) | ((B & 0xF8) >> 3)。对应例程:21_libpng/show_png_image.c(另有 setjmp.c 演示错误跳转)。
| 结构体 | 说明 |
|---|---|
png_struct(句柄 png_structp png_ptr) |
libpng 内部使用,几乎所有库函数的第一个参数;png_create_read_struct() 创建 |
png_info(句柄 png_infop info_ptr) |
描述 PNG 图像信息;新版本通过 png_get_XXX / png_set_XXX 访问成员 |
libpng 默认错误处理会调用 longjmp() 跳转到错误返回点,以便程序执行销毁、释放等清理工作。
#include <setjmp.h>
int setjmp(jmp_buf env); /* 设置跳转点,初次返回 0 */
void longjmp(jmp_buf env, int val); /* 跳回跳转点,val 作为 setjmp 的"伪"返回值 */
用 png_jmpbuf(png_ptr) 取出 png_struct 中的 jmp_buf:
/* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return -1;
}
val 不能设为 0,否则无法区分初次返回与“伪”返回。示例 setjmp.c:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf buf;
static void hello(void)
{
printf("hello world!\n");
longjmp(buf,1);
printf("Nice to meet you!\n");
}
int main(void)
{
if(0 == setjmp(buf)) {
printf("First return\n");
hello();
}
else
printf("Second return\n");
exit(0);
}
运行结果:先打印 First return、hello world!,longjmp 跳回后打印 Second return,Nice to meet you! 不会打印。
| 方式 | 函数 | 特点 | 适用条件 |
|---|---|---|---|
| high-level | png_read_png() |
一个函数一次性解码全部数据,内部自动分配缓冲区 | 内存足够大、且输出格式限定为 libpng 预定义转换 |
| low-level | png_read_info + png_set_xxx + png_read_update_info + png_read_image/png_read_rows |
灵活,需用户分配缓冲区 | 需要自定义转换或逐行处理 |
libpng 预定义数据转换类型(可 | 组合,作为 png_read_png 第三参数):
| 转换宏 | 说明 |
|---|---|
PNG_TRANSFORM_IDENTITY |
No transformation |
PNG_TRANSFORM_STRIP_16 |
Strip 16-bit samples to 8 bits |
PNG_TRANSFORM_STRIP_ALPHA |
Discard the alpha channel |
PNG_TRANSFORM_PACKING |
Expand 1, 2 and 4-bit samples to bytes |
PNG_TRANSFORM_PACKSWAP |
Change order of packed pixels to LSB first |
PNG_TRANSFORM_EXPAND |
Perform set_expand() |
PNG_TRANSFORM_INVERT_MONO |
Invert monochrome images |
PNG_TRANSFORM_SHIFT |
Normalize pixels to the sBIT depth |
PNG_TRANSFORM_BGR |
Flip RGB to BGR, RGBA to BGRA |
PNG_TRANSFORM_SWAP_ALPHA |
Flip RGBA to ARGB or GA to AG |
PNG_TRANSFORM_INVERT_ALPHA |
Change alpha from opacity to transparency |
PNG_TRANSFORM_SWAP_ENDIAN |
Byte-swap 16-bit samples |
PNG_TRANSFORM_GRAY_TO_RGB |
Expand grayscale samples to RGB |
png_read_png() 等价于依次执行:png_read_info → 按 transforms 设置转换 → png_read_image → png_read_end。
颜色类型宏:
#define PNG_COLOR_TYPE_GRAY 0
#define PNG_COLOR_TYPE_PALETTE (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE)
#define PNG_COLOR_TYPE_RGB (PNG_COLOR_MASK_COLOR)
#define PNG_COLOR_TYPE_RGB_ALPHA (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_ALPHA)
#define PNG_COLOR_TYPE_GRAY_ALPHA (PNG_COLOR_MASK_ALPHA)
| 函数 | 作用 |
|---|---|
png_create_read_struct(PNG_LIBPNG_VER_STRING, ...) |
创建解码 png_struct,失败返回 NULL |
png_create_info_struct(png_ptr) |
创建 png_info,失败返回 NULL |
png_jmpbuf(png_ptr) |
获取错误跳转点 jmp_buf |
png_init_io(png_ptr, fp) |
以标准 I/O 文件流指定数据源 |
png_read_png(png_ptr, info_ptr, transforms, NULL) |
high-level 一次性解码 |
png_read_info / png_read_update_info / png_read_end |
low-level 读取信息 / 更新信息 / 结束 |
png_read_image(png_ptr, row_pointers) |
low-level 一次性读全部数据 |
png_read_rows(png_ptr, &row_buf, NULL, 1) |
每次读 1 行或多行 |
png_get_image_width / png_get_image_height |
获取宽 / 高 |
png_get_bit_depth / png_get_color_type |
获取位深 / 颜色类型 |
png_get_rows(png_ptr, info_ptr) |
获取指向每一行数据缓冲区的指针数组(high-level 内部缓冲区) |
png_get_rowbytes |
每行数据字节数 |
png_set_strip_16 / png_set_expand / png_set_gray_to_rgb |
16→8 位 / 低位深扩展 / 灰度转 RGB |
png_malloc(png_ptr, size) |
libpng 提供的内存分配(等价 malloc) |
png_destroy_read_struct(&png_ptr, &info_ptr, NULL) |
销毁 png_struct(及其关联资源) |
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <png.h>
static int width; /* LCD X 分辨率 */
static int height; /* LCD Y 分辨率 */
static unsigned short *screen_base = NULL; /* 映射后的显存基地址 */
static unsigned long line_length; /* LCD 一行的长度(字节为单位) */
static unsigned int bpp; /* 像素深度 bpp */
static int show_png_image(const char *path)
{
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
FILE *png_file = NULL;
unsigned short *fb_line_buf = NULL; /* 行缓冲区:存储写入到 LCD 显存的一行数据 */
unsigned int min_h, min_w;
unsigned int valid_bytes;
unsigned int image_h, image_w;
png_bytepp row_pointers = NULL;
int i, j, k;
/* 打开 png 文件 */
png_file = fopen(path, "r"); /* 以只读方式打开 */
if (NULL == png_file) {
perror("fopen error");
return -1;
}
/* 分配和初始化 png_ptr、info_ptr */
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
fclose(png_file);
return -1;
}
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, NULL, NULL);
fclose(png_file);
return -1;
}
/* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file);
return -1;
}
/* 指定数据源 */
png_init_io(png_ptr, png_file);
/* 读取 png 文件 */
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
image_h = png_get_image_height(png_ptr, info_ptr);
image_w = png_get_image_width(png_ptr, info_ptr);
printf("分辨率: %d*%d\n", image_w, image_h);
/* 判断是不是 RGB888 */
if ((8 != png_get_bit_depth(png_ptr, info_ptr)) &&
(PNG_COLOR_TYPE_RGB != png_get_color_type(png_ptr, info_ptr))) {
printf("Error: Not 8bit depth or not RGB color");
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(png_file);
return -1;
}
/* 判断图像和 LCD 屏哪个的分辨率更低 */
if (image_w > width)
min_w = width;
else
min_w = image_w;
if (image_h > height)
min_h = height;
else
min_h = image_h;
valid_bytes = min_w * bpp / 8;
/* 读取解码后的数据 */
fb_line_buf = malloc(valid_bytes);
row_pointers = png_get_rows(png_ptr, info_ptr); /* 获取数据 */
unsigned int temp = min_w * 3; /* RGB888 一个像素 3 个字节 */
for (i = 0; i < min_h; i++) {
/* RGB888 转为 RGB565 */
for (j = k = 0; j < temp; j += 3, k++)
fb_line_buf[k] = ((row_pointers[i][j] & 0xF8) << 8) |
((row_pointers[i][j+1] & 0xFC) << 3) |
((row_pointers[i][j+2] & 0xF8) >> 3);
memcpy(screen_base, fb_line_buf, valid_bytes); /* 将一行数据刷入显存 */
screen_base += width; /* 定位到显存下一行 */
}
/* 结束、销毁/释放内存 */
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
free(fb_line_buf);
fclose(png_file);
return 0;
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验 */
if (2 != argc) {
fprintf(stderr, "usage: %s <png_file>\n", argv[0]);
exit(-1);
}
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
line_length = fb_fix.line_length;
bpp = fb_var.bits_per_pixel;
screen_size = line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示图片 */
memset(screen_base, 0xFF, screen_size); /* 屏幕刷白 */
show_png_image(argv[1]);
/* 退出 */
munmap(screen_base, screen_size); /* 取消映射 */
close(fd); /* 关闭文件 */
exit(EXIT_SUCCESS); /* 退出进程 */
}
png_read_png() 一次性解码;PNG_TRANSFORM_STRIP_ALPHA 丢掉 alpha 通道,得到 RGB888。row_pointers 由 png_get_rows() 取得,指向 libpng 内部为每一行分配的缓冲区;销毁 png_struct 时该缓冲区自动释放。R=row[i][j]、G=row[i][j+1]、B=row[i][j+2],转 RGB565。移植顺序:先 zlib 后 libpng。
zlib:
tar -xzf zlib-1.2.10.tar.gz
./configure --prefix=/home/dt/tools/zlib/
make
make install
libpng(需先导出 zlib 路径):
export LDFLAGS="${LDFLAGS} -L/home/dt/tools/zlib/lib"
export CFLAGS="${CFLAGS} -I/home/dt/tools/zlib/include"
export CPPFLAGS="${CPPFLAGS} -I/home/dt/tools/zlib/include"
./configure --prefix=/home/dt/tools/png --host=arm-poky-linux-gnueabi
make
make install
移植到开发板前删除出厂库并保持符号链接:
rm -rf /usr/lib/libz.* /lib/libz.*
rm -rf /lib/libpng* /usr/lib/libpng*
| 维度 | libjpeg | libpng |
|---|---|---|
| 压缩 | 有损 | 无损(LZ77 派生) |
| 依赖 | 无 | 依赖 zlib |
| 主要对象 | jpeg_decompress_struct + jpeg_error_mgr |
png_struct + png_info |
| 读取方式 | 逐行 jpeg_read_scanlines |
high-level 一次性 png_read_png 或 low-level 逐行 |
| 输出格式 | 默认 BGR888 | 由转换参数决定,STRIP_ALPHA 后为 RGB888 |
| 错误处理 | jpeg_std_error 默认 exit,可自定义 error_exit |
默认 longjmp,用 setjmp(png_jmpbuf()) 设返回点 |
| 结束 | jpeg_finish_decompress + jpeg_destroy_decompress |
png_destroy_read_struct |
对应例程:22_lcd_vertical_display/lcd_vertical_display.c。
核心结论:横屏/竖屏切换与驱动程序无关,是应用层要解决的问题。
以 800×480 为例,屏幕正向放置时(横屏):
左上角 (0, 0) 右上角 (800-1, 0)
左下角 (0, 480-1) 右下角 (800-1, 480-1)
像素排列从左到右、从上到下,这是硬件固定属性,无法配置。像素 (x, y) 的显存地址(字节基地址 base):
base + (y * width + x) * pix_bytes
如果应用层想把左下角当原点 (0, 0)(一种常见的竖屏坐标分布),则应用坐标与物理坐标的对应为:
左上角 (480-1, 0) 右上角 (480-1, 800-1)
左下角 (0, 0) 右下角 (0, 800-1)
此时应用坐标 (x, y) 不能再用 base + (y * width + x),而应通过物理坐标 (y, height-1-x) 计算:
竖屏 (x, y) → 显存地址 = base + ((height - 1 - x) * width + y) * pix_bytes
在 RGB565 的 unsigned short *screen_base 视角下(lcd_max_y = lcd_height - 1):
screen_base[(lcd_max_y - x) * lcd_width + y]
flowchart LR
A["竖屏逻辑坐标 (x, y)"] -->|"lcd_max_y - x 得到物理行<br/>y 得到物理列"| B["物理像素 (y, lcd_max_y - x)"]
B --> C["screen_base[(lcd_max_y - x) * lcd_width + y]"]
说明:该公式只适用于上面这种“左下角为原点”的竖屏分布。把上图旋转 180° 也是竖屏,但公式不同;坐标变换推导本身很简单,按同样的方法推即可。
(x, y) 中 x 沿宽方向、y 沿高方向,水平线 x 递增地址 +1,垂直线 y 递增地址 +width。-lcd_width,垂直线每走一步地址 +1。#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int lcd_width; /* LCD X 分辨率 */
static int lcd_height; /* LCD Y 分辨率 */
static int lcd_max_y; /* LCD Y 坐标最大值 */
static int user_width; /* 竖屏模式下 X 分辨率 */
static int user_height; /* 竖屏模式下 Y 分辨率 */
static unsigned short *screen_base = NULL; /* 映射后的显存基地址 */
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
/* 对传入参数的校验 */
if (x >= user_width)
x = user_width - 1;
if (y >= user_height)
y = user_height - 1;
/* 填充颜色 */
screen_base[(lcd_max_y-x) * lcd_width + y] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
unsigned int end;
unsigned long temp;
/* 对传入参数的校验 */
if (x >= user_width)
x = user_width - 1;
if (y >= user_height)
y = user_height - 1;
/* 填充颜色 */
temp = (lcd_max_y-x) * lcd_width + y;
if (dir) { /* 水平线 */
end = x + length - 1;
if (end >= user_width)
end = user_width - 1;
for ( ; x <= end; x++, temp -= lcd_width)
screen_base[temp] = rgb565_color;
}
else { /* 垂直线 */
end = y + length - 1;
if (end >= user_height)
end = user_height - 1;
for ( ; y <= end; y++, temp++)
screen_base[temp] = rgb565_color;
}
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color); /* 上边 */
lcd_draw_line(start_x, end_y, 1, x_len, color); /* 下边 */
lcd_draw_line(start_x, start_y + 1, 0, y_len, color); /* 左边 */
lcd_draw_line(end_x, start_y + 1, 0, y_len, color); /* 右边 */
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
unsigned long temp;
unsigned long step_size_count;
int x;
/* 对传入参数的校验 */
if (end_x >= user_width)
end_x = user_width - 1;
if (end_y >= user_height)
end_y = user_height - 1;
/* 填充颜色 */
temp = (lcd_max_y-start_x) * lcd_width + start_y;
for ( ; start_y <= end_y; start_y++, temp++) {
step_size_count = 0;
for (x = start_x; x <= end_x; x++, step_size_count += lcd_width)
screen_base[temp - step_size_count] = rgb565_color;
}
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
lcd_width = fb_var.xres;
lcd_height = fb_var.yres;
lcd_max_y = lcd_height - 1;
user_width = fb_var.yres; /* 竖屏 X = 原 Y */
user_height = fb_var.xres; /* 竖屏 Y = 原 X */
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块 */
int w = user_height * 0.25; /* 方块的宽度为 1/4 屏幕高度 */
lcd_fill(0, user_width-1, 0, user_height-1, 0x0); /* 清屏 */
lcd_fill(0, w, 0, w, 0xFF0000); /* 红色方块 */
lcd_fill(user_width-w, user_width-1, 0, w, 0xFF00); /* 绿色方块 */
lcd_fill(0, w, user_height-w, user_height-1, 0xFF); /* 蓝色方块 */
lcd_fill(user_width-w, user_width-1, user_height-w, user_height-1, 0xFFFF00);/* 黄色方块 */
/* 画线: 十字交叉线 */
lcd_draw_line(0, user_height * 0.5, 1, user_width, 0xFFFFFF); /* 白色水平线 */
lcd_draw_line(user_width * 0.5, 0, 0, user_height, 0xFFFFFF); /* 白色垂直线 */
/* 画矩形 */
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * user_width;
s_y = w;
e_x = user_width - s_x;
e_y = user_height - s_y;
for ( ; (s_x <= e_x) && (s_y <= e_y);
s_x+=5, s_y+=5, e_x-=5, e_y-=5)
lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
/* 退出 */
munmap(screen_base, screen_size);
close(fd);
exit(EXIT_SUCCESS);
}
user_width = fb_var.yres(原 Y 变竖屏 X),user_height = fb_var.xres(原 X 变竖屏 Y)。lcd_test.c(图 19.4.3 横屏效果)对比,本例程显示的画面变成竖向(图 22.2.1)。对应例程:23_freetype/show_char.c(取模方式)与 23_freetype/freetype_test.c(FreeType 方式)。
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 取模显示 | 用取模软件生成字符点阵二维数组,逐 bit 打点 | 简单、无需库 | 只能显示固定几个字符,很“low” |
| 字体引擎 | 解析 .ttf/.otf/.ttc 等字体文件,读取字形位图 |
通用、支持任意字符与字号 | 需移植 FreeType 等库 |
取模原理:字符点阵中每个小方块对应一个 bit,填充用 1、不填充用 0。例如“正”是 64×86 点阵,用 unsigned char arr[86][8] 存储(宽 64 点 = 8 字节/行)。取模时选择“从左到右、从上到下”。
FreeType 是完全免费(开源)的软件字体引擎,设计小巧、高效、可定制、可移植,提供统一接口访问多种字体格式(FreeType 2 已取代废弃的 FreeType 1)。
移植步骤(本书选择 2.8 版本):
freetype-2.8.tar.gz。tools 下建安装目录 freetype,解压后进入源码目录。打开 include/freetype/config/ftoption.h,去掉这两个宏的注释以启用系统 zlib 与 PNG 位图支持:
#define FT_CONFIG_OPTION_SYSTEM_ZLIB
#define FT_CONFIG_OPTION_USE_PNG
配置、编译、安装:
./configure --prefix=/home/dt/tools/freetype/ --host=arm-poky-linux-gnueabi \
--with-zlib=yes --with-bzip2=no --with-png=yes --with-harfbuzz=no \
ZLIB_CFLAGS="-I/home/dt/tools/zlib/include -L/home/dt/tools/zlib/lib" ZLIB_LIBS=-lz \
LIBPNG_CFLAGS="-I/home/dt/tools/png/include -L/home/dt/tools/png/lib" LIBPNG_LIBS=-lpng
make
make install
应用需包含两个头文件(后者是用宏定义的头文件):
#include <ft2build.h>
#include FT_FREETYPE_H
移植到开发板前删除旧库,再拷贝新库并保持符号链接:
rm -rf /usr/lib/libfreetype.*
像素点数 = 点数 * dpi / 72。字形布局参数(水平布局为例):
| 参数 | 含义 |
|---|---|
origin / 基准线 |
水平基线与垂直基线用于定位与对齐字形 |
width / height |
字形轮廓最左到最右、最上到最下的距离 |
bearingX(bitmap_left) |
垂直基线到字形轮廓最左边的距离;水平布局为正 |
bearingY(bitmap_top) |
水平基线到字形轮廓最上边的距离;轮廓在基线上方为正 |
xMin/xMax、yMin/yMax |
字形轮廓四边位置,构成边界框(bbox) |
advance(advance.x) |
步进宽度/字间距,相邻两原点距离;26.6 固定浮点格式 |
bitmap.buffer |
字形位图,每点 1 字节;值为 0 不填充,大于 0 填充 |
bitmap.rows / bitmap.width |
位图行数 / 列数 |
对齐:水平基线负责上下对齐,垂直基线负责左右对齐。画字形先定位左上角:若原点为 (100,100),则左上角为 (100+bearingX, 100-bearingY)。
⚠️ 来源说明:
FT_Load_Glyph+FT_Render_Glyph的两步加载/渲染方式、以及FT_Get_Char_Index的完整用法,本节按 FreeType 通用接口补充,教材对应段落以图示为主。
| 函数 | 作用 |
|---|---|
FT_Init_FreeType(&library) |
初始化 FreeType 库对象,成功返回 0 |
FT_New_Face(library, path, face_index, &face) |
加载字体文件,创建 face;face_index 通常为 0,face->num_faces 指示文件内 face 数 |
FT_Set_Pixel_Sizes(face, w, h) |
以像素为单位设置字体宽高;某一位为 0 则等于另一位 |
FT_Set_Char_Size(face, w, h, hdpi, vdpi) |
以 1/64 点为单位的宽高 + dpi 设置字号 |
FT_Set_Transform(face, &matrix, &pen) |
设置 2×2 变换矩阵与原点,实现旋转/斜体 |
FT_Get_Char_Index(face, code) |
字符编码转字形索引 |
FT_Load_Char(face, char, flags) |
加载字符字形;FT_LOAD_RENDER 直接渲染为位图 |
FT_Load_Glyph(face, index, flags) |
按字形索引加载 |
FT_Render_Glyph(slot, mode) |
把已加载的字形轮廓渲染为位图 |
FT_Done_Face(face) / FT_Done_FreeType(library) |
释放 face / 库 |
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <math.h> /* 数学库函数头文件 */
#include <wchar.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#define FB_DEV "/dev/fb0" /* LCD 设备节点 */
#define argb8888_to_rgb565(color) ( \
{ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static unsigned int width; /* LCD 宽度 */
static unsigned int height; /* LCD 高度 */
static unsigned short *screen_base = NULL; /* LCD 显存基地址 RGB565 */
static unsigned long screen_size;
static int fd = -1;
static FT_Library library;
static FT_Face face;
static int fb_dev_init(void)
{
struct fb_var_screeninfo fb_var = {0};
struct fb_fix_screeninfo fb_fix = {0};
/* 打开 framebuffer 设备 */
fd = open(FB_DEV, O_RDWR);
if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
return -1;
}
/* 获取 framebuffer 设备信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 内存映射 */
screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
return -1;
}
/* LCD 背景刷成黑色 */
memset(screen_base, 0xFF, screen_size);
return 0;
}
static int freetype_init(const char *font, int angle)
{
FT_Error error;
FT_Vector pen;
FT_Matrix matrix;
float rad; /* 旋转角度 */
/* FreeType 初始化 */
FT_Init_FreeType(&library);
/* 加载 face 对象 */
error = FT_New_Face(library, font, 0, &face);
if (error) {
fprintf(stderr, "FT_New_Face error: %d\n", error);
exit(EXIT_FAILURE);
}
/* 原点坐标 */
pen.x = 0 * 64;
pen.y = 0 * 64; /* 原点设置为(0, 0) */
/* 2x2 矩阵初始化 */
rad = (1.0 * angle / 180) * M_PI; /* 角度转换为弧度,M_PI 是圆周率 */
#if 0 /* 非水平方向 */
matrix.xx = (FT_Fixed)( cos(rad) * 0x10000L);
matrix.xy = (FT_Fixed)(-sin(rad) * 0x10000L);
matrix.yx = (FT_Fixed)( sin(rad) * 0x10000L);
matrix.yy = (FT_Fixed)( cos(rad) * 0x10000L);
#endif
#if 1 /* 斜体 水平方向显示的 */
matrix.xx = (FT_Fixed)(cos(rad) * 0x10000L);
matrix.xy = (FT_Fixed)(sin(rad) * 0x10000L);
matrix.yx = (FT_Fixed)(0 * 0x10000L);
matrix.yy = (FT_Fixed)(1 * 0x10000L);
#endif
/* 设置 */
FT_Set_Transform(face, &matrix, &pen);
FT_Set_Pixel_Sizes(face, 40, 0); /* 设置字体大小 */
return 0;
}
static void lcd_draw_character(int x, int y,
const wchar_t *str, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
FT_GlyphSlot slot = face->glyph;
size_t len = wcslen(str); /* 计算字符的个数 */
long int temp;
int n;
int i, j, p, q;
int max_x, max_y, start_y, start_x;
/* 循环加载各个字符 */
for (n = 0; n < len; n++) {
/* 加载字形、转换得到位图数据 */
if (FT_Load_Char(face, str[n], FT_LOAD_RENDER))
continue;
start_y = y - slot->bitmap_top; /* 字形轮廓上边 y 坐标起点,注意减去 bitmap_top */
if (0 > start_y) { /* 如果为负数 */
q = -start_y;
temp = 0;
j = 0;
}
else { /* 正数 */
q = 0;
temp = width * start_y;
j = start_y;
}
max_y = start_y + slot->bitmap.rows; /* 字形轮廓下边 y 坐标结束位置 */
if (max_y > (int)height)
max_y = height;
for (; j < max_y; j++, q++, temp += width) {
start_x = x + slot->bitmap_left; /* 起点要加上左边空余部分长度 */
if (0 > start_x) {
p = -start_x;
i = 0;
}
else {
p = 0;
i = start_x;
}
max_x = start_x + slot->bitmap.width;
if (max_x > (int)width)
max_x = width;
for (; i < max_x; i++, p++) {
/* 如果数据不为 0,则表示需要填充颜色 */
if (slot->bitmap.buffer[q * slot->bitmap.width + p])
screen_base[temp + i] = rgb565_color;
}
}
/* 调整到下一个字形的原点 */
x += slot->advance.x / 64; /* 26.6 固定浮点格式 */
y -= slot->advance.y / 64;
}
}
int main(int argc, char *argv[])
{
/* LCD 初始化 */
if (fb_dev_init())
exit(EXIT_FAILURE);
/* freetype 初始化 */
if (freetype_init(argv[1], atoi(argv[2])))
exit(EXIT_FAILURE);
/* 在 LCD 上显示中文 */
int y = height * 0.25;
lcd_draw_character(30, 80, L"路漫漫其修远兮,吾将上下而求索", 0x000000);
lcd_draw_character(30, y + 80, L"莫愁前路无知己,天下谁人不识君", 0x9900FF);
lcd_draw_character(30, 2 * y + 80, L"君不见黄河之水天上来,奔流到海不复回", 0xFF0099);
lcd_draw_character(30, 3 * y + 80, L"君不见高堂明镜悲白发,朝如青丝暮成雪", 0x9932CC);
/* 退出程序 */
FT_Done_Face(face);
FT_Done_FreeType(library);
munmap(screen_base, screen_size);
close(fd);
exit(EXIT_SUCCESS);
}
逐段说明:
fb_dev_init():完整的 FrameBuffer 初始化模板(open → ioctl → mmap → memset)。freetype_init():初始化库、加载字体、设置变换矩阵与字号。0x10000L 是 FreeType 的 16.16 定点;FT_Set_Transform 配合 pen 原点实现旋转/斜体;FT_Set_Pixel_Sizes(face, 40, 0) 设置字号 40 像素,高度 0 表示与宽度相等。lcd_draw_character():用 wcslen 计算宽字符个数,逐个 FT_Load_Char(..., FT_LOAD_RENDER) 直接得到位图。bitmap_top/bitmap_left 用于把字形左上角对齐到 (x, y);bitmap.buffer 中非 0 的点才填充颜色。x += advance.x / 64(26.6 定点右移 6 位)、y -= advance.y / 64,移动到下一个字形原点。取模方式不依赖 FreeType,直接用字符点阵数组打点。核心函数:
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
/********************************************************************
* 函数名称: lcd_draw_character
* 功能描述: 在 LCD 屏指定位置处(x, y)画字符;指针 ch 指向字符子模数组,
* 参数 w、h 分别表示字符的宽度和高度
********************************************************************/
static void lcd_draw_character(unsigned int x, unsigned int y,
const unsigned char *ch, unsigned int w,
unsigned int h, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);/* 得到 RGB565 颜色值 */
unsigned long temp;
unsigned int end_x, end_y;
int j;
int columns;
/*
* 计算二维数组有多少列。参数 w 是字符宽度,1 个宽度对应 1 个 bit 位,
* 并不是一个字节;若宽度不是 8 的整数倍通常会补零。
*/
columns = w / 8; /* 1byte = 8bit */
if (0 != w % 8) columns++;
/* 对参数进行限定 */
if (w < 1 || h < 1) return;
if (x >= width || y >= height) return;
/* 计算出结束坐标位置 */
end_x = x + w - 1;
end_y = y + h - 1;
/* 对结束坐标位置进行限定 */
if (end_x >= width)
end_x = width - 1;
if (end_y >= height)
end_y = height - 1;
/* 计算有效宽度 */
h = end_y - y + 1;
w = end_x - x + 1;
/* 打点 */
temp = y * width + x; /* 定位到起点 */
for (y = 0; y < h; y++, temp += width) {
for (x = 0, j = 0; x < w; ) {
if (*(ch + y * columns + j) & (0x1 << (x % 8)))
screen_base[temp + x] = rgb565_color;
x++;
if (0 == x % 8) j++;
}
}
}
字体数组形如 static unsigned char ch_char1[86][8] = { {0x00,...}, ... },四个数组分别对应“正点原子”,调用时一个个居中绘制:
int x = width * 0.5 - 128;
int y = height * 0.5 - 43;
lcd_draw_character(x, y, (unsigned char *)ch_char1, 64, 86, 0xFF00FF);
lcd_draw_character(x + 64, y, (unsigned char *)ch_char2, 64, 86, 0xFF00FF);
lcd_draw_character(x + 128, y, (unsigned char *)ch_char3, 64, 86, 0xFF00FF);
lcd_draw_character(x + 192, y, (unsigned char *)ch_char4, 64, 86, 0xFF00FF);
要点: columns = w / 8(向上取整)得到二维数组列数;每个字节从左到右对应 8 个 bit;0x1 << (x % 8) 逐 bit 判断是否填充。
Windows 的字体放在 C:\Windows\Fonts,Linux 通常放在 /usr/share/fonts,格式有 otf、ttf、ttc 等。移植的 FreeType 与开发板自带字体可能不匹配,可从 Windows 拷贝一个字体(如宋体 SIMSUN.TTC)到开发板使用。
先初始化交叉编译环境(每个新终端一次):
export CC=arm-poky-linux-gnueabi-gcc # 或用环境初始化脚本导出的 ${CC}
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
| 例程 | 依赖库 | 交叉编译命令 |
|---|---|---|
lcd_info.c / lcd_test.c / bmp_show.c / lcd_vertical_display.c |
无 | ${CC} -o testApp xxx.c |
show_jpeg_image.c |
libjpeg | ${CC} -o testApp show_jpeg_image.c -I /home/dt/tools/jpeg/include -L /home/dt/tools/jpeg/lib -ljpeg |
show_png_image.c |
libpng + zlib | ${CC} -o testApp show_png_image.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -lpng -lz |
freetype_test.c |
freetype + zlib + libpng + libm | ${CC} -o testApp freetype_test.c -I/home/dt/tools/freetype/include/freetype2 -L/home/dt/tools/freetype/lib -lfreetype -L/home/dt/tools/zlib/lib -lz -L/home/dt/tools/png/lib -lpng -lm |
编译选项说明:
-I:头文件搜索路径(libpng 不需要指定 zlib 头文件)。-L:库文件搜索路径(可重复多次)。-l:链接的库,jpeg→libjpeg.so、png→libpng.so、z→libz.so、freetype→libfreetype.so、m→libm.so。include/freetype2 目录下,所以 -I 要指到 freetype2。testApp。.jpg/.png/.ttc 文件)拷到用户家目录,例如 /home/root。运行:
./testApp # lcd_info / lcd_test
./testApp image.bmp # bmp_show
./testApp image.jpg # libjpeg
./testApp image.png # libpng
./testApp # 竖屏
./testApp SIMSUN.TTC 0 # freetype,参数:字体文件 + 旋转角度
验证库移植:djpeg --help(libjpeg)能打印帮助即成功。
| 现象 | 可能原因 | 处理 |
|---|---|---|
open error |
设备节点不存在 / 权限不足 | 确认 /dev/fb0 存在;用 root 或加权限 |
mmap error |
screen_size 计算错误或参数非法 |
检查 line_length * yres;先跑 lcd_info 确认参数 |
| 花屏 / 颜色错乱 | 把 RGB565 当 RGB888(或反之) | 用 lcd_info 打印像素格式,按实际位域转换 |
| 图片上下颠倒 | BMP 正/倒向位图处理错 | 按 biHeight 正负分别处理 |
| 竖屏画面方向不对 | 坐标变换公式与预期布局不一致 | 确认原点定义(左下角/左上角),套用对应公式 |
| 屏幕内容被 Qt 覆盖 | 出厂 Qt GUI 未退出 | 先退出 Qt 程序再运行 |
| 运行报缺库 | 目标板 /usr/lib 无对应 .so |
拷贝库文件并保持符号链接 |
| FreeType 显示乱码 | 字体文件与程序不匹配 | 换一个 .ttf/.ttc 字体(如宋体) |
| 删除旧库后 Qt 图片空白 | 出厂 Qt 依赖旧版 libjpeg/libpng 等 | 属预期现象,学习阶段知道即可 |
编译报缺 -lz/-lpng |
未链接依赖库 | 按第 10 节补全 -L/-l |
常用辅助命令:
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125 # 清屏为黑
djpeg --help # 验证 libjpeg
cat /proc/fb # 查看已注册的 fb 设备
| 平台 / 场景 | 显示抽象 | 上屏方式 | 特点 |
|---|---|---|---|
| 嵌入式 Linux(FrameBuffer) | /dev/fb0 + 显存 |
mmap 后直接写显存 |
接口简单、稳定,本书主线 |
| 嵌入式 Linux(DRM/KMS) | /dev/dri/card0 + plane |
drmMode* 提交 framebuffer |
现代方案,支持多图层/硬件合成 |
| Windows | GDI / DIB | 位图 API、绘图函数 | 图形子系统封装,不直接暴露显存 |
| 裸机 MCU(如 STM32 + 并口屏) | LCD 控制器寄存器 + FSMC/SDRAM | 写帧缓存地址,靠 DMA/控制器扫描 | 无操作系统,需自己配时序 |
| Android | SurfaceFlinger + Gralloc | Surface/Canvas,合成后送显示 | 多层合成,应用不碰显存 |
⚠️ 来源说明:DRM/KMS、Android SurfaceFlinger、Windows GDI 的对比属于教材之外的扩展知识,用于建立全局认知;教材只讲解 Linux FrameBuffer。
本知识库内的驱动侧视角见 [[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/03-LCD驱动]],触摸部分见 [[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/04-触摸屏驱动]],输入设备与 tslib 见 [[03-外设与高级IO编程/03-输入设备与tslib]]。
FrameBuffer 是 Linux 的显示驱动接口,把显示设备抽象成一块保存一帧图像的显存,设备节点为 /dev/fbX,读写它相当于读写显示缓冲区。用 mmap 是因为一帧数据量很大(如 1920×1080 ARGB8888 约 8MB)且画面频繁更新,普通 read/write 需要内核与用户空间之间反复拷贝,效率低;mmap 把显存映射到用户地址空间后可直接内存访问,一次映射、反复读写。
用 ioctl(fd, FBIOGET_VSCREENINFO, &fb_var) 取 fb_var,看 bits_per_pixel 和 red/green/blue 的 offset、length。例如 R<11 5> G<5 6> B<0 5> 且 bpp=16 就是 RGB565。RGB888→RGB565:
fb = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
流程:jpeg_create_decompress → jpeg_stdio_src → jpeg_read_header → 设置 out_color_space(默认 JCS_RGB)/scale → jpeg_start_decompress → 循环 jpeg_read_scanlines 每次读 1 行 → jpeg_finish_decompress → jpeg_destroy_decompress。libjpeg 默认输出 BGR888(R 在低 8 位、B 在高 8 位),所以按 B、G、R 的内存顺序取分量再转 RGB565。
libpng 默认错误处理会调用 longjmp() 跳转,需要用 setjmp(png_jmpbuf(png_ptr)) 设置错误返回点,跳回后执行 png_destroy_read_struct 等清理,避免内存泄漏;longjmp 的 val 不能为 0。high-level 用 png_read_png() 一次性解码全部数据并自动分配缓冲区,简单但不灵活、输出格式受预定义转换限制;low-level 用 png_read_info + png_set_xxx + png_read_update_info + png_read_image/png_read_rows,需自己分配缓冲区但灵活。high-level 获取数据用 png_get_rows。
竖屏把应用坐标重新定义:例如以左下角为原点,应用 (x, y) 对应物理坐标 (y, height-1-x),代入横屏公式 base + (y*width + x)*pix_bytes,得到 base + ((height-1-x)*width + y)*pix_bytes(RGB565 下即 screen_base[(lcd_max_y-x)*lcd_width + y])。该公式只对应一种竖屏布局,旋转 180° 的竖屏公式不同。FreeType 中 bitmap_top(bearingY)是水平基线到字形轮廓上边的距离,用来把字形左上角对齐到绘制点 (x, y-bearingY);advance.x 是步进宽度(26.6 定点),画完一个字符后 x += advance.x/64 定位到下一个字符原点,保证字符间距与对齐。
内容来源
11、Linux C 应用编程例程源码 → 19_lcd、20_libjpeg、21_libpng、22_lcd_vertical_display、23_freetype