MakerCell v1.0 DIY LiPo Battery Pack

Full Project Download

1) 3D Model

powered by Advanced iFrame

2) Arduino Code

makercell_firmware_v1.ino
#include <Arduino.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <avr/io.h>

// ===== Pin configuration =====
#define LED1        PIN_PA4
#define LED2        PIN_PA5
#define LED3        PIN_PA6
#define LED4        PIN_PA7

#define SDA_PIN     PIN_PA1
#define SCL_PIN     PIN_PA2

#define PIN_USBDET  PIN_PC0   // 5V detect input
#define PIN_CHG     PIN_PC2   // TP4056 CHG? active LOW
#define PIN_BUTTON  PIN_PC3   // Button active LOW

// ===== MAX17048 =====
static const uint8_t MAX_ADDR = 0x36;
static const uint8_t REG_VCELL   = 0x02;
static const uint8_t REG_SOC     = 0x04;
static const uint8_t REG_MODE    = 0x06;
static const uint8_t REG_VERSION = 0x08;

// ===== Thresholds =====
const float V1 = 3.65, V2 = 3.80, V3 = 3.95, V4 = 4.10;
const float HYST = 0.02;

// ===== Timing =====
const unsigned long flashPeriod     = 1000;  // 1s blink
const unsigned long displayDuration = 3000;
const unsigned long pollInterval    = 1000;
const uint16_t DEBOUNCE_MS = 80;

// ===== Bitbang timings =====
static const uint8_t SCL_HIGH_US = 10;  // how long we FORCE SCL high (contention window)
static const uint8_t SCL_LOW_US  = 15;  // low time
static const uint8_t BIT_REST_US = 5;   // optional extra rest between bits

// ===== State =====
volatile bool usbEvent = false;
volatile bool buttonEvent = false;
volatile unsigned long lastButtonPress = 0;

bool usbPlugged = false;
bool blinkState = false;
unsigned long lastToggle = 0;
unsigned long lastPoll = 0;
bool gaugeOK = false;

// ---------- GPIO helpers ----------
static inline void sdaRelease(){ pinMode(SDA_PIN, INPUT); }                       // float high via pullup
static inline void sclRelease(){ pinMode(SCL_PIN, INPUT); }                       // float high via pullup
static inline void sdaLow()    { pinMode(SDA_PIN, OUTPUT); digitalWrite(SDA_PIN, LOW); }
static inline void sclLow()    { pinMode(SCL_PIN, OUTPUT); digitalWrite(SCL_PIN, LOW); }
// PUSH-PULL HIGH (abusive).
static inline void sclHighDrive(){ pinMode(SCL_PIN, OUTPUT); digitalWrite(SCL_PIN, HIGH); }

static inline uint8_t rd(uint8_t p){ delayMicroseconds(2); return digitalRead(p); }
static inline bool sdaHigh(){ return rd(SDA_PIN) == HIGH; }
static inline bool sclHigh(){ return rd(SCL_PIN) == HIGH; }

// Button must do NOTHING while actively charging
static inline bool isChargingNow() {
  return (usbPlugged && (digitalRead(PIN_CHG) == LOW));
}

void setAllLEDs(bool s){
  digitalWrite(LED1, s); digitalWrite(LED2, s);
  digitalWrite(LED3, s); digitalWrite(LED4, s);
}

static float median3(float a, float b, float c) {
  if (a > b) { float t = a; a = b; b = t; }
  if (b > c) { float t = b; b = c; c = t; }
  if (a > b) { float t = a; a = b; b = t; }
  return b;
}

// ---------- Bit-banged I2C (SDA open-drain, SCL forced push-pull) ----------
static inline void bbDelayRest(){ if (BIT_REST_US) delayMicroseconds(BIT_REST_US); }

static inline void bbStart(){
  sdaRelease();
  sclHighDrive();
  delayMicroseconds(SCL_HIGH_US);
  sdaLow();                 // SDA falls while SCL high
  delayMicroseconds(5);
  sclLow();
  delayMicroseconds(SCL_LOW_US);
}

