04-FrameBuffer与LCD应用编程.md 91 KB


title: FrameBuffer 与 LCD 应用编程 tags: [

嵌入式Linux,
Linux应用编程,
FrameBuffer,
LCD,
fb0,
mmap,
RGB565,
libjpeg,
libpng,
FreeType,
竖屏,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十九章 FrameBuffer应用编程;第二十章 在LCD上显示jpeg图像;第二十一章 在LCD上显示png图片;第二十二章 LCD横屏切换为竖屏;第二十三章 在LCD上显示字符"

FrameBuffer 与 LCD 应用编程

💡 关联知识:[[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 显示字符,所有例程完整收录,可直接交叉编译运行。


1. FrameBuffer 是什么

1.1 帧缓冲与设备节点

  • Frame = 帧,buffer = 缓冲,Framebuffer = 帧缓冲 = 保存着一帧图像的一块内存
  • 它是 Linux 的显示驱动接口:屏蔽不同显示硬件的实现,对应用层抽象为一块显示内存(显存)。应用直接读写这块缓冲区,物理显存的位置等细节由 FrameBuffer 驱动处理。
  • 显示设备在 Linux 中就叫 FrameBuffer 设备,所以 LCD 就是 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

1.2 显示通路

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

1.3 操作 /dev/fbX 的五个步骤

  1. 打开 /dev/fbX 设备文件,得到文件描述符 fd
  2. ioctl() 获取当前显示设备的参数信息(分辨率、像素格式),据此计算显存大小。
  3. 存储映射 I/Ommap)把屏幕显存映射到用户空间。
  4. 映射成功后直接读写显存,进行绘图、图片显示等操作。
  5. 完成后 munmap() 取消映射,close() 关闭设备文件。

1.4 为什么用 mmap 而不是 read/write

普通 I/O(read/write)也能操作显存,但数据量大时效率低。举例:1920×1080、ARGB8888,刷一帧就是

1920 × 1080 × 32 / 8 = 8294400 字节 ≈ 8MB

显示画面还在动态更新,数据量庞大,普通 I/O 必然效率低下,所以采用存储映射 I/O:映射一次,之后就是内存访问。


2. 屏幕参数:两个结构体与三个 ioctl

FrameBuffer 应用编程绕不开 <linux/fb.h> 里的两个结构体:可变参数 struct fb_var_screeninfo 与固定参数 struct fb_fix_screeninfo

2.1 ioctl request 速查

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);

2.2 struct fb_var_screeninfo

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 物理尺寸(毫米),与分辨率无关。

2.3 struct fb_bitfield

struct fb_bitfield {
    __u32 offset;       /* 偏移量 */
    __u32 length;       /* 长度 */
    __u32 msb_right;    /* != 0 : Most significant bit is right */
};

打印 red.offset/lengthgreen.offset/lengthblue.offset/length 就能确定像素格式。例如正点原子 7 寸 800×480 屏打印出 R<11 5> G<5 6> B<0 5>,表示:

16bit 颜色值:高 5 位 = R,中间 6 位 = G,低 5 位 = B  →  RGB565

2.4 struct fb_fix_screeninfo

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 得到显示缓冲区大小。

2.5 字段速查表

结构体 字段 含义 典型用途
fb_var xres / yres 可视分辨率 计算 widthheight
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 显存物理地址/长度 物理信息,应用不可直接用

2.6 像素格式与 RGB565

一个像素点的字节数 = bpp / 8。RGB888 用 3 字节,RGB565 用 2 字节。开发板出厂系统把 LCD 实现为 RGB565 显示设备,所以例程里普遍使用 unsigned short 指针访问显存。

RGB565 的位域掩码为 0xF8000x07E00x001F。从 ARGB8888 的颜色值转 RGB565 的宏:

#define argb8888_to_rgb565(color)   ({ \
            unsigned int temp = (color); \
            ((temp & 0xF80000UL) >> 8) | \
            ((temp & 0xFC00UL) >> 5) | \
            ((temp & 0xF8UL) >> 3); \
            })

3. 例程一:读取屏幕参数 lcd_info.c

对应例程: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/blueoffsetlength,就能按第 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 改参数,驱动支持不完善时可能出问题。


4. 例程二:LCD 基本操作(画点/画线/画矩形/填充)lcd_test.c

对应例程: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);                 /* 退出进程 */
}

