// 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 ",") // // 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 #include #include #include #include #include // 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(); } } }