« 2009年5月 | トップページ | 2009年7月 »

2009年6月の記事

Arduinoで漢字表示

GLCD(SG12864A)を使って漢字の表示に挑戦中です。今回は試験的に作ったフォントデーター1文字をGLCDに表示するところまでです。漢字表示の目的としては以前にも書きましたが、RSSリーダーを作ることです。

基本構想

  1. Arduino標準であるks0108ライブラリを拡張する
    グラフィック・ディスプレイ表示の高速化で投稿した改造版をベースにする
  2. 漢字フォントに関しても、ks0108ライブラリのフォント構造を踏襲する
  3. RSSはUTF-8でエンコードされているため、漢字フォントはUnicode (UCS-2)で収録する
  4. フォントは「東雲(shinonome)ゴシック」の12ドットを使用
    → 12ドットフォントになるとArduino MegaのROMでも入らないですが、可読性を優先して外付けEEPROMにフォントを格納

SG12864AのDisplay Data RAMの構造

GLCDに漢字を表示するために、SG12864AのDisplay Data RAMの構造を調べました。

Glcd_vram_2

Display Data RAMのアドレスは、Chip番号、Colum Address(X座標)、Page Address(Y座標)で指定します。SG12864AではLCDコントローラーCHIPを2つ使用しており、X座標の63~64にかけて選択するCHIPの切り替えが必要となります。

SG12864Aでは、8-bitデーターバス経由でDisplay Data RAMにデーターを書き込むと、指定したPageに対応する縦方向8ドットに対して描画が行われます。例えば図に示すように、(x = 1, y = 0)座標に「N」を表示したい場合、(Chip = 1, Page = 0, Colum = 1)に0xFFを書き込みます。

SG12864Aはドットマトリクスディスプレイのため、Pageにまたがる描画も発生ししますが、その場合はks0108ライブラリの中で2つのPageに分割して描画を行っています(作者ではないですが、コードの解析より)。

フォントの構造

今回使用する東雲フォントは、BDF形式で配布されています。例えば、shnmk12min.bdfファイルでは、全角「あ」のビットマップは以下のようになっています。(こちらはゴシックではなく、明朝体でした)

 STARTCHAR 2422
 ENCODING 9250
 SWIDTH 960 0
 DWIDTH 12 0
 BBX 12 12 0 -2
 BITMAP
  1000
  0b00
  7c00
  1200
  1f00
  1280
  3440
  5440
  4840
  5080
  2300
  0000
 ENDCHAR

ビットマップ情報を画面イメージに展開すると以下のようになります。横方向(X軸方向)にビットマップを展開していることが分かります。

Bfd_font_6

Arduinoのks0108ライブラリはどうかというと、GLCDがPage単位(縦方向8ドット単位)の描画であることから、縦方向にビットマップを展開します。上記の「あ」のフォントデーターをks0108ライブラリのフォント形式に変換すると以下のようになります。12ドットフォントの描画は2 Pageに渡りますが、2 Page目のデーター4bitはMSB側にアラインして書き込み時にシフトを行っています。

Arduino_font_3

サンプルフォントの作成と表示の実験

ks0108ライブラリの形式に従って、先ほどの「あ」を示すフォントファイルをつくり、「あ」の文字を表示させてみます。まだ、ks0108ライブラリは2バイト文字が認識できませんので、文字コード0x21として定義します。

以下に、「あ」と「A」を手動でArduino形式のフォントイメージに変換したフォントファイル(sample.h)の内容を示します。

#include <inttypes.h>
#include <avr/pgmspace.h>

#ifndef SAMPLE_H
#define SAMPLE_H

#define SAMPLE_WIDTH 12
#define SAMPLE_HEIGHT 12

static uint8_t Sample[] PROGMEM = {
  0x0, 0x0, // size of zero indicates fixed width font, actual length is width * height
  0x0C, // width
  0x0C, // height
  0x20, // first char
  0x02, // char count

  // font data
  0x00, 0x00, 0x00, 0xE0, 0x98, 0x86, 0x98, 0xE0, 0x00, 0x00, 0x00, 0x00,    //A
  0x00, 0x40, 0x70, 0x40, 0x00, 0x00, 0x00, 0x40, 0x70, 0x40, 0x00, 0x00,
  0x00, 0x84, 0x44, 0xFD, 0x16, 0xD4, 0x7A, 0x12, 0x20, 0xC0, 0x00, 0x00,    //あ
  0x00, 0x30, 0x40, 0x20, 0x10, 0x00, 0x40, 0x40, 0x20, 0x10, 0x00, 0x00
};
#endif

上記のフォントファイルを展開して、「あ」を表示する試験スケッチを以下に示します。

#include <ks0108.h>  // library header
#include <Sample.h>
#include <MsTimer2.h>

#define REFRESH   24

void setup()
{
  Serial.begin(9600);
  GLCD.Init(NON_INVERTED);
  GLCD.SelectFont(Sample);
  MsTimer2::set(1000/REFRESH/8, GlcdUpdate);
  MsTimer2::start();
}

void GlcdUpdate()
{
  GLCD.Update();
}

void  loop()
{
  GLCD.ClearScreen(); 
  delay(500);
  showKanji(0x21, 0, 0, 12, 12);
  delay(5000);
}

void showKanji(char c, uint8_t posX, uint8_t posY, uint8_t fontWidth, uint8_t fontHight)
{
  for (posY = 0; posY < 63 - fontHight; posY += fontHight)
  {
    for (posX = 0; posX < 127 - fontWidth; posX += fontWidth)
    {
      GLCD.GotoXY(posX, posY);
      GLCD.PutChar(c);
    }
  }
}

写真のフォーカスがいまいちですが、「あ」の文字が表示できました。

Kanjidispfirst_3

今後の課題

1. 東雲(shinonome)ゴシックの12ドットフォント全体を今回示したArduino形式にコンバートする必要があります。こちらは機械的な処理ですので、PC側で処理ができそうです。

