google-site-verification: google3bd66dd162ef54c7.html
FC2ブログ

シリアル通信 (Serial.print) は RAM を大量に消費する (Arduino)

 Arduinoについて気付いた話です。前回のPROGMEMに続き、今回はシリアル通信でおなじみの Serial.print です。

 Serial.print は使い方が簡単なので便利です。でもこれ、すごい量のRAMを消費します。ミニオシロ作りではRAMが足りなくて、対策としてPROGMEMを使ったり、無駄な変数を使わないようにして出来るだけRAMを節約しています。ところが、シリアルを使うとそんな苦労をしているのがアホらしくなるくらい大量のRAMが使われます。

 ということで、そのあたりの様子を詳しく見てみます。

▼シリアルのRAM使用量のテスト
メモリー使用量
 RAMの残りのサイズは、コンパイル後にIDEの画面下に表示されるので、この値を見て行きます。

 ところで話は脱線しますが、画面のスクリーンショットは違法なんて話がありますが、こういうのやっちゃいけなくなるんでしょうか? 違法と知らなくてもダメらしいのですが、「画面内のその書き方は、だれそれの著作権を犯しているぞ」なんて因縁つけることだって出来る気がします。スクショがダメなら、「hogeというプログラムのバージョンaaaを画面サイズcc X ddで開いた時の、画面座標(x1,y1)-(x2,y2)の範囲に、」なんて書くしかないので、web文化は終わっちゃいます。

 話を戻しましょう。

 以下はSerial.print を使った時のRAMの使用量を調べた結果です。
// RAM使用量のテスト
String a = "Hellow "; // A
String b = "world!"; // B

void setup() {
Serial.begin(115200); // C
}

void loop() {
Serial.println(a + b); // D
// Serial.println(F("Hellow world!")); // E
while (1) {} // F
}

/* 条件別の残りRAMの量
1. (全部無し) :ローカル変数で2039バイト使うことができます。
2. F :ローカル変数で2039バイト使うことができます。-0
3. C +F :ローカル変数で1866バイト使うことができます。-173
4. C +E+F :ローカル変数で1862バイト使うことができます。-4
5. A+B+C+D +F :ローカル変数で1826バイト使うことができます。-36
*/
 プログラム自体の説明は不要と思います。コメントのAからFまでの行を追加して行った時のRAMの量を、末尾のコメント欄に記入しています。

 順番に数値を追ってみます。
1.setupとloopしか無い状態で、これがArduinoのミニマムの状態です。RAMの物理的な容量は2048バイトありますが、ここでは2039バイトとなっていて9バイト少なくなっています。この9バイトは Arduinoとして動かすために必要な量なんでしょうが、思ってたより少ないです。

2.無限ループを作っているwhileだけ追加した状態です。RAMは使わないので、残りRAMの量に変化はありません。なお、フラッシュメモリの量はもちろん変化しています。

3.Serial.begin(115200); を追加しただけですが、ここで一気に173バイトもRAMが減っています。これあんまりじゃないの、と思ったのがこの記事を書いている動機なのですが、とりあえず話を先に進めます。

4.Serial.println(F("Hellow world!")); を追加して実際に文字の出力を行っています。13文字出力していますが、引数をF( )でくくって、PROGMEM として扱っているので、RAM は4バイトしか減っていません。

5.文字列をStringクラスで扱っているので、RAMの消費量が36バイト増えています。まあ便利さの代償としてこれくらいは仕方がない感じです。ちなみにString はメモリの消費が多いと言われていますが、実際にどれくらいなのか調べたことがありませんでした。この例では、さほどでもない感じです。

