Skip to content

Commit d88634f

Browse files
jespinoona-agent
andcommitted
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>
1 parent 707d37a commit d88634f

File tree

3 files changed

+113
-2
lines changed

3 files changed

+113
-2
lines changed

src/machine/machine_attiny85.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package machine
55
import (
66
"device/avr"
77
"runtime/volatile"
8+
"unsafe"
89
)
910

1011
const (
@@ -375,3 +376,113 @@ func (pwm PWM) Set(channel uint8, value uint32) {
375376
}
376377
}
377378
}
379+
380+
// SPIConfig is used to store config info for SPI.
381+
type SPIConfig struct {
382+
Frequency uint32
383+
LSBFirst bool
384+
Mode uint8
385+
}
386+
387+
// SPI is the USI-based SPI implementation for ATTiny85
388+
// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI (Universal Serial Interface)
389+
// which can be configured to work as SPI in "Three-wire mode"
390+
type SPI struct {
391+
// USI registers
392+
usidr *volatile.Register8 // Data Register
393+
usisr *volatile.Register8 // Status Register
394+
usicr *volatile.Register8 // Control Register
395+
396+
// The io pins for the USI-SPI
397+
// Note: Pin mapping is different from ISP programming pins
398+
sck Pin // PB2 (USCK) - Clock
399+
sdo Pin // PB1 (DO) - MOSI (Master Out Slave In)
400+
sdi Pin // PB0 (DI) - MISO (Master In Slave Out)
401+
cs Pin // User-defined CS pin (USI doesn't manage CS)
402+
}
403+
404+
// SPI0 is the USI-based SPI interface on the ATTiny85
405+
var SPI0 = &SPI{
406+
usidr: avr.USIDR,
407+
usisr: avr.USISR,
408+
usicr: avr.USICR,
409+
410+
sck: PB2, // USCK
411+
sdo: PB1, // DO (MOSI)
412+
sdi: PB0, // DI (MISO)
413+
cs: PB3, // Default CS pin (can be any available pin)
414+
}
415+
416+
// Configure sets up the USI for SPI communication
417+
func (s *SPI) Configure(config SPIConfig) error {
418+
// Validate configuration - check that USI registers are set
419+
if s.usicr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) ||
420+
s.usisr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) ||
421+
s.usidr == (*volatile.Register8)(unsafe.Pointer(uintptr(0))) {
422+
return errSPIInvalidMachineConfig
423+
}
424+
425+
// Configure pins
426+
// PB1 (DO/MOSI) -> OUTPUT
427+
// PB2 (USCK/SCK) -> OUTPUT
428+
// PB0 (DI/MISO) -> INPUT with pull-up
429+
s.sdo.Configure(PinConfig{Mode: PinOutput})
430+
s.sck.Configure(PinConfig{Mode: PinOutput})
431+
s.sdi.Configure(PinConfig{Mode: PinInput})
432+
433+
// Enable pull-up on MISO (PB0) for better signal integrity
434+
avr.PORTB.SetBits(1 << uint8(s.sdi))
435+
436+
// Configure CS pin - prevent glitches by setting HIGH first
437+
s.cs.High()
438+
s.cs.Configure(PinConfig{Mode: PinOutput})
439+
440+
// Reset USI data register
441+
s.usidr.Set(0)
442+
s.usisr.Set(0)
443+
444+
// Configure USI for SPI mode:
445+
// - USIWM0: Three-wire mode (SPI)
446+
// - USICS1: External clock source (software controlled via USITC)
447+
// - USICLK: Clock strobe - enables counter increment on USITC toggle
448+
//
449+
// Note: ATTiny85 USI doesn't have configurable frequency dividers like dedicated SPI hardware
450+
// The SPI clock speed is determined by how fast the software toggles the clock
451+
// For now, we'll ignore the Frequency parameter as it runs at maximum software speed
452+
//
453+
// Note: LSBFirst and Mode configurations are not directly supported by USI
454+
// These would need to be implemented in software if required
455+
// For now, we use the standard MSB-first, Mode 0 (CPOL=0, CPHA=0)
456+
s.usicr.Set(avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK)
457+
458+
return nil
459+
}
460+
461+
// Transfer performs a single byte SPI transfer (send and receive simultaneously)
462+
// This implements the USI-based SPI transfer using the "clock strobing" technique
463+
func (s *SPI) Transfer(b byte) (byte, error) {
464+
// Load the byte to transmit into the USI Data Register
465+
s.usidr.Set(b)
466+
467+
// Clear the counter overflow flag by writing 1 to it (AVR quirk)
468+
// This also resets the 4-bit counter to 0
469+
s.usisr.Set(avr.USISR_USIOIF)
470+
471+
// Clock the data out/in
472+
// We need 16 clock toggles (8 bits × 2 edges per bit)
473+
// The USI counter counts each clock edge, so it overflows at 16
474+
//
475+
// IMPORTANT: Only toggle USITC here, not USICLK!
476+
// - USITC toggles the clock pin
477+
// - With USICS1 set (software clock strobe mode), the data shifts on clock edges
478+
// - USICLK is a separate strobe that would cause extra shifts if set here
479+
//
480+
// The USICR register was configured in Configure() with USIWM0 | USICS1 | USICLK.
481+
// We use SetBits to preserve that configuration and only toggle USITC.
482+
for !s.usisr.HasBits(avr.USISR_USIOIF) {
483+
s.usicr.SetBits(avr.USICR_USITC)
484+
}
485+
486+
// After 8 bits are transferred, return the received byte
487+
return s.usidr.Get(), nil
488+
}

src/machine/spi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !baremetal || atmega || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2)
1+
//go:build !baremetal || atmega || attiny85 || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2)
22

33
package machine
44

src/machine/spi_tx.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build atmega || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2)
1+
//go:build atmega || attiny85 || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2)
22

33
// This file implements the SPI Tx function for targets that don't have a custom
44
// (faster) implementation for it.

0 commit comments

Comments
 (0)