RTC 1분마다 내부 시간 업데이트 softwire
This commit is contained in:
@@ -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(¤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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user