Jump từ bootloader sang application có thể chỉ cần vài lệnh, nhưng phải chuyển giao đúng trạng thái CPU, vector table và phần cứng. Firmware vượt qua CRC (Cyclic Redundancy Check, phép kiểm tra dư thừa tuần hoàn) vẫn có thể chết nếu stack pointer sai, interrupt còn treo hoặc application nhận những giả định chỉ đúng sau reset.
Bài trước đã xác định application nằm ở đâu trong flash. Địa chỉ đó không chỉ dành cho linker. Bootloader cũng dùng nó để tìm vector table, kiểm tra entry point và thiết lập đường chuyển quyền thực thi.
Vấn đề nằm ở chỗ CPU không tự tạo lại trạng thái reset khi một hàm được gọi tại địa chỉ khác. Application nhận nguyên những gì bootloader để lại, trừ các trạng thái đã được xử lý rõ ràng trước lúc chuyển quyền.
flowchart LR
B[Bootloader đang chạy] --> V[Kiểm tra vector table]
V --> S[Dọn trạng thái dùng chung]
S --> C[Thiết lập context application]
C --> R[Reset Handler]
R --> I[SystemInit và C runtime]
I --> M[main]
Mục lục
Jump không phải gọi hàm
Một lời gọi hàm thông thường giữ nguyên stack, chế độ thực thi, vector table và hầu hết trạng thái phần cứng. Bên được gọi phải tuân theo Application Binary Interface (ABI) của bên gọi rồi quay về qua link register. Application sau bootloader lại không có quan hệ đó. Reset_Handler không được thiết kế để return về bootloader.
Vì vậy, bước này gần với chain-loading hơn là gọi một API. Bootloader phải tạo ra context mà startup code của application chấp nhận. Context đó không nhất thiết giống power-on reset hoàn toàn, nhưng mọi khác biệt được giữ lại phải là lựa chọn có chủ đích, không phải hệ quả bỏ sót.
Image hợp lệ trả lời câu hỏi “dữ liệu có đáng tin và nguyên vẹn không”. Handoff hợp lệ trả lời câu hỏi khác: “CPU có thể bắt đầu image này trong trạng thái hiện tại không”.
Hai giá trị quyết định điểm vào
Với Cortex-M, hai giá trị 32-bit đầu của vector table quyết định điểm vào application. Giá trị đầu tiên được nạp vào Main Stack Pointer (MSP). Giá trị kế tiếp là reset vector, tức địa chỉ Reset_Handler của application.
Tài liệu Cortex-M4 của STMicroelectronics mô tả rằng CPU nạp MSP từ word đầu và Program Counter (PC) từ reset vector khi reset. Bit 0 của reset vector phải bằng 1 để biểu diễn Thumb state. Khi application nằm tại APP_BASE, bootloader áp dụng cùng cấu trúc tại APP_BASE và APP_BASE + 4.
flowchart TB
A[APP_BASE] --> MSP[Word 0: Initial MSP]
B[APP_BASE + 4] --> RH[Word 1: Reset Handler]
MSP --> RAM[Phải thuộc vùng RAM hợp lệ]
RH --> FLASH[Phải thuộc vùng code của application]
RH --> THUMB[Bit 0 phải bằng 1]
Hai phép kiểm tra địa chỉ chỉ là bước rà soát sơ bộ. Chúng giúp loại image bị xóa, vector table hỏng hoặc linker layout không khớp. Chúng không thay thế CRC, hash, chữ ký số, chính sách phiên bản hay quy trình xác thực image đã được thiết kế ở lớp khác.
Vector table phải trỏ đúng
Sau khi branch, exception và interrupt phải được phân phối tới handler của application. Trên các core có Vector Table Offset Register (VTOR), thanh ghi này thuộc System Control Block (SCB) và được truy cập qua SCB->VTOR. Nếu VTOR vẫn trỏ vào bootloader, interrupt đầu tiên có thể chạy nhầm handler dù Reset_Handler đã vào đúng application.
VTOR không có mặt trên mọi cấu hình Cortex-M. Một số vi điều khiển (MCU) dùng memory remap hoặc cơ chế riêng. Alignment của vector table cũng phụ thuộc core và số lượng interrupt được triển khai. Device header, programming manual và reference manual phải được dùng thay cho một hằng số truyền miệng.
APP_BASE, linker script và địa chỉ vector table là cùng một quyết định kiến trúc. Ba giá trị lệch nhau thường tạo ra firmware vẫn build được nhưng không thể khởi động ổn định.
Kiểm tra trước khi chuyển quyền
Bootloader cần từ chối jump khi hai vector đầu không hợp lý. Phạm vi chính xác phải lấy từ memory map của chip và flash layout của sản phẩm, không lấy từ một ví dụ STM32 bất kỳ.
Phần kiểm tra tối thiểu thường gồm các điều kiện sau:
APP_BASEthỏa alignment mà vector table yêu cầu.- Initial MSP nằm trong một vùng RAM được application sử dụng và thỏa alignment của stack.
- Reset handler có bit Thumb hợp lệ.
- Địa chỉ code sau khi bỏ bit Thumb nằm trong vùng executable của application.
- Image đã vượt qua chính sách kiểm tra tính toàn vẹn, nguồn gốc và phiên bản của sản phẩm.
Đoạn mã dưới đây chỉ minh họa bước rà soát sơ bộ. RAM_START, RAM_END, APP_EXEC_START và APP_EXEC_END phải đến từ linker hoặc memory map thật của target. Ví dụ coi RAM_END là địa chỉ đầu tiên ngay sau vùng RAM dùng được, nên MSP có thể bằng giá trị này vì stack tăng theo hướng địa chỉ giảm.
#include <stdbool.h>
#include <stdint.h>
static bool app_vectors_sane(uint32_t app_base)
{
const uint32_t initial_msp = *(const volatile uint32_t *)app_base;
const uint32_t reset_vector =
*(const volatile uint32_t *)(app_base + 4U);
const uint32_t reset_address = reset_vector & ~1UL;
const bool msp_in_ram =
(initial_msp > RAM_START) && (initial_msp <= RAM_END);
const bool msp_aligned = (initial_msp & 0x7U) == 0U;
const bool thumb_entry = (reset_vector & 1U) == 1U;
const bool entry_in_app =
(reset_address >= APP_EXEC_START) &&
(reset_address < APP_EXEC_END);
return msp_in_ram && msp_aligned && thumb_entry && entry_in_app;
}
Để viết đúng phép kiểm tra initial_msp, bootloader phải hiểu cách bố trí bộ nhớ thật của target. MCU có thể có nhiều bank Static Random-Access Memory (SRAM), các vùng RAM không liên tục hoặc một phần RAM dành cho secure world.
Một biểu thức mask kiểu 0x20000000 chỉ đúng trong phạm vi mà thiết kế cụ thể đã chứng minh.
Trạng thái cũ phải được xử lý
Bootloader thường đã bật clock, timer, communication peripheral, Direct Memory Access (DMA) hoặc watchdog trước khi quyết định chạy application. Các khối này không biến mất sau một lệnh branch. Chúng có thể tiếp tục phát interrupt, ghi vào RAM hoặc reset hệ thống trong lúc C runtime của application còn đang khởi tạo.
SysTick cần được dừng nếu bootloader đã sử dụng. Các Interrupt Request (IRQ) liên quan phải được disable và trạng thái pending phải được xử lý. Quan trọng hơn, nguồn phát IRQ ở peripheral cũng phải được tắt; xóa pending tại Nested Vectored Interrupt Controller (NVIC) nhưng để cờ level-sensitive còn tồn tại chỉ khiến ngắt pending trở lại.
flowchart TD
P[Peripheral còn hoạt động] --> Q{Nguồn IRQ còn asserted?}
Q -- Có --> N[NVIC pending trở lại]
N --> W[Handler chạy trước khi app sẵn sàng]
Q -- Không --> H[Tiếp tục handoff]
D[DMA còn chạy] --> X[Ghi vào RAM của application]
T[Watchdog còn chạy] --> Y[Reset giữa startup]
Clock tree có thể được trả về trạng thái mặc định hoặc được giữ nguyên. Cả hai đều khả thi, nhưng application phải biết điều gì được bảo đảm. Nếu SystemInit() luôn cấu hình clock từ đầu, deinit clock ở bootloader giúp giảm phụ thuộc.
Nếu thời gian boot bị siết chặt, clock có thể được giữ lại. Đổi lại, contract phải mô tả thêm trạng thái clock và số kịch bản cần kiểm thử cũng tăng lên.
Các core có cấu hình cao hơn còn có cache, Memory Protection Unit (MPU), trạng thái Floating-Point Unit (FPU) hoặc cơ chế phân vùng bảo mật. DMA và bus master nằm ngoài core cũng cần được xem xét. Gọi một hàm HAL_DeInit() không thể thay thế danh sách trạng thái cần dọn riêng cho từng sản phẩm.
Handoff contract là danh sách trạng thái được bảo đảm tại điểm chuyển giao, không phải lời hứa chung chung rằng bootloader đã “dọn sạch”.
Interrupt là điểm dễ sai
__disable_irq() của Cortex Microcontroller Software Interface Standard (CMSIS) đặt thanh ghi mask ngắt PRIMASK. Nếu bootloader branch khi PRIMASK vẫn bị mask, application sẽ không nhận interrupt cho tới khi có code mở lại. Startup code chạy đúng sau hardware reset thường không cần làm việc này vì reset state của PRIMASK đã khác.
Cách xử lý phải được chọn dứt khoát. Bootloader có thể disable tạm thời, dọn nguồn ngắt, xóa enable/pending rồi khôi phục global interrupt mask ngay trước branch. Hoặc application startup phải chịu trách nhiệm mở lại interrupt tại một điểm đã định nghĩa. Trộn hai cách khiến lỗi phụ thuộc build và startup library.
Direct jump cũng không nên được bắt đầu trong Handler mode. Một exception đang active không thể được biến thành reset context chỉ bằng cách xóa pending bit. Nếu quyết định chuyển image được đưa ra từ Interrupt Service Routine (ISR), ISR nên ghi yêu cầu rồi quay về Thread mode; phương án reset thường gọn và an toàn hơn.
Compiler có thể dùng lại stack
Một mẫu code phổ biến gọi __set_MSP(initial_msp) rồi tiếp tục chạy vài câu lệnh C trước khi gọi reset handler. Mẫu này nhìn có vẻ hợp lý, nhưng compiler vẫn có thể dùng stack cho local variable, register spill hoặc function epilogue. Sau khi MSP đổi, các truy cập đó không còn trỏ vào stack frame của bootloader.
Phần đổi MSP và branch vì vậy phải nằm ở đoạn code cuối cùng, không return và không phát sinh truy cập stack. Cách triển khai thường là một trampoline assembly hoặc hàm naked dành riêng cho compiler/toolchain, sau khi toàn bộ validation và cleanup bằng C đã hoàn tất.
/* Mã giả mô tả ranh giới triển khai, không phải code dùng chung. */ prepare_hardware_for_handoff(app_base); const uint32_t app_msp = *(const uint32_t *)app_base; const uint32_t app_reset = *(const uint32_t *)(app_base + 4U); /* Từ đây phải đi vào trampoline assembly đã audit. * Trampoline đặt MSP, khôi phục interrupt policy đã chọn, * thực hiện barrier cần thiết và dùng BX (Branch and Exchange) * để chuyển tới app_reset. * Không được quay lại C hoặc return. */ arch_branch_to_image(app_msp, app_reset);
Nếu bootloader chạy Real-Time Operating System (RTOS) bằng Process Stack Pointer (PSP), chạy unprivileged hoặc thay đổi thanh ghi điều khiển CONTROL, trampoline còn phải xử lý context đó theo kiến trúc và toolchain cụ thể. Khi yêu cầu đã phức tạp đến mức này, system reset thường là lựa chọn dễ kiểm chứng hơn.
Jump trực tiếp hay reset
Jump trực tiếp giữ nguyên quá trình thực thi và chuyển thẳng tới application. Ưu điểm rõ nhất là thời gian chuyển giao ngắn và không phải lưu quyết định boot qua reset. Đổi lại, mọi trạng thái dùng chung còn sót đều trở thành một phần của giao diện giữa hai image.
Phương án reset trước khi chuyển giao lưu quyết định boot vào nơi còn dữ liệu qua system reset, sau đó gọi NVIC_SystemReset() hoặc cơ chế reset của MCU. Bootloader chạy lại từ đầu, đọc quyết định rồi chọn application.
CPU core được đưa gần trạng thái reset hơn. Chuỗi khởi động của application vì vậy bớt phụ thuộc vào bootloader trước đó.
| Tiêu chí | Jump trực tiếp | Reset trước chuyển giao |
|---|---|---|
| Thời gian boot | Ngắn hơn | Thêm một vòng reset và bước đọc quyết định boot |
| Trạng thái dùng chung | Phải dọn hoặc mô tả rõ | Ít hơn, nhưng không mặc nhiên bằng power-on reset |
| Độ phức tạp | Nằm trong trampoline và contract | Nằm trong boot flag và state machine |
| RTOS/PSP/ISR | Khó kiểm soát hơn | Thường dễ chuẩn hóa hơn |
| Nguy cơ reset loop | Thấp | Phải thiết kế consume/clear flag an toàn |
Software system reset không nhất thiết reset mọi miền peripheral, backup domain hoặc retained RAM giống power-on reset. Reference manual của MCU vẫn là nguồn quyết định. Boot flag cũng phải có trạng thái hợp lệ, cách đọc và xóa rõ ràng, cùng đường phục hồi khi mất nguồn hoặc reset lặp.
Với phần lớn thiết bị không bị ràng buộc quá chặt về thời gian boot, tôi ưu tiên reset trước khi chuyển giao. Jump trực tiếp phù hợp khi độ trễ thực sự đáng giá và toàn bộ chuỗi khởi động nằm trong quyền kiểm soát của cùng một đội phát triển.
Contract cần viết thành tài liệu
Contract tốt không dùng các từ mơ hồ như “trạng thái đủ sạch”. Tài liệu phải liệt kê chính xác trách nhiệm của từng phía. Bootloader, startup code của application và cấu hình linker phải cùng tuân theo nội dung có thể review và kiểm thử.
Ít nhất, tài liệu nên chốt các điểm sau:
- Application base, vector alignment và executable range.
- Các bước kiểm tra image phải thành công trước khi chuyển giao.
- CPU phải ở Thread mode, privileged hay không, dùng MSP hay PSP.
- Trạng thái thanh ghi mask ngắt
PRIMASK, thanh ghi mask ưu tiênBASEPRI, SysTick và NVIC. - Peripheral, DMA, clock, watchdog nào được giữ lại.
- Cache, MPU, FPU và security state nếu core có hỗ trợ.
- RAM nào bị xóa, giữ lại hoặc dùng để truyền boot reason.
- Reset type nào được dùng và miền nào thực sự bị reset.
flowchart LR
L[Linker contract] --> A[Application base và vectors]
B[Bootloader contract] --> A
A --> S[Startup contract]
S --> T[Test từ mọi boot path]
T --> P[Power-on]
T --> U[Sau update]
T --> R[Recovery hoặc rollback]
T --> W[Watchdog/system reset]
Kiểm thử đúng đường khởi động
Application chạy khi bấm Run trong debugger chưa chứng minh handoff đúng. Debugger có thể reset core, nạp lại thanh ghi hoặc khởi tạo memory theo cách khác bootloader. Bản release phải được chạy qua đúng đường mà thiết bị ngoài hiện trường sẽ sử dụng.
Tập kịch bản tối thiểu nên bao gồm power-on, software reset, watchdog reset, boot sau update, boot khi image bị từ chối và chuyển giao khi peripheral vừa hoạt động.
Một interrupt phát sinh ngay sát điểm branch cũng là ca kiểm thử có giá trị. Trường hợp này giúp bộc lộ xung đột thời điểm trong quy trình dọn trạng thái.
Tiêu chí hoàn thành không phải “đã tới main một lần”. Application phải khởi động lặp lại được từ mọi boot path mà sản phẩm công bố hỗ trợ.
Lựa chọn nên dùng
Nếu bootloader đơn giản, không có RTOS, chạy privileged Thread mode bằng MSP và thời gian boot quan trọng, jump trực tiếp có thể được giữ gọn. Phần kiểm tra, dọn trạng thái, VTOR và trampoline phải được coi là code kiến trúc, có vòng review riêng và được kiểm thử với đúng mức tối ưu compiler của bản release.
Nếu bootloader dùng RTOS, có nhiều peripheral hoặc DMA, hỗ trợ nhiều image hay được nhiều đội cùng bảo trì, reset trước khi chuyển giao thường tối ưu hơn. Chi phí thêm một vòng reset nhỏ hơn chi phí duy trì một giao diện trạng thái ngầm ngày càng lớn.
Bản chất của bước jump không nằm ở lệnh BX. Nó nằm ở việc hai firmware thống nhất trạng thái nào được chuyển giao, trạng thái nào phải được xóa và startup code được phép giả định điều gì. Khi contract đó rõ, vài lệnh cuối mới thực sự đơn giản.








