Lưu các biến CHỈ ĐỌC với PROGMEM

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

Trong bài Tiết kiệm RAM trong Arduino?, chúng ta đã biết cách lưu chuỗi hằng vào bộ nhớ FLASH thay cho việc lưu hết bọn chúng vào RAM. Như vậy, một hằng chuỗi có thể được lưu vào bộ nhớ FLASH thay vì lưu vào RAM. Vậy, câu hỏi đặt ra là, những biến hằng khác (hằng số, hẳng mảng, hẳng số thực) có thể được lưu vào FLASH thay vì vào RAM hay không?

Trong thực tế, các biến hằng (trừ hằng chuỗi) hầu hết chỉ tốn vài chục byte để lưu trữ nên RAM, nên chúng ta cũng chưa gặp vấn đề gì trong việc lưu trữ hằng số hay hằng mảng cả. Nhưng thỉnh thoảng, có những lúc, ta phải tìm cách lưu trữ chúng ở một nơi khác, ví dụ Bài 12: Phát nhạc bằng Arduino với một cái loa hoặc buzzer.

Chần chừ gì nữa, biết muốn phám khá khả năng của Arduino - hay không?

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

  1. Hiểu vì sao vùng nhớ dữ liệu trong bộ nhớ FLASH chỉ có thể lưu biến hằng
  2. Lưu trữ hằng số, hằng mảng vào RAM

Lưu ý, các bạn nên đọc chuỗi bài Cách lưu trữ các biến số, mảng, chuỗi trong ArduinoTiết kiệm RAM trong Arduino? để có một số kiến thức nền tảng và xây dựng sự hứng thú "tột đỉnh" khi đọc bài viết này!

Đặt vấn đề

Trong 2 bài viết trước (Cách lưu trữ các biến số, mảng, chuỗi trong ArduinoTiết kiệm RAM trong Arduino?), chúng ta đã biết được có thể di chuyển việc lưu trữ biến và hàm; biết được làm thế nào để giảm thiểu sự tiêu thụ RAM trong khai báo chuỗi. Và từ những nhận xét, kết luận rút ra được, chắc hẳn, bạn cũng muốn xem bộ nhớ FLASH còn những gì hay ho nữa, đúng không nào?

Từ bài viết Tiết kiệm RAM trong Arduino?, chúng ta biết được rằng, chúng ta có thể lưu trữ các hằng chuỗi trong bộ nhớ FLASH nhằm tiết kiệm một lượng lớn RAM, mà chưa đề cập đến việc "có thể lưu trữ được những biến số khác hay không". Vậy rốt cuộc là nó có thể hay không? Đó là câu hỏi mà bạn và tôi có lẻ đang đau đáu muốn trả lời, bởi vì, chúng ta có một điểm chung đó là muốn khám phá những điều mới mẻ mà từ trước đây chúng ta chưa biết.

Vì sao chỉ có thể lưu BIẾN HẰNG trong FLASH

Như đã nói ở bài trước, bộ nhớ FLASH có một phần nào đó giống như bộ nhớ ROM của máy tính. Nó chính là nơi lưu giữ liệu sketch của bạn. Và nó chỉ bị thay đổi khi bạn upload một sketch mới lên mà thôi. Như vậy, khi một sketch đã được bạn upload lên vđk thì nó không thể bị thay đổi hoặc mất. Tương tự, vì các biến của bạn đã được lưu lên flash thì ta chỉ có thể truy cập đến nó mà không thể sửa chữa (trừ phi bạn upload sketch mới với những biến hằng mới)

Cách lưu trữ

Cũng giống như lưu trữ string trong RAM, đầu tiên bạn phải include thư viện avr/pgmspace.h vào sketch của bạn.

#include <avr/pgmspace.h>

Sau đó, bạn chỉ cần thêm từ khóa PROGMEM vào sau tên biến, hoặc tên mảng là bạn đã có một biến hằng được lưu vào FLASH mà chẳng tốn một ô nhớ nào trong RAM rồi!