2. BDF形式で配布されている東雲フォントのデーターは、JISX 0208のコード体系で収録されています。最終的にUTF-8の文字コードから表示を行うためには、UCS-2のコード体系でフォントデーターを格納しておくと表示の際のコード変換処理が楽になりそうです。UTF-8では漢字コートは3バイトで表現されてしまうため、2バイトで表現できるUCS-2のコード体系でフォントデーターを作ることを考えています(UTF-8からUCS-2へのコード変換は簡単なビット演算で可能)。

ただ、UCS-2の漢字コードは、CJK統合漢字と呼ばれる体系になっており、JIS漢字とは全く異なる文字配置のため、機械的にUCS-2コード順に並び替えができるかが分かっていません。またCJK統合漢字では、中国・日本・韓国で使用する類似の漢字を同一コードにマッピングしていますが、中国のみの漢字コードが混在するようなので、その部分はフォントデーターをブランクにするなどの処理が必要になりそうです。

3. 手持ちのI2C EEPROMは1M bit(128KB)ものが2個です。一文字あたり24Bのデーター量となり、JISの8800文字を収録すると200KB程度ですので容量的には入ると思うのですが、場合によってはフォントデーターの圧縮を考えないとだめかも。12ドットフォントの場合4bitは使っていない(常に0)のため、その部分を詰めることもできそうですが、Arduino ks0108ライブラリの描画ルーチンをそのまま使う基本方針をとった場合、データー量が増えても今回示したフォントデーターで通してしまうのが楽ちんです。

どこかに、UCS-2(もしくは、何らかのUnicode)形式で収録されたフリーの漢字フォントデーターがあるとよいのですが。

2009/7/5更新: 続編として、Arduinoで漢字表示(2)を公開

2009/7/12更新Arduinoで漢字表示(3)を公開

2009/7/21更新Arduinoで漢字表示(4)を公開

グラフィック・ディスプレイ表示の高速化

Arduinoでグラフィック・ディスプレイを使用するで紹介したGLCD(SG12864A)の表示高速化を行いました。

高速化の手法

前回記載した手法を採用ししました。内容を再掲します。

現状描画時の書き込みは、VRAMを更新すると同時にチップへの書き込みを行っています。チップへはランダムアクセスとなるため、データーを書き込む毎にコントロールレジスタにアドレスを設定しており、その点がオーバーヘッド要因と思われます。そのため、描画の際は一旦VRAMを更新した後、一定周期毎に1ライン分のVRAMデーターをブロック転送します。ブロック転送の際は、GLCD側でアドレスを自動インクリメントしてくれるためアドレス設定のオーバーヘッドがなくなる分高速化が期待できます。

一方で、前回の処理は変更が発生した箇所のみGLCDにデーターを送っているのに対して、今回のブロック転送方式では画面全体を更新するためデーター転送量が増加し、こちらが別のオーバーヘッド要因になってしまう可能性があります。

結果は最後に示しますが、微妙なところでした。

コードの概要

WriteData()関数で、vlanに画面データーを保存すると同時にデバイスへの書き込み(青字の部分)を行っていましたが、デバイスへの書き込みを削除。

void ks0108::WriteData(uint8_t data) {
  uint8_t displayData, yOffset;

  GotoXY(this->Coord.x, this->Coord.y);

  yOffset = this->Coord.y%8;

  if(yOffset != 0) {
    // first page
    displayData = this->ReadData();
    displayData |= data << yOffset;
    if(this->Inverted)
      displayData = ~displayData;
    //lcdDataOut( displayData);           // write data
    //this->Enable();                         // enable

    // -- Addition for Frame Buffer --
    vram[Coord.page][Coord.x] = displayData;  // Write back vram

    // second page
    this->GotoXY(this->Coord.x, this->Coord.y+8);
    displayData = this->ReadData();
    displayData |= data >> (8-yOffset);
    if(this->Inverted)
      displayData = ~displayData;
    //lcdDataOut(displayData);               // write data
    //this->Enable();                            //enable

    // -- Addition for Frame Buffer --
    vram[Coord.page][Coord.x] = displayData;

    //this->GotoXY(this->Coord.x+1, this->Coord.y-8);
    Coord.x++;
    Coord.y -= 8;
  }
  else {
    // just this code gets executed if the write is on a single page
    if(this->Inverted)
      data = ~data;   
    //EN_DELAY();
    //lcdDataOut(data);                          // write data
    //this->Enable();                             // enable
    // -- Addition for Frame Buffer --
    vram[Coord.page][Coord.x] = data;
    this->Coord.x++;
  }
}

Update()関数を追加して、この関数の中で1ページ分(Y座標8ライン分に相当)のデーターをブロック転送します。スケッチの中で、GLCD.Update()を周期的に呼び出すことで画面を更新していきます。7 Seg LEDのダイナミック点灯と同じ原理です。

void ks0108::Update()
{
  uint8_t chip, colum, x, cmd;
  uint8_t displayData;

  for (chip = 0; chip < DISPLAY_WIDTH/CHIP_WIDTH ; chip++)
  {
    cmd = LCD_SET_PAGE | page;
    this->WriteCommand(cmd, chip);
    cmd = LCD_SET_ADD | 0;
    this->WriteCommand(cmd, chip);
    fastWriteHigh(D_I);   // D/I = 1 (Data)
    for (colum = 0; colum < CHIP_WIDTH; colum++)
    {
      if (chip == 0)
        x = colum;
      else
        x = colum + CHIP_WIDTH;
      displayData = vram[page][x];
     lcdDataOut(displayData);
     this->Enable();

    }
  }
  page++;
  if (page >= DISPLAY_PAGE)
    page = 0;
}

茶色の箇所でデバイスにデーターを書き込みます、Enable()を呼び出して、Enable信号をHigh→Lowに変化させる立ち下がりタイミングでアドレスの自動インクリメントが行われると思われます(データーシートには詳細な情報なし)。

