2012年5月19日土曜日

KOZOS EXPBRD #00 (KOZOSをしゃぶりつくしたい人の為の拡張基板を設計しました)

KOZOS EXPBRD #00とは、KOZOSをしゃぶりつくしたい人の為の拡張基板です。
12ステップでは飽き足らない、もっとKOZOSで色々やりたい人を想定して設計しました。


基板には以下の部品を搭載しています。
  • 122x32ドットのモノクロ・グラフィック・ディスプレイ
  • マイクロSDカード・スロット
  • 2つのLED
  • 2つのスイッチ
  • ロータリー・エンコーダー
  • mp3エンコーダー・チップ
  • ステレオ・フォン・ジャック
  • 赤外線受光素子
この基板は、秋月電子通商から販売されている「H8/3069Fネット対応マイコンLANボード」を裏返しにした状態で、その上にドッキングさせる事を前提に設計してあります。

使用の際は、分厚くなった二階建ての基板を机に縦に置いて使うイメージです。
もちろん支柱を立てて寝かせて使う事もできます。

ドッキングした状態でも、動作モード切り替えスイッチやリセットスイッチ、シリアルポートが使えるので便利。加えて、先に挙げた追加接続されたデバイス達を制御できるというわけです。

もちろんアイ・キャッチは坂井弘亮さんのオリジナル・キャラクタである「ニジマス君」です。


著者いわく「KOZOS本は初心者向け」とおっしゃっていますが、現役の組み込みエンジニアが見ても勉強になる部分は多々あるように思います。
その場合、実際に色々なデバイスの制御をカーネルを介して実装していく事で、なかなか他では味わえないような楽しさに遭遇する事ができます。

基板は例によってFusionPCBに頼みました。
実際に自分で作ったスクリプトに助けられたりして、意外に忘れるのは早いなぁと感じたり。

本当は、別件の基板設計に着手していたのですが、先にこちらが完了してしまいました。
気分転換の方が先に出来てしまうなんて、なんというかどうしようもない感じです。
まぁ、それはそれ。
このプロジェクトでも色々と考えた事があるので無駄にはならない気がしています。

2012年5月6日日曜日

Natural Tiny Shell (NT-Shell) をLX9で使ってみる

Natural Tiny Shell (NT-Shell)の次のリリースに向けて「Eating your own dog food」を実践しているわけですが、今回はLX9で試してみることにしました。


と言っても、NT-Shellはもともと「UARTのReadとWriteさえ用意してくれればうまく行くよ。」という設計なのであまり苦労する事はありません。

UARTの付いたHardware Platformを使ってHello Worldプロジェクトを新規作成します。


次にNT-Shellのソースコードを持ってきてプロジェクトに追加します。


後はhelloworld.cにNT-Shell用の記述を追加するだけ。
以下のようにシンプルな実装で直ぐにNT-Shellを試す事ができます。

簡単です。

#include "platform.h"
#include "ntshell.h"
#include "xuartlite.h"

XUartLite xu;
ntshell_t ntshell;

static int func_read(char *buf, int cnt)
{
 int i;
 for (i = 0; i < cnt; i++) {
  u8 c;
  while (XUartLite_Recv(&xu, &c, 1) != 1) {
  }
  buf[i] = c;
 }
    return 0;
}

static int func_write(const char *buf, int cnt)
{
 int i;
 for (i = 0; i < cnt; i++) {
  u8 c = buf[i];
  while (XUartLite_Send(&xu, &c, 1) != 1) {
  }
 }
    return 0;
}

static int func_callback(const char *text, void *extobj)
{
    fprintf(stdout, "The user input text is '%s'\n", text);
    return 0;
}

int main()
{
    init_platform();

    u16 UartDeviceId = 1;
    XUartLite_Initialize(&xu, UartDeviceId);
    ntshell_execute(&ntshell, func_read, func_write, func_callback, NULL);

    cleanup_platform();

    return 0;
}

BSPに含まれるライブラリのインターフェースを見て、stdinとstdoutが使えそうかと思う方もいるかもしれません。 実際にやってみるとわかるのですが、stdoutは通常のlibcと同様バッファリングされているのでNT-Shellで使うのには向きません。fflush(stdout)してもバッファはフラッシュされませんでした。そこで上記のようにXUartLiteのAPIをそのまま呼んでいます。


同様にしてXilkernel上での実装も可能ですので、履歴編集機能を持つシェル(風味)インターフェースを気軽に追加する事ができます。

ちなみに、ハードウェアプラットフォーム211に対してのBSPを合わせて以下の容量になりました。

   text    data     bss     dec     hex filename
  69044    1428    8780   79252   13594 hello_world_0.elf

