Files
tinygo/src/machine/spi_tx.go
T
Jesús Espino 5d8e071bfb machine/attiny85: add USI-based SPI support (#5181)
* machine/attiny85: add USI-based SPI support

Implement SPI communication for ATTiny85 using the USI (Universal Serial
Interface) hardware in three-wire mode. The ATTiny85 lacks dedicated SPI
hardware but can emulate SPI using the USI module with software clock
strobing.

Implementation details:
- Configure USI in three-wire mode for SPI operation
- Use clock strobing technique to shift data in/out
- Pin mapping: PB2 (SCK), PB1 (MOSI/DO), PB0 (MISO/DI)
- Support both Transfer() and Tx() methods

The implementation uses the USI control register (USICR) to toggle the
clock pin, which triggers automatic bit shifting in hardware. This is
more efficient than pure software bit-banging.

Current limitations:
- Frequency configuration not yet implemented (runs at max software speed)
- Only SPI Mode 0 (CPOL=0, CPHA=0) supported
- Only MSB-first bit order supported

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Co-authored-by: Ona <no-reply@ona.com>

* machine/attiny85: add SPI frequency configuration support

Add software-based frequency control for USI SPI. The ATtiny85 USI lacks
hardware prescalers, so frequency is controlled via delay loops between
clock toggles.

- Calculate delay cycles based on requested frequency and CPU clock
- Fast path (no delay) when frequency is 0 or max speed requested
- Delay loop uses nop instructions for timing control

Co-authored-by: Ona <no-reply@ona.com>

* machine/attiny85: add SPI mode configuration support

Add support for all 4 SPI modes (Mode 0-3) using USI hardware:
- Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge
- Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge
- Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge
- Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge

CPOL is controlled by setting the clock pin idle state.
CPHA is controlled via the USICS0 bit in USICR.

Co-authored-by: Ona <no-reply@ona.com>

* machine/attiny85: add LSB-first bit order support

Add software-based LSB-first support for USI SPI. The USI hardware only
supports MSB-first, so bit reversal is done in software before sending
and after receiving.

Uses an efficient parallel bit swap algorithm (3 operations) to reverse
the byte.

Co-authored-by: Ona <no-reply@ona.com>

* GNUmakefile: add mcp3008 SPI example to digispark smoketest

Test the USI-based SPI implementation for ATtiny85/digispark.

Co-authored-by: Ona <no-reply@ona.com>

* machine/attiny85: minimize SPI RAM footprint

Reduce SPI struct from ~14 bytes to 1 byte to fit in ATtiny85's limited
512 bytes of RAM.

Changes:
- Remove register pointers (use avr.USIDR/USISR/USICR directly)
- Remove pin fields (USI pins are fixed: PB0/PB1/PB2)
- Remove CS pin management (user must handle CS)
- Remove frequency control (runs at max speed)
- Remove LSBFirst support

The SPI struct now only stores the USICR configuration byte.

Co-authored-by: Ona <no-reply@ona.com>

* Revert "machine/attiny85: minimize SPI RAM footprint"

This reverts commit 387ccad494.

Co-authored-by: Ona <no-reply@ona.com>

* machine/attiny85: reduce SPI RAM usage by 10 bytes

Remove unnecessary fields from SPI struct while keeping all functionality:
- Remove register pointers (use avr.USIDR/USISR/USICR directly)
- Remove pin fields (USI pins are fixed: PB0/PB1/PB2)
- Remove CS pin (user must manage it, standard practice)

Kept functional fields:
- delayCycles for frequency control
- usicrValue for SPI mode support
- lsbFirst for bit order support

SPI struct reduced from 14 bytes to 4 bytes.

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
2026-01-17 21:22:15 +01:00

62 lines
1.5 KiB
Go

//go:build atmega || attiny85 || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2)
// This file implements the SPI Tx function for targets that don't have a custom
// (faster) implementation for it.
package machine
// Tx handles read/write operation for SPI interface. Since SPI is a synchronous write/read
// interface, there must always be the same number of bytes written as bytes read.
// The Tx method knows about this, and offers a few different ways of calling it.
//
// This form sends the bytes in tx buffer, putting the resulting bytes read into the rx buffer.
// Note that the tx and rx buffers must be the same size:
//
// spi.Tx(tx, rx)
//
// This form sends the tx buffer, ignoring the result. Useful for sending "commands" that return zeros
// until all the bytes in the command packet have been received:
//
// spi.Tx(tx, nil)
//
// This form sends zeros, putting the result into the rx buffer. Good for reading a "result packet":
//
// spi.Tx(nil, rx)
func (spi *SPI) Tx(w, r []byte) error {
var err error
switch {
case w == nil:
// read only, so write zero and read a result.
for i := range r {
r[i], err = spi.Transfer(0)
if err != nil {
return err
}
}
case r == nil:
// write only
for _, b := range w {
_, err = spi.Transfer(b)
if err != nil {
return err
}
}
default:
// write/read
if len(w) != len(r) {
return ErrTxInvalidSliceSize
}
for i, b := range w {
r[i], err = spi.Transfer(b)
if err != nil {
return err
}
}
}
return nil
}