4.1 显存定位公式

以显存基地址 screen_baseunsigned short *,RGB565)为例,像素 (x, y) 对应:

screen_base[y * width + x]

若用字节指针 (unsigned char *)base,则:

base + (y * width + x) * pix_bytes

其中 pix_bytes 是一个像素占用的字节数。一行跨度为 widthunsigned short,所以换行时指针 +width 即可。

4.2 四个函数职责

函数 作用 关键参数
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 用双层循环逐行填充。

4.3 主函数流程

  1. open 打开 LCD,得到 fd
  2. ioctl 取可变/固定参数,算出 screen_size = line_length * yreswidth = xresheight = yres
  3. mmap 建立映射。注意 MAP_FAILED 判断要先把指针转成 void * 再比较。
  4. 直接操作 screen_base 画方块、十字线、同心矩形。
  5. munmap + close 退出。

4.4 画斜线说明

本组函数只支持水平/垂直线。画斜线需要 Bresenham 之类的算法,不属于本章要点。


5. 例程三:在 LCD 上显示 BMP 图片 bmp_show.c

对应例程:19_lcd/bmp_show.c。BMP 未压缩、解析简单,是理解“图像文件 → 显存”的最好入口。

5.1 BMP 文件由四部分组成

数据段 大小(Byte) 说明
BMP 文件头(bmp file header) 14 文件格式、大小、到位图数据的偏移量
位图信息头(bitmap information) 通常 40 或 56 头大小、图像尺寸、图像大小、位平面数、压缩方式、颜色索引
调色板(color palette) 由颜色索引数决定 可选;索引色图像才有
位图数据(bitmap data) 由图像尺寸决定 图像数据本体

16 位(R5/G6/B5)、24 位(R8/G8/B8)真彩色图像不需要调色板,位图信息头后紧跟位图数据。

5.2 BMP 文件头成员

变量名 地址偏移 大小 作用
bfType 00H 2 bytes 文件类型,BM 表示 Windows 位图
bfSize 02H 4 bytes 文件大小
bfReserved1 06H 2 bytes 保留,必须为 0
bfReserved2 08H 2 bytes 保留,必须为 0
bfOffBits 0AH 4 bytes 从文件头到图像数据的字节偏移量,用它快速定位图像数据

5.3 位图信息头成员

变量名 地址偏移 大小 作用
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 的位域掩码是 0xF8000x07E00x001F

5.4 正向位图与倒向位图

  • 正向位图biHeight < 0):从左上角到右下角排列,水平从左到右、垂直从上到下。
  • 倒向位图biHeight > 0):从左下角到右上角排列,水平从左到右、垂直从下到上。一般 BMP 都是倒向位图。

5.5 完整源码

#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);                 /* 退出进程 */
}

5.6 关键点

  • 两个结构体用 __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
  • 该例程不做 565/888 兼容判断,默认传入 RGB565 的 BMP;可用 Photoshop“存储为 → BMP → 16 位色 → 高级模式 → RGB565”得到 16 位色 BMP。

6. 例程四:在 LCD 上显示 JPEG(libjpeg)

对应例程:20_libjpeg/show_jpeg_image.c

6.1 JPEG 与 libjpeg

  • JPEG(Joint Photographic Experts Group):国际数字图像压缩标准,支持有损压缩,压缩比高。它损失的是人眼不敏感的高频信息,后缀 .jpg / .jpeg
  • libjpeg:完全用 C 语言编写的函数库,包含 JPEG 解码(解压缩)、编码(生成压缩文件)及其他功能。用它解码即可得到原始 RGB 数据。

6.2 libjpeg 解码流程

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/>销毁对象"]

6.3 核心数据结构

结构体 说明
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_SPACEJCS_UNKNOWNJCS_GRAYSCALEJCS_RGBJCS_YCbCrJCS_CMYKJCS_YCCKJCS_BG_RGBJCS_BG_YCC

6.4 API 速查

函数 作用
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) 销毁解码对象、释放资源

6.5 错误处理

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;

