【デバッグが超捗る!】Arduinoにシリアルモニタコンソールを実装する
シリアルモニタって出力だけじゃねェから
Arduinoで何かしらデバッグするとき、シリアル通信の出力機能(Serial.print・Serial.write)とシリアルモニタを使ってセンサーの出力値を確認したり変数の中身を確認したり…てことは普通にやっていると思います。
シリアル通信は出力だけでなく入力機能もあるのですが、パソコン相手にデータを送受信するモノを作ってる場合を除いて、入力機能はイマイチ使い所がわからない…という人も多いのではないでしょうか。
この記事では、シリアル送受信を利用した 対話型コマンドコンソール を実装してハードウェアデバッグや検証・動作確認を効率よく行う方法を紹介します。Arduinoの例を紹介しますが、Raspberry Piや一般的なマイコンでも同じような考え方でデバッグを効率化することができます。
対話型コマンドコンソールって何?
対話型コマンドコンソールというのは、WindowsでいうところのコマンドプロンプトとかPower Shell、MacOSでいうターミナルのようなイメージです。
立ち上げるとプロンプトが表示され、コマンドを入力すると何かしらの動作や表示を行って、終わるとまたプロンプトが表示されますね。
これと同じようなものをArduinoに実装してデバッグに活用するのです。
制御対象がせいぜい数個、たとえばセンサーから取得した信号を何かしら計算してLCDに表示する・ある閾値で判定してLEDを点灯させる…という程度ならloop()関数の中に処理をベタ書きすれば良いですが、「センサーからの信号を監視しつつバッテリーの電圧も監視して、もしバッテリーの電圧が既定値以下になったらLEDを定期的に点滅させると同時にセンサーからの信号監視頻度を落とす」…など多くの機能を実装する場合は少々無理が出てきます。
こういう場合は機能ごとに関数化して単体で動作確認した後、少しずつ関数の組み合わせを増やしていくのが一般的な手法です。その際にたとえば内部パラメータを覗いたりパラメータを書き換えてあるモードを試したり…といったことをやれると捗りますね。
そんなときこそシリアル通信を利用した対話型コマンドコンソールを実装して、デバッグに活用します。
中身のパラメータを覗いたり書き換えたりはICE(In Circuit Emulator)を使っても同じことができます。
シリアル通信による対話型コマンドコンソールは送受信がそれなりに遅いので、この遅さが問題になる場合はICEを使うのが良いでしょう。
ただし、使うマイコンによっては大きな制約が伴うことと、カット&トライのデバッグに陥りがちなのでとくに初心者の利用は避けるべきです。 ICEは期待通りに 動かない理由を知るため・動くことを確認するため に使うものであり、 動くようにするため に使うものではありません。
「とりあえずコードのサンプルを見てみたい」「対話型コマンドコンソールの雛形に使いたいから早く成果物をちょうだい」という方は以下のリンク先をご覧くださいませ。
| リンク | |
|---|---|
| Github | arduino_serial_console_tmct |
受信ループを実装する
まずは受信ループを実装します。
Arduinoの場合、setup()関数内でSerialを初期化し、受信ループをloop()関数内に記述します。
シリアルから何も受信されない場合は「受信待ち」の部分で無限ループします。loop()関数内に記述している内容をここに記述すると、コマンドコンソール実装前と同じような動作をするでしょう。
もし[LF]が受信された場合はwhile(true){ }のループから抜けます。
それまでに入力された内容はconsole_buf[]に、入力された文字数はconsole_bufposに格納されているので、ループを抜けた後に入力内容に応じた処理を行います。
void setup() {
Serial.begin(115200, SERIAL_8N1);
Serial.println(F("Debug monitor"));
}
void loop() {
const char console_ppt = '>'; // プロンプト文字
const unsigned char console_len = 15; // 受信バッファの文字数
char console_buf[console_len]; // 受信バッファ
char console_byte = 0; // 受信したデータ
unsigned char console_bufpos = 0; // 受信したデータのバイト数
unsigned char i = 0;
Serial.write(console_ppt);
//-------------------------------------------------------
// 受信ループ
// [LF]が受信されるとループを抜けます
//------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
for (i = 0; i < console_len; i++) console_buf[i] = 0;
while (true) {
//----- 受信待ち -----
while (Serial.available() <= 0) {
//
// データが受信されるまで、ここでループします
// 1バイト以上のデータが受信されるとこのループを抜けます
//
}
//----- 受信されたデータを処理 -----
console_byte = Serial.read();
if (console_byte == 0x0a) {
// [LF]が受信された場合
Serial.write(0x0d); // [CR]
Serial.write(0x0a); // [LF]
break;
}
else if (console_byte == 0x08) {
// [BS]が受信された場合
// 前の文字を消す
if (console_bufpos > 0) {
Serial.write(0x08); // [BS]
Serial.write(' '); // [SP]
Serial.write(0x08); // [BS]
console_buf[console_bufpos] = 0;
console_bufpos--;
}
console_buf[console_bufpos] = 0;
}
else if (console_byte < ' ') {
// その他の制御コードの場合
// 何もしない
}
else {
// その他の文字と思われる場合
if (console_bufpos < console_len) {
console_buf[console_bufpos] = console_byte;
Serial.write(console_byte);
console_bufpos++;
}
}
}
//-------------------------------------------------------
// コマンド処理
// console_buf[] 受信されたバイト列
// console_bufpos 受信されたバイト数
//------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
// 試しに表示してみます
// 受信された中身を表示
for (i = 0; i < console_bufpos; i++) {
if (console_buf[i] == 0) break; else Serial.write(console_buf[i]);
}
// 受信されたバイト数を表示
Serial.write(0x0d);
Serial.write(0x0a);
Serial.println(console_bufpos, DEC);
}
コマンド処理を実装する
先ほどの受信ループでは、入力された内容はconsole_buf[]に、入力された文字数はconsole_bufposに格納された状態でループを抜けます。あとは「コマンド処理」のところでそれをどうにかしてやれば良いのです。
そこでバイト列比較(文字列比較)を実装してー…とかやりたいところですが、バイト列比較はわりと重い処理なのでコマンドコンソールごときで実装するのはかなり大げさです。少々わかりにくくなりますが、if文のネストで対応することにします。
以下にirコマンド・iwコマンドを実装したコマンド処理部の実装例を示します。
この例ではシリアルモニタを115200bps・改行コードを[LF]に設定した後、ir[Enter]で「IR command」、iw[Enter]で「IW command」と表示されます。この部分を書き換えてコマンドを変更・追加したり、パラメータを書き換え・表示などするとデバッグが捗るでしょう。
//-------------------------------------------------------
// コマンド処理
// console_buf[] 受信されたバイト列
// console_bufpos 受信されたバイト数
//------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
if ((console_buf[0] == 'i') || (console_buf[0] == 'I')) {
// 1バイト目 = 'i' or 'I' の場合
if ((console_buf[1] == 'r') || (console_buf[1] == 'R')) {
// 2バイト目 = 'r' or 'R' の場合
Serial.println(F("IR command"));
}
else if ((console_buf[1] == 'w') || (console_buf[1] == 'W')) {
// 2バイト目 = 'w' or 'W' の場合
Serial.println(F("IW command"));
}
}
試しにI2Cコンソールを実装してみる
コマンド処理の実装例として、以下のコマンドをサポートするI2Cのコマンドコンソールを実装してみます。
- iw<SlaveAddress><LocalAddress><Data>
SlaveAddressのデバイスのLocadAddressにDataを書き込みます。
<SlaveAddress>
アクセス対象のスレーブアドレスを2バイトの16進数ASCIIコードで指定します。
<LocalAddress>
デバイス内アドレスを2バイトの16進数ASCIIコードで指定します。
<Data>
書き込むデータを2バイトの16進数ASCIIコードで指定します。 - ir<SlaveAddress><LocalAddress>
SlaveAddressのデバイスのLocadAddressの値を返します。
<SlaveAddress>
アクセス対象のスレーブアドレスを2バイトの16進数ASCIIコードで指定します。
<LocalAddress>
デバイス内アドレスを2バイトの16進数ASCIIコードで指定します。
たとえばスレーブアドレス0x12、レジスタ0x03のレジスタの値を取得するには、シリアルモニタから ir1203[Enter] を入力します。桁数は固定なので1桁の値の先頭の0は省略できません。
//-------------------------------------------------------
// コマンド処理
// console_buf[] 受信されたバイト列
// console_bufpos 受信されたバイト数
//------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
if ((console_buf[0] == 'i') || (console_buf[0] == 'I')) {
//----- 1バイト目 = 'i' or 'I' の場合 -----
if ((console_buf[1] == 'r') || (console_buf[1] == 'R')) {
//----- 2バイト目 = 'r' or 'R' の場合 -----
// 3~4バイト目を数値に変換
slave_addr = (int)toCharHexByteString(&console_buf[2]);
// 5~6バイト目を数値に変換
device_addr = (int)toCharHexByteString(&console_buf[4]);
// I2CバスにReadアクセス
Wire.beginTransmission(slave_addr);
Wire.write(device_addr);
Wire.endTransmission();
Wire.requestFrom(slave_addr, 1);
if (Wire.available()) i = Wire.read();
Wire.endTransmission();
Serial.println(i, HEX);
}
else if ((console_buf[1] == 'w') || (console_buf[1] == 'W')) {
//----- 2バイト目 = 'w' or 'W' の場合 -----
// 3~4バイト目を数値に変換
slave_addr = (int)toCharHexByteString(&console_buf[2]);
// 5~6バイト目を数値に変換
device_addr = (int)toCharHexByteString(&console_buf[4]);
// 7~8バイト目を数値に変換
data = (int)toCharHexByteString(&console_buf[6]);
// I2CバスにWriteアクセス
Wire.beginTransmission(slave_addr);
Wire.write(device_addr);
Wire.write(data);
Wire.endTransmission();
Serial.println(' ');
}
}
なお、このコードは以下の「その他役立つ処理」の toCharHexByteString 関数を使用しています。
その他役立つ処理
コマンドコンソールを実装するにあたり、役立つ処理を関数化したものを載せます。
以下に挙げるソースコードはコメント部のURLを削除しない限り、商業用途であってもそのままコピペして使って構いません。上記のコマンドコンソールとともにGithubにて公開していますので自由にご利用ください。ただしサポートはしませんのでその辺は自己責任で。
| リンク | |
|---|---|
| Github | arduino_serial_console_tmct |
その前に注意!!
エントリーレベルのArduino(UNO/NANO/LEONARDO/MICRO/EVERY)はメモリが数キロバイトしかありません。
関数内で使っている変数はメモリ(スタック)に積まれ、その関数から抜けるまでは残ったままになります。調子に乗って関数から関数を呼び出すこと(ネスト)を繰り返しているとあっという間にメモリの空き容量がなくなり、いわゆるスタックオーバーフローによるメモリ内容の破壊が起きます。
たとえばこんなことが起きたらスタックオーバーフローによるメモリ内容の破壊を疑ってください。
- 動いていたコードを関数化して呼び出すようにしたら、その関数を呼び出した途端、またはその関数の中の処理で動作が止まる(暴走する)ようになった。
- ループ記述がない部分で動作が止まる。(暴走する)
- 書き換えた覚えがない変数の中身が勝手に変わる。
- 関数から戻ってくる値がめちゃくちゃ。
- 条件分岐であらぬところに処理がすっ飛ぶ。
メモリが少ないArduinoの場合、関数のネスト数は3〜4程度に抑えた方が良いです。つまりloop()から関数呼び出し→その中で関数呼び出し→その中で関数呼び出し→その中で関数呼び出し、という程度。もちろん各関数内での変数の使用状況によるのでただの目安ですが、経験上、ネスト数がこれ以上になると動作が怪しくなることが多いです。
非力なマイコンを使用する上でメモリ消費を抑えることは大変重要なテクニックです。あえて関数化せずベタ書きした方が良いケースもありますので、臨機応変に対応しましょう。
バイト列を比較する
バイト列の一致を比較する関数です。先ほどのコマンドコンソールではif文のネストで文字比較していますが、原始的すぎてイヤだ!!という場合に使えます。(ただし処理は重めです。)
//---------------------------------------------------------
// ASCIIバイト列を比較して結果を返す
//---------------------------------------------------------
//[Arguments]
// *str1 : 比較されるcharバイト列へのポインタ
// *str2 : 比較するcharバイト列へのポインタ
//[Returns]
// unsigned char : 0 = 最後まで一致した
// 1 = バイト列は異なる
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
unsigned char compareByteString(char *str1, char *str2) {
unsigned char flag = 0;
while (*str1 != 0) {
if (*str1 != *str2) {
flag = 1;
break;
}
str1++;
str2++;
}
if ((*str1 != 0) || (*str2 != 0)) flag = 1;
return (flag);
}
たとえばchar型バイト列bufの内容が"abcd"であるか比較する場合は以下のような記述をします。
以下の例ではbufの内容が"abcd"と一致する場合は 0 、一致しない場合は 1 がシリアルモニタに出力されます。
char buf[15];
Serial.println( compareByteString(&buf[0], (char*)"abcd") , HEX );
ちなみに、初期値を指定していない変数の中身は不定です。
バイト列変数の中身があらかじめ’¥0’で埋められていることを期待してコードを書くと、「一致するはずなのに一致判定が出ない」とハマることがあります。トラブルを避けるためにも、バイト列は中身を’¥0’に初期化してから使うようにしましょう。
小文字→大文字に変換する
ASCIIバイト列の小文字を大文字に変換する関数です。
toUpperCopyByteString はもとのバイト列を大文字に変換しながら別のバイト列に複製します。toUpperByteString は渡したバイト列そのものを大文字に変換します。
//---------------------------------------------------------
// ASCIIバイト列を小文字→大文字変換しながら複製して返す
//---------------------------------------------------------
//[Arguments]
// *src : 変換もとのバイト列へのポインタ
// *dst : 変換後の結果を格納するバイト列へのポインタ
//[Returns]
// なし
//[Note]
// *dstには *srcを格納できるだけの領域が必要です
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
void toUpperCopyByteString(char *src, char *dst) {
while (*src != 0) {
if ((*src >= 'a') && (*src <= 'z')) *dst = (*src - 0x20); else *dst = *src;
src++;
dst++;
}
}
//---------------------------------------------------------
// ASCIIバイト列を小文字→大文字変換して返す
//---------------------------------------------------------
//[Arguments]
// *str : 変換対象のバイト列へのポインタ
//[Returns]
// なし
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
void toUpperByteString(char *str) {
while (*str != 0) {
if ((*str >= 'a') && (*str <= 'z')) *str -= 0x20;
str++;
}
}
大文字→小文字に変換する
ASCIIバイト列の大文字を小文字に変換する関数です。
toLowerCopyByteString はもとのバイト列を小文字に変換しながら別のバイト列に複製します。toLowerByteString は渡したバイト列そのものを小文字に変換します。
//---------------------------------------------------------
// ASCIIバイト列を大文字→小文字変換しながら複製して返す
//---------------------------------------------------------
//[Arguments]
// *src : 変換もとのバイト列へのポインタ
// *dst : 変換後の結果を格納するバイト列へのポインタ
//[Returns]
// なし
//[Note]
// *dstには *srcを格納できるだけの領域が必要です
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
void toLowerCopyByteString(char *src, char *dst) {
while (*src != 0) {
if ((*src >= 'A') && (*src <= 'Z')) *dst = (*src + 0x20); else *dst = *src;
src++;
dst++;
}
}
//---------------------------------------------------------
// ASCIIバイト列を大文字→小文字変換して返す
//---------------------------------------------------------
//[Arguments]
// *str : 変換対象のバイト列へのポインタ
//[Returns]
// なし
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
void toLowerByteString(char *str) {
while (*str != 0) {
if ((*str >= 'A') && (*str <= 'Z')) *str += 0x20;
str++;
}
}
16進ASCIIバイト列を数値に変換する
コマンドコンソールを作るとき、ASCIIの16進数文字列でパラメータを指定したい時がよくあります。
以下は16進数のASCII文字列をバイナリ数値に変換して返す関数です。戻り値の型に応じて3種類用意しました。(ただしASCII文字列に16進数以外の文字(0〜9,a〜f,A〜F以外)が含まれると変換後の数値は変な値になります。)
//---------------------------------------------------------
// 2桁までの16進数ASCIIバイト列を数値変換してunsigned char型の値として返す
//---------------------------------------------------------
//[Arguments]
// *str : 変換対象のバイト列へのポインタ
//[Returns]
// unsigned char : 変換後の値
//[Note]
// 変換対象のASCIIバイト列に16進数に変換できない文字が含まれる場合は
// 異常な値が返ります
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
unsigned char toCharHexByteString(char *str) {
unsigned char r = 0;
if(*str > 0x39) r = *str - 0x07; else r = *str;
str++;
if (*str != 0) {
r <<= 4;
if(*str > 0x39) r |= (*str - 0x07) & 0x0f; else r |= (*str & 0x0f);
}
return (r);
}
//---------------------------------------------------------
// 4桁までの16進数ASCIIバイト列を数値変換してunsigned short型の値として返す
//---------------------------------------------------------
//[Arguments]
// *str : 変換対象のバイト列へのポインタ
//[Returns]
// unsigned short : 変換後の値
//[Note]
// 変換対象のASCIIバイト列に16進数に変換できない文字が含まれる場合は
// 異常な値が返ります
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
unsigned short toShortHexByteString(char *str) {
unsigned short r = 0;
unsigned char i;
if(*str > 0x39) r = *str - 0x07; else r = *str;
for (i = 0; i < 3; i++)
{
str++;
if (*str != 0) {
r <<= 4;
if(*str > 0x39) r |= (*str - 0x07) & 0x0f; else r |= (*str & 0x0f);
}
else break;
}
return (r);
}
//---------------------------------------------------------
// 8桁までの16進数ASCIIバイト列を数値変換してunsigned long型の値として返す
//---------------------------------------------------------
//[Arguments]
// *str : 変換対象のバイト列へのポインタ
//[Returns]
// unsigned long : 変換後の値
//[Note]
// 変換対象のASCIIバイト列に16進数に変換できない文字が含まれる場合は
// 異常な値が返ります
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
unsigned long toLongHexByteString(char *str) {
unsigned long r = 0;
unsigned char i;
if(*str > 0x39) r = *str - 0x07; else r = *str;
for (i = 0; i < 7; i++)
{
str++;
if (*str != 0) {
r <<= 4;
if(*str > 0x39) r |= (*str - 0x07) & 0x0f; else r |= (*str & 0x0f);
}
else break;
}
return (r);
}
バイト列を複製する
バイト列を別のバイト列に複製する関数です。
//---------------------------------------------------------
// バイト列を複製する
//---------------------------------------------------------
//[Arguments]
// *src : 複製もとのバイト列へのポインタ
// *dst : 複製後の結果を格納するバイト列へのポインタ
// st : *srcの複製開始位置
// length: 複製するバイト数
// dstcol: *dstへの複製開始位置
//[Returns]
// なし
//--------------- https://ss1.xrea.com/tmct.s1009.xrea.com/
void copyByteString(unsigned char *src, unsigned char *dst, unsigned char st, unsigned char length, unsigned char dstcol) {
unsigned char i;
dst += dstcol;
src += st;
for(i = 0; i < length; i++) {
*dst = *src;
dst++;
src++;
}
}
まとめ
対話型コマンドコンソールのサンプルといくつかの補助関数を紹介しました。
コマンドコンソールをデバッグに応用するには最低限C++言語のスキルが必要になりますが、それほど難しいものではないのでぜひ理解して使ってみてください。