前回導入したATtiny2313というMCUチップなのですが(下図)、兎に角、メモリ(Flash/RAM)が少なくて、不用意にプログラムを組むと、簡単にメモリを溢れさせてしまいます。治水工事(省メモリでプログラミングするテクニック)が必要です。
加えて、プログラムやシステムのデバッグ中に利いて来るのがシリアルモニタだと気が付きました。UNO等を使ったシステムのデバッグでは、「考え方間違ってないよね?」「正しく計算できた?」「ここの条件成立してるの?」等々、デバッグに必要な情報をSerial.print()などを使ってシリアルモニタに出力することで、PC画面で情報を確認しながらデバッグを進めることが出来ました。
ATtiny2313でも、同様なデバッグ環境があると力強いのですが、前回の実験で判ったのは、「Serialライブラリの物量が思いの他大きい」と言う事でした。シリアルモニタを使って"hello, world"と送信する、ただそれだけのことでFlashは半分以上の1,096バイトを使ってしまいましたし、致命的なのはRAMが92バイトも喰っているという事です。
ATtiny2313のRAMは128バイトしか搭載されていないので、その内の92バイトを使ったら、もう他の用途にRAMを使うことはできません。コンパイル時に表示されるRAM使用量は「グローバル変数」用に確保したサイズであって、それ以外にもauto変数(関数内だけで使う変数)やスタックなどにもRAMが必要となるからです。
つまるところ、Serialライブラリを使ってしまうと、他のプログラムが全く入らなくなってしまって、デバッグどころではなくなってしまうのです。
Serialライブラリには、Streamクラスが継承されています。どうも、こいつの物量が大きい様です。print()メソッドなどで数値を文字列に変換して送信できるなど、いろいろと便利に使えるのはとても良いのですが、ATtiny2313での使用に限っては、コードサイズが肥大化してしまって否定的です。
今やりたいのは、デバッグ用にちょっとした情報をPCに送るだけなので、多少使い難くても我慢です。
目標仕様:
ATtiny2313のハードウェアシリアル(USART)ユニットの使い方を簡単に解説しておきます。足りない部分は各自ATtiny2313データシートで補完してください。
3個の制御レジスタ(UCSRA,B,C)に必要なビットをセットします(↓)。
// USART setting
// async, 8 bit, no parity, 1 stop, RXCI enable, RX & TX enable
UCSRA=0x00;
UCSRB=0x80 + 0x18;
UCSRC=0x06;
パリティやストップビット等の設定ビットが含まれているので、必要に応じて変更してください。UCSRBレジスタに送信機能TXと受信機能RXを有効化するビットが2ビット有って、このビットを有効に設定すると自動的にI/OポートがTXD端子/RXD端子として機能するように乗っ取られるので、pinMode()関数などで別途設定する必要はありません。
通信速度(ボーレート)は、データシートに記載の通り、以下の様に計算してUBRRH,UBRRLレジスタに書き込めば終了です。
baud=((F_CPU/16) + baud/2)/baud - 1;
UBRRH=(baud>>8)&0x0f;
UBRRL=baud&0xff;
初期設定はこれで終了。簡単ですね。
※baudが変数だった場合、上コードの「baud=〜」部分はint32の演算になってしまうので、32ビット演算ルーチンがリンクされて、一気にコードサイズが肥大化します。「tser_init (9600);」の様に、baudを定数にすると、「baud=〜」部分はCコンパイラ(GCC)が計算してくれるので、32ビット演算ルーチンをリンクしません。……tser_init()関数がインライン展開されることが条件です。
USARTユニットには、送信データバッファが1段あって、UDRレジスタがそれです。1段しかないので、前に書き込んだデータの送信が終わっていない場合には、新しいデータを書き込むことが出来ません。
これをUCSRAレジスタのUDREビット(第5ビット)で確かめて、送信データバッファが空になってからUDRレジスタへ書き込む必要があります(下コード)。
// polling while UDRE=0
while (!(UCSRA&0x20))
;
// send data
UDR=dat;
送信もこれだけ。簡単ですね(1^^)。
受信データはいつ送られて来るかわからないのと、受信したデータはすぐに読み出してRAM上のバッファ(リングバッファ)に記憶して行かないと、次のデータを受信できずに消失してしまいかねません。そこで、受信した時に割り込みを発生させて、タイムリに受信データを読み出す必要があります。
USARTユニットの受信データバッファは2段あって、UDRレジスタから1個づつ読み出すことが出来ます。読み出したデータはUSARTユニットの受信データバッファ(2段)からは消えてしまうので、RAMに確保したリングバッファに書き込んでおきます。
以下、割り込みサービスルーチン(ISR)のコードです。
ISR (USART_RX_vect)
{
// RXCI interrupt service routine
uint8 nxt_wptr;
// update write pointer
nxt_wptr=tser_wptr + 1;
if (nxt_wptr>=tser_MAXBF)
nxt_wptr=0;
if (nxt_wptr==tser_rptr)
{
// ERROR: buffer full
// discard new data and ignore error for robustness
UDR;
return;
}
// receive data
tser_rbuf[int (tser_wptr)]=UDR;
tser_wptr=nxt_wptr;
}
リングバッファがフルだった場合、新たに受信したデータは消失してしまいますので、バッファフルにならないように制御してください。リングバッファの制御についてはこちらに解説が有ります。
受信制御は少々複雑でしたね(1^^;)。上のコードだと、「ただ受信しただけ」なので、改行コードまで受け取ったり、受け取ったデータを解釈したりと、受信側のコードはどうしてもコードサイズが大きくなります。メモリの限られているATtiny2313では、なるべく使用を控えるべきでしょう。
まず、物量を確認しておきましょう。以下の様なデバッグ情報を送信するコードをsetup()関数内に記述して、Serialライブラリとtinyシリアル関数群でメモリ使用量を比較してみます(↓)。
#ifdef USE_TINY_SERIAL
tser_init (9600);
tser_write (prog_name);
tser_write (F(" ver."));
tser_write (prog_vers);
tser_write (F("\r\n"));
tser_write_dec (dat,' ');
tser_write_dec (-dat,' ');
tser_write_hex (dat,' ');
tser_write_hex (-dat);
tser_write (F("\r\n"));
#else // USE_TINY_SERIAL
Serial.begin (9600);
while (!Serial)
;
delay (10);
Serial.print (prog_name);
Serial.print (F(" ver."));
Serial.println (prog_vers);
Serial.print (dat);
Serial.print (' ');
Serial.print (-dat);
Serial.print (' ');
Serial.print (dat, HEX);
Serial.print (' ');
Serial.println (-dat, HEX);
#endif // USE_TINY_SERIAL
結果を以下表に示します。
シリアル | メモリ使用量[バイト] | 備考 | |
Flash | RAM | ||
Serialライブラリ | 1,994 | 110 |
ギリギリだけど、何とか入った。 ただ、動作せず。 恐らく、RAMの使用量が多過ぎて、 スタックが溢れたものと推定。 |
tinyシリアル関数群 | 1,156 | 27 |
コードサイズ(Flash)は思いの他大きかった。 それでも、−800バイトは大きな優位。 RAMの方は圧倒的に小さいので、まだ他の アプリケーションコードを記述する余地がある。 正常動作も確認できた。 |
ちなみに、tinyシリアルで使用したRAM容量27バイトの内、約17バイトは、以下に示す文字列宣言によるもので、プログラム名やバージョン情報を記憶する必要が無い場合には、削減可能です。
const char prog_name[ ]="tiny_serial";
const char prog_vers[ ]="1.00";
残りはそもそもコアライブラリが消費している分ですので、RAMに関しては、ほぼ手つかずで残っています。これは非常に大きな優位です。
続いて、いつもUNO等のプログラムで使っている様に、シリアルモニタから【who】/【help】コマンドだけを受け付けるプログラムを作りました。LED_BUILTINのLチカを追加してあります。ATtiny2313の20DIPピン番号で#16ピンに適切な電流制限抵抗とLEDを接続すると、パイロットランプとしてチカチカ光ります(↓)。
tinyシリアル関数群プログラム:tiny_serial.ino
※Arduino as ISPを使ったATtiny2313へのプログラム書き込みについてはこちらを参照してください。
「#ifdef USE_TINY_SERIAL」で場合分けした部分(setup()関数/loop()関数内を除く)が今回作成した関数群です。ライブラリ化していないので、他のプログラムのデバッグで使用する場合には、この部分をコピー&ペーストして使用してください。
FTDI等のUSB⇔UART変換モジュール(下図)を使って、ATtiny2313との非同期シリアル(UART)接続を確立しましょう(下表)。
UNO as ISP | 接続 | ATtiny2313 | 接続 | FTDI | |
5V | → | 20 | VCC | ||
GND | ⇔ | 10 | GND | ⇔ | GND |
2 | D0(RXD) | ← | TXD | ||
3 | D1(TXD) | → | RXD | ||
D10 | → | 1 | RESET | ||
D11(MOSI) | → | 17 | D14(MOSI) | ||
D12(MISO) | ← | 18 | D15(MISO) | ||
D13(SCK) | → | 19 | D16(SCK) |
※FTDIの電圧ジャンパは5V側で使用。ATtiny2313 #16ピン(D13)に適切な電流制限抵抗+LEDを接続すると、チカチカ光る。
以下、実験中の様子です。
通常のデバッグ中を想定して、UNO as ISPをSPI接続したまま(頻繁にプログラムを書き込みするため)の状態で、FTDIがATtiny2313の非同期シリアル(UART)ポートとつながっています。Arduino IDEは、UNOのUSBポートを選択しておくのが順当ですから、FTDIのシリアルポートはTera Termなど他のソフトウェアで開いた方が使い易いと思います。
実験サンプル:【 】内はTera Termからの送信データ
tiny_serial ver.1.00
100 -100 0x64 0xff9c
RDY
【help】
OK+ who
OK: help
この状態のメモリ使用量は、Flash 1,592バイト、RAM 53バイトでした。さすがに、シリアルモニタからコマンドを受け付けるのは、メモリ容量的に厳しいかもしれませんね(残りFlash空き容量は400バイト程)。受信関数を使わずに、デバッグ情報をPCへ送信するだけなら何とか使用できるのではないでしょうか?
デバッグ用途としては合格ラインだと思います。
C言語のプログラムは、Cコンパイラ(GCC)がコンパイルしてオブジェクトコードに変換すると、リンカと言うソフトウェアが必要なコードだけを集めて、最終的にArduinoへ書き込むデータを生成しています。リンカは賢いので、C言語のソース中に記述してあっても、使わなかった関数のオブジェクトコードは最終的な書き込みデータには含まれません。
前回ATtiny2313の開発環境(Arduino IDE)を構築したのに引き続き、今回はデバッグを容易にするため、非同期シリアル通信環境を整備してみました。ストレス😣なくデバッグできるかどうか?は、システムを構築する上で非常に重要な要因ですからね。