422 lines
13 KiB
C
422 lines
13 KiB
C
// Wheel revolution counter
|
|
// ========================
|
|
//
|
|
// Battery-powered node that counts exercise-wheel revolutions and reports them
|
|
// over an RFM69 radio.
|
|
//
|
|
// Hardware (ATmega328PB @ 8 MHz):
|
|
// PD3/INT1 reed switch, closes to ground once per wheel revolution
|
|
// PD2/INT0 MAX31329 RTC alarm output (open drain), fires once per minute
|
|
// SPI1 RFM69 packet radio + M95128 EEPROM (used as a retry spool)
|
|
// I2C MAX31329 RTC + ST25DV NFC tag (holds "<name>,<diameter>")
|
|
//
|
|
// Operation:
|
|
// 1. Sleep in power-down. A reed pulse increments the count for the
|
|
// current minute slot; an RTC alarm advances to the next slot.
|
|
// 2. Every SEND_INTERVAL minutes, pack the per-minute counts into one
|
|
// radio packet (name, diameter, battery, timestamp, counts, hash).
|
|
// 3. If the base station does not acknowledge, spool the packet to
|
|
// EEPROM; whenever a live packet is acknowledged, retry one spooled
|
|
// packet.
|
|
|
|
#include "adc.h"
|
|
#include "defines.h"
|
|
#include "interrupts.h"
|
|
#include "m95128.h"
|
|
#include "max31329.h"
|
|
#include "ndef.h"
|
|
#include "power_mgmt.h"
|
|
#include "rfm69.h"
|
|
#include "st25dv.h"
|
|
#include "states.h"
|
|
#include "uart.h"
|
|
|
|
#include <avr/interrupt.h>
|
|
#include <avr/io.h>
|
|
#include <avr/sleep.h>
|
|
#include <stdbool.h>
|
|
#include <util/atomic.h>
|
|
#include <util/delay.h>
|
|
|
|
// How many minute slots are collected before a packet is sent. ITERATING is a
|
|
// bench-test mode that sends every minute (and lets the reed switch stand in
|
|
// for the minute alarm).
|
|
#if ITERATING
|
|
#define SEND_INTERVAL 1
|
|
#else
|
|
#define SEND_INTERVAL 15
|
|
#endif
|
|
|
|
#define WHEEL_COUNT_SLOTS 15
|
|
|
|
// Erased EEPROM reads back as 0xFF; anything else is a real spool depth.
|
|
#define EEPROM_LAST_PAGE_UNINIT 0xFF
|
|
|
|
// The NFC identity is cached; re-read the tag every 4th send (~1 h), so a
|
|
// renamed nugget still takes effect without a reset.
|
|
#define TAG_REREAD_SEND_CYCLES 4
|
|
|
|
// Re-request the time daily even when the RTC is running, to bound its drift.
|
|
#define TIME_RESYNC_SEND_CYCLES 96 // 96 x 15 min = 24 h
|
|
|
|
// How many spooled packets one successful cycle may retry, so a huge backlog
|
|
// cannot keep the node awake for minutes.
|
|
#define SPOOL_DRAIN_MAX 10
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// State shared with the interrupt handlers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static volatile bool minute_alarm_fired = false; // Set by INT0, consumed by main loop
|
|
static volatile bool reed_is_debouncing = false; // Set by INT1, cleared by WDT expiry
|
|
static volatile uint8_t minute_slot = 0; // Which wheel_counts[] slot is being filled
|
|
static volatile uint16_t wheel_counts[WHEEL_COUNT_SLOTS]; // Revolutions per minute slot
|
|
|
|
// Whether we ever got a valid timestamp from the base station
|
|
static RTC_RFM69_STATUS time_sync_status;
|
|
static uint8_t sends_since_tag_read = 0;
|
|
static uint8_t sends_since_time_sync = 0;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Interrupt handlers
|
|
//
|
|
// Both external interrupts are low-level triggered (the only mode that can
|
|
// wake the MCU from power-down), so each handler must mask itself while its
|
|
// source still holds the line low. See interrupts.c.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// RTC minute alarm. The RTC holds INTB low until main reads its flag
|
|
// registers, so mask INT0 here; main re-arms it after clearing the flags.
|
|
ISR(INT0_vect)
|
|
{
|
|
EIMSK &= ~(1 << INT0);
|
|
minute_alarm_fired = true;
|
|
LOG("\t\t\t\tMINUTE INTERRUPT\n");
|
|
}
|
|
|
|
// Reed switch: one revolution. The magnet holds the reed closed far longer
|
|
// than one bounce, so mask INT1 for a WDT-timed debounce window; the WDT
|
|
// handler below re-arms it.
|
|
ISR(INT1_vect)
|
|
{
|
|
#if ITERATING
|
|
minute_alarm_fired = true;
|
|
#endif
|
|
LOG("\t\t\t\tREED INTERRUPT\n");
|
|
|
|
if (!reed_is_debouncing) {
|
|
if (minute_slot < WHEEL_COUNT_SLOTS) {
|
|
wheel_counts[minute_slot]++;
|
|
}
|
|
reed_is_debouncing = true;
|
|
EIMSK &= ~(1 << INT1);
|
|
wdt_isr_enable();
|
|
}
|
|
}
|
|
|
|
// Debounce window over: allow the next reed pulse to count.
|
|
ISR(WDT_vect)
|
|
{
|
|
reed_is_debouncing = false;
|
|
wdt_isr_disable();
|
|
reed_interrupt_enable();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Power management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Cut power to every peripheral and enter power-down until the reed switch,
|
|
// the RTC alarm, or the debounce watchdog wakes us.
|
|
static void sleep_until_interrupt(void)
|
|
{
|
|
spi_eeprom_select(false);
|
|
spi_rfm69_select(false);
|
|
rfid_set_low_power_down(true);
|
|
rfid_set_i2c_power(false);
|
|
ldo_set_state(false);
|
|
_delay_ms(10);
|
|
|
|
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
|
|
|
|
// avr-libc sleep idiom: test for pending work with interrupts off, and
|
|
// sei() only immediately before sleep_cpu() (the next instruction always
|
|
// executes before any pending interrupt), so an alarm that fired while the
|
|
// rails were dropping cannot be slept through. sleep_bod_disable() is a
|
|
// timed 3-cycle sequence and must sit directly before sleep_cpu().
|
|
cli();
|
|
if (!minute_alarm_fired) {
|
|
sleep_enable();
|
|
sleep_bod_disable();
|
|
sei();
|
|
sleep_cpu();
|
|
sleep_disable();
|
|
}
|
|
sei();
|
|
}
|
|
|
|
// Restore the supplies that sleep_until_interrupt() dropped. Everything on the
|
|
// I2C bus is dead until this runs.
|
|
static void wake_peripheral_rails(void)
|
|
{
|
|
ldo_set_state(true);
|
|
rfid_set_i2c_power(true);
|
|
_delay_ms(1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Measurement helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Battery voltage in millivolts, measured by reading the 1.1 V internal
|
|
// bandgap against the AVcc (battery) reference: Vcc = 1100 mV * 1023 / raw.
|
|
// The first conversions after enabling the ADC read low, so take three and
|
|
// keep the last. Returns 0 when the ADC fails, which the base station can
|
|
// recognise as "no reading".
|
|
static uint16_t read_battery_millivolts(void)
|
|
{
|
|
adc_Enable();
|
|
adc_GetConversion(ADC_CHANNEL_BANDGAP);
|
|
adc_GetConversion(ADC_CHANNEL_BANDGAP);
|
|
uint16_t raw = adc_GetConversion(ADC_CHANNEL_BANDGAP);
|
|
adc_Disable();
|
|
|
|
if (raw == 0) {
|
|
return 0;
|
|
}
|
|
return (uint16_t)((1100UL * 1023UL) / raw);
|
|
}
|
|
|
|
// Spread nodes out: a name-hash-derived delay (0-236 ms) before transmitting
|
|
// keeps two nodes that woke on the same RTC second from colliding on every
|
|
// single cycle.
|
|
static void tx_backoff_delay(void)
|
|
{
|
|
for (uint8_t i = 0; i < IDENTIFIER.hashed; i++) {
|
|
_delay_ms(4);
|
|
}
|
|
}
|
|
|
|
// Atomically hand out the collected counts and start the next collection
|
|
// period, so a reed pulse landing mid-copy is neither lost nor double-counted.
|
|
static void take_counts_snapshot(uint16_t snapshot[WHEEL_COUNT_SLOTS])
|
|
{
|
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
|
|
{
|
|
for (uint8_t c = 0; c < WHEEL_COUNT_SLOTS; c++) {
|
|
snapshot[c] = wheel_counts[c];
|
|
wheel_counts[c] = 0;
|
|
}
|
|
minute_slot = 0;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Radio reporting
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Build and send the periodic counts packet. Unacknowledged packets go to the
|
|
// EEPROM spool; each acknowledged send buys retries of spooled packets.
|
|
static void send_wheel_counts_report(void)
|
|
{
|
|
rfm69_init();
|
|
|
|
// Reading the tag costs an I2C transaction and a tag power-up, so use the
|
|
// cached identity and only re-read about once an hour -- or immediately, if
|
|
// the last read failed to parse.
|
|
sends_since_tag_read++;
|
|
if ((sends_since_tag_read >= TAG_REREAD_SEND_CYCLES) || (NDEF_MSG.success != 0)) {
|
|
IDENTIFIER = get_nugget_data();
|
|
sends_since_tag_read = 0;
|
|
}
|
|
|
|
// Sync time when we never got it, and re-sync daily to bound RTC drift.
|
|
if (sends_since_time_sync < 255) {
|
|
sends_since_time_sync++;
|
|
}
|
|
if ((time_sync_status == RTC_RFM69_SET_TIME_FAILED)
|
|
|| (sends_since_time_sync >= TIME_RESYNC_SEND_CYCLES)) {
|
|
time_sync_status = set_time_from_rfm69(IDENTIFIER);
|
|
if (time_sync_status == RTC_RFM69_SET_TIME_SUCCESS) {
|
|
sends_since_time_sync = 0;
|
|
}
|
|
}
|
|
|
|
uint16_t counts_snapshot[WHEEL_COUNT_SLOTS];
|
|
take_counts_snapshot(counts_snapshot);
|
|
|
|
reset_txrx_struct(&TX_DATA);
|
|
TX_DATA = generate_wheel_counts_message(
|
|
IDENTIFIER, rtc_read_time(), read_battery_millivolts(), counts_snapshot);
|
|
LOG("TX DATA Sent\n");
|
|
#if DO_UART
|
|
uart_print_tx_rx_data(TX_DATA);
|
|
#endif
|
|
|
|
tx_backoff_delay();
|
|
DATA_SEND_STATUS result = send_message(TX_DATA);
|
|
if (result == DATA_NOT_SENT) {
|
|
LOG(" TX DATA not sent, writing to SPI\n");
|
|
write_struct_to_last_page(TX_DATA);
|
|
return;
|
|
}
|
|
|
|
// The base station is listening -- drain the spool while sends keep
|
|
// succeeding, capped at SPOOL_DRAIN_MAX per cycle. At one per cycle a long
|
|
// outage took days to catch up.
|
|
for (uint8_t drained = 0; (drained < SPOOL_DRAIN_MAX) && (get_last_page() > 0); drained++) {
|
|
reset_txrx_struct(&TX_DATA);
|
|
TX_DATA = read_struct_last_page();
|
|
TX_DATA.flags = MSG_RESENT_COUNTS;
|
|
_delay_ms(250);
|
|
|
|
tx_backoff_delay();
|
|
result = send_message(TX_DATA);
|
|
LOG("TX DATA From SPI Memory\n");
|
|
#if DO_UART
|
|
uart_print_tx_rx_data(TX_DATA);
|
|
#endif
|
|
|
|
// Only drop the spooled page once it is actually acknowledged;
|
|
// deleting on failure would lose the data.
|
|
if (result != DATA_SEND_SUCCESS) {
|
|
LOG(" SPI not sent\n");
|
|
break;
|
|
}
|
|
delete_last_page();
|
|
}
|
|
}
|
|
|
|
// One RTC alarm has fired: clear it, advance the minute slot, and send a
|
|
// report if a full period has been collected.
|
|
static void handle_minute_alarm(void)
|
|
{
|
|
LOG("In minute index\n");
|
|
|
|
uint8_t slots_filled;
|
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
|
|
{
|
|
if (minute_slot < WHEEL_COUNT_SLOTS) {
|
|
minute_slot += 1;
|
|
}
|
|
slots_filled = minute_slot;
|
|
}
|
|
|
|
wake_peripheral_rails();
|
|
|
|
// Reading the RTC flag registers releases the (level-triggered) INTB line,
|
|
// after which INT0 can safely be re-armed.
|
|
rtc_read_interrupt_register();
|
|
rtc_read_status_register();
|
|
minute_interrupt_enable();
|
|
|
|
if (slots_filled >= SEND_INTERVAL) {
|
|
send_wheel_counts_report();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Start-up
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void init_all_hardware(void)
|
|
{
|
|
ldo_set_state(true);
|
|
_delay_ms(10);
|
|
init_pins();
|
|
|
|
#if DO_UART
|
|
uart_init();
|
|
LOG("---- STARTING ----\n");
|
|
uart_wait_until_sent();
|
|
#endif
|
|
|
|
i2c_init();
|
|
init_spi();
|
|
adc_Initialize();
|
|
|
|
// Gate the clocks of everything unused; adc_Enable() lifts the ADC's gate
|
|
// for the duration of each battery reading.
|
|
shutdown_all_peripherals();
|
|
|
|
set_up_reed_interrupt();
|
|
set_up_minute_interrupt();
|
|
LOG("Set up AVR interrupts\n");
|
|
|
|
rtc_set_per_minute_alarm();
|
|
rtc_set_alarm_config();
|
|
rtc_enable_interrupts();
|
|
rtc_read_interrupt_register(); // Clear any alarm already pending
|
|
rtc_read_status_register();
|
|
LOG("Set up RTC interrupts\n");
|
|
|
|
rfm69_init();
|
|
LOG("Initialized RFM69\n");
|
|
|
|
// Only initialise the spool pointer when it has never been written --
|
|
// clearing it unconditionally would discard every unsent message across a
|
|
// reset.
|
|
if (get_last_page() == EEPROM_LAST_PAGE_UNINIT) {
|
|
write_last_page_value(0);
|
|
}
|
|
LOG("Set up last page value for SPI flash\n");
|
|
}
|
|
|
|
// Five long blinks for a successful time sync, five short ones for a failure.
|
|
static void blink_time_sync_result(bool success)
|
|
{
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
led_1_set_state(true);
|
|
if (success) {
|
|
_delay_ms(90);
|
|
} else {
|
|
_delay_ms(10);
|
|
}
|
|
led_1_set_state(false);
|
|
if (success) {
|
|
_delay_ms(10);
|
|
} else {
|
|
_delay_ms(90);
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
init_all_hardware();
|
|
|
|
for (uint8_t c = 0; c < WHEEL_COUNT_SLOTS; c++) {
|
|
wheel_counts[c] = 0;
|
|
}
|
|
|
|
// The nugget's name and wheel diameter live on the NFC tag
|
|
IDENTIFIER = get_nugget_data();
|
|
LOG("Got nugget data from RFID\n");
|
|
|
|
// Ask the base station for the current time and load it into the RTC
|
|
time_sync_status = set_time_from_rfm69(IDENTIFIER);
|
|
blink_time_sync_result(time_sync_status == RTC_RFM69_SET_TIME_SUCCESS);
|
|
if (time_sync_status == RTC_RFM69_SET_TIME_SUCCESS) {
|
|
LOG("Success in get time \n");
|
|
} else {
|
|
LOG("Failed to get time \n");
|
|
}
|
|
|
|
read_battery_millivolts(); // Throwaway read to settle the ADC path
|
|
|
|
while (1) {
|
|
sleep_until_interrupt();
|
|
|
|
bool minute_elapsed;
|
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
|
|
{
|
|
minute_elapsed = minute_alarm_fired;
|
|
minute_alarm_fired = false;
|
|
}
|
|
|
|
if (minute_elapsed) {
|
|
handle_minute_alarm();
|
|
}
|
|
}
|
|
}
|