2012年4月29日日曜日

kz_h8writeとkz_xmodemの2つのソフトウェアを、Mac OSとLinuxとWIndowsの3つのプラットフォームで動作確認する

XMODEM for KOZOS (12ステップで作る 組込みOS自作入門 KOZOS用ユティリティ kz_xmodem)を実装したことで、kz_h8writeとあわせてKOZOS用ユティリティが2つになりました。

ダウンロードはプロジェクトページからどうぞ。
http://sourceforge.jp/projects/kz-h8write/
http://sourceforge.jp/projects/kz-xmodem/

kz_h8writeとkz_xmodemは、Mac OS、Linux、Windowsで動作するように実装してあります。
ただ、あまり積極的にテストしていなかったので、2つのツールが揃ったところでまとめてテストしました。
  • それぞれのバイナリは2012/04/29現在のコードから生成したものを使用しています。
  • Mac OSには10.7.3、LinuxにはUbuntu 11.04、WindowsにはWindows 7を使っています。
  • 各環境で特別な手順を踏まずに手に入るビルド環境を使っています。
OSkz_h8writekz_xmodem
Mac OS
Linux
Windows

2種類のソフトウェアを3つの環境で動作させるわけですから意外に手間がかかります。
クロスプラットフォームともなると自動テストというわけにもいきません。
ターゲットボードにあるスイッチを切り替えなければならないとなればなおさらです。

それでも自分で確認しておくと安心度が違います。
これからもこういった動作確認の報告はしていきたいところです。

2012年4月28日土曜日

XMODEM for KOZOS (12ステップで作る 組込みOS自作入門 KOZOS用ユティリティ kz_xmodem)

「KOZOSってなぁに?」という方は本家のホームーページをご覧下さい。

kz_xmodem

12ステップで作る組込みOS自作入門(KOZOS)の第8ステップ以降では、ROMにブートローダ(kzload)を書き込み、ブートローダ経由でOSRAMに流し込んで動作させるようになっています。転送プロトコルにはXMODEMを採用していますが、複数のプラットフォーム上に存在する汎用ツールの多くが、何ステップかの操作をした後でようやく転送が開始されるような仕組みになっています。また、環境やタイミングに依存して、汎用ツールとKOZOSブートローダの組み合わせでうまく転送できない事もあるようです。
kz_xmodemは、上記の「複数の操作を段階的に行なう事の煩雑さ」や「転送に失敗する事の面倒さ」などを取り除く事を目的に、設計実装されたKOZOS専用XMODEMプログラムです。

kz_xmodemの特徴

l  KOZOSブートローダの動作に合わせて設計したKOZOS専用XMODEMプログラム。
l  LinuxWindowsMac OSに対応。
l  KOZOSブートローダに対して内部でload状態に自動遷移。
l  ターゲットをリセットしてkz_xmodemを実行するだけで転送完了。
l  コマンドプログラムなのでMakefileからの自動実行なども可能。
l  MITライセンスを採用し、商用、非商用を問わず自由に再利用可能。

汎用ツールとkz_xmodemの比較

汎用ツールの場合

汎用ツールの場合、操作は大まかにわけて3段階必要です。
まず、KOZOSブートローダをload状態にします。

次にXMODEM転送の為にファイルを指定して転送を開始します。
ツールによっては、ファイル名の入力などに手間取ると期待したような動作にならない事があるようです。うーん。ここは気合いを入れて操作したいところではありません。

転送が正常に終了したらKOZOSブートローダのプロンプトに戻ってrunコマンドを実行します。


上記のように汎用ツールの場合、ツールの中の画面をいちいち行ったり来たりしなければなりません。OSの動作確認をしたくてうずうずしている時に、色々な操作が伴いとても面倒です。

kz_xmodemの場合

kz_xmodemを使うと、シンプルな2つのステップでOSの動作確認まで進めます。
kz_xmodemに、書き込み対象ファイル名と使用するシリアルポート名を与えて実行します。

書き込みが完了したらKOZOSブートローダにシリアルコンソールを使って接続します。
後はrunコマンドを実行するだけ。

kz_xmodemはコマンドツールですから、Makefileでビルド後に呼ぶようにすれば自動ダウンロードが可能です。要するに、ビルドした後でOSの動作確認に至るまでがmake一発で一気にできてしまうのです。従来のようにわざわざ別のツールを立ち上げてからアレコレ操作する必要はありません。

プロジェクトページ

プロジェクトはsourceforgeで管理しています。 http://sourceforge.jp/projects/kz-xmodem/

2012年4月8日日曜日

WAVファイルを手軽にきちんと扱いたい!

WAVファイルを手軽にきちんと扱いたい!

