Low-power ESP32 + LoRa with deep sleep mode (TTGO LoRa32 OLED Board V1.0)

Hi there,

I'm trying to transmit from one ESP32 LoRa node to another one in low-power mode and in compliance with european regulations.

I used this ESP32 board: TTGO LoRa32 OLED Board V1.0 (here and here).

Below the LoRa sender sketch I wrote:

/*
 * LoRa (low-power) sender for IoT projects
 *  
 * Tested on a TTGO LoRa32 OLED Board V1.0 
 * 
 * Based on the LoRa examples and the board 
 * documentation
 * 
 * More on the deep sleep with timer wake up: 
 *  
 *    Examples > ESP32 > Deep Sleep > TimerWakeUp sketch
 *    
 *    
 * ERC Recommendation
 * h1.4 frequency band requires <= 1% duty cycle and 25mW (14 db) maximum power
 * => 36 seconds every hour (so: 1 sec transmitting, 99 secs idle) 
 * 
 * 
 * Created 11 June 2019
 * by DP 
 *    
 */
 
#include <SPI.h>   // allows communication with SPI devices 
#include <LoRa.h>
#include <SSD1306.h>   // provides API to work with OLED displays

// defines the pins used by the transceiver module
#define SS 18   // GPIO18 - SX1278's CS   - LoRa radio chip select
#define RST 14   // GPIO14 - SX1278's RESET   - LoRa radio reset
#define DI0 26   // GPIO26 - SX1278's IRQ   - IRQ pin

#define BAND 868E6   // EU   - Italy

// deep sleep 
#define uS_TO_S_FACTOR 1000000  // conversion factor for micro seconds to seconds 
#define TIME_TO_SLEEP  5        // time ESP32 will go to sleep (in seconds)   - 99 for (about) 1% duty cycle  


// an object of class SSD1306 
// first parameter: the I2C address of the display
// second parameter: the number of the SDA
// third parameter: the SCL pin
SSD1306 display(0x3c, 4, 15);

// stores the data on the RTC memory so that it will not be deleted during the deep sleep
RTC_DATA_ATTR int bootCount = 0; 
RTC_DATA_ATTR int pckCounter = 0;   // sending packet number...

 
void setup() {   
  Serial.begin(115200);   // initializes serial data transmission  
  while(!Serial);   // waits for serial port to connect 

  Serial.println("LoRa low-power Sender");
  
  pinMode(16, OUTPUT);
  digitalWrite(16, LOW);   // sets GPIO16 low to reset the OLED
  delay(50);
  digitalWrite(16, HIGH);   // while the OLED is running, GPIO16 must go to high  
  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);   // sets the onboard LED low 
  
  // LoRa transceiver module setup 
  LoRa.setPins(SS, RST, DI0);   // overrides the default CS, reset, and IRQ pins used by the library

  // initializes the transceiver module with a specified frequency
  while (!LoRa.begin(BAND)) {   // LoRa.begin returns 1 on success, 0 on failure
    Serial.println(".");
  }

  // changes the spreading factor to 12 -> slower speed but better noise immunity
  LoRa.setSpreadingFactor(12);   // ranges from 6-12, default is 7 

  // changes the sync word (0xF3) to match the receiver
  // the sync word assures you don't get LoRa messages from other LoRa transceivers  
  LoRa.setSyncWord(0xF3);   // ranges from 0-0xFF     
 
  // LoRa.setTxPower(txPower);   // defaults to 17

  // initializes the display by calling the init method of the display object 
  display.init();   // receives no arguments and returns void

  // display.flipScreenVertically();   // LCD is broken!
  display.setFont(ArialMT_Plain_16);   // sets the current font
  display.drawString(0, 0, "Initialization");   // x, y, message to show
  display.drawString(0, 16, "completed");
  display.display();
  delay(1500);   // small delay so that the user can read it  

  Serial.println("LoRa init completed");    

  //Increments boot number and prints it every reboot
  bootCount++;
  Serial.println("Boot number: " + String(bootCount));

  sendData();   // sends the data...

  // deep sleep
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds");
  Serial.println("Going to sleep now");
  Serial.flush();   // waits for the transmission of outgoing serial data to complete 
  esp_deep_sleep_start();   // enters deep sleep with the configured wakeup options
}