デバイスへの書き込みを行った後にデバイスのビジー状態を確認するためのWaitReady()という関数がありますが、この関数を使ってタイミングを取るとうまく表示ができませんでした。WaitReady()関数はコントロールレジスタの読み出し(リード)を行いますが、データーのライト後にリードサイクルが入るとインクリメントがうまくいかないようです。(ひょっとして、リードサイクルでもインクリメントが発生して余計にアドレスを進めているのか?)。

そのため、ビジー状態は確認せずに連続して書き込みを行いますが、Enable()の中で使用しているdelay値(EN_DELAY_VALUE)を12まで大きくしないと書き込み速度にデバイス(コントローラー)が追随できませんでした。

Updateの呼び出し

MsTimer2ライブラリを使って、タイマー割り込みを発生させUpdate()関数を呼び出しています。MsTimer2ライブラリに割り込みハンドラーを登録するする必要があるのですが、ks0108ライブラリーの初期化コードの中でUpdate()関数を登録しようとしてもエラーが発生してうまく行きませんでした。

仕方がないので、メインのスケッチの中で、以下のコードを書いてUpdate()を割り込みハンドラーとして登録しています。

#include <ks0108.h>
#include <MsTimer2.h>

void setup(){
  GLCD.Init(NON_INVERTED); 
  MsTimer2::set(1000/REFRESH/8, GlcdUpdate);   //割り込みハンドラーを登録
  MsTimer2::start();                                           //タイマー割り込みを開始

}

void GlcdUpdate()
{
  GLCD.Update();
}

効果の程は

例によって、FPSを計るデモスケッチを使って画面の書き換え速度を計測します。更新速度はUpdate()を呼び出すタイマー割り込み周期に依存します。

先ずは、通常のLCDディスプレーと同レベルの60Hzを設定します。一回のUpdate()呼び出しで1ページ分を更新しており、画面全体を更新するためには8回の呼び出しが必要です。そのため、1秒間に60回画面全体を更新するための割り込み周期(ms)は;
 1000(ms) / 60 (回) / 8 = 2ms
となります。

リフレッシュ60HzでのFPS値は、なんと13 FPSで、前回のVRAM版の性能を下回りました・・・ やはり画面全体の転送を1sに60回も行うとこちらの処理オーバーヘッドの方が大きくなります。 

リフレッシュレートを、24Hz (割り込み周期5ms)にすると、20FPS!となりました。これなら(自己)満足の数字です。前回の結果とあわせて測定値の一覧を示します。

項目OriginalVRAM版ブロック転送
最小DELAY値  4  1 12
FPS  8  14 20


動作中の写真を以下に示します。

Glcd_wb1

ブレッドボード左側のチップは漢字フォント格納用のEEPROMです。漢字表示は現在トライ中ですので、動くようになったら公開します。

Glcd_wb2

最後に今回使用したks0108ライブラリー(改)のコード一式を以下のリンクにアップしておきます。
 「ks0108_wb.zip」をダウンロード
注)このソースは、オリジナルのks0108ライブラリを高速化のためにArduino Mega専用に改造しております。そのため、Mega以外のボードでは動作いたしません。

ArduinoライブラリのMCUタイプ依存性

2009/8/14追記:
以下に示す問題は最新のIDE-0017で改善されました

Arduinoライブラリの中に、Duemilanove (ATMega168/328)からMega (ATMega1280)などMCUのタイプを変えた場合に動かなくなるものがあります。以下のライブラリが該当します。

  • MsTimer2(タイマー割り込みライブラリ。標準ライブラリではなく、Contributed Libraryですが、周期処理を行う際に便利)
  • Wire (I2C/TWIのライブラリ)

原因は分かっていません。Timer2やTWI関連のレジスタマッピングが上記のMCUで異なるわけではありませんし。MsTimer2.cppでは、以下のように、MCUタイプ毎のコードがありますが、168/328/1280は同じif definedブロック内です。

void MsTimer2::start() {
    count = 0;
    overflowing = 0;
#if defined (__AVR_ATmega168__) || defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) || defined (__AVR_ATmega328P__) || (__AVR_ATmega1280__) →同じif def内
    TCNT2 = tcnt2;
    TIMSK2 |= (1<<TOIE2);
#elif defined (__AVR_ATmega128__)
    TCNT2 = tcnt2;
    TIMSK |= (1<<TOIE2);
#elif defined (__AVR_ATmega8__)
    TCNT2 = tcnt2;
    TIMSK |= (1<<TOIE2);
#endif
}

インストール時にデフォルトで入っているライブラリのコンパイル済みオブジェクトはどうもATMega168/328用にコンパイルしてあるみたいで、Arduino Mega (ATMega1280)をターゲットにリンクすると上記のライブラリはMega上ではうまく動きません。

hardware\cores\arduino\配下に存在する、IOピンの定義など(pins_arduino.c)もろにボードタイプ(MCU種別)に依存するコードは、スケッチ毎のappletフォルダーにオブジェクトファイルを作ってリンクするため、ボードタイプが変わっても影響がありません。一方、ライブラリはライブラリフォルダー内のオブジェクトファイルをリンクする(ボードタイプを変えても自動的に再コンパイルしてくれない)ため、ボードタイプの変更に追随できないケースがあるようです。

そんなときは、ライブラリのフォルダーにある、xxx.o(例えば、hardware\libraries\MsTimer2\MsTimer2.o)を一旦削除して再コンパイルすると動くようになります。

Arduinoメールチェッカー(その3)

2015/3/23追記: Arduino 1.6.1対応のソースをアップしました。

MailChecker30_public.zipをダウンロード

メールチェッカーの不安定要因であたメモリーリーク問題が解決したため、最新版のコードをアップします。

対策は、Arduino TexitStringライブラリのメモリーリークに示したTextStringライブラリコードの変更になります。変更箇所のみを以下に再掲します。

Stringライブラリのコードに、以下のデストラクタを追加します。

<WString.hへの追加>
  public:
    ~String();   ←追加

<WString.cppへの追加>
 String::~String()
 {
   free(_array);
 }

