/******************************************************************************* * @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 Hardware SPI (nrfx_spim, SPIM2) implementation for ADC121S051. * Replaces bit-bang SPI to eliminate __disable_irq() and prevent * SoftDevice assertion failures during BLE connection events. * 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 "nrfx_spim.h" #include "nrf52840.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); extern void dr_piezo_power_off(void); #include "parser.h" /*============================================================================== * DEBUG CONFIGURATION *============================================================================*/ #define DEBUG_ADC #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) /*============================================================================== * PRIVATE VARIABLES *============================================================================*/ static bool m_initialized = false; static uint32_t m_vref_mv = DR_ADC_VREF_MV; /* Hardware SPI instance (SPIM3 - supports up to 32MHz, 16MHz for ADC121S051) */ static nrfx_spim_t m_spim = NRFX_SPIM_INSTANCE(3); /* Echo capture buffer (module-level for external access) */ static uint16_t m_echo_buffer[DR_ADC_ECHO_SAMPLES_MAX]; /*============================================================================== * PRIVATE FUNCTIONS *============================================================================*/ /** * @brief Perform one ADC121S051 conversion via hardware SPI (SPIM2). * @return 12-bit ADC value (0–4095) * * @details ADC121S051 serial frame (16 bits, MSB first): * Bit 15-13: Leading zeros (Z2, Z1, Z0) * Bit 12-1: Data bits D11-D0 * Bit 0: Don't care * Extract: (raw16 >> 1) & 0x0FFF * * SPI Mode 2 (CPOL=1, CPHA=0): * - SCLK idle HIGH * - Data valid on SCLK falling edge (sampled by SPIM on falling edge) * - CS falling edge starts ADC conversion */ static uint16_t spim_read_raw(void) { volatile uint8_t rx_buf[2] = {0, 0}; NRF_SPIM_Type *spim = m_spim.p_reg; nrf_gpio_pin_clear(DR_ADC_PIN_CS); /* CS LOW */ spim->RXD.PTR = (uint32_t)rx_buf; spim->RXD.MAXCNT = 2; spim->EVENTS_END = 0; spim->TASKS_START = 1; while (!spim->EVENTS_END) {} nrf_gpio_pin_set(DR_ADC_PIN_CS); /* CS HIGH */ uint16_t raw16 = ((uint16_t)rx_buf[0] << 8) | rx_buf[1]; return (raw16 >> 1) & 0x0FFF; } /*============================================================================== * PUBLIC FUNCTIONS - INITIALIZATION *============================================================================*/ dr_adc_err_t dr_adc_init(void) { nrfx_err_t err; ADC_LOG("ADC121S051 init (HW SPI, SPIM3)..."); nrfx_spim_config_t config = NRFX_SPIM_DEFAULT_CONFIG; config.sck_pin = DR_ADC_PIN_SCLK; /* P0.14 */ config.mosi_pin = NRFX_SPIM_PIN_NOT_USED;/* not used (read-only ADC) */ config.miso_pin = DR_ADC_PIN_SDATA; /* P0.15 */ config.ss_pin = DR_ADC_PIN_CS; /* P0.19 */ config.frequency = NRF_SPIM_FREQ_16M; config.mode = NRF_SPIM_MODE_3; /* CPOL=1 (idle HIGH), CPHA=1 (sample on rising - ADC presents on falling, stable on rising) */ config.bit_order = NRF_SPIM_BIT_ORDER_MSB_FIRST; config.ss_active_high = false; /* CS active LOW */ err = nrfx_spim_init(&m_spim, &config, NULL, NULL); ADC_LOG("SPIM3 init result: 0x%08X (%s)", err, (err == NRFX_SUCCESS) ? "OK" : "FAIL"); if (err != NRFX_SUCCESS) { return DR_ADC_ERR_NOT_INIT; } /* Wait for ADC power stabilization */ nrf_delay_us(10); /* Dummy read to wake up ADC and clear stale conversion data */ (void)spim_read_raw(); m_initialized = true; ADC_LOG("ADC121S051 ready (VREF=%dmV, HW SPI)", m_vref_mv); return DR_ADC_OK; } void dr_adc_uninit(void) { if (!m_initialized) return; nrfx_spim_uninit(&m_spim); 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; *raw_value = spim_read_raw(); 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) { volatile uint8_t rx_buf[2]; uint32_t cs_mask = (1UL << DR_PIN_NUM(DR_ADC_PIN_CS)); uint16_t raw16; uint16_t i; NRF_SPIM_Type *spim = m_spim.p_reg; 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; spim->RXD.MAXCNT = 2; for (i = 0; i < num_samples; i++) { NRF_P0->OUTCLR = cs_mask; spim->RXD.PTR = (uint32_t)rx_buf; spim->EVENTS_END = 0; spim->TASKS_START = 1; while (!spim->EVENTS_END) {} NRF_P0->OUTSET = cs_mask; raw16 = ((uint16_t)rx_buf[0] << 8) | rx_buf[1]; buffer[i] = (raw16 >> 1) & 0x0FFF; } 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); /* Platform interface (log function) */ #include "parser.h" /*============================================================================== * INTEGRATED BURST + CAPTURE + TRANSMIT *============================================================================*/ /* External piezo burst function */ extern void dr_piezo_burst_sw(uint8_t cycles); /* BLE packet constants */ #define BLE_MTU_SIZE 244 /* ATT MTU 247 - 3 (ATT header) = 244 */ #define BLE_REB_HEADER_LEN 6 /* "reb:" tag(4) + num_samples(2) */ #define BLE_RED_HEADER_LEN 6 /* "red:" tag(4) + pkt_idx(2) */ #define BLE_REB_DATA_LEN (BLE_MTU_SIZE - BLE_REB_HEADER_LEN) /* 238 bytes = 119 samples */ #define BLE_RED_DATA_LEN (BLE_MTU_SIZE - BLE_RED_HEADER_LEN) /* 238 bytes = 119 samples */ #define BLE_PACKET_DELAY_MS 100 /* Inter-packet delay - allow BLE TX buffer to drain */ /*============================================================================== * 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) * * reb+red merged protocol: header와 데이터를 첫 패킷에 합침 * * Response format: * Packet 1 (reb:): num_samples(2) + raw_data(up to 238 bytes = 119 samples) * Packet 2~N (red:): pkt_idx(2) + raw_data(up to 238 bytes) — 119샘플 초과 시만 * Final (raa:): status(2) — skip_raa=0 일 때만 * * With 100 samples: 200 bytes = reb:(6+200) = 206 bytes (단일 패킷) */ 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 > 7) cycles = 5; /* Valid range: 3~7, default 5 */ if (averaging == 0) averaging = 1; /* Minimum 1 */ if (averaging > 1000) averaging = 1000; /* Maximum 1000 */ if (piezo_ch >= MAA_NUM_CHANNELS) piezo_ch = 0; /* 채널 범위 검증 */ /* echo 분석은 PC에서 raw 데이터로 직접 수행 */ /* 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); /* MUX 전환 후 S/H 커패시터 안정화를 위한 dummy read */ (void)spim_read_raw(); /*--- 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: Transmit via BLE - reb: + red: merged protocol ---*/ /* Packet 1 (reb:): tag(4) + num_samples(2) + data(up to 238 bytes = 119 samples) */ /* Packet 2~N (red:): tag(4) + pkt_idx(2) + data(up to 238 bytes) — only if > 119 samples */ uint16_t total_data_bytes = num_samples * 2; uint16_t src_idx = 0; /* Packet 1: reb: header + data merged (Big-Endian) */ ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(num_samples >> 8); ble_buffer[5] = (uint8_t)(num_samples & 0xFF); uint16_t dst_idx = BLE_REB_HEADER_LEN; uint16_t first_chunk = (total_data_bytes > BLE_REB_DATA_LEN) ? BLE_REB_DATA_LEN : total_data_bytes; while (src_idx < num_samples && (dst_idx - BLE_REB_HEADER_LEN) < first_chunk) { uint16_t sample = m_echo_buffer[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); } dr_binary_tx_safe(ble_buffer, dst_idx / 2); /* Continuation packets (red:) — only if data didn't fit in reb: */ uint16_t pkt = 0; while (src_idx < num_samples) { dr_sd_delay_ms(BLE_PACKET_DELAY_MS); ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt >> 8); ble_buffer[5] = (uint8_t)(pkt & 0xFF); dst_idx = BLE_RED_HEADER_LEN; while (src_idx < num_samples && (dst_idx - BLE_RED_HEADER_LEN) < BLE_RED_DATA_LEN) { uint16_t sample = m_echo_buffer[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); } dr_binary_tx_safe(ble_buffer, dst_idx / 2); pkt++; } /* raa: completion — skip_raa: 0=send (mec), 1=skip (maa - caller sends final raa) */ if (!skip_raa) { dr_sd_delay_ms(BLE_PACKET_DELAY_MS); 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); } ADC_LOG("mec: reb+data merged, skip_raa=%u (%u samples)", skip_raa, num_samples); return DR_ADC_OK; } /*============================================================================== * 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 > 7) 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); /* 채널 선택(MUX) : 1.3ms */ dr_piezo_select_channel(piezo_ch); /* MUX 전환 후 S/H 커패시터 안정화를 위한 dummy read */ (void)spim_read_raw(); /* 채널당 반복 측정 횟수만큼 */ for (uint16_t avg_iter = 0; avg_iter < averaging; avg_iter++) { /* TX 펄스 출력(Burst) */ switch (freq_option) { case 0: default: dr_piezo_burst_sw_18mhz(cycles); break; case 1: dr_piezo_burst_sw(cycles); /* 2.1MHz */ break; case 2: dr_piezo_burst_sw_20mhz(cycles); break; case 3: dr_piezo_burst_sw_17mhz(cycles); break; } /* TX 펄스 출력 후 ADC 시작 시까지 대기 시간 */ if (delay_us > 0) { nrf_delay_us(delay_us); } /* 측정 ADC 샘플 수만큼 캡처 */ 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; /* peak/baseline 분석은 PC에서 raw 데이터로 직접 수행 */ return DR_ADC_OK; } dr_adc_err_t dr_adc_transmit_channel(const dr_maa_channel_t *ch_data, uint8_t *ble_buffer) { if (ch_data == NULL || ble_buffer == NULL) { return DR_ADC_ERR_INVALID_PARAM; } uint16_t total_data_bytes = ch_data->num_samples * 2; uint16_t src_idx = 0; /* Packet 1: reb: header + data merged (Big-Endian) */ ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(ch_data->num_samples >> 8); ble_buffer[5] = (uint8_t)(ch_data->num_samples & 0xFF); uint16_t dst_idx = BLE_REB_HEADER_LEN; uint16_t first_chunk = (total_data_bytes > BLE_REB_DATA_LEN) ? BLE_REB_DATA_LEN : total_data_bytes; while (src_idx < ch_data->num_samples && (dst_idx - BLE_REB_HEADER_LEN) < first_chunk) { uint16_t sample = ch_data->samples[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); } dr_binary_tx_safe(ble_buffer, dst_idx / 2); /* Continuation packets (red:) — only if data didn't fit in reb: */ uint16_t pkt = 0; while (src_idx < ch_data->num_samples) { dr_sd_delay_ms(BLE_PACKET_DELAY_MS); ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt >> 8); ble_buffer[5] = (uint8_t)(pkt & 0xFF); dst_idx = BLE_RED_HEADER_LEN; while (src_idx < ch_data->num_samples && (dst_idx - BLE_RED_HEADER_LEN) < BLE_RED_DATA_LEN) { uint16_t sample = ch_data->samples[src_idx++] & 0x0FFF; ble_buffer[dst_idx++] = (uint8_t)(sample >> 8); ble_buffer[dst_idx++] = (uint8_t)(sample & 0xFF); } dr_binary_tx_safe(ble_buffer, dst_idx / 2); pkt++; } 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 (Big-Endian) */ out_buffer[idx++] = (uint8_t)(samples[0] >> 8); out_buffer[idx++] = (uint8_t)(samples[0] & 0xFF); /* 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] >> 8); out_buffer[idx++] = (uint8_t)(samples[i] & 0xFF); } } *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)); /* Packet 1: rdb: header(8) + delta data merged (Big-Endian) */ ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'b'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(ch_data->num_samples >> 8); ble_buffer[5] = (uint8_t)(ch_data->num_samples & 0xFF); ble_buffer[6] = (uint8_t)(compressed_size >> 8); ble_buffer[7] = (uint8_t)(compressed_size & 0xFF); #define BLE_RDB_HEADER_LEN 8 /* rdb: tag(4) + num_samples(2) + compressed_size(2) */ uint16_t rdb_data_cap = BLE_MTU_SIZE - BLE_RDB_HEADER_LEN; /* 236 bytes */ uint16_t dst_idx = BLE_RDB_HEADER_LEN; uint16_t src_idx = 0; uint16_t first_chunk = (compressed_size > rdb_data_cap) ? rdb_data_cap : compressed_size; while (src_idx < first_chunk) { ble_buffer[dst_idx++] = delta_buffer[src_idx++]; } /* Pad to word boundary if needed */ if (dst_idx & 1) ble_buffer[dst_idx++] = 0; dr_binary_tx_safe(ble_buffer, dst_idx / 2); /* Continuation packets (rdd:) — only if data didn't fit */ uint16_t pkt = 0; while (src_idx < compressed_size) { dr_sd_delay_ms(BLE_PACKET_DELAY_MS); ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'd'; ble_buffer[3] = ':'; ble_buffer[4] = (uint8_t)(pkt >> 8); ble_buffer[5] = (uint8_t)(pkt & 0xFF); dst_idx = BLE_RED_HEADER_LEN; while (src_idx < compressed_size && (dst_idx - BLE_RED_HEADER_LEN) < BLE_RED_DATA_LEN) { ble_buffer[dst_idx++] = delta_buffer[src_idx++]; } if (dst_idx & 1) ble_buffer[dst_idx++] = 0; dr_binary_tx_safe(ble_buffer, dst_idx / 2); pkt++; } 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 제거 — reb+red merged protocol) */ /** * @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; dr_adc_err_t err = 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] ); return err; } /** * @brief Send reb: header + data merged for current channel * * reb: tag(4) + num_samples(2) + data(up to 238 bytes) * 100샘플(200B) 이하면 이 패킷 하나로 채널 전송 완료 */ 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; /* reb: header + data merged (Big-Endian) */ buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'b'; buf[3] = ':'; buf[4] = (uint8_t)(ch->num_samples >> 8); buf[5] = (uint8_t)(ch->num_samples & 0xFF); uint16_t dst_idx = BLE_REB_HEADER_LEN; uint16_t first_chunk = (total_data_bytes > BLE_REB_DATA_LEN) ? BLE_REB_DATA_LEN : total_data_bytes; uint16_t src_idx = 0; while (src_idx < ch->num_samples && (dst_idx - BLE_REB_HEADER_LEN) < first_chunk) { uint16_t sample = ch->samples[src_idx++]; buf[dst_idx++] = (uint8_t)(sample >> 8); buf[dst_idx++] = (uint8_t)(sample & 0xFF); } dr_binary_tx_safe(buf, dst_idx / 2); dr_sd_delay_ms(5); /* dr_binary_tx_safe 내부 재시도(40ms)로 TX 완료 보장, 최소 딜레이만 */ g_maa_ctx.current_pkt = 0; g_maa_ctx.data_offset = src_idx * 2; /* 이미 전송한 바이트 수 */ /* 데이터가 첫 패킷에 다 들어갔으면 바로 다음 채널로 */ if (g_maa_ctx.data_offset >= total_data_bytes) { g_maa_ctx.state = MAA_ASYNC_TX_DATA; /* send_data_packet()에서 false 반환 → 다음 채널 */ } else { g_maa_ctx.state = MAA_ASYNC_TX_DATA; } } /** * @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_RED_DATA_LEN) ? BLE_RED_DATA_LEN : remaining; /* red: packet header (Big-Endian) */ buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'd'; buf[3] = ':'; buf[4] = (uint8_t)(pkt_idx >> 8); buf[5] = (uint8_t)(pkt_idx & 0xFF); /* 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 >> 8); buf[dst_idx++] = (uint8_t)(sample & 0xFF); } 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 >> 8); buf[5] = (uint8_t)(status & 0xFF); dr_binary_tx_safe(buf, 3); //if (g_plat.log) g_plat.log("-------------------------------------------------------------------------------------\r\n"); /* 캡처 완료 → Piezo TX/RX 전원 OFF */ dr_piezo_power_off(); g_maa_ctx.state = MAA_ASYNC_IDLE; //ADC_LOG("maa_async: complete, status=0x%04X", status); /* 완료 콜백 호출 (mbb? 등에서 센서 측정 체인 트리거용) */ if (g_maa_ctx.on_complete_cb) { void (*cb)(void) = g_maa_ctx.on_complete_cb; g_maa_ctx.on_complete_cb = NULL; /* 1회성: 재호출 방지 */ cb(); } } /*============================================================================== * 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) { dr_adc_err_t err; uint8_t ch; 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 > 7) ? 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; g_maa_ctx.on_complete_cb = NULL; /* 기본: 콜백 없음 (maa?) */ /* 전채널 캡처: BLE 전송 없이 6채널 전부 캡처 완료 후 TX 시작 */ g_maa_ctx.state = MAA_ASYNC_CAPTURING; for (ch = 0; ch < MAA_NUM_CHANNELS; ch++) { err = maa_async_capture_channel(ch); if (err != DR_ADC_OK) { if (g_plat.log) g_plat.log("[maa] maa_async_start: CH%u capture failed (%d)", ch, err); maa_async_send_completion(0xFFF0 | ch); return err; } } /* peak 로그와 raw 덤프 사이 구분 */ //if (g_plat.log) g_plat.log("\r\n"); /* 캡처 완료 → Piezo TX/RX 전원 OFF (BLE 전송 중 불필요) */ dr_piezo_power_off(); /* 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 { /* 이미 전채널 캡처됨, 바로 헤더 전송 */ g_maa_ctx.state = MAA_ASYNC_CAPTURING; 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); } void maa_async_set_pre_capture_all(bool on) { g_maa_ctx.pre_capture_all = on; } maa_async_state_t maa_async_get_state(void) { return g_maa_ctx.state; } /*============================================================================== * 비동기 측정 상태 IDLE로 초기화 - BLE 연결이 끊어지는 경우 먹통 현상 방지 *============================================================================*/ 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; // 측정 상태 IDLE로 초기화 g_maa_ctx.on_complete_cb = NULL; // abort 후 콜백 호출 방지 dr_piezo_power_off(); // 전체 측정 중간에 끊기는 경우 Piezo TX/RX Sleep } } void maa_async_set_on_complete(void (*cb)(void)) { g_maa_ctx.on_complete_cb = cb; }