Arduino

Arduino RSSリーダー

これまでに準備した、GLCDへの漢字表示やRSSサイトのXML解析などのパーツをまとめてRSSリーダーを作りました。最低限の機能しか実装してありませんが、Yahoo天気情報を読み込んで1週間分の天気予報を表示するリーダーです。

ハード構成

自作のGLCDシールドをベースに、RSSの記事選択等に使うタクトスイッチを加えて、以下のハード構成としました。

Glcd_shield3

スケッチ

ArduinoでXMLを解析する」で使用したNunniMCAX XMLパーサーを使用しました。このライブラリはXMLパーサーとしては非常にシンプル・コンパクトですが、Arduino向けとしてはメモリー使用量が多くATMega328ではSRAM容量が不足してしまい、Arduino Megaが必要となります。今回は、ハードリソース的にもディジタルピンの必要本数が328では足りず、Arduino Megaが必要となっているためよしとしました。

スケッチのソースを以下に示します。
  Weather11pub.zip」をダウンロード

スケッチが起動すると、Yahoo天気情報のRSSサイトにアクセスして今日の天気をGLCDに表示します。対象地域は神奈川県東部(私のすみか)固定です、、

Rssreader

RSS情報の解析

Yahoo天気情報は、以下のソース情報抜粋に示すように、RSS 2.0の形式で配信されます。RSS 2.0はXML文書の一種です。

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>Yahoo!天気情報 - 東部(横浜)の天気</title>
    <link>http://rd.yahoo.co.jp/rss...............</link>
    <description>Yahoo! JAPANの天気情報に掲載されている最新の情報を提供しています。</description>
    <language>ja</language>
    <copyright>Copyright (C) 2009 Yahoo Japan Corporation. All Rights Reserved.</copyright>
    <lastBuildDate>Sun, 06 Sep 2009 09:51:29 +0900</lastBuildDate>
    <item>
      <title>【 6日(日) 東部(横浜) 】 晴時々曇 - 27℃/22℃ - Yahoo!天気情報</title>
      <link>http://rd.yahoo.co.jp/rss..............</link>
      <description>晴時々曇 - 27℃/22℃</description>
      <pubDate>Sun, 06 Sep 2009 05:00:00 +0900</pubDate>
    </item>
    <item>
      <title>【 7日(月) 東部(横浜) 】 曇時々晴 - 26℃/22℃ - Yahoo!天気情報</title>
      <link>http://rd.yahoo.co.jp/rss..............</link>
      <description>曇時々晴 - 26℃/22℃</description>
      <pubDate>Sun, 06 Sep 2009 05:00:00 +0900</pubDate>
     </item>

       ~ 中略 ~

   </channel>
</rss>

XML情報解析のプログラム部分は、「ArduinoでXMLを解析する」に示したサンプルスケッチと同様です。<item>~</item>タグに囲まれた部分(以後"item"要素と呼びます)が、日毎の天気情報(記事)になります。そのため、"item"要素の開始を見つけた時点で、ItemCount変数をインクリメントし、n番目のitem(デフォルトは1)内にある、"title"要素と"pubDate"要素を抜き取ってGLCDへの表示情報とします。

タクトスイッチを操作することで、翌日・前日の予報に移動します。表示対象日を切り替えた場合はページ全体をリロードして新たな対象日の予報を表示します。item毎の情報をメモリーに保持した方がレスポンスが上がるのですが、現状は手抜きをしています。

RSS 2.0を使用したサイトなら、この方法で記事のタイトル・概要(description)を取り出すことができると思います。RSSには1.0という別の形式がありこちらはフォーマットが全く異なります(RSS 1.0の方が複雑です)。今回は使用していませんが、NunniMCAXにはアトリビュート情報を取り出す機能もあるため、rssタグ(<rss version="2.0">)からversionアトリビュートを取り出して2.0 or 1.0の判定を行うことによって、RSS 2.0と1.0の両方に対応したRSSリーダーを作ることも可能です。

使用したライブラリ

XML解析パーサーとして使用したNunniMCAXとSPI Flash memory(AT45DB161D)ライブラリ以外は、Arduino - Librariesから入手できます。今回のプロジェクト用に変更を行った箇所は以下です:

1) ks0108 GLCD Library
最新のV2をベースに、「Arduinoで漢字表示(4)」で示した漢字表示のコードを追加しています。今回さらに、画面幅を超える長い文字列を表示する際に、自動的に改行を行う機能を追加しました。また、画面の最終行(12ドットフォントの場合、Y座標52~63にかけて)に表示ができない問題を見つけたため修正を盛り込みました。

2) Ethernet Library
WIZ812MJ(W5100搭載モジュール)とAT45DB161D(SPIフラシュメモリー)を同時使用するための変更を実施。

3) String Library
このライブラリも結構メモリーを喰いますが、文字列操作が楽ちんなので使用。メモリーリークバグの修正を盛り込んでいます。

今回使用したライブラリ一式(Arduinoサイトから入手できるライブラリは変更を行ったファイルのみ)を以下に示します。
 「LibraryForWeather11.zip」をダウンロード

余談

ks0108ライブラリの最終行が表示できない問題の解決には結構時間を取られました。GLCD制御レジスタ(ピクセル位置の指定)の操作、書き込むフォントデーターのビット操作が少々複雑で、私の頭の中ではプログラムの動作がイメージし切れませんでした。だいぶ前に、優秀なプログラマーは、部屋数が20位あるお屋敷の総模様替えを頭の中で出来る人、即ちプログラムの実行ステップに従って刻々と変化する変数やハードの状態を頭の中で追える人だとありましたが、その通りだと思いました。

EclipseやVisualStudioではビジュアルなデバック機能に助けられるのですが(上記の状態変化を追いかける仕事の多くを実行・補助してくれる)、Arduinoのデバック環境が貧弱な点も苦戦要素の一つでした(言い訳ですが)。 昔ならもう少し脳内トレースもできたと思うのですが、歳のせいもありそうです。

今回見つけたks0108の問題の修正イメージをArduino Forumにアップしたところ、ライブラリの作者さんから速攻でより美しい修正イメージが提示されました。悩むよりさっさとバグレポートした方が早かったような、、

| | コメント (4)

SPIデバイスの混在とSS信号の初期状態

自作のGLCDシールドにて突然一部の漢字が表示できなくなる事象に何回か遭遇しました。漢字フォントを格納したSPI Flash memory(AT45DB161D)の内容をダンプするとページ単位でall-FFに初期化されています(データーが残っているページもあります)。なにやら、ページ・セクター単位で消去が動いたような状態です。この問題の原因と対策について考えてみました。

問題の発生トリガー

時間の関係で事象を再現させる試験はできていませんが、以下の状況で消去が発生しているように見えます:

  • Ethernet shield互換のWIZ812MJとSPI Flash memory双方を基板に搭載して通電している(以下、ここではWIZ812MJをEthernet shieldと記載)
  • Ethernet shieldのライブラリのみ使用して、Ethernet関連の試験を行った(SPI Flashは初期化を行っていない)
  • SPI Flash memoryのSS(CS)信号入力は、以下の回路図に示す通り、汎用I/OのPL0(PORTL bit0)を使用している

Glcd_shield2

すなわち、MCUに対してPL0の制御レジスタ設定を行っていない(リセット時の初期状態のままにしている)ため、SPI Flashから見るとSS(CS)信号が有効に見えてしまうことを疑っています。想定される問題の発生メカニズムは以下です:

  • Ethernet shieldへのデーター書き込みを行うと、SCK/MOSIが駆動される
  • このとき、SPI FlashのSS(CS)がLowレベルに見えてしまう
  • Ethernet shieldとSPI Flashの両方が選択されてしまい、Ethernet shieldへのコマンドがSPI Flashへのページ・セクタ消去コマンドと誤認識された(こんな偶然の一致があるのだろうかと思いますが)

上記の仮定が成り立つかは、SS信号ピンの初期状態に依存します。

AVRにおけるSS信号ピンの初期状態

データーシートには以下の記載があります:

  1. SPIを有効にすると、SS/MOSI/MISO/SCKの入出力方向をSPIモード用に設定する
  2. MCUをMasterに設定すると、SS信号の入出力方向はユーザーのDDRBレジスタ設定に従う

1項の「SPIを有効にする」とは、SPCRレジスタのSPE(bit6)を1にすることだと思います。リセット時の初期状態ではSPEは0(SPI無効)となります。この場合のSS信号ピンの方向性や出力レベルはどうなるのでしょうか。SS信号を収容するPORTBは汎用I/Oとして機能すると考えられます。汎用I/Oの初期状態は入力ポートでハイインピーダンス状態になります。

以下の試験スケッチを使って、リセット時のSS信号ピン(PB2)電圧を測りました(測定器はテスターを使用)。MCUはATmega328(Arduino nano)を使用。

1) SPCRを設定しない(SPI無効): SS = 0.4V
 → PORTBは入力ポート(Hi-z)として動作

2) SPE=1, MSTR=1(マスター): SS = 0.4V
 → PB2のDDRを設定していない→ 入力として動いていると思われる

3) SPE=1, MSTR=1, DDRB=4, PORTB=4:SS = 4.7V(HIGH)
 → PB2を明示的に、出力・HIGHに設定

上記の1)はSPIライブラリの初期化ルーチンを実行していない状態に相当し、この場合MCUのSSピンに接続されたSPIデバイスはLOW入力(選択状態)と等価になると思われます。冒頭の回路図に示したSPI FlashのSS(CS)入力は汎用I/Oで制御しているため、リセット直後は入力ポート状態となり上記1)と同様になります。このためライブラリの初期化ルーチンを実行していない状態では問題の発生条件が成立すると言えます。

対策

Ethernet shieldに加えてSPI Flash memoryもライブラリ経由で初期化を行えば、SS信号ピンにHighを出力するため問題が発生することはありません。Ethernetのみを使用する場合でも必ずSPI Flash memory(AT45DB161D)の初期化を行うようにすればよのですが、忘れてしまいそうです。

そのため、SS信号を5Vにプルアップしました。Ethernet shiedのみを動作させる試験を少し行った範囲では、Flash memoryの誤消去は発生しておらずうまく動いているようです。ハード・ソフトのどちらの責任で初期設定を行うかを設計仕様として決める必要があることを意味し、なかなか奥が深いです。大規模開発では重要なポイントになります。このプロジェクトは個人レベルの日曜大工なので、行き当たりばったりでやっていますが、、、

| | コメント (2)

ArduinoでXMLを解析する

GLCDで漢字表示ができるようになりましたが、Arduino RSSリーダーを作るためには、RSSサイトから配信されるXMLデーターの解析を行う必要があります。この解析を行うライブラリを自作するか、フリーライブラリを移植するか迷っていたのですが、Arduinoでも動くライブラリが見つかったので試してみました。

XML構文解析を行うパーサー(Parser)の種類

標準ベースの実装としては以下の2つがあります:

1) DOM(Document Object Model)

W3Cが勧告しているXML文書を操作するためのAPI。XML文書全体をメモリーに読み込み、XML文書のツリー構造をメモリー上に展開した上で各要素へのアクセスを行います。データー全体のツリー構造を保持することからランダムに要素の取り出しを行う用途に向いていますが、反面メモリー消費が多いのが欠点です。Arduino/AVRのようにメモリーリソースが限られた組み込み系では使いづらいと思います。

2) SAX(Simple API for XML)

SAXはXML文書全体を読み込まず、データーを読み込みながら逐次処理を行います。SAXでは、開始タグ・終了タグを見つけるとイベントが発生し、call back関数が呼び出されます。このcall back関数に必要な処理を記述することで特定の要素を抽出するなのど操作が可能となります。DOMのようにランダムアクセス用途には向きませんが、メモリー消費が少ないため、組込用途には向いています。SAXの概要はここを参考にしました

当初、XMLパーサーはDOMしかない(もしくは、.Net FramewarkのXmlTextReaderのような独自の実装)と思っていたため、Arduinoで使うことをためらっていましたが、XML解析について調べていくうちにSAXを発見しました。RSSリーダーとしての用途なら、データーの先頭から逐次データーを読みながら1パスで処理することで十分なため、SAXなら使えそうです。

フリーのSAXライブラリ

例によってフリーのライブラリを物色。NunniMCAXというライブラリを見つけました。Arduinoと同じMade In Italyなのですが、読み方分かりません、、

オリジナルのライブラリは、FILE *fileを引数としてパーサーを呼び出す形式になっています。パーサー本体の中では、fgetc(file)を使ってテキストファイルから文字を読み込みながら逐次処理を行っています。この部分だけを変更して、関数ポインタを渡すようにしました。Ethernet shieldから1文字読み込む関数を作ってこの関数のポインタを渡してやることでwebサーバーから取得したXMLデーターの解析ができます。

Arduino用に変更を盛り込んだライブラリソースは以下です:
「NunniMCAX-1.4.1_arduino.zip」をダウンロード

使い方ですが、IDEのライブラリフォルダー配下に「NunniMCAX」というフォルダーを作って解凍したファイルを格納して下さい。

XML文書を解析する過程でイベントが発生すると以下の関数がcall backされますので、ユーザープログラムの中で実体を記述します。

  1. startDocument: XML文書を見つけた
  2. startElement: 要素ノードを見つけた → 開始タグを見つけた
  3. characters: タグに挟まれたテキスト情報を見つけた → 文字単位にイベントが発生するようです
  4. endElement: 要素の終了→ 終了タグを見つけた
  5. endDocument:

例えば、次のXMLデーターを入力した際の発生イベントを示します。
<?xml version="1.0" encoding="utf-8"?>
<Hello>
   <World>こんにちはみなさん!</World>
</Hello>④⑤

丸付き数字が各イベントが発生するポイントになります。③のchractorsイベントは、「こんにちはみなさん!」の各文字単位に発生します。

