电子网首页 > 开源与设计

【Let'sdo2025第三期活动】【拾色播放器DIY】显示屏

2026-01-01 20:45 | 来源:电子世界报

前言

这是"拾颜播放器"项目学习记录的第二部分。关于显示屏和中文字体显示。

ESP32-S3 Reverse TFT Feather 板子自带一块 1.14 寸的 ST7789 显示屏,分辨率 240x135。刚开始以为显示文字很简单,结果发现中文字体才是最大的坑——CircuitPython 默认不支持中文,需要自己制作字体文件。这篇笔记记录了整个过程。


一、硬件介绍1.1 显示屏规格参数值
型号ST7789
分辨率240 x 135 像素
尺寸1.14 英寸
颜色16-bit RGB (65536色)
接口SPI
1.2 初始化方式

这块板子的显示屏是板载的,CircuitPython 已经自动初始化好了,直接用 board.DISPLAY 就行:

import board
display = board.DISPLAY

print(f"分辨率: {display.width} x {display.height}")  # 240 x 135

不用像其他屏幕那样自己接线和初始化,很方便。

1.3 CircuitPython 显示库

用到了这几个库:

import displayio           # 显示系统核心
import terminalio          # 默认英文字体
from adafruit_display_text 
import label  # 文本标签
from adafruit_bitmap_font 
import bitmap_font  # 位图字体
二、显示文字基础2.1 最简单的显示

 terminalio.FONT 显示英文:

from adafruit_display_text import labelimport terminalio

