dc22-wp-badge-firmware/fw_dc22_attiny88/src/main.c

575 lines
14 KiB
C

/*
* Whiskey Pirates Badge "PEGLEG CPU"
* Sub MCU Firmware for ATtiny88
*
* Created: 5/21/2014 9:24:48 PM
* Author: true
* ts:4
*
********
* PEGLEG CPU is responsible for:
* - aux gpio (todo: list specifically which GPIOs on the badge)
* - eye RGBLED matrix
* - temperature sensor
* - light sensor (using eye RGBLEDs; in practice uses the left eye red LED)
* - user config eeprom
*
********
* Use the following fuses for this project. Needs to run at 8MHz for 400khz I2C operation.
* 8MHz, self-program enabled (bootloader support), preserve EEPROM, BOD disabled, ISP enabled
* -U lfuse:w:0xEE:m -U hfuse:w:0xD7:m -U efuse:w:0x00:m
* (efuse will likely show as 0x06 after setting; this is fine, don't worry about it)
*
* Resources used: Mainline ISR 5% (3/63 timer counts simulated, as of some old rev)
* Flash 26.8%, SRAM 10.4% (as of the above rev)
*
**********
* Register Variables:
* main.c:comm_cmd:r15
* adc.h:adc_read_step:r14
* led.c:rgbled_idx:r13
*/
#include "config.h"
/* system */
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include "i2c.h"
#include "timer.h"
#include "adc.h"
#include "led.h"
/* modes (commands) */
#define MODE_NONE 0b0000
#define MODE_EXT_CMD 0b0001 // bit 4 unset = extra parameters needed, up to 6 commands
#define MODE_LED_SET_LEVEL 0b0010 // (0x00 reserved, 0x1? extra, leaving 0x2? through 0x7?)
#define MODE_TEMPSENSOR_CAL 0b0011
#define MODE_EEPROM_READ 0b0100
#define MODE_EEPROM_WRITE 0b0101
#define MODE_TEMPSENSOR_READ 0b1000 // bit 4 set = immediate, no extra parameters, process now
#define MODE_LIGHTSENSOR_READ 0b1001
#define MODE_SLEEP 0b1111
#define MODE_AUX_PIN_GET 0x10 // extended (send 0x10, cmd, data...)
#define MODE_AUX_PIN_SET 0x11 // ext commands MUST be 0x10 or higher! 0x00-0x0f are RESERVED!
#define MODE_LIGHTSENSOR_SENS 0x19
/* aux pin mode */
#define AUX_PIN_COUNT 10
#define AUX_PIN_OUTPUT 0x01 // sets to output if set, input if cleared
#define AUX_PIN_PORTSET 0x02 // output: sets high/low; input: sets pullup on/off
/* adc modes */
#define ADC_MODE_TEMPSENSOR 8
#define ADC_MODE_LIGHTSENSOR 1
/* prototypes */
static void system_init();
static void system_io_init();
static void tempsensor_cal();
/* globals */
static uint8_t tim0_milli;
static uint8_t tim0_centi;
static uint8_t tim0_profiler;
#define COMM_DATA_SIZE 4 // must be power of 2; default value is 4
register uint8_t comm_cmd asm("r15"); // currently processed SPI command
uint8_t comm_data_idx; // command data write index
uint8_t comm_data[COMM_DATA_SIZE]; // command data register
uint8_t comm_timeout; // command invalidate timeout
/* core */
static uint8_t pirate_sleep_mode = SLEEP_MODE_IDLE;
/* tempsensor */
static int8_t temperature;
static int16_t temp_offset;
/* eeprom */
#define EEPROM_ADDR_TEMPCAL 62 // 2 bytes
/* adc */
volatile uint8_t adc_busy;
volatile uint16_t adc_result[ADC_MAX_FEEDBACK + 1];
/* led */
uint8_t rgbled_pwm_lf[4]; // pwm value for TIMER1 OCA LED outputs
uint8_t rgbled_pwm_rt[4]; // pwm value for TIMER1 OCB LED outputs
uint8_t rgbled_light_level[4];
/* it begins */
int main(void)
{
// configure device and IO
system_init();
system_io_init();
// configure RGBLEDs
rgbled_io_init();
// configure i2c communications
i2c_slave_init(I2C_SLAVE_ADDRESS, I2C_ENABLE_ADDR_ZERO);
// configure system timer
timer0_init();
// configure led pwm timer
timer1_init();
// initialize adc
adc_init();
// try calibrating temperature sensor if needed
tempsensor_cal();
// and now we wait.
while (1) {
// set sleep mode, then set for idle sleep (CPU off, all peripherals on)
set_sleep_mode(pirate_sleep_mode);
pirate_sleep_mode = SLEEP_MODE_IDLE;
// re-enable interrupts and nap.
sei();
sleep_mode();
}
}
/* func */
static void system_init()
{
// make SURE we are running at 8MHz
// do this by first enabling clock divider setup mode,
CLKPR = _BV(CLKPCE);
// and disabling any divider
CLKPR = 0;
}
static void system_io_init()
{
// enable break-before-make outputs on all IO
PORTCR |= (_BV(BBMA) | _BV(BBMB) | _BV(BBMC) | _BV(BBMD));
// set ALL pins as inputs except PB0
DDRA = 0x00;
DDRB = 0x01;
DDRC = 0x00;
DDRD = 0x00;
// and enable pullups (this reduces power consumption)
// in the case of PB0, set it LOW
PORTA = 0xff;
PORTB = 0xfe;
PORTC = 0xff;
PORTD = 0xff;
}
/* temperature */
uint8_t tempsensor_process()
{
int8_t temp_gain;
int8_t temp_adj;
temp_adj = adc_result[ADC_CHAN_TEMP] - temp_offset; // our offset adjusted temperature
temp_gain = temp_adj >> 3; // our gain adjust amount
while (temp_gain > 0) {temp_gain--; temp_adj--;}; // correct positive gain
while (temp_gain < 0) {temp_gain++; temp_adj++;}; // correct negative gain
// save temperature
return temp_adj;
}
void tempsensor_read()
{
if (adc_read_step == 0) {
// select temperature channel
adc_channel(ADC_CHAN_TEMP, ADC_REF_BANDGAP);
} else if (adc_read_step == 3) {
// we've waited 1.5ms so the bandgap voltage should have been set.
// start conversion after bandgap ref change timeout per datasheet
adc_start(4, 1);
} else if (adc_read_step > 3) {
adc_read_step--;
if (!adc_busy) {
// revert reference to AVCC...per the datasheet only changes to
// bandgap should take ~1ms but changes back to AVCC seem to
// take a while to be accurate as well
adc_channel(ADC_CHAN_GND, ADC_REF_AVCC);
adc_read_mode = 0;
temperature = tempsensor_process();
}
}
}
void tempsensor_cal()
{
// NOTE: CALIBRATION IS SINGLE-POINT TO 0 DEGREES CENTIGRADE! ICE YOUR PEGLEG, MOTHERFUCKER!
// todo: figure out wtf I meant by this note in 2014 (this is me in 2023)
// looking at the code, it looks like a pin needs manually changed to calibrate.
// todo: add a zeroing offset calibration command to the command protocol?
// is pin PA2 low? (pin 3 on attiny88 - is pulled up - short to pin 5 / ground somewhere)
if ((PINA & _BV(PINA2)) == 0) {
// turn on the ADC, wait for it to warm up
adc_init();
_delay_ms(100); // todo: is this right? this seems slow
// is the pin still low? not a glitch?
if ((PINA & _BV(PINA2)) == 0) {
// enable interrupts
sei();
// start reading the temp sensor; stall until adc is done
adc_read_mode = ADC_MODE_TEMPSENSOR;
tempsensor_read();
while (adc_busy);
// here's our correction factor!
temp_offset = adc_result[ADC_CHAN_TEMP];
// write it to eeprom
eeprom_write_word((uint16_t *)EEPROM_ADDR_TEMPCAL, temp_offset);
// and that's it - we're done.
adc_read_mode = 0;
return;
}
}
// not saving a value - read the value from eeprom
temp_offset = eeprom_read_word((uint16_t *)EEPROM_ADDR_TEMPCAL);
// does it seem invalid?
if (temp_offset > 560) {
// yeah, this isn't right. load rough value for 0degC from datasheet.
// but you really should calibrate your chip, you lazy fuck.
temp_offset = 273;
}
}
/* comms */
static inline uint8_t pegleg_data_tx()
{
uint8_t ret;
switch (comm_cmd) {
case MODE_EEPROM_READ: {
if (comm_data[0] < 64) {
i2c_slave_tx(eeprom_read_byte((uint8_t *)(uint16_t)comm_data[0]), 1);
ret = 1;
} else {
// invalid address...
i2c_disable_slave();
ret = 255;
}
comm_cmd = 0;
comm_timeout = 0;
return ret;
}
case MODE_TEMPSENSOR_READ: {
i2c_slave_tx(temperature, 1);
comm_cmd = 0;
comm_timeout = 0;
return 1;
}
case MODE_LIGHTSENSOR_READ: {
i2c_slave_tx(adc_result[comm_data[0] & 0x03] >> 2, 1);
comm_cmd = 0;
comm_timeout = 0;
return 1;
}
default: {
// nothing active; invalid :(
i2c_disable_slave();
return 255;
}
}
return 0;
}
static uint8_t pegleg_cmd()
{
switch (comm_cmd) {
// no command: we just ACK this as we shouldn't ever get here
case MODE_NONE: {
comm_cmd = 0;
comm_timeout = 0;
break;
}
// standard commands
case MODE_EXT_CMD: { // used for more commands
if (comm_data_idx >= 1) {
comm_cmd = comm_data[1];
// set data index to last (will be reset on next data sent)
// and also reset timeout since this is effectively a new command
comm_data_idx = COMM_DATA_SIZE - 1;
comm_timeout = 0;
}
break;
}
case MODE_LED_SET_LEVEL: { // sets PWM rate for the 8 PWM LEDs
if (comm_data[0] < 4) {
rgbled_pwm_lf[comm_data[0]] = comm_data[1];
} else if (comm_data[0] < 8) {
rgbled_pwm_rt[comm_data[0] - 4] = comm_data[1];
}
comm_cmd = 0;
comm_timeout = 0;
break;
}
case MODE_TEMPSENSOR_CAL: {
// to use this function, make sure the temperature has been previously read.
// calculates the appropriate offset based on the reported temperature.
if (comm_data_idx >= 1) {
// update the offset
if (adc_result[ADC_CHAN_TEMP]) {
temp_offset = adc_result[ADC_CHAN_TEMP] - (int8_t)comm_data[1];
// write it to eeprom
eeprom_write_word((uint16_t *)EEPROM_ADDR_TEMPCAL, temp_offset);
// and that's it - we're done.
comm_cmd = 0;
comm_timeout = 0;
} else {
// no valid read performed; I fucking told you to do this!
comm_cmd = 0;
comm_timeout = 0;
return 1;
}
}
break;
}
case MODE_EEPROM_READ: {
// our data packet is already set; ready for read.
// but if the host keeps writing...
if (comm_data_idx > 1) {
comm_cmd = 0;
comm_timeout = 0;
}
break;
}
case MODE_EEPROM_WRITE: {
// TODO: implement
comm_cmd = 0;
comm_timeout = 0;
break;
}
// immediate commands
case MODE_TEMPSENSOR_READ: {
// attempt another read
if (!adc_read_mode) {
adc_read_mode = 0;
adc_read_step = 0;
adc_read_mode = ADC_MODE_TEMPSENSOR;
}
// now we are ready to read.
// data will probably be prior data unless the host waits. (need to verify)
comm_timeout = 0;
break;
}
case MODE_LIGHTSENSOR_READ: {
if (!adc_read_mode) {
// set the led and step to first step
adc_read_mode = 0;
adc_read_step = 0;
rgbled_sensor_read_idx(comm_data[0]);
adc_read_mode = ADC_MODE_LIGHTSENSOR;
}
// now we are ready to read.
// data will probably be prior data unless the host waits.
comm_timeout = 0;
break;
}
case MODE_SLEEP: {
// put this bitch to bed.
// we'll wake up when we get another I2C command.
pirate_sleep_mode = SLEEP_MODE_PWR_DOWN;
comm_cmd = 0;
comm_timeout = 0;
break;
}
// extended commands
case MODE_AUX_PIN_SET: {
// TODO: come up with an implementation that isn't shitty
}
case MODE_AUX_PIN_GET: {
// TODO: implement when response sending is ...implemented
comm_cmd = 0;
comm_timeout = 0;
break;
}
case MODE_LIGHTSENSOR_SENS: { // 0x10 0x19 <led 0-3> <sens 1-200>
if (comm_data_idx >= 1) {
if ((comm_data[0] <= 0x03) && (comm_data[1])) {
rgbled_sensor_sensitivity(comm_data[0], comm_data[1]);
}
comm_cmd = 0;
comm_timeout = 0;
}
break;
}
// invalid commands
default: {
// send NAK
comm_cmd = 0;
comm_timeout = 0;
return 1;
}
}
return 0;
}
static uint8_t pegleg_data_rx(uint8_t data)
{
if (comm_cmd == MODE_NONE) {
// we aren't processing any commands, so this must be a new command/request.
// first 4 bits = command
comm_cmd = data >> 4;
// last 4 bits = optional data
comm_data[0] = data & 0x0f;
// clear our data watchdog and data index
comm_data_idx = 0;
comm_timeout = 0;
// if this is an immediate command, process it
if (data & 0x80) {
return pegleg_cmd();
}
} else {
// command in progress - add data and continue
comm_data_idx++;
comm_data_idx &= (COMM_DATA_SIZE - 1); // data will LOOP in case of problems; TODO: NAK on failure
comm_data[comm_data_idx] = data;
return pegleg_cmd();
}
return 0;
}
#include "i2c_interrupt.h"
/* ISR handlers */
ISR(TIMER0_COMPA_vect)
{
/****
* this mainline loop runs at 2KHz, or 0.5ms.
* at the faster speed, this is ~3968 cycles/loop @8MHz.
****/
/* TIMEKEEPING */
// we only count a total of 10 seconds this way before we loop.
tim0_milli++;
if (tim0_milli >= 200) {
tim0_milli = 0;
tim0_centi++;
if (tim0_centi >= 100) {
tim0_centi = 0;
}
}
// 2khz fix. our timer isn't evenly divisible to get our 2K, so we do this...
OCR0A = (OCR0A == TIMER0_COMPARE) ? TIMER0_COMPARE + 1 : TIMER0_COMPARE;
/* ADC / LIGHT SENSOR / TEMP SENSOR */
// main adc handler
if (adc_read_mode) {
switch (adc_read_mode) {
case ADC_MODE_TEMPSENSOR: {
tempsensor_read();
break;
}
case ADC_MODE_LIGHTSENSOR: {
rgbled_sensor_read();
break;
}
}
if (adc_read_step <= 0xfe) {
adc_read_step++;
}
} /* else {
if (adc_read_step != 0xff) {
adc_read_step++;
if (adc_read_step == 0xff) {
// disable the ADC
ADCSRA &= ~(_BV(ADEN));
}
}
} */ // NOTE: we need to keep the ADC enabled; it takes too long to warm up.
/* LEDs */
// TODO: test common cathode mode
// figure out next LED to update (order is always RGB(X) repeating)
if (adc_read_mode != ADC_MODE_LIGHTSENSOR) {
rgbled_idx++;
#ifdef LED_RGBX_4LED
rgbled_idx &= 0x03;
#else
if (rgbled_idx > 2) {
rgbled_idx = 0;
}
#endif
// and now update them
rgbled_update();
}
/* COMMAND IO */
// stop processing the command if it isn't complete in 100ms.
if (comm_cmd != MODE_NONE) {
if (++comm_timeout >= 50) {
comm_cmd = MODE_NONE;
comm_timeout = 0;
i2c_disable_slave();
i2c_enable_slave();
}
}
/* PROFILING */
// determine how much CPU usage we are consuming
tim0_profiler = TCNT0;
}