サンプルスケッチ1

Yahoo JAPANのお天気情報RSS(http://rss.weather.yahoo.co.jp/rss/days/4610.xml)を読み込んで解析を行うサンプルスケッチを以下に示します。

このスケッチでは、①~⑤のイベント毎にシリアルポートに結果の出力を行います。③はcharactorsイベント発生毎に検出した文字をバッファに蓄積し、④のイベント発生時に文字列として出力しています。Attribute(タグの中に書き込まれた属性)情報を取得するAPIもあるのですが、ここでは割愛します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <NunniMCAX.h>
#include <Ethernet.h>

#define MAXLEN 256
static char m_characters[MAXLEN];
struct NunniMCAXContentHandler handler;
bool RootDetected = false;

byte mac[] = { xx, xx, xx, xx, xx, xx };
byte ip[] = { 192, 168, 0, 111 };
byte server[] = { 192, 168, 0, 10 };
Client client(server, 80);

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

  /* set up the paser handler functions */
  handler.startDocument = startDocument;
  handler.startElement = startElement;
  handler.characters = characters;
  handler.endElement = endElement;
  handler.endDocument = endDocument;
}

void loop()
{
  Serial.println("##connecting...");
  if (client.connect()) {
    Serial.println("##connected");
    client.write("GET /ansi.xml HTTP/1.0\r\n\r\n");
  } else {
    Serial.println("##connection failed");
    delay(5000);
    return;
  }

  // パーサーの呼び出し 
  NunniMCAXparse( &getcFunc, &handler );

  Serial.println("##disconnecting.");
  Serial.println("");
  client.stop();
  RootDetected = false;
  delay(5000);
  return;
}

// Ethernetからの1文字読み取るコールバック関数
// 最初の'<'(タグの開始)まで読み飛ばし
//  → HTTPサーバーレスポンス文字を読み飛ばす
int getcFunc()
{
  while(!RootDetected)
  {
    if (getChar() == '<')
    {
      RootDetected = true;
      return '<';
    }
  }

  return getChar();
}

int getChar()
{
  if (client.available()) 
    return client.read();

  if (!client.connected()) 
    return EOF;
}

/* SAXパーサーのイベントハンドラー */
int startDocument(void) 
{
  Serial.println( "startDocument" );
  return 0;
}

int startElement( const char *tagname, struct NunniHashtable *args ) 
{
  const int size = NunniHashtableSize( args );
  char ** keys;
  int i, ret;
  const char *name, *value;
  keys = (char**)calloc( size, sizeof( char * ) );
  Serial.print("start element: ");
  Serial.println(tagname);
  ret = NunniHashtableKeys( args, keys );
  for ( i = 0; i < size; ++i ) {
    name = keys[i];
    value = NunniHashtableGet( args, name );
    Serial.print("  attrName: ");
    Serial.print(name);
    Serial.print("   attrValue: ");
    Serial.println(value);
  }
  memset( m_characters, 0, MAXLEN );
  return 0;
}

int characters( char ch[], int start, int length ) 
{
  int i = strlen( m_characters );
  if ( i == MAXLEN )
    return -1;
  strncat( m_characters, &(ch[start]), length );
  return 0;
}

int endElement( const char *tagname ) 
{
  int len;
  char *data = m_characters;
  while( isspace( *data ) ) {
    ++data;
  }
  len = strlen( data );
  while( isspace( data[--len] ) ) {
    data[len] = 0;
  }
  if ( data != NULL && strncmp( data, "", 1 ) )
  {
    Serial.print("text: ");
    Serial.println(data);
  }
  memset( m_characters, 0, MAXLEN );
  Serial.print("end element: ");
  Serial.println(tagname);

  return 0;
}

int endDocument(void) 
{
  Serial.println( "endDocument" );
  return 0;
}

実行結果は以下の通りです。IDEのSerial Monitorは漢字表示に対応していないため、UTF-8に対応したターミナルソフト(UTF-8 TeraTerm Proなど)に表示させます。

##connecting...
##connected
startDocument
start element: rss
start element: channel
start element: title
text: Yahoo!天気情報 - 東部(横浜)の天気
end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html
end element: link
start element: description
text: Yahoo! JAPANの天気情報に掲載されている最新の情報を提供しています。
end element: description
start element: language
text: ja
end element: language
start element: copyright
text: Copyright (C) 2009 Yahoo Japan Corporation. All Rights Reserved.
end element: copyright
start element: lastBuildDate
text: Mon, 24 Aug 2009 23:55:16 +0900
end element: lastBuildDate
start element: item
start element: title
text: 【 24日(月) 東部(横浜) 】 雨後曇 - 29℃/23℃ - Yahoo!天気情報
end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html?d=20090824
end element: link
start element: description
text: 雨後曇 - 29℃/23℃
end element: description
start element: pubDate
text: Mon, 24 Aug 2009 17:00:00 +0900
end element: pubDate
end element: item
start element: item
start element: title
text: 【 25日(火) 東部(横浜) 】 曇時々晴 - 28℃/22℃ - Yahoo!天気情報

end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html?d=20090825
end element: link
start element: description
text: 曇時々晴 - 28℃/22℃
end element: description
start element: pubDate
text: Mon, 24 Aug 2009 17:00:00 +0900
end element: pubDate
end element: item
~中略~
end element: channel
end element: rss
endDocument
##disconnecting.

上記のように、RSS情報を、要素名(タグ名)やTextに分解して取得できます。

サンプルスケッチ2

サンプルスケッチ1では、サイト情報全体を表示していますが、例えば上記の例で、1番目の"item"要素→"title"要素→text情報を抜き出すと、今日の天気を取得できます。このスケッチを以下に示します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <NunniMCAX.h>
#include <Ethernet.h>

#define MAXLEN 256
static char m_characters[MAXLEN];
struct NunniMCAXContentHandler handler;
bool RootDetected = false;
int ItemCount = 0;
int ItemToGet = 1;

byte mac[] = { XX, XX, XX, XX, XX, XX };
byte ip[] = { 192, 168, 0, 111 };
byte server[] = { 124, 83, 139, 175 };    // rss.weather.yahoo.co.jp
Client client(server, 80);

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

  /* set up the paser handler functions */
  handler.startDocument = startDocument;
  handler.startElement = startElement;
  handler.characters = characters;
  handler.endElement = endElement;
  handler.endDocument = endDocument;
}

void loop()
{
  Serial.println("##connecting...");
  if (client.connect()) {
    Serial.println("##connected");
    client.write("GET /rss/days/4610.xml HTTP/1.0\r\n\r\n");
  } else {
    Serial.println("##connection failed");
    delay(5000);
    return;
  }

  // パーサーの呼び出し
  NunniMCAXparse( &getcFunc, &handler );

  Serial.println("##disconnecting.");
  Serial.println("");
  client.stop();
  for (;;);    // stop
}

// Ethernetからの1文字読み取るコールバック関数
// 最初の'<'(タグの開始)まで読み飛ばす
//  → HTTPサーバーレスポンス文字を読み飛ばす
int getcFunc()
{
  while(!RootDetected)
  {
    if (getChar() == '<')
    {
       RootDetected = true;
       return '<';
    }
  }

  return getChar();
}

int getChar()
{   
  if (client.available())
    return client.read();

  if (!client.connected())
    return EOF;   
}

/* SAXパーサーのイベントハンドラー */
int startDocument(void)
{   
  return 0;
}

int startElement( const char *tagname, struct NunniHashtable *args )
{   
  if (strcmp(tagname, "item") == 0)
    ItemCount++;
  memset( m_characters, 0, MAXLEN );
  return 0;
}

int characters( char ch[], int start, int length )
{
  int i = strlen( m_characters );
  if ( i == MAXLEN )
    return -1;
  strncat( m_characters, &(ch[start]), length );
  return 0;
}

int endElement( const char *tagname )
{
  int len;
  char *data = m_characters;
  while( isspace( *data ) ) {
    ++data;
  }
  len = strlen( data );
  while( isspace( data[--len] ) ) {
    data[len] = 0;
  }

  if ( data != NULL && strncmp( data, "", 1 ) )
    if (ItemCount == ItemToGet && strcmp(tagname, "title")  == 0)
    {
      Serial.println(data);
    }
  memset( m_characters, 0, MAXLEN );

  return 0;
}

int endDocument(void)
{
  return 0;
}

実行結果を以下に示します。

##connecting...
##connected
【 24日(月) 東部(横浜) 】 雨後曇 - 29℃/23℃ - Yahoo!天気情報
##disconnecting.

イベントハンドラー部分を書き替えることで抜き出す情報を制御できることが分かります。

メモリー使用量

気になるサンプルスケッチ2のメモリー使用量は以下です:

  • プログラムサイズ(ROM占有量):20944bytes
  • データーサイズ(SRAM占有量):3021bytes

データーサイズは、「Arduinoメールチェッカー(その3)」に記載した、avr-objdump -hコマンドで調べました。SRAMを3KB程度占有している点が大きく、Arduino Megaでないと実行できないサイズになっています。ATmega 328でも動かせる程度のメモリー量に収めたいところですが、NunniMCAXライブラリーはなかなかよく出来ており捨てがたいです。

| | コメント (0)

Arduino Ethernet Shieldと他SPIデバイスの混在

GLCDシールドの制作にてArduino上でSPIデバイスの混在を行う場合の考慮点を記載しましたが、Official Ethernet Shieldではさらに制約があることが分かりました。

MISO信号のハイインピーダンス制御

複数のSPIデバイスを使用する場合MISO信号をバス状に配線しますが、MCUと通信できるデバイスは常に1つのみです。そのため、選択されていないデバイスはMISOをハイインピーダンス状態にして、バスから電気的に切り離した状態にする必要があります。

GLCDシールドの制作にて使用したFlash Memory(AT45DB161D)はSS信号をInactiveにするとMISOをハイインピーダンス状態にする仕様になっています。普通こういう設計だろと思うのですが、W5100(Ethernet ShieldのTCP/IP処理チップ)は動作が異なると以下のArduino Forumスレッドに記載がありました:
  Arduino + Ethernet Shield + Another SPI device

W5100チップのMISOハイインピーダンス制御

Forumのスレッドによると、W5100 チップにてMISOをハイインピーダンスにするためには、SS(W5100ではSCS)を制御するのではなく、SEN(SPI enable)信号をLowレベルに落としてやる必要があるとのことです(データーシートには記載がなし)。Official Ethernet ShieldではSENをプルアップしているのみのため、SENをLowに落としてMISOをハイインピーダンスにすることができません。すなわち、このままでは他のSPIデバイスと混在して使用することができません。

対策としては、上記のスレッドに記載があったのですが、「PROG」とシルク印刷されたジャンパーピン用のスルーホールを使うことです。このピンはSENにつながっているため、ここにMCUのI/OピンをつないでEthernet Shieldを使用しない期間はSENをLowレベルに落します。やり方としては、以下の2つが考えられます:

  1. SSはActive Low, SENはActive Highと論理が逆になっています。そのため、Ethernet Shieldに入力するSSを反転してSENに入力する。この場合、インバーター回路を追加する必要があり、Ethernet Shieldに部品や配線を追加することになるため難しいです
  2. もう一つのSPIデバイス用のSS信号をSENに接続すると、もう一つのSPIデバイスを選択(SS = Low)した際にSENがLowに落ちます。この方法だと、PROGピンに配線を追加するだけですので1)より簡単です。ただし、1)に比べてSENをLowに落とすタイミングが遅れると思われ、タイミング的に問題がないかの検証が必要と思われます。

GLCDシールドの制作ではWIZ812MJとAT45DB161DをSPIバス上で混在動作させることができました。WIZ812MJはEthernet Shieldと互換性がありますが、SS信号をモ ジュール内で反転してSENに入力しているため、1)相当の動作をしていることが今回分かりました。GLCDシールドの製作ではこの点はまったく意識していなかったのですが、結果オーライでした。

NKC ElectronicsさんのEthernt ShieldはWIZ812MJを使用しているため、他のSPIデバイスとの混在が前提の場合、こちらの製品を使用するという選択肢もあると思います(この製品はキットのため部品の半田付けが必要です)。

2009/8/17追記:
hamayanさんからW5100 MISO/SEN信号の動作は、SPI Guide for W3150A+ and W5100SPI application note for W5100 and W3150A+に記載がある点をご指摘いただきました。データーシートだけでなく、関連ドキュメントにも目を通さないといかんですね。

| | コメント (2)

Arduino 0017

Arduino 0017が公開されました。色々改善されています。使ってみて気がついたことを記載します。

改善点

1. LiquidCrystal libraryのアップデート

LiquidCrystal(LCD)ライブラリの初期化コード」と同等の初期化コードが追加されました。電源ON時にLCD表示が出ない問題が解消しています。カーソル表示のON/OFF、左右のスクロールなど機能が追加されました。

2. スケッチ毎にライブラリをコンパイル・リンクするようになった

Board Typeを変更した際の「ArduinoライブラリのMCUタイプ依存性」が解消しました。また、ライブラリのコードをいじった際に、ライブラリフォルダー内の.oファイルをいちいち削除する必要がなくなりました。

3. 操作性の改善

  • 複数行を指定してタブ位置の左右移動ができるようになった
  • 複数行の一括コメントアウト・コメント削除
  • File Menuからのスケッチオープンは新規ウインドウが開く(マルチウインドウ対応)
  • シリアル・モニターが別ウインドウになり視認性が向上

4. ユーザーライブラリフォルダーの新設

スケッチフォルダー配下に「libraries」フォルダを作成し、web等で公開されているサードパティーライブラリを格納できるようになりました。例えば;
 スケッチフォルダー\libraries\ks0108GLCD
