Timothy Lovett
Published © Apache-2.0

Solar LoRa'r

Offline at the edge bird classification and reporting over LoRa. Using TinyML for high accuracy many output detection.

IntermediateWork in progress5 hours94
Solar LoRa'r

Things used in this project

Hardware components

Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
MCU on the Solar LoRa'r
×1
Solar LoRa'r
Custom circuit board designed by Cosmic Bee (me)
×1
Grove Vision AI Module V2
Seeed Studio Grove Vision AI Module V2
×1
Cable Gland, PG7
Cable Gland, PG7
×1
2 Inch Acrylic Circle
×1
AM312
×1
3.7V 2500 mAh Lithium Ion Battery
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1
DFRobot Solar Panel 6V 1A
×1

Software apps and online services

TensorFlow
TensorFlow

Story

Read more

Custom parts and enclosures

SolarLoRarBottom

Sketchfab still processing.

SolarLoRarFront

Sketchfab still processing.

SolarLoRarRPiWindow

Sketchfab still processing.

SolarLoRarTop

Sketchfab still processing.

SolarLoRarThreadAttachment

Sketchfab still processing.

SolarLoRarSolarPanelAttachment

Sketchfab still processing.

Schematics

Solar LoRa'r Schematic

Code

arduino-sd.ino

C/C++
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h> 
#include "Adafruit_MCP23008.h"
#include <U8g2lib.h>

// ---------- I2C ----------
#define MCP_ADDR   0x20
#define OLED_ADDR  0x3C

// ---------- MCP23008 ----------
#define MCP_LORA_NRESET_GP 1    
#define MCP_SD_PWR_GP      3

// ---------- SPI pins (Sense defaults) ----------
#define PIN_SD_SCK   D8
#define PIN_SD_MISO  D9
#define PIN_SD_MOSI  D10
#define PIN_SD_CS    D0      

// ---------- Other SPI device CS ----------
#define PIN_LORA_CS  D2        

// ---------- OLED ----------
U8G2_SH1107_SEEED_128X128_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// ---------- MCP ----------
Adafruit_MCP23008 mcp;

// ---------- Results ----------
static const uint8_t MAX_ITEMS = 12;
struct DirEntry { char name[26]; bool isDir; uint32_t size; };
DirEntry items[MAX_ITEMS];
uint8_t nItems = 0;

bool sdOK = false;
const char* sdMsg = "";
uint8_t attempts = 0;
bool powerCycled = false;

// ---------- UI ----------
static void header(const char* t){
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(0,10,t);
  u8g2.drawHLine(0,12,128);
}
static void draw(const char* phase, const char* msg){
  u8g2.firstPage(); do{
    header("SD (Arduino SD, HW SPI)");
    u8g2.setFont(u8g2_font_6x10_tf);
    u8g2.drawStr(0,26,"Phase:");      u8g2.drawStr(42,26,phase);
    u8g2.drawStr(0,38,"SD Power:");   u8g2.drawStr(62,38, "ON (GP3=HI)");
    u8g2.drawStr(0,50,"SD Init:");    u8g2.drawStr(50,50, sdOK ? "OK" : "FAIL");

    char b1[32]; snprintf(b1,sizeof(b1),"Attempts:%u", attempts);
    u8g2.drawStr(0,62,b1);

    char b2[32]; snprintf(b2,sizeof(b2),"PwrCycle:%s", powerCycled?"YES":"NO");
    u8g2.drawStr(80,62,b2);

    if(msg&&*msg){ u8g2.drawStr(0,74,"Msg:"); u8g2.drawStr(28,74,msg); }

    u8g2.drawHLine(0,86,128);
    u8g2.drawStr(0,98,"Root entries:");
    uint8_t y=110;
    for(uint8_t i=0;i<nItems && i<MAX_ITEMS;i++){
      char nm[22]; size_t L=strlen(items[i].name);
      if(L<=20){ strncpy(nm,items[i].name,sizeof(nm)); nm[L]='\0'; }
      else{ snprintf(nm,sizeof(nm),"\xE2\x80\xA6%s", items[i].name+(L-19)); }
      u8g2.drawStr(0,y,nm);

      char inf[18];
      if(items[i].isDir) snprintf(inf, sizeof(inf), "<DIR>");
      else snprintf(inf, sizeof(inf), "%lu B", (unsigned long)items[i].size);
      u8g2.drawStr(90,y,inf);

      y += 12;
      if(y > 124) break;
    }
  } while(u8g2.nextPage());
}

