diff --git a/ESP32_CAN_Logger.ino b/ESP32_CAN_Logger.ino index ee6c226..db33485 100644 --- a/ESP32_CAN_Logger.ino +++ b/ESP32_CAN_Logger.ino @@ -1,11 +1,13 @@ /* - * Byun CAN Logger with Web Interface + Time Synchronization - * Version: 1.2 + * Byun CAN Logger with Web Interface + RTC Time Synchronization + * Version: 1.3 + * Added: DS3231 RTC support via SoftWire (I2C2: SDA=GPIO25, SCL=GPIO26) */ #include #include #include +#include #include #include #include @@ -21,7 +23,6 @@ #include "graph.h" #include "graph_viewer.h" - // GPIO 핀 정의 #define CAN_INT_PIN 27 @@ -37,6 +38,11 @@ #define VSPI_SCLK 18 #define VSPI_CS 5 +// I2C2 핀 (RTC DS3231) - SoftWire 사용 +#define RTC_SDA 25 +#define RTC_SCL 26 +#define DS3231_ADDRESS 0x68 + // 버퍼 설정 #define CAN_QUEUE_SIZE 1000 #define FILE_BUFFER_SIZE 8192 @@ -44,6 +50,9 @@ #define RECENT_MSG_COUNT 100 #define MAX_TX_MESSAGES 20 +// RTC 동기화 설정 +#define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화 + // CAN 메시지 구조체 - 마이크로초 단위 타임스탬프 struct CANMessage { uint64_t timestamp_us; // 마이크로초 단위 Unix timestamp @@ -75,7 +84,9 @@ struct TimeSyncStatus { uint64_t lastSyncTime; int32_t offsetUs; uint32_t syncCount; -} timeSyncStatus = {false, 0, 0, 0}; + bool rtcAvailable; + uint32_t rtcSyncCount; +} timeSyncStatus = {false, 0, 0, 0, false, 0}; // WiFi AP 설정 const char* ssid = "Byun_CAN_Logger"; @@ -91,9 +102,11 @@ WebSocketsServer webSocket = WebSocketsServer(81); QueueHandle_t canQueue; SemaphoreHandle_t sdMutex; +SemaphoreHandle_t rtcMutex; TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL; +TaskHandle_t rtcTaskHandle = NULL; volatile bool loggingEnabled = false; volatile bool sdCardReady = false; @@ -102,6 +115,10 @@ char currentFilename[MAX_FILENAME_LEN]; uint8_t fileBuffer[FILE_BUFFER_SIZE]; uint16_t bufferIndex = 0; +// RTC 관련 +SoftWire rtcWire(RTC_SDA, RTC_SCL); +char rtcSyncBuffer[20]; + // CAN 속도 설정 CAN_SPEED currentCanSpeed = CAN_1000KBPS; const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"}; @@ -118,6 +135,130 @@ uint32_t lastMsgCount = 0; TxMessage txMessages[MAX_TX_MESSAGES]; uint32_t totalTxCount = 0; +// ======================================== +// RTC 관련 함수 +// ======================================== + +// BCD to Decimal 변환 +uint8_t bcd2dec(uint8_t val) { + return ((val / 16 * 10) + (val % 16)); +} + +// Decimal to BCD 변환 +uint8_t dec2bcd(uint8_t val) { + return ((val / 10 * 16) + (val % 10)); +} + +// DS3231에서 시간 읽기 (Non-blocking, SoftWire 사용) +bool readRTC(struct tm* timeinfo) { + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(10)) != pdTRUE) { + return false; // Mutex를 얻지 못하면 실패 반환 + } + + rtcWire.beginTransmission(DS3231_ADDRESS); + rtcWire.write(0x00); // 초 레지스터부터 시작 + + if (rtcWire.endTransmission() != 0) { + xSemaphoreGive(rtcMutex); + return false; + } + + rtcWire.requestFrom(DS3231_ADDRESS, 7); + + if (rtcWire.available() < 7) { + xSemaphoreGive(rtcMutex); + return false; + } + + uint8_t seconds = bcd2dec(rtcWire.read() & 0x7F); + uint8_t minutes = bcd2dec(rtcWire.read()); + uint8_t hours = bcd2dec(rtcWire.read() & 0x3F); + rtcWire.read(); // day of week (skip) + uint8_t day = bcd2dec(rtcWire.read()); + uint8_t month = bcd2dec(rtcWire.read()); + uint8_t year = bcd2dec(rtcWire.read()); + + xSemaphoreGive(rtcMutex); + + timeinfo->tm_sec = seconds; + timeinfo->tm_min = minutes; + timeinfo->tm_hour = hours; + timeinfo->tm_mday = day; + timeinfo->tm_mon = month - 1; // 0-11 + timeinfo->tm_year = year + 100; // years since 1900 + + return true; +} + +// DS3231에 시간 설정 (Non-blocking, SoftWire 사용) +bool writeRTC(const struct tm* timeinfo) { + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(10)) != pdTRUE) { + return false; + } + + rtcWire.beginTransmission(DS3231_ADDRESS); + rtcWire.write(0x00); // 초 레지스터부터 시작 + rtcWire.write(dec2bcd(timeinfo->tm_sec)); + rtcWire.write(dec2bcd(timeinfo->tm_min)); + rtcWire.write(dec2bcd(timeinfo->tm_hour)); + rtcWire.write(dec2bcd(1)); // day of week (1-7) + rtcWire.write(dec2bcd(timeinfo->tm_mday)); + rtcWire.write(dec2bcd(timeinfo->tm_mon + 1)); + rtcWire.write(dec2bcd(timeinfo->tm_year - 100)); + + bool success = (rtcWire.endTransmission() == 0); + + xSemaphoreGive(rtcMutex); + + return success; +} + +// RTC 초기화 확인 +bool initRTC() { + rtcWire.begin(); + rtcWire.setTimeout(100); // 100ms timeout + + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) { + return false; + } + + rtcWire.beginTransmission(DS3231_ADDRESS); + bool available = (rtcWire.endTransmission() == 0); + + xSemaphoreGive(rtcMutex); + + if (available) { + Serial.println("✓ DS3231 RTC 감지됨"); + + // RTC에서 시간 읽어서 시스템 시간 초기 설정 + struct tm rtcTime; + if (readRTC(&rtcTime)) { + time_t t = mktime(&rtcTime); + struct timeval tv = { .tv_sec = t, .tv_usec = 0 }; + settimeofday(&tv, NULL); + + timeSyncStatus.synchronized = true; + timeSyncStatus.rtcAvailable = true; + timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); + timeSyncStatus.rtcSyncCount++; + + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &rtcTime); + Serial.printf("⏰ RTC에서 시간 로드: %s (RTC 동기화 횟수: %u)\n", + timeStr, timeSyncStatus.rtcSyncCount); + } + } else { + Serial.println("✗ DS3231 RTC를 찾을 수 없음 (웹 동기화 사용)"); + timeSyncStatus.rtcAvailable = false; + } + + return available; +} + +// ======================================== +// 시간 관련 함수 +// ======================================== + // 정밀한 현재 시간 가져오기 (마이크로초) uint64_t getMicrosecondTimestamp() { struct timeval tv; @@ -125,7 +266,7 @@ uint64_t getMicrosecondTimestamp() { return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec; } -// 시간 동기화 설정 +// 웹에서 시간 동기화 설정 void setSystemTime(uint64_t timestampMs) { struct timeval tv; tv.tv_sec = timestampMs / 1000; @@ -143,8 +284,58 @@ void setSystemTime(uint64_t timestampMs) { char timeStr[64]; strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); - Serial.printf("⏰ 시간 동기화 완료: %s.%03d (동기화 횟수: %u)\n", + Serial.printf("⏰ 웹 시간 동기화 완료: %s.%03d (동기화 횟수: %u)\n", timeStr, (int)(tv.tv_usec / 1000), timeSyncStatus.syncCount); + + // RTC가 있으면 RTC도 업데이트 + if (timeSyncStatus.rtcAvailable) { + if (writeRTC(&timeinfo)) { + Serial.println("✓ RTC 시간도 함께 업데이트됨"); + } + } +} + +// RTC 동기화 Task (최저 우선순위, 로깅에 영향 없음) +void rtcSyncTask(void *pvParameters) { + Serial.println("RTC 동기화 Task 시작"); + + TickType_t lastSyncTick = xTaskGetTickCount(); + + while (1) { + // RTC_SYNC_INTERVAL_MS 마다 동기화 + vTaskDelayUntil(&lastSyncTick, pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS)); + + if (!timeSyncStatus.rtcAvailable) { + continue; // RTC가 없으면 스킵 + } + + struct tm rtcTime; + if (readRTC(&rtcTime)) { + // RTC 시간을 시스템 시간으로 설정 + time_t t = mktime(&rtcTime); + struct timeval tv = { .tv_sec = t, .tv_usec = 0 }; + + // 기존 시스템 시간과의 오차 계산 + struct timeval currentTv; + gettimeofday(¤tTv, NULL); + int64_t driftUs = (int64_t)(tv.tv_sec - currentTv.tv_sec) * 1000000LL + + (int64_t)(tv.tv_usec - currentTv.tv_usec); + + // 1분마다 무조건 보정 + settimeofday(&tv, NULL); + + timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); + timeSyncStatus.rtcSyncCount++; + timeSyncStatus.offsetUs = (int32_t)(driftUs / 1000); // ms 단위로 저장 + + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &rtcTime); + Serial.printf("⏰ RTC 자동 보정: %s (오차: %ld ms, 보정 횟수: %u)\n", + timeStr, timeSyncStatus.offsetUs, timeSyncStatus.rtcSyncCount); + } else { + Serial.println("⚠️ RTC 읽기 실패"); + } + } } // 함수 선언 @@ -219,7 +410,7 @@ bool createNewLogFile() { // 시간 동기화 경고 if (!timeSyncStatus.synchronized) { - Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); + Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); } return true; @@ -560,7 +751,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length // 시간 동기화 상태 전송 String syncStatus = "{\"type\":\"timeSyncStatus\",\"synchronized\":"; syncStatus += timeSyncStatus.synchronized ? "true" : "false"; - syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount) + "}"; + syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount); + syncStatus += ",\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false"); + syncStatus += ",\"rtcSyncCount\":" + String(timeSyncStatus.rtcSyncCount) + "}"; webSocket.sendTXT(num, syncStatus); } break; @@ -660,6 +853,8 @@ void webUpdateTask(void *pvParameters) { status += "\"msgSpeed\":" + String(msgPerSecond) + ","; status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ","; status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ","; + status += "\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false") + ","; + status += "\"rtcSyncCount\":" + String(timeSyncStatus.rtcSyncCount) + ","; if (loggingEnabled && logFile) { status += "\"currentFile\":\"" + String(currentFilename) + "\""; @@ -722,7 +917,7 @@ void setup() { Serial.begin(115200); delay(1000); Serial.println("\n========================================"); - Serial.println(" ESP32 CAN Logger with Time Sync "); + Serial.println(" ESP32 CAN Logger with RTC Time Sync "); Serial.println("========================================"); memset(recentData, 0, sizeof(recentData)); @@ -730,14 +925,17 @@ void setup() { pinMode(CAN_INT_PIN, INPUT_PULLUP); + // SPI 초기화 hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); + // MCP2515 초기화 mcp2515.reset(); mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); mcp2515.setNormalMode(); Serial.println("✓ MCP2515 초기화 완료"); + // SD 카드 초기화 if (SD.begin(VSPI_CS, vspi)) { sdCardReady = true; Serial.println("✓ SD 카드 초기화 완료"); @@ -745,13 +943,23 @@ void setup() { Serial.println("✗ SD 카드 초기화 실패"); } + // Mutex 생성 + sdMutex = xSemaphoreCreateMutex(); + rtcMutex = xSemaphoreCreateMutex(); + + // RTC 초기화 (SoftWire 사용) + initRTC(); + + // WiFi AP 시작 WiFi.softAP(ssid, password); Serial.print("✓ AP IP: "); Serial.println(WiFi.softAPIP()); + // WebSocket 시작 webSocket.begin(); webSocket.onEvent(webSocketEvent); + // 웹 서버 라우팅 server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", index_html); }); @@ -795,21 +1003,29 @@ void setup() { }); server.begin(); + // Queue 생성 canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); - sdMutex = xSemaphoreCreateMutex(); - if (canQueue == NULL || sdMutex == NULL) { + if (canQueue == NULL || sdMutex == NULL || rtcMutex == NULL) { Serial.println("✗ RTOS 객체 생성 실패!"); while (1) delay(1000); } + // CAN 인터럽트 활성화 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + // Task 생성 (우선순위: CAN RX > SD Write > Web > RTC Sync) xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 4, &canRxTaskHandle, 1); xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1); xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); + // RTC 동기화 Task (최저 우선순위 - 우선순위 0) + if (timeSyncStatus.rtcAvailable) { + xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0); + Serial.println("✓ RTC 자동 동기화 Task 시작 (1분 주기)"); + } + Serial.println("✓ 모든 태스크 시작 완료"); Serial.println("\n========================================"); Serial.println(" 웹 인터페이스 접속"); @@ -822,7 +1038,12 @@ void setup() { Serial.println(" - Transmit: /transmit"); Serial.println(" - Graph: /graph"); Serial.println("========================================\n"); - Serial.println("⚠️ 시간 동기화를 위해 웹페이지에서 '⏰ 시간 동기화' 버튼을 클릭하세요"); + + if (timeSyncStatus.rtcAvailable) { + Serial.println("✓ RTC 모듈 감지됨 - 자동 시간 보정 활성화"); + } else { + Serial.println("⚠️ RTC 모듈 없음 - 웹페이지에서 '⏰ 시간 동기화' 버튼을 클릭하세요"); + } } void loop() { @@ -831,12 +1052,14 @@ void loop() { static uint32_t lastPrint = 0; if (millis() - lastPrint > 10000) { - Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s\n", + Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s | RTC: %s(%u)\n", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, loggingEnabled ? "ON " : "OFF", sdCardReady ? "OK" : "NO", totalMsgCount, totalTxCount, - timeSyncStatus.synchronized ? "OK" : "NO"); + timeSyncStatus.synchronized ? "OK" : "NO", + timeSyncStatus.rtcAvailable ? "OK" : "NO", + timeSyncStatus.rtcSyncCount); lastPrint = millis(); } } \ No newline at end of file