Mostly functional ATTINY monitor

This commit is contained in:
2021-01-30 13:15:38 -05:00
parent 8257c8c1be
commit bc316375e0
9 changed files with 493 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
#include "AttinyDebounce.h"
#include <Arduino.h>
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;
}
}

View File

@@ -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_

View File

@@ -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 <Wire.h>
#include <Arduino.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#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<I2cReq>(Wire.read());
}
}
static void i2cRequestHook() {
switch (i2c_cmd_byte) {
case I2cReq::READ_WAKE_SRC:
Wire.write(static_cast<uint8_t>(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;
}
}

View File

@@ -0,0 +1,54 @@
#include <Arduino.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#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;
}
}