diff --git a/src/machine/machine_esp32.go b/src/machine/machine_esp32.go index ed878677..9936f80d 100644 --- a/src/machine/machine_esp32.go +++ b/src/machine/machine_esp32.go @@ -4,7 +4,9 @@ package machine import ( "device/esp" + "errors" "runtime/volatile" + "unsafe" ) const peripheralClock = 80000000 // 80MHz @@ -15,6 +17,10 @@ func CPUFrequency() uint32 { return 160e6 // 160MHz } +var ( + ErrInvalidSPIBus = errors.New("machine: invalid SPI bus") +) + type PinMode uint8 const ( @@ -26,6 +32,24 @@ const ( // Configure this pin with the given configuration. func (p Pin) Configure(config PinConfig) { + // Output function 256 is a special value reserved for use as a regular GPIO + // pin. Peripherals (SPI etc) can set a custom output function by calling + // lowercase configure() instead with a signal name. + p.configure(config, 256) +} + +// configure is the same as Configure, but allows for setting a specific input +// or output signal. +// Signals are always routed through the GPIO matrix for simplicity. Output +// signals are configured in FUNCx_OUT_SEL_CFG which selects a particular signal +// to output on a given pin. Input signals are configured in FUNCy_IN_SEL_CFG, +// which sets the pin to use for a particular input signal. +func (p Pin) configure(config PinConfig, signal uint32) { + if p == NoPin { + // This simplifies pin configuration in peripherals such as SPI. + return + } + var muxConfig uint32 // The mux configuration. // Configure this pin as a GPIO pin. @@ -56,6 +80,10 @@ func (p Pin) Configure(config PinConfig) { } else { esp.GPIO.ENABLE1_W1TS.Set(1 << (p - 32)) } + // Set the signal to read the output value from. It can be a peripheral + // output signal, or the special value 256 which indicates regular GPIO + // usage. + p.outFunc().Set(signal) case PinInput, PinInputPullup, PinInputPulldown: // Clear the 'output enable' bit. if p < 32 { @@ -63,9 +91,29 @@ func (p Pin) Configure(config PinConfig) { } else { esp.GPIO.ENABLE1_W1TC.Set(1 << (p - 32)) } + if signal != 256 { + // Signal is a peripheral function (not a simple GPIO). Connect this + // signal to the pin. + // Note that outFunc and inFunc work in the opposite direction. + // outFunc configures a pin to use a given output signal, while + // inFunc specifies a pin to use to read the signal from. + inFunc(signal).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(p)<= 40e6 { + // Don't use a prescaler, but directly connect to the APB clock. This + // results in a SPI clock frequency of 40MHz. + clockReg |= esp.SPI_CLOCK_CLK_EQU_SYSCLK + } else { + // Use a prescaler for frequencies below 40MHz. They will get rounded + // down to the next possible frequency (20MHz, 13.3MHz, 10MHz, 8MHz, + // 6.7MHz, 5.7MHz, 5MHz, etc). + // This code is much simpler than how ESP-IDF configures the frequency, + // but should be just as accurate. The only exception is for frequencies + // below 4883Hz, which will need special support. + if config.Frequency < 4883 { + // The current lower limit is 4883Hz. + // The hardware supports lower frequencies by setting the h and n + // variables, but that's not yet implemented. + config.Frequency = 4883 + } + // The prescaler value is 40e6 / config.Frequency, but rounded up so + // that the actual frequency is never higher than the frequency + // requested in config.Frequency. + var ( + pre uint32 = (40e6 + config.Frequency - 1) / config.Frequency + n uint32 = 2 // this value seems to equal the number of ticks per SPI clock tick + h uint32 = 1 // must be half of n according to the formula in the reference manual + l uint32 = n // must equal n according to the reference manual + ) + clockReg |= (pre - 1) << esp.SPI_CLOCK_CLKDIV_PRE_Pos + clockReg |= (n - 1) << esp.SPI_CLOCK_CLKCNT_N_Pos + clockReg |= (h - 1) << esp.SPI_CLOCK_CLKCNT_H_Pos + clockReg |= (l - 1) << esp.SPI_CLOCK_CLKCNT_L_Pos + } + spi.Bus.CLOCK.Set(clockReg) + + // SPI_CTRL_REG controls bit order. + var ctrlReg uint32 + if config.LSBFirst { + ctrlReg |= esp.SPI_CTRL_WR_BIT_ORDER + ctrlReg |= esp.SPI_CTRL_RD_BIT_ORDER + } + spi.Bus.CTRL.Set(ctrlReg) + + // SPI_CTRL2_REG, SPI_USER_REG and SPI_PIN_REG control SPI clock polarity + // (mode), among others. + var ctrl2Reg, userReg, pinReg uint32 + // For mode configuration, see table 29 in the reference manual (page 128). + var delayMode uint32 + switch config.Mode { + case 0: + delayMode = 2 + case 1: + delayMode = 1 + userReg |= esp.SPI_USER_CK_OUT_EDGE + case 2: + delayMode = 1 + userReg |= esp.SPI_USER_CK_OUT_EDGE + pinReg |= esp.SPI_PIN_CK_IDLE_EDGE + case 3: + delayMode = 2 + pinReg |= esp.SPI_PIN_CK_IDLE_EDGE + } + // Extra configuration necessary for correct data input at high frequencies. + // This is only necessary when MISO goes through the GPIO matrix (which it + // currently does). + if config.Frequency >= 40e6 { + // Delay mode must be set to 0 and SPI_USR_DUMMY_CYCLELEN should be set + // to 0 (the default). + userReg |= esp.SPI_USER_USR_DUMMY + } else if config.Frequency >= 20e6 { + // Nothing to do here, delay mode should be set to 0 according to the + // datasheet. + } else { + // Follow the delay mode as given in table 29 on page 128 of the + // reference manual. + // Note that this is only specified for SPI frequency of 10MHz and + // below (≤Fapb/8), so 13.3MHz appears to be left unspecified. + ctrl2Reg |= delayMode << esp.SPI_CTRL2_MOSI_DELAY_MODE_Pos + } + // Enable full-duplex communication. + userReg |= esp.SPI_USER_DOUTDIN + userReg |= esp.SPI_USER_USR_MOSI + // Write values to registers. + spi.Bus.CTRL2.Set(ctrl2Reg) + spi.Bus.USER.Set(userReg) + spi.Bus.PIN.Set(pinReg) + + // Configure pins. + // TODO: use direct output if possible, if the configured pins match the + // possible direct configurations (e.g. for SPI2, when SCK is pin 14 etc). + if spi.Bus == esp.SPI2 { + config.SCK.configure(PinConfig{Mode: PinOutput}, 8) // HSPICLK + config.SDI.configure(PinConfig{Mode: PinInput}, 9) // HSPIQ + config.SDO.configure(PinConfig{Mode: PinOutput}, 10) // HSPID + } else if spi.Bus == esp.SPI3 { + config.SCK.configure(PinConfig{Mode: PinOutput}, 63) // VSPICLK + config.SDI.configure(PinConfig{Mode: PinInput}, 64) // VSPIQ + config.SDO.configure(PinConfig{Mode: PinOutput}, 65) // VSPID + } else { + // Don't know how to configure this bus. + return ErrInvalidSPIBus + } + + return nil +} + +// Transfer writes/reads a single byte using the SPI interface. If you need to +// transfer larger amounts of data, Tx will be faster. +func (spi SPI) Transfer(w byte) (byte, error) { + spi.Bus.MISO_DLEN.Set(7 << esp.SPI_MISO_DLEN_USR_MISO_DBITLEN_Pos) + spi.Bus.MOSI_DLEN.Set(7 << esp.SPI_MOSI_DLEN_USR_MOSI_DBITLEN_Pos) + + spi.Bus.W0.Set(uint32(w)) + + // Send/receive byte. + spi.Bus.CMD.Set(esp.SPI_CMD_USR) + for spi.Bus.CMD.Get() != 0 { + } + + // The received byte is stored in W0. + return byte(spi.Bus.W0.Get()), nil +} + +// Tx handles read/write operation for SPI interface. Since SPI is a syncronous write/read +// interface, there must always be the same number of bytes written as bytes read. +// This is accomplished by sending zero bits if r is bigger than w or discarding +// the incoming data if w is bigger than r. +// +func (spi SPI) Tx(w, r []byte) error { + toTransfer := len(w) + if len(r) > toTransfer { + toTransfer = len(r) + } + + for toTransfer != 0 { + // Do only 64 bytes at a time. + chunkSize := toTransfer + if chunkSize > 64 { + chunkSize = 64 + } + + // Fill tx buffer. + transferWords := (*[16]volatile.Register32)(unsafe.Pointer(uintptr(unsafe.Pointer(&spi.Bus.W0)))) + var outBuf [16]uint32 + txSize := 64 + if txSize > len(w) { + txSize = len(w) + } + for i := 0; i < txSize; i++ { + outBuf[i/4] = outBuf[i/4] | uint32(w[i])<<((i%4)*8) + } + for i, word := range outBuf { + transferWords[i].Set(word) + } + + // Do the transfer. + spi.Bus.MISO_DLEN.Set((uint32(chunkSize)*8 - 1) << esp.SPI_MISO_DLEN_USR_MISO_DBITLEN_Pos) + spi.Bus.MOSI_DLEN.Set((uint32(chunkSize)*8 - 1) << esp.SPI_MOSI_DLEN_USR_MOSI_DBITLEN_Pos) + spi.Bus.CMD.Set(esp.SPI_CMD_USR) + for spi.Bus.CMD.Get() != 0 { + } + + // Read rx buffer. + rxSize := 64 + if rxSize > len(r) { + rxSize = len(r) + } + for i := 0; i < rxSize; i++ { + r[i] = byte(transferWords[i/4].Get() >> ((i % 4) * 8)) + } + + // Cut off some part of the output buffer so the next iteration we will + // only send the remaining bytes. + if len(w) < chunkSize { + w = nil + } else { + w = w[chunkSize:] + } + if len(r) < chunkSize { + r = nil + } else { + r = r[chunkSize:] + } + toTransfer -= chunkSize + } + + return nil +}