Mở đầu: cái máy tính không ai nhìn thấy
Máy giặt nhà bạn có một máy tính bên trong. Cái điều khiển điều hoà cũng vậy. Chìa khoá ô tô thông minh, lò vi sóng, máy đo huyết áp, thậm chí cái bàn chải đánh răng điện — mỗi thứ đều đang chạy một chương trình, trên một con chip nhỏ xíu không ai gọi là "máy tính" cả. Đó là vi điều khiển (MCU) — nhân vật chính của toàn bộ series này. Khác hẳn với chiếc laptop bạn đang đọc bài viết trên đó, một MCU thường chỉ có vài chục KB RAM (không phải GB), chạy bằng pin cúc áo trong nhiều năm liền, và phải phản hồi một sự kiện trong vài micro-giây — chậm một nhịp là túi khí không bung kịp lúc cần.
Bài mở màn của lộ trình 16 bài này không nhảy ngay vào code. Trước khi viết được một dòng firmware có ý nghĩa, bạn cần một tấm bản đồ: MCU khác gì máy tính thường, bên trong nó có những gì, và quan trọng nhất — mọi thứ bên trong một MCU đều là một địa chỉ bộ nhớ. Hiểu đúng ý tưởng này ngay từ Bài 1 sẽ khiến toàn bộ 15 bài sau (thanh ghi, ngắt, ngoại vi, RTOS) trở thành hệ quả logic thay vì phép thuật ghi nhớ. Cuối bài, bạn sẽ tự tay đọc/ghi vào bộ nhớ của một MCU ảo chạy ngay trong trình duyệt — không cần mua board, không cần cài toolchain.
uint8_t, uint32_t) sẽ dùng liên tục từ Bài 2 trở đi.
Nên có: Series Điện Tử
— nhận biết LED, điện trở, breadboard giúp các bài sau (2–3, 6) dễ hình dung hơn hẳn, tuy không bắt buộc
vì mọi mạch điện liên quan đều được nhắc lại ngắn gọn khi cần.
1. Hệ nhúng quanh ta: máy tính "giấu mặt" trong thiết bị
Một hệ thống nhúng (embedded system) là một máy tính chuyên dụng, nhúng bên trong một thiết bị lớn hơn, làm đúng một việc, và người dùng cuối không hề biết (hay cần biết) rằng bên trong có một máy tính. So với máy tính đa dụng (PC, điện thoại), hệ nhúng sống với những ràng buộc hoàn toàn khác:
- Bộ nhớ: vài KB đến vài trăm KB — không phải GB.
- Năng lượng: nhiều thiết bị chạy bằng pin, kỳ vọng sống nhiều tháng đến nhiều năm.
- Thời gian thực: phải phản hồi một sự kiện trong khoảng thời gian đảm bảo (µs–ms), chứ không chỉ "cảm giác nhanh" như ứng dụng desktop.
- Giá thành: một MCU giá vài chục nghìn đồng, sản xuất hàng triệu con.
Ba thuật ngữ dễ lẫn nhau khi mới vào nghề — và chọn nhầm nền tảng ở đây là nguyên nhân phổ biến khiến một dự án phần cứng chết ngay từ ngày thiết kế đầu tiên:
| Thuật ngữ | Chạy được gì | Khi nào chọn |
|---|---|---|
| MCU (Microcontroller) | CPU + Flash + SRAM + ngoại vi trên một chip, không hệ điều hành đầy đủ (hoặc RTOS rất nhỏ) | Cần realtime chặt, giá rẻ, tiết kiệm điện — đúng đối tượng của series này |
| MPU (Microprocessor) | Chỉ CPU — cần RAM/Flash/ngoại vi rời gắn ngoài để chạy được | Cần chạy Linux đầy đủ, network stack phức tạp, nhiều tiến trình song song |
| SoC (System on Chip) | MPU + nhiều khối chuyên dụng (GPU, modem, mã hoá...) tích hợp cùng die — như chip trong điện thoại | Thiết bị cần hiệu năng cao + nhiều chức năng cùng lúc (điện thoại, camera thông minh) |
Series này tập trung hoàn toàn vào MCU: rẻ, tiết kiệm điện, và — quan trọng với người học — đơn giản đủ để hiểu trọn vẹn từ thanh ghi đến RTOS trong 16 bài, thay vì chìm trong hàng chục lớp trừu tượng của một hệ điều hành Linux đầy đủ.
2. Giải phẫu một MCU
Mở nắp một MCU ra (theo nghĩa bóng), bạn sẽ thấy 3 khối chính cùng nằm trên một die (miếng bán dẫn silicon duy nhất — chi tiết cách một die được thiết kế và sản xuất, các bạn có thể xem sâu hơn ở Series VLSI): một lõi CPU (thường là kiến trúc ARM Cortex-M), bộ nhớ Flash chứa chương trình, bộ nhớ SRAM cho biến chạy, và một dàn ngoại vi (peripheral) — các mạch phần cứng chuyên trách như GPIO, timer, UART, ADC mà 10 bài tiếp theo sẽ lần lượt "nối dây" vào.
Một câu hỏi kiến trúc âm thầm quyết định tốc độ: CPU lấy lệnh và lấy dữ liệu qua chung một đường bus hay hai đường riêng? Kiến trúc Von Neumann cổ điển dùng chung một bus cho cả lệnh và dữ liệu — đơn giản nhưng lệnh và dữ liệu phải "xếp hàng" chờ nhau. Kiến trúc Harvard tách hẳn hai bus: CPU có thể lấy lệnh tiếp theo đồng thời với việc đọc/ ghi dữ liệu của lệnh hiện tại. Hầu hết MCU ARM Cortex-M dùng biến thể gọi là Harvard cải tiến (modified Harvard): bus lệnh và bus dữ liệu tách riêng ở tầng vật lý, nhưng vẫn cùng nhìn về một không gian địa chỉ thống nhất — ý tưởng sẽ rõ hơn hẳn ở Mục 3.
Mọi khối trên die đều "thở" theo một nhịp chung gọi là clock — một tín hiệu dao động đều đặn hàng triệu lần mỗi giây. CPU thực thi lệnh theo nhịp clock, ngoại vi cũng đếm theo nhịp clock (chính clock này, chia nhỏ ra bằng một bộ đếm gọi là prescaler, sẽ trở thành công thức tính tần số PWM ở Bài 6). Ở mức khái niệm bây giờ, chỉ cần nhớ: không có clock, không gì chạy cả — kể cả CPU.
3. Memory map — "mọi thứ là một địa chỉ"
Đây là ý tưởng quan trọng nhất của cả bài, và có lẽ của cả nửa đầu series. Trên một MCU 32-bit, CPU có thể
đánh địa chỉ tối đa $2^{32}$ — khoảng 4 tỷ địa chỉ byte khác nhau, từ 0x00000000 đến
0xFFFFFFFF. Điều bất ngờ với người mới:
Flash, SRAM, và thanh ghi điều khiển của mọi ngoại vi đều cùng nằm trên MỘT không gian địa chỉ
đó
— không có "cõi riêng" nào cho bộ nhớ và "cõi riêng" khác cho phần cứng.
VMCU (MCU ảo mà series này tự xây và sẽ dùng xuyên suốt 16 bài) mô phỏng đúng cách chia vùng phổ biến trên các MCU Cortex-M thật (kiểu STM32F1):
| Vùng | Địa chỉ bắt đầu | Vai trò |
|---|---|---|
| Flash | 0x08000000 |
Chứa mã máy đã biên dịch (firmware) — chỉ đọc qua bus thường |
| SRAM | 0x20000000 |
Đọc/ghi tự do — biến toàn cục, stack, heap sống ở đây lúc chạy |
| Peripheral | 0x40000000 |
Thanh ghi điều khiển phần cứng — GPIO, timer, UART... (Bài 2 trở đi mới nối) |
Hệ quả trực tiếp: đọc hoặc ghi vào một địa chỉ cụ thể có ý nghĩa hoàn toàn khác nhau tuỳ nó rơi vào vùng nào. Đây chính là ý tưởng gọi là ánh xạ vào/ra vào bộ nhớ (Memory-Mapped I/O — sẽ đi sâu ở Bài 2): điều khiển phần cứng không qua lệnh CPU đặc biệt, mà qua đúng phép đọc/ghi bộ nhớ bạn đã biết từ Series C.
// Mot con tro toi dia chi CO DINH trong vung SRAM
volatile uint32_t *o = (volatile uint32_t *)0x20000010;
*o = 0x42; // ghi byte thap vao SRAM - hop le, binh thuong
uint32_t v = *o; // doc lai - duoc 0x42
// Mot con tro toi dia chi trong vung Flash
volatile uint32_t *f = (volatile uint32_t *)0x08000100;
// *f = 0x99; // KHONG THE ghi qua bus thuong - Flash chi doc
// Mot dia chi KHONG thuoc vung nao da anh xa
volatile uint32_t *bad = (volatile uint32_t *)0x00001000;
// uint32_t crash = *bad; // HardFault - CPU khong biet phai lam gi o day
Segmentation fault kèm stack trace, tên hàm, số dòng — hệ điều hành (có bộ nhớ ảo, có trình
gỡ lỗi tích hợp) đứng ra "dọn dẹp" hộ bạn. Trên MCU chạy bare-metal (không có hệ điều hành đứng giữa để
bảo vệ), truy cập một địa chỉ không thuộc vùng nhớ nào đã được ánh xạ khiến lõi ARM nhảy thẳng vào một
HardFault
— về cơ bản là "tôi không biết phải làm gì ở địa chỉ này, dừng lại đây". Không có tên biến, không có số
dòng, chỉ có vài thanh ghi CPU tại thời điểm gặp lỗi để bạn tự suy luận ngược. Đây là lý do dân nhúng
cẩn trọng với con trỏ hơn hẳn dân lập trình ứng dụng — một lỗi con trỏ nhỏ có thể treo cứng cả thiết bị
mà không để lại manh mối nào. Thử ngay điều này ở demo Mục 5.
4. Toolchain thật sự trông thế nào
Series này chạy VMCU ngay trong trang để bạn không cần mua board hay cài đặt gì trước khi học khái niệm — nhưng để không bỡ ngỡ khi cầm board thật lên tay, mọi bài đều chỉ rõ lệnh và công cụ thật đứng sau mỗi khái niệm. Quy trình phát triển firmware thật gồm 4 mảnh:
-
Cross-compiler (
arm-none-eabi-gcc): biên dịch mã C bạn viết trên máy tính (kiến trúc x86/ARM64) thành mã máy cho kiến trúc khác — ARM Cortex-M của MCU đích. "Cross" nghĩa là máy biên dịch và máy chạy không cùng kiến trúc. - Linker script: một tệp mô tả chính xác memory map ở Mục 3 cho trình liên kết biết — phần nào của chương trình (mã lệnh, hằng số, biến khởi tạo) rơi vào Flash, phần nào rơi vào SRAM.
- Debug probe (ST-Link, J-Link): một thiết bị phần cứng nhỏ nối máy tính với MCU qua giao thức SWD/JTAG, dùng để nạp firmware vào Flash và đặt breakpoint gỡ lỗi trực tiếp trên chip thật.
- Vòng lặp build → flash → debug: sửa code, biên dịch lại, nạp lại, chạy thử trên phần cứng thật — lặp lại hàng chục/hàng trăm lần cho một tính năng nhỏ.
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
/* Lenh nam trong FLASH; bien khoi tao/stack/heap nam trong SRAM */
SECTIONS
{
.text : { *(.text) } > FLASH
.data : { *(.data) } > SRAM AT > FLASH
.bss : { *(.bss) } > SRAM
}
.data vừa
nằm ở SRAM vừa "AT" Flash, và điều gì thật sự chạy trước cả hàm main() sẽ được mổ xẻ đầy đủ
ở Bài 12 (Boot & bản đồ bộ nhớ chương trình). Ở đây chỉ cần nhận ra: linker script chính là bản dịch
bằng văn bản của cùng một bức tranh memory map bạn vừa học.
5. Thực hành: Memory Map Explorer
Demo dưới đây chạy trên VMCU thật — không giả lập bằng hoạt ảnh, mà là một lớp
VMCU với đúng 3 vùng nhớ ở Mục 3, có hàm read8/write8 thật sự kiểm
tra địa chỉ và ném lỗi khi cần. Bấm một vùng để xem chi tiết, rồi tự tay đọc/ghi bằng địa chỉ hex:
Gợi ý thử ngay: đọc/ghi ở 0x20000010 (SRAM — hoạt động bình thường) · ghi ở
0x08000100 (Flash — bị từ chối, KHÔNG phải crash) · đọc ở 0x00001000
(không thuộc vùng nào — HardFault, đúng cạm bẫy Mục 3).
#include <stdint.h>
// Doc/ghi dung 1 dia chi SRAM - dung C bare-metal that
volatile uint8_t *sram_byte = (volatile uint8_t *)0x20000010;
*sram_byte = 0x42; // ghi - hop le
uint8_t v = *sram_byte; // doc lai - duoc 0x42, giong het demo
// Ghi vao Flash qua bus thuong KHONG lam gi ca / bi tu choi o phan cung that
// (can trinh tu lap trinh lai chuyen biet, ngoai pham vi bai nay)
// Doc mot dia chi KHONG duoc anh xa -> HardFault_Handler duoc goi
volatile uint8_t *bad = (volatile uint8_t *)0x00001000;
uint8_t crash = *bad; // treo tai day tren phan cung that
import { VMCU, findRegion, HardFaultError, FlashProtectedError } from './vmcu.js';
const cpu = new VMCU();
try {
cpu.write8(0x20000010, 0x42); // SRAM - thanh cong
console.log(cpu.read8(0x20000010)); // 0x42
} catch (e) { /* khong xay ra o day */ }
try {
cpu.write8(0x08000100, 0x99); // Flash - bi tu choi
} catch (e) {
console.log(e instanceof FlashProtectedError); // true
}
try {
cpu.read8(0x00001000); // dia chi khong hop le
} catch (e) {
console.log(e instanceof HardFaultError); // true
}
Tóm lược
- ✅ Hệ nhúng là máy tính chuyên dụng "giấu mặt" trong thiết bị, ràng buộc bởi RAM ít, pin lâu, deadline chặt. MCU tích hợp CPU+Flash+SRAM+ngoại vi trên một chip — khác MPU (cần linh kiện rời) và SoC (nhiều khối chuyên dụng tích hợp).
- ✅ Kiến trúc Harvard cải tiến của ARM Cortex-M tách bus lệnh/dữ liệu để fetch song song, nhưng vẫn dùng chung một không gian địa chỉ thống nhất.
- ✅ Memory map: Flash, SRAM, và thanh ghi ngoại vi cùng nằm trên MỘT không gian địa chỉ 32-bit — đây là nền tảng của mọi khái niệm firmware từ Bài 2 trở đi.
- ✅ Truy cập địa chỉ không được ánh xạ → HardFault, không có stack trace thân thiện như trên PC — cẩn trọng với con trỏ hơn hẳn lập trình ứng dụng thường.
- ✅ Toolchain thật gồm cross-compiler, linker script, debug probe — series chạy VMCU để học nhanh, nhưng mọi khái niệm đều ánh xạ 1-1 sang board thật.
Trắc nghiệm ôn tập
Câu 1
Một dự án cần chạy Linux đầy đủ với network stack phức tạp và nhiều tiến trình song song. Nên chọn nền tảng nào?
Câu 2
Kiến trúc Harvard (và biến thể "Harvard cải tiến" trên ARM Cortex-M) tách bus lệnh và bus dữ liệu riêng. Lợi ích chính là gì?
Câu 3
Trên VMCU (và trên MCU Cortex-M thật), Flash, SRAM và thanh ghi ngoại vi có mối quan hệ không gian địa chỉ như thế nào?
Câu 4
Trong demo Memory Map Explorer: ghi vào một địa chỉ Flash bị từ chối (không có tác dụng), còn đọc ở một
địa chỉ hoàn toàn không được ánh xạ (như 0x00001000) thì ném ra HardFault. Vì sao đây là 2
loại phản hồi khác nhau?
Tải file code thực hành minh họa bài học
File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, kèm self-test đối chiếu
đúng mọi hành vi trong bài (chạy node vmcu.js, không cần cài thêm gì):
Bình luận