Arduino Unoのメモリ

はじめに

Arduino Unoのメモリについての説明です。

メモリの種類

Arduino Unoには以下の種類のメモリが搭載されています。Arduino Unoが採用しているATmega328Pは、ハーバードアーキテクチャを採用しており、プログラムとデータが物理的に分離された領域に配置されます。

項番 種類 用途 容量 揮発性 その他
1 フラッシュメモリ プログラム、ブートローダ、読み取り専用ユーザデータ 32キロバイト(内ブートローダが0.5キロバイト 不揮発性 スケッチのアップロード後はユーザデータの変更不可。
2 SRAM ユーザデータ 2キロバイト(ユーザデータ) 揮発性
3 EEPROM ユーザデータ 1キロバイト 不揮発性

プログラム内で利用する変数は、通常、SRAM上に配置されます。読み取り専用のデータ(つまり、変数定義時に初期化できる変数)は、フラッシュメモリにおいてSRAMを節約することができます。また、EEPROMにデータを格納することもできます。フラッシュメモリとEEPROMに配置したデータは、スケッチの中では「直接」利用することはできず(変数によるアクセスができません)、API(関数)を利用して一旦SRAM上の変数に値をコピーする必要があります。

フラッシュメモリのデータはスケッチ内で初期化できるだけで、スケッチの実行中の書き換えはできません。EEPROMに初期値を書き込むには、avr-gccの機能を利用する必要があります。

フラッシュメモリ

フラッシュメモリには、スケッチとブートローダが配置されます。プログラム内で利用する変数も格納することもできます。

フラッシュメモリにデータを格納には、スケッチ上でフラッシュメモリに格納する旨を宣言する必要があります。フラッシュメモリに配置したデータは、API(関数)を利用してSRAM上の変数に読み込む必要があります。このため、基本的には大量のデータ(配列など)をフラッシュメモリに配置しておき、必要な部分をSRAMに読み込むということになります。同時に使う必要のあるデータをフラッシュメモリに配置しても効果は限られます。

利用方法

フラッシュメモリにデータを配置するには、変数を定義するときに、PROGMEMというキーワードをつけて宣言します。PROGMEMは、avr/pgmspace.hに定義されているため、このヘッダファイルをインクルードする必要があります。リファレンスはこちら。PROGMEMをつけるとその変数は読み出し専用となり、プログラム内での修正はできないので、変数を定義する際に必ず初期化する必要があります。constを付け定義する必要があります。また、グローバル変数もしくはstatic変数として宣言する必要があります。関数内のauto変数として宣言した場合は、コンパイルは正常終了しましたが、定義した値とは異なる値が読みだされました。

1
2
3
const データ型名 変数名 PROGMEM = ;
const PROGMEM データ型名 変数名 = 値;
const データ型名 PROGMEM 変数名 = ;  // このパターンはArduinoのリファレンスでは推奨されていませんが、動作するようです。

フラッシュメモリからデータを読み出すには、以下の関数を利用します。関数の引数は、フラッシュメモリ上の「アドレス」です。

  • pgm_read_byte()
  • pgm_read_word()
  • pgm_read_dword()
  • pgm_read_float()

上記を利用した例を以下に示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ヘッダファイルをインクルードする。
#include <avr/pgmspace.h>
 
// フラッシュメモリ上にデータを配置する。
const PROGMEM char  s_flash[] = {'a', 'b', 'c', 'd', 'e'};
const PROGMEM int   i_flash[] = {1, 100, 1000, 10000};
const PROGMEM long  l_flash[] = {100000, 10000000, 100000000};
const PROGMEM float f_flash[] = {1.4142, 2.7182, 3.1415};
 
// RAM上にデータを配置する。
char s_data[] = "This is on RAM(.data)";
char s_bss[21];
 
void setup() {
  char c;
  int j;
  long l;
  float f;
 
  Serial.begin(9600);
 
  for (int i = 0; i < sizeof(s_flash) / sizeof(*s_flash); i++) {
    // 1バイトのデータを読む。
    c = pgm_read_byte(s_flash + i);
    Serial.println((char)c);
  }
 
  for (int i = 0; i < sizeof(i_flash) / sizeof(*i_flash); i++) {
    // 2バイトのデータを読む。
    j = pgm_read_word(i_flash + i);
    Serial.println(j);
  }
 
  for (int i = 0; i < sizeof(l_flash) / sizeof(*l_flash); i++) {
    // 4バイトのデータを読む。
    l = pgm_read_dword(l_flash + i);
    Serial.println(l);
  }
 
  for (int i = 0; i < sizeof(f_flash) / sizeof(*f_flash); i++) {
    // floatのデータを読む。
    f = pgm_read_float(f_flash + i);
    Serial.println(f);
  }
 
  Serial.println(s_data);
  strcpy(s_bss, "This is on RAM(.bss)");
  Serial.println(s_bss);
}
 
void loop() {
}

上記のプログラムをコンパイルした後、各変数がどのように割付けられているかを、avr-nmコマンドを利用して確認してみます。

C:\> nm -n -C progmem_test.cpp.elf
~省略~
00000068 t f_flash
00000074 t l_flash
00000080 t i_flash
00000088 t s_flash
~省略~
00800100 D __data_start
00800100 d s_data
~省略~
0080014c B __bss_start
0080014c D __data_end
0080014c D _edata
0080014c b s_bss
~省略~
00800207 B __bss_end
00800207 N _end
00810000 N __eeprom_end

t_flashなどのアドレスは、00000068番地以降にあり、テキストセクションであることがわかります。一方、s_dataは、00800100番地にあり、(初期化された)データであることがわかります。また、s_bssは0080014c番地にあり、(初期化されない)データであることがわかります。nmコマンドでは、RAM上のアドレスは、00800000のオフセット付きで表示されるようです。

次に、最もよく使われるパターンと思われる、文字列の配列をフラッシュメモリに配置して利用する例を以下に示します。

  1. 文字列の変数を定義する。
  2. 文字列の変数の配列を定義する。

という2段階でプログラミングします。文字列の配列を直接定義することはできないようです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ヘッダファイルをインクルードする。
#include <avr/pgmspace.h>
 
// 文字列をフラッシュメモリ上に配置する。
const char message0[] PROGMEM = "message 0";
const char message1[] PROGMEM = "message 1";
const char message2[] PROGMEM = "message 2";
const char message3[] PROGMEM = "message 3";
 
// 文字列の配列をフラッシュメモリ上に配置する。
// const は変数の直前に書く必要がある
char * const message_table[] PROGMEM = {
  message0,
  message1,
  message2,
  message3,
};
 
/*
   以下のパターンはコンパイルできるが動作しない。
 
 
  PROGMEM char * const message_table[] = {
  "message 0",
  "message 1",
  "message 2",
  "message 3",
  };
*/
 
void setup() {
  // メッセージが入るのに十分な大きさを確保する。
  // static 宣言したのは、avr-nm でSRAM上に確保されることを確認するため。
  static char message_buffer[10];
  Serial.begin(9600);
 
  for (int i = 0; i < sizeof(message_table) / sizeof(*message_table); i++) {
    strcpy_P(message_buffer, (char *)pgm_read_byte(&(message_table[i])));
    Serial.println(message_buffer);
  }
 
}
 
void loop() {
}

strcpy_P()は、フラッシュメモリ上の文字列をRAM上の変数にコピーする関数です。pgmspace.hには、他にも以下の関数が定義されていました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
extern const void * memchr_P(const void *, int __val, size_t __len) __ATTR_CONST__;
extern int memcmp_P(const void *, const void *, size_t) __ATTR_PURE__;
extern void *memccpy_P(void *, const void *, int __val, size_t);
extern void *memcpy_P(void *, const void *, size_t);
extern void *memmem_P(const void *, size_t, const void *, size_t) __ATTR_PURE__;
extern const void * memrchr_P(const void *, int __val, size_t __len) __ATTR_CONST__;
extern char *strcat_P(char *, const char *);
extern const char * strchr_P(const char *, int __val) __ATTR_CONST__;
extern const char * strchrnul_P(const char *, int __val) __ATTR_CONST__;
extern int strcmp_P(const char *, const char *) __ATTR_PURE__;
extern char *strcpy_P(char *, const char *);
extern int strcasecmp_P(const char *, const char *) __ATTR_PURE__;
extern char *strcasestr_P(const char *, const char *) __ATTR_PURE__;
extern size_t strcspn_P(const char *__s, const char * __reject) __ATTR_PURE__;
extern size_t strlcat_P (char *, const char *, size_t );
extern size_t strlcpy_P (char *, const char *, size_t );
extern size_t strnlen_P(const char *, size_t) __ATTR_CONST__; /* program memory can't change */
extern int strncmp_P(const char *, const char *, size_t) __ATTR_PURE__;
extern int strncasecmp_P(const char *, const char *, size_t) __ATTR_PURE__;
extern char *strncat_P(char *, const char *, size_t);
extern char *strncpy_P(char *, const char *, size_t);
extern char *strpbrk_P(const char *__s, const char * __accept) __ATTR_PURE__;
extern const char * strrchr_P(const char *, int __val) __ATTR_CONST__;
extern char *strsep_P(char **__sp, const char * __delim);
extern size_t strspn_P(const char *__s, const char * __accept) __ATTR_PURE__;
extern char *strstr_P(const char *, const char *) __ATTR_PURE__;
extern char *strtok_P(char *__s, const char * __delim);
extern char *strtok_rP(char *__s, const char * __delim, char **__last);
extern size_t strlen_PF(uint_farptr_t src) __ATTR_CONST__; /* program memory can't change */
extern size_t strnlen_PF(uint_farptr_t src, size_t len) __ATTR_CONST__; /* program memory can't change */
extern void *memcpy_PF(void *dest, uint_farptr_t src, size_t len);
extern char *strcpy_PF(char *dest, uint_farptr_t src);
extern char *strncpy_PF(char *dest, uint_farptr_t src, size_t len);
extern char *strcat_PF(char *dest, uint_farptr_t src);
extern size_t strlcat_PF(char *dst, uint_farptr_t src, size_t siz);
extern char *strncat_PF(char *dest, uint_farptr_t src, size_t len);
extern int strcmp_PF(const char *s1, uint_farptr_t s2) __ATTR_PURE__;
extern int strncmp_PF(const char *s1, uint_farptr_t s2, size_t n) __ATTR_PURE__;
extern int strcasecmp_PF(const char *s1, uint_farptr_t s2) __ATTR_PURE__;
extern int strncasecmp_PF(const char *s1, uint_farptr_t s2, size_t n) __ATTR_PURE__;
extern char *strstr_PF(const char *s1, uint_farptr_t s2);
extern size_t strlcpy_PF(char *dst, uint_farptr_t src, size_t siz);
extern int memcmp_PF(const void *, uint_farptr_t, size_t) __ATTR_PURE__;

また、F()というマクロを利用する方法もあります。Serial.print()などで利用できます。

1
2
3
4
5
6
7
8
void setup() {
  Serial.begin(9600);
   
  Serial.println(F("This is stored in flash memory."));
}
 
void loop() {
}

この場合、Printクラスで、フラッシュメモリから1バイトずつ読み取って表示するので、2048バイト以上の文字列も表示することができます(もちろん、フラッシュメモリに入りきる範囲でですが)。

SRAM

SRAMは、ユーザデータが配置される2キロバイトの領域です。通常、プログラムで定義した変数はSRAMに配置されます。

ユーザデータが配置される領域は、以下の領域に分かれています。

領域 意味 プログラムとの関係
1 data 初期化済みデータ 静的記憶域期間を持つ変数で、初期値を与えたもtの。初期化したグローバル変数やstatic変数。
2 bss 非初期化済みデータ 静的記憶域期間を持つ変数で、初期値を与えていないもの。初期化していないグローバル変数やstatic変数。
3 ヒープ プログラム実行時に動的に確保するメモリ領域。 malloc()などで確保する領域。
4 スタック 実行中の関数に関するデータを格納する領域。 自動記憶域期間を持つ変数。関数内で定義したauto変数、関数の引数、関数終了後に戻るアドレスなど。

プログラムによりデータ構造が変わるため、各領域がどのアドレスから始まるかはプログラムにより異なります。以下は、SRAMの使い方を模式的に示した図です。ヒープはアドレスの大きい方向に伸びていき、スタックはアドレスの小さい方向に伸びていきます。両者がぶつかった時点でプログラムの動作は不定となります。

なお、avr-libcのmalloc()は、割り当て可能なメモリがなくなった時点で、エラーとなります。

初期化済みデータや非初期化データが上記に配置される様子は、上述のフラッシュメモリのセクションの結果を参照してください。

EEPROM

Arduino Unoは、1キロバイトのEEPROMを持っています。EEPROMライブラリ を利用することで、データの読み書きを行うことができます。アドレスを直接指定して、1バイトのデータ(0から255の値)を読み書きすることができます。

リファレンス

バージョン

Arduino 1.8.0

最終更新日

January 23, 2021

inserted by FC2 system