Page 1 of 1

Use SPI from deep_sleep_wake_stub

Posted: Tue Jul 16, 2024 7:02 pm
by darkzeros
Hi, I am looking for help on a very niche topic,
use the SPI in deep_sleep_wake_stub.

I have a low power application, where I want to access a display via SPI and send a quick "2 Byte" transfer, then go back to sleep.
If I can do this from the deep_sleep_wake_stub, then I measured a save of 33% of power.

I have tried manually driving the GPIOs to send the command:

Code: Select all

void RTC_IRAM_ATTR delayMicroseconds(uint32_t us) {
  const auto ticks = esp_rom_get_cpu_ticks_per_us();
  auto m = esp_cpu_get_cycle_count();
  auto e = (m + us * ticks);
  while (esp_cpu_get_cycle_count() < e) {
    asm volatile("nop");
  }
}
void RTC_IRAM_ATTR delayNanoseconds(uint32_t ns) {
  const auto ticks = esp_rom_get_cpu_ticks_per_us();
  auto m = esp_cpu_get_cycle_count();
  auto e = (m + ns * ticks / 1000);
  while (esp_cpu_get_cycle_count() < e) {
    asm volatile("nop");
  }
}

void RTC_IRAM_ATTR _transfer(uint8_t value)
{
  for (auto i=0; i<8; i++)
  {
    // Set value
    GPIO_OUTPUT_SET(13, (value >> (7-i)) & 0b1);
    // Cycle Clock
    GPIO_OUTPUT_SET(14, 1);
    delayNanoseconds(1000/20);
    GPIO_OUTPUT_SET(14, 0);
    delayNanoseconds(1000/20);
  }
}

void RTC_IRAM_ATTR _transferCommand(uint8_t value)
{
    GPIO_OUTPUT_SET(HW::DisplayPin::Dc, 0);
    _transfer(value);
    GPIO_OUTPUT_SET(HW::DisplayPin::Dc, 1);
}
But this does not seem to work ok.

I canĀ“t use the SPI.h commands since this is outside the RTC_IRAM.
Any way to drive the SPI registers manually without the SPI.h?

Any idea or solutions?
Thanks.

Re: Use SPI from deep_sleep_wake_stub

Posted: Tue Aug 13, 2024 9:56 pm
by darkzeros
I am answering my own question, with an answer. I managed to strip all the unnecessary parts from SPI.h and fit into a very basic uSpi class that I can run in RTC_IRAM_ATTR. This class directly access the SPI3 bus without any manual GPIO. Can be used in wakeup stub, and as far as I tested works great!

Code: Select all

#include "SPI.h"
#include "soc/reg_base.h"
#include "soc/spi_struct.h"
#include "soc/dport_reg.h"
#include "soc/io_mux_reg.h"

#define GPIO_INPUT_ENABLE(pin) \
  PIN_FUNC_SELECT(GPIO_PIN_REG_##pin, PIN_FUNC_GPIO); \
  PIN_INPUT_ENABLE(GPIO_PIN_REG_##pin); \
  REG_WRITE(GPIO_ENABLE_W1TC_REG, BIT##pin);

#define GPIO_INPUT_DISABLE(pin) \
  PIN_FUNC_SELECT(GPIO_PIN_REG_##pin, PIN_FUNC_GPIO); \
  PIN_INPUT_DISABLE(GPIO_PIN_REG_##pin); \
  REG_WRITE(GPIO_ENABLE_W1TC_REG, (1UL << pin));

namespace uSpi {
  void RTC_IRAM_ATTR init () {
    spi_dev_t& dev = *(reinterpret_cast<volatile spi_dev_t *>(DR_REG_SPI3_BASE));

    auto clockDiv = 266241; // or even 8193 for 26MHz SPI !

    // pinMode(HW::DisplayPin::Sck, INPUT);
    GPIO_INPUT_ENABLE(18);
    // TODO: Make it using the variable HW::DisplayPin::Sck

    dev.slave.trans_done = 0;
    dev.slave.val = 0;
    dev.pin.val = 0;
    dev.user.val = 0;
    dev.user1.val = 0;
    dev.ctrl.val = 0;
    dev.ctrl1.val = 0;
    dev.ctrl2.val = 0;
    dev.clock.val = 0;

    dev.user.usr_mosi = 1;
    dev.user.usr_miso = 1;
    dev.user.doutdin = 1;

    // Mode 0
    dev.pin.ck_idle_edge = 0;
    dev.user.ck_out_edge = 0;
    dev.ctrl.wr_bit_order = 0; //MSBFIRST
    dev.ctrl.rd_bit_order = 0;
    dev.clock.val = clockDiv;

    // pinMode(HW::DisplayPin::Sck, OUTPUT);
    gpio_matrix_out(HW::DisplayPin::Sck, VSPICLK_OUT_IDX, false, false);

    // pinMode(HW::DisplayPin::Mosi, OUTPUT);
    gpio_matrix_out(HW::DisplayPin::Mosi, VSPID_IN_IDX, false, false);

    dev.user.cs_setup = 1;
    dev.user.cs_hold = 1;
    // pinMode(HW::DisplayPin::Cs, OUTPUT);
    gpio_matrix_out(HW::DisplayPin::Cs, VSPICS0_OUT_IDX, false, false);
    dev.pin.val = dev.pin.val & ~((1 << 0) & SPI_SS_MASK_ALL);

    // pinMode(HW::DisplayPin::Dc, OUTPUT);
    GPIO_INPUT_DISABLE(10);
    // TODO: Make it using the variable HW::DisplayPin::Dc
  }

  void RTC_IRAM_ATTR transfer(const void *data_in, uint32_t len) {
    spi_dev_t& dev = *(reinterpret_cast<volatile spi_dev_t *>(DR_REG_SPI3_BASE));
    size_t longs = len >> 2;
    if (len & 3) {
        longs++;
    }
    uint32_t *data = (uint32_t *)data_in;
    size_t c_len = 0, c_longs = 0;

    while (len) {
        c_len = (len > 64) ? 64 : len;
        c_longs = (longs > 16) ? 16 : longs;

        dev.mosi_dlen.usr_mosi_dbitlen = (c_len * 8) - 1;
        dev.miso_dlen.usr_miso_dbitlen = 0;
        for (size_t i = 0; i < c_longs; i++) {
            dev.data_buf[i] = data[i];
        }
        dev.cmd.usr = 1;
        while (dev.cmd.usr); // Wait till previous commands have finished

        data += c_longs;
        longs -= c_longs;
        len -= c_len;
    }
  }

  void RTC_IRAM_ATTR transfer(const uint8_t data_in) {
    transfer(&data_in, 1);
  }

  void RTC_IRAM_ATTR command(uint8_t value)
  {
    GPIO_OUTPUT_SET(HW::DisplayPin::Dc, 0);
    transfer(value);
    GPIO_OUTPUT_SET(HW::DisplayPin::Dc, 1);
  }
}


Example use case:

Code: Select all

uSpi::init();
uSpi::command(0x44);
uSpi::transfer(0x56);
As can be seen my code refers to other HW::DisplayPin definitions, but this can be easily adapted for individual needs.
It was tested on ESP32 pico D4, probably wont work on C3/C6/H2 due to changes in SPI registers.