From db8f8408afc76c6b6c92798f583c2edb806256e5 Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Mon, 22 Sep 2025 19:46:11 +0800 Subject: [PATCH] Initial commit --- .gitignore | 57 +++++++ config.h | 73 +++++++++ eeprom.cpp | 50 ++++++ eeprom.h | 46 ++++++ iodefs.h | 49 ++++++ lsense.cpp | 254 +++++++++++++++++++++++++++++ lsense.h | 43 +++++ rsense.cpp | 466 +++++++++++++++++++++++++++++++++++++++++++++++++++++ rsense.h | 71 ++++++++ sbc_fw.ino | 139 ++++++++++++++++ utils.cpp | 57 +++++++ utils.h | 49 ++++++ 12 files changed, 1354 insertions(+) create mode 100644 .gitignore create mode 100644 config.h create mode 100644 eeprom.cpp create mode 100644 eeprom.h create mode 100644 iodefs.h create mode 100644 lsense.cpp create mode 100644 lsense.h create mode 100644 rsense.cpp create mode 100644 rsense.h create mode 100644 sbc_fw.ino create mode 100644 utils.cpp create mode 100644 utils.h 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