Full Project Download
1) 3D Model
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

