Phân biệt Flash, SRAM, EEPROM

Kiến trúc bộ nhớ trong Vi điều khiển: Phân biệt Flash, SRAM, EEPROM và cách tối ưu hóa bộ nhớ khi viết code

Đối với một lập trình viên phần mềm trên máy tính (PC), dung lượng RAM vài Gigabyte là chuyện bình thường. Nhưng khi bước chân vào thế giới của Vi điều khiển (Microcontroller – MCU), tài nguyên bộ nhớ được tính bằng từng Kilobyte (KB) hay thậm chí là Byte. Việc không hiểu rõ vi điều khiển của mình lưu trữ dữ liệu ở đâu và như thế nào sẽ rất dễ dẫn đến lỗi crash hệ thống, reset liên tục hoặc vòng đời sản phẩm bị rút ngắn.

Một vi điều khiển tiêu chuẩn (như họ AVR trên Arduino, STM32 hay ESP32) thường sở hữu 3 loại bộ nhớ vật lý riêng biệt: Flash, SRAM và EEPROM.

1. Bộ nhớ Flash (Program Memory / ROM)

  • Bản chất và Chức năng: Flash là bộ nhớ bất biến (Non-volatile), nghĩa là dữ liệu không bị mất đi khi mất điện. Đây là nơi lưu trữ “Firmware” – toàn bộ các dòng code chương trình mà bạn đã biên dịch và nạp vào chip.
  • Đặc tính kỹ thuật:
    • Dung lượng: Lớn nhất trong 3 loại. Ví dụ trên Arduino Uno (ATmega328P) là 32KB, trong khi ESP32 có thể từ 4MB đến 16MB.
    • Tốc độ: Đọc rất nhanh (để CPU lấy lệnh thực thi liên tục), nhưng ghi/xóa thì chậm.
    • Tuổi thọ (Write Cycles): Giới hạn số lần ghi/xóa (thường khoảng 10,000 đến 100,000 lần). Do đó, chúng ta chỉ ghi vào Flash khi nạp code hoặc cập nhật Firmware (OTA), tuyệt đối không dùng Flash để lưu trữ dữ liệu biến đổi liên tục trong quá trình mạch chạy.
    • Lưu trữ tĩnh: Ngoài code chương trình, Flash cũng lưu trữ các mảng dữ liệu hằng số (const) hoặc các chuỗi văn bản cố định nếu được lập trình viên chỉ định.

2. Bộ nhớ SRAM (Static Random Access Memory)

  • Bản chất và Chức năng: SRAM là bộ nhớ khả biến (Volatile), dữ liệu sẽ bay hơi hoàn toàn ngay khi tắt nguồn. Đây là “không gian làm việc” của vi điều khiển, nơi lưu trữ tất cả các biến (variables), mảng (arrays) được tạo ra khi code đang chạy.
  • Đặc tính kỹ thuật:
    • Dung lượng: Rất nhỏ và vô cùng quý giá. (Arduino Uno chỉ có vỏn vẹn 2KB SRAM).
    • Tốc độ: Truy xuất cực kỳ nhanh (nhanh nhất trong các loại bộ nhớ), hoạt động đồng bộ với tốc độ xung nhịp của CPU.
    • Tuổi thọ: Không giới hạn số lần đọc/ghi.
  • Cấu trúc phân bổ trong SRAM: SRAM thường được chia làm 3 khu vực chính khi chạy code:
    • Static Data: Nơi chứa các biến toàn cục (Global variables) và biến tĩnh (Static variables). Vùng này được cấp phát cố định ngay từ lúc khởi động.
    • Heap: Vùng nhớ cấp phát động. Khi bạn dùng các lệnh như malloc() trong C hoặc khai báo biến String trong C++, bộ nhớ sẽ được lấy từ Heap.
    • Stack: Vùng nhớ chứa các biến cục bộ (Local variables) bên trong các hàm (functions) và địa chỉ trả về khi gọi hàm. Khi hàm chạy xong, vùng Stack này lập tức được giải phóng.
    • (Lưu ý tử thần: Nếu Stack phình to ra và đụng vào Heap, hiện tượng “Stack Collision” hay “Tràn RAM” sẽ xảy ra, khiến vi điều khiển bị treo hoặc reset).