static inline void bbStop(){
  sdaLow();
  sclHighDrive();
  delayMicroseconds(SCL_HIGH_US);
  sdaRelease();             // SDA rises while SCL high
  delayMicroseconds(5);
  sclLow();                 // leave low briefly then release
  delayMicroseconds(5);
  sclRelease();
}

static inline void bbWriteBit(bool bit){
  if (bit) sdaRelease(); else sdaLow();
  sclHighDrive();
  delayMicroseconds(SCL_HIGH_US);
  sclLow();
  delayMicroseconds(SCL_LOW_US);
  bbDelayRest();
}

static inline bool bbReadBit(){
  sdaRelease();
  sclHighDrive();
  delayMicroseconds(SCL_HIGH_US / 2);
  bool bit = sdaHigh();
  delayMicroseconds(SCL_HIGH_US - (SCL_HIGH_US / 2));
  sclLow();
  delayMicroseconds(SCL_LOW_US);
  bbDelayRest();
  return bit;
}

static bool bbWriteByte(uint8_t byte){
  for (uint8_t i = 0; i < 8; i++){
    bbWriteBit(byte & 0x80);
    byte <<= 1;
  }
  // ACK bit
  sdaRelease();
  sclHighDrive();
  delayMicroseconds(SCL_HIGH_US / 2);
  bool ack = !sdaHigh();   // ACK = 0
  delayMicroseconds(SCL_HIGH_US - (SCL_HIGH_US / 2));
  sclLow();
  delayMicroseconds(SCL_LOW_US);
  bbDelayRest();
  return ack;
}

static uint8_t bbReadByte(bool ack){
  uint8_t val = 0;
  for (uint8_t i = 0; i < 8; i++){
    val <<= 1;
    if (bbReadBit()) val |= 1;
  }
  // send ACK (0) or NACK (1)
  bbWriteBit(!ack); // ack=true -> drive 0; ack=false -> release for 1
  return val;
}

static bool maxRead16(uint8_t reg, uint16_t &out){
  bbStart();
  if (!bbWriteByte((MAX_ADDR << 1) | 0)) { bbStop(); return false; } // W
  if (!bbWriteByte(reg))                 { bbStop(); return false; }
  bbStart(); // repeated start
  if (!bbWriteByte((MAX_ADDR << 1) | 1)) { bbStop(); return false; } // R
  uint8_t hi = bbReadByte(true);
  uint8_t lo = bbReadByte(false); // NACK last byte
  bbStop();
  out = ((uint16_t)hi << 8) | lo;
  return true;
}

static bool maxWrite16(uint8_t reg, uint16_t val){
  bbStart();
  if (!bbWriteByte((MAX_ADDR << 1) | 0))  { bbStop(); return false; }
  if (!bbWriteByte(reg))                  { bbStop(); return false; }
  if (!bbWriteByte((uint8_t)(val >> 8)))  { bbStop(); return false; }
  if (!bbWriteByte((uint8_t)(val & 0xFF))){ bbStop(); return false; }
  bbStop();
  return true;
}

// Best-effort “quickStart” (set MODE bit14 then it self-clears)
static void maxQuickStartBestEffort(){
  uint16_t mode = 0;
  if (maxRead16(REG_MODE, mode)){
    mode |= (1u << 14);
    (void)maxWrite16(REG_MODE, mode);
  }
}

// Read VCELL/SOC with sanity checks
static bool readGauge(float &volts, float &soc){
  uint16_t vraw=0, sraw=0, ver=0;
  if (!maxRead16(REG_VERSION, ver)) return false;
  if (!maxRead16(REG_VCELL, vraw))  return false;
  if (!maxRead16(REG_SOC, sraw))    return false;

  volts = (float)vraw * 78.125e-6f;
  soc   = (float)((sraw >> 8) & 0xFF) + (float)(sraw & 0xFF) / 256.0f;

  if (!(volts > 2.5f && volts < 5.3f)) return false;
  if (!(soc >= 0.0f && soc <= 100.5f)) return false;
  return true;
}

// “Ensure ready”: try quickstart once when it works
static bool ensureGaugeReady(bool verbose){
  float v=0, s=0;
  if (readGauge(v, s)){
    if (!gaugeOK){
      maxQuickStartBestEffort();
      if (verbose) Serial.println(F("Gauge OK (bitbang)."));
    }
    gaugeOK = true;
    return true;
  }
  gaugeOK = false;
  if (verbose){
    Serial.print(F("Gauge read FAIL. Lines: SDA=")); Serial.print(sdaHigh()?1:0);
    Serial.print(F(" SCL=")); Serial.println(sclHigh()?1:0);
  }
  return false;
}

