RAK3172 / MBedOs help (randomly not going to deep sleep)

Hi,
Have been developing smart soil sensors using different i2c sensors and RAK3172 module as brains of everything.

I know this question is not related to TTN, but still, maybe someone can point me in the right direction.

This is my first time working with MBedOs firmware.

I have written code based on mbedOs LoRa example, that reads multiple I2C sensors, encodes data and sends it to TTN.
Things that don’t allow me to sleep:

  1. ADC readings are all over the place. First ADC reading is nonsense, then it is ok and stable, then after the first uplink, it shows something different again. I really don’t know the inner workings of mbedOS AnalogIn. This is not crucial but still annoying.
  2. Managed to get node to sleep ~8uA average but then randomly after some uplink it doesn’t go to deep sleep and stays at ~3.1mA. This is something I really don’t know how to debug and find the cause of it.
    I have attached the code of main.cpp below.
#include "events/EventQueue.h"
#include "lorawan/LoRaWANInterface.h"
#include "lorawan/system/lorawan_data_structures.h"
#include "mbed.h"
#include <cstdint>
#include <stdio.h>

// Application helpers
// #include "trace_helper.h"
#include "helpers.h"

#define DEBUGGING false

// create  I2C instance
I2C i2c(PA_11, PA_12);
// I2C slave addreses
bool nodeOk = true;
const uint8_t tempSensorAddr[6] = {0x1A, 0x1C, 0x1B, 0x19, 0x18, 0x1D};
const uint8_t tempSensType[6] = {0x00, 0x00, 0x00, 0x00,
                                 0x00, 0x01}; // 0x00 - soil tmep 0x01 - air

DigitalOut sensorEN(PA_7);
DigitalOut I2CExpanderEN(PB_3);

/**
values for reading battery voltage (5-3.3V)
*/
DigitalOut batVoltageADCEN(PB_5);
PinName batADCPin = PB_4;
// calibration is done to find out 2 coefficients
// vBat = ADCk*ADCValue*1000 + ADCb
const float ADCk = 0.0083;
const float ADCb = -0.2801;

#include "STM32WL_LoRaRadio.h"
STM32WL_LoRaRadio radio;

using namespace events;

// Max payload size can be LORAMAC_PHY_MAXPAYLOAD.
// This example only communicates with much shorter messages (<30 bytes).
// If longer messages are used, these buffers must be changed accordingly.
uint8_t tx_buffer[30];
uint8_t txBufferPointer = 0;
uint8_t rx_buffer[30];

uint32_t sleepIntervalMs = 30000; // default sleep time 30s

/**
 * Maximum number of events for the event queue.
 * 10 is the safe number for the stack events, however, if application
 * also uses the queue for whatever purposes, this number should be increased.
 */
#define MAX_NUMBER_OF_EVENTS 15

/**
 * Maximum number of retries for CONFIRMED messages before giving up
 */
#define CONFIRMED_MSG_RETRY_COUNTER 3

/**
 * This event queue is the global event queue for both the
 * application and stack. To conserve memory, the stack is designed to run
 * in the same thread as the application and the application is responsible for
 * providing an event queue to the stack that will be used for ISR deferment as
 * well as application information event queuing.
 */
static EventQueue ev_queue(MAX_NUMBER_OF_EVENTS *EVENTS_EVENT_SIZE);

// /**
//  * Event handler.
//  *
//  * This will be passed to the LoRaWAN stack to queue events for the
//  * application which in turn drive the application.
//  */
static void lora_event_handler(lorawan_event_t event);

// /**
//  * Constructing Mbed LoRaWANInterface and passing it the radio object from
//  lora_radio_helper.
//  */
static LoRaWANInterface lorawan(radio);

// /**
//  * Application specific callbacks
//  */
static lorawan_app_callbacks_t callbacks;

// I2C low level functions
static uint8_t readReg(uint8_t addr, uint8_t reg) {
  char regAddr[1] = {reg};
  char data[1];

  i2c.write(addr, regAddr, 1, true);

  i2c.read(addr, data, 1, false);

  i2c.stop();

  return data[0];
}

static uint16_t readReg16(uint8_t addr, uint8_t reg) {

  char regAddr[1] = {reg};
  char data[2];

  i2c.write(addr, regAddr, 1, true);

  i2c.read(addr, data, 2, false);

  i2c.stop();

  return (data[0] << 8 | data[1]);
}

