Tài liệu về Arduino cho biết, có thể giữ các hằng số như chuỗi hoặc bất cứ điều gì tôi không muốn thay đổi trong thời gian chạy trong bộ nhớ chương trình.
Tất cả các hằng số ban đầu là trong bộ nhớ chương trình. Họ sẽ ở đâu khi mất điện?
Tôi nghĩ về nó như được nhúng ở đâu đó trong đoạn mã, điều này hoàn toàn có thể xảy ra trong một kiến trúc von-Neumann.
Đó thực sự là kiến trúc Harvard .
Tại sao tôi phải sao chép nội dung chết tiệt vào RAM trước khi truy cập?
Bạn không. Trong thực tế, có một lệnh phần cứng (LPM - Bộ nhớ chương trình tải) để di chuyển dữ liệu trực tiếp từ bộ nhớ chương trình vào một thanh ghi.
Tôi có một ví dụ về kỹ thuật này trong đầu ra Arduino Uno sang màn hình VGA . Trong mã đó có một phông chữ bitmap được lưu trữ trong bộ nhớ chương trình. Nó được đọc từ đó một cách nhanh chóng và được sao chép vào đầu ra như thế này:
// blit pixel data to screen
while (i--)
UDR0 = pgm_read_byte (linePtr + (* messagePtr++));
Một sự phân tách của các dòng cho thấy (một phần):
f1a: e4 91 lpm r30, Z+
f1c: e0 93 c6 00 sts 0x00C6, r30
Bạn có thể thấy rằng một byte bộ nhớ chương trình đã được sao chép vào R30, và sau đó ngay lập tức được lưu vào thanh ghi USART UDR0. Không có RAM liên quan.
Tuy nhiên có một sự phức tạp. Đối với các chuỗi thông thường, trình biên dịch sẽ tìm thấy dữ liệu trong RAM chứ không phải PROGMEM. Chúng là các không gian địa chỉ khác nhau và do đó 0x200 trong RAM là một cái gì đó khác với 0x200 trong PROGMEM. Do đó, trình biên dịch gặp rắc rối khi sao chép các hằng số (như chuỗi) vào RAM khi khởi động chương trình, do đó không phải lo lắng về việc biết sự khác biệt sau này.
Mã (32kiB) được xử lý như thế nào chỉ với RAM 2kiB?
Câu hỏi hay. Bạn sẽ không tránh khỏi việc có nhiều hơn 2 KB chuỗi liên tục, vì sẽ không có chỗ để sao chép tất cả.
Đó là lý do tại sao những người đang viết những thứ như menu và các công cụ dài dòng khác, thực hiện các bước bổ sung để cung cấp cho chuỗi thuộc tính PROGMEM, vô hiệu hóa chúng được sao chép vào RAM.
Nhưng tôi hoang mang trước những hướng dẫn chỉ đọc và in dữ liệu từ bộ nhớ chương trình:
Nếu bạn thêm thuộc tính PROGMEM, bạn phải thực hiện các bước để cho trình biên dịch biết rằng các chuỗi này nằm trong một không gian địa chỉ khác. Tạo một bản sao hoàn chỉnh (tạm thời) là một cách. Hoặc chỉ in trực tiếp từ PROGMEM, một byte mỗi lần. Một ví dụ về điều đó là:
// Print a string from Program Memory directly to save RAM
void printProgStr (const char * str)
{
char c;
if (!str)
return;
while ((c = pgm_read_byte(str++)))
Serial.print (c);
} // end of printProgStr
Nếu bạn truyền hàm này một con trỏ tới một chuỗi trong PROGMEM, thì nó sẽ "đọc đặc biệt" (pgm_read_byte) để kéo dữ liệu từ PROGMEM thay vì RAM và in ra nó. Lưu ý rằng điều này cần thêm một chu kỳ xung nhịp, mỗi byte.
Và thậm chí thú vị hơn: Điều gì xảy ra với các hằng số theo nghĩa đen như trong biểu thức này a = 5*(10+7)
là 5, 10 và 7 thực sự được sao chép vào RAM trước khi tải chúng vào các thanh ghi? Tôi không thể tin điều đó.
Không, bởi vì họ không phải như vậy. Điều đó sẽ biên dịch thành một lệnh "nạp chữ vào thanh ghi". Hướng dẫn đó đã có trong PROGMEM, vì vậy nghĩa đen hiện đang được xử lý. Không cần phải sao chép nó vào RAM và sau đó đọc lại.
Tôi có một mô tả dài về những điều này trên trang Đưa dữ liệu không đổi vào bộ nhớ chương trình (PROGMEM) . Điều đó có mã ví dụ để thiết lập chuỗi và mảng chuỗi, một cách hợp lý dễ dàng.
Nó cũng đề cập đến macro F (), một cách dễ dàng để in đơn giản từ PROGMEM:
Serial.println (F("Hello, world"));
Một chút phức tạp của tiền xử lý cho phép biên dịch thành hàm trợ giúp, kéo các byte trong chuỗi từ PROGMEM một byte mỗi lần. Không cần sử dụng RAM trung gian.
Thật dễ dàng để sử dụng kỹ thuật đó cho những thứ khác ngoài Nối tiếp (ví dụ: LCD của bạn) bằng cách lấy bản in LCD từ lớp In.
Ví dụ, trong một trong những thư viện LCD tôi đã viết, tôi đã làm chính xác điều đó:
class I2C_graphical_LCD_display : public Print
{
...
size_t write(uint8_t c);
};
Điểm mấu chốt ở đây là xuất phát từ In và ghi đè chức năng "ghi". Bây giờ chức năng ghi đè của bạn làm bất cứ điều gì nó cần để xuất ra một ký tự. Vì nó có nguồn gốc từ Print, giờ bạn có thể sử dụng macro F (). ví dụ.
lcd.println (F("Hello, world"));
string_table
mảng. Mảng đó có thể là 20KB và sẽ không bao giờ vừa trong bộ nhớ (thậm chí là tạm thời). Tuy nhiên, bạn có thể tải chỉ một chỉ mục bằng phương pháp trên.