powermeter/main.c

551 lines
14 KiB
C

/***************************************************************************
* Copyright (C) 11/2012 by Olaf Rempel *
* razzor@kopf-tisch.de *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; version 2 of the License, *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>
#include <util/crc16.h>
#define F_CPU 8000000
#include <util/delay.h>
#define OSCCAL_VALUE 0xA4
/*
* attiny26
* lfuse: 0xe4 (internal 8MHz)
* hfuse: 0x14 (BOD enabled)
*
* PA0 => current sense (0-2.56V => 0-25.6A)
* PA1 => volatage sense (0-2.56V => 0-30.72V)
* PA2 => mode button (low active)
* PA3 => ext. 2.56V reference
* PA4-7 => LCD D4-7
* PB0 => MOSI (ISP)
* PB1 => MISO (ISP)
* PB2 => SCK (ISP)
* PB3 => free
* PB4 => LCD RS
* PB5 => LCD RW
* PB6 => LCD EN
* PB7 => /RST (ISP)
*/
#define LCD_DATA_MASK 0xF0
#define LCD_RS PORTB4
#define LCD_RW PORTB5
#define LCD_EN PORTB6
#define BUTTON PORTA2
#define ADC_CURRENT 0
#define ADC_VOLTAGE 1
#define ADC_COMPLETE 0xFE
#define ADC_IDLE 0xFF
/* 8ms @8MHz / 256 */
#define TIMER_TICK_RELOAD (256 - 250)
/* 25 * 8ms => 200ms */
#define TIMER_LCD_UPDATE 25
/* 125 * 8ms => 1s */
#define TIMER_SECOND 125
/* autosave every minute */
#define TIMER_NVRAM_SAVE 60
/* bargraph 20 - 28V */
#define VOLTAGE_BAR_MIN 2000
#define VOLTAGE_BAR_MAX 2800
/* bargraph 0 - 20A */
#define CURRENT_BAR_MIN 0
#define CURRENT_BAR_MAX 2000
/* minimum discharge current 100mA */
#define CURRENT_IDLE 10
#define BUTTON_TIMER_IDLE 0xFF
#define EVENT_NONE 0
#define EVENT_BUTTON_PRESSED 1
#define EVENT_BUTTON_RELEASED 2
#define EVENT_BUTTON_TIMEOUT 3
#define STATE_IDLE 0
#define STATE_PRESSED 1
#define STATE_WARNING 2
struct _nvdata {
uint8_t nvram_size; /* first */
uint16_t discharge_time;
uint32_t discharge_product;
uint16_t nvram_crc; /* last */
};
static uint8_t nvram_write_pos;
static struct _nvdata nvram_data;
static struct _nvdata nvram_eeprom EEMEM;
static struct _nvdata nvram_defaults PROGMEM = { 0 };
/* create crc and store nvram data to eeprom */
static void nvram_start_write(void)
{
uint8_t i;
uint16_t crc = 0x0000;
uint8_t *tmp = (uint8_t *)&nvram_data;
/* write in progress? */
if (EECR & (1<<EEWE))
return;
nvram_data.nvram_size = sizeof(struct _nvdata);
for (i = 0; i < sizeof(struct _nvdata) -2; i++) {
crc = _crc_ccitt_update(crc, *tmp++);
}
nvram_data.nvram_crc = crc;
nvram_write_pos = 0;
EEAR = nvram_write_pos;
EEDR = ((uint8_t *)&nvram_data)[nvram_write_pos++];
cli();
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
EECR |= (1<<EERIE);
sei();
}
/* store nvram data to eeprom */
ISR(EE_RDY_vect) {
if (nvram_write_pos < sizeof(struct _nvdata)) {
EEAR = nvram_write_pos;
EEDR = ((uint8_t *)&nvram_data)[nvram_write_pos++];
EECR |= (1<<EEMWE);
EECR |= (1<<EEWE);
EECR |= (1<<EERIE);
} else {
EECR &= ~(1<<EERIE);
}
}
/* read nvram from eeprom and check crc */
static void nvram_read(void)
{
uint8_t i;
uint16_t crc = 0x0000;
uint8_t *tmp = (uint8_t *)&nvram_data;
eeprom_read_block(&nvram_data, &nvram_eeprom, sizeof(struct _nvdata));
for (i = 0; i < sizeof(struct _nvdata); i++) {
crc = _crc_ccitt_update(crc, *tmp++);
}
/* if nvram content is invalid, overwrite with defaults */
if ((nvram_data.nvram_size != sizeof(struct _nvdata)) || (crc != 0x0000)) {
memcpy_P(&nvram_data, &nvram_defaults, sizeof(struct _nvdata));
nvram_start_write();
}
}
static PROGMEM uint8_t bargraph[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00,
0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00,
0x00, 0x00, 0x1c, 0x1c, 0x1c, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x1e, 0x1e, 0x1e, 0x1e, 0x00, 0x00,
0x00, 0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x00,
};
static uint8_t lcd_port_write(uint8_t data)
{
uint8_t retval;
/* keep button pullup */
PORTA = (data & LCD_DATA_MASK) | (1<<BUTTON);
asm volatile("NOP");
asm volatile("NOP");
PORTB |= (1<<LCD_EN);
asm volatile("NOP");
asm volatile("NOP");
retval = (PINA & LCD_DATA_MASK);
PORTB &= ~(1<<LCD_EN);
return retval;
}
static uint8_t lcd_read(uint8_t reg)
{
uint8_t retval;
if (reg) {
PORTB |= (1<<LCD_RS);
} else {
PORTB &= ~(1<<LCD_RS);
}
PORTB |= (1<<LCD_RW);
DDRA &= ~(LCD_DATA_MASK);
retval = lcd_port_write(0x00);
asm volatile("NOP");
asm volatile("NOP");
retval |= (lcd_port_write(0x00) >> 4);
return retval;
}
static void lcd_write_no_busy_check(uint8_t reg, uint8_t data)
{
if (reg) {
PORTB |= (1<<LCD_RS);
} else {
PORTB &= ~(1<<LCD_RS);
}
PORTB &= ~(1<<LCD_RW);
DDRA |= LCD_DATA_MASK;
lcd_port_write((data & 0xF0));
asm volatile("NOP");
asm volatile("NOP");
lcd_port_write(((data & 0x0F) << 4));
}
static void lcd_write(uint8_t reg, uint8_t data)
{
lcd_write_no_busy_check(reg, data);
while ((lcd_read(0x00) & 0x80));
}
static void lcd_print_dec2(uint8_t value)
{
lcd_write(0x01, '0' + (value / 10));
lcd_write(0x01, '0' + (value % 10));
}
static void lcd_print_dec2p2(uint16_t value)
{
lcd_print_dec2(value / 100);
lcd_write(0x01, '.');
lcd_print_dec2(value % 100);
}
static void lcd_print_dec2p3(uint16_t value)
{
lcd_print_dec2(value / 1000);
lcd_write(0x01, '.');
value = value % 1000;
lcd_write(0x01, '0' + (value / 100));
lcd_print_dec2(value % 100);
}
static void lcd_print_time(uint16_t time)
{
lcd_print_dec2(time / 3600);
lcd_write(0x01, ':');
time = time % 3600;
lcd_print_dec2(time / 60);
lcd_write(0x01, ':');
lcd_print_dec2(time % 60);
}
static void lcd_bargraph(uint8_t len, uint16_t min, uint16_t max, uint16_t value)
{
if (value < min)
value = min;
else if (value > max)
value = max;
/* scale to bargraph length */
value = ((value - min) * (len * 5)) / (max - min) ;
while (len--) {
if (value >= 5) {
lcd_write(0x01, 0x05);
value -= 5;
} else {
lcd_write(0x01, value);
value = 0;
}
}
}
static void lcd_write_stringP(const char *ptr)
{
while (1) {
uint8_t data = pgm_read_byte_near(ptr++);
if (data == 0x00)
break;
lcd_write(0x01, data);
}
}
static volatile uint8_t adc_state;
static volatile uint16_t adc_value[2];
static void adc_start(uint8_t channel)
{
adc_state = channel;
/* ext. 2.56V ref, start, irq enable, F_CPU/64 -> ~225us conversion time */
ADMUX = (1<<REFS0) | channel;
ADCSR = (1<<ADEN) | (1<<ADSC) | (1<<ADIE) | (1<<ADIF) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
ISR(ADC_vect)
{
uint8_t channel = adc_state;
adc_value[channel] = ADCW;
/* get next channel */
if (channel == ADC_VOLTAGE) {
adc_start(ADC_CURRENT);
} else if (channel == ADC_CURRENT) {
adc_state = ADC_COMPLETE;
}
}
static volatile uint8_t timer_tick;
ISR(TIMER0_OVF0_vect)
{
TCNT0 = TIMER_TICK_RELOAD;
/* start sampling both channels */
adc_start(ADC_VOLTAGE);
timer_tick = 1;
}
int main(void) __attribute__ ((noreturn));
int main(void)
{
/* calibrate to 8Mhz */
OSCCAL = OSCCAL_VALUE;
DDRA = LCD_DATA_MASK;
DDRB = (1<<LCD_RS) | (1<<LCD_RW) | (1<<LCD_EN) | (1<<PORTB3);
/* pullup for mode button */
PORTA = (1<<BUTTON);
/* F_CPU/256, overflow irq */
TCCR0 = (1<<CS02);
TIMSK = (1<<TOV0);
/* execute reset (according to datasheet) */
_delay_ms(50);
lcd_write_no_busy_check(0x00, 0x30);
_delay_ms(5);
lcd_write_no_busy_check(0x00, 0x30);
_delay_ms(1);
lcd_write_no_busy_check(0x00, 0x30);
/* switch to 4bit */
lcd_write(0x00, 0x28); /* 4bit data bus */
/* again, now with valid lower nibble */
lcd_write(0x00, 0x28); /* 4bit data bus, 2 lines */
lcd_write(0x00, 0x0C); /* display on, no cursor, no blinking */
lcd_write(0x00, 0x06); /* increase address, no shift */
lcd_write(0x00, 0x01); /* clear display */
{
uint8_t i;
lcd_write(0x00, 0x40); /* write CGRAM address 0x00 */
for (i = 0; i < sizeof(bargraph); i++) {
lcd_write(0x01, pgm_read_byte_near(&bargraph[i]));
}
}
sei();
nvram_read();
uint8_t sec_timer = 0;
uint8_t lcd_timer = 0;
uint8_t button_timer = BUTTON_TIMER_IDLE;
uint8_t lcd_page = 0x02;
uint8_t button_prev = 0;
uint8_t button_state = 0;
uint8_t nvram_save = 0;
uint16_t voltage = 0;
uint16_t current = 0;
uint8_t discharging = 0;
uint16_t discharge_time = nvram_data.discharge_time;
uint32_t discharge_product = nvram_data.discharge_product;
while (1) {
if (timer_tick) {
timer_tick = 0;
lcd_timer++;
if (discharging) {
sec_timer++;
if (sec_timer == TIMER_SECOND) {
sec_timer = 0;
discharge_time++;
/* autosave during discharge */
if ((discharge_time % TIMER_NVRAM_SAVE) == 0) {
nvram_save = 1;
}
}
}
uint8_t button_event = EVENT_NONE;
uint8_t button = PINA & (1<<BUTTON);
if (!button && button_prev) {
button_event = EVENT_BUTTON_PRESSED;
} else if (button && !button_prev) {
button_event = EVENT_BUTTON_RELEASED;
} else {
if (button_timer == 0) {
button_event = EVENT_BUTTON_TIMEOUT;
button_timer = BUTTON_TIMER_IDLE;
} else if (button_timer != BUTTON_TIMER_IDLE) {
button_timer--;
}
}
button_prev = button;
if ((button_state == STATE_IDLE) && (button_event == EVENT_BUTTON_PRESSED)) {
lcd_write(0x00, 0x01); /* clear display */
lcd_timer = TIMER_LCD_UPDATE;
lcd_page ^= 0x01;
button_state = STATE_PRESSED;
button_timer = 250; /* 2s timeout */
} else if ((button_event == EVENT_BUTTON_TIMEOUT) && (button_state == STATE_PRESSED)) {
lcd_write(0x00, 0x01); /* clear display */
lcd_write(0x00, 0x80 | 1);
lcd_write_stringP(PSTR("reset counter?"));
lcd_page = 0;
button_state = STATE_WARNING;
button_timer = 250; /* another 2s timeout */
} else if (((button_event == EVENT_BUTTON_TIMEOUT) && (button_state == STATE_WARNING)) ||
(button_event == EVENT_BUTTON_RELEASED)) {
if (button_event == EVENT_BUTTON_TIMEOUT) {
discharge_time = 0;
discharge_product = 0;
nvram_save = 1;
}
if (button_state == STATE_WARNING) {
lcd_write(0x00, 0x01); /* clear display */
lcd_timer = TIMER_LCD_UPDATE;
lcd_page = 2;
}
button_state = STATE_IDLE;
button_timer = BUTTON_TIMER_IDLE;
}
}
if (adc_state == ADC_COMPLETE) {
adc_state = ADC_IDLE;
/* voltage in 10mV increments */
voltage = adc_value[ADC_VOLTAGE] * 3;
/* current in 10mA increments */
current = (adc_value[ADC_CURRENT] * 5) >> 1;
if (discharging) {
discharge_product += current;
}
if (current >= CURRENT_IDLE) {
discharging = 1;
} else if (sec_timer == 0) {
discharging = 0;
}
}
if (lcd_timer == TIMER_LCD_UPDATE) {
lcd_timer = 0;
if (lcd_page) {
lcd_write(0x00, 0x80 | 0);
lcd_print_dec2p2(voltage);
lcd_write(0x01, 'V');
lcd_write(0x00, 0x80 | 40);
lcd_print_dec2p2(current);
lcd_write(0x01, 'A');
if (lcd_page == 2) {
lcd_write(0x00, 0x80 | 8);
lcd_print_time(discharge_time);
lcd_write(0x00, 0x80 | 48);
/* 125 ticks per second, 3600s per hour, 10mA Steps */
lcd_print_dec2p3(discharge_product / (TIMER_SECOND * 3600ULL / 10ULL));
lcd_write(0x01, 'A');
lcd_write(0x01, 'h');
} else if (lcd_page == 3) {
lcd_write(0x00, 0x80 | 7);
lcd_bargraph(9, VOLTAGE_BAR_MIN, VOLTAGE_BAR_MAX, voltage);
lcd_write(0x00, 0x80 | 47);
lcd_bargraph(9, CURRENT_BAR_MIN, CURRENT_BAR_MAX, current);
}
}
}
if (nvram_save) {
nvram_save = 0;
nvram_data.discharge_time = discharge_time;
nvram_data.discharge_product = discharge_product;
nvram_start_write();
}
}
}