◆まとめ
 シリアルを使うと予想外にRAMが減って、Serial.begin( )で宣言しただけで173バイトも持って行かれます。PROGMEMなどを使ってちびちびと節約しているのに、その横でこんな大盤振る舞いをされたら、たまったもんじゃありません。

 調べてみたら、Serial では受信と送信バッファに各々64バイト、合計で128バイトのRAMを使うので、こんなRAMの消費量になるようです。シリアルのバッファはハードのFIFOでやっているのだと思っていたのですが、CPUのRAMだったんですね。あと、シリアルの受信なんて、私はコマンド文字を1文字受信するくらいがせいぜいの使い方なので、受信バッファは64バイトもいりません。でもArduinoはいろんな人が使うので、多めに確保しておいた方が良いという判断なんでしょうね。

 ちなみにシリアルのバッファサイズをユーザーのスケッチから再定義出来るといいのですが、そういうことは出来ないようです。どうしてもやりたいなら、元のデバイスの定義を修正するしか無いようですが、それをやると、思わぬ問題の種を蒔くことになります。

 そんなことで、RAMが足らなくなったら、Serial を使わないようにするのが特効薬になりそうです。但し、Serial.printを使ったデバッグは出来なくなるので、液晶などの画面があればそこに変数の内容を表示させる、などの工夫が必要になります。

PROGMEMの使い方 (Arduino)

 OLEDを使った小さなオシロ作りはソフトとハードまで出来て、あとは適当なケースを探す段階まで進みました。久しぶりに新しい物を作ることに取り組んでみたのですが、その過程でいくつか発見がありました。

 そんなことで、発見と言うほど大袈裟なものではありませんが、気付いた点について記事にまとめておきたいと思います。まずはタイトルの通り PROGMEM です。なお、以下の情報はこの記事を書いた時点(2019/2/15)のもので今後変わる可能性がある点はご注意ください。

1.PROGMEMの使い方が判らない
 PROGMEMはデーターの保存先をRAMからフラッシュメモリーに移動させることで、RAMの空き容量を拡げる機能です。詳しい説明はネットにいっぱい書かれているのでここでは省略します。問題はその使い方、というかプログラムの書き方です。

 ネットにはPROGMEMの使い方について解説した記事がいっぱいあります。また、Arduinoの日本語リファレンスにも書かれているので、そういうのを見れば済むと思っていたら、なんだか変です。ちゃんと動かないどころか、例として掲載されているプログラムすらコンパイラが通りません。

2.PROGMEMの仕様が変わっていた
 おかしいな、と思って調べてみると、どうも2017年頃にPROGMEMの使い方に仕様変更があったようです。変更の前後で互換性は無いので、古い仕様で書かれたプログラムは動かなくなっていました。

 ネットには新旧の仕様の情報が混在していて、どちらかと言うと古い仕様で描かれた情報の方が多いようです。ということで、そういう状況になっていることを知らないと、問題解決のために無駄な時間を使うことになります。(私がそうでした)

3.PROGMEMの使い方についてのお勧めのサイト
 上に書いたような状況になるのは実は日本語のリファレンスのページを読んだ場合で、本家の英語のサイトにはPROGMEMの使い方の正しい情報が記載されています。そんなことで本家のサイトを読むようにしましょう、と書けば済むのですが、やはり日本語で書かれた情報の方が断然読み易いです。

 ということで改めて探してみると、jumbleatさんのブログの、PROGMEMを使うと言う記事がすごく判り易かったので、これを読むことをお勧めします。

4.PROGMEMのプログラム例
 OLEDオシロの記事中でPROGMEMを使ったプログラムを公開していますが、説明用にPROGMEMの部分だけ取り出して書き直したのが以下のプログラムです。
//  PROGMEM 使用例 2019/02/14 ラジオペンチ

// #include <avr/pgmspace.h> // PROGMEMを使うために使用(たぶんインクルード不要)

const char Name[10][5] PROGMEM = {
"A50V", "A 5V", " 50V", " 20V", " 10V", " 5V", " 2V", " 1V", "0.5V", "0.2V"
}; // 縦軸表示文字の定義(10項目、5文字(\0含んだ文字数が必要))                  
const char * const string_table[] PROGMEM = {
Name[0], Name[1], Name[2], Name[3], Name[4], Name[5], Name[6], Name[7], Name[8], Name[9]
}; // アクセス用のテーブルの定義