あわせて、メールチェッカーのコードも若干見直しを行いました。

  • メールサーバーに送信するpop3コマンド文字列を保持するバッファ量を拡大
  • String.indexOf()メンバー関数の引数(検索する文字の指定)を、" "から' 'に変更。
    " "を指定するとStringオブジェクトを引数として渡します。
    ' 'の場合は、charのスペース(0x20)を引数として渡します。
    → " "を引数にした場合のメモリーリークは解決したのですが、処理が重いオブジェクトを使用するまでもないため、charデータ型に変更しました

メールチェッカーのメモリー使用量

メールチェッカーのコードがスタック・ヒープをどの程度使っているのかを調べてみました。yagihiroさんのblogを参考にしました。

IDEと一緒にインストールされる「avr-objdump, avr-nm」を使用してヒープの割当を調べると以下の通りでした。elfファイルは、スケッチを保存したフォルダーのapplet配下に生成されます。

C:>avr-objdump -h MailChecker23.elf

MailChecker23.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         000001f8  00800100  00003c26  00003cba  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00003c26  00000000  00000000  00000094  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .bss          0000026b  008002f8  008002f8  00003eb2  2**0
                  ALLOC

.data + .bssセクションが 0x463 = 1123byte確保されています。この領域は、グローバルないしは静的に宣言している変数・インスタンス・定数などを格納しているようです。私は、SRAM 2KBのATmega328を使用しているため、SRAMの約半分をこの領域で使用していることになりす。

avr-nmコマンドにて、シンボル毎のメモリ割付を確認することができます。
C:>avr-nm -n -C MailChecker23.elf

00800100 D __data_start
008002f8 B __bss_start
 ~中略~
008002f8 D __data_end
008002f8 D _edata
 ~中略~
00800563 B __bss_end
00800563 N __heap_start

ヒープが0x0563から始まることが分かります。

Arduino TexitStringライブラリのメモリーリークに示した、heap pointerを出力するコードを入れて、pop3サーバーアクセス開始時のheap pointerを調べると以下でした。

Heap ptr :6C3
Stack ptr:8D6

プログラム開始時に確保するヒープ領域が0x160 = 352byte程度あることを示します。

Stack Endが0x08FF(2KBのSRAM空間の終わり)だとすると、Stack pointerが0x8D6のため、スタックを0x29 = 41byte使っていることになります。

Stack pointer - Heap pointerが空きメモリーということになるため、0x213 = 531byteが空きメモリーということになります。DateTime, NTPなどのライブラリも取り込んだため、残りメモリーが少なくなってきました。

上記から分かったメモリーマップのイメージを図にすると以下となります。Memmap_3

グローバル変数を多用して汚いコードになっている部分を、関数への引数渡しに書き替えてデーターの流れをすっきり見るようにしようかとも思ったのですが、スタックの使用量増加がよく分からないため(その分.data + .bssは減るのですが)当面は現状のままで行こうと思います。

Arduino TexitStringライブラリのメモリーリーク

メールチェッカーがメモリーリークを起こしている原因がやっと分かりました。犯人はやはりTextStringライブラリでした。

確認用スケッチ

以下のスケッチを実行することでメモリーリークが確認できます。テストコードはメールチェッカーのStringクラスを使用した文字列操作部分を抜粋しています。Stack/Heapトップの取得方法(check_mem関数)は、Arduino Playground記載のコードを流用しました。

#include <WString.h>

#define BufferLength   128
String ReceiveBuffer(BufferLength + 2);
int Count = 0;

void setup()
{
  Serial.begin(115200);
  check_mem();
  ReceiveBuffer = "+OK 14 327647";   //ダミーで固定値を入れておく
}

void loop()
{
  Serial.print("Count=");
  Serial.println(Count, DEC);
  checkString();
  Count++;
  check_mem();
  Serial.println();
  delay(5000);
}

void checkString()
{
  int firstSpace = ReceiveBuffer.indexOf(" ", 0);
  int secondSpace = ReceiveBuffer.indexOf(" ", firstSpace+1);
  String numberOfMails = ReceiveBuffer.substring(firstSpace+1, secondSpace );
  int lastNumMails = atoi((char*)numberOfMails);
  Serial.println(lastNumMails);
}

uint8_t * heapptr, * stackptr;
void check_mem()
{
  stackptr = (uint8_t *)malloc(4);          // use stackptr temporarily
  heapptr = stackptr;                     // save value of heap pointer
  free(stackptr);      // free up the memory again (sets stackptr to 0)
  stackptr =  (uint8_t *)(SP);           // save value of stack pointer

  Serial.print("Heap ptr :");
  Serial.println((long)heapptr, HEX);
  Serial.print("Stack ptr:");
  Serial.println((long)stackptr, HEX);
}

実行結果は以下の通りです;

Count=0
Heap ptr :287
Stack ptr:8F5

Count=1
Heap ptr :296
Stack ptr:8F5

Count=2
Heap ptr :2A5
Stack ptr:8F5

Count=3
Heap ptr :2B4
Stack ptr:8F5

Count=4
Heap ptr :2C3
Stack ptr:8F5

ループが回る毎に、16byte (0xF)Heap Pointerが増加している、即ち解放されないメモリー領域がヒープエリアに蓄積していることが分かります。Heap ptrがStack ptrに届いたときに、プログラムがクラッシュしてしまいます。

どこでメモリーリークが起きているのか

問題になるStringライブラリのメンバー関数は以下です;
1)int firstSpace = ReceiveBuffer.indexOf(" ", 0);
2)String numberOfMails = ReceiveBuffer.substring(firstSpace+1, secondSpace );

2)のsubstring()メンバー関数については、以下のライブラリソースに問題がありそうです。substring()は、文字列の指定範囲を切りだして、切りだした部分をStringオブジェクトとして呼び出し側に返します。戻り値となるStringオブジェクトを関数内で宣言していますが(赤字の部分)、このオブジェクトが解放されずに残ってしまうのだと思います。

