Trong quá trình phát triển các ứng dụng nhúng, đôi khi chúng ta cần lưu trữ những thông số cấu hình (Calibration), địa chỉ ID thiết bị hoặc dữ liệu lịch sử mà không muốn chúng bị mất đi khi hệ thống bị ngắt nguồn điện. Bộ nhớ Flash tích hợp bên trong STM32 chính là giải pháp lý tưởng cho vấn đề này. Bài viết hôm nay sẽ giúp các bạn hiểu rõ cấu trúc và cách lập trình đọc/ghi Flash an toàn trên dòng chip STM32F407.
🎯 Mục tiêu bài học
- Nắm rõ thông số kỹ thuật bộ nhớ Flash của STM32F407.
- Hiểu cấu trúc phân chia Sector và vùng nhớ.
- Thành thạo cơ chế Lock/Unlock để bảo vệ bộ nhớ.
- Lập trình quy trình Erase (Xóa) và Program (Ghi) dữ liệu.
- Nắm vững quy tắc “Xóa trước khi Ghi” và quản lý lỗi Flash.
- Thực hành đọc dữ liệu trực tiếp từ địa chỉ ô nhớ.
1. Tổng quan về bộ nhớ Flash trên STM32F407
a) Đặc điểm kỹ thuật
Bộ nhớ Flash trong STM32F407 không chỉ là nơi chứa mã chương trình (Firmware) mà còn có thể được sử dụng làm bộ nhớ lưu trữ dữ liệu không bay hơi (Non-volatile memory). Các đặc điểm nổi bật bao gồm:
- Dung lượng: Lên đến 1 Mbyte.
- Độ linh hoạt: Hỗ trợ ghi theo Byte (8-bit), Half-word (16-bit), Word (32-bit) hoặc Double Word (64-bit).
- Cơ chế xóa: Xóa theo từng Sector hoặc xóa toàn bộ (Mass Erase).
- Vùng nhớ OTP: 512 Bytes bộ nhớ chỉ được ghi một lần (One-Time Programmable).
- Option Bytes: Dùng để cấu hình bảo vệ đọc/ghi, Watchdog và các chế độ Reset.
b) Phân chia vùng nhớ (Memory Map)
Vùng nhớ chính (Main Memory Block) của STM32F407 được chia thành các Sector có kích thước khác nhau. Việc nắm rõ kích thước này cực kỳ quan trọng vì bạn chỉ có thể xóa dữ liệu theo đơn vị tối thiểu là 1 Sector.
| Vùng nhớ |
Sector |
Kích thước |
Địa chỉ bắt đầu |
| Main Memory |
Sector 0 – 3 |
16 Kbytes mỗi Sector |
0x0800 0000 |
| Main Memory |
Sector 4 |
64 Kbytes |
0x0801 0000 |
| Main Memory |
Sector 5 – 11 |
128 Kbytes mỗi Sector |
0x0802 0000 |
| System Memory |
– |
30 Kbytes |
Dùng cho Bootloader |
2. Quy trình thao tác với Flash
a) Đọc dữ liệu từ Flash
Việc đọc dữ liệu từ Flash rất đơn giản vì nó được ánh xạ trực tiếp vào bản đồ bộ nhớ. Tuy nhiên, để đọc chính xác ở tần số cao, chúng ta cần cấu hình Latency (trạng thái chờ) trong thanh ghi FLASH_ACR tương ứng với điện áp và xung nhịp HCLK.
Ví dụ hàm đọc một giá trị 32-bit từ một địa chỉ:
uint32_t flash_read(uint32_t address) {
return (*(__IO uint32_t*) address);
}
b) Cơ chế bảo vệ: Lock và Unlock
Để tránh việc ghi nhầm do nhiễu hoặc lỗi chương trình, thanh ghi điều khiển Flash (FLASH_CR) luôn bị khóa sau khi Reset. Trước khi xóa hoặc ghi, bạn bắt buộc phải mở khóa:
HAL_FLASH_Unlock(); : Mở khóa Flash.
HAL_FLASH_Lock(); : Khóa Flash lại sau khi thao tác xong (Khuyến nghị để bảo mật).
c) Xóa Flash (Erase Sequence)
Một ô nhớ Flash chỉ có thể chuyển từ trạng thái 1 sang 0 khi ghi. Do đó, để ghi dữ liệu mới vào vùng đã có dữ liệu, bạn phải XÓA vùng đó trước (đưa tất cả về mức 1 hay 0xFF).
Sử dụng hàm: HAL_FLASHEx_Erase(&EraseInit, &SectorError);
Các thông số trong cấu trúc FLASH_EraseInitTypeDef:
- TypeErase: Xóa theo Sector hoặc toàn bộ chip.
- Sector: Sector bắt đầu xóa.
- NbSectors: Số lượng sector muốn xóa tính từ sector bắt đầu.
- VoltageRange: Dải điện áp (thường là
FLASH_VOLTAGE_RANGE_3 cho mức 2.7V – 3.6V).
d) Ghi dữ liệu (Programming Sequence)
Sau khi đã xóa, bạn có thể ghi dữ liệu bằng hàm:
HAL_FLASH_Program(TypeProgram, Address, Data);
Bạn có thể chọn kiểu ghi 8, 16, 32 hoặc 64 bit tùy vào nhu cầu tối ưu bộ nhớ.
3. Thực hành lập trình: Ghi và Kiểm tra dữ liệu Flash
Dưới đây là đoạn mã nguồn thực tế để xóa một vùng nhớ và ghi một giá trị 32-bit vào Sector người dùng (User Sector).
// Địa chỉ bắt đầu vùng nhớ người dùng (Ví dụ Sector 2)
#define FLASH_USER_START_ADDR ADDR_FLASH_SECTOR_2
#define FLASH_USER_END_ADDR ADDR_FLASH_SECTOR_3
#define DATA_32 ((uint32_t)0x12345678)
int main(void) {
HAL_Init();
SystemClock_Config();
/* 1. Mở khóa Flash */
HAL_FLASH_Unlock();
/* 2. Xóa vùng Flash dự định ghi */
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t SectorError = 0;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;
EraseInitStruct.Sector = FLASH_SECTOR_2;
EraseInitStruct.NbSectors = 1;
if (HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) {
// Xử lý lỗi xóa ở đây
Error_Handler();
}
/* 3. Ghi dữ liệu vào địa chỉ mong muốn */
uint32_t Address = FLASH_USER_START_ADDR;
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, DATA_32) != HAL_OK) {
// Xử lý lỗi ghi ở đây
Error_Handler();
}
/* 4. Khóa Flash lại */
HAL_FLASH_Lock();
/* 5. Kiểm tra lại dữ liệu đã ghi */
uint32_t read_data = *(__IO uint32_t*)Address;
if (read_data == DATA_32) {
// Ghi thành công!
}
while (1) {
// Chương trình chính
}
}
📌 Lưu ý quan trọng khi Debug
Để kiểm tra kết quả ghi Flash trong Keil MDK hoặc STM32CubeIDE:
- Vào chế độ Debug.
- Mở cửa sổ Memory.
- Nhập địa chỉ
0x08008000 (Địa chỉ của Sector 2).
- Quan sát giá trị tại ô nhớ đó trước và sau khi chạy code ghi.
🚀 Bài tập thực hành
- Lưu cấu hình: Viết chương trình đọc một giá trị từ biến trở (ADC), khi nhấn nút nhấn, hãy lưu giá trị này vào Flash. Khi khởi động lại chip, hãy đọc giá trị từ Flash ra để hiển thị lại.
- Xử lý mảng: Viết hàm ghi một mảng 10 số nguyên vào Flash và hàm đọc ngược lại để so sánh tính toàn vẹn.
📝 Tóm tắt: Giao tiếp Flash yêu cầu sự cẩn trọng cao vì số lần xóa/ghi của Flash là hữu hạn (thường khoảng 10,000 – 100,000 lần). Hãy luôn nhớ quy tắc: Unlock -> Erase -> Program -> Lock để đảm bảo dữ liệu được lưu trữ an toàn và bền vững.
“Nắm vững kỹ thuật quản lý bộ nhớ là bước tiến lớn để bạn xây dựng những hệ thống nhúng có khả năng tự phục hồi và lưu trữ cấu hình chuyên nghiệp.”
Gợi ý bài tiếp theo: Chuyên Sâu Bootloader: Cơ Chế Khởi Động, Quản Lý Vector Table Và Kỹ Thuật Jump Firmware.