/******************************************************************************* * @file battery_adc.c * @brief Battery voltage ADC measurement * * ADC 채널 설정은 디바이스트리 overlay에서 관리: * - AIN2 (P0.04), Single-ended * - 12-bit 해상도, 1/6 gain, 4X oversample, 40us acq time * * 변환 공식: mV = (ADC × 600 / 4095) × 6 × 1.42 * → 프로젝트 고유 분압비(1.42x) 때문에 adc_raw_to_millivolts_dt() 사용 불가 * * 동작 모드: * - 단독 측정 (msn? 커맨드) → battery_read_mv() 호출 * - 주기 모니터링 (60초) → 저전압 또는 고온 5회 연속 시 자동 전원 OFF * - 측정 중(processing) 또는 IMU FIFO 동작 중에는 주기 모니터링 스킵 * - mbb? 측정 응답 -> parser에서 battery_read_mv() 호출 후 패킷에 포함 ******************************************************************************/ #include #include #include #include #include #include "battery_adc.h" #include "debug_print.h" #include "imu_i2c.h" #include "main.h" /* ADC 디바이스트리 설정 */ #define ZEPHYR_USER_NODE DT_PATH(zephyr_user) /* 디바이스트리에서 ADC 채널 스펙 가져오기 (gain, ref, acq time, input, resolution, oversampling) */ static const struct adc_dt_spec battery_adc = ADC_DT_SPEC_GET_BY_NAME(ZEPHYR_USER_NODE, battery); static int16_t adc_buffer; /* Module variables */ volatile bool battery_saadc_done = false; volatile uint16_t info_batt = 0; static struct k_timer battery_timer; static struct k_work battery_work; /* * 60초 내부 모니터 전용 카운터 * - 앱 측정 명령(mbb?/mtb?)이 없는 일반 상태에서만 사용 * - mbb? 성공 시 mbb? 측정 판정으로 넘어가므로 0으로 초기화 */ static uint8_t periodic_low_battery_cnt; static uint8_t periodic_high_temperature_cnt; /* * 주기 측정(mbb?) 응답값 전용 카운터 * - mbb? 응답에 포함된 배터리/IMU 온도 값으로 판정 * - 측정 실패 시 호출되지 않으므로 기존 카운트 유지 */ static uint8_t continuous_low_battery_cnt; static uint8_t continuous_high_temperature_cnt; /* * 정렬 상태 보호 판정 차단 플래그 * - mls?로 ALIGN_SEARCHING/ALIGN_COMPLETE 진입 시 true * - 정렬 중에는 배터리/온도 보호 판정 자체를 진행하지 않음 */ static bool alignment_mode_active; /* * 최근 mbb? 성공 이후 내부 60초 모니터를 잠시 막는 기준 시각 * - 주기 측정 중에는 mbb? 응답값으로 보호 판정 * - 이 값보다 현재 시간이 작으면 60초 내부 모니터는 skip */ static int64_t continuous_monitor_active_until_ms; #define BATTERY_MONITOR_LIMIT_COUNT 5U // 정렬모드(mtb?)나 주기 측정(mbb?)이 아닌 경우 60초 주기 5회 연속 저전압 감지 시 전원 OFF(5분) #define BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT 10U // 주기 측정 10회 연속 저전압 감지 시 전원 OFF(100초) #define HIGH_TEMPERATURE_CDEG 4000 // 40.00 C #define CONTINUOUS_MONITOR_ACTIVE_MS 15000 // mbb? 10초 주기 대비 여유 #define BATTERY_MONITOR_INTERVAL_MS 60000 // 정렬모드(mtb?)나 주기 측정(mbb?)이 아닌 경우 저전압/고온 모니터 주기 60초 /* * 전압 변환 (ADC raw → mV 변환) * 공식: (raw × 600 / 4095) × 6 × 1.42 * 정수 연산: raw × 600 × 6 × 142 / (4095 × 100) */ static int adc_raw_to_mv(int16_t raw) { if (raw < 0) { raw = 0; } // 오버플로 방지: 600 × 6 × 142 = 511,200 → raw 최대 4095 → 4095 × 511200 = ~2B → int32 OK int32_t mv = ((int32_t)raw * 600 * 6 * 142) / (4095 * 100); return (int)mv; } static bool continuous_monitor_is_active(void) { // 마지막 mbb? 성공 이후 CONTINUOUS_MONITOR_ACTIVE_MS 안쪽이면 주기 측정(mbb?) 중으로 간주 return (continuous_monitor_active_until_ms > 0) && (k_uptime_get() < continuous_monitor_active_until_ms); } /* * mode별 카운터만 전달받아 공통 판정 로직 사용 * - periodic: 60초 내부 모니터, limit 5 * - continuous: mbb? 응답값 기반, limit 10 */ static void record_battery_value(int batt_mv, uint8_t *cnt, uint8_t limit, const char *mode) { if (batt_mv <= LOW_BATTERY_VOLTAGE) { if (*cnt < UINT8_MAX) { (*cnt)++; } DBG_PRINTF("[BATT] LOW! mode=%s cnt=%u/%u mv=%d\r\n", mode, *cnt, limit, batt_mv); if (*cnt >= limit) { *cnt = 0; DBG_ERR("[BATT] %ux low (%s) -> Power OFF\r\n", limit, mode); sleep_mode_enter_reason("battery-low"); } } else { *cnt = 0; } } /* * IMU 온도는 centi-degree 단위 * - 4000 = 40.00 C * - 40.00 C 이상이면 고온 카운트 증가 * - 기준 미만이면 해당 mode 카운터 초기화 */ static void record_temperature_value(int16_t temp_cdeg, uint8_t *cnt, uint8_t limit, const char *mode) { if (temp_cdeg >= HIGH_TEMPERATURE_CDEG) { if (*cnt < UINT8_MAX) { (*cnt)++; } DBG_PRINTF("[TEMP] HIGH! mode=%s cnt=%u/%u temp=%d.%02d C\r\n", mode, *cnt, limit, temp_cdeg / 100, (temp_cdeg < 0 ? -temp_cdeg : temp_cdeg) % 100); if (*cnt >= limit) { *cnt = 0; DBG_ERR("[TEMP] %ux high (%s) -> Power OFF\r\n", limit, mode); sleep_mode_enter_reason("temperature-high"); } } else { *cnt = 0; } } /* Public functions */ void battery_adc_init(void) { // ADC 디바이스 준비 상태 확인 if (!adc_is_ready_dt(&battery_adc)) { DBG_PRINTF("[BATT] ADC device not ready\r\n"); return; } // 디바이스트리 설정으로 채널 구성 int err = adc_channel_setup_dt(&battery_adc); if (err) { DBG_PRINTF("[BATT] Channel setup failed (err %d)\r\n", err); return; } DBG_PRINTF("[BATT] ADC init OK (DT-based, ch=%d, res=%d, os=%d)\r\n", battery_adc.channel_id, battery_adc.resolution, battery_adc.oversampling); } int battery_read_mv(void) { // 매번 채널 재설정 (다른 센서와 SAADC 공유 시 필요) int err = adc_channel_setup_dt(&battery_adc); if (err) { DBG_PRINTF("[BATT] Channel setup failed (err %d)\r\n", err); return -1; } // 디바이스트리에서 시퀀스 초기화 (channels, resolution, oversampling) struct adc_sequence seq = {0}; err = adc_sequence_init_dt(&battery_adc, &seq); if (err) { DBG_PRINTF("[BATT] Sequence init failed (err %d)\r\n", err); return -1; } seq.buffer = &adc_buffer; seq.buffer_size = sizeof(adc_buffer); // ADC 읽기 err = adc_read_dt(&battery_adc, &seq); if (err) { DBG_PRINTF("[BATT] Read failed (err %d)\r\n", err); return -1; } int16_t raw = adc_buffer; int mv = adc_raw_to_mv(raw); //DBG_PRINTF("[BATT] raw=%d mv=%d\r\n", raw, mv); battery_saadc_done = true; return mv; } /* * 정렬 상태 진입/해제는 mls? 명령에서 전달 * - enabled true: 보호 판정 중지 + 기존 누적값 초기화 * - enabled false: 다음 조건부터 다시 판정 가능 */ void battery_protection_set_alignment_mode(bool enabled) { alignment_mode_active = enabled; if (enabled) { periodic_low_battery_cnt = 0; periodic_high_temperature_cnt = 0; continuous_low_battery_cnt = 0; continuous_high_temperature_cnt = 0; } } /* * mbb? 측정 성공 시에만 호출 * - 실패한 측정은 호출되지 않아서 continuous 카운트 유지 * - 정렬 중에는 응답값이 있어도 보호 판정 제외 * - mbb?가 들어온 직후에는 내부 60초 모니터 대신 응답값 기반 판정 사용 */ void battery_protection_record_continuous(int batt_mv, int16_t temp_cdeg) { // 정렬모드 중에는 continuous 보호 판정 제외 if (alignment_mode_active) { return; } // 15초 후 일반 60초 주기 모니터 재개 continuous_monitor_active_until_ms = k_uptime_get() + CONTINUOUS_MONITOR_ACTIVE_MS; // periodic 모니터 카운트 초기화 periodic_low_battery_cnt = 0; periodic_high_temperature_cnt = 0; record_battery_value(batt_mv, &continuous_low_battery_cnt, BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT, "continuous"); record_temperature_value(temp_cdeg, &continuous_high_temperature_cnt, BATTERY_CONTINUOUS_MONITOR_LIMIT_COUNT, "continuous"); } /* k_work 핸들러: 스레드 컨텍스트에서 ADC 읽기 (adc_read는 블로킹이라 ISR 불가) */ static void battery_work_handler(struct k_work *work) { ARG_UNUSED(work); // 일반 60초 주기 보호 판정 제외 구간: 정렬모드 또는 주기 측정 if (alignment_mode_active || continuous_monitor_is_active()) { return; } // I2C 접근 충돌 방지: 긴 측정 명령 처리 중이거나 IMU FIFO 동작 중 if (processing || imu_fifo_is_active()) { return; } int batt = battery_read_mv(); if (batt < 0) { return; } // 정렬모드나 주기 측정 중이 아닌 경우 60초 내부 모니터 판정(배터리) record_battery_value(batt, &periodic_low_battery_cnt, BATTERY_MONITOR_LIMIT_COUNT, "periodic"); int16_t temp_cdeg = 0; int temp_ret = imu_read_temperature_cdeg(&temp_cdeg); if (temp_ret != 0) { DBG_PRINTF("[TEMP] IMU read failed ret=%d\r\n", temp_ret); return; } // 정렬모드나 주기 측정 중이 아닌 경우 60초 내부 모니터 판정(온도) record_temperature_value(temp_cdeg, &periodic_high_temperature_cnt, BATTERY_MONITOR_LIMIT_COUNT, "periodic"); } /* 타이머 ISR → k_work 예약 */ static void battery_timer_handler(struct k_timer *timer) { ARG_UNUSED(timer); k_work_submit(&battery_work); } void battery_timer_init(void) { k_work_init(&battery_work, battery_work_handler); k_timer_init(&battery_timer, battery_timer_handler, NULL); } void battery_timer_start(void) { k_timer_start(&battery_timer, K_MSEC(BATTERY_MONITOR_INTERVAL_MS), K_MSEC(BATTERY_MONITOR_INTERVAL_MS)); } void battery_timer_stop(void) { k_timer_stop(&battery_timer); }