Protocol rework

This commit is contained in:
ThePetrovich 2026-05-10 15:33:08 +08:00
parent 3e77d34ccc
commit 2b713a3e3a
15 changed files with 1347 additions and 1155 deletions

View file

@ -1,6 +1,7 @@
/*
* @file adc.cpp
* @brief
* @brief ATmega4809 ADC0 driver: configuration profiles, oversampling reads,
* and conversions to engineering units.
*
* Created: 27.09.2025 05:06:33
* Author: ThePetrovich
@ -13,19 +14,38 @@
#include "adc.h"
#include "config.h"
#include "utils.h"
#include <Arduino.h>
#include <util/atomic.h>
uint8_t adc_flags = 0;
/**
* @brief Internal driver state.
* Bit 0 = fast mode currently enabled.
*/
static uint8_t adc_flags = 0;
int16_t adc_to_temperature_c(uint16_t adc_value)
{
// MCP9700: 500 mV at 0°C, 10 mV per degree, vref = 3.0V
// Result in 0.1°C units
uint16_t adc_resolution = ADC_RESOLUTION
<< config.flags.adc_oversample_bits; // Adjust resolution based on oversampling
return (int16_t)((adc_value * ADC_VREF_MV / adc_resolution - config.tempcal));
/*
* MCP9700: 500 mV at 0 °C, 10 mV/°C, result returned in 0.1 °C units.
* Use calibration points when available (adccal3 != 0xFFFF):
* adccal0 = ADC reading at GND (offset),
* adccal3 = ADC reading at 3.0 V reference.
* Falls back to nominal constants when uncalibrated.
*/
uint16_t cal0 = config.calibration.adccal0;
uint16_t cal3 = config.calibration.adccal3;
int16_t tempcal = (int16_t)config.calibration.tempcal;
int32_t mv;
if (cal3 != 0xFFFF && cal3 != cal0) {
mv = (int32_t)(adc_value - cal0) * ADC_VREF_MV / (int32_t)(cal3 - cal0);
} else {
uint16_t adc_resolution = ADC_RESOLUTION << config.flags.fields.adc_oversample_bits;
mv = (int32_t)adc_value * ADC_VREF_MV / adc_resolution;
}
/* Result in 0.1 °C: (mv - 500 - tempcal_offset) * 10 / 10. */
return (int16_t)((mv - MCP9700_OFFSET_MV - tempcal) * 10L / MCP9700_SCALE_MV);
}
uint16_t adc_to_voltage_mv(uint16_t adc_value) { return (uint16_t)(adc_value * VOLTAGE_MV_PER_STEP); }
@ -33,14 +53,11 @@ uint16_t adc_to_voltage_mv(uint16_t adc_value) { return (uint16_t)(adc_value * V
uint16_t adc_read_oversampled(uint8_t pin, uint8_t samples)
{
uint32_t sum = 0;
// unrolled loop
uint8_t shift = ((samples == 64) ? 3 : (samples == 16) ? 2 : (samples == 4) ? 1 : 0);
for (uint8_t i = 0; i < samples; i++) {
sum += analogRead(pin);
}
return (uint16_t)(sum >> shift);
}
@ -50,21 +67,21 @@ void adc_enable_fast(void)
{
ADC0.CTRLA |= ADC_ENABLE_bm;
// Enable oversampling x16 for 12-bit result
ADC0.CTRLB |= (config.flags.adc_oversample_bits << 2); // 2, 4, or 6 for 4x, 16x, or 64x oversampling
/* Oversampling factor selected by config.flags.adc_oversample_bits. */
ADC0.CTRLB |= (config.flags.fields.adc_oversample_bits << 2);
ADC0.CTRLC = 0; // reset
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VREFA_gc; // CLK_PER/32, VREFA as reference
ADC0.CTRLC = 0;
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VREFA_gc; /* CLK_PER/32, VREFA reference */
ADC0.EVCTRL |= ADC_STARTEI_bm; // EVSYS input to start conversion
ADC0.INTCTRL |= ADC_RESRDY_bm; // Enable interrupt on conversion complete
ADC0.EVCTRL |= ADC_STARTEI_bm; /* EVSYS input starts conversion */
ADC0.INTCTRL |= ADC_RESRDY_bm; /* result-ready interrupt */
// Configure VREF, Microchip DS40002015B page 424
/* Microchip DS40002015B p. 424. */
VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc;
ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // DET_SIG_PIN
ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; /* DET_SIG_PIN */
adc_flags |= 0x01; // Fast mode enabled
adc_flags |= 0x01;
}
}
@ -72,19 +89,21 @@ void adc_restore_default(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
// Disable oversampling
ADC0.CTRLB &= ~ADC_SAMPNUM_gm;
ADC0.CTRLC &= ~ADC_REFSEL_gm;
ADC0.CTRLC |= ADC_REFSEL_VREFA_gc; // VREFA as reference
ADC0.CTRLC |= ADC_REFSEL_VREFA_gc;
ADC0.EVCTRL &= ~ADC_STARTEI_bm;
ADC0.INTCTRL &= ~ADC_RESRDY_bm;
adc_flags &= ~0x01; // Fast mode disabled
adc_flags &= ~0x01;
}
}
/**
* @brief Trigger a single conversion and block until the result is ready.
*/
static uint16_t adc_conversion(void)
{
ADC0.COMMAND = ADC_STCONV_bm;
@ -93,48 +112,45 @@ static uint16_t adc_conversion(void)
return ADC0.RES;
}
static uint16_t adc_read_tempsense(void)
uint16_t adc_read_tempsense(void)
{
/* DS40002015B-page 425
1. Configure the internal voltage reference to 1.1V by configuring the VREF peripheral.
2. Select the internal voltage reference by writing the REFSEL bits in ADCn.CTRLC to 0x0.
3. Select the ADC temperature sensor channel by configuring the MUXPOS register
(ADCn.MUXPOS). This enables the temperature sensor.
5. In ADCn.SAMPCTRL select INITDLY 32 µs × CLK_ADC
6. In ADCn.CTRLC select SAMPLEN 32 µs × CLK_ADC
7. Acquire the temperature sensor output voltage by starting a conversion.
8. Process the measurement result as described below.
*/
/*
* Microchip DS40002015B p. 425 procedure:
* 1. Configure VREF to 1.1 V via the VREF peripheral.
* 2. Select internal voltage reference (REFSEL = 0x0 in ADCn.CTRLC).
* 3. Select the temperature sensor channel via ADCn.MUXPOS.
* 4. Set INITDLY 32 µs × CLK_ADC in ADCn.SAMPCTRL.
* 5. Set SAMPLEN 32 µs × CLK_ADC in ADCn.CTRLC.
* 6. Start a conversion to acquire the sensor output voltage.
* 7. Process the result as below.
*/
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
ADC0.CTRLA |= ADC_ENABLE_bm;
ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc;
ADC0.CTRLC = 0; // reset
ADC0.CTRLC = 0;
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_INTREF_gc;
// Configure VREF, Microchip DS40002015B page 424
VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc;
// Select temperature sensor channel
ADC0.MUXPOS = ADC_MUXPOS_TEMPSENSE_gc;
}
delay(10);
int8_t sigrow_offset = SIGROW.TEMPSENSE1; // Read signed value from signature row
int8_t sigrow_offset = SIGROW.TEMPSENSE1;
uint8_t sigrow_gain = SIGROW.TEMPSENSE0;
// Read unsigned value from signature row
uint16_t adc_reading = adc_conversion() >> 4;
// ADC conversion result with 1.1 V internal reference
/* Conversion result with 1.1 V internal reference; result may exceed 16
* bits during the multiply (10-bit reading × 8-bit gain). */
uint32_t temp = adc_reading - sigrow_offset;
temp *= sigrow_gain; // Result might overflow 16 bit variable (10bit+8bit)
temp += 0x80;
// Add 1/2 to get correct rounding on division below
temp *= sigrow_gain;
temp += 0x80; /* Round-half-up before the >>8 division. */
temp >>= 8;
// Divide result to get Kelvin
uint16_t temperature_in_K = temp;
uint16_t temperature_in_C = temp - 273; /* Convert from Kelvin to Celsius. */
adc_restore_default();
@ -142,73 +158,5 @@ static uint16_t adc_read_tempsense(void)
adc_enable_fast();
}
return temperature_in_K;
}
void adc_cmd_calibrate(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
ADC0.EVCTRL &= ~ADC_STARTEI_bm; // Start event input
ADC0.INTCTRL &= ~ADC_RESRDY_bm;
ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc; // 16 samples for oversampling to get 12-bit result from 10-bit ADC
ADC0.CTRLC = 0; // reset
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VDDREF_gc; // CLK_PER/32, VDD as reference
// Configure VREF, Microchip DS40002015B page 424
VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc;
}
delay(10); // Wait for VREF to stabilize
// set MUX to gnd and read offset
ADC0.MUXPOS = ADC_MUXPOS_GND_gc;
delay(1);
uint16_t offset = adc_conversion() >> 2; // 12-bit result
Serial.write(offset & 0xFF);
Serial.write(offset >> 8);
if (config.adccal0 == 0xFFFF) {
config.adccal0 = offset;
}
// set MUX to external AREF and read 3.0V value
ADC0.MUXPOS = ADC_MUXPOS_AIN7_gc; // AREF pin
delay(1);
uint16_t aref = adc_conversion() >> 2; // 12-bit result
Serial.write(aref & 0xFF);
Serial.write(aref >> 8);
if (config.adccal3 == 0xFFFF) {
config.adccal3 = aref;
}
// set MUX to DACREF0 and read
ADC0.MUXPOS = ADC_MUXPOS_DACREF_gc;
delay(1);
uint16_t dacref = adc_conversion() >> 2; // 12-bit result
Serial.write(dacref & 0xFF);
Serial.write(dacref >> 8);
uint16_t tempsense = adc_read_tempsense();
Serial.write(tempsense & 0xFF);
Serial.write(tempsense >> 8);
Serial.write(SIGROW.TEMPSENSE1);
Serial.write(SIGROW.TEMPSENSE0);
Serial.flush();
SERIAL_BUFFER_CLEAR();
adc_restore_default();
if (adc_flags & 0x01) {
adc_enable_fast();
}
return temperature_in_C * 10; /* Return in 0.1 °C for consistency */
}

View file

@ -1,57 +1,61 @@
/*
* @file adc.h
* @brief
*
* @brief ATmega4809 ADC0 driver and engineering-unit conversions.
*
* Created: 27.09.2025 05:06:42
* Author: ThePetrovich
*
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
* See the LICENSE file for details.
*
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef ADC_H_
#define ADC_H_
#include <stdint.h>
/**
* @brief Enable ADC for fast radiation detection
* @brief Enable ADC0 in fast event-driven mode for radiation pulse capture.
* ADC0 conversions are started by the detector trigger via EVSYS and
* completed by the ADC0_RESRDY ISR.
*/
void adc_enable_fast(void);
/**
* @brief Restore ADC to default settings
* @brief Restore ADC0 to its default configuration suitable for blocking
* analogRead()-style reads of housekeeping channels.
*/
void adc_restore_default(void);
/**
* @brief Convert ADC reading to temperature in 0.1°C
* @param adc_value 12-bit ADC reading
* @return Temperature in 0.1°C
* @brief Convert a raw ADC reading to temperature in 0.1 °C.
* Uses calibration values from @c config when available.
* @param adc_value Raw ADC reading.
* @return Temperature in 0.1 °C.
*/
int16_t adc_to_temperature_c(uint16_t adc_value);
/**
* @brief Convert ADC reading to voltage in mV
* @param adc_value ADC reading
* @return Voltage in mV
* @brief Convert a raw ADC reading from the V28 feedback divider to mV.
* @param adc_value Raw ADC reading.
* @return Voltage at the sensor input in mV.
*/
uint16_t adc_to_voltage_mv(uint16_t adc_value);
/**
* @brief Read ADC with oversampling
* @param pin Analog pin to read
* @param samples Number of samples for oversampling
* @return Averaged ADC value
* @brief Block-read an analog pin with software oversampling.
* @param pin Analog pin to read.
* @param samples Sample count (1, 4, 16, or 64).
* @return Sum of samples right-shifted by log2(samples) to give the average.
*/
uint16_t adc_read_oversampled(uint8_t pin, uint8_t samples);
/**
* @brief Command to calibrate ADC and read reference values
* @brief Read the MCU internal temperature sensor (blocking, ~10 ms).
* @return Temperature in Kelvin.
*/
void adc_cmd_calibrate(void);
uint16_t adc_read_tempsense(void);
#endif // ADC_H_
#endif /* ADC_H_ */

View file