WAVファイルを手軽にきちんと扱いたくなったので仕様や巷の実装を調べていました。

WAVファイルはチャンク構造になっていて、内部データ表現を自由に選ぶ事ができます。
自由に選ぶ事が出来ると言う事は、それなりにきちんと設計実装したプログラムでなければ正しく扱えない事を意味します。


手元にあった書籍(名前は出しませんが)や巷の実装を見ると、随分と厳しい暗黙の前提条件が用いられていて、色々なWAVファイルをあまり正しく読める事を期待できないなぁという感じでした。

また、データ保持の方式が「スタックにずんっと置く」ような実装だったり、「ファイルサイズに応じて超巨大なメモリをヒープから取る」ような実装だったりと、いくら巨大なメモリがPCに搭載されている昨今とは言え、何だかなぁ~という感じです。

そこで、巷の実装例を少し紹介した後で、設計実装したWAVファイルライブラリを御紹介します。

巷の実装を見てみる

今回設計したライブラリの紹介の前に、巷の実装を見てみましょう。
実装をそのまま掲載する事はできないので、ちょっと修正してあります。

巷の実装例(とある書籍のサンプルコード)

この実装は、出現するチャンク構造の順序に暗黙の前提条件が使用されている例です。

  fp = fopen(file_name, "rb");
  fread(riff_chunk_ID, 1, 4, fp);
  fread(&riff_chunk_size, 4, 1, fp);
  fread(riff_form_type, 1, 4, fp);
  fread(fmt_chunk_ID, 1, 4, fp);
  fread(&fmt_chunk_size, 4, 1, fp);
  fread(&fmt_wave_format_type, 2, 1, fp);
  fread(&fmt_channel, 2, 1, fp);
  fread(&fmt_samples_per_sec, 4, 1, fp);
  fread(&fmt_bytes_per_sec, 4, 1, fp);
  fread(&fmt_block_size, 2, 1, fp);
  fread(&fmt_bits_per_sample, 2, 1, fp);
  fread(data_chunk_ID, 1, 4, fp);
  fread(&data_chunk_size, 4, 1, fp);

この実装の場合、出現するチャンク構造の順序が異なるだけで全く正しい処理ができません。
そして、この後の処理が以下のようになっています。

  pcm->fs = fmt_samples_per_sec;
  pcm->bits = fmt_bits_per_sample;
  pcm->length = data_chunk_size / 2;
  pcm->s = calloc(pcm->length, sizeof(double));

data_chunk_sizeに期待するチャンク構造のチャンク・データ・サイズが格納されているとは限りません。そしてその値をそのまま使ってcallocしているので危ないです。

このコードは落第点です。
でも音を扱うという書籍のサンプル実装です。

この実装の危ない点は、間違っていても処理がどんどん進んでしまう事です。
そして、間違っている場合、何がどう間違っているのか検知していないので、APIの外から見た場合にどうしようもありません。

巷の適当に実装されたコードは意外にこういうのばかりであまり再利用には向いていません。
仮に入門者がこのコードを見て育った時に、「いつこの設計実装レベルから抜け出せるのだろう?」と考えると少し心配になってしまいます。

Tiny WAV I/O Moduleプロジェクト

Tiny WAV I/O Moudleプロジェクトとは、小規模組み込みシステムで使用可能なWAVファイルライブラリを作ろうという目的で作られたプロジェクトです。

今回実装したライブラリは、その設計の前段にあたるもので、「どういったインターフェースなら使いやすいかなぁ」というのを検討する為のもの。libcが使用可能な環境でのみ使用可能です。成果物はTiny WAV I/O Moduleプロジェクトのlibc-basedに追加しました。

まぁ、ざっくり言うとlibc-basedは「パソコンで使えるお手軽WAVライブラリ」ですね。

wavfileモジュールの特徴

以下に今回設計実装したお手軽WAVライブラリ、wavfileモジュールの特徴を示します。

  • 省メモリ設計。(ライブラリ側では巨大なメモリを要求しない。)
  • ファイル形式に依らず0.0から1.0で正規化されたデータ入出力インターフェースを採用。
  • 複数チャネルデータに対応。
  • ヘッダの実装詳細を把握しなくても使える。
データをどこに配置するのかについてはアプリケーション層が決めたい事の一つです。
巨大なメモリを勝手にアロケーションするようなライブラリは使いづらくて仕方ありません。
よってwavfileモジュールでは、これらに関知しないようにインターフェースを設計しました。

また、WAVファイルの処理を実装する時に意外に面倒なのが、データ形式の違いです。