// ---------- Helpers ----------
static void resetItems(){
  nItems = 0;
  for(uint8_t i=0;i<MAX_ITEMS;i++){
    items[i].name[0] = '\0';
    items[i].isDir = false;
    items[i].size = 0;
  }
}

static void addItem(const char* n, bool isDir, uint32_t sz){
  if(nItems >= MAX_ITEMS) return;
  strncpy(items[nItems].name, (n && *n) ? n : "(unnamed)", sizeof(items[nItems].name));
  items[nItems].name[sizeof(items[nItems].name)-1] = '\0';
  items[nItems].isDir = isDir;
  items[nItems].size  = sz;
  nItems++;
}

static void listRootOnce(const char** outMsg){
  resetItems();
  File root = SD.open("/");
  if(!root){ *outMsg = "open('/') failed"; return; }

  for(;;){
    File e = root.openNextFile();
    if(!e) break;
    const char* nm = e.name();
    if(e.isDirectory()){
      addItem(nm, true, 0);
    } else {
      addItem(nm, false, (uint32_t)e.size());
    }
    e.close();
    if(nItems >= MAX_ITEMS) break;
  }
  root.close();
  *outMsg = "OK";
}

// Try init up to n attempts with short delays
static bool sd_try_begin(uint8_t n, uint16_t wait_ms_between){
  for(uint8_t i=0;i<n;i++){
    attempts++;
    if(SD.begin(PIN_SD_CS)) return true;
    delay(wait_ms_between);
  }
  return false;
}

// Hard power-cycle SD rail via MCP, then retry
static bool sd_powercycle_and_retry(){
  powerCycled = true;

  // Put SD CS HIGH and float the bus briefly
  pinMode(PIN_SD_CS, OUTPUT);
  digitalWrite(PIN_SD_CS, HIGH);

  // Power OFF
  mcp.digitalWrite(MCP_SD_PWR_GP, LOW);
  delay(50);

  // Power ON and settle
  mcp.digitalWrite(MCP_SD_PWR_GP, HIGH);
  delay(300);

  // Retry a few times
  return sd_try_begin(3, 60);
}

// ---------- Setup ----------
void setup(){
  // I2C + OLED (no Serial used)
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.setI2CAddress(OLED_ADDR << 1);

  // MCP: power SD; keep LoRa out of the way just by keeping CS HIGH (no reset)
  mcp.begin(MCP_ADDR);
  mcp.pinMode(MCP_SD_PWR_GP, OUTPUT);
  mcp.digitalWrite(MCP_SD_PWR_GP, HIGH);       // SD rail ON
  mcp.pinMode(MCP_LORA_NRESET_GP, OUTPUT);
  mcp.digitalWrite(MCP_LORA_NRESET_GP, HIGH);  // LoRa NOT reset

  delay(200); // rail settle

  // Bring up SPI on Sense defaults (already D8/D9/D10; no remap needed)
  SPI.begin();

  // Deassert chip selects on MCU side
  pinMode(PIN_LORA_CS, OUTPUT); digitalWrite(PIN_LORA_CS, HIGH);
  pinMode(PIN_SD_CS,  OUTPUT);  digitalWrite(PIN_SD_CS,  HIGH);

  draw("POWER ON", "starting...");

  bool ok = sd_try_begin(3, 60);       // three attempts with short gaps
  if(!ok) ok = sd_powercycle_and_retry();

  sdOK  = ok;
  sdMsg = ok ? "OK" : "SD.begin() failed";

  if(!sdOK){
    draw("INIT FAIL", sdMsg);
    return;
  }

  const char* msg = "";
  listRootOnce(&msg);
  draw("LIST ROOT", msg);
}

