一、第 1 节:字符的编码方式(6.1)------ 计算机怎么存 "字"?(秋招必背概念)
1.1 核心概念:编码 vs 字体
- 编码 :字符的「数字编号」,是文件里存的内容(比如字符 'A' 的编码是
0x41),是唯一的 - 字体 :字符的「显示形状」,是屏幕上画出来的样子,可以有无数种
- 本质:TXT 文件里存的是编码,Notepad 根据编码 + 字体,把字符渲染出来
1.2 3 种核心编码标准(面试必问)
1. ASCII(美国信息交换标准代码)
- 范围:0~127,仅支持英文、数字、符号,1 个字节(7 位有效,最高位恒为 0)
- 特点:所有编码的基础,UTF-8、Unicode 都向下兼容 ASCII
- 嵌入式场景:所有系统通用,英文显示必用
2. ANSI(ASCII 扩展,本地化编码)
- 本质:ASCII 的扩展,向下兼容 ASCII,非 ASCII 字符用 2 字节表示
- 问题:无统一标准,和地区强相关
- 中国大陆默认:GB2312/GBK(简体中文)
- 港澳台默认:BIG5(繁体中文)
- 致命缺陷:同一个数值对应不同字符(比如
0xd0d6,GB2312 是 "中",BIG5 是 "篦"),跨地区乱码
3. Unicode(统一编码,全球通用)
- 核心目标:给全球所有字符一个唯一的数值编号,彻底解决乱码
- 范围:
0x0000 ~ 0x10FFFF,支持 100 多万个字符,完全覆盖全球语言 - 向下兼容 ASCII:ASCII 字符的 Unicode 编号和原编码完全一致
- 问题:Unicode 只是「编号标准」,不是存储格式,需要具体的编码实现来存到文件里
1.3 Unicode 的 4 种编码实现(嵌入式 Linux 核心用 UTF-8)
| 编码格式 | 字节数 | 特点 | 嵌入式场景 |
|---|---|---|---|
| UTF-32 | 固定 4 字节 | 简单,直接存 Unicode 编号 | 浪费空间,极少用 |
| UTF-16 LE(小端) | 2/4 字节 | 用 BOM(0xFFFE)标识,小端字节序 |
Windows 常用,嵌入式少用 |
| UTF-16 BE(大端) | 2/4 字节 | 用 BOM(0xFEFF)标识,大端字节序 |
网络传输常用,嵌入式少用 |
| UTF-8 | 变长 1~4 字节 | 无 BOM,兼容 ASCII,ASCII 字符 1 字节,中文 3 字节 | 嵌入式 Linux 标准! 国际化必用 |
UTF-8 核心优势(面试必背)
- 完全兼容 ASCII:ASCII 文件直接当 UTF-8 打开,无乱码
- 变长编码,节省空间:英文 1 字节,中文 3 字节,比 UTF-16/32 省空间
- 无 BOM,无字节序问题:不用处理大小端,跨平台无压力
- 容错性强:单个字节损坏,仅影响当前字符,不影响后续字符
1.4 秋招考点总结(编码部分)
- **ASCII、ANSI、Unicode、UTF-8 的区别?**答:ASCII 是英文基础编码;ANSI 是本地化扩展,跨地区乱码;Unicode 是全球统一编号;UTF-8 是 Unicode 的变长存储实现,兼容 ASCII,是嵌入式 Linux 标准。
- **为什么嵌入式 Linux 用 UTF-8,不用 GB2312?**答:UTF-8 是国际通用编码,支持多语言,兼容 ASCII,无乱码;GB2312 仅支持简体中文,国际化差。
- **UTF-8 和 UTF-16 的核心区别?**答:UTF-8 是变长编码,无 BOM,兼容 ASCII;UTF-16 是定长 2 字节,有 BOM,不兼容 ASCII。
二、第 2 节:ASCII 字符的点阵显示(6.2)------ 最简单的文字显示
2.1 核心原理:点阵字库
- 本质:把每个字符的形状,用「像素点阵」存起来(比如 8×16 点阵,每个字符 16 字节,1 字节对应一行 8 个像素)
- 规则:位为 1→点亮像素(白色),位为 0→熄灭像素(黑色)
- 流程:根据字符的 ASCII 码,从字库数组中取出对应点阵→遍历点阵→调用
lcd_put_pixel画点
所有编码的字符要显示出来,都得先转成点阵这类显示数据
2.2 完整开发流程(对应教材show_ascii.c)
步骤 1:准备点阵字库数组
cpp
#define FONTDATAMAX 4096
// 完整按ASCII顺序排列的 8x16 字库(从空格0x20开始,每个字符16字节)
// 高位在右,非常规字库从高位到低位,从左到右显示
const unsigned char fontdata_8x16[FONTDATAMAX] = {
// 0x20 空格
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x21 !
0x00,0x00,0x00,0x18,0x38,0x38,0x38,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00,
// 0x22 "
0x00,0x00,0x00,0x36,0x36,0x36,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x23 #
0x00,0x00,0x00,0x36,0x36,0x7F,0x36,0x36,0x36,0x7F,0x36,0x36,0x00,0x00,0x00,0x00,
// 0x24 $
0x00,0x00,0x00,0x0C,0x3E,0x63,0x62,0x3C,0x06,0x03,0x63,0x3E,0x0C,0x00,0x00,0x00,
// 0x25 %
0x00,0x00,0x00,0x00,0x63,0x66,0x0C,0x18,0x30,0x63,0x63,0x00,0x00,0x00,0x00,0x00,
// 0x26 &
0x00,0x00,0x00,0x1C,0x36,0x36,0x1C,0x3E,0x66,0x66,0x66,0x3B,0x00,0x00,0x00,0x00,
// 0x27 '
0x00,0x00,0x00,0x06,0x06,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x28 (
0x00,0x00,0x00,0x18,0x0C,0x06,0x06,0x06,0x06,0x06,0x06,0x0C,0x18,0x00,0x00,0x00,
// 0x29 )
0x00,0x00,0x00,0x06,0x0C,0x18,0x18,0x18,0x18,0x18,0x18,0x0C,0x06,0x00,0x00,0x00,
// 0x2A *
0x00,0x00,0x00,0x00,0x00,0x36,0x1C,0x7F,0x1C,0x36,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x2B +
0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x3F,0x0C,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x2C ,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x06,0x00,0x00,0x00,0x00,
// 0x2D -
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x2E .
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x00,0x00,0x00,0x00,
// 0x2F /
0x00,0x00,0x00,0x00,0x00,0x60,0x30,0x18,0x0C,0x06,0x03,0x01,0x00,0x00,0x00,0x00,
// 0x30 0
0x00,0x00,0x00,0x1C,0x36,0x63,0x63,0x63,0x63,0x63,0x36,0x1C,0x00,0x00,0x00,0x00,
// 0x31 1
0x00,0x00,0x00,0x0C,0x1C,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3F,0x00,0x00,0x00,0x00,
// 0x32 2
0x00,0x00,0x00,0x1E,0x33,0x30,0x18,0x0C,0x06,0x03,0x33,0x3F,0x00,0x00,0x00,0x00,
// 0x33 3
0x00,0x00,0x00,0x1E,0x33,0x30,0x18,0x30,0x30,0x33,0x1E,0x0C,0x00,0x00,0x00,0x00,
// 0x34 4
0x00,0x00,0x00,0x18,0x38,0x30,0x36,0x33,0x33,0x7F,0x30,0x30,0x00,0x00,0x00,0x00,
// 0x35 5
0x00,0x00,0x00,0x3F,0x03,0x03,0x1F,0x30,0x30,0x33,0x33,0x1E,0x00,0x00,0x00,0x00,
// 0x36 6
0x00,0x00,0x00,0x1C,0x06,0x03,0x1F,0x33,0x33,0x33,0x33,0x1E,0x00,0x00,0x00,0x00,
// 0x37 7
0x00,0x00,0x00,0x3F,0x33,0x30,0x18,0x0C,0x06,0x06,0x06,0x06,0x00,0x00,0x00,0x00,
// 0x38 8
0x00,0x00,0x00,0x1C,0x36,0x33,0x36,0x1C,0x36,0x33,0x33,0x1C,0x00,0x00,0x00,0x00,
// 0x39 9
0x00,0x00,0x00,0x1C,0x36,0x33,0x33,0x3E,0x30,0x18,0x0C,0x1C,0x00,0x00,0x00,0x00,
// 0x3A :
0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x00,0x00,0x0C,0x0C,0x00,0x00,0x00,0x00,0x00,
// 0x3B ;
0x00,0x00,0x00,0x00,0x00,0x0C,0x0C,0x00,0x00,0x0C,0x0C,0x06,0x00,0x00,0x00,0x00,
// 0x3C <
0x00,0x00,0x00,0x00,0x00,0x18,0x0C,0x06,0x03,0x06,0x0C,0x18,0x00,0x00,0x00,0x00,
// 0x3D =
0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,0x00,0x00,
// 0x3E >
0x00,0x00,0x00,0x00,0x00,0x06,0x0C,0x18,0x30,0x18,0x0C,0x06,0x00,0x00,0x00,0x00,
// 0x3F ?
0x00,0x00,0x00,0x1C,0x36,0x30,0x18,0x0C,0x0C,0x00,0x0C,0x0C,0x00,0x00,0x00,0x00,
// 0x40 @
0x00,0x00,0x00,0x1E,0x33,0x33,0x3B,0x3B,0x3B,0x30,0x1E,0x00,0x00,0x00,0x00,0x00,
// 0x41 A
0x00,0x00,0x00,0x0C,0x1E,0x33,0x33,0x3F,0x33,0x33,0x33,0x33,0x00,0x00,0x00,0x00,
// 0x42 B
0x00,0x00,0x00,0x3F,0x66,0x66,0x66,0x3E,0x66,0x66,0x66,0x3F,0x00,0x00,0x00,0x00,
// 0x43 C
0x00,0x00,0x00,0x3C,0x66,0x03,0x03,0x03,0x03,0x03,0x66,0x3C,0x00,0x00,0x00,0x00,
// 0x44 D
0x00,0x00,0x00,0x1F,0x36,0x66,0x66,0x66,0x66,0x66,0x36,0x1F,0x00,0x00,0x00,0x00,
// 0x45 E
0x00,0x00,0x00,0x7F,0x66,0x06,0x06,0x3E,0x06,0x06,0x66,0x7F,0x00,0x00,0x00,0x00,
// 0x46 F
0x00,0x00,0x00,0x7F,0x66,0x06,0x06,0x3E,0x06,0x06,0x06,0x0F,0x00,0x00,0x00,0x00,
// 0x47 G
0x00,0x00,0x00,0x3C,0x66,0x03,0x03,0x03,0x73,0x63,0x66,0x3C,0x00,0x00,0x00,0x00,
// 0x48 H
0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x3F,0x33,0x33,0x33,0x33,0x00,0x00,0x00,0x00,
// 0x49 I
0x00,0x00,0x00,0x1E,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00,
// 0x4A J
0x00,0x00,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x0C,0x00,0x00,0x00,0x00,
// 0x4B K
0x00,0x00,0x00,0x67,0x66,0x36,0x1E,0x36,0x66,0x66,0x66,0x67,0x00,0x00,0x00,0x00,
// 0x4C L
0x00,0x00,0x00,0x0F,0x06,0x06,0x06,0x06,0x06,0x06,0x66,0x7F,0x00,0x00,0x00,0x00,
// 0x4D M
0x00,0x00,0x00,0x63,0x77,0x7F,0x7F,0x6B,0x63,0x63,0x63,0x63,0x00,0x00,0x00,0x00,
// 0x4E N
0x00,0x00,0x00,0x63,0x67,0x6F,0x7B,0x73,0x63,0x63,0x63,0x63,0x00,0x00,0x00,0x00,
// 0x4F O
0x00,0x00,0x00,0x1C,0x36,0x63,0x63,0x63,0x63,0x63,0x36,0x1C,0x00,0x00,0x00,0x00,
// 0x50 P
0x00,0x00,0x00,0x3F,0x66,0x66,0x66,0x3E,0x06,0x06,0x06,0x0F,0x00,0x00,0x00,0x00,
// 0x51 Q
0x00,0x00,0x00,0x1C,0x36,0x63,0x63,0x63,0x63,0x63,0x36,0x1C,0x30,0x30,0x00,0x00,
// 0x52 R
0x00,0x00,0x00,0x3F,0x66,0x66,0x66,0x3E,0x36,0x66,0x66,0x67,0x00,0x00,0x00,0x00,
// 0x53 S
0x00,0x00,0x00,0x1E,0x33,0x03,0x06,0x0C,0x18,0x30,0x33,0x1E,0x00,0x00,0x00,0x00,
// 0x54 T
0x00,0x00,0x00,0x3F,0x2D,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00,
// 0x55 U
0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x33,0x3F,0x00,0x00,0x00,0x00,
// 0x56 V
0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x33,0x33,0x36,0x1C,0x0C,0x00,0x00,0x00,0x00,
// 0x57 W
0x00,0x00,0x00,0x63,0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x63,0x00,0x00,0x00,0x00,
// 0x58 X
0x00,0x00,0x00,0x63,0x63,0x36,0x1C,0x1C,0x36,0x63,0x63,0x63,0x00,0x00,0x00,0x00,
// 0x59 Y
0x00,0x00,0x00,0x33,0x33,0x33,0x33,0x33,0x36,0x1C,0x0C,0x0C,0x00,0x00,0x00,0x00,
// 0x5A Z
0x00,0x00,0x00,0x7F,0x63,0x60,0x30,0x18,0x0C,0x06,0x63,0x7F,0x00,0x00,0x00,0x00,
};
步骤 2:实现 ASCII 字符显示函数
cpp
/* draw ascii text */
void draw_ascii_char(int x, int y, char c, char *text, int color, int bg_color)
{
// 单个ASCII字符绘制(从字库取模)
const unsigned char *font = (unsigned char *)text + (c - ' ') * 16;
for (size_t j = 0; j < 16; j++)
{
unsigned char byte = font[j];
for (size_t i = 0; i < 8; i++) // 现在写的数组是:非常规字库------低位在左
{
if (byte & (1<<(i)) )
{
draw_point(x+i,y+j,color);
}
else
{
draw_point(x+i,y+j,bg_color);
}
}
}
}
// 字符串绘制函数(封装单个字符)
void draw_ascii_text(int x, int y, char *text, char *font, int color, int bg_color)
{
int i = 0;
while (text[i] != '\0')
{
draw_ascii_char(x + i*8, y, text[i], font, color, bg_color);
i++;
}
}
步骤 3:主函数完整流程(复用 Framebuffer 代码)
cpp
int main(int argc, char *argv[])
{
/* open fb0 file */
fd_fb = open("/dev/fb0", O_RDWR );
if( fd_fb < 0 )
{
perror("open /dev/fb0 failed");
return -1;
}
fd_zk = open("./HZK16", O_RDONLY);
if( fd_zk < 0 )
{
perror("open ./HZK16 failed");
return -1;
}
/* get lcd date */
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var) == -1)
{
perror("ioctl get failure");
close(fd_fb);
return -2;
}
/* calculation Video memory */
screen_size = var.xres * var.yres *var.bits_per_pixel/8;
fb_base = mmap(NULL, screen_size, PROT_WRITE | PROT_READ, MAP_SHARED, fd_fb, 0);
if (fb_base == MAP_FAILED)
{
perror("mmap failed");
close(fd_fb); // 修复:mmap失败必须关闭文件描述符
close(fd_zk);
return -3;
}
if (fstat(fd_zk,&zk_stat))
{
perror("get zk fuiled");
}
fb_zk_base = mmap(NULL, zk_stat.st_size, PROT_READ, MAP_PRIVATE, fd_zk, 0);
if (fb_zk_base == MAP_FAILED)
{
perror("mmap failed");
close(fd_fb);
close(fd_zk);
return -3;
}
/* Draw a picture */
draw_ascii_char(200, 200, 'L', (char *)fontdata_8x16, 0x0, 0xFFFFFF);
draw_ascii_text(20, 20, "CCCCCC", (char *)fontdata_8x16, 0x0, 0xFFFFFF);
draw_chinese_char(60,60,"我",fb_zk_base, 0x0, 0xFFFFFF);
draw_mixed_text(60,60,"中国",fontdata_8x16,fb_zk_base, 0x0, 0xFFFFFF);
draw_mixed_text(60,60,"中国666GOOD",(char *)fontdata_8x16,fb_zk_base, 0x0, 0xFFFFFF);
/* release memory and Handle */
munmap(fb_base,screen_size);
close(fd_fb);
return 0;
}
2.3 秋招考点总结(点阵 ASCII)
- **点阵字库的原理?**答:用像素点阵存储字符形状,位为 1 点亮、0 熄灭,根据字符编码取对应点阵,遍历画点。
- **点阵字库的优缺点?**答:优点:简单,无需库,直接操作;缺点:放大模糊,不同字号需要不同字库,占用空间大。
- **8x16 点阵中,每个字符占多少字节?为什么?**答:16 字节。因为 8×16=128 个像素,1 字节 = 8 位,128/8=16 字节。
三、第 3 节:中文字符的点阵显示(6.3)------ 从英文到中文
3.1 核心难点:编码格式与 GB2312 区位码
1. 中文编码:GB2312(点阵字库常用)
- 每个中文字符用2 字节表示,兼容 ASCII(ASCII 字符 1 字节,最高位为 0)
- 区位码结构:
- 第 1 字节:区码 (
0xA1 ~ 0xF7,共 94 个区) - 第 2 字节:位码 (
0xA1 ~ 0xFE,每个区 94 个位)
- 第 1 字节:区码 (
- 公式:汉字在 HZK16 字库中的偏移 =
(区码 - 0xA1) * 94 + (位码 - 0xA1)× 32(每个汉字 16×16 点阵,32 字节)
2. 编码格式坑(教材重点)
- C 程序编码 vs 可执行程序编码:
- 用
-finput-charset指定源文件编码 - 用
-fexec-charset指定可执行程序编码 - 嵌入式 Linux 默认:
-finput-charset=UTF-8 -fexec-charset=UTF-8 - 中文点阵字库(HZK16)常用 GB2312,需指定:
-finput-charset=GB2312 -fexec-charset=GB2312
- 用
3.2 完整开发流程(对应教材show_chinese.c,HZK16 字库)
步骤 1:打开 HZK16 点阵字库文件,mmap 映射
int fd_hzk16;
struct stat hzk_stat;
unsigned char *hzkmem;
// 1. 打开HZK16字库文件
fd_hzk16 = open("HZK16", O_RDONLY);
if (fd_hzk16 < 0) { perror("open HZK16"); return -1; }
// 2. fstat获取文件大小
if (fstat(fd_hzk16, &hzk_stat)) { close(fd_hzk16); return -1; }
// 3. mmap映射字库到用户态内存
hzkmem = mmap(NULL, hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);
if (hzkmem == MAP_FAILED) { close(fd_hzk16); return -1; }
普通文件读写要经过用户缓冲区和内核缓冲区的多次数据拷贝,每次操作都得调用 read、write 这样的系统调用,上下文切换开销大。而 mmap 把文件直接映射成内存地址,访问内存就相当于访问文件,省去了数据拷贝步骤,尤其是像字库这种需要频繁随机读取的场景,速度提升非常明显
步骤 2:实现中文显示函数lcd_put_chinese
cpp
/* draw chinese text */
void draw_chinese_char(int x, int y, char *text, uint8_t *font, int color, int bg_color)
{
// 因为一个人中文字符两个byte
unsigned int area = text[0] - 0xA1;
unsigned int where = text[1] - 0xA1;
unsigned char *dots = font + (area * 94 + where)*32;
for (size_t j = 0; j < 16; j++)
{
for (size_t n = 0; n < 2; n++)
{
// 修复:有符号char改为无符号unsigned char,位运算不异常
unsigned char byte = dots[j*2+n];
for (size_t i = 0; i < 8; i++)
{
if (byte & (1<<(7-i)) ) // 标准字库,高位在左
{
draw_point(x+i+n*8,y+j,color);
}
else
{
draw_point(x+i+n*8,y+j,bg_color);
}
}
}
}
}
/* 绘制中文/ASCII混合字符串 (GB2312编码) */
void draw_mixed_text(int x, int y, char *str, uint8_t *ascii_font, uint8_t *hzk, int color, int bg_color)
{
int i = 0;
int cur_x = x;
while (str[i] != '\0') {
if ((uint8_t)str[i] >= 0xA1) { // GB2312汉字(首字节>=0xA1)
draw_chinese_char(cur_x, y, &str[i], hzk, color, bg_color);
cur_x += 16; // 汉字宽16像素
i += 2; // 汉字占2字节
} else { // ASCII字符
draw_ascii_char(cur_x, y, str[i], ascii_font, color, bg_color);
cur_x += 8; // ASCII宽8像素
i += 1; // ASCII占1字节
}
}
}
步骤 3:主函数调用
cpp
draw_chinese_char(60,60,"我",fb_zk_base, 0x0, 0xFFFFFF);
draw_mixed_text(60,60,"中国",fontdata_8x16,fb_zk_base, 0x0, 0xFFFFFF);
draw_mixed_text(60,60,"中国666GOOD",(char *)fontdata_8x16,fb_zk_base, 0x0, 0xFFFFFF);
3.3 秋招考点总结(中文点阵)
- **GB2312 区位码的原理?**答:GB2312 用区码 + 位码定位汉字,区码 0xA1~0xF7,位码 0xA1~0xFE,每个区 94 个汉字,偏移 =(区码 - 0xA1)*94+(位码 - 0xA1)。
- **中文点阵显示和 ASCII 显示的区别?**答:ASCII 用 1 字节编码,8×16 点阵;中文用 2 字节 GB2312 编码,16×16 点阵,需要区位码计算偏移。
- **为什么中文程序会乱码?**答:源文件编码、可执行程序编码、字库编码不统一,比如源文件 UTF-8,字库 GB2312,未做编码转换。
四、第 4 节:交叉编译 freetype(6.4)------ 矢量字库的基础
4.1 交叉编译核心知识(嵌入式 Linux 必懂)
1. 头文件、库文件、运行时库的区别
| 类型 | 作用 | 位置 | 编译时是否需要 | 运行时是否需要 |
|---|---|---|---|---|
| 头文件(.h) | 函数声明、结构体定义 | 工具链include目录 |
✅ 必须 | ❌ 不需要 |
| 库文件(.a/.so) | 函数实现 | 工具链lib目录 |
✅ 链接时需要 | ✅ 运行时需要(.so) |
| 运行时库 | 动态链接库(.so) | 开发板/lib//usr/lib |
❌ 不需要 | ✅ 必须 |
2. 交叉编译万能命令(教材核心)
数组形式的字库很多也是在 PC 上开发的,但它本质是用 C 语言数组定义的静态数据,不涉及复杂的库依赖和架构相关代码。比如你在 PC 上写一个 16×16 点阵的数组字库,只要数组的存储格式符合嵌入式编译器的要求,直接把代码复制到嵌入式项目中编译就行。而 FreeType 这类矢量字库包含大量架构相关的优化代码和动态内存管理逻辑,必须针对目标嵌入式架构交叉编译才能正常工作。
# 以freetype为例,工具链前缀arm-buildroot-linux-gnueabihf-
./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp
make
make install
--host:指定目标架构(交叉编译)--prefix:指定安装目录(生成bin/lib/include)
3. 常见错误解决
- 头文件找不到 :用
-I指定头文件目录,或把头文件放到工具链include目录 - 库文件找不到(undefined reference) :用
-L指定库目录,-lxxx链接库(比如-lfreetype) - 运行时找不到库(error while loading shared libraries) :把
.so放到开发板/lib//usr/lib,或用LD_LIBRARY_PATH指定
4.2 freetype 交叉编译流程(IMX6ULL 示例)
freetype 依赖zlib、libpng,工具链好像buildroot工具链自带无需编译,否则按顺序编译1和2后移到对应目录再编译3:
-
编译 zlib
cppCC=arm-buildroot-linux-gnueabihf-gcc ./configure --prefix=$PWD/tmp #因为不支持host指定,所以请在环境变量指定交叉编译器 make make install -
编译 libpng
./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp make make install -
编译 freetype
./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp make make install
头/库 文件问题
把头文件、库文件放到工具链对应目录(1/2/3都需要) ,后续编译直接链接
-lfreetype确定头文件目录和库文件目录:
bashecho 'main(){}' | arm-buildroot-linux-gnueabihf-gcc -E -v - /usr/local/arm/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include /usr/local/arm/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/lib/ cp ./include/* -rf /usr/local/arm/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include cp ./lib/* -rf /usr/local/arm/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/lib/运行可执行文件时、交叉编译应用freetype库的C文件时也需要确保++系统路径++、环境变量存在或者也可以自己指定:链接时用 " -L dir "选项指定,也是同理
开发板:
cppcp lib/libfreetype.so /usr/lib #或者export LD_LIBRARY_PATH=./tmp/lib:$LD_LIBRARY_PATH #系统目录:就是板子上的/lib、/usr/lib 目录
4.3 秋招考点总结(交叉编译)
- 交叉编译的本质? 答:在 x86 主机上编译出能在 ARM 架构上运行的程序,核心是指定
--host目标架构。 - **头文件和库文件的作用?**答:头文件用于编译时的函数声明,库文件用于链接时的函数实现,运行时仅需库文件。
- **动态库和静态库的区别?**答:静态库(.a)编译时嵌入程序,程序体积大,不依赖库;动态库(.so)编译时不嵌入,运行时加载,程序体积小,依赖库。
五、第 5 节:使用 freetype 显示单个文字(6.5)------ 矢量字库核心
先看看:Freetype 介绍
FreeType是一个开源的高质量字体渲染库,它提供了一个统一的接口来访问多种字体格式文件,包括TrueType、OpenType、Type1等
它的应用非常广泛,包括操作系统、浏览器、办公软件等多个领域。它不仅能够提高字体渲染的质量,还能通过其跨平台特性,为开发者提供一致的用户体验
what is 矢量字体:
矢量字体形成分三步:
第1步 确定关键点,
第2步 使用数学曲线(贝塞尔曲线)连接头键点,
第3步 填充闭合区线内部空间。
什么是关键点?以字母"A"为例,它的的关键点如图 6.16 中的黄色所示。

而为达到显示矢量字体的目的我们只需要移植这个字体引擎,调用对应的 API 接口,提供字体文件,就可以让 freetype 库帮我们取出关键点、实现闭合曲线,填充颜色。
矢量字库获取:
Windows 使 用 的 字 体 文 件 在 c:\Windows\Fonts 目录下,扩展名为 TTF 的都是矢量字库------教材用的simsun.ttc

有了以上基础,一个文字的显示过程可以概括如下:
- 给定一个字符可以确定它的编码值(ASCII、UNICODE、GB2312);
- 设置字体大小;
- 根据编码值,从文件头部中通过 charmap(字符映射表) 找到对应的关键点(glyph),它会根据字体大小调整关键点;
- 把关键点转换为位图点阵;
- 在 LCD 上显示出来
5.1 核心原理:矢量字库 vs 点阵字库
| 特性 | 点阵字库 | 矢量字库(freetype+TrueType) |
|---|---|---|
| 存储内容 | 像素点阵 | 字符轮廓(贝塞尔曲线) |
| 缩放 | 放大模糊,有锯齿 | 任意缩放,无锯齿,清晰 |
| 字库大小 | 每个字号一个字库,体积大 | 一个字库支持所有字号,体积小 |
| 依赖 | 无需库,直接操作 | 需要 freetype 库解析 |
| 嵌入式场景 | 老款小屏、低功耗系统 | 现代嵌入式 Linux HMI、智能设备 |
5.2 freetype 核心流程(教材 9 步,面试必背)
一般官网有介绍文档,其次去看源码。调库的开发就是这样。当然常用的找开源或论坛也行
- 初始化 freetype 库 :
FT_Init_FreeType(&library) - 加载字体文件(.ttf/.ttc) :
FT_New_Face(library, font_path, 0, &face) - 设置字体大小 :
FT_Set_Pixel_Sizes(face, font_size, 0) - 选择字符编码 :
FT_Select_Charmap(face, FT_ENCODING_UNICODE) - 根据编码获取 glyph 索引 :
FT_Get_Char_Index(face, char_code) - 加载 glyph(字符轮廓) :
FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT) - 渲染 glyph 到位图 :
FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL) - (可选)设置变换 / 旋转 :
FT_Set_Transform(face, &matrix, &pen) - 遍历位图,调用 lcd_put_pixel 画点
5.3 完整代码实现(对应教材freetype_show_font.c)
步骤 1:freetype 初始化与位图绘制函数
关于wchar_t:
wchar_t 用于存储单个宽字符常量,本质是unsigned short
L是标准 C 语言,用于宽字符 / 宽字符串;用于告诉编译器,后面跟的字符串或字符是宽字符类型,每个字符占 2 字节或 4 字节,对应 wchar_t 类型可用来直接赋值获得字符的 UNICODE 值
注意:如果 .c 文件 是以 ANSI(GB2312)格式保存,那么需要使用以下命
令来编译:
bashgcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_wchar test_wchar.c在 ++C 语言(VScode)++ 里,处理字符编码时,通常需要先重新加载目标编码方式,让程序能正确解析新的字符数据,之后再以这个编码方式保存文件,这样才能确保宽字符内容按预期编码格式存储。否则荣容易乱套
cpp
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <ft2build.h>
#include FT_FREETYPE_H
unsigned char *fb_base;
struct fb_var_screeninfo var;
/* draw point 无修改,逻辑正确 */
void draw_point(int x, int y, int color)
{
if (x < 0 || y < 0 || x >= var.xres || y >= var.yres)
{
return ;
}
uint32_t offset = (x+y*var.xres) * var.bits_per_pixel /8;
uint8_t *point = fb_base + offset;
switch (var.bits_per_pixel)
{
case 32:
*(uint32_t *)point = color ;
break;
case 24:
point[2] = (color>>16) & 0xff ;
point[1] = (color>>8) & 0xff ;
point[0] = color & 0xff ;
break;
case 16:
*(uint16_t *)point = (((color >> 19) & 0x1F) << 11) | (((color >> 10) & 0x3F) << 5) | ((color >> 3) & 0x1F);
break;
default:
break;
}
}
void draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y, int color)
{
// 遍历位图的 每一行(高度)
for (FT_Int j = 0; j < bitmap->rows; j++)
{
// 遍历位图的 每一列(宽度)
for (FT_Int i = 0; i < bitmap->width; i++)
{
// 计算真实屏幕坐标
FT_Int screen_x = x + i;
FT_Int screen_y = y + j;
// 屏幕越界判断
if (screen_x < 0 || screen_y < 0 || screen_x >= var.xres || screen_y >= var.yres)
continue;
// 【关键修复】FreeType位图索引:j * pitch + i
unsigned char pixel = bitmap->buffer[j * bitmap->pitch + i];
// 非0表示有像素,绘制
if (pixel)
draw_point(screen_x, screen_y, color);
}
}
}
int main(int argc, char *argv[])
{
FT_Library library;
FT_Face face;
FT_Error error;
int ret = 0;
int fd_fb;
if (argc != 2)
{
printf("用法: %s <字体文件路径>\n", argv[0]);
printf("示例: %s ./simhei.ttf\n", argv[0]);
ret = -1;
goto exit_re;
}
fd_fb = open("/dev/fb0",O_RDWR);
if (fd_fb < 0)
{
perror("open /dev/fb0 failed");
ret = -1;
goto exit_re;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
perror("ioctl FBIOGET_VSCREENINFO failed");
ret = -2;
goto exit_cl;
}
size_t screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
fb_base = mmap(NULL, screen_size, PROT_WRITE | PROT_READ ,MAP_SHARED,fd_fb,0 );
if (fb_base == MAP_FAILED)
{
perror("mmap failed");
ret = -2;
goto exit_cl;
}
memset(fb_base, 0, screen_size);
// 1. 初始化freetype库
error = FT_Init_FreeType(&library);
if (error)
{
printf("FT_Init_FreeType error\n");
ret = -3;
goto exit_mp;
}
// 2. 加载字体文件
error = FT_New_Face(library, argv[1], 0, &face);
if (error == FT_Err_Unknown_File_Format)
{
printf("unsupported font format\n");
ret = -4;
goto exit_ft; // 统一释放资源
} else if (error)
{
printf("FT_New_Face error\n");
ret = -4;
goto exit_ft;
}
/***************************
【修复】参数顺序规范:(0, 像素高度)
***************************/
error = FT_Set_Pixel_Sizes(face, 24, 0);
if (error)
{
printf("FT_Set_Pixel_Sizes error\n");
ret = -5;
goto exit_face;
}
// 编码设置
error = FT_Select_Charmap(face, FT_ENCODING_UNICODE);
if (error)
{
printf("FT_Select_Charmap error\n");
ret = -6;
goto exit_face;
}
// 绘制汉字:繁
const wchar_t *w = L"我";
FT_UInt glyph_index = FT_Get_Char_Index(face, *w);
error = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
if (error) { printf("FT_Load_Glyph error\n"); ret = -7; goto exit_face; }
error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
if (error) { printf("FT_Render_Glyph error\n"); ret = -8; goto exit_face; }
/***************************
【修复】颜色改为白色 0xFFFFFF(之前传0=黑色,看不见)
***************************/
draw_bitmap(&face->glyph->bitmap,
var.xres/2 - face->glyph->bitmap_left,
var.yres/2 - face->glyph->bitmap_top,
0xFFFFFF);
// 资源释放(统一出口,更规范)
exit_face:
FT_Done_Face(face);
exit_ft:
FT_Done_FreeType(library);
exit_mp:
munmap(fb_base,screen_size);
exit_cl:
close(fd_fb);
exit_re:
return ret;
}
步骤 2:编译命令
# 交叉编译,链接freetype库
arm-buildroot-linux-gnueabihf-gcc -o freetype_show_font freetype_show_font.c -lfreetype
5.4 秋招考点总结(freetype 单个文字)
- **freetype 的核心作用?**答:开源矢量字库解析库,支持 TrueType 等字体,把矢量轮廓转换成像素点阵,实现任意缩放的文字显示。
- **freetype 显示文字的完整流程?**答:初始化库→加载字体→设置大小→选择编码→获取 glyph 索引→加载 glyph→渲染位图→绘制到 LCD。
- **矢量字库为什么放大不模糊?**答:存储的是字符的贝塞尔曲线轮廓,放大时重新计算轮廓,生成新的点阵,无锯齿。
六、第 6 节:使用 freetype 显示一行文字(6.6)------ 从单个到多行
6.1 核心难点:坐标转换与字符排版
1. 坐标系统转换