今回のwavfileモジュールでは、ファイルのデータ形式によらず0.0から1.0で正規化されたデータ入出力インターフェースを採用しました。これによって、「8ビット形式のファイルは0から255で1バイトだよね。」とか「16ビット形式のファイルは-32768から32767で2バイトだよね。」とか考えなくて済みます。とにかくサンプルを1つ得るインターフェースを呼ぶだけで良いのです。

typedef struct {
    uint16_t num_channels;
    double channel_data[WAVFILE_MAXIMUM_CHANNELS];
} wavfile_data_t;

wavfile_read_dataを呼ぶと上記の構造体にサンプルデータが格納されて返ってきます。
channel_dataの中身は先の規格化された値が入っている事になります。

wavfileモジュールのインターフェース

wavfileモジュールを使うために必要なインターフェースは、リード用、ライト用と合わせてもたったの6つです。

WAVFILE *wavfile_open(const char *filename, WavFileMode mode, WavFileResult *result);
WavFileResult wavfile_read_info(WAVFILE *p, wavfile_info_t *info);
WavFileResult wavfile_read_data(WAVFILE *p, wavfile_data_t *data);
WavFileResult wavfile_write_info(WAVFILE *p, const wavfile_info_t *info);
WavFileResult wavfile_write_data(WAVFILE *p, const wavfile_data_t *data);
WavFileResult wavfile_close(WAVFILE *p);

基本思想

infoとdataの2段階で簡単に実現しちゃうよ!というのが本ライブラリの基本思想です。

読み込み
  • wavfile_openにファイル名とWavFileModeReadを与えてオープン。
  • wavfile_read_infoでヘッダ情報を読み込む。
  • wavfile_read_dataでデータを読み込む。 (必要に応じて繰り返す)
  • wavfile_closeでクローズ。
書き込み
  • wavfile_openにファイル名とWavFileModeWriteを与えてオープン。
  • ヘッダ情報を設定する。
  • wavfile_write_infoでヘッダ情報を書き込む。
  • データを設定する。
  • wavfile_write_dataでデータを書き込む。(必要に応じて繰り返す)
  • wavfile_closeでクローズ。
どんな風に使えるの?

単にデータを読みたい場合

よくあるやりたい仕事の1つが、とにかくデータを読んでみたい!というものです。
Tiny WAV I/O Moduleのlibc-based実装を使えば簡単に実現できてしまいます。

WavFileResult result;
wavfile_info_t info;
wavfile_data_t data;
WAVFILE *wf = wavfile_open("YourWavFileName.WAV", WavFileModeRead, &result);
if (wf != NULL) {
    wavfile_read_info(wf, &info);
    while (1) {
        wavfile_read_data(wf, &data);
        if (data.num_channels == 0) {
            // 読むべきデータが無くなったらチャネル数に0を返してくる。
            break;
        }
        // ここでデータを確認すれば良い。
    }
    wavfile_close(wf);
}

ソースコード

ソースコードは、以下からダウンロード可能です。
http://pt.sourceforge.jp/projects/tinywavio/

ライセンスはMITです。

今後の計画

最近は色々と忙しくて基板設計まで手が回らないので、今年は中途半端に拘るのを諦め、外販されているモジュールを活用してシステム物をやりたいなぁと計画しています。

実は、金子システム株式会社さんからUMB-SSM2603なるものが販売されています。


そうなのです。
このモジュールはACB-BF592と組み合わせてもミニサイズなのです。


動作も高速なので、実は今回のlibc-basedな実装もそのまま動かせたりして?!なんて甘い考えを持っていたりします。ファイルシステム周辺は何らかの抽象化層に乗せようかなぁ。

とにかく組み込み装置としてエレガントにWAVを扱えるようしていこうと考えています。

2012年3月25日日曜日

私が好んで使用しているクロス・プラットフォームの実装方法(シリアル・ポート・ライブラリの実例)

クロス・プラットフォーム!

プロフェッショナル・ソフトウェア・エンジニアの場合、自分が書いたコードが一体何のプラットフォームに依存しているのかを意識して書くのが通例です。この意識の下で設計実装する事で、異なるプラットフォームへの移植要求に対して柔軟に対応できます。結果的に機能実現に投資した労力に対する効果を最大にする事ができます。まぁ、簡単に言うと喜んで頂ける方が増えるわけです。

クロス・プラットフォームの実装方法にはいくつも方法があります。
今回は私が好んで使用している方法について述べたいと思います。

ソースコードのダウンロード

今回の説明に用いたソースコードは以下からダウンロードできます。
ダウンロードはこちらから。
  • ライセンスはMITです。
  • 実際にkz_h8writeの下層で使用しているシリアル・ライブラリです。
  • 好ましいとは言えない例は、ここでは掲載しませんので実例を探してみて下さい。
