目录

使用ESP32micropython的硬件I2C总线驱动SSD1306

使用ESP32(micropython)的硬件I2C总线驱动SSD1306

一、问题的产生

不知道大家用micropython玩SSD1306时,有没有留意到下面一行警告:

Warning: I2C(-1, …) is deprecated, use SoftI2C(…) instead

大概意思就是你在使用I2C总线,提示你应该用SoftI2C类比较好。

https://i-blog.csdnimg.cn/blog_migrate/8456cf64c17f2ce9084bbb76d27ca2e3.jpeg

我们知道硬件I2C和软件I2C的区别在于,软件I2C是通过软件编程使CPU拉高拉低SDA和SCL引脚,模拟出I2C总线的;而硬件I2C则是使用ESP32内部的I2C硬件驱动器实现总线的读写。

很明显的,硬件I2C比软件I2C更加节约CPU资源,因为CPU不用去频繁操作SDA和SCL引脚了。如果你操作屏幕频繁,硬件I2C将是你最佳的选择。

ESP32明明就有硬件I2C总线,为什么会出现这个提示呢?

二、解决方案

1.现有的驱动版本

问题的根源肯定在SSD1306的驱动上面。网上传的最多的SSD1306的micropython驱动版本应该是这个:

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
from micropython import const
import time
import framebuf
import sys

currentBoard=""
if(sys.platform=="esp8266"):
currentBoard="esp8266"
elif(sys.platform=="esp32"):
currentBoard="esp32"
elif(sys.platform=="pyboard"):
currentBoard="pyboard"
import pyb

# register definitions

SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xa4)
SET_NORM_INV = const(0xa6)
SET_DISP = const(0xae)
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)
class SSD1306:
def **init**(self, width, height, external_vcc):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
self.buffer = bytearray(self.pages * self.width)
self.framebuf = framebuf.FrameBuffer(self.buffer, self.width, self.height, framebuf.MVLSB)
self.poweron()
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00, # off # address setting
SET_MEM_ADDR, 0x00, # horizontal # resolution and layout
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12, # timing and driving scheme
SET_DISP_CLK_DIV, 0x80,
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
SET_VCOM_DESEL, 0x30, # 0.83*Vcc # display
SET_CONTRAST, 0xff, # maximum
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted # charge pump
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01): # on
self.write_cmd(cmd)
self.fill(0)
self.show()
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
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):
x0 = 0
x1 = self.width - 1
if self.width == 64: # displays with width of 64 pixels are shifted by 32
x0 += 32
x1 += 32
self.write_cmd(SET_COL_ADDR)
self.write_cmd(x0)
self.write_cmd(x1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
self.write_data(self.buffer)
def fill(self, col):
self.framebuf.fill(col)
def pixel(self, x, y, col):
self.framebuf.pixel(x, y, col)
def scroll(self, dx, dy):
self.framebuf.scroll(dx, dy)
def text(self, string, x, y, col=1):
self.framebuf.text(string, x, y, col)
def hline(self, x, y, w, col):
self.framebuf.hline(x, y, w, col)
def vline(self, x, y, h, col):
self.framebuf.vline(x, y, h, col)
def line(self, x1, y1, x2, y2, col):
self.framebuf.line(x1, y1, x2, y2, col)
def rect(self, x, y, w, h, col):
self.framebuf.rect(x, y, w, h, col)
def fill_rect(self, x, y, w, h, col):
self.framebuf.fill_rect(x, y, w, h, col)
def blit(self, fbuf, x, y):
self.framebuf.blit(fbuf, x, y)
def test(self,value):
self.buffer[0]=value

class SSD1306_I2C(SSD1306):
def **init**(self, width, height, i2c, addr=0x3c, external_vcc=False):
self.i2c = i2c
self.addr = addr
self.temp = bytearray(2)
super().**init**(width, height, external_vcc)
def write_cmd(self, cmd):
self.temp[0] = 0x80 # Co=1, D/C#=0
self.temp[1] = cmd
#IF SYS :
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.i2c.writeto(self.addr, self.temp)
elif currentBoard=="pyboard":
self.i2c.send(self.temp,self.addr)
#ELSE:

def write_data(self, buf):
self.temp[0] = self.addr << 1
self.temp[1] = 0x40 # Co=0, D/C#=1
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.i2c.start()
self.i2c.write(self.temp)
self.i2c.write(buf)
self.i2c.stop()
#self.i2c.writeto_mem(self.temp[1],self.temp[0],buf)
elif currentBoard=="pyboard":
#self.i2c.send(self.temp,self.addr)
#self.i2c.send(buf,self.addr)
self.i2c.mem_write(buf,self.addr,0x40)
def poweron(self):
pass

class SSD1306_SPI(SSD1306):
def **init**(self, width, height, spi, dc, res, cs, external_vcc=False):
self.rate = 10 _ 1024 _ 1024
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=0)
cs.init(cs.OUT, value=1)
self.spi = spi
self.dc = dc
self.res = res
self.cs = cs
super().**init**(width, height, external_vcc)
def write_cmd(self, cmd):
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
elif currentBoard=="pyboard":
self.spi.init(mode = pyb.SPI.MASTER,baudrate=self.rate, polarity=0, phase=0)
self.cs.high()
self.dc.low()
self.cs.low()
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.spi.write(bytearray([cmd]))
elif currentBoard=="pyboard":
self.spi.send(bytearray([cmd]))
self.cs.high()
def write_data(self, buf):
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
elif currentBoard=="pyboard":
self.spi.init(mode = pyb.SPI.MASTER,baudrate=self.rate, polarity=0, phase=0)
self.cs.high()
self.dc.high()
self.cs.low()
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.spi.write(buf)
elif currentBoard=="pyboard":
self.spi.send(buf)
self.cs.high()
def poweron(self):
self.res.high()
time.sleep_ms(1)
self.res.low()
time.sleep_ms(10)
self.res.high()

