From 3e77d34ccc905d5e9a6c72971bed7918d2655bf8 Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Thu, 7 May 2026 23:21:11 +0800 Subject: [PATCH 1/2] Freeze --- sbc_fw/adc.cpp | 214 ++++++++++++++++++++++++++++++++ sbc_fw/adc.h | 57 +++++++++ sbc_fw/config.h | 70 +++++++---- sbc_fw/eeprom.cpp | 2 + sbc_fw/protocol.h | 308 ++++++++++++++++++++++++++++++++++++++++++++++ sbc_fw/rsense.cpp | 101 ++++----------- sbc_fw/rsense.h | 2 +- sbc_fw/sbc_fw.ino | 2 +- sbc_fw/utils.cpp | 24 +--- sbc_fw/utils.h | 35 ++---- 10 files changed, 669 insertions(+), 146 deletions(-) create mode 100644 sbc_fw/adc.cpp create mode 100644 sbc_fw/adc.h create mode 100644 sbc_fw/protocol.h diff --git a/sbc_fw/adc.cpp b/sbc_fw/adc.cpp new file mode 100644 index 0000000..cfa6b36 --- /dev/null +++ b/sbc_fw/adc.cpp @@ -0,0 +1,214 @@ +/* + * @file adc.cpp + * @brief + * + * 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 "utils.h" +#include +#include + +uint8_t adc_flags = 0; + +int16_t adc_to_temperature_c(uint16_t adc_value) +{ + // MCP9700: 500 mV at 0°C, 10 mV per degree, vref = 3.0V + // Result in 0.1°C units + uint16_t adc_resolution = ADC_RESOLUTION + << config.flags.adc_oversample_bits; // Adjust resolution based on oversampling + return (int16_t)((adc_value * ADC_VREF_MV / adc_resolution - config.tempcal)); +} + +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; + + // unrolled loop + uint8_t shift = ((samples == 64) ? 3 : (samples == 16) ? 2 : (samples == 4) ? 1 : 0); + + for (uint8_t i = 0; i < samples; i++) { + sum += analogRead(pin); + } + + return (uint16_t)(sum >> shift); +} + +void adc_enable_fast(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + ADC0.CTRLA |= ADC_ENABLE_bm; + + // Enable oversampling x16 for 12-bit result + ADC0.CTRLB |= (config.flags.adc_oversample_bits << 2); // 2, 4, or 6 for 4x, 16x, or 64x oversampling + + ADC0.CTRLC = 0; // reset + ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VREFA_gc; // CLK_PER/32, VREFA as reference + + ADC0.EVCTRL |= ADC_STARTEI_bm; // EVSYS input to start conversion + ADC0.INTCTRL |= ADC_RESRDY_bm; // Enable interrupt on conversion complete + + // Configure VREF, Microchip DS40002015B page 424 + VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc; + + ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // DET_SIG_PIN + + adc_flags |= 0x01; // Fast mode enabled + } +} + +void adc_restore_default(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + // Disable oversampling + ADC0.CTRLB &= ~ADC_SAMPNUM_gm; + + ADC0.CTRLC &= ~ADC_REFSEL_gm; + ADC0.CTRLC |= ADC_REFSEL_VREFA_gc; // VREFA as reference + + ADC0.EVCTRL &= ~ADC_STARTEI_bm; + ADC0.INTCTRL &= ~ADC_RESRDY_bm; + + adc_flags &= ~0x01; // Fast mode disabled + } +} + +static uint16_t adc_conversion(void) +{ + ADC0.COMMAND = ADC_STCONV_bm; + while (!(ADC0.INTFLAGS & ADC_RESRDY_bm)) + ; + return ADC0.RES; +} + +static uint16_t adc_read_tempsense(void) +{ + /* DS40002015B-page 425 + 1. Configure the internal voltage reference to 1.1V by configuring the VREF peripheral. + 2. Select the internal voltage reference by writing the REFSEL bits in ADCn.CTRLC to 0x0. + 3. Select the ADC temperature sensor channel by configuring the MUXPOS register + (ADCn.MUXPOS). This enables the temperature sensor. + 5. In ADCn.SAMPCTRL select INITDLY ≥ 32 µs × CLK_ADC + 6. In ADCn.CTRLC select SAMPLEN ≥ 32 µs × CLK_ADC + 7. Acquire the temperature sensor output voltage by starting a conversion. + 8. Process the measurement result as described below. + */ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + ADC0.CTRLA |= ADC_ENABLE_bm; + + ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc; + + ADC0.CTRLC = 0; // reset + ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_INTREF_gc; + + // Configure VREF, Microchip DS40002015B page 424 + VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc; + + // Select temperature sensor channel + ADC0.MUXPOS = ADC_MUXPOS_TEMPSENSE_gc; + } + + delay(10); + + int8_t sigrow_offset = SIGROW.TEMPSENSE1; // Read signed value from signature row + uint8_t sigrow_gain = SIGROW.TEMPSENSE0; + // Read unsigned value from signature row + uint16_t adc_reading = adc_conversion() >> 4; + // ADC conversion result with 1.1 V internal reference + uint32_t temp = adc_reading - sigrow_offset; + temp *= sigrow_gain; // Result might overflow 16 bit variable (10bit+8bit) + temp += 0x80; + // Add 1/2 to get correct rounding on division below + temp >>= 8; + // Divide result to get Kelvin + uint16_t temperature_in_K = temp; + + adc_restore_default(); + + if (adc_flags & 0x01) { + adc_enable_fast(); + } + + return temperature_in_K; +} + +void adc_cmd_calibrate(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + ADC0.EVCTRL &= ~ADC_STARTEI_bm; // Start event input + ADC0.INTCTRL &= ~ADC_RESRDY_bm; + + ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc; // 16 samples for oversampling to get 12-bit result from 10-bit ADC + + ADC0.CTRLC = 0; // reset + ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VDDREF_gc; // CLK_PER/32, VDD as reference + + // Configure VREF, Microchip DS40002015B page 424 + VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc; + } + + delay(10); // Wait for VREF to stabilize + + // set MUX to gnd and read offset + ADC0.MUXPOS = ADC_MUXPOS_GND_gc; + delay(1); + uint16_t offset = adc_conversion() >> 2; // 12-bit result + + Serial.write(offset & 0xFF); + Serial.write(offset >> 8); + + if (config.adccal0 == 0xFFFF) { + config.adccal0 = offset; + } + + // set MUX to external AREF and read 3.0V value + ADC0.MUXPOS = ADC_MUXPOS_AIN7_gc; // AREF pin + delay(1); + + uint16_t aref = adc_conversion() >> 2; // 12-bit result + + Serial.write(aref & 0xFF); + Serial.write(aref >> 8); + + if (config.adccal3 == 0xFFFF) { + config.adccal3 = aref; + } + + // set MUX to DACREF0 and read + ADC0.MUXPOS = ADC_MUXPOS_DACREF_gc; + delay(1); + + uint16_t dacref = adc_conversion() >> 2; // 12-bit result + Serial.write(dacref & 0xFF); + Serial.write(dacref >> 8); + + uint16_t tempsense = adc_read_tempsense(); + Serial.write(tempsense & 0xFF); + Serial.write(tempsense >> 8); + + Serial.write(SIGROW.TEMPSENSE1); + Serial.write(SIGROW.TEMPSENSE0); + + Serial.flush(); + SERIAL_BUFFER_CLEAR(); + + adc_restore_default(); + + if (adc_flags & 0x01) { + adc_enable_fast(); + } +} diff --git a/sbc_fw/adc.h b/sbc_fw/adc.h new file mode 100644 index 0000000..b8d60e5 --- /dev/null +++ b/sbc_fw/adc.h @@ -0,0 +1,57 @@ +/* + * @file adc.h + * @brief + * + * 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 ADC for fast radiation detection + */ +void adc_enable_fast(void); + +/** + * @brief Restore ADC to default settings + */ +void adc_restore_default(void); + +/** + * @brief Convert ADC reading to temperature in 0.1°C + * @param adc_value 12-bit ADC reading + * @return Temperature in 0.1°C + */ +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 adc_read_oversampled(uint8_t pin, uint8_t samples); + +/** + * @brief Command to calibrate ADC and read reference values + */ +void adc_cmd_calibrate(void); + +#endif // ADC_H_ \ No newline at end of file diff --git a/sbc_fw/config.h b/sbc_fw/config.h index e240d5c..2d44580 100644 --- a/sbc_fw/config.h +++ b/sbc_fw/config.h @@ -20,36 +20,23 @@ // 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 +#define RSENSE_CHANNEL_16_MASK 0x7FFU // 11-bit channel mask +#define RSENSE_CHANNEL_32_MASK 0x3FFU // 10-bit channel mask +#define RSENSE_CHANNEL_COUNT 2048 +#define RSENSE_CHANNEL_32_COUNT 1024 // 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 +#define MCP9700_OFFSET_MV 500L // 500 mV at 0°C +#define MCP9700_SCALE_MV 10L // 10 mV per degree +#define ADC_OVERSAMPLING_BITS 2 // 16x oversampling -> 2 extra bits +#define ADC_VREF_MV 3000L // 3.0V reference +#define ADC_RESOLUTION 1024L // 10-bit ADC // Voltage divider constants #define VOLTAGE_DIVIDER_RATIO 20.0f // 200k and 10k resistors @@ -70,4 +57,43 @@ #define LSENSE_EXPECTED_ID 0xE0 #define LSENSE_DATA_SIZE 12 +typedef struct config_t { + uint8_t magic1; // 0xA5 to indicate valid config + uint8_t magic2; // 0x5A to indicate valid config + uint8_t pot_hv; + uint8_t pot_amp; + uint8_t pot_det; + struct { + uint16_t adccal0; // ADC offset at GND + uint16_t adccal3; // ADC reading at 3.0V reference + uint16_t tempcal; + uint16_t tempdrift_mvK; // SiPM temperature drift compensation in mV/K, normally around 20 mV/K + float gainlow; // gain value for pot_amp = 0 + float gainhigh; // gain value for pot_amp = 255 + } calibration __attribute__((packed)); + union { + struct { + uint8_t spectrum_oversample_bits + : 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x oversampling, only for spectrum measurement + uint8_t spectrum_channel_bits : 1; // 0 for 16-bit channels, 1 for 32-bit channels + uint8_t temperature_drift_compensation + : 1; // 0 to disable, 1 to enable temperature compensation + uint8_t adc_oversample_bits : 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x ADC oversampling for + // temperature and voltage measurements + uint8_t reserved : 2; + } fields __attribute__((packed)); + uint8_t value; + } flags __attribute__((packed)); + struct { + uint8_t tempdrift_pot_hv_low; + uint8_t tempdrift_pot_hv_high; + uint8_t tempdrift_pot_amp_low; + uint8_t tempdrift_pot_amp_high; + uint8_t tempdrift_pot_det_low; + uint8_t tempdrift_pot_det_high; + } temp_drift_compensation_ranges __attribute__((packed)); +} __attribute__((packed)) config_t; + +extern config_t config; + #endif // CONFIG_H diff --git a/sbc_fw/eeprom.cpp b/sbc_fw/eeprom.cpp index 65452a3..c44d5ac 100644 --- a/sbc_fw/eeprom.cpp +++ b/sbc_fw/eeprom.cpp @@ -16,6 +16,8 @@ #include #include +config_t config; + void eeprom_init(void) { if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) { diff --git a/sbc_fw/protocol.h b/sbc_fw/protocol.h new file mode 100644 index 0000000..52c0073 --- /dev/null +++ b/sbc_fw/protocol.h @@ -0,0 +1,308 @@ +/* + * @file protocol.h + * @brief + * + * Created: 07.05.2026 10:15:36 + * 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 + +/* + * @details + * General Protocol Description: + * - All commands are initiated by the host + * - Command format: [CMD (2 bytes)][Length (2 bytes)][Payload (variable)][CRC16 (2 bytes)] + * - CMD: 2 bytes command identifier (e.g., 'HV' for set potentiometers) + * - Length: 2 bytes unsigned integer indicating the length of the payload in bytes + * - Payload: Variable length data specific to the command + * - CRC16: 2 bytes CRC16-XMODEM checksum of the CMD, Length, and Payload fields + * - Response: For commands that require a response, the device will send back a similar structured + * message with the appropriate CMD and payload. Otherwise, it should send an acknowledgment + * (e.g., 'ACK') or an error response if the command is invalid. + * - If a valid command is received, but cannot be executed (e.g. locked state, parameters out of range), + * the device should send a command failure response (e.g., 'NAK') without performing any action. + * - Error Handling: If a command is malformed (e.g., incorrect CRC, invalid length), the device will + * ignore the command and should send an error response (e.g., 'ERR') to the host. + * The device should not perform any action if the command is invalid. + * Example Command: Set Potentiometers + * - CMD: 'HV' (0x48 0x56) + * - Length: 3 (0x00 0x03) + * - Payload: [HV_POT (1 byte)][AMP_POT (1 byte)][DET_POT (1 byte)] + * - CRC16: Calculated over 'HV', Length, and Payload + * Response: 'ACK' if successful, 'NAK' if parameters are out of safe range, 'ERR' if command is malformed + */ + +uint16_t protocol_crc16_xmodem(uint8_t *data, uint16_t len); + +typedef struct protocol_header_t { + uint16_t cmd; + uint16_t length; +} __attribute__((packed)) protocol_header_t; + +void protocol_send_header(uint16_t cmd, uint16_t length); + +void protocol_send_ack(void); +void protocol_send_nak(void); +void protocol_send_error(void); + +void protocol_send_message(uint16_t cmd, void *payload, uint16_t length); + +/* + * @brief Firmware version command and response structure + * @param[out] major Firmware major version + * @param[out] minor Firmware minor version + */ +#define CMD_FIRMWARE_VERSION 0x7600 +typedef struct protocol_firmware_version_response_t { + uint8_t major; + uint8_t minor; +} __attribute__((packed)) protocol_firmware_version_response_t; + +void handle_version_command(void *payload, uint16_t length); + +/* + * @brief Light sensor presence command and response structure + * @param[out] total_detected Total number of sensors detected across all buses + * @param[out] bus_x_count Number of sensors detected on bus X + * @param[out] bus_x_neg_addr I2C address of negative sensor on bus X (0 if not present) + * @param[out] bus_x_pos_addr I2C address of positive sensor on bus X (0 if not present) + * @param[out] bus_y_count Number of sensors detected on bus Y + * @param[out] bus_y_neg_addr I2C address of negative sensor on bus Y (0 if not present) + * @param[out] bus_y_pos_addr I2C address of positive sensor on bus Y (0 if not present) + * @param[out] bus_z_count Number of sensors detected on bus Z + * @param[out] bus_z_neg_addr I2C address of negative sensor on bus Z (0 if not present) + * @param[out] bus_z_pos_addr I2C address of positive sensor on bus Z (0 if not present) + */ +#define CMD_LIGHT_SENSOR_PRESENCE 0x6C00 +typedef struct protocol_light_sensor_presence_response_t { + uint8_t total_detected; + uint8_t bus_x_count; + uint8_t bus_x_neg_addr; + uint8_t bus_x_pos_addr; + uint8_t bus_y_count; + uint8_t bus_y_neg_addr; + uint8_t bus_y_pos_addr; + uint8_t bus_z_count; + uint8_t bus_z_neg_addr; + uint8_t bus_z_pos_addr; +} __attribute__((packed)) protocol_light_sensor_presence_response_t; + +void handle_light_sensor_presence_command(void *payload, uint16_t length); + +/* + * @brief Read light sensor data command and response structure + * @param[in] channel Spectral channel (0 = red, 1 = green, 2 = blue, 3 = IR, 4 = green2, 5 = composite visible, 6 = + * composite all) + * @param[out] bus_x_neg_channel_value Channel value for negative sensor on bus X + * @param[out] bus_x_pos_channel_value Channel value for positive sensor on bus X + * @param[out] bus_y_neg_channel_value Channel value for negative sensor on bus Y + * @param[out] bus_y_pos_channel_value Channel value for positive sensor on bus Y + * @param[out] bus_z_neg_channel_value Channel value for negative sensor on bus Z + * @param[out] bus_z_pos_channel_value Channel value for positive sensor on bus Z + */ +#define CMD_READ_LIGHT_SENSORS 0x7200 +typedef struct protocol_read_light_sensors_command_t { + uint8_t channel; +} __attribute__((packed)) protocol_read_light_sensors_command_t; + +typedef struct protocol_read_light_sensors_response_t { + uint16_t bus_x_neg_channel_value; + uint16_t bus_x_pos_channel_value; + uint16_t bus_y_neg_channel_value; + uint16_t bus_y_pos_channel_value; + uint16_t bus_z_neg_channel_value; + uint16_t bus_z_pos_channel_value; +} __attribute__((packed)) protocol_read_light_sensors_response_t; + +void handle_read_light_sensors_command(void *payload, uint16_t length); + +/* + * @brief Get configuration command and response structure + * @param[out] config Current device configuration + */ +#define CMD_GET_CONFIGURATION 0x6D00 +typedef struct protocol_get_configuration_response_t { + config_t config; +} __attribute__((packed)) protocol_get_configuration_response_t; + +void handle_get_configuration_command(void *payload, uint16_t length); + +/* + * @brief Set configuration command and response structure + * @param[in] config New device configuration to apply + */ +#define CMD_SET_CONFIGURATION 0x7D00 +typedef struct protocol_set_configuration_command_t { + config_t config; +} __attribute__((packed)) protocol_set_configuration_command_t; + +void handle_set_configuration_command(void *payload, uint16_t length); + +/* + * @brief Set potentiometers command and response structure + * @param[in] hv_pot High voltage potentiometer value + * @param[in] amp_pot Amplifier potentiometer value + * @param[in] det_pot Detector potentiometer value + */ +#define CMD_SET_POTENTIOMETERS 0x7D01 +typedef struct protocol_set_potentiometers_command_t { + uint8_t hv_pot; + uint8_t amp_pot; + uint8_t det_pot; +} __attribute__((packed)) protocol_set_potentiometers_command_t; + +void handle_set_potentiometers_command(void *payload, uint16_t length); + +/* + * @brief Set calibration data command and response structure + * @param[in] adccal0 ADC offset at GND + * @param[in] adccal3 ADC reading at 3.0V reference + * @param[in] tempcal Temperature calibration value + * @param[in] tempdrift_mvK Temperature drift compensation in mV/K + */ +#define CMD_SET_CALIBRATION 0x7D02 +typedef struct protocol_set_calibration_command_t { + uint16_t adccal0; + uint16_t adccal3; + uint16_t tempcal; + uint16_t tempdrift_mvK; +} __attribute__((packed)) protocol_set_calibration_command_t; + +void handle_set_calibration_command(void *payload, uint16_t length); + +/* + * @brief Set flags command and response structure + * @param[in] flags Configuration flags to set (e.g., oversampling, channel mode, temperature compensation) + */ +#define CMD_SET_FLAGS 0x7D03 +typedef struct protocol_set_flags_command_t { + uint8_t flags; +} __attribute__((packed)) protocol_set_flags_command_t; + +/* +* @brief Set temperature drift compensation ranges command and response structure +* @param[in] tempdrift_pot_hv_low Low threshold for high voltage potentiometer +* @param[in] tempdrift_pot_hv_high High threshold for high voltage potentiometer +* @param[in] tempdrift_pot_amp_low Low threshold for amplifier potentiometer +* @param[in] tempdrift_pot_amp_high High threshold for amplifier potentiometer +* @param[in] tempdrift_pot_det_low Low threshold for detector potentiometer +* @param[in] tempdrift_pot_det_high High threshold for detector potentiometer +*/ +#define CMD_SET_TEMP_DRIFT_COMPENSATION_RANGES 0x7D04 +typedef struct protocol_set_temp_drift_compensation_ranges_command_t { + uint8_t tempdrift_pot_hv_low; + uint8_t tempdrift_pot_hv_high; + uint8_t tempdrift_pot_amp_low; + uint8_t tempdrift_pot_amp_high; + uint8_t tempdrift_pot_det_low; + uint8_t tempdrift_pot_det_high; +} __attribute__((packed)) protocol_set_temp_drift_compensation_ranges_command_t; + +void handle_set_flags_command(void *payload, uint16_t length); + +/* + * @brief Get telemetry command and response structure + * @param[out] hv_temp_c High voltage component temperature in 0.1°C + * @param[out] amp_temp_c Amplifier component temperature in 0.1°C + * @param[out] det_temp_c Detector component temperature in 0.1°C + * @param[out] mcu_temp_c MCU temperature in 0.1°C + * @param[out] vbias_mv SiPM bias voltage in mV + * @param[out] hv_pot Current high voltage potentiometer setting + * @param[out] amp_pot Current amplifier potentiometer setting + * @param[out] det_pot Current detector potentiometer setting + * @param[out] cps Current counts per second (CPM/60) + * @param[out] cp10s Counts in the last 10 seconds + * @param[out] total_counts Total counts since last reset + */ +#define CMD_GET_TELEMETRY 0x7400 +typedef struct protocol_get_telemetry_response_t { + uint16_t hv_temp_c; + uint16_t amp_temp_c; + uint16_t det_temp_c; + uint16_t mcu_temp_c; + uint16_t vbias_mv; + uint8_t hv_pot; + uint8_t amp_pot; + uint8_t det_pot; + uint16_t cps; + uint32_t cp10s; + uint32_t total_counts; +} __attribute__((packed)) protocol_get_telemetry_response_t; + +void handle_get_telemetry_command(void *payload, uint16_t length); + +/* + * @brief Flush counters command + * @details Resets total counts and 10-second counts to zero + */ +#define CMD_FLUSH_COUNTERS 0x7401 +void handle_flush_counters_command(void *payload, uint16_t length); + +/* + * @brief Flush spectrum command + * @details Clears all channel data and resets total counts to zero + */ +#define CMD_FLUSH_SPECTRUM 0x7402 +void handle_flush_spectrum_command(void *payload, uint16_t length); + +/* + * @brief Turn on the spectrometer circutry and enable fast ADC mode for radiation detection + */ +#define CMD_RSENSE_ENABLE 0x7500 +void handle_rsense_enable_command(void *payload, uint16_t length); + +/* + * @brief Turn off the spectrometer circutry and disable fast ADC mode for radiation detection + */ +#define CMD_RSENSE_DISABLE 0x7501 +void handle_rsense_disable_command(void *payload, uint16_t length); + +/* + * @brief Freeze spectrum data for reading + */ +#define CMD_SPECTRUM_FREEZE 0x7502 +void handle_spectrum_freeze_command(void *payload, uint16_t length); + +/* + * @brief Unfreeze spectrum data after reading + */ +#define CMD_SPECTRUM_UNFREEZE 0x7503 +void handle_spectrum_unfreeze_command(void *payload, uint16_t length); + +/* +* @brief Get counts since last request +*/ +#define CMD_GET_COUNTS 0x7504 +typedef struct protocol_get_counts_response_t { + uint32_t counts; +} __attribute__((packed)) protocol_get_counts_response_t; + +void handle_get_counts_command(void *payload, uint16_t length); + +/* + * @brief Read spectrum data (arbitrary chunking for large data) + */ +#define CMD_READ_SPECTRUM_DATA 0x7505 +typedef struct protocol_read_spectrum_chunk_command_t { + uint16_t offset; + uint16_t length; +} __attribute__((packed)) protocol_read_spectrum_chunk_command_t; + +typedef struct protocol_read_spectrum_chunk_response_t { + uint16_t offset; + uint16_t length; + // Followed by 'length' bytes of spectrum data +} __attribute__((packed)) protocol_read_spectrum_chunk_response_t; +void handle_read_spectrum_chunk_command(void *payload, uint16_t length); + +#endif /* PROTOCOL_H */ diff --git a/sbc_fw/rsense.cpp b/sbc_fw/rsense.cpp index 6744221..571416b 100644 --- a/sbc_fw/rsense.cpp +++ b/sbc_fw/rsense.cpp @@ -23,13 +23,15 @@ #include "iodefs.h" #include "rsense.h" #include "utils.h" +#include "adc.h" static volatile union __attribute__((packed)) { - uint16_t channels_16[DETECTOR_CHANNEL_COUNT]; - uint32_t channels_32[DETECTOR_CHANNEL_32_COUNT]; + uint16_t channels_16[RSENSE_CHANNEL_COUNT]; + uint32_t channels_32[RSENSE_CHANNEL_32_COUNT]; } g_detector_counts = {0}; static uint8_t g_spectrum_mode = 0; // 0 = 16-bit, 1 = 32-bit +static uint8_t g_oversampling_bits = config.flags.adc_oversample_bits; static uint16_t g_read_pointer = 0; static uint32_t g_total_counts = 0; @@ -44,8 +46,6 @@ 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); @@ -110,61 +110,6 @@ static void initialize_event_system(void) 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 */ @@ -188,7 +133,7 @@ static inline void handle_analog_read_interrupt(void) #error "Unsupported ADC oversampling setting" #endif - g_detector_counts.channels_16[channel & ADC_CHANNEL_16_MASK]++; + g_detector_counts.channels_16[channel & RSENSE_CHANNEL_16_MASK]++; } else { // 32-bit spectrum mode // 2 extra bits from oversampling + averaging x2 @@ -203,7 +148,7 @@ static inline void handle_analog_read_interrupt(void) #error "Unsupported ADC oversampling setting" #endif - g_detector_counts.channels_32[channel & ADC_CHANNEL_32_MASK]++; + g_detector_counts.channels_32[channel & RSENSE_CHANNEL_32_MASK]++; } g_total_counts++; @@ -231,7 +176,7 @@ static void clear_detector_counters(void) */ static void clear_channel_data(void) { - for (int i = 0; i < DETECTOR_CHANNEL_COUNT; i++) { + for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) { g_detector_counts.channels_16[i] = 0; } } @@ -294,7 +239,7 @@ static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det) void rsense_cmd_telemetry(void) { - restore_default_adc(); + adc_restore_default(); // Send total counts as 4 bytes Serial.write(g_total_counts & 0xFF); @@ -303,7 +248,7 @@ void rsense_cmd_telemetry(void) 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_raw = adc_read_oversampled(V28V0_FB_PIN, 16); uint16_t voltage_mv = adc_to_voltage_mv(voltage_raw); Serial.write(voltage_mv & 0xFF); Serial.write(voltage_mv >> 8); @@ -318,7 +263,7 @@ void rsense_cmd_telemetry(void) Serial.flush(); SERIAL_BUFFER_CLEAR(); - enable_fast_adc(); + adc_enable_fast(); } /** @@ -326,9 +271,9 @@ void rsense_cmd_telemetry(void) */ 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)); + int16_t hv_temp_c = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16)); + int16_t amp_temp_c = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16)); + int16_t det_temp_c = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16)); Serial.write(hv_temp_c & 0xFF); Serial.write(hv_temp_c >> 8); @@ -359,7 +304,7 @@ void rsense_cmd_enable(void) digitalWrite(HV_EN, HIGH); digitalWrite(DET_EN, HIGH); - enable_fast_adc(); + adc_enable_fast(); SERIAL_SEND_OK(); } @@ -369,7 +314,7 @@ void rsense_cmd_disable(void) digitalWrite(DET_EN, LOW); digitalWrite(HV_EN, LOW); - restore_default_adc(); + adc_restore_default(); SERIAL_SEND_OK(); } @@ -383,7 +328,7 @@ void rsense_cmd_flush(void) void rsense_cmd_dump_channels(void) { - restore_default_adc(); + adc_restore_default(); uint16_t read_from = 0; @@ -398,8 +343,8 @@ void rsense_cmd_dump_channels(void) 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; + if (g_read_pointer >= (RSENSE_CHANNEL_32_COUNT - 32)) { + g_read_pointer = RSENSE_CHANNEL_32_COUNT - 32; } // Send 2 bytes of CRC16 checksum for the 32 channels @@ -408,7 +353,7 @@ void rsense_cmd_dump_channels(void) 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++) { + for (uint16_t i = 0; i < 32 && g_read_pointer < RSENSE_CHANNEL_32_COUNT; i++, g_read_pointer++) { uint32_t channel_value = g_detector_counts.channels_32[g_read_pointer]; Serial.write(channel_value & 0xFF); Serial.write((channel_value >> 8) & 0xFF); @@ -422,7 +367,7 @@ void rsense_cmd_dump_channels(void) Serial.flush(); SERIAL_BUFFER_CLEAR(); - enable_fast_adc(); + adc_enable_fast(); } void rsense_cmd_get_cpm(void) @@ -433,10 +378,10 @@ void rsense_cmd_get_cpm(void) SERIAL_BUFFER_CLEAR(); } -void rsense_cmd_set_mode(void) +void rsense_cmd_set_configuration(void) { // Wait for 1 byte specifying mode - while (Serial.available() < 1) + while (Serial.available() < 8) ; uint8_t mode = Serial.read(); @@ -446,6 +391,8 @@ void rsense_cmd_set_mode(void) SERIAL_SEND_OK(); } + // 7 bytes reserved for future use + Serial.flush(); SERIAL_BUFFER_CLEAR(); } diff --git a/sbc_fw/rsense.h b/sbc_fw/rsense.h index 2f6c5e3..46f7cbf 100644 --- a/sbc_fw/rsense.h +++ b/sbc_fw/rsense.h @@ -59,7 +59,7 @@ void rsense_cmd_get_cpm(void); /** * @brief Set spectrum mode (16-bit or 32-bit) */ -void rsense_cmd_set_mode(void); +void rsense_cmd_set_configuration(void); /** * @brief Periodic tasks for radiation sensor diff --git a/sbc_fw/sbc_fw.ino b/sbc_fw/sbc_fw.ino index d365375..87f7478 100644 --- a/sbc_fw/sbc_fw.ino +++ b/sbc_fw/sbc_fw.ino @@ -88,7 +88,7 @@ void handle_serial_command(char command) rsense_cmd_telemetry(); break; case 'm': // Set spectrum mode (16-bit or 32-bit) - rsense_cmd_set_mode(); + rsense_cmd_set_configuration(); break; default: // Unknown command - ignore diff --git a/sbc_fw/utils.cpp b/sbc_fw/utils.cpp index acd22fc..e75aaaa 100644 --- a/sbc_fw/utils.cpp +++ b/sbc_fw/utils.cpp @@ -32,26 +32,4 @@ uint16_t calculate_crc16_xmodem(uint8_t *data, uint16_t len) } 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); -} +} \ No newline at end of file diff --git a/sbc_fw/utils.h b/sbc_fw/utils.h index cd37faf..b1d5a39 100644 --- a/sbc_fw/utils.h +++ b/sbc_fw/utils.h @@ -16,6 +16,19 @@ #include +#define SERIAL_BUFFER_CLEAR() \ + do { \ + while (Serial.available()) { \ + Serial.read(); \ + } \ + } while (0) +#define SERIAL_SEND_OK() \ + do { \ + Serial.print("ok\n"); \ + Serial.flush(); \ + SERIAL_BUFFER_CLEAR(); \ + } while (0) + /** * @brief Calculate CRC16 using XMODEM polynomial * @param data Pointer to data buffer @@ -24,26 +37,4 @@ */ 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 From 2b713a3e3ae9aacc985e1368d37ec63dbefd812d Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Sun, 10 May 2026 15:33:08 +0800 Subject: [PATCH 2/2] Protocol rework --- sbc_fw/adc.cpp | 176 +++++--------- sbc_fw/adc.h | 44 ++-- sbc_fw/config.h | 165 ++++++++------ sbc_fw/eeprom.cpp | 140 ++++++++++-- sbc_fw/eeprom.h | 40 ++-- sbc_fw/iodefs.h | 50 ++-- sbc_fw/lsense.cpp | 161 +++++-------- sbc_fw/lsense.h | 77 +++++-- sbc_fw/protocol.cpp | 328 ++++++++++++++++++++++++++ sbc_fw/protocol.h | 395 ++++++++++++-------------------- sbc_fw/rsense.cpp | 545 +++++++++++++++++++------------------------- sbc_fw/rsense.h | 98 ++++---- sbc_fw/sbc_fw.ino | 208 +++++++++-------- sbc_fw/utils.cpp | 35 --- sbc_fw/utils.h | 40 ---- 15 files changed, 1347 insertions(+), 1155 deletions(-) create mode 100644 sbc_fw/protocol.cpp delete mode 100644 sbc_fw/utils.cpp delete mode 100644 sbc_fw/utils.h diff --git a/sbc_fw/adc.cpp b/sbc_fw/adc.cpp index cfa6b36..0d708bd 100644 --- a/sbc_fw/adc.cpp +++ b/sbc_fw/adc.cpp @@ -1,6 +1,7 @@ /* * @file adc.cpp - * @brief + * @brief ATmega4809 ADC0 driver: configuration profiles, oversampling reads, + * and conversions to engineering units. * * Created: 27.09.2025 05:06:33 * Author: ThePetrovich @@ -13,19 +14,38 @@ #include "adc.h" #include "config.h" -#include "utils.h" #include #include -uint8_t adc_flags = 0; +/** + * @brief Internal driver state. + * Bit 0 = fast mode currently enabled. + */ +static uint8_t adc_flags = 0; int16_t adc_to_temperature_c(uint16_t adc_value) { - // MCP9700: 500 mV at 0°C, 10 mV per degree, vref = 3.0V - // Result in 0.1°C units - uint16_t adc_resolution = ADC_RESOLUTION - << config.flags.adc_oversample_bits; // Adjust resolution based on oversampling - return (int16_t)((adc_value * ADC_VREF_MV / adc_resolution - config.tempcal)); + /* + * MCP9700: 500 mV at 0 °C, 10 mV/°C, result returned in 0.1 °C units. + * Use calibration points when available (adccal3 != 0xFFFF): + * adccal0 = ADC reading at GND (offset), + * adccal3 = ADC reading at 3.0 V reference. + * Falls back to nominal constants when uncalibrated. + */ + uint16_t cal0 = config.calibration.adccal0; + uint16_t cal3 = config.calibration.adccal3; + int16_t tempcal = (int16_t)config.calibration.tempcal; + + int32_t mv; + if (cal3 != 0xFFFF && cal3 != cal0) { + mv = (int32_t)(adc_value - cal0) * ADC_VREF_MV / (int32_t)(cal3 - cal0); + } else { + uint16_t adc_resolution = ADC_RESOLUTION << config.flags.fields.adc_oversample_bits; + mv = (int32_t)adc_value * ADC_VREF_MV / adc_resolution; + } + + /* Result in 0.1 °C: (mv - 500 - tempcal_offset) * 10 / 10. */ + return (int16_t)((mv - MCP9700_OFFSET_MV - tempcal) * 10L / MCP9700_SCALE_MV); } uint16_t adc_to_voltage_mv(uint16_t adc_value) { return (uint16_t)(adc_value * VOLTAGE_MV_PER_STEP); } @@ -33,14 +53,11 @@ uint16_t adc_to_voltage_mv(uint16_t adc_value) { return (uint16_t)(adc_value * V uint16_t adc_read_oversampled(uint8_t pin, uint8_t samples) { uint32_t sum = 0; - - // unrolled loop uint8_t shift = ((samples == 64) ? 3 : (samples == 16) ? 2 : (samples == 4) ? 1 : 0); for (uint8_t i = 0; i < samples; i++) { sum += analogRead(pin); } - return (uint16_t)(sum >> shift); } @@ -50,21 +67,21 @@ void adc_enable_fast(void) { ADC0.CTRLA |= ADC_ENABLE_bm; - // Enable oversampling x16 for 12-bit result - ADC0.CTRLB |= (config.flags.adc_oversample_bits << 2); // 2, 4, or 6 for 4x, 16x, or 64x oversampling + /* Oversampling factor selected by config.flags.adc_oversample_bits. */ + ADC0.CTRLB |= (config.flags.fields.adc_oversample_bits << 2); - ADC0.CTRLC = 0; // reset - ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VREFA_gc; // CLK_PER/32, VREFA as reference + ADC0.CTRLC = 0; + ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VREFA_gc; /* CLK_PER/32, VREFA reference */ - ADC0.EVCTRL |= ADC_STARTEI_bm; // EVSYS input to start conversion - ADC0.INTCTRL |= ADC_RESRDY_bm; // Enable interrupt on conversion complete + ADC0.EVCTRL |= ADC_STARTEI_bm; /* EVSYS input starts conversion */ + ADC0.INTCTRL |= ADC_RESRDY_bm; /* result-ready interrupt */ - // Configure VREF, Microchip DS40002015B page 424 + /* Microchip DS40002015B p. 424. */ VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc; - ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // DET_SIG_PIN + ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; /* DET_SIG_PIN */ - adc_flags |= 0x01; // Fast mode enabled + adc_flags |= 0x01; } } @@ -72,19 +89,21 @@ void adc_restore_default(void) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - // Disable oversampling ADC0.CTRLB &= ~ADC_SAMPNUM_gm; ADC0.CTRLC &= ~ADC_REFSEL_gm; - ADC0.CTRLC |= ADC_REFSEL_VREFA_gc; // VREFA as reference + ADC0.CTRLC |= ADC_REFSEL_VREFA_gc; ADC0.EVCTRL &= ~ADC_STARTEI_bm; ADC0.INTCTRL &= ~ADC_RESRDY_bm; - adc_flags &= ~0x01; // Fast mode disabled + adc_flags &= ~0x01; } } +/** + * @brief Trigger a single conversion and block until the result is ready. + */ static uint16_t adc_conversion(void) { ADC0.COMMAND = ADC_STCONV_bm; @@ -93,48 +112,45 @@ static uint16_t adc_conversion(void) return ADC0.RES; } -static uint16_t adc_read_tempsense(void) +uint16_t adc_read_tempsense(void) { - /* DS40002015B-page 425 - 1. Configure the internal voltage reference to 1.1V by configuring the VREF peripheral. - 2. Select the internal voltage reference by writing the REFSEL bits in ADCn.CTRLC to 0x0. - 3. Select the ADC temperature sensor channel by configuring the MUXPOS register - (ADCn.MUXPOS). This enables the temperature sensor. - 5. In ADCn.SAMPCTRL select INITDLY ≥ 32 µs × CLK_ADC - 6. In ADCn.CTRLC select SAMPLEN ≥ 32 µs × CLK_ADC - 7. Acquire the temperature sensor output voltage by starting a conversion. - 8. Process the measurement result as described below. - */ + /* + * Microchip DS40002015B p. 425 procedure: + * 1. Configure VREF to 1.1 V via the VREF peripheral. + * 2. Select internal voltage reference (REFSEL = 0x0 in ADCn.CTRLC). + * 3. Select the temperature sensor channel via ADCn.MUXPOS. + * 4. Set INITDLY ≥ 32 µs × CLK_ADC in ADCn.SAMPCTRL. + * 5. Set SAMPLEN ≥ 32 µs × CLK_ADC in ADCn.CTRLC. + * 6. Start a conversion to acquire the sensor output voltage. + * 7. Process the result as below. + */ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.CTRLA |= ADC_ENABLE_bm; ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc; - ADC0.CTRLC = 0; // reset + ADC0.CTRLC = 0; ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_INTREF_gc; - // Configure VREF, Microchip DS40002015B page 424 VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc; - // Select temperature sensor channel ADC0.MUXPOS = ADC_MUXPOS_TEMPSENSE_gc; } delay(10); - int8_t sigrow_offset = SIGROW.TEMPSENSE1; // Read signed value from signature row + int8_t sigrow_offset = SIGROW.TEMPSENSE1; uint8_t sigrow_gain = SIGROW.TEMPSENSE0; - // Read unsigned value from signature row uint16_t adc_reading = adc_conversion() >> 4; - // ADC conversion result with 1.1 V internal reference + + /* Conversion result with 1.1 V internal reference; result may exceed 16 + * bits during the multiply (10-bit reading × 8-bit gain). */ uint32_t temp = adc_reading - sigrow_offset; - temp *= sigrow_gain; // Result might overflow 16 bit variable (10bit+8bit) - temp += 0x80; - // Add 1/2 to get correct rounding on division below + temp *= sigrow_gain; + temp += 0x80; /* Round-half-up before the >>8 division. */ temp >>= 8; - // Divide result to get Kelvin - uint16_t temperature_in_K = temp; + uint16_t temperature_in_C = temp - 273; /* Convert from Kelvin to Celsius. */ adc_restore_default(); @@ -142,73 +158,5 @@ static uint16_t adc_read_tempsense(void) adc_enable_fast(); } - return temperature_in_K; -} - -void adc_cmd_calibrate(void) -{ - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) - { - ADC0.EVCTRL &= ~ADC_STARTEI_bm; // Start event input - ADC0.INTCTRL &= ~ADC_RESRDY_bm; - - ADC0.CTRLB |= ADC_SAMPNUM_ACC16_gc; // 16 samples for oversampling to get 12-bit result from 10-bit ADC - - ADC0.CTRLC = 0; // reset - ADC0.CTRLC |= ADC_PRESC_DIV32_gc | ADC_REFSEL_VDDREF_gc; // CLK_PER/32, VDD as reference - - // Configure VREF, Microchip DS40002015B page 424 - VREF.CTRLA |= VREF_ADC0REFSEL_4V34_gc; - } - - delay(10); // Wait for VREF to stabilize - - // set MUX to gnd and read offset - ADC0.MUXPOS = ADC_MUXPOS_GND_gc; - delay(1); - uint16_t offset = adc_conversion() >> 2; // 12-bit result - - Serial.write(offset & 0xFF); - Serial.write(offset >> 8); - - if (config.adccal0 == 0xFFFF) { - config.adccal0 = offset; - } - - // set MUX to external AREF and read 3.0V value - ADC0.MUXPOS = ADC_MUXPOS_AIN7_gc; // AREF pin - delay(1); - - uint16_t aref = adc_conversion() >> 2; // 12-bit result - - Serial.write(aref & 0xFF); - Serial.write(aref >> 8); - - if (config.adccal3 == 0xFFFF) { - config.adccal3 = aref; - } - - // set MUX to DACREF0 and read - ADC0.MUXPOS = ADC_MUXPOS_DACREF_gc; - delay(1); - - uint16_t dacref = adc_conversion() >> 2; // 12-bit result - Serial.write(dacref & 0xFF); - Serial.write(dacref >> 8); - - uint16_t tempsense = adc_read_tempsense(); - Serial.write(tempsense & 0xFF); - Serial.write(tempsense >> 8); - - Serial.write(SIGROW.TEMPSENSE1); - Serial.write(SIGROW.TEMPSENSE0); - - Serial.flush(); - SERIAL_BUFFER_CLEAR(); - - adc_restore_default(); - - if (adc_flags & 0x01) { - adc_enable_fast(); - } + return temperature_in_C * 10; /* Return in 0.1 °C for consistency */ } diff --git a/sbc_fw/adc.h b/sbc_fw/adc.h index b8d60e5..ded854a 100644 --- a/sbc_fw/adc.h +++ b/sbc_fw/adc.h @@ -1,57 +1,61 @@ /* * @file adc.h - * @brief - * + * @brief ATmega4809 ADC0 driver and engineering-unit conversions. + * * Created: 27.09.2025 05:06:42 * Author: ThePetrovich - * + * * Copyright YKSA - Sakha Aerospace Systems, LLC. * See the LICENSE file for details. - * + * * SPDX-License-Identifier: BSD-3-Clause */ - #ifndef ADC_H_ #define ADC_H_ #include /** - * @brief Enable ADC for fast radiation detection + * @brief Enable ADC0 in fast event-driven mode for radiation pulse capture. + * ADC0 conversions are started by the detector trigger via EVSYS and + * completed by the ADC0_RESRDY ISR. */ void adc_enable_fast(void); /** - * @brief Restore ADC to default settings + * @brief Restore ADC0 to its default configuration suitable for blocking + * analogRead()-style reads of housekeeping channels. */ void adc_restore_default(void); /** - * @brief Convert ADC reading to temperature in 0.1°C - * @param adc_value 12-bit ADC reading - * @return Temperature in 0.1°C + * @brief Convert a raw ADC reading to temperature in 0.1 °C. + * Uses calibration values from @c config when available. + * @param adc_value Raw ADC reading. + * @return Temperature in 0.1 °C. */ int16_t adc_to_temperature_c(uint16_t adc_value); /** - * @brief Convert ADC reading to voltage in mV - * @param adc_value ADC reading - * @return Voltage in mV + * @brief Convert a raw ADC reading from the V28 feedback divider to mV. + * @param adc_value Raw ADC reading. + * @return Voltage at the sensor input in mV. */ uint16_t adc_to_voltage_mv(uint16_t adc_value); /** - * @brief Read ADC with oversampling - * @param pin Analog pin to read - * @param samples Number of samples for oversampling - * @return Averaged ADC value + * @brief Block-read an analog pin with software oversampling. + * @param pin Analog pin to read. + * @param samples Sample count (1, 4, 16, or 64). + * @return Sum of samples right-shifted by log2(samples) to give the average. */ uint16_t adc_read_oversampled(uint8_t pin, uint8_t samples); /** - * @brief Command to calibrate ADC and read reference values + * @brief Read the MCU internal temperature sensor (blocking, ~10 ms). + * @return Temperature in Kelvin. */ -void adc_cmd_calibrate(void); +uint16_t adc_read_tempsense(void); -#endif // ADC_H_ \ No newline at end of file +#endif /* ADC_H_ */ \ No newline at end of file diff --git a/sbc_fw/config.h b/sbc_fw/config.h index 2d44580..04ea4ca 100644 --- a/sbc_fw/config.h +++ b/sbc_fw/config.h @@ -1,6 +1,7 @@ /* * @file config.h - * @brief System configuration and constants + * @brief System configuration, persisted-config struct definition, and + * compile-time constants shared between modules. * * Created: 21.09.2025 * Author: ThePetrovich @@ -14,86 +15,122 @@ #ifndef CONFIG_H #define CONFIG_H -// Firmware version -#define FIRMWARE_VERSION_MAJOR 2 -#define FIRMWARE_VERSION_MINOR 03 +#include -// Serial communication +/* Firmware version */ +#define FIRMWARE_VERSION_MAJOR 3 +#define FIRMWARE_VERSION_MINOR 00 + +/* Serial communication */ #define SERIAL_BAUD_RATE 38400 -// Timing constants +/* Timing constants */ #define STATUS_LED_BLINK_PERIOD 500U #define MAIN_LOOP_DELAY 10 -#define CPM_UPDATE_PERIOD 10000U +#define RSENSE_UPDATE_PERIOD 1000U -#define RSENSE_CHANNEL_16_MASK 0x7FFU // 11-bit channel mask -#define RSENSE_CHANNEL_32_MASK 0x3FFU // 10-bit channel mask -#define RSENSE_CHANNEL_COUNT 2048 -#define RSENSE_CHANNEL_32_COUNT 1024 +/* Spectrum channel masks and counts */ +#define RSENSE_CHANNEL_MASK 0x3FFU /* 10-bit channel mask */ +#define RSENSE_CHANNEL_COUNT 1024 -// Temperature sensor constants (MCP9700) -#define MCP9700_OFFSET_MV 500L // 500 mV at 0°C -#define MCP9700_SCALE_MV 10L // 10 mV per degree -#define ADC_OVERSAMPLING_BITS 2 // 16x oversampling -> 2 extra bits -#define ADC_VREF_MV 3000L // 3.0V reference -#define ADC_RESOLUTION 1024L // 10-bit ADC +/* MCP9700 temperature sensor calibration constants */ +#define MCP9700_OFFSET_MV 500L /* 500 mV at 0 °C */ +#define MCP9700_SCALE_MV 10L /* 10 mV per °C */ +#define ADC_OVERSAMPLING_BITS 2 /* 16x oversampling -> 2 extra bits */ +#define ADC_VREF_MV 3000L /* 3.0 V reference */ +#define ADC_RESOLUTION 1024L /* 10-bit ADC */ -// Voltage divider constants -#define VOLTAGE_DIVIDER_RATIO 20.0f // 200k and 10k resistors -#define VOLTAGE_MV_PER_STEP 14.6484375f // (3.0 * 20) / 4096 +/* Voltage divider constants */ +#define VOLTAGE_DIVIDER_RATIO 20.0f /* 200k and 10k resistors */ +#define VOLTAGE_MV_PER_STEP 14.6484375f /* (3.0 * 20) / 4096 */ -// EEPROM addresses for potentiometer settings -#define EEPROM_POT_HV_ADDR 0 -#define EEPROM_POT_AMP_ADDR 1 -#define EEPROM_POT_DET_ADDR 2 -#define EEPROM_MAGIC_ADDR 3 -#define EEPROM_MAGIC_VALUE 0xA5 // Magic value to check if EEPROM is initialized - -// Default potentiometer values +/* Default potentiometer wiper value (mid-scale, 127/255) */ #define DEFAULT_POT_VALUE 127 -// Light sensor constants +/* Light sensor I2C constants */ #define LSENSE_I2C_BASE_ADDR 0x38 #define LSENSE_EXPECTED_ID 0xE0 #define LSENSE_DATA_SIZE 12 +/** + * @brief AD5160 wiper settings for the three digital potentiometers. + */ +typedef struct potentiometers_t { + uint8_t pot_hv; /**< HV regulator wiper. */ + uint8_t pot_amp; /**< Amplifier gain wiper. */ + uint8_t pot_det; /**< Detector threshold wiper. */ +} __attribute__((packed)) potentiometers_t; + +/** + * @brief ADC and temperature-sensor calibration coefficients. + * Reused as the payload of #CMD_SET_CALIBRATION. + */ +typedef struct calibration_t { + uint16_t adccal0; /**< ADC reading at GND (offset). */ + uint16_t adccal3; /**< ADC reading at 3.0 V reference. */ + uint16_t tempcal; /**< Temperature offset correction. */ +} __attribute__((packed)) calibration_t; + +/** + * @brief Runtime feature flags packed into a single byte. + * Reused as the payload of #CMD_SET_FLAGS. + */ +typedef union config_flags_t { + struct __attribute__((packed)) { + /** Spectrum oversampling: 0/1/2/3 = 1x/4x/16x/64x. */ + uint8_t spectrum_oversample_bits : 2; + uint8_t reserved1 : 1; + /** 0 = disable, 1 = enable temperature drift compensation. */ + uint8_t temperature_drift_compensation : 1; + /** ADC oversampling for temperature/voltage: 0/1/2/3 = 1x/4x/16x/64x. */ + uint8_t adc_oversample_bits : 2; + uint8_t reserved2 : 2; + } fields; + uint8_t value; +} __attribute__((packed)) config_flags_t; + +/** + * @brief Temperature drift compensation parameters. + * Reused as the payload of #CMD_SET_TEMP_DRIFT_COMPENSATION. + * + * Drift coefficients are in pot_units/255 per °C. When @c pot_X_low equals + * @c pot_X_high, drift compensation for that channel is disabled. + * + * Sensor → potentiometer mapping: + * - temp_drift : DET_TEMP (SiPM carrier) → pot_hv + * - vbias_drift : HV_TEMP (+28V supply) → pot_hv + * - gain_drift : AMP_TEMP (amplifier) → pot_amp + * - threshold_drift : AMP_TEMP (amplifier) → pot_det + */ +typedef struct temp_drift_compensation_t { + int16_t temp_drift; + int16_t vbias_drift; + int16_t gain_drift; + int16_t threshold_drift; + uint32_t drift_period_ms; /**< Minimum interval between adjustments, in ms. */ + uint8_t pot_hv_low; + uint8_t pot_hv_high; + uint8_t pot_amp_low; + uint8_t pot_amp_high; + uint8_t pot_det_low; + uint8_t pot_det_high; +} __attribute__((packed)) temp_drift_compensation_t; + +/** + * @brief Persistent device configuration. + * + * Stored in EEPROM with dual-partition wear leveling (see eeprom.cpp). + * The pair @c magic1 / @c magic2 (0xA5 / 0x5A) marks a partition as valid. + */ typedef struct config_t { - uint8_t magic1; // 0xA5 to indicate valid config - uint8_t magic2; // 0x5A to indicate valid config - uint8_t pot_hv; - uint8_t pot_amp; - uint8_t pot_det; - struct { - uint16_t adccal0; // ADC offset at GND - uint16_t adccal3; // ADC reading at 3.0V reference - uint16_t tempcal; - uint16_t tempdrift_mvK; // SiPM temperature drift compensation in mV/K, normally around 20 mV/K - float gainlow; // gain value for pot_amp = 0 - float gainhigh; // gain value for pot_amp = 255 - } calibration __attribute__((packed)); - union { - struct { - uint8_t spectrum_oversample_bits - : 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x oversampling, only for spectrum measurement - uint8_t spectrum_channel_bits : 1; // 0 for 16-bit channels, 1 for 32-bit channels - uint8_t temperature_drift_compensation - : 1; // 0 to disable, 1 to enable temperature compensation - uint8_t adc_oversample_bits : 2; // 0, 1, 2, or 3 for 1x, 4x, 16x, or 64x ADC oversampling for - // temperature and voltage measurements - uint8_t reserved : 2; - } fields __attribute__((packed)); - uint8_t value; - } flags __attribute__((packed)); - struct { - uint8_t tempdrift_pot_hv_low; - uint8_t tempdrift_pot_hv_high; - uint8_t tempdrift_pot_amp_low; - uint8_t tempdrift_pot_amp_high; - uint8_t tempdrift_pot_det_low; - uint8_t tempdrift_pot_det_high; - } temp_drift_compensation_ranges __attribute__((packed)); + uint8_t magic1; /**< 0xA5 — valid-partition marker, byte 1. */ + uint8_t magic2; /**< 0x5A — valid-partition marker, byte 2. */ + potentiometers_t pots; + calibration_t calibration; + config_flags_t flags; + temp_drift_compensation_t temp_drift_compensation; } __attribute__((packed)) config_t; extern config_t config; -#endif // CONFIG_H +#endif /* CONFIG_H */ diff --git a/sbc_fw/eeprom.cpp b/sbc_fw/eeprom.cpp index c44d5ac..5106088 100644 --- a/sbc_fw/eeprom.cpp +++ b/sbc_fw/eeprom.cpp @@ -1,6 +1,6 @@ /* * @file eeprom.cpp - * @brief EEPROM management implementation + * @brief EEPROM management with dual-partition wear leveling. * * Created: 21.09.2025 * Author: ThePetrovich @@ -18,35 +18,127 @@ config_t config; +/* + * Each partition occupies sizeof(config_t)+1 bytes: + * [0 .. sizeof(config_t)-1] config_t struct + * [sizeof(config_t)] generation counter (uint8_t) + */ +#define PARTITION_SIZE ((uint16_t)(sizeof(config_t) + 1)) +#define PARTITION_A_ADDR 0 +#define PARTITION_B_ADDR PARTITION_SIZE +#define GEN_OFFSET ((uint16_t)sizeof(config_t)) + +/** + * @brief Track which partition was last written so saves alternate. + * 0 = partition A is current, 1 = partition B is current. + */ +static uint8_t g_active_partition = 0; + +/** + * @brief Check whether the partition at @p base contains a valid magic pair. + */ +static bool partition_valid(uint16_t base) +{ + uint8_t m1 = EEPROM.read(base + offsetof(config_t, magic1)); + uint8_t m2 = EEPROM.read(base + offsetof(config_t, magic2)); + return (m1 == 0xA5 && m2 == 0x5A); +} + +/** + * @brief Read a config_t struct from the partition at @p base into @p dst. + */ +static void read_partition(uint16_t base, config_t *dst) +{ + uint8_t *p = (uint8_t *)dst; + for (uint16_t i = 0; i < sizeof(config_t); i++) { + p[i] = EEPROM.read(base + i); + } +} + +/** + * @brief Write @p src and a generation byte to the partition at @p base. + * Uses EEPROM.update() so unchanged cells incur no wear. + */ +static void write_partition(uint16_t base, const config_t *src, uint8_t generation) +{ + const uint8_t *p = (const uint8_t *)src; + for (uint16_t i = 0; i < sizeof(config_t); i++) { + EEPROM.update(base + i, p[i]); + } + EEPROM.update(base + GEN_OFFSET, generation); +} + +/** + * @brief Populate @p cfg with safe factory defaults. + * Used on first boot or when both partitions are corrupt. + */ +static void set_defaults(config_t *cfg) +{ + cfg->magic1 = 0xA5; + cfg->magic2 = 0x5A; + cfg->pots.pot_hv = DEFAULT_POT_VALUE; + cfg->pots.pot_amp = DEFAULT_POT_VALUE; + cfg->pots.pot_det = DEFAULT_POT_VALUE; + cfg->calibration.adccal0 = 0xFFFF; + cfg->calibration.adccal3 = 0xFFFF; + cfg->calibration.tempcal = 0; + cfg->flags.value = 0; + /* pot_low == pot_high disables drift compensation per channel. */ + cfg->temp_drift_compensation.temp_drift = 0; + cfg->temp_drift_compensation.vbias_drift = 0; + cfg->temp_drift_compensation.gain_drift = 0; + cfg->temp_drift_compensation.threshold_drift = 0; + cfg->temp_drift_compensation.drift_period_ms = 5000; + cfg->temp_drift_compensation.pot_hv_low = DEFAULT_POT_VALUE; + cfg->temp_drift_compensation.pot_hv_high = DEFAULT_POT_VALUE; + cfg->temp_drift_compensation.pot_amp_low = DEFAULT_POT_VALUE; + cfg->temp_drift_compensation.pot_amp_high = DEFAULT_POT_VALUE; + cfg->temp_drift_compensation.pot_det_low = DEFAULT_POT_VALUE; + cfg->temp_drift_compensation.pot_det_high = DEFAULT_POT_VALUE; +} + void eeprom_init(void) { - if (EEPROM.read(EEPROM_MAGIC_ADDR) != EEPROM_MAGIC_VALUE) { - potentiometer_settings_t default_settings = { - .hv_pot = DEFAULT_POT_VALUE, .amp_pot = DEFAULT_POT_VALUE, .det_pot = DEFAULT_POT_VALUE}; + bool a_valid = partition_valid(PARTITION_A_ADDR); + bool b_valid = partition_valid(PARTITION_B_ADDR); - eeprom_save_pot_settings(&default_settings); - EEPROM.write(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_VALUE); + if (a_valid && b_valid) { + uint8_t gen_a = EEPROM.read(PARTITION_A_ADDR + GEN_OFFSET); + uint8_t gen_b = EEPROM.read(PARTITION_B_ADDR + GEN_OFFSET); + /* Signed delta handles wrap-around: B is newer if (gen_b - gen_a) > 0 mod 256. */ + if ((int8_t)(gen_b - gen_a) > 0) { + read_partition(PARTITION_B_ADDR, &config); + g_active_partition = 1; + } else { + read_partition(PARTITION_A_ADDR, &config); + g_active_partition = 0; + } + } else if (a_valid) { + read_partition(PARTITION_A_ADDR, &config); + g_active_partition = 0; + } else if (b_valid) { + read_partition(PARTITION_B_ADDR, &config); + g_active_partition = 1; + } else { + set_defaults(&config); + write_partition(PARTITION_A_ADDR, &config, 0); + g_active_partition = 0; } } -void eeprom_load_pot_settings(potentiometer_settings_t *settings) +void eeprom_save_config(void) { - if (settings == nullptr) { - return; - } + /* Write to whichever partition is NOT currently active. */ + uint8_t target_partition = (g_active_partition == 0) ? 1 : 0; + uint16_t target_base = (target_partition == 0) ? PARTITION_A_ADDR : PARTITION_B_ADDR; + uint16_t active_base = (g_active_partition == 0) ? PARTITION_A_ADDR : PARTITION_B_ADDR; - settings->hv_pot = EEPROM.read(EEPROM_POT_HV_ADDR); - settings->amp_pot = EEPROM.read(EEPROM_POT_AMP_ADDR); - settings->det_pot = EEPROM.read(EEPROM_POT_DET_ADDR); -} - -void eeprom_save_pot_settings(const potentiometer_settings_t *settings) -{ - if (settings == nullptr) { - return; - } - - EEPROM.write(EEPROM_POT_HV_ADDR, settings->hv_pot); - EEPROM.write(EEPROM_POT_AMP_ADDR, settings->amp_pot); - EEPROM.write(EEPROM_POT_DET_ADDR, settings->det_pot); + uint8_t old_gen = EEPROM.read(active_base + GEN_OFFSET); + uint8_t new_gen = old_gen + 1; + + config.magic1 = 0xA5; + config.magic2 = 0x5A; + + write_partition(target_base, &config, new_gen); + g_active_partition = target_partition; } diff --git a/sbc_fw/eeprom.h b/sbc_fw/eeprom.h index 2ba3e2a..5f6ec26 100644 --- a/sbc_fw/eeprom.h +++ b/sbc_fw/eeprom.h @@ -1,6 +1,16 @@ /* * @file eeprom.h - * @brief EEPROM management for persistent settings + * @brief EEPROM management for persistent configuration storage. + * + * Dual-partition layout in ATmega4809 EEPROM (256 bytes): + * + * [0 .. sizeof(config_t)] Partition A: config_t + 1-byte generation counter + * [sizeof(config_t)+1 .. 2*(sizeof(config_t)+1)-1] Partition B: same layout + * + * On save, the alternate (older) partition is overwritten. If power fails + * mid-write the other partition retains the last good config. The generation + * counter wraps around uint8_t; the partition with the higher counter + * (mod-256 comparison via signed delta) is the newer one. * * Created: 21.09.2025 * Author: ThePetrovich @@ -17,30 +27,16 @@ #include /** - * @brief Potentiometer settings structure - */ -typedef struct { - uint8_t hv_pot; ///< High voltage potentiometer value - uint8_t amp_pot; ///< SiPM Pre-amp gain potentiometer value - uint8_t det_pot; ///< Detection threshold potentiometer value -} potentiometer_settings_t; - -/** - * @brief Initialize EEPROM settings - * Sets default values if EEPROM is uninitialized + * @brief Load the global @c config from EEPROM. + * Picks the freshest valid partition; writes factory defaults if both + * partitions are blank or corrupt. */ void eeprom_init(void); /** - * @brief Load potentiometer settings from EEPROM - * @param settings Pointer to settings structure to populate + * @brief Persist the current global @c config to the alternate partition, + * bumping the generation counter to flip the active partition. */ -void eeprom_load_pot_settings(potentiometer_settings_t *settings); +void eeprom_save_config(void); -/** - * @brief Save potentiometer settings to EEPROM - * @param settings Pointer to settings structure to save - */ -void eeprom_save_pot_settings(const potentiometer_settings_t *settings); - -#endif // DET_EEPROM_H +#endif /* DET_EEPROM_H */ diff --git a/sbc_fw/iodefs.h b/sbc_fw/iodefs.h index 6dabb27..7cb2d68 100644 --- a/sbc_fw/iodefs.h +++ b/sbc_fw/iodefs.h @@ -1,6 +1,6 @@ /* * @file iodefs.h - * @brief + * @brief Hardware pin assignments for the SBC firmware. * * Created: 21.09.2025 05:39:48 * Author: ThePetrovich @@ -18,32 +18,38 @@ #include #include -#define DET_TRIG_PIN 21 // PC7, event input -#define DET_READ_PIN A3 // PD3 (ADC3) -#define DET_PREAMP_PIN A2 // PD2 (ADC2) +/* Detector signal path */ +#define DET_TRIG_PIN 21 /* PC7, event input */ +#define DET_READ_PIN A3 /* PD3 (ADC3) */ +#define DET_PREAMP_PIN A2 /* PD2 (ADC2) */ -#define STATUS_LED 31 // PE1 +/* Status indicator */ +#define STATUS_LED 31 /* PE1 */ -#define HV_EN 16 // PC2 -#define DET_EN 18 // PC4 -#define DET_RST 20 // PC6 +/* Power and reset control */ +#define HV_EN 16 /* PC2 */ +#define DET_EN 18 /* PC4 */ +#define DET_RST 20 /* PC6 */ -#define V28V0_FB_PIN A0 // PD0 (ADC0) -#define HV_TEMP_PIN A1 // PD1 (ADC1) -#define AMP_TEMP_PIN A4 // PD4 (ADC4) -#define DET_TEMP_PIN A5 // PD5 (ADC5) +/* Housekeeping ADC inputs */ +#define V28V0_FB_PIN A0 /* PD0 (ADC0) */ +#define HV_TEMP_PIN A1 /* PD1 (ADC1) */ +#define AMP_TEMP_PIN A4 /* PD4 (ADC4) */ +#define DET_TEMP_PIN A5 /* PD5 (ADC5) */ -#define HV_CS 15 // PC1 -#define AMP_CS 17 // PC3 -#define DET_CS 19 // PC5 +/* AD5160 digital potentiometer chip selects */ +#define HV_CS 15 /* PC1 */ +#define AMP_CS 17 /* PC3 */ +#define DET_CS 19 /* PC5 */ -#define SCL0 8 // PB0 -#define SDA0 9 // PB1 +/* Light sensor I2C bus pins (software-bitbanged) */ +#define SCL0 8 /* PB0 */ +#define SDA0 9 /* PB1 */ -#define SCL1 10 // PB2 -#define SDA1 11 // PB3 +#define SCL1 10 /* PB2 */ +#define SDA1 11 /* PB3 */ -#define SCL2 12 // PB4 -#define SDA2 13 // PB5 +#define SCL2 12 /* PB4 */ +#define SDA2 13 /* PB5 */ -#endif // LSENSE_IODEFS_H \ No newline at end of file +#endif /* LSENSE_IODEFS_H */ diff --git a/sbc_fw/lsense.cpp b/sbc_fw/lsense.cpp index 2d71454..f773d78 100644 --- a/sbc_fw/lsense.cpp +++ b/sbc_fw/lsense.cpp @@ -1,6 +1,7 @@ /* * @file lsense.cpp - * @brief Light sensor implementation + * @brief Light sensor implementation: software I2C, detection, and channel + * readout for the six TCS-style light sensors on three buses. * * Created: 21.09.2025 * Author: ThePetrovich @@ -21,9 +22,6 @@ static SoftwareI2C g_i2c_buses[NUM_BUSES]; -/** - * @brief Light sensor data structure - */ typedef union __attribute__((packed)) { struct __attribute__((packed)) { uint16_t red; @@ -39,21 +37,9 @@ typedef union __attribute__((packed)) { static light_sensor_data_t g_sensor_data[NUM_BUSES][SENSORS_PER_BUS]; static uint8_t g_sensor_addresses[NUM_BUSES][SENSORS_PER_BUS]; -// Presence counters static uint8_t g_sensors_detected_total = 0; static uint8_t g_sensors_detected_per_bus[NUM_BUSES]; -static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr); -static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_sensor_data_t *sensor_data); -static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr); -static void initialize_i2c_bus(int bus_number, bool pin_order_inverted); -static void detect_sensors_on_bus_generic(int bus_number); - -/** - * @brief Configure a light sensor for measurement - * @param i2c_bus Reference to I2C bus instance - * @param sensor_addr LSB of sensor address (0 or 1) - */ static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr) { uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; @@ -64,17 +50,11 @@ static void configure_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr) i2c_bus.beginTransmission(addr); i2c_bus.write(0x41); - i2c_bus.write(0b00101101); // IR gain x1, RGB gain x1, 35ms mode - i2c_bus.write(0b00010000); // RGB_EN = 1, measurement active + i2c_bus.write(0b00101101); /* IR gain x1, RGB gain x1, 35ms mode */ + i2c_bus.write(0b00010000); /* RGB_EN = 1, measurement active */ i2c_bus.endTransmission(); } -/** - * @brief Read data from a light sensor - * @param i2c_bus Reference to I2C bus instance - * @param sensor_addr LSB of sensor address (0 or 1) - * @param sensor_data Pointer to data structure to populate - */ static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_sensor_data_t *sensor_data) { if (sensor_data == nullptr) @@ -83,7 +63,7 @@ static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_s uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; i2c_bus.beginTransmission(addr); - i2c_bus.write(0x50); // Start from red register + i2c_bus.write(0x50); i2c_bus.endTransmission(); i2c_bus.requestFrom(addr, (uint8_t)LSENSE_DATA_SIZE); @@ -93,37 +73,25 @@ static void read_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr, light_s i2c_bus.endTransmission(); } -/** - * @brief Detect if a light sensor is present - * @param i2c_bus Reference to I2C bus instance - * @param sensor_addr LSB of sensor address (0 or 1) - * @return Manufacturer ID (0xE0 if valid sensor detected, 0 otherwise) - */ static uint8_t detect_light_sensor(SoftwareI2C &i2c_bus, uint8_t sensor_addr) { uint8_t response = 0; uint8_t addr = LSENSE_I2C_BASE_ADDR | sensor_addr; i2c_bus.beginTransmission(addr); - i2c_bus.write(0x92); // Manufacturer ID register + i2c_bus.write(0x92); i2c_bus.endTransmission(); i2c_bus.requestFrom(addr, (uint8_t)1); if (i2c_bus.available()) { response = i2c_bus.read(); if (response == 0xFF) { - response = 0; // Invalid response + response = 0; } } - return response; } -/** - * @brief Initialize an I2C bus and configure sensors - * @param bus_number Bus number (0, 1, or 2) - * @param pin_order_inverted Whether to invert SDA/SCL pin order - */ static void initialize_i2c_bus(int bus_number, bool pin_order_inverted) { uint8_t sda_pin, scl_pin; @@ -142,29 +110,22 @@ static void initialize_i2c_bus(int bus_number, bool pin_order_inverted) scl_pin = pin_order_inverted ? SDA2 : SCL2; break; default: - // Invalid bus number return; } SoftwareI2C *i2c_bus = &g_i2c_buses[bus_number]; - i2c_bus->begin(sda_pin, scl_pin); configure_light_sensor(*i2c_bus, 0); configure_light_sensor(*i2c_bus, 1); } -/** - * @brief Generic function to detect sensors on any I2C bus - * @param bus_number Bus number (0, 1, or 2) - */ -static void detect_sensors_on_bus_generic(int bus_number) +static void detect_sensors_on_bus(int bus_number) { if (bus_number < 0 || bus_number >= NUM_BUSES) return; g_sensors_detected_per_bus[bus_number] = 0; - // Try normal pin order first initialize_i2c_bus(bus_number, false); g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0); if (g_sensor_addresses[bus_number][SENSOR_NEG] == LSENSE_EXPECTED_ID) { @@ -177,7 +138,6 @@ static void detect_sensors_on_bus_generic(int bus_number) g_sensors_detected_per_bus[bus_number]++; } - // If no sensors found, try inverted pin order if (g_sensors_detected_per_bus[bus_number] == 0) { initialize_i2c_bus(bus_number, true); g_sensor_addresses[bus_number][SENSOR_NEG] = detect_light_sensor(g_i2c_buses[bus_number], 0); @@ -193,62 +153,67 @@ static void detect_sensors_on_bus_generic(int bus_number) } } -/*******************************************************************************/ -/*******************************************************************************/ -/* Command handlers */ -/*******************************************************************************/ -/*******************************************************************************/ - -void lsense_cmd_presence(void) +void lsense_detect_all(void) { g_sensors_detected_total = 0; - - detect_sensors_on_bus_generic(BUS_X); - detect_sensors_on_bus_generic(BUS_Y); - detect_sensors_on_bus_generic(BUS_Z); - - Serial.write(g_sensors_detected_total); - Serial.write(g_sensors_detected_per_bus[BUS_X]); - Serial.write(g_sensor_addresses[BUS_X][SENSOR_NEG]); - Serial.write(g_sensor_addresses[BUS_X][SENSOR_POS]); - Serial.write(g_sensors_detected_per_bus[BUS_Y]); - Serial.write(g_sensor_addresses[BUS_Y][SENSOR_NEG]); - Serial.write(g_sensor_addresses[BUS_Y][SENSOR_POS]); - Serial.write(g_sensors_detected_per_bus[BUS_Z]); - Serial.write(g_sensor_addresses[BUS_Z][SENSOR_NEG]); - Serial.write(g_sensor_addresses[BUS_Z][SENSOR_POS]); - - Serial.flush(); - SERIAL_BUFFER_CLEAR(); + detect_sensors_on_bus(BUS_X); + detect_sensors_on_bus(BUS_Y); + detect_sensors_on_bus(BUS_Z); } -void lsense_cmd_read(void) +void lsense_get_presence(lsense_presence_t *out) { - detect_sensors_on_bus_generic(BUS_X); - read_light_sensor(g_i2c_buses[BUS_X], 0, &g_sensor_data[BUS_X][SENSOR_NEG]); - read_light_sensor(g_i2c_buses[BUS_X], 1, &g_sensor_data[BUS_X][SENSOR_POS]); + if (out == nullptr) + return; + out->total_detected = g_sensors_detected_total; + out->bus_x_count = g_sensors_detected_per_bus[BUS_X]; + out->bus_x_neg_addr = g_sensor_addresses[BUS_X][SENSOR_NEG]; + out->bus_x_pos_addr = g_sensor_addresses[BUS_X][SENSOR_POS]; + out->bus_y_count = g_sensors_detected_per_bus[BUS_Y]; + out->bus_y_neg_addr = g_sensor_addresses[BUS_Y][SENSOR_NEG]; + out->bus_y_pos_addr = g_sensor_addresses[BUS_Y][SENSOR_POS]; + out->bus_z_count = g_sensors_detected_per_bus[BUS_Z]; + out->bus_z_neg_addr = g_sensor_addresses[BUS_Z][SENSOR_NEG]; + out->bus_z_pos_addr = g_sensor_addresses[BUS_Z][SENSOR_POS]; +} - detect_sensors_on_bus_generic(BUS_Y); - read_light_sensor(g_i2c_buses[BUS_Y], 0, &g_sensor_data[BUS_Y][SENSOR_NEG]); - read_light_sensor(g_i2c_buses[BUS_Y], 1, &g_sensor_data[BUS_Y][SENSOR_POS]); +static uint16_t extract_channel(const light_sensor_data_t *d, uint8_t channel) +{ + switch (channel) { + case 0: + return d->data.red; + case 1: + return d->data.green; + case 2: + return d->data.blue; + case 3: + return d->data.ir; + case 4: + return d->data.green2; + case 5: /* visible: R+G+B+G2 */ + return (uint16_t)((uint32_t)d->data.red + d->data.green + d->data.blue + d->data.green2); + case 6: /* all: R+G+B+G2+IR */ + return (uint16_t)((uint32_t)d->data.red + d->data.green + d->data.blue + d->data.green2 + + d->data.ir); + default: + return 0; + } +} - detect_sensors_on_bus_generic(BUS_Z); - read_light_sensor(g_i2c_buses[BUS_Z], 0, &g_sensor_data[BUS_Z][SENSOR_NEG]); - read_light_sensor(g_i2c_buses[BUS_Z], 1, &g_sensor_data[BUS_Z][SENSOR_POS]); +void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out) +{ + if (out == nullptr) + return; - Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_X][SENSOR_NEG].data.blue >> 8); - Serial.write(g_sensor_data[BUS_X][SENSOR_POS].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_X][SENSOR_POS].data.blue >> 8); - Serial.write(g_sensor_data[BUS_Y][SENSOR_NEG].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_Y][SENSOR_NEG].data.blue >> 8); - Serial.write(g_sensor_data[BUS_Y][SENSOR_POS].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_Y][SENSOR_POS].data.blue >> 8); - Serial.write(g_sensor_data[BUS_Z][SENSOR_NEG].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_Z][SENSOR_NEG].data.blue >> 8); - Serial.write(g_sensor_data[BUS_Z][SENSOR_POS].data.blue & 0xFF); - Serial.write(g_sensor_data[BUS_Z][SENSOR_POS].data.blue >> 8); + for (int bus = 0; bus < NUM_BUSES; bus++) { + read_light_sensor(g_i2c_buses[bus], 0, &g_sensor_data[bus][SENSOR_NEG]); + read_light_sensor(g_i2c_buses[bus], 1, &g_sensor_data[bus][SENSOR_POS]); + } - Serial.flush(); - SERIAL_BUFFER_CLEAR(); -} \ No newline at end of file + 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); +} diff --git a/sbc_fw/lsense.h b/sbc_fw/lsense.h index 1d7cca3..8ef49b0 100644 --- a/sbc_fw/lsense.h +++ b/sbc_fw/lsense.h @@ -1,8 +1,8 @@ /* * @file lsense.h - * @brief Light sensor management and command handling + * @brief Light sensor management. * - * Created: 21.09.2025 05:53:32 + * Created: 21.09.2025 * Author: ThePetrovich * * Copyright YKSA - Sakha Aerospace Systems, LLC. @@ -14,30 +14,69 @@ #ifndef LSENSE_H #define LSENSE_H -#include +#include -#define NUM_BUSES 3 +#define NUM_BUSES 3 #define SENSORS_PER_BUS 2 -enum { - BUS_X = 0, // X-axis (bus 0) - BUS_Y = 1, // Y-axis (bus 1) - BUS_Z = 2 // Z-axis (bus 2) -}; - -enum { - SENSOR_NEG = 0, // Negative sensor (addresses 0) - SENSOR_POS = 1 // Positive sensor (addresses 1) -}; +enum { BUS_X = 0, BUS_Y = 1, BUS_Z = 2 }; +enum { SENSOR_NEG = 0, SENSOR_POS = 1 }; /** - * @brief Send light sensor presence information via serial + * @brief Detection result for the six light sensors across three I2C buses. + * Reused as the response payload of #CMD_LIGHT_SENSOR_PRESENCE. */ -void lsense_cmd_presence(void); +typedef union lsense_presence_t { + struct __attribute__((packed)) { + uint8_t total_detected; + uint8_t bus_x_count; + uint8_t bus_x_neg_addr; + uint8_t bus_x_pos_addr; + uint8_t bus_y_count; + uint8_t bus_y_neg_addr; + uint8_t bus_y_pos_addr; + uint8_t bus_z_count; + uint8_t bus_z_neg_addr; + uint8_t bus_z_pos_addr; + }; + uint8_t bytes[10]; +} __attribute__((packed)) lsense_presence_t; /** - * @brief Read and send light sensor data via serial + * @brief Per-sensor channel values for a single read across all 6 sensors. + * Reused as the response payload of #CMD_READ_LIGHT_SENSORS. */ -void lsense_cmd_read(void); +typedef union lsense_channel_values_t { + struct __attribute__((packed)) { + uint16_t bus_x_neg_value; + uint16_t bus_x_pos_value; + uint16_t bus_y_neg_value; + uint16_t bus_y_pos_value; + uint16_t bus_z_neg_value; + uint16_t bus_z_pos_value; + }; + uint16_t values[6]; +} __attribute__((packed)) lsense_channel_values_t; -#endif // LSENSE_H +/** + * @brief Detect all sensors on all three buses. + * Populates internal state used by #lsense_get_presence and + * #lsense_read_channel. + */ +void lsense_detect_all(void); + +/** + * @brief Fill @p out with detection results from the most recent + * #lsense_detect_all call. + */ +void lsense_get_presence(lsense_presence_t *out); + +/** + * @brief Read a spectral channel from all six sensors into @p out. + * @param channel 0=red 1=green 2=blue 3=IR 4=green2 + * 5=visible (R+G+B+G2) 6=all (R+G+B+G2+IR) + * @param out Output values, ordered X_neg, X_pos, Y_neg, Y_pos, Z_neg, Z_pos. + */ +void lsense_read_channel(uint8_t channel, lsense_channel_values_t *out); + +#endif /* LSENSE_H */ diff --git a/sbc_fw/protocol.cpp b/sbc_fw/protocol.cpp new file mode 100644 index 0000000..fa57776 --- /dev/null +++ b/sbc_fw/protocol.cpp @@ -0,0 +1,328 @@ +/* + * @file protocol.cpp + * @brief Protocol framing, CRC, and command handler implementations. + * + * Created: 07.05.2026 + * Author: ThePetrovich + * + * Copyright YKSA - Sakha Aerospace Systems, LLC. + * See the LICENSE file for details. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#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 index 52c0073..52e5425 100644 --- a/sbc_fw/protocol.h +++ b/sbc_fw/protocol.h @@ -1,8 +1,19 @@ /* * @file protocol.h - * @brief + * @brief Serial protocol definitions, command structures and dispatch table. * - * Created: 07.05.2026 10:15:36 + * Frame format: [CMD : 2B][LEN : 2B][PAYLOAD : LEN B][CRC16 : 2B] + * - CMD : 16-bit little-endian command identifier. + * - LEN : 16-bit little-endian payload length, excluding CRC. + * - PAYLOAD : LEN bytes, command-specific. + * - CRC16 : CRC16-XMODEM (poly 0x1021, init 0x0000) over CMD+LEN+PAYLOAD. + * + * Response identifiers: + * - 0x414B "AK" : command accepted and executed. + * - 0x4E4B "NK" : valid command but cannot execute (bad params, wrong state). + * - 0x4552 "ER" : malformed frame (bad CRC, bad length). + * + * Created: 07.05.2026 * Author: ThePetrovich * * Copyright YKSA - Sakha Aerospace Systems, LLC. @@ -15,294 +26,188 @@ #define PROTOCOL_H #include "config.h" +#include "lsense.h" #include -/* - * @details - * General Protocol Description: - * - All commands are initiated by the host - * - Command format: [CMD (2 bytes)][Length (2 bytes)][Payload (variable)][CRC16 (2 bytes)] - * - CMD: 2 bytes command identifier (e.g., 'HV' for set potentiometers) - * - Length: 2 bytes unsigned integer indicating the length of the payload in bytes - * - Payload: Variable length data specific to the command - * - CRC16: 2 bytes CRC16-XMODEM checksum of the CMD, Length, and Payload fields - * - Response: For commands that require a response, the device will send back a similar structured - * message with the appropriate CMD and payload. Otherwise, it should send an acknowledgment - * (e.g., 'ACK') or an error response if the command is invalid. - * - If a valid command is received, but cannot be executed (e.g. locked state, parameters out of range), - * the device should send a command failure response (e.g., 'NAK') without performing any action. - * - Error Handling: If a command is malformed (e.g., incorrect CRC, invalid length), the device will - * ignore the command and should send an error response (e.g., 'ERR') to the host. - * The device should not perform any action if the command is invalid. - * Example Command: Set Potentiometers - * - CMD: 'HV' (0x48 0x56) - * - Length: 3 (0x00 0x03) - * - Payload: [HV_POT (1 byte)][AMP_POT (1 byte)][DET_POT (1 byte)] - * - CRC16: Calculated over 'HV', Length, and Payload - * Response: 'ACK' if successful, 'NAK' if parameters are out of safe range, 'ERR' if command is malformed - */ +#define PROTOCOL_RESP_ACK 0x414B /* "AK" */ +#define PROTOCOL_RESP_NAK 0x4E4B /* "NK" */ +#define PROTOCOL_RESP_ERR 0x4552 /* "ER" */ -uint16_t protocol_crc16_xmodem(uint8_t *data, uint16_t len); +/** + * @brief Compute CRC16-XMODEM (poly 0x1021, init 0x0000) over a buffer. + * @param data Pointer to input bytes. + * @param len Number of bytes to process. + * @return Final CRC value. + */ +uint16_t protocol_crc16_xmodem(const uint8_t *data, uint16_t len); + +/** + * @brief Incrementally update a running CRC16-XMODEM with additional bytes. + * Pass @c crc=0 for the first call, then chain the result through + * further calls. This is the single CRC primitive used by the protocol. + * @param crc Current CRC value. + * @param data Pointer to input bytes. + * @param len Number of bytes to process. + * @return Updated CRC value. + */ +uint16_t protocol_crc16_update(uint16_t crc, const uint8_t *data, uint16_t len); typedef struct protocol_header_t { uint16_t cmd; uint16_t length; } __attribute__((packed)) protocol_header_t; -void protocol_send_header(uint16_t cmd, uint16_t length); +/** + * @brief Send a complete framed message: header + payload + CRC. + * @param cmd Command identifier. + * @param payload Optional payload pointer (may be NULL when @p length is 0). + * @param length Payload length in bytes. + */ +void protocol_send_message(uint16_t cmd, const void *payload, uint16_t length); +/** + * @brief Send a framed message whose payload is split across two fragments. + * CRC is computed across header + frag1 + frag2 in order. + */ +void protocol_send_message_v2(uint16_t cmd, const void *frag1, uint16_t len1, const void *frag2, uint16_t len2); + +/** @brief Send an ACK response (cmd = PROTOCOL_RESP_ACK, no payload). */ void protocol_send_ack(void); + +/** @brief Send a NAK response (cmd = PROTOCOL_RESP_NAK, no payload). */ void protocol_send_nak(void); + +/** @brief Send an ERR response (cmd = PROTOCOL_RESP_ERR, no payload). */ void protocol_send_error(void); -void protocol_send_message(uint16_t cmd, void *payload, uint16_t length); - /* - * @brief Firmware version command and response structure - * @param[out] major Firmware major version - * @param[out] minor Firmware minor version + * X-macro command dispatch table. + * + * X(EnumName, CmdId, HandlerFn) is the single source of truth for all + * supported commands. It is expanded to: + * - an enumeration of command IDs (e.g. CMD_FIRMWARE_VERSION), + * - prototypes for every handler function, + * - the dispatch switch in sbc_fw.ino. + * The same X-macro can be re-included on the host/master side to generate + * matching send helpers. */ -#define CMD_FIRMWARE_VERSION 0x7600 +#define X_PROTOCOL_handlerS \ + X(CMD_FIRMWARE_VERSION, 0x7600, protocol_version_handler) \ + X(CMD_LIGHT_SENSOR_PRESENCE, 0x6C00, protocol_light_sensor_presence_handler) \ + X(CMD_READ_LIGHT_SENSORS, 0x7200, protocol_read_light_sensors_handler) \ + X(CMD_GET_CONFIGURATION, 0x6D00, protocol_get_configuration_handler) \ + X(CMD_SET_CONFIGURATION, 0x7D00, protocol_set_configuration_handler) \ + X(CMD_SET_POTENTIOMETERS, 0x7D01, protocol_set_potentiometers_handler) \ + X(CMD_SET_CALIBRATION, 0x7D02, protocol_set_calibration_handler) \ + X(CMD_SET_FLAGS, 0x7D03, protocol_set_flags_handler) \ + X(CMD_SET_TEMP_DRIFT_COMPENSATION, 0x7D04, protocol_set_temp_drift_compensation_handler) \ + X(CMD_SET_DRIFT_COMPENSATION_ENABLE, 0x7D05, protocol_set_drift_compensation_enable_handler) \ + X(CMD_GET_TELEMETRY, 0x7400, protocol_get_telemetry_handler) \ + X(CMD_FLUSH_COUNTERS, 0x7401, protocol_flush_counters_handler) \ + X(CMD_FLUSH_SPECTRUM, 0x7402, protocol_flush_spectrum_handler) \ + X(CMD_RSENSE_ENABLE, 0x7500, protocol_rsense_enable_handler) \ + X(CMD_RSENSE_DISABLE, 0x7501, protocol_rsense_disable_handler) \ + X(CMD_SPECTRUM_FREEZE, 0x7502, protocol_spectrum_freeze_handler) \ + X(CMD_SPECTRUM_UNFREEZE, 0x7503, protocol_spectrum_unfreeze_handler) \ + X(CMD_GET_COUNTS, 0x7504, protocol_get_counts_handler) \ + X(CMD_READ_SPECTRUM_DATA, 0x7505, protocol_read_spectrum_chunk_handler) + +/** @brief Symbolic command IDs generated from #X_PROTOCOL_handlerS. */ +enum protocol_cmd_id { +#define X(Enum, Cmd_id, Handler) Enum = Cmd_id, + X_PROTOCOL_handlerS +#undef X +}; + +/* Command handler prototypes generated from X_PROTOCOL_handlerS. */ +#define X(Enum, Cmd_id, Handler) void Handler(void *payload, uint16_t length); +X_PROTOCOL_handlerS +#undef X + +/** @brief Response payload for #CMD_FIRMWARE_VERSION. */ typedef struct protocol_firmware_version_response_t { uint8_t major; uint8_t minor; } __attribute__((packed)) protocol_firmware_version_response_t; -void handle_version_command(void *payload, uint16_t length); +/* Light sensor protocol payloads reuse the lsense module types directly + * so handlers can populate them via a single call without field copies. */ -/* - * @brief Light sensor presence command and response structure - * @param[out] total_detected Total number of sensors detected across all buses - * @param[out] bus_x_count Number of sensors detected on bus X - * @param[out] bus_x_neg_addr I2C address of negative sensor on bus X (0 if not present) - * @param[out] bus_x_pos_addr I2C address of positive sensor on bus X (0 if not present) - * @param[out] bus_y_count Number of sensors detected on bus Y - * @param[out] bus_y_neg_addr I2C address of negative sensor on bus Y (0 if not present) - * @param[out] bus_y_pos_addr I2C address of positive sensor on bus Y (0 if not present) - * @param[out] bus_z_count Number of sensors detected on bus Z - * @param[out] bus_z_neg_addr I2C address of negative sensor on bus Z (0 if not present) - * @param[out] bus_z_pos_addr I2C address of positive sensor on bus Z (0 if not present) +/** @brief Response payload for #CMD_LIGHT_SENSOR_PRESENCE — alias of #lsense_presence_t. */ +typedef lsense_presence_t protocol_light_sensor_presence_response_t; + +/** + * @brief Command payload for #CMD_READ_LIGHT_SENSORS. + * + * channel: 0=red 1=green 2=blue 3=IR 4=green2 + * 5=visible (R+G+B+G2) 6=all (R+G+B+G2+IR) */ -#define CMD_LIGHT_SENSOR_PRESENCE 0x6C00 -typedef struct protocol_light_sensor_presence_response_t { - uint8_t total_detected; - uint8_t bus_x_count; - uint8_t bus_x_neg_addr; - uint8_t bus_x_pos_addr; - uint8_t bus_y_count; - uint8_t bus_y_neg_addr; - uint8_t bus_y_pos_addr; - uint8_t bus_z_count; - uint8_t bus_z_neg_addr; - uint8_t bus_z_pos_addr; -} __attribute__((packed)) protocol_light_sensor_presence_response_t; - -void handle_light_sensor_presence_command(void *payload, uint16_t length); - -/* - * @brief Read light sensor data command and response structure - * @param[in] channel Spectral channel (0 = red, 1 = green, 2 = blue, 3 = IR, 4 = green2, 5 = composite visible, 6 = - * composite all) - * @param[out] bus_x_neg_channel_value Channel value for negative sensor on bus X - * @param[out] bus_x_pos_channel_value Channel value for positive sensor on bus X - * @param[out] bus_y_neg_channel_value Channel value for negative sensor on bus Y - * @param[out] bus_y_pos_channel_value Channel value for positive sensor on bus Y - * @param[out] bus_z_neg_channel_value Channel value for negative sensor on bus Z - * @param[out] bus_z_pos_channel_value Channel value for positive sensor on bus Z - */ -#define CMD_READ_LIGHT_SENSORS 0x7200 -typedef struct protocol_read_light_sensors_command_t { +typedef struct protocol_read_light_sensors_handler_t { uint8_t channel; -} __attribute__((packed)) protocol_read_light_sensors_command_t; +} __attribute__((packed)) protocol_read_light_sensors_handler_t; -typedef struct protocol_read_light_sensors_response_t { - uint16_t bus_x_neg_channel_value; - uint16_t bus_x_pos_channel_value; - uint16_t bus_y_neg_channel_value; - uint16_t bus_y_pos_channel_value; - uint16_t bus_z_neg_channel_value; - uint16_t bus_z_pos_channel_value; -} __attribute__((packed)) protocol_read_light_sensors_response_t; +/** @brief Response payload for #CMD_READ_LIGHT_SENSORS — alias of #lsense_channel_values_t. */ +typedef lsense_channel_values_t protocol_read_light_sensors_response_t; -void handle_read_light_sensors_command(void *payload, uint16_t length); +/* Configuration commands reuse the persisted-config types from config.h + * directly. The on-the-wire layout is the corresponding struct, so handlers + * can use a single struct assignment in place of field-by-field copies. */ -/* - * @brief Get configuration command and response structure - * @param[out] config Current device configuration - */ -#define CMD_GET_CONFIGURATION 0x6D00 -typedef struct protocol_get_configuration_response_t { - config_t config; -} __attribute__((packed)) protocol_get_configuration_response_t; +/** @brief Response payload for #CMD_GET_CONFIGURATION — alias of #config_t. */ +typedef config_t protocol_get_configuration_response_t; -void handle_get_configuration_command(void *payload, uint16_t length); +/** @brief Command payload for #CMD_SET_CONFIGURATION — alias of #config_t. */ +typedef config_t protocol_set_configuration_handler_t; -/* - * @brief Set configuration command and response structure - * @param[in] config New device configuration to apply - */ -#define CMD_SET_CONFIGURATION 0x7D00 -typedef struct protocol_set_configuration_command_t { - config_t config; -} __attribute__((packed)) protocol_set_configuration_command_t; +/** @brief Command payload for #CMD_SET_POTENTIOMETERS — alias of #potentiometers_t. */ +typedef potentiometers_t protocol_set_potentiometers_handler_t; -void handle_set_configuration_command(void *payload, uint16_t length); +/** @brief Command payload for #CMD_SET_CALIBRATION — alias of #calibration_t. */ +typedef calibration_t protocol_set_calibration_handler_t; -/* - * @brief Set potentiometers command and response structure - * @param[in] hv_pot High voltage potentiometer value - * @param[in] amp_pot Amplifier potentiometer value - * @param[in] det_pot Detector potentiometer value - */ -#define CMD_SET_POTENTIOMETERS 0x7D01 -typedef struct protocol_set_potentiometers_command_t { - uint8_t hv_pot; - uint8_t amp_pot; - uint8_t det_pot; -} __attribute__((packed)) protocol_set_potentiometers_command_t; +/** @brief Command payload for #CMD_SET_FLAGS — alias of #config_flags_t. */ +typedef config_flags_t protocol_set_flags_handler_t; -void handle_set_potentiometers_command(void *payload, uint16_t length); +/** @brief Command payload for #CMD_SET_TEMP_DRIFT_COMPENSATION — alias of #temp_drift_compensation_t. */ +typedef temp_drift_compensation_t protocol_set_temp_drift_compensation_handler_t; -/* - * @brief Set calibration data command and response structure - * @param[in] adccal0 ADC offset at GND - * @param[in] adccal3 ADC reading at 3.0V reference - * @param[in] tempcal Temperature calibration value - * @param[in] tempdrift_mvK Temperature drift compensation in mV/K - */ -#define CMD_SET_CALIBRATION 0x7D02 -typedef struct protocol_set_calibration_command_t { - uint16_t adccal0; - uint16_t adccal3; - uint16_t tempcal; - uint16_t tempdrift_mvK; -} __attribute__((packed)) protocol_set_calibration_command_t; +/** @brief Command payload for #CMD_SET_DRIFT_COMPENSATION_ENABLE. */ +typedef struct protocol_set_drift_compensation_enable_handler_t { + uint8_t enable; /* 1 = enable, 0 = disable */ +} __attribute__((packed)) protocol_set_drift_compensation_enable_handler_t; -void handle_set_calibration_command(void *payload, uint16_t length); - -/* - * @brief Set flags command and response structure - * @param[in] flags Configuration flags to set (e.g., oversampling, channel mode, temperature compensation) - */ -#define CMD_SET_FLAGS 0x7D03 -typedef struct protocol_set_flags_command_t { - uint8_t flags; -} __attribute__((packed)) protocol_set_flags_command_t; - -/* -* @brief Set temperature drift compensation ranges command and response structure -* @param[in] tempdrift_pot_hv_low Low threshold for high voltage potentiometer -* @param[in] tempdrift_pot_hv_high High threshold for high voltage potentiometer -* @param[in] tempdrift_pot_amp_low Low threshold for amplifier potentiometer -* @param[in] tempdrift_pot_amp_high High threshold for amplifier potentiometer -* @param[in] tempdrift_pot_det_low Low threshold for detector potentiometer -* @param[in] tempdrift_pot_det_high High threshold for detector potentiometer -*/ -#define CMD_SET_TEMP_DRIFT_COMPENSATION_RANGES 0x7D04 -typedef struct protocol_set_temp_drift_compensation_ranges_command_t { - uint8_t tempdrift_pot_hv_low; - uint8_t tempdrift_pot_hv_high; - uint8_t tempdrift_pot_amp_low; - uint8_t tempdrift_pot_amp_high; - uint8_t tempdrift_pot_det_low; - uint8_t tempdrift_pot_det_high; -} __attribute__((packed)) protocol_set_temp_drift_compensation_ranges_command_t; - -void handle_set_flags_command(void *payload, uint16_t length); - -/* - * @brief Get telemetry command and response structure - * @param[out] hv_temp_c High voltage component temperature in 0.1°C - * @param[out] amp_temp_c Amplifier component temperature in 0.1°C - * @param[out] det_temp_c Detector component temperature in 0.1°C - * @param[out] mcu_temp_c MCU temperature in 0.1°C - * @param[out] vbias_mv SiPM bias voltage in mV - * @param[out] hv_pot Current high voltage potentiometer setting - * @param[out] amp_pot Current amplifier potentiometer setting - * @param[out] det_pot Current detector potentiometer setting - * @param[out] cps Current counts per second (CPM/60) - * @param[out] cp10s Counts in the last 10 seconds - * @param[out] total_counts Total counts since last reset - */ -#define CMD_GET_TELEMETRY 0x7400 +/** @brief Response payload for #CMD_GET_TELEMETRY. */ typedef struct protocol_get_telemetry_response_t { - uint16_t hv_temp_c; - uint16_t amp_temp_c; - uint16_t det_temp_c; - uint16_t mcu_temp_c; + uint16_t hv_temp_c; /**< 0.1 °C — HV_TEMP_PIN, +28V supply sensor. */ + uint16_t amp_temp_c; /**< 0.1 °C — AMP_TEMP_PIN, amplifier sensor. */ + uint16_t sipm_temp_c; /**< 0.1 °C — DET_TEMP_PIN, SiPM carrier sensor. */ + uint16_t mcu_temp_c; /**< Kelvin — MCU internal temperature sensor. */ uint16_t vbias_mv; - uint8_t hv_pot; - uint8_t amp_pot; - uint8_t det_pot; - uint16_t cps; - uint32_t cp10s; + potentiometers_t pots; + uint16_t cps; /**< counts per second = CPM / 60. */ + uint32_t cp10s; /**< counts in the last 10 s window. */ uint32_t total_counts; + uint8_t flags; /**< bit 0: power on ok, bit 1: sensing enabled, bit 2: drift compensation enabled, bit 3: reserved. */ } __attribute__((packed)) protocol_get_telemetry_response_t; -void handle_get_telemetry_command(void *payload, uint16_t length); - -/* - * @brief Flush counters command - * @details Resets total counts and 10-second counts to zero - */ -#define CMD_FLUSH_COUNTERS 0x7401 -void handle_flush_counters_command(void *payload, uint16_t length); - -/* - * @brief Flush spectrum command - * @details Clears all channel data and resets total counts to zero - */ -#define CMD_FLUSH_SPECTRUM 0x7402 -void handle_flush_spectrum_command(void *payload, uint16_t length); - -/* - * @brief Turn on the spectrometer circutry and enable fast ADC mode for radiation detection - */ -#define CMD_RSENSE_ENABLE 0x7500 -void handle_rsense_enable_command(void *payload, uint16_t length); - -/* - * @brief Turn off the spectrometer circutry and disable fast ADC mode for radiation detection - */ -#define CMD_RSENSE_DISABLE 0x7501 -void handle_rsense_disable_command(void *payload, uint16_t length); - -/* - * @brief Freeze spectrum data for reading - */ -#define CMD_SPECTRUM_FREEZE 0x7502 -void handle_spectrum_freeze_command(void *payload, uint16_t length); - -/* - * @brief Unfreeze spectrum data after reading - */ -#define CMD_SPECTRUM_UNFREEZE 0x7503 -void handle_spectrum_unfreeze_command(void *payload, uint16_t length); - -/* -* @brief Get counts since last request -*/ -#define CMD_GET_COUNTS 0x7504 +/** @brief Response payload for #CMD_GET_COUNTS. */ typedef struct protocol_get_counts_response_t { - uint32_t counts; + uint32_t counts; } __attribute__((packed)) protocol_get_counts_response_t; -void handle_get_counts_command(void *payload, uint16_t length); +/** @brief Command payload for #CMD_READ_SPECTRUM_DATA. */ +typedef struct protocol_read_spectrum_chunk_handler_t { + uint16_t offset; /* byte offset into spectrum data */ + uint16_t length; /* number of bytes to read */ +} __attribute__((packed)) protocol_read_spectrum_chunk_handler_t; -/* - * @brief Read spectrum data (arbitrary chunking for large data) +/** + * @brief Response header for #CMD_READ_SPECTRUM_DATA. + * Followed on the wire by @c length bytes of raw spectrum data. */ -#define CMD_READ_SPECTRUM_DATA 0x7505 -typedef struct protocol_read_spectrum_chunk_command_t { - uint16_t offset; - uint16_t length; -} __attribute__((packed)) protocol_read_spectrum_chunk_command_t; - typedef struct protocol_read_spectrum_chunk_response_t { - uint16_t offset; - uint16_t length; - // Followed by 'length' bytes of spectrum data + uint16_t offset; + uint16_t length; } __attribute__((packed)) protocol_read_spectrum_chunk_response_t; -void handle_read_spectrum_chunk_command(void *payload, uint16_t length); #endif /* PROTOCOL_H */ diff --git a/sbc_fw/rsense.cpp b/sbc_fw/rsense.cpp index 571416b..d739df3 100644 --- a/sbc_fw/rsense.cpp +++ b/sbc_fw/rsense.cpp @@ -18,49 +18,55 @@ #include #include +#include "adc.h" #include "config.h" #include "eeprom.h" #include "iodefs.h" #include "rsense.h" -#include "utils.h" -#include "adc.h" static volatile union __attribute__((packed)) { uint16_t channels_16[RSENSE_CHANNEL_COUNT]; - uint32_t channels_32[RSENSE_CHANNEL_32_COUNT]; } g_detector_counts = {0}; -static uint8_t g_spectrum_mode = 0; // 0 = 16-bit, 1 = 32-bit -static uint8_t g_oversampling_bits = config.flags.adc_oversample_bits; - -static uint16_t g_read_pointer = 0; +static volatile uint32_t g_counts_cps = 0; /* counts per second accumulator, reset by periodic tick */ +static uint32_t g_cps_time = 0; +static uint16_t g_cps_last = 0; static uint32_t g_total_counts = 0; -static uint32_t g_total_time = 0; -static uint32_t g_flush_time = 0; -static volatile uint16_t g_counts_cpm = 0; +static volatile uint32_t g_counts_delta = 0; /* counts since last CMD_GET_COUNTS */ -static uint32_t g_cpm_time = 0; -static uint16_t g_cpm_last = 0; +static struct { + uint8_t index; + uint16_t cps[10]; /* Circular buffer of the last 10 CPS values, updated by periodic tick */ +} g_cp10s_data; -static potentiometer_settings_t g_current_pot_settings; +/* + * Drift compensation state. + * + * Three independent reference temperatures: + * det_ref — DET_TEMP (SiPM carrier), drives pot_hv via temp_drift. + * hv_ref — HV_TEMP (+28V supply), drives pot_hv via vbias_drift. + * amp_ref — AMP_TEMP (amplifier), drives pot_amp and pot_det. + * + * pot_hv adjustment = temp_drift*delta_det + vbias_drift*delta_hv (summed). + */ +static uint32_t g_drift_last_ms = 0; +static int16_t g_drift_ref_det = 0; /* DET_TEMP reference in 0.1 °C */ +static int16_t g_drift_ref_hv = 0; /* HV_TEMP reference in 0.1 °C */ +static int16_t g_drift_ref_amp = 0; /* AMP_TEMP reference in 0.1 °C */ +static bool g_drift_ref_valid = false; +bool g_rsense_enabled = false; /* true when HV/AMP are powered on */ -static void apply_potentiometer_settings(const potentiometer_settings_t *settings); +static void apply_potentiometer_settings(void); static void initialize_event_system(void); -static inline void handle_analog_read_interrupt(void); -static void clear_detector_counters(void); -static void clear_channel_data(void); -static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det); -static void send_temperature_data(void); +static inline void analog_read_interrupt(void); +static void rsense_drift_compensation_tick(void); void rsense_init(void) { - eeprom_load_pot_settings(&g_current_pot_settings); - pinMode(HV_EN, OUTPUT); pinMode(DET_EN, OUTPUT); pinMode(DET_RST, OUTPUT); - // Disable HV and Detector on startup digitalWrite(HV_EN, LOW); digitalWrite(DET_EN, LOW); @@ -72,342 +78,263 @@ void rsense_init(void) digitalWrite(DET_CS, HIGH); digitalWrite(AMP_CS, HIGH); - apply_potentiometer_settings(&g_current_pot_settings); + apply_potentiometer_settings(); pinMode(DET_TRIG_PIN, INPUT); initialize_event_system(); } -/** - * @brief Initialize event system for radiation detection - */ static void initialize_event_system(void) { - // Enable event on DET_TRIG_PIN Event2.set_generator(event::gen2::pin_pc7); Event2.set_user(event::user::adc0_start); - - // Pin events are async, level; convert to sync, edge via CCL - // Logic0.enable = true; - // Logic0.input0 = logic::in::event_a; - // Logic0.input1 = logic::in::masked; - // Logic0.input2 = logic::in::masked; - // Logic0.output = logic::out::enable; - // Logic0.output_swap = logic::out::no_swap; - // Logic0.filter = logic::filter::synchronizer; - // Logic0.edgedetect = logic::edgedetect::enable; - // Logic0.clocksource = logic::clocksource::clk_per; - // Logic0.sequencer = logic::sequencer::disable; - // Logic0.truth = 0b00000010; // A and not previous A - // Logic0.init(); - - // Route CCL output to ADC - // Event0.set_generator(event::gen::ccl0_out); - // Event0.set_user(event::user::adc0_start); - - //Event0.start(); Event2.start(); } -/** - * @brief Handle ADC conversion results for radiation detection - */ -static inline void handle_analog_read_interrupt(void) +static inline void analog_read_interrupt(void) { digitalWriteFast(DET_RST, HIGH); - uint16_t channel = ADC0.RES; // read result to clear flag + uint16_t channel = ADC0.RES; digitalWriteFast(STATUS_LED, LOW); - if (g_spectrum_mode == 0) { - // 16-bit spectrum mode - // 2 extra bits from oversampling + averaging - // shift down to equiv. 11 bits - #if ADC_OVERSAMPLING_BITS == 1 - channel = channel >> 1; - #elif ADC_OVERSAMPLING_BITS == 2 - channel = channel >> (ADC_OVERSAMPLING_BITS + 1); - #elif ADC_OVERSAMPLING_BITS == 3 - channel = channel >> (ADC_OVERSAMPLING_BITS + 2); - #else - #error "Unsupported ADC oversampling setting" - #endif - - g_detector_counts.channels_16[channel & RSENSE_CHANNEL_16_MASK]++; - } else { - // 32-bit spectrum mode - // 2 extra bits from oversampling + averaging x2 - // shift down to equiv. 10 bits - #if ADC_OVERSAMPLING_BITS == 1 - channel = channel >> 2; - #elif ADC_OVERSAMPLING_BITS == 2 - channel = channel >> (ADC_OVERSAMPLING_BITS + 2); - #elif ADC_OVERSAMPLING_BITS == 3 - channel = channel >> (ADC_OVERSAMPLING_BITS + 3); - #else - #error "Unsupported ADC oversampling setting" - #endif - - g_detector_counts.channels_32[channel & RSENSE_CHANNEL_32_MASK]++; - } + channel = channel >> (ADC_OVERSAMPLING_BITS + 2); + g_detector_counts.channels_16[channel & RSENSE_CHANNEL_MASK]++; g_total_counts++; - g_counts_cpm++; + g_counts_cps++; + g_counts_delta++; digitalWriteFast(DET_RST, LOW); } -/** - * @brief Clear all detector counters - */ -static void clear_detector_counters(void) -{ - ATOMIC_BLOCK(ATOMIC_RESTORESTATE) - { - g_total_counts = 0; - g_total_time = 0; - g_flush_time = millis(); - g_counts_cpm = 0; - g_cpm_time = millis(); - } -} +ISR(ADC0_RESRDY_vect) { analog_read_interrupt(); } /** - * @brief Clear channel data + * @brief Clamp a signed value into the unsigned interval [lo, hi]. */ -static void clear_channel_data(void) +static uint8_t clamp_u8(int16_t v, uint8_t lo, uint8_t hi) { - for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) { - g_detector_counts.channels_16[i] = 0; - } + if (v < (int16_t)lo) + return lo; + if (v > (int16_t)hi) + return hi; + return (uint8_t)v; } -/** - * @brief Apply potentiometer settings to hardware - * @param settings Pointer to potentiometer settings - */ -static void apply_potentiometer_settings(const potentiometer_settings_t *settings) +static void rsense_drift_compensation_tick(void) { - if (settings == nullptr) + if (!config.flags.fields.temperature_drift_compensation || !g_rsense_enabled) return; - // HV potentiometer setup - digitalWrite(HV_CS, LOW); - SPI.transfer(settings->hv_pot); - digitalWrite(HV_CS, HIGH); - delay(1); + uint32_t period = config.temp_drift_compensation.drift_period_ms; + if (period == 0) + period = + 300000; /* Default to 5 minutes if not set, to avoid excessive compensation when misconfigured. */ - // SiPM Pre-amp gain potentiometer setup - digitalWrite(AMP_CS, LOW); - SPI.transfer(settings->amp_pot); - digitalWrite(AMP_CS, HIGH); - delay(1); + if (millis() - g_drift_last_ms < period) + return; - // Detection threshold potentiometer setup - digitalWrite(DET_CS, LOW); - SPI.transfer(settings->det_pot); - digitalWrite(DET_CS, HIGH); - delay(1); -} + /* Read all three temperature sensors (each ~10 ms due to oversampling) */ + int16_t det_temp = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16)); + int16_t hv_temp = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16)); + int16_t amp_temp = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16)); -/** - * @brief Set new potentiometer values - * @param hv High voltage potentiometer value (0-255) - * @param amp Amplifier potentiometer value (0-255) - * @param det Detector potentiometer value (0-255) - */ -static void set_potentiometer_values(uint8_t hv, uint8_t amp, uint8_t det) -{ - digitalWrite(DET_EN, LOW); - digitalWrite(HV_EN, LOW); - delay(5); - - // Update current settings and save to EEPROM - g_current_pot_settings.hv_pot = hv; - g_current_pot_settings.amp_pot = amp; - g_current_pot_settings.det_pot = det; - - apply_potentiometer_settings(&g_current_pot_settings); - - eeprom_save_pot_settings(&g_current_pot_settings); -} - -/*******************************************************************************/ -/*******************************************************************************/ -/* Command handlers */ -/*******************************************************************************/ -/*******************************************************************************/ - -void rsense_cmd_telemetry(void) -{ - adc_restore_default(); - - // Send total counts as 4 bytes - Serial.write(g_total_counts & 0xFF); - Serial.write((g_total_counts >> 8) & 0xFF); - Serial.write((g_total_counts >> 16) & 0xFF); - Serial.write((g_total_counts >> 24) & 0xFF); - - // 2 bytes of voltage in mV - uint16_t voltage_raw = adc_read_oversampled(V28V0_FB_PIN, 16); - uint16_t voltage_mv = adc_to_voltage_mv(voltage_raw); - Serial.write(voltage_mv & 0xFF); - Serial.write(voltage_mv >> 8); - - // 6 bytes of temperature data - send_temperature_data(); - - // 3 bytes of potentiometer settings - Serial.write(g_current_pot_settings.hv_pot); - Serial.write(g_current_pot_settings.amp_pot); - Serial.write(g_current_pot_settings.det_pot); - - Serial.flush(); - SERIAL_BUFFER_CLEAR(); - adc_enable_fast(); -} - -/** - * @brief Send temperature sensor data - */ -static void send_temperature_data(void) -{ - int16_t hv_temp_c = adc_to_temperature_c(adc_read_oversampled(HV_TEMP_PIN, 16)); - int16_t amp_temp_c = adc_to_temperature_c(adc_read_oversampled(AMP_TEMP_PIN, 16)); - int16_t det_temp_c = adc_to_temperature_c(adc_read_oversampled(DET_TEMP_PIN, 16)); - - Serial.write(hv_temp_c & 0xFF); - Serial.write(hv_temp_c >> 8); - Serial.write(amp_temp_c & 0xFF); - Serial.write(amp_temp_c >> 8); - Serial.write(det_temp_c & 0xFF); - Serial.write(det_temp_c >> 8); -} - -/* TODO: CRC for parameters */ -void rsense_cmd_set_potentiometers(void) -{ - // Wait for HV, AMP, DET potentiometer values - while (Serial.available() < 3) - ; - - uint8_t hv = Serial.read(); - uint8_t amp = Serial.read(); - uint8_t det = Serial.read(); - - set_potentiometer_values(hv, amp, det); - - SERIAL_SEND_OK(); -} - -void rsense_cmd_enable(void) -{ - digitalWrite(HV_EN, HIGH); - digitalWrite(DET_EN, HIGH); - - adc_enable_fast(); - - SERIAL_SEND_OK(); -} - -void rsense_cmd_disable(void) -{ - digitalWrite(DET_EN, LOW); - digitalWrite(HV_EN, LOW); - - adc_restore_default(); - - SERIAL_SEND_OK(); -} - -void rsense_cmd_flush(void) -{ - clear_detector_counters(); - clear_channel_data(); - SERIAL_SEND_OK(); -} - -void rsense_cmd_dump_channels(void) -{ - adc_restore_default(); - - uint16_t read_from = 0; - - // Wait for 2 bytes specifying read position - while (Serial.available() < 2) - ; - read_from = Serial.read(); - read_from |= (uint16_t)Serial.read() << 8; - - // Send 2 bytes of read position - Serial.write(g_read_pointer & 0xFF); - Serial.write(g_read_pointer >> 8); - - g_read_pointer = read_from; - if (g_read_pointer >= (RSENSE_CHANNEL_32_COUNT - 32)) { - g_read_pointer = RSENSE_CHANNEL_32_COUNT - 32; + if (!g_drift_ref_valid) { + g_drift_ref_det = det_temp; + g_drift_ref_hv = hv_temp; + g_drift_ref_amp = amp_temp; + g_drift_ref_valid = true; + g_drift_last_ms = millis(); + return; } - // Send 2 bytes of CRC16 checksum for the 32 channels - uint16_t crc16 = calculate_crc16_xmodem((uint8_t *)(&g_detector_counts.channels_32[g_read_pointer]), 32 * 4); - Serial.write(crc16 & 0xFF); - Serial.write(crc16 >> 8); + /* Deltas in 0.1°C units */ + int16_t delta_det = det_temp - g_drift_ref_det; + int16_t delta_hv = hv_temp - g_drift_ref_hv; + int16_t delta_amp = amp_temp - g_drift_ref_amp; - // Send 32 channel values (128 bytes total) - for (uint16_t i = 0; i < 32 && g_read_pointer < RSENSE_CHANNEL_32_COUNT; i++, g_read_pointer++) { - uint32_t channel_value = g_detector_counts.channels_32[g_read_pointer]; - Serial.write(channel_value & 0xFF); - Serial.write((channel_value >> 8) & 0xFF); - Serial.write((channel_value >> 16) & 0xFF); - Serial.write((channel_value >> 24) & 0xFF); + /* Drift coefficients in pot_units per °C, scaled by 256. + * adj = coeff * delta_0.1C / 10 / 256 = coeff * delta_0.1C / 2560 */ + bool needs_update = false; - Serial.flush(); - SERIAL_BUFFER_CLEAR(); - delay(1); + int16_t new_hv = config.pots.pot_hv; + int16_t new_amp = config.pots.pot_amp; + int16_t new_det = config.pots.pot_det; + + const temp_drift_compensation_t *dc = &config.temp_drift_compensation; + + if (dc->pot_hv_low != dc->pot_hv_high) { + /* pot_hv: SiPM carrier (DET_TEMP) + 28V supply (HV_TEMP) contributions. */ + int16_t adj = + (int16_t)(((int32_t)dc->temp_drift * delta_det + (int32_t)dc->vbias_drift * delta_hv) / 2560L); + if (adj != 0) { + new_hv = (int16_t)config.pots.pot_hv + adj; + needs_update = true; + } } - Serial.flush(); - SERIAL_BUFFER_CLEAR(); - adc_enable_fast(); -} - -void rsense_cmd_get_cpm(void) -{ - Serial.write(g_cpm_last & 0xFF); - Serial.write(g_cpm_last >> 8); - Serial.flush(); - SERIAL_BUFFER_CLEAR(); -} - -void rsense_cmd_set_configuration(void) -{ - // Wait for 1 byte specifying mode - while (Serial.available() < 8) - ; - uint8_t mode = Serial.read(); - - if (mode <= 1) { - g_spectrum_mode = mode; - clear_channel_data(); - SERIAL_SEND_OK(); + if (dc->pot_amp_low != dc->pot_amp_high) { + int16_t adj = (int16_t)(((int32_t)dc->gain_drift * delta_amp) / 2560L); + if (adj != 0) { + new_amp = (int16_t)config.pots.pot_amp + adj; + needs_update = true; + } } - // 7 bytes reserved for future use + if (dc->pot_det_low != dc->pot_det_high) { + int16_t adj = (int16_t)(((int32_t)dc->threshold_drift * delta_amp) / 2560L); + if (adj != 0) { + new_det = (int16_t)config.pots.pot_det + adj; + needs_update = true; + } + } - Serial.flush(); - SERIAL_BUFFER_CLEAR(); + if (!needs_update) { + g_drift_last_ms = millis(); + return; + } + + config.pots.pot_hv = clamp_u8(new_hv, dc->pot_hv_low, dc->pot_hv_high); + config.pots.pot_amp = clamp_u8(new_amp, dc->pot_amp_low, dc->pot_amp_high); + config.pots.pot_det = clamp_u8(new_det, dc->pot_det_low, dc->pot_det_high); + + rsense_apply_potentiometers(); + + g_drift_ref_det = det_temp; + g_drift_ref_hv = hv_temp; + g_drift_ref_amp = amp_temp; + g_drift_last_ms = millis(); } void rsense_periodic(void) { - if (millis() - g_cpm_time > CPM_UPDATE_PERIOD) { - uint32_t elapsed_time = millis() - g_cpm_time; - g_cpm_last = (uint16_t)(((uint32_t)g_counts_cpm * 60UL * 1000UL) / elapsed_time); - g_counts_cpm = 0; - g_cpm_time = millis(); + if (millis() - g_cps_time > RSENSE_UPDATE_PERIOD) { + uint32_t elapsed_time = millis() - g_cps_time; + g_cps_last = (uint16_t)(((uint32_t)g_counts_cps * 1000UL) / elapsed_time); + g_counts_cps = 0; + g_cps_time = millis(); + + /* Update 10-second CPS circular buffer for moving average */ + g_cp10s_data.cps[g_cp10s_data.index] = g_cps_last; + g_cp10s_data.index = (g_cp10s_data.index + 1) % 10; + + rsense_drift_compensation_tick(); } } /** - * @brief ADC conversion complete interrupt handler + * @brief Clock @c value out to one AD5160 selected by @p cs_pin. */ -ISR(ADC0_RESRDY_vect) { handle_analog_read_interrupt(); } +static inline void write_pot(uint8_t cs_pin, uint8_t value) +{ + digitalWrite(cs_pin, LOW); + SPI.transfer(value); + digitalWrite(cs_pin, HIGH); + delay(1); +} + +/** + * @brief Push the current @c config.pots values to the AD5160 digital + * potentiometers over SPI. Does not touch power-enable lines; + * see #rsense_apply_potentiometers for the safe re-sequencing version. + */ +static void apply_potentiometer_settings(void) +{ + write_pot(HV_CS, config.pots.pot_hv); + write_pot(AMP_CS, config.pots.pot_amp); + write_pot(DET_CS, config.pots.pot_det); +} + +void rsense_apply_potentiometers(void) +{ + if (!g_rsense_enabled) { + /* System is off — just write SPI registers without power cycling */ + apply_potentiometer_settings(); + return; + } + + /* Power sequence: freeze → disable amp → disable HV → set pots → enable HV → enable amp → unfreeze */ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL &= ~ADC_RESRDY_bm; } + digitalWrite(DET_EN, LOW); + digitalWrite(HV_EN, LOW); + delay(10); + apply_potentiometer_settings(); + delay(10); + digitalWrite(HV_EN, HIGH); + delay(100); + digitalWrite(DET_EN, HIGH); + delay(100); + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ADC0.INTCTRL |= ADC_RESRDY_bm; } +} + +void rsense_enable(void) +{ + g_rsense_enabled = true; + digitalWrite(HV_EN, HIGH); + digitalWrite(DET_EN, HIGH); + adc_enable_fast(); +} + +void rsense_disable(void) +{ + g_rsense_enabled = false; + digitalWrite(DET_EN, LOW); + digitalWrite(HV_EN, LOW); + adc_restore_default(); +} + +void rsense_flush_counters(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + g_total_counts = 0; + g_counts_cps = 0; + g_counts_delta = 0; + g_cps_time = millis(); + } +} + +void rsense_flush_spectrum(void) +{ + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + for (int i = 0; i < RSENSE_CHANNEL_COUNT; i++) { + g_detector_counts.channels_16[i] = 0; + } + } + rsense_flush_counters(); +} + +uint32_t rsense_get_total_counts(void) +{ + uint32_t v; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { v = g_total_counts; } + return v; +} + +uint16_t rsense_get_cps(void) { return g_cps_last; } + +uint32_t rsense_get_cp10s(void) +{ + uint32_t sum = 0; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + for (int i = 0; i < 10; i++) { + sum += g_cp10s_data.cps[i]; + } + } + return sum / 10; +} + +uint32_t rsense_get_counts_since_last(void) +{ + uint32_t v; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) + { + v = g_counts_delta; + g_counts_delta = 0; + } + return v; +} + +const volatile void *rsense_get_spectrum_ptr(void) { return (const volatile void *)&g_detector_counts; } diff --git a/sbc_fw/rsense.h b/sbc_fw/rsense.h index 46f7cbf..54d883f 100644 --- a/sbc_fw/rsense.h +++ b/sbc_fw/rsense.h @@ -1,8 +1,9 @@ /* * @file rsense.h - * @brief Radiation sensor management and command handling + * @brief Radiation sensor subsystem: counters, spectrum, drift compensation, + * and HV/detector power control. * - * Created: 21.09.2025 06:01:56 + * Created: 21.09.2025 * Author: ThePetrovich * * Copyright YKSA - Sakha Aerospace Systems, LLC. @@ -14,58 +15,59 @@ #ifndef RSENSE_H #define RSENSE_H -#include +#include /** - * @brief Initialize radiation sensor subsystem + * @brief Configure GPIO, SPI digital potentiometers, and the event system + * used by the detector trigger pin to start ADC conversions. */ void rsense_init(void); /** - * @brief Send telemetry data via serial - */ -void rsense_cmd_telemetry(void); - -/** - * @brief Dump channel data via serial - */ -void rsense_cmd_dump_channels(void); - -/** - * @brief Flush detector counters - */ -void rsense_cmd_flush(void); - -/** - * @brief Enable radiation detection - */ -void rsense_cmd_enable(void); - -/** - * @brief Disable radiation detection - */ -void rsense_cmd_disable(void); - -/** - * @brief Set potentiometer values - */ -void rsense_cmd_set_potentiometers(void); - -/** - * @brief Send counts per minute data - */ -void rsense_cmd_get_cpm(void); - -/** - * @brief Set spectrum mode (16-bit or 32-bit) - */ -void rsense_cmd_set_configuration(void); - -/** - * @brief Periodic tasks for radiation sensor - * Updates CPM calculation every 10 seconds - * Call from main loop + * @brief Periodic housekeeping: CPM accumulation window and drift + * compensation tick. Should be called from the main loop. */ void rsense_periodic(void); -#endif // RSENSE_H \ No newline at end of file +/** @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 */ diff --git a/sbc_fw/sbc_fw.ino b/sbc_fw/sbc_fw.ino index 87f7478..bc3e670 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,15 +15,126 @@ #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); @@ -38,102 +149,9 @@ void setup() rsense_init(); } -/** - * @brief Initialize SPI interface for potentiometer control - */ -void initialize_spi(void) -{ - SPI.begin(); - SPI.setClockDivider(SPI_CLOCK_DIV128); // 125 kHz @ 16 MHz - SPI.setDataMode(SPI_MODE0); // AD5160 (Rev C.) - SPI.setBitOrder(MSBFIRST); // AD5160 (Rev C.), p. 13, fig. 37 -} - -/** - * @brief Handle incoming serial commands - * @param command Command character received - */ -void handle_serial_command(char command) -{ - switch (command) { - case 'v': // Firmware version - handle_version_command(); - break; - case 'd': // Detect/ping - handle_detect_command(); - break; - case 'l': // Light sensor presence - lsense_cmd_presence(); - break; - case 'r': // Read sensors - lsense_cmd_read(); - rsense_cmd_get_cpm(); - break; - case 'c': // Dump 128 bytes of channel data - rsense_cmd_dump_channels(); - break; - case 'f': // Flush counters - rsense_cmd_flush(); - break; - case 'e': // Enable radiation detection - rsense_cmd_enable(); - break; - case 's': // Disable radiation detection - rsense_cmd_disable(); - break; - case 'p': // Set potentiometers - rsense_cmd_set_potentiometers(); - break; - case 't': // Telemetry - rsense_cmd_telemetry(); - break; - case 'm': // Set spectrum mode (16-bit or 32-bit) - rsense_cmd_set_configuration(); - break; - default: - // Unknown command - ignore - break; - } -} - -/** - * @brief Handle firmware version command - */ -void handle_version_command(void) -{ - Serial.write(FIRMWARE_VERSION_MAJOR); - Serial.write(FIRMWARE_VERSION_MINOR); - Serial.flush(); - SERIAL_BUFFER_CLEAR(); -} - -/** - * @brief Handle detect/ping command - */ -void handle_detect_command(void) { SERIAL_SEND_OK(); } - -/** - * @brief Update status LED (blink) - */ -inline void update_status_led(void) -{ - if (millis() - status_led_last_blink > STATUS_LED_BLINK_PERIOD) { - digitalWrite(STATUS_LED, !digitalRead(STATUS_LED)); - status_led_last_blink = millis(); - } -} - -/** - * @brief Main program loop - */ void loop() { - // Process incoming serial commands - while (Serial.available()) { - char command = Serial.read(); - handle_serial_command(command); - } + protocol_receive(); update_status_led(); - rsense_periodic(); } diff --git a/sbc_fw/utils.cpp b/sbc_fw/utils.cpp deleted file mode 100644 index e75aaaa..0000000 --- a/sbc_fw/utils.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * @file utils.cpp - * @brief Utility functions implementation - * - * Created: 21.09.2025 - * Author: ThePetrovich - * - * Copyright YKSA - Sakha Aerospace Systems, LLC. - * See the LICENSE file for details. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "utils.h" -#include "config.h" -#include - -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; -} \ No newline at end of file diff --git a/sbc_fw/utils.h b/sbc_fw/utils.h deleted file mode 100644 index b1d5a39..0000000 --- a/sbc_fw/utils.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * @file utils.h - * @brief Utility functions and helpers - * - * Created: 21.09.2025 - * Author: ThePetrovich - * - * Copyright YKSA - Sakha Aerospace Systems, LLC. - * See the LICENSE file for details. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef UTILS_H -#define UTILS_H - -#include - -#define SERIAL_BUFFER_CLEAR() \ - do { \ - while (Serial.available()) { \ - Serial.read(); \ - } \ - } while (0) -#define SERIAL_SEND_OK() \ - do { \ - Serial.print("ok\n"); \ - Serial.flush(); \ - SERIAL_BUFFER_CLEAR(); \ - } while (0) - -/** - * @brief Calculate CRC16 using XMODEM polynomial - * @param data Pointer to data buffer - * @param len Length of data in bytes - * @return CRC16 checksum - */ -uint16_t calculate_crc16_xmodem(uint8_t *data, uint16_t len); - -#endif // UTILS_H