@ -1,6 +1,7 @@
/*
* @file config.h
* @brief System configuration and constants
* @brief System configuration, persisted-config struct definition, and
* compile-time constants shared between modules.
*
* Created: 21.09.2025
* Author: ThePetrovich
@ -14,86 +15,122 @@
#ifndef CONFIG_H
#define CONFIG_H
// Firmware version
#define FIRMWARE_VERSION_MAJOR 2
#define FIRMWARE_VERSION_MINOR 03
#include <stdint.h>
// Serial communication
/* Firmware version */
#define FIRMWARE_VERSION_MAJOR 3
#define FIRMWARE_VERSION_MINOR 00
/* Serial communication */
#define SERIAL_BAUD_RATE 38400
// Timing constants
/* Timing constants */
#define STATUS_LED_BLINK_PERIOD 500U
#define MAIN_LOOP_DELAY 10
#define CPM_UPDATE_PERIOD 10000U
#define RSENSE_UPDATE_PERIOD 1000U
#define RSENSE_CHANNEL_16_MASK 0x7FFU // 11-bit channel mask
#define RSENSE_CHANNEL_32_MASK 0x3FFU // 10-bit channel mask
#define RSENSE_CHANNEL_COUNT 2048
#define RSENSE_CHANNEL_32_COUNT 1024
/* Spectrum channel masks and counts */
#define RSENSE_CHANNEL_MASK 0x3FFU /* 10-bit channel mask */
#define RSENSE_CHANNEL_COUNT 1024
// Temperature sensor constants (MCP9700)
#define MCP9700_OFFSET_MV 500L // 500 mV at 0°C
#define MCP9700_SCALE_MV 10L // 10 mV per degree
#define ADC_OVERSAMPLING_BITS 2 // 16x oversampling -> 2 extra bits
#define ADC_VREF_MV 3000L // 3.0V reference
#define ADC_RESOLUTION 1024L // 10-bit ADC
/* MCP9700 temperature sensor calibration constants */
#define MCP9700_OFFSET_MV 500L /* 500 mV at 0 °C */
#define MCP9700_SCALE_MV 10L /* 10 mV per °C */
#define ADC_OVERSAMPLING_BITS 2 /* 16x oversampling -> 2 extra bits */
#define ADC_VREF_MV 3000L /* 3.0 V reference */
#define ADC_RESOLUTION 1024L /* 10-bit ADC */
// Voltage divider constants
#define VOLTAGE_DIVIDER_RATIO 20.0f // 200k and 10k resistors
#define VOLTAGE_MV_PER_STEP 14.6484375f // (3.0 * 20) / 4096
/* Voltage divider constants */
#define VOLTAGE_DIVIDER_RATIO 20.0f /* 200k and 10k resistors */
#define VOLTAGE_MV_PER_STEP 14.6484375f /* (3.0 * 20) / 4096 */
// EEPROM addresses for potentiometer settings
#define EEPROM_POT_HV_ADDR 0
#define EEPROM_POT_AMP_ADDR 1
#define EEPROM_POT_DET_ADDR 2
#define EEPROM_MAGIC_ADDR 3
#define EEPROM_MAGIC_VALUE 0xA5 // Magic value to check if EEPROM is initialized
// Default potentiometer values
/* Default potentiometer wiper value (mid-scale, 127/255) */
#define DEFAULT_POT_VALUE 127
// Light sensor constants
/* Light sensor I2C constants */
#define LSENSE_I2C_BASE_ADDR 0x38
#define LSENSE_EXPECTED_ID 0xE0
#define LSENSE_DATA_SIZE 12
/**
* @brief AD5160 wiper settings for the three digital potentiometers.
*/
typedef struct potentiometers_t {
uint8_t pot_hv; /**< HV regulator wiper. */
uint8_t pot_amp; /**< Amplifier gain wiper. */
uint8_t pot_det; /**< Detector threshold wiper. */
} __attribute__((packed)) potentiometers_t;
/**
* @brief ADC and temperature-sensor calibration coefficients.
* Reused as the payload of #CMD_SET_CALIBRATION.
*/
typedef struct calibration_t {
uint16_t adccal0; /**< ADC reading at GND (offset). */
uint16_t adccal3; /**< ADC reading at 3.0 V reference. */
uint16_t tempcal; /**< Temperature offset correction. */
} __attribute__((packed)) calibration_t;
/**
* @brief Runtime feature flags packed into a single byte.
* Reused as the payload of #CMD_SET_FLAGS.
*/
typedef union config_flags_t {
struct __attribute__((packed)) {
/** Spectrum oversampling: 0/1/2/3 = 1x/4x/16x/64x. */
uint8_t spectrum_oversample_bits : 2;
uint8_t reserved1 : 1;
/** 0 = disable, 1 = enable temperature drift compensation. */
uint8_t temperature_drift_compensation : 1;
/** ADC oversampling for temperature/voltage: 0/1/2/3 = 1x/4x/16x/64x. */
uint8_t adc_oversample_bits : 2;
uint8_t reserved2 : 2;
} fields;
uint8_t value;
} __attribute__((packed)) config_flags_t;
/**
* @brief Temperature drift compensation parameters.
* Reused as the payload of #CMD_SET_TEMP_DRIFT_COMPENSATION.
*
* Drift coefficients are in pot_units/255 per °C. When @c pot_X_low equals
* @c pot_X_high, drift compensation for that channel is disabled.
*
* Sensor potentiometer mapping:
* - temp_drift : DET_TEMP (SiPM carrier) pot_hv
* - vbias_drift : HV_TEMP (+28V supply) pot_hv
* - gain_drift : AMP_TEMP (amplifier) pot_amp
* - threshold_drift : AMP_TEMP (amplifier) pot_det
*/
typedef struct temp_drift_compensation_t {
int16_t temp_drift;
int16_t vbias_drift;
int16_t gain_drift;
int16_t threshold_drift;
uint32_t drift_period_ms; /**< Minimum interval between adjustments, in ms. */
uint8_t pot_hv_low;
uint8_t pot_hv_high;
uint8_t pot_amp_low;
uint8_t pot_amp_high;
uint8_t pot_det_low;
uint8_t pot_det_high;
} __attribute__((packed)) temp_drift_compensation_t;
/**
* @brief Persistent device configuration.
*
* Stored in EEPROM with dual-partition wear leveling (see eeprom.cpp).
* The pair @c magic1 / @c magic2 (0xA5 / 0x5A) marks a partition as valid.
*/
typedef struct config_t {
uint8_t magic1; // 0xA5 to indicate valid config
uint8_t magic2; // 0x5A to indicate valid config
uint8_t pot_hv;
uint8_t pot_amp;
uint8_t pot_det;
struct {
uint16_t adccal0; // ADC offset at GND
uint16_t adccal3; // ADC reading at 3.0V reference
uint16_t tempcal;
uint16_t tempdrift_mvK; // SiPM temperature drift compensation in mV/K, normally around 20 mV/K
float gainlow; // gain value for pot_amp = 0
float gainhigh; // gain value for pot_amp = 255
} calibration __attribute__((packed));
union {
struct {
uint8_t spectrum_oversample_bits
: 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x oversampling, only for spectrum measurement
uint8_t spectrum_channel_bits : 1; // 0 for 16-bit channels, 1 for 32-bit channels
uint8_t temperature_drift_compensation
: 1; // 0 to disable, 1 to enable temperature compensation
uint8_t adc_oversample_bits : 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x ADC oversampling for
// temperature and voltage measurements
uint8_t reserved : 2;
} fields __attribute__((packed));
uint8_t value;
} flags __attribute__((packed));
struct {
uint8_t tempdrift_pot_hv_low;
uint8_t tempdrift_pot_hv_high;
uint8_t tempdrift_pot_amp_low;
uint8_t tempdrift_pot_amp_high;
uint8_t tempdrift_pot_det_low;
uint8_t tempdrift_pot_det_high;
} temp_drift_compensation_ranges __attribute__((packed));
uint8_t magic1; /**< 0xA5 — valid-partition marker, byte 1. */
uint8_t magic2; /**< 0x5A — valid-partition marker, byte 2. */
potentiometers_t pots;
calibration_t calibration;
config_flags_t flags;
temp_drift_compensation_t temp_drift_compensation;
} __attribute__((packed)) config_t;
extern config_t config;
#endif // CONFIG_H
#endif /* CONFIG_H */

View file

@ -1,6 +1,6 @@
/*
* @file eeprom.cpp
* @brief EEPROM management implementation
* @brief EEPROM management with dual-partition wear leveling.
*
* Created: 21.09.2025
* Author: ThePetrovich
@ -18,35 +18,127 @@
config_t config;
/*
* Each partition occupies sizeof(config_t)+1 bytes:
* [0 .. sizeof(config_t)-1] config_t struct
* [sizeof(config_t)] generation counter (uint8_t)
*/
#define PARTITION_SIZE ((uint16_t)(sizeof(config_t) + 1))
#define PARTITION_A_ADDR 0
#define PARTITION_B_ADDR PARTITION_SIZE
#define GEN_OFFSET ((uint16_t)sizeof(config_t))
/**
* @brief Track which partition was last written so saves alternate.
* 0 = partition A is current, 1 = partition B is current.
*/
static uint8_t g_active_partition = 0;
/**
* @brief Check whether the partition at @p base contains a valid magic pair.
*/
static bool partition_valid(uint16_t base)
{
uint8_t m1 = EEPROM.read(base + offsetof(config_t, magic1));
uint8_t m2 = EEPROM.read(base + offsetof(config_t, magic2));
return (m1 == 0xA5 && m2 == 0x5A);
}
/**
* @brief Read a config_t struct from the partition at @p base into @p dst.
*/
static void read_partition(uint16_t base, config_t *dst)
{
uint8_t *p = (uint8_t *)dst;
for (uint16_t i = 0; i < sizeof(config_t); i++) {
p[i] = EEPROM.read(base + i);
}
}
/**
* @brief Write @p src and a generation byte to the partition at @p base.
* Uses EEPROM.update() so unchanged cells incur no wear.
*/
static void write_partition(uint16_t base, const config_t *src, uint8_t generation)
{
const uint8_t *p = (const uint8_t *)src;
for (uint16_t i = 0; i < sizeof(config_t); i++) {
EEPROM.update(base + i, p[i]);
}
EEPROM.update(base + GEN_OFFSET, generation);
}
/**
* @brief Populate @p cfg with safe factory defaults.
* Used on first boot or when both partitions are corrupt.
*/
static void set_defaults(config_t *cfg)
{
cfg->magic1 = 0xA5;
cfg->magic2 = 0x5A;
cfg->pots.pot_hv = DEFAULT_POT_VALUE;
cfg->pots.pot_amp = DEFAULT_POT_VALUE;
cfg->pots.pot_det = DEFAULT_POT_VALUE;
cfg->calibration.adccal0 = 0xFFFF;
cfg->calibration.adccal3 = 0xFFFF;
cfg->calibration.tempcal = 0;
cfg->flags.value = 0;
/* pot_low == pot_high disables drift compensation per channel. */
cfg->temp_drift_compensation.temp_drift = 0;
cfg->temp_drift_compensation.vbias_drift = 0;
cfg->temp_drift_compensation.gain_drift = 0;
cfg->temp_drift_compensation.threshold_drift = 0;
cfg->temp_drift_compensation.drift_period_ms = 5000;
cfg->temp_drift_compensation.pot_hv_low = DEFAULT_POT_VALUE;
cfg->temp_drift_compensation.pot_hv_high = DEFAULT_POT_VALUE;
cfg->temp_drift_compensation.pot_amp_low = DEFAULT_POT_VALUE;
cfg->temp_drift_compensation.pot_amp_high = DEFAULT_POT_VALUE;
cfg->temp_drift_compensation.pot_det_low = DEFAULT_POT_VALUE;
cfg->temp_drift_compensation.pot_det_high = DEFAULT_POT_VALUE;
}
void eeprom_init(void)
{
if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) {
potentiometer_settings_t default_settings = {
.hv_pot = DEFAULT_POT_VALUE, .amp_pot = DEFAULT_POT_VALUE, .det_pot = DEFAULT_POT_VALUE};
bool a_valid = partition_valid(PARTITION_A_ADDR);
bool b_valid = partition_valid(PARTITION_B_ADDR);
eeprom_save_pot_settings(&default_settings);
EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE);
if (a_valid && b_valid) {
uint8_t gen_a = EEPROM.read(PARTITION_A_ADDR + GEN_OFFSET);
uint8_t gen_b = EEPROM.read(PARTITION_B_ADDR + GEN_OFFSET);
/* Signed delta handles wrap-around: B is newer if (gen_b - gen_a) > 0 mod 256. */
if ((int8_t)(gen_b - gen_a) > 0) {
read_partition(PARTITION_B_ADDR, &config);
g_active_partition = 1;
} else {
read_partition(PARTITION_A_ADDR, &config);
g_active_partition = 0;
}
} else if (a_valid) {
read_partition(PARTITION_A_ADDR, &config);
g_active_partition = 0;
} else if (b_valid) {
read_partition(PARTITION_B_ADDR, &config);
g_active_partition = 1;
} else {
set_defaults(&config);
write_partition(PARTITION_A_ADDR, &config, 0);
g_active_partition = 0;
}
}
void eeprom_load_pot_settings(potentiometer_settings_t *settings)
void eeprom_save_config(void)
{
if (settings == nullptr) {
return;
}
/* Write to whichever partition is NOT currently active. */
uint8_t target_partition = (g_active_partition == 0) ? 1 : 0;
uint16_t target_base = (target_partition == 0) ? PARTITION_A_ADDR : PARTITION_B_ADDR;
uint16_t active_base = (g_active_partition == 0) ? PARTITION_A_ADDR : PARTITION_B_ADDR;
settings->hv_pot = EEPROM.read(EEPROM_POT_HV_ADDR);
settings->amp_pot = EEPROM.read(EEPROM_POT_AMP_ADDR);
settings->det_pot = EEPROM.read(EEPROM_POT_DET_ADDR);
}
void eeprom_save_pot_settings(const potentiometer_settings_t *settings)
{
if (settings == nullptr) {
return;
}
EEPROM.write(EEPROM_POT_HV_ADDR, settings->hv_pot);
EEPROM.write(EEPROM_POT_AMP_ADDR, settings->amp_pot);
EEPROM.write(EEPROM_POT_DET_ADDR, settings->det_pot);
uint8_t old_gen = EEPROM.read(active_base + GEN_OFFSET);
uint8_t new_gen = old_gen + 1;
config.magic1 = 0xA5;
config.magic2 = 0x5A;
write_partition(target_base, &config, new_gen);
g_active_partition = target_partition;
}

