Arduino - 利用PROGMEM將數據寫到閃存(程序存儲空間)

AVR存儲器簡介:

AVR 系列單片機內部有三種類型的被獨立編址的存儲器,它們分別爲:
1、Flash 程序存儲器(即:程序存儲空間、閃存)
2、SRAM 數據存儲器(即:動態內存)
3、EEPROM 數據存儲器

單片機採用哈弗結構,將程序存儲器和數據存儲器分開,而數據存儲器RAM通常比較小,而程序存儲器Flash空間比較大,因此就需要將佔用空間較大的不需要改變的數據放在Flash中。

比如需要單片機支持LCD顯示文字,就需要一個龐大的字體庫,可達到幾kb,這樣打的數據量放在RAM中是不合適的,只能放在Flash中。

pgmspace.h就提供了與之相關的讀寫操作。

問題導引:

編譯Arduino程序時,會提示:

項目使用了 656 字節,佔用了 (0%) 程序存儲空間。最大爲 253952 字節。
全局變量使用了9字節,(0%)的動態內存,餘留8183字節局部變量。最大爲8192字節。

而我們編譯時經常遇到的問題是:

項目使用了 4756 字節,佔用了 (15%) 程序存儲空間。最大爲 30720 字節。
全局變量使用了2246字節,(109%)的動態內存,餘留-198字節局部變量。最大爲2048字節。
沒有足夠的內存; 編譯時出錯。

【程序存儲空間】剩餘很多,而【動態內存】不足,導致無法成功寫入。這個問題往往出現在聲明瞭數據“較大”的常量特別是數組的情況下。

解決方案:

爲解決這個問題,我們可以將本來應該寫到【動態內存】的常量,寫入【程序存儲空間】,以達到節約【動態內存】空間的目的。

定義全局常量時,使用 PROGMEM 關鍵字,或使用 PROGMEM 數據類型,告訴編譯器 “ 把這個信息存到程序存儲空間 ”,而不是存到“ 動態內存 ”。

PROGMEM 關鍵字(或數據類型)使用到的庫:pgmspace.h

#include <avr/pgmspace.h>

數據定義:

const dataType variableName [] PROGMEM = {data0,data1,data3 ...};
// dataType- 任何變量類型
// variableName- 數據數組的名稱
程序存儲空間FLASH是不可改變的,因此定義時加關鍵字const 是個好的習慣。

1、作爲【全局】常量時,直接使用 PROGMEM 關鍵字即可, PROGMEM 關鍵字的位置比較隨意,但爲了Arduino早期版本的兼容性,推薦放到後面。如:

