Tiết kiệm RAM trong Arduino?

Tại sao bạn cần đọc bài này ?

Như đã nói ở bài trước Cách lưu trữ các biến số, mảng, chuỗi trong Arduino, chúng ta đã biết rằng các loại biến trong Arduino được lưu ở những vùng nhớ khác nhau trong RAM, và khi hết RAM thì chương trình của bạn sẽ die một cách bất ngờ - vì lỗi không nằm trong code.

Vì vậy, hôm nay, chúng ta sẽ tìm cách giải quyết vấn đề "làm thế nào để giảm thiểu việc sử dụng RAM trong một sketch Arduino?".

Những điều bạn sẽ rút ra được từ bài này

  1. Sự tương đối trong việc sử dụng bộ nhớ RAM và bộ nhớ FLASH
  2. Giảm thiểu RAM mà kiểu string sử dụng!

Lưu ý: Bạn cần phải đọc bài Cách lưu trữ các biến số, mảng, chuỗi trong Arduino trước khi tiến hành nghiên cứu bài viết này. Bài viết được chạy trên Arduino IDE 1.0.4 nên nếu bạn sử dụng một phiên bản Arduino IDE khác thì lượng RAM còn trống sẽ có một sự chênh lệch nhẹ (khoảng vài chục byte).

Các định luật bảo toàn

Từ hồi học trung học cơ sở, chúng ta đã học rất nhiều về các định luật bảo toàn. Bây giờ, mình vẫn còn có thể kể tên một số định luật bảo toàn mà mình đã học như là:

  • Định luật bảo toàn năng lượng
  • Định luật bảo toàn khối lượng
  • Định luật bảo toàn động lượng
  • Định luật bảo toàn mômen động lượng
  • Định luật bảo toàn điện tích

Điểm chung của các định luật bảo toàn đó chính là sự bảo toàn một thông số vật lý nhất định. Ví dụ: định luật bảo toàn năng lượng được phát biểu nôm na rằng, năng lượng không tự sinh ra và không tự mất đi. Nó chỉ chuyển từ dạng năng lượng này sang dạng năng lượng khác mà thôi.

Vậy các định luật bảo toàn có liên quan đến việc sử dụng RAM hay bộ nhớ FLASH?

Vâng, nó có liên quan đấy. Bởi vì, khi bạn tìm cách giảm thiểu việc sử dụng bộ nhớ RAM của bạn thì thực chất việc bạn làm là chuyển những gì đáng nhẻ ra nó phải lưu trên bộ nhớ RAM sang một bộ nhớ khác mà thôi. Định luật bảo toàn bộ nhớ! Như vậy, bạn không thể nào "dịch chuyển" những gì mà trước đây bạn lưu vào bộ nhớ RAM vào "không khí" được! Nhưng, bạn có thể đưa một số thứ mà trước đây bạn lưu trên bộ nhớ RAM vào bộ nhớ FLASH.

Tìm cách giảm thiểu RAM bằng các kĩ thuật lập trình đơn giản

Đầu tiên, chúng ta hãy thử xem đoạn code đơn giản này tốn bao nhiêu RAM bộ nhớ để lưu trữ! À, bởi vì chúng ta chỉ xét đến việc đoạn code này "tốn bao nhiêu RAM" và tìm cách giảm thiểu nó thôi nên bạn không cần phải lắp mạch gì đâu nhé! Bạn hãy bỏ chọn Autoscroll trong Serial Monitor đi nhé.

int getMemoryFree() {
  // Trong trường hợp này, ta có thể hiểu extern sẽ khai báo một biến toàn cục trong chương trình (nếu chưa có) hoặc include một biến toàn cục đã được extern trước đó
  extern int __heap_start;
  extern int *__brkval; 
  
  //Dấu & phía trước tên biến / tên con trỏ sẽ cho ta biết vị trí ô nhớ mà nó đang đứng
  //Lưu ý: bài viết này không dành cho beginner và bạn cần tưởng tượng một chút để có thể mườn tượng vấn đề
  return (int) SP - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

const byte buttonPin[] = {2, 3, 4, 5, 6, 7};
const byte pinCount = 6; // Bạn có thể dùng hàm sizeof(buttonPin) / sizeof(byte) để tính ra số này

byte buttonPressed() {
  for (byte i = 0; i < pinCount; i++)
    if (digitalRead(buttonPin[i]) == HIGH)
      return i;
  return pinCount;
}

void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
  for (byte i = 0; i < pinCount; i++)
    pinMode(buttonPin[i], INPUT);
    
  Serial.println(getMemoryFree());
}