といった感じです。これまでサードパティーライブラリは、オフィシャルライブラリと同じ「IDEインストールフォルダ\hardware\libraries」に格納する必要がありました。私はIDEを更新した際、旧環境を残すためにIDEインストールフォルダ名前を変えており、サードパティーライブラリをコピーする必要があったのですが、この作業が不要になります。

注意点

1. LiquidCrystal libraryのデフォルト行数が1行表示になりました

Arduino 0016以前はLCDの初期化の中で2行モードに設定していました。以下に0016のモード設定コードを示します。
  command(0x28);  // function set: 4 bits, 1 line, 5x8 dots
 →コメントは1 lineとありますが、bit 3を1にしているため2 lineを指定しています
  command(0x0C);  // display control: turn display on, cursor off, no blinking
  command(0x06);  // entry mode set: increment automatically, display shift, right shift

Arduino 0017に移行後2行表示を行うためには、setupの中でlcd.begin(16, 2)を指定する必要があります。beginメソッドの第2引数が行数の指定です。そのため、既存スケッチの変更が必要になります。以下のようにライブラリのコードを書き替えれば既存スケッチの変更は不要になると思います(試してはいません)。

void LiquidCrystal::init(uint8_t fourbitmode, uint8_t rs, uint8_t rw, uint8_t enable,
             uint8_t d0, uint8_t d1, uint8_t d2, uint8_t d3,
             uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7)
{
  _rs_pin = rs;
  _rw_pin = rw;
  _enable_pin = enable;
 
  _data_pins[0] = d0;
  _data_pins[1] = d1;
  _data_pins[2] = d2;
  _data_pins[3] = d3;
  _data_pins[4] = d4;
  _data_pins[5] = d5;
  _data_pins[6] = d6;
  _data_pins[7] = d7;

  pinMode(_rs_pin, OUTPUT);
  // we can save 1 pin by not using RW. Indicate by passing -1 instead of pin#
  if (_rw_pin != -1) {
    pinMode(_rw_pin, OUTPUT);
  }
  pinMode(_enable_pin, OUTPUT);
 
  if (fourbitmode)
    _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
  else
    _displayfunction = LCD_8BITMODE | LCD_1LINE | LCD_5x8DOTS;
 
  begin(16, 1); // → begin(16, 2)に変更
}

問題点

1. IDE起動時に前回終了時のウインドウ位置・サイズ・スケッチが復元されない

Arduino 0016以前のバージョンでは、IDE起動時に、前回終了時のWindow位置・サイズ・使用していたスケッチを復元してくれたのですが、Arduino 0017ではこの機能がうまく動きません。

Arduino 0017では、Processing 1.0.3のコードを取り込んだとありますが、Processingのバグのようです。(確かにProcessingのIDEでも同様の現象になります)。

私はいつもWindow右上の×アイコンをクリックしてIDEを終了しますが、この場合、必ず問題が発生します。回避策としては、File Menu→Quitもしくは Ctrl-QでIDEを終了すると、前回の状態を復元してくれます。ただ、File Menu→Quitで終了した場合でも何かの拍子にデフォルト状態で起動してしまうことがあります。

2. タスクバー・タイトルバーのアイコンがJAVAになる

Arduino 0016以前はArduinoのアイコンが表示されていましたが、Arduino 0017ではJAVAアプリのデフォルトアイコンになってしまいました。私はWindows Vista環境で使用していますが、Arduino Forumの投稿を見るとXP環境でも同様の問題があるようです。

その他

Windows用のIDEにはJava VMが添付されていますが、SunのJava VMをインストールしている場合、IDE添付のVMを削除可能です。

2009/8/17追記:
問題点2を追加。

2009/8/18追記:
注意点の記載を変更(Arduino 0016では、LCDへの初期値として2行表示を指定していることに気がつきました)。

| | コメント (0)

Arduino Nano 3.0 Get

Arduino 3.0 Nanoを買ってしまいました。手持ちのArduinoファミリーと並べると小ささがよく分かります。

Arduinonano

左から順にNano, Duemilanove, Mega, REDUINO-SP328(マイクロファンさん)です。

発売前にスイッチサイエンスさんにて予約の案内が出ていたのですが、私がwebを見た際は「売り切れ」中になっていました。ひょっとすると初回ロットの割当が少なく、次の入荷まで時間がかかるのでは思い、本家のGarvitechさんのWebを見ると先行予約(割引あり)かつ発売日に発送可能でした。そのため、購入はGarvitechさんから直接行いました。

価格は先行予約価格で$29.74+送料$6.65でした。発売日が7/27に対して、7/30に発送のメールが届き、商品が届いたのが8/8でした。発送はUSPS(アメリカ合衆国郵便公社)郵便小包でしたので、配送に一週間程度かかるのは致し方ないと思います。ちなみに、封筒に切手相当のシールが貼ってあり郵便料金が$1.44の表示がありました。う~ん、$5.21がGarvitechさんの手数料なのですね。

私がGarvitechさんに予約を行った直後(発売前)にスイッチサイエンスさんでも予約ができるようになり、8/3に発送したとありましたので、結果的にはスイッチサイエンスさんに注文した方が少し早く届いたと思います。

NanoはAnalog入力(ADC)が8chと、Duemilanoveの6chに対して2ch多い仕様になっています。DuemilanoveのAnalog 入力はDigital I/Oとしても使用できるため、NanoではAnalog入力8ch全てをDigital I/Oとして使用できると期待してNano購入に走ったのですが、この点はハズレでした。事前にATMega328のデーターシートを見れば分かったことですが、ADC0~ADC5の6chはPort CのDigital I/Oとしても使用できますが、Nanoのプラス2chはADC専用でした。

Nano 3.0を使用する場合、IDE-0016ではBoard typeに「Duemilanove w/ ATmega328」を指定します。IDE-0017では「Arduino Duemilanove or Nano w/ ATmega328」を指定します。IDE-0016でArduino Nanoを指定するとスケッチのアップロードができません(Arduino NanoはATmega168のため)。Duemilanove w/ ATmega328の指定で、プラス2chのADCは問題なく使用できました。

Duemilanove/NanoはDigital I/Oが14 + 6 pinありますが(Analog入力をDigital I/Oとして使用した場合)、次に示すように汎用I/Oポートの8bitを連続して使えない点が不満です。Port Dはdigital pin 0~7として8bit全てが配線されていますが、0, 1 pinはシリアル通信用のため実質的にはdigital pin 2~7の6 pinしか使えません。Port Bは水晶発振子をつなぐために2 pinつぶれており、digital pin 8~13の6 pinしか使えません。

一方Arduino MegaはDitigal I/Oが54pinもあるのですが、こんなにあっても使い切れないです。個人的な理想は以下のPin構成です;

  • Digital 16pin(汎用I/Oポート2本の8 bitを分断なしで割り当てる)
  • 別のpin 8本に、シリアル2ch(PCとの通信用に加えて、もう1chハードウェアシリアルがあると便利)、SPI/I2Cの通信関係pin
  • Analog入力8ch

Sanguinoがこの構成に近いので、Pin数を要するスケッチならSanguinoが使いやすそうに思います。どこかでコレクションが増えるか、、、

| | コメント (0)

SPIの基本動作とArduinoでの使い方

漢字表示とGLCDシールド作成でSPIをお勉強しました。SPIの動作やArduinoでの基本的な使い方について分かったことを記載します。

SPIのブロック構成

SPIはボード内のデバイス間通信規格です。2つのデバイスがSPIを使って通信する場合、通信を主導する側がMaster、受動的に動作する側がSlaveとなります。余談ですが、私が以前関わったシステム開発ではMaster(主人)・Slave(奴隷)という用語は差別的な響きがあるためよろしくないと言われて、Main・Subordinary(主・従)という用語を使ったことがありました。

図1にSPIの機能ブロックとMaster~ Slave間の接続構成を示します。Arduinoの場合、AVRがMasterになります。

Spi_block_diagram

図1に示す通り、Master・SlaveデバイスのShift Registerをぐるっとループ接続した形になっており、MasterのShift Registerに設定したデーターがSPI Clockに同期してShift outされSlave側のShift Registerに書き込まれます。

AVR~Slaveデバイス間のデーター送受信

AVRにはSPI専用のShift RegisterとしてSPDR(SPI Data Register)と呼ばれる8bit長のレジスターが存在します。AVRがMasterになる場合の動作概要は以下のとおりです:

  1. SPDRに送信データーを書き込む
  2. SPIクロックがスタートし、データー転送が始まる
  3. 8bitのデーター転送が終了するとSPIクロックが停止し、SPSR(SPI Status Register)のSPIFフラグをセットする

上記のように、SPIは1byteをサイクルとしたデーター転送になります。複数バイトのデーター転送を行う場合は、SPIFフラグセットを確認した後に後続のbyteをSPDRに書き込みます。

SPIには、以下の組み合わせによって、4つのモード(Mode 0~3)があります。

  • クロック極性(CLPOL):クロック停止(アイドル)状態状態にてクロック信号がLow/Highのどちらを取るか
  • クロック位相(CPHA):入出力データーの取り込み・送出をクロックの立ち上がり・立ち下がりのどちらで行うか

Ethernet ShieldやFlash Memoryライブラリで使用しているmode 0は以下の動作となります(SPCRレジスタを以下に設定):

  • CPOL = 0:アイドル時SCKをLowレベルに保持(正パルスのクロックを生成)
  • CPHA = 0:クロックの立ち上がりでMISO(入力)をサンプリング、クロックの立ち下がりでMOSI(出力)を次のデーターに切り替える

データー送信のビット・オーダーはMSB First or LSB FirstをSPCR(SPI Control Register)の設定で指定することができます。ライブラリではMSB Firstになっています。

図2・図3にMasver(AVR) - Slaveデバイス間のデーター送受信イメージを示します。
Spi_datatransferppt

1) Master(AVR)→ Slaveへの送信(図2)
 ①SPDRにデーターをセット
 ②SPIクロックがスタートしデーターを送信(例ではMSB First)
 ③同時にSlave側のShift Registerから送られたデーターを受信する

2) Slave→ Master(AVR)への送信 (図3)
SPIはMaster主導で通信を開始するため、MasterからSlaveに対してSPI Clockを送る必要があります。そのため以下のシーケンスになります;
 ①AVRのSPDRにダミーデーターを設定する(値は任意)
 ②SPI Clockがスタートし、Slaveがデーターを送信する
 ③Slaveから受信したデーターがSPDRに格納される

上記以外に、AVR側のプログラムにてSS(Slave Select)信号を制御する必要があります(ハード自律制御機能はありません)。即ち、データー転送サイクル開始前にSS信号をLowに落として、転送終了後にHighに戻す必要があります。

実際のSPI制御イメージ

GLCDシールドの制作で使用したSPI Flash Memory(AT45DB161Dライブラリ)を例に、SPIの制御イメージを示します。

Flash MemoryからManufacturer・Device IDを読み出してみます。Arduinoのスケッチは以下となります。

#include <at45db161d.h>
ATD45DB161D dataflash;

void setup()
{   
  /* Set baud rate for serial communication */
  Serial.begin(115200);

  uint8_t  status;
  ATD45DB161D::ID id;      // ID情報を保持する構造体
 
  /* Let's wait 1 second, allowing use to press the serial monitor button :p */
  delay(1000);
 
  /* Initialize dataflash */
  dataflash.Init();
  Serial.println("\nDataflash Init");
      
  delay(10);
 
  /* Read status register */
  status = dataflash.ReadStatusRegister();
  Serial.println("Status Register read");

  /* Display status register */
  Serial.print("Status register :");
  Serial.print(status, BIN);
  Serial.print('\n');
 
  /* Read manufacturer and device ID */
  dataflash.ReadManufacturerAndDeviceID(&id);

  /* Read manufacturer and device ID */
  Serial.print("Manufacturer ID :");  // Should be 00011111 (1FH)
  Serial.print(id.manufacturer, HEX);
  Serial.print('\n');

  Serial.print("Device ID (part 1) :"); // Should be 00100110 (26H)
  Serial.print(id.device[0], HEX);
  Serial.print('\n');

  Serial.print("Device ID (part 2) :"); // Should be 00000000
  Serial.print(id.device[1], HEX);
  Serial.print('\n');

  Serial.print("Extended Device Information String Length  :"); // 00000000
  Serial.print(id.extendedInfoLength, HEX);
  Serial.print('\n');

  dataflash.EndAndWait();                //  終了処理(SSをLowに落とす)
}

void loop()
{
}

dataflash.ReadManufacturerAndDeviceID(&id);にてManufacturer・Device IDを読み出します。ReadManufacturerAndDeviceIDメソッドはライブラリ上では、以下のコーディングになっています。

void ATD45DB161D::ReadManufacturerAndDeviceID(struct ATD45DB161D::ID *id)
{
        DF_CS_inactive;    /* Make sure to toggle CS signal in order */
        DF_CS_active;      /* to reset Dataflash command decoder     */
 
       /* Send command */
       spi_transfer(AT45DB161D_READ_MANUFACTURER_AND_DEVICE_ID);

        /* Read Manufacturer ID */
        id->manufacturer = spi_transfer(0x00);
        /* Read Device ID (part 1) */
        id->device[0] = spi_transfer(0x00);
        /* Read Device ID (part 2) */
        id->device[1] = spi_transfer(0x00);
        /* Read Extended Device Information String Length */
        id->extendedInfoLength = spi_transfer(0x00);
}

spi_transfer()関数を呼び出すことによって、ID読み出しのコマンド(0x9F)をSlave(Flash Memory)に送信します。

コマンドを送信した後で、ID情報として4byteを受信する必要があるのですが、図3に示したシーケンスを起こすために、ダミーの0x00を引数としたspi_transfer()の呼び出しを4回行っています。この際の戻り値が受信データーとなります。

spi_transfer関数の定義は以下となっています;

inline uint8_t spi_transfer(uint8_t data)
{
        SPDR = data;
        while(!(SPSR & (1 << SPIF))) ;
        return SPDR;
}

