Livt packages

Livt.IO

Version 1.8.1

Memory, UART, I2C, SPI, and Ethernet input/output components for Livt hardware designs.

Published files (126)

src/spi/SPIFlash.lvt

namespace Livt.IO

/**
 * Standard single-lane, 24-bit-address SPI NOR commands over a borrowed transport.
 * Owns one page staging buffer and a context-timed readiness deadline. One owner
 * serializes calls and retains exclusive transport access. Devices must already
 * be in ordinary SPI command mode; board-specific configuration handover is external.
 * Capacity and erase geometry must match the device. No chip erase, protection
 * changes, quad mode, four-byte addresses or implicit retries of writes.
 * STYLE selects the default RAM preference. B can replace the owned buffer, including
 * ArrayRam for resettable logic storage; it must provide at least PAGE_SIZE bytes.
 * Access and initialization guarantees are independent of backing.
 */
component SPIFlash<T: ISPITransactions, CAPACITY: int, PAGE_SIZE: int = 256,
	SECTOR_SIZE: int = 4096, ERASE_COMMAND: byte = 0x20, TIMEOUT: Time = 2s, STYLE: MemoryStyle = MemoryStyle.Block,
	B: IRam<byte> & new() = Ram<byte, PAGE_SIZE, STYLE>> : ISPIFlash
{
	static assert CAPACITY > 0 && CAPACITY <= 16777216, "Capacity must fit 24-bit addressing"
	static assert PAGE_SIZE > 0 && PAGE_SIZE <= CAPACITY, "Page size must fit capacity"
	static assert SECTOR_SIZE >= PAGE_SIZE && SECTOR_SIZE % PAGE_SIZE == 0, "Sectors must contain whole pages"
	static assert CAPACITY % SECTOR_SIZE == 0, "Capacity must contain whole sectors"
	static assert TIMEOUT > 0ns, "Readiness timeout must be positive"

	// Standard NOR instructions and status masks.
	const READ: byte = 0x03
	const PROGRAM: byte = 0x02
	const WRITE_ENABLE: byte = 0x06
	const READ_STATUS: byte = 0x05
	const BUSY: int = 1
	const WRITE_ENABLED: int = 2

	// Borrowed exclusive transaction transport and owned page staging storage.
	transport: T
	buffer: B
	bufferInitialized: bool

	// Free-running context ticks; unsigned subtraction tolerates one wrap.
	ticks: uint
	timeoutTicks: uint

	/**
	 * Binds an exclusive transport; staging is zeroed lazily before its first use.
	 * Construction and staging initialization never access the SPI device.
	 * TIMEOUT must fit the context's unsigned tick range.
	 */
	new(transport: T)
	{
		this.transport = transport
		buffer = new B()
		timeoutTicks = context.TicksFor(TIMEOUT)
		assert timeoutTicks > 0, "Timeout must fit the context tick range"
	}

	/**
	 * Preserves zero-initialized staging semantics without resettable payload registers.
	 * The exclusive caller initializes staging RAM once before its first staging access.
	 */
	fn InitializeBuffer()
	{
		if (!bufferInitialized) {
			for (var index = 0; index < PAGE_SIZE; index++) {
				buffer.Write(index, 0x00)
			}

			bufferInitialized = true
		}
	}

	/**
	 * Advances readiness time independently of scheduled SPI operations.
	 */
	process Clock()
	{
		state {
			ticks++
		}
	}

	/**
	 * Reads manufacturer/type/capacity as a 24-bit JEDEC identity. Caller verifies
	 * device geometry before mounting writable storage. Requires ordinary SPI mode;
	 * this does not reset the device, clear protection, or cancel an ongoing write.
	 */
	public fn ReadJedecId() uint
	{
		transport.Select()
		transport.Transfer(0x9F)
		var manufacturer = transport.Transfer(0x00)
		var kind = transport.Transfer(0x00)
		var capacity = transport.Transfer(0x00)
		transport.Deselect()
		return ((manufacturer as uint) << 16) | ((kind as uint) << 8) | (capacity as uint)
	}

	/**
	 * Returns the configured byte capacity.
	 */
	override fn GetCapacity() int
	{
		return CAPACITY
	}

	/**
	 * Returns the page and staging-buffer size.
	 */
	override fn GetPageSize() int
	{
		return PAGE_SIZE
	}

	/**
	 * Returns the configured erase-sector size.
	 */
	override fn GetSectorSize() int
	{
		return SECTOR_SIZE
	}

	/**
	 * Writes staging only when index fits the page buffer.
	 */
	override fn SetByte(index: int, value: byte) bool
	{
		if (index < 0 || index >= PAGE_SIZE) {
			return false
		}

		InitializeBuffer()
		buffer.Write(index, value)
		return true
	}

	/**
	 * Returns staging content, or zero outside its bounds.
	 */
	override fn GetByte(index: int) byte
	{
		if (index < 0 || index >= PAGE_SIZE) {
			return 0x00
		}

		InitializeBuffer()
		return buffer.Read(index)
	}

	/**
	 * Checks a complete transfer without overflowing address arithmetic.
	 */
	fn IsValidRange(address: int, length: int) bool
	{
		return address >= 0 && length > 0 && length <= PAGE_SIZE && address <= CAPACITY - length
	}

	/**
	 * Sends a command and three address bytes with chip select held active.
	 */
	fn BeginAddress(command: byte, address: int)
	{
		transport.Select()
		transport.Transfer(command)
		transport.Transfer((address >> 16) as byte)
		transport.Transfer((address >> 8) as byte)
		transport.Transfer(address as byte)
	}

	/**
	 * Samples status register one and releases the bus.
	 */
	fn ReadStatus() int
	{
		transport.Select()
		transport.Transfer(READ_STATUS)
		var status = transport.Transfer(0x00)
		transport.Deselect()
		return status as int
	}

	/**
	 * Polls WIP until ready or the context deadline expires. Releases chip select
	 * after every poll. A timeout does not cancel an external operation.
	 */
	fn WaitReady() bool
	{
		var started = ticks

		while (true) {
			var status = ReadStatus()

			if ((status & BUSY) == 0) {
				return true
			}

			if (ticks - started >= timeoutTicks) {
				return false
			}
		}
	}

	/**
	 * Waits for readiness, issues WREN and verifies the write-enable latch.
	 */
	fn EnableWrite() bool
	{
		if (!WaitReady()) {
			return false
		}

		transport.Select()
		transport.Transfer(WRITE_ENABLE)
		transport.Deselect()
		return (ReadStatus() & WRITE_ENABLED) != 0
	}

	/**
	 * Reads a bounded sequential range into staging, including across page boundaries.
	 */
	override fn Read(address: int, length: int) bool
	{
		if (!IsValidRange(address, length)) {
			return false
		}

		if (!WaitReady()) {
			return false
		}

		InitializeBuffer()
		BeginAddress(READ, address)

		for (var index = 0; index < length; index++) {
			var value = transport.Transfer(0x00)
			buffer.Write(index, value)
		}

		transport.Deselect()
		return true
	}

	/**
	 * Programs one bounded page fragment and waits for WIP to clear.
	 */
	override fn ProgramPage(address: int, length: int) bool
	{
		if (!IsValidRange(address, length) || length > PAGE_SIZE - address % PAGE_SIZE) {
			return false
		}

		if (!EnableWrite()) {
			return false
		}

		InitializeBuffer()
		BeginAddress(PROGRAM, address)

		for (var index = 0; index < length; index++) {
			var value = buffer.Read(index)
			transport.Transfer(value)
		}

		transport.Deselect()
		return WaitReady()
	}

	/**
	 * Erases exactly one aligned sector and waits for WIP to clear.
	 */
	override fn EraseSector(address: int) bool
	{
		if (address < 0 || address > CAPACITY - SECTOR_SIZE || address % SECTOR_SIZE != 0) {
			return false
		}

		if (!EnableWrite()) {
			return false
		}

		BeginAddress(ERASE_COMMAND, address)
		transport.Deselect()
		return WaitReady()
	}
}