Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,8 @@ endif
@$(MD5SUM) test.hex
$(TINYGO) build -size short -o test.hex -target=digispark examples/pwm
@$(MD5SUM) test.hex
$(TINYGO) build -size short -o test.hex -target=digispark examples/mcp3008
@$(MD5SUM) test.hex
$(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1
@$(MD5SUM) test.hex
ifneq ($(XTENSA), 0)
Expand Down
167 changes: 167 additions & 0 deletions src/machine/machine_attiny85.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,170 @@ func (pwm PWM) Set(channel uint8, value uint32) {
}
}
}

// SPIConfig is used to store config info for SPI.
type SPIConfig struct {
Frequency uint32
LSBFirst bool
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) 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 {
// Delay cycles for frequency control (0 = max speed)
delayCycles uint16

// USICR value configured for the selected SPI mode
usicrValue uint8

// LSB-first mode (requires software bit reversal)
lsbFirst bool
}

// SPI0 is the USI-based SPI interface on the ATTiny85
var SPI0 = SPI{}

// Configure sets up the USI for SPI communication.
// Note: The user must configure and control the CS pin separately.
func (s *SPI) Configure(config SPIConfig) error {
// Configure USI pins (fixed by hardware)
// PB1 (DO/MOSI) -> OUTPUT
// PB2 (USCK/SCK) -> OUTPUT
// PB0 (DI/MISO) -> INPUT
PB1.Configure(PinConfig{Mode: PinOutput})
PB2.Configure(PinConfig{Mode: PinOutput})
PB0.Configure(PinConfig{Mode: PinInput})

// 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
// - 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
PB2.Low()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
case Mode1: // CPOL=0, CPHA=1: idle low, sample falling
PB2.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
PB2.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
PB2.High()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
default: // Default to Mode 0
PB2.Low()
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
}
avr.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

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
avr.USIDR.Set(b)

// Clear the counter overflow flag by writing 1 to it (AVR quirk)
// This also resets the 4-bit counter to 0
avr.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 !avr.USISR.HasBits(avr.USISR_USIOIF) {
avr.USICR.SetBits(avr.USICR_USITC)
}
} else {
// Frequency-controlled path: add delay between clock toggles
for !avr.USISR.HasBits(avr.USISR_USIOIF) {
avr.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 := avr.USIDR.Get()

// For LSB-first mode, reverse the received bits
if s.lsbFirst {
result = reverseByte(result)
}

return result, nil
}
2 changes: 1 addition & 1 deletion src/machine/spi.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !baremetal || atmega || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2)
//go:build !baremetal || atmega || attiny85 || esp32 || fe310 || k210 || nrf || (nxp && !mk66f18) || rp2040 || rp2350 || sam || (stm32 && !stm32f7x2 && !stm32l5x2)

package machine

Expand Down
2 changes: 1 addition & 1 deletion src/machine/spi_tx.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build atmega || fe310 || k210 || (nxp && !mk66f18) || (stm32 && !stm32f7x2 && !stm32l5x2)
//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.
Expand Down
Loading