void loop(){
  // this is not going to be called
}

// sends the data to the receiver
void sendData() {
  
  Serial.print("Sending packet: ");
  Serial.println(pckCounter);

  display.clear();   // clears the display
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.drawString(0, 0, "Sending ");
  display.drawString(0, 16, "packet: " + String(pckCounter, DEC));
  display.display();
  
  digitalWrite(2, HIGH);   // LED is ON during transmission

  // sends the LoRa packet to the receiver
  LoRa.beginPacket();
  LoRa.print("hello ");
  LoRa.print(pckCounter);
  LoRa.endPacket();
  
  digitalWrite(2, LOW);   // turns the LED off after transmission

  pckCounter++;
}

The sketch runs without errors (the code on the shop's page contains some errors, so I encourage you to use this one just to play with it). My questions:

  • I forgot to initialize the SPI communication: SPI.begin (SCK, MISO, MOSI, SS);

But nevertheless, it worked anyway. I didn't expect it (maybe is it due to the Espressif library for Arduino IDE that initializes the SPI?). My first question is: what am I missing?

  • I live in Europe. ERC Recommendation document says that h1.4 frequency band (I'm transmitting at 868 Mhz) requires <=1% duty cycle and 25mW (14db) maximum power. This means that I can transmit for 36 seconds every hour (let's say that I transmit for 1 sec and then the ESP32 'sleeps' for 99 seconds). I can achieve that writing (it's not the more accurate way but it should work):
  • #define TIME_TO_SLEEP 99*

What about the erp? Let's say that the antenna has a 2db gain, so I can set the transmission power to 12db (12db + 2db -> 14db, great. It's ok!):

  • LoRa.setTxPower(12);*

My second question: am I right (about both settings)? Am I missing something?

  • Last question: please, feel free to give me any (if you have) feedback/suggestion to improve this sketch!

Thank you all for helping.

Perhaps the LoRa.h library files setup the SPI ?

For ERP, its assumed you using the minimum practical antenna, a 1/4 vertical with groundplane\radials or a dipole and that antenna is assumed to be 2.1dBi.

So you can transmit 25mW through that 2.1dBi antenna, but if you want to use a 5dBi antenna you would need to reduce transmit power by 5-2.1 = 2.9dBm.

The reality of these restrictions is that in practice the only real reason to use gain antennas is for one way communications since you can add as big a gain antennas as you like to a receiver.

Hi srnet, thank you for your answer.

Perhaps the LoRa.h library files setup the SPI ?

Or maybe the Arduino core for the ESP32?

So you can transmit 25mW through that 2.1dBi antenna, but if you want to use a 5dBi antenna you would need to reduce transmit power by 5-2.1 = 2.9dBm.

25mW -> 14 dBm. So my reasoning about power is wrong: you are saying that I can set the LoRa module to LoRa.setTxPower(14);?

The ESP32 core should not be initialising interfaces unless you ask it to.

LoRa.setTxPower(14) should set the TX power to 14dBm\25mW.

If you want to be sure you are following regulations, then you should be measuring the transmit power.

LoRa.setTxPower(14) should set the TX power to 14dBm\25mW.

Ok, I think we are on the same page! LoRa.setTxPower(12) should set the TX power to 12dBm. Assuming that the antenna in the bundle has a 2.1dBi gain, I should have about 14dBm, that is what I need to follow regulations.

If you want to be sure you are following regulations, then you should be measuring the transmit power.

Absolutely! Just wanted to understand if my reasoning was right or wrong.

dipido:
Ok, I think we are on the same page! LoRa.setTxPower(12) should set the TX power to 12dBm. Assuming that the antenna in the bundle has a 2.1dBi gain, I should have about 14dBm, that is what I need to follow regulations.

No, you can use LoRa.setTxPower(14) with a 2.1dBi antenna.

Read post #1 again, more slowly.

LoRa.setPins(SS, RST, DI0);   // overrides the default CS, reset, and IRQ pins used by the library

Most likely the above setting initialized SPI.

By loading up freeRTOS you can gain a few points of increased operation. You could assign the LoRa to a task placed onto a core and have the lcd display task run on the other core. You can use vTaskDelay or vTaskDelayUntill instead of delay(). vTaskDelay and vTaskDelayUntill will not stop the cores from executing other tasks whiles the delayed task sleeps. If there are other task that can be run, during a vTaskDelay those tasks will run, whiles delay() stops program execution.

The ESP32 has 3 cores, Core0, Core1, and ULP. The Ultra Low Power core, with its own RAM, is only programmed in assembler but there are lots of examples on the internet.

If you can, learn how to use the ESP32 SPI API and write your own SPI communications tasks.

Sorry for the late reply..

No, you can use LoRa.setTxPower(14) with a 2.1dBi antenna.

Read post #1 again, more slowly.

Technical specifications are written assuming that the minimum practical antenna is being used. Now it's clear, thank you.

If you can, learn how to use the ESP32 SPI API and write your own SPI communications tasks.

Have you any suggestion or resources I could begin with? Thank you Idahowalker.

Yes.

https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/spi_master.html

Very helpful:

https://esp-idf-zh.readthedocs.io/zh_CN/latest/api-reference/peripherals/spi_master.html

This code is messy but works quite well, myESP32_SPI.h:

#ifndef myESP32SPI
#define myESP32SPI
////
#include <driver/spi_master.h>
#include "sdkconfig.h"
// #include <driver/gpio.h>
////
////
class ESP32_SPI_API
{
  public:
    ESP32_SPI_API();
    ESP32_SPI_API( int q_size );
    ESP32_SPI_API( int q_size, int _clk_speed, int cs_gpio_pin, int mosi_gpio_pin, int miso_gpio_pin, int clk_gpio_pin, bool dma_enable, bool hspi );
    void setHost_HSPI( );
    void setHost_VSPI( );
    int get_rxData8bit( int _bit );
    int get_rxData16bit( int _bit );
    int fSPIdeviceConfig( );
    void setClockSpeed( int _clk_speed );
    void setQSize( int _size );
    void setCS_pin( int _gpio_pin );
    void setMOSI_pin( int _gpio_pin );
    void setMISO_pin( int _gpio_pin );
    void setEnable_DMA( bool _enable );
    void setSPIclkPIN( int _gpio_pin );
    int fSPIbusConfig( );
    // int fwriteSPIregister( int addr, int sendData );
    int fReadSPI( int byteReadSize, int addressToRead );
    int fWriteSPIdata8bits( int _address, int _DataToSend);
    int fSPIqueueupTransactions_read( int byteReadSize, int addressToRead );
    int fSPIreadQueuedTransaction( int NumofTransactions );
  private:
    ////
    spi_device_handle_t hSPI;
    bool HSPI_host = true; // HSPI_HOST = true, VSPI_HOST = false
    int  spi_csPin = 15;
    int spi_MOSI_pin = 13;
    int spi_MISO_pin = 12;
    int spi_CLK_pin = 14;
    bool enableDMA = true;
    int qSize = 1;
    int clock_speed = 1000000;
    uint8_t txData[2] = { };
    int8_t rxData[24] = { };
  protected:
////
};
#endif

My ESP32 cpp file

#include "myESP32_SPI.h"
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
ESP32_SPI_API::ESP32_SPI_API() {}
ESP32_SPI_API::ESP32_SPI_API( int q_size ) 
{
  qSize = q_size;
}
ESP32_SPI_API::ESP32_SPI_API( int q_size, int _clk_speed, int cs_gpio_pin, int mosi_gpio_pin, int miso_gpio_pin, int clk_gpio_pin, bool dma_enable, bool hspi )
{
  qSize = q_size;
   clock_speed = _clk_speed;
  spi_csPin = cs_gpio_pin;
  spi_MOSI_pin = mosi_gpio_pin;
  spi_MISO_pin = miso_gpio_pin;
  spi_CLK_pin = clk_gpio_pin;
  enableDMA = dma_enable;
  HSPI_host = hspi;
}
//////////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setClockSpeed( int _clk_speed )
{
  clock_speed = _clk_speed;
}
////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setCS_pin( int _gpio_pin )
{
  spi_csPin = _gpio_pin;
}
////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setMOSI_pin( int _gpio_pin )
{
  spi_MOSI_pin = _gpio_pin;
}
////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setMISO_pin( int _gpio_pin )
{
  spi_MISO_pin = _gpio_pin;
}
////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setEnable_DMA( bool _enable )
{
  enableDMA = _enable;
}
////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setSPIclkPIN( int _gpio_pin )
{
  spi_CLK_pin = _gpio_pin;
}
//////////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setHost_VSPI( )
{
  HSPI_host = false;
}
//////////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setHost_HSPI( )
{
  HSPI_host = true;
}
//////////////////////////////////////////////////////////////////////////////////////////
void ESP32_SPI_API::setQSize( int _size )
{
  qSize = _size;
}
////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::fReadSPI( int byteReadSize, int addressToRead )
{
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 2) + (8 * byteReadSize) ; // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = byteReadSize * 8 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = rxData;
  txData[0] = (uint8_t)addressToRead;
  txData[1] = 0x0;
  return spi_device_transmit( hSPI, &trans_desc);
} // int fReadSPI( int byteReadSize, int addressToRead )
////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::get_rxData8bit( int _bit )
{
  int8_t temp;
  temp = rxData[_bit];
  return temp;
}
//////////////////////////////////////////////////////////////////////////////////
//// for _bit use low bit
int ESP32_SPI_API::get_rxData16bit( int _bit )
{
  int16_t temp;
  temp = rxData[ _bit] << 8;
  temp |= rxData[ _bit + 1];
  return temp;
}
////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::fWriteSPIdata8bits( int _address, int _DataToSend)
{
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr = 0;
  trans_desc.cmd = 0;
  trans_desc.flags  = 0;
  trans_desc.length = 8 * 2; // total data bits
  trans_desc.tx_buffer = txData;
  txData[0] = (uint8_t)_address;
  txData[1] = (uint8_t)_DataToSend;
  return spi_device_transmit( hSPI, &trans_desc);
} // void fSendSPI( uint8_t count, uint8_t address, uint8_t DataToSend)
/////////////////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::fSPIdeviceConfig( )
{
  spi_device_interface_config_t dev_config = { };  // initializes all field to 0
  dev_config.address_bits     = 0;
  dev_config.command_bits     = 0;
  dev_config.dummy_bits       = 0;
  dev_config.mode             = 3;
  dev_config.duty_cycle_pos   = 0;
  dev_config.cs_ena_posttrans = 0;
  dev_config.cs_ena_pretrans  = 0;
  dev_config.clock_speed_hz   = clock_speed;
  dev_config.spics_io_num     = spi_csPin;
  dev_config.flags            = 0;
  dev_config.queue_size       = qSize;
  dev_config.pre_cb           = NULL;
  dev_config.post_cb          = NULL;
  if ( HSPI_host )
  {
    return spi_bus_add_device( HSPI_HOST, &dev_config, &hSPI ); 
  }
  else
  {
   return  spi_bus_add_device( VSPI_HOST, &dev_config, &hSPI );
  }
}
///////////////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::fSPIbusConfig( )
{
  spi_bus_config_t bus_config = { };
  bus_config.sclk_io_num = spi_CLK_pin; // CLK
  bus_config.mosi_io_num = spi_MOSI_pin; // MOSI
  bus_config.miso_io_num = spi_MISO_pin; // MISO
  bus_config.quadwp_io_num = -1; // Not used
  bus_config.quadhd_io_num = -1; // Not used
    if ( HSPI_host )
  {
    return spi_bus_initialize( HSPI_HOST, &bus_config, enableDMA );
  }
  else
  {
    return spi_bus_initialize( VSPI_HOST, &bus_config, enableDMA ); 
  }
  } //int fSPIinit ( )
