PCに簡単な外部表示装置をつなげたいと思ったのですが、お金や手間は掛けたくないのでPCと表示装置のコントローラーというか橋渡し役として安価なRaspberry Pi Pico(以下RPiPico)を使いたい。USBシリアル変換モジュールをわざわざ外付けしたりしなくてもRPiPicoにはUSBコネクタがあるので、これを使いUSBケーブル1本でPCとデータを通常のコンソール入出力とは別に1シリアル通信したい2という話です。
個人的な事情によりPC側は.NETのWindowsForms、RPiPico側はCircuitPythonを使います。
んなこと簡単にできるでしょ、と思って調べ始めましたがいつもの如く情報(日本語)が少なかったのでここにまとめておくシリーズです。
必要なもの
- Rasberry Pi Pico (Wでも2でも何でもいいはずです)
- USBケーブル (MicroUSB)
- PC
だけです。あともちろん事前に必要なこととしてCircuitPythonとかMu Editorとかをインストールしておいてください。それはここでは詳細は説明しません。
RPiPico側 boot.py
RPiPicoのUSBコネクタをデータのシリアル通信用に使うには設定用のboot.py
ファイルをcode.py
ファイルなどを置くのと同じRPiPicoのフラッシュメモリ(CURCUITPY
)に置く必要があります。
boot.py
はcode.py
などとは違いファイル名通りRPiPicoに電源をいれて(=USBケーブルを刺して)RPiPicoがブートアップするときだけ、つまり最初に1度だけ読み込まれ、起動時には終了しています。この設定を起動後にcode.py
などから変更する方法はありません。
import usb_cdc
import board
import digitalio
serial_pin = digitalio.DigitalInOut(board.GP0)
serial_pin.direction = digitalio.Direction.INPUT
serial_pin.pull = digitalio.Pull.UP
if serial_pin.value:
usb_cdc.enable(console=True, data=True)
usb_cdc
(Communications Device Class)というモジュールを使います。通信を設定するのに本当に必要なのは最後の一行だけです。
起動時にこの設定を無効にしたい場合がありますが前述のとおりboot.py
は(usb_cdc.enable
は)RPiPico起動後には実行・変更ができません。起動時に変更するしかないのですがコードをいじらずに無効にしたい場合の処理がそれ以前の行です。GP0のピンとGNDをジャンパ接続してから電源をつなぐと最後の行はスキップされますので通常のブート動作となるはずです。念のためです。
それでも何か問題が起きて再起動してもCIRCUITPY
のドライブ(H:
とか)がPCから見えなくなったりすることがよくあるので、その場合はフラッシュメモリをクリアして31からやり直す必要があります4。
シリアルポート
ここでRPiPicoを再起動すると、PCのデバイスマネージャにいつものコンソール用COMポートのほかにもうひとつデータ用のCOMポートが見えるようになります。COMポートの数字は環境依存というかWindowsが勝手に割り振りますので数字は下記と違うかもしれません5。試行錯誤してください。
以下の例ではCOM8
がいつものコンソール、COM9
がデータ用です。
PC側
ふつーにシリアル通信用のルーチンを書きます6。
最初の方のString PortName = "COM9"
のポート番号は先ほど調べたデータ用のポート番号を記入します。
フォームコンストラクタ内のSerialPort
インスタンスの作成時にボーレート115200
を指定していますが、USBは仮想シリアル通信なのであまり意味がありません。そのときのUSBの速度で通信します。
そのあと、RPiPico側からの通信はいつ送られてくるのかPC側ではわかりませんのでPC側でデータを受信したときに実行されるイベントハンドラを登録しておきます。
テストなので送信部分は無限ループになっています。必要であれば適当に直してください。
using System.Diagnostics;
using System.IO.Ports;
namespace CDC_Test
{
public partial class Form1 : Form
{
private SerialPort serialPort;
private const String PortName = "COM9"; // Default port name, can be changed as needed
public Form1()
{
InitializeComponent();
serialPort = new(PortName, 115200, Parity.None, 8, StopBits.One);
serialPort.DataReceived += DataReceivedHandler; // Attach the data received event handler
serialPort.Open();
Send_test();
serialPort.Close();
}
private void Send_test()
{
while (true)
{
try
{
serialPort.WriteLine("Hello, CDC!"); // 送信
Debug.WriteLine("data sent"); // 確認用ローカルメッセージ
Thread.Sleep(500); // Delay to avoid overwhelming the serial port
}
catch (Exception ex)
{
MessageBox.Show("Error sending data: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
string data = sp.ReadExisting();
Debug.WriteLine("Received from CircuitPython: " + data);
}
}
}
RPiPico側 code.py
ここもあまり説明することはありません。送受信されるデータはバイト列なので文字として表示するにはデコードする必要があります。バイト列なので文字以外のバイナリデータの送受信も可能です。
import usb_cdc
import time
dataPort = usb_cdc.data
while True:
data = dataPort.readline()
if data:
print(data.decode()) # RPiPicoコンソール(COM8)へPC(COM9)から受信した内容の表示
dataPort.write(b'Data Received\n') # PC(COM9)へ返信メッセージ送信
time.sleep(0.5)
実行結果
巷ではシリアル通信と言えば通常のGP0・GP1ピンとかにUSBシリアル変換モジュールをつなぐもの(busio.UART
とか)の情報とCDCの話が特に明記されずに記載されていることもあってちょっと混乱しましたが、結果的にはこういう事だったらしいです。
これでPCの情報を簡単に電子ペーパーやOLEDやLEDマトリクスに表示できそうでひと安心。
というわけで今回は以上です。
-
標準のコンソール用COMポートをデータ送受信に使うと
REPL
などインタプリタ的なデバッグがしにくいし、コンソールは元々そういう用途ではないはずなので。 ↩ -
httpを使う、という手もなくはないが、常時データを流し込む用途にはあまり向いてなさそうなので。 ↩
-
https://learn.adafruit.com/getting-started-with-raspberry-pi-pico-circuitpython/circuitpython から
flash_nuke.uf2
をPCにダウンロードして、RPiPicoのファームウエアのインストールと同じ手順でRPiPicoに入れるとフラッシュメモリがクリアされます。もちろんcode.py
など自分で置いておいたものも消えますのでバックアップ大事。 ↩ -
そういうこともあるので自己責任で。 ↩
-
Macとかでも
/dev/cu.usbmodemfoobar
とかでプログラム書けばできると思う。 ↩ -
事前にnugetで
System.IO.Ports
を追加する必要があります。 ↩