本帖最后由 Walkline 于 2021-9-5 00:10 编辑
MicroPython New FontLib
### 项目介绍
使用`MicroPython 开发板`读取自定义字库并显示
### 获取完整项目
因为项目中使用了子模块 [FontMaker Client](https://gitee.com/walkline/fontmaker-client.git) 的`binary`分支 和 [OLED Research](https://gitee.com/walkline/oled-research.git),所以要获取完整项目代码需要如下操作
#### 克隆方式
```bash
$ git clone --recursive https://gitee.com/walkline/micropython-new-fontlib.git
```
#### 下载压缩文件方式
或者克隆时未使用`--recursive`参数的,使用如下代码更新子模块
```bash
$ cd micropython-new-fontlib
$ git submodule update --init --recursive
```
### 如何使用和测试
#### 生成字库文件
直接运行
```bash
$ client/youyuan_16.cmd
```
会生成一个`combined.bin`文件,字库文件信息如下
| 参数 | 数值 | 说明 |
| :-: | :-: | :-: |
| 字体 | 幼圆 | |
| 字号 | 16 | 像素 |
| 字重 | 普通 | 不加粗、不斜体、无下划线 |
| 字符宽度 | 固定 | |
| 宽度 | 16 | 像素 |
| 高度 | 16 | 像素 |
| 水平偏移 | 0 | 像素 |
| 垂直偏移 | 0 | 像素 |
| 扫描方式 | 垂直扫描 | |
| 字节顺序 | 低位在前 | |
#### 使用电脑测试
直接运行
```bash
$ python fontlib.py
```
会显示相关信息,包括:
* 字库信息
* 获取的字模数据
* 字模打印预览
完整输出内容如下
```docs
HZK Info: .//client/combined.bin
file size : 303520
font height : 16
data size : 32
scan mode : Horizontal
byte order : LSB
characters : 8932
!: b'\x00\x00\x00\x00\x00\x00\x08\x00\x0c\x00\x08\x00\x08\x00\x08\x00\x08\x00\x08\x00\x08\x00\x08\x00\x00\x00\x08\x00\x08\x00\x00\x00'
☆: b'\x00\x00\x00\x00\x00\x80\x01\x80\x01\x80\x01@\x02@~? \x04\x10\x08\x0c\x10\x08\x10\x08\x90\x0bH\x148\x18\x08'
⒉: b'\x00\x00\x00\x00\x00\x00\x07\xc0\x08@\x00@\x00@\x00@\x00\x80\x01\x00\x01\x00\x02\x00\x04\x00\x0c\x08\x0f\xcc\x00\x00'
,: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x000\x00 \x00'
我: b'\x00\x00\x00@\x07PxH\x08D\x08D\x7f\xfe\x08D\x08D\t(\x0e0x0\x080\x08R\t\x8ax\x06'
ㄘ: b'\x00\x00\x00\x00\x00\x00\x01\x00\x01\x80\x00\x80\x01\x00\x03\xf0\x1d\x00\x01\x00\x02\xe0\x03`\x00@\x00\x80\x00\x00\x00\x00'
■: b'\x00\x00\x00\x00\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff\x7f\xff'
B: b'\x00\x00\x00\x00\x00\x00\x00\x00\x07\xf0\x060\x068\x060\x06p\x07\xf0\x06\x18\x06\x18\x06\x18\x07\xf0\x00\x00\x00\x00'
中: b'\x00\x00\x01\x00\x01\x00\x01\x00?\xfc!\x04!\x04!\x04!\x04!\x04!\x04?\xfc\x01\x00\x01\x00\x01\x00\x01\x00'
爱: b'\x00\x00\x00\x00?\xf8\x11\x10\t\x10?\xfcD\x02B\x02\x1f\xf8\x04\x00\x07\xf8\x0e\x08\x13\x10 \xe0\x01\xe0\x1e\x1e'
β: b'\x00\x00\x00\x00\x00\x00\x01\xe0\x01\x10\x030\x02 \x02\xc0\x02`\x06 \x04 \x04`\x04`\x07\xc0\x0c\x00\x08\x00'
あ: b"\x00\x00\x00\x00\x03\x00\x03\x00\x02\xc0\x1f\x80\x02\x00\x06\x80\x07\xf0\x0c\x98\x15\x0c'\x0c&\x0c/\x088\x10\x01\xe0"
H: b'\x00\x00\x00\x00\x00\x00\xe7\x00B\x00B\x00B\x00B\x00~\x00B\x00B\x00B\x00B\x00\xe7\x00\x00\x00\x00\x00'
华: b'\x00\x00\x00\x00\x08@\x08H\x18p(\xc0+BHB\x08~\x01\x00\x01\x00\x7f\xfe\x01\x00\x01\x00\x01\x00\x01\x00'
ǚ: b"\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x08\x004\x00C\x00C\x00C\x00C\x00C\x00C\x00'\x00\x18\x00\x00\x00"
e: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00B\x00~\x00@\x00@\x00B\x00<\x00\x00\x00\x00\x00'
l: b'\x00\x00\x00\x00\x00\x00p\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00|\x00\x00\x00\x00\x00'
o: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00B\x00B\x00B\x00B\x00B\x00<\x00\x00\x00\x00\x00'
⑴: b'\x00\x00\x00\x00\x08\x00\x10\x08!\x84 \x82@\x82@\x83@\x81@\x81@\x81@\x82 \x82 \x84\x13\xc4\x10\x08'
```
#### 使用开发板测试 1
推荐使用 [AMPY Batch Tool](https://gitee.com/walkline/a-batch-tool) (`ab`工具) 进行文件上传和调试操作
```bahs
# 上传文件
$ ab
# 进入 repl 模式
$ ab --repl
# 运行开发板上的文件
# 使用快捷键 Ctrl-T
>>>
Run onboard file
[1] /boot.py
[2] /drivers/ssd1306.py
[3] /fontlib.py
Choose a file: 3
>>>
# 在开发板上运行本地文件
# 使用快捷键 Ctrl-R
>>> Run local file
[1] fontlib.py
[2] drivers\ssd1306.py
Choose a file: 1
>>>
```
#### 使用开发板测试 2
这是把`fontlib.py`文件中非`MicroPython`代码精简掉,并编译为字节码再运行测试的方法
```bash
# 上传文件
$ ab abconfig-mpy
```
因为上传文件中已经包含了`main.py`,所以直接复位就可以看到效果了
### 使用图标字体生成字库
方法参见 [使用图标字体生成字库](./ICONFONT.md)
### 关于速度
> 以当前字库文件举例:
> * 文件名:`combined.bin`
> * 文件大小:`303,520`字节
> * `GB2312`索引表大小:`17,672`字节
> * 点阵大小:`16x16`像素
> * 字符数据大小:`32`字节
> * 字符总数:`8932`个(包含`ASCII`和`GB2312`)
* 实例化`FontLib`时打开字库文件,读取文件头信息,大约 12 ms
* 同时读取`?`的字符数据作为占位符,大约 5 ms
* 打印字库信息,大约 39 ms(简直无语,建议实际使用时不要打印)
* 检索字符数据前再次打开字库文件,大约 12 ms
* 检索字符数据,每字符大约 12 ms
* 字符数据使用`Dict`存储,字符`Unicode`值作为关键字,取值耗时可以忽略
* 显示字符耗时未统计
### 关于提速
决定显示速度的因素有两方面:
* 从字库中读取指定字符的字模数据
* 在 oled 中显示
显示部分,`MicroPython`已经提供了`SSD1306`驱动,使用`FrameBuffer`进行数据管理,我相信官方的实力,所以显示部分的速度假设已经没有提升空间了
数据部分,最开始的思路就是“完成功能”,所以在读取的时候是这个流程:
1. 打开字库文件
2. 取一个字符
3. `分段`查找字符在`索引表`中的偏移量
4. `定位`到偏移量读取字模数据
5. 从第`2`步开始循环,直到读取所有字符数据
> `分段`的意思是:在`Python`中打开文件,它不提供查找功能,需要先读取文件内容再进行查找,对于开发板来说,一次性读取全部文件肯定会导致`内存溢出`,所以需要分段读取一段合理长度的数据再进行查找,在查找到之前就需要不停的分段读取
这个方法的问题在于,每个字符都要从`索引表`的开始位置进行查找,比较浪费
提速的思路就是,在每次`分段`读取数据的时候查找所有字符,减少`分段`次数
继续查找资料,有人说使用`read`代替`seek`可以提高`定位`的效率,在电脑上进行了简单测试,300M 的文件使用`read`比`seek`快大约`0.2 ms`,但是这个方法有个致命问题,虽然`read`读取到的数据并不使用,但也会分配内存空间,在开发板上运行的结果就是,更慢了
对比一下三种方式的读取速度(毫秒)
字库文件信息:
* 文件大小:500024 字节
* 字符总数:8932 个
* 数据大小:54 字节
| | 原方法 | 新方法(seek) | 新方法(read) |
| :-: | :-: | :-: | :-: |
| 打开字库 | 27.51 | 26.22 | 61.21 |
| 字符数 | 原方法 | 新方法(seek) | 新方法(read) |
| :-: | :-: | :-: | :-: |
| 240 | 13.09 | 6.50 | 159.28 |
| 227 | 16.05 | 8.22 | / |
| 767 | 内存溢出 | 内存溢出 | / |
> 注:统计结果为**平均时间**
这里的字符数是包含了重复字符的总数,实际获取数据时是要去重的
读取速度还与字符在`索引表`中的相对位置有关,相对位置靠前的字符当然能更快被找到,反之则更慢
一次性读取太多的数据也会导致`内存溢出`,所以在实际使用时不建议一次性读取太多
手动调用垃圾回收(gc.collect())的时机一定要掌握好,并不是任何时候手动调用都能起到预期的作用,有时还会起到反作用
对于能够自动回收的,如函数内部的变量,可以省去手动调用的操作
> 统计运行时间使用的装饰器
```python
from utime import ticks_us, ticks_diff
def cal_time(fn):
def wrapper(*args,**kwargs):
starTime = ticks_us()
f = fn(*args,**kwargs)
print('%s() runtime: %s ms' % (fn.__name__, ticks_diff(ticks_us(), starTime) / 1000))
return f
return wrapper
```
### 关于测速
又增加了一个单独测速的文件`fontlib_test.py`,发现一个现象,虽然知道和缓存有关,但是具体怎么实现的并不了解
这里测速使用了两个方法
* 一次获取所有字符的字模数据
* 分段获取所有字符的字模数据
单独测试这两种方法,得到的结果如下
```docs
### method: load all
### 240 chars, get 111 chars: 1145.765 ms, avg: 10.32221 ms
```
```docs
### method separated
### 0 get 18 chars: 201.403 ms, avg: 11.18906 ms
...
### 9 get 22 chars: 169.531 ms, avg: 7.705955 ms
### 240 chars, get 197 chars: 1760.472 ms, avg: 9.978033 ms
```
但是同时测试两种方法的结果却是这样的
```docs
### method: load all
### 240 chars, get 111 chars: 1145.765 ms, avg: 10.32221 ms
### method separated
### 0 get 18 chars: 110.468 ms, avg: 6.137111 ms
...
### 9 get 22 chars: 125.306 ms, avg: 5.695727 ms
### 240 chars, get 197 chars: 1170.494 ms, avg: 6.635651 ms
```
调换顺序的结果也是类似
```docs
### method separated
### 0 get 18 chars: 201.403 ms, avg: 11.18906 ms
...
### 9 get 22 chars: 169.531 ms, avg: 7.705955 ms
### 240 chars, get 197 chars: 1760.472 ms, avg: 9.978033 ms
### method: load all
### 240 chars, get 111 chars: 565.726 ms, avg: 5.096631 ms
```
结论就是,不管是一次性还是分段,只要提前获取了所有字符的字模数据,下次再获取的时候速度就会快很多,不过显然这种提速并没有实际意义
### 合作交流
* 联系邮箱:
* QQ 交流群:
* 走线物联:163271910
* 扇贝物联:31324057