Skip to content

Commit

Permalink
Added driver for SPI Flash chips
Browse files Browse the repository at this point in the history
Enables using SPI Flash on hexpansions, works with the existing
bdevice.py driver.
  • Loading branch information
hairymnstr committed May 26, 2024
1 parent e60e676 commit d000901
Showing 1 changed file with 189 additions and 0 deletions.
189 changes: 189 additions & 0 deletions modules/lib/flash_spi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# flash_spi.py MicroPython driver for SPI NOR flash devices.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2019-2020 Peter Hinch

import time
from micropython import const
from bdevice import FlashDevice

# Supported instruction set:
# 3 and 4 byte address commands
_READ = const(0) # Index of _CMDSxBA
_PP = const(1)
_SE = const(2)
_CMDS3BA = b"\x03\x02\x20"
_CMDS4BA = b"\x13\x12\x21"
# No address
_WREN = const(6) # Write enable
_RDSR1 = const(5) # Read status register 1
_RDID = const(0x9F) # Read manufacturer ID
_CE = const(0xC7) # Chip erase (takes minutes)

_SEC_SIZE = const(4096) # Flash sector size 0x1000

# Logical Flash device comprising one or more physical chips sharing an SPI bus.
class FLASH(FlashDevice):
def __init__(
self, spi, cspins, size=None, verbose=True, sec_size=_SEC_SIZE, block_size=9, cmd5=None
):
self._spi = spi
self._cspins = cspins
self._ccs = None # Chip select Pin object for current chip
self._bufp = bytearray(6) # instruction + 4 byte address + 1 byte value
self._mvp = memoryview(self._bufp) # cost-free slicing
self._page_size = 256 # Write uses 256 byte pages.
# Defensive code: application should have done the following.
# Pyboard D 3V3 output may just have been switched on.
for cs in cspins: # Deselect all chips
cs(1)
time.sleep_ms(1) # Meet Tpu 300μs

if size is None: # Get from chip
size = self.scan(verbose, size) # KiB
super().__init__(block_size, len(cspins), size * 1024, sec_size)

# Select the correct command set
if (cmd5 is None and size <= 4096) or (cmd5 == False):
self._cmds = _CMDS3BA
self._cmdlen = 4
else:
self._cmds = _CMDS4BA
self._cmdlen = 5

self.initialise() # Initially cache sector 0

# **** API SPECIAL METHODS ****
# Scan: return chip size in KiB as read from ID.
def scan(self, verbose, size):
mvp = self._mvp
for n, cs in enumerate(self._cspins):
mvp[:] = b"\0\0\0\0\0\0"
mvp[0] = _RDID
cs(0)
self._spi.write_readinto(mvp[:4], mvp[:4])
cs(1)
scansize = 1 << (mvp[3] - 10)
if size is None:
size = scansize # Save size of 1st chip
if size != scansize: # Mismatch passed size or 1st chip.
raise ValueError(f"Flash size mismatch: expected {size}KiB, found {scansize}KiB")
if not 0x10 < mvp[3] < 0x22:
raise ValueError(f"Invalid chip size {size}KiB. Specify size arg.")

if verbose:
s = "{} chips detected. Total flash size {}MiB."
n += 1
print(s.format(n, (n * size) // 1024))
return size

# Chip erase. Can take minutes.
def erase(self):
mvp = self._mvp
for cs in self._cspins: # For each chip
mvp[0] = _WREN
cs(0)
self._spi.write(mvp[:1]) # Enable write
cs(1)
mvp[0] = _CE
cs(0)
self._spi.write(mvp[:1]) # Start erase
cs(1)
self._wait_rdy() # Wait for erase to complete

# **** INTERFACE FOR BASE CLASS ****
# Write cache to a sector starting at byte address addr
def flush(self, cache, addr): # cache is memoryview into buffer
self._sector_erase(addr)
mvp = self._mvp
nbytes = self.sec_size
ps = self._page_size
start = 0 # Current offset into cache buffer
while nbytes > 0:
# write one page at a time
self._getaddr(addr, 1)
cs = self._ccs # Current chip select from _getaddr
mvp[0] = _WREN
cs(0)
self._spi.write(mvp[:1]) # Enable write
cs(1)
mvp[0] = self._cmds[_PP]
cs(0)
self._spi.write(mvp[: self._cmdlen]) # Start write
self._spi.write(cache[start : start + ps])
cs(1)
self._wait_rdy() # Wait for write to complete
nbytes -= ps
start += ps
addr += ps

# Read from chip into a memoryview. Address range guaranteed not to be cached.
def rdchip(self, addr, mvb):
nbytes = len(mvb)
mvp = self._mvp
start = 0 # Offset into buf.
while nbytes > 0:
npage = self._getaddr(addr, nbytes) # No. of bytes in current chip
cs = self._ccs
mvp[0] = self._cmds[_READ]
cs(0)
self._spi.write(mvp[: self._cmdlen])
self._spi.readinto(mvb[start : start + npage])
cs(1)
nbytes -= npage
start += npage
addr += npage

# Read or write multiple bytes at an arbitrary address.
# **** Also part of API ****
def readwrite(self, addr, buf, read):
mvb = memoryview(buf)
self.read(addr, mvb) if read else self.write(addr, mvb)
return buf

# **** INTERNAL METHODS ****
def _wait_rdy(self): # After a write, wait for device to become ready
mvp = self._mvp
cs = self._ccs # Chip is already current
while True: # TODO read status register 2, raise OSError on nonzero.
mvp[0] = _RDSR1
cs(0)
self._spi.write_readinto(mvp[:2], mvp[:2])
cs(1)
if not (mvp[1] & 1):
break
time.sleep_ms(1)

# Given an address, set current chip select and address buffer.
# Return the number of bytes that can be processed in the current chip.
def _getaddr(self, addr, nbytes):
if addr >= self._a_bytes:
raise RuntimeError("Flash Address is out of range")
ca, la = divmod(addr, self._c_bytes) # ca == chip no, la == offset into chip
self._ccs = self._cspins[ca] # Current chip select
cmdlen = self._cmdlen
mvp = self._mvp[:cmdlen]
if cmdlen > 3:
mvp[-4] = la >> 24
mvp[-3] = la >> 16 & 0xFF
mvp[-2] = (la >> 8) & 0xFF
mvp[-1] = la & 0xFF
pe = (addr & -self._c_bytes) + self._c_bytes # Byte 0 of next chip
return min(nbytes, pe - la)

# Erase sector. Address is start byte address of sector. Optimisation: skip
# if sector is already erased.
def _sector_erase(self, addr):
if not self.is_empty(addr):
self._getaddr(addr, 1)
cs = self._ccs # Current chip select from _getaddr
mvp = self._mvp
mvp[0] = _WREN
cs(0)
self._spi.write(mvp[:1]) # Enable write
cs(1)
mvp[0] = self._cmds[_SE]
cs(0)
self._spi.write(mvp[: self._cmdlen]) # Start erase
cs(1)
self._wait_rdy() # Wait for erase to complete

0 comments on commit d000901

Please sign in to comment.