static bool writeReg(uint8_t addr, uint8_t reg, uint8_t value) {
  char msg[2] = {reg, value};
  return i2c.write(addr, msg, 2, false);
}

static bool writeReg16(uint8_t addr, uint8_t reg, uint16_t value) {
  uint8_t MSB = value >> 8;
  uint8_t LSB = value & 0xFF;
  char msg[3] = {reg, MSB, LSB};

  return i2c.write(addr, msg, 3, false);
}

// MCP9808 processing
/**
    sets mode of the sensor
    @param state    true - enter shutdown mode
                    false - sampling mode
*/
static void sensorSleep(uint8_t addr, bool state) {
  const uint8_t placeSHDN = 0x08;
  const uint8_t regCONFIG = 0x01;
  uint16_t currentCONFIG = readReg16(addr, regCONFIG);
  // printf("Current Config: "BYTE_TO_BINARY_PATTERN "
  // "BYTE_TO_BINARY_PATTERN"\n",
  // BYTE_TO_BINARY(currentCONFIG>>8),BYTE_TO_BINARY(currentCONFIG));
  uint16_t CONFIG =
      (currentCONFIG & ~(1UL << placeSHDN)) | (state << placeSHDN);
  // printf("Set CONFIG: "BYTE_TO_BINARY_PATTERN " "BYTE_TO_BINARY_PATTERN"\n",
  // BYTE_TO_BINARY(CONFIG>>8),BYTE_TO_BINARY(CONFIG));
  writeReg16(addr, regCONFIG, CONFIG);
}

static void setResolution(uint8_t addr, uint8_t resolution) {
  const uint8_t regRES = 0x08;

  // no need to read resolution because it is the only setting in register
  writeReg(addr, regRES, resolution);
}

static float getTemp(uint8_t addr) {
  const uint8_t regAMBTEMP = 0x05;

  uint16_t regValue = readReg16(addr, regAMBTEMP);
#if DEBUGGING
  printf("ABIENT TEMP reg: " BYTE_TO_BINARY_PATTERN " " BYTE_TO_BINARY_PATTERN
         "\n",
         BYTE_TO_BINARY(regValue >> 8), BYTE_TO_BINARY(regValue));
#endif
  /**
      temperature registr contains 3 sections
      [0:11]  - Ambient Temp Bits
      [12]    - Sign bit
      [13-15] - Alert cause
  */
  uint16_t maskALRTBits = 0xE000;
  uint16_t maskNegativeBit = 0x1000;
  uint16_t maskTempBits = 0xFFF;

  uint8_t alerCauseBits = regValue & maskALRTBits;
  bool negativeTemp = regValue & maskNegativeBit;

  // remove alert cause bits and negative temp bit
  uint16_t tempBits = regValue & maskTempBits;
  // printf("tempBits: "BYTE_TO_BINARY_PATTERN " "BYTE_TO_BINARY_PATTERN"\n",
  // BYTE_TO_BINARY(tempBits>>8),BYTE_TO_BINARY(tempBits));

  uint8_t tempUpper = tempBits >> 8;
  // printf("upper: "BYTE_TO_BINARY_PATTERN"\n", BYTE_TO_BINARY(tempUpper));
  uint8_t tempLower = tempBits & 0xFF;
  // printf("lower: "BYTE_TO_BINARY_PATTERN"\n", BYTE_TO_BINARY(tempLower));

  float temp;
  if (negativeTemp) {
    temp = (256 - (tempUpper * 16 + ((float)tempLower / 16)));
  } else {
    temp = (tempUpper * 16 + ((float)tempLower / 16));
  }

  return temp;
}

float readBatteryVoltage() {
  batVoltageADCEN = 1;
  AnalogIn batVoltageADC(batADCPin);
  batVoltageADC.set_reference_voltage(3.3);
  batVoltageADC.read(); // first read is nonsense

  //   thread_sleep_for(100); //this did not help
  float adcValRaw = batVoltageADC.read();
  int adcVal = (int)(adcValRaw * 1000);

  float vBat = adcVal * ADCk + ADCb;
  batVoltageADCEN = 0;

#if DEBUGGING
  printf("adcraw: %1.3f\t", adcValRaw);
  printf("adcVal: %d \t", adcVal);
  printf("adcVoltage %1.4f \r\n", vBat);
#endif
  return vBat;
}

/**
 * Entry point for application
 */