label = label.Label(
    terminalio.FONT,
    text="Hello!",
    color=0xFFFFFF,  # 白色
    x=20,
    y=60,
    scale=2          # 放大2倍)display.show(label)
2.2 颜色格式

CircuitPython 用 16 进制表示颜色:

COLORS = {
    "red": 0xFF0000,     # 红
    "green": 0x00FF00,   # 绿
    "blue": 0x0000FF,    # 蓝
    "yellow": 0xFFFF00,  # 黄
    "cyan": 0x00FFFF,    # 青
    "white": 0xFFFFFF,   # 白
    "black": 0x000000,   # 黑}

RGB 转 16 进制公式:0xRRGGBB

def rgb_to_hex(r, g, b):
    return (r << 16) | (g << 8) | b

# 测试
print(f"红色: {rgb_to_hex(255, 0, 0):#X}")  # 0xFF0000
2.3 显示组 (Group)

CircuitPython 用显示组来管理图层:

import displayio

# 创建显示组
text_group = displayio.Group(scale=1, x=0, y=0)
display.root_group = text_group

# 添加标签
label = label.Label(terminalio.FONT, text="Test", color=0xFFFFFF, x=10, y=10)
text_group.append(label)
三、中文字体 - 最大的坑3.1 问题

CircuitPython 默认只支持英文,要显示中文必须自己制作字体文件。

试过几种方案:

  1. 直接用中文字符 - 报错,不支持

  2. 下载现成的中文PCF字体 - 文件太大,3MB+,板子装不下

  3. 只提取需要的字符 - 这是最终的解决方案

3.2 PCF字体格式

PCF (Portable Compiled Format) 是 X11 的位图字体格式,CircuitPython 的 adafruit_bitmap_font 库支持。

关键点:

  • CircuitPython 要求小端序格式

  • 签名是 \x01fcp(不是标准的 \x00pcf)

  • 只需要包含用到的字符,文件可以很小

3.3 字体生成流程

 ttf_to_pcf_v2.py 脚本生成字体:

# 1. 定义需要的字符CHARS = """色彩播放器颜色情感音乐播放器开始运行红色绿色蓝色黄色青色品红白色黑色深海蓝炽热红清新绿温暖黄神秘紫中性灰舒缓热情自然活力梦幻平静检测到颜色情感模式播放音乐物体靠近物体离开停止播放准备就绪状态版本音量条形图环形图渐变效果动画波浪欢迎"""# 2. 用PIL渲染字符from PIL import Image, ImageDraw, ImageFont

font = ImageFont.truetype("fonts/OPPOSans-M.ttf", 16)
img = Image.new('1', (width, height), 0)
draw = ImageDraw.Draw(img)
draw.text((1, 1), char, fill=1, font=font)
# 3. 转换为位图数据
bitmap_data = bytearray()
for y in range(height):
    byte_val = 0
    for x in range(width):
        if img.getpixel((x, y)):
            byte_val |= 1 << (7 - (x % 8))
        if x % 8 == 7:
            bitmap_data.append(byte_val)
3.4 PCF文件结构

生成的 PCF 文件包含 4 个表:

表类型说明
Accelerators (1)字体属性(大小等)
Metrics (2)字符度量(宽度、高度)
Bitmaps (4)字符位图数据
Encoding (5)字符编码映射
# 写入小端序格式def write_pcf_file(path, glyph_data):
    with open(path, 'wb') as f:
        # 1. 签名:0x01 + "fcp" (小端序)
        f.write(b'\x01fcp')

        # 2. 表头
        f.write(struct.pack('<I', len(tables)))  # 表数量

        # 3. 各表数据
        for table_type, table_data in tables:
            f.write(struct.pack('<I', table_type))   # 表类型
            f.write(struct.pack('<I', len(table_data)))  # 表大小
            f.write(struct.pack('<I', offset))       # 偏移量
3.5 生成结果
输入字体: OPPOSans-M.ttf (3.2 MB)
需要提取: 90 个字符
输出文件: fonts/OPPOSans-M-16.pcf
文件大小: ~50 KB
字符数量: 90

从 3MB 压缩到 50KB,够用了!

3.6 生成步骤
# 1. 运行字体生成脚本python3 tools/ttf_to_pcf_v2.py
# 2. 复制到设备cp fonts/OPPOSans-M-16.pcf /Volumes/CIRCUITPY/fonts/
四、封装 DisplayTest 类

把显示逻辑封装起来:

class DisplayTest:
    def __init__(self):
        self.display = None
        self.text_group = None
        self.cn_font = None

    def setup(self):
        """初始化显示屏"""
        self.display = board.DISPLAY
        self.width = self.display.width
        self.height = self.display.height        # 创建显示组
        self.text_group = displayio.Group(x=0, y=0)
        self.display.root_group = self.text_group        # 加载中文字体
        self._load_chinese_font()
        return True

    def _load_chinese_font(self):
        """加载中文字体"""
        try:
            self.cn_font = bitmap_font.load_font("fonts/OPPOSans-M-16.pcf")
            print("  中文字体加载成功")
        except Exception as e:
            print(f"  中文字体加载失败: {e}")
            self.cn_font = None
五、创建标签的几种方式5.1 英文标签
def create_label(self, text, x, y, fontsize=2, color=0xFFFFFF):
    """创建英文标签"""
    return label.Label(
        terminalio.FONT,
        text=text,
        color=color,
        x=x,
        y=y,
        scale=fontsize    )
5.2 中文标签
def create_cn_label(self, text, x, y, fontsize=1, color=0xFFFFFF):
    """创建中文标签"""
    if self.cn_font is not None:
        return label.Label(
            self.cn_font,
            text=text,
            color=color,
            x=x,
            y=y        )
    # 没有中文字体时回退
    return label.Label(
        terminalio.FONT,
        text=text,
        color=color,
        x=x,
        y=y,
        scale=fontsize    )
5.3 中英文混排

这是比较麻烦的部分,需要把中英文分开处理:

def create_mixed_label(self, text, x, y, fontsize=2, color=0xFFFFFF):
    """中英文混合显示"""
    chinese_chars = ""
    english_chars = ""

    # 分离字符
    for char in text:
        if '\u4e00' <= char <= '\u9fff':  # 中文字符范围
            chinese_chars += char        else:
            english_chars += char

    labels = []

    # 中文字符
    if chinese_chars and self.cn_font is not None:
        cn_label = label.Label(
            self.cn_font,
            text=chinese_chars,
            color=color,
            x=x,
            y=y        )
        labels.append(cn_label)

    # 英文字符
    if english_chars:
        cn_width = len(chinese_chars) * 16  # 估算中文宽度
        en_label = label.Label(
            terminalio.FONT,
            text=english_chars,
            color=color,
            x=x + cn_width,
            y=y,
            scale=fontsize        )
        labels.append(en_label)

    return labels
六、背景与清屏6.1 设置背景色
def set_background(self, color):
    """设置背景色"""
    # 清空当前显示
    while len(self.text_group) > 0:
        self.text_group.pop()

    # 创建背景
    color_bitmap = displayio.Bitmap(1, 1, 1)
    color_palette = displayio.Palette(1)
    color_palette[0] = color
    bg = displayio.TileGrid(
        color_bitmap,
        pixel_shader=color_palette,
        x=0, y=0,
        width=self.width,
        height=self.height    )
    self.text_group.append(bg)
6.2 清屏
def clear_display(self):
    """清屏"""
    self.text_group = displayio.Group(x=0, y=0)
    self.display.root_group = self.text_group
七、颜色渐变
def interpolate_color(self, color1, color2, factor):
    """颜色插值"""
    r1 = (color1 >> 16) & 0xFF
    g1 = (color1 >> 8) & 0xFF
    b1 = color1 & 0xFF

    r2 = (color2 >> 16) & 0xFF
    g2 = (color2 >> 8) & 0xFF
    b2 = color2 & 0xFF

    r = int(r1 + (r2 - r1) * factor)
    g = int(g1 + (g2 - g1) * factor)
    b = int(b1 + (b2 - b1) * factor)

    return (r << 16) | (g << 8) | bdef test_gradient(self):
    """渐变效果"""
    steps = 10
    for i in range(steps):
        factor = i / (steps - 1)
        color = self.interpolate_color(0xFF0000, 0x0000FF, factor)
        # 绘制...
八、图形绘制8.1 条形图
def test_bar_chart(self):
    """条形图"""
    bar_data = [
        (30, 0xFF0000),
        (60, 0xFFFF00),
        (90, 0x00FF00),
    ]

    for i, (value, bar_color) in enumerate(bar_data):
        x = 10 + i * 20
        height = int(value * 70 / 100)

        bitmap = displayio.Bitmap(15, height, 1)
        palette = displayio.Palette(1)
        palette[0] = bar_color
        grid = displayio.TileGrid(bitmap, pixel_shader=palette, x=x, y=100 - height)
        self.text_group.append(grid)
8.2 环形图/进度条
def test_circle_chart(self):
    """环形进度条"""
    center_x, center_y = 64, 70
    radius = 35

    # 画背景圆
    for angle in range(360):
        x = int(center_x + radius * math.cos(math.radians(angle)))
        y = int(center_y + radius * math.sin(math.radians(angle)))
        # 绘制点...

    # 画进度
    progress = 75
    end_angle = int(progress * 360 / 100)
    for angle in range(end_angle):
        # 绘制进度弧...
8.3 正弦波动画
def test_wave_effect(self):
    """波浪效果"""
    amplitude = 25
    frequency = 0.2
    center_y = 70

    for x in range(0, 240, 4):
        y = int(center_y + amplitude * math.sin(frequency * x + time.monotonic() * 2))
        # 绘制点
        bitmap = displayio.Bitmap(3, 3, 1)
        palette = displayio.Palette(1)
        palette[0] = 0x00FFFF
        grid = displayio.TileGrid(bitmap, pixel_shader=palette, x=x, y=y)
        self.text_group.append(grid)
        time.sleep(0.02)
九、完整测试流程
def run_all_tests(self):
    """运行所有测试"""
    self.test_chinese()          # 中文显示
    time.sleep(0.5)

    self.test_welcome()          # 欢迎界面
    time.sleep(0.5)

    self.test_colors()           # 颜色背景
    time.sleep(0.5)

    self.test_text()             # 文本显示
    time.sleep(0.5)

    self.test_mixed_language()   # 中英文混排
    time.sleep(0.5)

    self.test_all_chinese_chars() # 所有中文字符
    time.sleep(0.5)

    self.test_bar_chart()        # 条形图
    time.sleep(0.5)

    self.test_circle_chart()     # 环形图
    time.sleep(0.5)

    self.test_gradient()         # 渐变效果
    time.sleep(0.5)

    self.test_color_detection()  # 颜色检测显示
    time.sleep(0.5)

    self.test_animations()       # 动画
    time.sleep(0.5)

    self.test_wave_effect()      # 波浪效果
十、常见问题Q1: 中文字体加载失败
ValueError: Unknown magic number

原因:PCF 文件格式不对。解决:

  • 确认使用小端序格式(\x01fcp 签名)

  •  xxd fonts/OPPOSans-M-16.pcf | head -2 检查文件头

Q2: 中文字符显示为方框

原因:字体里没有这个字符。解决:

  • 检查 CHARS 变量是否包含该字符

  • 重新生成字体文件

Q3: 字体文件太大

原因:提取了太多字符。解决:

  • 只提取项目实际用到的字符

  • 本项目 90 个字符约 50KB

Q4: 显示闪烁

原因:刷新太频繁或背景设置方式不对。解决:

  • 减少 time.sleep() 间隔

  •  displayio.Group 统一管理元素

Q5: 内存不足
MemoryError: Failed to allocate bitmap

原因:图片或位图太大。解决:

  • 减小位图尺寸

  •  TileGrid 复用位图

  • 减少同时显示的元素数量

十一、项目中的使用

在"拾颜播放器"中,显示屏用来:

  1. 显示欢迎界面 - 项目启动时

  2. 显示检测到的颜色 - 颜色名称和 RGB 值

  3. 显示当前模式 - 如"深海蓝 | 舒缓"

  4. 显示音量条 - 音乐播放时

# 显示颜色检测结果
self.set_background(color_rgb)

for lbl in self.create_mixed_label(f"检测到: {color_name}", 20, 50, fontsize=2, color=0xFFFFFF):
    self.text_group.append(lbl)
    
rgb_label = self.create_label(f"R:{r} G:{g} B:{b}", 20, 85, fontsize=1, color=0xDDDDDD)
self.text_group.append(rgb_label)
十二、测试效果

实际测试效果如下:

iShot_2025-12-28_00.31.09.png



十三、总结

通过这个项目学到的:

  1. CircuitPython 显示系统 - displayio、Group、TileGrid 的关系

  2. 位图字体原理 - 如何从 TTF 提取字符转为 PCF

  3. 中文字体处理 - 只提取需要的字符,文件可以很小

  4. 颜色处理 - RGB 和 16 进制转换

  5. 基本图形绘制 - 条形图、环形图、动画

关键点:

  • 板载屏幕直接用 board.DISPLAY

  • 中文字体需要自己制作,用小端序 PCF 格式

  • 90 个字符的精简字体约 50KB

  • 中英文混排需要分开处理


推荐技术

返回顶部