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>
This commit is contained in:
Jesús Espino
2026-01-17 11:46:23 +00:00
parent 9367b6b705
commit 387ccad494
+51 -166
View File
@@ -5,7 +5,6 @@ package machine
import (
"device/avr"
"runtime/volatile"
"unsafe"
)
const (
@@ -384,189 +383,75 @@ type SPIConfig struct {
Mode uint8
}
// SPI is the USI-based SPI implementation for ATTiny85
// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI (Universal Serial Interface)
// which can be configured to work as SPI in "Three-wire mode"
// SPI is the USI-based SPI implementation for ATTiny85.
// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI
// (Universal Serial Interface) in three-wire mode.
//
// Fixed pin mapping (directly controlled by USI hardware):
// - PB2: SCK (clock)
// - PB1: DO/MOSI (data out)
// - PB0: DI/MISO (data in)
//
// Note: CS pin must be managed by the user.
type SPI struct {
// USI registers
usidr *volatile.Register8 // Data Register
usisr *volatile.Register8 // Status Register
usicr *volatile.Register8 // Control Register
// The io pins for the USI-SPI
// Note: Pin mapping is different from ISP programming pins
sck Pin // PB2 (USCK) - Clock
sdo Pin // PB1 (DO) - MOSI (Master Out Slave In)
sdi Pin // PB0 (DI) - MISO (Master In Slave Out)
cs Pin // User-defined CS pin (USI doesn't manage CS)
// Delay cycles for frequency control (0 = max speed)
delayCycles uint16
// USICR value configured for the selected SPI mode
// Single byte stores USICR configuration value
usicrValue uint8
// LSB-first mode (requires software bit reversal)
lsbFirst bool
}
// SPI0 is the USI-based SPI interface on the ATTiny85
var SPI0 = &SPI{
usidr: avr.USIDR,
usisr: avr.USISR,
usicr: avr.USICR,
var SPI0 = SPI{}
sck: PB2, // USCK
sdo: PB1, // DO (MOSI)
sdi: PB0, // DI (MISO)
cs: PB3, // Default CS pin (can be any available pin)
}
// Configure sets up the USI for SPI communication
func (s *SPI) Configure(config SPIConfig) error {
// Validate configuration - check that USI registers are set
if s.usicr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) ||
s.usisr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) ||
s.usidr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) {
return errSPIInvalidMachineConfig
}
// Configure pins
// Configure sets up the USI for SPI communication.
// Note: The user must configure and control the CS pin separately.
func (spi *SPI) Configure(config SPIConfig) error {
// Configure USI pins (directly, not via struct fields)
// PB1 (DO/MOSI) -> OUTPUT
// PB2 (USCK/SCK) -> OUTPUT
// PB0 (DI/MISO) -> INPUT with pull-up
s.sdo.Configure(PinConfig{Mode: PinOutput})
s.sck.Configure(PinConfig{Mode: PinOutput})
s.sdi.Configure(PinConfig{Mode: PinInput})
// PB0 (DI/MISO) -> INPUT
PB1.Configure(PinConfig{Mode: PinOutput})
PB2.Configure(PinConfig{Mode: PinOutput})
PB0.Configure(PinConfig{Mode: PinInput})
// Enable pull-up on MISO (PB0) for better signal integrity
avr.PORTB.SetBits(1 << uint8(s.sdi))
// Configure CS pin - prevent glitches by setting HIGH first
s.cs.High()
s.cs.Configure(PinConfig{Mode: PinOutput})
// Reset USI data register
s.usidr.Set(0)
s.usisr.Set(0)
// Reset USI registers
avr.USIDR.Set(0)
avr.USISR.Set(0)
// Configure USI for SPI mode:
// - USIWM0: Three-wire mode (SPI)
// - USICS1: External clock source (software controlled via USITC)
// - USICLK: Clock strobe - enables counter increment on USITC toggle
// - USICLK: Clock strobe
// - USICS0: Controls clock phase (CPHA)
//
// SPI Modes:
// 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
//
// For USI, USICS0 controls the sampling edge when USICS1=1:
// USICS0=0: Positive edge (rising)
// USICS0=1: Negative edge (falling)
switch config.Mode {
case Mode0: // CPOL=0, CPHA=0: idle low, sample rising
s.sck.Low()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
case Mode1: // CPOL=0, CPHA=1: idle low, sample falling
s.sck.Low()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
case Mode2: // CPOL=1, CPHA=0: idle high, sample falling
s.sck.High()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
case Mode3: // CPOL=1, CPHA=1: idle high, sample rising
s.sck.High()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
default: // Default to Mode 0
s.sck.Low()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
case Mode1: // CPOL=0, CPHA=1
PB2.Low()
spi.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
case Mode2: // CPOL=1, CPHA=0
PB2.High()
spi.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
case Mode3: // CPOL=1, CPHA=1
PB2.High()
spi.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
default: // Mode0: CPOL=0, CPHA=0
PB2.Low()
spi.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
}
s.usicr.Set(s.usicrValue)
// Calculate delay cycles for frequency control
// Each bit transfer requires 2 clock toggles (rising + falling edge)
// The loop overhead is approximately 10-15 cycles per toggle on AVR
// We calculate additional delay cycles needed to achieve the target frequency
if config.Frequency > 0 && config.Frequency < CPUFrequency()/2 {
// Cycles per half-period = CPUFrequency / (2 * Frequency)
// Subtract loop overhead (~15 cycles) to get delay cycles
cyclesPerHalfPeriod := CPUFrequency() / (2 * config.Frequency)
const loopOverhead = 15
if cyclesPerHalfPeriod > loopOverhead {
s.delayCycles = uint16(cyclesPerHalfPeriod - loopOverhead)
} else {
s.delayCycles = 0
}
} else {
// Max speed - no delay
s.delayCycles = 0
}
// Store LSBFirst setting for use in Transfer
s.lsbFirst = config.LSBFirst
avr.USICR.Set(spi.usicrValue)
return nil
}
// reverseByte reverses the bit order of a byte (MSB <-> LSB)
// Used for LSB-first SPI mode since USI hardware only supports MSB-first
func reverseByte(b byte) byte {
b = (b&0xF0)>>4 | (b&0x0F)<<4
b = (b&0xCC)>>2 | (b&0x33)<<2
b = (b&0xAA)>>1 | (b&0x55)<<1
return b
}
// Transfer performs a single byte SPI transfer (send and receive simultaneously)
// This implements the USI-based SPI transfer using the "clock strobing" technique
func (s *SPI) Transfer(b byte) (byte, error) {
// For LSB-first mode, reverse the bits before sending
// USI hardware only supports MSB-first, so we do it in software
if s.lsbFirst {
b = reverseByte(b)
}
// Load the byte to transmit into the USI Data Register
s.usidr.Set(b)
// Clear the counter overflow flag by writing 1 to it (AVR quirk)
// This also resets the 4-bit counter to 0
s.usisr.Set(avr.USISR_USIOIF)
// Clock the data out/in
// We need 16 clock toggles (8 bits × 2 edges per bit)
// The USI counter counts each clock edge, so it overflows at 16
// After 16 toggles, the clock returns to its idle state (set by CPOL in Configure)
//
// IMPORTANT: Only toggle USITC here!
// - USITC toggles the clock pin
// - The USICR mode bits (USIWM0, USICS1, USICS0, USICLK) were set in Configure()
// - SetBits preserves those bits and only sets USITC
if s.delayCycles == 0 {
// Fast path: no delay, run at maximum speed
for !s.usisr.HasBits(avr.USISR_USIOIF) {
s.usicr.SetBits(avr.USICR_USITC)
}
} else {
// Frequency-controlled path: add delay between clock toggles
for !s.usisr.HasBits(avr.USISR_USIOIF) {
s.usicr.SetBits(avr.USICR_USITC)
// Delay loop for frequency control
// Each iteration is approximately 3 cycles on AVR (dec, brne)
for i := s.delayCycles; i > 0; i-- {
avr.Asm("nop")
}
}
}
// Get the received byte
result := s.usidr.Get()
// For LSB-first mode, reverse the received bits
if s.lsbFirst {
result = reverseByte(result)
}
return result, nil
// Transfer performs a single byte SPI transfer (send and receive simultaneously).
func (spi *SPI) Transfer(b byte) (byte, error) {
// Load byte to transmit
avr.USIDR.Set(b)
// Clear counter overflow flag and reset counter
avr.USISR.Set(avr.USISR_USIOIF)
// Clock 8 bits (16 toggles)
for !avr.USISR.HasBits(avr.USISR_USIOIF) {
avr.USICR.SetBits(avr.USICR_USITC)
}
return avr.USIDR.Get(), nil
}