Guidance on GPIO reuse or port expander

Hello everybody,
i am running out of available gpios on my freenove cam board and would like to get your perspective on a potential solution.
Link to a tutorial including the Board Pinout:

A lot of the Pins are used by the Cam.
The other modules connected to my Board are a SPI based microSD Card Module and a SPI based LoRa NiceRF SX1280 Module.
The goal is to take an image, store it on the microSD Card and send it via LoRa to a receiver station.
To do so i already cannot use the serial monitor anymore for debugging because i have to use GPIO1 and GPIO3 as well (which is the UART for the Serial Monitor). But that is fine and all is working so far quite nicely.

Now it would be good to have at least one more gpio to make a sound on a piezzo buzzer for feedback in the field using the tone library (e.g. to know if the image was actually taken, saved to the microSD Card and when the transfer was finished successfully).

I tried to reuse the SD CS pin when the SD Card is not in use and also tried to stop the SPI Bus and use the MISO Pin after redefining it as an output pin first to make the sound via the piezzo buzzer.
Both approaches create random noices on the buzzer and are not really working out.

So i wonder if you think any of the Cam pins would be a better fit for a pin reuse when the Cam is not needed (i know pin reuse is not ideal in the first place) - or if you would suggest another solution?

I first thought about using shift registers, but across both SPI SD and LoRa devices, there are not so many Input or Output Pins so that i could use the 3 pins needed for the shift register to handle 4 or more pins of the modules to effectively free up one pin to use for the piezzo buzzer (hope this explanation makes sense...)

A MCP23017 Port Expander will most likely also not work to connect the SPI devices over that Port Expander due to the Speed limitation of I2C in comparison to the SPI BUS right?

How would you go about it?

Hard to say, depends on which pins you using and for what.

Maybe post your code that has the details of the pins you are using ?

Hi there,
The main workflow is to generate a random filename, use the EloquentEsp32cam Library to take images (3 to get around an issue with the cam module in which the first image taken has a green tint), save it to the microSD Card and then use the SX128XLT library to send it via LoRa.

These are the Pin Definitions in the Settings.h file.

#define NSS 12
#define RFBUSY 14
#define NRESET 3               
#define DIO1 13                
#define DIO2 -1                 //not used 
#define DIO3 -1                 //not used                      
#define RX_EN -1               //pin for RX enable, used on some SX1280 devices, set to -1 if not used
#define TX_EN -1               //pin for TX enable, used on some SX1280 devices, set to -1 if not used

This is the main.cpp, with the main pin configs beeing:
#define SD_CS 1
#define SCK 15
#define MISO 33
#define MOSI 32

#include <Arduino.h>
#define USELORA
#include <eloquent_esp32cam.h>
#include <eloquent_esp32cam/viz/mjpeg.h>
#include <WiFi.h>
#include <vector>
#include <cstdint>
#include <SPI.h>
#include <SD.h>
#include <SX128XLT.h>
#include <ProgramLT_Definitions.h>
#include "Settings.h"


using namespace eloq;
using namespace eloq::viz;
using eloq::camera;
unsigned long lastPictureTime = 0;
const long pictureInterval = 30000;
String currentFileName = "";


#define SD_CS 1  
#define SCK 15
#define MISO 33
#define MOSI 32
SX128XLT LoRa;                               //create an SX128XLT library instance called LoRa, required by SDtransfer.h
//#define ENABLEMONITOR                      //enable monitor prints
#define PRINTSEGMENTNUM                      //enable this define to print segment numbers 
#define ENABLEFILECRC                        //enable this define to uses and show file CRCs
//#define DISABLEPAYLOADCRC                  //enable this define if you want to disable payload CRC checking
//#define DEBUG                              //see additional debug info

#define SDLIB                               //define SDLIB for SD.h or SDFATLIB for SDfat.h
//#define SDFATLIB
#include <DTSDlibrary.h>                     //library of SD functions
#include <SDtransfer.h> 

bool transmitting = false;