int main(void) {

  // as per low-power suggestion
  mbed_file_handle(STDIN_FILENO)->enable_input(false);
  
  // stores the status of a call to LoRaWAN protocol
  lorawan_status_t retcode;

  // Initialize LoRaWAN stack
  if (lorawan.initialize(&ev_queue) != LORAWAN_STATUS_OK) {
    printf("\r\n LoRa initialization failed! \r\n");
    return -1;
  }

  printf("\r\n Mbed LoRaWANStack initialized \r\n");

  // prepare application callbacks
  callbacks.events = mbed::callback(lora_event_handler);
  lorawan.add_app_callbacks(&callbacks);

  // Set number of retries in case of CONFIRMED messages
  if (lorawan.set_confirmed_msg_retries(CONFIRMED_MSG_RETRY_COUNTER) !=
      LORAWAN_STATUS_OK) {
    printf("\r\n set_confirmed_msg_retries failed! \r\n\r\n");
    return -1;
  }

  printf("\r\n CONFIRMED message retries : %d \r\n",
         CONFIRMED_MSG_RETRY_COUNTER);

  // Enable adaptive data rate
  if (lorawan.enable_adaptive_datarate() != LORAWAN_STATUS_OK) {
    printf("\r\n enable_adaptive_datarate failed! \r\n");
    return -1;
  }

  printf("\r\n Adaptive data  rate (ADR) - Enabled \r\n");

  retcode = lorawan.connect();

  if (retcode == LORAWAN_STATUS_OK ||
      retcode == LORAWAN_STATUS_CONNECT_IN_PROGRESS) {
  } else {
    printf("\r\n Connection error, code = %d \r\n", retcode);
    return -1;
  }

  printf("\r\n Connection - In Progress ...\r\n");

  // make your event queue dispatching events forever
  ev_queue.dispatch_forever();

  return 0;
}

// /**
//  * Sends a message to the Network Server
//  */
static void send_message() {

  I2CExpanderEN = 1; // enable I2C expander
  sensorEN = 1;      // enable sensor power
  i2c.init();        // init I2C

  // read all sensors
  for (uint8_t sensId = 0; sensId < sizeof(tempSensorAddr); sensId++) {

    uint8_t addr8Bit = tempSensorAddr[sensId] << 1;
    sensorSleep(addr8Bit, false);
    thread_sleep_for(300);
    float temp = getTemp(addr8Bit);
    sensorSleep(addr8Bit, true);

    // this is so that we never have negative values and use as much bits as
    // necessary, not more
    uint8_t tempOffset = 40;                           //(273.15-233.15);
    uint16_t tempOffseted = (temp + tempOffset) * 100; //*100 to have 2 decimals

    uint8_t tempDataUpper = tempOffseted >> 8;
    uint8_t tempDataLower = tempOffseted & 0xFF;

    uint8_t idByte = (sensId << 4) | tempSensType[sensId];

#if DEBUGGING
    printf("Sensor: %d \n", (tempSensorAddr[sensId]));
    printf("%2.4f \r\n", temp);
    printf("%d \r\n", tempOffseted);
    printf("id Byte " BYTE_TO_BINARY_PATTERN "\r\n", BYTE_TO_BINARY(idByte));
    printf("temp data " BYTE_TO_BINARY_PATTERN " " BYTE_TO_BINARY_PATTERN
           "\r\n",
           BYTE_TO_BINARY(tempDataUpper), BYTE_TO_BINARY(tempDataLower));
#endif
    // fill tx buffer
    tx_buffer[txBufferPointer] = idByte;
    tx_buffer[txBufferPointer + 1] = tempDataUpper;
    tx_buffer[txBufferPointer + 2] = tempDataLower;
    txBufferPointer = txBufferPointer + 3; // set ready for the next player.
  }

  i2c.free();        // release I2C
  sensorEN = 0;      // disable sensor power
  I2CExpanderEN = 0; // disable I2C expander

  // read battery voltage
  float voltageOffset = 3.3;
  uint8_t batVValue = (readBatteryVoltage() - voltageOffset) * 100;
  uint8_t idByte = (0x00 | 0x0A); // probe level is left empty and type value is
                                  // added to the 0:3 bits

  // fill tx buffer
  tx_buffer[txBufferPointer] = idByte;
  tx_buffer[txBufferPointer + 1] = batVValue;
  txBufferPointer = txBufferPointer + 2; // set ready for the next player.

  //   /**
  //   NOTES for encoding
  //   each sensor node data start with idByte

  //   bit 0:4 - data type:
  //       0x00 - soil temperature
  //       0x01 - air temperature
  //       0x02 -
  //       0x03 -
  //       0x04 -
  //       0x05 -
  //       0x06 -
  //       0x07 -
  //       0x08 -
  //       0x09 -
  //       0x0A - battery voltage
  //       0x0B -
  //       0x0C -
  //       0x0D -
  //       0x0E -
  //       0x0F - error

  //   bit 5:8 - sensor level
  //       0x00 - closes to the bottom
  //       0xF0 - closes to the top
  //       always start by using 0x00 and then move to the top
  //       as different sensor can be easly added above the surface.
  //   **/

  int16_t retcode = lorawan.send(MBED_CONF_LORA_APP_PORT, tx_buffer,
                                 txBufferPointer, MSG_UNCONFIRMED_FLAG);

  if (retcode < 0) {
    retcode == LORAWAN_STATUS_WOULD_BLOCK
        ? printf("send - WOULD BLOCK\r\n")
        : printf("\r\n send() - Error code %d \r\n", retcode);

    if (retcode == LORAWAN_STATUS_WOULD_BLOCK) {
      // retry in 3 seconds
      if (MBED_CONF_LORA_DUTY_CYCLE_ON) {
        ev_queue.call_in(3000, send_message);
      }
    }
    return;
  }

  //   printf("\r\n %d bytes scheduled for transmission \r\n", retcode);
  memset(tx_buffer, 0, sizeof(tx_buffer)); // clear buffer
  txBufferPointer = 0;
}