char chrBuff[8]; // 文字列操作バッファ

void setup() {
Serial.begin(115200);
}

void loop() {
for (int i = 0; i <= 9; i++) {
// strcpy_Pによりテーブル番号の文字列をchrBuffに取り出し
strcpy_P(chrBuff, (char*)pgm_read_word(&(string_table[i])));
Serial.println(chrBuff);
}
while (1) {}
}
 ここではフラッシュ領域に文字列(オシロの縦軸レンジの表示名)を保存し、それをstrcpy_Pで取り出してシリアルに表示するまでの手順を示しています。

 なんでこんなに面倒なの?と思うくらいごちゃごちゃしています。また、ポインターを使っていて判り難いです。ともかく、こういうものだと思って、あまり考えずに使うのが良いと思います。8行目が const char * const string_table[] PROGMEM = となっていて、* (アスタリスク)の前後に意味不明なスペースが入っていますが、これも気にしないことにします。

◆まとめ
 ともかくこれでPROGMEMの使い方が判ったので、ミニオシロのプログラムを完成形まで持って行くことが出来ました。 判り易い情報を開示していただいた iumbleatさんに感謝します。実はRAMが足らなくなって、すごく困ってました。

 PROGMEMでは、文字を扱っているのにbyteでは無くword (pgm_read_word) という単語が出てきたリ、数値の扱いでは far や near なんて単語が出て来て面くらいます。前者は、フラッシュメモリーのデーター幅が16ビットなので word と呼ばれているのでしょう。また far/near は参照ページの内外を表しているようなんですが、いずれにしても違う文化の風に当たったみたいで、面白いです。

参考:Arduinoの日本語のレファレンスのPROGMEMのページ
武蔵野電波さんのArduino 日本語リファレンスの、PROGMEMとFマクロのページ
garretlabさんの、PROGMEMのページ
変数の宣言で、prog_char などとなっていたら古い仕様なので、その先は読んでも無駄です。

0.96インチOLEDを使ったオシロ、機能確認版の製作 (Arduino)

 0.96インチOLEDとArduinoを使ったミニオシロの試作版を作ってみたら具合が良さそうでした。それなら、ということで改良版を作っていたのですが、ほぼ一段落したので現在の状態を公開します。

▼外観
Arduinoで作るオシロ
 まだArduino UNO とブレッドボードで動かしています。

▼OLED画面と操作ボタン
オシロ画面んと操作ボタン
 操作スイッチはとりあえずタクトスイッチを使っていて、左から、Hold, DOWN, UP, SELECTです。

▼回路図 (図をクリックで別窓に拡大)
Arduinoで作るオシロの回路図
 出来るだけ少ない部品で作ることを目指しています。というか、オペアンプをきちんと使いこなすのは大変なので思いっきり手抜きしています。

 R3の反対側をポートにつなぎ、LOWとHi-Zに切り替えることで入力アッテネーターの切り替えをやっています。ちなみにハードのアッテネーターは R3無しで1/5、R3有りの1/50 の2段切り替えだけで、それより細かいステップはソフトで刻んでいます。

 スイッチの読み取りはピンチェンジ割り込みでやりたかったのですが、たまに取りこぼしがあったので、ダイオードで合成してPin2にも入力し、Arduinoの普通の割り込みの、attachInterrupt で検出する方式に変更しました。ちなみに、最初にやったピンチェンジ割り込みは、こういうやり方です→ピンチェンジ割り込みについての私の記事

 なお、R51はタクトスイッチの接点にコンデンサ (C51) から大きな電流が流れ込むのを防止するために入れたものですが、ダイオードが入っているので不要だったかも知れません。ただ、スイッチ操作から割込み発生までのタイミングを少し遅らせる効果はあるはずです。

 ちなみに、この抵抗が必要な理由については、以下に解説があります。