View file

@ -1,6 +1,16 @@
/*
* @file eeprom.h
* @brief EEPROM management for persistent settings
* @brief EEPROM management for persistent configuration storage.
*
* Dual-partition layout in ATmega4809 EEPROM (256 bytes):
*
* [0 .. sizeof(config_t)] Partition A: config_t + 1-byte generation counter
* [sizeof(config_t)+1 .. 2*(sizeof(config_t)+1)-1] Partition B: same layout
*
* On save, the alternate (older) partition is overwritten. If power fails
* mid-write the other partition retains the last good config. The generation
* counter wraps around uint8_t; the partition with the higher counter
* (mod-256 comparison via signed delta) is the newer one.
*
* Created: 21.09.2025
* Author: ThePetrovich
@ -17,30 +27,16 @@
#include <stdint.h>
/**
* @brief Potentiometer settings structure
*/
typedef struct {
uint8_t hv_pot; ///< High voltage potentiometer value
uint8_t amp_pot; ///< SiPM Pre-amp gain potentiometer value
uint8_t det_pot; ///< Detection threshold potentiometer value
} potentiometer_settings_t;
/**
* @brief Initialize EEPROM settings
* Sets default values if EEPROM is uninitialized
* @brief Load the global @c config from EEPROM.
* Picks the freshest valid partition; writes factory defaults if both
* partitions are blank or corrupt.
*/
void eeprom_init(void);
/**
* @brief Load potentiometer settings from EEPROM
* @param settings Pointer to settings structure to populate
* @brief Persist the current global @c config to the alternate partition,
* bumping the generation counter to flip the active partition.
*/
void eeprom_load_pot_settings(potentiometer_settings_t *settings);
void eeprom_save_config(void);
/**
* @brief Save potentiometer settings to EEPROM
* @param settings Pointer to settings structure to save
*/
void eeprom_save_pot_settings(const potentiometer_settings_t *settings);
#endif // DET_EEPROM_H
#endif /* DET_EEPROM_H */

View file

@ -1,6 +1,6 @@
/*
* @file iodefs.h
* @brief
* @brief Hardware pin assignments for the SBC firmware.
*
* Created: 21.09.2025 05:39:48
* Author: ThePetrovich
@ -18,32 +18,38 @@
#include <avr/io.h>
#include <avr/pgmspace.h>
#define DET_TRIG_PIN 21 // PC7, event input
#define DET_READ_PIN A3 // PD3 (ADC3)
#define DET_PREAMP_PIN A2 // PD2 (ADC2)
/* Detector signal path */
#define DET_TRIG_PIN 21 /* PC7, event input */
#define DET_READ_PIN A3 /* PD3 (ADC3) */
#define DET_PREAMP_PIN A2 /* PD2 (ADC2) */
#define STATUS_LED 31 // PE1
/* Status indicator */
#define STATUS_LED 31 /* PE1 */
#define HV_EN 16 // PC2
#define DET_EN 18 // PC4
#define DET_RST 20 // PC6
/* Power and reset control */
#define HV_EN 16 /* PC2 */
#define DET_EN 18 /* PC4 */
#define DET_RST 20 /* PC6 */
#define V28V0_FB_PIN A0 // PD0 (ADC0)
#define HV_TEMP_PIN A1 // PD1 (ADC1)
#define AMP_TEMP_PIN A4 // PD4 (ADC4)
#define DET_TEMP_PIN A5 // PD5 (ADC5)
/* Housekeeping ADC inputs */
#define V28V0_FB_PIN A0 /* PD0 (ADC0) */
#define HV_TEMP_PIN A1 /* PD1 (ADC1) */
#define AMP_TEMP_PIN A4 /* PD4 (ADC4) */
#define DET_TEMP_PIN A5 /* PD5 (ADC5) */
#define HV_CS 15 // PC1
#define AMP_CS 17 // PC3
#define DET_CS 19 // PC5
/* AD5160 digital potentiometer chip selects */
#define HV_CS 15 /* PC1 */
#define AMP_CS 17 /* PC3 */
#define DET_CS 19 /* PC5 */
#define SCL0 8 // PB0
#define SDA0 9 // PB1
/* Light sensor I2C bus pins (software-bitbanged) */
#define SCL0 8 /* PB0 */
#define SDA0 9 /* PB1 */
#define SCL1 10 // PB2
#define SDA1 11 // PB3
#define SCL1 10 /* PB2 */
#define SDA1 11 /* PB3 */
#define SCL2 12 // PB4
#define SDA2 13 // PB5
#define SCL2 12 /* PB4 */
#define SDA2 13 /* PB5 */
#endif // LSENSE_IODEFS_H
#endif /* LSENSE_IODEFS_H */

View file

@ -1,6 +1,7 @@
/*
* @file lsense.cpp
* @brief Light sensor implementation
* @brief Light sensor implementation: software I2C, detection, and channel
* readout for the six TCS-style light sensors on three buses.
*
* Created: 21.09.2025
* Author: ThePetrovich
@ -21,9 +22,6 @@
static SoftwareI2C g_i2c_buses[NUM_BUSES];
/**
* @brief Light sensor data structure
*/
typedef union __attribute__((packed)) {
struct __attribute__((packed)) {
uint16_t red;
@ -39,21 +37,9 @@ typedef union __attribute__((packed)) {
static light_sensor_data_t g_sensor_data[NUM_BUSES][SENSORS_PER_BUS];
static uint8_t g_sensor_addresses[NUM_BUSES][SENSORS_PER_BUS];
// Presence counters
static uint8_t g_sensors_detected_total = 0;
static uint8_t g_sensors_detected_per_bus[NUM_BUSES];
static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr);
static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_sensor_data_t *sensor_data);
static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr);
static void initialize_i2c_bus(int bus_number, bool pin_order_inverted);
static void detect_sensors_on_bus_generic(int bus_number);
/**
* @brief Configure a light sensor for measurement
* @param i2c_bus Reference to I2C bus instance
* @param sensor_addr LSB of sensor address (0 or 1)
*/
static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr)
{
uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr;
@ -64,17 +50,11 @@ static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr)
i2c_bus.beginTransmission(addr);
i2c_bus.write(0x41);
i2c_bus.write(0b00101101); // IR gain x1, RGB gain x1, 35ms mode
i2c_bus.write(0b00010000); // RGB_EN = 1, measurement active
i2c_bus.write(0b00101101); /* IR gain x1, RGB gain x1, 35ms mode */
i2c_bus.write(0b00010000); /* RGB_EN = 1, measurement active */
i2c_bus.endTransmission();
}
/**
* @brief Read data from a light sensor
* @param i2c_bus Reference to I2C bus instance
* @param sensor_addr LSB of sensor address (0 or 1)
* @param sensor_data Pointer to data structure to populate
*/
static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_sensor_data_t *sensor_data)
{
if (sensor_data == nullptr)
@ -83,7 +63,7 @@ static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_s
uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr;
i2c_bus.beginTransmission(addr);
i2c_bus.write(0x50); // Start from red register
i2c_bus.write(0x50);
i2c_bus.endTransmission();
i2c_bus.requestFrom(addr, (uint8_t)LSENSE_DATA_SIZE);
@ -93,37 +73,25 @@ static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_s
i2c_bus.endTransmission();
}
/**
* @brief Detect if a light sensor is present
* @param i2c_bus Reference to I2C bus instance
* @param sensor_addr LSB of sensor address (0 or 1)
* @return Manufacturer ID (0xE0 if valid sensor detected, 0 otherwise)
*/
static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr)
{
uint8_t response = 0;
uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr;
i2c_bus.beginTransmission(addr);
i2c_bus.write(0x92); // Manufacturer ID register
i2c_bus.write(0x92);
i2c_bus.endTransmission();
i2c_bus.requestFrom(addr, (uint8_t)1);
if (i2c_bus.available()) {
response = i2c_bus.read();
if (response == 0xFF) {
response = 0; // Invalid response
response = 0;
}
}
return response;
}
/**
* @brief Initialize an I2C bus and configure sensors
* @param bus_number Bus number (0, 1, or 2)
* @param pin_order_inverted Whether to invert SDA/SCL pin order
*/
static void initialize_i2c_bus(int bus_number, bool pin_order_inverted)
{
uint8_t sda_pin, scl_pin;
@ -142,29 +110,22 @@ static void initialize_i2c_bus(int bus_number, bool pin_order_inverted)
scl_pin = pin_order_inverted ? SDA2 : SCL2;
break;
default:
// Invalid bus number
return;
}
SoftwareI2C *i2c_bus = &g_i2c_buses[bus_number];
i2c_bus->begin(sda_pin, scl_pin);
configure_light_sensor(*i2c_bus, 0);
configure_light_sensor(*i2c_bus, 1);
}
/**
* @brief Generic function to detect sensors on any I2C bus
* @param bus_number Bus number (0, 1, or 2)
*/
static void detect_sensors_on_bus_generic(int bus_number)
static void detect_sensors_on_bus(int bus_number)
{
if (bus_number < 0 || bus_number >= NUM_BUSES)
return;
g_sensors_detected_per_bus[bus_number] = 0;
// Try normal pin order first
initialize_i2c_bus(bus_number, false);
g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0);
if (g_sensor_addresses[bus_number][SENSOR_NEG] == LSENSE_EXPECTED_ID) {
@ -177,7 +138,6 @@ static void detect_sensors_on_bus_generic(int bus_number)
g_sensors_detected_per_bus[bus_number]++;
}
// If no sensors found, try inverted pin order
if (g_sensors_detected_per_bus[bus_number] == 0) {
initialize_i2c_bus(bus_number, true);
g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0);
@ -193,62 +153,67 @@ static void detect_sensors_on_bus_generic(int bus_number)
}
}
/*******************************************************************************/
/*******************************************************************************/
/* Command handlers */
/*******************************************************************************/
/*******************************************************************************/
void lsense_cmd_presence(void)
void lsense_detect_all(void)
{
g_sensors_detected_total = 0;
detect_sensors_on_bus_generic(BUS_X);
detect_sensors_on_bus_generic(BUS_Y);
detect_sensors_on_bus_generic(BUS_Z);
Serial.write(g_sensors_detected_total);
Serial.write(g_sensors_detected_per_bus[BUS_X]);
Serial.write(g_sensor_addresses[BUS_X][SENSOR_NEG]);
Serial.write(g_sensor_addresses[BUS_X][SENSOR_POS]);
Serial.write(g_sensors_detected_per_bus[BUS_Y]);
Serial.write(g_sensor_addresses[BUS_Y][SENSOR_NEG]);
Serial.write(g_sensor_addresses[BUS_Y][SENSOR_POS]);
Serial.write(g_sensors_detected_per_bus[BUS_Z]);
Serial.write(g_sensor_addresses[BUS_Z][SENSOR_NEG]);
Serial.write(g_sensor_addresses[BUS_Z][SENSOR_POS]);
Serial.flush();
SERIAL_BUFFER_CLEAR();
detect_sensors_on_bus(BUS_X);
detect_sensors_on_bus(BUS_Y);
detect_sensors_on_bus(BUS_Z);
}
void lsense_cmd_read(void)
void lsense_get_presence(lsense_presence_t *out)
{
detect_sensors_on_bus_generic(BUS_X);
read_light_sensor(g_i2c_buses[BUS_X], 0, &g_sensor_data[BUS_X][SENSOR_NEG]);
read_light_sensor(g_i2c_buses[BUS_X], 1, &g_sensor_data[BUS_X][SENSOR_POS]);
if (out == nullptr)
return;
out->total_detected = g_sensors_detected_total;
out->bus_x_count = g_sensors_detected_per_bus[BUS_X];
out->bus_x_neg_addr = g_sensor_addresses[BUS_X][SENSOR_NEG];
out->bus_x_pos_addr = g_sensor_addresses[BUS_X][SENSOR_POS];
out->bus_y_count = g_sensors_detected_per_bus[BUS_Y];
out->bus_y_neg_addr = g_sensor_addresses[BUS_Y][SENSOR_NEG];
out->bus_y_pos_addr = g_sensor_addresses[BUS_Y][SENSOR_POS];
out->bus_z_count = g_sensors_detected_per_bus[BUS_Z];
out->bus_z_neg_addr = g_sensor_addresses[BUS_Z][SENSOR_NEG];
out->bus_z_pos_addr = g_sensor_addresses[BUS_Z][SENSOR_POS];
}
detect_sensors_on_bus_generic(BUS_Y);
read_light_sensor(g_i2c_buses[BUS_Y], 0, &g_sensor_data[BUS_Y][SENSOR_NEG]);
read_light_sensor(g_i2c_buses[BUS_Y], 1, &g_sensor_data[BUS_Y][SENSOR_POS]);
static uint16_t extract_channel(const light_sensor_data_t *d, uint8_t channel)
{
switch (channel) {
case 0:
return d->data.red;
case 1:
return d->data.green;
case 2:
return d->data.blue;
case 3:
return d->data.ir;
case 4:
return d->data.green2;
case 5: /* visible: R+G+B+G2 */
return (uint16_t)((uint32_t)d->data.red + d->data.green + d->data.blue + d->data.green2);
case 6: /* all: R+G+B+G2+IR */
return (uint16_t)((uint32_t)d->data.red + d->data.green + d->data.blue + d->data.green2 +
d->data.ir);
default:
return 0;
}
}
detect_sensors_on_bus_generic(BUS_Z);
read_light_sensor(g_i2c_buses[BUS_Z], 0, &g_sensor_data[BUS_Z][SENSOR_NEG]);
read_light_sensor(g_i2c_buses[BUS_Z], 1, &g_sensor_data[BUS_Z][SENSOR_POS]);
void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out)
{
if (out == nullptr)
return;
Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue >> 8);
Serial.write(g_sensor_data[BUS_X][SENSOR_POS].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_X][SENSOR_POS].data.blue >> 8);
Serial.write(g_sensor_data[BUS_Y][SENSOR_NEG].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_Y][SENSOR_NEG].data.blue >> 8);
Serial.write(g_sensor_data[BUS_Y][SENSOR_POS].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_Y][SENSOR_POS].data.blue >> 8);
Serial.write(g_sensor_data[BUS_Z][SENSOR_NEG].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_Z][SENSOR_NEG].data.blue >> 8);
Serial.write(g_sensor_data[BUS_Z][SENSOR_POS].data.blue & 0xFF);
Serial.write(g_sensor_data[BUS_Z][SENSOR_POS].data.blue >> 8);
for (int bus = 0; bus < NUM_BUSES; bus++) {
read_light_sensor(g_i2c_buses[bus], 0, &g_sensor_data[bus][SENSOR_NEG]);
read_light_sensor(g_i2c_buses[bus], 1, &g_sensor_data[bus][SENSOR_POS]);
}
Serial.flush();
SERIAL_BUFFER_CLEAR();
}
out->values[0] = extract_channel(&g_sensor_data[BUS_X][SENSOR_NEG], channel);
out->values[1] = extract_channel(&g_sensor_data[BUS_X][SENSOR_POS], channel);
out->values[2] = extract_channel(&g_sensor_data[BUS_Y][SENSOR_NEG], channel);
out->values[3] = extract_channel(&g_sensor_data[BUS_Y][SENSOR_POS], channel);
out->values[4] = extract_channel(&g_sensor_data[BUS_Z][SENSOR_NEG], channel);
out->values[5] = extract_channel(&g_sensor_data[BUS_Z][SENSOR_POS], channel);
}

