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/i2c/I2CMaster.lvt

namespace Livt.IO

/**
 * Byte-level, single-controller I2C master with context-derived standard-mode timing.
 * Borrows an open-drain bus and inherits the consumer clock/reset context.
 * One serialized caller owns commands. Begin methods report acceptance; HasResult
 * reports completion. Reset releases drive requests and clears pending results.
 * A stretch timeout releases both lines without emitting recovery clock pulses.
 */
component I2CMaster<BUS_FREQUENCY: Frequency = 100000Hz,
	STRETCH_TIMEOUT: Time = 1ms>
{
	static assert BUS_FREQUENCY >= 1Hz && BUS_FREQUENCY <= 100000Hz,
		"I2C bus frequency must be between 1 Hz and 100 kHz (standard mode)"

	static assert STRETCH_TIMEOUT >= 1ns, "I2C clock-stretch timeout must be positive"

	// Supported unshifted device address range.
	public const MAX_7_BIT_ADDRESS: byte = 0x7F

	// The consumer selects the bus rate; its inherited context supplies the clock.
	public const TICKS_PER_HALF_PERIOD: int = ((this.context.TicksPer(BUS_FREQUENCY) + 1) / 2) as int

	// Maximum consecutive observed-low cycles while the master releases SCL.
	const STRETCH_TIMEOUT_TICKS: uint = this.context.TicksFor(STRETCH_TIMEOUT)

	// Command codes consumed by the byte-transfer state machine.
	const COMMAND_NONE: int = 0
	const COMMAND_START: int = 1
	const COMMAND_STOP: int = 2
	const COMMAND_WRITE: int = 3
	const COMMAND_READ: int = 4

	// Borrowed open-drain bus attachment.
	bus: I2CBus

	// Completion status and result retained for the caller.
	busy: bool
	hasResult: bool
	timedOut: bool
	resultAck: bool
	resultByte: byte

	// Active byte command, bit position, and phase timing.
	currentByte: byte
	sendAck: bool
	bitIndex: int
	tickCount: int
	command: int

	// Sampled bus levels used for clock stretching and input data.
	observedScl: logic
	observedSda: logic

	/**
	 * Borrows the open-drain bus, releases both lines, and starts idle with no result. Timing
	 * derives from the inherited context and BUS_FREQUENCY; use one serialized command owner.
	 */
	new(bus: flip I2CBus)
	{
		this.bus = bus
		this.bus.scl_drive_low = 0b0
		this.bus.sda_drive_low = 0b0
		this.busy = false
		this.hasResult = false
		this.timedOut = false
		this.resultAck = false
		this.resultByte = 0x00
		this.currentByte = 0x00
		this.sendAck = false
		this.bitIndex = 0
		this.tickCount = 0
		this.command = COMMAND_NONE
		this.observedScl = 0b1
		this.observedSda = 0b1
	}

	/**
	 * Returns the context-derived minimum phase count. State transitions and observed
	 * SCL synchronization can extend wire timing; BUS_FREQUENCY is a rate ceiling.
	 */
	public fn HalfPeriodTicks() int
	{
		return TICKS_PER_HALF_PERIOD
	}

	/**
	 * Accepts a START request when idle. True means accepted, not completed; poll HasResult.
	 * Returns false without changing the active command when busy.
	 */
	public fn BeginStart() bool
	{
		if (this.busy == true) {
			return false
		}

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.command = COMMAND_START
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Accepts a STOP request when idle. True means accepted, not completed; poll HasResult.
	 * Returns false while busy.
	 */
	public fn BeginStop() bool
	{
		if (this.busy == true) {
			return false
		}

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.command = COMMAND_STOP
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Accepts one outgoing data byte when idle and clears previous completion/ACK status. Poll
	 * HasResult before inspecting ACK; returns false while busy.
	 */
	public fn BeginWriteByte(data: byte) bool
	{
		if (this.busy == true) {
			return false
		}

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.resultAck = false
			this.currentByte = data
			this.bitIndex = 0
			this.command = COMMAND_WRITE
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Accepts a seven-bit address byte with the write bit cleared. Returns false while busy or
	 * for addresses above 0x7F. Does not issue START; poll HasResult for completion.
	 */
	public fn BeginWriteAddress(address: byte) bool
	{
		if (this.busy == true) {
			return false
		}

		if (address > MAX_7_BIT_ADDRESS) {
			return false
		}

		var addressValue: int = address as int
		var writeAddress: byte = (addressValue << 1) as byte

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.resultAck = false
			this.currentByte = writeAddress
			this.bitIndex = 0
			this.command = COMMAND_WRITE
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Accepts a seven-bit address byte with the read bit set. Returns false while busy or for
	 * addresses above 0x7F. Does not issue START; poll HasResult for completion.
	 */
	public fn BeginReadAddress(address: byte) bool
	{
		if (this.busy == true) {
			return false
		}

		if (address > MAX_7_BIT_ADDRESS) {
			return false
		}

		var addressValue: int = address as int
		var readAddress: byte = ((addressValue << 1) | 1) as byte

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.resultAck = false
			this.currentByte = readAddress
			this.bitIndex = 0
			this.command = COMMAND_WRITE
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Accepts one byte-read command when idle. sendAck selects ACK versus NACK after
	 * reception; inspect GetReadByte only after HasResult. Returns false while busy.
	 */
	public fn BeginReadByte(sendAck: bool) bool
	{
		if (this.busy == true) {
			return false
		}

		state {
			this.busy = true
			this.hasResult = false
			this.timedOut = false
			this.resultByte = 0x00
			this.sendAck = sendAck
			this.bitIndex = 0
			this.command = COMMAND_READ
			this.tickCount = 0
		}

		return true
	}

	/**
	 * Returns a snapshot of command activity; it does not reserve the command slot.
	 */
	public fn IsBusy() bool
	{
		return this.busy
	}

	/**
	 * Reports completion of the accepted command. Completion remains set until ClearResult or
	 * acceptance of another command.
	 */
	public fn HasResult() bool
	{
		return this.hasResult
	}

	/**
	 * Reports that the completed command exceeded STRETCH_TIMEOUT while waiting for
	 * released SCL to rise. Both drive requests are released; a new command clears
	 * this status. This does not generate recovery pulses for a stuck target.
	 */
	public fn HasTimedOut() bool
	{
		return this.timedOut
	}

	/**
	 * Returns ACK status for the completed write-byte/address command. Valid only after
	 * HasResult for that command.
	 */
	public fn WasAckReceived() bool
	{
		return this.resultAck
	}

	/**
	 * Reports NACK for a completed write-byte/address command. A clock-stretch timeout
	 * is a separate failure and returns false; inspect HasTimedOut after HasResult.
	 */
	public fn WasNackReceived() bool
	{
		return this.resultAck == false && !this.timedOut
	}

	/**
	 * Returns the retained received byte. Valid after HasResult for a read-byte command; does
	 * not clear completion.
	 */
	public fn GetReadByte() byte
	{
		return this.resultByte
	}

	/**
	 * Clears retained completion, timeout and ACK status without canceling the active command or
	 * clearing the received byte.
	 */
	public fn ClearResult()
	{
		state {
			this.hasResult = false
			this.timedOut = false
			this.resultAck = false
		}
	}

	/**
	 * Samples SCL and SDA in the component clock context for data reception and clock-stretch
	 * handling.
	 */
	process SampleBus()
	{
		state {
			this.observedScl = this.bus.scl
			this.observedSda = this.bus.sda
		}
	}

	/**
	 * Executes accepted byte commands with standard-mode phase timing. Waits for released SCL
	 * to rise, retaining bus drive between commands. A clock-stretch timeout releases
	 * both lines and completes with HasTimedOut set; no recovery pulses are emitted.
	 */
	process Main()
	{
		var stretchTicks: uint = 0
		var phaseTick: int = 0
		var phaseBit: int = 0
		var heldSclDriveLow: logic = 0b0
		var heldSdaDriveLow: logic = 0b0
		var commandInFlight: bool = false

		state Idle {
			stretchTicks = 0
			phaseTick = 0
			phaseBit = 0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (commandInFlight == true) {
				if (this.busy == false) {
					commandInFlight = false
				}

				goto Idle
			} elif (this.busy == false) {
				goto Idle
			} elif (this.command == COMMAND_START) {
				commandInFlight = true
				goto StartRelease
			} elif (this.command == COMMAND_STOP) {
				commandInFlight = true
				goto StopSdaLow
			} elif (this.command == COMMAND_WRITE) {
				commandInFlight = true
				goto WriteBitLow
			} elif (this.command == COMMAND_READ) {
				commandInFlight = true
				goto ReadBitLow
			} else {
				goto Idle
			}
		}

		state StartRelease {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					phaseTick = 0
					goto StartSetup
				} else {
					phaseTick = phaseTick + 1
					goto StartRelease
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto StartRelease
				}
			}
		}

		state StartSetup {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b1
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					phaseTick = 0
					goto StartSclLow
				} else {
					phaseTick = phaseTick + 1
					goto StartSetup
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto StartSetup
				}
			}
		}

		state StartSclLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b1
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				this.busy = false
				this.hasResult = true
				this.command = COMMAND_NONE
				phaseTick = 0
				goto Idle
			} else {
				phaseTick = phaseTick + 1
				goto StartSclLow
			}
		}

		state StopSdaLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b1
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				phaseTick = 0
				goto StopSclHigh
			} else {
				phaseTick = phaseTick + 1
				goto StopSdaLow
			}
		}

		state StopSclHigh {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b1
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					phaseTick = 0
					goto StopRelease
				} else {
					phaseTick = phaseTick + 1
					goto StopSclHigh
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto StopSclHigh
				}
			}
		}

		state StopRelease {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				this.busy = false
				this.hasResult = true
				this.command = COMMAND_NONE
				phaseTick = 0
				goto Idle
			} else {
				phaseTick = phaseTick + 1
				goto StopRelease
			}
		}

		state WriteBitLow {
			heldSclDriveLow = 0b1
			var byteValue: int = this.currentByte as int
			var shiftAmount: int = 7 - phaseBit
			var bitValue: int = (byteValue >> shiftAmount) & 1

			if (bitValue == 0) {
				heldSdaDriveLow = 0b1
			} else {
				heldSdaDriveLow = 0b0
			}

			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				phaseTick = 0
				goto WriteBitHigh
			} else {
				phaseTick = phaseTick + 1
				goto WriteBitLow
			}
		}

		state WriteBitHigh {
			heldSclDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					phaseTick = 0

					if (phaseBit == 7) {
						goto WriteAckLow
					} else {
						phaseBit = phaseBit + 1
						goto WriteBitLow
					}
				} else {
					phaseTick = phaseTick + 1
					goto WriteBitHigh
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto WriteBitHigh
				}
			}
		}

		state WriteAckLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				phaseTick = 0
				goto WriteAckHigh
			} else {
				phaseTick = phaseTick + 1
				goto WriteAckLow
			}
		}

		state WriteAckHigh {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					this.resultAck = (this.observedSda == 0b0)
					phaseTick = 0
					goto WriteAckFinishLow
				} else {
					phaseTick = phaseTick + 1
					goto WriteAckHigh
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto WriteAckHigh
				}
			}
		}

		state WriteAckFinishLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				this.busy = false
				this.hasResult = true
				this.command = COMMAND_NONE
				phaseTick = 0
				goto Idle
			} else {
				phaseTick = phaseTick + 1
				goto WriteAckFinishLow
			}
		}

		state ReadBitLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				phaseTick = 0
				goto ReadBitHigh
			} else {
				phaseTick = phaseTick + 1
				goto ReadBitLow
			}
		}

		state ReadBitHigh {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					phaseTick = 0
					goto ReadBitSample
				} else {
					phaseTick = phaseTick + 1
					goto ReadBitHigh
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto ReadBitHigh
				}
			}
		}

		state ReadBitSample {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0
				var sampledBit: int = 0

				if (this.observedSda == 0b1) {
					sampledBit = 1
				}

				var currentValue: int = this.resultByte as int
				var shiftAmount: int = 7 - phaseBit
				this.resultByte = (currentValue | (sampledBit << shiftAmount)) as byte

				if (phaseBit == 7) {
					goto SendAckLow
				} else {
					phaseBit = phaseBit + 1
					goto ReadBitLow
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto ReadBitHigh
				}
			}
		}

		state SendAckLow {
			heldSclDriveLow = 0b1

			if (this.sendAck == true) {
				heldSdaDriveLow = 0b1
			} else {
				heldSdaDriveLow = 0b0
			}

			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				phaseTick = 0
				goto SendAckHigh
			} else {
				phaseTick = phaseTick + 1
				goto SendAckLow
			}
		}

		state SendAckHigh {
			heldSclDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (this.observedScl == 0b1) {
				stretchTicks = 0

				if (phaseTick >= TICKS_PER_HALF_PERIOD) {
					heldSdaDriveLow = 0b0
					this.bus.sda_drive_low = heldSdaDriveLow
					phaseTick = 0
					goto SendAckFinishLow
				} else {
					phaseTick = phaseTick + 1
					goto SendAckHigh
				}
			} else {
				phaseTick = 0
				stretchTicks++

				if (stretchTicks >= STRETCH_TIMEOUT_TICKS) {
					goto ClockTimeout
				} else {
					goto SendAckHigh
				}
			}
		}

		state SendAckFinishLow {
			heldSclDriveLow = 0b1
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = heldSclDriveLow
			this.bus.sda_drive_low = heldSdaDriveLow

			if (phaseTick >= TICKS_PER_HALF_PERIOD) {
				this.busy = false
				this.hasResult = true
				this.command = COMMAND_NONE
				phaseTick = 0
				goto Idle
			} else {
				phaseTick = phaseTick + 1
				goto SendAckFinishLow
			}
		}

		// One failure path releases the bus and preserves a distinct completion reason.
		state ClockTimeout {
			heldSclDriveLow = 0b0
			heldSdaDriveLow = 0b0
			this.bus.scl_drive_low = 0b0
			this.bus.sda_drive_low = 0b0
			this.busy = false
			this.hasResult = true
			this.timedOut = true
			this.resultAck = false
			this.resultByte = 0x00
			this.command = COMMAND_NONE
			goto Idle
		}
	}
}