diff --git a/sbc_fw/adc.cpp b/sbc_fw/adc.cpp deleted file mode 100644 index 0d708bd..0000000 --- a/sbc_fw/adc.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/* - * @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 -#include - -/** - * @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 */ -} diff --git a/sbc_fw/adc.h b/sbc_fw/adc.h deleted file mode 100644 index ded854a..0000000 --- a/sbc_fw/adc.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @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 - -/** - * @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_ */ \ No newline at end of file diff --git a/sbc_fw/config.h b/sbc_fw/config.h index 04ea4ca..e240d5c 100644 --- a/sbc_fw/config.h +++ b/sbc_fw/config.h @@ -1,7 +1,6 @@ /* * @file config.h - * @brief System configuration, persisted-config struct definition, and - * compile-time constants shared between modules. + * @brief System configuration and constants * * Created: 21.09.2025 * Author: ThePetrovich @@ -15,122 +14,60 @@ #ifndef CONFIG_H #define CONFIG_H -#include +// Firmware version +#define FIRMWARE_VERSION_MAJOR 2 +#define FIRMWARE_VERSION_MINOR 03 -/* Firmware version */ -#define FIRMWARE_VERSION_MAJOR 3 -#define FIRMWARE_VERSION_MINOR 00 - -/* Serial communication */ +// Serial communication #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 MAIN_LOOP_DELAY 10 -#define RSENSE_UPDATE_PERIOD 1000U +#define CPM_UPDATE_PERIOD 10000U -/* Spectrum channel masks and counts */ -#define RSENSE_CHANNEL_MASK 0x3FFU /* 10-bit channel mask */ -#define RSENSE_CHANNEL_COUNT 1024 +// ADC and detector settings +#define ADC_OVERSAMPLING_BITS 2 // 16x oversampling -> 2 extra bits +#define ADC_CHANNEL_16_MASK 0x7FFU // 11-bit channel mask +#define ADC_CHANNEL_32_MASK 0x3FFU // 10-bit channel mask +#define DETECTOR_CHANNEL_COUNT 2048 +#define DETECTOR_CHANNEL_32_COUNT 1024 -/* 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 */ +// Temperature sensor constants (MCP9700) +#define TEMP_SENSOR_OFFSET_MV 500L // 500 mV at 0°C +#define TEMP_SENSOR_SCALE_MV 10L // 10 mV per degree +#define ADC_VREF_MV 3000L // 3.0V reference +#define ADC_RESOLUTION 4096L // 12-bit effective resolution -/* 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 -/* Default potentiometer wiper value (mid-scale, 127/255) */ +// 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 #define DEFAULT_POT_VALUE 127 -/* Light sensor I2C constants */ +// Light sensor 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 — 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 diff --git a/sbc_fw/eeprom.cpp b/sbc_fw/eeprom.cpp index 5106088..65452a3 100644 --- a/sbc_fw/eeprom.cpp +++ b/sbc_fw/eeprom.cpp @@ -1,6 +1,6 @@ /* * @file eeprom.cpp - * @brief EEPROM management with dual-partition wear leveling. + * @brief EEPROM management implementation * * Created: 21.09.2025 * Author: ThePetrovich @@ -16,129 +16,35 @@ #include #include -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) { - bool a_valid = partition_valid(PARTITION_A_ADDR); - bool b_valid = partition_valid(PARTITION_B_ADDR); + 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}; - 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; + eeprom_save_pot_settings(&default_settings); + EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE); } } -void eeprom_save_config(void) +void eeprom_load_pot_settings(potentiometer_settings_t *settings) { - /* 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; + if (settings == nullptr) { + return; + } - 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; + 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); } diff --git a/sbc_fw/eeprom.h b/sbc_fw/eeprom.h index 5f6ec26..2ba3e2a 100644 --- a/sbc_fw/eeprom.h +++ b/sbc_fw/eeprom.h @@ -1,16 +1,6 @@ /* * @file eeprom.h - * @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. + * @brief EEPROM management for persistent settings * * Created: 21.09.2025 * Author: ThePetrovich @@ -27,16 +17,30 @@ #include /** - * @brief Load the global @c config from EEPROM. - * Picks the freshest valid partition; writes factory defaults if both - * partitions are blank or corrupt. + * @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 */ void eeprom_init(void); /** - * @brief Persist the current global @c config to the alternate partition, - * bumping the generation counter to flip the active partition. + * @brief Load potentiometer settings from EEPROM + * @param settings Pointer to settings structure to populate */ -void eeprom_save_config(void); +void eeprom_load_pot_settings(potentiometer_settings_t *settings); -#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 diff --git a/sbc_fw/iodefs.h b/sbc_fw/iodefs.h index 7cb2d68..6dabb27 100644 --- a/sbc_fw/iodefs.h +++ b/sbc_fw/iodefs.h @@ -1,6 +1,6 @@ /* * @file iodefs.h - * @brief Hardware pin assignments for the SBC firmware. + * @brief * * Created: 21.09.2025 05:39:48 * Author: ThePetrovich @@ -18,38 +18,32 @@ #include #include -/* 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 DET_TRIG_PIN 21 // PC7, event input +#define DET_READ_PIN A3 // PD3 (ADC3) +#define DET_PREAMP_PIN A2 // PD2 (ADC2) -/* Status indicator */ -#define STATUS_LED 31 /* PE1 */ +#define STATUS_LED 31 // PE1 -/* Power and reset control */ -#define HV_EN 16 /* PC2 */ -#define DET_EN 18 /* PC4 */ -#define DET_RST 20 /* PC6 */ +#define HV_EN 16 // PC2 +#define DET_EN 18 // PC4 +#define DET_RST 20 // PC6 -/* 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 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) -/* AD5160 digital potentiometer chip selects */ -#define HV_CS 15 /* PC1 */ -#define AMP_CS 17 /* PC3 */ -#define DET_CS 19 /* PC5 */ +#define HV_CS 15 // PC1 +#define AMP_CS 17 // PC3 +#define DET_CS 19 // PC5 -/* Light sensor I2C bus pins (software-bitbanged) */ -#define SCL0 8 /* PB0 */ -#define SDA0 9 /* PB1 */ +#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 \ No newline at end of file diff --git a/sbc_fw/lsense.cpp b/sbc_fw/lsense.cpp index f773d78..2d71454 100644 --- a/sbc_fw/lsense.cpp +++ b/sbc_fw/lsense.cpp @@ -1,7 +1,6 @@ /* * @file lsense.cpp - * @brief Light sensor implementation: software I2C, detection, and channel - * readout for the six TCS-style light sensors on three buses. + * @brief Light sensor implementation * * Created: 21.09.2025 * Author: ThePetrovich @@ -22,6 +21,9 @@ static SoftwareI2C g_i2c_buses[NUM_BUSES]; +/** + * @brief Light sensor data structure + */ typedef union __attribute__((packed)) { struct __attribute__((packed)) { uint16_t red; @@ -37,9 +39,21 @@ 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; @@ -50,11 +64,17 @@ 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) @@ -63,7 +83,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); + i2c_bus.write(0x50); // Start from red register i2c_bus.endTransmission(); i2c_bus.requestFrom(addr, (uint8_t)LSENSE_DATA_SIZE); @@ -73,25 +93,37 @@ 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); + i2c_bus.write(0x92); // Manufacturer ID register i2c_bus.endTransmission(); i2c_bus.requestFrom(addr, (uint8_t)1); if (i2c_bus.available()) { response = i2c_bus.read(); if (response == 0xFF) { - response = 0; + response = 0; // Invalid 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) { uint8_t sda_pin, scl_pin; @@ -110,22 +142,29 @@ 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); } -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) 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) { @@ -138,6 +177,7 @@ static void detect_sensors_on_bus(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); @@ -153,67 +193,62 @@ static void detect_sensors_on_bus(int bus_number) } } -void lsense_detect_all(void) +/*******************************************************************************/ +/*******************************************************************************/ +/* Command handlers */ +/*******************************************************************************/ +/*******************************************************************************/ + +void lsense_cmd_presence(void) { g_sensors_detected_total = 0; - detect_sensors_on_bus(BUS_X); - detect_sensors_on_bus(BUS_Y); - detect_sensors_on_bus(BUS_Z); + + 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(); } -void lsense_get_presence(lsense_presence_t *out) +void lsense_cmd_read(void) { - 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_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]); -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_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]); -void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out) -{ - if (out == nullptr) - return; + 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]); - 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.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); - 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); -} + Serial.flush(); + SERIAL_BUFFER_CLEAR(); +} \ No newline at end of file diff --git a/sbc_fw/lsense.h b/sbc_fw/lsense.h index 8ef49b0..1d7cca3 100644 --- a/sbc_fw/lsense.h +++ b/sbc_fw/lsense.h @@ -1,8 +1,8 @@ /* * @file lsense.h - * @brief Light sensor management. + * @brief Light sensor management and command handling * - * Created: 21.09.2025 + * Created: 21.09.2025 05:53:32 * Author: ThePetrovich * * Copyright YKSA - Sakha Aerospace Systems, LLC. @@ -14,69 +14,30 @@ #ifndef LSENSE_H #define LSENSE_H -#include +#include -#define NUM_BUSES 3 +#define NUM_BUSES 3 #define SENSORS_PER_BUS 2 -enum { BUS_X = 0, BUS_Y = 1, BUS_Z = 2 }; -enum { SENSOR_NEG = 0, SENSOR_POS = 1 }; +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) +}; /** - * @brief Detection result for the six light sensors across three I2C buses. - * Reused as the response payload of #CMD_LIGHT_SENSOR_PRESENCE. + * @brief Send light sensor presence information via serial */ -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; +void lsense_cmd_presence(void); /** - * @brief Per-sensor channel values for a single read across all 6 sensors. - * Reused as the response payload of #CMD_READ_LIGHT_SENSORS. + * @brief Read and send light sensor data via serial */ -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; +void lsense_cmd_read(void); -/** - * @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 */ +#endif // LSENSE_H diff --git a/sbc_fw/protocol.cpp b/sbc_fw/protocol.cpp deleted file mode 100644 index fa57776..0000000 --- a/sbc_fw/protocol.cpp +++ /dev/null @@ -1,328 +0,0 @@ -/* - * @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 -#include -#include -#include - -#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); -} diff --git a/sbc_fw/protocol.h b/sbc_fw/protocol.h deleted file mode 100644 index 52e5425..0000000 --- a/sbc_fw/protocol.h +++ /dev/null @@ -1,213 +0,0 @@ -/* - * @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 - -#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 */ diff --git a/sbc_fw/rsense.cpp b/sbc_fw/rsense.cpp index d739df3..6744221 100644 --- a/sbc_fw/rsense.cpp +++ b/sbc_fw/rsense.cpp @@ -18,55 +18,49 @@ #include #include -#include "adc.h" #include "config.h" #include "eeprom.h" #include "iodefs.h" #include "rsense.h" +#include "utils.h" static volatile union __attribute__((packed)) { - uint16_t channels_16[RSENSE_CHANNEL_COUNT]; + uint16_t channels_16[DETECTOR_CHANNEL_COUNT]; + uint32_t channels_32[DETECTOR_CHANNEL_32_COUNT]; } g_detector_counts = {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 uint8_t g_spectrum_mode = 0; // 0 = 16-bit, 1 = 32-bit + +static uint16_t g_read_pointer = 0; static uint32_t g_total_counts = 0; -static volatile uint32_t g_counts_delta = 0; /* counts since last CMD_GET_COUNTS */ +static uint32_t g_total_time = 0; +static uint32_t g_flush_time = 0; +static volatile uint16_t g_counts_cpm = 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 uint32_t g_cpm_time = 0; +static uint16_t g_cpm_last = 0; -/* - * 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 potentiometer_settings_t g_current_pot_settings; -static void apply_potentiometer_settings(void); +static void apply_potentiometer_settings(const potentiometer_settings_t *settings); static void initialize_event_system(void); -static inline void analog_read_interrupt(void); -static void rsense_drift_compensation_tick(void); +static void enable_fast_adc(void); +static void restore_default_adc(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) { + 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); @@ -78,263 +72,395 @@ void rsense_init(void) digitalWrite(DET_CS, HIGH); digitalWrite(AMP_CS, HIGH); - apply_potentiometer_settings(); + apply_potentiometer_settings(&g_current_pot_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(); } -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); - uint16_t channel = ADC0.RES; + uint16_t channel = ADC0.RES; // read result to clear flag digitalWriteFast(STATUS_LED, LOW); - channel = channel >> (ADC_OVERSAMPLING_BITS + 2); - g_detector_counts.channels_16[channel & RSENSE_CHANNEL_MASK]++; + 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 & 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_counts_cps++; - g_counts_delta++; + g_counts_cpm++; digitalWriteFast(DET_RST, LOW); } -ISR(ADC0_RESRDY_vect) { analog_read_interrupt(); } - /** - * @brief Clamp a signed value into the unsigned interval [lo, hi]. + * @brief Clear all detector counters */ -static uint8_t clamp_u8(int16_t v, uint8_t lo, uint8_t hi) -{ - if (v < (int16_t)lo) - return lo; - if (v > (int16_t)hi) - return hi; - return (uint8_t)v; -} - -static void rsense_drift_compensation_tick(void) -{ - if (!config.flags.fields.temperature_drift_compensation || !g_rsense_enabled) - return; - - 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. */ - - if (millis() - g_drift_last_ms < period) - return; - - /* 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)); - - 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; - } - - /* 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; - - /* 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; - - 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; - } - } - - 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; - } - } - - 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; - } - } - - 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_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 Clock @c value out to one AD5160 selected by @p cs_pin. - */ -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) +static void clear_detector_counters(void) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { g_total_counts = 0; - g_counts_cps = 0; - g_counts_delta = 0; - g_cps_time = millis(); + g_total_time = 0; + g_flush_time = millis(); + g_counts_cpm = 0; + g_cpm_time = millis(); } } -void rsense_flush_spectrum(void) +/** + * @brief Clear channel data + */ +static void clear_channel_data(void) { - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) - { - for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) { - g_detector_counts.channels_16[i] = 0; - } + for (int i = 0; i < DETECTOR_CHANNEL_COUNT; i++) { + g_detector_counts.channels_16[i] = 0; } - rsense_flush_counters(); } -uint32_t rsense_get_total_counts(void) +/** + * @brief Apply potentiometer settings to hardware + * @param settings Pointer to potentiometer settings + */ +static void apply_potentiometer_settings(const potentiometer_settings_t *settings) { - uint32_t v; - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { v = g_total_counts; } - return v; + if (settings == nullptr) + return; + + // HV potentiometer setup + digitalWrite(HV_CS, LOW); + SPI.transfer(settings->hv_pot); + digitalWrite(HV_CS, HIGH); + delay(1); + + // SiPM Pre-amp gain potentiometer setup + digitalWrite(AMP_CS, LOW); + SPI.transfer(settings->amp_pot); + digitalWrite(AMP_CS, HIGH); + delay(1); + + // Detection threshold potentiometer setup + digitalWrite(DET_CS, LOW); + SPI.transfer(settings->det_pot); + digitalWrite(DET_CS, HIGH); + delay(1); } -uint16_t rsense_get_cps(void) { return g_cps_last; } - -uint32_t rsense_get_cp10s(void) +/** + * @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) { - uint32_t sum = 0; - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) - { - for (int i = 0; i < 10; i++) { - sum += g_cp10s_data.cps[i]; - } + 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; } - 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; + // 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); + + // Send 32 channel values (128 bytes total) + for (uint16_t i = 0; i < 32 && g_read_pointer < DETECTOR_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); + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); + delay(1); } - return v; + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); + enable_fast_adc(); } -const volatile void *rsense_get_spectrum_ptr(void) { return (const volatile void *)&g_detector_counts; } +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(); + SERIAL_BUFFER_CLEAR(); +} + +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(); + } +} + +/** + * @brief ADC conversion complete interrupt handler + */ +ISR(ADC0_RESRDY_vect) { handle_analog_read_interrupt(); } diff --git a/sbc_fw/rsense.h b/sbc_fw/rsense.h index 54d883f..2f6c5e3 100644 --- a/sbc_fw/rsense.h +++ b/sbc_fw/rsense.h @@ -1,9 +1,8 @@ /* * @file rsense.h - * @brief Radiation sensor subsystem: counters, spectrum, drift compensation, - * and HV/detector power control. + * @brief Radiation sensor management and command handling * - * Created: 21.09.2025 + * Created: 21.09.2025 06:01:56 * Author: ThePetrovich * * Copyright YKSA - Sakha Aerospace Systems, LLC. @@ -15,59 +14,58 @@ #ifndef RSENSE_H #define RSENSE_H -#include +#include /** - * @brief Configure GPIO, SPI digital potentiometers, and the event system - * used by the detector trigger pin to start ADC conversions. + * @brief Initialize radiation sensor subsystem */ void rsense_init(void); /** - * @brief Periodic housekeeping: CPM accumulation window and drift - * compensation tick. Should be called from the main loop. + * @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_mode(void); + +/** + * @brief Periodic tasks for radiation sensor + * Updates CPM calculation every 10 seconds + * Call from main loop */ void rsense_periodic(void); -/** @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 */ +#endif // RSENSE_H \ No newline at end of file diff --git a/sbc_fw/sbc_fw.ino b/sbc_fw/sbc_fw.ino index bc3e670..d365375 100644 --- a/sbc_fw/sbc_fw.ino +++ b/sbc_fw/sbc_fw.ino @@ -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,126 +15,15 @@ #include #include #include -#include #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); @@ -149,9 +38,102 @@ 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_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() { - protocol_receive(); + // Process incoming serial commands + while (Serial.available()) { + char command = Serial.read(); + handle_serial_command(command); + } update_status_led(); + rsense_periodic(); } diff --git a/sbc_fw/utils.cpp b/sbc_fw/utils.cpp new file mode 100644 index 0000000..acd22fc --- /dev/null +++ b/sbc_fw/utils.cpp @@ -0,0 +1,57 @@ +/* + * @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 + +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); +} diff --git a/sbc_fw/utils.h b/sbc_fw/utils.h new file mode 100644 index 0000000..cd37faf --- /dev/null +++ b/sbc_fw/utils.h @@ -0,0 +1,49 @@ +/* + * @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 + +/** + * @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