View file

@ -1,8 +1,8 @@
/*
* @file lsense.h
* @brief Light sensor management and command handling
* @brief Light sensor management.
*
* Created: 21.09.2025 05:53:32
* Created: 21.09.2025
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
@ -14,30 +14,69 @@
#ifndef LSENSE_H
#define LSENSE_H
#include <Arduino.h>
#include <stdint.h>
#define NUM_BUSES 3
#define NUM_BUSES 3
#define SENSORS_PER_BUS 2
enum {
BUS_X = 0, // X-axis (bus 0)
BUS_Y = 1, // Y-axis (bus 1)
BUS_Z = 2 // Z-axis (bus 2)
};
enum {
SENSOR_NEG = 0, // Negative sensor (addresses 0)
SENSOR_POS = 1 // Positive sensor (addresses 1)
};
enum { BUS_X = 0, BUS_Y = 1, BUS_Z = 2 };
enum { SENSOR_NEG = 0, SENSOR_POS = 1 };
/**
* @brief Send light sensor presence information via serial
* @brief Detection result for the six light sensors across three I2C buses.
* Reused as the response payload of #CMD_LIGHT_SENSOR_PRESENCE.
*/
void lsense_cmd_presence(void);
typedef union lsense_presence_t {
struct __attribute__((packed)) {
uint8_t total_detected;
uint8_t bus_x_count;
uint8_t bus_x_neg_addr;
uint8_t bus_x_pos_addr;
uint8_t bus_y_count;
uint8_t bus_y_neg_addr;
uint8_t bus_y_pos_addr;
uint8_t bus_z_count;
uint8_t bus_z_neg_addr;
uint8_t bus_z_pos_addr;
};
uint8_t bytes[10];
} __attribute__((packed)) lsense_presence_t;
/**
* @brief Read and send light sensor data via serial
* @brief Per-sensor channel values for a single read across all 6 sensors.
* Reused as the response payload of #CMD_READ_LIGHT_SENSORS.
*/
void lsense_cmd_read(void);
typedef union lsense_channel_values_t {
struct __attribute__((packed)) {
uint16_t bus_x_neg_value;
uint16_t bus_x_pos_value;
uint16_t bus_y_neg_value;
uint16_t bus_y_pos_value;
uint16_t bus_z_neg_value;
uint16_t bus_z_pos_value;
};
uint16_t values[6];
} __attribute__((packed)) lsense_channel_values_t;
#endif // LSENSE_H
/**
* @brief Detect all sensors on all three buses.
* Populates internal state used by #lsense_get_presence and
* #lsense_read_channel.
*/
void lsense_detect_all(void);
/**
* @brief Fill @p out with detection results from the most recent
* #lsense_detect_all call.
*/
void lsense_get_presence(lsense_presence_t *out);
/**
* @brief Read a spectral channel from all six sensors into @p out.
* @param channel 0=red 1=green 2=blue 3=IR 4=green2
* 5=visible (R+G+B+G2) 6=all (R+G+B+G2+IR)
* @param out Output values, ordered X_neg, X_pos, Y_neg, Y_pos, Z_neg, Z_pos.
*/
void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out);
#endif /* LSENSE_H */

328
sbc_fw/protocol.cpp Normal file
View file

@ -0,0 +1,328 @@
/*
* @file protocol.cpp
* @brief Protocol framing, CRC, and command handler implementations.
*
* Created: 07.05.2026
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
* See the LICENSE file for details.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <Arduino.h>
#include <stdint.h>
#include <string.h>
#include <util/atomic.h>
#include "adc.h"
#include "config.h"
#include "eeprom.h"
#include "iodefs.h"
#include "lsense.h"
#include "protocol.h"
#include "rsense.h"
extern bool g_rsense_enabled; /* from rsense.cpp */
uint16_t protocol_crc16_update(uint16_t crc, const uint8_t *data, uint16_t len)
{
if (data == NULL) {
return crc;
}
while (len--) {
crc ^= (uint16_t)(*data++) << 8;
for (uint8_t i = 0; i < 8; i++) {
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1;
}
}
return crc;
}
uint16_t protocol_crc16_xmodem(const uint8_t *data, uint16_t len) { return protocol_crc16_update(0, data, len); }
/**
* @brief Write a frame header (CMD + LEN) to the serial port.
* Internal helper, not exported.
*/
static void send_header_bytes(uint16_t cmd, uint16_t length)
{
Serial.write((uint8_t)(cmd & 0xFF));
Serial.write((uint8_t)(cmd >> 8));
Serial.write((uint8_t)(length & 0xFF));
Serial.write((uint8_t)(length >> 8));
}
/**
* @brief Write a 16-bit CRC trailer (LSB first) and flush the TX queue.
*/
static void send_crc_trailer(uint16_t crc)
{
Serial.write((uint8_t)(crc & 0xFF));
Serial.write((uint8_t)(crc >> 8));
Serial.flush();
}
void protocol_send_message_v2(uint16_t cmd, const void *frag1, uint16_t len1, const void *frag2, uint16_t len2)
{
protocol_header_t hdr;
hdr.cmd = cmd;
hdr.length = (uint16_t)(len1 + len2);
uint16_t crc = protocol_crc16_update(0, (const uint8_t *)&hdr, sizeof(hdr));
crc = protocol_crc16_update(crc, (const uint8_t *)frag1, len1);
crc = protocol_crc16_update(crc, (const uint8_t *)frag2, len2);
send_header_bytes(hdr.cmd, hdr.length);
if (frag1 != NULL && len1 > 0) {
Serial.write((const uint8_t *)frag1, len1);
}
if (frag2 != NULL && len2 > 0) {
Serial.write((const uint8_t *)frag2, len2);
}
send_crc_trailer(crc);
}
void protocol_send_message(uint16_t cmd, const void *payload, uint16_t length)
{
protocol_send_message_v2(cmd, payload, length, NULL, 0);
}
void protocol_send_ack(void) { protocol_send_message(PROTOCOL_RESP_ACK, NULL, 0); }
void protocol_send_nak(void) { protocol_send_message(PROTOCOL_RESP_NAK, NULL, 0); }
void protocol_send_error(void) { protocol_send_message(PROTOCOL_RESP_ERR, NULL, 0); }
/**
* @brief Validate that a received payload is at least @p expected bytes long.
* Sends a NAK on the wire and returns false if the check fails.
*/
static bool require_payload(uint16_t length, uint16_t expected)
{
if (length < expected) {
protocol_send_nak();
return false;
}
return true;
}
void protocol_version_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
protocol_firmware_version_response_t resp;
resp.major = FIRMWARE_VERSION_MAJOR;
resp.minor = FIRMWARE_VERSION_MINOR;
protocol_send_message(CMD_FIRMWARE_VERSION, &resp, sizeof(resp));
}
void protocol_light_sensor_presence_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
lsense_detect_all();
protocol_light_sensor_presence_response_t resp;
lsense_get_presence(&resp);
protocol_send_message(CMD_LIGHT_SENSOR_PRESENCE, &resp, sizeof(resp));
}
void protocol_read_light_sensors_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_read_light_sensors_handler_t))) {
return;
}
protocol_read_light_sensors_handler_t *cmd = (protocol_read_light_sensors_handler_t *)payload;
if (cmd->channel > 6) {
protocol_send_nak();
return;
}
protocol_read_light_sensors_response_t resp;
lsense_read_channel(cmd->channel, &resp);
protocol_send_message(CMD_READ_LIGHT_SENSORS, &resp, sizeof(resp));
}
void protocol_get_configuration_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
protocol_send_message(CMD_GET_CONFIGURATION, &config, sizeof(config));
}
void protocol_set_configuration_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_configuration_handler_t))) {
return;
}
protocol_set_configuration_handler_t *cmd = (protocol_set_configuration_handler_t *)payload;
if (cmd->magic1 != 0xA5 || cmd->magic2 != 0x5A) {
protocol_send_nak();
return;
}
config = *cmd;
eeprom_save_config();
rsense_apply_potentiometers();
protocol_send_ack();
}
void protocol_set_potentiometers_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_potentiometers_handler_t))) {
return;
}
config.pots = *(protocol_set_potentiometers_handler_t *)payload;
rsense_apply_potentiometers();
eeprom_save_config();
protocol_send_ack();
}
void protocol_set_calibration_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_calibration_handler_t))) {
return;
}
config.calibration = *(protocol_set_calibration_handler_t *)payload;
eeprom_save_config();
protocol_send_ack();
}
void protocol_set_flags_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_flags_handler_t))) {
return;
}
config.flags = *(protocol_set_flags_handler_t *)payload;
eeprom_save_config();
protocol_send_ack();
}
void protocol_set_temp_drift_compensation_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_temp_drift_compensation_handler_t))) {
return;
}
config.temp_drift_compensation = *(protocol_set_temp_drift_compensation_handler_t *)payload;
eeprom_save_config();
protocol_send_ack();
}
void protocol_set_drift_compensation_enable_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_set_drift_compensation_enable_handler_t))) {
return;
}
protocol_set_drift_compensation_enable_handler_t *cmd =
(protocol_set_drift_compensation_enable_handler_t *)payload;
config.flags.fields.temperature_drift_compensation = (cmd->enable != 0) ? 1 : 0;
eeprom_save_config();
protocol_send_ack();
}
void protocol_get_telemetry_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
adc_restore_default();
protocol_get_telemetry_response_t resp;
resp.hv_temp_c = (uint16_t)adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16));
resp.amp_temp_c = (uint16_t)adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16));
resp.sipm_temp_c = (uint16_t)adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16));
resp.mcu_temp_c = adc_read_tempsense();
resp.vbias_mv = adc_to_voltage_mv(adc_read_oversampled(V28V0_FB_PIN, 16));
resp.pots = config.pots;
resp.cps = rsense_get_cps() / 60;
resp.cp10s = rsense_get_cp10s();
resp.total_counts = rsense_get_total_counts();
resp.flags = (1 << 0) | /* power on ok */
((g_rsense_enabled ? 1 : 0) << 1) |
((config.flags.fields.temperature_drift_compensation ? 1 : 0) << 2);
adc_enable_fast();
protocol_send_message(CMD_GET_TELEMETRY, &resp, sizeof(resp));
}
void protocol_flush_counters_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
rsense_flush_counters();
protocol_send_ack();
}
void protocol_flush_spectrum_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
rsense_flush_spectrum();
protocol_send_ack();
}
void protocol_rsense_enable_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
rsense_enable();
protocol_send_ack();
}
void protocol_rsense_disable_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
rsense_disable();
protocol_send_ack();
}
void protocol_spectrum_freeze_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
/* Disable ADC result-ready interrupt so the ISR stops accumulating. */
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL &= ~ADC_RESRDY_bm; }
protocol_send_ack();
}
void protocol_spectrum_unfreeze_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL |= ADC_RESRDY_bm; }
protocol_send_ack();
}
void protocol_get_counts_handler(void *payload, uint16_t length)
{
(void)payload;
(void)length;
protocol_get_counts_response_t resp;
resp.counts = rsense_get_counts_since_last();
protocol_send_message(CMD_GET_COUNTS, &resp, sizeof(resp));
}
void protocol_read_spectrum_chunk_handler(void *payload, uint16_t length)
{
if (!require_payload(length, sizeof(protocol_read_spectrum_chunk_handler_t))) {
return;
}
protocol_read_spectrum_chunk_handler_t *cmd = (protocol_read_spectrum_chunk_handler_t *)payload;
if (cmd->offset >= RSENSE_CHANNEL_COUNT * sizeof(uint16_t)) {
protocol_send_nak();
return;
}
uint16_t available = (RSENSE_CHANNEL_COUNT * sizeof(uint16_t)) - cmd->offset;
uint16_t send_len = (cmd->length < available) ? cmd->length : available;
const uint8_t *spectrum = (const uint8_t *)rsense_get_spectrum_ptr();
protocol_read_spectrum_chunk_response_t resp_hdr;
resp_hdr.offset = cmd->offset;
resp_hdr.length = send_len;
protocol_send_message_v2(CMD_READ_SPECTRUM_DATA, &resp_hdr, sizeof(resp_hdr), spectrum + cmd->offset, send_len);
}