// ---------- Loop ----------
void loop(){
  static uint32_t t0 = 0; uint32_t now = millis();
  if(now - t0 >= 1000){
    t0 = now;
    draw(sdOK ? "LIST ROOT" : "INIT FAIL", sdMsg);
  }
}

arduino-boost.ino

C/C++
#include <Arduino.h>
#include <Wire.h>
#include "Adafruit_MCP23008.h"
#include <U8g2lib.h>

// ---------- I2C addresses ----------
#define MCP_ADDR   0x20
#define OLED_ADDR  0x3C

// ---------- MCP23008 ----------
#define MCP_BOOST_EN_GP   6   // GP6 drives 5V boost EN: LOW=OFF, HIGH=ON

// ---------- OLED ----------
U8G2_SH1107_SEEED_128X128_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// ---------- MCP ----------
Adafruit_MCP23008 mcp;

// ---------- LED helpers ----------
#ifndef LED_RED
  #define LED_RED   LED_BUILTIN
#endif
#ifndef LED_GREEN
  #define LED_GREEN LED_BUILTIN
#endif
#ifndef LED_BLUE
  #define LED_BLUE  LED_BUILTIN
#endif

static inline void ledWrite(uint8_t pin, bool onActiveLow) {
  pinMode(pin, OUTPUT);
  digitalWrite(pin, onActiveLow ? LOW : HIGH);
}
static void setLED(bool r, bool g, bool b) {
  bool activeLow = true;
  if ((LED_RED == LED_GREEN) && (LED_GREEN == LED_BLUE)) {
    ledWrite(LED_RED, activeLow ? (r || g || b) : !(r || g || b));
  } else {
    ledWrite(LED_RED,   activeLow ? r : !r);
    ledWrite(LED_GREEN, activeLow ? g : !g);
    ledWrite(LED_BLUE,  activeLow ? b : !b);
  }
}
static void ledOff() { setLED(false,false,false); }

#define PHASE_PREBOOST  0u
#define PHASE_BOOST     1u

// Durations
static const uint32_t PHASE_MS = 60000UL;  // 60 s per phase
static const uint32_t BLINK_MS = 300UL;    // yellow blink cadence

// Run-time state
static uint8_t  phase      = PHASE_PREBOOST;
static uint32_t phaseStart = 0;
static uint32_t lastBlinkT = 0;
static bool     blinkOn    = false;

// ---------- UI ----------
static void drawPage(uint8_t ph, uint32_t msRemaining) {
  char line[40];

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x10_tf);
    u8g2.drawStr(0,10,"Boost Test (GP6 via MCP23008)");
    u8g2.drawHLine(0,12,128);

    const char* phaseName = (ph == PHASE_PREBOOST) ? "PRE-BOOST (OFF)" : "BOOST ON";
    u8g2.drawStr(0,28, "Phase:");
    u8g2.drawStr(50,28, phaseName);

    u8g2.drawStr(0,42, "GP6 (EN):");
    u8g2.drawStr(60,42, (ph == PHASE_BOOST) ? "HIGH" : "LOW");

    uint32_t secs = (msRemaining + 999) / 1000;
    snprintf(line, sizeof(line), "Time left: %lus", (unsigned long)secs);
    u8g2.drawStr(0,56, line);

    if (ph == PHASE_PREBOOST) {
      u8g2.drawStr(0,70, "LED: Blinking YELLOW");
    } else {
      u8g2.drawStr(0,70, "LED: Solid GREEN");
    }

    u8g2.drawHLine(0,114,128);
    u8g2.drawStr(0,126,"Cycle: 60s OFF (blink) -> 60s ON -> repeat");
  } while (u8g2.nextPage());
}