3. Bộ nhớ EEPROM (Electrically Erasable Programmable Read-Only Memory)

  • Bản chất và Chức năng: Giống như Flash, EEPROM là bộ nhớ bất biến (giữ dữ liệu khi mất điện). Tuy nhiên, nó được sinh ra để lưu trữ các thông số cấu hình, trạng thái cài đặt của người dùng, hoặc dữ liệu hiệu chuẩn (calibration data).
  • Đặc tính kỹ thuật:
    • Dung lượng: Rất nhỏ (Arduino Uno có 1KB EEPROM).
    • Cơ chế ghi: Khác với Flash phải xóa theo từng khối (Block), EEPROM cho phép đọc/ghi/xóa theo từng Byte đơn lẻ, rất linh hoạt.
    • Tốc độ: Ghi cực kỳ chậm (mất vài mili-giây cho mỗi byte).
    • Tuổi thọ: Thường chịu được khoảng 100,000 lần ghi/xóa. Do đó, cần có chiến lược ghi EEPROM hợp lý để không làm hỏng ô nhớ.

4. Cách tối ưu hóa bộ nhớ khi viết code (Memory Optimization)

Để một dự án hoạt động ổn định 24/7 mà không bị treo, kỹ năng quản lý bộ nhớ là bắt buộc. Dưới đây là các kỹ thuật tối ưu hóa thiết yếu:

  • Tối ưu hóa SRAM – Tránh dùng String:
    • Hạn chế tối đa việc sử dụng đối tượng String trong C++ (đặc biệt là các toán tử nối chuỗi String1 + String2). Việc này làm phân mảnh vùng nhớ Heap (Heap Fragmentation) rất nhanh.
    • Giải pháp: Sử dụng mảng ký tự kiểu C (char array[]) và các hàm tiêu chuẩn như strcpy(), strcat() để kiểm soát chính xác dung lượng RAM bị chiếm dụng.
  • Đẩy dữ liệu hằng số sang Flash (PROGMEM):
    • Theo mặc định, mọi biến bạn khai báo (dù là chuỗi văn bản hằng số) đều được MCU copy từ Flash sang SRAM khi khởi động. Nếu bạn có các mảng dữ liệu lớn (như font chữ cho màn hình OLED, mảng biểu tượng bitmap) hoặc các chuỗi debug dài Serial.println("Loi ket noi WiFi");, chúng sẽ ăn cạn kiệt SRAM.
    • Giải pháp: Sử dụng từ khóa PROGMEM (đối với họ AVR) hoặc đưa vào bộ nhớ ROM để ép MCU giữ dữ liệu đó ở nguyên trong Flash, chỉ đọc ra khi cần. Ví dụ: Dùng hàm F() macro: Serial.println(F("Loi ket noi WiFi"));.
  • Tối ưu hóa kiểu dữ liệu (Data Types):
    • Không lạm dụng kiểu int (chiếm 2 byte hoặc 4 byte tùy kiến trúc MCU) nếu giá trị bạn cần lưu chỉ từ 0 đến 255.
    • Giải pháp: Sử dụng các kiểu dữ liệu có kích thước cụ thể như uint8_t (chiếm đúng 1 byte), int16_t, uint32_t. Nếu bạn chỉ cần lưu trạng thái Đúng/Sai (1/0), hãy dùng kiểu bool hoặc sử dụng kỹ thuật “Bit mask” để gom 8 trạng thái vào trong một biến 1 byte duy nhất.
  • Chiến lược bảo vệ tuổi thọ EEPROM:
    • Đừng bao giờ đặt lệnh ghi EEPROM vào trong hàm loop() chạy liên tục.
    • Giải pháp: Chỉ ghi khi dữ liệu thực sự thay đổi. Thay vì dùng hàm EEPROM.write(), hãy dùng EEPROM.update() (hàm này sẽ đọc ô nhớ trước, nếu giá trị mới trùng với giá trị cũ, nó sẽ bỏ qua lệnh ghi, giúp tiết kiệm được 1 chu kỳ sống của EEPROM).

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Xem thêm: Phân biệt Flash, SRAM, EEPROM

Chia sẻ bài viết này

Share Facebook