6.6 移植 libjpeg 到开发板

  1. 下载源码(http://www.ijg.org/files/ ),本书以 jpegsrc.v9b.tar.gz 为例,解压得 jpeg-9b
  2. 初始化交叉编译环境:

    source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
    
  3. 配置、编译、安装:

    ./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/jpeg/
    make
    make install
    

--host 指定目标平台(通常取交叉编译器名称前缀),--prefix 指定安装目录。安装目录含 bin(测试工具)、include(头文件,应用只需包含 jpeglib.h)、lib(动态库,libjpeg.solibjpeg.so.9.2.0)。

  1. 移植到开发板:bin 下工具拷到 /usr/binlib 下库文件拷到 /usr/lib,注意保持符号链接(可先打包再解压)。移植前删除出厂旧库:

    rm -rf /usr/lib/libjpeg.*
    
  2. 验证:执行 djpeg --help 能打印帮助即成功。

Tips:删除出厂 libjpeg 后,出厂 Qt GUI 程序对 jpeg 图片解码会出问题(原图位置变空白)。新库版本不同也救不回来,知道即可。

6.7 完整源码

#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);                 /* 退出进程 */
}

6.8 逐段解释

  • 数据格式:libjpeg 默认解码得到 BGR888,即内存里每像素依次是 B、G、R 三个字节(结构体 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 取图像与屏幕分辨率的较小值,超过屏幕的部分不显示。
  • RGB888 → RGB565((R & 0xF8) << 8) | ((G & 0xFC) << 3) | ((B & 0xF8) >> 3)

7. 例程五:在 LCD 上显示 PNG(libpng)

对应例程:21_libpng/show_png_image.c(另有 setjmp.c 演示错误跳转)。

7.1 PNG 与 libpng / zlib

  • PNG(Portable Network Graphic Format):采用无损压缩(LZ77 派生算法),压缩比高、体积小、支持 256 级透明效果,用于替代 GIF/TIFF。
  • libpng:免费开源的 C 语言函数库,支持 PNG 解码/编码。
  • zlib:数据压缩函数库,libpng 依赖 zlib,所以移植 libpng 前必须先移植 zlib。

7.2 两个核心数据结构

结构体 说明
png_struct(句柄 png_structp png_ptr libpng 内部使用,几乎所有库函数的第一个参数;png_create_read_struct() 创建
png_info(句柄 png_infop info_ptr 描述 PNG 图像信息;新版本通过 png_get_XXX / png_set_XXX 访问成员

7.3 setjmp / longjmp 与错误返回点

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 returnhello world!longjmp 跳回后打印 Second returnNice to meet you! 不会打印。

7.4 high-level 与 low-level 接口

方式 函数 特点 适用条件
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_imagepng_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)

7.5 API 速查

函数 作用
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(及其关联资源)

7.6 libpng 应用编程完整源码

#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);                 /* 退出进程 */
}

7.7 逐段解释与移植

  • 示例使用 high-level 接口:直接 png_read_png() 一次性解码;PNG_TRANSFORM_STRIP_ALPHA 丢掉 alpha 通道,得到 RGB888。
  • row_pointerspng_get_rows() 取得,指向 libpng 内部为每一行分配的缓冲区;销毁 png_struct 时该缓冲区自动释放。
  • 转换时每行按 3 字节遍历:R=row[i][j]G=row[i][j+1]B=row[i][j+2],转 RGB565。
  • 新版本 libpng 检查更严,运行时会打印一些警告,不影响使用。

移植顺序:先 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*

7.8 libpng 与 libjpeg 对比

维度 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

8. 例程六:LCD 横屏切换为竖屏

对应例程:22_lcd_vertical_display/lcd_vertical_display.c

核心结论:横屏/竖屏切换与驱动程序无关,是应用层要解决的问题。

8.1 硬件坐标是固定的

以 800×480 为例,屏幕正向放置时(横屏):

左上角 (0, 0)        右上角 (800-1, 0)
左下角 (0, 480-1)    右下角 (800-1, 480-1)

像素排列从左到右、从上到下,这是硬件固定属性,无法配置。像素 (x, y) 的显存地址(字节基地址 base):

base + (y * width + x) * pix_bytes

8.2 竖屏只是应用程序“假装”的坐标

如果应用层想把左下角当原点 (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° 也是竖屏,但公式不同;坐标变换推导本身很简单,按同样的方法推即可。

8.3 坐标变化对绘图函数的影响

  • 横屏下:(x, y) 中 x 沿宽方向、y 沿高方向,水平线 x 递增地址 +1,垂直线 y 递增地址 +width
  • 竖屏下:x 变成了“物理行号”(从下往上),所以竖屏的水平线每走一步地址 -lcd_width垂直线每走一步地址 +1

8.4 完整源码

#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);
}

8.5 说明

  • 自定义的 4 个函数都基于竖屏坐标定义:应用传入竖屏坐标,函数内部转成实际物理坐标再算显存地址。
  • user_width = fb_var.yres(原 Y 变竖屏 X),user_height = fb_var.xres(原 X 变竖屏 Y)。
  • lcd_test.c(图 19.4.3 横屏效果)对比,本例程显示的画面变成竖向(图 22.2.1)。
  • 缩放:本例程只做坐标旋转,不做分辨率缩放。如果在竖屏坐标下需要将内容按比例显示,只需在坐标转换前对逻辑坐标乘以缩放系数(例如按屏幕短边取整),再套用同一地址公式即可。

9. 例程七:在 LCD 上显示字符(FreeType)

对应例程:23_freetype/show_char.c(取模方式)与 23_freetype/freetype_test.c(FreeType 方式)。

9.1 两种方式

方式 原理 优点 缺点
取模显示 用取模软件生成字符点阵二维数组,逐 bit 打点 简单、无需库 只能显示固定几个字符,很“low”
字体引擎 解析 .ttf/.otf/.ttc 等字体文件,读取字形位图 通用、支持任意字符与字号 需移植 FreeType 等库

取模原理:字符点阵中每个小方块对应一个 bit,填充用 1、不填充用 0。例如“正”是 64×86 点阵,用 unsigned char arr[86][8] 存储(宽 64 点 = 8 字节/行)。取模时选择“从左到右、从上到下”。

9.2 FreeType 简介与移植

FreeType 是完全免费(开源)的软件字体引擎,设计小巧、高效、可定制、可移植,提供统一接口访问多种字体格式(FreeType 2 已取代废弃的 FreeType 1)。

移植步骤(本书选择 2.8 版本):

  1. 下载:https://download.savannah.gnu.org/releases/freetype/ ,取 freetype-2.8.tar.gz
  2. tools 下建安装目录 freetype,解压后进入源码目录。
  3. 打开 include/freetype/config/ftoption.h,去掉这两个宏的注释以启用系统 zlib 与 PNG 位图支持:

    #define FT_CONFIG_OPTION_SYSTEM_ZLIB
    #define FT_CONFIG_OPTION_USE_PNG
    
  4. 配置、编译、安装:

    ./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
    
  5. 应用需包含两个头文件(后者是用宏定义的头文件):

    #include <ft2build.h>
    #include FT_FREETYPE_H
    
  6. 移植到开发板前删除旧库,再拷贝新库并保持符号链接:

    rm -rf /usr/lib/libfreetype.*
    

9.3 核心概念

  • 字形(glyph):字符图像。同一字符可有多种字形(宋体“国”与微软雅黑“国”)。
  • 字形索引(glyph index):字体文件中通过索引找到字形;索引由字符编码(ASCII、GB2312、Unicode 等)转换而来。
  • 点(point):数字印刷中的物理单位,1 点 = 1/72 英寸(1 英寸 = 25.4 毫米)。
  • dpi:每英寸像素点数。换算公式:像素点数 = 点数 * dpi / 72

字形布局参数(水平布局为例):

参数 含义
origin / 基准线 水平基线与垂直基线用于定位与对齐字形
width / height 字形轮廓最左到最右、最上到最下的距离
bearingXbitmap_left 垂直基线到字形轮廓最左边的距离;水平布局为正
bearingYbitmap_top 水平基线到字形轮廓最上边的距离;轮廓在基线上方为正
xMin/xMaxyMin/yMax 字形轮廓四边位置,构成边界框(bbox)
advanceadvance.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 通用接口补充,教材对应段落以图示为主。

9.4 API 速查

函数 作用
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 / 库

9.5 FreeType 显示字符完整源码

#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,移动到下一个字形原点。
  • 运行参数:第一个是字体文件,第二个是旋转角度(0 表示不旋转)。

9.6 取模方式显示字符(show_char.c)

取模方式不依赖 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 判断是否填充。

9.7 字体文件

Windows 的字体放在 C:\Windows\Fonts,Linux 通常放在 /usr/share/fonts,格式有 otfttfttc 等。移植的 FreeType 与开发板自带字体可能不匹配,可从 Windows 拷贝一个字体(如宋体 SIMSUN.TTC)到开发板使用。


10. 编译与运行命令汇总

先初始化交叉编译环境(每个新终端一次):

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:链接的库,jpeglibjpeg.sopnglibpng.sozlibz.sofreetypelibfreetype.somlibm.so
  • FreeType 头文件在 include/freetype2 目录下,所以 -I 要指到 freetype2

11. 实验步骤与调试

11.1 实验步骤

  1. 连接 LCD:断电状态下用软排线把 LCD 接到开发板,再上电。
  2. 退出出厂 Qt GUI:测试前先退出出厂系统对应的 Qt GUI 应用程序,避免抢占显存。
  3. 交叉编译:按上表编译对应例程,得到 testApp
  4. 拷贝到开发板:把可执行文件(图片/字体例程还要带上 .jpg/.png/.ttc 文件)拷到用户家目录,例如 /home/root
  5. 运行

    ./testApp                       # lcd_info / lcd_test
    ./testApp image.bmp             # bmp_show
    ./testApp image.jpg             # libjpeg
    ./testApp image.png             # libpng
    ./testApp                       # 竖屏
    ./testApp SIMSUN.TTC 0          # freetype,参数:字体文件 + 旋转角度
    
  6. 验证库移植djpeg --help(libjpeg)能打印帮助即成功。

11.2 常见问题与调试

现象 可能原因 处理
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 设备

12. 跨平台对比

平台 / 场景 显示抽象 上屏方式 特点
嵌入式 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]]。


