このプロジェクトは、ライブラリで提供されている標準入出力
(stdio)の使い方を紹介しています。標準入出力がどのように使われるかは標準Cアプリケーションでは基本的知識だと見なしています。ここではOSサポートのある通常のコンピュータ環境に対し、それらがないマイクロコントローラ環境がどのようなものかを中心に解説します。
このデモは 標準入出力解説 の補足となるものですが、これに代わるものではありません。
デモはSTK500開発キットと共に供給されるATmega16上で動作するように設定されています。UARTポートとRS232C「スペア」ポートを接続する必要があります。PD0とRxD、PD1とTxDを接続してください。RS232Cチャンネルが標準入力
(stdin) と標準出力 (stdout) に割り当てられます。
異なるデバイスが標準エラー出力 (stderr) を利用できるように、HD44780互換LCDコントローラを持つ工業標準規格のLEDディスプレイを選びました。このディスプレイはSTK500のポートAコネクタに以下のように接続してください。
Port |
Header |
Function |
 |
A0 |
1 |
LCD D4 |
A1 |
2 |
LCD D5 |
A2 |
3 |
LCD D6 |
A3 |
4 |
LCD D7 |
A4 |
5 |
LCD R/~W |
A5 |
6 |
LCD E |
A6 |
7 |
LCD RS |
A7 |
8 |
使用せず |
GND |
9 |
GND |
VCC |
10 |
Vcc |
LCDコントローラはbusyフラグ監視を含む4bitモードで使われますので、R/~Wラインも接続する必要があります。LCDコントラストを設定するためにもう1つ電圧供給ピン(V5)が必要であるのに注意してください。通常はこのピンはVcc-GND勘に可変抵抗を置き、その可動極につなぎます。時にはこのピンはGNDにつなぐだけで動作することもありますが、どこにも接続しないと通常は表示不可能になります。
LCDを接続するために、1つのポートに7ピン(※8pinあるが、7つしか使わない)あるポートAを選びました。他のポートは一部が予約済みであるからです:ポートBはISPに使われ、ポートCはデバッグに使われるTAG用ピンを持ち、ポートDはUART接続に使われます。
このプロジェクトは以下のファイルで構成されています:
通常は、インクルードファイル指定が最初にあります。便宜のため、システムヘッダファイル
( <>で囲まれたもの) が先に置かれ、アプリケーション独自ののヘッダファイル(ダブルクォート
" で囲まれたもの) がそれに続きます。しかしここでは、defines.h
が先頭ヘッダファイルとしておかれています。その主な理由は、このヘッダファイルが
F_CPU
を定義しており、これは <utils/delay.h>
より前に置かれなければならないからです。※util/delay.hに含まれるマクロ類は
F_CPU
を使用しているため
関数 ioinit()
は全てのハードウェア初期化作業をまとめてあるものです。この関数はstatic宣言がついたモジュール内部のみで有効な関数(stdiodemo.c以外から参照されない)であり、コンパイラはその単純さに着目し、適切な最適化レベルが指定されていればこの関数をインライン関数にします。(※ioinit()内のコードを、呼び出し側のioinit();
と置き換えたような形をとる)
このことはデバッグ時に気にとめておく必要があります。インライン化によりデバッガのシングルステップ実行はあちこちの行にとぶような挙動を示します。
(※参照:プログラムをavr-gdbでシングルステップデバッグするとPC(プログラムカウンタ)がランダムにジャンプしちゃうのは何故?)
uart_str
とlcd_str
の宣言は2つの標準入出力ストリームを形成します。ストリームの初期化は
FDEV_SETUP_STREAM()
初期化テンプレートマクロによってなされ、入出力目的に使える性的オブジェクトが形成されます。この初期化マクロは3つの引数をとります;最初の2つは出力と入力の関数に対応する2つの関数マクロ、それと3つめはストリームの意図(出力、入力、入出力)を定めます。意図する動作から要求されない関数については(出力専用として設定される
lcd_str
に対する入力関数など)、NULLを充てることができます。
ストリーム uart_str
は、RS-232接続のターミナル(ターミナルプログラムが走っているPC)との入出力動作に当たり、lcd_str
ストリームはLCDテキスト表示器への文字出力を供給します。
関数 delay_1s()
はおよそ1秒間プログラム実行を止めておくものです。これは<util/delay.h>
が提供する_delay_ms()
関数を使って実現されています。これはまたループカウンタの調整のため
F_CPU
マクロを必要とします。_delay_ms()
関数は引数の上限があるので ( F_CPU
値に依存) 、CPUクロックが26MHzまで問題ない10msecを与えてベースになるディレイを生成しています。これを100回回して1秒ディレイを生成します。
実践的なアプリケーションでは今回のような長いディレイはハードウェアタイマーを用いて実現する方がベターです。こちらの方法なら1秒待ちの間CPUはフリーになり、他の動作をしたりスリープに入ったりすることができます。
main()
の先頭で、周辺装置の初期化の後、デフォルトの標準入出力ストリームである
stdin
, stdout
, stderr
がすでにある静的FILEストリームオブジェクトを用いて設定されます。これは強制ではないですが、stdin
とstdout
が使えると、簡略な入出力関数が使えます。(たとえばfprintf()
の代わりにprintf()
が使える)。stderr
はデバッグメッセージを送るとき参照されます。
デモンストレーションの目的で??、stdin
と stdout
は、UART入出力を扱うstreamに接続されています。stderr
はLCDテキストディスプレイへデータを出力します。
最後に、メインループが続きます。これはRS-232接続を通して入力された簡単なコマンドを受けて、コマンドに応じたいくつかの簡単な動作を行います。
- まず最初に、
printf_P()
(この関数は引数にプログラムスペース文字列をとります) を使ってプロンプトが出力されます。入力された文字列はfgets()
により1行分ずつ内蔵バッファに読みとります。ここで、gets()
(暗黙にstdinから読み込む関数)を使うことも可能なのですが、gets()
はユーザーの入力が入力バッファをオーバーフローさせないようにコントロールする機能がないため、使われるべきではありません。
- もし
fgets()
が何も読み出せなかった場合は、メインループは先に進みます。通常、マイクロコントローラーアプリケーションのメインループは終了せず無限ループになっていますので、再び巡ってくることになります。デモンストレーション目的により、標準入出力のエラーハンドリングを説明します。
fgets()
は、入力エラーまたは入力のEnd-Of-File状況に出会うとNULLを返します。この両方のコンディション(error,EOF)は、ストリームを形成する関数(ここではuart_putchar()
)により把握されています。手短に言うと、シリアル回線が
"break" 状況(拡張スタートコンディション)だとこの関数はEOFを返します。一般的なPCターミナルプログラムは、この状況をRS232のある種の規格外信号として伝えます??
Both these conditions are in the domain of
the function that is used to establish the
stream, uart_putchar()
in this case. In short, this function returns
EOF in case of a serial line "break"
condition (extended start condition) has
been recognized on the serial line. Common
PC terminal programs allow to assert this
condition as some kind of out-of-band signalling
on an RS-232 connection.
- メインループを離れるとき、さよならメッセージが標準エラー出力(ここではLCD)に送られます。さらに1秒ずつ間を置いて3つのドットが後に続き、最後にLCDをクリアします。
- 最後に、
main()
が終了され、ライブラリは無限ループに入ります。アプリケーションを再開するにはCPUリセットするしかありません。
- 3つのコマンドを認識します。それぞれは入力行の最初の1文字(小文字に変換される)で決定されます。
- 'q' (quit) コマンドはメインループから離脱させる機能を持ちます。
- 'l' (LCD) コマンドは2番目の引数をとり、それをLCDに送ります。
- 'u' (UART) コマンドは2番目の引数をとり、それをUARTに送り返します。
- コマンドの認識は
sscanf()
を使って行われます。フォーマット文字列の最初のフォーマットはコマンド自身をスキップするためにあります。変数への割り当てを抑止する修飾子
* が与えられています。
このファイルはいくつかの周辺機能定義を含んでいます。
F_CPU
マクロはCPUクロック周波数を定義します。これはdelay
loopマクロや、UARTボーレート設定計算で使われます。
UART_BAUD
マクロはRS-232 ボーレートを定義します。CPUクロック周波数に依存し、制限された範囲のボーレートがサポートされます。
残りのマクロ定義はHD44780 LCDドライバのIOポートやピンを定義するものです。
このファイルは低レベルHD44780 LCDコントローラとの仲介をするLCDドライバの汎用インターフェイスを提示します。コントローラを4bitモードで初期化したり、コントローラのビジービットがクリアされるまで待ったり、コントローラと1バイト読み書きを行ったりする外部に公開された関数が利用できます。
コントローラ入出力には2つの異なった書式があります。1つはコマンドを送ったりコントローラの状態を受けたとったり(RS信号がクリアの時)するモードで、もう1つはコントローラのSRAMとの間でデータをやりとりする(RS信号はセット)ものです。これらの単純な関数を実現するマクロが用意されています。
おしまいに、LCDコントローラコマンド全てに対するマクロが用意されています。これによりコマンドwシンボルとして扱えます。HD44780データシートにはこれらの基本的な機能が詳しく載っています。
これは低レベルHD44780 LCD controller driverの実装を行っています。
先頭で、LCDコントローラのハードウェアポートをシンボリックアクセスするためのプリプロセッサの小仕掛けがされています。これはdefines.hの定義に基づいて行われます。
hd44780_pulse_e()
関数はコントローラの E (enable) ピンに短いパルスを送るものです。
コントローラは4ビットインタフェースモードで使用されるので、コントローラからの入出力バイトは2つのニブル(4bit)入出力で扱わなければなりません。関数hd44780_outnibble()
と hd44780_innibble()
はそれを実現しています。これらは外部に公開されないインタフェースで、staticとして宣言されています。
これらの関数の上に、外部に公開された関数
hd44780_outbyte()
と hd44780_inbyte()
があり、コントローラへ/コントローラから1バイト転送します。
hd44780_wait_ready()
はコントローラがREADYになるまで待つ関数です。これはRS=クリアの状態でコントローラの状態を読み出し続け、ステータスバイトのBUSYフラグをチェックすることで実現しています。この関数は他のコントローラ入出力に先立って呼ばれるべきです(※BUSYでない状態まで待つため)。
最後に、 hd44780_init()
はLCDコントローラを 4-bit modeに設定します。これはデータシートにある初期化シーケンスに則っています。この時点ではBUSYフラグの点検はできないので、時間待ちが使われています。コントローラは電源供給の立ち上がり時間の制約を満たしていればパワーオンリセットを行うことはできますが、ここでは常にスタートアップでソフトウェア初期化ルーチンを呼び定められた状態入れるようにしています。この関数はまたインタフェースを4bitモードに設定しています(これはパワーオンリセットで自動的には行われません)。
LCDドライバのハイレベル(文字入出力)インタフェースを宣言しています。
高レベルLCDドライバーです。このドライバーは低レベルLCDコントローラHD44780ドライバの上にあり、文字入出力インタフェースを標準入出力で直接使えるようにしてあります。低レベルHD44780ドライバはコントローラのSRAMアドレスをセットし、コントローラのSRAMにデータを書き込み、ディスプレイクリア・カーソル移動などのコントロール機能を行いますが、この高レベルドライバはLCDにただ文字を書き込むことだけを許可し、これを引き受けて??何らかの表示をディスプレイに行います。
※低レベルLCD関連関数をあれこれ呼ばずに、文字を送るだけでコントロールできるようにしています
このレベルではコントロール文字が扱え、これがLCDに特別な動作を行わせます。今のところ、扱えるコントロール文字は1つだけです:改行文字(\n
) はディスプレイをクリアしてカーソルを初期位置に戻します。こうして
"新しい行"のテキストが表示可能になります。それゆえ、受信された
newlineキャラクタは次文字がアプリケーションから送られてくるまで到着するまで記憶されており、つつき(の文字列)の直前にクリア動作を行います。これにより、行いっぱいのテキストが送られたあと、次の行が表示されるまでは表示され続けるという便利な抽象化??がなされます。
※写真でも分かるように、このプロジェクトは1行LCDを前提にしている模様です。1行ディスプレイでは改行すると表示が消えてしまうので、次の行データの到着まで改行動作を待つ仕様としているようです。2−4行LCDでもこの方が便利かと思われます。
たとえばエスケープシーケンスなど、さらなるコントロール文字の追加も可能でしょう。このようにして、自己スクロール表示などの実装も可能でしょう。
外部公開された関数lcd_init()
は、まず低レベルHD44780ドライバのエントリポイントを初期化し、LCDをのぞみの方法で初期化し(ディスプレイクリア、非点滅カーソル表示、SRAMアドレスは自動増加し文字は左から右に書かれる)ます。
外部公開された関数 lcd_putchar()
は、put関数としてそのポインタを標準入出力初期化関数/マクロ(fdevopen()
, FDEV_SETUP_STREAM()
等 ) に(stdiodemo.c
内で)渡されます。 このput関数 (ここでは lcd_putchar
のこと) は2つの引数をとります。表示する文字自身と、それを表示させるストリームオブジェクトです。put関数は送信に成功すれば0を返すことを期待されます。
この関数は最後の、処理されなかった改行文字をスタティック変数
nl_seen
に記憶しています。改行文字に出会ったとき、この関数は単にこの変数(
nl_seen
)を true にセットし、呼び出し元に戻ります。nl_seen
変数がtrueである間に次の改行以外の文字が到着すると、LCDコントローラへディスプレイクリア、カーソルホームポジション、SRAMアドレスをゼロに戻すよう指令します。そして(改行文字以外の)文字はディスプレイに送られます。(※このとき
nl_seen
はクリアされる)
単独のスタティックな関数内部変数 nl_seen
はこの目的のために働いています。同じドライバ関数で複数のLCDをコントロールする場合はこのやりかたではうまく働きません。複数のディスプレイを区別する方法が必要になります。これが2番目のパラメータ(ストリーム自身への参照)が使われる状況となります。関数内のプライベートな変数を使う代わりに、ストリーム自身の内部に保持できるプライベートなオブジェクトを用いることができます。このプライベートオブジェクトはfdev_set_udata()
でstreamに貼り付けることができます。(たとえばlcd_init()
内で。この場合、streamへの参照を引数として渡す必要がでてきます)。また、lcd_putchar()内で
fdev_get_udata() を用いて参照することができます。
※LCD装置1つにつき1つのストリームを割り当てると言うこと。1つの出力関数で複数の装置に送信する場合、lcd_putcharはどっちの装置に送信するかとか、内部関数はどっち用のものを使うべきかが分かる必要がある。ここでlcd_putcharの第二引数(stream)が生きてくる。たとえばlcd1_str、lcd2_strの2つのストリームを作り、それぞれのストリームに対し
内部作業用の変数をfdev_set_udata()でくくりつけてやれば、lcd_putchar()は、fdev_get_udata()でこの内部作業用の変数を取り出して、表示LCD番号やnl_seen相当のフラグを得ることができる。アドレス参照なので書き込みもOKです。
RS-232 UARTドライバ用の公開されたインタフェース宣言。
lcd.h と似ていますが、文字入力関数も利用可能である点が異なります。
この例ではRS-232 入力は行単位でバッファされており、マクロ
RX_BUFSIZE
でこのバッファの大きさを決定できます。
これは標準入出力互換の RS-232 ドライバで、AVRの標準UART
(もしくは USART の非同期モード) を利用しています。文字出力・文字入力双方が実装されています。文字出力については内部改行文字
\n
を外部表現 return/line feed (\r\n
) に変換されることにご注意ください。
文字入力は行単位バッファされており、carriage
return (\r
) かnewline (\n
) どちらかが受信されて行データが"送られて"(※送信側からみた表現)しまう前なら、行バッファの最低限の編集を可能にしています。実装された編集機能は以下のものがあります:
\b
(back space、BS、0x09) or \177
(delete、DEL、0x7F) は、直前に受信した文字を削除します- ^u (control-U, ASCII NAK) 行バッファ全体をクリアします
- ^w (control-W, ASCII ETB) 直前に受信した一語を削除します。「語」はホワイトスペースで区切られた単位を意味します(※2バイトのことではない)
- ^r (control-R, ASCII DC2) は 、uart_putchar()に
\r
を送り、バッファをreprintします。
結果、編集文字で乱れた端末の画面を書き直すことになります。
\t
(tabulator) は1つのスペース文字に変換されます。
関数 uart_init()
はUARTに必要な全てのハードウェア初期化を担当しています。8ビットデータ/パリティなし/ストップビット1(一般的に8N1と言われるもの)で、ボーレートはdefines.hにて設定されます。低いCPUクロック周波数ではUART設定のU2Xビットをセットし、UARTのオーバーサンプリング機能を16xから8xに減らし、1MHz内蔵RC発振でも9600bps通信を許容範囲内の誤差ににしています。
外部公開された関数 uart_putchar()
は(lcd_putchar同様) 標準入出力ストリームインタフェースが直接使うために適切な引数を持っています。これは
\n
を見つけたときはこれを \r\n
に変換し、再帰コールによって送信します。デモ目的のため、
\a
(audible bell, ASCII BEL) 文字はstderrへ送信するようにしていますので、この文字はLCDに表示されます。
外部公開された関数 uart_getchar()
にはラインエディタが内包されています。line
バッファ内に文字があるときは(ポインタ変数rxpがNULLではない場合)、次の文字はUART受信装置からではなくバッファから返されます。
バッファ内二文字がない場合は、UARTから受信データを読み込む入力ループに入り、得られたデータを適切に処理します。UARTにframing
error (FE
ビットが立ち上がる) が起こった場合、関数は_FDEV_EOF
を用いてEOF状況を返します。フレーミングエラーが起こるのは端末がラインブレークコンディションを送った場合などが代表的です。これは文字の終わりより長くスタートビットが続いた場合=ストップビット位置がHiという形で伝えられます。入力時にデータオーバーラン状況が起こった場合(DORビットが立ち上がる、AVR側のデータ処理が間に合わない場合などに起こる)は、_FDEV_ERR
が返されます。
行編集文字はループ内で扱われます。場合によってはバッファの状態を変化させます。ラインバッファサイズを超えた文字列が入力された場合、越えた分の受け入れは拒否され、文字
\a
がターミナルに送られます。もし \r と \n
文字が受信された場合は変数rxp(受信ポインタ)はバッファの先頭にセットされ、ループは続行し、バッファの最初の文字がアプリケーションに返されます。
※訳注:
1回目のuart_getchar()コール:改行文字が来るまでUART受信ループを続け、行バッファにため込みを行う。改行文字が来たら先頭文字を返す
2回目のuart_getchar()コール:バッファ2番目の文字を返す。UART受信は行わず
:
最後のuart_getcha()コール:バッファ n 番目の文字が\n。これを返すと同時にバッファをクリアする
UARTから受信データを取得するのは1回目のuart_getchar()コールの時=AVR側が入力を要求している時のみです。それ以外の時にUARTに受信があってもとりこぼします。また、アプリケーション側で行データ処理中はUART受信処理を行わないでのでデータは失われます。
割り込みを用いていないこともあり、この1回目のuart_getchar()コールでは送信側が\n/\rを送るまでループを続け、他の処理ができません。たとえfgets()ではなくfgetchar()でコールしたとしても、\nが1行入力が完了するまで返ってこないことに注意してください。
この受信ライブラリは、アプリケーションが端末へプロンプトを送り、それに対して端末から1行入力+改行を送り、その後アプリケーションが端末へプロンプトを返すまで端末から新たな入力を行わないことを前提としています。アプリケーションが入力を要求する前に端末から送信するとデータをとりこぼします。
これらを解決するには割り込み+複数行バッファやリングバッファなどによる処理を必要とします。しかし上記の用途に限定するなら本例の方が単純でコンパクトです。
Automatically generated by Doxygen 1.4.1
on 23 Jan 2006.