commit db8f8408afc76c6b6c92798f583c2edb806256e5 Author: ThePetrovich Date: Mon Sep 22 19:46:11 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1785675 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Ignore build artifacts from Arduino IDE, Arduino CLI and PlatformIO +build/ +tmp/ +output/ +.pio/ +.pioenvs/ +.platformio/ +*.hex +*.elf +*.bin +*.eep +*.map +*.o +*.obj +*.a +*.d +*.lst +*.sym + +# Project-specific generated files +*.log +*.tmp + +# IDE/editor configs +.vscode/ +.idea/ +*.suo +*.user +*.userprefs +*.sln +*.proj +*.project +*.classpath +.settings/ +.vscode/* +.idea/* + +# OS files +.DS_Store +Thumbs.db + +# Arduino cache and global state +.arduino15/ +.arduino/ +.arduino-builder/ + +# Optional: ignore compiled library objects but keep source (adjust if storing built libs) +**/lib/**/*.o +**/libraries/**/*.o + +# Keep your libraries and sketches under version control +!libraries/ +!**/*.ino +!**/*.pde + +# tooling +*.exe \ No newline at end of file diff --git a/config.h b/config.h new file mode 100644 index 0000000..e240d5c --- /dev/null +++ b/config.h @@ -0,0 +1,73 @@ +/* + * @file config.h + * @brief System configuration and constants + * + * 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 CONFIG_H +#define CONFIG_H + +// Firmware version +#define FIRMWARE_VERSION_MAJOR 2 +#define FIRMWARE_VERSION_MINOR 03 + +// 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 +#define STATUS_LED_BLINK_PERIOD 500U +#define MAIN_LOOP_DELAY 10 +#define CPM_UPDATE_PERIOD 10000U + +// 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 + +// 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 + +// 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 constants +#define LSENSE_I2C_BASE_ADDR 0x38 +#define LSENSE_EXPECTED_ID 0xE0 +#define LSENSE_DATA_SIZE 12 + +#endif // CONFIG_H diff --git a/eeprom.cpp b/eeprom.cpp new file mode 100644 index 0000000..65452a3 --- /dev/null +++ b/eeprom.cpp @@ -0,0 +1,50 @@ +/* + * @file eeprom.cpp + * @brief EEPROM management 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 "eeprom.h" +#include "config.h" +#include +#include + +void eeprom_init(void) +{ + if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) { + potentiometer_settings_t default_settings = { + .hv_pot = DEFAULT_POT_VALUE, .amp_pot = DEFAULT_POT_VALUE, .det_pot = DEFAULT_POT_VALUE}; + + eeprom_save_pot_settings(&default_settings); + EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE); + } +} + +void eeprom_load_pot_settings(potentiometer_settings_t *settings) +{ + if (settings == nullptr) { + return; + } + + 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/eeprom.h b/eeprom.h new file mode 100644 index 0000000..2ba3e2a --- /dev/null +++ b/eeprom.h @@ -0,0 +1,46 @@ +/* + * @file eeprom.h + * @brief EEPROM management for persistent settings + * + * 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 DET_EEPROM_H +#define DET_EEPROM_H + +#include + +/** + * @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 Load potentiometer settings from EEPROM + * @param settings Pointer to settings structure to populate + */ +void eeprom_load_pot_settings(potentiometer_settings_t *settings); + +/** + * @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/iodefs.h b/iodefs.h new file mode 100644 index 0000000..6dabb27 --- /dev/null +++ b/iodefs.h @@ -0,0 +1,49 @@ +/* + * @file iodefs.h + * @brief + * + * Created: 21.09.2025 05:39:48 + * Author: ThePetrovich + * + * Copyright YKSA - Sakha Aerospace Systems, LLC. + * See the LICENSE file for details. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef LSENSE_IODEFS_H +#define LSENSE_IODEFS_H + +#include +#include +#include + +#define DET_TRIG_PIN 21 // PC7, event input +#define DET_READ_PIN A3 // PD3 (ADC3) +#define DET_PREAMP_PIN A2 // PD2 (ADC2) + +#define STATUS_LED 31 // PE1 + +#define HV_EN 16 // PC2 +#define DET_EN 18 // PC4 +#define DET_RST 20 // PC6 + +#define V28V0_FB_PIN A0 // PD0 (ADC0) +#define HV_TEMP_PIN A1 // PD1 (ADC1) +#define AMP_TEMP_PIN A4 // PD4 (ADC4) +#define DET_TEMP_PIN A5 // PD5 (ADC5) + +#define HV_CS 15 // PC1 +#define AMP_CS 17 // PC3 +#define DET_CS 19 // PC5 + +#define SCL0 8 // PB0 +#define SDA0 9 // PB1 + +#define SCL1 10 // PB2 +#define SDA1 11 // PB3 + +#define SCL2 12 // PB4 +#define SDA2 13 // PB5 + +#endif // LSENSE_IODEFS_H \ No newline at end of file diff --git a/lsense.cpp b/lsense.cpp new file mode 100644 index 0000000..2d71454 --- /dev/null +++ b/lsense.cpp @@ -0,0 +1,254 @@ +/* + * @file lsense.cpp + * @brief Light sensor 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 +#include +#include + +#include "config.h" +#include "iodefs.h" +#include "lsense.h" + +static SoftwareI2C g_i2c_buses[NUM_BUSES]; + +/** + * @brief Light sensor data structure + */ +typedef union __attribute__((packed)) { + struct __attribute__((packed)) { + uint16_t red; + uint16_t green; + uint16_t blue; + uint16_t reserved; + uint16_t ir; + uint16_t green2; + } data; + uint8_t bytes[LSENSE_DATA_SIZE]; +} light_sensor_data_t; + +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; + + i2c_bus.beginTransmission(addr); + i2c_bus.write(0x41); + i2c_bus.endTransmission(); + + 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.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) + return; + + uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; + + i2c_bus.beginTransmission(addr); + i2c_bus.write(0x50); // Start from red register + i2c_bus.endTransmission(); + + i2c_bus.requestFrom(addr, (uint8_t)LSENSE_DATA_SIZE); + for (uint8_t i = 0; i < LSENSE_DATA_SIZE; i++) { + sensor_data->bytes[i] = i2c_bus.read(); + } + i2c_bus.endTransmission(); +} + +/** + * @brief Detect if a light sensor is present + * @param i2c_bus Reference to I2C bus instance + * @param sensor_addr LSB of sensor address (0 or 1) + * @return Manufacturer ID (0xE0 if valid sensor detected, 0 otherwise) + */ +static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr) +{ + uint8_t response = 0; + uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; + + i2c_bus.beginTransmission(addr); + i2c_bus.write(0x92); // Manufacturer ID register + i2c_bus.endTransmission(); + + i2c_bus.requestFrom(addr, (uint8_t)1); + if (i2c_bus.available()) { + response = i2c_bus.read(); + if (response == 0xFF) { + 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; + + switch (bus_number) { + case BUS_X: + sda_pin = pin_order_inverted ? SCL1 : SDA1; + scl_pin = pin_order_inverted ? SDA1 : SCL1; + break; + case BUS_Y: + sda_pin = pin_order_inverted ? SCL0 : SDA0; + scl_pin = pin_order_inverted ? SDA0 : SCL0; + break; + case BUS_Z: + sda_pin = pin_order_inverted ? SCL2 : SDA2; + scl_pin = pin_order_inverted ? SDA2 : SCL2; + break; + default: + // Invalid bus number + return; + } + + SoftwareI2C *i2c_bus = &g_i2c_buses[bus_number]; + + i2c_bus->begin(sda_pin, scl_pin); + configure_light_sensor(*i2c_bus, 0); + configure_light_sensor(*i2c_bus, 1); +} + +/** + * @brief Generic function to detect sensors on any I2C bus + * @param bus_number Bus number (0, 1, or 2) + */ +static void detect_sensors_on_bus_generic(int bus_number) +{ + 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) { + g_sensors_detected_total++; + g_sensors_detected_per_bus[bus_number]++; + } + g_sensor_addresses[bus_number][SENSOR_POS] = detect_light_sensor(g_i2c_buses[bus_number], 1); + if (g_sensor_addresses[bus_number][SENSOR_POS] == LSENSE_EXPECTED_ID) { + g_sensors_detected_total++; + 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); + if (g_sensor_addresses[bus_number][SENSOR_NEG] == LSENSE_EXPECTED_ID) { + g_sensors_detected_total++; + g_sensors_detected_per_bus[bus_number]++; + } + g_sensor_addresses[bus_number][SENSOR_POS] = detect_light_sensor(g_i2c_buses[bus_number], 1); + if (g_sensor_addresses[bus_number][SENSOR_POS] == LSENSE_EXPECTED_ID) { + g_sensors_detected_total++; + g_sensors_detected_per_bus[bus_number]++; + } + } +} + +/*******************************************************************************/ +/*******************************************************************************/ +/* Command handlers */ +/*******************************************************************************/ +/*******************************************************************************/ + +void lsense_cmd_presence(void) +{ + g_sensors_detected_total = 0; + + detect_sensors_on_bus_generic(BUS_X); + detect_sensors_on_bus_generic(BUS_Y); + detect_sensors_on_bus_generic(BUS_Z); + + Serial.write(g_sensors_detected_total); + Serial.write(g_sensors_detected_per_bus[BUS_X]); + Serial.write(g_sensor_addresses[BUS_X][SENSOR_NEG]); + Serial.write(g_sensor_addresses[BUS_X][SENSOR_POS]); + Serial.write(g_sensors_detected_per_bus[BUS_Y]); + Serial.write(g_sensor_addresses[BUS_Y][SENSOR_NEG]); + Serial.write(g_sensor_addresses[BUS_Y][SENSOR_POS]); + Serial.write(g_sensors_detected_per_bus[BUS_Z]); + Serial.write(g_sensor_addresses[BUS_Z][SENSOR_NEG]); + Serial.write(g_sensor_addresses[BUS_Z][SENSOR_POS]); + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); +} + +void lsense_cmd_read(void) +{ + 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]); + + 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]); + + 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]); + + 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); + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); +} \ No newline at end of file diff --git a/lsense.h b/lsense.h new file mode 100644 index 0000000..1d7cca3 --- /dev/null +++ b/lsense.h @@ -0,0 +1,43 @@ +/* + * @file lsense.h + * @brief Light sensor management and command handling + * + * Created: 21.09.2025 05:53:32 + * Author: ThePetrovich + * + * Copyright YKSA - Sakha Aerospace Systems, LLC. + * See the LICENSE file for details. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef LSENSE_H +#define LSENSE_H + +#include + +#define NUM_BUSES 3 +#define SENSORS_PER_BUS 2 + +enum { + BUS_X = 0, // X-axis (bus 0) + BUS_Y = 1, // Y-axis (bus 1) + BUS_Z = 2 // Z-axis (bus 2) +}; + +enum { + SENSOR_NEG = 0, // Negative sensor (addresses 0) + SENSOR_POS = 1 // Positive sensor (addresses 1) +}; + +/** + * @brief Send light sensor presence information via serial + */ +void lsense_cmd_presence(void); + +/** + * @brief Read and send light sensor data via serial + */ +void lsense_cmd_read(void); + +#endif // LSENSE_H diff --git a/rsense.cpp b/rsense.cpp new file mode 100644 index 0000000..6744221 --- /dev/null +++ b/rsense.cpp @@ -0,0 +1,466 @@ +/* + * @file rsense.cpp + * @brief Radiation sensor 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 +#include +#include +#include +#include +#include + +#include "config.h" +#include "eeprom.h" +#include "iodefs.h" +#include "rsense.h" +#include "utils.h" + +static volatile union __attribute__((packed)) { + uint16_t channels_16[DETECTOR_CHANNEL_COUNT]; + uint32_t channels_32[DETECTOR_CHANNEL_32_COUNT]; +} g_detector_counts = {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 uint32_t g_total_time = 0; +static uint32_t g_flush_time = 0; +static volatile uint16_t g_counts_cpm = 0; + +static uint32_t g_cpm_time = 0; +static uint16_t g_cpm_last = 0; + +static potentiometer_settings_t g_current_pot_settings; + +static void apply_potentiometer_settings(const potentiometer_settings_t *settings); +static void initialize_event_system(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); + + pinMode(HV_CS, OUTPUT); + pinMode(DET_CS, OUTPUT); + pinMode(AMP_CS, OUTPUT); + + digitalWrite(HV_CS, HIGH); + digitalWrite(DET_CS, HIGH); + digitalWrite(AMP_CS, HIGH); + + 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(); +} + +/** + * @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; // read result to clear flag + digitalWriteFast(STATUS_LED, LOW); + + if (g_spectrum_mode == 0) { + // 16-bit spectrum mode + // 2 extra bits from oversampling + averaging + // shift down to equiv. 11 bits + #if ADC_OVERSAMPLING_BITS == 1 + channel = channel >> 1; + #elif ADC_OVERSAMPLING_BITS == 2 + channel = channel >> (ADC_OVERSAMPLING_BITS + 1); + #elif ADC_OVERSAMPLING_BITS == 3 + channel = channel >> (ADC_OVERSAMPLING_BITS + 2); + #else + #error "Unsupported ADC oversampling setting" + #endif + + g_detector_counts.channels_16[channel & 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_cpm++; + digitalWriteFast(DET_RST, LOW); +} + +/** + * @brief Clear all detector counters + */ +static void clear_detector_counters(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + g_total_counts = 0; + g_total_time = 0; + g_flush_time = millis(); + g_counts_cpm = 0; + g_cpm_time = millis(); + } +} + +/** + * @brief Clear channel data + */ +static void clear_channel_data(void) +{ + for (int i = 0; i < DETECTOR_CHANNEL_COUNT; i++) { + g_detector_counts.channels_16[i] = 0; + } +} + +/** + * @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) + 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); +} + +/** + * @brief Set new potentiometer values + * @param hv High voltage potentiometer value (0-255) + * @param amp Amplifier potentiometer value (0-255) + * @param det Detector potentiometer value (0-255) + */ +static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det) +{ + digitalWrite(DET_EN, LOW); + digitalWrite(HV_EN, LOW); + delay(5); + + // Update current settings and save to EEPROM + g_current_pot_settings.hv_pot = hv; + g_current_pot_settings.amp_pot = amp; + g_current_pot_settings.det_pot = det; + + apply_potentiometer_settings(&g_current_pot_settings); + + eeprom_save_pot_settings(&g_current_pot_settings); +} + +/*******************************************************************************/ +/*******************************************************************************/ +/* Command handlers */ +/*******************************************************************************/ +/*******************************************************************************/ + +void rsense_cmd_telemetry(void) +{ + 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 + 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); + } + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); + enable_fast_adc(); +} + +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/rsense.h b/rsense.h new file mode 100644 index 0000000..2f6c5e3 --- /dev/null +++ b/rsense.h @@ -0,0 +1,71 @@ +/* + * @file rsense.h + * @brief Radiation sensor management and command handling + * + * Created: 21.09.2025 06:01:56 + * Author: ThePetrovich + * + * Copyright YKSA - Sakha Aerospace Systems, LLC. + * See the LICENSE file for details. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef RSENSE_H +#define RSENSE_H + +#include + +/** + * @brief Initialize radiation sensor subsystem + */ +void rsense_init(void); + +/** + * @brief Send telemetry data via serial + */ +void rsense_cmd_telemetry(void); + +/** + * @brief Dump channel data via serial + */ +void rsense_cmd_dump_channels(void); + +/** + * @brief Flush detector counters + */ +void rsense_cmd_flush(void); + +/** + * @brief Enable radiation detection + */ +void rsense_cmd_enable(void); + +/** + * @brief Disable radiation detection + */ +void rsense_cmd_disable(void); + +/** + * @brief Set potentiometer values + */ +void rsense_cmd_set_potentiometers(void); + +/** + * @brief Send counts per minute data + */ +void rsense_cmd_get_cpm(void); + +/** + * @brief Set spectrum mode (16-bit or 32-bit) + */ +void rsense_cmd_set_mode(void); + +/** + * @brief Periodic tasks for radiation sensor + * Updates CPM calculation every 10 seconds + * Call from main loop + */ +void rsense_periodic(void); + +#endif // RSENSE_H \ No newline at end of file diff --git a/sbc_fw.ino b/sbc_fw.ino new file mode 100644 index 0000000..d365375 --- /dev/null +++ b/sbc_fw.ino @@ -0,0 +1,139 @@ +/* + * @file sbc_fw.ino + * @brief Main firmware for sensor board controller, YKSA PL/EDU16 RSENSE + * + * 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 +#include +#include +#include + +#include "config.h" +#include "eeprom.h" +#include "iodefs.h" +#include "lsense.h" +#include "rsense.h" + +static uint32_t status_led_last_blink = 0; + +void setup() +{ + pinMode(STATUS_LED, OUTPUT); + + Serial.pins(0, 1); + Serial.begin(SERIAL_BAUD_RATE); + + initialize_spi(); + + eeprom_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() +{ + // Process incoming serial commands + while (Serial.available()) { + char command = Serial.read(); + handle_serial_command(command); + } + update_status_led(); + + rsense_periodic(); +} diff --git a/utils.cpp b/utils.cpp new file mode 100644 index 0000000..acd22fc --- /dev/null +++ b/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/utils.h b/utils.h new file mode 100644 index 0000000..cd37faf --- /dev/null +++ b/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