好ましいとは言えない例

クロス・プラットフォームの実装方法の中で、実利的でありながらあまり好ましくない例があります。
例えば、シリアルポートに対する制御を行なうライブラリをWindowsやLinuxで提供する事を考えてみましょう。

好ましいとは言えない例の場合、例えば以下のような感じの実装だったりします。

<シリアルポートをオープンする関数>
#ifdef WIN32
    // Windows用のシリアルポートをオープンする処理
#else
    // Linux用のシリアルポートをオープンする処理
#endif

<シリアルポートに書き込む関数>
#ifdef WIN32
    // Windows用のシリアルポートに書き込む処理
#else
    // Linux用のシリアルポートに書き込む処理
#endif

<シリアルポートから読み込む関数>
#ifdef WIN32
    // Windows用のシリアルポートから読み込む処理
#else
    // Linux用のシリアルポートから読み込む処理
#endif

<シリアルポートをクローズする関数>
#ifdef WIN32
    // Windows用のシリアルポートをクローズする処理
#else
    // Linux用のシリアルポートをクローズする処理
#endif

どうですか?
見やすいでしょうか?

Windows向けの実装に目を通している時でも、Linux向けのコードも一緒に目に入ってきます。

万が一Linux向けのコードを知らずにいじってしまったらどうなるでしょう?
Windows向けの機能修正後に知らずにコミットした場合には目も当てられません。

Windows向けの機能修正後にLinux側のテストもしてくれるでしょうか?
本来は「テストして当たり前」の事なのですが、実際に期待通りテストしてくれるとは限りません。

フローが曖昧な現場やプロジェクトもあれば、テストしない乱暴な人もいるからです。
プロジェクトにおいて緊急対応などになればなおさらです。
そんな事はあってはならないのですが、実際にはよくある話です。


残念な事に、実際の開発現場では上記のようなコードが多く存在します。

業務上の開発において、あまりこの手のコードを信用する気になれません。
ペアになって存在すべきテスト用コードが無ければなおさらです。

プラットフォームの切り替えがコードに記述されているという事は、依存する定義ファイルへのincludeも#ifdefで切り替えている事になります。対象プラットフォームのビルド環境に依存して、依存する定義ファイルが異なる場合、上記のプラットフォーム切り替え以外の#ifdefも混在します。
プラットフォーム依存コードだけの問題でない事になります。

また、上記の実装はソースコードがビルド・スクリプトにも依存している事を暗に示しています。
これも私が上記のような実装を好まない理由の1つです。
実装詳細を知らないとビルド・スクリプトが書けない事を意味し、更にこの手のコードが数十個、数百個存在して、なおかつそれらが相反する要求になっていた場合どうなるのだろう?と考えただけで嫌になります。

例えばWIN32なの?それともWINDOWSなの?えーとWINなの?とかビルド・スクリプトを書く時に考えたくないですよね?

そして#ifdefの中身が実際にコードとしてコンパイルされているのか?などと余計な事を考えなくてはなりません。これは本来考えなくて良い内容です。

私が好んで使用している方法

私は好ましいとは言えない例に挙げたようなコードを実際の開発現場で多く見てきました。

更にこういった実装をしている下層ライブラリを使う事になった場合、本当に不安になるわけです。
不安の背景には、先に挙げた小さな要素が複数存在します。

私の場合、以下のようなアプローチで先に挙げた不安をできるだけ解消するようにしています。
  • インターフェースはヘッダで、実装の詳細はソースで、を厳密に扱う。(当たり前) 
  • プラットフォーム毎に実装を分離する。プラットフォーム切り替えのために#ifdefは使わない。
  • ハンドラを賢く実装する。
Cの実装においてインターフェースはヘッダで、実装の詳細はソースでというのは当たり前の話でが、実際に実装されているコードを見ているとあまり厳密に扱っていない例を多く見かけます。

ヘッダで定義する内容や外部依存定義を最小限にする事で、外部依存が最小で移植性の高いソフトウェア・コンポーネントが実現できます。厳密なインターフェースの定義に対して、それぞれの実装の詳細を提供する事で機能を実現するのが基本です。

私の場合、ヘッダに対する実装という形で「ファイル名を見ただけで想像できる」ように名称をつけるようにしています。


Windows向けのライブラリの実装は、概ね以下のとおりです。

<シリアルポートをオープンする関数>
    // Windows用のシリアルポートをオープンする処理
<シリアルポートに書き込む関数>
    // Windows用のシリアルポートに書き込む処理
<シリアルポートから読み込む関数>
    // Windows用のシリアルポートから読み込む処理
<シリアルポートをクローズする関数>
    // Windows用のシリアルポートをクローズする処理

