如果你在B站、抖音或者一些复古游戏开发社区里,经常看到那种充满“像素感”和“复古风”的数字倒计时、状态显示,心里会不会好奇:这种效果是怎么做出来的?是自己一笔一画描点,还是有什么现成的“素材包”可以快速复用?
最近,一个来自B站视频(BV1aN411S7Dc)的“5×7点阵字体素材”在开发者圈子里小火了一把。视频本身只有9秒,展示了一个简洁的倒计时效果,但评论区里“求字体”、“求素材”的呼声却很高。这背后反映了一个很实际的开发痛点:在嵌入式系统、LED显示屏、小型游戏或任何需要极简图形界面的场景下,如何快速获得一套清晰、易用且风格统一的点阵字体?
自己画?效率太低,且难以保证每个字符的视觉权重一致。用现成的TTF字体渲染再采样?在资源极度受限的微控制器(如STM32、Arduino)或低分辨率屏幕上,效果往往不尽人意,且会占用宝贵的存储空间。
本文要解决的,正是这个问题。我们将围绕这个“5×7点阵字体素材”,不仅告诉你如何“直接拿走”并使用它,更会深入拆解点阵字体的原理、制作方法、优化技巧,以及在不同平台(Arduino、嵌入式Linux、Python PIL)下的实战应用。你会发现,掌握这套小小的点阵字体,能为你从单片机HMI到复古游戏UI的众多项目,节省大量前期开发时间。
1. 点阵字体:为什么在2024年它依然不可替代?
在矢量字体和高清屏普及的今天,点阵字体听起来像是个“老古董”。但它在特定领域的生命力极其顽强,核心原因在于其绝对的确定性和极致的资源效率。
确定性:一个5×7的点阵字体,每个字符就是35个比特(bit)的数据,定义死了每个像素点是亮(1)还是灭(0)。在任何显示器、任何驱动库上,它的显示效果都是一模一样的,没有抗锯齿、没有字体渲染引擎的差异。这对于工业控制、仪器仪表等要求显示绝对稳定可靠的场景至关重要。
资源效率:我们算一笔账。一套包含数字(0-9)、大写字母(A-Z)、常用符号(如冒号、百分号)的ASCII字符集,大约有100个字符。使用5×7点阵:
- 每个字符:5字节(每列1字节,共5列,每字节的低7位表示一列的7个点)。
- 100个字符:仅需
100 * 5 = 500字节。 这500字节,可以直接作为常量数组编译进代码,存储在单片机的Flash中。相比之下,即便是一个最小的矢量字体文件,也要以KB甚至MB计。在只有几十KB RAM和Flash的STM32F103系列芯片上,这500字节的性价比是无与伦比的。
应用场景:
- 嵌入式显示:OLED (SSD1306)、LCD (1602, 12864)、LED点阵屏。
- 复古游戏与艺术:模拟老式计算机、游戏机(如Game Boy)的像素风格界面。
- 低分辨率UI:在分辨率极低的屏幕(如128x32像素)上,点阵字体是保证可读性的唯一选择。
所以,当你在BV1aN411S7Dc视频里看到那个倒计时,它不仅仅是一个效果,更代表了一种在资源约束下优雅解决问题的工程思路。
2. 5×7点阵字体素材解析:从视频到数据
原视频展示了一个9秒倒计时,数字变化流畅,风格统一。我们可以推断,作者使用了一套预先定义好的5×7点阵字体库。所谓“素材”,通常就是指这个字体库的数据数组。
2.1 数据结构是如何组织的?
一个5×7的点阵字符,最常见的存储方式是按列存储。将字符放置在一个5列(宽)x 7行(高)的网格中。
- 每一列用一个字节(8位)来表示。
- 字节的低位(LSB)通常对应网格的顶部,高位对应底部,或者相反,这取决于屏幕的扫描方向。
- 一个字符由5个这样的字节组成。
例如,数字 “0” 的5×7点阵可能看起来像这样(@代表点亮像素,空格代表熄灭):
@@@ @ @ @ @ @ @ @ @ @ @ @@@将其转换为按列存储的字节数组(假设LSB为顶部,1为亮,0为灭): 第一列:0111110->0x3E第二列:1000001->0x41第三列:1000001->0x41第四列:1000001->0x41第五列:0111110->0x3E所以,字符‘0’的数据就是{0x3E, 0x41, 0x41, 0x41, 0x41, 0x3E}。注意,这里为了视觉对称,有时会在左侧或右侧增加一列空白,实际可能是6列,但核心是5列有效像素。
2.2 如何从视频/图片中提取字体素材?
如果你只有视频或图片,可以通过“取模软件”或手动方式提取。这里介绍一个通用流程:
- 准备字符图片:将视频中出现的所有字符(0-9, A-Z等)截图,并排成整齐的一行或一列,背景尽量纯净,前景色单一。
- 使用取模软件:这是最常用的方法。搜索“LCD字模提取软件”、“点阵取模工具”,有很多选择(如PCtoLCD2002、DotMatrix Font Generator)。在软件中:
- 设置字宽=5, 字高=7。
- 设置取模方式:列行式,顺向(高位在前还是低位在前需与你的显示驱动匹配)。
- 导入图片,软件会自动识别像素并生成十六进制或二进制数组。
- 手动编码:对于少量字符,可以自己画在5x7的网格纸上,然后手动换算成十六进制数。虽然效率低,但有助于理解原理。
3. 环境准备:跨平台使用的核心思路
使用点阵字体,不依赖于复杂的IDE或框架。核心是准备好字体数据数组和对应的显示驱动函数。我们将为三个典型环境做准备:
- Arduino / AVR / STM32 (C/C++):用于驱动OLED、LCD等硬件屏幕。
- Python (PIL/Pillow):用于在电脑上生成图片、模拟效果或处理像素艺术。
- 嵌入式Linux / Framebuffer:用于在树莓派等平台上的控制台或图形显示。
通用前置条件:
- 定义清晰的字体数据数组。
- 了解你的显示设备或画布是如何处理像素数据的(坐标系、颜色格式)。
4. 字体数据定义:一个完整的ASCII字符集示例
下面我们定义一个包含数字0-9、冒号、空格和字母A-F(用于十六进制显示)的5×7点阵字体数组。这是实现倒计时等功能的最小集合。
注意:取模方式为列行式,每列一个字节,字节的第0位(LSB)对应最上面的像素。如果实际显示上下颠倒,调整驱动代码中的位顺序即可。
// font_5x7.h #ifndef FONT_5X7_H #define FONT_5X7_H #include <stdint.h> // 字体宽度和高度定义 #define FONT_WIDTH 5 #define FONT_HEIGHT 7 // 字符索引:使用ASCII码作为索引。这里只定义部分字符。 // 空格(32) 到 ‘_’(95) 是常用可打印字符范围的一部分。 #define FONT_START_ASCII 32 // 空格 #define FONT_END_ASCII 95 // ‘_’ // 声明字体数据数组(在.c文件中定义) extern const uint8_t font_5x7[][FONT_WIDTH]; // 根据ASCII字符获取其点阵数据指针 const uint8_t* get_font_data(char c); #endif // FONT_5X7_H// font_5x7.c #include "font_5x7.h" // 5x7 点阵字体数据 // 每个字符由5个字节组成,每个字节代表一列,LSB为顶部像素。 const uint8_t font_5x7[][FONT_WIDTH] = { // 空格 (ASCII 32) {0x00, 0x00, 0x00, 0x00, 0x00}, // 以下省略部分不常用字符的数据,用全0占位... // 我们直接从数字 ‘0’ (ASCII 48) 开始 // ‘0’ {0x3E, 0x51, 0x49, 0x45, 0x3E}, // ‘1’ {0x00, 0x42, 0x7F, 0x40, 0x00}, // ‘2’ {0x42, 0x61, 0x51, 0x49, 0x46}, // ‘3’ {0x21, 0x41, 0x45, 0x4B, 0x31}, // ‘4’ {0x18, 0x14, 0x12, 0x7F, 0x10}, // ‘5’ {0x27, 0x45, 0x45, 0x45, 0x39}, // ‘6’ {0x3C, 0x4A, 0x49, 0x49, 0x30}, // ‘7’ {0x01, 0x71, 0x09, 0x05, 0x03}, // ‘8’ {0x36, 0x49, 0x49, 0x49, 0x36}, // ‘9’ {0x06, 0x49, 0x49, 0x29, 0x1E}, // 冒号 ‘:’ (ASCII 58) {0x00, 0x36, 0x36, 0x00, 0x00}, // 分号 ‘;’ ... 等其他符号 // 大写字母 ‘A’ (ASCII 65) {0x7C, 0x12, 0x11, 0x12, 0x7C}, // ‘B’ {0x7F, 0x49, 0x49, 0x49, 0x36}, // ‘C’ {0x3E, 0x41, 0x41, 0x41, 0x22}, // ‘D’ {0x7F, 0x41, 0x41, 0x22, 0x1C}, // ‘E’ {0x7F, 0x49, 0x49, 0x49, 0x41}, // ‘F’ {0x7F, 0x09, 0x09, 0x09, 0x01}, // ... 可以继续添加其他字母和符号 }; // 辅助函数:获取字符数据 const uint8_t* get_font_data(char c) { if (c < FONT_START_ASCII || c > FONT_END_ASCII) { c = ' '; // 超出范围返回空格 } return font_5x7[c - FONT_START_ASCII]; }数据解读:以数字‘0’的数据{0x3E, 0x51, 0x49, 0x45, 0x3E}为例,将其转换为二进制并可视化(1画@):
0x3E: 0011 1110 -> 列1: .@@@. (注意,这里最前面两位是0,实际是5宽,两边可能有留白) 0x51: 0101 0001 -> 列2: @.@...@ ... 组合起来就形成了数字0的形状。重要提示:网络上或不同取模软件生成的十六进制数可能完全不同,这取决于扫描方向(顺向/逆向)、字节位顺序(高位在上/低位在上)和起始点。拿到一组数据后,第一件事就是写一个简单的测试程序,在屏幕或模拟器上显示出来看是否正确,如果不正确,可能需要在显示函数里对每个字节进行位反转(bit-reverse)或调整扫描顺序。
5. 实战应用一:在Arduino SSD1306 OLED上显示倒计时
让我们实现视频中的9秒倒计时效果。我们将使用流行的Adafruit_SSD1306和Adafruit_GFX库,它们已经内置了绘制像素的功能,我们需要做的就是根据字体数据“画点”。
5.1 硬件与库准备
- 硬件:Arduino Uno/Nano + 0.96寸 I2C SSD1306 OLED屏。
- 库:在Arduino IDE库管理中搜索并安装
Adafruit SSD1306和Adafruit GFX Library。
5.2 核心显示函数
我们不是使用库自带的字体,而是用自定义的draw_char_5x7函数。
// sketch_5x7_countdown.ino #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include "font_5x7.h" // 包含我们刚才定义的字体数据 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // 在指定位置(x, y)绘制一个5x7字符 // color: SSD1306_WHITE 或 SSD1306_BLACK void draw_char_5x7(int16_t x, int16_t y, char c, uint16_t color) { const uint8_t* char_data = get_font_data(c); if (!char_data) return; // 遍历字符的每一列 (5列) for (int8_t col = 0; col < FONT_WIDTH; col++) { uint8_t column_data = char_data[col]; // 遍历列的每一位 (7行) for (int8_t row = 0; row < FONT_HEIGHT; row++) { // 检查当前位是否为1 (点亮像素) if (column_data & (1 << row)) { // 注意:这里假设字节LSB(bit0)对应row=0(顶部) display.drawPixel(x + col, y + row, color); } } } } // 绘制字符串 void draw_string_5x7(int16_t x, int16_t y, const char* str, uint16_t color) { int16_t current_x = x; while (*str) { draw_char_5x7(current_x, y, *str, color); current_x += FONT_WIDTH + 1; // 字符宽度 + 1像素间距 str++; } } void setup() { Serial.begin(9600); if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306 allocation failed")); for(;;); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); } void loop() { display.clearDisplay(); // 模拟9秒倒计时 for (int count = 9; count >= 0; count--) { display.clearDisplay(); // 将数字转换为字符串 char count_str[2]; count_str[0] = '0' + count; count_str[1] = '\0'; // 在屏幕中央绘制数字 (简单计算居中位置) int16_t x = (SCREEN_WIDTH - FONT_WIDTH) / 2; int16_t y = (SCREEN_HEIGHT - FONT_HEIGHT) / 2; draw_char_5x7(x, y, count_str[0], SSD1306_WHITE); display.display(); delay(1000); // 等待1秒 } delay(3000); // 倒计时结束后停留3秒 }代码关键点:
draw_char_5x7函数是核心:它读取字符的5个字节数据,逐位判断,并在对应位置调用display.drawPixel画点。if (column_data & (1 << row)):这是位操作,检查column_data的第row位是否为1。这是最容易出错的地方。如果显示上下颠倒,请尝试if (column_data & (0x80 >> row))(检查高位)或在取模时调整设置。- 位置计算:
x + col, y + row确保了按列扫描。 - 居中显示:通过屏幕尺寸和字体尺寸简单计算起始坐标。
6. 实战应用二:使用Python PIL生成点阵字体图片
如果你没有硬件,或者想先在电脑上预览、测试字体效果,Python的PIL(Pillow)库是绝佳选择。我们可以用它将字体数据渲染成PNG图片。
# generate_font_image.py from PIL import Image, ImageDraw # 定义5x7字体数据 (这里以数字0-9为例,数据与C语言示例一致) FONT_5X7 = { '0': [0x3E, 0x51, 0x49, 0x45, 0x3E], '1': [0x00, 0x42, 0x7F, 0x40, 0x00], '2': [0x42, 0x61, 0x51, 0x49, 0x46], '3': [0x21, 0x41, 0x45, 0x4B, 0x31], '4': [0x18, 0x14, 0x12, 0x7F, 0x10], '5': [0x27, 0x45, 0x45, 0x45, 0x39], '6': [0x3C, 0x4A, 0x49, 0x49, 0x30], '7': [0x01, 0x71, 0x09, 0x05, 0x03], '8': [0x36, 0x49, 0x49, 0x49, 0x36], '9': [0x06, 0x49, 0x49, 0x29, 0x1E], ':': [0x00, 0x36, 0x36, 0x00, 0x00], } FONT_WIDTH = 5 FONT_HEIGHT = 7 PIXEL_SIZE = 10 # 每个“点阵像素”用多大方块来显示,方便观看 SPACING = 2 def draw_char(draw, x, y, char_data, color=(0, 0, 0)): """在指定位置绘制一个字符""" for col in range(FONT_WIDTH): byte_val = char_data[col] for row in range(FONT_HEIGHT): if byte_val & (1 << row): # LSB对应顶部 # 绘制一个放大的像素块 x1 = x + col * PIXEL_SIZE y1 = y + row * PIXEL_SIZE x2 = x1 + PIXEL_SIZE - 1 y2 = y1 + PIXEL_SIZE - 1 draw.rectangle([x1, y1, x2, y2], fill=color) def main(): # 计算画布大小 chars = list(FONT_5X7.keys()) num_chars = len(chars) img_width = num_chars * (FONT_WIDTH * PIXEL_SIZE + SPACING) + SPACING img_height = FONT_HEIGHT * PIXEL_SIZE + 2 * SPACING # 创建白色背景图片 image = Image.new('RGB', (img_width, img_height), color='white') draw = ImageDraw.Draw(image) # 绘制所有字符 for i, char in enumerate(chars): x = SPACING + i * (FONT_WIDTH * PIXEL_SIZE + SPACING) y = SPACING draw_char(draw, x, y, FONT_5X7[char], color='black') # 可选:在字符下方标注ASCII # draw.text((x, y + FONT_HEIGHT * PIXEL_SIZE), char, fill='gray') # 保存图片 image.save('font_5x7_preview.png') image.show() print("字体预览图已生成: font_5x7_preview.png") if __name__ == '__main__': main()运行这个脚本,你会得到一个PNG图片,清晰地展示出每个字符的点阵形状。这是验证字体数据是否正确的最直观方法。
7. 常见问题与排查思路
在使用自定义点阵字体的过程中,90%的问题都出在数据与显示驱动的匹配上。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 字符显示为乱码或完全不对 | 1. 字体数据数组索引错误(ASCII码偏移计算不对)。 2. 显示函数遍历行列的顺序错误。 | 1. 先用Python脚本生成预览图,确认数据本身是否正确。 2. 在显示函数中打印出某个字符(如‘0’)的字节数据,与标准数据对比。 3. 尝试只画一个简单的字符(如‘I’)或一个固定图案(如0x7F)。 | 1. 检查get_font_data函数中的索引计算。2. 调整 draw_char函数中col和row的循环顺序,或像素点亮条件((1 << row)改为(0x80 >> row))。 |
| 字符显示上下颠倒 | 字节的位顺序(Bit Order)与屏幕扫描方向不匹配。 | 在Python预览中正常,在硬件上颠倒。 | 在显示函数中,对每个column_data执行位反转。例如:`uint8_t reversed = (column_data & 0x01) << 7 |
| 字符显示左右颠倒或镜像 | 列扫描顺序错误。 | 观察字符是整体镜像了,还是只是错位。 | 将for (int8_t col = 0; col < FONT_WIDTH; col++)改为for (int8_t col = FONT_WIDTH-1; col >=0; col--),即从右向左画列。 |
| 字符间有奇怪的竖线或粘连 | 字符间距不够。每个字符画完后,没有留出空白列。 | 检查绘制字符串的函数,在绘制完一个字符后,x坐标增量是否等于FONT_WIDTH + 1(1像素间隔)。 | 确保draw_string函数中current_x += FONT_WIDTH + 1;。也可以尝试增加间隔像素数。 |
| 显示位置偏移,不居中 | 坐标计算错误。屏幕坐标系原点可能在不同角落(左上角或左下角)。 | 先尝试在(0,0)位置画字符,看它出现在屏幕哪里。 | 仔细阅读显示驱动库的文档,确认坐标系。居中计算应为x = (screen_width - char_width) / 2。 |
| 部分像素点不亮或常亮 | 1. 硬件连接问题(如I2C地址错误)。 2. 颜色值设置错误(单色屏通常1/0代表亮/灭)。 | 1. 先用驱动库自带的示例程序测试屏幕是否正常。 2. 检查 drawPixel的颜色参数是SSD1306_WHITE还是1。 | 1. 确认硬件接线和I2C地址。 2. 将 drawPixel的color参数固定为1试试。 |
8. 最佳实践与进阶技巧
掌握了基础显示后,这些技巧能让你的点阵字体应用更上一层楼。
8.1 字体数据优化与存储
- 使用
const和PROGMEM(AVR Arduino):字体数组通常不会改变,务必声明为const。在AVR架构上,为了节省宝贵的RAM,需要使用PROGMEM关键字将数组存储在Flash中,并通过pgm_read_byte函数读取。// AVR Arduino 专用优化 #include <avr/pgmspace.h> const uint8_t font_5x7[][5] PROGMEM = { // ... 数据 }; // 读取时 uint8_t column_data = pgm_read_byte(&(font_5x7[char_index][col])); - 压缩存储:5×7实际只用了35位,不到5字节。可以尝试位压缩(如5个7位整数),但会牺牲一些读取速度,需权衡。
8.2 高级显示功能
- 反色显示:在
draw_char函数中增加一个invert参数,当其为真时,将判断条件取反,即可实现黑底白字。 - 缩放:在画点时,不画一个像素,而画一个2x2或3x3的小方块,即可实现2倍、3倍放大。
Adafruit_GFX库本身支持setTextSize,但那是针对内置字体。自定义字体需要自己实现缩放循环。 - 滚动与动画:通过动态改变绘制字符的起始x或y坐标,可以实现横向或纵向的滚动效果。9秒倒计时本质上就是一个逐帧动画。
8.3 扩展字符集与多语言
- 5×7分辨率对于显示英文数字和简单符号尚可,但显示中文几乎不可能。如果需要中文,需要更大的点阵(如16×16)。
- 可以定义多个字体数组,根据需要切换,实现不同大小的字体。
8.4 从“素材”到“工具链”
真正的效率提升不在于有一个现成的字体数组,而在于建立一套从字体选择->取模生成->代码集成->效果预览的流程。
- 选择一款喜欢的等宽位图字体(如Terminus, Fixedsys)。
- 使用取模软件批量生成所有需要的字符(包括大小写、符号)。
- 编写一个脚本,将取模软件输出的文本格式(可能是C数组、Python列表)自动转换成你项目所需的头文件/源文件格式。
- 用Python预览脚本做最终验证。
9. 总结与资源
回过头看,BV1aN411S7Dc这个9秒视频提供的不仅仅是一个“素材”,它更像一个引子,引出了在嵌入式和小型图形化项目中,如何处理文本显示这个经典问题。通过本文的拆解,你应该已经掌握了:
- 点阵字体的核心价值:确定性、资源效率,及其在嵌入式领域的不可替代性。
- 从图像到数据的完整流程:如何通过取模软件或手动方式,将视觉符号转化为可编程的字节数组。
- 跨平台的核心实现:无论是Arduino C++还是Python PIL,操作的本质都是遍历字节数组的每一位,在对应位置输出像素。
- 关键的调试技巧:遇到显示问题,从“数据-顺序-坐标”三个维度进行系统性排查。
下一步你可以做什么?
- 完善字体库:找到或制作一套包含更多字符(如小写字母、更多符号)的5×7字体。
- 挑战更优存储:尝试用更紧凑的方式(如位压缩)存储字体,并测量节省的空间和增加的CPU开销。
- 集成到实际项目:将它用到你的温湿度计、迷你游戏机、自定义仪表盘的显示部分。
- 探索矢量替代方案:在资源稍丰富的平台(如ESP32),可以研究如何嵌入微型矢量字体(如u8g2字体),并与点阵方案对比优劣。
点阵字体是计算机图形显示最原始的浪漫,它用一种最直接的方式,在有限的画布上勾勒出信息的世界。理解并运用它,是每个从事底层开发或热爱像素艺术的工程师的必修课。希望这份从“素材”出发的深度解析,能成为你下一个精彩项目的坚实起点。