/******************************************************************************* * @file measurements.c * @brief LED/PD measurement control module for NIRS system * @author Charles KWON * @version V2.0.0 * @date 2025-12-31 * * Copyright (c) 2025 Medithings Inc. * All rights reserved. * * This module provides LED and photodetector (PD) control functions for the * NIRS (Near-Infrared Spectroscopy) bladder monitoring system. * * ============================================================================ * MODULE OVERVIEW * ============================================================================ * * Hardware Components Controlled: * - 48 NIR LEDs (LED0-LED47) via CAT9532 I2C LED driver * - 4 Photodetectors (PD0, PD1, MOD, MOD2) via FSA5157P6X analog switch * - MCP4725 DAC for PD gain control (I2C, 12-bit) * - DS3930 EEPROM for LED power calibration storage * * LED-PD Mapping: * - LED 0-23 -> PD0 (photodetector 0) * - LED 24-47 -> PD1 (photodetector 1) * - MOD/MOD2 -> Lock-in amplifier modulation channels * * DAC Gain Range: * - MCP4725 output: 0.1V ~ 1.1V * - DAC value range: 125 ~ 1366 (12-bit) * - Default value: 1000 (mid-range) * * ============================================================================ * MAIN FUNCTIONS * ============================================================================ * * LED Control: * - led_on(index) : Turn on specific LED (0-47) * - led_off(index) : Turn off LED (99=all off, 98=clear) * - led_select(index) : Select LED via GPIO multiplexer * * PD Control: * - pd_on(index) : Turn on specific PD (0-3) * - pd_off(index) : Turn off PD (99=all off) * - pd_select(index) : Select PD channel * * Gain Control: * - pd_gain_set(led) : Set PD gain from led_pd_dac_v[] lookup table * - imm_gain_set(dac) : Set immediate DAC value * - led_pd_mod_set() : Set modulation gain for lock-in * * EEPROM Operations: * - led_power_read(index) : Read LED power from ROM * - led_power_save_mem(index) : Save LED power to ROM * - led_power_save_mem_48() : Save all 48 LED powers * ******************************************************************************/ /*============================================================================*/ /* Includes - Charles KWON */ /*============================================================================*/ #include "sdk_common.h" #include #include #include #include #include "nrf.h" #include "nrf_drv_gpiote.h" #include "nrf_drv_spi.h" #include "nrf_drv_saadc.h" #include "nrf_drv_ppi.h" #include "nrf_drv_timer.h" #include "boards.h" #include "bsp.h" #include "app_error.h" #include "nrf_delay.h" #include "app_util_platform.h" #include "nrf_pwr_mgmt.h" #include "nrf_log.h" #include "ble_nus.h" #include "LED_Parse.h" #include "app_timer.h" #include "measurements.h" #include "main.h" //#include "ad5272_i2c.h" #include "mcp4725_i2c.h" #include #include "debug_print.h" #include "i2c_manager.h" /*============================================================================*/ /* Configuration Constants - Charles KWON */ /*============================================================================*/ /** * @brief Total number of LEDs in the system * - Charles KWON * * Currently using 24 LEDs (LED0-LED23) for single PD configuration. * Can be extended to 48 for dual PD configuration. */ #define LED_NUM 24 /** @brief Timer interval for BLE data transmission (ms) */ #define MEA_SEND_LOOP_INTERVAL 100 /*============================================================================*/ /* State Variables - Charles KWON */ /*============================================================================*/ /** * @brief Currently activated LED index * - Charles KWON * * Tracks which LED is currently turned on. * Value 99 indicates no LED is active (all off). */ uint8_t activated_led = 99; /** * @brief Currently activated photodetector index * - Charles KWON * * Tracks which PD channel is currently selected. * Value 99 indicates no PD is active (all off). */ uint8_t activated_pd = 99; /*============================================================================*/ /* Timer Definitions - Charles KWON */ /*============================================================================*/ /** @brief Timer for sequenced BLE data transmission */ APP_TIMER_DEF(m_mea_send_loop_timer_id); /** @brief Counter for BLE transmission sequence */ static uint8_t cnt = 0; /*============================================================================*/ /* Communication Buffers - Charles KWON */ /*============================================================================*/ /** @brief TX buffer for BLE data transmission */ char mea_tx_buffer[BLE_NUS_MAX_DATA_LEN]; /** @brief External flag indicating command processing state */ extern volatile bool processing; /*============================================================================*/ /* LED-PD Gain Calibration Table - Charles KWON */ /*============================================================================*/ /** * @brief DAC values for LED-PD gain matching * - Charles KWON * * Each LED has a corresponding DAC value that sets the optimal * PD amplifier gain for that LED-PD pair. Values are calibrated * during AGC (Automatic Gain Control) and stored in EEPROM. * * DAC Range: * - Minimum: 125 (0.1V output) * - Maximum: 1366 (1.1V output) * - Default: 1000 (mid-range) * * This array is populated from EEPROM at startup or after AGC. * Defined in device_config.c */ extern uint16_t led_pd_dac_v[]; /** * @brief DAC value when all LEDs are off * - Charles KWON * * Used as baseline/reference gain when no LED is active. */ uint16_t led_off_dac_v = 1360; /** * @brief Default PD channel when LED is off * - Charles KWON */ uint8_t led_off_pd = 0; /*============================================================================*/ /* BLE Data Transmission Functions - Charles KWON */ /*============================================================================*/ /** * @brief Start LED-PD gain array printout via BLE * - Charles KWON * * Initiates sequenced transmission of the led_pd_dac_v[] array * over BLE. Data is sent in chunks to comply with BLE MTU limits. */ void led_pd_gain_array_printout(void) { cnt = 0; mea_send_timer_start(); } /** * @brief Timer callback for sending LED-PD gain data via BLE * - Charles KWON * * Called periodically to transmit gain calibration data. * Sends data in 3 chunks (cnt 0-2), then outputs debug info * and clears the processing flag. * * Output Format: "Tag-Currrnt [cnt], [dac0], [dac1], ..., [dac23]\r\n" * * @param[in] p_context Unused timer context */ void mea_send_loop(void *p_context) { UNUSED_PARAMETER(p_context); DBG_PRINTF("\r\n1CNT =%d\r\n", cnt); mea_send_timer_stop(); if (cnt < 6) { uint8_t i = (int)(cnt); if (cnt < 3) { /* Build string dynamically for easy LED_NUM changes */ int offset = sprintf(mea_tx_buffer, "Tag-Currrnt %d", i); for (uint8_t j = 0; j < LED_NUM; j++) { offset += sprintf(mea_tx_buffer + offset, ",\t%d", led_pd_dac_v[j]); } sprintf(mea_tx_buffer + offset, "\r\n"); data_tx_handler(mea_tx_buffer); } DBG_PRINTF("\r\n2CNT =%d\r\n", cnt); mea_send_timer_start(); } else { /* Final iteration: print debug summary */ DBG_PRINTF("\r\nLED-PD Gain Array =\r\n"); for (uint16_t j = 0; j < LED_NUM; j++) { DBG_PRINTF("%d,\t", led_pd_dac_v[j]); } DBG_PRINTF("\r\n"); processing = false; } cnt++; } /*============================================================================*/ /* LED Control Functions - Charles KWON */ /*============================================================================*/ /** * @brief Turn on specified LED * - Charles KWON * * Activates the specified LED through the CAT9532 LED driver. * Also enables the analog switch trigger for signal routing. * * @param[in] led_index LED index (0-47 for specific LED, <=100 for valid range) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on failure * * @note Sets activated_led to track current LED state * @note Calls trig_SW(true) to enable signal path */ ret_code_t led_on(uint8_t led_index) { uint32_t err_code = NRF_SUCCESS; #if FEATURE_PRINTF DBG_PRINTF("\tled_on, %d =====================\r\n", led_index); #endif if (led_index <= 100) { activated_led = led_index; trig_SW(true); /* Enable analog switch for signal routing */ if (NRF_SUCCESS != led_select(led_index)) { DBG_PRINTF("ERR!!! LED Select\r\n"); return NRF_ERROR_INTERNAL; } } else { DBG_PRINTF("ERR!!! led_index Failed! %d\r\n", led_index); return NRF_ERROR_INTERNAL; } return err_code; } /** * @brief Turn off LED * - Charles KWON * * Deactivates LED and disables the analog switch trigger. * * @param[in] led_index 99 = all LEDs off, 98 = clear state * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on invalid index * * @note Calls trig_SW(false) to disable signal path */ ret_code_t led_off(uint8_t led_index) { uint32_t err_code = NRF_SUCCESS; trig_SW(false); /* Disable analog switch */ #if FEATURE_PRINTF DBG_PRINTF("led_off\r\n"); #endif if (led_index == 99) { LED_ALLOFF(); /* Turn off all LEDs */ activated_led = 99; } else if (led_index == 98) { LED99(); /* Clear LED state */ DBG_PRINTF("clear \r\n"); activated_led = 98; } else { DBG_PRINTF("ERR!!! led_index Failed! %d\r\n", led_index); return NRF_ERROR_INTERNAL; } return err_code; } /*============================================================================*/ /* Gain Control Functions - Charles KWON */ /*============================================================================*/ /** * @brief Set modulation gain via MCP4725 DAC * - Charles KWON * * Configures the DAC for lock-in amplifier modulation mode. * Used when measuring with the MOD photodetector channel. * * @param[in] mod_gain DAC value (0-2000) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on failure * * @note Initializes SW I2C if not already done * @note Activates MOD channel (PD index 2) */ ret_code_t led_pd_mod_set(uint16_t mod_gain) { uint32_t err_code = NRF_SUCCESS; if (mod_gain <= 2000) { sw_i2c_init_once(); /* Ensure I2C is initialized */ mcp4725_writeFastMode(mod_gain); if (NRF_SUCCESS != pd_on(2)) { /* Enable MOD channel */ DBG_PRINTF("ERR!!! MOD_on\r\n"); return NRF_ERROR_INTERNAL; } } else { DBG_PRINTF("ERR!!! lockin Test Failed!\r\n"); return NRF_ERROR_INTERNAL; } return err_code; } /** * @brief Set PD gain matching value for specified LED * - Charles KWON * * Configures the PD amplifier gain based on the LED index. * Automatically selects the appropriate PD channel: * - LED 0-23 -> PD0 * - LED 24-47 -> PD1 * - LED 99 -> Default PD (led_off_pd) * * @param[in] led_index LED index for gain lookup * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on failure */ ret_code_t led_pd_matching_value_set(uint8_t led_index) { uint32_t err_code = NRF_SUCCESS; if (led_index <= 100) { /* Set DAC gain from lookup table */ if (NRF_SUCCESS != pd_gain_set(led_index)) { DBG_PRINTF("ERR!!! pd_gain_set\r\n"); return NRF_ERROR_INTERNAL; } /* Determine PD channel based on LED index */ uint8_t pd_index; if (led_index < 24) { pd_index = 0; /* PD0 for LED 0-23 */ } else if ((led_index < 48) && (led_index > 23)) { pd_index = 1; /* PD1 for LED 24-47 */ } else if (led_index == 99) { pd_index = led_off_pd; /* Default PD when off */ } /* Activate the selected PD channel */ if (NRF_SUCCESS != pd_on(pd_index)) { DBG_PRINTF("ERR!!! pd_on\r\n"); return NRF_ERROR_INTERNAL; } } else { DBG_PRINTF("ERR!!! led_indexFailed! %d\r\n", led_index); return NRF_ERROR_INTERNAL; } return err_code; } /*============================================================================*/ /* Photodetector Control Functions - Charles KWON */ /*============================================================================*/ /** * @brief Turn on specified photodetector * - Charles KWON * * Activates one of the 4 PD channels via FSA5157P6X analog switch. * * PD Channels: * - 0: PD0 (for LED 0-23) * - 1: PD1 (for LED 24-47) * - 2: MOD (lock-in modulation) * - 3: MOD2 (secondary modulation) * * @param[in] pd_index Photodetector index (0-3) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on failure */ ret_code_t pd_on(uint8_t pd_index) { uint32_t err_code = NRF_SUCCESS; #if FEATURE_FOR_SCOPE /* Debug pulse for oscilloscope timing */ nrf_gpio_pin_set(PD_CLK_26); nrf_gpio_pin_clear(PD_CLK_26); #endif #if FEATURE_PRINTF DBG_PRINTF("pd_on, %d ===== \r\n", pd_index); #endif if (pd_index <= 3) { activated_pd = pd_index; if (NRF_SUCCESS != pd_select(pd_index)) { DBG_PRINTF("ERR!!! pd_on Failed! %d\r\n", pd_index); return NRF_ERROR_INTERNAL; } } else { DBG_PRINTF("ERR!!! pd_index Failed! %d\r\n", pd_index); return NRF_ERROR_INTERNAL; } return err_code; } /** * @brief Turn off photodetector * - Charles KWON * * Deactivates all PD channels. Only accepts index 99 for safety. * * @param[in] pd_index Must be 99 for all-off operation * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on invalid index */ ret_code_t pd_off(uint8_t pd_index) { uint32_t err_code = NRF_SUCCESS; #if FEATURE_PRINTF DBG_PRINTF("pd_off\r\n"); #endif if (pd_index == 99) { PD_ALLOFF(); activated_pd = 99; } else { DBG_PRINTF("ERR!!! PD Off Failed! %d\r\n", pd_index); return NRF_ERROR_INTERNAL; } return err_code; } /*============================================================================*/ /* LED Power EEPROM Functions - Charles KWON */ /*============================================================================*/ /** * @brief Read all 24 LED power values from ROM * - Charles KWON * * Reads LED power calibration values from DS3930 EEPROM. * Each read has 1ms delay for EEPROM access timing. * * @param[out] data Output buffer for 24 uint16_t values */ void led_power_read_48(uint16_t *data) { for (uint8_t i = 0; i < 24; i++) { data[i] = LED_READ_ROM(i); nrf_delay_ms(1); /* EEPROM access delay */ } } /** * @brief Read single LED power value from ROM * - Charles KWON * * @param[in] led_index LED index (0-47) * @return LED power value, or NRF_ERROR_INTERNAL on invalid index */ int16_t led_power_read(uint8_t led_index) { int16_t led_power; if (led_index <= 47) { led_power = LED_READ_ROM(led_index); } else { DBG_PRINTF("ERR!!! led_index Failed! %d\r\n", led_index); return NRF_ERROR_INTERNAL; } return led_power; } /** * @brief Save LED power value to ROM * - Charles KWON * * Writes LED power calibration value to DS3930 EEPROM. * * @param[in] led_index LED index (0-47) * @param[in] led_power Power value (0-255) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on failure */ ret_code_t led_power_save_mem(uint8_t led_index, int16_t led_power) { uint32_t err_code = NRF_SUCCESS; if ((led_index <= 47) || (led_power <= 255)) { if (NRF_SUCCESS != LED_WRITE_ROM(led_index, led_power)) { DBG_PRINTF("ERR!!! DS3930 1\r\n"); err_code = NRF_ERROR_INTERNAL; } } else { DBG_PRINTF("ERR!!! led_index || pd_index Failed! %d, %d\r\n", led_index, led_power); return NRF_ERROR_INTERNAL; } return err_code; } /** * @brief Save same LED power value to all 48 LEDs * - Charles KWON * * Writes identical power value to all LED EEPROM locations. * Useful for factory reset or uniform calibration. * * @param[in] led_index Unused parameter * @param[in] led_power Power value to set for all LEDs * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on any write failure * * @note 10ms delay between writes for EEPROM programming time */ ret_code_t led_power_save_mem_6(uint8_t led_index, int16_t led_power) { uint32_t err_code = NRF_SUCCESS; for (uint8_t i = 0; i < 48; i++) { if (NRF_SUCCESS != LED_WRITE_ROM(i, led_power)) { DBG_PRINTF("ERR!!! DS3930 1\r\n"); err_code = NRF_ERROR_INTERNAL; } nrf_delay_ms(10); /* EEPROM programming delay */ } return err_code; } /** * @brief Save array of LED power values to all 48 LEDs * - Charles KWON * * Writes individual power values for each LED from input array. * * @param[in] led_power Array of 48 power values (uint8_t) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on any write failure * * @note 10ms delay between writes for EEPROM programming time */ ret_code_t led_power_save_mem_48(uint8_t *led_power) { uint32_t err_code = NRF_SUCCESS; for (uint8_t i = 0; i < 48; i++) { if (NRF_SUCCESS != LED_WRITE_ROM(i, (int16_t)(led_power[i]))) { DBG_PRINTF("ERR!!! DS3930 1\r\n"); err_code = NRF_ERROR_INTERNAL; } nrf_delay_ms(10); /* EEPROM programming delay */ } return err_code; } /** * @brief Set LED power (placeholder function) * - Charles KWON * * Currently not implemented. Reserved for future LED current control. * * @param[in] led_index LED index * @return NRF_SUCCESS always */ ret_code_t led_power_set(uint8_t led_index) { uint32_t err_code = NRF_SUCCESS; return err_code; } /*============================================================================*/ /* DAC Gain Control Functions - Charles KWON */ /*============================================================================*/ /** * @brief Set PD gain via MCP4725 DAC based on LED-PD matching table * - Charles KWON * * Looks up the calibrated DAC value for the specified LED from * led_pd_dac_v[] array and programs the MCP4725 DAC. * * @param[in] activated_led LED index for gain lookup (0-47, 99=off) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on invalid index * * @note Uses SW I2C for MCP4725 communication * @note LED 99 uses led_off_dac_v as default gain */ ret_code_t pd_gain_set(uint8_t activated_led) { uint32_t err_code = NRF_SUCCESS; #if FEATURE_PRINTF DBG_PRINTF("<>\r\n", activated_led, pd_index); #endif if (activated_led <= 47) { sw_i2c_init_once(); /* Ensure I2C is ready */ mcp4725_writeFastMode(led_pd_dac_v[activated_led]); } else if (activated_led == 99) { mcp4725_writeFastMode(led_off_dac_v); /* Use default off-state gain */ } else { DBG_PRINTF("ERR!!! led_index Failed! %d\r\n", activated_led); return NRF_ERROR_INTERNAL; } return err_code; } /** * @brief Set immediate DAC gain value * - Charles KWON * * Directly programs the MCP4725 DAC with specified value. * Used for manual gain adjustment and testing. * * @param[in] imm_dac DAC value (0-2000) * @return NRF_SUCCESS on success, NRF_ERROR_INTERNAL on out-of-range */ ret_code_t imm_gain_set(uint16_t imm_dac) { uint32_t err_code = NRF_SUCCESS; if (imm_dac <= 2000) { sw_i2c_init_once(); mcp4725_writeFastMode(imm_dac); DBG_PRINTF("dac_v %d\r\n", imm_dac); } else { DBG_PRINTF("ERR!!! range Failed! %d\r\n", activated_led); return NRF_ERROR_INTERNAL; } return err_code; } /*============================================================================*/ /* Hardware Selection Functions - Charles KWON */ /*============================================================================*/ /** * @brief Select and activate specific LED via GPIO multiplexer * - Charles KWON * * Programs the CAT9532 LED driver to activate the specified LED. * LED selection is done via I2C commands to set the appropriate * output pins on the LED driver IC. * * @param[in] led_index LED index (0-47 for specific LED, 99=clear/off) * @return NRF_SUCCESS on success, NRF_ERROR_NOT_FOUND on invalid index * * @note 1ms delay after selection for GPIO stabilization * @note LED macros (LED0, LED1, etc.) are defined in LED_Parse.h */ ret_code_t led_select(uint8_t led_index) { uint32_t err_code = NRF_SUCCESS; switch (led_index) { case 0: LED0(); break; case 1: LED1(); break; case 2: LED2(); break; case 3: LED3(); break; case 4: LED4(); break; case 5: LED5(); break; case 6: LED6(); break; case 7: LED7(); break; case 8: LED8(); break; case 9: LED9(); break; case 10: LED10(); break; case 11: LED11(); break; case 12: LED12(); break; case 13: LED13(); break; case 14: LED14(); break; case 15: LED15(); break; case 16: LED16(); break; case 17: LED17(); break; case 18: LED18(); break; case 19: LED19(); break; case 20: LED20(); break; case 21: LED21(); break; case 22: LED22(); break; case 23: LED23(); break; case 24: LED24(); break; case 25: LED25(); break; case 26: LED26(); break; case 27: LED27(); break; case 28: LED28(); break; case 29: LED29(); break; case 30: LED30(); break; case 31: LED31(); break; case 32: LED32(); break; case 33: LED33(); break; case 34: LED34(); break; case 35: LED35(); break; case 36: LED36(); break; case 37: LED37(); break; case 38: LED38(); break; case 39: LED39(); break; case 40: LED40(); break; case 41: LED41(); break; case 42: LED42(); break; case 43: LED43(); break; case 44: LED44(); break; case 45: LED45(); break; case 46: LED46(); break; case 47: LED47(); break; case 99: LED99(); break; /* All LEDs off / clear state */ default: err_code = NRF_ERROR_NOT_FOUND; break; } /* GPIO stabilization delay */ nrf_delay_ms(1); return err_code; } /** * @brief Select photodetector channel * - Charles KWON * * Programs the FSA5157P6X analog switch to route the selected * PD output to the ADC input. * * PD Channel Mapping: * - 0: PD0 - Primary photodetector (LED 0-23) * - 1: PD1 - Secondary photodetector (LED 24-47) * - 2: MOD - Lock-in amplifier modulation channel * - 3: MOD2 - Secondary modulation channel * * @param[in] pd_index Photodetector index (0-3) * @return NRF_SUCCESS on success, NRF_ERROR_NOT_FOUND on invalid index * * @note All PD channels are turned off before selecting new one */ ret_code_t pd_select(uint8_t pd_index) { uint32_t err_code = NRF_SUCCESS; PD_ALLOFF(); /* Ensure clean state before switching */ switch (pd_index) { case 0: PD0(); break; /* Photodetector 0 */ case 1: PD1(); break; /* Photodetector 1 */ case 2: MOD(); break; /* Modulation channel */ case 3: MOD2(); break; /* Secondary modulation */ default: err_code = NRF_ERROR_NOT_FOUND; break; } return err_code; } /*============================================================================*/ /* Timer Control Functions - Charles KWON */ /*============================================================================*/ /** * @brief Start measurement send timer * - Charles KWON * * Starts the single-shot timer for sequenced BLE data transmission. */ void mea_send_timer_start(void) { APP_ERROR_CHECK(app_timer_start(m_mea_send_loop_timer_id, APP_TIMER_TICKS(MEA_SEND_LOOP_INTERVAL), NULL)); } /** * @brief Stop measurement send timer * - Charles KWON */ void mea_send_timer_stop(void) { APP_ERROR_CHECK(app_timer_stop(m_mea_send_loop_timer_id)); } /** * @brief Initialize measurement send timer * - Charles KWON * * Creates the single-shot timer used for sequenced BLE transmission. * Must be called during system initialization. */ void mea_send_timer_init(void) { APP_ERROR_CHECK(app_timer_create(&m_mea_send_loop_timer_id, APP_TIMER_MODE_SINGLE_SHOT, mea_send_loop)); }