void useBuzzer() {
  // try to reuse a already used PIN like the SD_CS or MISO
  SPI.end(); // End SD card communication
  pinMode(MISO, OUTPUT);
  tone(MISO, 2000, 200);
  delay(250);
  noTone(MISO);
  tone(MISO, 2000, 200);
  delay(250);
  noTone(MISO);
  tone(MISO, 2000, 200);
  delay(250);
  noTone(MISO);
  delay(1000);
  ESP.restart();
}



void setRandomFileName() {
    // Define the characters that can be part of the random string
    const String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    String randomString = "";
    // Initialize random seed
    randomSeed(esp_random());
    // always generate a 8 length random string as SD library can handle 8 length file names ( plus the.jpg)
    for (size_t i = 0; i < 8; i++) {
        // Pick a random character from chars
        int index = random(chars.length()); // random() returns a number from 0 to chars.length()-1
        randomString += chars[index];
    }
    currentFileName = "/"+randomString+".jpg";
}


// take in filename, take 3 pictures, save the latest to sd card
void takeAndStorePicture() {
   //Serial.println("taking first image");
    if (!camera.capture().isOk()) {
        //Serial.println(camera.exception.toString());
        return;
    }
    delay(1000);
    //Serial.println("taking second image");
    if (!camera.capture().isOk()) {
        //.println(camera.exception.toString());
        return;
    }
    delay(1000);
    //Serial.println("taking third image");
    if (!camera.capture().isOk()) {
        //Serial.println(camera.exception.toString());
        return;
    }

    uint32_t imageSize = camera.getSizeInBytes(); // Get the size of the image in bytes
    uint8_t* imageBuffer = camera.frame -> buf;

    // Create a new file on the SD card with filename currentFileName and use the imageBuffer
    File imgFile = SD.open(currentFileName, FILE_WRITE); // Open the file in write mode
    if (!imgFile) {
        //Serial.println("Failed to open file for writing");
        return;
    }
    imgFile.write(imageBuffer, imageSize); // Write the image buffer to file
    imgFile.close(); // Close the file after writing
    //Serial.println("Image saved to SD card.");
}

// send the just taken and saved image from sd card via lora
void initTransmitOfSavedFile() {
  transmitting = true;
  #ifdef ENABLEMONITOR
  Monitorport.println(F("Transfer started"));
  #endif

  uint32_t filelength;

  // Allocate a modifiable array
  char FileName[currentFileName.length() + 1];  
  strcpy(FileName, currentFileName.c_str()); 

  filelength = SDsendFile(FileName, currentFileName.length());

    if (filelength)
    {
      transmitting = false;
  #ifdef ENABLEMONITOR
      Monitorport.println(F("Transfer finished"));
  #endif
    }
    else
    {
      transmitting = false;
  #ifdef ENABLEMONITOR
      Monitorport.println(F("Transfer failed"));
      Monitorport.println();
  #endif
    }
}


void setup() {

    #ifdef ENABLEMONITOR
  Monitorport.begin(115200);
  Monitorport.println();
  Monitorport.println(F(__FILE__));
  Monitorport.flush();
    #endif

  SPI.begin(SCK,MISO,MOSI);

  
  if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE))
  {
  }
  else
  {
#ifdef ENABLEMONITOR
    Monitorport.println(F("LoRa device error"));
#endif
    while (1)
    {
      //flash
    }
  }

#ifdef USELORA
  LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate);
  //Serial.println(F("Using LoRa packets"));
#endif

#ifdef USEFLRC
  LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword);
  Serial.println(F("Using FLRC packets"));
#endif


#ifdef ENABLEMONITOR
  Monitorport.println();
  Monitorport.print(F("Initializing SD card..."));
#endif
 
  if (DTSD_initSD(SD_CS))
  {
#ifdef ENABLEMONITOR
    Monitorport.println(F("SD Card initialized."));
#endif
  }
  else
  {
    //Monitorport.println(F("SD Card failed, or not present."));
    while (1) ; //led_Flash(100, 25);
  }

#ifdef ENABLEMONITOR
  Monitorport.println();
#endif

#ifdef DISABLEPAYLOADCRC
  LoRa.setReliableConfig(NoReliableCRC);
#endif


  if (LoRa.getReliableConfig(NoReliableCRC))
  {
#ifdef ENABLEMONITOR
    Monitorport.println(F("Payload CRC disabled"));
#endif
  }
  else
  {
#ifdef ENABLEMONITOR
    Monitorport.println(F("Payload CRC enabled"));
#endif
  }

  SDDTFileTransferComplete = false;

