Initial commit

This commit is contained in:
Correl Roush 2023-05-17 18:10:49 -04:00
commit 363eaa46ef
4 changed files with 361 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
settings.json

29
main.py Normal file
View file

@ -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()

164
mcp4.py Normal file
View file

@ -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 "<Network HW={hw} A={a} W={w} B={b}".format(
hw=self.forced_hardware_shutdown,
a=self.terminal_a_connected,
w=self.wiper_connected,
b=self.terminal_b_connected,
)
class TerminalControl:
def __init__(self, resistor_0: NetworkControl, resistor_1: NetworkControl) -> 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 "<Terminals 0:{r0} 1:{r1}>".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)

167
ssd1306.py Normal file
View file

@ -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()