View file

@ -1,8 +1,19 @@
/*
* @file protocol.h
* @brief
* @brief Serial protocol definitions, command structures and dispatch table.
*
* Created: 07.05.2026 10:15:36
* Frame format: [CMD : 2B][LEN : 2B][PAYLOAD : LEN B][CRC16 : 2B]
* - CMD : 16-bit little-endian command identifier.
* - LEN : 16-bit little-endian payload length, excluding CRC.
* - PAYLOAD : LEN bytes, command-specific.
* - CRC16 : CRC16-XMODEM (poly 0x1021, init 0x0000) over CMD+LEN+PAYLOAD.
*
* Response identifiers:
* - 0x414B "AK" : command accepted and executed.
* - 0x4E4B "NK" : valid command but cannot execute (bad params, wrong state).
* - 0x4552 "ER" : malformed frame (bad CRC, bad length).
*
* Created: 07.05.2026
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
@ -15,294 +26,188 @@
#define PROTOCOL_H
#include "config.h"
#include "lsense.h"
#include <stdint.h>
/*
* @details
* General Protocol Description:
* - All commands are initiated by the host
* - Command format: [CMD (2 bytes)][Length (2 bytes)][Payload (variable)][CRC16 (2 bytes)]
* - CMD: 2 bytes command identifier (e.g., 'HV' for set potentiometers)
* - Length: 2 bytes unsigned integer indicating the length of the payload in bytes
* - Payload: Variable length data specific to the command
* - CRC16: 2 bytes CRC16-XMODEM checksum of the CMD, Length, and Payload fields
* - Response: For commands that require a response, the device will send back a similar structured
* message with the appropriate CMD and payload. Otherwise, it should send an acknowledgment
* (e.g., 'ACK') or an error response if the command is invalid.
* - If a valid command is received, but cannot be executed (e.g. locked state, parameters out of range),
* the device should send a command failure response (e.g., 'NAK') without performing any action.
* - Error Handling: If a command is malformed (e.g., incorrect CRC, invalid length), the device will
* ignore the command and should send an error response (e.g., 'ERR') to the host.
* The device should not perform any action if the command is invalid.
* Example Command: Set Potentiometers
* - CMD: 'HV' (0x48 0x56)
* - Length: 3 (0x00 0x03)
* - Payload: [HV_POT (1 byte)][AMP_POT (1 byte)][DET_POT (1 byte)]
* - CRC16: Calculated over 'HV', Length, and Payload
* Response: 'ACK' if successful, 'NAK' if parameters are out of safe range, 'ERR' if command is malformed
*/
#define PROTOCOL_RESP_ACK 0x414B /* "AK" */
#define PROTOCOL_RESP_NAK 0x4E4B /* "NK" */
#define PROTOCOL_RESP_ERR 0x4552 /* "ER" */
uint16_t protocol_crc16_xmodem(uint8_t *data, uint16_t len);
/**
* @brief Compute CRC16-XMODEM (poly 0x1021, init 0x0000) over a buffer.
* @param data Pointer to input bytes.
* @param len Number of bytes to process.
* @return Final CRC value.
*/
uint16_t protocol_crc16_xmodem(const uint8_t *data, uint16_t len);
/**
* @brief Incrementally update a running CRC16-XMODEM with additional bytes.
* Pass @c crc=0 for the first call, then chain the result through
* further calls. This is the single CRC primitive used by the protocol.
* @param crc Current CRC value.
* @param data Pointer to input bytes.
* @param len Number of bytes to process.
* @return Updated CRC value.
*/
uint16_t protocol_crc16_update(uint16_t crc, const uint8_t *data, uint16_t len);
typedef struct protocol_header_t {
uint16_t cmd;
uint16_t length;
} __attribute__((packed)) protocol_header_t;
void protocol_send_header(uint16_t cmd, uint16_t length);
/**
* @brief Send a complete framed message: header + payload + CRC.
* @param cmd Command identifier.
* @param payload Optional payload pointer (may be NULL when @p length is 0).
* @param length Payload length in bytes.
*/
void protocol_send_message(uint16_t cmd, const void *payload, uint16_t length);
/**
* @brief Send a framed message whose payload is split across two fragments.
* CRC is computed across header + frag1 + frag2 in order.
*/
void protocol_send_message_v2(uint16_t cmd, const void *frag1, uint16_t len1, const void *frag2, uint16_t len2);
/** @brief Send an ACK response (cmd = PROTOCOL_RESP_ACK, no payload). */
void protocol_send_ack(void);
/** @brief Send a NAK response (cmd = PROTOCOL_RESP_NAK, no payload). */
void protocol_send_nak(void);
/** @brief Send an ERR response (cmd = PROTOCOL_RESP_ERR, no payload). */
void protocol_send_error(void);
void protocol_send_message(uint16_t cmd, void *payload, uint16_t length);
/*
* @brief Firmware version command and response structure
* @param[out] major Firmware major version
* @param[out] minor Firmware minor version
* X-macro command dispatch table.
*
* X(EnumName, CmdId, HandlerFn) is the single source of truth for all
* supported commands. It is expanded to:
* - an enumeration of command IDs (e.g. CMD_FIRMWARE_VERSION),
* - prototypes for every handler function,
* - the dispatch switch in sbc_fw.ino.
* The same X-macro can be re-included on the host/master side to generate
* matching send helpers.
*/
#define CMD_FIRMWARE_VERSION 0x7600
#define X_PROTOCOL_handlerS \
X(CMD_FIRMWARE_VERSION, 0x7600, protocol_version_handler) \
X(CMD_LIGHT_SENSOR_PRESENCE, 0x6C00, protocol_light_sensor_presence_handler) \
X(CMD_READ_LIGHT_SENSORS, 0x7200, protocol_read_light_sensors_handler) \
X(CMD_GET_CONFIGURATION, 0x6D00, protocol_get_configuration_handler) \
X(CMD_SET_CONFIGURATION, 0x7D00, protocol_set_configuration_handler) \
X(CMD_SET_POTENTIOMETERS, 0x7D01, protocol_set_potentiometers_handler) \
X(CMD_SET_CALIBRATION, 0x7D02, protocol_set_calibration_handler) \
X(CMD_SET_FLAGS, 0x7D03, protocol_set_flags_handler) \
X(CMD_SET_TEMP_DRIFT_COMPENSATION, 0x7D04, protocol_set_temp_drift_compensation_handler) \
X(CMD_SET_DRIFT_COMPENSATION_ENABLE, 0x7D05, protocol_set_drift_compensation_enable_handler) \
X(CMD_GET_TELEMETRY, 0x7400, protocol_get_telemetry_handler) \
X(CMD_FLUSH_COUNTERS, 0x7401, protocol_flush_counters_handler) \
X(CMD_FLUSH_SPECTRUM, 0x7402, protocol_flush_spectrum_handler) \
X(CMD_RSENSE_ENABLE, 0x7500, protocol_rsense_enable_handler) \
X(CMD_RSENSE_DISABLE, 0x7501, protocol_rsense_disable_handler) \
X(CMD_SPECTRUM_FREEZE, 0x7502, protocol_spectrum_freeze_handler) \
X(CMD_SPECTRUM_UNFREEZE, 0x7503, protocol_spectrum_unfreeze_handler) \
X(CMD_GET_COUNTS, 0x7504, protocol_get_counts_handler) \
X(CMD_READ_SPECTRUM_DATA, 0x7505, protocol_read_spectrum_chunk_handler)
/** @brief Symbolic command IDs generated from #X_PROTOCOL_handlerS. */
enum protocol_cmd_id {
#define X(Enum, Cmd_id, Handler) Enum = Cmd_id,
X_PROTOCOL_handlerS
#undef X
};
/* Command handler prototypes generated from X_PROTOCOL_handlerS. */
#define X(Enum, Cmd_id, Handler) void Handler(void *payload, uint16_t length);
X_PROTOCOL_handlerS
#undef X
/** @brief Response payload for #CMD_FIRMWARE_VERSION. */
typedef struct protocol_firmware_version_response_t {
uint8_t major;
uint8_t minor;
} __attribute__((packed)) protocol_firmware_version_response_t;
void handle_version_command(void *payload, uint16_t length);
/* Light sensor protocol payloads reuse the lsense module types directly
* so handlers can populate them via a single call without field copies. */
/*
* @brief Light sensor presence command and response structure
* @param[out] total_detected Total number of sensors detected across all buses
* @param[out] bus_x_count Number of sensors detected on bus X
* @param[out] bus_x_neg_addr I2C address of negative sensor on bus X (0 if not present)
* @param[out] bus_x_pos_addr I2C address of positive sensor on bus X (0 if not present)
* @param[out] bus_y_count Number of sensors detected on bus Y
* @param[out] bus_y_neg_addr I2C address of negative sensor on bus Y (0 if not present)
* @param[out] bus_y_pos_addr I2C address of positive sensor on bus Y (0 if not present)
* @param[out] bus_z_count Number of sensors detected on bus Z
* @param[out] bus_z_neg_addr I2C address of negative sensor on bus Z (0 if not present)
* @param[out] bus_z_pos_addr I2C address of positive sensor on bus Z (0 if not present)
/** @brief Response payload for #CMD_LIGHT_SENSOR_PRESENCE — alias of #lsense_presence_t. */
typedef lsense_presence_t protocol_light_sensor_presence_response_t;
/**
* @brief Command payload for #CMD_READ_LIGHT_SENSORS.
*
* channel: 0=red 1=green 2=blue 3=IR 4=green2
* 5=visible (R+G+B+G2) 6=all (R+G+B+G2+IR)
*/
#define CMD_LIGHT_SENSOR_PRESENCE 0x6C00
typedef struct protocol_light_sensor_presence_response_t {
uint8_t total_detected;
uint8_t bus_x_count;
uint8_t bus_x_neg_addr;
uint8_t bus_x_pos_addr;
uint8_t bus_y_count;
uint8_t bus_y_neg_addr;
uint8_t bus_y_pos_addr;
uint8_t bus_z_count;
uint8_t bus_z_neg_addr;
uint8_t bus_z_pos_addr;
} __attribute__((packed)) protocol_light_sensor_presence_response_t;
void handle_light_sensor_presence_command(void *payload, uint16_t length);
/*
* @brief Read light sensor data command and response structure
* @param[in] channel Spectral channel (0 = red, 1 = green, 2 = blue, 3 = IR, 4 = green2, 5 = composite visible, 6 =
* composite all)
* @param[out] bus_x_neg_channel_value Channel value for negative sensor on bus X
* @param[out] bus_x_pos_channel_value Channel value for positive sensor on bus X
* @param[out] bus_y_neg_channel_value Channel value for negative sensor on bus Y
* @param[out] bus_y_pos_channel_value Channel value for positive sensor on bus Y
* @param[out] bus_z_neg_channel_value Channel value for negative sensor on bus Z
* @param[out] bus_z_pos_channel_value Channel value for positive sensor on bus Z
*/
#define CMD_READ_LIGHT_SENSORS 0x7200
typedef struct protocol_read_light_sensors_command_t {
typedef struct protocol_read_light_sensors_handler_t {
uint8_t channel;
} __attribute__((packed)) protocol_read_light_sensors_command_t;
} __attribute__((packed)) protocol_read_light_sensors_handler_t;
typedef struct protocol_read_light_sensors_response_t {
uint16_t bus_x_neg_channel_value;
uint16_t bus_x_pos_channel_value;
uint16_t bus_y_neg_channel_value;
uint16_t bus_y_pos_channel_value;
uint16_t bus_z_neg_channel_value;
uint16_t bus_z_pos_channel_value;
} __attribute__((packed)) protocol_read_light_sensors_response_t;
/** @brief Response payload for #CMD_READ_LIGHT_SENSORS — alias of #lsense_channel_values_t. */
typedef lsense_channel_values_t protocol_read_light_sensors_response_t;
void handle_read_light_sensors_command(void *payload, uint16_t length);
/* Configuration commands reuse the persisted-config types from config.h
* directly. The on-the-wire layout is the corresponding struct, so handlers
* can use a single struct assignment in place of field-by-field copies. */
/*
* @brief Get configuration command and response structure
* @param[out] config Current device configuration
*/
#define CMD_GET_CONFIGURATION 0x6D00
typedef struct protocol_get_configuration_response_t {
config_t config;
} __attribute__((packed)) protocol_get_configuration_response_t;
/** @brief Response payload for #CMD_GET_CONFIGURATION — alias of #config_t. */
typedef config_t protocol_get_configuration_response_t;
void handle_get_configuration_command(void *payload, uint16_t length);
/** @brief Command payload for #CMD_SET_CONFIGURATION — alias of #config_t. */
typedef config_t protocol_set_configuration_handler_t;
/*
* @brief Set configuration command and response structure
* @param[in] config New device configuration to apply
*/
#define CMD_SET_CONFIGURATION 0x7D00
typedef struct protocol_set_configuration_command_t {
config_t config;
} __attribute__((packed)) protocol_set_configuration_command_t;
/** @brief Command payload for #CMD_SET_POTENTIOMETERS — alias of #potentiometers_t. */
typedef potentiometers_t protocol_set_potentiometers_handler_t;
void handle_set_configuration_command(void *payload, uint16_t length);
/** @brief Command payload for #CMD_SET_CALIBRATION — alias of #calibration_t. */
typedef calibration_t protocol_set_calibration_handler_t;
/*
* @brief Set potentiometers command and response structure
* @param[in] hv_pot High voltage potentiometer value
* @param[in] amp_pot Amplifier potentiometer value
* @param[in] det_pot Detector potentiometer value
*/
#define CMD_SET_POTENTIOMETERS 0x7D01
typedef struct protocol_set_potentiometers_command_t {
uint8_t hv_pot;
uint8_t amp_pot;
uint8_t det_pot;
} __attribute__((packed)) protocol_set_potentiometers_command_t;
/** @brief Command payload for #CMD_SET_FLAGS — alias of #config_flags_t. */
typedef config_flags_t protocol_set_flags_handler_t;
void handle_set_potentiometers_command(void *payload, uint16_t length);
/** @brief Command payload for #CMD_SET_TEMP_DRIFT_COMPENSATION — alias of #temp_drift_compensation_t. */
typedef temp_drift_compensation_t protocol_set_temp_drift_compensation_handler_t;
/*
* @brief Set calibration data command and response structure
* @param[in] adccal0 ADC offset at GND
* @param[in] adccal3 ADC reading at 3.0V reference
* @param[in] tempcal Temperature calibration value
* @param[in] tempdrift_mvK Temperature drift compensation in mV/K
*/
#define CMD_SET_CALIBRATION 0x7D02
typedef struct protocol_set_calibration_command_t {
uint16_t adccal0;
uint16_t adccal3;
uint16_t tempcal;
uint16_t tempdrift_mvK;
} __attribute__((packed)) protocol_set_calibration_command_t;
/** @brief Command payload for #CMD_SET_DRIFT_COMPENSATION_ENABLE. */
typedef struct protocol_set_drift_compensation_enable_handler_t {
uint8_t enable; /* 1 = enable, 0 = disable */
} __attribute__((packed)) protocol_set_drift_compensation_enable_handler_t;
void handle_set_calibration_command(void *payload, uint16_t length);
/*
* @brief Set flags command and response structure
* @param[in] flags Configuration flags to set (e.g., oversampling, channel mode, temperature compensation)
*/
#define CMD_SET_FLAGS 0x7D03
typedef struct protocol_set_flags_command_t {
uint8_t flags;
} __attribute__((packed)) protocol_set_flags_command_t;
/*
* @brief Set temperature drift compensation ranges command and response structure
* @param[in] tempdrift_pot_hv_low Low threshold for high voltage potentiometer
* @param[in] tempdrift_pot_hv_high High threshold for high voltage potentiometer
* @param[in] tempdrift_pot_amp_low Low threshold for amplifier potentiometer
* @param[in] tempdrift_pot_amp_high High threshold for amplifier potentiometer
* @param[in] tempdrift_pot_det_low Low threshold for detector potentiometer
* @param[in] tempdrift_pot_det_high High threshold for detector potentiometer
*/
#define CMD_SET_TEMP_DRIFT_COMPENSATION_RANGES 0x7D04
typedef struct protocol_set_temp_drift_compensation_ranges_command_t {
uint8_t tempdrift_pot_hv_low;
uint8_t tempdrift_pot_hv_high;
uint8_t tempdrift_pot_amp_low;
uint8_t tempdrift_pot_amp_high;
uint8_t tempdrift_pot_det_low;
uint8_t tempdrift_pot_det_high;
} __attribute__((packed)) protocol_set_temp_drift_compensation_ranges_command_t;
void handle_set_flags_command(void *payload, uint16_t length);
/*
* @brief Get telemetry command and response structure
* @param[out] hv_temp_c High voltage component temperature in 0.1°C
* @param[out] amp_temp_c Amplifier component temperature in 0.1°C
* @param[out] det_temp_c Detector component temperature in 0.1°C
* @param[out] mcu_temp_c MCU temperature in 0.1°C
* @param[out] vbias_mv SiPM bias voltage in mV
* @param[out] hv_pot Current high voltage potentiometer setting
* @param[out] amp_pot Current amplifier potentiometer setting
* @param[out] det_pot Current detector potentiometer setting
* @param[out] cps Current counts per second (CPM/60)
* @param[out] cp10s Counts in the last 10 seconds
* @param[out] total_counts Total counts since last reset
*/
#define CMD_GET_TELEMETRY 0x7400
/** @brief Response payload for #CMD_GET_TELEMETRY. */
typedef struct protocol_get_telemetry_response_t {
uint16_t hv_temp_c;
uint16_t amp_temp_c;
uint16_t det_temp_c;
uint16_t mcu_temp_c;
uint16_t hv_temp_c; /**< 0.1 °C — HV_TEMP_PIN, +28V supply sensor. */
uint16_t amp_temp_c; /**< 0.1 °C — AMP_TEMP_PIN, amplifier sensor. */
uint16_t sipm_temp_c; /**< 0.1 °C — DET_TEMP_PIN, SiPM carrier sensor. */
uint16_t mcu_temp_c; /**< Kelvin — MCU internal temperature sensor. */
uint16_t vbias_mv;
uint8_t hv_pot;
uint8_t amp_pot;
uint8_t det_pot;
uint16_t cps;
uint32_t cp10s;
potentiometers_t pots;
uint16_t cps; /**< counts per second = CPM / 60. */
uint32_t cp10s; /**< counts in the last 10 s window. */
uint32_t total_counts;
uint8_t flags; /**< bit 0: power on ok, bit 1: sensing enabled, bit 2: drift compensation enabled, bit 3: reserved. */
} __attribute__((packed)) protocol_get_telemetry_response_t;
void handle_get_telemetry_command(void *payload, uint16_t length);
/*
* @brief Flush counters command
* @details Resets total counts and 10-second counts to zero
*/
#define CMD_FLUSH_COUNTERS 0x7401
void handle_flush_counters_command(void *payload, uint16_t length);
/*
* @brief Flush spectrum command
* @details Clears all channel data and resets total counts to zero
*/
#define CMD_FLUSH_SPECTRUM 0x7402
void handle_flush_spectrum_command(void *payload, uint16_t length);
/*
* @brief Turn on the spectrometer circutry and enable fast ADC mode for radiation detection
*/
#define CMD_RSENSE_ENABLE 0x7500
void handle_rsense_enable_command(void *payload, uint16_t length);
/*
* @brief Turn off the spectrometer circutry and disable fast ADC mode for radiation detection
*/
#define CMD_RSENSE_DISABLE 0x7501
void handle_rsense_disable_command(void *payload, uint16_t length);
/*
* @brief Freeze spectrum data for reading
*/
#define CMD_SPECTRUM_FREEZE 0x7502
void handle_spectrum_freeze_command(void *payload, uint16_t length);
/*
* @brief Unfreeze spectrum data after reading
*/
#define CMD_SPECTRUM_UNFREEZE 0x7503
void handle_spectrum_unfreeze_command(void *payload, uint16_t length);
/*
* @brief Get counts since last request
*/
#define CMD_GET_COUNTS 0x7504
/** @brief Response payload for #CMD_GET_COUNTS. */
typedef struct protocol_get_counts_response_t {
uint32_t counts;
uint32_t counts;
} __attribute__((packed)) protocol_get_counts_response_t;
void handle_get_counts_command(void *payload, uint16_t length);
/** @brief Command payload for #CMD_READ_SPECTRUM_DATA. */
typedef struct protocol_read_spectrum_chunk_handler_t {
uint16_t offset; /* byte offset into spectrum data */
uint16_t length; /* number of bytes to read */
} __attribute__((packed)) protocol_read_spectrum_chunk_handler_t;
/*
* @brief Read spectrum data (arbitrary chunking for large data)
/**
* @brief Response header for #CMD_READ_SPECTRUM_DATA.
* Followed on the wire by @c length bytes of raw spectrum data.
*/
#define CMD_READ_SPECTRUM_DATA 0x7505
typedef struct protocol_read_spectrum_chunk_command_t {
uint16_t offset;
uint16_t length;
} __attribute__((packed)) protocol_read_spectrum_chunk_command_t;
typedef struct protocol_read_spectrum_chunk_response_t {
uint16_t offset;
uint16_t length;
// Followed by 'length' bytes of spectrum data
uint16_t offset;
uint16_t length;
} __attribute__((packed)) protocol_read_spectrum_chunk_response_t;
void handle_read_spectrum_chunk_command(void *payload, uint16_t length);
#endif /* PROTOCOL_H */