const char str1[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n"
const PROGMEM char str2[] = "Hi, I would like to tell you a bit about myself.\n"
PROGMEM const char str3[] = "Hi, I would like to tell you a bit about myself.\n"

2、作爲【局部】常量時,需要配合 static 關鍵字使用,如:

const static char flash_str[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n"

3、另外一種定義形式,不用 PROGMEM 關鍵字,而是直接用 PROGMEM 數據類型,如:

const prog_char flash_str[] = "Hi, I would like to tell you a bit about myself.\n"

4、字符串常量的定義

// 全局定義形式:
const char flash_str[] PROGMEM = “Hello, world!”;

// 函數內定義形式:
		/* pgmspace.h提供了一個宏 PSTR 用來申明Flash中的字符串:
		# define PSTR(s) ((const PROGMEM char *)(s))
		所以,函數內可以採用下面的定義形式:*/
const char *flash_str = PSTR(“Hello, world!”);

// 以下爲應用示例:
const char flash_str1[] PROGMEM = “全局定義字符串”;
int main(void)
{
	char *flash_str2=PSTR(“函數內定義字符串”);
	printf_P(flash_str1);
	printf_P(flash_str2);
}

數據讀取:

到這裏,我們的程序還不能正常工作。
因爲當你向一個函數傳遞指向Flash的指針時,它會認爲這是指向RAM的指針,從而在RAM中尋找數據,使得程序出錯。
所以還需要專門的函數來處理指向Flash的指針。
數據保存到程序存儲空間後,需要特殊的方法(函數)來讀取:

1、非數組常量的讀取方法
char ram_val; //存到 ram 內的變量
const PROGMEM flash_val = 1; // 存到 flash 內的常量
// 讀取
ram_val = pgm_read_byte( &flash_val ); // 讀 flash 常量值到 RAM 變量,參數使用【地址&】傳遞。
2、數組常量的讀取方法
char displayInt;
const char charSet [] RROGMEM = {0,1,2,3,4,5,6,7,8,9};
// 讀取
for (int i = 0; i < 10; i++)
  {
    displayInt = pgm_read_byte(charSet + i);   // 第一種方法
    displayInt = pgm_read_byte(&charSet[i]);   // 第二種方法
  }
3、字符串複製方法

strcpy_P 函數負責從【程序存儲空間】複製一個字符串到【動態內存】緩衝區"buffer"。
注意:複製時要確保緩衝區足夠大。

char buffer[30]; 
// 方式一
strcpy_P(buffer,PSTR("dGltQuY29t\r\n"));  
// 方式二
const char string_0[] PROGMEM = "This is a String"; 
strcpy_P(buffer, (char *)pgm_read_word(&string_0)); 

相關的處理函數:

// 數據讀取函數
pgm_read_byte(addr)
pgm_read_word(addr)
pgm_read_dword(addr)
pgm_read_float(addr)
pgm_read_ptr(addr)

// 字符串處理函數
void *memcpy_P(void *, const void *, size_t);
char *strcat_P(char *, const char *);
int strcmp_P(const char *, const char *);
char *strcpy_P(char *, const char *);

可以看到,字符串處理函數與標準處理函數一樣,只是以_P結尾,它們的功能也是一樣的。 
看這一句代碼:

strcmp_P("ram item", PSTR("flash item"));

這句代碼用來比較兩個字符串,第一個字符串”ram item”是申明在RAM中的,第二個字符串”flash item”通過宏PSTR申明在Flash中。

關於使用F()宏:

通常我們都使用如下語句,進行串口輸出:

Serial.print("Write something on  the Serial Monitor");

但這樣使用,每次調用時,都會先將數據保存在【動態內存】中。
當我們要輸出長的字符串時,就會佔用很多的【動態內存】空間。
使用 F() 就可以很好的解決這個問題,F() 可以將字符串輕鬆的保存在FLASH中。

Serial.print(F("Write something on the Serial Monitor that is stored in FLASH"));

關於數據讀取

1、對於Flash中數組的處理,pgmspace.h也提供了幾個宏:
pgm_read_byte(addr)
pgm_read_word(addr)
pgm_read_dword(addr)
pgm_read_float(addr)
pgm_read_ptr(add)

分別用來讀取地址addr處的1、2、4個字節和讀取浮點數。
考慮這樣一個問題,Flash中保存着若干個字符串,每個字符串的地址又以數組形式保存在Flash中,即:

const char *s1 PROGMEM = "s1";const char *s2 PROGMEM = "s2";const char* strPointer[] PROGMEM = {s1, s2};

要如何比較s1和s2呢?
首先需要讀取兩個字符串的地址,然後通過strcmp_P函數來比較字符串。
要注意,這兩個指針都是16位的,而不是8位!
所以,代碼應該是:

strcmp_P(pgm_read_word(&strPointer[0]),pgm_read_word(&strPointer[1]));

編譯器會對這樣的代碼給出一個警告,因爲pgm_read_byte()得到的是16位整形,而從函數原型中可以看到,函數需要的是指針,可以用兩種方法消除這個警告:

(1)強制類型轉換

strcmp_P(pgm_read_word((char*)&strPointer[0]), (char*)pgm_read_word(&strPointer[1]));

(2)使用另一個宏

strcmp_P(pgm_read_ptr(&strPointer[0]), pgm_read_ptr(&strPointer[1]));
2、讀取數據的問題

之前說到,指針是16位的,能尋址64kB的地址空間。
而在AVR的有些芯片上比如mega2560,Flash空間爲256kB,超過64kB的空間將如何尋址呢?
pgmspace.h提供了兩種Flash尋址的宏:

一種是採用16位地址的短地址尋址,最多尋址64kB:

pgm_read_byte_near(address_short)
pgm_read_word_near(address_short)
pgm_read_dword_near(address_short)
pgm_read_float_near(address_short)
pgm_read_ptr_near(address_short)

另一種是採用32位地址的長地址尋址,最多尋址4GB空間:

pgm_read_byte_far(address_short)
pgm_read_word_far(address_short)
pgm_read_dword_far(address_short)
pgm_read_float_far(address_short)
pgm_read_ptr_faraddress_short)

兩種尋址在性能上有所差別,短地址尋址速度要快很多,而且64kB也足夠使用了,因此默認使用短地址尋址:

#define pgm_read_byte(address_short) pgm_read_byte_near(address_short)
#define pgm_read_word(address_short) pgm_read_word_near(address_short)
#define pgm_read_dword(address_short) pgm_read_dword_near(address_short)
#define pgm_read_float(address_short) pgm_read_float_near(address_short)
#define pgm_read_ptr(address_short) pgm_read_ptr_near(address_short)

當必須尋址超過64kB空間時,可以手動的使用長地址尋址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章