// ---------- LEDs ----------
void updateChargeLEDs(float v, unsigned long now) {
  if (now - lastToggle >= flashPeriod / 2) {
    blinkState = !blinkState;
    lastToggle = now;
  }

  bool charging = (digitalRead(PIN_CHG) == LOW);

  if (v < V1 - HYST) {
    digitalWrite(LED1, charging ? blinkState : HIGH);
    digitalWrite(LED2, LOW); digitalWrite(LED3, LOW); digitalWrite(LED4, LOW);
  } else if (v < V2 - HYST) {
    digitalWrite(LED1, HIGH);
    digitalWrite(LED2, charging ? blinkState : HIGH);
    digitalWrite(LED3, LOW); digitalWrite(LED4, LOW);
  } else if (v < V3 - HYST) {
    digitalWrite(LED1, HIGH); digitalWrite(LED2, HIGH);
    digitalWrite(LED3, charging ? blinkState : HIGH);
    digitalWrite(LED4, LOW);
  } else {
    digitalWrite(LED1, HIGH); digitalWrite(LED2, HIGH); digitalWrite(LED3, HIGH);
    digitalWrite(LED4, charging ? blinkState : HIGH);
  }
}

void updateDisplayLEDs(float v) {
  digitalWrite(LED1, v >= V1 ? HIGH : LOW);
  digitalWrite(LED2, v >= V2 ? HIGH : LOW);
  digitalWrite(LED3, v >= V3 ? HIGH : LOW);
  digitalWrite(LED4, v >= V4 ? HIGH : LOW);
}

// Take one clean voltage
float readStableVoltage() {
  digitalWrite(LED1, LOW); pinMode(LED1, INPUT);
  digitalWrite(LED2, LOW); pinMode(LED2, INPUT);
  digitalWrite(LED3, LOW); pinMode(LED3, INPUT);
  digitalWrite(LED4, LOW); pinMode(LED4, INPUT);

  delay(80);

  float s=0, r1=0, r2=0, r3=0;
  if (!readGauge(r1, s)) r1 = 0;
  delay(50);
  if (!readGauge(r2, s)) r2 = r1;
  delay(50);
  if (!readGauge(r3, s)) r3 = r2;

  pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT); pinMode(LED4, OUTPUT);
  setAllLEDs(LOW);

  return median3(r1, r2, r3);
}

// --- Sleep ---
void enterDeepStandby() {
  Serial.flush();
  Serial.end();

  setAllLEDs(LOW);
  pinMode(LED1, INPUT);
  pinMode(LED2, INPUT);
  pinMode(LED3, INPUT);
  pinMode(LED4, INPUT);

  sdaRelease();
  sclRelease();

  _PROTECTED_WRITE(WDT.CTRLA, 0);

  // Wake sources
  PORTC.PIN3CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc; // Button wake
  PORTC.PIN0CTRL = PORT_ISC_BOTHEDGES_gc;                    // USB wake
  VPORTC.INTFLAGS = 0xFF;

  // Power-Down sleep
  SLPCTRL.CTRLA = (SLPCTRL_SMODE_PDOWN_gc | SLPCTRL_SEN_bm);
  sei();
  sleep_cpu();
  cli();

  // Reinit after wake
  pinMode(PIN_USBDET, INPUT);
  pinMode(PIN_CHG, INPUT_PULLUP);
  pinMode(PIN_BUTTON, INPUT_PULLUP);

  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  setAllLEDs(LOW);

  PORTC.PIN0CTRL = PORT_ISC_BOTHEDGES_gc;
  PORTC.PIN3CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc;

  sei();
  Serial.begin(9600);
  Serial.println(F("Woke up"));

  gaugeOK = false;
}

