/ adafruit_ssd1306.py
adafruit_ssd1306.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2017 Michael McWethy
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_ssd1306`
 24  ====================================================
 25  
 26  MicroPython SSD1306 OLED driver, I2C and SPI interfaces
 27  
 28  * Author(s): Tony DiCola, Michael McWethy
 29  """
 30  
 31  import time
 32  
 33  from micropython import const
 34  from adafruit_bus_device import i2c_device, spi_device
 35  
 36  try:
 37      import framebuf
 38  except ImportError:
 39      import adafruit_framebuf as framebuf
 40  
 41  __version__ = "0.0.0-auto.0"
 42  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SSD1306.git"
 43  
 44  # pylint: disable-msg=bad-whitespace
 45  # register definitions
 46  SET_CONTRAST = const(0x81)
 47  SET_ENTIRE_ON = const(0xA4)
 48  SET_NORM_INV = const(0xA6)
 49  SET_DISP = const(0xAE)
 50  SET_MEM_ADDR = const(0x20)
 51  SET_COL_ADDR = const(0x21)
 52  SET_PAGE_ADDR = const(0x22)
 53  SET_DISP_START_LINE = const(0x40)
 54  SET_SEG_REMAP = const(0xA0)
 55  SET_MUX_RATIO = const(0xA8)
 56  SET_COM_OUT_DIR = const(0xC0)
 57  SET_DISP_OFFSET = const(0xD3)
 58  SET_COM_PIN_CFG = const(0xDA)
 59  SET_DISP_CLK_DIV = const(0xD5)
 60  SET_PRECHARGE = const(0xD9)
 61  SET_VCOM_DESEL = const(0xDB)
 62  SET_CHARGE_PUMP = const(0x8D)
 63  # pylint: enable-msg=bad-whitespace
 64  
 65  
 66  class _SSD1306(framebuf.FrameBuffer):
 67      """Base class for SSD1306 display driver"""
 68  
 69      # pylint: disable-msg=too-many-arguments
 70      def __init__(self, buffer, width, height, *, external_vcc, reset):
 71          super().__init__(buffer, width, height)
 72          self.width = width
 73          self.height = height
 74          self.external_vcc = external_vcc
 75          # reset may be None if not needed
 76          self.reset_pin = reset
 77          if self.reset_pin:
 78              self.reset_pin.switch_to_output(value=0)
 79          self.pages = self.height // 8
 80          # Note the subclass must initialize self.framebuf to a framebuffer.
 81          # This is necessary because the underlying data buffer is different
 82          # between I2C and SPI implementations (I2C needs an extra byte).
 83          self._power = False
 84          self.poweron()
 85          self.init_display()
 86  
 87      @property
 88      def power(self):
 89          """True if the display is currently powered on, otherwise False"""
 90          return self._power
 91  
 92      def init_display(self):
 93          """Base class to initialize display"""
 94          for cmd in (
 95              SET_DISP | 0x00,  # off
 96              # address setting
 97              SET_MEM_ADDR,
 98              0x00,  # horizontal
 99              # resolution and layout
100              SET_DISP_START_LINE | 0x00,
101              SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
102              SET_MUX_RATIO,
103              self.height - 1,
104              SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
105              SET_DISP_OFFSET,
106              0x00,
107              SET_COM_PIN_CFG,
108              0x02 if self.height == 32 or self.height == 16 else 0x12,
109              # timing and driving scheme
110              SET_DISP_CLK_DIV,
111              0x80,
112              SET_PRECHARGE,
113              0x22 if self.external_vcc else 0xF1,
114              SET_VCOM_DESEL,
115              0x30,  # 0.83*Vcc
116              # display
117              SET_CONTRAST,
118              0xFF,  # maximum
119              SET_ENTIRE_ON,  # output follows RAM contents
120              SET_NORM_INV,  # not inverted
121              # charge pump
122              SET_CHARGE_PUMP,
123              0x10 if self.external_vcc else 0x14,
124              SET_DISP | 0x01,
125          ):  # on
126              self.write_cmd(cmd)
127          if self.width == 72:
128              self.write_cmd(0xAD)
129              self.write_cmd(0x30)
130          self.fill(0)
131          self.show()
132  
133      def poweroff(self):
134          """Turn off the display (nothing visible)"""
135          self.write_cmd(SET_DISP | 0x00)
136          self._power = False
137  
138      def contrast(self, contrast):
139          """Adjust the contrast"""
140          self.write_cmd(SET_CONTRAST)
141          self.write_cmd(contrast)
142  
143      def invert(self, invert):
144          """Invert all pixels on the display"""
145          self.write_cmd(SET_NORM_INV | (invert & 1))
146  
147      def write_framebuf(self):
148          """Derived class must implement this"""
149          raise NotImplementedError
150  
151      def write_cmd(self, cmd):
152          """Derived class must implement this"""
153          raise NotImplementedError
154  
155      def poweron(self):
156          "Reset device and turn on the display."
157          if self.reset_pin:
158              self.reset_pin.value = 1
159              time.sleep(0.001)
160              self.reset_pin.value = 0
161              time.sleep(0.010)
162              self.reset_pin.value = 1
163              time.sleep(0.010)
164          self.write_cmd(SET_DISP | 0x01)
165          self._power = True
166  
167      def show(self):
168          """Update the display"""
169          xpos0 = 0
170          xpos1 = self.width - 1
171          if self.width == 64:
172              # displays with width of 64 pixels are shifted by 32
173              xpos0 += 32
174              xpos1 += 32
175          if self.width == 72:
176              # displays with width of 72 pixels are shifted by 28
177              xpos0 += 28
178              xpos1 += 28
179          self.write_cmd(SET_COL_ADDR)
180          self.write_cmd(xpos0)
181          self.write_cmd(xpos1)
182          self.write_cmd(SET_PAGE_ADDR)
183          self.write_cmd(0)
184          self.write_cmd(self.pages - 1)
185          self.write_framebuf()
186  
187  
188  class SSD1306_I2C(_SSD1306):
189      """
190      I2C class for SSD1306
191  
192      :param width: the width of the physical screen in pixels,
193      :param height: the height of the physical screen in pixels,
194      :param i2c: the I2C peripheral to use,
195      :param addr: the 8-bit bus address of the device,
196      :param external_vcc: whether external high-voltage source is connected.
197      :param reset: if needed, DigitalInOut designating reset pin
198      """
199  
200      def __init__(
201          self, width, height, i2c, *, addr=0x3C, external_vcc=False, reset=None
202      ):
203          self.i2c_device = i2c_device.I2CDevice(i2c, addr)
204          self.addr = addr
205          self.temp = bytearray(2)
206          # Add an extra byte to the data buffer to hold an I2C data/command byte
207          # to use hardware-compatible I2C transactions.  A memoryview of the
208          # buffer is used to mask this byte from the framebuffer operations
209          # (without a major memory hit as memoryview doesn't copy to a separate
210          # buffer).
211          self.buffer = bytearray(((height // 8) * width) + 1)
212          self.buffer[0] = 0x40  # Set first byte of data buffer to Co=0, D/C=1
213          super().__init__(
214              memoryview(self.buffer)[1:],
215              width,
216              height,
217              external_vcc=external_vcc,
218              reset=reset,
219          )
220  
221      def write_cmd(self, cmd):
222          """Send a command to the SPI device"""
223          self.temp[0] = 0x80  # Co=1, D/C#=0
224          self.temp[1] = cmd
225          with self.i2c_device:
226              self.i2c_device.write(self.temp)
227  
228      def write_framebuf(self):
229          """Blast out the frame buffer using a single I2C transaction to support
230          hardware I2C interfaces."""
231          with self.i2c_device:
232              self.i2c_device.write(self.buffer)
233  
234  
235  # pylint: disable-msg=too-many-arguments
236  class SSD1306_SPI(_SSD1306):
237      """
238      SPI class for SSD1306
239  
240      :param width: the width of the physical screen in pixels,
241      :param height: the height of the physical screen in pixels,
242      :param spi: the SPI peripheral to use,
243      :param dc: the data/command pin to use (often labeled "D/C"),
244      :param reset: the reset pin to use,
245      :param cs: the chip-select pin to use (sometimes labeled "SS").
246      """
247  
248      # pylint: disable=no-member
249      # Disable should be reconsidered when refactor can be tested.
250      def __init__(
251          self,
252          width,
253          height,
254          spi,
255          dc,
256          reset,
257          cs,
258          *,
259          external_vcc=False,
260          baudrate=8000000,
261          polarity=0,
262          phase=0
263      ):
264          self.rate = 10 * 1024 * 1024
265          dc.switch_to_output(value=0)
266          self.spi_device = spi_device.SPIDevice(
267              spi, cs, baudrate=baudrate, polarity=polarity, phase=phase
268          )
269          self.dc_pin = dc
270          self.buffer = bytearray((height // 8) * width)
271          super().__init__(
272              memoryview(self.buffer),
273              width,
274              height,
275              external_vcc=external_vcc,
276              reset=reset,
277          )
278  
279      def write_cmd(self, cmd):
280          """Send a command to the SPI device"""
281          self.dc_pin.value = 0
282          with self.spi_device as spi:
283              spi.write(bytearray([cmd]))
284  
285      def write_framebuf(self):
286          """write to the frame buffer via SPI"""
287          self.dc_pin.value = 1
288          with self.spi_device as spi:
289              spi.write(self.buffer)