Cách lưu trữ các biến số, mảng, chuỗi trong Arduino

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

Bạn có bao giờ tự hỏi những biến số, biến chuỗi hay biến mảng của mình được lâu ở đâu trên Arduino chưa? Trước kia, mình từng nghĩ rằng, nó được lưu ở vùng nhớ flash, nơi lưu trữ code mà chúng ta tải lên. Nhưng không, bình thường nó được lưu ở RAM!

Vậy RAM (viết tắt từ Random Access Memory) là gì? Nó là chữ viết tắt của một loại bộ nhớ chính của máy tính (Arduino cũng có thể xem là một máy tính). Như vậy nếu hết RAM, chương trình của bạn sẽ crash (hư – đỗ vỡ,…) một cách bất ngờ mà bạn không tài nào debug được (nếu bạn chưa đọc về bài này – hoặc những nội dung tương đương).

Vậy, thiết nghĩ, chúng ta cần nắm rõ hơn bản chất của vấn đề này. Nó thật thú vị phải không nào?

Nội dung cần nắm

  • Biết được các biến mà bạn khai báo được lưu trữ ở đâu?
  • Các vùng nhớ trong RAM
    • Vùng heap, stack
    • Vùng tĩnh (static)
  • Chuỗi được lưu trữ như thế nào trong RAM của Atmega328?

Lưu ý: Tác giả sử dụng chương trình Arduino IDE phiên bản 1.0.4 nên khi các bạn thực hành trên các bản IDE khác số lượng RAM còn trống có thể bị chênh lệch vài chục KB. Điều đó không sao cả, vì càng về sau, thư viện Serial Software của Arduino ngày càng được nâng cấp và được gọt tỉa dung lượng rất tỉ mỉ.

Các loại bộ nhớ thông dụng trong Arduino

Thứ nhất, khi nói đến Arduino thì hầu hết chúng ta nghĩ đến vi điều khiển Atmega328 (vđk), con vđk này có đến 32KB flash (đã bao gồm bootloader), là một nơi rất lý tưởng cho việc lập trình ứng dụng nhúng trên môi trường lập trình Arduino. Để sử dụng hết 32KB này thì đòi hỏi code của bạn phải cực kỳ cực kỳ nặng (tính theo số dòng code). Mình đã từng thử sử dụng hết 32KB flash này bằng một file sketch nặng đến cả MB! Bạn có muốn thử không nào?

Một điều khá may mắn cho chúng ta là Arduino IDE cho ta biết đúng chính xác đến từng byte bộ nhớ flash mà sketch của bạn sử dụng (giống như chính xác đến từng nano mét trong việc thiết kế vi xử lý vậy ^_^).

Thứ hai, thiết nghĩ, có lẻ, đó là bộ nhớ EEPROM, và việc sử dụng hết dung lượng nhớ EEPROM của Atmega328 (lên đến 512 byte) cũng là một điều khó có thể xảy ra! Có chữ ROM nên bạn có thể tạm xem nó như là bộ nhớ ROM (Read Only Memory) trên Arduino, nhưng cái hay của nó là bạn có thể chỉnh được giá trị trong từng ô nhớ của EEPROM. Bạn nên tham khảo thư viện EEPROM để có thể thông tin về phần này.

Cuối cùng, đó là bộ nhớ RAM. Từ trước giờ, khi mua một chiếc máy vi tính, thông số về RAM, tốc độ xử lý, core i5, i7,… luôn được chúng ta quan tâm hàng đầu. Nhưng khi nhảy qua một loại “super mini computer” như Arduino, chúng ta lại không chú trọng nhiều đến những thông số tương tự như RAM, tốc độ xử lý,… mà chỉ còn quan tâm đến dung lượng bộ nhớ flash. Đồng ý với bạn là dung lượng bộ nhớ flash cực kỳ quan trọng nhất, vì không lưu được code thì nói gì đến việc sử dụng RAM. Nhưng RAM cũng không kém cạnh gì, và thiết nghĩ nếu so sánh về mức độ quan trọng trên Arduino thì RAM chỉ đứng sau bộ nhớ flash, kế đến là ROM.