SPDR(SPI Data Register)に送信dataを設定することでデーター転送を起動し、SPSR(SPI Status Register)にSPIFフラグが立ったら受信データー(SPDRの格納値)を返しています。

SPIの特長として、ID読み出しコマンド送信のようにSlaveデバイスからの受信データーを期待しないケースでも、何らかの情報がSlaveから返ってきます。この値がどうなっているのか興味があったため、spi_transfer関数に以下のprint文を入れて実行してみました。

inline uint8_t spi_transfer(uint8_t data)
{
        SPDR = data;
        Serial.print("Send data="); Serial.println(data, HEX);
        while(!(SPSR & (1 << SPIF))) ;
        Serial.print("SPDRrx="); Serial.println(SPDR, HEX);
        return SPDR;
}

<Manufacturer・Device ID読み出しスケッチの実行結果>
(青字がデバック用のprint文による出力です)

Dataflash Init
Send data=D7
SPDRrx=FF
Send data=0
SPDRrx=AC

Status Register read
Status register :10101100

Send data=9F  → ID読み出しコマンド
SPDRrx=FF   → Flash MemoryからはFFが返っている
Send data=0  → ID取得のためのダミーデーター送信
SPDRrx=1F   → Flash Memoryが送信したID情報
Send data=0
SPDRrx=26
Send data=0
SPDRrx=0
Send data=0
SPDRrx=0

Manufacturer ID :1F
Device ID (part 1) :26
Device ID (part 2) :0
Extended Device Information String Length  :0

Send data=D7  → Status要求コマンド
SPDRrx=FF
Send data=0   → Status取得のためのダミーデーター送信
SPDRrx=AC    → 取得したStatus値

上記の結果に示すように、コマンド送信時にFlash Memoryから戻ってくる値は0xFFでした。Flash Memoryにて、コマンド受信開始時点でのShift Register値が送られると思いますが、送ったコマンドがそのまま返ってくるということはshift registerの構造的にないということです。

SPIを使ったブロック転送

I2Cを使用したEEPROMでは、ライブラリを使用して指定したアドレスから連続したデーターをブロック転送することができました。SPIを使用したFlash Memoryでも同様にブロック転送が可能です。AT45DB161Dライブラリにはブロック転送のメソッドがないため、コマンドを発行した後でspi_transfer()を連続して呼び出すことによってブロック転送を実現します。例として、漢字表示のライブラリでAT45DB161Dから1文字分のフォントデーターをブロック読み出しするためのコードを示します。

    dataflash.ContinuousArrayRead(dfAddress[0], dfAddress[1], 1);  // コマンド発行
    for (i = 0; i < dataLength; i++)        // ブロック転送
        fontBuf[i] = spi_transfer(0xff);
    dataflash.EndAndWait();                  // 終了処理(SSをLowに落とす)

Flash Memoryのアドレスは、ContinuousArrayRead()メソッドにて転送開始アドレスを指定し、後続データー取得時はFlash Memory側でアドレスを自動インクリメントしてくれるため、オーバーヘッドは最小限です。

一方Ethernetライブラリでは、W5100(TCP/IP処理チップ)からのデーター読み出しは以下となっています。(読みやすさのために、ダイレクトバス接続のコードを削除し、SPI接続のコードのみを抜粋)

/**
@brief    This function reads into W5100 memory(Buffer)
*/
uint16 wiz_read_buf(uint16 addr, uint8* buf, uint16 len)
{
    uint16 idx = 0;
     IINCHIP_ISR_DISABLE();

    IINCHIP_SpiInit();
   
 for (idx=0; idx<len; idx++)
        {
        IINCHIP_CSoff();                             // CS=0, SPI start

        IINCHIP_SpiSendData(0x0F);        // W5100へのReadコマンド発行
        IINCHIP_SpiSendData(((addr+idx) & 0xFF00) >> 8);   // 読み出しアドレス送信
        IINCHIP_SpiSendData((addr+idx) & 0x00FF);


        IINCHIP_SpiSendData(0);             // ダミーデーターの送信
        buf[idx] = IINCHIP_SpiRecvData();

        IINCHIP_CSon();                             // CS=1, SPI end       
       }

       IINCHIP_ISR_ENABLE();
    return len;
}

なんと、forループの内側で毎回読み出しアドレス 2byteを指定しています(リストの青字部分)。W5100のデーターシートを見ると、Readコマンド(0x0F)+アドレス2 byte+データー1byteの32bitが、1byteのデーターをW5100から読み出すための基本サイクルだと書いてあります。W5100では、SPI接続でのブロック転送モードという概念はないということになります。ArduinoのEthernet接続でスループットを要求することはないでしょうからまぁ許せるのですがオーバーヘッドがでかくてもったいないです。W5100では、8bit data busと15bit address busを使用したMCUとのダイレクト接続モードがあるため、高速処理を行いたい場合はこちらを使えということなのだと思います(address busを2bit接続に縮退したIndirect Busという接続構成もあります)。

SS信号(コードではCS)のLow/High制御もループの内側で1byte受信毎に行っているためオーバーヘッドが多いように思います。他のSPIデバイスと混在した際に、W5100からパケットデーターの途中まで受信したところで処理を中断して、もう一方のSPIデバイスから優先的に受信を行いたいようなケースを加味してこのようなコードになっていると思われます。(他の割り込み処理を考えなければ、関数の出入り口で一回だけSSのON/OFFを行えばよいです)。

AT45DB161DはSPIインタフェース専用のため、ブロック転送の考慮がされています。AT45DB161Dのデーターシートを見ると、SPIクロックは最大66MHz(High Frequency Mode)まで許容しており結構な高速動作が可能です。また、最大SPIクロックが33MHzのLow Frequency Modeが存在します。High Frequency Modeでは、address指定の後にダミーの1byteを追加転送する必要があります(Flash Memoryがデーターを準備する時間を確保するためのwait代わりなのだと思います)。AVRと接続する場合、AVRのクロック÷2が最大SPIクロック周波数のため(SPIの倍速化を参照)、余計なダミーbyte送信が不要となるLow Frequency Modeが最適です。

SPI接続のFlash MemoryはPCのBIOS格納用に使用されているらしく、そのため高クロック化が進んでいるようです。PC起動時には真っ先にBIOSが動いて、POSTや各デバイスの初期化を行います。BIOSが起動するためにはSPI FlashからDRAMにBIOSイメージを展開してBIOSの開始アドレスにJumpする必要がありますが、誰がこの処理を行っているのか(ひょっとしてチップセット?)など、新たな疑問がわいてきます。

その他

SPIのクロック周波数を調べていくうち、I2Cのクロックはどうなっているのか興味が出てきたのでArduinoのライブラリを調べてみました。答えは、100KHzです(以下に示すwireライブラリのコードより)。

  #define CPU_FREQ 16000000L
  #define TWI_FREQ 100000L

// initialize twi prescaler and bit rate
  cbi(TWSR, TWPS0);        // TWI(I2C)のクロック分周比を1:1にセット
  cbi(TWSR, TWPS1);
  TWBR = ((CPU_FREQ / TWI_FREQ) - 16) / 2;   // TWI_FREQにするための調整値をセット

  /* twi bit rate formula from atmega128 manual pg 204
  SCL Frequency = CPU Clock Frequency / (16 + (2 * TWBR))
  note: TWBR should be 10 or higher for master mode
  It is 72 for a 16mhz Wiring board with 100kHz TWI */

以前に、漢字フォントの格納先として「I2C/SPI Memoryのどちらが早いか?」と書いた事があったのですが、圧倒的にSPIが早いです。

余談

漢字表示が一段落したため、次のRSSリーダーをどうするか考えているのですが、今ひとつ進んでいません。XMLの構文解析が重そうです。

RSSなら構文(タグの名称など)はある程度決め打ちにできるため、超簡単なタグ解析処理を自分で作るか、TinyXMLのようなXML Parserを移植するのがよいか(そこそこプログラム規模があるXML ParserがArduinoに乗るか)を考えているのですが、自分で作るのも大変そう(作っても中途半端なものしか出来ない)、一方で出来合いのライブラリを持ってくるのもおもしろくなく、今ひとつモチベーションが上がらない状態です。

| | コメント (0)

SPIの倍速化

今日は小ネタです。

Ethernet shieldで使用しているSPIインターフェースのクロック設定ですが、標準ライブラリではシステムクロックの1/4(4MHz)で動作しています。AVRのデーターシートを眺めていたところ、SPI2X(Double SPI Speed Bit)と呼ばれる倍速設定があることが分かりました。

標準ライブラリではSPIクロック設定として、SPCRレジスタのbit1(SPR1)とbit0(SPR0)を0に設定しています。この設定でシステムクロックの1/4で動作します。加えて、SPSRレジスタのbit0(SPI2X bit)を1にすることで、SPIクロックをシステムクロックの1/2(8MHz)にすることができます。具体的には、hardware\libraries\Ethernet\utility\spi.hに以下(青字)の定義を追加します。

// PB4(MISO), PB3(MOSI), PB5(SCK), PB2(/SS)         // CS=1, waiting for SPI start
// Set DDR -> SS:output, SCLK: output, MOSI: output. Others: input (default)
// Set PORTB -> SS_HIGH
// Set PORTB -> Input pin pull-up (MISO)
// SPI mode 0, Master, MBS First, 8MHz (SPI2X Enable)
#define SPI0_Init()          DDRB  |= SPI0_SS_BIT|SPI0_SCLK_BIT|SPI0_MOSI_BIT;\
                                        PORTB |= SPI0_SS_BIT; PORTB &= ~(SPI0_SCLK_BIT|SPI0_MOSI_BIT);\
                                        SPCR  = 0x50;\
                                        SPSR  = 0x01

上記の倍速モードで、GLCDシールドメールチェッカーが動作しています。通信速度が倍になっているかの測定は出来ていませんので、効果の程は未検証ですが、、

| | コメント (0)

Arduinoで漢字表示(4)

漢字表示プロジェクトの最新状況です。やっと、UTF-8でエンコードした文字列を表示できるようになりました。

Unicode版のフォントデーター

このプロジェクトの最終目的はRSSリーダーを作ることです。そのため;

efontさんのUnicodeフォント全部を収録するとデーター量が多くなるため、日本語表示に必要なブロックを抜き取って使用しました。以下のフォント情報を収録しています。

Codespace_4

JISだと、全角となる記号類(☆○など)や、ギリシャ文字(αβなど)・キリル文字がこのフォントでは半角となります。GLCDの小さいスクリーンに表示するという点では、横幅が少ない半角がよいですが、判読性という点ではやや劣ります。ただし、ロシア語Webサイトではキリル文字は全角でなく半角相当で表示されるので、本来は半角相当のフォントデザインが正解なのだと思いますが、6ドット幅だとちょっときついようです。

フォントのデーター量はData Flashへの格納サイズで573KBとなり、かなり大きいです。JIS第一・二水準文字ならこの半分程度で格納できますので、日本語表示のみであればデーター量的にはJISの方が有利です。ただし、以前にも書きましたがJISコード並びでフォントROMを作るとUnicodeからJISコードに変換するための巨大なテーブルが別途必要になります(この文字コード表を見ても分かるように、UnicodeとJISのコード体系は機械的に変換できず、文字毎の対応表をメモリ上に作る必要があると思います)。対応表はアクセス速度を考えると、シリアル接続の外部メモリーでなくMCU内蔵のFlashメモリーに格納する必要があります。MCU内蔵Flash容量の制約を考えると、データー量が増大してもコード変換が不要なUnicode形式がベターと考えています。

Unicodeの概要は以下のリンクが参考になります:

Unicode - Wikipedia
UCSとUTF
文字コード、標準化について

今回作成したArduino形式のフォントデーターを以下に示します:
 「glcdfont_12_ucs2.zip」をダウンロード

Arduinoへのフォントデーターの転送

PCからシリアル回線経由でフォントデーターを転送し、ArduinoのData Flash Memoryに書き込む、以下のツールを作っています。

  • UploadUcs2.exe(PC側のアップローダー)
  • FontWriteDF.pde(Arduino側のデーター書き込みスケッチ)

UpladUcs2.exeの実行には.Net Framework 2.0以上が必要です。送信を始めてしまうとマウスイベントを受け付けなくなるため、中断するためにはタスクマネージャーから強制終了する必要があります。ArduinoからのレスポンスをSerialPort.ReadChar メソッド で監視しているのですが、このメソッドがブロッキングタイプで他のイベントを受け付けなくなるようです。Arduinoからの受信処理を別スレッドに分ける必要があると思いますが、ここは手抜きをしています。

上記のファイルを以下に示します:
 「UploadTools.zip」をダウンロード

ks0108 GLCDライブラリの機能追加

漢字を表示するために、以下のメソッドを追加しました:

  • PutUChar(unsigned int c): 引数としてUCS2コードを指定して対応する文字を表示する
  • Puts_U(char* str): UTF-8でエンコードした文字列を表示する。文字列の最後はnull (0)で終わっていること。この関数内でUTF-8をUCS2に変換し、一文字毎にPutUChar()を呼び出します

上記の追加を行ったソースを以下に示します:
 「ks0108_unicode.zip」をダウンロード

1byteの英数字もPuts_U()メソッドで表示が可能です。フォントはData Flashに書き込んだ半角フォントを使用します。SelectFont()関数によるフォントの指定は不要です。

ライブラリの使用方法

簡単な表示を行うスケッチを以下に示します:

#include <at45db161d.h>
#include <ks0108.h>

ATD45DB161D dataflash;      // SPI Data Flash Memory libraryのインスタンス宣言

