Compare commits

..

5 Commits

Author SHA1 Message Date
thebears 089be9564b fixes 2026-08-31 23:12:28 -04:00
thebears 2152b8f727 Merge remote-tracking branch 'origin/main' 2026-08-31 23:04:25 -04:00
thebears bbdcc1e623 Refactor firmware for clarity; no functional changes
- main.c: file-header comment describing the hardware and operation, logic
  split into named phases (sleep_until_interrupt, wake_peripheral_rails,
  take_counts_snapshot, send_wheel_counts_report, handle_minute_alarm,
  init_all_hardware); shared state renamed to say what it is and made static.
- rfm69.c: reorganized into six labeled sections; cond_1/2/3 and hash scratch
  globals replaced by a reply_acknowledges() helper with clear locals; packet
  layout and every init register write documented.
- LOG() macro (compiled out when DO_UART is off) replaces the #if DO_UART
  blocks that obscured the logic.
- Drivers: file-header comments; named RTC_REG_*/RTC_ALM_MASK_BIT constants;
  EEPROM spool scheme documented; ADC_CHANNEL_BANDGAP named; repeated pin
  if/else helpers collapsed to SET_PIN_TO().
- Removed unused globals/buffers and commented-out code; ran clang-format
  with the project style.

Register writes and radio protocol are byte-identical. Builds clean under
-Wall -Wextra on gnu17 and c23; flash 13028 -> 12830 B, static RAM
1038 -> 999 B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVJKatfeMJjAmuH9KYiLuv
2026-08-31 23:04:00 -04:00
thebears 8550e74a8a Merge pull request 'Fix wake-from-sleep, RAM overrun, and peripheral hangs in AVR firmware' (#1) from fix/avr-wake-memory-safety into main
Reviewed-on: #1
2026-08-31 22:56:13 -04:00
thebears ed476473b6 Fix wake-from-sleep, RAM overrun, and peripheral hangs in AVR firmware
Three defects prevented the board from working at all:

- INT0/INT1 were falling-edge triggered. Edge detection needs the I/O
  clock, which SLEEP_MODE_PWR_DOWN stops, so neither the reed switch nor
  the RTC alarm could wake the MCU. Both are now low-level triggered (the
  only asynchronous mode), and each handler masks its own interrupt while
  the source is still asserted so the low level cannot retrigger. The reed
  and RTC pins also get their pull-ups; they were explicitly driven low.

- Statics were 1440 B of 2048 with a 538 B main frame, so the first NFC
  read ran the stack into .data. Shrank the oversized buffers and made the
  NFC scratch buffer static: statics 1440 -> 1038 B, main frame -> 204 B.

- The FIFO was filled after entering TX mode with TxStart = FifoNotEmpty,
  so transmission began before the payload was loaded. Load in standby.

Memory safety: clamp the unvalidated RX length (len - 4 underflowed to
>=252 into a 60-byte buffer), fix writes one byte past DATA_BUFFER_65,
fix the diameter copy length in st25dv.c, NUL-terminate remove_spaces,
and bounds-check the NDEF parser (dropping its tag-sized VLA and its
unchecked payload_length decrements).

Hangs: add bail-outs to every peripheral poll loop - RFM69 mode/TX/RX
waits, the EEPROM WIP poll, all six I2C TWINT spins, and the ADC. The
LDO is cut before sleeping, so a slow peripheral hung the firmware with
no watchdog armed.

Correctness: boot no longer wipes the EEPROM spool; the replayed packet
is sent once and deleted only on success; short ATOMIC_BLOCK sections
replace the blanket cli() that lost reed pulses during the radio window;
the I2C rail comes up before the RTC is touched; sleep_bod_disable() moves
into the timed sequence with the sleep race closed; sei() no longer runs
inside ISRs; REG_FDEV_MSB was 0x06 twice so deviation was 0; ADC uses
return ADC and a /64 prescaler; SS1 is an output before SPE is set.

VAL_DATA_MODUL_OOK was misnamed rather than wrong - 0x01 lands in
ModulationShaping, not ModulationType - so the register value is
unchanged and on-air behavior still matches the base station.

Verified: builds clean under -Wall -Wextra on both gnu17 and c23.
Not yet run on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVJKatfeMJjAmuH9KYiLuv
2026-08-31 22:54:56 -04:00
28 changed files with 1290 additions and 1954 deletions
+39 -23
View File
@@ -1,6 +1,15 @@
#include "adc.h" #include "adc.h"
int8_t adc_Initialize() #define ADC_CONVERSION_TIMEOUT 1000U
// The ADC needs a 50-200 kHz clock. At F_CPU = 8 MHz that is a /64 prescaler
// (125 kHz); the old /2 ran it at 4 MHz, far out of spec.
#define ADC_PRESCALER_64 ((1 << ADPS2) | (1 << ADPS1))
// The 1.1 V bandgap reference needs time to settle after the mux is switched.
#define ADC_SETTLE_US 200
int8_t adc_Initialize(void)
{ {
// REFS VAL_0x01; ADLAR disabled; MUX adc0; // REFS VAL_0x01; ADLAR disabled; MUX adc0;
ADMUX = 0x40; ADMUX = 0x40;
@@ -8,59 +17,66 @@ int8_t adc_Initialize()
// ACME disabled; ADTS VAL_0x00; // ACME disabled; ADTS VAL_0x00;
ADCSRB = 0x00; ADCSRB = 0x00;
//ADEN enabled; ADSC disabled; ADATE disabled; ADIF disabled; ADIE disabled; ADPS VAL_0x01; ADCSRA = (1 << ADEN) | ADC_PRESCALER_64;
ADCSRA = 0x81;
return 0; return 0;
} }
void adc_Disable() // 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); ADCSRA &= ~(1 << ADEN);
power_adc_disable();
} }
void adc_Enable() void adc_Enable(void)
{ {
ADCSRA |= (1 << ADEN); power_adc_enable();
ADCSRA = (1 << ADEN) | ADC_PRESCALER_64;
} }
void adc_StartConversion(uint8_t channel) void adc_StartConversion(uint8_t channel)
{ {
if (channel == 0) if (channel == 0) {
{
ADMUX = 0b01000000; ADMUX = 0b01000000;
} } else if (channel == ADC_CHANNEL_BANDGAP) {
else if (channel == 14)
{
// ADMUX=0b01001110; // ADMUX=0b01001110;
ADMUX = (0x01 << REFS0) | (0 << ADLAR) | (0x0e << MUX0); ADMUX = (0x01 << REFS0) | (0 << ADLAR) | (0x0e << MUX0);
} } else {
else
{
ADMUX &= ~0x0f; ADMUX &= ~0x0f;
ADMUX |= channel; ADMUX |= channel;
} }
_delay_us(ADC_SETTLE_US);
ADCSRA |= (1 << ADSC); ADCSRA |= (1 << ADSC);
} }
bool adc_IsConversionDone() bool adc_IsConversionDone(void) { return ((ADCSRA & (1 << ADIF))); }
{
return ((ADCSRA & (1 << ADIF)));
}
uint16_t adc_GetConversionResult(void) uint16_t adc_GetConversionResult(void)
{ {
return (ADCL | ADCH << 8); // ADC reads ADCL then ADCH in the right order. Reading the two volatile
// registers in one expression leaves the order unspecified, and taking ADCH
// first breaks the data-register lock and corrupts the result.
return ADC;
} }
uint16_t adc_GetConversion(uint8_t channel) uint16_t adc_GetConversion(uint8_t channel)
{ {
adc_StartConversion(channel); adc_StartConversion(channel);
while (!adc_IsConversionDone());
// A conversion is 13 ADC clocks (~104 us at 125 kHz); bail out rather than
// hang if the ADC is disabled or its clock is gated off.
uint16_t attempts = 0;
while (!adc_IsConversionDone()) {
if (++attempts > ADC_CONVERSION_TIMEOUT) {
return 0;
}
_delay_us(10);
}
uint16_t res = adc_GetConversionResult(); uint16_t res = adc_GetConversionResult();
ADCSRA |= (1 << ADIF); ADCSRA |= (1 << ADIF);
return res; return res;
+10 -6
View File
@@ -7,20 +7,25 @@
#ifndef ADC_H #ifndef ADC_H
#define ADC_H #define ADC_H
#include "defines.h"
#include <avr/io.h> #include <avr/io.h>
#include <stdint.h> #include <avr/power.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
#include <util/delay.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
// Mux channel 14 is the internal 1.1 V bandgap, used to infer battery voltage.
#define ADC_CHANNEL_BANDGAP 14
int8_t adc_Initialize(); int8_t adc_Initialize(void);
void adc_Enable(); void adc_Enable(void);
void adc_Disable(); void adc_Disable(void);
void adc_StartConversion(uint8_t channel); void adc_StartConversion(uint8_t channel);
bool adc_IsConversionDone(); bool adc_IsConversionDone(void);
uint16_t adc_GetConversionResult(void); uint16_t adc_GetConversionResult(void);
uint16_t adc_GetConversion(uint8_t channel); uint16_t adc_GetConversion(uint8_t channel);
@@ -29,4 +34,3 @@ uint16_t adc_GetConversion(uint8_t channel);
#endif #endif
#endif /* ADC_H */ #endif /* ADC_H */
+1 -3
View File
@@ -1,9 +1,7 @@
#include "defines.h" #include "defines.h"
unsigned char DATA_BUFFER_65[64]; unsigned char DATA_BUFFER_65[65];
uint8_t DATA_BUFFER_7[7]; uint8_t DATA_BUFFER_7[7];
// uint8_t DATA_BUFFER_254[255];
unsigned char DATA_BUFFER_20[20];
ndef_message NDEF_MSG; ndef_message NDEF_MSG;
trimmed_string_struct TRIMMED_STRING; trimmed_string_struct TRIMMED_STRING;
identifier_results IDENTIFIER; identifier_results IDENTIFIER;
+22 -19
View File
@@ -15,26 +15,40 @@ extern "C" {
#include "stdint.h" #include "stdint.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#define F_CPU 8000000UL // 16 MHz clock speed #include <stdlib.h>
#ifndef F_CPU
#define F_CPU 8000000UL // 8 MHz clock speed; prefer -DF_CPU=8000000UL in the build flags
#endif
#define BAUD 38400 #define BAUD 38400
#define F_SCL 200000UL #define F_SCL 200000UL
// Build switches: DO_UART compiles in serial logging, ITERATING is a bench
// mode that reports every minute instead of every 15.
#define DO_UART true #define DO_UART true
#define ITERATING false #define ITERATING false
// 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_P(PSTR(msg))
#else
#define LOG(msg) ((void)0)
#endif
#define MIN(a, b) (((a) < (b)) ? (a) : (b)) #define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b)) #define MAX(a, b) (((a) > (b)) ? (a) : (b))
extern unsigned char DATA_BUFFER_65[64]; // Shared scratch buffers (RAM is tight: 2 KB total). DATA_BUFFER_65 holds one
// EEPROM page plus a terminator; DATA_BUFFER_7 holds one RTC time readout.
extern unsigned char DATA_BUFFER_65[65];
extern uint8_t DATA_BUFFER_7[7]; extern uint8_t DATA_BUFFER_7[7];
//extern uint8_t DATA_BUFFER_254[255];
extern unsigned char DATA_BUFFER_20[20];
typedef struct { typedef struct {
uint8_t payload_len; uint8_t payload_len;
char payload[255]; char payload[48];
uint8_t success; uint8_t success;
} ndef_message; } ndef_message;
extern ndef_message NDEF_MSG; extern ndef_message NDEF_MSG;
@@ -47,14 +61,13 @@ extern trimmed_string_struct TRIMMED_STRING;
typedef struct { typedef struct {
uint8_t name_len; uint8_t name_len;
char name_str[128]; char name_str[16];
char diameter_str[128]; char diameter_str[16];
uint8_t diameter_len; uint8_t diameter_len;
uint8_t hashed; uint8_t hashed;
} identifier_results; } identifier_results;
extern identifier_results IDENTIFIER; extern identifier_results IDENTIFIER;
typedef struct { typedef struct {
uint8_t len; uint8_t len;
uint8_t to; uint8_t to;
@@ -77,18 +90,15 @@ typedef struct {
} time_struct; } time_struct;
extern time_struct TIME; extern time_struct TIME;
typedef enum { typedef enum {
RTC_RFM69_SET_TIME_SUCCESS = 1, RTC_RFM69_SET_TIME_SUCCESS = 1,
RTC_RFM69_SET_TIME_FAILED = 2, RTC_RFM69_SET_TIME_FAILED = 2,
} RTC_RFM69_STATUS; } RTC_RFM69_STATUS;
typedef enum // Goes into ID typedef enum // Goes into ID
{ DATA_SEND_SUCCESS = 1, { DATA_SEND_SUCCESS = 1,
DATA_NOT_SENT = 2 } DATA_SEND_STATUS; DATA_NOT_SENT = 2 } DATA_SEND_STATUS;
typedef enum // Goes into ID typedef enum // Goes into ID
{ MSG_TYPE_STRING = 1, { MSG_TYPE_STRING = 1,
MSG_TYPE_BINARY = 2 } MSG_DATA_TYPE; MSG_TYPE_BINARY = 2 } MSG_DATA_TYPE;
@@ -101,15 +111,8 @@ typedef enum // Goes into flags
MSG_RECV_COUNTS_FAIL = 33, MSG_RECV_COUNTS_FAIL = 33,
MSG_RESENT_COUNTS = 34 } MSG_REQUEST_TYPE_FLAG; MSG_RESENT_COUNTS = 34 } MSG_REQUEST_TYPE_FLAG;
#define WHILE_BREAK(counter, attempts) \
counter+=1; \
if ((counter+1) > attempts) { break;};
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* DEFINES_H */ #endif /* DEFINES_H */
+42 -18
View File
@@ -1,7 +1,22 @@
// Blocking TWI (I2C) master for the RTC and the NFC tag.
#include "i2c.h" #include "i2c.h"
#include <util/twi.h> #include <util/twi.h>
void i2c_init() // Every one of these loops used to spin forever. The peripheral rail is cut
// before sleeping, so a device that is slow or absent on wake would otherwise
// hang the firmware with no watchdog reset armed.
static bool i2c_wait_twint(void)
{
for (uint16_t attempts = 0; attempts < I2C_TIMEOUT_LOOPS; attempts++) {
if (TWCR & (1 << TWINT)) {
return true;
}
}
return false;
}
void i2c_init(void)
{ {
// Set SCL and SDA as inputs (automatically done by TWI hardware) // Set SCL and SDA as inputs (automatically done by TWI hardware)
TWSR = 0; // Prescaler = 1 TWSR = 0; // Prescaler = 1
@@ -13,13 +28,13 @@ void i2c_init()
uint8_t i2c_start(uint8_t address) uint8_t i2c_start(uint8_t address)
{ {
TWCR = (1 << TWSTA) | (1 << TWINT) | (1 << TWEN); // Send START condition TWCR = (1 << TWSTA) | (1 << TWINT) | (1 << TWEN); // Send START condition
while (!(TWCR & (1 << TWINT))) if (!i2c_wait_twint())
; // Wait for TWINT flag to be set return 1;
TWDR = address; // Load address into data register TWDR = address; // Load address into data register
TWCR = (1 << TWINT) | (1 << TWEN); // Send address TWCR = (1 << TWINT) | (1 << TWEN); // Send address
while (!(TWCR & (1 << TWINT))) if (!i2c_wait_twint())
; // Wait for TWINT flag to be set return 1;
uint8_t status = TWSR & 0xF8; uint8_t status = TWSR & 0xF8;
if (status != 0x18 && status != 0x40) if (status != 0x18 && status != 0x40)
return 1; return 1;
@@ -36,9 +51,15 @@ uint8_t write_one_byte(uint8_t device_addr, uint8_t register_addr, uint8_t data)
uint8_t write_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t* data, uint8_t n_bytes) uint8_t write_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t* data, uint8_t n_bytes)
{ {
I2C_START_WRITE(device_addr); I2C_START_WRITE(device_addr);
i2c_write(register_addr); if (i2c_write(register_addr)) {
i2c_stop();
return 1;
}
for (uint8_t i = 0; i < n_bytes; i++) { for (uint8_t i = 0; i < n_bytes; i++) {
i2c_write(data[i]); if (i2c_write(data[i])) {
i2c_stop();
return 1;
}
} }
i2c_stop(); i2c_stop();
return 0; return 0;
@@ -106,18 +127,21 @@ read_n_bytes_16bit_addr(uint8_t device_addr, uint16_t register_addr, uint8_t* da
} }
// Stop i2c communication // Stop i2c communication
void i2c_stop() void i2c_stop(void)
{ {
TWCR = (1 << TWSTO) | (1 << TWINT) | (1 << TWEN); // Send STOP condition TWCR = (1 << TWSTO) | (1 << TWINT) | (1 << TWEN); // Send STOP condition
while (!(TWCR & (1 << TWSTO))) for (uint16_t attempts = 0; attempts < I2C_TIMEOUT_LOOPS; attempts++) {
; // Wait for STOP to complete if (!(TWCR & (1 << TWSTO))) {
return; // STOP complete
}
}
} }
uint8_t i2c_read_ack() uint8_t i2c_read_ack(void)
{ {
TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA); TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
while (!(TWCR & (1 << TWINT))) if (!i2c_wait_twint())
; // Wait for TWINT flag to be set return 0xFF;
return TWDR; return TWDR;
} }
@@ -126,17 +150,17 @@ uint8_t i2c_write(uint8_t data)
// Load data into TWDR // Load data into TWDR
TWDR = data; TWDR = data;
TWCR = (1 << TWEN) | (1 << TWINT); TWCR = (1 << TWEN) | (1 << TWINT);
while (!(TWCR & (1 << TWINT))) if (!i2c_wait_twint())
; // Wait for TWINT flag set return 1;
if ((TWSR & 0xF8) != TW_MT_DATA_ACK) if ((TWSR & 0xF8) != TW_MT_DATA_ACK)
return 1; // Check ACK return 1; // Check ACK
return 0; return 0;
} }
uint8_t i2c_read_nack() uint8_t i2c_read_nack(void)
{ {
TWCR = (1 << TWEN) | (1 << TWINT); TWCR = (1 << TWEN) | (1 << TWINT);
while (!(TWCR & (1 << TWINT))) if (!i2c_wait_twint())
; // Wait for TWINT flag to be set return 0xFF;
return TWDR; return TWDR;
} }
+22 -25
View File
@@ -2,6 +2,14 @@
#include "uart.h" #include "uart.h"
#include <avr/io.h> #include <avr/io.h>
#ifndef i2c_H
#define i2c_H
#define TWSR TWSR0
#define TWDR TWDR0
#define TWBR TWBR0
#define TWCR TWCR0
#define I2C_START_WRITE(device_addr) \ #define I2C_START_WRITE(device_addr) \
{ \ { \
if (i2c_start((device_addr << 1) | 0x00)) { \ if (i2c_start((device_addr << 1) | 0x00)) { \
@@ -16,39 +24,28 @@
} \ } \
} }
#ifndef i2c_H // A byte at F_SCL takes well under 100 us; anything past this means the bus is
#define i2c_H // stuck (peripheral unpowered, SDA held low) and we must not spin forever.
#define I2C_TIMEOUT_LOOPS 20000U
#define TWSR TWSR0 void i2c_init(void);
#define TWDR TWDR0
#define TWBR TWBR0
#define TWCR TWCR0
void i2c_init();
uint8_t i2c_start(uint8_t address); uint8_t i2c_start(uint8_t address);
uint8_t write_one_byte(uint8_t device_addr, uint8_t register_addr, uint8_t write_one_byte(uint8_t device_addr, uint8_t register_addr, uint8_t data);
uint8_t data); uint8_t write_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t* data, uint8_t n_bytes);
uint8_t write_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t *data,
uint8_t n_bytes);
uint8_t read_one_byte_16bit_addr_no_err_register(uint8_t device_addr, uint16_t register_addr); uint8_t read_one_byte_16bit_addr_no_err_register(uint8_t device_addr, uint16_t register_addr);
uint8_t read_one_byte_16bit_addr(uint8_t device_addr, uint16_t register_addr, uint8_t read_one_byte_16bit_addr(uint8_t device_addr, uint16_t register_addr, uint8_t* data);
uint8_t *data); uint8_t read_n_bytes_16bit_addr(
uint8_t read_n_bytes_16bit_addr(uint8_t device_addr, uint16_t register_addr, uint8_t *data, uint8_t device_addr, uint16_t register_addr, uint8_t* data, uint8_t n_bytes);
uint8_t n_bytes);
uint8_t read_one_byte_no_err_register(uint8_t device_addr, uint8_t register_addr); uint8_t read_one_byte_no_err_register(uint8_t device_addr, uint8_t register_addr);
uint8_t read_one_byte(uint8_t device_addr, uint8_t register_addr, uint8_t read_one_byte(uint8_t device_addr, uint8_t register_addr, uint8_t* data);
uint8_t *data); uint8_t read_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t* data, uint8_t n_bytes);
uint8_t read_n_bytes(uint8_t device_addr, uint8_t register_addr, uint8_t *data,
uint8_t n_bytes);
void i2c_stop(); void i2c_stop(void);
void i2c_scan(); uint8_t i2c_read_ack(void);
uint8_t i2c_read_ack(); uint8_t i2c_read_nack(void);
uint8_t i2c_read_nack();
uint8_t i2c_write(uint8_t data); uint8_t i2c_write(uint8_t data);
#endif #endif
+54 -29
View File
@@ -1,54 +1,79 @@
#include "interrupts.h" #include "interrupts.h"
void init_pins() { void init_pins(void)
{
// Set reed switch interrupt pin // The reed switch (PD3/INT1) switches to ground and the RTC alarm output
// (PD2/INT0) is open-drain, so both need the internal pull-up. Leaving them
// floating makes the inputs self-trigger.
SET_PIN_IN(DDRD, DDD3); SET_PIN_IN(DDRD, DDD3);
SET_PIN_LOW(PORTD, PD3); SET_PIN_HIGH(PORTD, PD3);
SET_PIN_IN(DDRD, DDD2);
SET_PIN_HIGH(PORTD, PD2);
} }
void set_up_reed_interrupt() { // Both external interrupts are configured low-level triggered (ISCn1:0 = 00).
// Falling edge interrupt // Edge detection needs the I/O clock, which SLEEP_MODE_PWR_DOWN stops, so a
EICRA |= (1 << ISC11); // falling-edge INT0/INT1 can never wake the MCU. Only level detection is
EICRA &= ~(1 << ISC10); // asynchronous. Each handler masks its own interrupt while the source is still
// asserted, so the low level does not retrigger in a loop.
// Enable INT1 interrupt void set_up_reed_interrupt(void)
{
EICRA &= ~((1 << ISC11) | (1 << ISC10));
EIMSK |= (1 << INT1); EIMSK |= (1 << INT1);
} }
void set_up_minute_interrupt() { void set_up_minute_interrupt(void)
EICRA |= (1 << ISC01); {
EICRA &= ~(1 << ISC00); EICRA &= ~((1 << ISC01) | (1 << ISC00));
// Enable INT1 interrupt
EIMSK |= (1 << INT0); EIMSK |= (1 << INT0);
} }
void wdt_isr_enable() { void reed_interrupt_enable(void)
{
EIFR = (1 << INTF1); // Drop anything latched while we were masked
EIMSK |= (1 << INT1);
}
void minute_interrupt_enable(void)
{
EIFR = (1 << INTF0);
EIMSK |= (1 << INT0);
}
void wdt_isr_enable(void)
{
uint8_t sreg = SREG;
cli(); cli();
wdt_reset(); wdt_reset();
WDTCSR |= (1 << WDCE) | (1 << WDE); // WDRF keeps WDE set, which would block the write below, so it must go
// first. The unlock is a single assignment: a read-modify-write does not
// open the 4-cycle change window.
MCUSR &= ~(1 << WDRF);
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = (1 << WDP2) | (1 << WDP0); // WDP[3:0] = 0b101 (2 seconds) // WDP[3:0] = 0b010 -> 64 ms debounce, capping the count at ~15 rev/s;
WDTCSR |= (1 << WDIE); // Enable WDT Interrupt mode // a reed contact settles in a few ms, so this is generous. Interrupt mode
sei(); // 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
} }
void wdt_isr_disable() { void wdt_isr_disable(void)
{
uint8_t sreg = SREG;
cli(); cli();
wdt_reset();
WDTCSR |= (1 << WDCE) | (1 << WDE); MCUSR &= ~(1 << WDRF);
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = 0x00; WDTCSR = 0x00;
sei(); SREG = sreg;
} }
//
//void set_debounce_timer_interrupt() {
// TCCR0A = 0;
// TCCR0B = (1 << CS01) | (1 << CS00);
// TIMSK0 = (1 << TOIE0);
// TCNT0 = 0;
//}
+8 -10
View File
@@ -5,8 +5,8 @@
* Created on December 18, 2024, 12:48 PM * Created on December 18, 2024, 12:48 PM
*/ */
#include <avr/io.h>
#include <avr/interrupt.h> #include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/sleep.h> #include <avr/sleep.h>
#include <avr/wdt.h> #include <avr/wdt.h>
@@ -18,18 +18,16 @@
extern "C" { extern "C" {
#endif #endif
void init_pins(void);
void set_up_reed_interrupt(void);
void init_pins(); void set_up_minute_interrupt(void);
void set_up_reed_interrupt(); void reed_interrupt_enable(void);
// void set_debounce_timer_interrupt(); void minute_interrupt_enable(void);
void set_up_minute_interrupt(); void wdt_isr_disable(void);
void wdt_isr_disable(); void wdt_isr_enable(void);
void wdt_isr_enable();
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* INTERRUPTS_H */ #endif /* INTERRUPTS_H */
+29 -18
View File
@@ -1,16 +1,21 @@
// M95128 SPI EEPROM used as a LIFO spool for unacknowledged radio packets.
//
// Page 0, byte 0: depth of the spool (the highest page currently in use;
// 0 means empty)
// Pages 1..N: one 64-byte packet each -- 60 msg bytes followed by
// flags, from, to, dtype
//
// write_struct_to_last_page() pushes, read_struct_last_page() peeks, and
// delete_last_page() pops.
#include "m95128.h" #include "m95128.h"
uint8_t read_value; uint8_t read_value;
uint8_t old_last_page; // EEPROM chip select (PB0, active low)
uint8_t new_last_page;
void spi_eeprom_select(bool state) void spi_eeprom_select(bool state)
{ {
SET_PIN_OUT(DDRB, DDB0); SET_PIN_OUT(DDRB, DDB0);
if (!state) { SET_PIN_TO(PORTB, PB0, !state);
SET_PIN_HIGH(PORTB, PB0);
} else {
SET_PIN_LOW(PORTB, PB0);
}
} }
void eeprom_write(uint8_t page, unsigned const char* msg, uint8_t msg_len) void eeprom_write(uint8_t page, unsigned const char* msg, uint8_t msg_len)
@@ -35,15 +40,18 @@ void eeprom_write(uint8_t page, unsigned const char* msg, uint8_t msg_len)
spi_write(EEPROM_WRDI); spi_write(EEPROM_WRDI);
spi_eeprom_select(false); spi_eeprom_select(false);
while (1) { // Poll the write-in-progress bit, but give up rather than spin forever if
// the EEPROM is unpowered or absent.
for (uint16_t attempts = 0; attempts < EEPROM_POLL_TIMEOUT_MS; attempts++) {
spi_eeprom_select(true); spi_eeprom_select(true);
spi_write(EEPROM_RDSR); spi_write(EEPROM_RDSR);
read_value = spi_read(); read_value = spi_read();
spi_eeprom_select(false); spi_eeprom_select(false);
if (read_value == 0x00) { if ((read_value & EEPROM_STATUS_WIP) == 0) {
return; return;
} }
}; _delay_ms(1);
}
} }
void eeprom_read(uint8_t page, unsigned char* msg, uint8_t msg_len) void eeprom_read(uint8_t page, unsigned char* msg, uint8_t msg_len)
@@ -63,19 +71,18 @@ void eeprom_read(uint8_t page, unsigned char* msg, uint8_t msg_len)
spi_eeprom_select(false); spi_eeprom_select(false);
} }
void delete_last_page() void delete_last_page(void)
{ {
old_last_page = get_last_page(); uint8_t old_last_page = get_last_page();
if (old_last_page == 0) // If we have nothing, no need to delete anything if (old_last_page == 0) // If we have nothing, no need to delete anything
{ {
return; return;
} }
new_last_page = old_last_page - 1; write_last_page_value(old_last_page - 1);
write_last_page_value(new_last_page);
eeprom_clear_page(old_last_page); eeprom_clear_page(old_last_page);
} }
uint8_t get_last_page() uint8_t get_last_page(void)
{ {
memset(DATA_BUFFER_65, 0, 1); memset(DATA_BUFFER_65, 0, 1);
eeprom_read(0, DATA_BUFFER_65, 1); eeprom_read(0, DATA_BUFFER_65, 1);
@@ -127,7 +134,7 @@ tx_rx_data_struct eeprom_read_tx_data(uint8_t page)
return TX_DATA; return TX_DATA;
} }
tx_rx_data_struct read_struct_last_page() tx_rx_data_struct read_struct_last_page(void)
{ {
uint8_t page_num = get_last_page(); uint8_t page_num = get_last_page();
return eeprom_read_tx_data(page_num); return eeprom_read_tx_data(page_num);
@@ -137,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 page_val = get_last_page();
uint8_t next_page_val; uint8_t next_page_val;
if (page_val == 255) { if (page_val >= EEPROM_MAX_PAGE) {
next_page_val = 1; // 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 { } else {
next_page_val = page_val + 1; next_page_val = page_val + 1;
} }
+12 -7
View File
@@ -26,27 +26,32 @@
#define EEPROM_RDLS 0b10000011 // 0x83 #define EEPROM_RDLS 0b10000011 // 0x83
#define EEPROM_LID 0b10000010 // 0x82 #define EEPROM_LID 0b10000010 // 0x82
#define PAGE_SIZE 64 #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.
#define EEPROM_POLL_TIMEOUT_MS 100U
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
w
#endif #endif
void void spi_eeprom_select(bool state);
spi_eeprom_select(bool state);
void eeprom_write(uint8_t page, unsigned const char* msg, uint8_t msg_len); void eeprom_write(uint8_t page, unsigned const char* msg, uint8_t msg_len);
void eeprom_read(uint8_t page, unsigned char* msg, uint8_t msg_len); void eeprom_read(uint8_t page, unsigned char* msg, uint8_t msg_len);
void eeprom_write_tx_data(uint8_t page, tx_rx_data_struct tx_data); void eeprom_write_tx_data(uint8_t page, tx_rx_data_struct tx_data);
void write_page_address(uint8_t page); void write_page_address(uint8_t page);
uint8_t get_last_page(); uint8_t get_last_page(void);
tx_rx_data_struct eeprom_read_tx_data(uint8_t page); tx_rx_data_struct eeprom_read_tx_data(uint8_t page);
void delete_last_page(); void delete_last_page(void);
uint8_t get_last_page(); uint8_t get_last_page(void);
void write_last_page_value(uint8_t page); void write_last_page_value(uint8_t page);
void eeprom_clear_page(uint8_t page); void eeprom_clear_page(uint8_t page);
void write_page_address(uint8_t page); void write_page_address(uint8_t page);
void eeprom_write_tx_data(uint8_t page, tx_rx_data_struct tx_data); void eeprom_write_tx_data(uint8_t page, tx_rx_data_struct tx_data);
void write_struct_to_last_page(tx_rx_data_struct tx_data); void write_struct_to_last_page(tx_rx_data_struct tx_data);
tx_rx_data_struct read_struct_last_page(); tx_rx_data_struct read_struct_last_page(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+343 -204
View File
@@ -1,3 +1,24 @@
// 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 "adc.h"
#include "defines.h" #include "defines.h"
#include "interrupts.h" #include "interrupts.h"
@@ -8,275 +29,393 @@
#include "rfm69.h" #include "rfm69.h"
#include "st25dv.h" #include "st25dv.h"
#include "states.h" #include "states.h"
#if DO_UART
#include "uart.h" #include "uart.h"
#endif
#include <avr/interrupt.h> #include <avr/interrupt.h>
#include <avr/io.h> #include <avr/io.h>
#include <avr/power.h>
#include <avr/sleep.h> #include <avr/sleep.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <util/atomic.h>
#include <util/delay.h> #include <util/delay.h>
#define WAIT_FOREVER \
while (1) { \
_delay_ms(100); \
};
// 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 #if ITERATING
#define SEND_INTERVAL 1 #define SEND_INTERVAL 1
#else #else
#define SEND_INTERVAL 15 #define SEND_INTERVAL 15
#endif #endif
uint16_t self_value;
tx_rx_data_struct CRAP;
uint16_t main_counter;
volatile uint8_t is_debouncing = 0;
volatile bool increment_minute_index = false;
volatile bool increment_wheel_count = false;
volatile uint8_t index_wheel_count = 0;
volatile uint16_t total_wheel_counts[15];
RTC_RFM69_STATUS rtc_rfm69_status; #define WHEEL_COUNT_SLOTS 15
ISR(INT0_vect) { // Erased EEPROM reads back as 0xFF; anything else is a real spool depth.
cli(); #define EEPROM_LAST_PAGE_UNINIT 0xFF
#if DO_UART
uart_sendString("\t\t\t\tMINUTE INTERRUPT\n"); // The NFC identity is cached; re-read the tag every 4th send (~1 h), so a
#endif // renamed nugget still takes effect without a reset.
increment_minute_index = true; #define TAG_REREAD_SEND_CYCLES 4
// sei();
// 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");
} }
ISR(INT1_vect) { // Reed switch: one revolution. The magnet holds the reed closed far longer
cli(); // than one bounce, so mask INT1 for a WDT-timed debounce window; the WDT
// handler below re-arms it.
ISR(INT1_vect)
{
#if ITERATING #if ITERATING
increment_minute_index = true; minute_alarm_fired = true;
#endif #endif
LOG("\t\t\t\tREED INTERRUPT\n");
#if DO_UART if (!reed_is_debouncing) {
uart_sendString("\t\t\t\tREED INTERRUPT\n"); if (minute_slot < WHEEL_COUNT_SLOTS) {
#endif wheel_counts[minute_slot]++;
}
reed_is_debouncing = true;
if (!is_debouncing) { EIMSK &= ~(1 << INT1);
total_wheel_counts[index_wheel_count]++;
is_debouncing = 1;
wdt_isr_enable(); wdt_isr_enable();
} }
// sei();
} }
ISR(WDT_vect) { // Debounce window over: allow the next reed pulse to count.
cli(); ISR(WDT_vect)
is_debouncing = 0; {
reed_is_debouncing = false;
wdt_isr_disable(); wdt_isr_disable();
// sei(); reed_interrupt_enable();
} }
void start_sleeping() { // ---------------------------------------------------------------------------
// 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_eeprom_select(false);
spi_rfm69_select(false); spi_rfm69_select(false);
rfid_set_low_power_down(true); rfid_set_low_power_down(true);
rfid_set_i2c_power(false); rfid_set_i2c_power(false);
ldo_set_state(false); ldo_set_state(false);
_delay_ms(10); _delay_ms(10);
sleep_bod_disable();
set_sleep_mode(SLEEP_MODE_PWR_DOWN); 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_enable();
sleep_bod_disable();
sei(); sei();
sleep_cpu(); sleep_cpu();
sleep_disable();
}
sei();
} }
uint16_t get_battery_reading() { // Restore the supplies that sleep_until_interrupt() dropped. Everything on the
adc_Enable(); // I2C bus is dead until this runs.
adc_GetConversion(14); static void wake_peripheral_rails(void)
adc_GetConversion(14); {
self_value = adc_GetConversion(14);
adc_Disable();
return self_value;
}
// i2c Addresses
// RTC
// 0x68 (0xD0 W) (0xD1 R)
// NFC
// 0x2D (0x5A W) (0x5B R)
// 0x53 (0xA6 W) (0xA7 R)
// 0x57 (0xAE W) (0xAF R)
int main(void) {
ldo_set_state(true); ldo_set_state(true);
_delay_ms(10);
init_pins();
#if DO_UART
uart_init();
uart_sendString("---- STARTING ----\n");
uart_wait_until_sent();
#endif
i2c_init();
init_spi();
adc_Initialize();
set_up_reed_interrupt();
set_up_minute_interrupt();
#if DO_UART
uart_sendString("Set up AVR interrupts\n");
#endif
rtc_set_per_minute_alarm();
rtc_set_alarm_config();
rtc_enable_interrupts();
rtc_read_interrupt_register();
rtc_read_status_register();
#if DO_UART
uart_sendString("Set up RTC interrupts\n");
#endif
rfm69_init();
#if DO_UART
uart_sendString("Initialized RFM69\n");
#endif
write_last_page_value(0);
#if DO_UART
uart_sendString("Set up last page value for SPI flash\n");
#endif
for (uint8_t c = 0; c < 15; c++) {
total_wheel_counts[c] = 0;
}
// Get the nugget's name and wheel diameter
IDENTIFIER = get_nugget_data();
#if DO_UART
uart_sendString("Got nugget data from RFID\n");
#endif
// Request time from radio
rtc_rfm69_status = set_time_from_rfm69(IDENTIFIER);
if (rtc_rfm69_status == RTC_RFM69_SET_TIME_SUCCESS) {
for (int i = 0; i < 5; i++) {
led_1_set_state(true);
_delay_ms(90);
led_1_set_state(false);
_delay_ms(10);
}
} else {
for (int i = 0; i < 5; i++) {
led_1_set_state(true);
_delay_ms(10);
led_1_set_state(false);
_delay_ms(90);
}
}
#if DO_UART
if (rtc_rfm69_status == RTC_RFM69_SET_TIME_FAILED) {
uart_sendString("Failed to get time \n");
} else {
uart_sendString("Success in get time \n");
};
#endif
get_battery_reading();
while (1) {
spi_rfm69_select(false);
spi_eeprom_select(false);
start_sleeping();
cli();
if (increment_minute_index) {
#if DO_UART
uart_sendString("In minute index\n");
#endif
increment_minute_index = false;
is_debouncing = 0;
index_wheel_count += 1;
rtc_read_interrupt_register();
rtc_read_status_register();
rtc_read_interrupt_register();
rtc_read_status_register();
if (index_wheel_count >= SEND_INTERVAL) {
index_wheel_count = 0;
ldo_set_state(true);
rfm69_init();
rfid_set_i2c_power(true); rfid_set_i2c_power(true);
rfid_set_low_power_down(false);
_delay_ms(1); _delay_ms(1);
IDENTIFIER = get_nugget_data();
// Request time from radio if we don't have a good time stamp yet
if (rtc_rfm69_status == RTC_RFM69_SET_TIME_FAILED) {
rtc_rfm69_status = set_time_from_rfm69(IDENTIFIER);
} }
// Generate wheel counts message // ---------------------------------------------------------------------------
reset_txrx_struct(&TX_DATA); // Measurement helpers
get_battery_reading(); // ---------------------------------------------------------------------------
TX_DATA = generate_wheel_counts_message(
IDENTIFIER, rtc_read_time(), get_battery_reading(), total_wheel_counts);
// 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 #if DO_UART
uart_sendString("TX DATA Sent\n");
uart_print_tx_rx_data(TX_DATA); uart_print_tx_rx_data(TX_DATA);
#endif #endif
tx_backoff_delay();
DATA_SEND_STATUS result = send_message(TX_DATA); DATA_SEND_STATUS result = send_message(TX_DATA);
if (result == DATA_NOT_SENT) { if (result == DATA_NOT_SENT) {
#if DO_UART LOG(" TX DATA not sent, writing to SPI\n");
uart_sendString(" TX DATA not sent, writing to SPI\n");
#endif
write_struct_to_last_page(TX_DATA); write_struct_to_last_page(TX_DATA);
return;
} }
for (uint8_t c = 0; c < 15; c++) { // The base station is listening -- drain the spool while sends keep
total_wheel_counts[c] = 0; // succeeding, capped at SPOOL_DRAIN_MAX per cycle. At one per cycle a long
} // outage took days to catch up.
if ((result == DATA_SEND_SUCCESS) && (get_last_page() > 0)) { for (uint8_t drained = 0; (drained < SPOOL_DRAIN_MAX) && (get_last_page() > 0); drained++) {
reset_txrx_struct(&TX_DATA); reset_txrx_struct(&TX_DATA);
TX_DATA = read_struct_last_page(); TX_DATA = read_struct_last_page();
TX_DATA.flags = MSG_RESENT_COUNTS; TX_DATA.flags = MSG_RESENT_COUNTS;
_delay_ms(250); _delay_ms(250);
result = send_message(TX_DATA);
#if DO_UART
if (result == DATA_NOT_SENT) {
uart_sendString(" SPI not sent\n");
}
#endif
send_message(TX_DATA); tx_backoff_delay();
delete_last_page(); result = send_message(TX_DATA);
LOG("TX DATA From SPI Memory\n");
#if DO_UART #if DO_UART
uart_sendString("TX DATA From SPI Memory\n");
uart_print_tx_rx_data(TX_DATA); uart_print_tx_rx_data(TX_DATA);
#endif #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();
}
} }
} }
+52 -34
View File
@@ -1,5 +1,6 @@
// MAX31329 RTC driver, plus the over-the-radio time sync that seeds it.
#include "max31329.h" #include "max31329.h"
bool result;
RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data) RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data)
{ {
@@ -14,13 +15,13 @@ RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data)
TX_DATA.dtype = MSG_TYPE_STRING; TX_DATA.dtype = MSG_TYPE_STRING;
rfm69_write_msg(TX_DATA); rfm69_write_msg(TX_DATA);
result = wait_rx_payload_ready_timeout(100);
if (result) { if (wait_rx_payload_ready_timeout(100)) {
RX_DATA = rfm69_read_msg(); RX_DATA = rfm69_read_msg();
if (RX_DATA.flags == 2) { if (RX_DATA.flags == MSG_RESP_CTIME) {
// TIME.Second = rx_data.msg[0]; // Seed the seconds from the name hash instead of the reply so
// nodes don't all wake and transmit in the same instant.
TIME.Second = id_data.hashed; TIME.Second = id_data.hashed;
TIME.Minute = RX_DATA.msg[1]; TIME.Minute = RX_DATA.msg[1];
@@ -30,51 +31,67 @@ RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data)
TIME.Year = RX_DATA.msg[5]; TIME.Year = RX_DATA.msg[5];
TIME.Wday = RX_DATA.msg[6]; TIME.Wday = RX_DATA.msg[6];
rtc_write_time(TIME); // 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; return RTC_RFM69_SET_TIME_SUCCESS;
} }
} else { LOG("RTC write failed\n");
// uart_sendString("Did not get RX\n"); }
} }
return RTC_RFM69_SET_TIME_FAILED; return RTC_RFM69_SET_TIME_FAILED;
} }
uint8_t rtc_set_per_minute_alarm() // Masking out minutes, hours, and day makes alarm 2 match once every minute
uint8_t rtc_set_per_minute_alarm(void)
{ {
DATA_BUFFER_7[0] = RTC_ALM_MASK_BIT;
DATA_BUFFER_7[1] = RTC_ALM_MASK_BIT;
DATA_BUFFER_7[2] = RTC_ALM_MASK_BIT;
DATA_BUFFER_7[0] = 0x80; return write_n_bytes(I2C_ADDR, RTC_REG_ALM2_MIN, DATA_BUFFER_7, 3);
DATA_BUFFER_7[1] = 0x80;
DATA_BUFFER_7[2] = 0x80;
return write_n_bytes(I2C_ADDR, 0x13, DATA_BUFFER_7, 3);
} }
void uart_print_rtc_time(time_struct td) void uart_print_rtc_time(time_struct td)
{ {
char str_rtc[26]; char str_rtc[26];
snprintf( snprintf(
str_rtc, sizeof(str_rtc), "%u/%02u/%02u %u:%02u:%02u", 2000 + td.Year, str_rtc, sizeof(str_rtc), "%u/%02u/%02u %u:%02u:%02u", 2000 + td.Year, td.Month, td.Day,
td.Month, td.Day, td.Hour, td.Minute, td.Second); td.Hour, td.Minute, td.Second);
uart_sendString(str_rtc); uart_sendString(str_rtc);
uart_sendString("\n"); uart_sendString("\n");
} }
uint8_t rtc_read_register(uint8_t addr) { return read_one_byte_no_err_register(I2C_ADDR, addr); } uint8_t rtc_read_register(uint8_t addr) { return read_one_byte_no_err_register(I2C_ADDR, addr); }
uint8_t rtc_read_status_register() { return rtc_read_register(0x00); } uint8_t rtc_read_status_register(void) { return rtc_read_register(RTC_REG_STATUS); }
uint8_t rtc_read_interrupt_register() { return rtc_read_register(0x01); } uint8_t rtc_read_interrupt_register(void) { return rtc_read_register(RTC_REG_INT_EN); }
uint8_t rtc_set_alarm_config() { return write_one_byte(I2C_ADDR, 0x04, 0b00001010); } uint8_t rtc_set_alarm_config(void) { return write_one_byte(I2C_ADDR, RTC_REG_CONFIG2, 0b00001010); }
uint8_t rtc_enable_interrupts() { return write_one_byte(I2C_ADDR, 0x01, 0b00000010); } uint8_t rtc_enable_interrupts(void)
uint8_t rtc_read_time_array(uint8_t* data) { return read_n_bytes(I2C_ADDR, 0x06, data, 7); }
time_struct rtc_read_time()
{ {
return write_one_byte(I2C_ADDR, RTC_REG_INT_EN, RTC_INT_EN_A2IE);
}
uint8_t rtc_read_time_array(uint8_t* data)
{
return read_n_bytes(I2C_ADDR, RTC_REG_SECONDS, data, 7);
}
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.Second = BCD2DEC(DATA_BUFFER_7[0]);
TIME.Minute = BCD2DEC(DATA_BUFFER_7[1]); TIME.Minute = BCD2DEC(DATA_BUFFER_7[1]);
TIME.Hour = BCD2DEC((DATA_BUFFER_7[2] & ~(1 << 6))); TIME.Hour = BCD2DEC((DATA_BUFFER_7[2] & ~(1 << 6)));
@@ -94,15 +111,16 @@ uint8_t rtc_write_time(time_struct tm)
if (i2c_start((I2C_ADDR << 1) | 0x00)) if (i2c_start((I2C_ADDR << 1) | 0x00))
return 1; return 1;
i2c_write(0x06); uint8_t err = 0;
i2c_write(DEC2BCD(tm.Second)); err |= i2c_write(RTC_REG_SECONDS);
i2c_write(DEC2BCD(tm.Minute)); err |= i2c_write(DEC2BCD(tm.Second));
i2c_write(DEC2BCD(tm.Hour)); err |= i2c_write(DEC2BCD(tm.Minute));
i2c_write(tm.Wday); err |= i2c_write(DEC2BCD(tm.Hour));
i2c_write(DEC2BCD(tm.Day)); err |= i2c_write(tm.Wday);
i2c_write(DEC2BCD(tm.Month)); err |= i2c_write(DEC2BCD(tm.Day));
i2c_write(DEC2BCD(y2kYearToTm(tm.Year))); err |= i2c_write(DEC2BCD(tm.Month));
err |= i2c_write(DEC2BCD(y2kYearToTm(tm.Year)));
i2c_stop(); i2c_stop();
return 0; return err ? 1 : 0;
} }
+25 -11
View File
@@ -10,7 +10,6 @@
#include "rfm69.h" #include "rfm69.h"
#include "st25dv.h" #include "st25dv.h"
#include "uart.h" #include "uart.h"
#define I2C_ADDR 0x68
#ifndef MAX31329_H #ifndef MAX31329_H
#define MAX31329_H #define MAX31329_H
@@ -19,20 +18,35 @@
extern "C" { extern "C" {
#endif #endif
#define DEC2BCD(n) (n + (6 * (n / 10))) #define I2C_ADDR 0x68
#define BCD2DEC(n) (n - (6 * (n >> 4)))
#define tmYearToY2k(Y) ((Y) - 30) // offset is from 2000 // MAX31329 register map (the subset this driver touches)
#define y2kYearToTm(Y) ((Y) + 30) #define RTC_REG_STATUS 0x00 // Reading clears the alarm flags / releases INTB
#define RTC_REG_INT_EN 0x01
#define RTC_REG_CONFIG2 0x04
#define RTC_REG_SECONDS 0x06 // Start of the 7-byte BCD time block
#define RTC_REG_ALM2_MIN 0x13 // Start of the 3-byte alarm-2 block
uint8_t rtc_enable_interrupts(); #define RTC_INT_EN_A2IE 0b00000010 // Alarm-2 interrupt enable
uint8_t rtc_set_per_minute_alarm(); #define RTC_ALM_MASK_BIT 0x80 // "Don't match this field" bit in each alarm register
#define DEC2BCD(n) ((n) + (6 * ((n) / 10)))
#define BCD2DEC(n) ((n) - (6 * ((n) >> 4)))
// time_struct.Year is years since 2000, which is exactly what the RTC's 2-digit
// year register holds -- no offset. (The old +/-30 round-tripped but stored the
// wrong year in the RTC and overflowed BCD above 2069.)
#define tmYearToY2k(Y) (Y)
#define y2kYearToTm(Y) (Y)
uint8_t rtc_enable_interrupts(void);
uint8_t rtc_set_per_minute_alarm(void);
uint8_t rtc_read_time_array(uint8_t* data); uint8_t rtc_read_time_array(uint8_t* data);
time_struct rtc_read_time(); time_struct rtc_read_time(void);
uint8_t rtc_write_time(time_struct tm); uint8_t rtc_write_time(time_struct tm);
uint8_t rtc_set_alarm_config(); uint8_t rtc_set_alarm_config(void);
uint8_t rtc_read_status_register(); uint8_t rtc_read_status_register(void);
uint8_t rtc_read_interrupt_register(); uint8_t rtc_read_interrupt_register(void);
uint8_t rtc_read_register(uint8_t addr); uint8_t rtc_read_register(uint8_t addr);
void uart_print_rtc_time(time_struct td); void uart_print_rtc_time(time_struct td);
RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data); RTC_RFM69_STATUS set_time_from_rfm69(identifier_results id_data);
+52 -17
View File
@@ -1,10 +1,26 @@
// Minimal NDEF parser: finds the first record in a raw tag dump and accepts
// only a short text record, whose text becomes the node's "name,diameter"
// identity string.
#include "ndef.h" #include "ndef.h"
ndef_message readNDEFText(unsigned char *buf) { // Everything read here comes off an NFC tag that anyone can write, so every
int addr = 0; // length taken from the buffer is bounds-checked before it is used.
NDEF_MSG.success = 0; #define NDEF_NEED(n) \
{ \
if ((addr + (uint16_t)(n)) > buf_len) { \
NDEF_MSG.success = NDEF_ERR_TRUNCATED; \
return NDEF_MSG; \
} \
}
ndef_message readNDEFText(unsigned char* buf, uint8_t buf_len)
{
uint16_t addr = 0;
NDEF_MSG.success = 0;
NDEF_MSG.payload_len = 0;
NDEF_MSG.payload[0] = '\0';
NDEF_NEED(2);
if (buf[0] != NDEF_TLV) { if (buf[0] != NDEF_TLV) {
NDEF_MSG.success = 1; NDEF_MSG.success = 1;
return NDEF_MSG; return NDEF_MSG;
@@ -18,6 +34,7 @@ ndef_message readNDEFText(unsigned char *buf) {
// int len_field = buf[1]; // int len_field = buf[1];
addr = 2; addr = 2;
NDEF_NEED(3);
// bool is_short_record = (buf[addr] & NDEF_SHORT_RECORD) == NDEF_SHORT_RECORD; // bool is_short_record = (buf[addr] & NDEF_SHORT_RECORD) == NDEF_SHORT_RECORD;
bool has_id_length = (buf[addr] & NDEF_ID_LEN) == NDEF_ID_LEN; bool has_id_length = (buf[addr] & NDEF_ID_LEN) == NDEF_ID_LEN;
uint8_t tnf = buf[addr] & 0x7; uint8_t tnf = buf[addr] & 0x7;
@@ -31,18 +48,18 @@ ndef_message readNDEFText(unsigned char *buf) {
uint8_t id_length = 0; uint8_t id_length = 0;
if (has_id_length) { if (has_id_length) {
NDEF_NEED(1);
id_length = buf[addr]; id_length = buf[addr];
addr += 1; addr += 1;
} }
uint8_t type_value[type_length + 1]; // Only the first type byte is ever inspected, so skip the rest rather than
for (uint8_t i = 0; i < type_length; i++) { // copying them into a tag-sized VLA.
type_value[i] = buf[addr]; NDEF_NEED(type_length);
addr += 1; // 6 uint8_t type_value_0 = (type_length > 0) ? buf[addr] : 0;
} addr += type_length;
type_value[type_length] = 0; if (type_value_0 != NDEF_TEXT_RECORD) {
if (type_value[0] != NDEF_TEXT_RECORD) {
NDEF_MSG.success = 11; NDEF_MSG.success = 11;
return NDEF_MSG; return NDEF_MSG;
}; };
@@ -52,25 +69,43 @@ ndef_message readNDEFText(unsigned char *buf) {
}; };
if (has_id_length && (id_length > 0)) { if (has_id_length && (id_length > 0)) {
NDEF_NEED(id_length);
addr += id_length; addr += id_length;
} }
NDEF_NEED(1);
uint8_t lang_str_len = buf[addr]; uint8_t lang_str_len = buf[addr];
addr += lang_str_len;
addr += 1; // payload_length covers the language-length byte plus the language code
// plus the text. Subtracting without this check wraps a uint8_t to ~250.
if (payload_length < ((uint16_t)lang_str_len + 1)) {
NDEF_MSG.success = NDEF_ERR_BAD_LENGTH;
return NDEF_MSG;
}
payload_length -= lang_str_len; // Language string payload_length -= lang_str_len; // Language string
payload_length -= 1; // The byte that says how long the language string is payload_length -= 1; // The byte that says how long the language string is
NDEF_NEED((uint16_t)lang_str_len + 1);
addr += lang_str_len;
addr += 1;
// Leave room for the terminator the UART print and strchr() both rely on.
if (payload_length > (sizeof(NDEF_MSG.payload) - 1)) {
payload_length = sizeof(NDEF_MSG.payload) - 1;
}
NDEF_NEED(payload_length);
for (uint8_t i = 0; i < (payload_length); i++) { for (uint8_t i = 0; i < (payload_length); i++) {
NDEF_MSG.payload[i] = buf[addr]; NDEF_MSG.payload[i] = buf[addr];
addr += 1; addr += 1;
} }
NDEF_MSG.payload[payload_length] = '\0';
// NDEF_MSG.payload = payload;
NDEF_MSG.payload_len = payload_length; NDEF_MSG.payload_len = payload_length;
#if DO_UART #if DO_UART
uart_sendString(NDEF_MSG.payload); uart_sendString(NDEF_MSG.payload); // Runtime string, so not LOG()
uart_sendString("\n");
#endif #endif
LOG("\n");
return NDEF_MSG; return NDEF_MSG;
}; };
+8 -4
View File
@@ -13,10 +13,10 @@ extern "C" {
#endif #endif
#include "defines.h" #include "defines.h"
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include "uart.h" #include "uart.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define NDEF_TLV 0x03 #define NDEF_TLV 0x03
#define NDEF_SHORT_RECORD (1 << 4) #define NDEF_SHORT_RECORD (1 << 4)
@@ -24,7 +24,11 @@ extern "C" {
#define NDEF_TEXT_RECORD 0x54 #define NDEF_TEXT_RECORD 0x54
#define TNF_KNOWN 0x01 #define TNF_KNOWN 0x01
ndef_message readNDEFText(unsigned char *buf) ; // readNDEFText failure codes reported through ndef_message.success
#define NDEF_ERR_TRUNCATED 13
#define NDEF_ERR_BAD_LENGTH 14
ndef_message readNDEFText(unsigned char* buf, uint8_t buf_len);
#ifdef __cplusplus #ifdef __cplusplus
} }
+14 -3
View File
@@ -1,14 +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" #include "power_mgmt.h"
void shutdown_all_peripherals() { void shutdown_all_peripherals(void)
{
power_adc_disable(); power_adc_disable();
power_timer0_disable(); power_timer0_disable();
power_timer1_disable(); power_timer1_disable();
power_timer2_disable(); power_timer2_disable();
power_timer3_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 #if !DO_UART
power_usart0_disable(); power_usart0_disable();
power_usart1_disable();
#endif #endif
} }
+2 -3
View File
@@ -4,6 +4,7 @@
* *
* Created on December 20, 2024, 3:54 PM * Created on December 20, 2024, 3:54 PM
*/ */
#include "defines.h" // for DO_UART, which shutdown_all_peripherals() tests
#include <avr/power.h> #include <avr/power.h>
#ifndef POWER_MGMT_H #ifndef POWER_MGMT_H
#define POWER_MGMT_H #define POWER_MGMT_H
@@ -12,12 +13,10 @@
extern "C" { extern "C" {
#endif #endif
void shutdown_all_peripherals(void);
void shutdown_all_peripherals();
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* POWER_MGMT_H */ #endif /* POWER_MGMT_H */
+308 -294
View File
@@ -1,152 +1,25 @@
// RFM69 packet radio driver, plus the node's send-with-acknowledgement
// protocol and the wheel-counts packet builder.
//
// Layout of this file:
// 1. Register access over SPI
// 2. Mode control and status waits
// 3. Raw packet write/read (FIFO)
// 4. Acknowledged send protocol
// 5. Wheel-counts packet builder and hashes
// 6. Radio configuration (rfm69_init)
#include "rfm69.h" #include "rfm69.h"
int8_t rssi; // ---------------------------------------------------------------------------
uint8_t i; // 1. Register access over SPI ("_rt" = register transfer)
uint8_t len_payload; // ---------------------------------------------------------------------------
uint32_t msg_hash;
uint8_t p_hash_1;
uint8_t p_hash_2;
uint8_t p_hash_3;
uint8_t c_hash_1;
uint8_t c_hash_2;
uint8_t c_hash_3;
bool cond_1;
bool cond_2;
bool cond_3;
bool cond_4;
bool cond_5;
DATA_SEND_STATUS send_message(tx_rx_data_struct tx_data)
{
rfm69_write_msg(tx_data);
p_hash_1 = tx_data.msg[57];
p_hash_2 = tx_data.msg[58];
p_hash_3 = tx_data.msg[59];
for (uint8_t i = 0; i < 10; i++) {
bool result = wait_rx_payload_ready_timeout(50);
if (result) {
RX_DATA = rfm69_read_msg();
#if DO_UART
uart_sendString("RX DATA\n");
uart_print_tx_rx_data(RX_DATA);
#endif
c_hash_1 = RX_DATA.msg[0];
c_hash_2 = RX_DATA.msg[1];
c_hash_3 = RX_DATA.msg[2];
cond_1 = (p_hash_1 == c_hash_1) && (p_hash_2 == c_hash_2) && (p_hash_3 == c_hash_3);
cond_2 = (tx_data.from == RX_DATA.to) && (tx_data.to == RX_DATA.from);
cond_3 = cond_1 && cond_2;
if (cond_3 && (RX_DATA.flags == MSG_RECV_COUNTS_SUCCESS) && (RX_DATA.msg[3] == 0xFF)) {
#if DO_UART
uart_sendString(" RX DATA SUCCESS\n");
#endif
return DATA_SEND_SUCCESS;
break;
} else if (
cond_3 && (RX_DATA.flags == MSG_RECV_COUNTS_FAIL) && (RX_DATA.msg[3] == 0x00)) {
uart_sendString(" RX DATA FAILED\n");
} else {
uart_sendString(" RX DATA ANOTHER ERROR\n");
}
} else {
}
}
return DATA_NOT_SENT;
}
void uart_print_tx_rx_data(tx_rx_data_struct tx_rx_print)
{
// uart_print_uint8(tx_rx_print.len, "LEN; ");
// uart_print_uint8(tx_rx_print.to, "TO;");
// uart_print_uint8(tx_rx_print.from, "FROM;");
// uart_print_uint8(tx_rx_print.dtype, "DTYPE;");
// uart_print_uint8(tx_rx_print.flags, "FLAGS;");
DATA_BUFFER_7[0] = tx_rx_print.len;
DATA_BUFFER_7[1] = tx_rx_print.to;
DATA_BUFFER_7[2] = tx_rx_print.from;
DATA_BUFFER_7[3] = tx_rx_print.dtype;
DATA_BUFFER_7[4] = tx_rx_print.flags;
uart_sendString(" ");
uart_print_uint8_array(DATA_BUFFER_7, 5, "LEN,TO,FROM,DTYPE,FLAGS\n");
uart_sendString(" ");
uart_sendStringArray(tx_rx_print.msg, 20);
uart_sendChar('\n');
uart_sendString(" ");
uart_print_uint8_array(tx_rx_print.msg, tx_rx_print.len, "\n");
}
void rfm69_write_msg(tx_rx_data_struct txrxd)
{
set_rfm69_rx_mode();
set_rfm69_tx_mode();
spi_rfm69_select(true);
spi_write(REG_FIFO | RFM69_SPI_WRITE);
if (txrxd.len > (60)) {
txrxd.len = 60;
}
spi_write(txrxd.len + 4);
spi_write(txrxd.to);
spi_write(txrxd.from);
spi_write(txrxd.dtype);
spi_write(txrxd.flags);
for (uint8_t x = 0; x < txrxd.len; x++) {
spi_write(txrxd.msg[x]);
}
spi_rfm69_select(false);
wait_tx_sent();
set_rfm69_rx_mode();
}
tx_rx_data_struct rfm69_read_msg()
{
memset(RX_DATA.msg, ' ', sizeof(RX_DATA.msg));
spi_rfm69_select(true);
spi_write(REG_FIFO);
RX_DATA.len = spi_read() - 4;
RX_DATA.to = spi_read();
RX_DATA.from = spi_read();
RX_DATA.dtype = spi_read();
RX_DATA.flags = spi_read();
uint8_t len_f = RX_DATA.len;
for (uint8_t idx_f = 0; idx_f < len_f; idx_f++) {
RX_DATA.msg[idx_f] = spi_read();
}
spi_rfm69_select(false);
set_rfm69_idle();
return RX_DATA;
}
void rfm69_set_state(bool state)
{
SET_PIN_OUT(DDRC, DDC2);
if (!state) {
SET_PIN_HIGH(PORTC, PC2);
} else {
SET_PIN_LOW(PORTC, PC2);
}
}
uint8_t spi_read_rfm69_rt(uint8_t reg) uint8_t spi_read_rfm69_rt(uint8_t reg)
{ {
spi_rfm69_select(true); spi_rfm69_select(true);
spi_write(reg); spi_write(reg);
uint8_t data_read = spi_read(0xFF); uint8_t data_read = spi_read();
spi_rfm69_select(false); spi_rfm69_select(false);
return data_read; return data_read;
} }
@@ -171,21 +44,255 @@ uint8_t spi_write_rfm69_multiple_rt(uint8_t reg, const char* vals, uint8_t len)
return data_init; return data_init;
} }
// name (max 10), 10 // ---------------------------------------------------------------------------
// diameter (max 10), 20 // 2. Mode control and status waits
// battery_value 16-bit, 22 //
// time_reading (min) 23 // Every wait has a bail-out: an absent or unpowered radio must not hang the
// time_reading (hour) 24 // firmware, since no watchdog reset is armed.
// time_reading (day) 25 // ---------------------------------------------------------------------------
// time_reading (month) 26
// time_reading (year) 27 void reset_rfm69(void)
// 15 * per-min +30 57 {
// three byte hash check 3 rfm69_reset_state(true); // Reset line is active high
_delay_ms(10);
rfm69_reset_state(false);
_delay_ms(10);
}
void set_rfm69_mode(uint8_t target_mode)
{
uint8_t mode = spi_read_rfm69_rt(REG_OP_MODE);
mode &= ~VAL_OPMODE_MASK;
mode |= (target_mode & VAL_OPMODE_MASK);
spi_write_rfm69_rt(REG_OP_MODE, mode);
}
bool wait_rfm69_mode_ready(void)
{
for (uint16_t attempts = 0; attempts < RFM69_TIMEOUT_MS; attempts++) {
if (MODE_READY) {
return true;
}
_delay_ms(1);
}
return false;
}
bool wait_tx_sent(void)
{
for (uint16_t attempts = 0; attempts < RFM69_TIMEOUT_MS; attempts++) {
if (TX_SENT) {
return true;
}
_delay_ms(1);
}
return false;
}
bool wait_rx_payload_ready_timeout(uint16_t attempts)
{
set_rfm69_rx_mode();
// Test the flag before spending the tick, so a payload that arrives on the
// last attempt is not thrown away.
for (uint16_t counter = 0; counter < attempts; counter++) {
if (RX_PAYLOAD_READY) {
return true;
}
_delay_ms(1);
}
return RX_PAYLOAD_READY != 0;
}
bool wait_rx_payload_ready(void) { return wait_rx_payload_ready_timeout(RFM69_TIMEOUT_MS); }
// The PA boost registers are only allowed during TX; OCP must be off for the
// +20 dBm path, per the datasheet's high-power sequence.
void set_rfm69_power_amp_boost(void)
{
spi_write_rfm69_rt(REG_OCP, VAL_OCP_OFF);
spi_write_rfm69_rt(REG_TEST_PA1, VAL_TEST_PA1_BOOST);
spi_write_rfm69_rt(REG_TEST_PA2, VAL_TEST_PA2_BOOST);
}
void set_rfm69_power_amp_normal(void)
{
spi_write_rfm69_rt(REG_TEST_PA1, VAL_TEST_PA1_NORMAL);
spi_write_rfm69_rt(REG_TEST_PA2, VAL_TEST_PA2_NORMAL);
spi_write_rfm69_rt(REG_OCP, VAL_OCP_ON);
}
void set_rfm69_tx_mode(void)
{
set_rfm69_power_amp_boost();
set_rfm69_mode(VAL_OPMODE_TX);
wait_rfm69_mode_ready();
}
void set_rfm69_rx_mode(void)
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_RX);
wait_rfm69_mode_ready();
}
void set_rfm69_standby(void)
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_STDBY);
wait_rfm69_mode_ready();
}
void set_rfm69_sleep(void)
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_SLEEP);
wait_rfm69_mode_ready();
}
// "Idle" between packets is just standby
void set_rfm69_idle(void) { set_rfm69_standby(); }
// ---------------------------------------------------------------------------
// 3. Raw packet write/read
//
// On-air packet layout (variable-length mode, CRC on):
// [len] [to] [from] [dtype] [flags] [msg bytes ...]
// where len counts everything after itself, so msg length + 4 header bytes.
// ---------------------------------------------------------------------------
#define PACKET_HEADER_LEN 4
void reset_txrx_struct(tx_rx_data_struct* s)
{
s->len = 0;
s->to = 255;
s->from = 255;
s->dtype = 0;
s->flags = 0;
memset(s->msg, ' ', sizeof(s->msg));
}
void rfm69_write_msg(tx_rx_data_struct txrxd)
{
// TxStart is configured as FifoNotEmpty, so the radio begins transmitting
// the moment the first byte lands. Fill the FIFO from standby and only then
// switch to TX, otherwise the packet goes out ahead of its own payload.
set_rfm69_standby();
if (txrxd.len > sizeof(txrxd.msg)) {
txrxd.len = sizeof(txrxd.msg);
}
spi_rfm69_select(true);
spi_write(REG_FIFO | RFM69_SPI_WRITE);
spi_write(txrxd.len + PACKET_HEADER_LEN);
spi_write(txrxd.to);
spi_write(txrxd.from);
spi_write(txrxd.dtype);
spi_write(txrxd.flags);
for (uint8_t x = 0; x < txrxd.len; x++) {
spi_write(txrxd.msg[x]);
}
spi_rfm69_select(false);
set_rfm69_tx_mode();
wait_tx_sent();
set_rfm69_rx_mode();
}
tx_rx_data_struct rfm69_read_msg(void)
{
memset(RX_DATA.msg, ' ', sizeof(RX_DATA.msg));
spi_rfm69_select(true);
spi_write(REG_FIFO);
uint8_t raw_len = spi_read();
RX_DATA.to = spi_read();
RX_DATA.from = spi_read();
RX_DATA.dtype = spi_read();
RX_DATA.flags = spi_read();
// The length byte comes off the air and is not trustworthy: below 4 it
// underflows to ~252, above 60 it walks off the end of msg[].
uint8_t msg_len = (raw_len < PACKET_HEADER_LEN) ? 0 : (uint8_t)(raw_len - PACKET_HEADER_LEN);
if (msg_len > sizeof(RX_DATA.msg)) {
msg_len = sizeof(RX_DATA.msg);
}
RX_DATA.len = msg_len;
for (uint8_t idx = 0; idx < msg_len; idx++) {
RX_DATA.msg[idx] = spi_read();
}
spi_rfm69_select(false);
set_rfm69_idle();
return RX_DATA;
}
// ---------------------------------------------------------------------------
// 4. Acknowledged send protocol
//
// Every counts packet ends in a 3-byte hash of its payload. The base station
// echoes that hash back in its reply, so a reply is accepted only when the
// echoed hash matches what we sent and the addresses are ours reversed.
// ---------------------------------------------------------------------------
#define SEND_ACK_ATTEMPTS 10
#define ACK_WAIT_MS 50
static bool reply_acknowledges(const tx_rx_data_struct* tx_data)
{
bool hash_echo_matches = (tx_data->msg[57] == RX_DATA.msg[0])
&& (tx_data->msg[58] == RX_DATA.msg[1]) && (tx_data->msg[59] == RX_DATA.msg[2]);
bool addresses_are_ours_reversed
= (tx_data->from == RX_DATA.to) && (tx_data->to == RX_DATA.from);
return hash_echo_matches && addresses_are_ours_reversed;
}
DATA_SEND_STATUS send_message(tx_rx_data_struct tx_data)
{
rfm69_write_msg(tx_data);
for (uint8_t attempt = 0; attempt < SEND_ACK_ATTEMPTS; attempt++) {
if (!wait_rx_payload_ready_timeout(ACK_WAIT_MS)) {
continue;
}
RX_DATA = rfm69_read_msg();
LOG("RX DATA\n");
#if DO_UART
uart_print_tx_rx_data(RX_DATA);
#endif
if (!reply_acknowledges(&tx_data)) {
LOG(" RX DATA ANOTHER ERROR\n");
continue;
}
if ((RX_DATA.flags == MSG_RECV_COUNTS_SUCCESS) && (RX_DATA.msg[3] == 0xFF)) {
LOG(" RX DATA SUCCESS\n");
return DATA_SEND_SUCCESS;
}
LOG(" RX DATA FAILED\n");
}
return DATA_NOT_SENT;
}
// ---------------------------------------------------------------------------
// 5. Wheel-counts packet builder and hashes
//
// 60-byte msg layout:
// [0..9] name (padded)
// [10..19] wheel diameter (padded)
// [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
// ---------------------------------------------------------------------------
tx_rx_data_struct generate_wheel_counts_message( tx_rx_data_struct generate_wheel_counts_message(
identifier_results idd, time_struct time, uint16_t battery_value, volatile uint16_t counts[15]) identifier_results idd, time_struct time, uint16_t battery_value, volatile uint16_t counts[15])
{ {
reset_txrx_struct(&TX_DATA); reset_txrx_struct(&TX_DATA);
memcpy(TX_DATA.msg, idd.name_str, MIN(10, idd.name_len)); memcpy(TX_DATA.msg, idd.name_str, MIN(10, idd.name_len));
memcpy(TX_DATA.msg + 10, idd.diameter_str, MIN(10, idd.diameter_len)); memcpy(TX_DATA.msg + 10, idd.diameter_str, MIN(10, idd.diameter_len));
@@ -204,7 +311,7 @@ tx_rx_data_struct generate_wheel_counts_message(
TX_DATA.msg[26 + (2 * idx + 2)] = (counts[idx] >> 8) & 0xFF; // MSB second TX_DATA.msg[26 + (2 * idx + 2)] = (counts[idx] >> 8) & 0xFF; // MSB second
} }
msg_hash = hash_3bytes(TX_DATA.msg, 57); uint32_t msg_hash = hash_3bytes(TX_DATA.msg, 57);
TX_DATA.msg[57] = msg_hash & 0xFF; TX_DATA.msg[57] = msg_hash & 0xFF;
TX_DATA.msg[58] = (msg_hash >> 8) & 0xFF; TX_DATA.msg[58] = (msg_hash >> 8) & 0xFF;
TX_DATA.msg[59] = (msg_hash >> 16) & 0xFF; TX_DATA.msg[59] = (msg_hash >> 16) & 0xFF;
@@ -217,53 +324,19 @@ tx_rx_data_struct generate_wheel_counts_message(
return TX_DATA; return TX_DATA;
} }
// 24-bit payload checksum carried in the last three message bytes
uint32_t hash_3bytes(unsigned const char* str, uint8_t str_len) uint32_t hash_3bytes(unsigned const char* str, uint8_t str_len)
{ {
uint32_t hash = 0; uint32_t hash = 0;
for (i = 0; i < str_len; i++) { for (uint8_t i = 0; i < str_len; i++) {
hash = (hash * 31 + str[i]) % 0xFFFFFF; hash = (hash * 31 + str[i]) % 0xFFFFFF;
} }
return hash; return hash;
} }
void set_rfm69_power_amp_boost() // Hash a NUL-terminated string into [min, max]; used to derive the node's
{ // radio address from its name.
spi_write_rfm69_rt(REG_TEST_PA1, VAL_TEST_PA1_BOOST);
spi_write_rfm69_rt(REG_TEST_PA2, VAL_TEST_PA2_BOOST);
}
void set_rfm69_power_amp_normal()
{
spi_write_rfm69_rt(REG_TEST_PA1, VAL_TEST_PA1_NORMAL);
spi_write_rfm69_rt(REG_TEST_PA2, VAL_TEST_PA2_NORMAL);
};
void reset_txrx_struct(tx_rx_data_struct* s)
{
s->len = 0;
s->to = 255;
s->from = 255;
s->dtype = 0;
s->flags = 0;
memset(s->msg, ' ', 60);
}
void set_rfm69_mode(uint8_t target_mode)
{
uint8_t mode = spi_read_rfm69_rt(REG_OP_MODE);
mode &= ~VAL_OPMODE_MASK;
mode |= (target_mode & VAL_OPMODE_MASK);
spi_write_rfm69_rt(REG_OP_MODE, mode);
}
void wait_tx_sent()
{
while (TX_NOT_SENT)
;
}
uint8_t hash(const char* str, uint8_t min, uint8_t max) uint8_t hash(const char* str, uint8_t min, uint8_t max)
{ {
unsigned int hash = 0; unsigned int hash = 0;
@@ -276,126 +349,67 @@ uint8_t hash(const char* str, uint8_t min, uint8_t max)
return (hash % range) + min; return (hash % range) + min;
} }
bool wait_rx_payload_ready_timeout(uint16_t attempts) void uart_print_tx_rx_data(tx_rx_data_struct tx_rx_print)
{ {
set_rfm69_rx_mode(); DATA_BUFFER_7[0] = tx_rx_print.len;
uint16_t counter = 0; DATA_BUFFER_7[1] = tx_rx_print.to;
while (1) { DATA_BUFFER_7[2] = tx_rx_print.from;
_delay_ms(1); DATA_BUFFER_7[3] = tx_rx_print.dtype;
WHILE_BREAK(counter, attempts); DATA_BUFFER_7[4] = tx_rx_print.flags;
if (RX_PAYLOAD_READY) { uart_sendString(" ");
return true; uart_print_uint8_array(DATA_BUFFER_7, 5, "LEN,TO,FROM,DTYPE,FLAGS\n");
break; uart_sendString(" ");
} uart_sendStringArray(tx_rx_print.msg, 20);
} uart_sendChar('\n');
return false; uart_sendString(" ");
uart_print_uint8_array(tx_rx_print.msg, tx_rx_print.len, "\n");
;
} }
void wait_rx_payload_ready() // ---------------------------------------------------------------------------
{ // 6. Radio configuration
// ---------------------------------------------------------------------------
while (RX_PAYLOAD_NOT_READY) { }; void rfm69_init(void)
}
void wait_rfm69_mode_ready()
{
while (MODE_NOT_READY)
;
}
void set_rfm69_tx_mode()
{
set_rfm69_power_amp_boost();
set_rfm69_mode(VAL_OPMODE_TX);
wait_rfm69_mode_ready();
};
void set_rfm69_rx_mode()
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_RX);
wait_rfm69_mode_ready();
};
void set_rfm69_standby()
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_STDBY);
wait_rfm69_mode_ready();
}
void set_rfm69_sleep()
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_SLEEP);
wait_rfm69_mode_ready();
}
void set_rfm69_idle()
{
set_rfm69_power_amp_normal();
set_rfm69_mode(VAL_OPMODE_STDBY);
wait_rfm69_mode_ready();
}
void reset_rfm69()
{
rfm69_reset_state(true);
_delay_ms(10);
rfm69_reset_state(false);
_delay_ms(10);
}
void set_rfm69_tx_power()
{
uint8_t PA_LEVEL_SET
= VAL_PALEVEL_PA1_ON | VAL_PALEVEL_PA2_ON | ((20 + 14) & VAL_PALEVEL_PA1_OUTPUTPOWER);
spi_write_rfm69_rt(REG_PA_LEVEL, PA_LEVEL_SET);
}
void rfm69_init()
{ {
reset_rfm69(); reset_rfm69();
_delay_ms(100); _delay_ms(100);
set_rfm69_idle(); set_rfm69_idle();
// Carrier: 434.0 MHz (see the VAL_FREQ_* derivation in rfm69.h)
spi_write_rfm69_rt(REG_FREQ_MSB, VAL_FREQ_433MHz_MSB); spi_write_rfm69_rt(REG_FREQ_MSB, VAL_FREQ_433MHz_MSB);
spi_write_rfm69_rt(REG_FREQ_MIDDLE_SB, VAL_FREQ_433MHz_MID_SB); spi_write_rfm69_rt(REG_FREQ_MIDDLE_SB, VAL_FREQ_433MHz_MID_SB);
spi_write_rfm69_rt(REG_FREQ_LSB, VAL_FREQ_433MHz_LSB); spi_write_rfm69_rt(REG_FREQ_LSB, VAL_FREQ_433MHz_LSB);
spi_write_rfm69_rt(REG_FREQ_DEV_MSB, VAL_FREQ_DEV_MSB); // Start transmitting as soon as the FIFO has data (rfm69_write_msg relies
// on filling the FIFO in standby because of this)
spi_write_rfm69_rt( spi_write_rfm69_rt(REG_FIFO_THRESH, VAL_TX_START_FIFO_NOT_EMPTY | VAL_FIFO_LEVEL_INTERRUPT);
REG_FIFO_THRESH, spi_write_rfm69_rt(REG_TEST_DAGC, VAL_TEST_DAGC_DEFAULT); // Fading margin improvement
VAL_TX_START_FIFO_NOT_EMPTY | VAL_FIFO_LEVEL_INTERRUPT); // TX condition
spi_write_rfm69_rt(REG_TEST_DAGC,
VAL_TEST_DAGC_DEFAULT); // Fading margin improvement
// 2-byte sync word shared with the base station
char sync_words[] = { 0x2d, 0xd4 }; char sync_words[] = { 0x2d, 0xd4 };
spi_write_rfm69_multiple_rt(REG_SYNC_VALUE_1, sync_words, 2); spi_write_rfm69_multiple_rt(REG_SYNC_VALUE_1, sync_words, 2);
spi_write_rfm69_rt(REG_SYNC_CONFIG, VAL_SYNCWORDS_ON | VAL_SYNCWORDS_SIZE_2_BYTES); spi_write_rfm69_rt(REG_SYNC_CONFIG, VAL_SYNCWORDS_ON | VAL_SYNCWORDS_SIZE_2_BYTES);
spi_write_rfm69_rt(REG_DATA_MODUL, // FSK packet mode, Gaussian shaping, 250 kbps, 25 kHz deviation
VAL_DATA_PACKET_MODE | VAL_DATA_MODUL_OOK); // RegDataModul
spi_write_rfm69_rt(REG_BITRATE_MSB,
VAL_BITRATE_250kbps_MSB); // RegBitrateMSB
spi_write_rfm69_rt(REG_BITRATE_LSB,
VAL_BITRATE_250kbps_LSB); // RegbBitrateLSB
spi_write_rfm69_rt(REG_FDEV_MSB, 0x10); // RegFdevMSB
spi_write_rfm69_rt(REG_FDEV_LSB, 0x00); // RegFdevLSB
spi_write_rfm69_rt(REG_RX_BW, 0xE0); // RegRxBw
spi_write_rfm69_rt(REG_AFC_BW, 0xE0); // RegAfcBw
spi_write_rfm69_rt( spi_write_rfm69_rt(
REG_PACKET_CONFIG_1, REG_DATA_MODUL, VAL_DATA_PACKET_MODE | VAL_DATA_MODUL_FSK | VAL_MODUL_SHAPING_GAUSS_BT_1_0);
VAL_PACKET_VARIABLE_LENGTH | VAL_PACKET_WHITENING | VAL_PACKET_CRCON); // RegPacketConfig1 spi_write_rfm69_rt(REG_BITRATE_MSB, VAL_BITRATE_250kbps_MSB);
spi_write_rfm69_rt(REG_BITRATE_LSB, VAL_BITRATE_250kbps_LSB);
spi_write_rfm69_rt(REG_FDEV_MSB, VAL_FDEV_MSB);
spi_write_rfm69_rt(REG_FDEV_LSB, VAL_FDEV_LSB);
spi_write_rfm69_rt(REG_PREAMBLE_MSB, 0x00); // RegPreambleMSB // Widest RX/AFC bandwidth settings
spi_write_rfm69_rt(REG_PREAMBLE_LSB, 0x04); // RegPreambleLSB spi_write_rfm69_rt(REG_RX_BW, 0xE0);
spi_write_rfm69_rt(REG_AFC_BW, 0xE0);
spi_write_rfm69_rt(REG_PA_LEVEL, // Variable-length packets with whitening and CRC
VAL_PA_PA1_ON | VAL_PA_PA2_ON | VAL_PA_20dB); // RegPaLevel spi_write_rfm69_rt(
REG_PACKET_CONFIG_1, VAL_PACKET_VARIABLE_LENGTH | VAL_PACKET_WHITENING | VAL_PACKET_CRCON);
// 4-byte preamble
spi_write_rfm69_rt(REG_PREAMBLE_MSB, 0x00);
spi_write_rfm69_rt(REG_PREAMBLE_LSB, 0x04);
// Both PA stages on, maximum output power
spi_write_rfm69_rt(REG_PA_LEVEL, VAL_PA_PA1_ON | VAL_PA_PA2_ON | VAL_PA_20dB);
} }
+35 -23
View File
@@ -15,14 +15,21 @@
#ifndef RFM69_H #ifndef RFM69_H
#define RFM69_H #define RFM69_H
#define MODE_NOT_READY !(spi_read_rfm69_rt(REG_IRQ_FLAGS1) & VAL_IRQ_FLAGS1_MODEREADY)
#define RX_PAYLOAD_READY spi_read_rfm69_rt(REG_IRQ_FLAGS2) & VAL_IRQ_FLAGS2_RX_PAYLOADREADY
#define RX_PAYLOAD_NOT_READY !(RX_PAYLOAD_READY)
#define TX_NOT_SENT !(spi_read_rfm69_rt(REG_IRQ_FLAGS2) & VAL_IRQ_FLAGS2_TX_SENT)
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
#define MODE_READY (spi_read_rfm69_rt(REG_IRQ_FLAGS1) & VAL_IRQ_FLAGS1_MODEREADY)
#define MODE_NOT_READY (!MODE_READY)
#define RX_PAYLOAD_READY (spi_read_rfm69_rt(REG_IRQ_FLAGS2) & VAL_IRQ_FLAGS2_RX_PAYLOADREADY)
#define RX_PAYLOAD_NOT_READY (!RX_PAYLOAD_READY)
#define TX_SENT (spi_read_rfm69_rt(REG_IRQ_FLAGS2) & VAL_IRQ_FLAGS2_TX_SENT)
#define TX_NOT_SENT (!TX_SENT)
// Bail-out for every RFM69 poll loop: an absent or unpowered radio must not
// hang the firmware, since no watchdog reset is armed.
#define RFM69_TIMEOUT_MS 100U
#define REG_FIFO 0x00 #define REG_FIFO 0x00
#define REG_FREQ_MSB 0x07 #define REG_FREQ_MSB 0x07
#define REG_FREQ_MIDDLE_SB 0x08 #define REG_FREQ_MIDDLE_SB 0x08
@@ -35,7 +42,7 @@ extern "C" {
#define REG_DATA_MODUL 0x02 #define REG_DATA_MODUL 0x02
#define REG_BITRATE_MSB 0x03 #define REG_BITRATE_MSB 0x03
#define REG_BITRATE_LSB 0x04 #define REG_BITRATE_LSB 0x04
#define REG_FDEV_MSB 0x06 #define REG_FDEV_MSB 0x05
#define REG_FDEV_LSB 0x06 #define REG_FDEV_LSB 0x06
#define REG_RX_BW 0x19 #define REG_RX_BW 0x19
#define REG_AFC_BW 0x1A #define REG_AFC_BW 0x1A
@@ -48,9 +55,15 @@ extern "C" {
#define REG_IRQ_FLAGS1 0x27 #define REG_IRQ_FLAGS1 0x27
#define REG_IRQ_FLAGS2 0x28 #define REG_IRQ_FLAGS2 0x28
#define REG_RSSI_VALUE 0x24 #define REG_RSSI_VALUE 0x24
#define REG_OCP 0x13
#define REG_FREQ_DEV_MSB 0x05 // Over-current protection must be off while the PA boost registers are set,
#define VAL_FREQ_DEV_MSB 0x10 // per the datasheet's high-power (+20 dBm) sequence.
#define VAL_OCP_OFF 0x0F
#define VAL_OCP_ON 0x1A
#define VAL_FDEV_MSB 0x10
#define VAL_FDEV_LSB 0x00
#define VAL_TEST_DAGC_DEFAULT 0x30 #define VAL_TEST_DAGC_DEFAULT 0x30
#define VAL_DATA_PACKET_MODE 0x00 #define VAL_DATA_PACKET_MODE 0x00
@@ -58,7 +71,8 @@ extern "C" {
#define VAL_BITRATE_250kbps_MSB 0x00 #define VAL_BITRATE_250kbps_MSB 0x00
#define VAL_BITRATE_250kbps_LSB 0x80 #define VAL_BITRATE_250kbps_LSB 0x80
#define VAL_DATA_MODUL_OOK 0x01 #define VAL_DATA_MODUL_FSK 0x00 // RegDataModul ModulationType is bits 4:3
#define VAL_MODUL_SHAPING_GAUSS_BT_1_0 0x01
#define VAL_TX_START_FIFO_NOT_EMPTY 0x80 #define VAL_TX_START_FIFO_NOT_EMPTY 0x80
#define VAL_FIFO_LEVEL_INTERRUPT 0x0f #define VAL_FIFO_LEVEL_INTERRUPT 0x0f
@@ -104,28 +118,26 @@ extern "C" {
#define VAL_FREQ_433MHz_LSB 0x00 #define VAL_FREQ_433MHz_LSB 0x00
DATA_SEND_STATUS send_message(tx_rx_data_struct tx_data); DATA_SEND_STATUS send_message(tx_rx_data_struct tx_data);
void rfm69_set_state(bool state);
uint8_t spi_read_rfm69_rt(uint8_t reg); uint8_t spi_read_rfm69_rt(uint8_t reg);
uint8_t spi_write_rfm69_rt(uint8_t reg, uint8_t val); uint8_t spi_write_rfm69_rt(uint8_t reg, uint8_t val);
uint8_t spi_write_rfm69_multiple_rt(uint8_t reg, const char* vals, uint8_t len); uint8_t spi_write_rfm69_multiple_rt(uint8_t reg, const char* vals, uint8_t len);
void set_rfm69_power_amp_boost(); void set_rfm69_power_amp_boost(void);
void set_rfm69_power_amp_normal(); void set_rfm69_power_amp_normal(void);
tx_rx_data_struct rfm69_read_msg(); tx_rx_data_struct rfm69_read_msg(void);
void reset_txrx_struct(tx_rx_data_struct* s); void reset_txrx_struct(tx_rx_data_struct* s);
void rfm69_write_msg(tx_rx_data_struct txrxd); void rfm69_write_msg(tx_rx_data_struct txrxd);
void set_rfm69_mode(uint8_t mode); void set_rfm69_mode(uint8_t mode);
void wait_rfm69_mode_ready(); bool wait_rfm69_mode_ready(void);
void set_rfm69_tx_mode(); void set_rfm69_tx_mode(void);
void wait_tx_sent(); bool wait_tx_sent(void);
void wait_rx_payload_ready(); bool wait_rx_payload_ready(void);
void reset_rfm69(); void reset_rfm69(void);
void set_rfm69_rx_mode(); void set_rfm69_rx_mode(void);
void set_rfm69_standby(); void set_rfm69_standby(void);
void set_rfm69_sleep(); void set_rfm69_sleep(void);
void set_rfm69_idle(); void set_rfm69_idle(void);
void set_rfm69_tx_power(); void rfm69_init(void);
void rfm69_init();
bool wait_rx_payload_ready_timeout(uint16_t attempts); bool wait_rx_payload_ready_timeout(uint16_t attempts);
uint8_t hash(const char* str, uint8_t min, uint8_t max); uint8_t hash(const char* str, uint8_t min, uint8_t max);
uint32_t hash_3bytes(unsigned const char* str, uint8_t str_len); uint32_t hash_3bytes(unsigned const char* str, uint8_t str_len);
+6 -14
View File
@@ -1,26 +1,18 @@
#include "spi.h" #include "spi.h"
// RFM69 chip select (SS1/PE2, active low)
void spi_rfm69_select(bool state) void spi_rfm69_select(bool state)
{ {
SET_PIN_OUT(DDRE, DDE2); SET_PIN_OUT(DDRE, DDE2);
if (!state) { SET_PIN_TO(PORTE, PE2, !state);
SET_PIN_HIGH(PORTE, PE2);
} else {
SET_PIN_LOW(PORTE, PE2);
}
} }
uint8_t spi_write(uint8_t data)
uint8_t spi_write(uint8_t data) { {
SPDR1 = data; // Load data into the SPI data register SPDR1 = data; // Load data into the SPI data register
while (!(SPSR1 & (1 << SPIF1))) { while (!(SPSR1 & (1 << SPIF1))) { }; // Wait for transmission to complete
}; // Wait for transmission to complete
return SPDR1; // Return received data return SPDR1; // Return received data
} }
uint8_t spi_read() { uint8_t spi_read(void) { return spi_write(0xFF); }
return spi_write(0xFF);
}
+2 -3
View File
@@ -1,13 +1,12 @@
#include <avr/io.h>
#include "defines.h" #include "defines.h"
#include "states.h" #include "states.h"
#include <avr/io.h>
#include <stdbool.h> #include <stdbool.h>
#ifndef SPI_H #ifndef SPI_H
#define SPI_H #define SPI_H
uint8_t spi_write(uint8_t data); uint8_t spi_write(uint8_t data);
uint8_t spi_read(); uint8_t spi_read(void);
void spi_rfm69_select(bool state); void spi_rfm69_select(bool state);
#endif #endif
+75 -43
View File
@@ -1,88 +1,120 @@
// ST25DV NFC tag driver. The tag holds a single NDEF text record of the form
// "<name>,<wheel diameter>", which get_nugget_data() parses into IDENTIFIER.
#include "st25dv.h" #include "st25dv.h"
identifier_results get_nugget_data() { #define IDENT_NAME_MAX (sizeof(IDENTIFIER.name_str) - 1)
#define IDENT_DIAM_MAX (sizeof(IDENTIFIER.diameter_str) - 1)
static void set_identifier(const char* name, uint8_t name_len, const char* diam, uint8_t diam_len)
{
if (name_len > IDENT_NAME_MAX) {
name_len = IDENT_NAME_MAX;
}
if (diam_len > IDENT_DIAM_MAX) {
diam_len = IDENT_DIAM_MAX;
}
memcpy(IDENTIFIER.name_str, name, name_len);
IDENTIFIER.name_str[name_len] = '\0';
IDENTIFIER.name_len = name_len;
memcpy(IDENTIFIER.diameter_str, diam, diam_len);
IDENTIFIER.diameter_str[diam_len] = '\0';
IDENTIFIER.diameter_len = diam_len;
IDENTIFIER.hashed = hash(IDENTIFIER.name_str, 0, 59);
}
identifier_results get_nugget_data(void)
{
NDEF_MSG = rfid_read_first_ndef_entry(); NDEF_MSG = rfid_read_first_ndef_entry();
// readNDEFText reports parse failures through success; without this check a
// missing or malformed tag leaves stale/uninitialised bytes in payload and
// we transmit them as the node identity.
if (NDEF_MSG.success != 0) {
#if DO_UART
uart_print_uint8(NDEF_MSG.success, "NDEF parse failed, code ");
#endif
set_identifier("UNKNOWN", 7, "N/A", 3);
return IDENTIFIER;
}
TRIMMED_STRING = remove_spaces(NDEF_MSG.payload, NDEF_MSG.payload_len); TRIMMED_STRING = remove_spaces(NDEF_MSG.payload, NDEF_MSG.payload_len);
char* delim_ptr = strchr(TRIMMED_STRING.str, ','); char* delim_ptr = strchr(TRIMMED_STRING.str, ',');
if (delim_ptr != NULL) { if (delim_ptr != NULL) {
uint8_t index_comma = delim_ptr - TRIMMED_STRING.str; uint8_t index_comma = (uint8_t)(delim_ptr - TRIMMED_STRING.str);
memcpy(IDENTIFIER.name_str, TRIMMED_STRING.str, index_comma); // The diameter is what follows the comma, so its length is the
memcpy( // remainder of the string -- not the whole string's length, which read
IDENTIFIER.diameter_str, TRIMMED_STRING.str + index_comma + 1, TRIMMED_STRING.length); // off the end of the 21-byte buffer.
IDENTIFIER.name_len = index_comma; uint8_t diam_len = (uint8_t)(TRIMMED_STRING.length - index_comma - 1);
IDENTIFIER.diameter_len = TRIMMED_STRING.length - index_comma; set_identifier(TRIMMED_STRING.str, index_comma, delim_ptr + 1, diam_len);
} else { } else {
memcpy(IDENTIFIER.name_str, TRIMMED_STRING.str, TRIMMED_STRING.length); set_identifier(TRIMMED_STRING.str, (uint8_t)TRIMMED_STRING.length, "N/A", 3);
IDENTIFIER.name_len = TRIMMED_STRING.length;
memcpy(IDENTIFIER.diameter_str, "N/A", 3);
IDENTIFIER.diameter_len = 3;
} }
IDENTIFIER.hashed = hash(IDENTIFIER.name_str, 0, 59);
return IDENTIFIER; return IDENTIFIER;
} }
trimmed_string_struct remove_spaces(char* str, uint8_t len_str) { trimmed_string_struct remove_spaces(char* str, uint8_t len_str)
uint8_t i = 0, j = 0; {
memset(TRIMMED_STRING.str, ' ', 20); const uint8_t max_len = sizeof(TRIMMED_STRING.str) - 1;
while (str[i]) { uint8_t j = 0;
if (str[i] != ' ') {
memset(TRIMMED_STRING.str, 0, sizeof(TRIMMED_STRING.str));
for (uint8_t i = 0; (i < len_str) && str[i]; i++) {
if ((str[i] != ' ') && (j < max_len)) {
TRIMMED_STRING.str[j++] = str[i]; TRIMMED_STRING.str[j++] = str[i];
} }
i++;
if (i >= len_str) {
break;
}
} }
// Callers run strchr() over this, so it has to be terminated.
TRIMMED_STRING.str[j] = '\0';
TRIMMED_STRING.length = j; TRIMMED_STRING.length = j;
return TRIMMED_STRING; return TRIMMED_STRING;
} }
void rfid_set_low_power_down(bool state) { // LPD pin: high puts the tag's I2C interface into low-power mode
void rfid_set_low_power_down(bool state)
{
SET_PIN_OUT(DDRD, DDD5); SET_PIN_OUT(DDRD, DDD5);
if (state) { SET_PIN_TO(PORTD, PD5, state);
SET_PIN_HIGH(PORTD, PD5);
} else {
SET_PIN_LOW(PORTD, PD5);
}
} }
ndef_message rfid_read_first_ndef_entry() { ndef_message rfid_read_first_ndef_entry(void)
{
rfid_set_low_power_down(false); rfid_set_low_power_down(false);
rfid_set_i2c_power(true); rfid_set_i2c_power(true);
_delay_ms(1); _delay_ms(1);
memset(DATA_BUFFER_65, ' ', 64); // static: a 65-byte frame here sat on top of an already deep call chain and
// was a large part of the stack overrun.
char DATA_BUFFER_INTERNAL[65]; static unsigned char DATA_BUFFER_INTERNAL[NDEF_READ_LEN];
rfid_read_memory(DATA_BUFFER_INTERNAL, 64, 0x0000 + 4); memset(DATA_BUFFER_INTERNAL, 0, sizeof(DATA_BUFFER_INTERNAL));
rfid_read_memory(DATA_BUFFER_INTERNAL, NDEF_READ_LEN, 0x0000 + 4);
NDEF_MSG = readNDEFText(DATA_BUFFER_INTERNAL);
NDEF_MSG = readNDEFText(DATA_BUFFER_INTERNAL, NDEF_READ_LEN);
rfid_set_low_power_down(true); rfid_set_low_power_down(true);
rfid_set_i2c_power(false); rfid_set_i2c_power(false);
return NDEF_MSG; return NDEF_MSG;
} }
uint8_t rfid_read_system_register() { uint8_t rfid_read_system_register(void)
{
return read_one_byte_16bit_addr_no_err_register(I2C_SYSTEM_ADDR, 0x0000); return read_one_byte_16bit_addr_no_err_register(I2C_SYSTEM_ADDR, 0x0000);
} }
void rfid_set_i2c_power(bool state) { // Switched supply for the tag's I2C side
void rfid_set_i2c_power(bool state)
{
SET_PIN_OUT(DDRE, DDE0); SET_PIN_OUT(DDRE, DDE0);
if (state) { SET_PIN_TO(PORTE, PE0, state);
SET_PIN_HIGH(PORTE, PE0);
} else {
SET_PIN_LOW(PORTE, PE0);
}
} }
uint8_t rfid_read_memory(uint8_t* data, uint8_t num_bytes, uint16_t address) { uint8_t rfid_read_memory(uint8_t* data, uint8_t num_bytes, uint16_t address)
{
return read_n_bytes_16bit_addr(I2C_USER_ADDR, address, data, num_bytes); return read_n_bytes_16bit_addr(I2C_USER_ADDR, address, data, num_bytes);
} }
+8 -7
View File
@@ -1,24 +1,25 @@
#include "i2c.h"
#include "defines.h" #include "defines.h"
#include "i2c.h"
#include "ndef.h" #include "ndef.h"
#include "rfm69.h"
#include "states.h" #include "states.h"
#include <stdbool.h> #include <stdbool.h>
#include "rfm69.h"
#include <util/delay.h> #include <util/delay.h>
#ifndef ST25DV_H #ifndef ST25DV_H
#define ST25DV_H #define ST25DV_H
#define I2C_SYSTEM_ADDR 0x57 #define I2C_SYSTEM_ADDR 0x57
#define I2C_USER_ADDR 0x53 #define I2C_USER_ADDR 0x53
identifier_results get_nugget_data(); // Bytes of tag memory pulled in one go to look for the first NDEF record.
ndef_message rfid_read_first_ndef_entry(); #define NDEF_READ_LEN 64
identifier_results get_nugget_data(void);
ndef_message rfid_read_first_ndef_entry(void);
void rfid_set_low_power_down(bool state); void rfid_set_low_power_down(bool state);
void rfid_set_i2c_power(bool state); void rfid_set_i2c_power(bool state);
uint8_t rfid_read_memory(uint8_t* data, uint8_t num_bytes, uint16_t address); uint8_t rfid_read_memory(uint8_t* data, uint8_t num_bytes, uint16_t address);
uint8_t rfid_read_system_register(); uint8_t rfid_read_system_register(void);
trimmed_string_struct remove_spaces(char* str, uint8_t len_str); trimmed_string_struct remove_spaces(char* str, uint8_t len_str);
+34 -37
View File
@@ -1,55 +1,52 @@
// GPIO helpers: every board control line lives here (except the two chip
// selects, which stay with their SPI drivers). Each helper sets the pin's
// direction on every call so it works no matter what ran before it.
#include "states.h" #include "states.h"
void init_spi() { void init_spi(void)
SET_PIN_OUT(DDRC, DDC1); // SCK {
SET_PIN_OUT(DDRE, DDE3); // MOSI SET_PIN_OUT(DDRC, DDC1); // SCK1
SET_PIN_IN(DDRC, DDC0); // MISO_RFM69 SET_PIN_OUT(DDRE, DDE3); // MOSI1
SET_PIN_HIGH(PORTC, PC0); SET_PIN_IN(DDRC, DDC0); // MISO1 (driven by the slave; no pull-up)
SPCR1= (1<<SPE1) | (1<<MSTR1); // Enable, Master, f_osc/16
// SS1 must be an output before SPE is set. If it is left as an input and
// reads low, the hardware clears MSTR and the port silently stops being a
// master.
SET_PIN_OUT(DDRE, DDE2);
SET_PIN_HIGH(PORTE, PE2);
SPCR1 = (1 << SPE1) | (1 << MSTR1); // Enable, Master, SPR1:0 = 00 -> f_osc/4
} }
// RFM69 reset line: high holds the radio in reset
void rfm69_reset_state(bool state) { void rfm69_reset_state(bool state)
{
SET_PIN_OUT(DDRC, DDC2); SET_PIN_OUT(DDRC, DDC2);
if (state) { SET_PIN_TO(PORTC, PC2, state);
SET_PIN_HIGH(PORTC, PC2);
} else {
SET_PIN_LOW(PORTC, PC2);
}
} }
void led_1_set_state(bool state) { void led_1_set_state(bool state)
{
SET_PIN_OUT(DDRD, DDD4); SET_PIN_OUT(DDRD, DDD4);
if (state) { SET_PIN_TO(PORTD, PD4, state);
SET_PIN_HIGH(PORTD, PD4);
} else {
SET_PIN_LOW(PORTD, PD4);
}
} }
void led_2_set_state(bool state) { void led_2_set_state(bool state)
{
SET_PIN_OUT(DDRD, DDD6); SET_PIN_OUT(DDRD, DDD6);
if (state) { SET_PIN_TO(PORTD, PD6, state);
SET_PIN_HIGH(PORTD, PD6);
} else {
SET_PIN_LOW(PORTD, PD6);
}
} }
void led_3_set_state(bool state) { void led_3_set_state(bool state)
{
SET_PIN_OUT(DDRD, DDD7); SET_PIN_OUT(DDRD, DDD7);
if (state) { SET_PIN_TO(PORTD, PD7, state);
SET_PIN_HIGH(PORTD, PD7);
} else {
SET_PIN_LOW(PORTD, PD7);
}
} }
void ldo_set_state(bool state) { // Enable line of the LDO that powers the radio and EEPROM
void ldo_set_state(bool state)
{
SET_PIN_OUT(DDRC, DDC3); SET_PIN_OUT(DDRC, DDC3);
if (state) { SET_PIN_TO(PORTC, PC3, state);
SET_PIN_HIGH(PORTC, PC3);
} else {
SET_PIN_LOW(PORTC, PC3);
}
} }
+4 -7
View File
@@ -8,9 +8,8 @@
#ifndef STATES_H #ifndef STATES_H
#define STATES_H #define STATES_H
#include <stdbool.h>
#include <avr/io.h> #include <avr/io.h>
#include <stdbool.h>
#define SET_PIN_OUT(DDR, PIN) ((DDR) |= (1 << (PIN))) // Set pin as output #define SET_PIN_OUT(DDR, PIN) ((DDR) |= (1 << (PIN))) // Set pin as output
#define SET_PIN_IN(DDR, PIN) ((DDR) &= ~(1 << (PIN))) // Set pin as input #define SET_PIN_IN(DDR, PIN) ((DDR) &= ~(1 << (PIN))) // Set pin as input
@@ -18,13 +17,14 @@
#define SET_PIN_HIGH(PORT, PIN) ((PORT) |= (1 << (PIN))) // Set pin high #define SET_PIN_HIGH(PORT, PIN) ((PORT) |= (1 << (PIN))) // Set pin high
#define SET_PIN_LOW(PORT, PIN) ((PORT) &= ~(1 << (PIN))) // Set pin low #define SET_PIN_LOW(PORT, PIN) ((PORT) &= ~(1 << (PIN))) // Set pin low
// Drive a pin to a boolean level
#define SET_PIN_TO(PORT, PIN, level) ((level) ? SET_PIN_HIGH(PORT, PIN) : SET_PIN_LOW(PORT, PIN))
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void init_spi(); void init_spi(void);
void rfm69_reset_state(bool state); void rfm69_reset_state(bool state);
void led_1_set_state(bool state); void led_1_set_state(bool state);
@@ -34,11 +34,8 @@ extern "C" {
void ldo_set_state(bool state); void ldo_set_state(bool state);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* STATES_H */ #endif /* STATES_H */
+8
View File
@@ -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) void uart_print_uint16(uint16_t meas, const char* buf)
{ {
snprintf(array_internal, sizeof(array_internal), "%u", meas); snprintf(array_internal, sizeof(array_internal), "%u", meas);
+6 -3
View File
@@ -6,9 +6,9 @@
*/ */
#include "defines.h" #include "defines.h"
#define UBRR_BAUD F_CPU / 16 / BAUD - 1
#include <avr/io.h> #include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@@ -22,9 +22,12 @@
extern "C" { extern "C" {
#endif #endif
void uart_init(); #define UBRR_BAUD ((F_CPU) / 16 / (BAUD) - 1)
void uart_init(void);
void uart_sendChar(char c); void uart_sendChar(char c);
void uart_sendString(const char* str); 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_sendStringArray(unsigned char str[], uint8_t len);
void uart_print_uint16(uint16_t meas, const char* buf); void uart_print_uint16(uint16_t meas, const char* buf);
void uart_print_hex(unsigned char vin, const char* buf); void uart_print_hex(unsigned char vin, const char* buf);
@@ -34,7 +37,7 @@ void uart_print_float(float meas, const char* buf);
void uart_print_binary(unsigned char vin, const char* buf); void uart_print_binary(unsigned char vin, const char* buf);
void uart_print_uint8(uint8_t vin, const char* buf); void uart_print_uint8(uint8_t vin, const char* buf);
void uart_print_uint8_array(uint8_t* array, size_t length, const char* buf); void uart_print_uint8_array(uint8_t* array, size_t length, const char* buf);
void uart_wait_until_sent(); void uart_wait_until_sent(void);
#ifdef __cplusplus #ifdef __cplusplus
} }