/* * ATtiny402 alkaline battery tester * 10ohm to test 150ma approx load and no load, plus the difference to be * displayed. * * PA1 SDA, PA2 SCL, PA3 mosfet control, PA7 ADC. * * Sherman Stebbins * 3/29/2026 version 1.0 * _ * vcc| |gnd * PA6| |PA3 * PA7| |PA0 * PA1| |PA2 * */
#define F_CPU 3333333UL
#include <avr/io.h>#include <util/delay.h>#include <stdio.h>
#define FONT_SMALL 0#define FONT_BIG 1#define FONT_XBIG 2
#define ADC_REF_MV 2500#define VOLTAGE_OFFSET_MV 0 //240 // adjust this (+ raises reading)#define ADC_SAMPLES 16 //#define LOAD_PIN PIN3_bm //load PA3#define ADC_PIN ADC_MUXPOS_AIN7_gc //adc PA7 char *status; //works//const char *status = "INIT";//works//char *version; //no work//version = " V 1 . 0";//const char *version = " V 1.0"; //worked but warningstatic const char version[] = " V 1 . 0"; //best//char version[] = "VERSION 1.0"; //worked
// ================= I2C =================void TWI0_init(void) {
// SDA (PA1), SCL (PA2) → inputs with pull-ups PORTA.DIRCLR = PIN1_bm | PIN2_bm; PORTA.PIN1CTRL = PORT_PULLUPEN_bm; PORTA.PIN2CTRL = PORT_PULLUPEN_bm;
// Set baud (~100kHz at 3.33MHz clock) TWI0.MBAUD = 17;
// Enable TWI TWI0.MCTRLA = TWI_ENABLE_bm;
// Set bus state to idle (important!) TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;}
void TWI0_start(uint8_t addr) { TWI0.MADDR = addr << 1; while (!(TWI0.MSTATUS & TWI_WIF_bm));}
void TWI0_write(uint8_t data) { TWI0.MDATA = data; while (!(TWI0.MSTATUS & TWI_WIF_bm));}
void TWI0_stop(void) { TWI0.MCTRLB = TWI_MCMD_STOP_gc;}
// ================= OLED =================#define OLED_ADDR 0x3C
void OLED_cmd(uint8_t cmd) { TWI0_start(OLED_ADDR); TWI0_write(0x00); TWI0_write(cmd); TWI0_stop();}
void OLED_data(uint8_t data) { TWI0_start(OLED_ADDR); TWI0_write(0x40); TWI0_write(data); TWI0_stop();}
void OLED_init(void) { _delay_ms(100);
OLED_cmd(0xAE); OLED_cmd(0x20); OLED_cmd(0x00); OLED_cmd(0xB0); OLED_cmd(0xC8); OLED_cmd(0x00); OLED_cmd(0x10); OLED_cmd(0x40); OLED_cmd(0x81); OLED_cmd(0x7F); OLED_cmd(0xA1); OLED_cmd(0xA6); OLED_cmd(0xA8); OLED_cmd(0x3F); OLED_cmd(0xA4); OLED_cmd(0xD3); OLED_cmd(0x00); OLED_cmd(0xD5); OLED_cmd(0x80); OLED_cmd(0xD9); OLED_cmd(0xF1); OLED_cmd(0xDA); OLED_cmd(0x12); OLED_cmd(0xDB); OLED_cmd(0x40); OLED_cmd(0x8D); OLED_cmd(0x14); OLED_cmd(0xAF);}
void OLED_clear(void) { for (uint16_t i = 0; i < 1024; i++) { OLED_data(0x00); }}
void OLED_setCursor(uint8_t x, uint8_t page) { OLED_cmd(0xB0 + page); OLED_cmd(0x00 + (x & 0x0F)); OLED_cmd(0x10 + (x >> 4));}
// 5x7 digitsconst uint8_t font5x7[][5] = { // 0–9 {0x3E,0x51,0x49,0x45,0x3E}, // 0 {0x00,0x42,0x7F,0x40,0x00}, // 1 {0x62,0x51,0x49,0x49,0x46}, // 2 {0x22,0x49,0x49,0x49,0x36}, // 3 {0x18,0x14,0x12,0x7F,0x10}, // 4 {0x2F,0x49,0x49,0x49,0x31}, // 5 {0x3E,0x49,0x49,0x49,0x32}, // 6 {0x01,0x71,0x09,0x05,0x03}, // 7 {0x36,0x49,0x49,0x49,0x36}, // 8 {0x26,0x49,0x49,0x49,0x3E}, // 9 // // A–Z (indexes 10–35)
{0x7E,0x11,0x11,0x11,0x7E}, // A {0x7F,0x49,0x49,0x49,0x36}, // B {0x3E,0x41,0x41,0x41,0x22}, // C {0x7F,0x41,0x41,0x22,0x1C}, // D {0x7F,0x49,0x49,0x49,0x41}, // E {0x7F,0x09,0x09,0x09,0x01}, // F {0x3E,0x41,0x49,0x49,0x7A}, // G {0x7F,0x08,0x08,0x08,0x7F}, // H {0x00,0x41,0x7F,0x41,0x00}, // I {0x20,0x40,0x41,0x3F,0x01}, // J {0x7F,0x08,0x14,0x22,0x41}, // K {0x7F,0x40,0x40,0x40,0x40}, // L {0x7F,0x02,0x0C,0x02,0x7F}, // M {0x7F,0x04,0x08,0x10,0x7F}, // N {0x3E,0x41,0x41,0x41,0x3E}, // O {0x7F,0x09,0x09,0x09,0x06}, // P {0x3E,0x41,0x51,0x21,0x5E}, // Q {0x7F,0x09,0x19,0x29,0x46}, // R {0x46,0x49,0x49,0x49,0x31}, // S {0x01,0x01,0x7F,0x01,0x01}, // T {0x3F,0x40,0x40,0x40,0x3F}, // U {0x1F,0x20,0x40,0x20,0x1F}, // V {0x7F,0x20,0x18,0x20,0x7F}, // W {0x63,0x14,0x08,0x14,0x63}, // X {0x03,0x04,0x78,0x04,0x03}, // Y {0x61,0x51,0x49,0x45,0x43}, // Z};
int8_t font_index(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c == 'A') return 10; if (c == 'B') return 11; if (c == 'C') return 12;
if (c == '.') return -1; // handled separately
return -1; // unknown}
void OLED_printString_big(const char *str, uint8_t x, uint8_t page) {
// ---- TOP HALF ---- OLED_setCursor(x, page);
for (const char *p = str; *p; p++) {
if (*p == '.') { OLED_data(0x00); OLED_data(0x00); continue; }
uint8_t d;
if (*p >= '0' && *p <= '9') { d = *p - '0'; } else if (*p >= 'A' && *p <= 'Z') { d = (*p - 'A') + 10; } else if (*p == ' ') { d = 254; // special case for space } else if (*p == '.') { d = 255; } else { continue; }
// 👇 space charactor: if (d == 254) { OLED_data(0x00); OLED_data(0x00); OLED_data(0x00); continue; }
for (uint8_t i = 0; i < 5; i++) { uint8_t col = font5x7[d][i];
uint16_t expanded = 0;
for (uint8_t b = 0; b < 7; b++) { if (col & (1 << b)) { expanded |= (3 << (b * 2)); } }
uint8_t top = expanded & 0xFF;
OLED_data(top); OLED_data(top); }
OLED_data(0x00); }
// ---- BOTTOM HALF ---- OLED_setCursor(x, page + 1);
for (const char *p = str; *p; p++) {
if (*p == '.') { OLED_data(0x00); OLED_data(0x60); continue; }
uint8_t d;
if (*p >= '0' && *p <= '9') { d = *p - '0'; } else if (*p >= 'A' && *p <= 'Z') { d = (*p - 'A') + 10; } else if (*p == ' ') { d = 254; // special case for space } else if (*p == '.') { d = 255; } else { continue; }
// 👇 space charactor: if (d == 254) { OLED_data(0x00); OLED_data(0x00); OLED_data(0x00); continue; }
for (uint8_t i = 0; i < 5; i++) { uint8_t col = font5x7[d][i];
uint16_t expanded = 0;
for (uint8_t b = 0; b < 7; b++) { if (col & (1 << b)) { expanded |= (3 << (b * 2)); } }
uint8_t bottom = (expanded >> 8) & 0xFF;
OLED_data(bottom); OLED_data(bottom); }
OLED_data(0x00); }}
// ================= ADC =================void ADC0_init(void) { VREF.CTRLA = VREF_ADC0REFSEL_2V5_gc; VREF.CTRLB = VREF_ADC0REFEN_bm; _delay_ms(2);
ADC0.CTRLC = ADC_PRESC_DIV4_gc | ADC_REFSEL_INTREF_gc; //ADC0.MUXPOS = ADC_MUXPOS_AIN7_gc; ADC0.MUXPOS = ADC_PIN; ADC0.CTRLA = ADC_ENABLE_bm;}
uint16_t ADC0_read(void) { ADC0.COMMAND = ADC_STCONV_bm; while (!(ADC0.INTFLAGS & ADC_RESRDY_bm)); ADC0.INTFLAGS = ADC_RESRDY_bm; return ADC0.RES;}
// ================= LOAD CONTROL =================#define LOAD_PIN PIN3_bm
void load_init(void) { PORTA.DIRSET = LOAD_PIN; PORTA.OUTCLR = LOAD_PIN;}
void load_on(void) { PORTA.OUTSET = LOAD_PIN;}
void load_off(void) { PORTA.OUTCLR = LOAD_PIN;}
// ================= MAIN =================int main(void) { TWI0_init(); OLED_init(); OLED_clear();
ADC0_init(); load_init(); uint8_t loopcount = 0;
while (1) {
// ----ADC NO LOAD ---- load_off(); _delay_ms(50);
uint32_t sum1 = 0; for (uint8_t i = 0; i < ADC_SAMPLES; i++) { sum1 += ADC0_read(); } uint16_t adc_no = sum1 / ADC_SAMPLES;
// ---- ADC LOAD ---- load_on(); _delay_ms(50);
uint32_t sum2 = 0; for (uint8_t i = 0; i < ADC_SAMPLES; i++) { sum2 += ADC0_read(); } uint16_t adc_ld = sum2 / ADC_SAMPLES;
load_off();
// Convert to mV uint16_t mv_no = ((uint32_t)adc_no * ADC_REF_MV / 1023) + VOLTAGE_OFFSET_MV; uint16_t mv_ld = ((uint32_t)adc_ld * ADC_REF_MV / 1023) + VOLTAGE_OFFSET_MV;
//string: // ---- NO LOAD STRING ---- char buf1[8];
uint16_t mv = mv_no;
buf1[0] = '0' + (mv / 1000); buf1[1] = ' '; buf1[2] = '.'; buf1[3] = ' '; buf1[4] = '0' + ((mv % 1000) / 100) % 10; buf1[5] = '0' + ((mv % 100) / 10); buf1[6] = 0;
// ---- LOAD STRING ---- char buf2[8];
mv = mv_ld;
buf2[0] = '0' + (mv / 1000); buf2[1] = ' '; buf2[2] = '.'; buf2[3] = ' '; buf2[4] = '0' + ((mv % 1000) / 100) % 10; buf2[5] = '0' + ((mv % 100) / 10); buf2[6] = 0;
// ---- DISPLAY ---- if(mv_no>12){ if(loopcount>0){ OLED_clear(); }
// ---- MEASUREMENTS ---- OLED_printString_big("NO LD", 0, 0); OLED_printString_big(buf1, 55, 0); OLED_printString_big("V", 94, 0);
OLED_printString_big("LOAD", 0, 2); OLED_printString_big(buf2, 55, 2); OLED_printString_big("V", 94, 2);
// ---- DIFF ---- uint16_t diff_mv = (mv_no > mv_ld) ? (mv_no - mv_ld) : 0; char buf3_local[8]; uint16_t mvv = diff_mv;
buf3_local[0] = '0' + (mvv / 1000); buf3_local[1] = ' '; buf3_local[2] = '.'; buf3_local[3] = ' '; buf3_local[4] = '0' + ((mvv % 1000) / 100) % 10; buf3_local[5] = '0' + ((mvv % 100) / 10); buf3_local[6] = 0; OLED_printString_big("DIFF", 0, 4); OLED_printString_big(buf3_local, 55, 4);
if (mv_ld >= 1380) { status = "NEW"; } else if (mv_ld >= 1200) { status = "ALMOST NEW"; } else if (mv_ld >= 1100) { status = "OK"; } else if (mv_ld >=850){ status = "BAD"; } else if (mv_ld >=15){ status = "FUCKED"; } else{ status = " YAWN"; }
// ---- STATUS SCREEN ---- OLED_printString_big(status, 0, 6); _delay_ms(1000);
loopcount=0; OLED_clear(); }else{ //OLED_clear(); if(loopcount<4){ loopcount++; //OLED_clear(); status = " NO BATTERY"; OLED_printString_big(status, 0, 0); status = " DETECTED"; OLED_printString_big(status, 0, 2); status = " VERSION 1.0"; OLED_printString_big(version, 30, 6); _delay_ms(1000); }else{ OLED_clear(); } } }}