// ---------- Setup ----------
void setup() {
  // I2C + OLED
  Wire.begin();
  Wire.setClock(400000);
  u8g2.begin();
  u8g2.setI2CAddress(OLED_ADDR << 1);

  // MCP: start boost OFF
  mcp.begin(MCP_ADDR);
  mcp.pinMode(MCP_BOOST_EN_GP, OUTPUT);
  mcp.digitalWrite(MCP_BOOST_EN_GP, LOW);

  // Initialize state
  phase = PHASE_PREBOOST;
  phaseStart = millis();
  lastBlinkT = phaseStart;
  blinkOn = false;

  // Initial UI/LED
  setLED(false, false, false);
  drawPage(phase, PHASE_MS);
}

// ---------- Loop ----------
void loop() {
  uint32_t now = millis();
  uint32_t elapsed = now - phaseStart;

  // Phase transition
  if (elapsed >= PHASE_MS) {
    phase = (phase == PHASE_PREBOOST) ? PHASE_BOOST : PHASE_PREBOOST;
    phaseStart = now;
    lastBlinkT = now;
    blinkOn = false;

    if (phase == PHASE_PREBOOST) {
      // Boost OFF, start blinking yellow
      mcp.digitalWrite(MCP_BOOST_EN_GP, LOW);
      setLED(true, true, false);   // yellow
      blinkOn = true;
    } else {
      // Boost ON, solid green
      mcp.digitalWrite(MCP_BOOST_EN_GP, HIGH);
      setLED(false, true, false);  // green
    }

    drawPage(phase, PHASE_MS);
    return; // done this tick
  }

  // In-phase behavior
  if (phase == PHASE_PREBOOST) {
    // Blink yellow
    if ((now - lastBlinkT) >= BLINK_MS) {
      lastBlinkT = now;
      blinkOn = !blinkOn;
      if (blinkOn) setLED(true, true, false);
      else         ledOff();
    }
  } else {
    // Ensure outputs stay asserted
    mcp.digitalWrite(MCP_BOOST_EN_GP, HIGH);
    setLED(false, true, false);
  }

  // UI refresh ~5 Hz
  static uint32_t lastUi = 0;
  if ((now - lastUi) >= 200) {
    lastUi = now;
    uint32_t remaining = (elapsed >= PHASE_MS) ? 0 : (PHASE_MS - elapsed);
    drawPage(phase, remaining);
  }
}

arduino-lora.ino

C/C++
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <RadioLib.h>
#include "Adafruit_MCP23008.h"
#include <U8g2lib.h>

// -------------------- Pins --------------------
#define RADIO_CS    D2
#define RADIO_DIO1  D3
#define RADIO_BUSY  D7        
#define RADIO_RXEN  D6  

#define MCP_ADDR        0x20
#define MCP_RESET_PIN   1   

#define OLED_ADDR       0x3C
U8G2_SH1107_SEEED_128X128_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

#ifndef LED_RED
  #define LED_RED   LED_BUILTIN
#endif
#ifndef LED_GREEN
  #define LED_GREEN LED_BUILTIN
#endif
#ifndef LED_BLUE
  #define LED_BLUE  LED_BUILTIN
#endif

Adafruit_MCP23008 mcp;
SX1262 radio = new Module(RADIO_CS, RADIO_DIO1, -1, RADIO_BUSY);

// -------------------- Globals --------------------
volatile uint32_t tx_count = 0;
volatile int last_tx_state = -999;
volatile bool last_retry = false;
int init_state = -999;
float tcxo_v = 2.4;   

