HOME
  Security
   Software
    Hardware
  
FPGA
  CPU
   Android
    Raspberry Pi
  
nLite
  Xcode
   etc.
    ALL
  
LINK
BACK
 

2023/04/26

Arduino AVRで PROGMEMを使用して RAMの使用量を減らす方法、2次元配列の文字列の定義方法 Arduino AVRで PROGMEMを使用して RAMの使用量を減らす方法、2次元配列の文字列の定義方法

(Arduino AVR PROGMEM How to use Arrays of strings for Reduce RAM usage)

Tags: [電子工作]




● Arduino AVRで PROGMEMを使用して RAMの消費を減らす方法、2次元配列の文字列の使い方

 Arduino AVR PROGMEM How to use Arrays of strings for Reduce RAM usage

 Arduino開発環境で AVR以外のマイコンチップの開発をしていますが、ATMEL AVRを使用した本物の Arduinoのアプリの開発をしたら PROGMEMの使い方で嵌りました。

 PROGMEMの使い方は Arduino公式に書いてあります。この内容で解決しました。

Arduino Reference - PROGMEM

 AVR系のマイコンチップ(Arduino Nano ATmega328、Arduino Micro ATmega32U4等)は RAMの容量が少なく、普通にプログラム内で文字列等を定義すると RAMにも割り当てられるので RAMの容量が不足する。と言う事が起こり易いです。

 特に ATmega168は 1024バイトしか無いので不足し易いです。

 その為、PROGMEMと言う修飾子を付ける事でプログラムのフラッシュエリアだけに定義される様になり、RAMの消費を抑える事ができます。
 ※ 但し、FLASH ROMに配置されるのでその文字列は「変更不可」と成ります

 そして AVRの場合、RAMに配置した場合と FLASH ROMに配置した場合とでその定数にアクセスする方法が異なるので注意が必要です。

PROGMEMの有無FLASH ROMRAM
PROGMEM有定義する領域を確保しない
PROGMEM無定義する領域を確保する

● PROGMEMの書き方、構文(文法)
下記のどっちでも良いが上の行の書き方が良い(と思う)
const dataType variableName[] PROGMEM = {}; // use this form
const PROGMEM dataType variableName[] = {}; // or this one

下記の書き方は駄目
const dataType PROGMEM variableName[] = {}; // not this one


● PROGMEMの例と RAMの削減量の実例(Example Code)

 Example Codeの場合の RAM使用量の比較(ATmega168でコンパイル)
PROGMEMの有無FLASH ROMRAM
PROGMEM有1782 bytes188 bytes
PROGMEM無1778 bytes268 bytes
PROGMEM有
文字列を 56文字削除
1726 bytes188 bytes
PROGMEM無
文字列を 56文字削除
1722 bytes212 bytes

 「PROGMEMの有無」でわかりますが、「PROGMEMの有」の場合は RAMを使用しないのでその分の RAMの使用量が減っています。

 また、「文字列を 56文字削除」の例でわかるように「PROGMEMの有」の場合は FLASH ROMの使用量が 56バイト、「PROGMEMの無」の場合は FLASH ROMと RAMのそれぞれの使用量が 56バイト減っています。
 これは「PROGMEMの無」の場合はプログラム起動時に FLASH ROMから RAM領域に値をコピーするからです。

 PROGMEMの無しの場合のプログラム部分の差分(下段)
  displayInt = pgm_read_word_near(charSet + k);
 ↓
  displayInt = charSet[k];