13. 面试题(含答案)

1. FrameBuffer 是什么?为什么显示编程通常用 mmap 而不是 read/write?

FrameBuffer 是 Linux 的显示驱动接口,把显示设备抽象成一块保存一帧图像的显存,设备节点为 /dev/fbX,读写它相当于读写显示缓冲区。用 mmap 是因为一帧数据量很大(如 1920×1080 ARGB8888 约 8MB)且画面频繁更新,普通 read/write 需要内核与用户空间之间反复拷贝,效率低;mmap 把显存映射到用户地址空间后可直接内存访问,一次映射、反复读写。

2. 如何判断 FrameBuffer 的像素格式?RGB888 怎么转 RGB565?

ioctl(fd, FBIOGET_VSCREENINFO, &fb_var)fb_var,看 bits_per_pixelred/green/blueoffsetlength。例如 R<11 5> G<5 6> B<0 5> 且 bpp=16 就是 RGB565。RGB888→RGB565:

fb = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);

3. 简述 libjpeg 的解码流程,为什么读取到的数据是 BGR888?

流程:jpeg_create_decompressjpeg_stdio_srcjpeg_read_header → 设置 out_color_space(默认 JCS_RGB)/scalejpeg_start_decompress → 循环 jpeg_read_scanlines 每次读 1 行 → jpeg_finish_decompressjpeg_destroy_decompress。libjpeg 默认输出 BGR888(R 在低 8 位、B 在高 8 位),所以按 B、G、R 的内存顺序取分量再转 RGB565。