使用的方法是这样的:

from machine import Pin
i2c=I2C(sda=Pin(22), scl=Pin(21), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

然后就可以 oled.text(…….)开始操作屏幕显示了。

注意:这里 i2c=I2C(sda=Pin(22), scl=Pin(21), freq=400000)没有指定 I2C 通道号,程序就会默认你使用 SoftI2C 来驱动。具体的说明见 micropython 的文档:

2.尝试使用硬件 I2C 来驱动

既然想换成硬件 I2C,那么就指定 I2C 的通道号不就行了?这样:

from machine import Pin #指定使用一个硬件 I2C 通道号,0 或者 1,见 micropython 的文档
i2c=I2C(0,sda=Pin(22), scl=Pin(21), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

但是问题来了,出现运行错误:

File “ssd1306.py”, line 137, in write_data

OSError: I2C operation not supported

看了下,问题出在 ssd1306.py 驱动的 137 行,代码 self.i2c.start()

处。查阅了相关资料后发现原因是,硬件 I2C 模式并不支持软件 I2C 特有的模拟总线操作方法,比如 start()、stop()、Write()等,硬件 I2C 只能用 writeto()等方法。

既然如此,驱动的作者为什么会使用软件 I2C 的方法来写驱动呢?不得而知。

3.原因分析

既然铁了心要用硬件 I2C 驱动屏幕,那就研究一下这个驱动。

仔细看在 class SSD1306_I2C(SSD1306)部分,其实只有 write_data(self, buf)方法里面使用了软件 I2C 特有的 start()、stop()、Write()方法,write_cmd(self, cmd)并没有(用的 writeto)。

那我们可以尝试把 write_data(self, buf)重写一下。

首先仔细分析下原作者这么写的原因。查阅 SSD1306 的手册,我们发现,往 SSD1306 写数据时,需要先写总线地址(Slave address),然后写控制字(control byte),然后才是数据(data),见下图:

https://i-blog.csdnimg.cn/blog_migrate/5f0899ea4ac6242f12b9302a412508ba.png

对应的我们看看原来的驱动(我加上注释):

def write_data(self, buf):
self.temp[0] = self.addr << 1#slave address 地址,十进制 120
self.temp[1] = 0x40 # Co=0, D/C#=1#控制字 control byte
global currentBoard
if currentBoard=="esp8266" or currentBoard=="esp32":
self.i2c.start()
self.i2c.write(self.temp)#写地址,写控制字
self.i2c.write(buf)#写数据
self.i2c.stop()
#self.i2c.writeto_mem(self.temp[1],self.temp[0],buf)

硬件 I2C 的 writeto 是这么用的:

self.i2c.writeto(slave address,data)

调用这个方法时,总线自动先写地址,然后接着写数据。

作者这么写的原因我想我也应该猜到了,那就是 slave address 和 data 中间还有一个控制字,作者没有找到更好的方法如何把这个控制字写进去。如果调用两次 writeto 方法,将会出现两次写 slave address,自然是不行了,所以干脆偷了个懒,使用软 I2C,顺序写 slave address、control byte 和 data。

4.解决办法

改造 write_data 方法,直接给出代码:(当然我忽略了 pyboard 部分,不过这不重要,因为我的 CPU 的 ESP32)

 def write_data(self,buf):
temp1=bytearray(1)
temp1[0]=0x40
temp1.extend(buf)
self.i2c.writeto(self.addr,temp1)

这里我们使用 bytearray 的 extend 方法,把控制字 0x40 插入到 buf(也就是要写到屏幕的数据)最前端,让 writeto 方法把控制字也当成一个数据来写,就实现了顺序写入 slave address、control byte 和 data。

把这段代码替换掉原驱动的 def write_data(self,buf)部分,然后使用驱动时就可以使用硬件 I2C 来驱动屏幕了:

from machine import Pin #指定使用一个硬件 I2C 通道号,0 或者 1,见 micropython 的文档
i2c=I2C(0,sda=Pin(22), scl=Pin(21), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

好了,烦人的 Warning: I2C(-1, …) is deprecated, use SoftI2C(…) instead 警告也消失了,屏幕刷新不会占用那么多 CPU 时间,程序运行也流畅了不少。