diff --git a/firmware/lowpower_mcu/.gitignore b/firmware/lowpower_mcu/.gitignore new file mode 100644 index 0000000..03f4a3c --- /dev/null +++ b/firmware/lowpower_mcu/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/firmware/lowpower_mcu/include/README b/firmware/lowpower_mcu/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/firmware/lowpower_mcu/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/firmware/lowpower_mcu/lib/README b/firmware/lowpower_mcu/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/firmware/lowpower_mcu/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/firmware/lowpower_mcu/platformio.ini b/firmware/lowpower_mcu/platformio.ini new file mode 100644 index 0000000..e3e3ea8 --- /dev/null +++ b/firmware/lowpower_mcu/platformio.ini @@ -0,0 +1,53 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = main + +[env] +platform = atmelavr +framework = arduino +board = attiny85 +upload_protocol = custom +upload_port = /dev/ttyACM1 +upload_speed = 19200 +upload_flags = + -C + $PROJECT_PACKAGES_DIR/tool-avrdude/avrdude.conf + -p + $BOARD_MCU + -P + $UPLOAD_PORT + -b + $UPLOAD_SPEED + -c + stk500v1 +upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i + +[common] +;; This is the defaullt src_filter +src_filter = + +<*> + -<.git/> + -<.svn/> + - + - + - + - + +[env:main] +src_filter = + ${common.src_filter} + - + +[env:test] +src_filter = + ${common.src_filter} + - diff --git a/firmware/lowpower_mcu/src/AttinyDebounce.cpp b/firmware/lowpower_mcu/src/AttinyDebounce.cpp new file mode 100644 index 0000000..488a018 --- /dev/null +++ b/firmware/lowpower_mcu/src/AttinyDebounce.cpp @@ -0,0 +1,45 @@ +#include "AttinyDebounce.h" + +#include + +AttinyDebounce::AttinyDebounce(unsigned int index, + unsigned long debounceMillis, + void (* const callback)(int), + bool assertValue) : + index(index), + debounceMillis(debounceMillis), + callback(callback), + assertValue(assertValue), + startAssertingTime(0), + state(IDLE) +{} + +void AttinyDebounce::update(unsigned long millisNow) { + bool assertedNow = ((PINB >> index) & 1) == assertValue; + switch (state) { + case IDLE: + if (assertedNow) { + state = DEBOUNCING; + startAssertingTime = millisNow; + } + break; + case DEBOUNCING: + if (!assertedNow) { + state = IDLE; + break; + } + + if (millisNow > startAssertingTime + debounceMillis) { + state = TRIGGERED; + callback(index); + } + + break; + case TRIGGERED: + // TODO: Debounce this part too + if (!assertedNow) { + state = IDLE; + } + break; + } +} diff --git a/firmware/lowpower_mcu/src/AttinyDebounce.h b/firmware/lowpower_mcu/src/AttinyDebounce.h new file mode 100644 index 0000000..3496be4 --- /dev/null +++ b/firmware/lowpower_mcu/src/AttinyDebounce.h @@ -0,0 +1,35 @@ +#ifndef __ATTINYDEBOUNCE_H_ +#define __ATTINYDEBOUNCE_H_ + +#pragma once + +class AttinyDebounce { + + public: + AttinyDebounce(unsigned int index, + unsigned long debounceMillis, + void (* const callback)(int index), + bool assertValue); + + void update(unsigned long millisNow); + + + private: + enum State { + IDLE, + DEBOUNCING, + TRIGGERED, + }; + + /* Maybe abstract out the HW specifcs (index, PORTB reads) if it seems useful */ + const unsigned int index; + const unsigned long debounceMillis; + void (* const callback)(int index); + const bool assertValue; + + unsigned long startAssertingTime; + enum State state; +}; + + +#endif // __ATTINYDEBOUNCE_H_ diff --git a/firmware/lowpower_mcu/src/main.cpp b/firmware/lowpower_mcu/src/main.cpp new file mode 100644 index 0000000..60ac5d2 --- /dev/null +++ b/firmware/lowpower_mcu/src/main.cpp @@ -0,0 +1,209 @@ +/** + * This code enables an ATTINY to act as a low-power monitor for the ESP8266 + * "main" MCU. It sleeps the ATTINY while waiting for a state change on some + * input pins, wakes the ESP8266 with a reset pulse, and becomes an I2C device + * which can return the source of the wakeup signal. + * + * Specifically, this is used to monitor for: + * - Short button press wakeups + * - Long button press wakeups + * - RTC wakeups (sourced from the ESP8266, which lacks state to know they occured) + * */ + +#include +#include + +#include +#include + +#include "AttinyDebounce.h" + + +// Output pins +constexpr uint8_t MAIN_MCU_NRESET_PIN = 3; +constexpr uint8_t SDA_PIN = 0; +constexpr uint8_t SCL_PIN = 2; + +// Wake source pins +constexpr uint8_t BUTTON_PIN = 1; // Button press +constexpr uint8_t RTC_PIN = 4; // RTC alarm (currently, from the ESP8266 RTC) +constexpr uint8_t AUX_PIN = 5; // Another wakeup source. May be used for other sensors in the future. + +constexpr uint8_t DEBUG_LED = RTC_PIN; + +constexpr unsigned long CMD_TIMEOUT_MILLIS = 1000; // After this period of with no commands, the device will sleep +constexpr unsigned long WAKE_PULSE_MILLIS = 100; // Length of pulse sent to wake the main MCU +constexpr unsigned long LONG_PRESS_MILLIS = 5000; +constexpr unsigned long I2C_WATCHDOG_PERIOD_MILLIS = 2000; // If we don't get a request from the Main MCU in this time, just go to sleep. +constexpr unsigned long DEBOUNCE_MILLIS = 100; + +constexpr uint8_t I2C_DEVICE_ADDR = 0x4F; // Chosen arbitrarily + +static void buttonCallback(int); + +AttinyDebounce ButtonDebounce(BUTTON_PIN, DEBOUNCE_MILLIS, buttonCallback, LOW); + +// Commands sent from the main MCU +enum class I2cReq : uint8_t { + // Request for info + READ_WAKE_SRC = 0x01, // Request the source of the wakeup + + // Commands + CMD_SLEEP = 0x81, +}; + +enum class WakeSource : uint8_t { + BUTTON_SHORT = 0, + BUTTON_LONG = 1, + // BUTTON_DOUBLE = 2, + // BUTTON_TRIPLE = 3, + // BUTTON_QUADRUPLE = 4, + // BUTTON_MANY = 5, + RTC = 10, + AUX = 11, + UNRESOLVED = 0xFE, + NONE = 0xFF +}; + +enum class AppState : uint8_t { + SLEEP, + WATCHING_INPUT, + START_WAKE_MAIN_MCU, + WAKING_MAIN_MCU, + WAITING_FOR_I2C, +}; + +static volatile AppState appState; +static unsigned long startWakeMillis, i2cWatchDogTime; +static I2cReq i2c_cmd_byte; +static volatile WakeSource wakeSource; +static volatile unsigned long endButtonMillis; + +static void i2cReceiveHook(int numBytes) { + while (numBytes > 0) { + i2c_cmd_byte = static_cast(Wire.read()); + } +} + +static void i2cRequestHook() { + switch (i2c_cmd_byte) { + case I2cReq::READ_WAKE_SRC: + Wire.write(static_cast(wakeSource)); + break; + case I2cReq::CMD_SLEEP: + appState = AppState::SLEEP; + break; + } +} + + + +static void enablePinChangeInterrupt() { + GIMSK |= _BV(PCIE); + PCMSK |= _BV(BUTTON_PIN); + sei(); +} + + +static void sleepUntilPinChange() { + enablePinChangeInterrupt(); + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + sleep_enable(); + sleep_cpu(); + sleep_disable(); +} + +static void resetState() { + wakeSource = WakeSource::NONE; + startWakeMillis = 0; + endButtonMillis = 0; + i2cWatchDogTime = 0; +} + +void setup() { + // Init pin states + pinMode(MAIN_MCU_NRESET_PIN, OUTPUT); + pinMode(BUTTON_PIN, INPUT); + pinMode(RTC_PIN, INPUT); + pinMode(AUX_PIN, INPUT); + pinMode(DEBUG_LED, OUTPUT); + + // On first run, reset the main MCU to get in sync. + digitalWrite(MAIN_MCU_NRESET_PIN, HIGH); + delay(WAKE_PULSE_MILLIS); + digitalWrite(MAIN_MCU_NRESET_PIN, LOW); + + appState = AppState::SLEEP; + + // Init I2C + Wire.begin(I2C_DEVICE_ADDR); + Wire.onReceive(i2cReceiveHook); + Wire.onRequest(i2cRequestHook); + + enablePinChangeInterrupt(); + + ADCSRA &= ~_BV(ADEN); // Disable ADC, save some power + +} + +static void buttonCallback(int index) { + wakeSource = WakeSource::BUTTON_SHORT; +} + +ISR(PCINT0_vect) { + unsigned long millisNow = millis(); + ButtonDebounce.update(millisNow); +} + + +void loop() { + unsigned long millisNow = millis(); + ButtonDebounce.update(millisNow); + + switch (appState) { + case AppState::SLEEP: + resetState(); + //digitalWrite(DEBUG_LED, HIGH); + sleepUntilPinChange(); + //digitalWrite(DEBUG_LED, LOW); + appState = AppState::WATCHING_INPUT; + break; + case AppState::WATCHING_INPUT: + if (wakeSource == WakeSource::UNRESOLVED) { + // Keep waiting. For example, waiting to see if long or short button press. + // TODO: Not yet implemented + } else if (wakeSource == WakeSource::NONE) { + // appState = AppState::SLEEP; + } else { + // We know the wakeup source. Time to inform the main MCU. + appState = AppState::START_WAKE_MAIN_MCU; + } + break; + case AppState::START_WAKE_MAIN_MCU: + digitalWrite(MAIN_MCU_NRESET_PIN, HIGH); + startWakeMillis = millis(); + appState = AppState::WAKING_MAIN_MCU; + break; + case AppState::WAKING_MAIN_MCU: + if (millis() > startWakeMillis + WAKE_PULSE_MILLIS) { + appState = AppState::WAITING_FOR_I2C; + digitalWrite(MAIN_MCU_NRESET_PIN, LOW); + } + break; + case AppState::WAITING_FOR_I2C: + if (i2cWatchDogTime == 0) { + i2cWatchDogTime = millis(); + } + + if (i2cWatchDogTime + I2C_WATCHDOG_PERIOD_MILLIS < millis()) { + appState = AppState::SLEEP; + break; + } + + if (i2c_cmd_byte == I2cReq::CMD_SLEEP) { + appState = AppState::SLEEP; + break; + } + break; + } +} diff --git a/firmware/lowpower_mcu/src/test.cpp b/firmware/lowpower_mcu/src/test.cpp new file mode 100644 index 0000000..6ab38ad --- /dev/null +++ b/firmware/lowpower_mcu/src/test.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#define LED 4 +#define BUTTON 0 + +void setup() { + pinMode(LED, OUTPUT); + digitalWrite(LED, HIGH); + pinMode(BUTTON, INPUT); + + for (int i = 0; i < 3; i++) { + digitalWrite(LED, HIGH); + delay(200); + digitalWrite(LED, LOW); + delay(200); + } + + // Enable pin change interrupt + GIMSK |= _BV(PCIE); + PCMSK |= _BV(PCINT0); + sei(); + +} + +static void doSleep() { + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + sleep_enable(); + sleep_cpu(); + sleep_disable(); +} + +volatile bool isrFlag = false; +ISR(PCINT0_vect) { + isrFlag = true; +} + + +volatile bool val; + +void loop() { + static unsigned long ts = millis(); + if (millis() > ts + 500) { + ts = millis(); + PORTB ^= _BV(LED); + } + + if (isrFlag) { + PORTB &= _BV(LED); + doSleep(); + isrFlag = false; + } +} diff --git a/firmware/lowpower_mcu/test/README b/firmware/lowpower_mcu/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/firmware/lowpower_mcu/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html