【C#連載】デバッグとウンチク

お知らせ

えーっと。実はプログラムが実はちゃんと動かない!という重大なバグを誰も指摘してくれないという事態が発覚したのでした。(キットを誰も持ってない説が大いにありえることなのですが。)
同時に、時間がないときは検証してないというのもバレた訳だが、前に作ったコードをコピペして修正してるわけではないので、間違うこともあります。とはいえ、まったく動かしてないわけではないのでどこがバグかはすぐつぶせるのですが、バグつぶし(デバッグ)は研修には良い題材なので、先週わざとやらずに、今週に引っ張ってみました。


v21

先週アップできなかった理由は多々あるのでここでは割愛しますが、デバッグについてうまく書くのは本当に難しいです。この記事もそれほど長くはないものの、何せ勘所なところなので、書きなれてない人には何がバグかわからないし、ほしい現象がでてこないこともバグというので、「ない」ことを「ある」として考えるのは意外と難しいところだったりして、それをうまく伝えられているかどうか確信が持てないところではあります。

ちなみに、デバッグは、プロクマラマにとっては腕の見せ所でもあります。一般的にプログラマはコードを書くのがお仕事ですが、ある程度の実力をつけると、究極的には、自分のコードのみならず他人の書いたコードでさえもつまびらかにし、デバッグすることさえあるお仕事です。
#私は経験的に言って、「よいプログラマ」=「デバッグが得意だ」と思います。

コードの切り貼りでアプリを仕上げることもある昨今では、他人様の書いたコードをデバッグするとか、自分の必要なようにカスタマイズするのは必須の技術でしょう。

そして、デバッグだけでなく、クライアントがほしい機能がなんなのかを見極めて、一緒に課題を乗り越えるときにも、問題の切り分けができるかどうかや別の視点から見えるかどうかが、仕事の効率をよくするカギを握ります。

今回は、問題の切り分け方についてウンチクを述べつつデバッグします。

デバッグの基本的な手順は次のとおりです。
(別にお作法があるわけじゃないけど、私は大体こんなかんじ)

  1. 現象を捉える
  2. 起きている現象から、原因を推測していくつか仮定する。
  3. 仮定が正しいかどうかを検証
  4. デバッグ、実装
  5. テストして、OKなら終了。ダメなら次の仮定を試す。

ここで問題になるのは、「仮定」がすべて出尽くしてもバグが取れなかったときなのですが、それはここでは書かないです。

前回のプログラムからデバッグしていきます。
前回は、Richtextboxを実装しました。これにより、Ackが返ってくるようになったのでデバッグ用にも使えます。
そこで、まず現象から捉えてみましょう。
#これが前回、すぐにデバッグしなかった理由の一つです。w

さて、それではデバッグに入りましょう。

【現象の確認】
MX-64RキットをPCに接続します。サーボ用の電源も忘れずに入れてください。
プログラム上でF5を押してデバッグモードでアプリを起動します。

20130529blog001

スライダーを動かします。(左右どっちでもいいです。)
ここでうまく行っていれば、サーボも動くし、RichtextBoxにも表示が何かしら出るはずです。
どうでしょう?
Ackが出てますが、サーボの動きは変ですね?
動いたり、動かなかったりしてませんか?

この「期待している動作が出ない」というのがここでの「現象」です。
日常生活ではあまりない感覚なので、「ない」ものを「現象」として捉える感覚は得るのがけっこう難しいのです。
何しろ「ない」のですから、「ない」=「起きてる現象」として捉えるのはちょっとコツがいります。何が起きてないのか、って考えることさえできれば割と簡単ではあるのですが、ないことに気づくのにはセンスがいるかもしれません。

では、まず問題の切り分けをしていきましょう。
起きている「期待している状況にならない」ことから、原因を推測していくつか仮定する。のステップに入ります。
この手のWindows Formアプリから考えるときは「イベント」がそもそもうまく実行されているのか?を考えることから始まります。

仮定1:スライダーの動作イベントが実行されていないのか?
仮想原因1-1:Tickerがうまくいっていない説
仮想原因1-2:スライダーが動いたイベントがうまく行ってない説

これがうまく行ってるとしたら、つぎはプログラムの中身をかんがえます。

仮定2:送信するパケットが間違っているのか?
仮想原因2-1:そもそもパケットの作り方が間違っている
仮想原因2-2:送ってるIDが違う
仮想原因2-3:パケットの順番が間違っている

自分の中にあるデバッグフローを明文化すると、こんな風に大体のあたりをつけていってます。
たいていの場合は、仮定1は現象を捉えたときに、「ここじゃないな」って思っている場合が多いですが、今回は練習も兼ねていますから、一緒に原因を特定しに行きましょう。

仮定1なのかどうかを切り分ける
これは意外と簡単で、このイベントが起きたときにRichTextBoxに表示させるということでイベントをキャッチできているかどうかがわかります。

スライダーを動かしてみましょう。
Richtextbox1に何が出てくるかを観察します。