for (byte k = 0; k < strlen_P(signMessage); k++) {
  myChar = pgm_read_byte_near(signMessage + k);
 ↓
for (byte k = 0; k < strlen(signMessage); k++) {
  myChar = signMessage[k];
 PROGMEMの無しの場合のプログラム部分の差分を見るとわかると思いますが、AVRで PROGMEMの定数にアクセスする場合は pgm_read_word_nearや strlen_Pの _P付き、pgm_read_byte_near等が必要です。

● pgm_read_byteと pgm_read_byte_nearの違い

 同じです。

 Arduino公式のサンプルでも pgm_read_byteを使用している場合、pgm_read_byte_nearを使用している場合でブレが有るので、どっちを使っても良いと思います。

#define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short))

#define pgm_read_byte(address_short)    pgm_read_byte_near(address_short)



● PROGMEMの2次元配列の文字列の例(Arrays of strings)
#include <avr/pgmspace.h> << この行は実際には不要
// "String 0" etc are strings to store - change to suit.
const char string_0[] PROGMEM = "String 0";
const char string_1[] PROGMEM = "String 1";
const char string_2[] PROGMEM = "String 2";
const char string_3[] PROGMEM = "String 3";
const char string_4[] PROGMEM = "String 4";
const char string_5[] PROGMEM = "String 5";

// Then set up a table to refer to your strings.
const char *const string_table[] PROGMEM = {string_0, string_1, string_2, string_3, string_4, string_5};

  char buffer[30];
  for (int i = 0; i < 6; i++) {
    // Necessary casts and dereferencing, just copy.
    strcpy_P(buffer, (char *)pgm_read_word(&(string_table[i])));
    Serial.println(buffer);
    delay(500);
  }

 pgm_read_wordで string_table[]内に定義の string_0等の要素の定義アドレスを取り出します。
 strcpy_Pで string_0等に定義の文字列(FLASH ROM領域)を buffer(RAM領域)にコピーします。
 Serial.println(buffer)で bufferの文字列を表示します。

 char buffer[30];で RAM領域にテンポラリ変数を定義したくない場合
 上記の buffer[30]はプログラム実行中に動的に確保するので、コンパイル時の「Global variables use」には出てきません。
 ざっくりな表現で「leaving xxx bytes for local variables.」が 30バイト未満の場合はコンパイルは成功しますが、プログラムの実行中にクラッシュします。
 また、文字列の長さが 30文字以上の時にバグります。
  for (int i = 0; i < 6; i++) {
    PGM_P p = pgm_read_word(&(string_table[i]));
    while(true) {
      char ch = pgm_read_byte(p++);
      if (ch == 0x00) break;
      Serial.write(ch);
    }
    Serial.println();
    delay(500);
  }

 ※ char buffer[30]の代わりに char chを RAM領域に確保します。ざっくりでRAM領域に動的に確保する量を 1バイトで済ます事ができ、結果的に 29バイト節約できます
 また、char buffer[30]の場合は文字列の長さが 30文字以上の時にバグりますが、while~pgm_read_byteの場合は文字列の長さ制限は有りません。

 pgm_read_wordで string_table[]内に定義の string_0等の要素の定義アドレスを取り出します。
 以下、whileループ内の処理。
  pgm_read_byteで string_0等に定義の文字列(FLASH ROM領域)を char ch(RAM領域)に 1文字コピーします。
  chが 0x00(文字列の終端の場合)は whileループを抜けます。
  Serial.write(ch)で chの1文字を表示して whileループします。
 Serial.println();で改行します。
 こう言う「ワンライナー(1行で書く)」書き方もできます
  char ch;
  while((ch = pgm_read_byte(p++)) != 0x00) {
    Serial.write(ch);
  }
 処理的に一番無駄が無いのは pのポインタのインクリメントを「外だし」です。
 (理由は「ch == 0x00」の時の無駄なインクリメントをしないから)
  char ch;
  while((ch = pgm_read_byte(p)) != 0x00) {
    Serial.write(ch);
    ++p;
  }
 ※ ++のインクリメント演算子を「前」に書いているのは「前」に書いた方が速いから。(ただし Arduinoを含め最近のコンパイラは頭が良いので、単体の場合には「後」に書いても最適化してくれるので「前」の場合と同じになる)
 ※ ザックリで「後」に書いた場合はインクリメント前の値の保持が必要だからとか何とかです。(今の時代はコンパイラの最適化で考慮不要だが、本当に知りたい場合はググって)



● STM32や ESP32、RP2040等の AVR以外の場合

 STM32や ESP32、RP2040等でも PROGMEM修飾子を使用できます。また、これらのマイコンチップは PROGMEM修飾子で FLASH ROMに配置した場合でも RAMに配置の物と区別せずに普通にアクセスできます。(AVRの様な考慮が不要)

 STM32や ESP32、RP2040等は下記の様に直接 string_table[n]で素直にアクセスできる。
 また、string_table[]の定義の中に文字列を素直に定義できる。
 ※ この書き方は STM32や ESP32、RP2040等で動きます。AVRでは動きません

const char* const string_table[] PROGMEM = {
  "String 0",
  "String 1",
  "String 2",
  "String 3",
  "String 4",
  "String 5"
};

  for (int i = 0; i < 6; i++) {
    Serial.println(string_table[i]);
    delay(500);
  }


● F()マクロ

 F()マクロでも PROGMEMと同じ効果が有ります。
 (F()マクロは文字列を直接関数に記述する場合に便利)

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

F()マクロの有無FLASH ROMRAM
F()マクロ有1506 bytes184 bytes
F()マクロ無1494 bytes224 bytes
F()マクロ有
文字列を 20文字削除
1486 bytes184 bytes
F()マクロ無
文字列を 20文字削除
1474 bytes204 bytes
 ※ F()マクロでも PROGMEMと同じ結果になる事がわかります

Sketch uses 1506 bytes
Global variables use 184 bytes

void setup() {
  Serial.begin(9600);
  Serial.print(F("Write something on  the Serial Monitor"));
}

void loop() {
  // put your main code here, to run repeatedly:
}
Sketch uses 1494 bytes
Global variables use 224 bytes

void setup() {
  Serial.begin(9600);
  Serial.print("Write something on  the Serial Monitor");
}

void loop() {
  // put your main code here, to run repeatedly:
}


● F()マクロを自前の関数で使用する方法

  hogehoge(F("ABCDEFG"));

void hogehoge(const __FlashStringHelper* str) {
  char buffer[30];
  // strcpy_P(buffer, (char *)pgm_read_word(str));
  // strncpy_P(buffer, (char *)pgm_read_word(str), sizeof(buffer));
  // buffer[sizeof(buffer) - 1] = 0x00; // null終端(strncpy_Pの場合)
  strlcpy_P(buffer, (char *)pgm_read_word(str), sizeof(buffer));
  Serial.println(buffer);
}
 strcpy_Pの場合、buffer[30]なので引数の strが 30文字以上の場合に buffer[30]を超えてコピーするので RAM領域を破壊して不正な動きになる(場合が有る)。
 strncpy_Pの場合、引数の strが 30文字以上の場合に bufferを null終端しないので不正な動きになる(文字列の後にゴミが出たりする)。
 (strncpy_Pの次の行の null終端の実装を追加すれば大丈夫)
 strlcpy_Pの場合、引数の strが 30文字以上の場合でも bufferを null終端するのでプログラムは正常に動く。(文字列の長さは最大 29文字)

 最初から strlcpy_Pを使うのが何も考えなくて良いので便利。
 但し、日本語等の長い文字列を扱う場合は 29バイトで尻切れになるので文字化け等が発生します。

 上記は strcpy_P方式を使った例です。
 char ch = pgm_read_byte(p++);で whileループの書き方も可能です。

 最初から whileループの書き方をすれば扱う文字列の長さを気にする必要は有りません。
  hogehoge(F("ABCDEFG"));
  hogehoge(F("超長い文字列~~~~~~~~~~~~~ABCDEFG"));

void hogehoge(const __FlashStringHelper* str) {
  PGM_P p = str;
  while(true) {
    char ch = pgm_read_byte(p++);
    if (ch == 0x00) break;
    Serial.write(ch);
  }
  Serial.println();
}



● Arduino UNO R4ではルネサスの RA4M1を使用

 Arduino公式に RA4M1が入ってくる事でプログラミングガイドもそれぞれに分断するんだろうな。

 Arduino UNO R3までは ATmega328Pを使用。

 Arduino UNO R4ではルネサスの RA4M1を使用すると言う事ですが、ルネサス自身が「(直接利益を生まない)アマチュア」相手に「(費用を掛けて)真面目」にサポートするのかが気になります。
 R4のコミュニティができあがれば有志と言う名の「ボランティア」で運営やメンテナンスが「まわる」と思うのですが。

 暫くは「UNO R3用のプログラムを UNO R4で動かせない!」と言った混乱が出るのでは?と思います。


● Arduino Portenta C33

 US$64の価格!誰が買うの?って思ったら Arduinoの公式の価格って元々高いのね。

Portenta C33
 Renesas R7FA6M5BH2CBG Arm Cortex-M33
 1コア

Arduino Portenta C33



Tags: [電子工作]

●関連するコンテンツ(この記事を読んだ人は、次の記事も読んでいます)

ESP32で東方の Bad Apple!!の動画を 128 x 64 dotの OLED SSD1306で再生する!
ESP32で東方の Bad Apple!!の動画を 128 x 64 dotの OLED SSD1306で再生する!

  ESP32で東方の Bad Apple!!の動画を再生する!実際にはパラパラ漫画です

2.4インチの大画面 OLED SSD1309で東方の Bad Apple!!の動画を再生する
2.4インチの大画面 OLED SSD1309で東方の Bad Apple!!の動画を再生する

  2.42inch 128x64 OLED LCD Display module SSD1309で Bad Apple!!を再生します

SSD1306 OLEDの描画を高速化する方法
SSD1306 OLEDの描画を高速化する方法

  How to Speed Up OLED Drawing Speed

EPS32の I2Cの SCLの周波数をクロックアップして SSD1306 OLEDの描画を高速化する方法
EPS32の I2Cの SCLの周波数をクロックアップして SSD1306 OLEDの描画を高速化する方法

  OLED SSD1306で I2Cの SCLK周波数をドーピングで高速化して描画速度を爆速にする方法

【技適付き】Freenoveの ESP32-S3-WROOMの Basic Starter Kitを買ってみた、カメラ、SD-Card付きの学習キット
【技適付き】Freenoveの ESP32-S3-WROOMの Basic Starter Kitを買ってみた、カメラ、SD-Card付きの学習キット

  Freenove Basic Starter Kit for ESP32-S3-WROOM Onboard Camera Wireless Python C FNK0084

【技適付き】Freenoveの ESP32-WROVERの Ultimate Starter Kitを買ってみた、カメラ付きの学習キット
【技適付き】Freenoveの ESP32-WROVERの Ultimate Starter Kitを買ってみた、カメラ付きの学習キット

  Freenove Ultimate Starter Kit for ESP32-WROVER Onboard Camera Wireless Python C FNK0047

LoRa通信を使用してポストに郵便物が投函されるとスマホの LINE宛に通知する IoTの作り方
LoRa通信を使用してポストに郵便物が投函されるとスマホの LINE宛に通知する IoTの作り方

  LoRaを使用した IoT郵便受け LoRa IoT Mailbox Sensor with LINE Messaging API

ESP32で Slackに「勤怠管理」メッセージをワンボタン操作で投稿する方法
ESP32で Slackに「勤怠管理」メッセージをワンボタン操作で投稿する方法

  Slackの勤怠チャンネルに毎日毎日毎日毎日 手動で投稿するのが馬鹿らしいので ESP32で作った

ESP32-S3で SPIを使う時に Arduinoでエラーが出た話
ESP32-S3で SPIを使う時に Arduinoでエラーが出た話

  ESP32 S3 SPI error VSPI was not declared in this scope

ESP32の I2Sで MCLKが必須の CS4344 DAC Audioを使用する方法
ESP32の I2Sで MCLKが必須の CS4344 DAC Audioを使用する方法

  ESP32で MCLKが必須の CS4344が問題無く使えます

ESP32で SPI接続の Color LCD ST7735S 160x80px IPSを動かす方法
ESP32で SPI接続の Color LCD ST7735S 160x80px IPSを動かす方法

  ESP32 SPI IPS Color LCD ST7735S tutorial

【ソースコード有】ESP32で I2C接続の OLED SSD1306 128x64pxを動かす方法
【ソースコード有】ESP32で I2C接続の OLED SSD1306 128x64pxを動かす方法

  ESP32 I2C OLED SSD1306 tutorial

ESP32-WROOMを購入したらヘンテコ技適マークもどきの基板が届いた話
ESP32-WROOMを購入したらヘンテコ技適マークもどきの基板が届いた話

  Fake ESP32-WROOM Ignore FCC ID and Strange TELEC mark




[HOME] | [BACK]
リンクフリー(連絡不要、ただしトップページ以外は Web構成の変更で移動する場合があります)
Copyright (c) 2023 FREE WING,Y.Sakamoto
Powered by 猫屋敷工房 & HTML Generator

http://www.neko.ne.jp/~freewing/hardware/arduino_avr_progmem/