Linux向けのライブラリの実装は以下です。

<シリアルポートをオープンする関数>
    // Linux用のシリアルポートをオープンする処理
<シリアルポートに書き込む関数>
    // Linux用のシリアルポートに書き込む処理
<シリアルポートから読み込む関数>
    // Linux用のシリアルポートから読み込む処理
<シリアルポートをクローズする関数>
    // Linux用のシリアルポートをクローズする処理

先のプラットフォーム混在時の実装モデルと見比べて分かるとおり、シンプルな実装が2つのプラットフォームに対して提供されている事がわかります。そして、互いの実装は異なるファイルに実装されています。

これが「インターフェースはヘッダで、実装の詳細はソースで、を厳密に扱う。(当たり前) 」と「プラットフォーム毎に実装を分離する。プラットフォーム切り替えのために#ifdefは使わない。」の実例です。


当然ですがヘッダにはプラットフォーム依存の内容は一切書けません。
例えば、プラットフォーム依存の要素はどのように実装するのでしょうか?

例えば、ハンドラひとつとってみてもWindowsとLinuxで事情が異なるかもしれません。
Windowsの場合HANDLEが使用され、Linuxの場合intが使用されます。

これから示すのが「ハンドラを賢く実装する。」の答えです。

ヘッダでは「ハンドラの構造体をSERIALという名前の型として定義するだけ」の実装です。
ヘッダでは実装の詳細について触れず、「とにかくstruct serialをSERAILとして扱う事だけ」を宣言します。


では、実装側はどうなっているのか?を見てみます。
まずはWindows用の実装です。

そしてLinux用の実装です。
(Linux用の実装はWindowsと異なり端末状態の復旧やミューテックスが含まれています。)


こんな感じで2つのプラットフォームに対する実装は、それぞれ異なる物を提供していますが、上位からは2つの差異を気にすることなく使用できます。(Windows側はちょっと手抜き感がありますが・・・。)


設計実装の判断基準

私の設計実装の判断基準の1つは「その設計や実装がN個になったらどうなるのか?」です。

先の例ではプラットフォームはWindowsとLinuxの2つでした。
では、3つ目のプラットフォームが出てきた場合どうなるのでしょうか?
あまり好ましい実装になりそうにありませんよね?

私のアプローチの場合、3つ目のプラットフォームが現れた場合、新たにファイルを1つ追加するだけで済みます。もちろん他のプラットフォームの実装には一切影響を与えません。

こんな感じで設計実装の判断を行なっています。
様々なアプローチに対して設計実装の判断基準を明確にしておくと、設計や実装の度に迷う必要がなくなります。

まとめ

今回はシリアル・ポート・ライブラリを実例に挙げ、異なるプラットフォームに対してどのように実装を提供するとシンプルにまとめる事ができるのか?について示しました。

2012年2月22日水曜日

小規模組み込みシステムでも使えるBMPライブラリ(Tiny BMP I/O)

はじめに

昨年の今頃に着手したBlacktank LPC1769からは複数の派生プロジェクトが生まれています。
Natural Tiny Shell (NT-Shell)もその一つですが、今回は小規模組み込みシステムでも使えるBMPライブラリ(Tiny BMP I/O)について触れます。


これは何?




Tiny BMP I/Oは、BMP Data StreamからBMPイメージを読み込んでPixel Storageにピクセル値を格納したり、Pixel Storageからピクセル値を読み込んで、BMP Data StreamにBMPイメージを書き込んだりすることのできるライブラリです。

一般に書かれたライブラリでは、「BMPデータがどこに格納されているのか=ファイルに格納されている」、「ピクセルデータがどこに格納されているのか=メモリ」という暗黙の前提が使われている事が多いのですが、Tiny BMP I/Oはそれらをシステム依存として捉えて設計してあります。

システムに依存する部分をきちんと切り出す事で「1つのライブラリでSDからも、フラッシュからもSRAMからだって読める!」、「カメラから読んだ画像を簡単にBMPファイルにできる!」など、色々な用途で使用する事が可能になります。

例えば、「SDカードにあるファイルを読み込んで液晶に表示したい」とか「カメラから取り出した映像をファイルに書き込みたい」などはすぐに考える事のできる応用例の1つです。


また、それだけではつまらないのでbmpimg_tというキャンバスに対して描画可能なプチユティリティも装備しました。描きたいパターンをさっと実現してファイル化する事が可能です。