void setup()
{
  delay(1000);
  Serial.begin(115200);
  dataflash.Init();                   // Data Flash libraryの初期化
  GLCD.Init(NON_INVERTED);  // GLCD libraryの初期化

  GLCD.ClearScreen();
  GLCD.GotoXY(0, 0);
  GLCD.Puts_U("Arduinoを始めよう\n");
  GLCD.Puts_U("123123ABCABC\n");
  GLCD.Puts_U("±×αβΣΩ☆△$¥@\n");
  GLCD.Puts_U("①②③ⅠⅡⅢ㍉㌔㌢㊤㊥");
}

void  loop()
{
}

ATD45DB161D dataflashの宣言は、ks0108ライブラリの中でもData Flashからフォントデーターを読み出すために使用しているため、dataflashのインスタンス名は変えないで下さい。インスタンス名が固定ですのでどこかのヘッダファイルに入れてしまうのが間違いを防ぐ意味でよいと思うのですが、ライブラリ間で参照するインスタンスをどこで宣言するのがよいかよく分からず、スケッチ本体で宣言しています。

自分のVista環境の場合、Puts_U()関数の引数に文字列リテラルを指定するとUTF-8でエンコードされ、正しく表示ができました。ちなみに、IDEのシリアルモニターはShit-JISで送信するようです。文字列リテラルに改行コード(\n)を入れると改行することができます。

スケッチを実行した際の表示を以下に示します。

Utf8_kanji

一部の記号類が半角になっていることが分かります。ローマ数字(4行目の丸付き数字の次)は殆ど判読できないです、、

漢字を表示するための最小限の機能は作ることができましたが、画面幅を超えた場合の折り返し処理やスクロールなど欲しい機能は他にもあります。ここから先はRSSリーダーアプリを作る中で少しずつ解決していきたいと思います。

| | コメント (0)

GLCDシールドの制作

これまでブレッドボードに載せていたGLCDをユニバーサル基板を使ってArduinoのシールド調にしてみました。

構成

以下の部品を載せるために、ちょっと大きめですがサンハヤト ICB-97GHD(95x135mm)を使用しました。GLCDは8-bitパラレルバスで必要なピン数が多いため、Arduino Megaを使用しています。

  • GLCD(SG12864)
  • SPI Data Flash Memory(AT45DB161D:漢字フォントの格納用)
  • Ethernet Module(WIZ812MJ:純正Ethrenet shieldと同じW5100 chipを使用)
  • 3.3V3端子レギュレータ(TA48033S
  • 3.3V用リセットIC(TCM809R

Arduino純正のEthernet Shieldでよく指摘されていますが(Under Power 研究所さんの参考記事)、ArduinoのRESETピンは5VにプルアップしてあるだけでEthernet Shieldで使用しているW5100 TCP/IP処理チップのパワーオンリセット(2μs以上Lowレベルに落とす必要がある)に使えません。そのため、リセットICを使用して3.3V系のパワーオンリセットリセットパルスを作っています。

回路図を以下に示します。

Glcd_shield2

外観の写真はこんな感じです(漢字表示を行っていますが、こちらは別途続編を投稿します)。W5100チップと3端子レギュレータは熱対策としてヒートシンクを貼り付けています(W5100はそれほど気にする必要はないのでしょうが気休めです)。

Glcd_shield1

裏面の写真です。ピンヘッダを使って、Arduinoを裏側から接続します。

Glcd_shield2

工作テクの限界で配線が汚いです。リセットIC(米粒大)の半田付けは結構苦戦。半田の熱でチップのパッケージが溶けそうになりました、、本当に動いているのか怪しいところがありますが、WIZ812MJが正しく起動しているので多分大丈夫でしょう。

AT45DB161D Data Flashの接続

前回の記事「Arduinoで漢字表示(3)」からの変更点を以下に記載します。WIZ812MJとSPI接続を共有するための変更です。

1)SPIのSlave Select(SS)ピンをPL0に移動しました。ATMega1280のSSピン(PB0)はWIZ812MJ(Ethernet Module)で使用します。SS信号はソフト制御のためどのピンに移動してもよいと思ったのですが、単にピンアサインをPL0に変更しただけでは動いてくれませんでした。Arduino Forumを漁ってみると、オリジナルのPB0をHighに設定してからSPIレジスタ(SPCR)の設定をせよとあります。確かにこの方法で動作します。

2)オリジナルのat45db161dライブラリでは、SPI mode 3を使っています。Ethernetライブラリにあわせてmode 0に変更しました。(WIZ812MJはEthernetライブラリがそのまま使えます)

3)オリジナルのat45db161dライブラリでは、各関数の入り口でSS信号をHigh→Low(active)にトグルしてデバイスをたたいた後、SSをLowのままで放置しています。他のSPIデバイスと共存するためには、アクセス終了後SS信号をHigh(inactive)にする必要があります。

at45db161dライブラリでは、ContinuousArrayRead()関数などでデバイスにコマンドを送った後、引き続きspi_transfer()を呼び出すことでデーターの転送を行うため、この一連のアクセスの中でデバイスをactive状態に保持する必要があります。すなわち、ContinuousArrayRead()関数の出口でSSをinactiveにできません。

at45db161dライブラリには、EndAndWait()という関数があり一連のアクセス終了後この関数を呼んでSSをinactiveにすればよさそうです。EndAndWait()でもSSをactiveのまま放置しているため、inactiveにするコードを追加しました。

4) ComparePageToBuffer()のバグ修正(前回と同様の内容を再掲)

at45db161dライブラリ(オリジナルはBlokoSさんのブログ)の変更点を以下に示します。

<at45db161d.h>

#ifndef AT45DB161D_H
#define AT45DB161D_H

#include <WProgram.h>

extern "C" {
#include <avr/pgmspace.h>
#include <inttypes.h>
#include "WConstants.h"
};

#include "at45db161d_commands.h"

#ifndef SPI

#if defined(__AVR_ATmega1280__)
  #define DATAOUT     51
  #define DATAIN      50
  #define SPICLOCK    52
  #define SLAVESELECT 49     // 53(PB0)→ 49(PL0)に変更
  #define SPI_SS      53
  #define RESET        8
  #define WP           7
#else
  #define DATAOUT     11
  #define DATAIN      12
  #define SPICLOCK    13
  #define SLAVESELECT 10
  #define RESET        8
  #define WP           7
#endif
・・以下略・・

<at45db161d.cpp>

void ATD45DB161D::Init()
{
        uint8_t clr;
       
        /* Initialize pinout */
        pinMode(DATAOUT, OUTPUT);
        pinMode(DATAIN, INPUT);
        pinMode(SPICLOCK, OUTPUT);
        pinMode(SLAVESELECT, OUTPUT);
        pinMode(DATAIN, INPUT);

        /* Disable device */
        DF_CS_inactive;                    // Deactivate alternative SS
#if defined(__AVR_ATmega1280__)
      
digitalWrite(SPI_SS, HIGH);        // Deactivate original SS (Must be HIGH befor SPCR set)
#endif

        /* Setup SPI mode=0 */
        //SPCR = (1 << SPE) | (1 << MSTR) | (1 << CPOL) | (1 << CPHA);
        SPCR = (1 << SPE) | (1 << MSTR);

        /* Cleanup registers */
        clr = SPSR;
        clr = SPDR;
}

void ATD45DB161D::EndAndWait()
{
        DF_CS_inactive;  /* End current operation */
        DF_CS_active;    /* Some internal operation may occur
                          * (buffer to page transfer, page erase, etc... ) */

        /* Wait for the chip to be ready */
        while(!(ReadStatusRegister() & READY_BUSY))
        {}
        DF_CS_inactive;  /* Release SPI Bus */
}

int8_t ATD45DB161D::ComparePageToBuffer(uint16_t page, uint8_t bufferNum)
{
        uint8_t status;
       
        DF_CS_inactive;    /* Make sure to toggle CS signal in order */
        DF_CS_active;      /* to reset Dataflash command decoder     */

        /* Send opcode */
        spi_transfer((bufferNum == 1) ? AT45DB161D_COMPARE_PAGE_TO_BUFFER_1 :
                                        AT45DB161D_COMPARE_PAGE_TO_BUFFER_2);
       
        /* Page address */
        spi_transfer((uint8_t)(page >> 6));
        spi_transfer((uint8_t)(page << 2));
        spi_transfer(0x00);
       
        DF_CS_inactive;  /* Start comparaison */
        DF_CS_active;

        /* Wait for the end of the comparaison and get the result */
        while(!((status = ReadStatusRegister()) & READY_BUSY))
        {}
       
        if ( status & COMPARE )
           return 0;       /* If (COMPARE bit = 1) then Error  */
        else
           return 1;

}

WIZ812MJの接続

1)電源系統

WIZ812MJは3.3V動作かつ200mA弱電力を喰うため、3.3Vボルテージレギュレーターから給電します。ボルテージレギュレーターの入力は、VIN(ACアダプターからの給電)を使用します。5Vから電源を取るとArduino本体のボルテージレギュレーターに結構電流が流れて発熱が大きくなるためです。Data Flashも3.3V動作のためこの電源を使います。

2)MISO信号の接続

AT45DB161Dと同様に、入力は5V許容のため、SCLK, MOSI, SCS(SS)はArduinoと直結します。悩んだのはMISO(WIZ812MJ側は3.3V出力)です。Ethernet shieldではMCUと直結になっています。また、Ethernetライブラリを見ると、MISOをつなぐ入力ピンの内蔵プルアップ抵抗を有効にしています。以下に、hardware\libraries\Ethernet\utility\spi.hの抜粋を示します(コメントは私が追記)

// Set DDR -> SS:output, SCLK: output, MOSI: output. Others: input (default)
// Set PORTB -> SS_HIGH
// Set PORTB -> Input pin pull-up (MISO)
#define SPI0_Init()        DDRB  |= SPI0_SS_BIT|SPI0_SCLK_BIT|SPI0_MOSI_BIT;\
                                 PORTB |= SPI0_SS_BIT; PORTB &= ~(SPI0_SCLK_BIT|SPI0_MOSI_BIT);\
                                 SPCR  = 0x50

MCUのMISOピンを5VにプルアップしてしまえばHighレベルの閾値電圧の問題はなくなりますが、SPIデバイス側で問題が出ないかです。WIZ812MJに載っているW5100 TCP/IPチップはVIN(DC Input Voltage)を5.5Vまで許容しているため、MISOを5Vにプルアップしても問題なさそうです。

一方AT45DB161D Data Flashの方は、出力を5Vにプルアップできるという記載はありませんでした。そのため、AT45DB161Dはレベルシフターをかませて3.3V→5V変換を行っています。レベルシフターのピンが余っていたのでWIZ812MJ側もレベルシフターを介して接続しています(W5100チップの5V入力許容に気がつく前に、レベルシフター経由で配線してしまったというのが実情ですが、、)。

(2009/7/22追記)
複数のSPIデバイスが存在する場合、MISO信号をMCUの手前で束ねる必要があります。このために、レベルシフターの5V出力側で、WIZ812MJとAT45DB161DのMISO信号をワイヤードオアしています。レベルシフターの5V出力側はMOS FETを使っており、オープンドレインのワイヤードオア回路相当になっているので出力をぶつけても問題ないかと思っています。AT45DB161DはSSがinactiveになるとMISOをtri-state(ハイインピーダンス)にするという記述がデーターシートにあります。複数デバイスのの出力を接続する場合は以下の方法がありますが、SPIは(b)の形態になるようです(I2Cはaでしょうか);

 (a)各デバイスのオープンドレインをワイヤードオア(プルアップ要)
 (b)各デバイスで出力ピンのハイインピーダンス制御を行ってバス接続

今にして思えば、WIZ812MJとAT45DB161DのMISOを3.3V側で束ねてやるのが正しい設計のように思えます。とりあえず動いていますが、どこかで修正して動作確認してみます。

(2009/7/31追記)
WIZ812MJとAT45DB161DのMISOを3.3V側で束ねる形態に回路を変更。問題なく動いています。回路図も修正しました。

(2009/8/15追記)
Arduino Forumにて次のスレッドを見つけました。
 Arduino + Ethernet Shield + Another SPI device
W5100チップにてMISOをハイインピーダンスにするためには、SS(SCS)の制御でなくSEN(SPI enable)信号をLowレベルに落としてやる必要があるとのことです(データーシートには記載がないのですが)。WIZ812MJはSS信号をモジュール内で反転してSENに入力しているため、MCUからSSをInactive(High)にすると自動的にSENがLowに落ちてくれます。そのため、WIZ812MJではSS信号に連動してMISOをハイインピーダンスにすることができるため、他のSPIデバイスとの混在が可能です。

Official Ethernet ShieldではSENをプルアップしているのみです。そのため、Official Ethernet ShieldではMISOをハイインピーダンスにすることができず、他のSPIデバイスと混在して使用する際は注意が必要です。

3)Ethernet Libraryの修正

Arduino IDE添付のライブラリはATMega168/328用です。Arduino Megaが使用しているATMega1280はSPI関連のピン収容が異なるためそのままでは動作しません。hardware\libraries\Ethernet\utility\spi.hを以下のように修正することでArduino Megaでも動作するようになります。

//------------------------------------------------------------------------
//AVR Mega168/1280 SPI HAL
#define BIT0                            0x01
#define BIT1                            0x02
#define BIT2                            0x04
#define BIT3                            0x08
#define BIT4                            0x10
#define BIT5                            0x20
#define BIT6                            0x40
#define BIT7                            0x80

#if defined(__AVR_ATmega1280__)            //Arduino Mega用ピン収容
#define SPI0_SS_BIT                    BIT0
#define SPI0_SS_DDR                    DDRB
#define SPI0_SS_PORT                    PORTB
#define SPI0_SCLK_BIT                    BIT1
#define SPI0_MOSI_BIT                    BIT2
#define SPI0_MISO_BIT                    BIT3