<tên kiểu dữ liệu> <tên biến> PROGMEM = <giá trị>;
<tên kiểu dữ liệu> <tên biến hằng>[] PROGMEM = <giá trị>;

//Ví dụ:

int HOURS_PER_DAY PROGMEM = 123;
int array[] PROGMEM = {0, 1, 2};
unsigned long TIME_IN_DAY PROGMEM = 86400;

Lưu ý

Chỉ có các kiểu số nguyên (int, long, long long, ...), kiểu char và các kiểu mảng một chiều của các dữ liệu nói trên mới được lưu trữ trong bộ nhớ FLASH.

Ví dụ thử nghiệm

#include <avr/pgmspace.h>

int HOURS_PER_DAY PROGMEM = 123;
int array[] PROGMEM = {0, 1, 2};
int ch[] PROGMEM  = {'a', 'b'};
unsigned long TIME_IN_DAY PROGMEM = 86400;

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

void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
 
  Serial.println(getMemoryFree());
  Serial.println(TIME_IN_DAY);
  Serial.println(HOURS_PER_DAY);
  Serial.println(array[0]);
  Serial.println(array[1]); 
  Serial.println(array[2]);
  Serial.println(char(ch[0]));
}

void loop () {}

Tuy nhiên, nếu bạn dùng một biến đếm để in giá trị như trong ví dụ sau:

#include <avr/pgmspace.h>
 
const int array1[] PROGMEM = {1, 10, 100};
const int array2[] PROGMEM = {1, 11, 111, 1111};
 
 void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
 
  /* In tung gia tri */
  Serial.println("Ko dung vong lap");
  Serial.println(array1[0]);
  Serial.println(array1[1]); 
  Serial.println(array1[2]);

  Serial.println(array2[0]);
  Serial.println(array2[1]);
  Serial.println(array2[2]);
  Serial.println(array2[3]);
  
  
  /* In dung vong lap */
  Serial.println("Dung vong lap");
  for (int i = 0; i < 3; i++)
    Serial.println(array1[i]);
    
  for (int j = 0; j < 4; j++)
    Serial.println(array2[j]);
}
 
void loop () {}

thì sẽ bị lỗi lúc In dùng vòng lặp. Lý do thì mình không rõ, tuy nhiên khi bạn truy xuất bằng chỉ số như lúc In từng giá trị thì lại bình thường. Như vậy, chẳng lẻ, chúng ta không có giải pháp nào khác để print nó ra hay sao? Câu trả lời cho bạn là có, chúng ta có giải pháp.

Như ở bài trước Tiết kiệm RAM trong Arduino?, chúng ta đã biết được cách in một chuỗi được lưu trong bộ nhớ Flash ra Serial. Tương tự, ta cũng sẽ dùng cách đó với những biến PROGMEM.

#include <avr/pgmspace.h>
 
const int array1[] PROGMEM = {1, 10, 100};
const int array2[] PROGMEM = {1, 11, 111, 1111};
 
 
void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
 
  /* In tung gia tri */
  Serial.println("Ko dung vong lap");
  Serial.println(array1[0]);
  Serial.println(array1[1]); 
  Serial.println(array1[2]);

  Serial.println(array2[0]);
  Serial.println(array2[1]);
  Serial.println(array2[2]);
  Serial.println(array2[3]);
  
  
  /* In dung vong lap */
  Serial.println("Dung vong lap");
  for (int i = 0; i < 3; i++)
    Serial.println(pgm_read_word(array1 + i)); // array1 + i Con trỏ tới vị trí thứ i của mảng array1
    
  for (int j = 0; j < 4; j++)
    Serial.println(pgm_read_word(array2+j));
}
 
void loop () {}

Hàm pgm_read_word có nhiệm vụ gì?