String String::substring(int beginIndex, int endIndex)
{
  if ( beginIndex > endIndex )
  {
    int tmp = endIndex;
    endIndex = beginIndex;
    beginIndex = tmp;
  }
  if ( endIndex > _length )
  {
    exit(1);
  }
  char ch = _array[ endIndex ];   
  _array[ endIndex ] = '\0';      
  String str = String( _array + beginIndex );
  _array[ endIndex ] = ch;      
  return str;
}

1)のindexOf()メンバー関数については、関数内でStringオブジェクトを新たに確保していることはありません。こちらは、呼び出し側で引数として指定した「" "」がStringオブジェクトとして生成され削除されずに残ってしまうものと思われます。

int String::indexOf(const String &str, int fromIndex)
{
  if(fromIndex >= _length)
    return -1;

  char *result = strstr(&_array[fromIndex], str.cstr());
  if(result == NULL)
    return -1;

  return result - _array;
}

対策は

そもそも、関数の戻り値としてオブジェクトを確保した場合、そのオブジェクトはどこで・誰が廃棄するの?

答えは、デストラクタを使うことで、Arduino Forumに回答がありました。C++は上記をスマートに解決する方法があったのですね。「Cとは違うのだよ、Cとは」とC++コンパイラさんにあざ笑われたような、、

Stringライブラリのコードに、以下のデストラクタを追加することで問題が解決しました。

<WString.hへの追加>
  public:
    ~String();   ←追加

<WString.cppへの追加>
 String::~String()
 {
   free(_array);
 }

上記のデストラクタを追加することによって、オブジェクトのスコープを抜けた際に、文字列格納格納領域(_array)を解放するようになりメモリーリークが解消しました。

メモリーリーク対策後のテストスケッチ実行結果

デストラクタを追加し、WString.oファイルを削除してから、テストスケッチを再コンパイルします。「warning: comparison between signed and unsigned integer」といった警告が一杯出るのですが、ビルドに成功すればOKとします。

実行結果は以下となり、heap pointerがピクリとも動かなくなりました。

Count=0
Heap ptr :278
Stack ptr:8F5

Count=1
Heap ptr :278
Stack ptr:8F5

Count=2
Heap ptr :278
Stack ptr:8F5

Count=3
Heap ptr :278
Stack ptr:8F5

Count=4
Heap ptr :278
Stack ptr:8F5

C++のデストラクタは、名前を知っている程度で、有用な利用シーンを見いだしたことがなかったのですが、こういう使い方があるのですね。

問題となった2)のケースでは、substring()終了時に、呼び出し元のString numberOfMailsにオブジェクトをコピーしてからsubstring()内で生成したStringオブジェクトをデストラクタが削除してくれるのだと思います。

う~、C++のメモリー管理は複雑だ、、
実は、Arduinoに触るまではC++をまともに使ったことがなかったのです。オブジェクト指向言語としては、C#なら少々触ったことがあるのですが、こちらはガベージコレクタが自動的に不要メモリ領域の回収行ってくれるので楽ちんですよね。

ArduinoでNTPを使用する

ArduinoのNTP libraryを見つけたので動かしてみました。DateTime libraryを使って時刻の管理を行いたい場合に、Arduino起動時に時計を合わせるのが面倒だったのですが、NTPがあれば便利です。DateTime libraryの時計精度は日差数秒程度で、秋月電子さんで売っているRTC(リアルタイムクロックモジュール)と大差ないレベルの精度が得られるのですが、バッテリーバックアップが効かないため、電源ON/リセット時の時計合わせが煩雑でした(PCからシリアル通信で時間情報をもらうなどの操作が必要)。

ライブラリのインストール

IDEは最新の0016を使用しました。Ethernet libraryのバグ修正が行われているため、Ethernet Shieldをお持ちの方は0016への更新がおすすめです。

NTPライブラリは、cynshard / arduino-ntpを使用しました。リンクページのdownloadボタンを押すとzipファイルがダウンロードできます。ダウンロードしたファイルを、\IDEフォルダ\hardware\libraries\ntpにコピーします。

NTPライブラリのダウンロードページにも記載がありますが、NTPを使用するためにUDPライブラリの追加が必要です。Ethernet library配下のutilityフォルダにある、socket.cにはsendto()というUDPデーターグラム送信関数が用意されているのですが、Ethernet libraryは現状TCPしかサポートしていません。

cynshard arduino-ntpは、bjoern / arduino_oscというライブラリの一部として公開されているUDPライブラリを使用します。ダウンロードページにある、arduino_osc-tip.zipというファイルをダウンロードし、その中にあるEthernetフォルダのファイルを、\IDEフォルダ\hardware\libraries\Ethernetにコピーします。

最後に、「UdpBytewise.h」の送受信バッファサイズを以下の値に変更します。(オリジナルは32です)

#define UDP_TX_PACKET_MAX_SIZE 64
#define UDP_RX_PACKET_MAX_SIZE 64

この変更を行わないとNTPが正しく動作しませんでした。

上記の作業が終わったら、ntp\examples\datetime-syncをビルドして動作確認ができます。

メールチェッカーへの応用

メールチェッカーに、DateTime libraryを使って現在時刻を表示できるようにしました。表示内容を以下に示します。

Mailcheker_w_clock

メールチェック・ログ(DEBUG_PRINT)のタイムスタンプが、以前はmillis()関数で取得した起動からの経過時だったのですが、こちらも日時を表示するようにしました。また、24hに1回NTPサーバーにアクセスして時刻同期を行います。

Ethernet libraryをIDE-0016に変更したため、TCPパケット送信用メンバー関数を、バグがあったclient.println()からclient.write()に変更しました。何回かこのブログにコメントをいただいた、hamayanさんのUnder Power 研究所でwrite()関数のことを知りました。ライブラリのドキュメントが更新されていないため、上記ブログを見ないとメンバー関数の追加に気がつかなかったかもしれません。情報ありがとうございました。

