Trong lộ trình phát triển sản phẩm nhúng chuyên nghiệp, việc chỉ biết viết code ứng dụng (Application) là chưa đủ. Để thiết bị có khả năng tự cập nhật từ xa (FOTA), khôi phục cài đặt gốc (Factory Reset) hay bảo mật Firmware, chúng ta cần một chương trình đóng vai trò “người gác cổng” – đó chính là Bootloader. Bài viết này sẽ đi sâu vào cấu trúc phần cứng của nhân Cortex-M4 để giải mã cách Bootloader điều khiển quy trình khởi động của hệ thống.
🎯 Mục tiêu bài học chuyên sâu
- Nắm vững khái niệm Bootloader và lộ trình thực thi Firmware (Current, FOTA, Factory).
- Giải mã vai trò của thanh ghi SP (Stack Pointer) và PC (Program Counter) trong 4 byte đầu tiên của bộ nhớ.
- Hiểu bản chất Vector Table và tại sao phải dời địa chỉ bảng Vector khi chạy Application.
- Lập trình quy trình “De-initialization” để đưa MCU về trạng thái sạch trước khi nhảy.
- Thực hành cấu hình Toolchain để nạp chồng nhiều chương trình vào Flash mà không bị xóa dữ liệu cũ.
1. Kiến trúc hệ thống: Bootloader vs Application
a) Bootloader là gì?
Bootloader là chương trình đầu tiên chạy khi Chip được cấp nguồn hoặc Reset. Nhiệm vụ của nó là kiểm tra các điều kiện phần cứng (ví dụ: nút bấm được giữ) hoặc phần mềm (ví dụ: cờ báo cập nhật trong Flash) để quyết định nhảy vào một trong các vùng nhớ:
- Current Firmware: Bản ứng dụng hiện tại đang hoạt động.
- FOTA Firmware: Bản cập nhật mới vừa được tải về vùng nhớ tạm.
- Factory Firmware: Bản Firmware gốc an toàn do nhà sản xuất nạp sẵn.
b) Phân vùng bộ nhớ Flash
Bootloader và Application là hai dự án (Project) hoàn toàn riêng biệt, được biên dịch ra hai file nhị phân khác nhau và lưu trữ tại các vùng địa chỉ khác nhau trên Flash:
- Bootloader: Bắt đầu tại
0x0800 0000 (Địa chỉ mặc định sau Reset).
- Application: Bắt đầu tại một địa chỉ do người dùng chỉ định, ví dụ:
0x0800 4000 (Sector 1).
2. Cơ chế khởi động của nhân ARM Cortex-M
Khi CPU thoát khỏi trạng thái Reset, nó thực hiện các bước sau một cách tự động tại địa chỉ 0x0800 0000:
- Nạp MSP: Đọc 4 byte đầu tiên để lấy giá trị khởi tạo cho Main Stack Pointer.
- Nạp PC: Đọc 4 byte tiếp theo (địa chỉ
+4) để lấy địa chỉ của Reset Handler.
- Thực thi: Nhảy đến địa chỉ Reset Handler để bắt đầu chạy code.
Lưu ý: Mọi chương trình Application nằm ở các vùng nhớ khác (như 0x0800 4000) cũng phải tuân thủ đúng cấu trúc này ở 8 byte đầu tiên của nó.
3. Vector Table Relocation – “Dời đô” cho bảng ngắt
Bảng Vector chứa địa chỉ các hàm xử lý ngắt (Interrupt Handlers). Nếu bạn nhảy sang App ở địa chỉ 0x0800 4000 mà không dời bảng Vector, khi có ngắt xảy ra (ví dụ ngắt Timer), CPU vẫn sẽ tìm địa chỉ hàm xử lý ở vùng của Bootloader (địa chỉ 0x0800 0000), dẫn đến hệ thống bị treo hoặc thực thi sai.
Cách xử lý: Trong chương trình Application, chúng ta phải định nghĩa lại VECT_TAB_OFFSET trong file system_stm32f4xx.c:
/* Trong file system_stm32f4xx.c của dự án Application */
#define USER_VECT_TAB_ADDRESS
#define VECT_TAB_OFFSET 0x00004000U // Khớp với địa chỉ nạp App
4. Lập trình Bootloader: Quy trình “Jump” an toàn
Để chương trình Application chạy ổn định sau khi nhảy từ Bootloader, chúng ta cần đưa vi điều khiển về trạng thái “như mới” bằng cách vô hiệu hóa các ngoại vi đã dùng ở Bootloader.
Bước 1: Vô hiệu hóa ngoại vi (De-init)
/* 1. Tắt Clock của các ngoại vi và Reset cấu hình RCC */
HAL_RCC_DeInit();
/* 2. Vô hiệu hóa Systick và các ngắt hệ thống */
HAL_DeInit();
/* 3. Tắt các Fault Handler để tránh lỗi nhảy bất ngờ */
SCB->SHCSR &= ~(SCB_SHCSR_USGFAULTENA_Msk | SCB_SHCSR_BUSFAULTENA_Msk | SCB_SHCSR_MEMFAULTENA_Msk);
Bước 2: Thiết lập con trỏ và Nhảy (Jump)
Chúng ta cần trích xuất giá trị SP và địa chỉ Reset Handler từ vùng nhớ của Application.
#define APP_START_ADDRESS 0x08004000
void jump_to_app(void) {
// Lấy giá trị Stack Pointer từ 4 byte đầu của App
uint32_t msp_val = *(volatile uint32_t*)APP_START_ADDRESS;
// Lấy địa chỉ Reset Handler từ 4 byte tiếp theo
uint32_t jump_addr = *(volatile uint32_t*)(APP_START_ADDRESS + 4);
// Định nghĩa con trỏ hàm trỏ tới địa chỉ Reset Handler
void (*app_reset_handler)(void) = (void*)jump_addr;
// Thiết lập MSP cho Application
__set_MSP(msp_val);
// Nhảy sang App
app_reset_handler();
}
5. Cấu hình Toolchain: Nạp chồng Firmware
Mặc định khi nhấn “Download” trong Keil MDK, nó sẽ xóa toàn bộ Chip (Full Chip Erase). Điều này sẽ xóa mất chương trình Bootloader khi bạn nạp App. Bạn cần cấu hình lại:
- Vào Options for Target -> Debug -> Settings.
- Chọn tab Flash Download.
- Trong mục “Erase”, chọn “Erase Sectors” (chỉ xóa những vùng cần nạp) thay vì “Full Chip Erase”.
🚀 Bài tập thực hành
- Điều kiện Jump: Viết Bootloader sao cho: Nếu chân
PA0 nối xuống GND khi khởi động thì nhảy vào App_1, nếu nối lên 3.3V thì nhảy vào App_2.
- Kiểm tra Flash: Trước khi thực hiện lệnh
app_reset_handler(), hãy kiểm tra xem 4 byte đầu tiên của địa chỉ App có phải là một địa chỉ RAM hợp lệ không (thường bắt đầu bằng 0x2000...). Nếu không đúng, không thực hiện Jump để tránh HardFault.
📝 Tóm tắt: Bootloader là chương trình “nền tảng” giúp quản lý vòng đời của thiết bị. Hiểu rõ cơ chế nạp MSP, PC và cách dời bảng Vector ngắt (VTOR) là chìa khóa để triển khai thành công các hệ thống nhúng có khả năng cập nhật linh hoạt và bền bỉ.
“Lập trình viên viết ứng dụng, nhưng Kỹ sư hệ thống làm chủ quy trình vận hành của ứng dụng đó.”
Gợi ý bài tiếp theo: Tổng Ôn Kiến Thức Hệ Thống Nhúng & STM32 (Phần 1).