에러수정
This commit is contained in:
@@ -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,10 +454,10 @@ 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;
|
||||||
volatile uint32_t totalSerialTxCount = 0;
|
volatile uint32_t totalSerialTxCount = 0;
|
||||||
volatile uint32_t totalSerial2RxCount = 0;
|
volatile uint32_t totalSerial2RxCount = 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,47 +1056,45 @@ 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) {
|
||||||
struct can_frame frame;
|
struct can_frame frame;
|
||||||
CANMessage msg;
|
CANMessage msg;
|
||||||
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) {
|
||||||
@@ -1101,9 +1110,9 @@ void canRxTask(void *parameter) {
|
|||||||
mcp2515.clearTXInterrupts();
|
mcp2515.clearTXInterrupts();
|
||||||
mcp2515.clearMERR();
|
mcp2515.clearMERR();
|
||||||
mcp2515.clearERRIF(); delay(10);
|
mcp2515.clearERRIF(); delay(10);
|
||||||
if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode();
|
if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode();
|
||||||
else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode();
|
else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode();
|
||||||
else mcp2515.setListenOnlyMode();
|
else mcp2515.setListenOnlyMode();
|
||||||
delay(50);
|
delay(50);
|
||||||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {}
|
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {}
|
||||||
mcp2515.clearRXnOVRFlags();
|
mcp2515.clearRXnOVRFlags();
|
||||||
@@ -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;
|
||||||
checkAutoTriggers(frame);
|
|
||||||
|
|
||||||
struct timeval tv; gettimeofday(&tv, NULL);
|
// INT LOW 동안 RX 버퍼 완전히 비우기 (손실 없음)
|
||||||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
while (digitalRead(CAN_INT_PIN) == LOW &&
|
||||||
msg.id = frame.can_id & 0x1FFFFFFF;
|
mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||||||
msg.dlc = frame.can_dlc;
|
checkAutoTriggers(frame);
|
||||||
memcpy(msg.data, frame.data, 8);
|
|
||||||
|
|
||||||
// ★ mutex 없음, blocking 없음 - 순수 메모리 쓰기
|
struct timeval tv; gettimeofday(&tv, NULL);
|
||||||
if (ring_can_push(&canRing, &msg)) {
|
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||||||
totalMsgCount++;
|
msg.id = frame.can_id & 0x1FFFFFFF;
|
||||||
|
msg.flags = (uint8_t)((frame.can_id >> 24) & 0xE0);
|
||||||
|
msg.dlc = frame.can_dlc;
|
||||||
|
memcpy(msg.data, frame.data, 8);
|
||||||
|
|
||||||
|
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(¤tWriteBuffer[writeBufferIndex], &rec, sizeof(PcapRecord));
|
memcpy(¤tWriteBuffer[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,90 +2432,110 @@ 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);
|
||||||
server.sendHeader("Content-Disposition","attachment; filename=\""+dispName+"\"");
|
server.sendHeader("Content-Disposition", "attachment; filename=\"" + dispName + "\"");
|
||||||
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;
|
|
||||||
while (w<r && client.connected()) { size_t wr=client.write(buf+w,r-w); w+=wr; if(!wr) delay(5); }
|
size_t w = 0;
|
||||||
total+=r; yield();
|
uint32_t chunkStart = millis();
|
||||||
|
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 통계 포함)
|
||||||
|
|||||||
Reference in New Issue
Block a user