mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-04 19:17:47 +00:00
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 387ccad494.
* 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.
---------
This commit is contained in:
committed by
deadprogram
parent
a1b44ddd58
commit
bb8196653f
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user