Add code for Astels' "Digital Circuits 6: EPROM Emulator" project guide
This commit is contained in:
parent
75f565ffb4
commit
60e38a7543
6 changed files with 685 additions and 0 deletions
22
EPROM_Emulator/LICENSE.md
Normal file
22
EPROM_Emulator/LICENSE.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018
|
||||
|
||||
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.
|
||||
3
EPROM_Emulator/README.md
Normal file
3
EPROM_Emulator/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# 2716 Eprom Emulator
|
||||
|
||||
This code goes along with the Digital Circuits 6: An EPROM Emulator learnign guide.
|
||||
105
EPROM_Emulator/debouncer.py
Normal file
105
EPROM_Emulator/debouncer.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dave Astels
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Debounce an input pin.
|
||||
"""
|
||||
|
||||
import time
|
||||
import digitalio
|
||||
|
||||
class Debouncer(object):
|
||||
"""Debounce an input pin"""
|
||||
|
||||
DEBOUNCED_STATE = 0x01
|
||||
UNSTABLE_STATE = 0x02
|
||||
CHANGED_STATE = 0x04
|
||||
|
||||
|
||||
def __init__(self, pin, mode=None, interval=0.010):
|
||||
"""Make am instance.
|
||||
:param int pin: the pin (from board) to debounce
|
||||
:param int mode: digitalio.Pull.UP or .DOWN (default is no pull up/down)
|
||||
:param int interval: bounce threshold in seconds (default is 0.010, i.e. 10 milliseconds)
|
||||
"""
|
||||
self.state = 0x00
|
||||
self.pin = digitalio.DigitalInOut(pin)
|
||||
self.pin.direction = digitalio.Direction.INPUT
|
||||
if mode != None:
|
||||
self.pin.pull = mode
|
||||
if self.pin.value:
|
||||
self.__set_state(Debouncer.DEBOUNCED_STATE | Debouncer.UNSTABLE_STATE)
|
||||
self.previous_time = 0
|
||||
if interval is None:
|
||||
self.interval = 0.010
|
||||
else:
|
||||
self.interval = interval
|
||||
|
||||
|
||||
def __set_state(self, bits):
|
||||
self.state |= bits
|
||||
|
||||
|
||||
def __unset_state(self, bits):
|
||||
self.state &= ~bits
|
||||
|
||||
|
||||
def __toggle_state(self, bits):
|
||||
self.state ^= bits
|
||||
|
||||
|
||||
def __get_state(self, bits):
|
||||
return (self.state & bits) != 0
|
||||
|
||||
|
||||
def update(self):
|
||||
"""Update the debouncer state. Must be called before using any of the properties below"""
|
||||
self.__unset_state(Debouncer.CHANGED_STATE)
|
||||
current_state = self.pin.value
|
||||
if current_state != self.__get_state(Debouncer.UNSTABLE_STATE):
|
||||
self.previous_time = time.monotonic()
|
||||
self.__toggle_state(Debouncer.UNSTABLE_STATE)
|
||||
else:
|
||||
if time.monotonic() - self.previous_time >= self.interval:
|
||||
if current_state != self.__get_state(Debouncer.DEBOUNCED_STATE):
|
||||
self.previous_time = time.monotonic()
|
||||
self.__toggle_state(Debouncer.DEBOUNCED_STATE)
|
||||
self.__set_state(Debouncer.CHANGED_STATE)
|
||||
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the current debounced value of the input."""
|
||||
return self.__get_state(Debouncer.DEBOUNCED_STATE)
|
||||
|
||||
|
||||
@property
|
||||
def rose(self):
|
||||
"""Return whether the debounced input went from low to high at the most recent update."""
|
||||
return self.__get_state(self.DEBOUNCED_STATE) and self.__get_state(self.CHANGED_STATE)
|
||||
|
||||
|
||||
@property
|
||||
def fell(self):
|
||||
"""Return whether the debounced input went from high to low at the most recent update."""
|
||||
return (not self.__get_state(self.DEBOUNCED_STATE)) and self.__get_state(self.CHANGED_STATE)
|
||||
202
EPROM_Emulator/directory_node.py
Normal file
202
EPROM_Emulator/directory_node.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dave Astels
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Manage a directory in the file system.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
class DirectoryNode(object):
|
||||
"""Display and navigate the SD card contents"""
|
||||
|
||||
def __init__(self, display, parent=None, name="/"):
|
||||
"""Initialize a new instance.
|
||||
:param adafruit_ssd1306.SSD1306 on: the OLED instance to display on
|
||||
:param DirectoryNode below: optional parent directory node
|
||||
:param string named: the optional name of the new node
|
||||
"""
|
||||
self.display = display
|
||||
self.parent = parent
|
||||
self.name = name
|
||||
self.files = []
|
||||
self.top_offset = 0
|
||||
self.old_top_offset = -1
|
||||
self.selected_offset = 0
|
||||
self.old_selected_offset = -1
|
||||
|
||||
|
||||
def __cleanup(self):
|
||||
"""Dereference things for speedy gc."""
|
||||
self.display = None
|
||||
self.parent = None
|
||||
self.name = None
|
||||
self.files = None
|
||||
return self
|
||||
|
||||
|
||||
def __is_dir(self, path):
|
||||
"""Determine whether a path identifies a machine code bin file.
|
||||
:param string path: path of the file to check
|
||||
"""
|
||||
if path[-2:] == "..":
|
||||
return False
|
||||
try:
|
||||
os.listdir(path)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def __sanitize(self, name):
|
||||
"""Nondestructively strip off a trailing slash, if any, and return the result.
|
||||
:param string name: the filename
|
||||
"""
|
||||
if name[-1] == "/":
|
||||
return name[:-1]
|
||||
return name
|
||||
|
||||
|
||||
def __path(self):
|
||||
"""Return the result of recursively follow the parent links, building a full
|
||||
path to this directory."""
|
||||
if self.parent:
|
||||
return self.parent.__path() + os.sep + self.__sanitize(self.name)
|
||||
return self.__sanitize(self.name)
|
||||
|
||||
|
||||
def __make_path(self, filename):
|
||||
"""Return a full path to the specified file in this directory.
|
||||
:param string filename: the name of the file in this directory
|
||||
"""
|
||||
return self.__path() + os.sep + filename
|
||||
|
||||
|
||||
def __number_of_files(self):
|
||||
"""The number of files in this directory, including the ".." for the parent
|
||||
directory if this isn't the top directory on the SD card."""
|
||||
self.__get_files()
|
||||
return len(self.files)
|
||||
|
||||
|
||||
def __get_files(self):
|
||||
"""Return a list of the files in this directory.
|
||||
If this is not the top directory on the SD card, a ".." entry is the first element.
|
||||
Any directories have a slash appended to their name."""
|
||||
if len(self.files) == 0:
|
||||
self.files = os.listdir(self.__path())
|
||||
self.files.sort()
|
||||
if self.parent:
|
||||
self.files.insert(0, "..")
|
||||
for index, name in enumerate(self.files, start=1):
|
||||
if self.__is_dir(self.__make_path(name)):
|
||||
self.files[index] = name + "/"
|
||||
|
||||
|
||||
def __update_display(self):
|
||||
"""Update the displayed list of files if required."""
|
||||
if self.top_offset != self.old_top_offset:
|
||||
self.__get_files()
|
||||
self.display.fill(0)
|
||||
for i in range(self.top_offset, min(self.top_offset + 4, self.__number_of_files())):
|
||||
self.display.text(self.files[i], 10, (i - self.top_offset) * 8)
|
||||
self.display.show()
|
||||
self.old_top_offset = self.top_offset
|
||||
|
||||
|
||||
def __update_selection(self):
|
||||
"""Update the selected file lighlight if required."""
|
||||
if self.selected_offset != self.old_selected_offset:
|
||||
if self.old_selected_offset > -1:
|
||||
self.display.text(">", 0, (self.old_selected_offset - self.top_offset) * 8, 0)
|
||||
self.display.text(">", 0, (self.selected_offset - self.top_offset) * 8, 1)
|
||||
self.display.show()
|
||||
self.old_selected_offset = self.selected_offset
|
||||
|
||||
|
||||
def __is_directory_name(self, filename):
|
||||
"""Is a filename the name of a directory.
|
||||
:param string filename: the name of the file
|
||||
"""
|
||||
return filename[-1] == '/'
|
||||
|
||||
|
||||
@property
|
||||
def selected_filename(self):
|
||||
"""The name of the currently selected file in this directory."""
|
||||
self.__get_files()
|
||||
return self.files[self.selected_offset]
|
||||
|
||||
|
||||
@property
|
||||
def selected_filepath(self):
|
||||
"""The full path of the currently selected file in this directory."""
|
||||
return self.__make_path(self.selected_filename)
|
||||
|
||||
|
||||
def force_update(self):
|
||||
"""Force an update of the file list and selected file highlight."""
|
||||
self.old_selected_offset = -1
|
||||
self.old_top_offset = -1
|
||||
self.__update_display()
|
||||
self.__update_selection()
|
||||
|
||||
|
||||
def down(self):
|
||||
"""Move down in the file list if possible, adjusting the selected file indicator
|
||||
and scrolling the display as required."""
|
||||
if self.selected_offset < self.__number_of_files() - 1:
|
||||
self.selected_offset += 1
|
||||
if self.selected_offset == self.top_offset + 4:
|
||||
self.top_offset += 1
|
||||
self.__update_display()
|
||||
self.__update_selection()
|
||||
|
||||
|
||||
def up(self):
|
||||
"""Move up in the file list if possible, adjusting the selected file indicator
|
||||
and scrolling the display as required."""
|
||||
if self.selected_offset > 0:
|
||||
self.selected_offset -= 1
|
||||
if self.selected_offset < self.top_offset:
|
||||
self.top_offset -= 1
|
||||
self.__update_display()
|
||||
self.__update_selection()
|
||||
|
||||
|
||||
def click(self):
|
||||
"""Handle a selection and return the new current directory.
|
||||
If the selected file is the parent, i.e. "..", return to the parent directory.
|
||||
If the selected file is a directory, go into it."""
|
||||
if self.selected_filename == "..":
|
||||
if self.parent:
|
||||
p = self.parent
|
||||
p.force_update()
|
||||
self.__cleanup()
|
||||
return p
|
||||
elif self.__is_directory_name(self.selected_filename):
|
||||
new_node = DirectoryNode(self.display, self, self.selected_filename)
|
||||
new_node.force_update()
|
||||
return new_node
|
||||
return self
|
||||
140
EPROM_Emulator/emulator.py
Normal file
140
EPROM_Emulator/emulator.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dave Astels
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Manage the emulator hardware.
|
||||
"""
|
||||
|
||||
import digitalio
|
||||
import adafruit_mcp230xx
|
||||
|
||||
# control pin values
|
||||
|
||||
PROGRAMMER_USE = False
|
||||
EMULATE_USE = True
|
||||
|
||||
WRITE_ENABLED = False
|
||||
WRITE_DISABLED = True
|
||||
|
||||
CHIP_ENABLED = False
|
||||
CHIP_DISABLED = True
|
||||
|
||||
CLOCK_ACTIVE = False
|
||||
CLOCK_INACTIVE = True
|
||||
|
||||
RESET_INACTIVE = False
|
||||
RESET_ACTIVE = True
|
||||
|
||||
LED_OFF = False
|
||||
LED_ON = True
|
||||
|
||||
ENABLE_HOST_ACCESS = False
|
||||
DISABLE_HOST_ACCESS = True
|
||||
|
||||
|
||||
class Emulator(object):
|
||||
"""Handle all interaction with the emulator circuit."""
|
||||
|
||||
def __init__(self, i2c):
|
||||
self.mcp = adafruit_mcp230xx.MCP23017(i2c)
|
||||
self.mcp.iodir = 0x0000 # Make all pins outputs
|
||||
|
||||
# Configure the individual control pins
|
||||
|
||||
self.mode_pin = self.mcp.get_pin(8)
|
||||
self.mode_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.mode_pin.value = PROGRAMMER_USE
|
||||
|
||||
self.write_pin = self.mcp.get_pin(9)
|
||||
self.write_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.write_pin.value = WRITE_DISABLED
|
||||
|
||||
self.chip_select_pin = self.mcp.get_pin(10)
|
||||
self.chip_select_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.chip_select_pin.value = CHIP_DISABLED
|
||||
|
||||
self.address_clock_pin = self.mcp.get_pin(11)
|
||||
self.address_clock_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.address_clock_pin.value = CLOCK_INACTIVE
|
||||
|
||||
self.clock_reset_pin = self.mcp.get_pin(12)
|
||||
self.clock_reset_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.clock_reset_pin.value = RESET_INACTIVE
|
||||
|
||||
self.led_pin = self.mcp.get_pin(13)
|
||||
self.led_pin.direction = digitalio.Direction.OUTPUT
|
||||
self.led_pin.value = False
|
||||
|
||||
|
||||
def __pulse_write(self):
|
||||
self.write_pin.value = WRITE_ENABLED
|
||||
self.write_pin.value = WRITE_DISABLED
|
||||
|
||||
|
||||
def __deactivate_ram(self):
|
||||
self.chip_select_pin.value = CHIP_DISABLED
|
||||
|
||||
|
||||
def __activate_ram(self):
|
||||
self.chip_select_pin.value = CHIP_ENABLED
|
||||
|
||||
|
||||
def __reset_address_counter(self):
|
||||
self.clock_reset_pin.value = RESET_ACTIVE
|
||||
self.clock_reset_pin.value = RESET_INACTIVE
|
||||
|
||||
|
||||
def __advance_address_counter(self):
|
||||
self.address_clock_pin.value = CLOCK_ACTIVE
|
||||
self.address_clock_pin.value = CLOCK_INACTIVE
|
||||
|
||||
|
||||
def __output_on_port_a(self, data_byte):
|
||||
"""A hack to get around the limitation of the 23017 library to use 8-bit ports"""
|
||||
self.mcp.gpio = (self.mcp.gpio & 0xFF00) | (data_byte & 0x00FF)
|
||||
|
||||
|
||||
def enter_program_mode(self):
|
||||
"""Enter program mode, allowing loading of the emulator RAM."""
|
||||
self.mode_pin.value = PROGRAMMER_USE
|
||||
self.led_pin.value = LED_OFF
|
||||
|
||||
|
||||
def enter_emulate_mode(self):
|
||||
"""Enter emulate mode, giving control of the emulator ram to the host."""
|
||||
self.mode_pin.value = EMULATE_USE
|
||||
self.led_pin.value = LED_ON
|
||||
|
||||
|
||||
def load_ram(self, code):
|
||||
"""Load the emulator RAM. Automatically switched to program mode.
|
||||
:param [byte] code: the list of bytes to load into the emulator RAM
|
||||
"""
|
||||
self.enter_program_mode()
|
||||
self.__reset_address_counter()
|
||||
for data_byte in code:
|
||||
self.__output_on_port_a(data_byte)
|
||||
self.__activate_ram()
|
||||
self.__pulse_write()
|
||||
self.__deactivate_ram()
|
||||
self.__advance_address_counter()
|
||||
213
EPROM_Emulator/main.py
Normal file
213
EPROM_Emulator/main.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Dave Astels
|
||||
|
||||
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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
EPROM emulator UI in CircuitPython.
|
||||
Targeted for the SAMD51 boards.
|
||||
|
||||
by Dave Astels
|
||||
"""
|
||||
|
||||
import digitalio
|
||||
import board
|
||||
import busio
|
||||
import adafruit_ssd1306
|
||||
import storage
|
||||
import adafruit_sdcard
|
||||
|
||||
from directory_node import DirectoryNode
|
||||
from emulator import Emulator
|
||||
from debouncer import Debouncer
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Initialize Rotary encoder
|
||||
|
||||
# Encoder button is a digital input with pullup on D2
|
||||
button = Debouncer(board.D2, digitalio.Pull.UP, 0.01)
|
||||
|
||||
# Rotary encoder inputs with pullup on D3 & D4
|
||||
rot_a = digitalio.DigitalInOut(board.D4)
|
||||
rot_a.direction = digitalio.Direction.INPUT
|
||||
rot_a.pull = digitalio.Pull.UP
|
||||
|
||||
rot_b = digitalio.DigitalInOut(board.D3)
|
||||
rot_b.direction = digitalio.Direction.INPUT
|
||||
rot_b.pull = digitalio.Pull.UP
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Initialize I2C and OLED
|
||||
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
|
||||
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
|
||||
oled.fill(0)
|
||||
oled.text("Initializing SD", 0, 10)
|
||||
oled.show()
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Initialize SD card
|
||||
|
||||
#SD_CS = board.D10
|
||||
# Connect to the card and mount the filesystem.
|
||||
spi = busio.SPI(board.D13, board.D11, board.D12) # SCK, MOSI, MISO
|
||||
cs = digitalio.DigitalInOut(board.D10)
|
||||
sdcard = adafruit_sdcard.SDCard(spi, cs)
|
||||
vfs = storage.VfsFat(sdcard)
|
||||
storage.mount(vfs, "/sd")
|
||||
|
||||
oled.fill(0)
|
||||
oled.text("Done", 0, 10)
|
||||
oled.show()
|
||||
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Initialize globals
|
||||
|
||||
encoder_counter = 0
|
||||
encoder_direction = 0
|
||||
|
||||
# constants to help us track what edge is what
|
||||
A_POSITION = 0
|
||||
B_POSITION = 1
|
||||
UNKNOWN_POSITION = -1 # initial state so we know if something went wrong
|
||||
|
||||
rising_edge = falling_edge = UNKNOWN_POSITION
|
||||
|
||||
PROGRAM_MODE = 0
|
||||
EMULATE_MODE = 1
|
||||
|
||||
current_mode = PROGRAM_MODE
|
||||
emulator = Emulator(i2c)
|
||||
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Helper functions
|
||||
|
||||
def is_binary_name(filename):
|
||||
return filename[-4:] == ".bin"
|
||||
|
||||
|
||||
def load_file(filename):
|
||||
data = []
|
||||
with open(filename, "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
|
||||
def display_emulating_screen():
|
||||
oled.fill(0)
|
||||
oled.text("Emulating", 0, 0)
|
||||
oled.text(current_dir.selected_filename, 0, 10)
|
||||
oled.show()
|
||||
|
||||
|
||||
def emulate():
|
||||
global current_mode
|
||||
data = load_file(current_dir.selected_filepath)
|
||||
emulator.load_ram(data)
|
||||
emulator.enter_emulate_mode()
|
||||
current_mode = EMULATE_MODE
|
||||
display_emulating_screen()
|
||||
|
||||
|
||||
def program():
|
||||
global current_mode
|
||||
emulator.enter_program_mode()
|
||||
current_mode = PROGRAM_MODE
|
||||
current_dir.force_update()
|
||||
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
# Main loop
|
||||
|
||||
current_dir = DirectoryNode(oled, name="/sd")
|
||||
current_dir.force_update()
|
||||
rising_edge = falling_edge = UNKNOWN_POSITION
|
||||
rotary_prev_state = [rot_a.value, rot_b.value]
|
||||
|
||||
while True:
|
||||
# reset encoder and wait for the next turn
|
||||
encoder_direction = 0
|
||||
|
||||
# take a 'snapshot' of the rotary encoder state at this time
|
||||
rotary_curr_state = [rot_a.value, rot_b.value]
|
||||
|
||||
# See https://learn.adafruit.com/media-dial/code
|
||||
if rotary_curr_state != rotary_prev_state:
|
||||
print("Was: {}".format(rotary_prev_state))
|
||||
print("Now: {}".format(rotary_curr_state))
|
||||
if rotary_prev_state == [True, True]:
|
||||
if not rotary_curr_state[A_POSITION]:
|
||||
print("Falling A")
|
||||
falling_edge = A_POSITION
|
||||
elif not rotary_curr_state[B_POSITION]:
|
||||
print("Falling B")
|
||||
falling_edge = B_POSITION
|
||||
else:
|
||||
continue
|
||||
|
||||
if rotary_curr_state == [True, True]:
|
||||
if not rotary_prev_state[B_POSITION]:
|
||||
rising_edge = B_POSITION
|
||||
print("Rising B")
|
||||
elif not rotary_prev_state[A_POSITION]:
|
||||
rising_edge = A_POSITION
|
||||
print("Rising A")
|
||||
else:
|
||||
continue
|
||||
|
||||
# check first and last edge
|
||||
if (rising_edge == A_POSITION) and (falling_edge == B_POSITION):
|
||||
encoder_counter -= 1
|
||||
encoder_direction = -1
|
||||
print("%d dec" % encoder_counter)
|
||||
elif (rising_edge == B_POSITION) and (falling_edge == A_POSITION):
|
||||
encoder_counter += 1
|
||||
encoder_direction = 1
|
||||
print("%d inc" % encoder_counter)
|
||||
else:
|
||||
# (shrug) something didn't work out, oh well!
|
||||
encoder_direction = 0
|
||||
|
||||
# reset our edge tracking
|
||||
rising_edge = falling_edge = UNKNOWN_POSITION
|
||||
|
||||
rotary_prev_state = rotary_curr_state
|
||||
|
||||
# Handle encoder rotation
|
||||
if current_mode == PROGRAM_MODE: #Ignore rotation if in EMULATE mode
|
||||
if encoder_direction == -1:
|
||||
current_dir.up()
|
||||
elif encoder_direction == 1:
|
||||
current_dir.down()
|
||||
|
||||
# look for a press of the rotary encoder switch press, with debouncing
|
||||
button.update()
|
||||
if button.fell:
|
||||
if current_mode == EMULATE_MODE:
|
||||
program()
|
||||
elif is_binary_name(current_dir.selected_filename):
|
||||
emulate()
|
||||
else:
|
||||
current_dir = current_dir.click()
|
||||
Loading…
Reference in a new issue