#ifdef ENABLEMONITOR
  Monitorport.println(F("SDfile transfer ready"));
  Monitorport.println();
#endif


//Serial.println("LoRa and SD setup complete");

//Setup Camera

delay(2000);
     
    camera.pinout.wrover();
    camera.brownout.disable();
    camera.resolution.hd();
    
    camera.quality.high();
    // init camera
    while (!camera.begin().isOk())
        delay(100);
        //Serial.println(camera.exception.toString());

    //Serial.println("Camera OK");
}


void loop() {
    
     unsigned long currentTime = millis();
    // Check if it's time to take and store a new picture
    if (currentTime - lastPictureTime > pictureInterval && !transmitting) {
        //Serial.println("Start Image Taking, Saving and Transmitting");
        lastPictureTime = currentTime; // Update lastPictureTime to current time

        // Set a new random file name
        setRandomFileName();
        delay(250);

        // Take a picture and store it
        takeAndStorePicture();
        delay(250);

        // Initialize transmission of the saved file

        // hier noch durch ein Timeout ergänzen so dass die Kamera durchgestartet wird, wenn der Transfer fehl schlägt
        initTransmitOfSavedFile();
        //useBuzzer();
    }
}

Well you can get a I2C bus in there.

DIO1 on the SX128x can be worked around since you can pick up TX and RX complete with a register read.

NRESET is needed to be sure the SX128X is reset properly, but thats only required at the init stage.

So if you used DIO1 and NRESET pins to drive an I2C IO expander you now have IO pins to drive NRESET and active buzzers etc. Plus now with an I2C bus you have a lot of other expansion possibilties.

However, the main issue you would have is that in my experience the SD interface is not that reliable when driven on the same SPI bus as the LoRa device. Careful code sequencing would be needed to prevent the SD and LoRa devices running at the same time, if there was a problem.

Good Point, i will give the GPIO expander a go.

Is there a reason you would just leave the DIO1 Pin away then or could the DIO1 itself be connected via the gpio expander.

You asked if there were spare pins ?

You can do without DIO1.

1 Like

A follow up on this one:
Running the SX1280XLT (especially the NRESET, RFBUSY and DIO1 pins) and a piezzo buzzer via software pwm using the MCP23017 seems to work quite well so far.

I still need to tweak the LoRa Transmission for better reach, the SD File Transfer works great for Bandwith LORA_BW_0800 and SpreadingFactor LORA_SF10, but not for Lower Bandwiths or higher Spreadingfactors like LORA_BW_0400 and LORA_SF12.
But i guess this is connected to the timouts.

Probably.

The example could have set the timeouts so that they coped with the longest range, i.e. slowest, packets, but long timeouts would really slow down the fast transfers.

If you want to see how far a particular set of LoRa parameters will reach its probably easier to use basic transmit and receive examples.

Yes the range testing to see if the target sender and receiver locations can actually make a connection are carried out with the more basic sender/receiver examples.

Which works well with these Lora Settings:

#define Frequency 2445000000                     //frequency of transmissions
#define Offset 0                                 //offset frequency for calibration purposes  
#define Bandwidth LORA_BW_0400                   //LoRa bandwidth
#define SpreadingFactor LORA_SF12                 //LoRa spreading factor
#define CodeRate LORA_CR_4_5                     //LoRa coding rate
const int8_t TXpower = 12;                      //LoRa transmit power in dBm
const uint16_t packet_delay = 1000;             //mS delay between packets

So the question is which config might be the reason the SD File transfer stops working at LORA_BW_0400 LORA_SF12.

This btw. is the working config at LORA_BW_0800 and LORA_SF10

const uint8_t Bandwidth = LORA_BW_0800;                    //LoRa bandwidth LORA_BW_0800
const uint8_t SpreadingFactor = LORA_SF10;                  //LoRa spreading factor
#define CodeRate LORA_CR_4_5                     //LoRa coding rate