void loop () {
  byte button = buttonPressed();
  switch (button) {
    case 0:
      Serial.println("Ban da nhan phim THU NHAT");
      break;
    case 1:
      Serial.println("Ban da nhan phim THU HAI");
      break;
    case 2:
      Serial.println("Ban da nhan phim THU BA");
      break;
    case 3:
      Serial.println("Ban da nhan phim THU TU");
      break;
    case 4:
      Serial.println("Ban da nhan phim THU NAM");
      break;
    case 5:
      Serial.println("Ban da nhan phim THU SAU");
      break;
  };
}

string được lưu trữ trong vùng nhớ tĩnh, tức là nó sẽ vi điều khiển (vđk) Atmega328 gửi đến lưu ở bộ nhớ RAM khi khởi động chương trình (trước khi chương trình chạy lệnh setup), nên chúng ta chỉ cần chạy dòng lệnh getMemoryFree() và biết được bao nhiêu RAM còn trống trong sketch này.

Như vậy, chúng ta còn 1686 byte ô nhớ RAM còn có thể sử dụng được. Tức là còn 2048 - 1686 = 362 byte ô nhớ RAM đã được sử dụng cho sketch trên. Một sketch khá là ngắn với chức năng quá ít ỏi. Nào, hãy thử cải thiện hàm loop một tí nào!

void loop () {
  byte button = buttonPressed();
  if (button < pinCount) {
    Serial.print("Ban da nhan phim THU ");
    switch (button) {
      case 0:
        Serial.println("NHAT");
        break;
      case 1:
        Serial.println("HAI");
        break;
      case 2:
        Serial.println("BA");
        break;
      case 3:
        Serial.println("TU");
        break;
      case 4:
        Serial.println("THU NAM");
        break;
      case 5:
        Serial.println("SAU");
        break;
    };
  }
}

Với một chút thay đổi nhỏ trong đoạn code trên, chúng ta đã giảm thiểu đến 104 ô nhớ RAM cần sử dụng. Vì vậy, bạn hãy cẩn thận trong việc đặt các message thông báo qua Serial Monitor như thế này nhé devil. Với việc tối ưu hóa code như trên thì chúng ta cũng chỉ giảm được một vài ô nhớ mà thôi. Vấn đề quan trọng trong bài này đó là chuyến hết việc lưu trữ string từ bộ nhớ RAM qua bộ nhớ FLASH!

Và thật may mắn, chúng ta có thể dễ dàng làm được điều đó bằng việc include thư viện avr/pgmspace.h

#include <avr/pgmspace.h>

Đây là bộ thư viện của các bé AVR (các dòng ATmega hay ATiny,...), nó cung cấp cho sketch của chúng ta những hàm và các bộ macro tiền xử lý nhằm định nghĩa các biến chuỗi để nó có thể được ghi vào bộ nhớ flash, và  giúp cho sketch của chúng ta có thể đọc được những chuỗi đó trong thời gian thực thi chương trình.

Vì sao chúng ta phải add cái thư viện này, nó thật phức tạp phải không nào? Vâng, điều này đúng là có hơi phức tạp. Nhưng chúng ta hãy cùng tìm hiểu sâu về bộ nhớ flash của máy tính một tí nhé.

Bộ nhớ flash của máy tính đáng tiếc là không có các địa chỉ ô nhớ như bộ nhớ RAM, nghĩa là ta không thể truy xuất đến một vùng nhớ trong flash bằng những địa chỉ trong vùng nhớ đó bằng cách thông thường được! Tuy nhiên, trong các họ vđk AVR, vùng nhớ flash của nó được chia ra làm 2 thành phần riêng biệt để lưu code và dữ liệu (tương tự ROM nếu xét về khả năng lưu trữ dữ liệu). Kiến trúc này là kiến trúc Harvard. Bạn nên đọc bài kiến trúc Harvard bằng tiếng Anh để nắm rõ hơn về cấu trúc này nếu muốn hiểu cặn kĩ vấn đề. Tuy nhiên, chúng ta cũng không tìm hiểu kĩ lắm về phần này.

Bây giờ, hãy quay lại với vấn đề chính của chúng ta nào!

Đây là đoạn code giúp bạn đọc dữ liệu

void showString (PGM_P s) {
    char c;
    while ((c = pgm_read_byte(s++)) != 0)
        Serial.print(c);
}

Tham số PGM_P không phải là con trỏ hằng chuỗi nhé! Bạn hãy xem nội dung thư viện avr/pgmspace.h, PGM_P đã được định nghĩa trong đó.