最新版のソースを以下に示します。

Arduinoスケッチのスタックサイズ

メールチェッカーに時刻表示の機能を追加した際に、「MM/DD hh:mm:ss」の形式に文字列の編集が必要となり、関数を追加したところ動作が不安定になってしまいました。

関数内で長さ4文字のStringオブジェクトを一つ定義し、このオブジェクトへの参照を別の関数に渡すようにしたのですが、この変更でスタックが溢れた可能性があります。上記のStringオブジェクトをグローバル変数にして関数間での受け渡しもやめたところ安定しました。

使用しているいるMCUはATmega328ですので、SRAMは2KBあります。ダイナミックデーターは1KBも使っていないと思うのですが、スタックにどれだけの領域が割り当てられているのが気になります。ローカル変数を一つなくすことで挙動が変わるため、ぎりぎりで動いている感じがします。

2009/6/8 追記

以下のコードを入れてheapの変化を追いかけると、どうもメモリーリークがあるようです。そのため、長時間動作させるとハング・リセットが発生します。

Ardunino Playground Available Memoryより

uint8_t * heapptr, * stackptr;
void check_mem() {
  stackptr = (uint8_t *)malloc(4);          // use stackptr temporarily
  heapptr = stackptr;                     // save value of heap pointer
  free(stackptr);      // free up the memory again (sets stackptr to 0)
  stackptr =  (uint8_t *)(SP);           // save value of stack pointer
}

6/6日版のコードはメールチェックあたり15byteのリークですが、6/7日版のコードはリークの量が不定でかつ増えてしまいました。時刻表示のために、Stringクラスを使用した文字列操作を多用しているのですが、このあたりが怪しそうです。

2009/6/9 追記:

メモリーリークの対策版を以下の記事で公開しました。
Arduino TexitStringライブラリのメモリーリーク
Arduinoメールチェッカー(その3)

Arduinoイーサーネットシールドの接続処理

Arduinoのイーサネットシールド(Ethernet Shield)にて、リセット後1回目のclient.connect()がtimeoutする問題があると書いたのですが、調べてみると、タイムアウトの発生条件は、「コネクションを切断した直後にリセットを行って、同一宛先に再接続した場合でした」。以下、分かったことを示します。

2009/6/7追記:hamayanさんからいただいたコメントに基づき確認方法を見直しました。

調査用のスケッチ

以下に示す調査用のスケッチを作成して、PC上のWeb Serverにアクセスしてみます。EthernetライブラリをIDE-0016に変更しました。

#include <Ethernet.h>

byte mac[] = { xx, xx, xx, xx, xx, xx };
byte ip[] = { 192, 168, 0, 110 };
byte server[] = { 192, 168, 0, 10 };

Client client(server, 80);

void setup()
{
  Ethernet.begin(mac, ip);
  Serial.begin(115200);
 
  delay(1000);
}

void loop()
{
  getpage();
  for(;;)
   ; //無限ループ
}

void getpage()
{
  Serial.println("connecting...");
  Serial.print("client.status = ");
  Serial.println(client.status(),HEX);
 
  if (client.connect()) {
    Serial.println("connected");
    Serial.print("client.status = ");
    client.write("GET /index.html HTTP/1.0\r\n\r\n");
    Serial.println(client.status(),HEX);
  } else {
    Serial.println("connection failed");
    Serial.print("client.status = ");
    Serial.println(client.status(),HEX);
    Serial.println();
    return;
  }

  while(true)
  {
    if (client.available()) {
      char c = client.read();
//      Serial.print(c);
    }
 
    if (!client.connected()) {
      Serial.println("disconnecting.");
      Serial.print("client.status = ");
      Serial.println(client.status(),HEX);
      client.stop();
      Serial.println("client.stop");
      Serial.print("client.status = ");
      Serial.println(client.status(),HEX);
      Serial.println();
      return;
    }
  }
}

スケッチアップロードの一回目の起動は、以下のように正しく動作します。各statusの意味を、「w5100.h」から抜粋しました。

connecting...
client.status = 0 → SOCK_CLOSED
connected
client.status = 17 → SOCK_ESTABLISHED
disconnecting.
client.status = 1C → SOCK_CLOSE_WAIT
client.stop
client.status = 0

一回目の動作が終了直後にリセットをかけて再接続すると、以下のように接続に失敗(タイムアウト)します。

connecting...
client.status = 0
connection failed
client.status = 0

LEDの点滅を見ていると、サーバーにパケットを投げているように見えます。WireSharkを使用してPC側でパケットキャプチャーを行うと、以下のように、Ethernet ShieldはTCP-SYNを投げてコネクション接続の要求を行っているのですが、PC(サーバー)側が応答していないことが分かりました。

Capture_2

なぜこうなるのかですが;

Ethernet Shieldからのアクセスが終わった直後に、netstatコマンドを使用してネットワークの接続状態を調べると、以下のようにEthernet Shieldとの接続は「TIME_WAIT」状態になっています。

C:>netstat -n
アクティブな接続
  プロトコル  ローカル アドレス          外部アドレス        状態
  TCP    127.0.0.1:27015        127.0.0.1:49178        ESTABLISHED
  TCP    127.0.0.1:49178        127.0.0.1:27015        ESTABLISHED
  TCP    192.168.0.10:80        192.168.0.110:1025     TIME_WAIT

TIME_WAITの意味は以下です(RFC793より):
 対向側のTCPがFIN-ACKを受信するまでの十分な時間を確保するための待ち時間。

Windows側がTIME_WAIT状態中にArduinoをリセットした場合、Ethernet Shieldは前回アクセスと同一のソース・ポートを使ってTCP-SYNを投げるため、Windowsが応答しないようです。TIME_WAIT状態が終了するまで待ってリセットを行うと接続ができます。

冒頭に示したスケッチでは、client.connect()は1回しか実行しませんが、タイムアウトした際に再度client.connect()を行う処理を追加すると、TIME_WAIT中であっても、Ethernet Sheildがソース・ポートを1026に変えてTCP-SYNを投げるため2回目のclient.connect()は成功します。即ち、client.connect()をリトライすると成功することになります。

