diff --git a/README.md b/README.md index a5faae7..82611fe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ Schematics and PCB layout made using KiCad (http://kicad-pcb.org/). -ATTiny85's firmware is built using PlatformIO (http://platformio.org). \ No newline at end of file +ATTiny85's firmware is built using PlatformIO (http://platformio.org). + +Before uploading the firmware I used the Arduino IDE and an "Arduino as ISP" to burn the 8 Mhz bootloader, you may get incorrect timing otherwise. I have not yet bothered to figure out how to do this with just PlatformIO. \ No newline at end of file diff --git a/Source/src/buttons.c b/Source/src/buttons.c new file mode 100644 index 0000000..3f6637a --- /dev/null +++ b/Source/src/buttons.c @@ -0,0 +1,197 @@ +#include "buttons.h" +#include +#include + + +ButtonState buttonUp; +ButtonState buttonDown; +ButtonState buttonOption; + + +void button_init(ButtonState* state) +{ + (*state).flags = BSFLastReported; + (*state).debounceTicks = 0; + (*state).repeatTicks = 0; +} + + +void buttons_init() +{ + button_init(&buttonUp); + button_init(&buttonDown); + button_init(&buttonOption); + + // Input with internal pull-up resistor + DDRB &= ~BUTTON_ALL; + PORTB |= BUTTON_ALL; + + // Mode 2: CTC + TCCR0A = _BV(WGM01); + + // Set prescaler to divide by 1024 + TCCR0B = _BV(CS02) | _BV(CS00); + + // 10ms + OCR0A = XTAL / 1024.0 * 10e-3 - 1; + + // Enable timer interrupt + TIMSK = _BV(OCIE0A); + + sei(); +} + + + +// Since it's a pull-up, the value is low when the button is pressed. +// This macro just helps make the code more readable. +#define isPressed(state) (!state) + +#define flagSet(flags, value) ((flags & value) != 0) +#define setFlag(flags, value) flags |= (value) +#define clearFlag(flags, value) flags &= ~(value) + + + +void button_check(ButtonState* state, uint8_t readState) +{ + #define stateRef (*state) + + uint8_t lastState = flagSet(stateRef.flags, BSFLastReported); + uint8_t newState = lastState; + + // Debounce + if (readState != lastState) + { + stateRef.debounceTicks++; + if (stateRef.debounceTicks >= DEBOUNCE_TIME) + { + // Pressed / released + newState = readState; + + if (newState) + setFlag(stateRef.flags, BSFLastReported); + else + clearFlag(stateRef.flags, BSFLastReported); + } + } + else + { + // State changed, reset debounce timer + stateRef.debounceTicks = 0; + } + + + // Check new state + if (isPressed(newState)) + { + if (!isPressed(lastState)) + { + // Pressed + clearFlag(stateRef.flags, BSFRepeated | BSFReleased); + setFlag(stateRef.flags, BSFPressed | BSFRepeatEnabled); + stateRef.repeatTicks = REPEAT_START; + } + else if (flagSet(stateRef.flags, BSFRepeatEnabled)) + { + // Held pressed + stateRef.repeatTicks--; + + if (stateRef.repeatTicks == 0) + { + stateRef.repeatTicks = REPEAT_NEXT; + setFlag(stateRef.flags, BSFRepeated); + } + } + } + else if (isPressed(lastState)) + { + // Released + setFlag(stateRef.flags, BSFReleased); + } + + #undef stateRef +} + + + +// every 10ms +ISR(TIMER0_COMPA_vect) +{ + uint8_t pinState = PINB; + + button_check(&buttonUp, flagSet(pinState, _BV(BUTTON_UP))); + button_check(&buttonDown, flagSet(pinState, _BV(BUTTON_DOWN))); + button_check(&buttonOption, flagSet(pinState, _BV(BUTTON_OPTION))); +} + + + +inline uint8_t readAndClearFlag(ButtonState* state, uint8_t flag) +{ + uint8_t result; + + ATOMIC_BLOCK(ATOMIC_FORCEON) + { + result = flagSet((*state).flags, flag); + (*state).flags &= ~(flag); + } + + return result; +} + + +uint8_t button_is_pressed(ButtonState* state) +{ + return readAndClearFlag(state, BSFPressed); +} + +uint8_t button_is_released(ButtonState* state) +{ + return readAndClearFlag(state, BSFReleased); +} + +uint8_t button_is_repeated(ButtonState* state) +{ + return readAndClearFlag(state, BSFRepeated); +} + + +uint8_t button_is_pressed_or_repeated(ButtonState* state) +{ + return readAndClearFlag(state, BSFPressed | BSFRepeated); +} + + +uint8_t button_is_released_short(ButtonState* state) +{ + uint8_t result; + + ATOMIC_BLOCK(ATOMIC_FORCEON) + { + result = flagSet((*state).flags, BSFReleased) && + (!flagSet((*state).flags, BSFRepeated)); + + if (result) + (*state).flags &= ~BSFReleased; + } + + return result; +} + +uint8_t button_is_pressed_long(ButtonState* state) +{ + uint8_t result; + + ATOMIC_BLOCK(ATOMIC_FORCEON) + { + result = flagSet((*state).flags, BSFPressed) && + flagSet((*state).flags, BSFRepeated); + + // Remove RepeatEnabled as well to prevent further repeat events + if (result) + (*state).flags &= ~(BSFPressed | BSFRepeated | BSFRepeatEnabled); + } + + return result; +} \ No newline at end of file diff --git a/Source/src/buttons.h b/Source/src/buttons.h new file mode 100644 index 0000000..a9b0dda --- /dev/null +++ b/Source/src/buttons.h @@ -0,0 +1,90 @@ +#ifndef __Buttons +#define __Buttons + +#include +#include + + +// Some code borrowed from Peter Dannegger: +// http://www.avrfreaks.net/forum/advanced-button-scan-routine +// +// While his version is optimized well, I found it hard to follow the logic +// enough to customize it for my needs, so I went with a less optimized version. +// It's good enough for this purpose. + +// Last reported state +#define BSFLastReported _BV(1) + +// Event flags, cleared when polled +#define BSFPressed _BV(2) +#define BSFRepeated _BV(3) +#define BSFReleased _BV(4) + +// If not set repeats are disabled while the button is pressed +#define BSFRepeatEnabled _BV(5) + + +typedef struct +{ + // Bitmask according to BSF defines above + uint8_t flags; + + // How many ticks the last state has been stable for + uint8_t debounceTicks; + + // How many ticks remaining for the next repeat event + uint8_t repeatTicks; +} ButtonState; + + +extern ButtonState buttonUp; +extern ButtonState buttonDown; +extern ButtonState buttonOption; + + + +// 8MHz +#define XTAL 8e6 + +#ifndef BUTTON_UP +#define BUTTON_UP PB1 +#endif + +#ifndef BUTTON_DOWN +#define BUTTON_DOWN PB3 +#endif + +#ifndef BUTTON_OPTION +#define BUTTON_OPTION PB4 +#endif + +#define BUTTON_ALL (_BV(BUTTON_UP) | _BV(BUTTON_DOWN) | _BV(BUTTON_OPTION)) + + +// after 20ms (if the timer runs every 10ms, 20ms is the minimum) +#define DEBOUNCE_TIME 2 + +// after 500ms +#define REPEAT_START 50 + +// every 200ms +#define REPEAT_NEXT 20 + + +extern void buttons_init(); + + +// Note: these functions act on events, not on the current state. +// Calling them will reset the corresponding event and further calls +// will return false until the event triggers again. + +extern uint8_t button_is_pressed(ButtonState* state); +extern uint8_t button_is_released(ButtonState* state); +extern uint8_t button_is_repeated(ButtonState* state); + +extern uint8_t button_is_pressed_or_repeated(ButtonState* state); + +extern uint8_t button_is_released_short(ButtonState* state); +extern uint8_t button_is_pressed_long(ButtonState* state); + +#endif \ No newline at end of file diff --git a/Source/src/main.c b/Source/src/main.c index f0942e4..9147b24 100644 --- a/Source/src/main.c +++ b/Source/src/main.c @@ -5,14 +5,17 @@ #include #include "shared.h" +#include "buttons.h" #include "screen/counter.h" -#define VccTreshold 3000 + +#define VccOffTreshold 3000 +#define VccOnTreshold 3100 // Forward declarations -uint8_t hasPower(); +void checkPower(); void waitForInput(); void handleCurrentScreen(); @@ -23,9 +26,12 @@ uint16_t readVCC(); int main() { + buttons_init(); + while (1) { - if (hasPower()) + checkPower(); + if (powerState == On) handleCurrentScreen(); } @@ -40,43 +46,128 @@ void waitForInput() } +uint8_t lastButton = 0; + void handleCurrentScreen() { - handleCounterScreen(); + ssd1306_setpos(0, 0); + + if (bit_is_set(PINB, BUTTON_DOWN)) + { + ssd1306_string("released"); + } + else + { + ssd1306_string("pressed "); + } + + char value[9]; + value[0] = (buttonDown.flags & BSFLastReported) ? '1' : '0'; + value[1] = (buttonDown.flags & BSFPressed) ? '1' : '0'; + value[2] = (buttonDown.flags & BSFRepeated) ? '1' : '0'; + value[3] = (buttonDown.flags & BSFReleased) ? '1' : '0'; + value[4] = (buttonDown.flags & BSFRepeatEnabled) ? '1' : '0'; + value[5] = '\0'; + + ssd1306_setpos(0, 1); + ssd1306_string(&value[0]); + + itoa(buttonDown.debounceTicks, value, 10); + ssd1306_setpos(0, 2); + ssd1306_string(&value[0]); + + itoa(buttonDown.repeatTicks, value, 10); + ssd1306_setpos(0, 3); + ssd1306_string(&value[0]); + + if (button_is_pressed(&buttonUp)) + { + ssd1306_setpos(0, 5); + ssd1306_string("+"); + } + else if (button_is_released(&buttonUp)) + { + ssd1306_setpos(0, 5); + ssd1306_string(" "); + } + + if (button_is_pressed(&buttonDown)) + { + ssd1306_setpos(0, 5); + ssd1306_string("-"); + } + else if (button_is_released(&buttonDown)) + { + ssd1306_setpos(0, 5); + ssd1306_string(" "); + } + + if (button_is_pressed(&buttonOption)) + { + ssd1306_setpos(0, 5); + ssd1306_string("O"); + } + else if (button_is_released(&buttonOption)) + { + ssd1306_setpos(0, 5); + ssd1306_string(" "); + } + _delay_ms(10); + + //handleCounterScreen(); } -uint8_t isOn = 0; -uint8_t hasPower() +void checkPower() { vcc = readVCC(); - // Turn display off below 3v. It holds up surprisingly well, but at around - // 1.5v it does corrupt the screen and requires reinitialization when the - // voltage is turned back up. - // - // ...although by then the battery would be damaged, but still, turning off at - // 3v means we're close enough to the recommended minimum of 2.7v for LiPo - // batteries to be safe. - // - // TODO go into a sleep cycle until the battery is recharged - if ((vcc > VccTreshold) != (isOn != 0)) + switch (powerState) { - isOn = !isOn; - if (isOn) - { - // Delay is required on power-on for the SSD1306 to initialize, - // to be sure we're simply delaying every time it's reinitialized - _delay_ms(40); - ssd1306_init(); - } + case On: + // Turn display off below 3v. It holds up surprisingly well, but at around + // 1.5v it does corrupt the screen and requires reinitialization when the + // voltage is turned back up. + // + // ...although by then the battery would be damaged, but still, turning off at + // 3v means we're still in the safe range when we go into battery saving mode + // and the reinitialization afterwards prevents any issues. + if (vcc < VccOffTreshold) + { + ssd1306_clear(); + powerState = BatteryLow; - ssd1306_clear(); + // TODO go into a sleep cycle until the battery is recharged + _delay_ms(100); + } + + break; + + case BatteryLow: + if (vcc > VccOnTreshold) + { + // Delay is required on power-on for the SSD1306 to initialize, + // to be sure we're simply delaying every time it's reinitialized + _delay_ms(40); + ssd1306_init(); + ssd1306_clear(); + powerState = On; + } + else + { + // TODO continue sleep cycle + _delay_ms(100); + } + + break; + + case ManualOff: + // TODO go into sleep mode + _delay_ms(100); + break; } - - return isOn; } diff --git a/Source/src/shared.c b/Source/src/shared.c index f29a95a..3c9050b 100644 --- a/Source/src/shared.c +++ b/Source/src/shared.c @@ -1,4 +1,5 @@ #include "shared.h" +PowerState powerState = BatteryLow; uint16_t vcc = 0; uint16_t counter = 0; \ No newline at end of file diff --git a/Source/src/shared.h b/Source/src/shared.h index f4b506b..a843c3b 100644 --- a/Source/src/shared.h +++ b/Source/src/shared.h @@ -3,6 +3,10 @@ #include +typedef enum { On, ManualOff, BatteryLow } PowerState; + + +extern PowerState powerState; extern uint16_t vcc; extern uint16_t counter;