View file

@ -18,49 +18,55 @@
#include <stdint.h>
#include <util/atomic.h>
#include "adc.h"
#include "config.h"
#include "eeprom.h"
#include "iodefs.h"
#include "rsense.h"
#include "utils.h"
#include "adc.h"
static volatile union __attribute__((packed)) {
uint16_t channels_16[RSENSE_CHANNEL_COUNT];
uint32_t channels_32[RSENSE_CHANNEL_32_COUNT];
} g_detector_counts = {0};
static uint8_t g_spectrum_mode = 0; // 0 = 16-bit, 1 = 32-bit
static uint8_t g_oversampling_bits = config.flags.adc_oversample_bits;
static uint16_t g_read_pointer = 0;
static volatile uint32_t g_counts_cps = 0; /* counts per second accumulator, reset by periodic tick */
static uint32_t g_cps_time = 0;
static uint16_t g_cps_last = 0;
static uint32_t g_total_counts = 0;
static uint32_t g_total_time = 0;
static uint32_t g_flush_time = 0;
static volatile uint16_t g_counts_cpm = 0;
static volatile uint32_t g_counts_delta = 0; /* counts since last CMD_GET_COUNTS */
static uint32_t g_cpm_time = 0;
static uint16_t g_cpm_last = 0;
static struct {
uint8_t index;
uint16_t cps[10]; /* Circular buffer of the last 10 CPS values, updated by periodic tick */
} g_cp10s_data;
static potentiometer_settings_t g_current_pot_settings;
/*
* Drift compensation state.
*
* Three independent reference temperatures:
* det_ref DET_TEMP (SiPM carrier), drives pot_hv via temp_drift.
* hv_ref HV_TEMP (+28V supply), drives pot_hv via vbias_drift.
* amp_ref AMP_TEMP (amplifier), drives pot_amp and pot_det.
*
* pot_hv adjustment = temp_drift*delta_det + vbias_drift*delta_hv (summed).
*/
static uint32_t g_drift_last_ms = 0;
static int16_t g_drift_ref_det = 0; /* DET_TEMP reference in 0.1 °C */
static int16_t g_drift_ref_hv = 0; /* HV_TEMP reference in 0.1 °C */
static int16_t g_drift_ref_amp = 0; /* AMP_TEMP reference in 0.1 °C */
static bool g_drift_ref_valid = false;
bool g_rsense_enabled = false; /* true when HV/AMP are powered on */
static void apply_potentiometer_settings(const potentiometer_settings_t *settings);
static void apply_potentiometer_settings(void);
static void initialize_event_system(void);
static inline void handle_analog_read_interrupt(void);
static void clear_detector_counters(void);
static void clear_channel_data(void);
static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det);
static void send_temperature_data(void);
static inline void analog_read_interrupt(void);
static void rsense_drift_compensation_tick(void);
void rsense_init(void)
{
eeprom_load_pot_settings(&g_current_pot_settings);
pinMode(HV_EN, OUTPUT);
pinMode(DET_EN, OUTPUT);
pinMode(DET_RST, OUTPUT);
// Disable HV and Detector on startup
digitalWrite(HV_EN, LOW);
digitalWrite(DET_EN, LOW);
@ -72,342 +78,263 @@ void rsense_init(void)
digitalWrite(DET_CS, HIGH);
digitalWrite(AMP_CS, HIGH);
apply_potentiometer_settings(&g_current_pot_settings);
apply_potentiometer_settings();
pinMode(DET_TRIG_PIN, INPUT);
initialize_event_system();
}
/**
* @brief Initialize event system for radiation detection
*/
static void initialize_event_system(void)
{
// Enable event on DET_TRIG_PIN
Event2.set_generator(event::gen2::pin_pc7);
Event2.set_user(event::user::adc0_start);
// Pin events are async, level; convert to sync, edge via CCL
// Logic0.enable = true;
// Logic0.input0 = logic::in::event_a;
// Logic0.input1 = logic::in::masked;
// Logic0.input2 = logic::in::masked;
// Logic0.output = logic::out::enable;
// Logic0.output_swap = logic::out::no_swap;
// Logic0.filter = logic::filter::synchronizer;
// Logic0.edgedetect = logic::edgedetect::enable;
// Logic0.clocksource = logic::clocksource::clk_per;
// Logic0.sequencer = logic::sequencer::disable;
// Logic0.truth = 0b00000010; // A and not previous A
// Logic0.init();
// Route CCL output to ADC
// Event0.set_generator(event::gen::ccl0_out);
// Event0.set_user(event::user::adc0_start);
//Event0.start();
Event2.start();
}
/**
* @brief Handle ADC conversion results for radiation detection
*/
static inline void handle_analog_read_interrupt(void)
static inline void analog_read_interrupt(void)
{
digitalWriteFast(DET_RST, HIGH);
uint16_t channel = ADC0.RES; // read result to clear flag
uint16_t channel = ADC0.RES;
digitalWriteFast(STATUS_LED, LOW);
if (g_spectrum_mode == 0) {
// 16-bit spectrum mode
// 2 extra bits from oversampling + averaging
// shift down to equiv. 11 bits
#if ADC_OVERSAMPLING_BITS == 1
channel = channel >> 1;
#elif ADC_OVERSAMPLING_BITS == 2
channel = channel >> (ADC_OVERSAMPLING_BITS + 1);
#elif ADC_OVERSAMPLING_BITS == 3
channel = channel >> (ADC_OVERSAMPLING_BITS + 2);
#else
#error "Unsupported ADC oversampling setting"
#endif
g_detector_counts.channels_16[channel & RSENSE_CHANNEL_16_MASK]++;
} else {
// 32-bit spectrum mode
// 2 extra bits from oversampling + averaging x2
// shift down to equiv. 10 bits
#if ADC_OVERSAMPLING_BITS == 1
channel = channel >> 2;
#elif ADC_OVERSAMPLING_BITS == 2
channel = channel >> (ADC_OVERSAMPLING_BITS + 2);
#elif ADC_OVERSAMPLING_BITS == 3
channel = channel >> (ADC_OVERSAMPLING_BITS + 3);
#else
#error "Unsupported ADC oversampling setting"
#endif
g_detector_counts.channels_32[channel & RSENSE_CHANNEL_32_MASK]++;
}
channel = channel >> (ADC_OVERSAMPLING_BITS + 2);
g_detector_counts.channels_16[channel & RSENSE_CHANNEL_MASK]++;
g_total_counts++;
g_counts_cpm++;
g_counts_cps++;
g_counts_delta++;
digitalWriteFast(DET_RST, LOW);
}
/**
* @brief Clear all detector counters
*/
static void clear_detector_counters(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
g_total_counts = 0;
g_total_time = 0;
g_flush_time = millis();
g_counts_cpm = 0;
g_cpm_time = millis();
}
}
ISR(ADC0_RESRDY_vect) { analog_read_interrupt(); }
/**
* @brief Clear channel data
* @brief Clamp a signed value into the unsigned interval [lo, hi].
*/
static void clear_channel_data(void)
static uint8_t clamp_u8(int16_t v, uint8_t lo, uint8_t hi)
{
for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) {
g_detector_counts.channels_16[i] = 0;
}
if (v < (int16_t)lo)
return lo;
if (v > (int16_t)hi)
return hi;
return (uint8_t)v;
}
/**
* @brief Apply potentiometer settings to hardware
* @param settings Pointer to potentiometer settings
*/
static void apply_potentiometer_settings(const potentiometer_settings_t *settings)
static void rsense_drift_compensation_tick(void)
{
if (settings == nullptr)
if (!config.flags.fields.temperature_drift_compensation || !g_rsense_enabled)
return;
// HV potentiometer setup
digitalWrite(HV_CS, LOW);
SPI.transfer(settings->hv_pot);
digitalWrite(HV_CS, HIGH);
delay(1);
uint32_t period = config.temp_drift_compensation.drift_period_ms;
if (period == 0)
period =
300000; /* Default to 5 minutes if not set, to avoid excessive compensation when misconfigured. */
// SiPM Pre-amp gain potentiometer setup
digitalWrite(AMP_CS, LOW);
SPI.transfer(settings->amp_pot);
digitalWrite(AMP_CS, HIGH);
delay(1);
if (millis() - g_drift_last_ms < period)
return;
// Detection threshold potentiometer setup
digitalWrite(DET_CS, LOW);
SPI.transfer(settings->det_pot);
digitalWrite(DET_CS, HIGH);
delay(1);
}
/* Read all three temperature sensors (each ~10 ms due to oversampling) */
int16_t det_temp = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16));
int16_t hv_temp = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16));
int16_t amp_temp = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16));
/**
* @brief Set new potentiometer values
* @param hv High voltage potentiometer value (0-255)
* @param amp Amplifier potentiometer value (0-255)
* @param det Detector potentiometer value (0-255)
*/
static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det)
{
digitalWrite(DET_EN, LOW);
digitalWrite(HV_EN, LOW);
delay(5);
// Update current settings and save to EEPROM
g_current_pot_settings.hv_pot = hv;
g_current_pot_settings.amp_pot = amp;
g_current_pot_settings.det_pot = det;
apply_potentiometer_settings(&g_current_pot_settings);
eeprom_save_pot_settings(&g_current_pot_settings);
}
/*******************************************************************************/
/*******************************************************************************/
/* Command handlers */
/*******************************************************************************/
/*******************************************************************************/
void rsense_cmd_telemetry(void)
{
adc_restore_default();
// Send total counts as 4 bytes
Serial.write(g_total_counts & 0xFF);
Serial.write((g_total_counts >> 8) & 0xFF);
Serial.write((g_total_counts >> 16) & 0xFF);
Serial.write((g_total_counts >> 24) & 0xFF);
// 2 bytes of voltage in mV
uint16_t voltage_raw = adc_read_oversampled(V28V0_FB_PIN, 16);
uint16_t voltage_mv = adc_to_voltage_mv(voltage_raw);
Serial.write(voltage_mv & 0xFF);
Serial.write(voltage_mv >> 8);
// 6 bytes of temperature data
send_temperature_data();
// 3 bytes of potentiometer settings
Serial.write(g_current_pot_settings.hv_pot);
Serial.write(g_current_pot_settings.amp_pot);
Serial.write(g_current_pot_settings.det_pot);
Serial.flush();
SERIAL_BUFFER_CLEAR();
adc_enable_fast();
}
/**
* @brief Send temperature sensor data
*/
static void send_temperature_data(void)
{
int16_t hv_temp_c = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16));
int16_t amp_temp_c = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16));
int16_t det_temp_c = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16));
Serial.write(hv_temp_c & 0xFF);
Serial.write(hv_temp_c >> 8);
Serial.write(amp_temp_c & 0xFF);
Serial.write(amp_temp_c >> 8);
Serial.write(det_temp_c & 0xFF);
Serial.write(det_temp_c >> 8);
}
/* TODO: CRC for parameters */
void rsense_cmd_set_potentiometers(void)
{
// Wait for HV, AMP, DET potentiometer values
while (Serial.available() < 3)
;
uint8_t hv = Serial.read();
uint8_t amp = Serial.read();
uint8_t det = Serial.read();
set_potentiometer_values(hv, amp, det);
SERIAL_SEND_OK();
}
void rsense_cmd_enable(void)
{
digitalWrite(HV_EN, HIGH);
digitalWrite(DET_EN, HIGH);
adc_enable_fast();
SERIAL_SEND_OK();
}
void rsense_cmd_disable(void)
{
digitalWrite(DET_EN, LOW);
digitalWrite(HV_EN, LOW);
adc_restore_default();
SERIAL_SEND_OK();
}
void rsense_cmd_flush(void)
{
clear_detector_counters();
clear_channel_data();
SERIAL_SEND_OK();
}
void rsense_cmd_dump_channels(void)
{
adc_restore_default();
uint16_t read_from = 0;
// Wait for 2 bytes specifying read position
while (Serial.available() < 2)
;
read_from = Serial.read();
read_from |= (uint16_t)Serial.read() << 8;
// Send 2 bytes of read position
Serial.write(g_read_pointer & 0xFF);
Serial.write(g_read_pointer >> 8);
g_read_pointer = read_from;
if (g_read_pointer >= (RSENSE_CHANNEL_32_COUNT - 32)) {
g_read_pointer = RSENSE_CHANNEL_32_COUNT - 32;
if (!g_drift_ref_valid) {
g_drift_ref_det = det_temp;
g_drift_ref_hv = hv_temp;
g_drift_ref_amp = amp_temp;
g_drift_ref_valid = true;
g_drift_last_ms = millis();
return;
}
// Send 2 bytes of CRC16 checksum for the 32 channels
uint16_t crc16 = calculate_crc16_xmodem((uint8_t *)(&g_detector_counts.channels_32[g_read_pointer]), 32 * 4);
Serial.write(crc16 & 0xFF);
Serial.write(crc16 >> 8);
/* Deltas in 0.1°C units */
int16_t delta_det = det_temp - g_drift_ref_det;
int16_t delta_hv = hv_temp - g_drift_ref_hv;
int16_t delta_amp = amp_temp - g_drift_ref_amp;
// Send 32 channel values (128 bytes total)
for (uint16_t i = 0; i < 32 && g_read_pointer < RSENSE_CHANNEL_32_COUNT; i++, g_read_pointer++) {
uint32_t channel_value = g_detector_counts.channels_32[g_read_pointer];
Serial.write(channel_value & 0xFF);
Serial.write((channel_value >> 8) & 0xFF);
Serial.write((channel_value >> 16) & 0xFF);
Serial.write((channel_value >> 24) & 0xFF);
/* Drift coefficients in pot_units per °C, scaled by 256.
* adj = coeff * delta_0.1C / 10 / 256 = coeff * delta_0.1C / 2560 */
bool needs_update = false;
Serial.flush();
SERIAL_BUFFER_CLEAR();
delay(1);
int16_t new_hv = config.pots.pot_hv;
int16_t new_amp = config.pots.pot_amp;
int16_t new_det = config.pots.pot_det;
const temp_drift_compensation_t *dc = &config.temp_drift_compensation;
if (dc->pot_hv_low != dc->pot_hv_high) {
/* pot_hv: SiPM carrier (DET_TEMP) + 28V supply (HV_TEMP) contributions. */
int16_t adj =
(int16_t)(((int32_t)dc->temp_drift * delta_det + (int32_t)dc->vbias_drift * delta_hv) / 2560L);
if (adj != 0) {
new_hv = (int16_t)config.pots.pot_hv + adj;
needs_update = true;
}
}
Serial.flush();
SERIAL_BUFFER_CLEAR();
adc_enable_fast();
}
void rsense_cmd_get_cpm(void)
{
Serial.write(g_cpm_last & 0xFF);
Serial.write(g_cpm_last >> 8);
Serial.flush();
SERIAL_BUFFER_CLEAR();
}
void rsense_cmd_set_configuration(void)
{
// Wait for 1 byte specifying mode
while (Serial.available() < 8)
;
uint8_t mode = Serial.read();
if (mode <= 1) {
g_spectrum_mode = mode;
clear_channel_data();
SERIAL_SEND_OK();
if (dc->pot_amp_low != dc->pot_amp_high) {
int16_t adj = (int16_t)(((int32_t)dc->gain_drift * delta_amp) / 2560L);
if (adj != 0) {
new_amp = (int16_t)config.pots.pot_amp + adj;
needs_update = true;
}
}
// 7 bytes reserved for future use
if (dc->pot_det_low != dc->pot_det_high) {
int16_t adj = (int16_t)(((int32_t)dc->threshold_drift * delta_amp) / 2560L);
if (adj != 0) {
new_det = (int16_t)config.pots.pot_det + adj;
needs_update = true;
}
}
Serial.flush();
SERIAL_BUFFER_CLEAR();
if (!needs_update) {
g_drift_last_ms = millis();
return;
}
config.pots.pot_hv = clamp_u8(new_hv, dc->pot_hv_low, dc->pot_hv_high);
config.pots.pot_amp = clamp_u8(new_amp, dc->pot_amp_low, dc->pot_amp_high);
config.pots.pot_det = clamp_u8(new_det, dc->pot_det_low, dc->pot_det_high);
rsense_apply_potentiometers();
g_drift_ref_det = det_temp;
g_drift_ref_hv = hv_temp;
g_drift_ref_amp = amp_temp;
g_drift_last_ms = millis();
}
void rsense_periodic(void)
{
if (millis() - g_cpm_time > CPM_UPDATE_PERIOD) {
uint32_t elapsed_time = millis() - g_cpm_time;
g_cpm_last = (uint16_t)(((uint32_t)g_counts_cpm * 60UL * 1000UL) / elapsed_time);
g_counts_cpm = 0;
g_cpm_time = millis();
if (millis() - g_cps_time > RSENSE_UPDATE_PERIOD) {
uint32_t elapsed_time = millis() - g_cps_time;
g_cps_last = (uint16_t)(((uint32_t)g_counts_cps * 1000UL) / elapsed_time);
g_counts_cps = 0;
g_cps_time = millis();
/* Update 10-second CPS circular buffer for moving average */
g_cp10s_data.cps[g_cp10s_data.index] = g_cps_last;
g_cp10s_data.index = (g_cp10s_data.index + 1) % 10;
rsense_drift_compensation_tick();
}
}
/**
* @brief ADC conversion complete interrupt handler
* @brief Clock @c value out to one AD5160 selected by @p cs_pin.
*/
ISR(ADC0_RESRDY_vect) { handle_analog_read_interrupt(); }
static inline void write_pot(uint8_t cs_pin, uint8_t value)
{
digitalWrite(cs_pin, LOW);
SPI.transfer(value);
digitalWrite(cs_pin, HIGH);
delay(1);
}
/**
* @brief Push the current @c config.pots values to the AD5160 digital
* potentiometers over SPI. Does not touch power-enable lines;
* see #rsense_apply_potentiometers for the safe re-sequencing version.
*/
static void apply_potentiometer_settings(void)
{
write_pot(HV_CS, config.pots.pot_hv);
write_pot(AMP_CS, config.pots.pot_amp);
write_pot(DET_CS, config.pots.pot_det);
}
void rsense_apply_potentiometers(void)
{
if (!g_rsense_enabled) {
/* System is off — just write SPI registers without power cycling */
apply_potentiometer_settings();
return;
}
/* Power sequence: freeze → disable amp → disable HV → set pots → enable HV → enable amp → unfreeze */
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL &= ~ADC_RESRDY_bm; }
digitalWrite(DET_EN, LOW);
digitalWrite(HV_EN, LOW);
delay(10);
apply_potentiometer_settings();
delay(10);
digitalWrite(HV_EN, HIGH);
delay(100);
digitalWrite(DET_EN, HIGH);
delay(100);
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL |= ADC_RESRDY_bm; }
}
void rsense_enable(void)
{
g_rsense_enabled = true;
digitalWrite(HV_EN, HIGH);
digitalWrite(DET_EN, HIGH);
adc_enable_fast();
}
void rsense_disable(void)
{
g_rsense_enabled = false;
digitalWrite(DET_EN, LOW);
digitalWrite(HV_EN, LOW);
adc_restore_default();
}
void rsense_flush_counters(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
g_total_counts = 0;
g_counts_cps = 0;
g_counts_delta = 0;
g_cps_time = millis();
}
}
void rsense_flush_spectrum(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) {
g_detector_counts.channels_16[i] = 0;
}
}
rsense_flush_counters();
}
uint32_t rsense_get_total_counts(void)
{
uint32_t v;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { v = g_total_counts; }
return v;
}
uint16_t rsense_get_cps(void) { return g_cps_last; }
uint32_t rsense_get_cp10s(void)
{
uint32_t sum = 0;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
for (int i = 0; i < 10; i++) {
sum += g_cp10s_data.cps[i];
}
}
return sum / 10;
}
uint32_t rsense_get_counts_since_last(void)
{
uint32_t v;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
v = g_counts_delta;
g_counts_delta = 0;
}
return v;
}
const volatile void *rsense_get_spectrum_ptr(void) { return (const volatile void *)&g_detector_counts; }