// -------------------- UI helpers ------------------
static inline void ledWrite(uint8_t pin, bool onActiveLow) {
  pinMode(pin, OUTPUT);
  digitalWrite(pin, onActiveLow ? LOW : HIGH);
}
void setLED(bool r, bool g, bool b) {
  bool activeLow = true;
  if ((LED_RED == LED_GREEN) && (LED_GREEN == LED_BLUE)) {
    ledWrite(LED_RED, activeLow ? (r||g||b) : !(r||g||b));
  } else {
    ledWrite(LED_RED,   activeLow ? r : !r);
    ledWrite(LED_GREEN, activeLow ? g : !g);
    ledWrite(LED_BLUE,  activeLow ? b : !b);
  }
}
void showStatusLED(int s) {
  switch (s) {
    case 0:  setLED(false,true,false); break; // green
    case -1: setLED(true,false,true);  break; // purple
    case -2: setLED(true,false,false); break; // red
    case -3: setLED(false,false,true); break; // blue
    default: setLED(false,false,false); break;
  }
}
void oledHeader(const char* title) {
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(0,10,title);
  u8g2.drawHLine(0,12,128);
}
const char* explainErr(int code) {
  switch (code) {
    case RADIOLIB_ERR_NONE:          return "OK";
    case -10001:                     return "BUSY STUCK";
    case RADIOLIB_ERR_CHIP_NOT_FOUND:return "CHIP NOT FOUND";
    default:                         return "ERR";
  }
}
void drawStatusPage(const char* phase, int initSt, int txSt, bool retried, int busyLevel, float tcxoV) {
  u8g2.firstPage();
  do {
    oledHeader("SX1262 (DIO2 RF-SW)");
    u8g2.setFont(u8g2_font_6x10_tf);

    u8g2.drawStr(0,26,"Phase:");   u8g2.drawStr(44,26, phase);
    u8g2.drawStr(0,38,"BUSY:");    u8g2.drawStr(44,38, busyLevel ? "HIGH" : "LOW");

    u8g2.drawStr(0,50,"Init:");
    { const char* e = (initSt== -999) ? "" : explainErr(initSt); u8g2.drawStr(44,50, e); }

    u8g2.drawStr(0,62,"LastTX:");
    { const char* e = (txSt== -999) ? "" : (txSt==RADIOLIB_ERR_NONE ? "OK" : explainErr(txSt));
      u8g2.drawStr(56,62, e); }

    u8g2.drawStr(0,74,"Retry:");   u8g2.drawStr(44,74, retried ? "YES" : "NO");

    u8g2.drawStr(0,86,"TX Cnt:");
    { char b[20]; snprintf(b,sizeof(b),"%lu",(unsigned long)tx_count); u8g2.drawStr(56,86,b); }

    u8g2.drawStr(0,98,"TCXO:");
    { char b[16]; snprintf(b,sizeof(b),"%.1f V", tcxoV); u8g2.drawStr(44,98,b); }

    u8g2.drawHLine(0,114,128);
    u8g2.drawStr(0,126,"915MHz SF7 BW125 CR5 P8 14dBm");
  } while (u8g2.nextPage());
}

// -------------------- Timing/Reset -----------------
void mcpResetPulse() {
  mcp.digitalWrite(MCP_RESET_PIN, LOW);
  delay(10);                        
  mcp.digitalWrite(MCP_RESET_PIN, HIGH);
}
bool waitBusyLow(uint32_t timeout_ms) {
  uint32_t t0 = millis();
  while (millis() - t0 < timeout_ms) {
    if (digitalRead(RADIO_BUSY) == LOW) return true;
    delay(1);
  }
  return false;
}

// -------------------- Radio init paths -------------
int radio_init_sequence(float tcxoVolts) {
  if (!waitBusyLow(1500)) return -10001;  // custom: busy stuck high

  // Use 1-byte sync in begin(); set 16-bit sync right after
  const uint8_t ONE_BYTE_SYNC = 0x12;

  int s = radio.begin(915.0, 125.0, 7, 5, ONE_BYTE_SYNC, 14, 8);
  if (s != RADIOLIB_ERR_NONE) return s;

  radio.setTCXO(tcxoVolts);     // DIO3 powers TCXO
  delay(5);

  radio.setSyncWord(0x1424);    // full 16-bit sync (no truncation)
  radio.setDio2AsRfSwitch(true);// DIO2 auto TX/RX control

  return RADIOLIB_ERR_NONE;
}

