Compare commits

...
Sign in to create a new pull request.

2 commits
main ... rework

Author SHA1 Message Date
ThePetrovich
2b713a3e3a Protocol rework 2026-05-10 15:33:08 +08:00
ThePetrovich
3e77d34ccc Freeze 2026-05-07 23:21:11 +08:00
15 changed files with 1556 additions and 841 deletions

162
sbc_fw/adc.cpp Normal file
View file

@ -0,0 +1,162 @@
/*
* @file adc.cpp
* @brief ATmega4809 ADC0 driver: configuration profiles, oversampling reads,
* and conversions to engineering units.
*
* Created: 27.09.2025 05:06:33
* Author: ThePetrovich
*
* Copyright YKSA - Sakha Aerospace Systems, LLC.
* See the LICENSE file for details.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "adc.h"
#include "config.h"
#include <Arduino.h>
#include <util/atomic.h>
/**
* @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/°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); }
uint16_t adc_read_oversampled(uint8_t pin, uint8_t samples)
{
uint32_t sum = 0;
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);
}
void adc_enable_fast(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
ADC0.CTRLA |= ADC_ENABLE_bm;
/* Oversampling factor selected by config.flags.adc_oversample_bits. */
ADC0.CTRLB |= (config.flags.fields.adc_oversample_bits << 2);
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 starts conversion */
ADC0.INTCTRL |= ADC_RESRDY_bm; /* result-ready interrupt */
/* Microchip DS40002015B p. 424. */
VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc;
ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; /* DET_SIG_PIN */
adc_flags |= 0x01;
}
}
void adc_restore_default(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
ADC0.CTRLB &= ~ADC_SAMPNUM_gm;
ADC0.CTRLC &= ~ADC_REFSEL_gm;
ADC0.CTRLC |= ADC_REFSEL_VREFA_gc;
ADC0.EVCTRL &= ~ADC_STARTEI_bm;
ADC0.INTCTRL &= ~ADC_RESRDY_bm;
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;
while (!(ADC0.INTFLAGS & ADC_RESRDY_bm))
;
return ADC0.RES;
}
uint16_t adc_read_tempsense(void)
{
/*
* 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;
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_INTREF_gc;
VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc;
ADC0.MUXPOS = ADC_MUXPOS_TEMPSENSE_gc;
}
delay(10);
int8_t sigrow_offset = SIGROW.TEMPSENSE1;
uint8_t sigrow_gain = SIGROW.TEMPSENSE0;
uint16_t adc_reading = adc_conversion() >> 4;
/* 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;
temp += 0x80; /* Round-half-up before the >>8 division. */
temp >>= 8;
uint16_t temperature_in_C = temp - 273; /* Convert from Kelvin to Celsius. */
adc_restore_default();
if (adc_flags & 0x01) {
adc_enable_fast();
}
return temperature_in_C * 10; /* Return in 0.1 °C for consistency */
}

61
sbc_fw/adc.h Normal file
View file

@ -0,0 +1,61 @@
/*
* @file adc.h
* @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 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 ADC0 to its default configuration suitable for blocking
* analogRead()-style reads of housekeeping channels.
*/
void adc_restore_default(void);
/**
* @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 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 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 Read the MCU internal temperature sensor (blocking, ~10 ms).
* @return Temperature in Kelvin.
*/
uint16_t adc_read_tempsense(void);
#endif /* ADC_H_ */

View file