/**
 *Send error message that something is worong and node is not functional
 */
void send_error_message() {
  tx_buffer[txBufferPointer] = 0x0F; // error message
  txBufferPointer = txBufferPointer + 1;

  int16_t retcode = lorawan.send(MBED_CONF_LORA_APP_PORT, tx_buffer,
                                 txBufferPointer, MSG_UNCONFIRMED_FLAG);

  memset(tx_buffer, 0, sizeof(tx_buffer)); // clear buffer
  txBufferPointer = 0;
}

/**
 * Receive a message from the Network Server
 */
static void receive_message() {
  uint8_t port;
  int flags;
  int16_t retcode = lorawan.receive(rx_buffer, sizeof(rx_buffer), port, flags);

  if (retcode < 0) {
    printf("\r\n receive() - Error code %d \r\n", retcode);
    return;
  }

#if DEBUGGING
  printf(" RX Data on port %u (%d bytes): ", port, retcode);
  for (uint8_t i = 0; i < retcode; i++) {
    printf("%02x ", rx_buffer[i]);
  }
  printf("\r\n");
#endif
  // extract command byte
  uint8_t cmdByte = rx_buffer[0] >> 4;
  uint8_t additionalInfo = rx_buffer[0] & 0x0F;

  switch (cmdByte) {
  case 0x05: {
#if DEBUGGING
    printf("setting uplink interval \r\n");
#endif
    uint16_t receivedInterval = (rx_buffer[1] << 8) | rx_buffer[2];
    if (additionalInfo & 0x01) {
      // time sent in minutes
      sleepIntervalMs = receivedInterval * 60 * 1000;
    } else {
      sleepIntervalMs = receivedInterval * 1000;
    }
    break;
  }
  default: {
#if DEBUGGING
    printf(" unrecognised command: %02x", cmdByte);
    printf("\r\n");
#endif
  }
  };

  memset(rx_buffer, 0, sizeof(rx_buffer)); // clear RX buffer
}

/**
 * Event handler
 */