サーボは動いたり、動かなかったり、でも、Ackはちゃんと返ってきています。Ackが返っているということは、イベントがうまく行ってたらAckは返ってくるので、仮定1でイベントがうまく行ってない説は関係ないということがわかりました。仮定1がうまく行っている場合、仮定2を切り分けます。

よくよく見るとAckにはエラーが出てますね。
マニュアルを参考にすると、FF FFはヘッダー、3番目はID、4番目は長さ、5番目がエラービット、最後のバイトはチェックサムです。ここで見ると(yukiのサーボはID=4になっています。そのため、図と解説ではID=4で表示します。プログラムでは最初からのID=1で書いています。)

FF FF 04 02 08 F1

エラーがなければ後ろから2番目のエラービットが00になってなければならないところが08になっています。(赤文字)

そこで、次に、仮定2を切り分けましょう
仮想原因2-1の場合は、そもそも間違っているのでAckが返ってくることはないはずです。
今はRichtextbox1にAckが返ってきていますので、2-1は除外されます。

次、2-2はIDだけなので、IDがただしければ、richtextBoxになんらかのAckが返ってくるはずです。
しかし、ここでエラーは08と示されていますので、IDでもなさそうです。

せっかくエラーがでているので調べてみましょう。
08の意味は、マニュアルを見るとout of rangeですから、目標値の設定が間違っていることになります。エラーログをよく見ると、エラーなしの00のときもありますから、何かがおかしいときがあるといえそうです。そうなってくると、2-3のパケットの順番が間違っている、が考えられそうです。

こうやってバグが潜んでそうな「あたり」をつけていきます。
(ここまでの時間でたぶん私の脳内だと30秒もかからないくらい。こうやってフローにして書くといっぱい時間がかかる。)

下記のプログラムを見てください。実際に調べましょう。
ハイライトの部分を見てください。

         private void timer1_Tick( object sender, EventArgs e)
        {
            int id = 1;
            byte [] data = new byte[3];
            int size = 0;

            if (packet_flag == 1)
            {
                data[size++] = GOAL_POSITION_L;
                data[size++] = ( byte )((degree & 0xFF00)>>8); //下位バイト
                data[size++] = ( byte )(degree & 0xFF);   //上位バイト
                make_packet(id, INST_WRITE, data, size);               
            }
}

本来、ここの指令マップは、マニュアルから、
GOAL_POSITION_L (30)
GOAL_POSITION_H (31)
の順番のはずです。

それが、
GOAL_POSITION_H 上位バイト
GOAL_POSITION_L 下位バイト

data[size++] = ( byte )((degree & 0xFF00)>>8); //下位バイト
data[size++] = ( byte )(degree & 0xFF);   //上位バイト

の順に記述されていました。

0xFF00は上位バイトのマスク、0xFFは下位バイトのマスクです。つまり、上位、下位を間違えて書いていたことになります。
これでは、セットされる数値が逆になって挙動不審になるはずですね。

ここを修正しましょう。

         private void timer1_Tick( object sender, EventArgs e)
        {
            int id = 1;
            byte [] data = new byte[3];
            int size = 0;

            if (packet_flag == 1)
            {
                data[size++] = GOAL_POSITION_L;
                data[size++] = ( byte )(degree & 0xFF); //下位
                data[size++] = ( byte )((degree & 0xFF00)>>8); //上位
                make_packet(id, INST_WRITE, data, size);               
            }
        }

では修正したものの挙動をみましょう。
F5を押してコンパイルし、MX-64Rキットを接続して動かしてみてください。
期待したとおり、サーボがするする動くところが見られると思います。
Ackも返ってきて最後から2番目の文字列も00となってエラーも出なくなりましたね?

20130529blog002

さて、動きましたか?
今回は問題なく動いたはずです。(検証もできてますから間違いないですよね)
#動かなかった人は、デバッグしてみてください。特にIDを自分で変えている人はこのプログラムだとID=1ですから注意です。

問題の切り分け方もなんとなくわかりましたか?

ちなみに、技術が相当いくと、オリジナルを書いた人のお作法とか志向までなんとなく見えてくるから他人様のコード読みは楽しくてやめられません。この人UNIX由来だなぁとか、DOSプログラマかとか、Visual系だなとかわかりますよ。WEB系だと切り貼りとかすぐわかります。ここからモジュールのプログラマ変わったのねとか。それにはいっぱい言語を勉強しないとならないですが、いろんな楽しみが生まれます。w

ある意味、バグ取り以前に、プログラミングそのものや作っているシステムで起きることに慣れてないと、原因の想定も難しかったりします。
たくさん作って、たくさん修正をすると勘所も鍛えられてきますから、レベルアップするには精進あるのみです。

次週は、トルクモードでの駆動のためのトルクモードのON/OFFをチェックボックスで実装します。
そして、スライダーでサーボをトルクモードで動かして、このC#の連載は終わりです。

では来週!
【関連商品】

MX64 kit
MX-64Rスターターセット購入ページへ
Dynamixel
Dynamixelショップページ

ROBOTIS関連商品ページ

タイトルとURLをコピーしました