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/SPIMaster.lvt
namespace Livt.IO
/**
* Byte-level, single-device SPI Mode 0 controller.
*
* Bytes are transferred most-significant bit first. Chip select is controlled
* separately so callers can keep one transaction active across command,
* address, and payload bytes. Timing is derived from the component context;
* the default 50 ns half-period produces an SPI clock no faster than 10 MHz.
* A longer compile-time half-period allows slower devices and registered adapters.
*/
component SPIMaster
{
// Command codes consumed by the transfer state machine.
const COMMAND_NONE: int = 0
const COMMAND_SELECT: int = 1
const COMMAND_DESELECT: int = 2
const COMMAND_TRANSFER: int = 3
// Borrowed physical signal contract.
bus: SPIBus
// Selection, command completion, and the retained receive result.
busy: bool
selected: bool
hasResult: bool
resultByte: byte
// Pending command payload and context-derived phase timing.
transmitByte: byte
command: int
halfPeriodTicks: uint
/**
* Creates an idle Mode 0 controller attached to the supplied SPI bus.
*
* SCLK and MOSI start low, chip select starts high, and no command result is
* pending. The positive half-period is rounded up to inherited context ticks.
* Choose it to cover device timing and any registered adapter round-trip latency.
*/
new(bus: flip SPIBus, halfPeriod: const Time = 50ns)
{
static assert halfPeriod >= 1ns, "SPI half-period must be at least one nanosecond"
this.bus = bus
this.bus.sclk = 0b0
this.bus.mosi = 0b0
this.bus.chip_select_n = 0b1
this.busy = false
this.selected = false
this.hasResult = false
this.resultByte = 0x00
this.transmitByte = 0x00
this.command = COMMAND_NONE
this.halfPeriodTicks = this.context.TicksFor(halfPeriod)
}
/**
* Returns the number of context ticks used for one SCLK half-period.
*/
public fn HalfPeriodTicks() uint
{
return this.halfPeriodTicks
}
/**
* Starts chip-select assertion and returns false when busy or already
* selected. A false result leaves the controller unchanged.
*/
public fn BeginSelect() bool
{
if (this.busy || this.selected) {
return false
}
state {
this.busy = true
this.hasResult = false
this.command = COMMAND_SELECT
}
return true
}
/**
* Starts chip-select deassertion and returns false when busy or not selected.
* A false result leaves the controller unchanged.
*/
public fn BeginDeselect() bool
{
if (this.busy || !this.selected) {
return false
}
state {
this.busy = true
this.hasResult = false
this.command = COMMAND_DESELECT
}
return true
}
/**
* Starts one full-duplex byte transfer and returns false when busy or when
* chip select is inactive. A false result leaves the controller unchanged.
*/
public fn BeginTransfer(data: byte) bool
{
if (this.busy || !this.selected) {
return false
}
state {
this.busy = true
this.hasResult = false
this.resultByte = 0x00
this.transmitByte = data
this.command = COMMAND_TRANSFER
}
return true
}
/**
* Returns whether a select, deselect, or transfer command is active.
*/
public fn IsBusy() bool
{
return this.busy
}
/**
* Returns whether chip select is currently asserted.
*/
public fn IsSelected() bool
{
return this.selected
}
/**
* Returns whether the most recent accepted command has completed.
*/
public fn HasResult() bool
{
return this.hasResult
}
/**
* Returns the byte sampled by the most recent completed transfer.
*
* Before the first transfer this returns 0x00. Select and deselect commands
* leave the last received byte unchanged.
*/
public fn GetReceivedByte() byte
{
return this.resultByte
}
/**
* Clears the sticky command-completion indication without changing data.
*/
public fn ClearResult()
{
this.hasResult = false
}
/**
* Executes accepted commands and generates Mode 0 bus timing.
*/
process Main()
{
var phaseTick: uint = 0n0
var bitIndex: int = 0
state Idle {
phaseTick = 0n0
bitIndex = 0
this.bus.sclk = 0b0
this.bus.mosi = 0b0
if (this.selected) {
this.bus.chip_select_n = 0b0
} else {
this.bus.chip_select_n = 0b1
}
if (!this.busy) {
goto Idle
} elif (this.command == COMMAND_SELECT) {
goto SelectDrive
} elif (this.command == COMMAND_DESELECT) {
goto DeselectDrive
} elif (this.command == COMMAND_TRANSFER) {
goto TransferLowDrive
} else {
goto Idle
}
}
state SelectDrive {
this.bus.chip_select_n = 0b0
this.bus.sclk = 0b0
this.bus.mosi = 0b0
goto SelectWait
}
state SelectWait {
if (phaseTick + 0n1 < this.halfPeriodTicks) {
phaseTick = phaseTick + 0n1
goto SelectWait
} else {
this.selected = true
this.busy = false
this.hasResult = true
this.command = COMMAND_NONE
goto Idle
}
}
state DeselectDrive {
this.bus.chip_select_n = 0b0
this.bus.sclk = 0b0
this.bus.mosi = 0b0
goto DeselectWait
}
state DeselectWait {
if (phaseTick + 0n1 < this.halfPeriodTicks) {
phaseTick = phaseTick + 0n1
goto DeselectWait
} else {
this.bus.chip_select_n = 0b1
this.selected = false
this.busy = false
this.hasResult = true
this.command = COMMAND_NONE
goto Idle
}
}
state TransferLowDrive {
this.bus.chip_select_n = 0b0
this.bus.sclk = 0b0
var shiftedTransmit = (this.transmitByte as int) >> (7 - bitIndex)
if (shiftedTransmit % 2 == 0) {
this.bus.mosi = 0b0
} else {
this.bus.mosi = 0b1
}
goto TransferLowWait
}
state TransferLowWait {
if (phaseTick + 0n1 < this.halfPeriodTicks) {
phaseTick = phaseTick + 0n1
goto TransferLowWait
} else {
phaseTick = 0n0
this.bus.sclk = 0b1
goto TransferHighWait
}
}
state TransferHighWait {
if (phaseTick + 0n1 < this.halfPeriodTicks) {
phaseTick = phaseTick + 0n1
goto TransferHighWait
} else {
var receivedValue = (this.resultByte as int) << 1
if (this.bus.miso == 0b1) {
receivedValue = receivedValue + 1
}
this.resultByte = receivedValue as byte
this.bus.sclk = 0b0
phaseTick = 0n0
goto TransferAdvance
}
}
// Advance in a separate state so both branches retain explicit phase transitions.
state TransferAdvance {
if (bitIndex < 7) {
bitIndex = bitIndex + 1
goto TransferLowDrive
} else {
goto TransferComplete
}
}
state TransferComplete {
this.bus.chip_select_n = 0b0
this.bus.sclk = 0b0
this.bus.mosi = 0b0
this.busy = false
this.hasResult = true
this.command = COMMAND_NONE
goto Idle
}
}
}