Raspberry Pi PicoのUSBコネクタでPCとデータ送受信をする (WindowsForms + CircuitPython)

PCに簡単な外部表示装置をつなげたいと思ったのですが、お金や手間は掛けたくないのでPCと表示装置のコントローラーというか橋渡し役として安価なRaspberry Pi Pico(以下RPiPico)を使いたい。USBシリアル変換モジュールをわざわざ外付けしたりしなくてもRPiPicoにはUSBコネクタがあるので、これを使いUSBケーブル1本でPCとデータを<strong>通常のコンソール入出力とは別に</strong>[^console]シリアル通信したい[^http]という話です。
個人的な事情によりPC側は.NETのWindowsForms、RPiPico側はCircuitPythonを使います。
んなこと簡単にできるでしょ、と思って調べ始めましたがいつもの如く情報(日本語)が少なかったのでここにまとめておくシリーズです。
[^console]:標準のコンソール用COMポートをデータ送受信に使うと`REPL`などインタプリタ的なデバッグがしにくいし、コンソールは元々そういう用途ではないはずなので。
[^http]:httpを使う、という手もなくはないが、常時データを流し込む用途にはあまり向いてなさそうなので。
## 必要なもの
- 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`などから変更する方法はありません。


```boot.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から見えなくなったりすることがよくあるので、その場合はフラッシュメモリをクリアして[^nuke]1からやり直す必要があります[^respons]。
[^nuke]:https://learn.adafruit.com/getting-started-with-raspberry-pi-pico-circuitpython/circuitpython から`flash_nuke.uf2`をPCにダウンロードして、RPiPicoのファームウエアのインストールと同じ手順でRPiPicoに入れるとフラッシュメモリがクリアされます。もちろん`code.py`など自分で置いておいたものも消えますのでバックアップ大事。
[^respons]:そういうこともあるので自己責任で。

## シリアルポート
ここでRPiPicoを再起動すると、PCのデバイスマネージャにいつものコンソール用COMポートのほかにもうひとつデータ用のCOMポートが見えるようになります。COMポートの数字は環境依存というかWindowsが勝手に割り振りますので数字は下記と違うかもしれません[^mac]。試行錯誤してください。
以下の例では`COM8`がいつものコンソール、`COM9`がデータ用です。

![スクリーンショット 2025-06-10 101807.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/025ca9f9-10e7-4100-b007-eb297f0c8096.png)
[^mac]:Macとかでも`/dev/cu.usbmodemfoobar`とかでプログラム書けばできると思う。
## PC側
ふつーにシリアル通信用のルーチンを書きます[^nuget]。
最初の方の`String PortName = "COM9"`のポート番号は先ほど調べたデータ用のポート番号を記入します。
フォームコンストラクタ内の`SerialPort`インスタンスの作成時にボーレート`115200`を指定していますが、USBは仮想シリアル通信なのであまり意味がありません。そのときのUSBの速度で通信します。
そのあと、RPiPico側からの通信はいつ送られてくるのかPC側ではわかりませんのでPC側でデータを受信したときに実行されるイベントハンドラを登録しておきます。
テストなので送信部分は無限ループになっています。必要であれば適当に直してください。
[^nuget]:事前にnugetで`System.IO.Ports`を追加する必要があります。
```cdc_test.cs
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
ここもあまり説明することはありません。送受信されるデータはバイト列なので文字として表示するにはデコードする必要があります。バイト列なので文字以外のバイナリデータの送受信も可能です。
```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)
```

## 実行結果
こんな感じです。
![スクリーンショット 2025-06-10 160614.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/c299364a-88dc-46c0-a300-e08f96e53d61.png) ![スクリーンショット 2025-06-11 211537.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986568/f87a83df-ca95-46a2-ae85-cd46da335194.png)

- - -
巷ではシリアル通信と言えば通常のGP0・GP1ピンとかにUSBシリアル変換モジュールをつなぐもの(`busio.UART`とか)の情報とCDCの話が特に明記されずに記載されていることもあってちょっと混乱しましたが、結果的にはこういう事だったらしいです。
これでPCの情報を簡単に電子ペーパーやOLEDやLEDマトリクスに表示できそうでひと安心。

というわけで今回は以上です。