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

文章讲述了在使用Micropython驱动SSD1306OLED屏时遇到的警告,指出硬件I2C比软件I2C更节省资源。作者分析了驱动代码,发现驱动使用了软件I2C的特定方法,导致硬件I2C无法正常工作。通过修改驱动中的`write_data`方法,实现了硬件I2C对SSD1306的正确驱动,消除了警告并提高了性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、问题的产生

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

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

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

我们知道硬件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的文档:

https://docs.micropython.org/en/latest/library/machine.I2C.html#machine-i2c

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),见下图:

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

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时间,程序运行也流畅了不少。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值