From 089be9564bcd22f13d9af9d90161461beb1b2c44 Mon Sep 17 00:00:00 2001 From: "Ishan S. Patel" Date: Mon, 31 Aug 2026 23:12:28 -0400 Subject: [PATCH] fixes --- avr_code/adc.c | 15 ++++++- avr_code/adc.h | 1 + avr_code/defines.h | 8 ++-- avr_code/interrupts.c | 8 ++-- avr_code/m95128.c | 8 +++- avr_code/m95128.h | 3 ++ avr_code/main.c | 95 +++++++++++++++++++++++++++++++++---------- avr_code/max31329.c | 18 ++++++-- avr_code/ndef.c | 4 +- avr_code/power_mgmt.c | 14 ++++++- avr_code/rfm69.c | 2 +- avr_code/uart.c | 8 ++++ avr_code/uart.h | 2 + 13 files changed, 147 insertions(+), 39 deletions(-) diff --git a/avr_code/adc.c b/avr_code/adc.c index 5f47f84..26f5bb3 100644 --- a/avr_code/adc.c +++ b/avr_code/adc.c @@ -22,9 +22,20 @@ int8_t adc_Initialize(void) return 0; } -void adc_Disable(void) { ADCSRA &= ~(1 << ADEN); } +// Power (PRR clock gate) and ADEN are managed together, so the ADC draws +// nothing between readings. Enable rewrites the full config because register +// access is unreliable while the clock is gated. +void adc_Disable(void) +{ + ADCSRA &= ~(1 << ADEN); + power_adc_disable(); +} -void adc_Enable(void) { ADCSRA |= (1 << ADEN); } +void adc_Enable(void) +{ + power_adc_enable(); + ADCSRA = (1 << ADEN) | ADC_PRESCALER_64; +} void adc_StartConversion(uint8_t channel) { diff --git a/avr_code/adc.h b/avr_code/adc.h index 67f961a..56f9285 100644 --- a/avr_code/adc.h +++ b/avr_code/adc.h @@ -9,6 +9,7 @@ #define ADC_H #include "defines.h" #include +#include #include #include #include diff --git a/avr_code/defines.h b/avr_code/defines.h index dadb781..07e22b9 100644 --- a/avr_code/defines.h +++ b/avr_code/defines.h @@ -28,10 +28,12 @@ extern "C" { #define DO_UART true #define ITERATING false -// Serial log line, compiled out entirely when DO_UART is off. The caller's -// file must include uart.h (directly or via another driver header). +// Serial log line, compiled out entirely when DO_UART is off. Takes a string +// LITERAL only: PSTR keeps the text in flash instead of copying it into RAM at +// boot. The caller's file must include uart.h (directly or via another driver +// header). Use uart_sendString() for runtime strings. #if DO_UART -#define LOG(msg) uart_sendString(msg) +#define LOG(msg) uart_sendString_P(PSTR(msg)) #else #define LOG(msg) ((void)0) #endif diff --git a/avr_code/interrupts.c b/avr_code/interrupts.c index 20f8fd8..fd96637 100644 --- a/avr_code/interrupts.c +++ b/avr_code/interrupts.c @@ -56,9 +56,11 @@ void wdt_isr_enable(void) MCUSR &= ~(1 << WDRF); WDTCSR = (1 << WDCE) | (1 << WDE); - // WDP[3:0] = 0b011 -> 0.125 s. Interrupt mode only (WDE clear), so an - // expiry wakes us to clear the debounce instead of resetting the part. - WDTCSR = (1 << WDIE) | (1 << WDP1) | (1 << WDP0); + // WDP[3:0] = 0b010 -> 64 ms debounce, capping the count at ~15 rev/s; + // a reed contact settles in a few ms, so this is generous. Interrupt mode + // only (WDE clear): an expiry wakes us to clear the debounce instead of + // resetting the part. + WDTCSR = (1 << WDIE) | (1 << WDP1); SREG = sreg; // Restore, never blanket-sei(): these run inside an ISR } diff --git a/avr_code/m95128.c b/avr_code/m95128.c index 0b75a6c..3718d3b 100644 --- a/avr_code/m95128.c +++ b/avr_code/m95128.c @@ -144,8 +144,12 @@ void write_struct_to_last_page(tx_rx_data_struct tx_data_in) { uint8_t page_val = get_last_page(); uint8_t next_page_val; - if (page_val == 255) { - next_page_val = 1; + if (page_val >= EEPROM_MAX_PAGE) { + // Spool full (~2.6 days of failed sends). Overwrite the newest spooled + // packet: one packet is lost either way, and this keeps the depth + // truthful -- the old wrap to page 1 stranded 254 packets the counter + // no longer admitted to. + next_page_val = EEPROM_MAX_PAGE; } else { next_page_val = page_val + 1; } diff --git a/avr_code/m95128.h b/avr_code/m95128.h index 79fd25e..f754d30 100644 --- a/avr_code/m95128.h +++ b/avr_code/m95128.h @@ -26,6 +26,9 @@ #define EEPROM_RDLS 0b10000011 // 0x83 #define EEPROM_LID 0b10000010 // 0x82 #define PAGE_SIZE 64 +// M95128 is 16 KB = 256 x 64-byte pages; page 0 holds the spool depth, so +// pages 1..255 hold packets. +#define EEPROM_MAX_PAGE 255 #define EEPROM_STATUS_WIP 0x01 // A page write takes ~5 ms; past this the device is not responding. diff --git a/avr_code/main.c b/avr_code/main.c index 331fcd5..d0322ce 100644 --- a/avr_code/main.c +++ b/avr_code/main.c @@ -52,6 +52,17 @@ // 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 // --------------------------------------------------------------------------- @@ -63,6 +74,8 @@ static volatile uint16_t wheel_counts[WHEEL_COUNT_SLOTS]; // Revolutions per min // 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 @@ -155,16 +168,33 @@ static void wake_peripheral_rails(void) // Measurement helpers // --------------------------------------------------------------------------- -// Battery level via the internal 1.1 V bandgap: the first conversions after -// enabling the ADC read low, so take three and keep the last. -static uint16_t read_battery_level(void) +// 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 level = adc_GetConversion(ADC_CHANNEL_BANDGAP); + uint16_t raw = adc_GetConversion(ADC_CHANNEL_BANDGAP); adc_Disable(); - return level; + + 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 @@ -186,19 +216,30 @@ static void take_counts_snapshot(uint16_t snapshot[WHEEL_COUNT_SLOTS]) // --------------------------------------------------------------------------- // Build and send the periodic counts packet. Unacknowledged packets go to the -// EEPROM spool; each acknowledged one buys a retry of one spooled packet. +// EEPROM spool; each acknowledged send buys retries of spooled packets. static void send_wheel_counts_report(void) { rfm69_init(); - rfid_set_low_power_down(false); - _delay_ms(1); - // Re-read the tag each period so a renamed nugget takes effect without a - // reset. - IDENTIFIER = get_nugget_data(); + // 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; + } - if (time_sync_status == RTC_RFM69_SET_TIME_FAILED) { + // 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]; @@ -206,12 +247,13 @@ static void send_wheel_counts_report(void) reset_txrx_struct(&TX_DATA); TX_DATA = generate_wheel_counts_message( - IDENTIFIER, rtc_read_time(), read_battery_level(), counts_snapshot); + 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"); @@ -219,14 +261,16 @@ static void send_wheel_counts_report(void) return; } - // The base station is listening -- use the chance to drain one packet from - // the spool. - if (get_last_page() > 0) { + // 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 @@ -235,11 +279,11 @@ static void send_wheel_counts_report(void) // Only drop the spooled page once it is actually acknowledged; // deleting on failure would lose the data. - if (result == DATA_SEND_SUCCESS) { - delete_last_page(); - } else { + if (result != DATA_SEND_SUCCESS) { LOG(" SPI not sent\n"); + break; } + delete_last_page(); } } @@ -291,6 +335,10 @@ static void init_all_hardware(void) 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"); @@ -348,10 +396,13 @@ int main(void) // 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); - LOG(time_sync_status == RTC_RFM69_SET_TIME_SUCCESS ? "Success in get time \n" - : "Failed to get time \n"); + if (time_sync_status == RTC_RFM69_SET_TIME_SUCCESS) { + LOG("Success in get time \n"); + } else { + LOG("Failed to get time \n"); + } - read_battery_level(); // Throwaway read to settle the ADC path + read_battery_millivolts(); // Throwaway read to settle the ADC path while (1) { sleep_until_interrupt(); diff --git a/avr_code/max31329.c b/avr_code/max31329.c index 3998388..1e5cfc2 100644 --- a/avr_code/max31329.c +++ b/avr_code/max31329.c @@ -31,8 +31,12 @@ RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data) TIME.Year = RX_DATA.msg[5]; TIME.Wday = RX_DATA.msg[6]; - rtc_write_time(TIME); - return RTC_RFM69_SET_TIME_SUCCESS; + // Only claim success once the time actually landed in the RTC, + // so a failed write is retried next cycle. + if (rtc_write_time(TIME) == 0) { + return RTC_RFM69_SET_TIME_SUCCESS; + } + LOG("RTC write failed\n"); } } return RTC_RFM69_SET_TIME_FAILED; @@ -78,8 +82,16 @@ uint8_t rtc_read_time_array(uint8_t* data) time_struct rtc_read_time(void) { + if (rtc_read_time_array(DATA_BUFFER_7)) { + // RTC unreachable: mark every field with an unmistakably invalid value + // rather than transmitting whatever was read last. The base station + // sees month 0xFF and knows the timestamp is unusable. + LOG("RTC read failed\n"); + TIME.Second = TIME.Minute = TIME.Hour = 0xFF; + TIME.Wday = TIME.Day = TIME.Month = TIME.Year = 0xFF; + return TIME; + } - rtc_read_time_array(DATA_BUFFER_7); TIME.Second = BCD2DEC(DATA_BUFFER_7[0]); TIME.Minute = BCD2DEC(DATA_BUFFER_7[1]); TIME.Hour = BCD2DEC((DATA_BUFFER_7[2] & ~(1 << 6))); diff --git a/avr_code/ndef.c b/avr_code/ndef.c index ce4d484..187e9f8 100644 --- a/avr_code/ndef.c +++ b/avr_code/ndef.c @@ -102,7 +102,9 @@ ndef_message readNDEFText(unsigned char* buf, uint8_t buf_len) NDEF_MSG.payload[payload_length] = '\0'; NDEF_MSG.payload_len = payload_length; - LOG(NDEF_MSG.payload); +#if DO_UART + uart_sendString(NDEF_MSG.payload); // Runtime string, so not LOG() +#endif LOG("\n"); return NDEF_MSG; diff --git a/avr_code/power_mgmt.c b/avr_code/power_mgmt.c index b597281..9af0c26 100644 --- a/avr_code/power_mgmt.c +++ b/avr_code/power_mgmt.c @@ -1,15 +1,25 @@ +// One-shot clock gating for every peripheral this firmware never uses (and +// the ADC, which adc_Enable()/adc_Disable() power up only around a reading). +// In use and left alone: SPI1 (radio + EEPROM), TWI0 (RTC + NFC tag), and +// USART0 when serial logging is compiled in. + #include "power_mgmt.h" void shutdown_all_peripherals(void) { - power_adc_disable(); power_timer0_disable(); power_timer1_disable(); power_timer2_disable(); power_timer3_disable(); + power_usart1_disable(); +#ifdef power_spi0_disable + power_spi0_disable(); +#endif +#ifdef power_twi1_disable + power_twi1_disable(); +#endif #if !DO_UART power_usart0_disable(); - power_usart1_disable(); #endif } diff --git a/avr_code/rfm69.c b/avr_code/rfm69.c index 1542f59..2617402 100644 --- a/avr_code/rfm69.c +++ b/avr_code/rfm69.c @@ -284,7 +284,7 @@ DATA_SEND_STATUS send_message(tx_rx_data_struct tx_data) // 60-byte msg layout: // [0..9] name (padded) // [10..19] wheel diameter (padded) -// [20..21] battery reading, little endian +// [20..21] battery voltage in millivolts, little endian (0 = read failed) // [22..26] timestamp: minute, hour, day, month, year // [27..56] 15 x uint16 per-minute counts, little endian // [57..59] 24-bit hash of bytes 0..56 diff --git a/avr_code/uart.c b/avr_code/uart.c index d1a0e56..ed27081 100644 --- a/avr_code/uart.c +++ b/avr_code/uart.c @@ -57,6 +57,14 @@ void uart_sendString(const char* str) } } +void uart_sendString_P(const char* progmem_str) +{ + char c; + while ((c = pgm_read_byte(progmem_str++)) != '\0') { + uart_sendChar(c); + } +} + void uart_print_uint16(uint16_t meas, const char* buf) { snprintf(array_internal, sizeof(array_internal), "%u", meas); diff --git a/avr_code/uart.h b/avr_code/uart.h index bc65316..365b895 100644 --- a/avr_code/uart.h +++ b/avr_code/uart.h @@ -8,6 +8,7 @@ #include "defines.h" #include +#include #include #include #include @@ -26,6 +27,7 @@ extern "C" { void uart_init(void); void uart_sendChar(char c); void uart_sendString(const char* str); +void uart_sendString_P(const char* progmem_str); // For strings kept in flash (PSTR) void uart_sendStringArray(unsigned char str[], uint8_t len); void uart_print_uint16(uint16_t meas, const char* buf); void uart_print_hex(unsigned char vin, const char* buf);