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?
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á).
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:
1. Cảm biến & ngưỡng cảnh báo
2. UART — log tự động & lệnh đặt tham số
3. Resource Dashboard — 4 task trên mini-RTOS thật
// 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"
}
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
- ☐ 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_taskcầ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
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ì):
Bình luận