int bmpimg_draw_box(bmpimg_t *p, const int x1, const int y1, const int x2, const int y2, bmpcol_t *color);
int bmpimg_fill_box(bmpimg_t *p, const int x1, const int y1, const int x2, const int y2, bmpcol_t *color);
int bmpimg_draw_string(bmpimg_t *p, const int x, const int y, const int size, const char *text, bmpcol_t *color);
int bmpimg_draw_line(bmpimg_t *p, const int x1, const int y1, const int x2, const int y2, bmpcol_t *color);
int bmpimg_draw_pixel(bmpimg_t *p, const int x, const int y, bmpcol_t *color);

機能
  • 24ビットBMPファイルの入出力に対応。
  • 特定プラットフォームに非依存。
  • 便利な描画用ユティリティ付属。
  • その他。
簡単に使えます

Tiny BMP I/Oはシステムに依存する以下の機能を実装するだけで簡単に使えます。
  • データのリード、ライト関数
  • ピクセルのリード、ライト関数
データのリード、ライト関数は、BMPのイメージが直列化されて格納されている領域を、ストリームとして入出力するための機能を提供するものです。
ピクセルのリードライト関数は、画像を実際に表示したりするための入出力機能を提供するものです。

ポイント

この手のライブラリは世の中に溢れています。
「また車輪の再開発か」と言われそうですが、必ずしもそうではありません。

ここではTiny BMP I/Oで考慮されている点を挙げてみます。
  1. データがどこに格納されているのか?に依存しない。
  2. 画像ピクセルがどこに、どのように格納されているのか?に依存しない。
データがどこに格納されているのか?に依存しない。

小規模組み込みシステムでは、データが必ずファイルシステム上に存在するとは限りません。
一般に流通しているライブラリの多くは「データはファイルシステム上のファイルとして存在する事」を前提に設計実装されています。これらのライブラリを使用する場合、小規模組み込みシステムにファイルシステムとlibcをポートする事になります。

Tiny BMP I/Oは、データがどこに格納されいるのか?についての前提を持ちません。
それがファイルシステム上であろうが、フラッシュメモリ上であろうが、SRAM上であろうが、何の問題もありません。どこに格納されていても

画像ピクセルがどこに、どのように格納されているのか?に依存しない。

ここで言う「画像ピクセル」とは、データを読み込んで解釈した映像の1つ1つのピクセルを指します。
「画像ピクセルがどこに格納されているのか?」ですが、普通に考えるとメモリ上に格納するわけですが、ちょっとした画像ファイルでも結構な容量になってしまいます。例えば、1920x1080ピクセルでR, G, Bの各レイヤーが8ビットだったとすると、1920x1080x1x3=6,220,800[Bytes] (約6[MB])になってしまいます。潤沢なメモリが対象システムに存在するかどうかはシステム次第です。

読み込んだデータをそのままディスプレイに表示させたいだけの場合もあります。
この場合、わざわざ読み込んだデータをメモリ上に展開する必要はありません。
そのまま表示してしまえば良いのです。

巷に出回っているBMPライブラリの多くは、画像ピクセルデータをメモリに展開する事を前提で書かれているため、小規模組み込みシステムでそのまま使用することは難しい事があります。
Tiny BMP I/Oはシステムに対するインターフェースを規定して設計してあります。
このインターフェースさえ守れば、画像ピクセルがどこにどのように格納されていても問題ありません。

ライブラリとサンプルプログラムのダウンロード

ライブラリとサンプルプログラムはここからダウンロードできます。

サンプルプログラムの動作とその出力画像

サンプルプログラムでは、インターフェースの動作を的確に示すために、汎用プラットフォームで動作するコードを示しています。このコードを見れば、Tiny BMP I/Oが規定している入出力インターフェースを簡単に理解することができます。

実際にTiny BMP I/Oを使って出力したサンプル画像を示します。



サンプルプログラムでは、汎用プラットフォーム向けの実装を提供していますが、Tiny BMP I/Oのインターフェースに従った実装をプラットーフォーム向けに提供しているだけです。

プラットフォームに依存した部分を書き換えれば色々なプラットフォームで使うことができます。

/**
 * @file sample.c
 * @author Shinichiro Nakamura
 * @brief 小規模組み込みシステム向けBMP I/Oのサンプル実装。
 */

/*
 * ===============================================================
 *  Tiny BMP I/O Module
 *  Version 0.0.1
 * ===============================================================
 * Copyright (c) 2010-2011 Shinichiro Nakamura
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * ===============================================================
 */

#include 
#include 
#include "pattern.h"

int main(int argc, char **argv);

/**
 * @brief 独自に規定したキャンバス構造。
 * @details
 * このサンプルでは、汎用プラットフォーム向けなのでメモリ上に画像を格納する。
 */
typedef struct {
    int w;              /**< 横方向サイズ。 */
    int h;              /**< 縦方向サイズ。 */
    bmpcol_t *buffer;   /**< バッファへのポインタ。 */
} canvas_t;