・居酒屋ガレージ日記さん:スイッチに並列に入るコンデンサ
・ikkeiさん:スイッチに並列コンデンサはダメ回路

 アンチエイリアシングのために、ADCの前にローパスフィルタを入れた方が良いのですが、入力インピーダンスが高いので、単純なCRのフィルターを入れのは無理がある感じです。一方で、入力のR1に並列に小さな容量のコンデンサを入れて、立ち上がり速度を改善したいところです。これ、よく考えると両者の効果は相反するので、間を取って何もしないことにしました。(本当にそれでいいのか?)

◆スケッチ
 スケッチはこちら→オシロのスケッチ_20190213OledOsillo (Shift JIS エンコードです)
 機能を無計画に追加していったので、構造がおかしなところがあります。また、レンジ操作を switch case文で書いたので、見易くなった半面、ソースが長くなちゃってます。

 OLEDの画面に1kB、波形メモリーに400バイトのRAMを消費するので、RAM容量が苦しいです。対策として、PROGMEMを使って文字データーをフラッシュ領域に置くようにしているのですが、ここのプログラムの書き方が判り難かったです。そのあたりは別途記事で紹介する予定です。

▼オシロの画面
オシロの画面
 垂直感度、スイープ速度、トリガスロープをスイッチ操作で選択可能。SELECTボタンで項目移動、UP/DOWNで内容選択するスタイルにしました。選択されている項目の下には、上向きの浅いコの字を表示します。Holdボタンを押すとその状態で画面を保持。再度押すと元に戻ります。なおHold中は画面に Hold と表示します。

 細かい点は下記の動画が判り易いと思います。

▼動画


◆主な仕様
・垂直感度(フルスケール当たりの値)
 固定レンジ:50V, 20V, 10V, 5V, 2V, 1V, 0.5V, 0.2V
 Autoレンジ:50V, 5V (波形が出来るだけ大きく表示されるように上/下限の値を自動調整)

・水平レンジ(div当たりの値、4div=フルスケール)
 50ms, 20ms, 10ms, 5ms, 2ms, 1ms, 500us, 200us

・トリガ
 波形PPの1/2レベルで自動トリガ。トリガスロープUP/DOWNの指定が可能。
 トリガ非検出でも波形は表示し、トリガ検出失敗表示する (unsync と表示)。

・電圧計機能
 波形メモリーの値の平均値を画面に表示。簡易電圧計として使えるはず。

・画面ホールド
 ホールドスイッチを押すと画面をホールド、再度スイッチを押すとホールド解除。なお、ホールド中はholdと画面に表示。

・設定状態のリジューム機能
 ボタン操作の5秒後に設定内容をEEPROMに保存。保存した内容は次回パワーON時に反映。

◆まとめ
 とりあえず現在やってみたいことをプログラムに書いてみました。欲を言えばキリが無いので、このあたりで手を打つのが良いかなと思います。

 最高サンプリング周期が8μsなので10kHz程度の波形までしか観察出来ません。それくらいだったら音にして耳で聴いたのと変わらないじゃん、という声も聞こえて来そうです。まあ波形を見た方が情報量は多いかと。

 ADCで読んでバッファメモリーに書くまでの処理をアセンブラで書けば、たぶんあと何倍かは早くなるはずです。どこかにヒントが書かれていないか、注意していたいと思います。

◆妄想をスケッチ
ペン型オシロ
 ペン型オシロです。最終的にはこれを作ってみたいんですよね。相手が負の電圧だったら、先端を逆に繋げばいいんです。

【2019/02/14追記】
▼ピン割り込みで取りこぼしが出た回路
ピン割り込み
 最初に試したのがこの回路です。CPUのレジスタを操作してPin 8,9,10からピン割り込(状態変化検出割り込み)みでスイッチの状態を読んだのですが、たまに取りこぼしが出ちゃいました。記事内にも書きましたが、こういう方法の割込みです→ピンチェンジ割り込みについての私の記事
 あと、この回路ではコンデンサの電荷をスイッチで一瞬で抜いているので、接点の寿命の問題があってあまりよろしくありません。理由は記事の中のリンク先をご覧ください。
カレンダー
01 | 2019/02 | 03
- - - - - 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 - -
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード