Mở đầu: 15 bài học, giờ ghép lại thành một sản phẩm thật

Suốt series, mỗi bài đều tách riêng đúng MỘT khái niệm — thanh ghi, ngắt, timer, UART, ADC, DMA, RTOS — để không có gì làm loãng bài học cốt lõi. Nhưng firmware thật không bao giờ tách rời như vậy: một sản phẩm thật cần TẤT CẢ những mảnh ghép đó phối hợp cùng lúc. Bài capstone này dựng một Trạm đo nhiệt độ hoàn chỉnh — đúng loại thiết bị IoT/công nghiệp nhỏ phổ biến nhất trong thực tế — chạy trọn vẹn trên VMCU, huy động lại gần như mọi kỹ thuật đã học.

Đây không phải bài học khái niệm mới — nó là bài KIỂM TRA: bạn có thể đọc kiến trúc dưới đây và nhận ra ngay bài nào đã dạy từng mảnh hay không?


📚 Điều kiện tiên quyết
Tổng hợp Bài 1–15; dùng đậm nhất: Bài 5 (FSM nút), Bài 6 (buzzer), Bài 9 (UART), Bài 10 (ADC), Bài 11 (DMA), Bài 14 (task/RTOS), Bài 15 (queue).

1. Đề bài & kiến trúc

Đề bài: một trạm đo nhiệt độ độc lập — đọc cảm biến analog, lọc nhiễu, hiển thị mức qua LED, cảnh báo buzzer khi vượt ngưỡng, cho phép người dùng đặt ngưỡng qua nút bấm, và báo cáo/nhận lệnh từ xa qua UART.

Kiến trúc chia thành 4 task chạy trên mini-RTOS (Bài 14), giao tiếp qua queue (Bài 15):

Task Vai trò Ưu tiên
sensor_task Đọc ADC định kỳ, đẩy mẫu thô vào queue Cao nhất (dữ liệu tới đều, không được trễ)
filter_task Nhận mẫu qua queue, lọc trung bình trượt, hiệu chuẩn ra °C Cao
ui_task Đọc nút bấm (FSM), cập nhật LED/buzzer, đổi chế độ/ngưỡng Trung bình
uart_task Gửi log định kỳ, nhận lệnh đặt tham số Thấp nhất (không khẩn cấp)

Ngân sách tài nguyên PHẢI được tính trước khi viết dòng code đầu tiên: mỗi task cần một vùng stack RIÊNG (Bài 12), kích thước ước lượng qua watermark thực đo trong lúc test — không đoán mò.

2. Tầng thu thập

sensor_task cấu hình ADC1 + DMA1 ping-pong (Bài 10–11) đọc liên tục vào một buffer vòng — CPU không đụng tay vào từng mẫu. filter_task nhận khối dữ liệu qua queue (Bài 15, xây trên ring buffer Bài 8), áp trung bình trượt (Bài 10) để lọc nhiễu, rồi hiệu chuẩn raw → °C bằng công thức thật của cảm biến LM35 (10mV/°C, tuyến tính, không cần bù offset):

$$T(°C) = \frac{V_{measured}}{10\text{mV}}$$

Verify bằng số thật: ở 0°C, LM35 xuất đúng 0mV → mã ADC 0 (đáy thang). Ở 25°C (nhiệt độ phòng), LM35 xuất đúng 250mV → mã ADC 78 (verify chính xác trên VMCU thật). Ở 100°C (nước sôi), LM35 xuất 1000mV → mã ADC 310 — vẫn nằm gọn trong ngưỡng 3300mV của Vref, không tràn thang.

3. Tầng ứng dụng

ui_task chạy một FSM nút bấm (Bài 5): nhấn ngắn chuyển qua lại giữa 2 chế độ — xem nhiệt độ (mặc định) và đặt ngưỡng (xoay biến trở để chọn ngưỡng cảnh báo mới); nhấn giữ (long-press, chỉ cần thêm đúng 1 trạng thái vào FSM có sẵn — đúng bài học "FSM nở đẹp thế nào" của Bài 5) để LƯU ngưỡng vừa đặt.

Khi nhiệt độ đã lọc vượt ngưỡng, buzzer phát cảnh báo 2 tông luân phiên (Bài 6 — đúng bài tập cuối bài đó đã gợi ý) và LED chuyển sang màu đỏ. uart_task định kỳ gửi log nhiệt độ hiện tại qua UART (Bài 9), và nhận lệnh đặt tham số từ xa theo đúng giao thức dòng lệnh mini đã học (framing bằng '\n') — ví dụ SET THRESHOLD 30 đổi ngưỡng cảnh báo ngay lập tức mà không cần chạm vào nút bấm vật lý.

4. Độ bền & đo đạc

Một hệ thống "chạy được" chưa đủ — cần ĐO được nó đang chạy khoẻ tới đâu. CPU load được tính qua bộ đếm idle-task: tỷ lệ % số tick scheduler thực sự KHÔNG có việc gì để làm (dùng lại đúng nhật ký timeline có sẵn của mini-RTOS Bài 14, không cần cơ chế đo riêng). Verify trên hệ 4 task đúng kiến trúc Mục 1, chạy 500 tick: CPU load nghỉ (idle) khoảng 64,6% — hệ còn nhiều dư địa, chưa cần tối ưu gấp.

Stack watermark từng task đo bằng đúng kỹ thuật canary của Bài 12 — chạy thử với dữ liệu đầu vào đa dạng, xem mỗi task dùng SÂU tới đâu trong vùng đã cấp, để không cấp thiếu (tràn stack Bài 12) hay cấp thừa (lãng phí RAM quý giá).

💡 Outlook: watchdog & chế độ tiết kiệm điện
Watchdog timer: một bộ đếm phần cứng riêng biệt, phải được "vuốt ve" (kick) định kỳ bởi firmware — nếu firmware treo (vòng lặp vô hạn, deadlock Bài 15...) và quên kick, watchdog tự reset toàn hệ thống. Đây chính xác là cơ chế đã cứu Mars Pathfinder khỏi bị kẹt vĩnh viễn (Bài 15 Mục 5) dù không giải quyết được NGUYÊN NHÂN gốc.

Low-power: khi không có task nào runnable, CPU thật có thể chạy lệnh WFI (Wait For Interrupt) — ngủ sâu tới khi có ngắt đánh thức, tiết kiệm điện đáng kể so với vòng lặp rỗng chờ đợi. Với CPU load nghỉ ~64,6% đo ở trên, một trạm chạy pin sẽ "sống" lâu hơn đáng kể nếu tận dụng đúng cơ chế này thay vì busy-loop.

5. Thực hành: Bảng Mạch Ảo hoàn chỉnh

Demo dưới đây chạy toàn bộ Trạm đo nhiệt độ trên VMCU thật — thao tác như một thiết bị thật: xoay "cảm biến" giả lập, đặt ngưỡng, xem log UART, và xem hệ 4 task hoạt động khoẻ mạnh ra sao:

🌡️ Trạm Đo Nhiệt Độ — Bảng Mạch Ảo hoàn chỉnh

1. Cảm biến & ngưỡng cảnh báo

🔇 Im lặng
Đang tải…

2. UART — log tự động & lệnh đặt tham số

3. Resource Dashboard — 4 task trên mini-RTOS thật

Bấm nút để chạy mô phỏng.
datalogger_main.c
// Kien truc 4 task tren mini-RTOS - moi task mot vung stack RIENG (Bai 12)
queue_t sensor_queue; // sensor_task -> filter_task (Bai 15)
float g_filtered_celsius = 0;
float g_threshold_celsius = 30.0f;

void sensor_task(void) {
    uint16_t raw = adc_read_dma_buffer(); // Bai 10-11
    queue_send(&sensor_queue, raw);
}

void filter_task(void) {
    uint16_t raw;
    if (queue_receive(&sensor_queue, &raw, BLOCKING)) {
        static uint16_t history[8];
        push_moving_average(history, raw);           // Bai 10
        g_filtered_celsius = adc_code_to_celsius(moving_average(history)); // hieu chuan LM35
        if (g_filtered_celsius > g_threshold_celsius) buzzer_alarm_2tone(); // Bai 6
    }
}

void ui_task(void) {
    button_fsm_update();   // Bai 5 - nhan ngan doi che do, giu de luu nguong
    update_led(g_filtered_celsius, g_threshold_celsius);
}

void uart_task(void) {
    uart_send_log(g_filtered_celsius);          // Bai 9
    if (uart_command_ready()) parse_command();  // "SET THRESHOLD 35"
}
capstone_demo.js (đúng logic đang chạy ở tab Xem trước)
import { celsiusToAdcCode, adcCodeToCelsius, movingAverage, MiniRTOS, cpuIdlePercent, VMCU, MEMORY_MAP } from './vmcu.js';

// Hieu chuan LM35 that (Muc 2)
const code = celsiusToAdcCode(25, 3300, 10);          // 78
const celsius = adcCodeToCelsius(code, 3300, 10);      // ~25

// Resource dashboard (Muc 4) - dung lai mini-RTOS Bai 14 + idle-task counter
const rtos = new MiniRTOS();
rtos.addTask('sensor_task', 5, 2, 10);
rtos.addTask('filter_task', 6, 1, 10);
rtos.addTask('ui_task', 8, 1, 20);
rtos.addTask('uart_task', 10, 3, 50);
rtos.run(500);
console.log(cpuIdlePercent(rtos).toFixed(1) + '% idle'); // ~64.6%

// Stack watermark tung task (Bai 12 tra no dung han)
const ram = MEMORY_MAP.find(r => r.kind === 'ram');
const cpu = new VMCU();
cpu.initStackRegion(ram.base + ram.size, 256);
cpu.simulateDeepRecursion(5, 16);
console.log(cpu.stackWatermarkBytes()); // 80 byte da dung / 256 byte cap
✅ Checklist tự đánh giá
  • ☐ Nêu được đúng 4 task và vai trò của từng task trong kiến trúc
  • ☐ Giải thích được vì sao sensor_task cần ưu tiên cao nhất trong hệ
  • ☐ Tính lại được mã ADC cho một nhiệt độ LM35 bất kỳ (công thức Mục 2)
  • ☐ Vẽ được đường đi dữ liệu từ ADC → queue → filter → LED/buzzer/UART
  • ☐ Giải thích được vì sao mỗi task cần vùng stack riêng và cách đo watermark
  • ☐ Nêu được vai trò của watchdog và WFI, dù không cài đặt chi tiết
📎 Phụ lục: port sang board STM32 thật khác gì?
Mọi địa chỉ thanh ghi trong VMCU (GPIOA, TIM1, USART1, ADC1, DMA1, NVIC) đều lấy đúng theo STM32F103 thật — code đọc/ghi thanh ghi gần như copy-paste được. Khác biệt thật sự khi lên phần cứng: (1) cần một RTOS thật (FreeRTOS, Zephyr...) thay cho MiniRTOS mô phỏng — API khái niệm giống hệt (task, mutex, queue) nhưng cú pháp cụ thể khác; (2) thời gian chuyển đổi ADC/DMA thật có độ trễ vài micro-giây (VMCU bỏ qua để đơn giản); (3) cần trình biên dịch + linker script thật (Bài 12) thay vì mô phỏng section bằng JavaScript; (4) nhiễu điện thật phức tạp hơn nhiều so với hàm deterministicNoiseMv minh hoạ của Bài 10 — cần lọc phần cứng (RC) bổ sung cho ứng dụng đo lường chính xác cao.

Tóm lược

  • ✅ Kiến trúc 4 task (thu thập, lọc, UI, UART) giao tiếp qua queue — tổng hợp Bài 5, 6, 8–15 thành một hệ thống thật.
  • ✅ Hiệu chuẩn cảm biến LM35 verified: 0°C→mã 0, 25°C→mã 78, 100°C→mã 310 — công thức tuyến tính đơn giản nhưng dùng hệ số cảm biến thật.
  • ✅ CPU load qua idle-task counter verified ≈64,6% rảnh trên hệ 4 task 500 tick — đo được, không đoán mò; stack watermark từng task đo bằng canary thật của Bài 12.
  • ✅ Watchdog + WFI là outlook quan trọng cho độ bền/tiết kiệm điện — biết vai trò dù ngoài phạm vi cài đặt chi tiết của series.

Trắc nghiệm ôn tập

Câu 1

Vì sao sensor_task cần độ ưu tiên cao nhất trong kiến trúc 4 task của trạm đo?

Câu 2

Verified: 25°C (nhiệt độ phòng) trên cảm biến LM35 cho đúng mã ADC 78. Công thức nào giải thích con số này?

Câu 3

CPU load được tính qua "bộ đếm idle-task" — verified ≈64,6% rảnh trên hệ 4 task. Cơ chế đo này hoạt động thế nào?

Câu 4

Watchdog timer đóng vai trò gì trong một hệ thống nhúng thật, và nó có giải quyết được nguyên nhân gốc của một bug như priority inversion (Bài 15) không?

Tải file code thực hành minh họa toàn series

File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, hoàn chỉnh với GPIO, SysTick, Timer/PWM, NVIC/EXTI, UART, ADC, DMA, boot/memory layout, cooperative + preemptive scheduler, mutex/semaphore/queue, và hiệu chuẩn cảm biến — 250 self-test check đối chiếu đúng mọi hành vi xuyên suốt series (chạy node vmcu.js, không cần cài thêm gì):

Tải về vmcu.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 15: Đồng bộ RTOS: mutex, semaphore, queue & priority inversion 🎉 Hoàn thành Series — Quay lại Lộ trình Series Hệ Thống Nhúng

Bình luận