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ả

Chứng chỉ Arduino - Phân loại trình độ Arduino để cùng làm dự án lớn

Giấy chứng nhận Arduino (gọi tắt là chứng nhận) là một sáng kiến của Ban quản trị Cộng đồng Arduino Việt Nam để chính thức chứng nhận người dùng ở các cấp độ khác nhau và xác nhận chuyên môn của họ trong các lĩnh vực quan trọng. Chứng chỉ được cung cấp tại 04 mức độ, gồm: ...

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

Kết nối điều khiển từ xa sử dụng ESP8266 và Arduino với hệ thống firmware iNut Cảm biến CloudMQTT - 1000 firmware miễn phí

Chuyện kể rằng, có 02 sinh viên Việt Nam, trong lúc làm đồ án đại học kết nối điều khiển từ xa qua Internet. Một người thì chọn phương án truyền thống sử dụng máy tính làm máy chủ và demo các tính năng theo yêu cầu đồ án. Người còn lại biết đến iNut Sensor và tìm cách tối ưu hóa phần mềm và phần cứng nhằm chau chuốt cho đồ án của mình trở nên "xịn" và "nhiều tính năng bá đạo". Đến lúc bảo vệ đồ án, cậu sinh viên 01 cảm thấy choáng ván với muôn vàn vấn đề từ việc nơi trình bày đồ án không có wifi, mạng chập chờn, máy tính mở không lên, đứt cáp biển,... Cuối cùng cũng bảo vệ được với điểm số không ưng ý lắm dù tốn rất nhiều thời gian và công sức. Cậu còn lại nhờ vào việc chau chuốt phần mềm, tối ưu hóa và comment kĩ từng lệnh trong dòng code, viết báo cáo bài bản chuẩn bị slide như ý, dùng điện thoại cài wifi, quét mã QRcode để chia sẻ quyền truy cập đến phần mềm rất chuyên nghiệp, mọi thứ cậu chủ động hoàn toàn mà không bị các vấn đề "học tài thi phận" bủa vây mà kết quả hết sức mĩ mãn, điểm số mĩ miều, kiến thức IoT được chuẩn hóa. Thực vậy, các dự án, đồ án sử dụng nền tảng iNut Platform bên dưới cho phép các bạn sinh viên làm các dự án hết sức hoàn hảo: từ xe điều khiển, bãi giữ xe thông minh, hệ thống máy lạnh, máy bơm, điều hòa thông minh,... do chinh các bạn tự làm nên đã đạt được những điểm số tốt và tuyệt đối. Cũng chính vì thế, iNut JSC (công ty chủ quản của iNut Platform) đã kết hợp với Khu Công nghệ phần mềm - ĐHQGHCM để tổ chức những khóa đào tạo ngắn hạn cho sinh viên Việt Nam sử dụng nền tảng IoT do iNut JSC phát triển. Và để mở con đường tri thức tiếp cận IoT trong nháy mắt, iNut JSC đã tạo ra một phiên bản firmware trị giá 50.000 đồng sử dụng clouding của CloudMQTT và tài trợ 1000 firmware cho tất cả các bạn học sinh, sinh viên Việt Nam có thể tiếp cận IoT một cách dễ dàng và nhanh chóng nhất!

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