従って、これまでEthernetライブラリのバグではと思っていた事象は、WindowsのTCPセッション管理との兼ね合いで発生していることになります。

Linuxだとどうなるか

Linuxの場合、TIME_WAIT期間中にリセットによる再接続を行ってもタイムアウトせずに接続できました。リセット後のTCP-SYNで一度TCP-RSTが入るのですが、ポート番号は1025のままでもLinuxは新規のコネクションを受け入れるようです。

シーケンス図でまとめると

パケットキャプチャーで分かった動作シーケンスをWindows/Linuxそれぞれまとめると、以下のようになります。Windows/Linux共、Web ServerをApache2.2にして動作条件を統一しました。

Tcpsequence_20090607

Windows/Linuxそれぞれの動作概要を以下に示します(数字は、図のまる付き数字に対応します)。

■Windows (Vista)

  1. Src Port 1025で一回目の接続
  2. 切断。Server側からTCP-FINを送信している
  3. Client (Ethernet Shield)もTCP-FINを送信する
  4. Windowsの接続状態がTIME_WAITになる
    → TCPの教科書だと、FINは双方向で送信すること、TIME_WAIT状態は先にFINを送信したActive close側の状態遷移であるため、不思議な状態遷移をしているように見えます
  5. Windows側でTIME_WAIT状態になっているSrc Port 1025を使って再接続
  6. Windowsが応答せずタイムアウト
  7. Src Port番号を変えて再接続すると成功する

■Linux

  1. Src Port 1025で一回目の接続
  2. 切断。Server側からTCP-FINを送信している
  3. Client (Ethernet Shield)もTCP-FINを送信する
  4. Linuxの接続状態がTIME_WAITになる
    → 双方向でTCP-FINを送信しており、一般的な終了シーケンスになっている
  5. Linux側でTIME_WAIT状態になっているSrc Port 1025を使って再接続
  6. LinuxがSYN-ACKを返さない(ACKフラグのみを返す)
  7. TCP-RST後の再接続でコネクションが確立する
    → clinet.connect()のタイムアウトは発生しない

WindowsとLinuxで使用したWebサーバーが異なりますが、TCP-FINシーケンスの差分はカーネルの実装によるものなのか、もしくはサーバーアプリのSocket操作によるものなのかは分かりませんでした。
当初、ServerアプリがAbyssの際は、Server(Windows)がTCP-SYNを送信しないように見えたのですが、ApacheではTCP-FINの送信がキャプチャーできるようになりました。一回目の接続・解放シーケンスは、Windows/Linux共同様になりました。

メールチェッカーでも同様に、一回目のチェック終了直後にリセットを行うとタイムアウトが発生します。当方が使用するメールサーバーも (niftyですが)Windows的な動作になっているのでしょうか。BBルーターのNATで同一ポートの再接続が蹴られるということはないと思うのですが。

ということで、client.connect()のタイムアウトに関しては、リトライで救うしかないということが分かりました。

Arduinoメールチェッカー(その2)

Arduinoメールチェッカーのプログラムを少々改良しました。

初版のプログラムは、メールサーバーにアクセスする毎に全てのメールヘッダをダウンロードしており効率がよくありませんでした。監視対象にしている個人メールアカウントに届くメールは殆ど広告系で毎日メールをチェックすることもないため、一週間も放っておくと50件以上メールがたまります。何十件分のヘッダを毎回ダウンロードすると、サーバーにも負担がかかります。

改良版では、前回アクセス時のメール件数を記憶しておき、最新アクセスでメール受信件数が増えている場合、増加分のメールヘッダのみをダウンロードするようにしました。メール受信件数の増加がない場合は、ステータスチェックのみでログアウトします。

改良版のアクセス負荷ならチェック周期を短縮しても許されると考え、周期を60分から30分に短縮してあります。

リセット後、1回目のclient.connect()がタイムアウトする問題ですが、Arduino Forumのこのスレッド#8にあるコードで改善されないかと思い試してみましたがダメでした。そのため、connection設定に失敗した場合、client.connect()をリトライする処理を追加しました。私の環境では、最初のリトライでほぼ接続ができます。この問題の改善はもう少しコードハックをして考えてみようと思います。

改良版のソースコードは以下です:
 「MailChecker_pub20090606.zip」をダウンロード

追記:
Arduinoイーサーネットシールドの接続処理に、リセット後のタイムアウト問題の調査結果を記載しました。

2009/6/8 追記:

掲載したコードにメモリリークがあることが分かりました。チェック一回ごとに、15byte heap領域が減少します)。サンプルとしてコードは掲載しておきますが、もし使用される場合はご注意下さい。

2009/6/9 追記:

メモリーリークの対策版を以下の記事で公開しました。
Arduino TexitStringライブラリのメモリーリーク
Arduinoメールチェッカー(その3)

Arduinoでグラフィック・ディスプレイを使用する

秋月電子さんのグラフィック・ディスプレイSG12864Aを使ってみました。後、ArduinoのGLCDライブラリを改造(高速化)してみたのでご紹介します。

使用したハード

GLCDは8bitパラレルのデーターバスに加えて、5本の制御信号があるため、Arduinoのディジタルポートを13本使用します。Arduino Duemilanoveでもアナログポートを加えると18本のI/Oポートがありますが(Digital 0, 1はハードウェアシリアル用のため除外してあります)、ポート数に余裕がありません。

また、高速なアクセスを行うためには、データーバスの8bitは同一I/Oポートに収容することが望ましいのですが、Duemilanoveの場合Port D以外は6bit分しか配線がないこと、Port DもPD0 (digital pin 0), PD1 (ditigal pin 1)はハードウェアシリアル用ですので実質的には残り6bitしか使えず8bitが連続したポートを確保できません。そのため、少々お高いのですが、Arduino Megaを使用しました。