///////////////////////////////////////////////////////////////////////////////////////////////////
int ESP32_SPI_API::fSPIqueueupTransactions_read( int byteReadSize, int addressToRead )
{
 spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 2) + (8 * byteReadSize) ; // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = byteReadSize * 8 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = rxData;
  txData[0] = (uint8_t)addressToRead;
  txData[1] = 0x0;
  return  spi_device_queue_trans( hSPI, &trans_desc, 3 );
}

You'll want to get to get the int ESP32_SPI_API::fSPIbusConfig( ) working, first. Use the return value to detect errors.

You'll be setting:
bus_config.sclk_io_num = spi_CLK_pin; // CLK
bus_config.mosi_io_num = spi_MOSI_pin; // MOSI
bus_config.miso_io_num = spi_MISO_pin; // MISO

with that function. Once the bus has been opened, you'll open the device. You can open several devices on one buss. The device handle will be used to address each device.

The int ESP32_SPI_API::fSPIdeviceConfig( ). I use the return value to detect if there was an error.

The values in the function you want to concern yourself with are:
dev_config.mode = 3;
dev_config.clock_speed_hz = clock_speed;
dev_config.spics_io_num = spi_csPin;
dev_config.queue_size = qSize;

Setting the config mode to 3 allows bidirectional communications. I'd just leave the setting at 3 even if your device does not use bidirectional SPI communication.