4. libpng 中 setjmp/longjmp 的作用?high-level 与 low-level 接口有何区别?

libpng 默认错误处理会调用 longjmp() 跳转,需要用 setjmp(png_jmpbuf(png_ptr)) 设置错误返回点,跳回后执行 png_destroy_read_struct 等清理,避免内存泄漏;longjmpval 不能为 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

5. 横屏转竖屏的显存地址公式怎么来的?FreeType 里 bearingY 和 advance 有什么作用?

竖屏把应用坐标重新定义:例如以左下角为原点,应用 (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 定位到下一个字符原点,保证字符间距与对齐。


内容来源

  • 正点原子《I.MX6U 嵌入式 Linux C 应用编程指南 V1.6》
    • 第十九章 FrameBuffer 应用编程
    • 第二十章 在 LCD 上显示 jpeg 图像
    • 第二十一章 在 LCD 上显示 png 图片
    • 第二十二章 LCD 横屏切换为竖屏
    • 第二十三章 在 LCD 上显示字符
  • 例程源码:11、Linux C 应用编程例程源码19_lcd20_libjpeg21_libpng22_lcd_vertical_display23_freetype
  • 开发板:正点原子 ALPHA / Mini I.MX6U(7 寸 800×480 RGB LCD,出厂系统 RGB565)