#else
#define SPI0_SS_BIT                    BIT2
#define SPI0_SS_DDR                    DDRB
#define SPI0_SS_PORT                    PORTB
#define SPI0_SCLK_BIT                    BIT5
#define SPI0_MOSI_BIT                    BIT3
#define SPI0_MISO_BIT                    BIT4
#endif

#define SPI0_SCLK_DDR                    DDRB
#define SPI0_SCLK_PORT                    PORTB

#define SPI0_MOSI_DDR                    DDRB
#define SPI0_MOSI_PORT                    PORTB

#define SPI0_MISO_DDR                    DDRB
#define SPI0_MISO_PORT                    PORTB


#define SPI0_WaitForReceive()
#define SPI0_RxData()                     (SPDR)

#define SPI0_TxData(Data)                (SPDR = Data)
#define SPI0_WaitForSend()                while( (SPSR & 0x80)==0x00 )

#define SPI0_SendByte(Data)                SPI0_TxData(Data);SPI0_WaitForSend()
#define SPI0_RecvBute()                    SPI0_RxData()

// PB4(MISO), PB3(MOSI), PB5(SCK), PB2(/SS)         // CS=1, waiting for SPI start // SPI mode 0, 4MHz
// Set DDR -> SS:output, SCLK: output, MOSI: output. Others: input (default)
// Set PORTB -> SS_HIGH
// Set PORTB -> Input pin pull-up (MISO)
#define SPI0_Init()                        DDRB  |= SPI0_SS_BIT|SPI0_SCLK_BIT|SPI0_MOSI_BIT;\
                                        PORTB |= SPI0_SS_BIT; PORTB &= ~(SPI0_SCLK_BIT|SPI0_MOSI_BIT);\
                                        SPCR  = 0x50

//------------------------------------------------------------------------

//------------------------------------------------------------------------
//IInChip SPI HAL
#define IINCHIP_SpiInit                    SPI0_Init
#define IINCHIP_SpiSendData                SPI0_SendByte   
#define IINCHIP_SpiRecvData                SPI0_RxData


#if defined(__AVR_ATmega1280__)
#define IINCHIP_CS_BIT                    BIT0
#define IINCHIP_CS_DDR                    DDRB
#define IINCHIP_CS_PORT                PORTB

#else
#define IINCHIP_CS_BIT                    BIT2
#define IINCHIP_CS_DDR                    DDRB
#define IINCHIP_CS_PORT                PORTB
#endif

#define IINCHIP_CSInit()                    (IINCHIP_CS_DDR |= IINCHIP_CS_BIT)
#define IINCHIP_CSon()                    (IINCHIP_CS_PORT |= IINCHIP_CS_BIT)
#define IINCHIP_CSoff()                    (IINCHIP_CS_PORT &= ~IINCHIP_CS_BIT)
//------------------------------------------------------------------------

その他

at45db161d, Ethernet libraryの両方を使って、漢字表示(SPI Data Flash memoryへのアクセス)とTCP/IP接続の併用ができるようになりました。

GLCDはピンソケットを使いユニバーサル基板面から持ち上げて実装しているため、GLCDの下側に広大な敷地があります。この敷地に生AVR(ATMega644あたり)を載せてしまえば、高価なArduino Megaを占有せずに回路が組めそうです。最終目的のRSSリーダー用ファームができるまでは頻繁にライブラリやスケッチの更新を行うためMegaを使うとして、安定版ができたら、ATMega644 + SanguinoにスイッチしてMegaを浮かせるという道もありそうです。

あと、コストパフォーマンスを考えると、ストロベリー・リナックスさんで販売しているWIZ200Webという選択肢もおもしろそうです。W5300とATMEGA128,フラッシュROM(4Mビット),外部RAM(256Kビット)がついて4300円と大変お得です。TCP/IPチップがW5300のため、Arduino Ethernet libraryはそのままでは使えないと思いますが。

2009/7/22追記:
MISO信号のバス接続に関する記述を追加。

2009/7/31追記:
MISO信号の接続を変更。

| | コメント (0)

Arduinoで漢字表示(3)

前回の予告に従い、漢字フォントデーターをI2C EEPROMより大容量のSPI Data Flash Memoryに格納してみました。

使用したメモリ

ATMEL社のAT45DB161Dをスイッチサイエンスさんから購入。容量は2MBとSDメモリーカードに比べると小さいですが、漢字フォントを格納するためには十分です。お値段も購入時点で360円でしたのでお安いです。

Arduinoとの接続

回路図を以下に示します。

Glcd_dataflash_2

AT45DB161DとArduinoはSPIで接続します。

AT45DB161Dの動作電圧は2.7V~3.6Vですので、Arduinoの3.3V電源出力を使用して駆動しました。SI, SCK, CS, REST入力は5V許容のため、Arduinoと直結です。SO信号はHighレベルの出力が3.1V~3.3Vとなり、5Vで動作するAVRがHレベルを認識できる下限値(0.6*VCC = 3.0V)ぎりぎりの値となります。直結でも動作しましたが、レベルシフターを入れてあります。レベルシフターもスイッチサイエンスさんで購入しました。

文字列をFlash Memoryに書き込んで読み出しを行う試験をした際に、最初は不定期に文字化けが発生する現象に悩まされました。パスコンの位置を変えると文字化けの出方が変わるのですが解消はしてくれません。最終的にAT45DB161DのGNDを最短距離で配線することで解決しました(当初は、ブレッドボードのグランドラインを使用したためグランド配線が長めに引き回されていました)。SPIのクロックが4MHz(システムクロックの1/4)の設定になっているため、これまで自作した回路の中では一番ノイズの影響を受けやすいのだと思います。オシロスコープがあれば波形を観測してノイズの影響など調べるとおもしろそう。オシロスコープ欲しいです。この機種あたりなら、手が届くのですが、、、

AT45DB161Dのライブラリ

Arduino用のライブラリが見つかりました。大抵のライブラリはArduinoコミュニティーから調達できるため便利です。

Arduino playgroundに掲載されている、dataflashライブラリも動作しましたが、AT45DB161Dで使用しているSPIコマンドが一部未サポートです。dataflashライブラリはAT45DB041BなどのB version(型番末尾のアルファベットがバージョンを示します)をターゲットにしているためCバージョン以降で追加されたコマンドが未サポートのようです。

Webを漁ると、BlockoSさんのblogにAT45DB161D用のライブラリがありました。丁寧なドキュメントもあります。私が試した範囲では、ComparePageToBuffer()関数にバグがありました。オリジナルのコードは比較を行った後にstatusレジスタBit6が1の場合正常の判定を行っていますが、0が正常値です。そのため、以下のようにコードを修正しました(青字が修正部分)。

int8_t ATD45DB161D::ComparePageToBuffer(uint16_t page, uint8_t bufferNum)
{
        uint8_t status;
       
        DF_CS_inactive;    /* Make sure to toggle CS signal in order */
        DF_CS_active;      /* to reset Dataflash command decoder     */

        /* Send opcode */
        spi_transfer((bufferNum == 1) ? AT45DB161D_COMPARE_PAGE_TO_BUFFER_1 :
                                        AT45DB161D_COMPARE_PAGE_TO_BUFFER_2);
       
        /* Page address */
        spi_transfer((uint8_t)(page >> 6));
        spi_transfer((uint8_t)(page << 2));
        spi_transfer(0x00);
       
        DF_CS_inactive;  /* Start comparaison */
        DF_CS_active;

        /* Wait for the end of the comparaison and get the result */
        while(!((status = ReadStatusRegister()) & READY_BUSY))
        {}
       
        if ( status & COMPARE )
           return 0;       /* If (COMPARE bit = 1) then Error  */
        else
           return 1;

}

動作試験

前回掲載したJIS配列の漢字フォントをAT45DB161Dに書き込んで動作試験を行いました。

AT45DB161Dは書き込みはページ単位、読み出しはページ番号+オフセットを指定して行います。ページサイズはデフォルトで528byteとなっています。512byte/pageに変更できますが、一度変更すると528byteには戻せないとデーターシートに記載があります。2のべき乗でないと使いづらそうなので512byte/pageに変えてやろうかと思ったのですが、ライブラリは528byte/pageを前提に作ってあります。

よく考えてみると、漢字フォントのデーターサイズは24byte(全角)or 12byte(半角)のため、1 pageにぴったり収容できます(全角22文字がぴったり入る)。そのため、528byte/pageのままで使用しました。フォトンデーターの読み出しは、ContinuousArrayRead()関数を使って、指定したページ + オフセット位置から24byte分のデーターを読み出すようにしています。

前回同様のJISコード順に漢字を表示するだけのテストプログラムですが、以下の通り動作させることが出来ました。

Jiskanji_spiflash

正確な測定はしていませんが、I2CインタフェースのEEPROMより表示が早くなったように思います。SPIクロックが4MHzのため、I2Cより高速動作ができているようです。

次の目標は、Unicode(UCS-2)版のフォントデーターを作成して表示を行うことです。あと、そろそろユニバーサル基板にGLCD,メモリーなどを搭載して、GLCDシールドに移行したいと思っています。

2009/7/21更新:Unicodeフォントを「Arduinoで漢字表示(4)」で公開しました。

| | コメント (2)

Arduinoで漢字表示(2)

前回の続編です。まだ最終的な方向性が定まっていないのですが、JISコードベースで漢字が表示できるようになりました。

漢字フォントデーター

RSSなどUTF-8でエンコードされたコンテンツを表示するためには(ちなみに、このブログもUTF-8です)、内部コードとしてUnicode(UCS-2)を使うと効率がよいと思われる点は前回記載しました。漢字をUnicode配列で収録したフリーのフォントデーターを探したところ、efontさんが東雲(Shinonome)フォントのUnicode版を配布していることを発見。また、efontさんからJISコードベースで収録した東雲フォントもダウンロード可能です。

問題は、Unicodeの場合ハングルや中国語固有の漢字など多数のフォントデーターが収録されており、JIS漢字の東雲フォントに比べるとデーター量が大きくなることです。以下にフォントファイルに収録されている文字数(bdfファイルのCHARS宣言の値)を示します:

  • shnmk12min.bdf(JIS): 6879文字
  • b12.bdf(Unicode):22759文字(半角英数字を含む)

12ドットフォントの場合、文字あたり24Bのデーター量になるため、2.2万文字のUnicodeフォントを格納するためには500KB程度のROM容量が必要です。日本語漢字コードが連続した領域に配置されている場合、そこだけ抜き取ってROMに書き込むことができますが、UnicodeではCJK(中国・日本・韓国)統合漢字として由来が同一の文字単位にまとめているせいか、JISに収録されている漢字だけを抜き取ると並びが不連続になってしまいます。そのため日本語漢字だけを抜き取った場合、文字コードからフォントの格納位置を一律に計算できないため、文字単位にアドレス変換テーブルが必要になります。

変換テーブルを置くくらいなら、CJK統合漢字全体をEEPROMに格納して文字コードで直接EEPROMアドレスをインデックスするのがよいのですが、手持ちのAT24C1024B(1Mbit I2C EEPROM)x2個には入りません。そのため、。先ずはJISの東雲フォントを使用することとしました。

漢字フォントは前述のI2C EEPROM x2個に格納しました。EEPROMを接続した全体の回路図を以下に示します。SCL/SDAのプルアップはAVRの内蔵プルアップ抵抗を使用(Wireライブラリを使用すると自動的に内部プルアップが有効になります)。

Glcd_eeprom

フォントデーターの変換

ArduinoのGLCDライブラリとしては、本家のwebで公開されているks0108 GLCDライブラリを使用します。先日Ver.2が公開されたため、V2をベースに漢字表示機能を追加します。V2への高速表示の移植は別途にします。(シリアル接続のEEPROMにフォントデーターを格納する時点で高速動作は期待できないため)。

先ずbdfフォントのビットマップデーターを、Arduinoのks0108 GLCDライブラリで使用しているビットマップデーターに変換します。変換の方法は前回の「Arduinoで漢字表示」に記載した通りです。ツールを作って変換を行ったファイルを以下に掲載します。
 「glcdfont_shnm12min.zip」をダウンロード

  • glcdfont_shnm12min.txt:外付けEEPROMに書き込む形式に変換した漢字フォントデーター(半角カナを含む)
  • shnm.h:東雲6x12の半角英数字フォントをks0108 GLCDライブラリのフォント形式に変換したファイル。スケッチ上で、shnm.hをincludeすることで使用可能(AVR内蔵Flashメモリにフォントデーターを格納)

JIS X 208の文字コードでは、「区(Row)」と呼ばれる面(ページのようなものでしょうか)に、94文字分のコードを割り当てます。例えば、「01区」は0x2121~0x217dのコードエリアを使用します。文字を割り当てていないコードポイント(例えば、0x2230~0x2238)にはフォントデーターが存在しないのですが、フォントを格納したEEPROMのアドレス計算を容易にするため、スペース相当のダミーデーターを挿入しています。

フォントデーターの転送

PCからシリアル通信を使ってArduinoにフォントデーターを転送します。PCのアップローダーアプリとArduino側の受信(およびEEPROMへの書き込み)スケッチを作成しました。アップローダーのバイナリファイルとArduinoのスケッチを以下に掲載します。
 「Upload.zip」をダウンロード

  • Upload.exe: PC側のアップローダー(.Net Framework 2.0以上が必要)
  • FrontWrite.pde: Arduino側のフォントデーター受信、書き込み用スケッチ

アップローダーの画面イメージを以下に示します。

Uploadapp_4

ファイルを選択して、Uploadボタンをクリックすると送信を開始します。プログラムを最初に動かした時は、シリアル通信が始まらない場合があります。その際は一旦プログラムを強制終了してやり直すとうまくいきます。Arduinoへデーター転送中に中断したい場合も、タスクマネージャーから強制終了する必要があります。このあたりは手抜きですが、、

