最近做了个使用 retro-go 的开源掌机 基于ESP32-S3的C19掌机(适配GBC外壳) - 立创开源硬件平台 ,做完后用提供的固件发现屏幕反显了,估计是屏幕型号不太对,随即自己拉 retro-go 官方库来编译,拉取的最新的 1.45 ,记录下适配的过程和解决的一些问题。
- 安装 esp-idf 5.2 ,retro-go 介绍可以用idf5.3 但我用5.3报错
- 拉取 retro-go ,在/components/retro-go/targets 下增加 一个target ,参考已经项目与自己硬件最接近版本,注意 key map 的设置格式
- 在/components/retro-go 下的 config.h 中 参考其它的target 将自己增加的target 引入进来
支持中文:
- 找一个中文的 ttf 可以用这个 https://github.com/TakWolf/fusion-pixel-font
- 用 font_converter.py 设置路径与字符范围及字体大小后生成c源文件
- 在 fonts.h 中增加生成的源文件内的变量名和设置枚举与 fonts 数组
- sdkconfig 中设置 CONFIG_FATFS_API_ENCODING_UTF_8=y 开启 fatfs 使用unicode 编码读取文件名
如果不开启 utf8 会导致 rg_utf8_get_codepoint 方法后台报 invalid utf-8 prefix,也就是无法正确得到文件名 。
参考:
Configuration Options Reference - ESP32-S3 - — ESP-IDF 编程指南 v5.5 文档
https://elm-chan.org/fsw/ff/doc/config.html#lfn_unicode
解决开启中文浏览列表会很卡的问题
增加中文字体后且能成功显示中文字体后,会发现列表变得很卡,查看源码后知道其查询字库是一个个遍历的,字符数变多后肯定会卡,解决方法是字符数据定长或再加一个索引数组。不管哪种方法其本质都是希望能通过字符号+偏移量定位到具体字符数据,下面是主要代码:
typedef struct
{char name[16];uint8_t type; // 0=monospace, 1=proportional ,2= location by mapuint8_t width; // median width of glyphsuint8_t height; // height of tallest glyphsize_t chars; // glyph countconst uint32_t *map; //索引数组uint32_t map_len;//索引数组长度uint32_t map_start_code;//索引的第一个字符码uint8_t data[]; // stream of rg_font_glyph_t (end of list indicated by an entry with 0x0000 codepoint)
} rg_font_t;//rg_gui.c get_glyph 增加 font->type == 2 的逻辑 小于等于255 直接查询,从map_start_code 开始使用索引
// 这只是方法的一部分
static size_t get_glyph(uint32_t *output, const rg_font_t *font, int points, int c)
{// Some glyphs are always zero widthif (!font || c == '\r' || c == '\n' || c == 0) // || c < 8 || c > 0xFFFF)return 0;if (points <= 0)points = font->height;const uint8_t *ptr = font->data;const rg_font_glyph_t *glyph = (rg_font_glyph_t *)ptr;if(font->type == 2){if (c <= 255){int times =0;while (glyph->code && glyph->code != c && times++ <=255){if (glyph->width != 0)ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;ptr += sizeof(rg_font_glyph_t);glyph = (rg_font_glyph_t *)ptr;}}else if(c >= font->map_start_code){uint32_t map_index = c - font->map_start_code;if (map_index < font->map_len){uint32_t data_index = font->map[map_index];glyph = (rg_font_glyph_t *)(ptr + data_index);}}}else{// for (size_t i = 0; i < font->chars && glyph->code && glyph->code != c; ++i)while (glyph->code && glyph->code != c){if (glyph->width != 0)ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;ptr += sizeof(rg_font_glyph_t);glyph = (rg_font_glyph_t *)ptr;}}
修改后的库:https://github.com/longxiangam/retro-go
tools 下生成字库的 font_converter.py 脚本增加对索引的支持,使用这个脚本生成字库时选择生成 map 并设置 start code 生成的代码就会生成 索引数组。
from PIL import Image, ImageDraw, ImageFont
from tkinter import Tk, Label, Entry, StringVar, Button, Frame, Canvas, filedialog, ttk, Checkbutton, IntVar
import os
import re
import uuid################################ - Font format - ################################
#
# font:
# |
# ├── glyph_bitmap[] -> 8 bit array containing the bitmap data for all glyph
# |
# └── glyph_data[] -> struct that contains all the data to correctly draw the glyph
#
######################## - Explanation of glyph_bitmap[] - #######################
# First, let's see an example : '!'
#
# we are going to convert glyph_bitmap[] bytes to binary :
# 11111111,
# 11111111,
# 11000111,
# 11100000,
#
# then we rearrange them :
# [3 bits wide]
# 111
# 111
# 111
# [9 111 We clearly reconize '!' character
# bits 111
# tall] 111
# 000
# 111
# 111
# (000000)
#
# Second example with '0' :
# 0x30,0x04,0x07,0x09,0x00,0x07,
# 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C,
#
# - width = 0x07 = 7
# - height = 0x09 = 9
# - data[n] = 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C
#
# in binary :
# 1111101
# 11111011
# 10111111
# 1111110
# 11111101
# 11111011
# 11111111
# 1111100
#
# We see that everything is not aligned so we add zeros ON THE LEFT :
# ->01111101
# 11111011
# 10111111
# ->01111110
# 11111101
# 11111011
# 11111111
# ->01111100
#
# Next, we rearrange the bits :
# [ 7 bits wide]
# 0111110
# 1111110
# 1110111
# [9 1110111
# bits 1110111 we can reconize '0' (if you squint a loooot)
# tall] 1110111
# 1110111
# 1111111
# 0111110
# (0)
#
# And that's basically how characters are encoded using this tool# Example usage (defaults parameters)
list_char_ranges_init = "32-126, 160-255,19968-40959"
font_size_init = 12
map_start_code_init = "19968" # Default map start codefont_path = ("arial.ttf") # Replace with your TTF font path# Variables to track panning
start_x = 0
start_y = 0def get_char_list():list_char = []for intervals in list_char_ranges.get().split(','):first = intervals.split('-')[0]# we check if the user input is a single char or an intervaltry:second = intervals.split('-')[1]except IndexError:list_char.append(int(first))else:for char in range(int(first), int(second) + 1):list_char.append(char)return list_chardef find_bounding_box(image):pixels = image.load()width, height = image.sizex_min, y_min = width, heightx_max, y_max = 0, 0for y in range(height):for x in range(width):if pixels[x, y] >= 1: # Looking for 'on' pixelsx_min = min(x_min, x)y_min = min(y_min, y)x_max = max(x_max, x)y_max = max(y_max, y)if x_min > x_max or y_min > y_max: # No target pixels foundreturn Nonereturn (x_min, y_min, x_max+1, y_max+1)def load_ttf_font(font_path, font_size):# Load the TTF fontenforce_font_size = enforce_font_size_bool.get()pil_font = ImageFont.truetype(font_path, font_size)font_name = ' '.join(pil_font.getname())font_data = []for char_code in get_char_list():char = chr(char_code)image = Image.new("1", (font_size * 2, font_size * 2), 0) # generate mono bmp, 0 = black colordraw = ImageDraw.Draw(image)# Draw at pos 1 otherwise some glyphs are clipped. we remove the added offset belowdraw.text((1, 0), char, font=pil_font, fill=255)bbox = find_bounding_box(image) # Get bounding boxif bbox is None: # control character / spacewidth, height = 0, 0offset_x, offset_y = 0, 0else:x0, y0, x1, y1 = bboxwidth, height = x1 - x0, y1 - y0offset_x, offset_y = x0, y0if offset_x:offset_x -= 1try: # Get the real glyph width including padding on the right that the box will removeadv_w = int(draw.textlength(char, font=pil_font))adv_w = max(adv_w, width + offset_x)except:adv_w = width + offset_x# Shift or crop glyphs that would be drawn beyond font_size. Most glyphs are not affected by this.# If enforce_font_size is false, then max_height will be calculated at the end and the font might# be taller than requested.if enforce_font_size and offset_y + height > font_size:print(f" font_size exceeded: {offset_y+height}")if font_size - height >= 0:offset_y = font_size - heightelse:offset_y = 0height = font_size# Extract bitmap datacropped_image = image.crop(bbox)bitmap = []row = 0i = 0for y in range(height):for x in range(width):if i == 8:bitmap.append(row)row = 0i = 0pixel = 1 if cropped_image.getpixel((x, y)) else 0row = (row << 1) | pixeli += 1bitmap.append(row << 8-i) # to "fill" with zero the remaining empty bitsbitmap = bitmap[0:int((width * height + 7) / 8)]# Create glyph entryglyph_data = {"char_code": char_code,"ofs_y": int(offset_y),"box_w": int(width),"box_h": int(height),"ofs_x": int(offset_x),"adv_w": int(adv_w),"bitmap": bitmap,}font_data.append(glyph_data)# The font render glyphs at font_size but they can shift them up or down which will cause the max_height# to exceed font_size. It's not desirable to remove the padding entirely (the "enforce" option above), # but there are some things we can do to reduce the discrepency without affecting the look.max_height = max(g["ofs_y"] + g["box_h"] for g in font_data)if max_height > font_size:min_ofs_y = min((g["ofs_y"] if g["box_h"] > 0 else 1000) for g in font_data)for key, glyph in enumerate(font_data):offset = glyph["ofs_y"]# If there's a consistent excess of top padding across all glyphs, we can remove itif min_ofs_y > 0 and offset >= min_ofs_y:offset -= min_ofs_y# In some fonts like Vera and DejaVu we can shift _ and | to gain an extra pixelif chr(glyph["char_code"]) in ["_", "|"] and offset + glyph["box_h"] > font_size and offset > 0:offset -= 1font_data[key]["ofs_y"] = offsetmax_height = max(g["ofs_y"] + g["box_h"] for g in font_data)print(f"Glyphs: {len(font_data)}, font_size: {font_size}, max_height: {max_height}")return (font_name, font_size, font_data)def load_c_font(file_path):# Load the C fontfont_name = "Unknown"font_size = 0font_data = []with open(file_path, 'r', encoding='UTF-8') as file:text = file.read()text = re.sub('//.*?$|/\*.*?\*/', '', text, flags=re.S|re.MULTILINE)text = re.sub('[\n\r\t\s]+', ' ', text)# FIXME: Handle parse errors...if m := re.search('\.name\s*=\s*"(.+)",', text):font_name = m.group(1)if m := re.search('\.height\s*=\s*(\d+),', text):font_size = int(m.group(1))if m := re.search('\.data\s*=\s*\{(.+?)\}', text):hexdata = [int(h, base=16) for h in re.findall('0x[0-9A-Fa-f]{2}', text)]while len(hexdata):char_code = hexdata[0] | (hexdata[1] << 8)if not char_code:breakofs_y = hexdata[2]box_w = hexdata[3]box_h = hexdata[4]ofs_x = hexdata[5]adv_w = hexdata[6]bitmap = hexdata[7:int((box_w * box_h + 7) / 8) + 7]glyph_data = {"char_code": char_code,"ofs_y": ofs_y,"box_w": box_w,"box_h": box_h,"ofs_x": ofs_x,"adv_w": adv_w,"bitmap": bitmap,}font_data.append(glyph_data)hexdata = hexdata[7 + len(bitmap):]return (font_name, font_size, font_data)def generate_font_data():if font_path.endswith(".c"):font_name, font_size, font_data = load_c_font(font_path)else:font_name, font_size, font_data = load_ttf_font(font_path, int(font_height_input.get()))window.title(f"Font preview: {font_name} {font_size}")font_height_input.set(font_size)max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))bounding_box = bounding_box_bool.get()canvas.delete("all")offset_x_1 = 1offset_y_1 = 1for glyph_data in font_data:offset_y = glyph_data["ofs_y"]width = glyph_data["box_w"]height = glyph_data["box_h"]offset_x = glyph_data["ofs_x"]adv_w = glyph_data["adv_w"]if offset_x_1+adv_w+1 > canva_width:offset_x_1 = 1offset_y_1 += max_height + 1byte_index = 0byte_value = 0bit_index = 0for y in range(height):for x in range(width):if bit_index == 0:byte_value = glyph_data["bitmap"][byte_index]byte_index += 1if byte_value & (1 << 7-bit_index):canvas.create_rectangle((x+offset_x_1+offset_x)*p_size, (y+offset_y_1+offset_y)*p_size, (x+offset_x_1+offset_x)*p_size+p_size, (y+offset_y_1+offset_y)*p_size+p_size,fill="white")bit_index += 1bit_index %= 8if bounding_box:canvas.create_rectangle((offset_x_1+offset_x)*p_size, (offset_y_1+offset_y)*p_size, (width+offset_x_1+offset_x)*p_size, (height+offset_y_1+offset_y)*p_size, width=1, outline="red", fill='')canvas.create_rectangle((offset_x_1)*p_size, (offset_y_1)*p_size, (offset_x_1+adv_w)*p_size, (offset_y_1+max_height)*p_size, width=1, outline='blue', fill='')offset_x_1 += adv_w + 1return (font_name, font_size, font_data)def save_font_data():font_name, font_size, font_data = generate_font_data()filename = filedialog.asksaveasfilename(title='Save Font',initialdir=os.getcwd(),initialfile=f"{font_name.replace('-', '_').replace(' ', '')}{font_size}",defaultextension=".c",filetypes=(('Retro-Go Font', '*.c'), ('All files', '*.*')))if filename:with open(filename, 'w', encoding='UTF-8') as f:f.write(generate_c_font(font_name, font_size, font_data))def generate_c_font(font_name, font_size, font_data):normalized_name = f"{font_name.replace('-', '_').replace(' ', '')}{font_size}"max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))memory_usage = sum(len(g["bitmap"]) + 7 for g in font_data) # 7 bytes for header# Calculate map data if enabledgenerate_map = generate_map_bool.get()map_start_code = int(map_start_code_input.get()) if generate_map else 0map_data = []if generate_map:# Find the range for the mapchar_codes = [g["char_code"] for g in font_data]max_char = max(char_codes)map_size = max_char - map_start_code + 1map_data = [0] * map_size # Initialize with zerosdata_index = 0for glyph in font_data:map_index = glyph["char_code"] - map_start_codeif 0 <= map_index < map_size:map_data[map_index] = data_indexdata_index += 7 + len(glyph["bitmap"]) # 7 bytes header + bitmap sizememory_usage += map_size * 4 # Each map entry is 4 bytes (uint32_t)file_data = "#include \"../rg_gui.h\"\n\n"file_data += "// File generated with font_converter.py (https://github.com/ducalex/retro-go/tree/dev/tools)\n\n"file_data += f"// Font : {font_name}\n"file_data += f"// Point Size : {font_size}\n"file_data += f"// Memory usage : {memory_usage} bytes\n"file_data += f"// # characters : {len(font_data)}\n"if generate_map:file_data += f"// Map start code : {map_start_code}\n"file_data += f"// Map size : {len(map_data)} entries\n"file_data += "\n"font_type = 1;if generate_map:file_data += f"static const uint32_t font_{normalized_name}_map[] = {{\n"for i in range(0, len(map_data), 8):line = map_data[i:i+8]file_data += " " + ", ".join([f"0x{val:04X}" for val in line]) + ",\n"file_data += "};\n\n"font_type = 2;file_data += f"const rg_font_t font_{normalized_name} = {{\n"file_data += f" .name = \"{font_name}\",\n"file_data += f" .type = {font_type},\n"file_data += f" .width = 0,\n"file_data += f" .height = {max_height},\n"file_data += f" .chars = {len(font_data)},\n"if generate_map:file_data += f" .map_start_code = {map_start_code},\n"file_data += f" .map = font_{normalized_name}_map,\n"file_data += f" .map_len = sizeof(font_{normalized_name}_map) / 4,\n"file_data += f" .data = {{\n"for glyph in font_data:char_code = glyph['char_code']header_data = [char_code & 0xFF, char_code >> 8, glyph['ofs_y'], glyph['box_w'],glyph['box_h'], glyph['ofs_x'], glyph['adv_w']]file_data += f" /* U+{char_code:04X} '{chr(char_code)}' */\n "file_data += ", ".join([f"0x{byte:02X}" for byte in header_data])file_data += f",\n "if len(glyph["bitmap"]) > 0:file_data += ", ".join([f"0x{byte:02X}" for byte in glyph["bitmap"]])file_data += f","file_data += "\n"file_data += "\n"file_data += " // Terminator\n"file_data += " 0x00, 0x00,\n"file_data += " },\n"file_data += "};\n"return file_datadef select_file():filename = filedialog.askopenfilename(title='Load Font',initialdir=os.getcwd(),filetypes=(('True Type Font', '*.ttf'), ('Retro-Go Font', '*.c'), ('All files', '*.*')))if filename:global font_pathfont_path = filenamegenerate_font_data()# Function to zoom in and out on the canvas
def zoom(event):scale = 1.0if event.delta > 0: # Scroll up to zoom inscale = 1.2elif event.delta < 0: # Scroll down to zoom outscale = 0.8# Get the canvas size and adjust scale based on cursor positioncanvas.scale("all", event.x, event.y, scale, scale)# Update the scroll region to reflect the new scalecanvas.configure(scrollregion=canvas.bbox("all"))def start_pan(event):global start_x, start_y# Record the current mouse positionstart_x = event.xstart_y = event.ydef pan_canvas(event):global start_x, start_y# Calculate the distance moveddx = start_x - event.xdy = start_y - event.y# Scroll the canvascanvas.move("all", -dx, -dy)# Update the starting positionstart_x = event.xstart_y = event.yif __name__ == "__main__":window = Tk()window.title("Retro-Go Font Converter")# Get screen width and heightscreen_width = window.winfo_screenwidth()screen_height = window.winfo_screenheight()# Set the window size to fill the entire screenwindow.geometry(f"{screen_width}x{screen_height}")p_size = 8 # pixel size on the renderercanva_width = screen_width//p_sizecanva_height = screen_height//p_size-16frame = Frame(window)frame.pack(anchor="center", padx=10, pady=2)# choose font button (file picker)choose_font_button = ttk.Button(frame, text='Choose font', command=select_file)choose_font_button.pack(side="left", padx=5)# Label and Entry for Font heightLabel(frame, text="Font height").pack(side="left", padx=5)font_height_input = StringVar(value=str(font_size_init))Entry(frame, textvariable=font_height_input, width=4).pack(side="left", padx=5)# Variable to hold the state of the checkboxenforce_font_size_bool = IntVar() # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Enforce size", variable=enforce_font_size_bool).pack(side="left", padx=5)# Label and Entry for Char ranges to includeLabel(frame, text="Ranges to include").pack(side="left", padx=5)list_char_ranges = StringVar(value=str(list_char_ranges_init))Entry(frame, textvariable=list_char_ranges, width=30).pack(side="left", padx=5)# Variable to hold the state of the checkboxbounding_box_bool = IntVar(value=1) # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Bounding box", variable=bounding_box_bool).pack(side="left", padx=10)# Variable to hold the state of the map generation checkboxgenerate_map_bool = IntVar() # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Generate map", variable=generate_map_bool).pack(side="left", padx=5)# Label and Entry for Map start codeLabel(frame, text="Map start code").pack(side="left", padx=5)map_start_code_input = StringVar(value=str(map_start_code_init))Entry(frame, textvariable=map_start_code_input, width=6).pack(side="left", padx=5)# Button to launch the font generation functionb1 = Button(frame, text="Preview", width=14, height=2, background="blue", foreground="white", command=generate_font_data)b1.pack(side="left", padx=5)# Button to launch the font exporting functionb1 = Button(frame, text="Save", width=14, height=2, background="blue", foreground="white", command=save_font_data)b1.pack(side="left", padx=5)frame = Frame(window).pack(anchor="w", padx=2, pady=2)canvas = Canvas(frame, width=canva_width*p_size, height=canva_height*p_size, bg="black")canvas.configure(scrollregion=(0, 0, canva_width*p_size, canva_height*p_size))canvas.bind("<MouseWheel>", zoom)canvas.bind("<ButtonPress-1>", start_pan) # Start panningcanvas.bind("<B1-Motion>",pan_canvas)canvas.focus_set()canvas.pack(fill="both", expand=True)window.mainloop()