Tại Sao Firmware Dual-Core Dễ Sai
Thêm một core không tự nhiên làm firmware tốt hơn. Thực tế, firmware Dual-Core kém thiết kế còn tệ hơn firmware đơn core vì:
- Race condition: 2 task cùng ghi một biến → data bị hỏng ngẫu nhiên
- Deadlock: Task A chờ Task B, Task B chờ Task A → cả 2 đứng
- Priority inversion: Task quan trọng bị chặn bởi task ít quan trọng giữ Mutex
- Stack overflow: Task mới tạo mà không ước tính đủ stack
- Watchdog reset: Task blocking quá lâu → watchdog không được feed
Bài này đưa ra blueprint thiết kế firmware để tránh những lỗi này ngay từ đầu.
Anti-Pattern #1: Biến Global Không Có Bảo Vệ
// SAI — race condition!
float g_temperature = 0.0f; // Biến global
// Task A (Core 0): cập nhật nhiệt độ
void sensorTask(void* p) {
while (true) {
g_temperature = readSensor(); // Ghi không bảo vệ
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// Task B (Core 1): đọc và gửi MQTT
void mqttTask(void* p) {
while (true) {
float temp = g_temperature; // Đọc không bảo vệ
publishMQTT(temp); // Có thể đọc giá trị đang bị ghi dở!
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
Trên ESP32-S3 với 2 core, 2 task này thực sự chạy đồng thời. g_temperature = readSensor() không phải phép gán nguyên tử với float 32-bit trên kiến trúc 32-bit — CPU cần nhiều lệnh để ghi đủ 4 bytes. Nếu Task B đọc giữa chừng, bạn nhận được giá trị rác.
Sửa đúng: Dùng Queue
QueueHandle_t temperatureQueue;
void sensorTask(void* p) {
while (true) {
float temp = readSensor();
xQueueOverwrite(temperatureQueue, &temp); // Queue size=1, luôn ghi đè
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void mqttTask(void* p) {
float temp;
while (true) {
xQueuePeek(temperatureQueue, &temp, portMAX_DELAY); // Đọc không xóa
publishMQTT(temp);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void setup() {
temperatureQueue = xQueueCreate(1, sizeof(float)); // Queue 1 phần tử
// ...
}
Anti-Pattern #2: delay() Trong Task
// SAI — blocking toàn task
void networkTask(void* p) {
while (true) {
if (WiFi.status() != WL_CONNECTED) {
WiFi.reconnect();
delay(5000); // Block 5 giây — watchdog nguy hiểm!
}
// ...
}
}
delay() block CPU hoàn toàn. FreeRTOS scheduler vẫn chạy và chuyển sang task khác, nhưng task này không thể phản hồi gì trong 5 giây.
Sửa đúng: vTaskDelay + State Machine
typedef enum {
WIFI_CONNECTING,
WIFI_CONNECTED,
WIFI_RECONNECTING
} WifiState;
void networkTask(void* p) {
WifiState state = WIFI_CONNECTING;
uint32_t reconnectTimer = 0;
while (true) {
switch (state) {
case WIFI_CONNECTING:
WiFi.begin(SSID, PASS);
state = WIFI_RECONNECTING;
reconnectTimer = xTaskGetTickCount();
break;
case WIFI_RECONNECTING:
if (WiFi.status() == WL_CONNECTED) {
state = WIFI_CONNECTED;
Serial.println("WiFi connected");
} else if (xTaskGetTickCount() - reconnectTimer > pdMS_TO_TICKS(10000)) {
// Timeout 10s, thử lại
WiFi.reconnect();
reconnectTimer = xTaskGetTickCount();
}
break;
case WIFI_CONNECTED:
if (WiFi.status() != WL_CONNECTED) {
state = WIFI_RECONNECTING;
}
// Làm việc thực tế
break;
}
vTaskDelay(pdMS_TO_TICKS(500)); // Check mỗi 500ms, không blocking
}
}
Anti-Pattern #3: Task Stack Quá Nhỏ
// SAI — stack 512 bytes cho task làm nhiều việc
xTaskCreatePinnedToCore(bigTask, "Big", 512, NULL, 1, NULL, 1);
// → Stack overflow → crash ngẫu nhiên sau vài giờ
Ước Tính Stack Size:
| Loại task | Stack gợi ý |
|---|---|
| Đọc sensor, gửi Queue | 1024–2048 bytes |
| WiFi/MQTT (Arduino) | 4096–8192 bytes |
| LCD/LVGL rendering | 8192–16384 bytes |
| HTTP request/response | 8192–12288 bytes |
| JSON parsing (ArduinoJson) | 4096–8192 bytes |
Quy tắc thực tế: bắt đầu với stack lớn hơn ước tính, dùng uxTaskGetStackHighWaterMark() để đo thực tế, rồi giảm dần nếu cần.
// Đo stack usage sau khi task đã chạy thật
UBaseType_t remaining = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("Stack còn lại tối thiểu: %u bytes\n",
remaining * sizeof(StackType_t));
// Nếu còn > 500 bytes: có thể giảm stack khi tạo task
// Nếu còn < 200 bytes: tăng stack ngay!
Blueprint Firmware Dual-Core IoT
Đây là kiến trúc kinh nghiệm cho firmware IoT production trên ESP32-S3:
Core 0 (PRO_CPU) — "Network Core"
├── WiFi stack (Espressif, tự động)
├── networkTask: WiFi reconnect, MQTT pub/sub
├── otaTask: OTA update check (priority thấp)
└── healthTask: Watchdog feed, memory monitor
Core 1 (APP_CPU) — "Application Core"
├── sensorTask: Đọc sensor, gửi queue
├── displayTask: LCD/LVGL render
├── logicTask: Business logic, state machine
└── logTask: Serial log, SD card log (priority thấp)
Shared (thread-safe):
├── sensorQueue: sensorTask → logicTask
├── displayQueue: logicTask → displayTask
├── mqttQueue: logicTask → networkTask
├── i2cMutex: protect I2C bus
└── spiffsMutex: protect filesystem
Ví Dụ: Firmware IoT 4 Task
#include <Arduino.h>
#include <WiFi.h>
// Queues và Mutex
QueueHandle_t sensorQueue;
QueueHandle_t mqttQueue;
SemaphoreHandle_t i2cMutex;
struct SensorData {
float temperature;
float humidity;
uint32_t timestamp;
};
struct MqttMessage {
char topic[64];
char payload[128];
};
// ── Core 0: Đọc sensor mỗi 2 giây ──────────────────────────────────────────
void sensorTask(void* p) {
SensorData data;
while (true) {
// Lấy I2C mutex trước khi đọc sensor I2C
if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(100)) == pdPASS) {
data.temperature = 25.5f + (esp_random() % 10) / 10.0f;
data.humidity = 60.0f + (esp_random() % 20) / 10.0f;
data.timestamp = millis();
xSemaphoreGive(i2cMutex);
}
xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// ── Core 0: Gửi MQTT khi có data ────────────────────────────────────────────
void networkTask(void* p) {
MqttMessage msg;
while (true) {
if (xQueueReceive(mqttQueue, &msg, pdMS_TO_TICKS(5000))) {
if (WiFi.isConnected()) {
Serial.printf("[MQTT] %s: %s\n", msg.topic, msg.payload);
// mqttClient.publish(msg.topic, msg.payload);
}
}
}
}
// ── Core 1: Logic xử lý và tạo MQTT message ─────────────────────────────────
void logicTask(void* p) {
SensorData data;
MqttMessage msg;
while (true) {
if (xQueueReceive(sensorQueue, &data, pdMS_TO_TICKS(3000))) {
// Tạo MQTT payload
snprintf(msg.topic, sizeof(msg.topic), "iotlabs/sensor/env");
snprintf(msg.payload, sizeof(msg.payload),
"{\"temp\":%.1f,\"hum\":%.1f,\"ts\":%lu}",
data.temperature, data.humidity, data.timestamp);
xQueueSend(mqttQueue, &msg, pdMS_TO_TICKS(100));
// Log
Serial.printf("[Logic] Temp:%.1f Hum:%.1f\n",
data.temperature, data.humidity);
}
}
}
// ── Core 1: Hiển thị LCD (placeholder) ──────────────────────────────────────
void displayTask(void* p) {
while (true) {
// Peek queue để đọc không xóa — sensor task vẫn giữ nguyên data
SensorData data;
if (xQueuePeek(sensorQueue, &data, 0) == pdPASS) {
// lcd.setCursor(0, 0);
// lcd.printf("T:%.1f H:%.1f", data.temperature, data.humidity);
}
vTaskDelay(pdMS_TO_TICKS(500)); // Refresh LCD 2 lần/giây
}
}
void setup() {
Serial.begin(115200);
// Khởi tạo primitives
sensorQueue = xQueueCreate(10, sizeof(SensorData));
mqttQueue = xQueueCreate(20, sizeof(MqttMessage));
i2cMutex = xSemaphoreCreateMutex();
// WiFi
WiFi.begin("SSID", "PASSWORD");
// Core 0: network + sensor (cùng core với WiFi stack)
xTaskCreatePinnedToCore(sensorTask, "Sensor", 2048, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(networkTask, "Network", 8192, NULL, 2, NULL, 0);
// Core 1: logic + display
xTaskCreatePinnedToCore(logicTask, "Logic", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(displayTask, "Display", 8192, NULL, 1, NULL, 1);
}
void loop() {
vTaskDelay(portMAX_DELAY); // loop() không làm gì — task lo hết
}
Checklist Thiết Kế Firmware Dual-Core
Trước khi viết code, trả lời các câu hỏi:
- [ ] Task nào cần chạy ở Core 0 (cùng WiFi stack)?
- [ ] Task nào thuần application, đặt ở Core 1?
- [ ] Tài nguyên nào dùng chung giữa task? → Cần Mutex
- [ ] Data nào cần truyền giữa task? → Dùng Queue
- [ ] Stack size mỗi task ước tính bao nhiêu? → Đo với HighWaterMark
- [ ] Watchdog task nào chịu trách nhiệm feed?
- [ ] Task nào có thể bị blocking lâu? → Không được dùng delay()
Tổng Kết
| Vấn Đề | Giải Pháp |
|---|---|
| Biến global dùng chung | Queue hoặc Mutex bảo vệ |
| delay() blocking | vTaskDelay() + state machine |
| Stack overflow | Đo HighWaterMark, stack đủ lớn |
| Race condition | Không share data thô, dùng Queue |
| Deadlock | Không Take 2 Mutex cùng lúc, timeout khi Take |
| Task không biết core nào | pinToCore() explicit |
Bài tiếp theo: Bài 5 — Memory Architecture Trên ESP32-S3: SRAM, Flash, PSRAM, Heap, Stack và Cache — Hiểu bản đồ bộ nhớ để allocate đúng, tránh fragmentation và dùng PSRAM hiệu quả.
📚 Series: Sức Mạnh ESP32-S3 Dual-Core
⬅️ Bài trước: S3 Dual-Core – Bài 3: FreeRTOS Từ loop() Đến Hệ Điều Hành Thời Gian Thực
➡️ Bài tiếp theo: S3 Dual-Core – Bài 5: Memory Architecture – SRAM, PSRAM, Flash, Heap


