From 363eaa46ef5ac7567d03e4175b07b39d7e8eaf3e Mon Sep 17 00:00:00 2001 From: Correl Date: Wed, 17 May 2023 18:10:49 -0400 Subject: [PATCH] Initial commit --- .gitignore | 1 + main.py | 29 ++++++++++ mcp4.py | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ssd1306.py | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 mcp4.py create mode 100644 ssd1306.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e38da20 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +settings.json diff --git a/main.py b/main.py new file mode 100644 index 0000000..bef42ec --- /dev/null +++ b/main.py @@ -0,0 +1,29 @@ +from machine import Pin, SPI, SoftI2C +import framebuf +import ssd1306 +import mcp4 + + +i2c = SoftI2C(sda=Pin(2), scl=Pin(16)) +oled_width = 128 +oled_height = 32 +oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c) +buf = bytearray((oled_height // 8) * oled_width) +fbuf = framebuf.FrameBuffer1(buf, oled_width, oled_height) + + +spi = SPI(1) +cs = Pin(15, mode=Pin.OUT, value=1) +pot = mcp4.MCP4(spi, cs) + + +def update(): + PW0 = pot.read(0) + PW1 = pot.read(1) + oled.fill(0) + oled.text(f"PW0: {PW0}", 0, 0) + oled.text(f"PW1: {PW1}", 0, 10) + oled.show() + + +update() diff --git a/mcp4.py b/mcp4.py new file mode 100644 index 0000000..521918d --- /dev/null +++ b/mcp4.py @@ -0,0 +1,164 @@ +"""MicroPython MCP413X/415X/423X/425X SPI driver + +Driver for the 7/8-Bit Single/Dual SPI Digital POT with Volatile Memory from +Microchip. (https://ww1.microchip.com/downloads/en/DeviceDoc/22060b.pdf) + +Copyright 2023 Correl Roush + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +from machine import Pin, SPI + + +class NetworkControl: + def __init__(self, hw=True, a=True, w=True, b=True) -> None: + self.forced_hardware_shutdown = hw + self.terminal_a_connected = a + self.wiper_connected = w + self.terminal_b_connected = b + + @staticmethod + def from_bin(data: int) -> "NetworkControl": + return NetworkControl( + hw=bool(data & 0b1000), + a=bool(data & 0b0100), + w=bool(data & 0b0010), + b=bool(data & 0b0001), + ) + + def __repr__(self): + return " None: + self.resistor_0 = resistor_0 + self.resistor_1 = resistor_1 + + @staticmethod + def from_bin(data: int) -> "TerminalControl": + return TerminalControl( + resistor_0=NetworkControl.from_bin(data), + resistor_1=NetworkControl.from_bin(data >> 4), + ) + + def __repr__(self): + return "".format( + r0=self.resistor_0, + r1=self.resistor_1, + ) + + +class MCP4: + """MicroPython MCP413X/415X/423X/425X SPI driver""" + + ADDRESS_WIPER_0 = 0x00 + ADDRESS_WIPER_1 = 0x01 + ADDRESS_TCON = 0x04 + ADDRESS_STATUS = 0x05 + + CMD_WRITE = 0b00 + CMD_INCREMENT = 0b01 + CMD_DECREMENT = 0b10 + CMD_READ = 0b11 + + def __init__(self, spi: SPI, cs: Pin) -> None: + self.spi = spi + self.cs = cs + + def _bytes(self, address: int, command: int, data: int = 0x0) -> bytearray: + """Translate an address, command, and data into bytes to send. + + - Address is a 4-bit memory address. + - Command is a 2-bit command code. + - Data is 2 bits for increment and decrement operations (ignored), and + 10 bits for read and write operations. + + """ + command_byte = address << 4 & 0b11110000 | command << 2 & 0b00001100 + if command in (0b00, 0b11): + # Include data byte for 10 total bits of data + return bytearray([command_byte | (0b11 & data >> 8), data & 0xFF]) + return bytearray([command_byte]) + + def _write(self, data: bytearray) -> bytearray: + """Write data to the SPI interface, returning its output.""" + output = bytearray(len(data)) + self.spi.write_readinto(data, output) + return output + + def do(self, address: int, command: int, data: int = 0x0) -> int: + """Execute a command on the MCP4, returning its integer result.""" + self.cs(0) + output = self._write(self._bytes(address, command, data)) + self.cs(1) + + OK = 0b11111110 + if OK != output[0] & OK: + self.cs(0) + raise ValueError("Invalid command") + result = output[0] & 0b01 + if len(output) > 1: + result <<= 8 + result |= output[1] + return result + + def increment(self, wiper: int = 0) -> int: + """Increment a wiper.""" + return self.do( + address=self.ADDRESS_WIPER_1 if wiper == 1 else self.ADDRESS_WIPER_0, + command=self.CMD_INCREMENT, + ) + + def decrement(self, wiper: int = 0) -> int: + """Decrement a wiper.""" + return self.do( + address=self.ADDRESS_WIPER_1 if wiper == 1 else self.ADDRESS_WIPER_0, + command=self.CMD_DECREMENT, + ) + + def read(self, wiper: int = 0) -> int: + """Read the current value of a wiper.""" + return self.do( + address=self.ADDRESS_WIPER_1 if wiper == 1 else self.ADDRESS_WIPER_0, + command=self.CMD_READ, + ) + + def write(self, wiper: int = 0, data: int = 0x00) -> int: + """Set a value for a wiper.""" + return self.do( + address=self.ADDRESS_WIPER_1 if wiper == 1 else self.ADDRESS_WIPER_0, + command=self.CMD_WRITE, + data=data, + ) + + def is_shutdown(self) -> bool: + status = self.do(address=self.ADDRESS_STATUS, command=self.CMD_READ) + return status & 0b10 == 0b10 + + @property + def control(self) -> TerminalControl: + data = self.do(address=self.ADDRESS_TCON, command=self.CMD_READ) + return TerminalControl.from_bin(data) diff --git a/ssd1306.py b/ssd1306.py new file mode 100644 index 0000000..39ec081 --- /dev/null +++ b/ssd1306.py @@ -0,0 +1,167 @@ +#MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruit + +import time +import framebuf + +# 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 + # Note the subclass must initialize self.framebuf to a framebuffer. + # This is necessary because the underlying data buffer is different + # between I2C and SPI implementations (I2C needs an extra byte). + 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_framebuf() + + 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) + + +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) + # Add an extra byte to the data buffer to hold an I2C data/command byte + # to use hardware-compatible I2C transactions. A memoryview of the + # buffer is used to mask this byte from the framebuffer operations + # (without a major memory hit as memoryview doesn't copy to a separate + # buffer). + self.buffer = bytearray(((height // 8) * width) + 1) + self.buffer[0] = 0x40 # Set first byte of data buffer to Co=0, D/C=1 + self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height) + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.temp[0] = 0x80 # Co=1, D/C#=0 + self.temp[1] = cmd + self.i2c.writeto(self.addr, self.temp) + + def write_framebuf(self): + # Blast out the frame buffer using a single I2C transaction to support + # hardware I2C interfaces. + self.i2c.writeto(self.addr, self.buffer) + + 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 + self.buffer = bytearray((height // 8) * width) + self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height) + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs.high() + self.dc.low() + self.cs.low() + self.spi.write(bytearray([cmd])) + self.cs.high() + + def write_framebuf(self): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs.high() + self.dc.high() + self.cs.low() + self.spi.write(self.buffer) + self.cs.high() + + def poweron(self): + self.res.high() + time.sleep_ms(1) + self.res.low() + time.sleep_ms(10) + self.res.high() \ No newline at end of file