View file

@ -1,8 +1,9 @@
/*
* @file rsense.h
* @brief Radiation sensor management and command handling
* @brief Radiation sensor subsystem: counters, spectrum, drift compensation,
* and HV/detector power control.
*
* Created: 21.09.2025 06:01:56
* Created: 21.09.2025
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
@ -14,58 +15,59 @@
#ifndef RSENSE_H
#define RSENSE_H
#include <Arduino.h>
#include <stdint.h>
/**
* @brief Initialize radiation sensor subsystem
* @brief Configure GPIO, SPI digital potentiometers, and the event system
* used by the detector trigger pin to start ADC conversions.
*/
void rsense_init(void);
/**
* @brief Send telemetry data via serial
*/
void rsense_cmd_telemetry(void);
/**
* @brief Dump channel data via serial
*/
void rsense_cmd_dump_channels(void);
/**
* @brief Flush detector counters
*/
void rsense_cmd_flush(void);
/**
* @brief Enable radiation detection
*/
void rsense_cmd_enable(void);
/**
* @brief Disable radiation detection
*/
void rsense_cmd_disable(void);
/**
* @brief Set potentiometer values
*/
void rsense_cmd_set_potentiometers(void);
/**
* @brief Send counts per minute data
*/
void rsense_cmd_get_cpm(void);
/**
* @brief Set spectrum mode (16-bit or 32-bit)
*/
void rsense_cmd_set_configuration(void);
/**
* @brief Periodic tasks for radiation sensor
* Updates CPM calculation every 10 seconds
* Call from main loop
* @brief Periodic housekeeping: CPM accumulation window and drift
* compensation tick. Should be called from the main loop.
*/
void rsense_periodic(void);
#endif // RSENSE_H
/** @brief Reset all CPM/CP10S/total/delta count accumulators. */
void rsense_flush_counters(void);
/** @brief Zero the spectrum histogram and reset all counters. */
void rsense_flush_spectrum(void);
/** @brief Total counts since the last #rsense_flush_counters call. */
uint32_t rsense_get_total_counts(void);
/** @brief Last computed counts-per-minute value. */
uint16_t rsense_get_cps(void);
/**
* @brief Returns the count delta since the previous call and resets it.
* Used to service CMD_GET_COUNTS without losing pulses.
*/
uint32_t rsense_get_counts_since_last(void);
/** @brief Counts in the current 10 s window. */
uint32_t rsense_get_cp10s(void);
/**
* @brief Apply @c config.pots to the AD5160s.
* When the radiation sensor is powered, performs a freeze
* disable set enable unfreeze sequence to avoid spurious
* counts during the wiper update.
*/
void rsense_apply_potentiometers(void);
/** @brief Current spectrum binning mode (0 = 16-bit channels, 1 = 32-bit). */
uint8_t rsense_get_spectrum_mode(void);
/** @brief Pointer to the spectrum histogram, for read-only chunked transfer. */
const volatile void *rsense_get_spectrum_ptr(void);
/** @brief Power on the high-voltage and detector rails. */
void rsense_enable(void);
/** @brief Power off the high-voltage and detector rails. */
void rsense_disable(void);
#endif /* RSENSE_H */