GLCDをつないだだけですが、回路図を以下に示します。

Glcd_test

Megaの信号ピンを全て回路図に書き込む根気がなくなったので、未使用ピンは一部省略してあります。接続では、以下が注意点です;

  • コントラスト調整用の出力電圧ピンは18番です。添付のデーターシートではVout/VEEなど記載が一貫しておらず混乱しました
  • バックライトの電流制限抵抗が必要。抵抗を入れないとパネルが思いっきり発熱します→ 当たり前ですね、、
  • 配線をなるべく短くする。後で述べますが、低ウェイトで高速動作させる場合の安定性に影響があるかも、です

GLCDのライブラリ

ArduinoのKS0108 Graphics LCD libraryを使用しました。KS0108コントローラーを使用したLCDには種類があるようで(秋月のSG12864Aもその一種)、このリンクで公開しているバージョンのライブラリはLCDによっては相性があります。SG12864Aは相性が悪い部類のようで、ライト時のウェイト値をかなり大きくしないと安定して動作しませんでした。

KS0108 libraryの作者さんが、βですが最新版をForumに投稿しています。こちらの方がSG12864Aと相性がよく、かつ性能が向上しているためお勧めです。最新版は以下のリンクから入手ができます。

 Graphic LCD (KS0108) library now availableのReply #113

回路図に示した構成で、ウェイト値の調整などなく、一発で動きました。
Mega側の使用ポートは決め打ちになっており、「ks0108_Mega.h」に定義があります。

性能のチューニング

ダウンロードファイル添付の「GLCDexample.pde」を使うと、FPSを測定することができます。ks0108_Panel.hファイルの"EN_DELAY_VALUE"の設定値によってチップアクセスのウエイト値を変更し、チューニング結果の確認ができます。標準設定は6ですが、私の環境では4までウエイトを削ることができ、その際のFPS値は8でした。

SG12864A/KS0108は若干癖のある構成になっており、このwebページにあるように2個のチップが画面の左右半分ずつの描画を担当します。即ち、X座標63~64にかけて描画を行う場合は、2つのチップにまたがって書き込みを行うことになります。

2つのチップにまたがる描画を行うテストパターンを作成し、表示・消去を繰り返した際に、チップ境界の部分で表示が乱れる場合はタイミングマージン不足を意味し、DELAY値を増加する必要があります。DELAY値が小さすぎる場合の表示乱れの例を以下に示します。

Timingng

ちなみに、「GLCDexample.pde」はチップにまたがる描画が発生しない画面デザインのため安定性の評価には使えません。

さらに性能を改善するためのコード変更を行いました。

オリジナルのコードはSRAM 1KBのATmega168でも動作するように、画面を書き替える際に、チップから現在のデータを読み出し変更部分の書き換えと書き込みを行います。この読み出しが曲者で、チップの仕様によって、2回読み出しを行う必要があること(一回目は読み捨て)、1回目の読み出し後アドレスが自動インクリメントされてしまうため1つ戻してから2回目の読み出しを行っています。この読み出しのオーバーヘッドが相当あると思われます。

そのため、VRAM (Frame Buffer)エリアを1KB (128 x 64 ÷ 8 = 1024)確保し、画面状態をVRAM上に保持することによって、画面書き換え時にチップから現在の画面データーを読み出すのではなく、VRAMから読み出すようにしました。VRAM版のコードはココからダウンロード可能です。

VRAM版では、ks0108::WriteData()を全面的に書き替えています。ATmega168/328用のコードはバッサリ削除して、ATmega1280用のコードのみを残しているため、Arduino Mega専用です。そもそもVRAMとしてSRAMを1KB使用しているので、ATmega168では動きません。

VRAM版では、DELAY値を最小の1まで削ることが可能となり、その際のFPS値は14になります。FPS値をまとめると以下の通りです。

項目OriginalVRAM版
最小DELAY値  4  1
FPS  8  14

VRAM版で高速動作している写真を以下に示します。

Glcd_testphoto

今後の課題

1. さらなる性能改善を目指して、ライトバッファ機能を作る予定です。

現状描画時の書き込みは、VRAMを更新すると同時にチップへの書き込みを行っています。チップへはランダムアクセスとなるため、データーを書き込む毎にコントロールレジスタにアドレスを設定しており、その点がオーバーヘッド要因と思われます。そのため、描画の際は一旦VRAMを更新した後、一定周期毎に1ライン分のVRAMデーターをブロック転送すれば、チップ内の画面データーアドレスが自動インクリメントされるため高速転送が期待できます(ランダムアクセス時のアドレス設定が不要となる)。

2. 漢字の表示機能

実は、GLCDを使う目的が漢字の表示でした。PCがなくても動作できるRSSリーダーを作りたいと思っています。漢字表示自体は、秋月信彦さんのGLCD LIB Ver.1.02 を移植することで可能にはなっています。漢字フォントが190KB程度になり、MegaのFlash ROMにも収まらないため、I2CEEPROMに漢字フォントを格納しています。以下の点が課題で、RSSリーダープロジェクトは現在頓挫中です。

  • MegaはEthernet Shieldが使えないため、インターネットアクセス手段がない。どうせなら、トイレにも置けるように無線LANが欲しいが高価。この製品に期待ですが、Megaは非対応か。
  • ks0108ライブラリに、GLCD LIBの漢字表示ドライバ部分だけを移植しようとしているのですが、うまく動作せず(デバッガがないとデバックできません、、)
  • RSSはUnicodeを使用するのに対して、漢字フォントがSJISのため、文字コード変換を作る必要あり。nkfあたりから持ってくることができると思うのですが
  • 漢字フォントのような大容量データーを扱う時点で、AVRでなく、ROM容量が大きい16bit以上のMCUを使うべきかという悩みもあります

2009/7/10追記:
GLCD DB0~7のピン番号(回路図)に誤りがあったため修正。

« 2009年5月 | トップページ | 2009年7月 »

2018年10月
  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      
無料ブログはココログ