Ram là nơi lưu giữ các biến, con trỏ, quá trình thực thi hàm,… và dung lượng của nó cũng khá là hạn chế. Trên Atmega328, dung lượng của RAM chỉ bằng 1 / 16 so với dung lượng flash.

Khác với hai loại bộ nhớ trên, bộ nhớ RAM rất dễ bị thiếu hụt, và điều này rất hay xảy ra! Và một khi nó xảy ra thì … bạn biết đấy, sketch của bạn trên Arduino sẽ bị “dừng” một cách đột ngột mà bạn debug code đến cỡ nào nó cũng không ra lỗi, cho đến khi bạn nắm vững bài này.

Bộ nhớ RAM trong Arduino, nó phân chia như thế nào?

Có ba vùng nhớ trên RAM của mọi vi điều khiển họ AVR (trong đó có các dòng Atmel, cụ thể trong bài viết này là Atmega328 – được gắn sẵn trên các bé Arduino UNO R3):

  1. Static data, vùng nhớ tĩnh, bao gồm các biến toàn cục, mảng và cả kiểu chuỗi nữa (string).
  2. Vùng “heap”, vùng này dùng để lưu các biến con trỏ, nó được dùng và xóa khi bạn gọi hàm malloc()free().
  3. Vùng “stack”, vùng này dùng để lưu trữ các biến, giá trị khi một hàm này gọi một hàm khác. (Đó là lý do vì sao người ta có khái niệm “khử đệ quy” vì mục đích tiết kiệm RAM)

Vùng “heap” sẽ tăng lên và được sử dụng trong một cách thức “ép không chắc chắn đều”. Nghĩa là, khi bạn giải phóng một vùng nhớ, sau đó vđk sẽ trỏ về vùng nhớ “vừa được giải phóng” đó, và vùng nhớ này sẽ được sử dụng nếu có một hàm malloc() gọi và đòi được cấp một vùng nhớ “vừa hoặc khít” (bằng hoặc nhỏ hơn) với những kích thước “vùng nhớ” được giải phóng. Ví dụ: chúng ta sẽ xem những con số 0 là vùng nhớ trống trong heap, và số 1 là vùng nhớ đã sử dụng trong heap. Giả sử vùng heap chưa được dùng và có dạng 0000000000000000… và sau một thời gian nó được dùng và giải phóng nó có dạng 0001111000011111…, bây giờ ta giải phóng (free) vùng nhớ thứ nhất - 4 ô nhớ đã sử dụng đầu tiên trong heap, thì con trỏ heap sẽ trỏ vệ vị trí byte thứ 3 (bắt đầu từ byte thứ 0 tính từ trái qua phải). Bây giờ, nếu ta yêu cầu ít hơn hoặc bằng 4 byte bởi hàm malloc() thì nó sẽ cấp ta đúng vùng nhớ vừa bị xóa và những byte thừa nếu có sẽ “trở nên dư thừa” và không được dùng đến nữa. Còn khi ta yêu cầu lớn hơn 4 byte, thì con trỏ heap sẽ được trỏ về vùng nhớ nào đáp ứng được yêu cầu của bạn gần nhất có thể được (phần này mình cũng chưa rõ). Các vùng nhớ dư thừa bị bỏ ra gọi là “unused memory”

Lưu ý: bạn nên xem hình sau và để ý vào vùng heap và stack để biết cách các vđk AVR lưu trữ chúng.

Một điểm hay của vđk AVR đó là cung cấp rất nhiều biến hệ thống mà từ đó bạn có thể biết được các thông tin về các vùng nhớ của RAM. Trong đó, có một biến khá hay đó là biến con trỏ __brkval. Nó sẽ chỉ cho bạn biệt “độ phình” của vùng heap, tức là vị trí cực đại của vùng heap.

Vùng nhớ stack được xác định ở cuối RAM, nó được mở rộng và rút ngắn ngược hướng với cách mà heap làm! Bạn nhìn hình mũi tên ở hình trên là rõ ngay. Vùng nhớ stack được sử dụng và được giải phóng (nếu cần) trong quá trình hàm này gọi một hàm khác. Đây cũng là nơi các biến cục bộ (local variable) được lưu trữ.

Làm thế nào để tiết kiệm RAM trên Arduino?