フォントデーターのEEPROMへの書き込みは、当初はページ書き込み(24byte or 12byte単位)を使用したのですが、特定のアドレスで書き込みエラーが発生する問題を解決できず、byte単位の書き込みを行っています。読み出しは、24 or 12Byte単位のシーケンシャル読み出しができています。

ks0108ライブラリの改造

とりあえず、JISコードを直接指定して1文字を表示するPutUChar()関数を追加しました。改造版のソースコードを以下に掲載します。
 「ks0108_jisKanji.zip」をダウンロード

改造版ライブラリで漢字を表示した画面を以下に示します。

Jiskanji

今後の計画

まだまだ先は長いです。次にやることの選択肢として以下があります。

1)Unicode→ JISコード変換テーブルの作成

このURLにJIS - Unicodeの対応表があるのですが、全く一貫性・法則性がなくビット演算レベルでの変換は無理そうです。そのため、力業ですが、文字コード毎に変換テーブルを作る方法が考えられます。Unicode側のコード・スペースを2万として、16bitのJISコードに変換するテーブルを作る場合、テーブルサイズは 2万 x 2≒ 40KBになります。

アクセス速度を考えると、このテーブルはシリアルEEPROMではなく、AVRの内蔵Flash ROMに格納する必要があります。Arduino MegaのATMega 1280なら容量的には格納できる範囲ではあります。

2)大容量メモリーにUnicodeフォントを格納

1MB程度のメモリーがあればUnicodeフォントを格納できますが、I2CのEEPROMでは1Mbit以上の製品はなさそうです。スイッチサイエンスさんのSPI接続フラッシュメモリAT45DB161Dあたりを使ってデーターを格納する解がありそうですが、以下が気になる点です:

  • AT24C1024B(I2C)よりアクセスのオーバーヘッドがあるかも
    → 当初は512Byteのページ単位アクセスのみかと思ったのですが、データーシートを見直すと開始アドレスを指定したシーケンシャル読み出しもあるため、アドレス指定が長くなる以外は(I2C EEPROMに比べて)特にオーバーヘッドはなさそうです
  • 動作電圧が3.3V。Arduinoと接続するためにはレベル変換回路が必要。スイッチサイエンスさんの「ロジックレベル変換モジュール」が使えそう。3.3V電源は、Flashメモリーを駆動するだけならArduinoの3V出力(50mA)が使えそうです

次は、AT45DB161D(SPI版、2MB)を使って、どの程度の表示速度が得られるかを実験してみます。現在使用しているAT24C1024B(I2C版、128KB)では、AVR内蔵Flashに比べるとかなり表示が遅くなりますが、RSSビューワー的な使い方なら許容範囲です。

2009/7/7追記:
AT45DB161Dのデーターシートを見直すと、DC特性表の注釈にI/O(SI, SCK, CS)は5-Volt tolerantという記述を発見(こういう情報はサマリーページに書いて欲しい!)。上記信号はArduinoと直結出来ることが分かりました。

SO(AT45DB161Dのデーター出力)は、3.3V駆動時のV-OH(min) が3.1Vとなるため、Arduinoに直結してもぎりぎりHレベルの認識は可能。SOに関してはレベルシフターをかまして、5Vにきっちり上げてやった方が安定性の点では望ましいですが、実力的には直結でも動きそうです(以前3.3V駆動のシリアルをArduinoに直結した際は正常に動作しました)。

直結・レベルシフター経由の双方で動作を試してみたいと思います。

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

2009/7/12追記:
AT45DB161Dの接続結果を「Arduinoで漢字表示(3)」に記載しました。

| | コメント (2)

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)を公開

| | コメント (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以外のボードでは動作いたしません。

| | コメント (0)

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)を一旦削除して再コンパイルすると動くようになります。

| | コメント (0)

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

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

 「MailChecker_pub20090609.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は減るのですが)当面は現状のままで行こうと思います。

| | コメント (2)

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#なら少々触ったことがあるのですが、こちらはガベージコレクタが自動的に不要メモリ領域の回収行ってくれるので楽ちんですよね。

| | コメント (0)

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)

| | コメント (0)

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()のタイムアウトに関しては、リトライで救うしかないということが分かりました。

| | コメント (2)

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)

| | コメント (0)

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のピン番号(回路図)に誤りがあったため修正。

| | コメント (0)

Arduinoメールチェッカー

Arduinoイーサーネットシールドの発熱量の投稿で予告した、イーサネットシールド(Ethernet Shield)を使用したメールチェッカーを作ってみました。以下に示す機能があります:

  • 登録したメールサーバー(POPサーバー)に定期的にアクセスして、着信メール数、送信元(ヘッダーのFrom:フィールド)が登録したアドレスかをチェックする
  • 登録したアドレスからメールが着信した際にLEDを点滅してお知らせする
  • 着信メール数、登録したアドレスからのメール数、登録したアドレスからのメールの場合どのアドレスかを、LCDに表示する
  • 周期チェックに加えて、ボタンによるマニュアル確認

ハード構成

LCDディスプレー、タクトスイッチ、LEDをプロトシールドに載せて、イーサネットシールド上にスタックしました。写真のような3階建て構造になります。

Mailchecker1_2

LCDはポピュラーなSC1602Bではなく、プロトシールドの幅にぴったり収まるSD1602HUOBを秋月さんで購入。このLCDはサイズがぴったりなのとオレンジバックライトがきれいでよいのですが、LCDパネルと基板をつなぐリボンケーブルが露出しておりかつこいつが傷つきやすいので注意が必要です。

結構背が高くなったので、写真のように、横において使用中です。

Mailchecker2

写真の表示は、着信メール数2件(MSG:2)、登録したアドレスからの着信1件(CHK:1)、登録した送信元アドレス"todot"を含むアドレスから着信があったことを示しています。

表示部分を含む全体の回路図は以下です。

Mailcheker_shcematic

電源ケーブルの接続ですが、Arduinoイーサーネットシールドの発熱量で記載したボルテージレギュレーターの発熱対策として、5V ACアダプターの出力をPOWER Pinの+5V(Pin3), GND(Pin4)に接続しました。電源の取り込みはプロトシールドで行い、シールド間の連結を介してイーサネットシールド・Arduino本体に電源を供給しています。

ACアダプタ用ジャックをプロトシールドに載せることができなかったため、パネル取り付け型ジャックから5cmほどケーブルの延ばしてプロトシールドに半田付けしてあります。電源コネクタの取り付けがスマートでないことが不満点です。

スケッチ

スケッチのソースコードはココからダウンロードできます。

POP3はテキスト形式でサーバーとコマンド・レスポンスをやり取りします。Arduinoからは、Ethernet libraryのclient.println()メソッドを使用してコマンドの送信が可能です。POPサーバーからのレスポンス受信はclient.read()メソッドを使用して、改行(LF)までの一行をバッファに格納するgetLine()関数を作って処理しています。

POPサーバーにアクセスしてヘッダーを取得する手順は以下です。

  1. client.connect()を実行してPOPサーバーとのTCPセッションをオープン
    サーバーから、"+OK"レスポンスが返るのを確認
  2. clinet.println()にてユーザー名を送信(0016以降はclient.write()を使用した方がよい)
    →USER xxxx (xxxxがユーザー名)コマンドを送信
  3. clinet.println()にてパスワードを送信(0016以降はclient.write()を使用した方がよい)
    →PASS xxxxxコマンドを送信
  4. STATコマンドを送信
    →"+OK 2 4436"のようなレスポンスが返ります
    →2がメッセージ数、次の数字がOctet数になります
  5. TOP 0 0コマンドを送信して1件目のメールヘッダをダウンロード
    "+OK Message follows"レスポンスに続いてヘッダーを受信し、"From:"を含む行を受信した際に登録したアドレスに一致するかをチェック
  6. ステップ5をメッセージ数繰り返す(TOP 1 0, TOP 2 0・・・の繰り返し)

ソースの以下のコメントを外してコンパイルすることでPOPサーバーとのやり取りをシリアルポートに出力し、PCにて動作確認ができます。(デバッグ機能です)
 //#define DEBUG
シリアルの通信速度(=表示速度)が遅いと、イーサネット通信が不安定になる現象があったため、シリアルの通信速度を最大値(115200bps)に設定してあります。

テスト当初、POPサーバーからヘッダをダウンロードするメールが2件以上になると、起動後最初のチェックはOKなのですが、チェック(サーバーアクセス)を繰り返すとリセットが発生する問題に悩まされました。スタック溢れの可能性を疑い、グローバル変数を使ってスタック領域を使わないようにしたため、少々汚いコードになってしまいました。

最終的には以下に示す通り、メールヘッダのFrom行を検出する処理で呼んでいたメソッドを変更することで解決しました。

当初のコード:ReceiveBuffer.startWith("From:") -- 受信バッファが"From:"で始まるかの判定。当初は、行頭に"From:"が来ることまで含めてマッチングを取ろうとしました

変更後:ReceiveBuffer.contains("From:") -- 受信バッファに"From:"を含むかの判定に変更。原因不明のリセットが解決せず、部分一致のメソッドに変更

ライブラリにバグがあるとも思えず、根本原因は不明です。

使用したライブラリ

IDE-0015標準のライブラリ以外に、以下のライブラリを使用しました。

  • WString.h:POPサーバーに送信するTOPコマンドの編集、受信バッファ内の文字列部分一致検索などのテキスト処理
  • Debounce.h:スイッチのチャタリング除去
  • Metro.h:POPサーバーのチェック周期、LEDの点滅周期など、周期処理の時間を管理
  • Sleep.h:致命的なエラーに遭遇した際に、MCUをStandbyモードに遷移して処理を停止するために使用(Haltの代わりに使用)。「なんでも作っちゃう、かも」さんのライブラリを使用させていただきました。

文字列処理などは、AVR Libcのライブラリ呼び出しでも実現できますが、上記のライブラリを使用するとお手軽かつ、Arduinoらしい簡潔なコードになります。既存のライブラリが豊富な点は、やはりArduinoのメリットだと思います。

設定機能

ありません、、
POPサーバーのIPアドレス(nslookupができないため、ドメイン名での指定はできません)、ユーザー名、パスワード、監視するメールアドレスなどはソースコードにハードコーディングです。

Ethrenet Libraryの修正

clinet.connect()メソッドでサーバーにTCPセッションが接続できない(タイムアウトする)ケースが何回かあったため、ArduinoのForumを覗いてみると、Ethernet Libraryの問題がいくつか報告されていました。その中から、以下の修正コードを盛り込みました。

1) 送信時、1文字単位にIPパケットを生成してしまう問題

オリジナルのコードでは、client.println("Hello World");を動かすと、1文字単位にIPパケットを送信するそうです。CR+LFを含めると13パケットも送信してしまいます。本来なら1パケットで済みますし、1文字(1 byte)のデーターを送るために40 byteのTCP/IPヘッダを送っていることになり、非効率この上ないです。

本職はIPネットワーク屋なので、このようにネットワークの帯域を無駄に消費するヤツは許せないため、即刻修正です。。

2) TCP FINの送信処理に問題があり、clinet.stop()で一旦接続を終了した後に再度client.connet()で再接続を行うと接続できない場合がある。

1)2)の対処を盛り込んだ、Libraryのソースはココから ダウンロードできます。

2009/6/7追記
上記1)2)の問題は、最新のIDE-0016で修正されています。1)の問題に対しては、Clientクラスのwrite()関数を使用することで解決します。IDE-0016を使用した最新版をこの記事で紹介しました。

残問題

Ethernet Shieldに関して以下の問題が残っています。

1)電源ONの際に、Link Upしないことが度々ある

Ethernet shieldの初期化(恐らくW5100チップのリセット)が完全に行われていない模様で、リセットボタンを押すとLink upします。Arduino Forumにも関連スレッドがあり、RESET - GNDピン間にコンデンサを追加すると問題が解消すると投稿がありますが、そこまでは踏み切れません。

2)電源ON後の最初の接続で、client.connetc()がタイムアウトする場合がある(Link upはしている)

どうもLCDの初期化と同様に、電源ON時のリセット処理が不十分なようです。商用のハード開発では、リセット回路のドライブ能力・リセット時間やタイミングは、設計の重要ポイントになっているのですが、このあたりがいまいちなのかしら、、

2009/6/6追記

メールサーバーアクセス方法を変更した改良版を公開しました。

2009/6/7追記

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

2009/6/8追記

掲載したスケッチにメモリリークがあることが分かりました。サンプルとしてコードは掲載しておきますが、もし使用される場合はご注意下さい。

2009/6/9追記

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

| | コメント (2)

LiquidCrystal(LCD)ライブラリの初期化コード

2009/8/14追記:
以下に示す問題は最新のIDE-0017で修正が盛り込まれました

ArduinoでLCD(液晶ディスプレイ)を使用する場合、LiquidCrystalライブラリを使用する場合が多いかと思います。このライブラリは、HD44780コントローラー互換チップを使用したLCDで使用可能となっており、秋月電子さんが販売しているSUNLIKE社液晶モジュール、SC1602、SD1602シリーズで使用することができます。

ArduinoのForumでも何回か話題になっているのを見かけましたが、電源ON時にLCDの表示が出ない場合があります。タイミング的な問題のようで、私の環境では数回に1回こうなります。この問題の原因と対策を考えてみました。

HD44780にはパワーオン時のリセット機能があるため、本来なら電源ON時に必要な初期化が自動的に行われる筈です。データーシートによると、「Vccが4.5Vに達した後10ms busy状態が継続する」とあるため、10msはパワーオンリセット期間であるように読み取れます。電源ON時、10ms経過前(パワーオンリセット完了前)にスケッチが起動し、LCDにアクセスしてしまうと表示が出ない問題が発生するのでしょうか?

