/******************************************************************************* * @file dr_adc121s051.c * @brief ADC121S051 12-bit ADC Driver for nRF52840 * For 1.2MHz Piezo Echo Envelope Detection * @author Charles KWON * @date 2025-12-15 * * @details Software SPI (bit-bang) implementation for ADC121S051. * Optimized for reading envelope-detected echo signals. * * ADC121S051 Serial Interface: * * CS ────────┐ ┌──────── * (HIGH) └──────────────────────────────┘ (HIGH) * * SCLK ────────┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ──────── * (HIGH) └──┘ └──┘ └── ... ┘ └──┘ └──┘ (HIGH) * 1 2 3 14 15 16 * * SDATA -------...------ * |<- 3 zeros ->|<-- 12 data bits -->| * * - CS idle HIGH, SCLK idle HIGH * - CS falling edge: starts conversion, samples VIN * - Data clocked out on SCLK falling edge * - MSB first, straight binary output * * @note WARNING: Never hardcode pin numbers! * Hardcoding may save a developer's time momentarily, * but it will also shorten their lifespan. ******************************************************************************/ #include "dr_adc121s051.h" #include "dr_util.h" #include "nrf_gpio.h" #include "nrf_delay.h" #include /* memset */ /* BLE connection state from main.c */ extern volatile bool data_tx_in_progress; extern volatile bool ble_connection_st; /*============================================================================== * EXTERNAL PIEZO BURST FUNCTIONS *============================================================================*/ extern void dr_piezo_burst_sw(uint8_t cycles); extern void dr_piezo_burst_sw_18mhz(uint8_t cycles); extern void dr_piezo_burst_sw_20mhz(uint8_t cycles); extern void dr_piezo_burst_sw_17mhz(uint8_t cycles); extern void dr_piezo_burst_sw_22mhz(uint8_t cycles); extern void dr_piezo_burst_sw_19mhz(uint8_t cycles); /*============================================================================== * DEBUG CONFIGURATION *============================================================================*/ #ifdef DEBUG_ADC #include "nrf_log.h" #define ADC_LOG(...) NRF_LOG_INFO(__VA_ARGS__) #else #define ADC_LOG(...) #endif /*============================================================================== * PRIVATE DEFINES *============================================================================*/ /* Extract pin number from NRF_GPIO_PIN_MAP (lower 5 bits) */ #define DR_PIN_NUM(pin) ((pin) & 0x1F) #define DR_PIN_PORT(pin) (((pin) >> 5) & 0x01) /* SPI bit masks for direct register access */ #define CS_MASK (1UL << DR_PIN_NUM(DR_ADC_PIN_CS)) #define SCLK_MASK (1UL << DR_PIN_NUM(DR_ADC_PIN_SCLK)) #define SDATA_MASK (1UL << DR_PIN_NUM(DR_ADC_PIN_SDATA)) /* Get port register based on pin */ #define DR_GET_PORT(pin) (DR_PIN_PORT(pin) ? NRF_P1 : NRF_P0) /* ADC timing constants */ #define SCLK_CYCLES 16 /**< Clock cycles per conversion */ /*============================================================================== * PRIVATE VARIABLES *============================================================================*/ static bool m_initialized = false; static uint32_t m_vref_mv = DR_ADC_VREF_MV; /* Echo capture buffer (module-level for external access) */ static uint16_t m_echo_buffer[DR_ADC_ECHO_SAMPLES_MAX]; /* Port registers for fast access (currently unused - using direct NRF_P0) */ /* static NRF_GPIO_Type *m_port_cs; */ /* static NRF_GPIO_Type *m_port_sclk; */ /* static NRF_GPIO_Type *m_port_sdata; */ /*============================================================================== * PRIVATE FUNCTION PROTOTYPES *============================================================================*/ static void adc_gpio_init(void); /* adc_transfer is always_inline, no prototype needed */ /*============================================================================== * INLINE GPIO FUNCTIONS (for speed) *============================================================================*/ /* cs_low() unused - using ADC_TRANSFER_16BITS macro with direct register access static inline void cs_low(void) { NRF_P0->OUTCLR = CS_MASK; } */ static inline void cs_high(void) { NRF_P0->OUTSET = CS_MASK; } /* sclk_low() unused - using direct register access in macro static inline void sclk_low(void) { NRF_P0->OUTCLR = SCLK_MASK; } */ static inline void sclk_high(void) { NRF_P0->OUTSET = SCLK_MASK; } /* read_sdata() unused - using direct register access in macro static inline uint32_t read_sdata(void) { return (NRF_P0->IN & SDATA_MASK) ? 1 : 0; } */ /*============================================================================== * PRIVATE FUNCTIONS *============================================================================*/ /** * @brief Initialize GPIO pins for ADC */ static void adc_gpio_init(void) { /* Port pointers removed - using direct NRF_P0 access for speed */ /* CS: Output, HIGH (idle) */ nrf_gpio_cfg_output(DR_ADC_PIN_CS); nrf_gpio_pin_set(DR_ADC_PIN_CS); /* SCLK: Output, HIGH (idle) - per Figure 4 */ nrf_gpio_cfg_output(DR_ADC_PIN_SCLK); nrf_gpio_pin_set(DR_ADC_PIN_SCLK); /* SDATA: Input (ADC has push-pull output) */ nrf_gpio_cfg_input(DR_ADC_PIN_SDATA, NRF_GPIO_PIN_NOPULL); ADC_LOG("ADC GPIO: CS=P%d.%02d SCLK=P%d.%02d SDATA=P%d.%02d", DR_PIN_PORT(DR_ADC_PIN_CS), DR_PIN_NUM(DR_ADC_PIN_CS), DR_PIN_PORT(DR_ADC_PIN_SCLK), DR_PIN_NUM(DR_ADC_PIN_SCLK), DR_PIN_PORT(DR_ADC_PIN_SDATA), DR_PIN_NUM(DR_ADC_PIN_SDATA)); } /** * @brief Perform one ADC conversion (16 clock cycles) * @return Raw 16-bit data (includes leading zeros) * * @details Timing optimized for maximum speed (~500ksps). * At 64MHz CPU: 1 NOP = 15.625ns * * ADC specs (@ 3.3V): * - tACC (data access time): 40ns max * - tH (hold time): 7ns min * - fSCLK: 4-10 MHz * * MUST be inlined to eliminate function call overhead for consistent timing. */ /* * ADC_TRANSFER_BIT macro - one SCLK cycle for bit-bang SPI * Captures data on falling edge, processes on rising edge * * Target: 10MHz (100ns cycle) = 50ns LOW + 50ns HIGH * ADC121S051 timing requirements: * - tCL (SCLK low time): 40ns min * - tCH (SCLK high time): 40ns min * - tACC (data access): 40ns max (data valid after SCLK falling) * * LOW phase: OUTCLR(~15ns) + NOP(~16ns) + IN read(~30ns) = ~61ns (meets tCL, tACC) * HIGH phase: OUTSET(~15ns) + shift/OR(~40ns) = ~55ns (meets tCH) * Total: ~116ns (~8.6MHz) * * Note: Removing HIGH phase NOP to reduce cycle time */ #define ADC_TRANSFER_BIT(data, in_reg) \ do { \ NRF_P0->OUTCLR = SCLK_MASK; \ __NOP(); \ in_reg = NRF_P0->IN; \ NRF_P0->OUTSET = SCLK_MASK; \ data = (data << 1) | ((in_reg & SDATA_MASK) ? 1 : 0); \ } while(0) #define ADC_TRANSFER_BIT_LAST(data, in_reg) \ do { \ NRF_P0->OUTCLR = SCLK_MASK; \ __NOP(); \ in_reg = NRF_P0->IN; \ NRF_P0->OUTSET = SCLK_MASK; \ data = (data << 1) | ((in_reg & SDATA_MASK) ? 1 : 0); \ } while(0) /* * ADC_TRANSFER_16BITS macro - Complete 16-bit transfer * Use this instead of adc_transfer() function for consistent first bit timing. * Variables data and in_reg MUST be declared and initialized before calling. * * IMPORTANT: Call ADC_PREHEAT(data, in_reg) before cs_low() to warm up pipeline. */ #define ADC_TRANSFER_16BITS(data, in_reg) \ do { \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 15 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 14 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 13 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 12 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 11 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 10 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 9 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 8 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 7 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 6 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 5 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 4 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 3 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 2 */ \ ADC_TRANSFER_BIT(data, in_reg); /* Bit 1 */ \ ADC_TRANSFER_BIT_LAST(data, in_reg); /* Bit 0 */ \ NRF_P0->OUTSET = CS_MASK; \ __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); \ __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); \ __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); \ __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); \ } while(0) /* * ADC_PREHEAT - Warm up instruction pipeline before timing-critical section * This prevents the first SCLK cycle from being longer than others. * * CRITICAL: Must perform ACTUAL SCLK toggle to prime the GPIO write path! * CS is still HIGH (idle), so this dummy toggle doesn't affect ADC. * * Sequence: * 1. Dummy SCLK LOW/HIGH cycle (exact same code as ADC_TRANSFER_BIT) * 2. SCLK remains HIGH (ready for cs_low) */ #define ADC_PREHEAT(data, in_reg) \ do { \ /* Dummy SCLK cycle - CS still HIGH, so ADC ignores this */ \ /* Must match ADC_TRANSFER_BIT timing exactly */ \ NRF_P0->OUTCLR = SCLK_MASK; \ __NOP(); \ in_reg = NRF_P0->IN; \ NRF_P0->OUTSET = SCLK_MASK; \ data = (data << 1) | ((in_reg & SDATA_MASK) ? 1 : 0); \ /* Second dummy cycle for better pipeline warm-up */ \ NRF_P0->OUTCLR = SCLK_MASK; \ __NOP(); \ in_reg = NRF_P0->IN; \ NRF_P0->OUTSET = SCLK_MASK; \ data = (data << 1) | ((in_reg & SDATA_MASK) ? 1 : 0); \ /* Reset data after priming */ \ data = 0; \ } while(0) /* adc_transfer() function replaced by ADC_TRANSFER_16BITS macro for speed * Kept here for reference but commented out to eliminate warning */ #if 0 /** * @brief Perform one ADC conversion (16 clock cycles) * @param data Pre-initialized to 0 by caller * @param in_reg Pre-declared register variable by caller * @return Raw 16-bit data (includes leading zeros) */ __attribute__((always_inline)) static inline uint16_t adc_transfer(uint16_t data, register uint32_t in_reg) { /* Implementation moved to ADC_TRANSFER_16BITS macro */ return data; } #endif /*============================================================================== * PUBLIC FUNCTIONS - INITIALIZATION *============================================================================*/ dr_adc_err_t dr_adc_init(void) { ADC_LOG("ADC121S051 init..."); /* Initialize GPIO */ adc_gpio_init(); /* Ensure idle state (SCLK idle HIGH per Figure 4) */ cs_high(); sclk_high(); /* Wait for power stabilization */ nrf_delay_us(10); /* Dummy read to wake up and clear stale data */ { uint16_t dummy_data = 0; register uint32_t dummy_reg = 0; ADC_PREHEAT(dummy_data, dummy_reg); NRF_P0->OUTCLR = CS_MASK; __NOP(); __NOP(); __NOP(); __NOP(); /* tSU: CS setup time */ ADC_TRANSFER_16BITS(dummy_data, dummy_reg); } /* Quiet time between conversions */ __NOP(); __NOP(); __NOP(); __NOP(); m_initialized = true; ADC_LOG("ADC121S051 ready (VREF=%dmV)", m_vref_mv); return DR_ADC_OK; } void dr_adc_uninit(void) { if (!m_initialized) return; cs_high(); sclk_high(); /* Idle HIGH per Figure 4 */ nrf_gpio_cfg_default(DR_ADC_PIN_CS); nrf_gpio_cfg_default(DR_ADC_PIN_SCLK); nrf_gpio_cfg_default(DR_ADC_PIN_SDATA); m_initialized = false; ADC_LOG("ADC121S051 uninitialized"); } bool dr_adc_is_initialized(void) { return m_initialized; } /*============================================================================== * PUBLIC FUNCTIONS - BASIC READ *============================================================================*/ dr_adc_err_t dr_adc_read_raw(uint16_t *raw_value) { if (!m_initialized) return DR_ADC_ERR_NOT_INIT; if (raw_value == NULL) return DR_ADC_ERR_INVALID_PARAM; uint16_t data = 0; register uint32_t in_reg = 0; /* Critical section for precise timing */ __disable_irq(); /* Preheat pipeline before timing-critical section */ ADC_PREHEAT(data, in_reg); /* CS LOW - starts conversion, samples VIN on this edge */ NRF_P0->OUTCLR = CS_MASK; __NOP(); /* Match timing with other SCLK cycles */ /* Transfer 16 bits + CS HIGH (macro for consistent timing) */ ADC_TRANSFER_16BITS(data, in_reg); __enable_irq(); /* * Extract 12-bit data from 16-bit frame: * Bit 15-13: Leading zeros (Z2, Z1, Z0) - must be 0 * Bit 12-1: Data bits (D11-D0) * Bit 0: Don't care (trailing) * * Result = (data >> 1) & 0x0FFF */ /* Verify leading zeros (upper 3 bits should be 0) */ if ((data & 0xE000) != 0) { ADC_LOG("ADC frame error: leading zeros invalid (0x%04X)", data); } *raw_value = (data >> 1) & 0x0FFF; return DR_ADC_OK; } dr_adc_err_t dr_adc_read(dr_adc_result_t *result) { if (!m_initialized) return DR_ADC_ERR_NOT_INIT; if (result == NULL) return DR_ADC_ERR_INVALID_PARAM; dr_adc_err_t err = dr_adc_read_raw(&result->raw); if (err != DR_ADC_OK) return err; result->voltage_mv = dr_adc_raw_to_mv(result->raw); return DR_ADC_OK; } dr_adc_err_t dr_adc_read_averaged(dr_adc_result_t *result, uint16_t num_samples) { if (!m_initialized) return DR_ADC_ERR_NOT_INIT; if (result == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; uint32_t sum = 0; uint16_t raw; for (uint16_t i = 0; i < num_samples; i++) { dr_adc_err_t err = dr_adc_read_raw(&raw); if (err != DR_ADC_OK) return err; sum += raw; } result->raw = (uint16_t)(sum / num_samples); result->voltage_mv = dr_adc_raw_to_mv(result->raw); return DR_ADC_OK; } /*============================================================================== * PUBLIC FUNCTIONS - ECHO DETECTION *============================================================================*/ dr_adc_err_t dr_adc_capture_echo(uint16_t *buffer, uint16_t num_samples) { if (!m_initialized) return DR_ADC_ERR_NOT_INIT; if (buffer == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; uint16_t data = 0; register uint32_t in_reg = 0; /* Disable interrupts for continuous high-speed capture */ __disable_irq(); /* Preheat pipeline once before the loop */ ADC_PREHEAT(data, in_reg); for (uint16_t i = 0; i < num_samples; i++) { data = 0; /* Reset for each sample */ /* CS LOW - starts conversion, samples VIN */ NRF_P0->OUTCLR = CS_MASK; __NOP(); /* Match timing with other SCLK cycles */ /* Transfer 16 bits + CS HIGH (macro for consistent timing) */ ADC_TRANSFER_16BITS(data, in_reg); /* Store 12-bit value */ buffer[i] = (data >> 1) & 0x0FFF; } __enable_irq(); return DR_ADC_OK; } dr_adc_err_t dr_adc_measure_echo(dr_adc_echo_t *echo, const dr_adc_echo_config_t *config) { if (!m_initialized) return DR_ADC_ERR_NOT_INIT; if (echo == NULL) return DR_ADC_ERR_INVALID_PARAM; /* Use default config if not provided */ dr_adc_echo_config_t cfg; if (config != NULL) { cfg = *config; } else { cfg.num_samples = DR_ADC_ECHO_SAMPLES_DEFAULT; cfg.threshold_raw = 100; /* ~80mV threshold */ cfg.delay_us = 0; } /* Validate config */ if (cfg.num_samples == 0 || cfg.num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; /* Use module-level buffer (16KB too large for stack) */ /* Optional delay before capture */ if (cfg.delay_us > 0) { nrf_delay_us(cfg.delay_us); } /* Capture echo samples into module-level buffer */ dr_adc_err_t err = dr_adc_capture_echo(m_echo_buffer, cfg.num_samples); if (err != DR_ADC_OK) return err; /* Analyze captured data */ return dr_adc_analyze_echo(m_echo_buffer, cfg.num_samples, echo, cfg.threshold_raw); } dr_adc_err_t dr_adc_burst_and_capture(uint8_t cycles, uint16_t delay_us, uint16_t num_samples, dr_adc_echo_t *echo) { (void)cycles; /* Not used - ADC test only */ if (echo == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; /* Initialize echo structure */ echo->peak_raw = 0; echo->peak_mv = 0; echo->peak_index = 0; echo->peak_time_us = 0; echo->baseline_raw = 0; echo->num_samples = 0; /* Initialize ADC if not ready */ if (!m_initialized) { dr_adc_err_t init_err = dr_adc_init(); if (init_err != DR_ADC_OK) return init_err; } /* Settling time for ADC power */ nrf_delay_us(100); /* Optional delay before capture */ if (delay_us > 0) { nrf_delay_us(delay_us); } /* Capture ADC samples into module-level buffer */ dr_adc_err_t err = dr_adc_capture_echo(m_echo_buffer, num_samples); if (err != DR_ADC_OK) return err; /* Analyze captured data */ return dr_adc_analyze_echo(m_echo_buffer, num_samples, echo, 100); } const uint16_t* dr_adc_get_echo_buffer(void) { return m_echo_buffer; } dr_adc_err_t dr_adc_analyze_echo(const uint16_t *buffer, uint16_t num_samples, dr_adc_echo_t *echo, uint16_t threshold) { if (buffer == NULL || echo == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0) return DR_ADC_ERR_INVALID_PARAM; /* Calculate baseline from first few samples */ uint16_t baseline_samples = (num_samples > 8) ? 8 : num_samples; echo->baseline_raw = dr_adc_calc_baseline(buffer, baseline_samples); /* Find peak */ dr_adc_find_peak(buffer, num_samples, &echo->peak_raw, &echo->peak_index); /* Convert to voltage and time */ echo->peak_mv = dr_adc_raw_to_mv(echo->peak_raw); /* peak_time_us: sample_index * 0.116us (8.6MHz sample rate) */ echo->peak_time_us = (uint32_t)((float)echo->peak_index * DR_ADC_SAMPLE_INTERVAL_US); echo->num_samples = num_samples; /* Check if valid echo detected */ if (echo->peak_raw < threshold || echo->peak_raw <= echo->baseline_raw) { return DR_ADC_ERR_NO_ECHO; } return DR_ADC_OK; } void dr_adc_find_peak(const uint16_t *buffer, uint16_t num_samples, uint16_t *peak_value, uint16_t *peak_index) { /* * Posterior wall detection for bladder measurement * - Skip initial decay zone (index 0-29) where transducer coupling causes high values * - Search in range [30, 70] where posterior wall echo is expected * - Ignore values > 1500 (likely noise/saturation) */ #define SEARCH_START 30 #define SEARCH_END 70 #define MAX_VALID_VALUE 1500 /* Values above this are likely noise */ uint16_t max_val = 0; uint16_t max_idx = 0; uint16_t start = (num_samples > SEARCH_START) ? SEARCH_START : 0; uint16_t end = (num_samples > SEARCH_END) ? SEARCH_END : num_samples; for (uint16_t i = start; i < end; i++) { uint16_t val = buffer[i]; /* Skip abnormally high values (noise/saturation) */ if (val > MAX_VALID_VALUE) continue; if (val > max_val) { max_val = val; max_idx = i; } } if (peak_value != NULL) *peak_value = max_val; if (peak_index != NULL) *peak_index = max_idx; } uint16_t dr_adc_calc_baseline(const uint16_t *buffer, uint16_t num_samples) { if (buffer == NULL || num_samples == 0) return 0; uint32_t sum = 0; for (uint16_t i = 0; i < num_samples; i++) { sum += buffer[i]; } return (uint16_t)(sum / num_samples); } /*============================================================================== * PUBLIC FUNCTIONS - UTILITY *============================================================================*/ uint32_t dr_adc_raw_to_mv(uint16_t raw_value) { /* V_mv = (raw * VREF_mv) / 4096 */ return ((uint32_t)raw_value * m_vref_mv) / (DR_ADC_MAX_VALUE + 1); } void dr_adc_set_vref(uint32_t vref_mv) { m_vref_mv = vref_mv; ADC_LOG("VREF set to %d mV", vref_mv); } uint32_t dr_adc_get_vref(void) { return m_vref_mv; } /*============================================================================== * PUBLIC FUNCTIONS - DEBUG *============================================================================*/ bool dr_adc_test(void) { if (!m_initialized) { ADC_LOG("Test FAIL: not initialized"); return false; } uint16_t raw; dr_adc_err_t err = dr_adc_read_raw(&raw); if (err != DR_ADC_OK) { ADC_LOG("Test FAIL: read error %d", err); return false; } if (raw > DR_ADC_MAX_VALUE) { ADC_LOG("Test FAIL: invalid value %d", raw); return false; } ADC_LOG("Test PASS: raw=%d (%dmV)", raw, dr_adc_raw_to_mv(raw)); return true; } void dr_adc_print_buffer(const uint16_t *buffer, uint16_t num_samples) { #ifdef DEBUG_ADC if (buffer == NULL || num_samples == 0) return; ADC_LOG("Echo buffer (%d samples):", num_samples); for (uint16_t i = 0; i < num_samples; i++) { ADC_LOG("[%3d] %4d (%4dmV)", i, buffer[i], dr_adc_raw_to_mv(buffer[i])); } #else (void)buffer; (void)num_samples; #endif } /*============================================================================== * BLE TRANSMISSION *============================================================================*/ /* External BLE NUS functions and variables from main.c */ extern void dr_binary_tx_safe(uint8_t const *ble_bin_buff, uint16_t length); extern void dr_sd_delay_ms(uint32_t ms); /*============================================================================== * INTEGRATED BURST + CAPTURE + TRANSMIT *============================================================================*/ /* External piezo burst function */ extern void dr_piezo_burst_sw(uint8_t cycles); /* BLE packet constants */ #define BLE_MTU_SIZE 240 #define BLE_FIRST_HEADER_LEN 14 /* "rec:" + header */ #define BLE_CONT_HEADER_LEN 6 /* "red:" + packet_index */ #define BLE_FIRST_DATA_LEN (BLE_MTU_SIZE - BLE_FIRST_HEADER_LEN) #define BLE_CONT_DATA_LEN (BLE_MTU_SIZE - BLE_CONT_HEADER_LEN) #define BLE_PACKET_DELAY_MS 100 /* Inter-packet delay - allow BLE TX buffer to drain */ #define BLE_FIRST_PACKET_DELAY 5 /* Minimal delay */ /* Protocol version markers (OR'd with packet count in reb: header) */ #define MEC_VERSION_MARKER 0xD000 /* vD: raa only (no ree) - ree causes packet loss */ #define MAA_VERSION_MARKER 0xF000 /* vF: maa? hardcoded 8-channel debug */ /*============================================================================== * PIEZO CHANNEL SELECTION *============================================================================*/ /* Piezo MUX pins (8ch) */ #define DR_PIEZO_EN_MUXA NRF_GPIO_PIN_MAP(0, 21) /**< MUXA Enable */ #define DR_PIEZO_EN_MUXB NRF_GPIO_PIN_MAP(0, 23) /**< MUXB Enable */ #define DR_PIEZO_MUX_SEL1 NRF_GPIO_PIN_MAP(0, 28) /**< MUX Select 1 */ #define DR_PIEZO_MUX_SEL0 NRF_GPIO_PIN_MAP(1, 10) /**< MUX Select 0 */ /* * Channel mapping (8ch) jhChun 26.01.29 * | EN MUXA | EN MUXB | SEL 0 | SEL 1 * ---------------------------------------------- * CH A0 | 1 | 0 | 0 | 0 * CH A1 | 1 | 0 | 0 | 1 * CH A2 | 1 | 0 | 1 | 0 * CH A3 | 1 | 0 | 1 | 1 * ---------------------------------------------- * CH B3 | 0 | 1 | 0 | 0 * CH B2 | 0 | 1 | 0 | 1 * CH B1 | 0 | 1 | 1 | 0 * CH B0 | 0 | 1 | 1 | 1 */ /* dr_piezo_select_channel is defined in dr_piezo.c */ extern void dr_piezo_select_channel(uint8_t channel); /*============================================================================== * INTEGRATED BURST + CAPTURE + TRANSMIT *============================================================================*/ /** * @brief Integrated burst + capture + BLE transmit (16-bit raw data) * * PROTOCOL v3: Small header packet + separate data packets with delays * (Large first packets were being dropped by BLE stack) * * Response format: * Packet 1 (reb:): header only (14 bytes) * Packet 2~N (red:): pkt_idx(2) + raw_data(up to 234 bytes = 117 samples) * Final (ree:): total_packets(2) * * With 140 samples: 280 bytes = reb:(14) + red:(6+234) + red:(6+46) + ree:(6) */ dr_adc_err_t dr_adc_burst_capture_transmit(uint8_t freq_option, uint16_t delay_us, uint16_t num_samples, uint8_t cycles, uint16_t averaging, uint8_t piezo_ch, uint8_t *ble_buffer, uint8_t skip_raa) { if (ble_buffer == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; if (freq_option > 9) freq_option = 0; /* Invalid -> default 1.8MHz */ if (cycles < 3 || cycles > 9) cycles = 5; /* Valid range: 3~9, default 5 */ if (averaging == 0) averaging = 1; /* Minimum 1 */ if (averaging > 1000) averaging = 1000; /* Maximum 1000 */ if (piezo_ch > 7) piezo_ch = 0; /* Validate piezo channel: 0~7 jhChun 26.01.29 */ dr_adc_echo_t echo; echo.peak_raw = 0; echo.peak_mv = 0; echo.peak_index = 0; echo.peak_time_us = 0; echo.baseline_raw = 0; echo.num_samples = 0; /* Accumulator buffer for averaging (32-bit to prevent overflow) */ static uint32_t accum_buffer[DR_ADC_ECHO_SAMPLES_MAX]; /* CRITICAL: Clear entire accumulator buffer at start of each mec call */ memset(accum_buffer, 0, sizeof(accum_buffer)); if (!m_initialized) { dr_adc_err_t init_err = dr_adc_init(); if (init_err != DR_ADC_OK) { return init_err; } } /* Settling time for ADC power */ nrf_delay_us(100); /*--- Step 2: Select piezo channel ---*/ dr_piezo_select_channel(piezo_ch); /*--- Step 3~5: Burst + Delay + Capture ---*/ if (averaging == 1) { /*=== SINGLE MEASUREMENT (no averaging) - direct path ===*/ /* Execute piezo burst based on frequency option */ switch (freq_option) { case 0: default: dr_piezo_burst_sw_18mhz(cycles); /* 1.8MHz (default) */ break; case 1: dr_piezo_burst_sw(cycles); /* 2.1MHz */ break; case 2: dr_piezo_burst_sw_20mhz(cycles); /* 2.0MHz */ break; case 3: dr_piezo_burst_sw_17mhz(cycles); /* 1.7MHz */ break; case 4: dr_piezo_burst_sw_22mhz(cycles); /* 2.2MHz */ break; case 9: dr_piezo_burst_sw_19mhz(cycles); /* 1.9MHz */ break; } /* Delay before capture (configurable, default 20us) */ if (delay_us > 0) { nrf_delay_us(delay_us); } /* Capture ADC samples directly to m_echo_buffer */ dr_adc_err_t err = dr_adc_capture_echo(m_echo_buffer, num_samples); if (err != DR_ADC_OK) return err; ADC_LOG("mec: single measurement (no averaging)"); } else { /*=== MULTIPLE MEASUREMENTS (with averaging) ===*/ /* Note: accum_buffer already cleared by memset at function start */ for (uint16_t avg_iter = 0; avg_iter < averaging; avg_iter++) { /* Wait for previous echo to decay before next measurement * 1ms = ~77cm round-trip decay time (sound speed 1.54mm/us) * Skip delay on first iteration */ if (avg_iter > 0) { nrf_delay_us(500); /* 500us between measurements */ } /* Re-select piezo channel before each burst * (burst functions may modify P1 port state) */ dr_piezo_select_channel(piezo_ch); /* Execute piezo burst based on frequency option */ switch (freq_option) { case 0: default: dr_piezo_burst_sw_18mhz(cycles); /* 1.8MHz (default) */ break; case 1: dr_piezo_burst_sw(cycles); /* 2.1MHz */ break; case 2: dr_piezo_burst_sw_20mhz(cycles); /* 2.0MHz */ break; case 3: dr_piezo_burst_sw_17mhz(cycles); /* 1.7MHz */ break; case 4: dr_piezo_burst_sw_22mhz(cycles); /* 2.2MHz */ break; case 9: dr_piezo_burst_sw_19mhz(cycles); /* 1.9MHz */ break; } /* Delay before capture (configurable, default 20us) */ if (delay_us > 0) { nrf_delay_us(delay_us); } /* Capture ADC samples */ dr_adc_err_t err = dr_adc_capture_echo(m_echo_buffer, num_samples); if (err != DR_ADC_OK) return err; /* Accumulate samples */ for (uint16_t i = 0; i < num_samples; i++) { accum_buffer[i] += m_echo_buffer[i]; } } /* Calculate average and store back to m_echo_buffer */ for (uint16_t i = 0; i < num_samples; i++) { m_echo_buffer[i] = (uint16_t)(accum_buffer[i] / averaging); } ADC_LOG("mec: averaged %u measurements", averaging); } /*--- Step 6: Analyze averaged data ---*/ dr_adc_analyze_echo(m_echo_buffer, num_samples, &echo, 100); /*--- Step 6: Transmit via BLE - PROTOCOL v3 ---*/ /* Packet 1: reb:(4) + header(10) = 14 bytes ONLY (no data) */ /* Packet 2~N: red:(4) + pkt_idx(2) + data(up to 234 bytes) */ /* Final: ree:(4) + total_pkts(2) = 6 bytes */ uint16_t total_data_bytes = echo.num_samples * 2; uint16_t cont_pkt_data_cap = BLE_MTU_SIZE - BLE_CONT_HEADER_LEN; /* 234 bytes */ /* Calculate data packets needed (header packet doesn't carry data) */ uint16_t data_packets = (total_data_bytes + cont_pkt_data_cap - 1) / cont_pkt_data_cap; /* Version marker: select based on skip_raa (0=mec, 1=maa) */ uint16_t version_marker = skip_raa ? MAA_VERSION_MARKER : MEC_VERSION_MARKER; uint16_t pkts_with_ver = data_packets | version_marker; ADC_LOG("mec v6: samples=%u data=%u bytes, packets=%u", echo.num_samples, total_data_bytes, data_packets); /* Packet 1: reb: header ONLY (14 bytes) - small packet won't be dropped */ ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkts_with_ver & 0xFF); ble_buffer[5] = (uint8_t)(pkts_with_ver >> 8); ble_buffer[6] = (uint8_t)(echo.peak_raw & 0xFF); ble_buffer[7] = (uint8_t)(echo.peak_raw >> 8); ble_buffer[8] = (uint8_t)(echo.peak_index & 0xFF); ble_buffer[9] = (uint8_t)(echo.peak_index >> 8); ble_buffer[10] = (uint8_t)(echo.baseline_raw & 0xFF); ble_buffer[11] = (uint8_t)(echo.baseline_raw >> 8); ble_buffer[12] = (uint8_t)(echo.num_samples & 0xFF); ble_buffer[13] = (uint8_t)(echo.num_samples >> 8); dr_binary_tx_safe(ble_buffer, 7); /* Send header only: 14 bytes = 7 words */ dr_sd_delay_ms(BLE_PACKET_DELAY_MS); /* Wait for BLE stack */ /* Data packets (red:) - starting from pkt index 0 */ uint16_t src_idx = 0; for (uint16_t pkt = 0; pkt < data_packets; pkt++) { ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt & 0xFF); ble_buffer[5] = (uint8_t)(pkt >> 8); uint16_t dst_idx = 6; uint16_t bytes_this_pkt = 0; while (src_idx < echo.num_samples && bytes_this_pkt < cont_pkt_data_cap) { uint16_t sample = m_echo_buffer[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); bytes_this_pkt += 2; } dr_binary_tx_safe(ble_buffer, dst_idx / 2); /* bytes to words */ dr_sd_delay_ms(BLE_PACKET_DELAY_MS); /* Inter-packet delay */ } /* vD: Send raa: only - ree: causes subsequent packet loss */ /* DrBench now saves channel data when receiving raa: */ /* skip_raa: 0=send raa (mec), 1=skip raa (maa - caller sends final raa) */ if (!skip_raa) { ble_buffer[0] = 'r'; ble_buffer[1] = 'a'; ble_buffer[2] = 'a'; ble_buffer[3] = ':'; ble_buffer[4] = 0x00; ble_buffer[5] = 0x00; dr_binary_tx_safe(ble_buffer, 3); /* 6 bytes = 3 words */ } ADC_LOG("mec v6: complete - reb + red*%u + skip_raa=%u (%u samples)", data_packets, skip_raa, echo.num_samples); return DR_ADC_OK; } /*============================================================================== * 4-CHANNEL CAPTURE FUNCTIONS (maa? command support) *============================================================================*/ dr_adc_err_t dr_adc_capture_channel_only(uint8_t freq_option, uint16_t delay_us, uint16_t num_samples, uint8_t cycles, uint16_t averaging, uint8_t piezo_ch, dr_maa_channel_t *out_channel) { if (out_channel == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > MAA_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; if (freq_option > 3) freq_option = 0; if (cycles < 3 || cycles > 9) cycles = 5; if (averaging == 0) averaging = 1; if (averaging > 1000) averaging = 1000; if (piezo_ch > (MAA_NUM_CHANNELS - 1)) piezo_ch = 0; /* Accumulator buffer for averaging */ static uint32_t accum_buffer[MAA_SAMPLES_MAX]; memset(accum_buffer, 0, num_samples * sizeof(uint32_t)); if (!m_initialized) { dr_adc_err_t init_err = dr_adc_init(); if (init_err != DR_ADC_OK) return init_err; } nrf_delay_us(100); /* Select piezo channel */ dr_piezo_select_channel(piezo_ch); /* Capture with averaging */ for (uint16_t avg_iter = 0; avg_iter < averaging; avg_iter++) { if (avg_iter > 0) { nrf_delay_us(500); /* Echo decay time */ } dr_piezo_select_channel(piezo_ch); /* Execute piezo burst */ switch (freq_option) { case 0: default: dr_piezo_burst_sw_18mhz(cycles); break; case 1: dr_piezo_burst_sw(cycles); break; case 2: dr_piezo_burst_sw_20mhz(cycles); break; case 3: dr_piezo_burst_sw_17mhz(cycles); break; } if (delay_us > 0) { nrf_delay_us(delay_us); } /* Capture to internal buffer */ dr_adc_err_t err = dr_adc_capture_echo(m_echo_buffer, num_samples); if (err != DR_ADC_OK) return err; /* Accumulate */ for (uint16_t i = 0; i < num_samples; i++) { accum_buffer[i] += m_echo_buffer[i]; } } /* Calculate average and copy to output */ for (uint16_t i = 0; i < num_samples; i++) { out_channel->samples[i] = (uint16_t)(accum_buffer[i] / averaging); } out_channel->num_samples = num_samples; /* Analyze echo data */ dr_adc_echo_t echo; dr_adc_analyze_echo(out_channel->samples, num_samples, &echo, 100); out_channel->peak_raw = echo.peak_raw; out_channel->peak_index = echo.peak_index; out_channel->baseline_raw = echo.baseline_raw; ADC_LOG("maa capture CH%u: peak=%u idx=%u baseline=%u", piezo_ch, out_channel->peak_raw, out_channel->peak_index, out_channel->baseline_raw); return DR_ADC_OK; } dr_adc_err_t dr_adc_transmit_channel(const dr_maa_channel_t *ch_data, uint8_t *ble_buffer) { /* Debug BEFORE null check to see if we even enter the function */ dr_ble_debug(0x00FF, 0xAAAA); /* Function called marker */ if (ch_data == NULL || ble_buffer == NULL) { dr_ble_debug(0x00FE, (ch_data == NULL ? 0x0001 : 0) | (ble_buffer == NULL ? 0x0002 : 0)); return DR_ADC_ERR_INVALID_PARAM; } dr_ble_debug(0x0100, ch_data->num_samples); /* Function entry - params valid */ uint16_t total_data_bytes = ch_data->num_samples * 2; uint16_t cont_pkt_data_cap = BLE_MTU_SIZE - BLE_CONT_HEADER_LEN; /* 234 bytes */ uint16_t data_packets = (total_data_bytes + cont_pkt_data_cap - 1) / cont_pkt_data_cap; uint16_t total_packets = 1 + data_packets; /* Version marker: MAA_VERSION_MARKER | total_packets */ uint16_t pkt_with_ver = total_packets | MAA_VERSION_MARKER; dr_ble_debug(0x0101, total_packets); /* Before reb: */ /* Packet 1: reb: header (14 bytes) */ ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt_with_ver & 0xFF); ble_buffer[5] = (uint8_t)(pkt_with_ver >> 8); ble_buffer[6] = (uint8_t)(ch_data->peak_raw & 0xFF); ble_buffer[7] = (uint8_t)(ch_data->peak_raw >> 8); ble_buffer[8] = (uint8_t)(ch_data->peak_index & 0xFF); ble_buffer[9] = (uint8_t)(ch_data->peak_index >> 8); ble_buffer[10] = (uint8_t)(ch_data->baseline_raw & 0xFF); ble_buffer[11] = (uint8_t)(ch_data->baseline_raw >> 8); ble_buffer[12] = (uint8_t)(ch_data->num_samples & 0xFF); ble_buffer[13] = (uint8_t)(ch_data->num_samples >> 8); /* Send reb: header */ /* Use dr_binary_tx_safe like mec? does, with dr_sd_delay_ms for SoftDevice */ dr_binary_tx_safe(ble_buffer, 7); dr_sd_delay_ms(BLE_PACKET_DELAY_MS); dr_ble_debug(0x0102, data_packets); /* After reb: */ /* Data packets (red:) */ uint16_t src_idx = 0; for (uint16_t pkt = 0; pkt < data_packets; pkt++) { ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt & 0xFF); ble_buffer[5] = (uint8_t)(pkt >> 8); uint16_t dst_idx = 6; uint16_t bytes_this_pkt = 0; while (src_idx < ch_data->num_samples && bytes_this_pkt < cont_pkt_data_cap) { uint16_t sample = ch_data->samples[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); bytes_this_pkt += 2; } dr_binary_tx_safe(ble_buffer, dst_idx / 2); dr_sd_delay_ms(BLE_PACKET_DELAY_MS); dr_ble_debug(0x0103, pkt); /* red: packet sent */ } dr_ble_debug(0x0104, 0); /* Before ree: */ /* Final packet: ree: */ ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'e'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(total_packets & 0xFF); ble_buffer[5] = (uint8_t)(total_packets >> 8); dr_binary_tx_safe(ble_buffer, 3); dr_ble_debug(0x010F, 0); /* Function complete */ return DR_ADC_OK; } /*============================================================================== * DELTA COMPRESSION FUNCTIONS *============================================================================*/ dr_adc_err_t dr_adc_delta_compress(const uint16_t *samples, uint16_t num_samples, uint8_t *out_buffer, uint16_t *out_size) { if (samples == NULL || out_buffer == NULL || out_size == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0) { *out_size = 0; return DR_ADC_OK; } uint16_t idx = 0; /* First sample: 16-bit raw value */ out_buffer[idx++] = (uint8_t)(samples[0] & 0xFF); out_buffer[idx++] = (uint8_t)(samples[0] >> 8); /* Delta encode remaining samples */ for (uint16_t i = 1; i < num_samples; i++) { int16_t delta = (int16_t)samples[i] - (int16_t)samples[i-1]; if (delta >= -127 && delta <= 127) { /* Normal delta: fits in signed 8-bit */ out_buffer[idx++] = (int8_t)delta; } else { /* Out of range: escape + 16-bit raw value */ out_buffer[idx++] = DELTA_ESCAPE_BYTE; out_buffer[idx++] = (uint8_t)(samples[i] & 0xFF); out_buffer[idx++] = (uint8_t)(samples[i] >> 8); } } *out_size = idx; return DR_ADC_OK; } dr_adc_err_t dr_adc_transmit_channel_delta(const dr_maa_channel_t *ch_data, uint8_t *ble_buffer) { if (ch_data == NULL || ble_buffer == NULL) return DR_ADC_ERR_INVALID_PARAM; /* Compress data first */ static uint8_t delta_buffer[400]; /* Worst case: 140*3 = 420 bytes */ uint16_t compressed_size = 0; dr_adc_err_t err = dr_adc_delta_compress(ch_data->samples, ch_data->num_samples, delta_buffer, &compressed_size); if (err != DR_ADC_OK) return err; ADC_LOG("maa delta: %u samples -> %u bytes (%.0f%%)", ch_data->num_samples, compressed_size, 100.0f * compressed_size / (ch_data->num_samples * 2)); uint16_t cont_pkt_data_cap = BLE_MTU_SIZE - BLE_CONT_HEADER_LEN; /* 234 bytes */ uint16_t data_packets = (compressed_size + cont_pkt_data_cap - 1) / cont_pkt_data_cap; uint16_t total_packets = 1 + data_packets; /* Version marker for delta mode: 0xC000 | total_packets */ uint16_t pkt_with_ver = total_packets | 0xC000; /* v2 delta marker */ /* Packet 1: rdb: header (16 bytes) - includes compressed_size */ ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt_with_ver & 0xFF); ble_buffer[5] = (uint8_t)(pkt_with_ver >> 8); ble_buffer[6] = (uint8_t)(ch_data->peak_raw & 0xFF); ble_buffer[7] = (uint8_t)(ch_data->peak_raw >> 8); ble_buffer[8] = (uint8_t)(ch_data->peak_index & 0xFF); ble_buffer[9] = (uint8_t)(ch_data->peak_index >> 8); ble_buffer[10] = (uint8_t)(ch_data->baseline_raw & 0xFF); ble_buffer[11] = (uint8_t)(ch_data->baseline_raw >> 8); ble_buffer[12] = (uint8_t)(ch_data->num_samples & 0xFF); ble_buffer[13] = (uint8_t)(ch_data->num_samples >> 8); ble_buffer[14] = (uint8_t)(compressed_size & 0xFF); ble_buffer[15] = (uint8_t)(compressed_size >> 8); dr_binary_tx_safe(ble_buffer, 8); /* 16 bytes = 8 words */ dr_sd_delay_ms(BLE_PACKET_DELAY_MS); /* Data packets (rdd:) */ uint16_t src_idx = 0; for (uint16_t pkt = 0; pkt < data_packets; pkt++) { ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt & 0xFF); ble_buffer[5] = (uint8_t)(pkt >> 8); uint16_t dst_idx = 6; uint16_t bytes_this_pkt = 0; while (src_idx < compressed_size && bytes_this_pkt < cont_pkt_data_cap) { ble_buffer[dst_idx++] = delta_buffer[src_idx++]; bytes_this_pkt++; } /* Pad to word boundary if needed */ if (dst_idx & 1) { ble_buffer[dst_idx++] = 0; } dr_binary_tx_safe(ble_buffer, dst_idx / 2); dr_sd_delay_ms(BLE_PACKET_DELAY_MS); } /* Final packet: rde: */ ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'e'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(total_packets & 0xFF); ble_buffer[5] = (uint8_t)(total_packets >> 8); dr_binary_tx_safe(ble_buffer, 3); dr_sd_delay_ms(BLE_PACKET_DELAY_MS); return DR_ADC_OK; } /*============================================================================== * ASYNC MAA - Non-blocking 8-channel capture * * State machine driven by BLE TX complete events (BLE_NUS_EVT_TX_RDY) * This prevents blocking the main loop and allows SoftDevice to process events. *============================================================================*/ /* Global async context */ static maa_async_ctx_t g_maa_ctx = { .state = MAA_ASYNC_IDLE }; /* Version marker for async MAA */ #define MAA_ASYNC_VERSION_MARKER 0xC000 /* vC: async 8-channel + raa delay fix + cleanup */ /** * @brief Capture one channel (internal helper) */ static dr_adc_err_t maa_async_capture_channel(uint8_t ch) { if (ch > (MAA_NUM_CHANNELS - 1)) return DR_ADC_ERR_INVALID_PARAM; return dr_adc_capture_channel_only( g_maa_ctx.freq_option, g_maa_ctx.delay_us, g_maa_ctx.num_samples, g_maa_ctx.cycles, g_maa_ctx.averaging, ch, &g_maa_ctx.channels[ch] ); } /** * @brief Send reb: header for current channel */ static void maa_async_send_header(void) { dr_maa_channel_t *ch = &g_maa_ctx.channels[g_maa_ctx.current_ch]; uint8_t *buf = g_maa_ctx.ble_buffer; uint16_t total_data_bytes = ch->num_samples * 2; uint16_t cont_pkt_data_cap = BLE_MTU_SIZE - BLE_CONT_HEADER_LEN; g_maa_ctx.data_packets = (total_data_bytes + cont_pkt_data_cap - 1) / cont_pkt_data_cap; g_maa_ctx.total_packets = 1 + g_maa_ctx.data_packets; uint16_t pkts_with_ver = g_maa_ctx.total_packets | MAA_ASYNC_VERSION_MARKER; /* reb: header (14 bytes) */ buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'b'; buf[3] = ':'; buf[4] = (uint8_t)(pkts_with_ver & 0xFF); buf[5] = (uint8_t)(pkts_with_ver >> 8); buf[6] = (uint8_t)(ch->peak_raw & 0xFF); buf[7] = (uint8_t)(ch->peak_raw >> 8); buf[8] = (uint8_t)(ch->peak_index & 0xFF); buf[9] = (uint8_t)(ch->peak_index >> 8); buf[10] = (uint8_t)(ch->baseline_raw & 0xFF); buf[11] = (uint8_t)(ch->baseline_raw >> 8); buf[12] = (uint8_t)(ch->num_samples & 0xFF); buf[13] = (uint8_t)(ch->num_samples >> 8); dr_binary_tx_safe(buf, 7); /* 14 bytes = 7 words */ dr_sd_delay_ms(50); /* Allow BLE stack to process TX */ g_maa_ctx.current_pkt = 0; g_maa_ctx.data_offset = 0; g_maa_ctx.state = MAA_ASYNC_TX_DATA; ADC_LOG("maa_async: CH%u reb: sent (pkts=%u)", g_maa_ctx.current_ch, g_maa_ctx.total_packets); } /** * @brief Send next red: data packet * @return true if more packets to send, false if done with current channel */ static bool maa_async_send_data_packet(void) { dr_maa_channel_t *ch = &g_maa_ctx.channels[g_maa_ctx.current_ch]; uint8_t *buf = g_maa_ctx.ble_buffer; uint16_t total_data_bytes = ch->num_samples * 2; if (g_maa_ctx.data_offset >= total_data_bytes) { return false; /* All data sent */ } uint16_t pkt_idx = g_maa_ctx.current_pkt; uint16_t remaining = total_data_bytes - g_maa_ctx.data_offset; uint16_t chunk_size = (remaining > BLE_CONT_DATA_LEN) ? BLE_CONT_DATA_LEN : remaining; /* red: packet header */ buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'd'; buf[3] = ':'; buf[4] = (uint8_t)(pkt_idx & 0xFF); buf[5] = (uint8_t)(pkt_idx >> 8); /* Copy sample data */ uint16_t src_sample_idx = g_maa_ctx.data_offset / 2; uint16_t dst_idx = 6; for (uint16_t i = 0; i < chunk_size / 2 && src_sample_idx < ch->num_samples; i++, src_sample_idx++) { uint16_t sample = ch->samples[src_sample_idx]; buf[dst_idx++] = (uint8_t)(sample & 0xFF); buf[dst_idx++] = (uint8_t)(sample >> 8); } dr_binary_tx_safe(buf, dst_idx / 2); dr_sd_delay_ms(50); /* Allow BLE stack to process TX */ g_maa_ctx.data_offset += chunk_size; g_maa_ctx.current_pkt++; ADC_LOG("maa_async: CH%u red:%u (%u/%u bytes)", g_maa_ctx.current_ch, pkt_idx, g_maa_ctx.data_offset, total_data_bytes); return (g_maa_ctx.data_offset < total_data_bytes); } /** * @brief Send raa: completion marker */ static void maa_async_send_completion(uint16_t status) { uint8_t *buf = g_maa_ctx.ble_buffer; /* Wait for previous TX to complete before sending raa: */ dr_sd_delay_ms(50); buf[0] = 'r'; buf[1] = 'a'; buf[2] = 'a'; buf[3] = ':'; buf[4] = (uint8_t)(status & 0xFF); buf[5] = (uint8_t)(status >> 8); dr_binary_tx_safe(buf, 3); g_maa_ctx.state = MAA_ASYNC_IDLE; ADC_LOG("maa_async: complete, status=0x%04X", status); } /*============================================================================== * PUBLIC ASYNC API *============================================================================*/ dr_adc_err_t maa_async_start(uint8_t freq_option, uint16_t delay_us, uint16_t num_samples, uint8_t cycles, uint16_t averaging, uint8_t *ble_buffer) { if (g_maa_ctx.state != MAA_ASYNC_IDLE) { ADC_LOG("maa_async_start: busy"); return DR_ADC_ERR_NOT_INIT; /* Already running */ } if (ble_buffer == NULL) return DR_ADC_ERR_INVALID_PARAM; if (num_samples == 0 || num_samples > DR_ADC_ECHO_SAMPLES_MAX) return DR_ADC_ERR_INVALID_PARAM; /* Initialize context */ g_maa_ctx.freq_option = freq_option; g_maa_ctx.delay_us = delay_us; g_maa_ctx.num_samples = num_samples; g_maa_ctx.cycles = (cycles < 3 || cycles > 9) ? 5 : cycles; g_maa_ctx.averaging = (averaging == 0) ? 1 : ((averaging > 1000) ? 1000 : averaging); g_maa_ctx.ble_buffer = ble_buffer; g_maa_ctx.current_ch = 0; g_maa_ctx.current_pkt = 0; g_maa_ctx.data_offset = 0; ADC_LOG("maa_async_start: freq=%u delay=%u samples=%u cycles=%u avg=%u", freq_option, delay_us, num_samples, g_maa_ctx.cycles, g_maa_ctx.averaging); /* Capture CH0 */ g_maa_ctx.state = MAA_ASYNC_CAPTURING; dr_adc_err_t err = maa_async_capture_channel(0); if (err != DR_ADC_OK) { ADC_LOG("maa_async_start: CH0 capture failed (%d)", err); maa_async_send_completion(0xFFF0); return err; } /* Send CH0 header - this will trigger TX_RDY for subsequent packets */ maa_async_send_header(); return DR_ADC_OK; } bool maa_async_on_tx_ready(void) { if (g_maa_ctx.state == MAA_ASYNC_IDLE) { return false; } switch (g_maa_ctx.state) { case MAA_ASYNC_TX_DATA: /* Send next data packet */ if (!maa_async_send_data_packet()) { /* Current channel done, move to next */ g_maa_ctx.current_ch++; if (g_maa_ctx.current_ch >= MAA_NUM_CHANNELS) { /* All channels done */ g_maa_ctx.state = MAA_ASYNC_COMPLETE; maa_async_send_completion(0x0000); return false; } else { /* Capture next channel */ g_maa_ctx.state = MAA_ASYNC_CAPTURING; dr_adc_err_t err = maa_async_capture_channel(g_maa_ctx.current_ch); if (err != DR_ADC_OK) { ADC_LOG("maa_async: CH%u capture failed", g_maa_ctx.current_ch); maa_async_send_completion(0xFFF0 | g_maa_ctx.current_ch); return false; } /* Send header for new channel */ maa_async_send_header(); } } return true; case MAA_ASYNC_TX_HEADER: /* Header sent, start sending data */ g_maa_ctx.state = MAA_ASYNC_TX_DATA; maa_async_send_data_packet(); return true; case MAA_ASYNC_CAPTURING: /* Shouldn't happen - capture is synchronous */ return true; case MAA_ASYNC_COMPLETE: case MAA_ASYNC_IDLE: default: return false; } } bool maa_async_is_busy(void) { return (g_maa_ctx.state != MAA_ASYNC_IDLE); } maa_async_state_t maa_async_get_state(void) { return g_maa_ctx.state; } void maa_async_abort(void) { if (g_maa_ctx.state != MAA_ASYNC_IDLE) { ADC_LOG("maa_async_abort: aborting from state %d", g_maa_ctx.state); g_maa_ctx.state = MAA_ASYNC_IDLE; } }