Trong bài viết tiết kiệm RAM, chúng ta đã dùng hàm pgm_read_byte  để đọc được từng byte (từng char) từ con trỏ chuỗi và trả về byte đó. Vậy pgm_read_word sẽ có nhiệm vụ đọc từng word (2 byte) từ một con trỏ và trả về một word. Vậy tại sao không dùng pgm_read_byte mà lại dùng pgm_read_word? Bởi vì, kiểu int trong Arduino UNO sử dụng 2 byte để lưu trữ, vì vậy ta dùng hàm pgm_read_word để đọc được 2 byte đó. Nhưng, từ từ, 2 byte đó sẽ là một kiểu word... như vậy thì khi biểu diễn số âm thì sao, nó sẽ bị tràn số. Như vậy, ta phải ép kiểu trả về pgm_read_word là int như code sau để tránh bị lỗi tràn số.

#include <avr/pgmspace.h>
 
const int array1[] PROGMEM = {1, 10, 100};
const int array2[] PROGMEM = {1, 11, 111, 1111};
 
 
void setup () {
  Serial.begin(9600);// Khởi động Serial ở mức baudrate 9600
 
  /* In tung gia tri */
  Serial.println("Ko dung vong lap");
  Serial.println(array1[0]);
  Serial.println(array1[1]); 
  Serial.println(array1[2]);

  Serial.println(array2[0]);
  Serial.println(array2[1]);
  Serial.println(array2[2]);
  Serial.println(array2[3]);
  
  
  /* In dung vong lap */
  Serial.println("Dung vong lap");
  for (int i = 0; i < 3; i++)
    Serial.println(int(pgm_read_word(array1 + i))); // array1 + i Con trỏ tới vị trí thứ i của mảng array1
    
  for (int j = 0; j < 4; j++)
    Serial.println(int(pgm_read_word(array2+j))); // hot fix
}
 
void loop () {}

Yeah, rất cảm ơn anh NTP_PRO đã giúp em tìm ra "con bọ" này và hoàn thiện hơn nữa bài viết của mình.

Mở rộng, để đọc được các biến hằng kiểu long thì bạn dùng hàm pgm_read_dword nhé. Vậy còng long long cheeky. Bạn hãy thử nghĩ và tìm hiểu thêm xem.

Kết luận

FLASH và RAM đều có những ưu và nhược điểm riêng, và là một người nghệ sĩ, bạn sẽ phải là người uyển chuyển dung hòa ưu nhược của chúng nhằm tạo một kiệt tác hoàn hảo. Mong rằng, qua một trick nhỏ như vầy, sẽ giúp bạn xây dựng một hệ thống lớn trong tương lai!

Mở rộng,

Chúc các bạn thành công!

lên
8 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ả

Kết nối mạng cho Intel Galileo từ máy tính laptop (Windows version)

Các bạn sẽ biết được cách kết nối Internet (để cài đặt các gói, để debug, để code, để vào Internet...) từ máy tính laptop của bạn. Thật là chuyên nghiệp phải không nào. Mỗi lần muốn code thì không cần có router, không cần usb tll. Cứ dây LAN gắn vô máy tính là ok ngay!

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

Lập trình PLC cơ bản - Bài 005 - Hướng dẫn kết nối iNut PLC tới server nội bộ / server tại biên / server không cần qua bên thứ 3

Khi sử dụng một thiết bị IoT trong công nghiệp, đại đa số chủ đầu tư sẽ quan tâm đến việc máy chủ của họ sẽ nằm ở đâu trong quá trình lưu trữ và sử dụng một hệ thống IoT. Vì sao lại như thế? Vì họ không bị phụ thuộc vào nhà cung cấp dịch vụ hoặc là nhà cung cấp Internet,... Máy móc thiết bị mua thì phải thuộc sỡ hữu của họ chứ không phải là đi thuê mướn,... Và giải pháp cho toàn bộ  việc đó chính là iNut PLC với khả năng tích hợp vào một máy chủ bên thứ 3 nhưng vẫn đảm bảo lưu thông của toàn bộ hệ thống IoT. Đem IoT từ trên mây (clouding) về nhà máy (tại biên - edge computing). Cùng khám phá nhé.

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