Việc quan trọng của một nhà thiết kế tài ba là họ không bao giờ để chương trình của mình sử dụng ram một cách “ngấu nghiến” và “vô vội vạ”. Và những coder Arduino là những nghệ nhân thực thụ (RAM trên Arduino chỉ có 2KB).

Dưới đây là một hàm nho nhỏ để biết chúng ta còn bao nhiêu RAM free.

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); 
}

Bây giờ bạn thêm đoạn code sau và bật Serial Monitor lên để xem số lương RAM chương trình chúng ta đã sử dụng.

void setup () {
    Serial.begin(9600);
    Serial.println("Free Mem: ");
    Serial.println(getMemoryFree());
}
void loop () {/*do nothing*/}

Như vậy, chúng ta còn khoảng 1,8 KB RAM còn trống trong một đoạn chương trình ngắn “chút chỉn”. Mà cái sketch này chẳng có làm nhiệm vụ gì hết nhưng đã ăn mất của chúng hết 2048 – 1832 = 216 byte RAM (trong đó là 128 byte cho phần đệm serial, bạn xem thêm trong datasheet của Atemega328 nếu muốn tìm hiểu thêm nhé).

Và khi hàm kia trả về giá trị là 0, thì chương trình của bạn sẽ “đứng cứng ngắt”, thử xem! Gợi ý: bạn hãy xay dựng một vòng lặp vô tận, chạy những lệnh tính toán phức tạp (số thực,…). Hoặc đơn giản chỉ là một vòng đệ quy liên tục!

Nào chúng ta sẽ làm một số thay đổi nhỏ trong đoạn code trên:

Serial.println("Free Mem(test 2): ");

Kết quả, ta được:

Free Mem(test 2):

1824

Có thể thấy là lượng ô nhớ cho RAM đã giảm! Như vậy, bạn đã yêu cầu thêm RAM khi thay đổi đoạn text để in ra Serial Monitor.

Nhận xét:

  1. Tất cả các chuỗi trong C được lưu trong RAM. Điều đó giải thích vì sao khi thêm các ký tự đơn vào chuỗi trên thì nó đã ăn bớt đi một lượng RAM không hề nhỏ
  2. Chuỗi cũng được lưu vào bộ nhớ flash. Bạn hãy thử xóa chữ test 2 và thay vào đó là test2 (tức là xóa đi 1 ký tự khoảnh trống). Nhưng dung lượng flash sau khi tải code lên vđk Atmega328 vẫn không đổi, thử tiếp một lần xóa đi ký tự số 2, ta thấy dung lượng flash bị giảm đi 2 byte. Điều đó cho thấy bộ nhớ flash được lưu trữ theo kiểu 2-byte (word), tức là mỗi ô nhớ là 2 byte!

Kết luận

 Hãy cẩn thận khi “thả” những message thông báo cho từng dòng code, vì điều đó sẽ dẫn đến việc sử dụng RAM một cách cực kì lãng phí. Ngoài ra, nó còn gây ra nhiều vấn đề hơn những gì bạn nghĩ. Vì vậy, hãy là một nghệ sĩ thông minh!

Vậy làm sao để chương trình của bạn không còn tốn nhiều RAM như thế nữa! Hãy đợi bài viết sau nhé, mình sẽ bật mí cho bạn cách làm điều đó!

lên
19 thành viên đã đánh giá bài viết này hữu ích.
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ả

Điều khiển 8 relay qua Internet như thế nào?

1, 2, 4 relay không đủ với nhu cầu của bạn? Bạn muốn hơn thế nữa! Vậy còn chần chừ gì mà không thử với module 8 relay điều khiển độc lập qua Internet. Bạn sẽ tự làm được một app dành riêng cho mình. Là một maker thì không nên tự hạn chế mình!

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

ESP8266 kết nối Internet - Phần 2: Arduino gặp ESP8266, hai đứa nói chuyện bằng JSON

Ở bài trước, chúng ta đã xây dựng phương thước giao tiếp giữa tầng 1 (socket server) và tầng 2 (ESP8266). Chúng ta đã xây dựng một chương trình thử nghiệm trên socket server để test ra lệnh cho ESP8266 và cũng thử nghiệm cho ESP8266 gửi sự kiện ngược lại Socket Server.

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