From 247a695c6b0e2ca467396c37ccf85d22314ec5df Mon Sep 17 00:00:00 2001 From: byun Date: Tue, 27 Jan 2026 19:17:06 +0000 Subject: [PATCH] =?UTF-8?q?flush=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP32_CAN_Logger-a.ino | 212 ++++++++++++++++++++++++++++++----------- 1 file changed, 157 insertions(+), 55 deletions(-) diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index 3d9377e..9b551e4 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -305,7 +305,17 @@ File serial2LogFile; // ⭐ Serial2 추가 char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가 -uint32_t bufferIndex = 0; // ⭐ uint16_t → uint32_t (오버플로우 방지) + +// ⭐⭐⭐ 더블 버퍼링 변수 +uint8_t* fileBuffer1 = NULL; // 쓰기 버퍼 +uint8_t* fileBuffer2 = NULL; // flush 버퍼 +uint8_t* currentWriteBuffer = NULL; // 현재 쓰기용 +uint8_t* currentFlushBuffer = NULL; // 현재 flush용 +uint32_t writeBufferIndex = 0; // 쓰기 버퍼 인덱스 +uint32_t flushBufferSize = 0; // flush할 크기 +volatile bool flushInProgress = false; // flush 진행중 플래그 +TaskHandle_t sdFlushTaskHandle = NULL; // flush 전용 태스크 + uint32_t serialCsvIndex = 0; // ⭐ uint16_t → uint32_t uint32_t serial2CsvIndex = 0; // ⭐ uint16_t → uint32_t // ⭐ Serial2 추가 volatile uint32_t currentFileSize = 0; @@ -491,13 +501,27 @@ bool initPSRAM() { Serial.printf("✓ PSRAM 총 용량: %d MB\n", ESP.getPsramSize() / 1024 / 1024); Serial.printf("✓ PSRAM 여유: %d KB\n\n", ESP.getFreePsram() / 1024); - // 버퍼 할당 - fileBuffer = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); - if (!fileBuffer) { - Serial.println("✗ fileBuffer 할당 실패"); + // ⭐⭐⭐ 더블 버퍼 할당 + fileBuffer1 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); + if (!fileBuffer1) { + Serial.println("✗ fileBuffer1 할당 실패"); return false; } - Serial.printf("✓ fileBuffer: %d KB\n", FILE_BUFFER_SIZE / 1024); + Serial.printf("✓ fileBuffer1: %d KB\n", FILE_BUFFER_SIZE / 1024); + + fileBuffer2 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); + if (!fileBuffer2) { + Serial.println("✗ fileBuffer2 할당 실패"); + return false; + } + Serial.printf("✓ fileBuffer2: %d KB\n", FILE_BUFFER_SIZE / 1024); + + // 초기 버퍼 포인터 설정 + currentWriteBuffer = fileBuffer1; + currentFlushBuffer = fileBuffer2; + writeBufferIndex = 0; + flushBufferSize = 0; + Serial.println("✓ 더블 버퍼링 초기화 완료"); serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE); if (!serialCsvBuffer) { @@ -844,7 +868,12 @@ void checkAutoTriggers(struct can_frame &frame) { if (logFile) { loggingEnabled = true; autoTriggerActive = true; - bufferIndex = 0; + + // ⭐⭐⭐ 더블 버퍼 초기화 + writeBufferIndex = 0; + flushBufferSize = 0; + flushInProgress = false; + currentFileSize = logFile.size(); Serial.println("🎯 Auto Trigger: 로깅 시작!"); @@ -889,12 +918,19 @@ void checkAutoTriggers(struct can_frame &frame) { if (anyMatch && result) { // 🎯 파일 닫기 로직 추가 if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { - // BIN 형식: 버퍼에 남은 데이터 강제 플러시 - if (bufferIndex > 0 && logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); + // ⭐⭐⭐ 더블 버퍼: 현재 쓰기 버퍼의 남은 데이터 flush + if (writeBufferIndex > 0 && logFile) { + size_t written = logFile.write(currentWriteBuffer, writeBufferIndex); logFile.flush(); - Serial.printf("✓ Auto Trigger 최종 플러시: %d bytes\n", written); - bufferIndex = 0; + Serial.printf("✓ Auto Trigger 최종 쓰기 버퍼 플러시: %d bytes\n", written); + writeBufferIndex = 0; + } + + // ⭐ flush 태스크가 작업 중이면 완료 대기 + int waitCount = 0; + while (flushInProgress && waitCount < 500) { + vTaskDelay(10); + waitCount++; } // CSV 형식: 최종 플러시 @@ -911,7 +947,8 @@ void checkAutoTriggers(struct can_frame &frame) { loggingEnabled = false; autoTriggerActive = false; currentFilename[0] = '\0'; - bufferIndex = 0; + writeBufferIndex = 0; + flushBufferSize = 0; Serial.println("🎯 Auto Trigger: 로깅 중지!"); Serial.printf(" 조건: ID 0x%03X 만족\n", frame.can_id); @@ -1467,6 +1504,55 @@ void canRxTask(void *parameter) { } } +// ======================================== +// SD Write Task +// ======================================== +// ======================================== +// ⭐⭐⭐ SD Flush 전용 태스크 (더블 버퍼링) +// ======================================== +void sdFlushTask(void *parameter) { + Serial.println("✓ sdFlushTask 시작 (더블 버퍼링 모드)"); + + while (1) { + uint32_t notifyValue; + + // flush 알림 대기 (무한 대기) + if (xTaskNotifyWait(0, 0xFFFFFFFF, ¬ifyValue, portMAX_DELAY) == pdTRUE) { + uint32_t flushSize = notifyValue; + + if (flushSize > 0 && flushSize <= FILE_BUFFER_SIZE) { + flushInProgress = true; + + uint32_t startTime = millis(); + + // ⭐ SD 뮤텍스 획득 (최대 5초 대기) + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { + if (logFile && sdCardReady && loggingEnabled) { + // ⭐ flush 버퍼를 SD에 쓰기 + size_t written = logFile.write(currentFlushBuffer, flushSize); + + // ⭐ 여기서 flush 호출 (200ms 블로킹되어도 CAN 수신은 계속!) + logFile.flush(); + + uint32_t elapsed = millis() - startTime; + Serial.printf("✓ SD Flush 완료: %d bytes, %d ms\n", written, elapsed); + + // 통계 + if (elapsed > 100) { + Serial.printf("⚠️ Flush 지연: %d ms (하지만 CAN은 정상!)\n", elapsed); + } + } + xSemaphoreGive(sdMutex); + } else { + Serial.println("✗ SD Flush: 뮤텍스 타임아웃"); + } + + flushInProgress = false; + } + } + } +} + // ======================================== // SD Write Task // ======================================== @@ -1528,7 +1614,7 @@ void sdWriteTask(void *parameter) { currentFileSize += lineLen; static int csvFlushCounter = 0; - if (++csvFlushCounter >= 500) { // ⭐ 200 → 500 (최대 성능) + if (++csvFlushCounter >= 2000) { // ⭐ 500 → 2000 (SD 블로킹 최소화) logFile.flush(); csvFlushCounter = 0; } @@ -1539,41 +1625,40 @@ void sdWriteTask(void *parameter) { } } else { - // BIN 형식 - static uint32_t binMsgCounter = 0; - static uint32_t binReopenCounter = 0; + // ⭐⭐⭐ BIN 형식 - 더블 버퍼링 - // ⭐⭐⭐ 1단계: 버퍼 가득 찼으면 먼저 플러시 - if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { - if (logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ BIN 버퍼 플러시 (FULL): %d bytes\n", written); - bufferIndex = 0; + // ⭐ 1단계: 버퍼 가득 찼으면 스왑 + if (writeBufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { + // flush가 진행중이면 잠시 대기 (매우 짧음, 버퍼 스왑만 기다림) + int waitCount = 0; + while (flushInProgress && waitCount < 100) { + vTaskDelay(1); + waitCount++; } + + if (waitCount >= 100) { + Serial.println("⚠️ Flush 대기 타임아웃 (버퍼 손실 방지)"); + } + + // ⭐ 버퍼 스왑 (즉시, 블로킹 없음!) + uint8_t* temp = currentWriteBuffer; + currentWriteBuffer = currentFlushBuffer; + currentFlushBuffer = temp; + flushBufferSize = writeBufferIndex; + writeBufferIndex = 0; + + // ⭐ flush 태스크에 알림 (비동기) + if (sdFlushTaskHandle != NULL) { + xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite); + } + + Serial.printf("✓ 버퍼 스왑: %d bytes → flush 태스크\n", flushBufferSize); } - // ⭐⭐⭐ 2단계: 이제 공간 확보됨, 데이터 추가 - memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage)); - bufferIndex += sizeof(CANMessage); + // ⭐ 2단계: 현재 쓰기 버퍼에 데이터 추가 + memcpy(¤tWriteBuffer[writeBufferIndex], &canMsg, sizeof(CANMessage)); + writeBufferIndex += sizeof(CANMessage); currentFileSize += sizeof(CANMessage); - binMsgCounter++; - binReopenCounter++; - - // ⭐⭐⭐ 3단계: 1000개 메시지마다 주기적 플러시 (500→1000, 최대 성능) - if (binMsgCounter % 1000 == 0) { - if (logFile && bufferIndex > 0) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ BIN 주기 플러시: %d bytes (메시지: %d)\n", written, binMsgCounter); - bufferIndex = 0; - } - } - - // ⭐⭐⭐ 4단계: 파일 재오픈 제거 (고부하 환경 최적화) - // 재오픈 시 SD 카드 블록으로 인한 오버플로우 방지 - // 대신 주기적 flush로 데이터 안정성 확보 - // if (binReopenCounter >= 2000) { ... } // 제거됨 } xSemaphoreGive(sdMutex); @@ -1986,9 +2071,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) if (logFile) { loggingEnabled = true; - bufferIndex = 0; + + // ⭐⭐⭐ 더블 버퍼 초기화 + writeBufferIndex = 0; + flushBufferSize = 0; + flushInProgress = false; + currentFileSize = logFile.size(); - Serial.printf("✅ 로깅 시작!\n"); + Serial.printf("✅ 로깅 시작! (더블 버퍼링 모드)\n"); Serial.printf(" 파일: %s\n", currentFilename); Serial.printf(" 형식: %s\n", canLogFormatCSV ? "CSV" : "BIN"); Serial.printf(" 초기 크기: %lu bytes\n", currentFileSize); @@ -2010,12 +2100,22 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) Serial.println("🛑 로깅 중지 요청..."); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { - // ⭐⭐⭐ BIN 형식: 버퍼에 남은 데이터 강제 플러시 - if (bufferIndex > 0 && logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); + // ⭐⭐⭐ 더블 버퍼: 현재 쓰기 버퍼의 남은 데이터 flush + if (writeBufferIndex > 0 && logFile) { + size_t written = logFile.write(currentWriteBuffer, writeBufferIndex); logFile.flush(); - Serial.printf("✓ 최종 플러시: %d bytes\n", written); - bufferIndex = 0; + Serial.printf("✓ 최종 쓰기 버퍼 플러시: %d bytes\n", written); + writeBufferIndex = 0; + } + + // ⭐ flush 태스크가 작업 중이면 완료 대기 + int waitCount = 0; + while (flushInProgress && waitCount < 500) { + vTaskDelay(10); + waitCount++; + } + if (waitCount > 0) { + Serial.printf("✓ Flush 태스크 완료 대기: %d ms\n", waitCount * 10); } // ⭐⭐⭐ CSV 형식: 최종 플러시 @@ -2032,7 +2132,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) loggingEnabled = false; currentFilename[0] = '\0'; - bufferIndex = 0; + writeBufferIndex = 0; + flushBufferSize = 0; xSemaphoreGive(sdMutex); } else { Serial.println("✗ sdMutex 획득 실패!"); @@ -3700,8 +3801,8 @@ void setup() { // - TX: CAN 전송 // - SEQ: 시퀀스 재생 // - MONITOR: 상태 모니터링 - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 20, &canRxTaskHandle, 1); // ⭐ 8KB → 12KB, Pri 24 (최고) - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 4, &webTaskHandle, 1); // ⭐ Core 0 → 1, Pri 4 (SD와 분리) + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1); // ⭐ 8KB → 12KB, Pri 24 (최고) + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0); // ⭐ Core 0 → 1, Pri 4 (SD와 분리) xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); // Pri 3 xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); // Pri 2 xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); // Pri 1 @@ -3709,7 +3810,8 @@ void setup() { // Core 0: I/O 전용 (SD, Serial, RTC) // - SD 쓰기와 Serial 수신만 처리 // - 웹은 Core 1에서 처리하므로 SD 지연 영향 없음 - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 8, &sdWriteTaskHandle, 0); // Pri 8 (최우선) + xTaskCreatePinnedToCore(sdFlushTask, "SD_FLUSH", 4096, NULL, 9, &sdFlushTaskHandle, 0); // ⭐ Pri 9 (최우선, flush 전용) + xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 8, &sdWriteTaskHandle, 0); // Pri 8 xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 6, &serialRxTaskHandle, 0); // Pri 6 xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle,0); // Pri 6