// === Interrupt handler ===
ISR(PORTC_PORT_vect) {
  uint8_t flags = VPORTC.INTFLAGS;
  VPORTC.INTFLAGS = 0xFF;
  unsigned long now = millis();

  if (flags & PIN0_bm) usbEvent = true; // USB detect edge

  if (flags & PIN3_bm) { // Button edge
    if (now - lastButtonPress > DEBOUNCE_MS) {
      lastButtonPress = now;

      // HARD RULE: ignore button while actively charging
      if (!(digitalRead(PIN_USBDET) && (digitalRead(PIN_CHG) == LOW))) {
        buttonEvent = true;
      }
    }
  }
}

// === setup ===
void setup() {
  Serial.begin(9600);
  delay(200);
  Serial.println(F("ATtiny1616: MAX17048 bitbang forced-SCL (prototype mode)"));

  pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT); pinMode(LED4, OUTPUT);
  setAllLEDs(LOW);

  pinMode(PIN_CHG, INPUT_PULLUP);
  pinMode(PIN_BUTTON, INPUT_PULLUP);
  pinMode(PIN_USBDET, INPUT);

  sdaRelease();
  sclRelease();

  PORTC.PIN0CTRL = PORT_ISC_BOTHEDGES_gc;
  PORTC.PIN3CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc;
  VPORTC.INTFLAGS = 0xFF;
  sei();

  usbPlugged = digitalRead(PIN_USBDET);
  ensureGaugeReady(true);
}

// === main loop ===
void loop() {
  unsigned long now = millis();

  // USB plug/unplug event
  if (usbEvent) {
    usbEvent = false;
    bool newUSB = digitalRead(PIN_USBDET);
    if (newUSB != usbPlugged) {
      usbPlugged = newUSB;
      if (!usbPlugged) {
        Serial.println(F("USB unplugged -> sleep"));
        delay(50);
        enterDeepStandby();
      } else {
        Serial.println(F("USB plugged"));
        gaugeOK = false;
      }
    }
  }

  // Button press -> show SoC/voltage, then sleep (ONLY when not charging)
  if (buttonEvent) {
    buttonEvent = false;

    // Do nothing at all while charging
    if (!isChargingNow() && (digitalRead(PIN_BUTTON) == LOW)) {
      Serial.println(F("-> Button press"));

      if (ensureGaugeReady(true)) {
        float v = readStableVoltage();
        float soc = 0, tmpv = 0;
        (void)readGauge(tmpv, soc);

        Serial.print(F("Voltage: ")); Serial.print(v, 3); Serial.println(F(" V"));
        Serial.print(F("SoC: "));     Serial.print(soc, 2); Serial.println(F("%"));

        updateDisplayLEDs(v);
      } else {
        Serial.println(F("Gauge unavailable"));
        setAllLEDs(LOW);
      }

      delay(displayDuration);
      setAllLEDs(LOW);
      enterDeepStandby();
    }
  }

  // Charging animation while USB present
  if (usbPlugged) {
    float v=0, s=0;
    if (readGauge(v, s)) {
      gaugeOK = true;
      updateChargeLEDs(v, now);
    } else {
      gaugeOK = false;
      if (now - lastToggle >= 500) { lastToggle = now; blinkState = !blinkState; }
      digitalWrite(LED1, blinkState);
      digitalWrite(LED2, LOW); digitalWrite(LED3, LOW); digitalWrite(LED4, LOW);
    }
  } else {
    setAllLEDs(LOW);
  }

  // Status print
  if (usbPlugged && (now - lastPoll >= pollInterval)) {
    lastPoll = now;
    bool chgLow = (digitalRead(PIN_CHG) == LOW);

    Serial.print(F("Status="));
    Serial.print(chgLow ? F("Charging") : F("Idle"));
    Serial.print(F("  Gauge="));
    Serial.print(gaugeOK ? F("OK") : F("OFF"));
    Serial.print(F("  Lines: SDA=")); Serial.print(sdaHigh()?1:0);
    Serial.print(F(" SCL=")); Serial.println(sclHigh()?1:0);

    if (gaugeOK) {
      float v=0, s=0;
      if (readGauge(v, s)) {
        Serial.print(F("V=")); Serial.print(v, 3);
        Serial.print(F("  SoC=")); Serial.print(s, 2);
        Serial.println(F("%"));
      }
    }
  }

  delay(20);
}

3) KiCad Schematic