Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added driver for SPI Flash chips #47

Merged
merged 1 commit into from
May 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading