Source code for openlabctrl.serial.spi


from ..io.digital import DigitalIo


[docs] class SPI: """ Bit-banged SPI master over a :class:`~openlabctrl.io.digital.DigitalIo` port. Supports all four SPI modes (CPOL × CPHA) and configurable pin assignment via single-bit masks. Call :meth:`io_config` once to initialize pin directions and idle states, then use :meth:`cs_low`, :meth:`write`, and :meth:`cs_high` to drive a transaction. :param io: :class:`~openlabctrl.io.digital.DigitalIo` instance used for bit-banging. :param clk_div: Clock divider relative to the frame clock. Must be a positive even integer ≥ 2. The SPI clock half-period is ``clk_div / 2`` frame clock cycles. :param cpol: Clock polarity. ``0`` = idle low, ``1`` = idle high. :param cpha: Clock phase. ``0`` = data sampled on leading edge, ``1`` = on trailing edge. :param sclk_mask: Single-bit mask selecting the SCLK pin. :param cs_mask: Single-bit mask selecting the CS pin, active-low. :param mosi_mask: Single-bit mask selecting the MOSI pin. :param miso_mask: Single-bit mask selecting the MISO pin. """ def __init__(self, io: DigitalIo, clk_div: int, cpol: int, cpha: int, sclk_mask: int = 0b0001, cs_mask: int = 0b0010, mosi_mask: int = 0b0100, miso_mask: int = 0b1000): if type(io) != DigitalIo: raise Exception(f"SPI requires a DigitalIo instance for IO control. Got {type(io)} instead.") if clk_div < 2 or clk_div % 2 != 0: raise Exception("Clock divider must be a positive multiple of 2.") self._io = io self._clk_div = clk_div self._cpol = cpol self._cpha = cpha self._sclk_mask = sclk_mask self._cs_mask = cs_mask self._mosi_mask = mosi_mask self._miso_mask = miso_mask
[docs] def io_config(self): """ Configure pin directions and set all SPI signals to their idle state. Sets MISO as input and SCLK, CS, MOSI as outputs. SCLK is driven to its idle level (determined by ``cpol``), CS is deasserted (high), and MOSI is driven low. Call this once before the first transaction. """ self._io.tristate(val=self._miso_mask, mask=0xf) clk = self._cpol * self._sclk_mask cs = self._cs_mask mosi = 0 self._io.output(val=(clk | cs | mosi), mask=(self._sclk_mask | self._cs_mask | self._mosi_mask))
[docs] def cs_low(self, wait: int = 1): """ Assert chip select (drive CS low) and wait. :param wait: Number of half-clock periods to wait after asserting CS (actual delay = ``clk_div * wait`` frame clock cycles). """ self._io.output(val=0, mask=self._cs_mask) self._io.delay(self._clk_div * wait)
[docs] def cs_high(self, wait: int = 1): """ Deassert chip select (drive CS high) after a settling delay. :param wait: Number of half-clock periods to wait before deasserting CS (actual delay = ``clk_div * wait`` frame clock cycles). """ self._io.delay(self._clk_div * wait) self._io.output(val=self._cs_mask, mask=self._cs_mask)
[docs] def write(self, data: int, size: int): """ Transmit ``size`` bits of ``data`` MSB-first over MOSI. Clock edges follow the configured CPOL/CPHA mode. SCLK is returned to its idle level at the end of the transfer. CS must be asserted before calling this method (see :meth:`cs_low`). :param data: Integer value to transmit. :param size: Number of bits to transmit (starting from the MSB). """ for i in range(size): mosi = ((data >> (size - 1 - i)) & 0x1) * self._mosi_mask clk = (self._cpol ^ self._cpha) * self._sclk_mask self._io.output(val=(mosi | clk), mask=(self._mosi_mask | self._sclk_mask)) self._io.delay(self._clk_div // 2) clk ^= self._sclk_mask self._io.output(val=clk, mask=self._sclk_mask) self._io.delay(self._clk_div // 2) clk = self._cpol * self._sclk_mask self._io.output(val=clk, mask=self._sclk_mask)