bool try_full_boot() {
  pinMode(RADIO_RXEN, INPUT);   
  pinMode(RADIO_BUSY, INPUT); 

  mcpResetPulse();
  drawStatusPage("RADIO RST", -999, last_tx_state, last_retry, digitalRead(RADIO_BUSY), tcxo_v);
  if (!waitBusyLow(1500)) { init_state = -10001; return false; }

  // Try 2.4 V first
  tcxo_v = 2.4;
  init_state = radio_init_sequence(tcxo_v);
  drawStatusPage("INIT 2.4V", init_state, last_tx_state, last_retry, digitalRead(RADIO_BUSY), tcxo_v);
  if (init_state == RADIOLIB_ERR_NONE) return true;

  // Fallback: 1.8 V
  mcpResetPulse();
  waitBusyLow(1500);
  tcxo_v = 1.8;
  init_state = radio_init_sequence(tcxo_v);
  drawStatusPage("INIT 1.8V", init_state, last_tx_state, last_retry, digitalRead(RADIO_BUSY), tcxo_v);
  return (init_state == RADIOLIB_ERR_NONE);
}

// -------------------- Robust TX --------------------
int reinit_after_fault() {
  const uint8_t ONE_BYTE_SYNC = 0x12;
  int s = radio.begin(915.0, 125.0, 7, 5, ONE_BYTE_SYNC, 14, 8);
  if (s != RADIOLIB_ERR_NONE) return s;
  radio.setTCXO(tcxo_v);
  delay(5);
  radio.setSyncWord(0x1424);
  radio.setDio2AsRfSwitch(true);
  return RADIOLIB_ERR_NONE;
}

int transmit_with_retry(const char* payload, bool &didRetry) {
  didRetry = false;
  int st = radio.transmit(payload);
  if (st == RADIOLIB_ERR_NONE) return st;

  mcpResetPulse();
  waitBusyLow(1500);
  int si = reinit_after_fault();
  if (si != RADIOLIB_ERR_NONE) return st;

  didRetry = true;
  return radio.transmit(payload);
}

// -------------------- Setup ------------------------
void setup() {
  showStatusLED(-1);

  Wire.begin();
  Wire.setClock(400000);

  u8g2.begin();
  u8g2.setI2CAddress(OLED_ADDR << 1);
  drawStatusPage("BOOT", -999, -999, false, -1, tcxo_v);

  mcp.begin(MCP_ADDR);
  mcp.pinMode(MCP_RESET_PIN, OUTPUT);
  mcp.digitalWrite(MCP_RESET_PIN, HIGH);
  drawStatusPage("MCP OK", -999, -999, false, digitalRead(RADIO_BUSY), tcxo_v);

  bool ok = try_full_boot();
  showStatusLED(ok ? 0 : -2);
}

// -------------------- Loop -------------------------
void loop() {
  const char* msg = "Hello World";

  bool retried = false;
  int st = transmit_with_retry(msg, retried);
  last_tx_state = st;
  last_retry = retried;

  if (st == RADIOLIB_ERR_NONE) { tx_count++; showStatusLED(0); }
  else                          { showStatusLED(-2); }

  drawStatusPage("TX", init_state, st, retried, digitalRead(RADIO_BUSY), tcxo_v);

  delay(150);
  drawStatusPage("IDLE", init_state, st, retried, digitalRead(RADIO_BUSY), tcxo_v);
  delay(1850);
}

arduino-interrupt.ino

C/C++
#include <Arduino.h>
#include <Wire.h>
#include "Adafruit_MCP23008.h"
#include <U8g2lib.h>

#ifndef IRAM_ATTR
#define IRAM_ATTR
#endif

// ---- I2C addresses ----
#define MCP_ADDR   0x20
#define OLED_ADDR  0x3C