The minimum queue size should be 1, which works quite well.

I, not included (yet), have a few more functions that can be useful.

I, highly, recommend you keep this code in the SPI functions:trans_desc = { }; to make sure the memory areas that the array is to use will be cleared prior to use.

void fReadSPIdata16bits( spi_device_handle_t &h, int _address )
{
  uint8_t address = _address;
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 3); // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = 8 * 2 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = rxData;
  txData[0] = address | 0x80;
  intError = spi_device_transmit( h, &trans_desc);
  low = rxData[0]; high = rxData[1];
} // void fSendSPI( uint8_t count, uint8_t address, uint8_t DataToSend)
int fWriteSPIdata32bits( spi_device_handle_t &h, int _sendData0, int _sendData1, int _sendData2, int _sendData3 )
{
  // uint8_t address =  _address;
  // uint8_t sendData = _sendData;
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 4); // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = 0 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = NULL;
  txData[0] = (uint8_t)_sendData0; // command bits
  txData[1] = (uint8_t)_sendData1; // lower bits
  txData[2] = (uint8_t)_sendData2; // higher bits
  txData[3] = (uint8_t)_sendData3; // address
  intError = spi_device_transmit( h, &trans_desc);
  return intError;
} // void fWriteSPIdata8bits(  spi_device_handle_t &h, uint8_t address, uint8_t sendData )
int fReadSPIdataXbits( spi_device_handle_t &h, int _readaddress, int *rxbuf, int rxlen )
{
  uint8_t address = _readaddress;
  int8_t rxBuf[rxlen] = {0};
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = ( (8 * 1) + (8 * rxlen)); // total data bits
  trans_desc.tx_buffer = txData ;
  trans_desc.rxlength = 8 * rxlen ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = rxBuf;
  txData[0] = address;
  // txData[1] = 0x00;
  intError = spi_device_transmit( h, &trans_desc);
  for (int i = 0; i < rxlen; i++)
  {
    rxbuf[i] = rxBuf[i];
  }
 return intError;
} // int fReadSPIdataXbits( spi_device_handle_t &h, int _readaddress, int *rxbuf, int rxlen )

I think I'll take a while to master all of that :slight_smile: Thank you Idahowalker. I was thinking to search for a book about ESP32 too. I'll start from your links!