Skip to content

Commit bb81966

Browse files
jespinodeadprogram
authored andcommitted
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 * 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 * 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. * 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. * GNUmakefile: add mcp3008 SPI example to digispark smoketest Test the USI-based SPI implementation for ATtiny85/digispark. * 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. * Revert "machine/attiny85: minimize SPI RAM footprint" This reverts commit 387ccad. * 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. ---------
1 parent a1b44dd commit bb81966

File tree

4 files changed

+171
-2
lines changed

4 files changed

+171
-2
lines changed

GNUmakefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,8 @@ endif
898898
@$(MD5SUM) test.hex
899899
$(TINYGO) build -size short -o test.hex -target=digispark examples/pwm
900900
@$(MD5SUM) test.hex
901+
$(TINYGO) build -size short -o test.hex -target=digispark examples/mcp3008
902+
@$(MD5SUM) test.hex
901903
$(TINYGO) build -size short -o test.hex -target=digispark -gc=leaking examples/blinky1
902904
@$(MD5SUM) test.hex
903905
ifneq ($(XTENSA), 0)

src/machine/machine_attiny85.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,170 @@ func (pwm PWM) Set(channel uint8, value uint32) {
375375
}
376376
}
377377
}
378+
379+
// SPIConfig is used to store config info for SPI.
380+
type SPIConfig struct {
381+
Frequency uint32
382+
LSBFirst bool
383+
Mode uint8
384+
}
385+
386+
// SPI is the USI-based SPI implementation for ATTiny85.
387+
// The ATTiny85 doesn't have dedicated SPI hardware, but uses the USI
388+
// (Universal Serial Interface) in three-wire mode.
389+
//
390+
// Fixed pin mapping (directly controlled by USI hardware):
391+
// - PB2: SCK (clock)
392+
// - PB1: DO/MOSI (data out)
393+
// - PB0: DI/MISO (data in)
394+
//
395+
// Note: CS pin must be managed by the user.
396+
type SPI struct {
397+
// Delay cycles for frequency control (0 = max speed)
398+
delayCycles uint16
399+
400+
// USICR value configured for the selected SPI mode
401+
usicrValue uint8
402+
403+
// LSB-first mode (requires software bit reversal)
404+
lsbFirst bool
405+
}
406+
407+
// SPI0 is the USI-based SPI interface on the ATTiny85
408+
var SPI0 = SPI{}
409+
410+
// Configure sets up the USI for SPI communication.
411+
// Note: The user must configure and control the CS pin separately.
412+
func (s *SPI) Configure(config SPIConfig) error {
413+
// Configure USI pins (fixed by hardware)
414+
// PB1 (DO/MOSI) -> OUTPUT
415+
// PB2 (USCK/SCK) -> OUTPUT
416+
// PB0 (DI/MISO) -> INPUT
417+
PB1.Configure(PinConfig{Mode: PinOutput})
418+
PB2.Configure(PinConfig{Mode: PinOutput})
419+
PB0.Configure(PinConfig{Mode: PinInput})
420+
421+
// Reset USI registers
422+
avr.USIDR.Set(0)
423+
avr.USISR.Set(0)
424+
425+
// Configure USI for SPI mode:
426+
// - USIWM0: Three-wire mode (SPI)
427+
// - USICS1: External clock source (software controlled via USITC)
428+
// - USICLK: Clock strobe - enables counter increment on USITC toggle
429+
// - USICS0: Controls clock phase (CPHA)
430+
//
431+
// SPI Modes:
432+
// Mode 0 (CPOL=0, CPHA=0): Clock idle low, sample on rising edge
433+
// Mode 1 (CPOL=0, CPHA=1): Clock idle low, sample on falling edge
434+
// Mode 2 (CPOL=1, CPHA=0): Clock idle high, sample on falling edge
435+
// Mode 3 (CPOL=1, CPHA=1): Clock idle high, sample on rising edge
436+
//
437+
// For USI, USICS0 controls the sampling edge when USICS1=1:
438+
// USICS0=0: Positive edge (rising)
439+
// USICS0=1: Negative edge (falling)
440+
switch config.Mode {
441+
case Mode0: // CPOL=0, CPHA=0: idle low, sample rising
442+
PB2.Low()
443+
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
444+
case Mode1: // CPOL=0, CPHA=1: idle low, sample falling
445+
PB2.Low()
446+
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
447+
case Mode2: // CPOL=1, CPHA=0: idle high, sample falling
448+
PB2.High()
449+
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICS0 | avr.USICR_USICLK
450+
case Mode3: // CPOL=1, CPHA=1: idle high, sample rising
451+
PB2.High()
452+
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
453+
default: // Default to Mode 0
454+
PB2.Low()
455+
s.usicrValue = avr.USICR_USIWM0 | avr.USICR_USICS1 | avr.USICR_USICLK
456+
}
457+
avr.USICR.Set(s.usicrValue)
458+
459+
// Calculate delay cycles for frequency control
460+
// Each bit transfer requires 2 clock toggles (rising + falling edge)
461+
// The loop overhead is approximately 10-15 cycles per toggle on AVR
462+
// We calculate additional delay cycles needed to achieve the target frequency
463+
if config.Frequency > 0 && config.Frequency < CPUFrequency()/2 {
464+
// Cycles per half-period = CPUFrequency / (2 * Frequency)
465+
// Subtract loop overhead (~15 cycles) to get delay cycles
466+
cyclesPerHalfPeriod := CPUFrequency() / (2 * config.Frequency)
467+
const loopOverhead = 15
468+
if cyclesPerHalfPeriod > loopOverhead {
469+
s.delayCycles = uint16(cyclesPerHalfPeriod - loopOverhead)
470+
} else {
471+
s.delayCycles = 0
472+
}
473+
} else {
474+
// Max speed - no delay
475+
s.delayCycles = 0
476+
}
477+
478+
// Store LSBFirst setting for use in Transfer
479+
s.lsbFirst = config.LSBFirst
480+
481+
return nil
482+
}
483+
484+
// reverseByte reverses the bit order of a byte (MSB <-> LSB)
485+
// Used for LSB-first SPI mode since USI hardware only supports MSB-first
486+
func reverseByte(b byte) byte {
487+
b = (b&0xF0)>>4 | (b&0x0F)<<4
488+
b = (b&0xCC)>>2 | (b&0x33)<<2
489+
b = (b&0xAA)>>1 | (b&0x55)<<1
490+
return b
491+
}
492+
493+
// Transfer performs a single byte SPI transfer (send and receive simultaneously)
494+
// This implements the USI-based SPI transfer using the "clock strobing" technique
495+
func (s *SPI) Transfer(b byte) (byte, error) {
496+
// For LSB-first mode, reverse the bits before sending
497+
// USI hardware only supports MSB-first, so we do it in software
498+
if s.lsbFirst {
499+
b = reverseByte(b)
500+
}
501+
502+
// Load the byte to transmit into the USI Data Register
503+
avr.USIDR.Set(b)
504+
505+
// Clear the counter overflow flag by writing 1 to it (AVR quirk)
506+
// This also resets the 4-bit counter to 0
507+
avr.USISR.Set(avr.USISR_USIOIF)
508+
509+
// Clock the data out/in
510+
// We need 16 clock toggles (8 bits × 2 edges per bit)
511+
// The USI counter counts each clock edge, so it overflows at 16
512+
// After 16 toggles, the clock returns to its idle state (set by CPOL in Configure)
513+
//
514+
// IMPORTANT: Only toggle USITC here!
515+
// - USITC toggles the clock pin
516+
// - The USICR mode bits (USIWM0, USICS1, USICS0, USICLK) were set in Configure()
517+
// - SetBits preserves those bits and only sets USITC
518+
if s.delayCycles == 0 {
519+
// Fast path: no delay, run at maximum speed
520+
for !avr.USISR.HasBits(avr.USISR_USIOIF) {
521+
avr.USICR.SetBits(avr.USICR_USITC)
522+
}
523+
} else {
524+
// Frequency-controlled path: add delay between clock toggles
525+
for !avr.USISR.HasBits(avr.USISR_USIOIF) {
526+
avr.USICR.SetBits(avr.USICR_USITC)
527+
// Delay loop for frequency control
528+
// Each iteration is approximately 3 cycles on AVR (dec, brne)
529+
for i := s.delayCycles; i > 0; i-- {
530+
avr.Asm("nop")
531+
}
532+
}
533+
}
534+
535+
// Get the received byte
536+
result := avr.USIDR.Get()
537+
538+
// For LSB-first mode, reverse the received bits
539+
if s.lsbFirst {
540+
result = reverseByte(result)
541+
}
542+
543+
return result, nil
544+
}

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)