// ---- Pins ----
#define MCP_INT_PIN  D1 
#define GP_REED      5        // MCP GP5 (reed, active-low)
#define GP_PIR       7        // MCP GP7 (AM312, active-high)

// ---- Debounce ----
const uint16_t REED_DEBOUNCE_MS = 30;
const uint16_t PIR_DEBOUNCE_MS  = 100;

// ---- Globals ----
U8G2_SH1107_SEEED_128X128_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
Adafruit_MCP23008 mcp;

volatile bool mcp_int_pending = false;

uint32_t reed_count = 0, pir_count = 0, irq_count = 0;
uint32_t last_reed_ms = 0, last_pir_ms = 0;

uint8_t  last_intf = 0x00, last_intcap = 0x00, cached_gpio = 0x00;

bool     sticky_irq = false;
uint32_t sticky_until_ms = 0;
const uint16_t STICKY_MS = 800;

bool     int_mismatch = false;

// ---- MCP23008 regs ----
enum : uint8_t {
  MCP_IODIR  = 0x00, MCP_IPOL   = 0x01, MCP_GPINTEN= 0x02,
  MCP_DEFVAL = 0x03, MCP_INTCON = 0x04, MCP_IOCON  = 0x05,
  MCP_GPPU   = 0x06, MCP_INTF   = 0x07, MCP_INTCAP = 0x08,
  MCP_GPIO   = 0x09, MCP_OLAT   = 0x0A
};

static inline void mcpWrite(uint8_t reg, uint8_t val){
  Wire.beginTransmission(MCP_ADDR); Wire.write(reg); Wire.write(val); Wire.endTransmission();
}
static inline uint8_t mcpRead(uint8_t reg){
  Wire.beginTransmission(MCP_ADDR); Wire.write(reg); Wire.endTransmission(false);
  Wire.requestFrom((int)MCP_ADDR, 1); return Wire.available()? Wire.read() : 0xFF;
}

// ---- UI ----
static void header(const char* t){ u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0,10,t); u8g2.drawHLine(0,12,128); }
static void draw(bool show_irq){
  char b[56];
  uint8_t gpio_now = cached_gpio; // cached snapshot

  u8g2.firstPage(); do {
    header("MCP23008 INT (edge/ISR)");

    u8g2.drawStr(0,26, show_irq ? "Phase: IRQ (sticky)" : "Phase: IDLE");

    int int_pin_level = digitalRead(MCP_INT_PIN);
    snprintf(b,sizeof(b),"INT(D1) level: %s", int_pin_level ? "HIGH" : "LOW");
    u8g2.drawStr(0,38,b);

    uint8_t live_intf = mcpRead(MCP_INTF);
    snprintf(b,sizeof(b),"MCP INTF (live): 0x%02X", live_intf);
    u8g2.drawStr(0,50,b);

    snprintf(b,sizeof(b),"GPIO cache: 0b%c%c%c%c%c%c%c%c",
      (gpio_now&0x80)?'1':'0',(gpio_now&0x40)?'1':'0',
      (gpio_now&0x20)?'1':'0',(gpio_now&0x10)?'1':'0',
      (gpio_now&0x08)?'1':'0',(gpio_now&0x04)?'1':'0',
      (gpio_now&0x02)?'1':'0',(gpio_now&0x01)?'1':'0');
    u8g2.drawStr(0,62,b);

    snprintf(b,sizeof(b),"Last INTF:0x%02X INTCAP:0x%02X", last_intf, last_intcap);
    u8g2.drawStr(0,74,b);

    snprintf(b,sizeof(b),"Reed(GP5): %s", (gpio_now&(1<<GP_REED))?"OPEN(1)":"CLOSED(0)");
    u8g2.drawStr(0,86,b);
    snprintf(b,sizeof(b),"PIR (GP7): %s", (gpio_now&(1<<GP_PIR))?"HIGH":"LOW");
    u8g2.drawStr(0,98,b);

    snprintf(b,sizeof(b),"Counts  Reed:%lu PIR:%lu IRQ:%lu",
      (unsigned long)reed_count,(unsigned long)pir_count,(unsigned long)irq_count);
    u8g2.drawStr(0,110,b);

    if (int_mismatch) u8g2.drawStr(86,26,"[INT?]");
  } while(u8g2.nextPage());
}