/**
 * @brief ストリームからデータを読み込む。
 * @details
 * インターフェースでは、何からどのように読み込むかについて一切感知していない。
 * この関数では、何からどのように読み込むかについて解決する。
 *
 * @param buf バッファへのポインタ。
 * @param size 読み込みバイトサイズ。
 * @param extobj ユーザが指定した拡張オブジェクト。
 */
int func_fread(void *buf, const unsigned int size, void *extobj)
{
    FILE *fp = (FILE *)extobj;
    return fread(buf, size, 1, fp);
}

/**
 * @brief ストリームへデータを書き込む。
 * @details
 * インターフェースでは、何にどのように書き込むかについて一切感知していない。
 * この関数では、何にどのように書き込むかについて解決する。
 *
 * @param buf バッファへのポインタ。
 * @param size 書き込みバイトサイズ。
 * @param extobj ユーザが指定した拡張オブジェクト。
 */
int func_fwrite(const void *buf, const unsigned int size, void *extobj)
{
    FILE *fp = (FILE *)extobj;
    return fwrite(buf, size, 1, fp);
}

/**
 * @brief ピクセル値を書き込む。
 * @details
 * インターフェースでは、何にどのように書き込むかについて一切感知していない。
 * この関数では、何にどのように書き込むかについて解決する。
 *
 * @param x X座標。
 * @param y Y座標。
 * @param r 赤。
 * @param g 緑。
 * @param b 青。
 * @param extobj ユーザが指定した拡張オブジェクト。
 */
void func_pixel_write(const int x, const int y, const uint8_t r, const uint8_t g, const uint8_t b, void *extobj)
{
    canvas_t *canvas = (canvas_t *)extobj;
    bmpcol_t *buffer = canvas->buffer + (canvas->w * y) + x;
    buffer->r = r;
    buffer->g = g;
    buffer->b = b;
}

/**
 * @brief ピクセル値を読み込む。
 * @details
 * インターフェースでは、何からどのように読み込むかについて一切感知していない。
 * この関数では、何からどのように読み込むかについて解決する。
 *
 * @param x X座標。
 * @param y Y座標。
 * @param r 赤。
 * @param g 緑。
 * @param b 青。
 * @param extobj ユーザが指定した拡張オブジェクト。
 */
void func_pixel_read(const int x, const int y, uint8_t *r, uint8_t *g, uint8_t *b, void *extobj)
{
    canvas_t *canvas = (canvas_t *)extobj;
    bmpcol_t *buffer = canvas->buffer + (canvas->w * y) + x;
    *r = buffer->r;
    *g = buffer->g;
    *b = buffer->b;
}

int main(int argc, char **argv)
{
    const int imgw = 1280;
    const int imgh = 720;
    canvas_t canvas;
    bmpimg_t bmpimg;
    FILE *fp;

    /*
     * 画像ピクセルを格納する領域を確保する。
     */
    canvas.w = imgw;
    canvas.h = imgh;
    canvas.buffer = (bmpcol_t *)malloc(sizeof(bmpcol_t) * imgw * imgh);

    /*
     * 開始処理。
     *
     * ピクセル入出力関数を渡して初期化する。
     * ユーザが指定可能な拡張オブジェクトに、独自に規定したキャンバスを渡しておく。
     */
    bmpimg_open(&bmpimg, imgw, imgh, func_pixel_write, &canvas, func_pixel_read, &canvas);

    /*
     * サンプルの実装では、ファイルから読み込む。
     */
    fp = fopen("input.bmp", "rb");
    if (fp != NULL) {
        bmpimg_bmp_read(&bmpimg, func_fread, fp);
        fclose(fp);
    }

    {
        /*
         * パターン1を書き込む。
         */
        pattern_sample1(&bmpimg);

        /*
         * サンプルの実装では、ファイルに書き込む。
         */
        fp = fopen("sample1.bmp", "wb");
        if (fp != NULL) {
            bmpimg_bmp_write(&bmpimg, func_fwrite, fp);
            fclose(fp);
        }
    }

    {
        /*
         * パターン2を書き込む。
         */
        pattern_sample2(&bmpimg);

        /*
         * サンプルの実装では、ファイルに書き込む。
         */
        fp = fopen("sample2.bmp", "wb");
        if (fp != NULL) {
            bmpimg_bmp_write(&bmpimg, func_fwrite, fp);
            fclose(fp);
        }
    }

    /*
     * 終了処理。
     */
    bmpimg_close(&bmpimg);

    /*
     * 画像ピクセルを格納する領域を破棄する。
     */
    free(canvas.buffer);

    return 0;
}