树莓派使用 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 文档。

好了,下面开始本文章的正文。创建 IICSPI 驱动的代码这里就不写出来了,请自己查看基础篇。这里我们默认都以 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 就可以知道怎么用的。但很多时候我们想要显示一张图片,这该怎么实现呢?同样也很简单。 基础篇我们分析了源码和实现,知道了屏幕显示的内容就是 pillowImage 实例。既然是 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

再看看其它同类文章吧!

有想说的评论一句吧!