에러수정

This commit is contained in:
2026-03-06 16:10:13 +00:00
parent 8a6c283f34
commit d33cdb8e82

View File

@@ -106,8 +106,9 @@ struct CANMessage {
uint64_t timestamp_us; uint64_t timestamp_us;
uint32_t id; uint32_t id;
uint8_t dlc; uint8_t dlc;
uint8_t flags; // bit7=EFF(29bit확장), bit6=RTR, bit5=ERR ← ★ PCAP EFF 복원용
uint8_t data[8]; uint8_t data[8];
} __attribute__((packed)); } __attribute__((packed)); // 22 bytes
// ======================================== // ========================================
// ★ PCAP 구조체 (Wireshark 호환) // ★ PCAP 구조체 (Wireshark 호환)
@@ -145,6 +146,12 @@ struct PcapRecord {
SocketCANFrame frame; SocketCANFrame frame;
} __attribute__((packed)); // 32 bytes } __attribute__((packed)); // 32 bytes
// ★ 컴파일 타임 크기 검증 (틀리면 빌드 에러로 즉시 알 수 있음)
static_assert(sizeof(PcapGlobalHeader) == 24, "PcapGlobalHeader must be 24 bytes");
static_assert(sizeof(PcapPacketHeader) == 16, "PcapPacketHeader must be 16 bytes");
static_assert(sizeof(SocketCANFrame) == 16, "SocketCANFrame must be 16 bytes");
static_assert(sizeof(PcapRecord) == 32, "PcapRecord must be 32 bytes");
struct SerialMessage { struct SerialMessage {
uint64_t timestamp_us; uint64_t timestamp_us;
uint16_t length; uint16_t length;
@@ -279,7 +286,8 @@ static inline bool IRAM_ATTR ring_can_push(CANRingBuffer* rb, const CANMessage*
rb->dropped++; rb->dropped++;
return false; // drop: 손실 카운트 후 즉시 반환 return false; // drop: 손실 카운트 후 즉시 반환
} }
rb->buf[h & rb->mask] = *msg; // 데이터 기록 // ★ packed struct(22bytes) → 비정렬 PSRAM 주소에 안전하게 memcpy
memcpy(&rb->buf[h & rb->mask], msg, sizeof(CANMessage));
__sync_synchronize(); // ★ 메모리 배리어: buf 쓰기 완료 후 head 갱신 __sync_synchronize(); // ★ 메모리 배리어: buf 쓰기 완료 후 head 갱신
rb->head = h + 1; rb->head = h + 1;
return true; return true;
@@ -289,7 +297,8 @@ static inline bool IRAM_ATTR ring_can_push(CANRingBuffer* rb, const CANMessage*
static inline bool ring_can_pop(CANRingBuffer* rb, CANMessage* msg) { static inline bool ring_can_pop(CANRingBuffer* rb, CANMessage* msg) {
uint32_t t = rb->tail; uint32_t t = rb->tail;
if (rb->head == t) return false; // 빈 버퍼 if (rb->head == t) return false; // 빈 버퍼
*msg = rb->buf[t & rb->mask]; // ★ packed struct(22bytes) → 비정렬 PSRAM 주소에서 안전하게 memcpy
memcpy(msg, &rb->buf[t & rb->mask], sizeof(CANMessage));
__sync_synchronize(); // ★ 메모리 배리어: 데이터 읽기 완료 후 tail 갱신 __sync_synchronize(); // ★ 메모리 배리어: 데이터 읽기 완료 후 tail 갱신
rb->tail = t + 1; rb->tail = t + 1;
return true; return true;
@@ -430,6 +439,7 @@ volatile uint32_t currentSerialFileSize = 0;
volatile uint32_t currentSerial2FileSize= 0; volatile uint32_t currentSerial2FileSize= 0;
volatile bool canLogFormatCSV = false; volatile bool canLogFormatCSV = false;
volatile bool canLogFormatPCAP = false; // ★ PCAP 추가 volatile bool canLogFormatPCAP = false; // ★ PCAP 추가
uint32_t pcapDbgCount = 0; // ★ PCAP 시리얼 디버그 카운터
volatile bool serialLogFormatCSV = true; volatile bool serialLogFormatCSV = true;
volatile bool serial2LogFormatCSV = true; volatile bool serial2LogFormatCSV = true;
volatile uint64_t canLogStartTime = 0; volatile uint64_t canLogStartTime = 0;
@@ -444,8 +454,8 @@ CAN_SPEED currentCanSpeed = CAN_1000KBPS;
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"}; const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
CAN_SPEED canSpeedValues[]= {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS}; CAN_SPEED canSpeedValues[]= {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};
uint32_t totalMsgCount = 0; volatile uint32_t totalMsgCount = 0; // Core 1 write / Core 0 read → volatile 필수
uint32_t msgPerSecond = 0; volatile uint32_t msgPerSecond = 0; // Core 0 webUpdateTask에서 계산
uint32_t lastMsgCountTime= 0; uint32_t lastMsgCountTime= 0;
uint32_t lastMsgCount = 0; uint32_t lastMsgCount = 0;
volatile uint32_t totalSerialRxCount = 0; volatile uint32_t totalSerialRxCount = 0;
@@ -458,7 +468,7 @@ SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
int commentCount = 0; int commentCount = 0;
// Forward declarations // Forward declarations
void IRAM_ATTR canISR(); void IRAM_ATTR canISR(); // 초경량 ISR 전방선언
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
void resetMCP2515(); void resetMCP2515();
@@ -829,6 +839,7 @@ void checkAutoTriggers(struct can_frame &frame) {
flushBufferSize = 0; flushBufferSize = 0;
flushInProgress = false; flushInProgress = false;
currentFileSize = logFile.size(); currentFileSize = logFile.size();
pcapDbgCount = 0; // ★ PCAP 디버그 카운터 리셋
Serial.printf("🎯 Auto Trigger 시작: %s\n", currentFilename); Serial.printf("🎯 Auto Trigger 시작: %s\n", currentFilename);
} }
} }
@@ -1045,13 +1056,22 @@ bool setMCP2515Mode(MCP2515Mode mode) {
} }
// ======================================== // ========================================
// ★★★ CAN 인터럽트 & canRxTask (RingBuffer 버전) ★★★ // ★★★ CAN 수신: 초경량 ISR + 레벨 폴링 하이브리드 ★★★
//
// [문제 분석]
// 기존 ISR: vTaskNotifyGiveFromISR + portYIELD_FROM_ISR
// = FreeRTOS 컨텍스트 스위치 × 8000회/s → 시스템 부하
// 순수 폴링: vTaskDelay(1~2ms) 공백 동안 MCP2515 RX버퍼(2개) 오버플로우 → 손실
//
// [해결책: 하이브리드]
// ISR → volatile 플래그 세팅만 (FreeRTOS 호출 0, 컨텍스트 스위치 0)
// Task → 플래그 + INT 레벨 폴링으로 즉시 읽기, 빈 상태엔 taskYIELD()만 사용
// ★ vTaskDelay 사용 금지: delay 중 HW 오버플로우 발생 가능
// ======================================== // ========================================
volatile bool IRAM_DATA_ATTR canIntFlag = false;
void IRAM_ATTR canISR() { void IRAM_ATTR canISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE; canIntFlag = true; // FreeRTOS 호출 없음 → 컨텍스트 스위치 없음
if (canRxTaskHandle != NULL)
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
} }
void canRxTask(void *parameter) { void canRxTask(void *parameter) {
@@ -1060,32 +1080,21 @@ void canRxTask(void *parameter) {
uint32_t lastErrorCheck = 0; uint32_t lastErrorCheck = 0;
uint32_t errorRecoveryCount= 0; uint32_t errorRecoveryCount= 0;
Serial.println("✓ CAN RX Task 시작 (RingBuffer SPSC, Core 1, Pri 24)"); Serial.println("✓ CAN RX Task 시작 (초경량ISR+레벨폴링 하이브리드, Core 1, Pri 24)");
// ★ setup() 완료 대기 (이중 방어: 태스크가 마지막 생성되더라도 안전하게)
vTaskDelay(pdMS_TO_TICKS(500));
// 초기 버퍼 클리어 // 초기 버퍼 클리어
if (digitalRead(CAN_INT_PIN) == LOW) { while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {}
int readCount = 0; canIntFlag = false;
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 100) {
struct timeval tv; gettimeofday(&tv, NULL);
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
msg.id = frame.can_id & 0x1FFFFFFF;
msg.dlc = frame.can_dlc;
memcpy(msg.data, frame.data, 8);
// ★ RingBuffer push (non-blocking, no mutex)
if (ring_can_push(&canRing, &msg)) totalMsgCount++;
readCount++;
}
}
while (1) { while (1) {
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)); // ── 주기적 에러 체크 (1초마다) ──────────────────────────────────
// 주기적 에러 체크 (1초마다)
uint32_t now = millis(); uint32_t now = millis();
if (now - lastErrorCheck > 1000) { if (now - lastErrorCheck > 1000) {
lastErrorCheck = now; lastErrorCheck = now;
uint8_t errorFlag = mcp2515.getErrorFlags(); uint8_t errorFlag = mcp2515.getErrorFlags();
uint8_t txErr = mcp2515.errorCountTX();
uint8_t rxErr = mcp2515.errorCountRX(); uint8_t rxErr = mcp2515.errorCountRX();
if (errorFlag & 0xC0) { if (errorFlag & 0xC0) {
@@ -1114,26 +1123,30 @@ void canRxTask(void *parameter) {
Serial.printf("⚠️ Error Passive! REC=%d\n", rxErr); Serial.printf("⚠️ Error Passive! REC=%d\n", rxErr);
} }
// ★ 배치 읽기 → RingBuffer push (완전 non-blocking) // ── ★ 엣지(ISR 플래그) + 레벨(INT핀) 이중 감지 ─────────────────
int batchCount = 0; if (canIntFlag || digitalRead(CAN_INT_PIN) == LOW) {
while (batchCount < 20 && mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { canIntFlag = false;
// INT LOW 동안 RX 버퍼 완전히 비우기 (손실 없음)
while (digitalRead(CAN_INT_PIN) == LOW &&
mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
checkAutoTriggers(frame); checkAutoTriggers(frame);
struct timeval tv; gettimeofday(&tv, NULL); struct timeval tv; gettimeofday(&tv, NULL);
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
msg.id = frame.can_id & 0x1FFFFFFF; msg.id = frame.can_id & 0x1FFFFFFF;
msg.flags = (uint8_t)((frame.can_id >> 24) & 0xE0);
msg.dlc = frame.can_dlc; msg.dlc = frame.can_dlc;
memcpy(msg.data, frame.data, 8); memcpy(msg.data, frame.data, 8);
// ★ mutex 없음, blocking 없음 - 순수 메모리 쓰기 if (ring_can_push(&canRing, &msg)) totalMsgCount++;
if (ring_can_push(&canRing, &msg)) {
totalMsgCount++;
} }
// dropped 시에도 canRing.dropped에 자동 기록됨 // 읽기 완료 후 즉시 다음 폴링 (loop 재진입)
} else {
batchCount++; // INT HIGH, 플래그도 없음 → 타 태스크에 CPU 양보
// ★ vTaskDelay 사용 금지: delay 중 HW 오버플로우 가능
taskYIELD();
} }
if (batchCount == 0) vTaskDelay(pdMS_TO_TICKS(1));
} }
} }
@@ -1319,23 +1332,31 @@ void sdWriteTask(void *parameter) {
} }
// PcapRecord 조립 후 버퍼에 기록 // PcapRecord 조립 후 버퍼에 기록
PcapRecord rec; PcapRecord rec;
memset(&rec, 0, sizeof(PcapRecord)); // ★ 안전 초기화
uint32_t sec = (uint32_t)(canMsg.timestamp_us / 1000000ULL); uint32_t sec = (uint32_t)(canMsg.timestamp_us / 1000000ULL);
uint32_t usec = (uint32_t)(canMsg.timestamp_us % 1000000ULL); uint32_t usec = (uint32_t)(canMsg.timestamp_us % 1000000ULL);
rec.hdr.ts_sec = sec; rec.hdr.ts_sec = sec;
rec.hdr.ts_usec = usec; rec.hdr.ts_usec = usec;
rec.hdr.incl_len = sizeof(SocketCANFrame); // 16 rec.hdr.incl_len = sizeof(SocketCANFrame); // 16
rec.hdr.orig_len = sizeof(SocketCANFrame); // 16 rec.hdr.orig_len = sizeof(SocketCANFrame); // 16
// SocketCAN ID: 확장 프레임이면 bit31 세트 // ★ flags 필드로 EFF/RTR 정확히 복원 (id>0x7FF 휴리스틱 불필요)
rec.frame.can_id = canMsg.id; rec.frame.can_id = canMsg.id;
if (canMsg.id > 0x7FF) rec.frame.can_id |= 0x80000000U; // CAN_EFF_FLAG if (canMsg.flags & 0x80) rec.frame.can_id |= 0x80000000U; // CAN_EFF_FLAG
if (canMsg.flags & 0x40) rec.frame.can_id |= 0x40000000U; // CAN_RTR_FLAG
rec.frame.can_dlc = canMsg.dlc; rec.frame.can_dlc = canMsg.dlc;
rec.frame.pad = 0; // data: dlc 바이트만 유효, 나머지는 memset으로 이미 0
rec.frame.res0 = 0; memcpy(rec.frame.data, canMsg.data,
rec.frame.res1 = 0; (canMsg.dlc <= 8) ? canMsg.dlc : 8);
memcpy(rec.frame.data, canMsg.data, 8);
memcpy(&currentWriteBuffer[writeBufferIndex], &rec, sizeof(PcapRecord)); memcpy(&currentWriteBuffer[writeBufferIndex], &rec, sizeof(PcapRecord));
writeBufferIndex += sizeof(PcapRecord); writeBufferIndex += sizeof(PcapRecord);
currentFileSize += sizeof(PcapRecord); currentFileSize += sizeof(PcapRecord);
// ★ 최초 3패킷 시리얼 디버그 (PCAP ID=0 확인용)
if (pcapDbgCount < 3) {
Serial.printf("[PCAP DBG #%lu] id=0x%X flags=0x%02X dlc=%d data=%02X%02X...\n",
pcapDbgCount, canMsg.id, canMsg.flags, canMsg.dlc,
canMsg.data[0], canMsg.data[1]);
pcapDbgCount++;
}
} else { } else {
// ── BIN: 더블 버퍼 (hot path, sdMutex 없음!) ───────────── // ── BIN: 더블 버퍼 (hot path, sdMutex 없음!) ─────────────
// writeBuffer는 sdWriteTask만 쓰므로 mutex 불필요 // writeBuffer는 sdWriteTask만 쓰므로 mutex 불필요
@@ -1375,11 +1396,7 @@ void sdWriteTask(void *parameter) {
void sdMonitorTask(void *parameter) { void sdMonitorTask(void *parameter) {
while (1) { while (1) {
uint32_t now = millis(); uint32_t now = millis();
if (now - lastMsgCountTime >= 1000) { // ★ msgPerSecond는 Core 0 webUpdateTask에서 계산 (Core 1 스타베이션 방지)
msgPerSecond = totalMsgCount - lastMsgCount;
lastMsgCount = totalMsgCount;
lastMsgCountTime= now;
}
if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) { if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
float rawV = analogRead(MONITORING_VOLT) * (3.3f / 4095.0f); float rawV = analogRead(MONITORING_VOLT) * (3.3f / 4095.0f);
powerStatus.voltage = rawV; powerStatus.voltage = rawV;
@@ -1644,6 +1661,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
loggingEnabled = true; loggingEnabled = true;
writeBufferIndex = 0; flushBufferSize = 0; flushInProgress = false; writeBufferIndex = 0; flushBufferSize = 0; flushInProgress = false;
currentFileSize = logFile.size(); currentFileSize = logFile.size();
pcapDbgCount = 0; // ★ PCAP 디버그 카운터 리셋
const char* fmtName = canLogFormatPCAP ? "PCAP" : (canLogFormatCSV ? "CSV" : "BIN"); const char* fmtName = canLogFormatPCAP ? "PCAP" : (canLogFormatCSV ? "CSV" : "BIN");
Serial.printf("✅ 로깅 시작 [%s]: %s\n", fmtName, currentFilename); Serial.printf("✅ 로깅 시작 [%s]: %s\n", fmtName, currentFilename);
} }
@@ -2102,8 +2120,23 @@ void webUpdateTask(void *parameter) {
vTaskDelay(pdMS_TO_TICKS(500)); vTaskDelay(pdMS_TO_TICKS(500));
while (1) { while (1) {
// ★ server.handleClient()를 Core 0에서 호출
// loop()(Core 1, Pri 1)는 canRxTask(Core 1, Pri 24)에 선점당해 실행 불가
server.handleClient();
webSocket.loop(); webSocket.loop();
// ★ msgPerSecond를 Core 0에서 직접 계산
// (sdMonitorTask가 Core 1에서 canRxTask(Pri24)에 스타베이션될 수 있으므로)
{
uint32_t now = millis();
if (now - lastMsgCountTime >= 1000) {
uint32_t cur = totalMsgCount; // volatile 읽기
msgPerSecond = cur - lastMsgCount;
lastMsgCount = cur;
lastMsgCountTime = now;
}
}
if (webSocket.connectedClients() > 0) { if (webSocket.connectedClients() > 0) {
DynamicJsonDocument doc(16384); DynamicJsonDocument doc(16384);
doc["type"] = "update"; doc["type"] = "update";
@@ -2399,47 +2432,44 @@ void setup() {
server.on("/serial2", HTTP_GET, [](){server.send_P(200,"text/html",serial2_terminal_html);}); server.on("/serial2", HTTP_GET, [](){server.send_P(200,"text/html",serial2_terminal_html);});
server.on("/download", HTTP_GET, []() { server.on("/download", HTTP_GET, []() {
if (!server.hasArg("file")) { server.send(400,"text/plain","Bad request"); return; } // ★★★ 주의: 이 핸들러는 webUpdateTask 컨텍스트에서 실행됨
// → webTaskHandle을 vTaskSuspend 하면 자기 자신을 정지시켜 영원히 멈춤
// → SD_MMC.end()/재마운트 불필요 (같은 SD 인스턴스 그대로 사용)
// → CHUNK를 충분히 크게 해서 Watchdog 타임아웃 방지
if (!server.hasArg("file")) {
server.send(400, "text/plain", "Bad request"); return;
}
String filename = "/" + server.arg("file"); String filename = "/" + server.arg("file");
// 로깅 중이면 잠시 중단 (sdWriteTask와 SD 버스 충돌 방지)
bool wasLogging = loggingEnabled; bool wasLogging = loggingEnabled;
if (wasLogging) { loggingEnabled = false; delay(200); } if (wasLogging) {
if (sdWriteTaskHandle) { vTaskSuspend(sdWriteTaskHandle); delay(100); } loggingEnabled = false;
if (webTaskHandle) { vTaskSuspend(webTaskHandle); delay(50); } // sdWriteTask가 현재 flush 중이면 완료 대기 (최대 500ms)
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10000)) != pdTRUE) { int waitMs = 0;
if (webTaskHandle) vTaskResume(webTaskHandle); while (flushInProgress && waitMs < 500) { delay(10); waitMs += 10; }
if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle);
if (wasLogging) loggingEnabled = true;
server.send(503,"text/plain","SD card busy"); return;
} }
SD_MMC.end(); delay(200);
if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0) || !SD_MMC.begin("/sdcard", true)) { // sdMutex 획득 (sdFlushTask와 동시 접근 차단)
xSemaphoreGive(sdMutex); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(3000)) != pdTRUE) {
if (webTaskHandle) vTaskResume(webTaskHandle);
if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle);
if (wasLogging) loggingEnabled = true; if (wasLogging) loggingEnabled = true;
server.send(500,"text/plain","SD remount failed"); return; server.send(503, "text/plain", "SD busy"); return;
} }
if (!SD_MMC.exists(filename)) { if (!SD_MMC.exists(filename)) {
SD_MMC.end(); delay(100);
SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3);
SD_MMC.begin("/sdcard", false);
xSemaphoreGive(sdMutex); xSemaphoreGive(sdMutex);
if (webTaskHandle) vTaskResume(webTaskHandle);
if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle);
if (wasLogging) loggingEnabled = true; if (wasLogging) loggingEnabled = true;
server.send(404, "text/plain", "File not found"); return; server.send(404, "text/plain", "File not found"); return;
} }
File file = SD_MMC.open(filename, FILE_READ); File file = SD_MMC.open(filename, FILE_READ);
if (!file) { if (!file) {
SD_MMC.end(); delay(100);
SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3);
SD_MMC.begin("/sdcard", false);
xSemaphoreGive(sdMutex); xSemaphoreGive(sdMutex);
if (webTaskHandle) vTaskResume(webTaskHandle);
if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle);
if (wasLogging) loggingEnabled = true; if (wasLogging) loggingEnabled = true;
server.send(500, "text/plain", "Failed to open file"); return; server.send(500, "text/plain", "Failed to open file"); return;
} }
size_t fileSize = file.size(); size_t fileSize = file.size();
String dispName = server.arg("file"); String dispName = server.arg("file");
server.setContentLength(fileSize); server.setContentLength(fileSize);
@@ -2447,42 +2477,65 @@ void setup() {
server.sendHeader("Content-Type", "application/octet-stream"); server.sendHeader("Content-Type", "application/octet-stream");
server.sendHeader("Connection", "close"); server.sendHeader("Connection", "close");
server.send(200, "application/octet-stream", ""); server.send(200, "application/octet-stream", "");
const size_t CHUNK = 512;
uint8_t* buf = (uint8_t*)heap_caps_aligned_alloc(32, CHUNK, MALLOC_CAP_DMA); // ★ 청크 크기 8KB (512 → 8192): 전송 횟수 대폭 감소, Watchdog 여유
const size_t CHUNK = 8192;
uint8_t* buf = (uint8_t*)heap_caps_malloc(CHUNK, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!buf) buf = (uint8_t*)malloc(CHUNK); if (!buf) buf = (uint8_t*)malloc(CHUNK);
if (buf) { if (buf) {
size_t total = 0; size_t total = 0;
WiFiClient client = server.client(); WiFiClient client = server.client();
uint32_t lastWdtFeed = millis();
while (file.available() && total < fileSize && client.connected()) { while (file.available() && total < fileSize && client.connected()) {
size_t r = file.read(buf, CHUNK); size_t r = file.read(buf, CHUNK);
if (!r) break; if (!r) break;
size_t w = 0; size_t w = 0;
while (w<r && client.connected()) { size_t wr=client.write(buf+w,r-w); w+=wr; if(!wr) delay(5); } uint32_t chunkStart = millis();
total+=r; yield(); while (w < r && client.connected()) {
size_t wr = client.write(buf + w, r - w);
if (wr > 0) {
w += wr;
} else {
// 클라이언트 버퍼 가득 찬 경우 짧게 대기
if (millis() - chunkStart > 5000) break; // 5초 타임아웃
delay(1);
}
}
total += r;
// ★ Watchdog 피드 (매 500ms마다)
if (millis() - lastWdtFeed > 500) {
esp_task_wdt_reset();
lastWdtFeed = millis();
}
yield();
} }
free(buf); free(buf);
Serial.printf("Download OK: %s (%u bytes)\n", filename.c_str(), total);
} }
file.close(); file.close();
SD_MMC.end(); delay(200);
SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3);
SD_MMC.begin("/sdcard", false);
xSemaphoreGive(sdMutex); xSemaphoreGive(sdMutex);
if (webTaskHandle) vTaskResume(webTaskHandle);
if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); // 로깅 재개
if (wasLogging) loggingEnabled = true; if (wasLogging) loggingEnabled = true;
}); });
server.begin(); server.begin();
Serial.println("✓ 웹 서버 시작"); Serial.println("✓ 웹 서버 시작");
// CAN 인터럽트 // ★ 초경량 ISR 등록 (플래그 세팅만, 컨텍스트 스위치 없음)
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
// ======================================== // ========================================
// Task 생성 // Task 생성
// ======================================== // ========================================
Serial.println("\nTask 생성 중..."); Serial.println("\nTask 생성 중...");
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1); // ★ 중요: canRxTask(Core 1, Pri 24)는 반드시 마지막에 생성
// → 먼저 생성하면 setup()이 Core 1에서 선점당해 이후 태스크 생성 불가
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0); xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0);
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
@@ -2493,6 +2546,8 @@ void setup() {
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle, 0); xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle, 0);
if (timeSyncStatus.rtcAvailable) if (timeSyncStatus.rtcAvailable)
xTaskCreatePinnedToCore(rtcSyncTask,"RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); xTaskCreatePinnedToCore(rtcSyncTask,"RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);
// ★ canRxTask 마지막 생성 (Core 1 Pri 24 → 이 시점엔 setup() 완료 직전)
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1);
Serial.println("\n========================================"); Serial.println("\n========================================");
Serial.println(" Task 구성 (RingBuffer v3.0)"); Serial.println(" Task 구성 (RingBuffer v3.0)");
@@ -2516,7 +2571,9 @@ void setup() {
// Loop // Loop
// ======================================== // ========================================
void loop() { void loop() {
server.handleClient(); // ★ server.handleClient()는 webUpdateTask(Core 0)에서 처리
// loop()는 Core 1, Pri 1 → canRxTask(Pri 24)에 선점당해 실질적으로 실행 안됨
// 통계 출력만 여기서 하지 않고 vTaskDelay로 양보
vTaskDelay(pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(10));
// 30초마다 상태 출력 (RingBuffer 통계 포함) // 30초마다 상태 출력 (RingBuffer 통계 포함)