// ---- ISR ----
void IRAM_ATTR onMcpIntFalling(){
  mcp_int_pending = true;
}

static void mcp_config(){
  mcpWrite(MCP_IODIR, 0xFF);
  mcpWrite(MCP_IPOL,  0x00);

  mcpWrite(MCP_GPPU, (1<<GP_REED));

  // Interrupt enable on GP5 & GP7
  mcpWrite(MCP_GPINTEN, (1<<GP_REED)|(1<<GP_PIR));
  mcpWrite(MCP_INTCON, 0x00);
  mcpWrite(MCP_DEFVAL, 0x00);

  // IOCON: ODR=1 (open-drain), INTPOL=0 (active-low)
  uint8_t iocon = 0x00;
  iocon |= 0x04; // ODR=1 open-drain
  // INTPOL=0 (active-low)
  mcpWrite(MCP_IOCON, iocon);
  cached_gpio = mcpRead(MCP_GPIO);
}

// ---- Setup ----
void setup(){
  Wire.begin(); Wire.setClock(400000);
  u8g2.begin(); u8g2.setI2CAddress(OLED_ADDR<<1);

  mcp.begin(MCP_ADDR);
  mcp_config();

  // External pull-up present -> MCU pin as plain INPUT
  pinMode(MCP_INT_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(MCP_INT_PIN), onMcpIntFalling, FALLING);

  draw(false);
}

// ---- Handle IRQ in main context ----
static void service_irq(){
  uint8_t intf   = mcpRead(MCP_INTF);
  uint8_t intcap = mcpRead(MCP_INTCAP);
  uint8_t gpio   = mcpRead(MCP_GPIO); 

  uint32_t now = millis();
  bool any=false;

  if (intf & (1<<GP_REED)) {
    // FALLING only: INTCAP[5]==0 means it just went low
    if ((intcap & (1<<GP_REED)) == 0) {
      if (now - last_reed_ms >= REED_DEBOUNCE_MS) { reed_count++; last_reed_ms = now; }
    }
    any=true;
  }
  if (intf & (1<<GP_PIR)) {
    // RISING only: INTCAP[7]==1 means it just went high
    if ((intcap & (1<<GP_PIR)) != 0) {
      if (now - last_pir_ms >= PIR_DEBOUNCE_MS) { pir_count++; last_pir_ms = now; }
    }
    any=true;
  }

  if (any){
    irq_count++; last_intf=intf; last_intcap=intcap; cached_gpio=gpio;
    sticky_irq = true; sticky_until_ms = now + STICKY_MS;
  }
}

// ---- Loop ----
void loop(){
  // Service hardware IRQ if flagged
  if (mcp_int_pending){
    noInterrupts(); mcp_int_pending=false; interrupts();
    service_irq();
  }

  uint8_t live_intf = mcpRead(MCP_INTF);
  int int_pin_level = digitalRead(MCP_INT_PIN);
  int_mismatch = (live_intf != 0) && (int_pin_level != LOW);

  // Refresh GPIO cache when no IRQ pending
  static uint32_t last_gpio_ms = 0;
  uint32_t now = millis();
  if (live_intf == 0 && (now - last_gpio_ms >= 200)) {
    last_gpio_ms = now;
    cached_gpio = mcpRead(MCP_GPIO);
  }

  bool show_irq = sticky_irq && (now < sticky_until_ms);
  if (sticky_irq && now >= sticky_until_ms) sticky_irq = false;

  static uint32_t last_draw = 0;
  if (now - last_draw >= 150) {
    last_draw = now;
    draw(show_irq);
  }
}

Credits

Timothy Lovett
22 projects • 18 followers
Maker. I spent over a decade working on backend systems in various languages.

Comments