static void lora_event_handler(lorawan_event_t event) {
  switch (event) {
  case CONNECTED: {
    printf("\r\n Connection - Successful \r\n");

    // verfy that we have connected all the sensors.
    I2CExpanderEN = 1; // enable I2C expander
    sensorEN = 1;      // enable  power for sensors

    for (uint8_t sensorAddr : tempSensorAddr) {
      printf("Sensor: %d \t", (sensorAddr));
      uint8_t addr8Bit = sensorAddr << 1;
      int i2cResposne = i2c.write(addr8Bit);
      printf("response: %d \r\n", i2cResposne);
      if (i2cResposne == 0) {
        nodeOk = false;
        printf("failed \r\n");
      }
    }
    i2c.free();        // release I2C for deep sleep
    I2CExpanderEN = 0; // disable I2C expander
    sensorEN = 0;      // disable power for sensors

    // we connected, send our first sensor reading or error message
    if (nodeOk) {
      send_message();
    } else {
      send_error_message();
    }
    break;
  }
  case DISCONNECTED:
    ev_queue.break_dispatch();
    printf("\r\n Disconnected Successfully \r\n");
    break;
  case TX_DONE:
    // this is where we land after TX is done
    // printf("\r\n Message Sent to Network Server \r\n");

    // enter sleep 
    //TODO: we should call this message only after RX window 
    if (nodeOk) {
      ev_queue.call_in(sleepIntervalMs, send_message);
    } else {
      while (true) {
        //Nothing happens!
      }
    }
    break;
  case TX_TIMEOUT:
  case TX_ERROR:
  case TX_CRYPTO_ERROR:
  case TX_SCHEDULING_ERROR:
    printf("\r\n Transmission Error - EventCode = %d \r\n", event);
    // try again
    if (MBED_CONF_LORA_DUTY_CYCLE_ON) {
      send_message();
    }
    break;
  case RX_DONE:
   // printf("\r\n Received message from Network Server \r\n");
    receive_message();
    break;
  case RX_TIMEOUT:
    printf("RX TIMEOUT");
    break;
  case RX_ERROR:
    printf("\r\n Error in reception - Code = %d \r\n", event);
    break;
  case JOIN_FAILURE:
    printf("\r\n OTAA Failed - Check Keys \r\n");
    break;
  case UPLINK_REQUIRED:
    printf("\r\n Uplink required by NS \r\n");
    if (MBED_CONF_LORA_DUTY_CYCLE_ON) {
      send_message();
    }
    break;
  default:
    MBED_ASSERT("Unknown Event");
  }
}
// EOF

Really thanks for everyone’s time who looks into this.

Dear @jenertsA I faced a similar issue with rak3172… Try to use i2c3 to find out wherher this port works properly

You need to figure out what is causing the extra power draw.

For example, radio stuck in other than the lowest power idle mode?

Signals left driven against pulling resistors? A badly wedged I2C bus could do that if you have both driven lines low against 2.2K pullups…

CPU left in a run mode with a fast PLL clock?

You’ll probably need to both do things like use test equipment to examine electrical state, and also make modified versions of code that eliminate or repeat possibilities to see what might be at fault.

Thanks for the replay.
Yes, this is github issue is known to me and I did manage to make local changes to I2C API, which allows calling i2c.free() to release I2C.

will look into it again maybe I have missed something. But still, it is odd, that I can be deep sleeping for 20+ mins and then suddenly after a single uplink we are at 3.3mA average.

I have added

void I2C::free(){
    i2c_free(&_i2c);
}

to mbed-os/I2C.cpp at master · ARMmbed/mbed-os · GitHub file

What is power supply for this node? Is it stable enough to support the TX current hit/duration without sagging such that it semi brown outs, without causing a full reset - which would be more obvious. May also explain ADC issue if the ADC Vref is sagging also and unstable for a short period after a Tx hit…

I am powering via Nordic power profiler 2

With an upper limit set sufficiently high to allow for Tx demand plus all other circuit needs & processing at same time? No risk of starvation? If so ignore me! (Most do anyway :wink: )

I will try it with lipo, but seems like it is I2C related.
Turned on MBED_SLEEP_TRACING_ENABLED and tracked that at some point
deepsleep locked by: [i2c_api.c x 1] occurs and the count gets up after some time.
Run the node for multiple hours and now it is at x118.

So it seems like I2C is blocking at some point, need to dig into it.

imagen

Try this in file mbed-os/targets/TARGET_STM/i2c_api.c

Can confirm, that these lines are there! :slight_smile:

Ok. This is exactly what I made:

In mbed-os/drivers/source/i2c.cpp

imagen

And in the driver of my I2C device (APDS9930) I call this method before the end of every read/write operation
imagen

1 Like

Aghā!

Setup is the same, but implementation is different.
I did use singe I2C object defined at the start of the file.

You are making new I2C in each function an then free it at the end. Will try it after the work.

Solved the problem!

two things.
Firstly,
call sleep_manager_lock_deep_sleep(); before ADC reading and sleep_manager_unlock_deep_sleep(); after.

Secondly,
Don’t define I2C globally, define it at each function and utilize i2c.free() at the end of the function.

2 Likes

Congratulations @jenertsA and thank you very much for sharing the solution.

Kind regards from Madrid

2 Likes

Awesome trick thanks, but in my case as I was using another lib that prevented that.
So I’ve done the reversed way, always allow deepsleep on I2C1 and I2C2 and doing lock by myself when need to talk I2C

You can check it out there

2 Likes