Arduinoでユーザースケッチが起動するまでには若干の遅延があります。Arduinoがパワーオン(もしくはリセットボタン押下)した場合、先ずブートローダーが起動します。ブートローダーは、PCからのアップロード開始をチェックし、タイムアウト時、Flash ROMに書き込み済みのユーザースケッチを起動します。Duemilanoveなどの自動リセット機能があるボードでは、この遅延は秒以下であるとの記載がArduinoのWebページにあります

ブートローダーがユーザースケッチを起動するまでに500ms程度の遅延があれば、タイミング的にはLCDコントローラーのパワーオンリセットは終わっているはずです。従って、何らかの理由でパワーオンリセットだけでは初期化が不十分になる、もしくはパワーオンリセットが働かないケースがあると思っています。2009/5/26 追記にてこの点を確認する追試を行っています。

データーシートには、パワーオンリセットの条件として、電源の立ち上がり時間が、0.1ms~10ms以内という規定があります。例えば、電源の立ち上がり時間が10msより長い場合、パワーオンリセットが働かないことになります。

そのため、対策としては、LiquidCrystalライブラリの初期化コードにてLCDコントローラーをMCUから初期化することが有効だと思われます。
<arduinoインストールディレクトリ>\hardware\libraries\LiquidCrystal配下のLiquidCrystal.cppに以下の初期化コードを追加することで、問題をほぼ回避できるようになりました。

// For 4-bit Mode
LiquidCrystal::LiquidCrystal(uint8_t rs, uint8_t rw, uint8_t enable,
  uint8_t d0, uint8_t d1, uint8_t d2, uint8_t d3) :
  _four_bit_mode(1), _rs_pin(rs), _rw_pin(rw), _enable_pin(enable)
{
  _data_pins[0] = d0;
  _data_pins[1] = d1;
  _data_pins[2] = d2;
  _data_pins[3] = d3;
 
  pinMode(_rs_pin, OUTPUT);
  pinMode(_rw_pin, OUTPUT);
  pinMode(_enable_pin, OUTPUT);
 
  for (int i = 0; i < 4; i++)
    pinMode(_data_pins[i], OUTPUT);

// ---- Initialize HD44780 ------- ←追加部分開始
  delayMicroseconds(15000);      //電源ONからの安定化時間を追加(2009/6/17)
  send_4bit(0x03);
  delayMicroseconds(4500);
  send_4bit(0x03);
  delayMicroseconds(500);
  send_4bit(0x03);
  delayMicroseconds(500);
  send_4bit(0x02);   // function set to 4-bit
// ---- Initialize HD44780 END ---- ←追加部分終了

  command(0x28);  // function set: 4 bits, 2 line, 5x8 dots
  command(0x0C);  // display control: turn display on, cursor off, no blinking
  command(0x06);  // entry mode set: increment automatically, display shift, right shift
  clear();
}

ファイルの末尾に以下の関数を追加
// ---- Initialize HD44780 -------
void LiquidCrystal::send_4bit(uint8_t value) {
  digitalWrite(_rs_pin, LOW);
  digitalWrite(_rw_pin, LOW);

  for (int i = 0; i < 4; i++) {
    digitalWrite(_data_pins[i], (value >> i) & 0x01);
  }

  digitalWrite(_enable_pin, HIGH);
  digitalWrite(_enable_pin, LOW);
}
// ---- Initialize HD44780 END ----

ヘッダファイルも、send_4bit()メンバー関数の追加に伴って変更が必要です。変更を盛り込んだライブラリソースをアップしておきます。
 「LiquidCrystal.zip」をダウンロード

上記のコードはLiquidCrystalクラスのコンストラクターにLCDコントローラーの初期化を追加したものです。コンストラクター(C++の用語ですね)は、スケッチの中で、
 LiquidCrystal lcd(6, 3, 7, 14, 15, 16, 17);
のようなコードを書いてLCDライブラリーの使用を宣言した際に呼び出されます。コンストラクターには4-bitモード用に加えて8-bitモード用がありますが、Arduinoユーザーは4-bitモードしか使用しないと思いますので、手抜きをして4-bitモードのコンストラクターのみにコードの追加を行っています。

ライブラリのソースコードを変更した際は、LiquidCrystal.cppと同じディレクトリに存在するLiquidCrystal.oファイル(コンパイラが生成したオブジェクトファイル)を削除してからスケッチのコンパイルをやり直します。オブジェクトファイルを一旦削除しないと、ソースの変更が反映されないため注意が必要です。

2009/5/26 追記

電源ON時のパワーオンリセット時間を十分確保することで問題が解消するか否かの追試を行いました。試験の方法としては、コンストラクターの呼び出しをスケッチ起動から1s遅延させます。そのために、コンストラクターの呼び出しをsetupの中に移動しています。

IDE-0015添付のオリジナルライブラリ(MCUからの初期化コードなし)に戻して以下のスケッチを実行すると、電源ON時にに"hello, world!"が表示されない場合があります。

#include <LiquidCrystal.h>

int ledPin = 13;                // LED connected to digital pin 13

void setup()
{
  pinMode(ledPin, OUTPUT);      // sets the digital pin as output
   
  //LiquidCrystalのコンストラクター呼び出しを電源ONから1s遅延させる
  //それでも、LCD表示が行われない場合あり
  delay(1000);
  LiquidCrystal lcd(6, 3, 7, 14, 15, 16, 17);
  lcd.print("hello, world!");
}

void loop()
{
  digitalWrite(ledPin, HIGH);   // sets the LED on
  delay(500);                  // waits for a second
  digitalWrite(ledPin, LOW);    // sets the LED off
  delay(200);                  // waits for a second
}

setupの中でコンストラクターを呼び出してしまうと、lcdインスタンスがsetup内でのみ有効となり、loopなど他のスコープで使えなくなるため実用的には意味がない検証用のコードです。この結果から、単にコンストラクター呼び出しを遅らせても問題が解決しないことが分かります。

このスケッチでは、LEDの点滅は始まりますので、プログラムがsetupの中でスタックしていることはなく、loopの処理まで進んでいます。即ち、パワーオンリセット期間中にスケッチが起動するため問題が発生するのではなく、オリジナルのライブラリでは初期化の内容が不足しているということになります、もしくは電源の立ち上がり時間が遅すぎるためパワーオンリセットが機能していないかです。

2009/6/17 追記

HD44780のデーターシートでは、MCUからの初期化の際は、電源ONから最初03Hを送るまでの間隔として15msの指定があります。パワーオンリセット期間満了後にMCUからの初期化を実施せよということですね。当初ブートローダーの起動遅延で、15ms程度の時間は経過するだろうと思い、最初のdelayを割愛したのですが(忘れただけだろ!)、Arduino本体とLCDが別基板の場合など電源ONのタイミングがそろわない場合もあるため、やはり最初の遅延は入れた方がよいかと思います。

私の環境では、15000μs (15ms)の遅延を入れてみました。それでも起動時に問題がある場合は遅延時間を個々に調整してみるとよいです。

2009/6/18 追記

全体的に記述を見直しました。

| | コメント (2)

Arduinoイーサーネットシールドの発熱量

イーサーネットシールド(Ethernet shield)を購入しました。
目的は、PCが起動していない間でもメールチェックを定期的に行って、登録したメールアドレスからの着信があった場合の通知を行うためです(例えば、cocologのコメント書き込み通知メールを監視する)。

これまでは、ブロードバンドルーターのメールチェック機能を使っていました。最近、無線LAN APの入れ替え(マルチセキュリティーとハイパワー対応)を行い、これまで無線LAN APとブロードバンドルーターが別だったのを1台にまとめたのですが(BuffaloのWZR-HP-G300NHに入れ替え)、後継機ではメールチェック機能がなくなってしまいました。個人アカウント宛のメールは毎日チェックしないため、私的には便利な機能だったのですが、プロバイダーのメールサーバーに負荷をかけるため歓迎されない機能なのでしょうか、、

そのため、Arduinoを使ってメールチェッカーを自作することにしました。これまでは、Arduinoにセンサーやモーターなどをつないで、動いたら自己満足の世界でしたが、初の実用アプリ(?)になります。

インターネットアクセス方法について試行錯誤したのですが、結局はイーサーネットシールドを使用することになりました。イーサーネットシールドは少々お高いので、La Foneraを改造してインターネットアクセスの踏み台にする方法(リンクを参照)をトライしていたのですが、安定動作が難しそうだったので断念(この顛末は機会があったら書きます)。

イーサーネットシールドの発熱が思った以上に大きかったので、今回はこの点について書きます。メールチェッカーの記事はこちらを参照して下さい。

Arduinoを単体で動かす場合(PCのUSBから給電しない場合)、私は9VのACアダプターを使用しています。Arduinoの推奨電圧は7~12Vですので、間をとって9Vにしました(あと7Vのアダプターはあまり見かけません)。

イーサーネットシールドをArduino本体にのっけて9Vアダプターから給電すると、Arduino本体のボルテージレギュレーターがかなり発熱します。イーサーネットシールド側でTCP/IP処理を行うW5100チップも思ったより発熱しています。データーシートによると、W5100の消費電力は3.3V動作で146mAです。

9V ACアダプターで長時間動かすのは危険を感じるため、5V ACアダプターをArduinoのPowerコネクタ5V pinにつないで使っています。この接続だと、ボルテージレギュレーターをバイパスするためボルテージレギュレーターの発熱を回避できます。

ちなみに、ACアダプターを接続する外部電源ジャックに5V ACアダプターをつなぐとArduinoが動作しません。理由はArduinoのボルテージレギュレーターを通過する際に一定のロス(電圧降下)が発生するため、ボルテージレギュレーターの出力として5V電圧が得られないためです。私の環境ですと、上記のケースではボルテージレギュレーターの出力は3.4Vになりました。

ArduinoのForumにEthernet Shieldが熱いというスレッドがあり、このスレッドによると、Arduinoのボルテージレギュレーターは安価なlinear typeを使っているため、9V→5V変換の「電圧差(この場合は4V)」x「電流」が熱になる、すなわち消費電流が大きいシールドを載せると発熱が増えるとあります。Arduino単体では消費電力が小さいため発熱の問題はないが、イーサーネットシールドは消費電力が大きくArduino本体の発熱を招くということになります。

Arduino本体 + イーサネットシールド + LCDをつないだ状態で電流を測定してみました。写真に示す通り182mA流れています(起動時200mA程度流れて、その後182mA程度に落ち着きます)。給電は5V ACアダプターから直送(ボルテージレギュレーターバイパス)です。

Ethershield_ma_2

Arudino本体のみの消費電流は24mAでした。

Arduino_ma_2

上記より、全体の消費電力配分は以下となります:

  • Arduino本体    :25mA
  • LCD(バックライト):10mA
  • イーサネットシールド:147mA

LCDのバックライトには、順方向電圧4.2Vに対して47Ωの抵抗を入れているため、17mA流れる計算になりますが、テスターの電圧降下のせいか、電流測定時はバックライトが通常の半分程度でしか点灯していなかったため10mAに補正しました。W5100チップの消費電力データー(146mA)と整合性がとれた値になります。

結論としては、イーサネットシールド(W5100チップ)はAVRに比べると5.8倍消費電力が大きく・そこそこ熱が出るということになります。W5100チップに指を強く押し当て続けるとそこそこ熱いので、気休めにヒートシンクを貼り付けてみました。温度は測っていませんが、40℃以上はありそう。W5100の消費電力はワットにすると0.5W程度ですが、1W以下でここまで発熱するものか、このあたりは詳しくないのでよくわかりません。

ちなみに、ブレッドボードで動いているのは冒頭に記載した、メールチェッカーです。LCDやメール着信表示のLEDやらをプロトシールドに移植したいと思っており、この作業ができたらブログに掲載予定です。

| | コメント (0)

Arduino始めました

半年以上PS3 Linux関連の更新をさぼっておりました。
この間何をしていたかというと、仕事ではまっていたことに加えて、Arduinoという8-bitのマイコンボードを使った電子工作にはまっていたのです。私が、Arduinoを知るきっかけとなった記事はココです。

半年間の成果(?)で、Arduino関連のネタがだいぶたまりましたので、ブログのタイトルを拡張(無理矢理です、、)して、Arduino関連の記事も掲載していこうと思います。また、半年でネタ切れになるかもしれませんが、、、

Arduinoは、Atmel社のAVR(ATMegaシリーズ)という8-bitマイコンを使用しているのですが、メモリーがFlash ROM 32KB + SRAM 2KB (ATMega 328の場合)、クロック16MHzだったりして、Cellに比べると超シンプルですが、ハードが自由にいじれる点が魅力です。

歳がばれますが、学生時代の主力PCだったNEC PC-8801, PC9801はCPUの信号(アドレス・データー線など)がそのまま拡張バスに見えており、IOカードを自作してLEDをチカチカさせたりステッピングモーターをまわしたりして遊んだことがありました。また、Z80CPUを使ったマイコンボードも比較的簡単に自作できました。

32-bit CPU(80486以降でしょうか)& Windowsが主力になってからは、次のようにハードが複雑になって、私が触れる領域を外れていました。

  • ピン数が膨大
  • クロックは100MHzクラス
  • IOはチップセット経由
  • メモリ管理機能などモダンOSを載せることが前提

Windows時代以降でも、H8マイコンなどを使ってマイコンハード工作はできたのだと思いますが、クロス開発環境の整備・作成したプログラムをいちいちROMに焼いたりといった作業が面倒そうなのもハード系から遠ざかる要因でした。ArduinoはWindows/Linux/Mac上で動作するフリーの開発環境が用意されていること、作成したプログラムをUSB経由でAVRのFlash ROMに書き込めるため非常にお手軽です(ROMライターなどの専用書き込み環境が不要)。

今年の正月休みに、Arduino Duemilanoveとブレッドボード・部品を買い込んで遊んでみたところ、久しぶりのハード工作の楽しさに目覚めた次第です。次からはArduinoを使ってみて気がついたことを書いていこうと思います。

| | コメント (0)