575 lines
14 KiB
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;
|
||
|
}
|
||
|
|