Files
firmware-test/pc_firm/dr_adc121s051/dr_adc121s051.c
jhChun 5e27eb762d BLE Piezo 6채널 데이터 패킷 병합 (reb+red -> reb 단일 패킷)
- BLE_MTU_SIZE 240 -> 244 (ATT MTU 247 - 3 = 244, 기존 4B 낭비 해소)
- reb: 헤더 축소(14B → 6B) 후 데이터 병합 -> 119샘플까지 단일 패킷(현재 100샘플)
- reb: 헤더에서 peak_raw/peak_index/baseline_raw/버전마커 제거
  → PC에서 raw 데이터로 직접 계산, 단일 패킷이므로 버전마커 불필요
- 채널 간 딜레이 50ms -> 5ms (dr_binary_tx_safe 내부 재시도로 TX 보장)
- delta 전송(rdb/rdd)도 동일 방식 적용, 종료 패킷(ree:/rde:) 제거
- 채널 완료 판단: 종료 패킷(ree:) 제거, reb: 수신 시 채널 완료 (100샘플 기준 단일 패킷)
- 전체 완료는 기존과 동일하게 raa:로 판단
2026-03-30 16:37:41 +09:00

1290 lines
44 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*******************************************************************************
* @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 -------<Z2 ><Z1 ><Z0 ><D11>...<D1 ><D0 >------
* |<- 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 <string.h> /* 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 (04095)
*
* @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);
/*--- 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 */
ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':';
ble_buffer[4] = (uint8_t)(num_samples & 0xFF);
ble_buffer[5] = (uint8_t)(num_samples >> 8);
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 & 0xFF);
ble_buffer[dst_idx++] = (uint8_t)(sample >> 8);
}
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 & 0xFF);
ble_buffer[5] = (uint8_t)(pkt >> 8);
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 & 0xFF);
ble_buffer[dst_idx++] = (uint8_t)(sample >> 8);
}
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);
/* 채널당 반복 측정 횟수만큼 */
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 */
ble_buffer[0] = 'r'; ble_buffer[1] = 'e'; ble_buffer[2] = 'b'; ble_buffer[3] = ':';
ble_buffer[4] = (uint8_t)(ch_data->num_samples & 0xFF);
ble_buffer[5] = (uint8_t)(ch_data->num_samples >> 8);
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 & 0xFF);
ble_buffer[dst_idx++] = (uint8_t)(sample >> 8);
}
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 & 0xFF);
ble_buffer[5] = (uint8_t)(pkt >> 8);
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 & 0xFF);
ble_buffer[dst_idx++] = (uint8_t)(sample >> 8);
}
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 */
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));
/* Packet 1: rdb: header(8) + delta data merged */
ble_buffer[0] = 'r'; ble_buffer[1] = 'd'; ble_buffer[2] = 'b'; ble_buffer[3] = ':';
ble_buffer[4] = (uint8_t)(ch_data->num_samples & 0xFF);
ble_buffer[5] = (uint8_t)(ch_data->num_samples >> 8);
ble_buffer[6] = (uint8_t)(compressed_size & 0xFF);
ble_buffer[7] = (uint8_t)(compressed_size >> 8);
#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 & 0xFF);
ble_buffer[5] = (uint8_t)(pkt >> 8);
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 */
buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'b'; buf[3] = ':';
buf[4] = (uint8_t)(ch->num_samples & 0xFF);
buf[5] = (uint8_t)(ch->num_samples >> 8);
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 & 0xFF);
buf[dst_idx++] = (uint8_t)(sample >> 8);
}
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 */
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);
//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;
}