RTC 1분마다 내부 시간 업데이트 softwire

This commit is contained in:
2025-10-12 23:19:35 +00:00
parent f1c989f635
commit 92d9c83971

View File

@@ -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 <Arduino.h>
#include <SPI.h>
#include <mcp2515.h>
#include <SoftWire.h>
#include <SD.h>
#include <WiFi.h>
#include <WebServer.h>
@@ -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(&currentTv, 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();
}
}