/* Current/Voltage logger with INA226 2015.08.17 Toshi Nagata */ #include #include #include #include // set_sleep_mode() etc. #include /* ST7032 library by Ore-Kobo */ /* http://ore-kb.net/archives/195 */ /* With the following patch: */ /* http://homepage1.nifty.com/alchemy/etc/20150719-ST7032.patch */ #include #define INA226_ADDRESS 0x40 // INA226 I2C address (A0 = A1 = GND) // INA226 calibration register value // 0.00512 / (current_LSB * R_shunt) // In case of INA226PRC, current_LSB = 0.1 mA, R_shunt = 0.025 ohm #define INA226_CAL_VALUE 2048 // INA226 Registers #define INA226_REG_CONGIGURATION_REG 0x00 // Configuration Register (R/W) #define INA226_REG_SHUNT_VOLTAGE 0x01 // Shunt Voltage (R) #define INA226_REG_BUS_VOLTAGE 0x02 // Bus Voltage (R) #define INA226_REG_POWER 0x03 // Power (R) #define INA226_REG_CURRENT 0x04 // Current (R) #define INA226_REG_CALIBRATION 0x05 // Calibration (R/W) #define INA226_REG_MASK_ENABLE 0x06 // Mask/Enable (R/W) #define INA226_REG_ALERT_LIMIT 0x07 // Alert Limit (R/W) #define INA226_REG_DIE_ID 0xFF // Die ID (R) // Operating Mode (Mode Settings [2:0]) #define INA226_CONF_MODE_POWER_DOWN 0x00 // Power-Down #define INA226_CONF_MODE_TRIG_SHUNT_VOLTAGE 0x01 // Shunt Voltage, Triggered #define INA226_CONF_MODE_TRIG_BUS_VOLTAGE 0x02 // Bus Voltage, Triggered #define INA226_CONF_MODE_TRIG_SHUNT_AND_BUS 0x03 // Shunt and Bus, Triggered #define INA226_CONF_MODE_POWER_DOWN2 0x04 // Power-Down #define INA226_CONF_MODE_CONT_SHUNT_VOLTAGE 0x05 // Shunt Voltage, Continuous #define INA226_CONF_MODE_CONT_BUS_VOLTAGE 0x06 // Bus Voltage, Continuous #define INA226_CONF_MODE_CONT_SHUNT_AND_BUS 0x07 // Shunt and Bus, Continuous (default) // Shunt Voltage Conversion Time (VSH CT Bit Settings [5:3]) #define INA226_CONF_VSH_140uS (0x00 << 3) // 140us #define INA226_CONF_VSH_204uS (0x01 << 3) // 204us #define INA226_CONF_VSH_332uS (0x02 << 3) // 332us #define INA226_CONF_VSH_588uS (0x03 << 3) // 588us #define INA226_CONF_VSH_1100uS (0x04 << 3) // 1.1ms (default) #define INA226_CONF_VSH_2116uS (0x05 << 3) // 2.116ms #define INA226_CONF_VSH_4156uS (0x06 << 3) // 4.156ms #define INA226_CONF_VSH_8244uS (0x07 << 3) // 8.244ms // Bus Voltage Conversion Time (VBUS CT Bit Settings [8:6]) #define INA226_CONF_VBUS_140uS (0x00 << 6) // 140us #define INA226_CONF_VBUS_204uS (0x01 << 6) // 204us #define INA226_CONF_VBUS_332uS (0x02 << 6) // 332us #define INA226_CONF_VBUS_588uS (0x03 << 6) // 588us #define INA226_CONF_VBUS_1100uS (0x04 << 6) // 1.1ms (default) #define INA226_CONF_VBUS_2116uS (0x05 << 6) // 2.116ms #define INA226_CONF_VBUS_4156uS (0x06 << 6) // 4.156ms #define INA226_CONF_VBUS_8244uS (0x07 << 6) // 8.244ms // Averaging Mode (AVG Bit Settings[11:9]) #define INA226_CONF_AVG_1 (0x00 << 9) // 1 (default) #define INA226_CONF_AVG_4 (0x01 << 9) // 4 #define INA226_CONF_AVG_16 (0x02 << 9) // 16 #define INA226_CONF_AVG_64 (0x03 << 9) // 64 #define INA226_CONF_AVG_128 (0x04 << 9) // 128 #define INA226_CONF_AVG_256 (0x05 << 9) // 256 #define INA226_CONF_AVG_512 (0x06 << 9) // 512 #define INA226_CONF_AVG_1024 (0x07 << 9) // 1024 // Reset Bit (RST bit [15]) #define INA226_CONF_RESET_ACTIVE (1 << 15) #define INA226_CONF_RESET_INACTIVE (0 << 15) // Bit mask constants #define MASK2 (1 << 2) #define MASK3 (1 << 3) #define MASK4 (1 << 4) #define MASK5 (1 << 5) #define MASK6 (1 << 6) #define MASK235 (MASK2 | MASK3 | MASK5) // Low battery threshold (analog0 input) #define LOW_BATTERY 2000 #define SPI_CHIP_SELECT 8 #define TIMER1_MAX 31250 // 8 MHz/256/31250 = 1 Hz // ST7032 LCD driver ST7032 lcd; // Log file char logname[10]; File logfile; // Current/voltage/cell voltage long voltage; // Multiple of 1 uV int current; // Multiple of 0.1 mA int cellvoltage; // Multiple of 1 mV // Message buffer char buf[40]; // Status variables enum { MODE_NOP = 0, MODE_READY, MODE_STARTREC, MODE_RECORDING, MODE_STOPREC, MODE_SDERROR, MODE_LOWBATTERY }; uint8_t mode = MODE_NOP; // Count up on timer1 interrupts volatile long timerCount; // timerCount value on starting log long startCount; long elapsedTime; // timerCount - startCount (not volatile) // Log interval uint8_t intervalIndex = 0; static int intervals[] = {1, 2, 5, 10, 20, 30, 60, 120, 300, 600, 1200, 1800}; #define ASIZE(array) (sizeof(array) / sizeof(array[0])) // Pin change interrupt // lastPD/lastPTimerCount/lastPTimer are set in the interrupt handler volatile uint8_t lastPD; // PIND value volatile long lastPTimerCount; // timerCount value volatile int lastPTimer; // TCNT1 value // In the main loop, pin interrupt is handled only if lastPD != savePD and // BOUNCING_TIME is passed from the last interrupt uint8_t savePD; long savePTimerCount; int savePTimer; #define BOUNCING_TIME (int)(20L * TIMER1_MAX / 1000) // 20 msec in TCNT1 unit // Timer1 is also used for long-press of the start button and dismissing "*" // during recording. // OCR1B interrupt is set every time it gets necessary volatile uint8_t timerBInvoked = 0; volatile int timerBCount = 0; unsigned int timerBOffset; // This value is added to OCR1B in the interrupt handler static void writeINARegister(uint8_t reg, int value) { Wire.beginTransmission(INA226_ADDRESS); Wire.write(reg); Wire.write((value >> 8) & 0xFF); Wire.write(value & 0xFF); Wire.endTransmission(); } static int readINARegister(uint8_t reg) { int res = 0x0000; Wire.beginTransmission(INA226_ADDRESS); Wire.write(reg); if (Wire.endTransmission() == 0) { if (Wire.requestFrom(INA226_ADDRESS, 2) >= 2) { res = (int)Wire.read() * 256; res += Wire.read(); } } return res; } static void setupINARegister(void) { writeINARegister(INA226_REG_CONGIGURATION_REG, INA226_CONF_RESET_INACTIVE | INA226_CONF_MODE_CONT_SHUNT_AND_BUS | INA226_CONF_VSH_1100uS | INA226_CONF_VBUS_1100uS | INA226_CONF_AVG_64 ); writeINARegister(INA226_REG_CALIBRATION, INA226_CAL_VALUE); } // Initialize CGRAM on ST7032 // Battery icons PROGMEM prog_uchar cgdata[] = { 0b00110, 0b01001, 0b01001, 0b01001, 0b01001, 0b01001, 0b01111, 0b00110, 0b01001, 0b01001, 0b01001, 0b01001, 0b01111, 0b01111, 0b00110, 0b01001, 0b01001, 0b01001, 0b01111, 0b01111, 0b01111, 0b00110, 0b01001, 0b01001, 0b01111, 0b01111, 0b01111, 0b01111, 0b00110, 0b01001, 0b01111, 0b01111, 0b01111, 0b01111, 0b01111, 0b00110, 0b01111, 0b01111, 0b01111, 0b01111, 0b01111, 0b01111 }; static void initCGRAM(void) { uint8_t charmap[8]; int i, n; charmap[7] = 0; for (n = 0; n < 6; n++) { for (i = 0; i < 7; i++) { charmap[i] = pgm_read_byte(cgdata + n * 7 + i); } lcd.createChar(n, charmap); } } // Find NNNNN.txt filename which is not used yet // (At the top level of the filesystem) static uint8_t makeFileName(void) { int n; for (n = 0; n < 30000; n++) { snprintf(logname, 10, "%05d.txt", n); if (!SD.exists(logname)) break; } if (n >= 30000) { logname[0] = 0; return 0; } return 1; } static void showTitleLine(void) { int n; long nn; lcd.setCursor(0, 0); switch(mode) { case MODE_READY: lcd.print("Ready "); n = intervals[intervalIndex]; if (n < 60) snprintf(buf, sizeof buf, "[%d sec] ", n); else snprintf(buf, sizeof buf, "[%d min] ", n / 60); buf[9] = 0; lcd.print(buf); break; case MODE_STARTREC: lcd.print("Start:"); lcd.print(logname); break; case MODE_RECORDING: nn = elapsedTime; snprintf(buf, sizeof buf, "%5ld %02d:%02d:%02d ", elapsedTime / intervals[intervalIndex] + 1, (int)(nn / 3600), (int)((nn / 60) % 60), (int)(nn % 60)); lcd.print(buf); break; case MODE_STOPREC: lcd.print("Stop:"); lcd.print(logname); lcd.print(" "); break; case MODE_SDERROR: lcd.print("SD Error "); break; case MODE_LOWBATTERY: lcd.print("Low Battery "); break; case MODE_NOP: // Welcome message lcd.print("TN Data Logger "); lcd.setCursor(0, 1); lcd.print("Ver 1.0 Rev 1 "); break; default: lcd.print(" "); break; } if (mode != MODE_NOP) { int n = ((long)(cellvoltage - LOW_BATTERY) * 6 / (2400 - LOW_BATTERY)); if (n < 0) n = 0; else if (n >= 6) n = 5; lcd.setCursor(15, 0); lcd.write(n); } } static void readDataValues(void) { // LSB = 1.25 mV -> 1 uV voltage = (long)((short)readINARegister(INA226_REG_BUS_VOLTAGE)) * 1250L; // LSB = 0.1 mA current = (short)readINARegister(INA226_REG_CURRENT); if (current < 0) current = 0; // The voltage is divided by 3.14 with 22k/47k registers. // The reference is internal 1.1 V. // Hence the full scale (1023) value corresponds to 1.1*3.14=3.45 V cellvoltage = (short)((long)analogRead(0) * 3450 / 1023); } static void showDataLine(void) { lcd.setCursor(0, 1); snprintf(buf, sizeof buf, " %5ldmV", (voltage + (1000 / 2)) / 1000); lcd.print(buf); if (current >= 10000) { snprintf(buf, sizeof buf, " %5dmA", current / 10); } else { snprintf(buf, sizeof buf, " %3d.%1dmA", current / 10, current % 10); } lcd.print(buf); } void setup(void) { // Initialize LCD and INA226 Wire.begin(); setupINARegister(); lcd.begin(16, 2); lcd.setContrast(30); lcd.clear(); initCGRAM(); mode = MODE_NOP; showTitleLine(); // Set analog reference to internal 1.1V analogReference(INTERNAL); // Check cell voltage readDataValues(); if (cellvoltage < LOW_BATTERY) { mode = MODE_LOWBATTERY; } // Initialize SD if (mode != MODE_LOWBATTERY) { if (!SD.begin(SPI_CHIP_SELECT) || !makeFileName()) { mode = MODE_SDERROR; } } delay(1000); // Set PD2-PD5 as input and enable internal pull-up DDRD &= 0b11000011; // PD2 to PD5 as input (0: input, 1: output) PORTD |= 0b00111100; // Enable internal pull-up // Set interrupt cli(); // Disable interrupt // Set pin-change interrupt PCICR |= 0b00000100; // turn on pin-change interrupt on PORTD PCMSK2 |= 0b00101100; // turn on pins PD2, PD3 and PD5 lastPD = 0; lastPTimerCount = 0; lastPTimer = 0; savePD = PIND & MASK235; // Set timer1 // WGM13..WGM10 = 0b0100 (CTC mode with OCR1A) TCCR1A = 0; TCCR1B = (1 << WGM12) | (1 << CS12); // CTC mode, clock source = clk/256 TIMSK1 = 0b00000010; // Enable "Output Compare A Match" interrupt TCNT1 = 0; // Reset timer OCR1A = TIMER1_MAX; // Interrupt every 1 second timerCount = 0; TIMSK0 = 0; set_sleep_mode(SLEEP_MODE_IDLE); sei(); // Enable interrupt if (mode == MODE_NOP) mode = MODE_READY; lcd.clear(); showTitleLine(); } // Interrupt handler (pin change interrupt on PORTD) ISR(PCINT2_vect) { lastPD = PIND & MASK235; lastPTimerCount = timerCount; lastPTimer = TCNT1; } // Interrupt handler (timer1 CTC) ISR(TIMER1_COMPA_vect) { timerCount++; } // Interrupt handler (timer1, used for long-press and "*" erase) ISR(TIMER1_COMPB_vect) { timerBInvoked = 1; timerBCount++; OCR1B = ((unsigned int)OCR1B + timerBOffset) % TIMER1_MAX; } enum { ACTION_UP = 1, ACTION_DOWN, ACTION_STARTREC, ACTION_STOPREC, ACTION_CANCEL_STARTREC, ACTION_CANCEL_STOPREC, ACTION_TIMERB, ACTION_TIMERA }; void loop(void) { static long lastTimerCount = 0; int action = 0; int n, n2; long nn; cli(); // Disable interrupt if (savePD != lastPD && BOUNCING_TIME < (lastPTimerCount - savePTimerCount) * TIMER1_MAX + (lastPTimer - savePTimer)) { // Pin change interrupt if (mode == MODE_READY) { if ((savePD & MASK2) && ((~lastPD) & MASK2)) { action = ACTION_DOWN; } else if ((savePD & MASK3) && ((~lastPD) & MASK3)) { action = ACTION_UP; } else if ((savePD & MASK5) && ((~lastPD) & MASK5)) { action = ACTION_STARTREC; } } else if (mode == MODE_RECORDING) { if ((savePD & MASK5) && ((~lastPD) & MASK5)) { action = ACTION_STOPREC; } } else if (mode == MODE_STARTREC) { if (((~savePD) & MASK5) && (lastPD & MASK5)) { action = ACTION_CANCEL_STARTREC; TIMSK1 &= 0b11111011; // Disable OCR1B interrupt } } else if (mode == MODE_STOPREC) { if (((~savePD) & MASK5) && (lastPD & MASK5)) { action = ACTION_CANCEL_STOPREC; TIMSK1 &= 0b11111011; // Disable OCR1B interrupt } } savePD = lastPD; savePTimerCount = lastPTimerCount; savePTimer = lastPTimer; } else if (timerBInvoked) { // Timer1 action (auxiliary interval) action = ACTION_TIMERB; timerBInvoked = 0; } else if (lastTimerCount != timerCount) { // Timer1 action (1-second interval) action = ACTION_TIMERA; lastTimerCount = timerCount; } sei(); // Re-enable interrupt switch (action) { case ACTION_UP: // Up button is pressed intervalIndex = (intervalIndex + 1) % ASIZE(intervals); showTitleLine(); break; case ACTION_DOWN: // Down button is pressed intervalIndex = (intervalIndex + (ASIZE(intervals) - 1)) % ASIZE(intervals); showTitleLine(); break; case ACTION_STARTREC: // Start button is pressed in MODE_READY if (makeFileName() == 0) { mode = MODE_SDERROR; break; } // Go to next case case ACTION_STOPREC: mode = (action == ACTION_STARTREC ? MODE_STARTREC : MODE_STOPREC); showTitleLine(); // Start OCR1B timer cli(); timerBOffset = TIMER1_MAX / 16; OCR1B = ((unsigned int)TCNT1 + timerBOffset) % TIMER1_MAX; TIMSK1 |= 0b00000100; // Enable OCR1B interrupt TIFR1 |= 0b00000100; // Clear OCF1B flag timerBInvoked = 0; timerBCount = 0; sei(); break; case ACTION_CANCEL_STARTREC: mode = MODE_READY; showTitleLine(); showDataLine(); break; case ACTION_CANCEL_STOPREC: mode = MODE_RECORDING; showTitleLine(); showDataLine(); break; case ACTION_TIMERB: if (mode == MODE_STARTREC || mode == MODE_STOPREC) { // Show progress bar for (n = 0; n < timerBCount; n++) { buf[n] = '.'; } for ( ; n < 16; n++) { buf[n] = ' '; } buf[16] = 0; lcd.setCursor(0, 1); lcd.print(buf); if (timerBCount >= 16) { // Move to next mode TIMSK1 &= 0b11111011; // Disable OCR1B interrupt lcd.clear(); if (mode == MODE_STARTREC) { // Start recording logfile = SD.open(logname, FILE_WRITE); if (!logfile) { mode = MODE_SDERROR; showTitleLine(); } else { logfile.print(" t_sec v_mV i_mA\n"); logfile.close(); mode = MODE_RECORDING; } } else { // Stop recording mode = MODE_READY; showTitleLine(); } // Reset timer1 cli(); TCNT1 = TIMER1_MAX - 1; lastTimerCount = timerCount; if (mode == MODE_RECORDING) startCount = timerCount + 1; // Recording will start at next interrupt sei(); } } else if (mode == MODE_RECORDING) { // Dismiss "*" TIMSK1 &= 0b11111011; // Disable OCR1B interrupt lcd.setCursor(0, 1); lcd.print(" "); } break; case ACTION_TIMERA: // Read values readDataValues(); // Check cell voltage if (cellvoltage < LOW_BATTERY) { mode = MODE_LOWBATTERY; } cli(); elapsedTime = timerCount - startCount; sei(); showTitleLine(); if (mode == MODE_LOWBATTERY) { // We do not do anything any more // Disable interrupt and go to sleep set_sleep_mode(SLEEP_MODE_PWR_DOWN); cli(); ADCSRA &= 0b01111111; // Disable ADC ACSR |= 0b10000000; // Disable Analog Comparator sleep_enable(); sleep_cpu(); // Never wake up return; } // Show voltage/current values to LCD // (all modes except STARTREC and STOPREC) if (mode != MODE_STARTREC && mode != MODE_STOPREC) { showDataLine(); } // Write to log file if (mode == MODE_RECORDING || mode == MODE_STOPREC) { n = intervals[intervalIndex]; if (elapsedTime % n == 0) { logfile = SD.open(logname, FILE_WRITE); if (logfile) { logfile.seek(logfile.size()); snprintf(buf, sizeof buf, "%6ld %5ld %4d.%d\n", elapsedTime, (voltage + (1000 / 2)) / 1000, current / 10, current % 10); logfile.print(buf); logfile.close(); } else { mode = MODE_SDERROR; } if (mode == MODE_RECORDING) { lcd.setCursor(0, 1); lcd.print("*"); // Start OCR1B timer (to dismiss "*") after 0.4 s timerBOffset = (unsigned int)((long)TIMER1_MAX * 4 / 10); cli(); OCR1B = ((unsigned int)TCNT1 + timerBOffset) % TIMER1_MAX; TIMSK1 |= 0b00000100; // Enable OCR1B interrupt TIFR1 |= 0b00000100; // Clear OCF1B flag timerBInvoked = 0; sei(); } } } break; default: // No action: go to sleep until some interrupt fires sleep_enable(); sleep_cpu(); sleep_disable(); // Wake up break; } }