@ -1,6 +1,7 @@
/* /*
* @file config.h * @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 * Created: 21.09.2025
* Author: ThePetrovich * Author: ThePetrovich
@ -14,60 +15,122 @@
#ifndef CONFIG_H #ifndef CONFIG_H
#define CONFIG_H #define CONFIG_H
// Firmware version #include <stdint.h>
#define FIRMWARE_VERSION_MAJOR 2
#define FIRMWARE_VERSION_MINOR 03
// Serial communication /* Firmware version */
#define FIRMWARE_VERSION_MAJOR 3
#define FIRMWARE_VERSION_MINOR 00
/* Serial communication */
#define SERIAL_BAUD_RATE 38400 #define SERIAL_BAUD_RATE 38400
#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)
// Timing constants /* Timing constants */
#define STATUS_LED_BLINK_PERIOD 500U #define STATUS_LED_BLINK_PERIOD 500U
#define MAIN_LOOP_DELAY 10 #define MAIN_LOOP_DELAY 10
#define CPM_UPDATE_PERIOD 10000U #define RSENSE_UPDATE_PERIOD 1000U
// ADC and detector settings /* Spectrum channel masks and counts */
#define ADC_OVERSAMPLING_BITS 2 // 16x oversampling -> 2 extra bits #define RSENSE_CHANNEL_MASK 0x3FFU /* 10-bit channel mask */
#define ADC_CHANNEL_16_MASK 0x7FFU // 11-bit channel mask #define RSENSE_CHANNEL_COUNT 1024
#define ADC_CHANNEL_32_MASK 0x3FFU // 10-bit channel mask
#define DETECTOR_CHANNEL_COUNT 2048
#define DETECTOR_CHANNEL_32_COUNT 1024
// Temperature sensor constants (MCP9700) /* MCP9700 temperature sensor calibration constants */
#define TEMP_SENSOR_OFFSET_MV 500L // 500 mV at 0°C #define MCP9700_OFFSET_MV 500L /* 500 mV at 0 °C */
#define TEMP_SENSOR_SCALE_MV 10L // 10 mV per degree #define MCP9700_SCALE_MV 10L /* 10 mV per °C */
#define ADC_VREF_MV 3000L // 3.0V reference #define ADC_OVERSAMPLING_BITS 2 /* 16x oversampling -> 2 extra bits */
#define ADC_RESOLUTION 4096L // 12-bit effective resolution #define ADC_VREF_MV 3000L /* 3.0 V reference */
#define ADC_RESOLUTION 1024L /* 10-bit ADC */
// Voltage divider constants /* Voltage divider constants */
#define VOLTAGE_DIVIDER_RATIO 20.0f // 200k and 10k resistors #define VOLTAGE_DIVIDER_RATIO 20.0f /* 200k and 10k resistors */
#define VOLTAGE_MV_PER_STEP 14.6484375f // (3.0 * 20) / 4096 #define VOLTAGE_MV_PER_STEP 14.6484375f /* (3.0 * 20) / 4096 */
// EEPROM addresses for potentiometer settings /* Default potentiometer wiper value (mid-scale, 127/255) */
#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
#define DEFAULT_POT_VALUE 127 #define DEFAULT_POT_VALUE 127
// Light sensor constants /* Light sensor I2C constants */
#define LSENSE_I2C_BASE_ADDR 0x38 #define LSENSE_I2C_BASE_ADDR 0x38
#define LSENSE_EXPECTED_ID 0xE0 #define LSENSE_EXPECTED_ID 0xE0
#define LSENSE_DATA_SIZE 12 #define LSENSE_DATA_SIZE 12
#endif // CONFIG_H /**
* @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 — 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 */

View file

@ -1,6 +1,6 @@
/* /*
* @file eeprom.cpp * @file eeprom.cpp
* @brief EEPROM management implementation * @brief EEPROM management with dual-partition wear leveling.
* *
* Created: 21.09.2025 * Created: 21.09.2025
* Author: ThePetrovich * Author: ThePetrovich
@ -16,35 +16,129 @@
#include <Arduino.h> #include <Arduino.h>
#include <EEPROM.h> #include <EEPROM.h>
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) void eeprom_init(void)
{ {
if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) { bool a_valid = partition_valid(PARTITION_A_ADDR);
potentiometer_settings_t default_settings = { bool b_valid = partition_valid(PARTITION_B_ADDR);
.hv_pot = DEFAULT_POT_VALUE, .amp_pot = DEFAULT_POT_VALUE, .det_pot = DEFAULT_POT_VALUE};
eeprom_save_pot_settings(&default_settings); if (a_valid && b_valid) {
EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE); 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) { /* Write to whichever partition is NOT currently active. */
return; 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); uint8_t old_gen = EEPROM.read(active_base + GEN_OFFSET);
settings->amp_pot = EEPROM.read(EEPROM_POT_AMP_ADDR); uint8_t new_gen = old_gen + 1;
settings->det_pot = EEPROM.read(EEPROM_POT_DET_ADDR);
} config.magic1 = 0xA5;
config.magic2 = 0x5A;
void eeprom_save_pot_settings(const potentiometer_settings_t *settings)
{ write_partition(target_base, &config, new_gen);
if (settings == nullptr) { g_active_partition = target_partition;
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);
} }