纵向上LCD坐标系+笛卡尔刚好是屏幕高度
- freetype 笛卡尔坐标系:原点在左下角,x 向右,y 向上
- LCD 屏幕坐标系:原点在左上角,x 向右,y 向下
- 转换公式:
LCD_y = 屏幕高度 - 笛卡尔_y
2. 字符排版核心参数
因为有的字符占用的宽度不一样
- pen(原点) :每个字符的绘制起点,后一个字符的原点 = 前一个字符的原点 +
advance(字符间距) - FT_BBox :字符的外框(
xMin/yMin/xMax/yMax),用于计算一行文字的整体外框,实现居中对齐 - baseline:一行文字的基准线,所有字符基于 baseline 对齐
6.2 完整开发流程(对应教材show_line.c)

单个字符外框有两个关键特点:一是以字符基线左下角为原点,坐标有正有负;二是每个字符外框独立,仅描述自身轮廓范围,不含与其他字符的间距信息
整合成行外框,你需要先获取每个字符的水平 advance 值,这个值是字符宽度加默认间距,用来确定下一个字符的起始位置。然后遍历所有字符,累加 advance 值得到行宽,同时比较所有字符的 yMin 和 yMax,取最小值和最大值作为行高范围。
步骤 1:计算一行文字的外框compute_string_bbox
cpp
/* 计算字符串外框:修复内存泄漏 + 缓存字符串长度 */
int compute_string_bbox(FT_Face face, wchar_t *wstr, FT_BBox *abbox)
{
int i;
int error;
FT_BBox bbox;
FT_BBox glyph_bbox;
FT_Vector pen;
FT_Glyph glyph;
FT_GlyphSlot slot = face->glyph;
size_t str_len = wcslen(wstr); // 缓存长度,避免重复计算
// 初始化外框
bbox.xMin = bbox.yMin = 32000;
bbox.xMax = bbox.yMax = -32000;
pen.x = 0;
pen.y = 0;
for (i = 0; i < str_len; i++)
{
FT_Set_Transform(face, 0, &pen);
error = FT_Load_Char(face, wstr[i], FT_LOAD_RENDER);
if (error)
{
printf("FT_Load_Char error\n");
return -1;
}
error = FT_Get_Glyph(face->glyph, &glyph);
if (error)
{
printf("FT_Get_Glyph error!\n");
return -1;
}
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &glyph_bbox);
// 更新外框
bbox.xMin = (glyph_bbox.xMin < bbox.xMin) ? glyph_bbox.xMin : bbox.xMin;
bbox.yMin = (glyph_bbox.yMin < bbox.yMin) ? glyph_bbox.yMin : bbox.yMin;
bbox.xMax = (glyph_bbox.xMax > bbox.xMax) ? glyph_bbox.xMax : bbox.xMax;
bbox.yMax = (glyph_bbox.yMax > bbox.yMax) ? glyph_bbox.yMax : bbox.yMax;
FT_Done_Glyph(glyph); // 🔥 修复内存泄漏:必须释放!
pen.x += slot->advance.x;
pen.y += slot->advance.y;
}
*abbox = bbox;
return 0;
}
步骤 2:显示一行文字display_string
cpp
/* 显示字符串:修复边界逻辑 + 删除无用变量 + 优化矩阵位置 */
int display_string(FT_Face face, wchar_t *wstr, int lcd_x, int lcd_y, double angle, int* y_limit_p)
{
int i;
int error;
FT_Vector pen;
FT_BBox bbox;
FT_GlyphSlot slot = face->glyph;
FT_Matrix matrix;
size_t str_len = wcslen(wstr); // 缓存长度
// 🔥 修复垂直边界保护逻辑(防止文字重叠)
if (*y_limit_p < 0)
*y_limit_p = var.yres;
if (lcd_y > *y_limit_p)
lcd_y = *y_limit_p;
// 坐标转换:LCD → FreeType笛卡尔坐标
int x = lcd_x;
int y = var.yres - lcd_y;
// 计算字符串总外框
compute_string_bbox(face, wstr, &bbox);
// 反推绘制原点(核心定位)
pen.x = (x - bbox.xMin) * 64;
pen.y = (y - bbox.yMax) * 64;
// 旋转矩阵:只设置一次(放循环外,优化效率)
matrix.xx = (FT_Fixed)(cos(angle) * 0x10000L);
matrix.xy = (FT_Fixed)(-sin(angle) * 0x10000L);
matrix.yx = (FT_Fixed)(sin(angle) * 0x10000L);
matrix.yy = (FT_Fixed)(cos(angle) * 0x10000L);
// 逐字符绘制
for (i = 0; i < str_len; i++)
{
FT_Set_Transform(face, &matrix, &pen);
error = FT_Load_Char(face, wstr[i], FT_LOAD_RENDER);
if (error)
{
printf("FT_Load_Char error\n");
return -1;
}
// 绘制到LCD
draw_bitmap(&slot->bitmap,
slot->bitmap_left,
var.yres - slot->bitmap_top,
0xFFFFFF);
// 移动画笔
pen.x += slot->advance.x;
pen.y += slot->advance.y;
}
// 更新垂直限制
*y_limit_p = lcd_y - (bbox.yMax - bbox.yMin);
return 0;
}
步骤 3:主函数调用
cpp
int main(int argc, char *argv[])
{
FT_Library library;
FT_Face face;
FT_Error error;
wchar_t *wstr = L"百问网www.100ask.net";
int ret = 0;
int fd_fb = -1;
int font_size = 24;
int lcd_x, lcd_y;
double angle;
int y_limit; // 原野值变量
// 🔥 修复1:初始化y_limit(核心)
y_limit = 0;
// 🔥 修复2:修正参数提示
if (argc < 5)
{
printf("usage: %s <字体文件> <X坐标> <Y坐标> <角度> [字号]\n", argv[0]);
printf("example: %s ./simhei.ttf 100 100 0 24\n", argv[0]);
return -1;
}
lcd_x = atoi(argv[2]);
lcd_y = atoi(argv[3]);
angle = strtod(argv[4], NULL);
angle = angle / 360.0 * 2 * M_PI; // 角度转弧度
if (argc == 6)
font_size = atoi(argv[5]);
// 打开FrameBuffer
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
perror("open /dev/fb0 failed");
ret = -1;
goto exit_re;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
perror("ioctl failed");
ret = -2;
goto exit_cl;
}
size_t screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
fb_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fb_base == MAP_FAILED)
{
perror("mmap failed");
ret = -2;
goto exit_cl;
}
memset(fb_base, 0, screen_size); // 清屏
// FreeType初始化
error = FT_Init_FreeType(&library);
if (error)
{
printf("FT_Init_FreeType error\n");
ret = -3;
goto exit_mp;
}
error = FT_New_Face(library, argv[1], 0, &face);
if (error)
{
printf("FT_New_Face error\n");
ret = -4;
goto exit_ft;
}
error = FT_Set_Pixel_Sizes(face, font_size, 0);
if (error)
{
printf("FT_Set_Pixel_Sizes error\n");
ret = -5;
goto exit_face;
}
display_string(face, wstr, lcd_x, lcd_y, angle, &y_limit);
exit_face:
FT_Done_Face(face);
exit_ft:
FT_Done_FreeType(library);
exit_mp:
munmap(fb_base, screen_size);
exit_cl:
close(fd_fb);
exit_re:
return ret;
}
6.3 秋招考点总结(一行文字)
- **freetype 坐标系统和 LCD 坐标的区别?如何转换?**答:freetype 原点在左下角,y 向上;LCD 原点在左上角,y 向下。转换公式:LCD_y = 屏幕高度 - 笛卡尔_y。
- **如何实现一行文字的居中对齐?**答:先计算整行文字的外框 FT_BBox,根据外框反推原点 pen,让文字中心对应屏幕中心。
- **advance 参数的作用?**答:字符间距,用于计算下一个字符的原点,实现文字的连续排列。
七、全章秋招考点总梳理(面试 / 笔试必背)
1. 概念题(面试必问)
- 编码、字体、点阵字库、矢量字库的区别?
- ASCII、Unicode、UTF-8 的关系?为什么嵌入式 Linux 用 UTF-8?
- freetype 的作用?显示文字的完整流程?
- 点阵字库和矢量字库的优缺点?嵌入式场景如何选择?
- 交叉编译的原理?头文件、库文件、动态库 / 静态库的区别?
2. 笔试题(手写代码)
- 手写 ASCII 点阵字符显示函数
- 手写 freetype 显示单个文字的核心流程
- 手写 freetype 显示一行文字的排版逻辑
- 手写 GB2312 区位码计算偏移的公式
3. 实战题(项目经验)
- 如何解决中文乱码问题?
- 如何实现文字的旋转、倾斜?
- 如何实现多行文字自动换行?
- 如何优化 freetype 的显示性能?
八、师傅给你的学习建议(效率优先,秋招导向)
- 学习顺序 :先吃透编码原理 →再学点阵字库(ASCII / 中文) →最后学freetype 矢量字库,循序渐进,不要跳
- 实践重点 :
- 先跑通
show_ascii.c,再跑通show_chinese.c,理解点阵的本质 - 再跑通
freetype_show_font.c,理解矢量字库的优势 - 最后跑通
show_line.c,掌握文字排版,为 GUI 开发打基础
- 先跑通
- 时间分配 :
- 编码 + 点阵:1-2 天(吃透原理,跑通代码)
- freetype 交叉编译 + 单个文字:1-2 天(重点是流程和代码)
- 一行文字 + 排版:1 天(重点是坐标转换和排版)
- 总耗时:3-5 天,完全足够秋招备考
- 秋招优先级 :
- 必背:编码原理、freetype 流程、点阵原理
- 必写:ASCII 画点函数、freetype 单个文字代码
- 加分:文字排版、旋转、多行换行
九、后续学习规划
- 下一步:基于 freetype,实现多行文字自动换行、指定颜色显示、中英文混合显示(教材课后作业)
- 再下一步:结合 Framebuffer,做一个简单的 GUI 界面(比如菜单、文本框)
- 秋招前:把全章原理、代码背熟,面试能完整说清流程,笔试能手写核心函数