/* ============================================================================= * timesync.c - SNTP 시간 동기 + RTC 반영 (timesync.h 구현) * * 동작: * - timesync_init(): LwIP apps/sntp 를 POLL 모드로 시작(서버=APP_SNTP_SERVER). * - SNTP 응답 수신 시 lwipopts.h 의 SNTP_SET_SYSTEM_TIME(sec) 매크로가 * timesync_apply_epoch() 를 호출 -> Unix 초를 분해해 HAL_RTC_Set{Time,Date}. * - timesync_now(): RTC 를 다시 읽어 Unix 초로 환산. (RTC 미설정 시 0) * * 시간대: 모든 시간은 UTC 로 다룬다. 서버 timestamp / TLS 인증서 유효기간 검증 * 모두 UTC 기준. RTC 에도 UTC 를 저장한다. * * RTC 클럭소스: 기본 LSI(저정밀, 외부 부품 불필요). 보드에 32.768kHz LSE 가 * 있으면 정밀도가 좋다. TODO(hw): 실제 보드에 맞춰 bsp 에서 RTC 클럭 * 소스를 LSE 로 전환(아래 RTC 초기화 가드 참고). * ===========================================================================*/ #include "timesync.h" #include "app_config.h" #include "applog.h" #include "stm32f4xx_hal.h" /* LwIP SNTP 앱 */ #include "lwip/apps/sntp.h" #include "FreeRTOS.h" #include "task.h" #include /* ── 내부 상태 ──────────────────────────────────────────────────────────── */ /* HAL RTC 핸들. bsp 가 RTC 클럭/도메인을 켠 뒤 여기서 Init 한다. */ static RTC_HandleTypeDef s_hrtc; /* 한 번이라도 SNTP 동기가 끝났는지(volatile: 콜백/태스크 경합). */ static volatile int s_is_set = 0; /* RTC 가 HAL_RTC_Init 까지 완료됐는지. */ static int s_rtc_ready = 0; /* RTC 연도 기준(STM32 RTC 는 2자리 연도 BCD: 0..99). 2000년 기준 사용. */ #define TS_RTC_YEAR_BASE 2000 /* RTC 가 표현 가능한 최소 Unix epoch(2000-01-01 00:00:00 UTC). */ #define TS_EPOCH_2000 946684800UL /* ── 시간 분해/합성 유틸 (gmtime/timegm 대체, UTC 고정) ─────────────────── */ static int ts_is_leap(int year) { return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); } /* Unix 초(UTC) -> 분해 시각. 윤년/월일 정확. */ typedef struct { int year; /* 절대 연도, 예: 2026 */ int mon; /* 1..12 */ int mday; /* 1..31 */ int hour; /* 0..23 */ int min; /* 0..59 */ int sec; /* 0..59 */ int wday; /* 1..7, 월=1 ... 일=7 (HAL RTC_WeekDay 호환) */ } ts_tm_t; static void ts_epoch_to_tm(uint32_t epoch, ts_tm_t *out) { static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; uint32_t days = epoch / 86400UL; uint32_t rem = epoch % 86400UL; out->hour = (int)(rem / 3600UL); rem %= 3600UL; out->min = (int)(rem / 60UL); out->sec = (int)(rem % 60UL); /* 1970-01-01 은 목요일(Thursday). HAL 의 RTC_WEEKDAY: 월=1..일=7. * Unix wday(일=0..토=6) -> ISO(월=1..일=7) 로 변환. */ int unix_wday = (int)((days + 4) % 7); /* 0=Sun..6=Sat */ out->wday = (unix_wday == 0) ? 7 : unix_wday; /* 1=Mon..7=Sun */ int year = 1970; while (1) { int ydays = ts_is_leap(year) ? 366 : 365; if (days >= (uint32_t)ydays) { days -= (uint32_t)ydays; year++; } else { break; } } out->year = year; int mon = 0; while (mon < 12) { int dm = mdays[mon]; if (mon == 1 && ts_is_leap(year)) dm = 29; if (days >= (uint32_t)dm) { days -= (uint32_t)dm; mon++; } else { break; } } out->mon = mon + 1; out->mday = (int)days + 1; } /* 분해 시각(UTC) -> Unix 초. */ static uint32_t ts_tm_to_epoch(int year, int mon, int mday, int hour, int min, int sec) { static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; uint32_t days = 0; for (int y = 1970; y < year; y++) { days += ts_is_leap(y) ? 366 : 365; } for (int m = 0; m < (mon - 1); m++) { int dm = mdays[m]; if (m == 1 && ts_is_leap(year)) dm = 29; days += (uint32_t)dm; } days += (uint32_t)(mday - 1); return days * 86400UL + (uint32_t)hour * 3600UL + (uint32_t)min * 60UL + (uint32_t)sec; } /* ── RTC 초기화 ─────────────────────────────────────────────────────────── */ /* RTC 페리페럴 초기화(1회). 클럭소스 선택 + 비동기/동기 프리스케일러로 1Hz. * 반환 0 성공, 음수 실패. */ static int ts_rtc_init(void) { if (s_rtc_ready) { return 0; } /* RTC 백업도메인 접근/클럭소스 설정. * 기본은 LSI(~32kHz, 부품 불필요). LSE 사용 시 아래 가드를 LSE 로 전환. * TODO(hw): 보드에 32.768kHz 크리스털이 있으면 LSE 경로를 활성화. */ RCC_OscInitTypeDef osc = {0}; RCC_PeriphCLKInitTypeDef pclk = {0}; /* 백업도메인 쓰기 보호 해제(PWR 클럭은 bsp 에서 켜졌다고 가정하나, 안전하게 한 번 더). */ __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); #if defined(TS_USE_LSE) /* TODO(hw): LSE 보드면 빌드에 -DTS_USE_LSE 추가 */ osc.OscillatorType = RCC_OSCILLATORTYPE_LSE; osc.PLL.PLLState = RCC_PLL_NONE; osc.LSEState = RCC_LSE_ON; if (HAL_RCC_OscConfig(&osc) != HAL_OK) { return -1; } pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC; pclk.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { return -2; } /* LSE 32768Hz -> async(127)+1, sync(255)+1 => 1Hz */ const uint32_t async_pre = 127; const uint32_t sync_pre = 255; #else osc.OscillatorType = RCC_OSCILLATORTYPE_LSI; osc.PLL.PLLState = RCC_PLL_NONE; osc.LSIState = RCC_LSI_ON; if (HAL_RCC_OscConfig(&osc) != HAL_OK) { return -1; } pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC; pclk.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { return -2; } /* LSI 약 32000Hz -> async(124)+1=125, sync(255)+1=256 => 125*256=32000 -> 1Hz. * LSI 는 오차가 크므로 SNTP 가 주기적으로 보정한다(SNTP_UPDATE_DELAY). */ const uint32_t async_pre = 124; const uint32_t sync_pre = 255; #endif __HAL_RCC_RTC_ENABLE(); s_hrtc.Instance = RTC; s_hrtc.Init.HourFormat = RTC_HOURFORMAT_24; s_hrtc.Init.AsynchPrediv = async_pre; s_hrtc.Init.SynchPrediv = sync_pre; s_hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; s_hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; s_hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; if (HAL_RTC_Init(&s_hrtc) != HAL_OK) { return -3; } s_rtc_ready = 1; return 0; } /* ── 공개 API ───────────────────────────────────────────────────────────── */ void timesync_init(void) { /* RTC 를 먼저 준비(아직 시간은 미설정). SNTP 응답이 와야 실제 시간이 채워진다. */ if (ts_rtc_init() != 0) { LOGE("timesync: RTC init failed (계속: SNTP 동기 시 재시도 안 함)"); /* RTC 가 없어도 SNTP 자체는 동작하지만, 시간 보존 불가. * apply_epoch 에서 다시 init 을 시도한다. */ } sntp_setoperatingmode(SNTP_OPMODE_POLL); sntp_setservername(0, APP_SNTP_SERVER); sntp_init(); LOGI("timesync: SNTP 시작 server=%s", APP_SNTP_SERVER); } void timesync_apply_epoch(uint32_t unix_seconds) { /* 비정상적으로 작은 값(2000년 이전)은 무시: 잘못된 SNTP 응답 방지. */ if (unix_seconds < TS_EPOCH_2000) { LOGW("timesync: epoch 무시(%lu < 2000) ", (unsigned long)unix_seconds); return; } /* 콜백 컨텍스트(tcpip 스레드)에서 호출될 수 있음. RTC 미준비 시 1회 시도. */ if (!s_rtc_ready) { if (ts_rtc_init() != 0) { /* RTC 사용 불가: 플래그만 세워 timesync_is_set 은 만족시키되, * timesync_now 는 RTC 미설정으로 0 을 줄 수 있음. */ s_is_set = 1; return; } } ts_tm_t tm; ts_epoch_to_tm(unix_seconds, &tm); RTC_TimeTypeDef t = {0}; RTC_DateTypeDef d = {0}; t.Hours = (uint8_t)tm.hour; t.Minutes = (uint8_t)tm.min; t.Seconds = (uint8_t)tm.sec; t.TimeFormat = RTC_HOURFORMAT12_AM; /* 24h 모드에선 무시됨 */ t.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; t.StoreOperation = RTC_STOREOPERATION_RESET; d.WeekDay = (uint8_t)tm.wday; /* RTC_WEEKDAY_MONDAY=1 .. SUNDAY=7 */ d.Month = (uint8_t)tm.mon; /* 1..12, BCD 변환은 HAL 이 처리(BIN 포맷) */ d.Date = (uint8_t)tm.mday; d.Year = (uint8_t)(tm.year - TS_RTC_YEAR_BASE); /* 0..99 */ if (HAL_RTC_SetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK || HAL_RTC_SetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) { LOGE("timesync: RTC set 실패"); /* 그래도 동기됐다고 표시: 최소한 한 번 시간이 들어왔음. */ s_is_set = 1; return; } s_is_set = 1; LOGI("timesync: 동기 완료 epoch=%lu (%04d-%02d-%02d %02d:%02d:%02d UTC)", (unsigned long)unix_seconds, tm.year, tm.mon, tm.mday, tm.hour, tm.min, tm.sec); } uint32_t timesync_now(void) { if (!s_is_set || !s_rtc_ready) { return 0; } /* HAL_RTC_GetTime 을 먼저 호출해야 섀도 레지스터가 잠기고, * 이후 GetDate 호출로 잠금이 풀린다(HAL 규약). 순서 준수 필수. */ RTC_TimeTypeDef t = {0}; RTC_DateTypeDef d = {0}; if (HAL_RTC_GetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK) { return 0; } if (HAL_RTC_GetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) { return 0; } int year = TS_RTC_YEAR_BASE + (int)d.Year; return ts_tm_to_epoch(year, (int)d.Month, (int)d.Date, (int)t.Hours, (int)t.Minutes, (int)t.Seconds); } int timesync_is_set(void) { return s_is_set ? 1 : 0; } int timesync_wait(uint32_t timeout_ms) { const uint32_t step_ms = 100; uint32_t waited = 0; while (!s_is_set) { if (timeout_ms != 0 && waited >= timeout_ms) { return -1; } vTaskDelay(pdMS_TO_TICKS(step_ms)); waited += step_ms; } return 0; }