const int8_t TXpower = 12;                      //LoRa transmit power in dBm
const uint16_t packet_delay = 1000;            //mS delay between packets
const uint32_t TXtimeoutmS = 5000;              //mS to wait for TX to complete
const uint32_t RXtimeoutmS = 60000;             //mS to wait for receiving a packet
const uint32_t ACKdelaymS = 0;                  //ms delay after packet actioned and ack sent
const uint32_t ACKsegtimeoutmS = 75;            //mS to wait for receiving an ACK before re-trying transmit segment
const uint32_t ACKopentimeoutmS = 250;          //mS to wait for receiving an ACK before re-trying transmit file open
const uint32_t ACKclosetimeoutmS = 250;         //mS to wait for receiving an ACK before re-trying transmit file close
const uint32_t DuplicatedelaymS = 10;           //ms delay if there has been an duplicate segment or command receipt
const uint32_t FunctionDelaymS = 0;             //delay between functions such as open file, send segments etc
const uint32_t PacketDelaymS = 1000;            //mS delay between transmitted packets such as DTInfo etc
const uint8_t StartAttempts = 2;                //number of attempts to start transfer before a fail
const uint8_t SendAttempts = 5;                 //number of attempts carrying out a process before a restart
const uint32_t NoAckCountLimit = 250;           //if no NoAckCount exceeds this value - restart transfer
const uint8_t HeaderSizeMax = 12;               //max size of header in bytes, minimum size is 7 bytes
const uint8_t DataSizeMax = 245;                //max size of data array in bytes
const uint8_t Maxfilenamesize = 32;             //size of DTfilename buffer

sorry for using this post as a rubber duck.

the issue was that i didn´t increase the timeouts enough.
this page helped me to get an idea:

and these are the settings required to send an 25825 bytes sized file with 400 bandwith and spreading factor 12 repeatedly.


const uint8_t Bandwidth = LORA_BW_0400;                    //LoRa bandwidth LORA_BW_0800
const uint8_t SpreadingFactor = LORA_SF12;                  //LoRa spreading factor
#define CodeRate LORA_CR_4_5                     //LoRa coding rate
const int8_t TXpower = 12;                      //LoRa transmit power in dBm
const uint16_t packet_delay = 1100;            //mS delay between packets
const uint32_t TXtimeoutmS = 5000;              //mS to wait for TX to complete
const uint32_t RXtimeoutmS = 60000;             //mS to wait for receiving a packet
const uint32_t ACKdelaymS = 0;                  //ms delay after packet actioned and ack sent
const uint32_t ACKsegtimeoutmS = 1000;            //mS to wait for receiving an ACK before re-trying transmit segment
const uint32_t ACKopentimeoutmS = 1000;          //mS to wait for receiving an ACK before re-trying transmit file open
const uint32_t ACKclosetimeoutmS = 1000;         //mS to wait for receiving an ACK before re-trying transmit file close
const uint32_t DuplicatedelaymS = 10;           //ms delay if there has been an duplicate segment or command receipt
const uint32_t FunctionDelaymS = 0;             //delay between functions such as open file, send segments etc
const uint32_t PacketDelaymS = 1100;            //mS delay between transmitted packets such as DTInfo etc
const uint8_t StartAttempts = 2;                //number of attempts to start transfer before a fail
const uint8_t SendAttempts = 5;                 //number of attempts carrying out a process before a restart
const uint32_t NoAckCountLimit = 250;           //if no NoAckCount exceeds this value - restart transfer
const uint8_t HeaderSizeMax = 12;               //max size of header in bytes, minimum size is 7 bytes
const uint8_t DataSizeMax = 245;                //max size of data array in bytes
const uint8_t Maxfilenamesize = 32;             //size of DTfilename buffer
const uint16_t NetworkID = 0x3210;              //a unique identifier to go out with packet

closing this now. :slight_smile:

Good you sorted it.

There is an advantage in using 2.4Ghz LoRa for this type of application, there are few duty cycle restrictions in this band so you can continuously send images etc.

However given the 2.4Ghz LoRa settings you are using are fairly slow, consider that you could send images faster and over much greater distances using UHF LoRa. The downside however is keeping to duty cycle restrictions.

You could save an IO pin too, no need for a BUSY pin on the SX127x UHF LoRa devices.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.