Arduiono や Raspberry Pi Pico を操作して I2C 通信などを実行する Windows アプリを作成したのですが、この記事ではその Windows アプリの基礎となる USB-Serial 通信の制御部分を紹介します。
前の記事では Visual Studio のインストールとお試しプログラムの作成を行いました。この記事では Raspberry Pi Pico との USB-Serial 通信を行うプログラムを作成します。
System.IO.Ports のインストール
Raspberry Pi Pico との通信ですが、Raspberry Pi Pico は Windows には Serial デバイスとして認識されるので Serial通信の関数を使えば簡単に通信できます・・・が、いざやろうとしたら Serial 通信の関数が使えません。追加でインストールが必要だったようです。
Serial 通信の関数は Visual Studio の上のメニューから「プロジェクト」-> 「NuGet パッケージの管理」を開きます。
NuGet パッケージマネージャが開くので「参照」タブを開いて検索欄に「serialport」または「System.IO.Ports」と記入して「System.IO.Ports 作成者: Microsoft」を見付けてインストールします。
これで Serial 通信の関数が使えるようになります。
どんなアプリを作るか
これから作成するアプリは以下のファームを書き込んだ Raspberry pi pico との通信を目的とします。通信に成功すると LED が点滅して知らせてくれます。
ysin1128.hatenablog.com
Raspberry Pi Pico に 1 byte 以上のデータを送ると (送信データの数 + 1 byte) のデータを返してきます。また、1 byte 目の送信データの値の回数だけ LED を点滅させます。そこでアプリには以下の動作を組み込もうと思います。
- フォーム上で Raspberry pi pico に送信するデータの値を設定する
- 値を 1 byte のデータにして Raspberry pi pico に送信する
- データを送信次第、2 byte のデータを受信する
- 受信したデータをテキストボックスに出力する
フォームにオブジェクトを追加する
Visual Studio に戻って、フォーム上で送信するデータの値を設定するために左のツールボックスから「NumericUpDown」を追加します。ここはテキストボックスでも良かったのですが、設定する値が数字に限るのでこれを使ってみました。
また、フォームの右上にテキストボックスを追加していますが、これは Serial 通信のためのものです。後ほど説明します。
関数を作成する
関数を作成します。書き込む場所は Hello World を作った時と同様に作成中のフォームで button1 をクリックすると開く button1_Click 関数の中です。Hello World を出力する記述を削除して新しいコードを書き込みます。
namespace RPP_CTRL_CS { using System.IO.Ports; public partial class Form1 : Form { static SerialPort SP1; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { int i; int intTO; byte[] DATA = {0,0,0,0}; textBox1.AppendText("initializing COM port..."); SP1 = new SerialPort(); SP1.PortName = textBox2.Text; SP1.BaudRate = 115200; SP1.Parity = Parity.None; SP1.DataBits = 8; SP1.StopBits = StopBits.One; SP1.DtrEnable = true; SP1.RtsEnable = true; SP1.Open(); textBox1.AppendText("done.\r\n"); textBox1.AppendText("sending a data to raspberry pi pico.\r\n"); DATA[0] = decimal.ToByte(numericUpDown1.Value); SP1.Write(DATA, 0, 1); textBox1.AppendText("receiving a data from raspberry pi pico.\r\n"); for (i = 0; i < 2; i++) { intTO = 0; while (SP1.BytesToRead == 0) { System.Threading.Thread.Sleep(1); intTO++; if (intTO == 100) break; } if (intTO < 100) { SP1.Read(DATA, 0, 1); textBox1.AppendText(DATA[0] + "\r\n"); } else { textBox1.AppendText("failed.\r\n"); } } SP1.Close(); } } }
以下は各記述の解説です。
using System.IO.Ports;
Serial 通信関数を使うために、先にインストールした System.IO.Ports を参照します。
static SerialPort SP1;
SerialPort オブジェクトを宣言します。名前は単純に SP1 としています。
SP1 = new SerialPort(); SP1.PortName = textBox2.Text; SP1.BaudRate = 115200; SP1.Parity = Parity.None; SP1.DataBits = 8; SP1.StopBits = StopBits.One; SP1.DtrEnable = true; SP1.RtsEnable = true; SP1.Open();
これらは button1_Click の中にあるので、button1 がクリックされた際に実行されます。
button1 がクリックされると SP1 にメモリを割り当て、Serial 通信の諸パラメータを設定して最後に SP1.Open() で Serial port の接続を開く、つまり、通信可能な状態にします。
Serial通信の諸パラメータは Raspberry Pi Pico の Default 設定を参考にしています。Raspberry Pi Pico 側は pico_enable_stdio_usb を有効にしているだけで、これら設定は変更していません。
Serial通信のパラメータのうち PortName はフォームに追加したテキストボックスの値を参照しています。ここには Raspberry pi pico を PC に接続した際に Windows が割り当てる COM番号を与える必要があるのですが、割り当てられる COM 番号が未定なので、フォーム上で後から設定できるようにしています。なお、私の作成例では追加したテキストボックスの名前は textBox2 でしたが、違う名前になっていた場合はそれに合わせて記述を変更する必要があります。
DATA[0] = decimal.ToByte(numericUpDown1.Value); SP1.Write(DATA, 0, 1);
Raspberry pi pico に Serial 通信でデータを送信する記述です。
まず、Byte型変数配列の DATA の [0] に、フォームに追加した NumericUpDown (numericUpDown1) の値を代入します。これが Raspberry pi pico に送信する値となり、Raspberry pi pico がこれを受け取るとこの値の回数だけ LED を点滅させます。NumericUpDown の値は decimal型で Byte 型とは異なり、そのままでは DATA[0] に代入できないので decimal.ToByte 関数で Byte型に変換しています。
DATA[0] の準備ができたら SP1.Write 関数でそれを Serial port から出力します。その先で待っているのが Raspberry pi pico になります。
SP1.Write の引数は順に送信データ配列 (Byte型) のポインタ、配列のうち送信するデータの開始位置、データ数となります。ここでの記述では Byte型配列 DATA を指定し、そのうち送信データが入っているのは DATA[0] だけなので開始位置は 0、データ数は 1 と記述しています。
例として、送信データが DATA[1] と DATA[2] に入っている場合は SP1.Write(DATA, 1, 2) という記述になります。
for (i = 0; i < 2; i++) { intTO = 0; while (SP1.BytesToRead == 0) { System.Threading.Thread.Sleep(1); intTO++; if (intTO == 100) break; } if (intTO < 100) { SP1.Read(DATA, 0, 1); textBox1.AppendText(DATA[0] + "\r\n"); } else { textBox1.AppendText("failed.\r\n"); } }
Raspberry pi pico からの Serial 通信のデータを受信するための記述です。
データの受信に関して、常に待ち受けておいて受信したら受信しただけ処理するという関数を作ることができればいいのですが、面倒な上に今回は 1 byte の送信に対して 2 byte が返ってくることが分かっているので専用の処理を用意します。一方、何か失敗して 2 byte が返って来ない場合、下手をすると延々と待ち続けることになってしまうので簡単なタイムアウトも組み込んでおきます。
Raspberry pi pico は 1 byte を送信すると 2 byte を返してくるので、送信処理の直後に受信処理を置きます。ただ、返信のタイミングの同期などは無いので、いつ返信が来てもいいように待ち受ける必要があります。
intTO = 0; while (SP1.BytesToRead == 0) { System.Threading.Thread.Sleep(1); intTO++; if (intTO == 100) break; }
Serial port はデータを受信すると自動的に Buffer に蓄えてくれます。これに関しては記述は不要です。それに対して必要なのは、Buffer にデータが蓄えられたかどうかを確認する処理となります。SP1.BytesToRead は Buffer に蓄えられたデータ数 (Byte) を返す関数です。これを利用して while 文で Raspberry pi pico からのデータの到着を待ちます。
しかし、それだけでは何らかの失敗により Raspberry pi pico がデータを返して来ないと while の無限ループに陥ってしまうので intTO というタイムアウトカウンタを用意しています。SP1.BytesToRead の値の確認は 1 ms 間隔で行い、値が 0 でループを繰り返すたびに intTO をカウントアップし、intTO = 100 (100 ms) になってもデータが返って来ない場合は while ループを抜けます。
if (intTO < 100) { SP1.Read(DATA, 0, 1); textBox1.AppendText(DATA[0] + "\r\n"); } else { textBox1.AppendText("failed.\r\n"); }
while ループを抜けた後の処理です。
intTO が 100 に達していない、つまり、タイムアウトになっていない場合は Buffer に受信データがあるということなので、SP1.Read でデータを読み出します。SP1.Read の引数は SP1.Write と同様で、受信データを格納する Byte型配列のポインタ、配列のうち格納を開始する位置、格納するデータ数を示しています。この記述では Byte型配列 DATA の 0 番目、つまり、DATA[0] に読み出したデータを格納します。DATA は送信で使ったものを再利用しています。そして、読み出したデータをそのまま textBox1 に出力します。
while ループでタイムアウトに達してここに至った場合は textBox1 に出力するデータが無いので、代わりに "failed" という文字列を出力します。
以上が受信データ 1 byte 分の処理となります。Raspberry pi pico が返信してくるデータは 2 byte なので、For 文でこの処理を 2 回繰り返しています。
SP1.Close();
通信が完了したら Serial port を閉じて終わりです。
動作確認
それでは、この記述でアプリを立ち上げて動作確認を行います。
アプリの立ち上げに先立って Raspberry pi pico を PC に接続して、割り当てられた COM 番号を確認します。COM番号は Windows のメニューバーの検索のところで「デバイスマネージャー」を検索して立ち上げると確認できます。
デバイスマネージャーにおいて Raspberry pi pico はポート (COM と LPT) の下で USB シリアルデバイスとして認識されます (名称は異なる場合があります)。上の例の場合は COM3 が割り当てられています。COM が複数あってどれが Raspberry pi pico か分からない場合はデバイスマネージャーを開いたまま Raspberry pi pico を PC から付け外ししてみてください。Raspberry pi pico に相当するものが現れたり消えたりするので、それで特定できます。
次に Visual Studio で作成したアプリの立ち上げです。立ち上げ方は Hello World の時と同様に上のメニューの「Debug」「Any CPU」と並んだ先の 「(緑三角) (プロジェクト名: ここでは RPP_CTRL_CS)」をクリックです。
まず、フォームの右上のテキストボックスに Raspberry pi pico に割り当てられた COM 番号を記入します。ここでは先ほど確認した "COM3" を記入しています。
次に、NumericUpDown に LED を点滅させたい回数を入力します。上の例では 5 回としていますが、あまり多いと点滅がいつまでも続くのでお勧めしません。
最後に button1 をクリックしてデータの送受信を行い、成功すると設定した回数だけ Raspberry pi pico の LED が点滅し、返ってきたデータがテキストボックスに出力されます。格好をつけてシステムメッセージっぽいものも出力されていますが、その後の"1", "4" や "1", "5" が Raspberry pi pico から返ってきたデータです。一行目は送信したデータ数 (1 byte の 1)、二行目は送信したデータがそのまま返ってきています。 (図の例では先に NumericUpDown の値を 4 にして button1 をクリックし、その後に 5 に変更してもう一度 button1 をクリックしています)
これで Raspberry pi pico に任意のデータを送り、その返信を受け取ることができるようになりました。ここから送信データや、それを受けた際の Raspberry pi pico の動作を変更すれば、 Lチカ以外のあらゆる制御を PC から行うことができるようになります。次の記事では GPIO を制御する例を紹介します。