掌控板micropython编程实现OLED中显示汉字
1. 介绍
在ESP32中显示中文有很多种办法,例如,使用支持汉字的固件(https://blog.csdn.net/weixin_42227421/article/details/134632037);使用PCtoLCD2002软件生成自定义字体点阵的方法;使用OLED显示屏的内置字体(如果支持);还有micropython不用取模软件,直接输入中文并用OLED显示中文的方法(https://blog.csdn.net/gaoke11240/article/details/134100612)。
micropython μFont是AntonVanke开发的一个开源项目,网址是https://github.com/AntonVanke/MicroPython-uFont。MicroPython-uFont是一个为MicroPython环境设计的轻量级、高效的字体解决方案。它包含一系列预处理的TrueType字体,这些字体被转换成Python字典,可以直接在MicroPython中使用,无需额外的解析或编译步骤。
2. 在掌控板的OLED中显示汉字的方法
(1)使用thinny软件安装esp32固件,固件网址是https://micropython.org/resources/firmware/ESP32_GENERIC-20241025-v1.24.0.bin。
(2)在https://github.com/AntonVanke/MicroPython-uFont上下载源文件,在thonny软件中将下载的unifont-14-12917-16.v3.bmf,ufont.py文件上传到掌控板中。
ufont.py代码如下:
# Github: https://github.com/AntonVanke/MicroPython-uFont
# Gitee: https://gitee.com/liio/MicroPython-uFont
# Tools: https://github.com/AntonVanke/MicroPython-ufont-Tools
# Videos:
# https://www.bilibili.com/video/BV12B4y1B7Ff/
# https://www.bilibili.com/video/BV1YD4y16739/
__version__ = 3import time
import structimport framebufDEBUG = Falsedef timeit(func, *args, **kwargs):"""测试函数运行时间"""# 当交叉编译后无法获取函数名try:_name = func.__name__except AttributeError:_name = "Unknown"def get_running_time(*args, **kwargs):if DEBUG:t = time.ticks_us()result = func(*args, **kwargs)delta = time.ticks_diff(time.ticks_us(), t)print('Function {} Time = {:6.3f}ms'.format(_name, delta / 1000))return resultelse:return func(*args, **kwargs)return get_running_timeclass BMFont:@timeitdef text(self, display, string: str, x: int, y: int,color: int = 0xFFFF, bg_color: int = 0, font_size: int = None,half_char: bool = True, auto_wrap: bool = False, show: bool = True, clear: bool = False,alpha_color: bool = 0, reverse: bool = False, color_type: int = -1, line_spacing: int = 0, **kwargs):"""Args:display: 显示对象string: 显示文字x: 字符串左上角 x 轴y: 字符串左上角 y 轴color: 字体颜色(RGB565)bg_color: 字体背景颜色(RGB565)font_size: 字号大小half_char: 半宽显示 ASCII 字符auto_wrap: 自动换行show: 实时显示clear: 清除之前显示内容alpha_color: 透明色(RGB565) 当颜色与 alpha_color 相同时则透明reverse: 逆置(MONO)color_type: 色彩模式 0:MONO 1:RGB565line_spacing: 行间距**kwargs:Returns:MoreInfo: https://github.com/AntonVanke/MicroPython-uFont/blob/master/README.md"""# 如果没有指定字号则使用默认字号font_size = font_size or self.font_size# 记录初始的 x 位置initial_x = x# 设置颜色类型if color_type == -1 and (display.width * display.height) > len(display.buffer):color_type = 0elif color_type == -1 or color_type == 1:palette = [[bg_color & 0xFF, (bg_color & 0xFF00) >> 8], [color & 0xFF, (color & 0xFF00) >> 8]]color_type = 1# 处理黑白屏幕的背景反转问题if color_type == 0 and color == 0 != bg_color or color_type == 0 and reverse:reverse = Truealpha_color = -1else:reverse = False# 清屏try:display.clear() if clear else 0except AttributeError:print("请自行调用 display.fill() 清屏")for char in range(len(string)):if auto_wrap and ((x + font_size // 2 > display.width and ord(string[char]) < 128 and half_char) or(x + font_size > display.width and (not half_char or ord(string[char]) > 128))):y += font_size + line_spacingx = initial_x# 对控制字符的处理if string[char] == '\n':y += font_size + line_spacingx = initial_xcontinueelif string[char] == '\t':x = ((x // font_size) + 1) * font_size + initial_x % font_sizecontinueelif ord(string[char]) < 16:continue# 超过范围的字符不会显示*if x > display.width or y > display.height:continue# 获取字体的点阵数据byte_data = list(self.get_bitmap(string[char]))# 分四种情况逐个优化# 1. 黑白屏幕/无放缩# 2. 黑白屏幕/放缩# 3. 彩色屏幕/无放缩# 4. 彩色屏幕/放缩if color_type == 0:byte_data = self._reverse_byte_data(byte_data) if reverse else byte_dataif font_size == self.font_size:display.blit(framebuf.FrameBuffer(bytearray(byte_data), font_size, font_size, framebuf.MONO_HLSB),x, y,alpha_color)else:display.blit(framebuf.FrameBuffer(self._HLSB_font_size(byte_data, font_size, self.font_size), font_size,font_size, framebuf.MONO_HLSB), x, y, alpha_color)elif color_type == 1 and font_size == self.font_size:display.blit(framebuf.FrameBuffer(self._flatten_byte_data(byte_data, palette), font_size, font_size,framebuf.RGB565), x, y, alpha_color)elif color_type == 1 and font_size != self.font_size:display.blit(framebuf.FrameBuffer(self._RGB565_font_size(byte_data, font_size, palette, self.font_size),font_size, font_size, framebuf.RGB565), x, y, alpha_color)# 英文字符半格显示if ord(string[char]) < 128 and half_char:x += font_size // 2else:x += font_sizedisplay.show() if show else 0@timeitdef _get_index(self, word: str) -> int:"""获取索引Args:word: 字符Returns:ESP32-C3: Function _get_index Time = 2.670ms"""word_code = ord(word)start = 0x10end = self.start_bitmapwhile start <= end:mid = ((start + end) // 4) * 2self.font.seek(mid, 0)target_code = struct.unpack(">H", self.font.read(2))[0]if word_code == target_code:return (mid - 16) >> 1elif word_code < target_code:end = mid - 2else:start = mid + 2return -1@timeitdef _HLSB_font_size(self, byte_data: bytearray, new_size: int, old_size: int) -> bytearray:old_size = old_size_temp = bytearray(new_size * ((new_size >> 3) + 1))_new_index = -1for _col in range(new_size):for _row in range(new_size):if (_row % 8) == 0:_new_index += 1_old_index = int(_col / (new_size / old_size)) * old_size + int(_row / (new_size / old_size))_temp[_new_index] = _temp[_new_index] | ((byte_data[_old_index >> 3] >> (7 - _old_index % 8) & 1) << (7 - _row % 8))return _temp@timeitdef _RGB565_font_size(self, byte_data: bytearray, new_size: int, palette: list, old_size: int) -> bytearray:old_size = old_size_temp = []_new_index = -1for _col in range(new_size):for _row in range(new_size):if (_row % 8) == 0:_new_index += 1_old_index = int(_col / (new_size / old_size)) * old_size + int(_row / (new_size / old_size))_temp.extend(palette[byte_data[_old_index // 8] >> (7 - _old_index % 8) & 1])return bytearray(_temp)@timeitdef _flatten_byte_data(self, _byte_data: bytearray, palette: list) -> bytearray:"""渲染彩色像素Args:_byte_data:palette:Returns:"""_temp = []for _byte in _byte_data:for _b in range(7, -1, -1):_temp.extend(palette[(_byte >> _b) & 0x01])return bytearray(_temp)@timeitdef _reverse_byte_data(self, _byte_data: bytearray) -> bytearray:for _pixel in range(len(_byte_data)):_byte_data[_pixel] = ~_byte_data[_pixel] & 0xffreturn _byte_data@timeitdef get_bitmap(self, word: str) -> bytes:"""获取点阵图Args:word: 字符Returns:bytes 字符点阵"""index = self._get_index(word)if index == -1:return b'\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x0f\xcf\xf3\xcf\xf3\xff\xf3\xff\xcf\xff?\xff?\xff\xff\xff' \b'?\xff?\xff\xff\xff\xff'self.font.seek(self.start_bitmap + index * self.bitmap_size, 0)return self.font.read(self.bitmap_size)@timeitdef __init__(self, font_file):"""Args:font_file: 字体文件路径"""self.font_file = font_file# 载入字体文件self.font = open(font_file, "rb")# 获取字体文件信息# 字体文件信息大小 16 byte ,按照顺序依次是# 文件标识 2 byte# 版本号 1 byte# 映射方式 1 byte# 位图开始字节 3 byte# 字号 1 byte# 单字点阵字节大小 1 byte# 保留 7 byteself.bmf_info = self.font.read(16)# 判断字体是否正确# 文件头和常用的图像格式 BMP 相同,需要添加版本验证来辅助验证if self.bmf_info[0:2] != b"BM":raise TypeError("字体文件格式不正确: " + font_file)self.version = self.bmf_info[2]if self.version != 3:raise TypeError("字体文件版本不正确: " + str(self.version))# 映射方式# 目前映射方式并没有加以验证,原因是 MONO_HLSB 最易于处理self.map_mode = self.bmf_info[3]# 位图开始字节# 位图数据位于文件尾,需要通过位图开始字节来确定字体数据实际位置self.start_bitmap = struct.unpack(">I", b'\x00' + self.bmf_info[4:7])[0]# 字体大小# 默认的文字字号,用于缩放方面的处理self.font_size = self.bmf_info[7]# 点阵所占字节# 用来定位字体数据位置self.bitmap_size = self.bmf_info[8]
(3)再将ssd1106.py文件上传到掌控板中。
# MicroPython SSD1106 OLED driver, I2C and SPI interfacesfrom micropython import const
import framebuf# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xa4)
SET_ENTIRE_ON1 = const(0xaf)
SET_NORM_INV = const(0xa6)
SET_DISP_OFF = const(0xae)
SET_DISP_ON = const(0xaf)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xa0)
SET_MUX_RATIO = const(0xa8)
SET_COM_OUT_DIR = const(0xc0)
SET_DISP_OFFSET = const(0xd3)
SET_COM_PIN_CFG = const(0xda)
SET_DISP_CLK_DIV = const(0xd5)
SET_PRECHARGE = const(0xd9)
SET_VCOM_DESEL = const(0xdb)
SET_CHARGE_PUMP = const(0x8d)
SET_CHARGE_PUMP1 = const(0xad)
SET_COL_ADDR_L = const(0x02)
SET_COL_ADDR_H = const(0x10)
SET_PAGE_ADDR1 = const(0xb0)
SET_CONTRACT_CTRL = const(0x81)
SET_DUTY = const(0x3f)
SET_VCC_SOURCE = const(0x8b)
SET_VPP = const(0x33)# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1106(framebuf.FrameBuffer):def __init__(self, width, height, external_vcc):self.width = widthself.height = heightself.external_vcc = external_vccself.pages = self.height // 8self.buffer = bytearray(self.pages * self.width)super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)self.init_display()def init_display(self):for cmd in (SET_DISP_OFF, # display offSET_DISP_CLK_DIV, 0x80, # timing and driving schemeSET_MUX_RATIO, 0x3f, #0xa8SET_DISP_OFFSET, 0x00, #0xd3SET_DISP_START_LINE | 0x00, #start lineSET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,SET_MEM_ADDR, 0x00, # address settingSET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0SET_COM_PIN_CFG, 0x12,SET_CONTRACT_CTRL, 0xcf,SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,SET_VCOM_DESEL, 0x40, # 0.83*VccSET_ENTIRE_ON, # output follows RAM contentsSET_NORM_INV,SET_DISP_ON): # onself.write_cmd(cmd)self.fill(0)self.show()def poweroff(self):self.write_cmd(SET_DISP_OFF)def poweron(self):self.write_cmd(SET_DISP_ON)def contrast(self, contrast):self.write_cmd(SET_CONTRAST)self.write_cmd(contrast)def invert(self, invert):self.write_cmd(SET_NORM_INV | (invert & 1))def show(self):for i in range(0, 8):self.write_cmd(SET_PAGE_ADDR1 + i)self.write_cmd(SET_COL_ADDR_L + 0) # offset 2 pixels for 128x64 panelself.write_cmd(SET_COL_ADDR_H + 0)self.write_data(self.buffer[i*128:(i+1)*128]) # send one page display dataclass SSD1106_I2C(SSD1106):def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):self.i2c = i2cself.addr = addrself.temp = bytearray(2)super().__init__(width, height, external_vcc)def write_cmd(self, cmd):self.temp[0] = 0x80 # Co=1, D/C#=0self.temp[1] = cmdself.i2c.writeto(self.addr, self.temp)def write_data(self, buf):# self.temp[0] = self.addr << 1# self.temp[1] = 0x40 # Co=0, D/C#=1# self.i2c.start()# self.i2c.write(self.temp)# self.i2c.write(buf)# self.i2c.stop()tmp = bytearray([0x40]) # Co=0, D/C#=1self.i2c.writeto(self.addr, tmp+buf)class SSD1106_SPI(SSD1106):def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):self.rate = 10 * 1024 * 1024dc.init(dc.OUT, value=0)res.init(res.OUT, value=0)cs.init(cs.OUT, value=1)self.spi = spiself.dc = dcself.res = resself.cs = csimport timeself.res(1)time.sleep_ms(1)self.res(0)time.sleep_ms(10)self.res(1)super().__init__(width, height, external_vcc)def write_cmd(self, cmd):self.spi.init(baudrate=self.rate, polarity=0, phase=0)self.cs(1)self.dc(0)self.cs(0)self.spi.write(bytearray([cmd]))self.cs(1)def write_data(self, buf):self.spi.init(baudrate=self.rate, polarity=0, phase=0)self.cs(1)self.dc(1)self.cs(0)self.spi.write(buf)self.cs(1)
(4)编写demo1.py代码
import random
import time
from machine import I2C, Pin
import ufont
import ssd1106i2c = I2C(scl=Pin(22), sda=Pin(23))
display = ssd1106.SSD1106_I2C(128, 64, i2c)font = ufont.BMFont("unifont-14-12917-16.v3.bmf")font.text(display, "你好", 0, 0, show=True)
font.text(display, "!@#¥", 0, 16, show=True)
font.text(display, "abcd", 0, 32, show=True)
font.text(display, "hello 北京", 0, 48, show=True)
掌控板中的文件如图1所示,其中boot.py是烧录esp32固件后自带的文件。
3. 运行结果
运行结果如图2所示。