Kết quả là chương trình sau đây đã không còn sử dụng RAM để lữu trữ chuỗi, không tin thì bạn hãy thử tăng độ dài chuỗi lên hehe.

#include <avr/pgmspace.h>
int getMemoryFree() {
  // Trong trường hợp này, ta có thể hiểu extern sẽ khai báo một biến toàn cục trong chương trình (nếu chưa có) hoặc include một biến toàn cục đã được extern trước đó
  extern int __heap_start;
  extern int *__brkval; 
  
  //Dấu & phía trước tên biến / tên con trỏ sẽ cho ta biết vị trí ô nhớ mà nó đang đứng
  //Lưu ý: bài viết này không dành cho beginner và bạn cần tưởng tượng một chút để có thể mườn tượng vấn đề
  return (int) SP - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

const byte buttonPin[] = {2, 3, 4, 5, 6, 7};
const byte pinCount = 6; // Bạn có thể dùng hàm sizeof(buttonPin) / sizeof(byte) để tính ra số này

byte buttonPressed() {
  for (byte i = 0; i < pinCount; i++)
    if (digitalRead(buttonPin[i]) == HIGH)
      return i;
  return pinCount;
}
void showString(PGM_P s) {
  char c;
  while ((c = pgm_read_byte(s++)) != 0)
      Serial.print(c);
}
void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
  for (byte i = 0; i < pinCount; i++)
    pinMode(buttonPin[i], INPUT);
    
  Serial.println(getMemoryFree());
}

void loop () {
  byte button = buttonPressed();
  if (button < pinCount) {
    showString(PSTR("Ban da nhan phim THU "));
    switch (button) {
      case 0:
        showString(PSTR("NHAT"));
        break;
      case 1:
        showString(PSTR("HAI"));
        break;
      case 2:
        showString(PSTR("BA"));
        break;
      case 3:
        showString(PSTR("TU"));
        break;
      case 4:
       showString(PSTR("NAM"));
        break;
      case 5:
        showString(PSTR("SAU"));
        break;
    };
    showString(PSTR("\n"));
  }
}

Nhận xét và kết luận

Giống như vấn đề chuyển từ thủy năng sang điện năng, chúng ta phải xây dựng mà máy thủy điện. Và để chuyển những ô nhớ đáng ra phải được lưu trên RAM (kiểu string) sang vùng nhớ dữ liệu flash, bạn phải sử dụng hàm PSTR(const char*). Thiết nghĩ, cái giá phải trả cho việc này khá rè heart nên chúng ta cứ trả thoải mái haha.

Lưu ý, chúng ta chỉ chuyển được const char*, tức là chuỗi được định nghĩa bên trong hai dấu ngoặc kép " ", nhưng nếu bạn gán một chuỗi char * khác trong một hàm bất kì thì chuỗi char * mới này vấn nằm trong RAM nhé. Nói nôm na dễ hiểu hơn như sau, nếu bạn CHỈ sử dụng các chuỗi để thả các message trong quá trình debug thì hãy làm theo các hướng dẫn trên. Còn nếu dùng chuỗi để tính toán, xử lý,... thì không tài nào chuyển qua flash được đâu bạn nhé!

lên
11 thành viên đã đánh giá bài viết này hữu ích.
Chuyên mục: 
Các dự án được truyền cảm hứng

Bộ điều khiển PID - ứng dụng phần 2 - xe dò line dùng thuật toán PID

Tiép nối bài viết về xe dò line cảm ơn Đỗ Hữu Toàn đã viết hộ mình phần 4. hôm nay mình sẽ làm cho chiếc xe dò line đi mượt và có hồn hơn 

lên
34 thành viên đã đánh giá bài viết này hữu ích.
Các bài viết cùng tác giả

Tự làm game Snake - Rắn ăn mồi với Arduino - Ví dụ về việc sử dụng thư viện XỬ LÝ BẤT ĐỒNG BỘ

Nếu là một người theo dõi cộng đồng Arduino Việt Nam trong thời gian dài, bạn sẽ để ý rằng, mảng Game là một mảng nhận đươc khá ít sự quan tâm vì độ khó của nó. Điển hình là chỉ có bài viết hướng dẫn làm game Flappy bird và Cá ăn mồi của bạn nguoimegame. Tuy nhiên, hôm nay, khi mình cảm thấy đã đủ lượng kiến thức và lượng thư viện nền tảng mình đã viết trước đó, mình sẽ hướng dẫn các bạn cách viết một game đơn giản với Arduino.

lên
19 thành viên đã đánh giá bài viết này hữu ích.