View file

@ -1,6 +1,16 @@
/* /*
* @file eeprom.h * @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 * Created: 21.09.2025
* Author: ThePetrovich * Author: ThePetrovich
@ -17,30 +27,16 @@
#include <stdint.h> #include <stdint.h>
/** /**
* @brief Potentiometer settings structure * @brief Load the global @c config from EEPROM.
*/ * Picks the freshest valid partition; writes factory defaults if both
typedef struct { * partitions are blank or corrupt.
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
*/ */
void eeprom_init(void); void eeprom_init(void);
/** /**
* @brief Load potentiometer settings from EEPROM * @brief Persist the current global @c config to the alternate partition,
* @param settings Pointer to settings structure to populate * bumping the generation counter to flip the active partition.
*/ */
void eeprom_load_pot_settings(potentiometer_settings_t *settings); void eeprom_save_config(void);
/** #endif /* DET_EEPROM_H */
* @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

View file

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

View file

@ -1,6 +1,7 @@
/* /*
* @file lsense.cpp * @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 * Created: 21.09.2025
* Author: ThePetrovich * Author: ThePetrovich
@ -21,9 +22,6 @@
static SoftwareI2C g_i2c_buses[NUM_BUSES]; static SoftwareI2C g_i2c_buses[NUM_BUSES];
/**
* @brief Light sensor data structure
*/
typedef union __attribute__((packed)) { typedef union __attribute__((packed)) {
struct __attribute__((packed)) { struct __attribute__((packed)) {
uint16_t red; 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 light_sensor_data_t g_sensor_data[NUM_BUSES][SENSORS_PER_BUS];
static uint8_t g_sensor_addresses[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_total = 0;
static uint8_t g_sensors_detected_per_bus[NUM_BUSES]; 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) static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr)
{ {
uint8_t addr = LSENSE_I2C_BASE_ADDR | 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.beginTransmission(addr);
i2c_bus.write(0x41); i2c_bus.write(0x41);
i2c_bus.write(0b00101101); // IR gain x1, RGB gain x1, 35ms mode i2c_bus.write(0b00101101); /* IR gain x1, RGB gain x1, 35ms mode */
i2c_bus.write(0b00010000); // RGB_EN = 1, measurement active i2c_bus.write(0b00010000); /* RGB_EN = 1, measurement active */
i2c_bus.endTransmission(); 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) static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_sensor_data_t *sensor_data)
{ {
if (sensor_data == nullptr) 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; uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr;
i2c_bus.beginTransmission(addr); i2c_bus.beginTransmission(addr);
i2c_bus.write(0x50); // Start from red register i2c_bus.write(0x50);
i2c_bus.endTransmission(); i2c_bus.endTransmission();
i2c_bus.requestFrom(addr, (uint8_t)LSENSE_DATA_SIZE); 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(); 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) static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr)
{ {
uint8_t response = 0; uint8_t response = 0;
uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr;
i2c_bus.beginTransmission(addr); i2c_bus.beginTransmission(addr);
i2c_bus.write(0x92); // Manufacturer ID register i2c_bus.write(0x92);
i2c_bus.endTransmission(); i2c_bus.endTransmission();
i2c_bus.requestFrom(addr, (uint8_t)1); i2c_bus.requestFrom(addr, (uint8_t)1);
if (i2c_bus.available()) { if (i2c_bus.available()) {
response = i2c_bus.read(); response = i2c_bus.read();
if (response == 0xFF) { if (response == 0xFF) {
response = 0; // Invalid response response = 0;
} }
} }
return response; 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) static void initialize_i2c_bus(int bus_number, bool pin_order_inverted)
{ {
uint8_t sda_pin, scl_pin; 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; scl_pin = pin_order_inverted ? SDA2 : SCL2;
break; break;
default: default:
// Invalid bus number
return; return;
} }
SoftwareI2C *i2c_bus = &g_i2c_buses[bus_number]; SoftwareI2C *i2c_bus = &g_i2c_buses[bus_number];
i2c_bus->begin(sda_pin, scl_pin); i2c_bus->begin(sda_pin, scl_pin);
configure_light_sensor(*i2c_bus, 0); configure_light_sensor(*i2c_bus, 0);
configure_light_sensor(*i2c_bus, 1); configure_light_sensor(*i2c_bus, 1);
} }
/** static void detect_sensors_on_bus(int bus_number)
* @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)
{ {
if (bus_number < 0 || bus_number >= NUM_BUSES) if (bus_number < 0 || bus_number >= NUM_BUSES)
return; return;
g_sensors_detected_per_bus[bus_number] = 0; g_sensors_detected_per_bus[bus_number] = 0;
// Try normal pin order first
initialize_i2c_bus(bus_number, false); initialize_i2c_bus(bus_number, false);
g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0); 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) { 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]++; g_sensors_detected_per_bus[bus_number]++;
} }
// If no sensors found, try inverted pin order
if (g_sensors_detected_per_bus[bus_number] == 0) { if (g_sensors_detected_per_bus[bus_number] == 0) {
initialize_i2c_bus(bus_number, true); initialize_i2c_bus(bus_number, true);
g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0); 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)
} }
} }
/*******************************************************************************/ void lsense_detect_all(void)
/*******************************************************************************/
/* Command handlers */
/*******************************************************************************/
/*******************************************************************************/
void lsense_cmd_presence(void)
{ {
g_sensors_detected_total = 0; g_sensors_detected_total = 0;
detect_sensors_on_bus(BUS_X);
detect_sensors_on_bus_generic(BUS_X); detect_sensors_on_bus(BUS_Y);
detect_sensors_on_bus_generic(BUS_Y); detect_sensors_on_bus(BUS_Z);
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();
} }
void lsense_cmd_read(void) void lsense_get_presence(lsense_presence_t *out)
{ {
detect_sensors_on_bus_generic(BUS_X); if (out == nullptr)
read_light_sensor(g_i2c_buses[BUS_X], 0, &g_sensor_data[BUS_X][SENSOR_NEG]); return;
read_light_sensor(g_i2c_buses[BUS_X], 1, &g_sensor_data[BUS_X][SENSOR_POS]); 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); static uint16_t extract_channel(const light_sensor_data_t *d, uint8_t channel)
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]); 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); void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out)
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]); if (out == nullptr)
return;
Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue & 0xFF); for (int bus = 0; bus < NUM_BUSES; bus++) {
Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue >> 8); read_light_sensor(g_i2c_buses[bus], 0, &g_sensor_data[bus][SENSOR_NEG]);
Serial.write(g_sensor_data[BUS_X][SENSOR_POS].data.blue & 0xFF); read_light_sensor(g_i2c_buses[bus], 1, &g_sensor_data[bus][SENSOR_POS]);
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);
Serial.flush(); out->values[0] = extract_channel(&g_sensor_data[BUS_X][SENSOR_NEG], channel);
SERIAL_BUFFER_CLEAR(); 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 * @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 * Author: ThePetrovich
* *
* Copyright YKSA - Sakha Aerospace Systems, LLC. * Copyright YKSA - Sakha Aerospace Systems, LLC.
@ -14,30 +14,69 @@
#ifndef LSENSE_H #ifndef LSENSE_H
#define LSENSE_H #define LSENSE_H
#include <Arduino.h> #include <stdint.h>
#define NUM_BUSES 3 #define NUM_BUSES 3
#define SENSORS_PER_BUS 2 #define SENSORS_PER_BUS 2
enum { enum { BUS_X = 0, BUS_Y = 1, BUS_Z = 2 };
BUS_X = 0, // X-axis (bus 0) enum { SENSOR_NEG = 0, SENSOR_POS = 1 };
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)
};
/** /**
* @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);
}

213
sbc_fw/protocol.h Normal file
View file

@ -0,0 +1,213 @@
/*
* @file protocol.h
* @brief Serial protocol definitions, command structures and dispatch table.
*
* 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.
* See the LICENSE file for details.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef PROTOCOL_H
#define PROTOCOL_H
#include "config.h"
#include "lsense.h"
#include <stdint.h>
#define PROTOCOL_RESP_ACK 0x414B /* "AK" */
#define PROTOCOL_RESP_NAK 0x4E4B /* "NK" */
#define PROTOCOL_RESP_ERR 0x4552 /* "ER" */
/**
* @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;
/**
* @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);
/*
* 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 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;
/* Light sensor protocol payloads reuse the lsense module types directly
* so handlers can populate them via a single call without field copies. */
/** @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)
*/
typedef struct protocol_read_light_sensors_handler_t {
uint8_t channel;
} __attribute__((packed)) protocol_read_light_sensors_handler_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;
/* 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 Response payload for #CMD_GET_CONFIGURATION — alias of #config_t. */
typedef config_t protocol_get_configuration_response_t;
/** @brief Command payload for #CMD_SET_CONFIGURATION — alias of #config_t. */
typedef config_t protocol_set_configuration_handler_t;
/** @brief Command payload for #CMD_SET_POTENTIOMETERS — alias of #potentiometers_t. */
typedef potentiometers_t protocol_set_potentiometers_handler_t;
/** @brief Command payload for #CMD_SET_CALIBRATION — alias of #calibration_t. */
typedef calibration_t protocol_set_calibration_handler_t;
/** @brief Command payload for #CMD_SET_FLAGS — alias of #config_flags_t. */
typedef config_flags_t protocol_set_flags_handler_t;
/** @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 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;
/** @brief Response payload for #CMD_GET_TELEMETRY. */
typedef struct protocol_get_telemetry_response_t {
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;
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;
/** @brief Response payload for #CMD_GET_COUNTS. */
typedef struct protocol_get_counts_response_t {
uint32_t counts;
} __attribute__((packed)) protocol_get_counts_response_t;
/** @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 Response header for #CMD_READ_SPECTRUM_DATA.
* Followed on the wire by @c length bytes of raw spectrum data.
*/
typedef struct protocol_read_spectrum_chunk_response_t {
uint16_t offset;
uint16_t length;
} __attribute__((packed)) protocol_read_spectrum_chunk_response_t;
#endif /* PROTOCOL_H */

View file

@ -18,49 +18,55 @@
#include <stdint.h> #include <stdint.h>
#include <util/atomic.h> #include <util/atomic.h>
#include "adc.h"
#include "config.h" #include "config.h"
#include "eeprom.h" #include "eeprom.h"
#include "iodefs.h" #include "iodefs.h"
#include "rsense.h" #include "rsense.h"
#include "utils.h"
static volatile union __attribute__((packed)) { static volatile union __attribute__((packed)) {
uint16_t channels_16[DETECTOR_CHANNEL_COUNT]; uint16_t channels_16[RSENSE_CHANNEL_COUNT];
uint32_t channels_32[DETECTOR_CHANNEL_32_COUNT];
} g_detector_counts = {0}; } g_detector_counts = {0};
static uint8_t g_spectrum_mode = 0; // 0 = 16-bit, 1 = 32-bit 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_read_pointer = 0; static uint16_t g_cps_last = 0;
static uint32_t g_total_counts = 0; static uint32_t g_total_counts = 0;
static uint32_t g_total_time = 0; static volatile uint32_t g_counts_delta = 0; /* counts since last CMD_GET_COUNTS */
static uint32_t g_flush_time = 0;
static volatile uint16_t g_counts_cpm = 0;
static uint32_t g_cpm_time = 0; static struct {
static uint16_t g_cpm_last = 0; 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 void initialize_event_system(void);
static void enable_fast_adc(void); static inline void analog_read_interrupt(void);
static void restore_default_adc(void); static void rsense_drift_compensation_tick(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);
void rsense_init(void) void rsense_init(void)
{ {
eeprom_load_pot_settings(&g_current_pot_settings);
pinMode(HV_EN, OUTPUT); pinMode(HV_EN, OUTPUT);
pinMode(DET_EN, OUTPUT); pinMode(DET_EN, OUTPUT);
pinMode(DET_RST, OUTPUT); pinMode(DET_RST, OUTPUT);
// Disable HV and Detector on startup
digitalWrite(HV_EN, LOW); digitalWrite(HV_EN, LOW);
digitalWrite(DET_EN, LOW); digitalWrite(DET_EN, LOW);
@ -72,395 +78,263 @@ void rsense_init(void)
digitalWrite(DET_CS, HIGH); digitalWrite(DET_CS, HIGH);
digitalWrite(AMP_CS, HIGH); digitalWrite(AMP_CS, HIGH);
apply_potentiometer_settings(&g_current_pot_settings); apply_potentiometer_settings();
pinMode(DET_TRIG_PIN, INPUT); pinMode(DET_TRIG_PIN, INPUT);
initialize_event_system(); initialize_event_system();
} }
/**
* @brief Initialize event system for radiation detection
*/
static void initialize_event_system(void) static void initialize_event_system(void)
{ {
// Enable event on DET_TRIG_PIN
Event2.set_generator(event::gen2::pin_pc7); Event2.set_generator(event::gen2::pin_pc7);
Event2.set_user(event::user::adc0_start); 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(); Event2.start();
} }
/** static inline void analog_read_interrupt(void)
* @brief Enable ADC for fast radiation detection
*/
static void enable_fast_adc(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
ADC0.CTRLA |= ADC_ENABLE_bm;
// Enable oversampling x16 for 12-bit result
#if ADC_OVERSAMPLING_BITS == 1
ADC0.CTRLB |= ADC_SAMPNUM_ACC4_gc;
#elif ADC_OVERSAMPLING_BITS == 2
ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc;
#elif ADC_OVERSAMPLING_BITS == 3
ADC0.CTRLB |= ADC_SAMPNUM_ACC64_gc;
#else
#error "Unsupported ADC oversampling setting"
#endif
ADC0.CTRLC = 0; // reset
ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VDDREF_gc; // CLK_PER/32, VDD as reference
// Enable interrupt on conversion complete
ADC0.EVCTRL |= ADC_STARTEI_bm; // Start event input
ADC0.INTCTRL |= ADC_RESRDY_bm;
// Configure VREF, Microchip DS40002015B page 424
VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc;
// Select ADC channel
ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // DET_SIG_PIN
}
}
/**
* @brief Restore ADC to default settings
*/
static void restore_default_adc(void)
{
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
// Disable oversampling
ADC0.CTRLB &= ~ADC_SAMPNUM_gm;
// Set reference back to VREFA
ADC0.CTRLC &= ~ADC_REFSEL_gm;
ADC0.CTRLC |= ADC_REFSEL_VREFA_gc; // VREFA as reference
// Disable interrupt on conversion complete
ADC0.EVCTRL &= ~ADC_STARTEI_bm; // Start event input
ADC0.INTCTRL &= ~ADC_RESRDY_bm;
}
}
/**
* @brief Handle ADC conversion results for radiation detection
*/
static inline void handle_analog_read_interrupt(void)
{ {
digitalWriteFast(DET_RST, HIGH); digitalWriteFast(DET_RST, HIGH);
uint16_t channel = ADC0.RES; // read result to clear flag uint16_t channel = ADC0.RES;
digitalWriteFast(STATUS_LED, LOW); digitalWriteFast(STATUS_LED, LOW);
if (g_spectrum_mode == 0) { channel = channel >> (ADC_OVERSAMPLING_BITS + 2);
// 16-bit spectrum mode g_detector_counts.channels_16[channel & RSENSE_CHANNEL_MASK]++;
// 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 & ADC_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 & ADC_CHANNEL_32_MASK]++;
}
g_total_counts++; g_total_counts++;
g_counts_cpm++; g_counts_cps++;
g_counts_delta++;
digitalWriteFast(DET_RST, LOW); digitalWriteFast(DET_RST, LOW);
} }
/** ISR(ADC0_RESRDY_vect) { analog_read_interrupt(); }
* @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();
}
}
/** /**
* @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 < DETECTOR_CHANNEL_COUNT; i++) { if (v < (int16_t)lo)
g_detector_counts.channels_16[i] = 0; return lo;
} if (v > (int16_t)hi)
return hi;
return (uint8_t)v;
} }
/** static void rsense_drift_compensation_tick(void)
* @brief Apply potentiometer settings to hardware
* @param settings Pointer to potentiometer settings
*/
static void apply_potentiometer_settings(const potentiometer_settings_t *settings)
{ {
if (settings == nullptr) if (!config.flags.fields.temperature_drift_compensation || !g_rsense_enabled)
return; return;
// HV potentiometer setup uint32_t period = config.temp_drift_compensation.drift_period_ms;
digitalWrite(HV_CS, LOW); if (period == 0)
SPI.transfer(settings->hv_pot); period =
digitalWrite(HV_CS, HIGH); 300000; /* Default to 5 minutes if not set, to avoid excessive compensation when misconfigured. */
delay(1);
// SiPM Pre-amp gain potentiometer setup if (millis() - g_drift_last_ms < period)
digitalWrite(AMP_CS, LOW); return;
SPI.transfer(settings->amp_pot);
digitalWrite(AMP_CS, HIGH);
delay(1);
// Detection threshold potentiometer setup /* Read all three temperature sensors (each ~10 ms due to oversampling) */
digitalWrite(DET_CS, LOW); int16_t det_temp = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16));
SPI.transfer(settings->det_pot); int16_t hv_temp = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16));
digitalWrite(DET_CS, HIGH); int16_t amp_temp = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16));
delay(1);
}
/** if (!g_drift_ref_valid) {
* @brief Set new potentiometer values g_drift_ref_det = det_temp;
* @param hv High voltage potentiometer value (0-255) g_drift_ref_hv = hv_temp;
* @param amp Amplifier potentiometer value (0-255) g_drift_ref_amp = amp_temp;
* @param det Detector potentiometer value (0-255) g_drift_ref_valid = true;
*/ g_drift_last_ms = millis();
static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det) return;
{
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)
{
restore_default_adc();
// 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 = read_adc_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();
enable_fast_adc();
}
/**
* @brief Send temperature sensor data
*/
static void send_temperature_data(void)
{
int16_t hv_temp_c = adc_to_temperature_c(read_adc_oversampled(HV_TEMP_PIN, 16));
int16_t amp_temp_c = adc_to_temperature_c(read_adc_oversampled(AMP_TEMP_PIN, 16));
int16_t det_temp_c = adc_to_temperature_c(read_adc_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);
enable_fast_adc();
SERIAL_SEND_OK();
}
void rsense_cmd_disable(void)
{
digitalWrite(DET_EN, LOW);
digitalWrite(HV_EN, LOW);
restore_default_adc();
SERIAL_SEND_OK();
}
void rsense_cmd_flush(void)
{
clear_detector_counters();
clear_channel_data();
SERIAL_SEND_OK();
}
void rsense_cmd_dump_channels(void)
{
restore_default_adc();
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 >= (DETECTOR_CHANNEL_32_COUNT - 32)) {
g_read_pointer = DETECTOR_CHANNEL_32_COUNT - 32;
} }
// Send 2 bytes of CRC16 checksum for the 32 channels /* Deltas in 0.1°C units */
uint16_t crc16 = calculate_crc16_xmodem((uint8_t *)(&g_detector_counts.channels_32[g_read_pointer]), 32 * 4); int16_t delta_det = det_temp - g_drift_ref_det;
Serial.write(crc16 & 0xFF); int16_t delta_hv = hv_temp - g_drift_ref_hv;
Serial.write(crc16 >> 8); int16_t delta_amp = amp_temp - g_drift_ref_amp;
// Send 32 channel values (128 bytes total) /* Drift coefficients in pot_units per °C, scaled by 256.
for (uint16_t i = 0; i < 32 && g_read_pointer < DETECTOR_CHANNEL_32_COUNT; i++, g_read_pointer++) { * adj = coeff * delta_0.1C / 10 / 256 = coeff * delta_0.1C / 2560 */
uint32_t channel_value = g_detector_counts.channels_32[g_read_pointer]; bool needs_update = false;
Serial.write(channel_value & 0xFF);
Serial.write((channel_value >> 8) & 0xFF);
Serial.write((channel_value >> 16) & 0xFF);
Serial.write((channel_value >> 24) & 0xFF);
Serial.flush(); int16_t new_hv = config.pots.pot_hv;
SERIAL_BUFFER_CLEAR(); int16_t new_amp = config.pots.pot_amp;
delay(1); 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(); if (dc->pot_amp_low != dc->pot_amp_high) {
SERIAL_BUFFER_CLEAR(); int16_t adj = (int16_t)(((int32_t)dc->gain_drift * delta_amp) / 2560L);
enable_fast_adc(); if (adj != 0) {
} new_amp = (int16_t)config.pots.pot_amp + adj;
needs_update = true;
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_mode(void)
{
// Wait for 1 byte specifying mode
while (Serial.available() < 1)
;
uint8_t mode = Serial.read();
if (mode <= 1) {
g_spectrum_mode = mode;
clear_channel_data();
SERIAL_SEND_OK();
} }
Serial.flush(); if (dc->pot_det_low != dc->pot_det_high) {
SERIAL_BUFFER_CLEAR(); 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;
}
}
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) void rsense_periodic(void)
{ {
if (millis() - g_cpm_time > CPM_UPDATE_PERIOD) { if (millis() - g_cps_time > RSENSE_UPDATE_PERIOD) {
uint32_t elapsed_time = millis() - g_cpm_time; uint32_t elapsed_time = millis() - g_cps_time;
g_cpm_last = (uint16_t)(((uint32_t)g_counts_cpm * 60UL * 1000UL) / elapsed_time); g_cps_last = (uint16_t)(((uint32_t)g_counts_cps * 1000UL) / elapsed_time);
g_counts_cpm = 0; g_counts_cps = 0;
g_cpm_time = millis(); 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 * @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 * Author: ThePetrovich
* *
* Copyright YKSA - Sakha Aerospace Systems, LLC. * Copyright YKSA - Sakha Aerospace Systems, LLC.
@ -14,58 +15,59 @@
#ifndef RSENSE_H #ifndef RSENSE_H
#define 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); void rsense_init(void);
/** /**
* @brief Send telemetry data via serial * @brief Periodic housekeeping: CPM accumulation window and drift
*/ * compensation tick. Should be called from the main loop.
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_mode(void);
/**
* @brief Periodic tasks for radiation sensor
* Updates CPM calculation every 10 seconds
* Call from main loop
*/ */
void rsense_periodic(void); 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 * @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 * Created: 21.09.2025
* Author: ThePetrovich * Author: ThePetrovich
@ -15,15 +15,126 @@
#include <SPI.h> #include <SPI.h>
#include <SoftwareI2C.h> #include <SoftwareI2C.h>
#include <stdint.h> #include <stdint.h>
#include <string.h>
#include "config.h" #include "config.h"
#include "eeprom.h" #include "eeprom.h"
#include "iodefs.h" #include "iodefs.h"
#include "lsense.h" #include "lsense.h"
#include "protocol.h"
#include "rsense.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; 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() void setup()
{ {
pinMode(STATUS_LED, OUTPUT); pinMode(STATUS_LED, OUTPUT);
@ -38,102 +149,9 @@ void setup()
rsense_init(); 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_mode();
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() void loop()
{ {
// Process incoming serial commands protocol_receive();
while (Serial.available()) {
char command = Serial.read();
handle_serial_command(command);
}
update_status_led(); update_status_led();
rsense_periodic(); rsense_periodic();
} }

View file

@ -1,57 +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;
}
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
return (int16_t)((adc_value * ADC_VREF_MV / ADC_RESOLUTION - TEMP_SENSOR_OFFSET_MV));
}
uint16_t adc_to_voltage_mv(uint16_t adc_value) { return (uint16_t)(adc_value * VOLTAGE_MV_PER_STEP); }
uint16_t read_adc_oversampled(uint8_t pin, uint8_t samples)
{
uint32_t sum = 0;
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);
}

View file

@ -1,49 +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>
/**
* @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);
/**
* @brief Convert ADC reading to temperature in 0.1°C
* @param adc_value 12-bit 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
*/
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
*/
uint16_t read_adc_oversampled(uint8_t pin, uint8_t samples);
#endif // UTILS_H