View file

@ -1,6 +1,6 @@
/*
* @file sbc_fw.ino
* @brief Main firmware for sensor board controller, YKSA PL/EDU16 RSENSE
* @brief Main firmware for sensor board controller, YKSA PL/EDU16 RSENSE.
*
* Created: 21.09.2025
* Author: ThePetrovich
@ -15,15 +15,126 @@
#include <SPI.h>
#include <SoftwareI2C.h>
#include <stdint.h>
#include <string.h>
#include "config.h"
#include "eeprom.h"
#include "iodefs.h"
#include "lsense.h"
#include "protocol.h"
#include "rsense.h"
/*
* Frame format: [CMD 2B][LEN 2B][PAYLOAD LEN bytes][CRC16 2B].
* Maximum payload size chosen to fit the largest command (config_t ~22 bytes);
* 256 bytes leaves headroom for future commands.
*/
#define PROTOCOL_MAX_PAYLOAD 256U
#define PROTOCOL_HEADER_SIZE ((uint16_t)sizeof(protocol_header_t))
#define PROTOCOL_CRC_SIZE 2U
#define PROTOCOL_BUF_SIZE (PROTOCOL_HEADER_SIZE + PROTOCOL_MAX_PAYLOAD + PROTOCOL_CRC_SIZE)
static uint8_t rx_buf[PROTOCOL_BUF_SIZE];
static uint16_t rx_pos = 0;
/**
* @brief Dispatch a fully-validated frame to its registered handler.
* Generated from the X-macro table in protocol.h.
*/
static void dispatch_handler(uint16_t cmd, void *payload, uint16_t length)
{
switch (cmd) {
#define X(Enum, Cmd_id, Handler) \
case Cmd_id: \
Handler(payload, length); \
break;
X_PROTOCOL_handlerS
#undef X
default : protocol_send_error();
break;
}
}
/**
* @brief Drain the serial RX buffer and assemble/dispatch any complete frames.
* On bad CRC, oversized length, or buffer overflow, an ERR is emitted
* and the receive state is reset.
*/
static void protocol_receive(void)
{
while (Serial.available()) {
uint8_t byte = (uint8_t)Serial.read();
if (rx_pos < PROTOCOL_BUF_SIZE) {
rx_buf[rx_pos++] = byte;
} else {
/* Buffer overflow — discard and reset. */
protocol_send_error();
rx_pos = 0;
continue;
}
/* Need a full header before total frame length is known. */
if (rx_pos < PROTOCOL_HEADER_SIZE) {
continue;
}
protocol_header_t hdr;
memcpy(&hdr, rx_buf, sizeof(hdr));
if (hdr.length > PROTOCOL_MAX_PAYLOAD) {
protocol_send_error();
rx_pos = 0;
continue;
}
uint16_t total = PROTOCOL_HEADER_SIZE + hdr.length + PROTOCOL_CRC_SIZE;
if (rx_pos < total) {
continue; /* not enough bytes yet */
}
uint16_t rx_crc = (uint16_t)rx_buf[total - 2] | ((uint16_t)rx_buf[total - 1] << 8);
uint16_t calc_crc = protocol_crc16_xmodem(rx_buf, total - PROTOCOL_CRC_SIZE);
if (calc_crc != rx_crc) {
protocol_send_error();
} else {
dispatch_handler(hdr.cmd, &rx_buf[PROTOCOL_HEADER_SIZE], hdr.length);
}
/* Shift any back-to-back trailing bytes to the front of the buffer. */
uint16_t extra = rx_pos - total;
if (extra > 0) {
memmove(rx_buf, rx_buf + total, extra);
}
rx_pos = extra;
}
}
static uint32_t status_led_last_blink = 0;
/**
* @brief Toggle the status LED at #STATUS_LED_BLINK_PERIOD ms intervals.
*/
static inline void update_status_led(void)
{
if (millis() - status_led_last_blink > STATUS_LED_BLINK_PERIOD) {
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
status_led_last_blink = millis();
}
}
/**
* @brief Bring up the SPI bus shared by the AD5160 digital potentiometers.
*/
static void initialize_spi(void)
{
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV128); /* 125 kHz @ 16 MHz */
SPI.setDataMode(SPI_MODE0); /* AD5160 (Rev C.) */
SPI.setBitOrder(MSBFIRST); /* AD5160 (Rev C.), p. 13, fig. 37 */
}
void setup()
{
pinMode(STATUS_LED, OUTPUT);
@ -38,102 +149,9 @@ void setup()
rsense_init();
}
/**
* @brief Initialize SPI interface for potentiometer control
*/
void initialize_spi(void)
{
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV128); // 125 kHz @ 16 MHz
SPI.setDataMode(SPI_MODE0); // AD5160 (Rev C.)
SPI.setBitOrder(MSBFIRST); // AD5160 (Rev C.), p. 13, fig. 37
}
/**
* @brief Handle incoming serial commands
* @param command Command character received
*/
void handle_serial_command(char command)
{
switch (command) {
case 'v': // Firmware version
handle_version_command();
break;
case 'd': // Detect/ping
handle_detect_command();
break;
case 'l': // Light sensor presence
lsense_cmd_presence();
break;
case 'r': // Read sensors
lsense_cmd_read();
rsense_cmd_get_cpm();
break;
case 'c': // Dump 128 bytes of channel data
rsense_cmd_dump_channels();
break;
case 'f': // Flush counters
rsense_cmd_flush();
break;
case 'e': // Enable radiation detection
rsense_cmd_enable();
break;
case 's': // Disable radiation detection
rsense_cmd_disable();
break;
case 'p': // Set potentiometers
rsense_cmd_set_potentiometers();
break;
case 't': // Telemetry
rsense_cmd_telemetry();
break;
case 'm': // Set spectrum mode (16-bit or 32-bit)
rsense_cmd_set_configuration();
break;
default:
// Unknown command - ignore
break;
}
}
/**
* @brief Handle firmware version command
*/
void handle_version_command(void)
{
Serial.write(FIRMWARE_VERSION_MAJOR);
Serial.write(FIRMWARE_VERSION_MINOR);
Serial.flush();
SERIAL_BUFFER_CLEAR();
}
/**
* @brief Handle detect/ping command
*/
void handle_detect_command(void) { SERIAL_SEND_OK(); }
/**
* @brief Update status LED (blink)
*/
inline void update_status_led(void)
{
if (millis() - status_led_last_blink > STATUS_LED_BLINK_PERIOD) {
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
status_led_last_blink = millis();
}
}
/**
* @brief Main program loop
*/
void loop()
{
// Process incoming serial commands
while (Serial.available()) {
char command = Serial.read();
handle_serial_command(command);
}
protocol_receive();
update_status_led();
rsense_periodic();
}

View file

@ -1,35 +0,0 @@
/*
* @file utils.cpp
* @brief Utility functions implementation
*
* Created: 21.09.2025
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
* See the LICENSE file for details.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "utils.h"
#include "config.h"
#include <Arduino.h>
uint16_t calculate_crc16_xmodem(uint8_t *data, uint16_t len)
{
uint16_t crc = 0;
uint8_t i;
while (len--) {
crc ^= (uint16_t)(*data++) << 8;
for (i = 0; i < 8; i++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}

View file

@ -1,40 +0,0 @@
/*
* @file utils.h
* @brief Utility functions and helpers
*
* Created: 21.09.2025
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
* See the LICENSE file for details.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef UTILS_H
#define UTILS_H
#include <stdint.h>
#define SERIAL_BUFFER_CLEAR() \
do { \
while (Serial.available()) { \
Serial.read(); \
} \
} while (0)
#define SERIAL_SEND_OK() \
do { \
Serial.print("ok\n"); \
Serial.flush(); \
SERIAL_BUFFER_CLEAR(); \
} while (0)
/**
* @brief Calculate CRC16 using XMODEM polynomial
* @param data Pointer to data buffer
* @param len Length of data in bytes
* @return CRC16 checksum
*/
uint16_t calculate_crc16_xmodem(uint8_t *data, uint16_t len);
#endif // UTILS_H