树莓派使用 Python 驱动 SSD1306 中文字体、图片动画高级教程(IIC/SPI 通信)
本文是树莓派上使用 python 驱动 SSD1306 OLED 屏幕的高级篇,详细讲解了如何显示中文字体,显示图片,制作动画等高级用法,让你在树莓派上玩转屏幕,做出任何你想要的效果。
阅读本文的用户,请先阅读 树莓派上使用 python 驱动 SSD1306 OLED 屏幕 入门篇,安装好驱动和了解基本的用法,先把屏幕驱动起来,再来看高级用法。
相关链接: 【ssd1306 数据手册下载】 【luma-oled 英文文档】 【luma-oled Github】
写在前面
凡是在代码里面涉及到中文的,都需要注意编码
和字符集
。在基础篇中,我为了考虑到 Python 初学者和不熟悉硬件编程的同学,在每一行代码中都加入了中文注释。
然而很多小伙伴是在 Windows 下编程的,或者没有将自己的树莓派系统字符集设置为 zh_CN.UTF-8
的,结果就是复制粘贴我的代码,连中文注释一起复制了,粘贴到树莓派上,编辑器中中文全部都乱码了。这时执行肯定会报错的。还有一些小伙伴不熟悉 Linux
命令行,不会用命令行编辑器,比如 vim
, nano
等,跑过来问我
怎么将代码放到树莓派上执行。更有甚者,只会使用 IDE
,比如 pycharm
。脱离了图形环境,连执行 python ssd1306-iic.py
都不会了。
对于此类,我建议多学学,多问问搜索引擎,多看看文档,毕竟我不是做公益的,更不是培训班。有人说你文档写的不清楚,这步骤关系到你的东西,你就得写出来。按照这个逻辑,我写这篇文章,我要先写一部 Linux
系统基础入门的书,然后才能写本文章了。不是我不愿意教,如果每个人都这样手把手教,那我要不要工作了。写一篇文章很累的,还要罗里吧嗦的基础知识一直重复的讲,没意思。何况我也没收学费啊,如果我像培训班一样收学费,那我很乐意教。
好了,废话结束,就是吐槽一下。下面先讲解上面的问题,再开始本文章。
先设置好字符集
首先确保你系统的字符集是 zh_CN.UTF-8。raspbian 同学可以使用 sudo raspi-config
,然后选择 4 Localisation Options
,进入菜单再选择 I1 Change Locale
,然后使用方向键选择 zh_CN.UTF-8
,使用空格可以选中。按回车后,再选择 zh_CN.UTF-8
作为系统默认的字符集就行了。记得重启一下。
对于像 volumio
这些第三方衍生的系统,默认是不带 raspi-config 的。可以使用 sudo apt-get install raspi-config
安装此软件,然后就可以像上面一样使用了。
设置好后,你可以在命令行或者编辑器中输入中文,如果能正常显示和编辑,那就说明支持中文了。如果显示乱码或者编辑时很诡异,那就说明设置输错了,重新设置一遍吧。
Python2.7 和 Python3
如果不额外声明,我默认是使用 Python2.7 的,Python3 也是支持的。使用 Python3 的同学,我默认你已经知道了它们的区别。
在 Python2.7 中,pillow
库它叫 PIL,在 Python3 中,它叫 pillow。详细的这里就不介绍了,请自己查看 api 文档。
好了,下面开始本文章的正文。创建 IIC
和 SPI
驱动的代码这里就不写出来了,请自己查看基础篇。这里我们默认都以 SPI
协议。
显示中文和指定中文字体
在基础篇中介绍了屏幕的 hello world!
,其中涉及到显示文本。但只能显示英文和符号,如果你在 draw.text()
中写中文,屏幕上是会显示乱码的。为什么?因为默认的字体是英文的,没有中文编码,所以显示不出来。那要如何显示中文呢?很简单,使用中文字体就好了啊。
这里我使用了阿里巴巴的免费字体,大家请自行下载字体,将它放到树莓派上。或者使用任何一款支持中文的 TTF
字体都可以。
# -*- coding: UTF-8 -*-
from luma.core.interface.serial import i2c, spi
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw, ImageFont
import time
serial = spi(device=0, port=0)
device = ssd1306(serial)
font = ImageFont.truetype("GenJyuuGothic-P-Regular.ttf", 16)
with canvas(device) as draw:
draw.rectangle(device.bounding_box, outline="white", fill="black")
draw.text((8, 20), "提莫的神秘商店".decode("UTF-8"), fill="white", font=font)
while (True):
pass
代码非常简单,注意开头我们使用了 # -*- coding: UTF-8 -*-
,Python2.7 默认不是 utf-8 编码,所以我们指定编码让它支持中文。接下来我们使用了 ImageFont
来加载第三方的字体。第一个参数是字体文件的路径
,请根据自己的实际情况做相应的修改,不要傻傻的复制粘贴,路径都不对,又跑来问我报错了。
第二个参数是字体的大小,这里我们就设置成 16,如果你想要更大或更小,自己调整就行了。不要又傻傻的问我怎么调整字体大小。更多参数和用法,自己查看 api 文档。文档前面已经给出链接了,不要又傻傻的问我文档在哪。还有,文档是英文的,不要问我有没有中文的文档。我现在是怕了。
下面在显示文字的时候,注意我们使用了 "提莫的神秘商店".decode("UTF-8")
,千万不要忘记后面的 decode
,否则还是会乱码的。在 Python3 中默认已经是 utf-8 编码了。最后我们指定字体,使用 font
参数把我们刚才加载的字体给它显示就行了。
如果你想要不同的文字使用不同的字体和大小怎么办?很简单,创建多个 font
就行了,在 draw.text()
的时候分别使用不同的字体就可以达到效果了,同学们要学会举一反三。
显示图片
基础篇中我们介绍了基本的绘图,点线面等。这些都非常简单,查看 api 就可以知道怎么用的。但很多时候我们想要显示一张图片,这该怎么实现呢?同样也很简单。
基础篇我们分析了源码和实现,知道了屏幕显示的内容就是 pillow 的 Image 实例。既然是 Image
,那我要显示图片就很简单了,我使用 pillow 提供的 Image 类去加载一张图片,然后给驱动显示不就完了。
这里我们在深入的看一下源码。
# https://github.com/rm-hull/luma.core/blob/master/luma/core/render.py#L22
def __init__(self, device, background=None, dither=False):
self.draw = None
if background is None:
self.image = Image.new("RGB" if dither else device.mode, device.size)
else:
assert(background.size == device.size)
self.image = background.copy()
self.device = device
self.dither = dither
我们可以看到 render()
是有一个参数叫 background
的,什么意思呢?字面意思,我们可以传一张图片作为背景。如果我们传了 background
参数,那么使用 background.copy()
的返回值作为画布。copy
也很好理解,看文档的作用是返回一份拷贝。所以最简单的要显示一张图片,我们传 background
参数,然后不要绘图就好了。当然你也可以在图片的基础上绘图。
下面我们使用另一种方法来显示图片。上面的方法简单,但是想要显示多张图片怎么办?我想要精细的控制我的画布怎么办?这个时候我们就要直接操作 Image 对象了。我们来看看 display
函数的实现:
# https://github.com/rm-hull/luma.oled/blob/master/luma/oled/device/__init__.py#L191
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the OLED
display.
:param image: Image to display.
:type image: :py:mod:`PIL.Image`
"""
assert(image.mode == self.mode)
assert(image.size == self.size)
image = self.preprocess(image)
self.command(
# Column start/end address
self._const.COLUMNADDR, self._colstart, self._colend - 1,
# Page start/end address
self._const.PAGEADDR, 0x00, self._pages - 1)
buf = bytearray(self._w * self._pages)
off = self._offsets
mask = self._mask
idx = 0
for pix in image.getdata():
if pix > 0:
buf[off[idx]] |= mask[idx]
idx += 1
self.data(list(buf))
同样也很简单,先看参数,只接受一个参数 image
,它就是 Image 的实例。它把 image
的像素拿出来,然后拼装成屏幕接受的格式,再发送给屏幕,这样屏幕显示的内容就和图片的内容一样了。所以你想要屏幕显示什么,你就把图片绘制成什么。
源码分析完了,下面是显示图片的代码:
# -*- coding: UTF-8 -*-
from luma.core.interface.serial import i2c, spi
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw
serial = spi(device=0, port=0)
device = ssd1306(serial)
im = Image.open("./demo.png").convert(device.mode)
device.display(im)
while (True):
pass
我们使用 Image.open
来加载一张图片,然后使用 convert(mode)
将图片转换为屏幕的格式。为什么要转换?因为我们的 PNG 图片是 RGBA 格式的,而我们的屏幕是单色的,所以需要转换。否则会报错,从上面的 display
源码分析中就能看到有这么两行:
assert(image.mode == self.mode)
assert(image.size == self.size)
注意,图片的路径需要替换成你自己的实际路径,并且,图片的格式是有要求的:分辨率一定是 128*64
。内容最好是黑白的,白色的部分会在屏幕上表现为点亮像素,黑色的会表现为熄灭像素。其余的颜色会按照算法自动转换为白色或者黑色。为了照顾新手,这里我把 demo.png
文件提供给大家下载,调试的时候使用我这张图片,熟悉后再自己作图吧。
图片下载地址:ssd1306-demo.png
最后记得调用 device.display(im)
把图片显示到屏幕上,否则也不会显示的。这样我们就完成了显示自定义图片的功能。它支持多种图片格式,大家自己发挥。但是 GIF
是不支持动画的,不要来问我为啥我显示一张 GIF
图片不会动。你要想会动也可以实现,下面我们会讲如何实现动画。
制作动画
接下来讲解如何实现动画,我们把前面的结合起来,做一个中文时钟,效果就是屏幕会实时刷新当前的时间。
要实现动画,就要知道动画的原理。上面我们能显示自定义的图片了,如果我们一直改变图片,每改变一次就显示一次,这样屏幕的像素会一直变,如果达到了每秒改变 60 次,那看起来是不是就会有动画的效果了?是的,这就是动画的原理。
# -*- coding: UTF-8 -*-
from luma.core.interface.serial import i2c, spi
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw, ImageFont
import time
serial = spi(device=0, port=0)
device = ssd1306(serial)
font = ImageFont.truetype("GenJyuuGothic-P-Regular.ttf", 16)
canvas = Image.new(device.mode, device.size)
draw = ImageDraw.Draw(canvas)
while (True):
draw.rectangle(device.bounding_box, outline="white", fill="black")
datestr = time.strftime("%Y年%m月%d日", time.localtime())
timestr = time.strftime("%H:%M:%S", time.localtime())
draw.text((5, 10), datestr.decode("UTF-8"), "white", font)
draw.text((30, 30), timestr, "white", font)
device.display(canvas)
注意,这次我们是创建了一张空白的图片,我们把它当做我们的画布,然后显示到屏幕上。我们接下来要做的就是一直改变这个画布的内容,每次改变内容后,都将它刷新到屏幕上。注意新建的图片大小和模式必须是和屏幕的一样。
图片对象本身提供的方法有限,只能提供像素操作,我们要显示文字的话,需要使用 ImageDraw 类提供的方法。我们使用 time
内置模块提供的方法,获取现在的时间,并且把它格式化成我们方便阅读的格式。
每次都是先把图片内容清空,然后再绘图。如果不清空的话,上一次的内容还在,效果是不对的,做动画一定要记得清空。
注意,我们是直接在 while(True)
里面绘图的,这样的话速率是不受控制的,可能会消耗大量的 CPU
,同学们可以自己使用 sleep
控制速率。我这里演示用的是 SPI
协议的,速率可以达到 2400 FPS
。