yoshiyuki's blog

Arduino/Teensy/Raspberry pi pico を利用した I2C, SPI 通信アプリを紹介します

Arduino / Teensy / Raspberry Pi Pico / MAX32625PICO で I2C

I2C 通信などを行うアプリ ACTRL を紹介します。

  • Windows アプリ と Android アプリがあります。
  • Arduino / Teensy / Raspberry Pi Pico / MAX32625PICO を利用します
  • I2C / SPI / UART 通信を行うことができます
  • GPIO 端子を出力端子に設定して任意の Logic pattern を出力することができます
  • GPIO 端子を入力端子に設定して入力された値を検出することができます
  • 任意の周波数の Clock を出力することができます
  • Logic Data Logger として使用することができます

 

Latest Update 

2023/06/26: ACTRL ver. 0.81

  • UART が動作しない問題を修正しました
  • Teensy 4.0 のファームウェアを修正しました (R08)
    • Sketch (.ino) を Hex に変更しました
    • I2C 関数を最適化しました
  • Raspberry Pi Pico のファームウェアを修正しました (R48)
    • UART が動作しない問題を修正しました
    • I2C 関数を最適化しました
  • 修正したファームウェアACTRL に同梱しています

2023/06/13: ACTRL ver. 0.80

  • MAX32625PICO に対応しました
  • I2C Search, I2C Bus Clear, I2C Write Bit を追加しました
  • Logic Data Logger に機能を追加しました
    • マウスドラッグによる波形の拡大を追加しました
    • I2C 信号解析を追加しました
  • マイコンボードの自動検出を追加しました
  • Arduino Nano のファームウェアを修正しました (R15)
  • Teensy 4.0 のファームウェアを修正しました (R07)
  • Rapsberry Pi Pico のファームウェアを修正しました (R47)

 

ダウンロード

ACTRL (Windows アプリ) 

最新ファームウェアと履歴

お問い合わせはこちら

 

準備と使い方

準備

マイコンボードのセットアップ

各ピンの役割

アプリ (ACTRL) の入手と使い方

コマンド (Windows/Android 共通)

 

使用例

I2C コマンドの使用例と解説

GPIO コマンドの使用例と解説

Clock コマンドの使用例と解説

繰り返しパターンの生成例と解説

Logic Data Logger の使用例

 

参考

High Level を 3.3 V や 1.8 V にする

 

 

Let's make Arduino I2C/SPI tool! (English)

Shokz OpenMove/OpenComm/OpenRun Pro のマイク性能の比較

この記事ではオンラインミーティング (Teams会議や Zoom会議) での使用を想定した Shokz の骨伝導イヤホンのマイク性能を比較しています。
OpenRun Pro を買ったので以前の比較をやり直しました。

比較対象

Shokz OpenMove
Shokz 骨伝導イヤホンのエントリーモデルです。USB Type-C で充電できるのはこのモデルだけです (他のモデルは専用の充電ケーブルが必要)。性能に関しては思った以上に必要十分でした。

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)
Shokz 骨伝導ヘッドセット。ヘッドセットなので顔の横に伸びるマイクが付いています。また、16 時間の最大使用時間を持つのが特徴です。

Shokz OpenRun Pro
Shokz 骨伝導イヤホンのフラッグシップモデル。重量は OpenMove と同じ 29 g なのに最大使用時間は OpenMove の 6 時間長い 10 時間というのが魅力です。

マイク性能比較 / 騒音なし

環境

騒音源の無い室内でマイクの音声を録音しました。

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Shokz OpenRun Pro

マイク性能比較 / 騒音あり

騒音源

室内で、以下の動画を最大音量で再生したスマートホン (Xperia 5 IV) を 2m 後方にスマホスタンドでスピーカーを上に向けた状態で置いてマイクの音声を録音しました。


www.youtube.com

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Shokz OpenRun Pro

マイク性能比較 / 騒音あり / 意地悪試験

騒音源

室内で、以下の動画を最大音量で再生したスマートホン (Xperia 5 IV) を自分の正面に置きながらマイクの音声を録音しました。


www.youtube.com

録音結果

Shokz OpenMove

Shokz OpenComm (実験に使用したモデル)
Shokz OpenComm 2 (現行モデル)

Shokz OpenRun Pro

感想

OpenRun Pro を加えて比較実験をやり直しました。以前の比較では OpenMove と OpenComm の差は認識できませんでしたが、今回の実験では騒音環境で OpenComm が少しだけクリアに聞こえる気がします。一方で、新たに加わった OpenRun Pro は OpenMove と同程度のように思います。
いずれにしろ、どのモデルのマイクも、酷い騒音環境であっても (実際に意地悪試験では自分の声が良く聞こえない) 十分に声を拾うことができる性能を備えていると思います。

雑談

結局、OpenComm で満足できずに OpenRun Pro を買ってしまいました。不満は重さと大きさでした。最初に買った OpenMove が良すぎたのです。 OpenComm の 33g (現行の OpenComm2 は 35g) は OpenMove の 29g と大差無いはずなのに、それでも OpenMove より重く感じました。そしてマイクが邪魔でした。以前の比較でマイク性能に差は無いと結論付けたせいで余計に邪魔に感じました。しかし、OpenMove には最大使用時間という問題がありました。実際に、一日中会議が続いた日に OpenMove でバッテリー切れを起こしたのが OpenComm を買った動機でした。
そんな時にふと目に付いたのが OpenRun Pro でした。スポーツ用ということだったので検討したことすら無かったのですが、調べてみると重さは OpenMove と同じ 29g で、スポーツ用というくらいだから邪魔にならない形状のはず。そのくせ最大使用時間は OpenMove より長いという、まさに欲しかったものそのものがそこにありました。これで充電が USB Type-C だったらもっと良かったのですが、そこは妥協しました。
結果、素晴らしく快適になりました!
・・・が、同時に、こんなことなら最初から OpenRun Pro だけを買っておけば良かったんじゃないか (OpenMove と OpenComm は無駄だったんじゃないか) とも思わずにいられません。せめて、この記事が私と同じように悩んでいる誰かの助けになってくれれば、私の出費と苦労も報われます。

Raspberry Pi Pico / Windows 開発環境の構築 (2023/07/09)

コメントでセットアップが簡単になったという情報を頂いたので試してみました。本当に簡単でした。わざわざ記事にする必要なんか無いんじゃないかと思うくらいに。

はじめに

この記事では WindowsC/C++Raspberry Pi Pico 開発環境をインストールする手順をまとめます。

ちなみに、Raspberry Pi Pico 本体は Amason やスイッチサイエンスで購入できます。
Amazon
スイッチサイエンス

Document の入手

関連 Document は以下のリンクより入手可能です。全部英語です。
Raspberry Pi Documentation (公式の Document のページ)
getting started with Raspberry Pi Pico (pdf, Raspberry Pi Pico の C/C++ 開発環境の構築手順)
Raspberry Pi Pico C/C++ SDK (pdf, SDK の解説。環境構築には不要ですが、参考として)

Windows 上での開発環境構築手順は getting started の 9.2. Building on MS Windows の項目で説明されており、この記事はその手順をなぞるだけとなっています。

Tool のインストール

1. getting started の 9.2.1. Installing the Toolchain の項目にある Windows Pico Installer のリンクを開く。

2. 開いたページの Download the latest release のリンクからインストーラー (pico-setup-windows-x64-standalone.exe) をダウンロードする。

3. pico-setup-windows-x64-standalone.exe を実行する。

インストール画面でフォルダの選択やチェックボックスが現れますが、必要が無ければそのままで大丈夫です。

最後の「Finish」をクリックすると続けて pico-examples のクローンが作成されます。

以上でインストールは完了です。

Tool の起動とセットアップ

Windows のスタートメニュー一覧から Raspberry Pi Pico SDK v1.5.1 (バージョンは異なるかもしれません) を開いて Pico - Visual Studio Code を起動します。

Visual Studio Code が起動したらメニューバーの File -> Open Folder から pico_examples フォルダを開きます。pico-examples はデフォルトでは C:\Users\(ユーザー名)\Documents\Pico-v1.5.1 に作成されています。(v1.5.1 の数字は異なるかもしれません)
フォルダを開くといくつかのダイアログが現れます。
"Do you trust the authors of the files in this folder?" に対しては "Yes, I trust the suthors" をクリックしてください。このコードの作り手を信頼するかどうかの問い合わせで、今回は Raspberry Pi Pico の SDK なので信頼して良いと考えます。
"Would you like to configure project "pico-examples"?" に対しては、 Yes をクリックすると続いて Select a Kit for pico-examples のプルダウンが現れるので Pico ARM GCC を選択します。

Pico ARM GCC を選択すると Visual Studio Code の Window の右側下半分にコンソールが現れて、いろいろと処理をしたログが流れます。これでセットアップが完了です。

Hello_world の Build

Visual Studio Code の右のアイコンバーから CMake アイコン (三角 + スパナ) をクリックして、開いたツリーから pico_example/hello_world/usb/hello_usb [hello_usb.elf] を開き、ファイル名の右側の Build アイコン (ゴミ箱っぽい) をクリックすると Build が行われます。

Pico-v1.5.1\pico-examples\build\hello_world\usb に hello_usb.uf2 が出来ていれば成功です。

実は Build で生成されるファイルはクローンの時点ですでに存在しているため、上記の操作に対して Tool は「Build 済み」と判断して実際の Build は行いません。実際に Build を実行させたい場合は Pico-v1.5.1\pico-examples\build\hello_world\ をフォルダごと削除した後に Build を実行してください。Build により削除したものを同じものが生成されます。

プロジェクトフォルダの作成

開発環境の準備ができたので自分のプロジェクトを作成するフォルダを用意します。

まずはフォルダの作成ですが、これは Windows の通常のフォルダ作成と同じで良いのでエクスプローラで作成します。ここでは例として pico_test というフォルダを pico-examples と同じ場所に作成します。

次に pico-test フォルダの中に、実際にコードを置くフォルダとして test_01 を作成します。また、Build に必要な設定ファイルとして CMakeLists.txt, pico_sdk.import.cmake, pico_extras_import_optional.cmake を置きます。これら設定ファイルはひとまず pico-examples フォルダの直下にあるものをコピペします。

フォルダを作成したら Visual Studio Code を立ち上げて上のメニューから File -> Open Folder を開き pico-test フォルダを選択します。
"Do you trust the authors of the files in this folder?", "Would you like to configure project "pico_test"?" のダイアログが現れたらそれぞれ Yes で回答して、Select a Kit for pico_test のプルダウンが現れたら GCC xx.x.x arm-none-eabi を選択してください。
そうするとさっそく何やら処理が行われて、結果として CMakeLists.txt が赤字で警告されます。これは CMakeLists.txt に問題があるという指摘で、実際、このファイルは pico-examples フォルダ用のものをそのままコピペしたものなので pico_test フォルダでそのまま使うには問題があります。

というわけで、このまま Visual Studio Code 上で CMakeList.txt を変更します。以下が変更前と変更後です。

変更前

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

include(pico_extras_import_optional.cmake)

project(pico_examples C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0")
    message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

include(example_auto_set_url.cmake)
# Add blink example
add_subdirectory(blink)

# Add hello world example
add_subdirectory(hello_world)

add_compile_options(-Wall
        -Wno-format          # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int
        -Wno-unused-function # we have some for the docs that aren't called
        )
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
    add_compile_options(-Wno-maybe-uninitialized)
endif()

# Hardware-specific examples in subdirectories:
add_subdirectory(adc)
add_subdirectory(clocks)
add_subdirectory(cmake)
add_subdirectory(divider)
add_subdirectory(dma)
add_subdirectory(flash)
add_subdirectory(gpio)
add_subdirectory(i2c)
add_subdirectory(interp)
add_subdirectory(multicore)
add_subdirectory(picoboard)
add_subdirectory(pico_w)
add_subdirectory(pio)
add_subdirectory(pwm)
add_subdirectory(reset)
add_subdirectory(rtc)
add_subdirectory(spi)
add_subdirectory(system)
add_subdirectory(timer)
add_subdirectory(uart)
add_subdirectory(usb)
add_subdirectory(watchdog)

変更後

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

include(pico_extras_import_optional.cmake)

project(pico_examples C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0")
    message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

add_compile_options(-Wall
        -Wno-format          # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int
        -Wno-unused-function # we have some for the docs that aren't called
        )
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
    add_compile_options(-Wno-maybe-uninitialized)
endif()

# Hardware-specific examples in subdirectories:
add_subdirectory(test_01)

CMakeLists.txt を変更、保存すると再チェックがかかります。この時点ではまだ add_subdirectory(test_01) の行がエラーとして指摘されますが、これは test_01 フォルダ内に必要なファイルが入っていないためです。具体的には c のソースファイルと Build 情報の CMakeLists.txt が無いことが問題で、これらはこれから自分で作成していくこととなります。が、とりあえずお試しとして Build までやっておきたいので、ここでは test_01 フォルダに以下のファイルを置きます。

test_01.c

#include <stdio.h>
#include "pico/stdlib.h"

int main() {
    setup_default_uart();
    printf("Hello, world!\n");
    return 0;
}

CMakeLists.txt

add_executable(test_01
    test_01.c
)

# Add pico_stdlib library which aggregates commonly used features
target_link_libraries(test_01 pico_stdlib)

# create map/bin/hex/uf2 file in addition to ELF.
pico_add_extra_outputs(test_01)

これら二つのファイルを test_01 フォルダに置くとエラーが全て消えるので、最後に Visual Studio Code の下のメニューバーから Build をクリックして実行します。pico_test\build\test_01 フォルダの中に test_01.uf2 ファイルが出来ていれば成功です。

これでプロジェクトフォルダができてお試しの Build にも成功したので、あとは好きなようにコードを書いていくだけとなります。

MAX32625PICO で I2C

Arduino を使った USB-I2C 変換ツールとアプリで Arduino の代わりに MAX32625PICO を使うためのファームを作成しました。

MAX32625PICO の入手方法

Amazon やスイッチサイエンスでは取り扱いがありません。RS や Mouser、Digi-Key などを利用すれば入手できます。それらを利用できる方であれば紹介は不要だと思うのでリンクは紹介しません。

ファームウェア

下記の記事をご参照ください。

ysin1128.hatenablog.com

セットアップ

1. PC に MAX32625PICO を Bootloader を有効にした状態で接続する
MAX32625PICO のボード上のボタンを押しながら PC に接続すると MAX32625PICO が USB メモリのように認識されます。

2. MAX32625PICO にファームウェアを書き込む
USB メモリのように認識された MAX32625PICO にファームウェア (max32625_ctrl_Rxx.bin) をドラッグ&ドロップします。書き込みに成功すれば MAX32625PICO が USB メモリとしては認識されなくなり、消えます。
しかし、私の PC との相性なのか何なのか、この書き込みはよく失敗します。ファームウェアをドラッグ&ドロップしたところで、書き込み準備中の表示のまま進まなくなります。この場合は MAX32625PICO をいったん PC から外すと書き込み準備中がエラーが止まるのでそれをキャンセルして、再度、MAX32625PICO を接続するところからやり直します。運が悪いとこれを 2, 3 回繰り返すことになります。

各ピンの役割

端子数が足りなかったので GPIO/D3、GPIO/D2 はありません。

アプリと使い方

対応するアプリのバージョンに注意してください。

Visual Studio で Raspberry Pi Pico と通信する Windows アプリを作る (3)

Arduiono や Raspberry Pi Pico を操作して I2C 通信などを実行する Windows アプリを作成したのですが、この記事ではその Windows アプリの基礎となる USB-Serial 通信の制御部分を紹介します。

前の記事ですでに Visual Studio で作ったアプリで Raspberry pi pico との通信を達成していますが、この記事では通信を利用してアプリから Raspberry pi pico の GPIO を制御してみます。

どんなアプリを作るか

この記事で作成するアプリは以下のファームを書き込んだ Raspberry pi pico の制御を目的とします。このファームを書き込むと Raspberry pi pico の GP3-0 が Output, GP7-4 が Input になり、USB-Serial 通信で GP3-0 の出力を制御し、GP7-0 の High/Low 状態を取得することができます。

ysin1128.hatenablog.com

Raspberry pi pico には 1 byte 以上のデータを送信すると同じ数のデータが返ってきます。

送信データのフォーマットは以下の通りです。
Bit 7/6/5/4: 0000 (固定)
Bit 3/2/1/0: GP3/2/1/0 の出力の指示 / 0 = Low, 1 = High

返信データのフォーマットは以下の通りです。
Bit 7/6/5/4/3/2/1/0: GP7/6/5/4/3/2/1/0 の出力状態 / 0 = Low, 1 = High

送信データで GP3-0 の出力の High/Low を指示します。複数 Byte を送信すると各 Byte の指示に従って 1 ms 間隔で GP3-0 の出力が High/Low 遷移します。
返信データは GP7-0 の High/Low 状態を示します。Raspberry pi pico は送信データを受け取ると上記の通りそのデータの指示に従って GP3-0 の High/Low 状態を変更し、1 ms 経過後に GP7-0 の High/Low 状態を読み取ってデータにして返信します。送信データが複数 Byte の場合も各 byte ごとに同じ処理を行って データを返信します。

Raspberry pi pico に 0x01, 0x02 を送信した際の処理は以下の通りです。

  1. Raspberry pi pico に 0x01, 0x02 を送信する
  2. Raspberry pi pico が 0x01 に従って GP3/2/1/0 = Low/Low/Low/High を出力する
  3. 1 ms 経過
  4. Raspberry pi pico が GP7-0 の High/Low 状態を読み取って 1 byte のデータにして返信する
  5. Raspberry pi pico が 0x02 に従って GP3/2/1/0 = Low/Low/High/Low を出力する
  6. 1 ms 経過
  7. Raspberry pi pico が GP7-0 の High/Low 状態を読み取って 1 byte のデータにして返信する

フォームを改造する

Visual Studio に戻ってアプリのフォームを作ります。前回の L チカのフォームを元に改造することにします。まずは不要になった NumericUpDown を削除して、左のツールボックスから代わりに送信データを入力するための「TextBox」を追加します。ついでに「Label」も追加します。

ちなみに Label のテキストは Label を選択した状態で右下のプロパティの中の「Text」の項目を変更します。

Label を追加したのは、今回は入力用の TextBox を 4個置くからです。各TextBox の前に名札代わりに Label を置きます。また、TextBox の名前が Default のままだと 4 個の入力のどれがどれか分からなくなりそうなので名前を変更します。TextBox の名前は、対象の TextBox を選択した状態にして右下のプロパティの中の「Name」の項目を変更します。名前は何でも良いのですが、ここでは TB_IN1/2/3/4 にしています。
この 4個の TextBox に入力した値が Raspberry pi pico への送信データになります。TextBox には GP3/2/1/0 の High/Low の指示を4桁の 0/1 で記入します。例えば 0101 と記入した場合は GP3/2/1/0 = Low/High/Low/High を意味します。TextBox を 4 個用意したのは送信データを 4 byte にするためです。

関数を作成する

関数を作成します。書き込む場所は前回と同じく、作成中のフォームの button1 をクリックすると開く button1_Click 関数の中です。前回の記述をばっさり削除して書き直します。

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 j;
            int intTO;
            byte[] byteDATA = {0,0,0,0};
            string strDATA = "";
            TextBox[] TB_IN = {TB_IN1,TB_IN2,TB_IN3,TB_IN4};

            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();

            for (j = 0; j < 4; j++)
            {
                strDATA = TB_IN[j].Text;

                for (i = 0; i < 4; i++)
                {
                    if (strDATA.Substring(i, 1) == "1")
                    {
                        byteDATA[j] |= (byte)(0x01 << (3 - i));
                    }
                }
            }

            SP1.Write(byteDATA, 0, 4);

            for (j = 0; j < 4; j++)
            {
                intTO = 0;

                while (SP1.BytesToRead == 0)
                {
                    System.Threading.Thread.Sleep(1);
                    intTO++;

                    if (intTO == 100) break;
                }

                if (intTO != 100)
                {
                    SP1.Read(byteDATA, 0, 1);

                    for (i = 0; i < 8; i++)
                    {
                        if ((byteDATA[0] & (((byte)0x01) << (7 - i))) > 0)
                        {
                            textBox1.AppendText("1");
                        }
                        else
                        {
                            textBox1.AppendText("0");
                        }
                    }
                    textBox1.AppendText("\r\n");
                }
                else
                {
                    textBox1.AppendText("failed.\r\n");
                }

            }

            SP1.Close();
        }
    }
}

SerialPort 関連の記述に関しては前回の記事をご参照ください。

            for (j = 0; j < 4; j++)
            {
                strDATA = TB_IN[j].Text;

                for (i = 0; i < 4; i++)
                {
                    if (strDATA.Substring(i, 1) == "1")
                    {
                        byteDATA[j] |= (byte)(0x01 << (3 - i));
                    }
                }
            }

TextBox の入力を Raspberry pi pico への送信データに変換します。
TextBox への入力は 4 桁の 0/1 で GP3/2/1/0 の出力の High/Low を指示します。例えば TextBox への入力が 0101 の場合は Byte型の 0x05 に変換します。
ここでは 4個の TextBox (TB_IN1/2/3/4) の入力データを Byte に変換して byteDATA[0]/[1]/[2]/[3] に代入しています。

                    for (i = 0; i < 8; i++)
                    {
                        if ((byteDATA[0] & (((byte)0x01) << (7 - i))) > 0)
                        {
                            textBox1.AppendText("1");
                        }
                        else
                        {
                            textBox1.AppendText("0");
                        }
                    }
                    textBox1.AppendText("\r\n");

Raspberry pi pico からの返信データを textBox1 に出力します。
Byte型のデータを 0/1 の 8 桁の文字列に変換しています。返信データが 0xA5 だった場合、8桁の文字列は 10100101 になり、GP8/7/6/5/4/3/2/1/0 = High/Low/High/Low/Low/High/Low/High であったことを示します。

動作確認

作成したアプリの動作確認を行います。
Raspberry pi pico を PC に接続して、Visual Studio で作成したアプリの立ち上げます。アプリの立ち上げ方は Visual Studio の上のメニューの「Debug」「Any CPU」と並んだ先の 「(緑三角) (プロジェクト名: ここでは RPP_CTRL_CS)」をクリックです。

立ち上がったアプリのフォームの右上の TextBox には Raspberry pi pico の COM番号を記入します。COM 番号の確認方法は前回の記事をご参照ください。

次にアプリのフォームの Step 1/2/3/4 の TextBox に GP3-0 への指示を入力します。例えば Step 1/2/3/4 = 0001/0010/0100/1000 を記入すると Raspberry pi pico の GP3/2/1/0 の出力は Low/Low/Low/High -> Low/Low/High/Low -> Low/High/Low/Low -> High/Low/Low/Low と遷移します。

最後に button1 をクリックすると Step 1/2/3/4 に入力した通りに Raspberry pi pico の GP3-0 が遷移し、その時の GP7-0 の状態が下の TextBox に出力されます。

下図の例は Step1/2/3/4 = 0001/0010/0100/1000 を記入して button1 をクリックし、続いて Step1/2/3/4 = 1110/1101/1011/0111 を記入して button1 をクリックした結果です。出力された GP7-0 の状態において、GP3-0 の状態は Step 1/2/3/4 で指示した通りになっています。また、Raspberry pi pico 側では GP0 - GP7 と GP2 - GP6 を短絡しているので GP0 が High を出力している時には GP7 も High, GP2 が High を出力している時には GP6 も High になっています。GP5/4 は Open のままなので状態は常に Low です。

以上のように、Visual Studio を使って Raspberry pi pico の GPIO の制御することができました。GPIO だけでなく I2C や SPI も基本的な制御は同様です。また、Arduino でも Teensy でも、USB-Serial 通信が成立すれば同様に制御することができます。

これらを積み重ねて出来たのがこの自作アプリとなります。

Visual Studio で Raspberry Pi Pico と通信する Windows アプリを作る (2)

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 を制御する例を紹介します。

Raspberry Pi Pico Sample code / USB-Serial 通信で GPIO を制御する

Raspberry pi pico の C++ のサンプルコードです。

  • Raspberry pi pico の GP3-0 は Digital Output, GP7-4 は Digital Input になります
  • Raspberry pi pico は USB-serial 通信で1 byte 以上のデータを受信すると受信データと同じ数のデータを返信します
  • Raspberry pi pico は受信データの各 byte の Bit 3-0 の値に応じて GP3-0 の High/Low を制御します。例えば Bit 0 の値が 1 だった場合、GP0 の出力を High にします。Bit 7-4 の値は参照しません
  • Raspberry pi pico の返信データは、受信データに応じて GP3-0 を制御してから 1 msec 経過後の GP7-0 の High/Low 状態を示します。Bit 7-0 の値が GP 7-0 の High/Low 状態を示します。例えば 0xA5 が返信された場合、GP7, 5, 2, 0 が High、GP6, 4, 3, 1 が Low だったことを示します
  • Raspberry pi pico は受信データが複数 byte の場合、受信したデータを 1 byte ずつ、 1 msec 間隔で処理します。例えば 0x05, 0x0A の 2 byte を受信した場合:
    1. GP3/2/1/0 = Low/High/Low/High を出力する
    2. 1 msec 経過
    3. GP7-0 の High/Low 状態をデータにして返信する
    4. GP3/2/1/0 = High/Low/High/Low を出力する
    5. 1 msec 経過
    6. GP7-0 の High/Low 状態をデータにして返信する

USB-Serial 通信アプリの動作確認用に作成しました。

プロジェクトフォルダ: pico_test

pico_test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
 |-- module_serial
 |    |-- module_serial.h
 |    |-- module_serial.c
 |     -- CMakeLists.txt
  -- test_02
      |-- test_02.c
       -- CMakeLists.txt

pico_test\CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pico_ctrl C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

include(example_auto_set_url.cmake)

# Add subdirectory
add_subdirectory(test_01)
add_subdirectory(module_serial)

pico_test\module_serial\module_serial.h

#ifndef MODULE_SERIAL_H
#define MODULE_SERIAL_H

uint32_t serial_available();

uint32_t serial_read(uint8_t *read_data, int read_data_length);

uint32_t serial_write(uint8_t *write_data, int write_data_length);

uint32_t serial_write_1byte(uint8_t write_data);

#endif

pico_test_module_serial\module_serial.c

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"
#include "./module_serial.h"

uint32_t serial_available(){
    return tud_cdc_available();
}

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    uint32_t uintTMP;
    uintTMP = tud_cdc_write(write_data, write_data_length);
    tud_cdc_write_flush();
    return uintTMP;
}

uint32_t serial_write_1byte(uint8_t write_data){
    int i = 0;
    uint8_t write_data_buffer[1];
    write_data_buffer[0] = write_data;
    while(tud_cdc_write_available() < 1){
        sleep_ms(1);
        i++;

        if(i > 1000){
            tud_cdc_write_clear();
        }
    }
    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();
    return uintTMP;
}

pico_test\module_serial\CMakeLists.txt

add_library(module_serial
        module_serial.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(module_serial pico_stdlib)

# enable usb output, disable uart output
pico_enable_stdio_usb(module_serial 1)
pico_enable_stdio_uart(module_serial 0)

pico_test\test_02\test_02.c

#include <stdio.h>
#include "pico/stdlib.h"

#include "../module_serial/module_serial.h"

int main() {
    int i;
    int cntDAT;
    uint8_t getDAT[1];
    uint8_t inDAT[256];
    uint8_t outDAT[256];

    stdio_init_all();   // Setup for Serial port

    for(i=0;i<8;i++){
        gpio_init(i);   // set GP0-7 to GPIO
        gpio_pull_down(i);  // enable pull-down for GP0-7

        if(i < 4){
            gpio_set_dir(i,true);   // set GP0-3 to Output
        }
        else{
            gpio_set_dir(i,false);  // set GP4-7 to Input
        }
    }

    while (true) {
        cntDAT = 0;

        while(serial_available() > 0){
            serial_read(getDAT, 1);
            inDAT[cntDAT] = getDAT[0];
            cntDAT++;
            sleep_ms(10);
        }

        if(cntDAT > 0){
            for(i=0;i<cntDAT;i++){
                gpio_put_all(0x00000000 | (0x0F & inDAT[i]));   // set GP0-3 output level
                sleep_ms(1);
                outDAT[i] = 0xFF & gpio_get_all();              // get GP0-7 state
            }

            for(i=0;i<cntDAT;i++){
                serial_write(outDAT + i, 1);
            }
        }
        
        sleep_ms(100);
    }
}

pico_test\test_02\CMakeLists.txt

add_executable(test_02
        test_02.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(test_02 pico_stdlib module_serial)

# enable usb output, disable uart output
pico_enable_stdio_usb(test_02 1)
pico_enable_stdio_uart(test_02 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(test_02)

# add url via pico_set_program_url
example_auto_set_url(test_02)

Raspberry Pi Pico Sample code / USB-Serial 通信で指定した回数だけ Lチカする

Raspberry pi pico の C++ のサンプルコードです。

  • Raspberry pi pico は USB-serial 通信で1 byte 以上のデータを受信すると (受信データの数 + 1 byte) のデータを返信します
  • Raspberry pi pico は受信データの最初の 1 byte の値の回数だけ LED を点滅させます
  • Raspberry pi pico の返信データの 1 byte目は受信したデータ数 (byte数)、2 byte目以降は受信データをそのまま返信します

USB-Serial 通信アプリの動作確認用に作成しました。

プロジェクトフォルダ: pico_test

pico_test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
 |-- module_serial
 |    |-- module_serial.h
 |    |-- module_serial.c
 |     -- CMakeLists.txt
  -- test_01
      |-- test_01.c
       -- CMakeLists.txt

pico_test\CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pico_ctrl C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

include(example_auto_set_url.cmake)

# Add subdirectory
add_subdirectory(test_01)
add_subdirectory(module_serial)

pico_test\module_serial\module_serial.h

#ifndef MODULE_SERIAL_H
#define MODULE_SERIAL_H

uint32_t serial_available();

uint32_t serial_read(uint8_t *read_data, int read_data_length);

uint32_t serial_write(uint8_t *write_data, int write_data_length);

uint32_t serial_write_1byte(uint8_t write_data);

#endif

pico_test_module_serial\module_serial.c

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"
#include "./module_serial.h"

uint32_t serial_available(){
    return tud_cdc_available();
}

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    uint32_t uintTMP;
    uintTMP = tud_cdc_write(write_data, write_data_length);
    tud_cdc_write_flush();
    return uintTMP;
}

uint32_t serial_write_1byte(uint8_t write_data){
    int i = 0;
    uint8_t write_data_buffer[1];
    write_data_buffer[0] = write_data;
    while(tud_cdc_write_available() < 1){
        sleep_ms(1);
        i++;

        if(i > 1000){
            tud_cdc_write_clear();
        }
    }
    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();
    return uintTMP;
}

pico_test\module_serial\CMakeLists.txt

add_library(module_serial
        module_serial.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(module_serial pico_stdlib)

# enable usb output, disable uart output
pico_enable_stdio_usb(module_serial 1)
pico_enable_stdio_uart(module_serial 0)

pico_test\test_01\test_01.c

#include <stdio.h>
#include "pico/stdlib.h"

#include "../module_serial/module_serial.h"

int main() {
    int i;
    int cntDAT;
    uint8_t getDAT[1];
    uint8_t inDAT[256];
    uint8_t outDAT[256];

    stdio_init_all();   // Setup for Serial port

    const uint LED_PIN = PICO_DEFAULT_LED_PIN;
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    while (true) {
        cntDAT = 0;

        while(serial_available() > 0){
            serial_read(getDAT, 1);
            inDAT[cntDAT] = getDAT[0];
            cntDAT++;
            sleep_ms(10);
        }

        if(cntDAT > 0){
            outDAT[0] = 0xFF & cntDAT;
            serial_write(outDAT, 1);
            serial_write(inDAT, cntDAT);

            for(i=0; i<inDAT[0]; i++){
                gpio_put(LED_PIN, 1);
                sleep_ms(500);
                gpio_put(LED_PIN, 0);
                sleep_ms(500);
            }
        }
        
        sleep_ms(100);
    }
}

pico_test\test_01\CMakeLists.txt

add_executable(test_01
        test_01.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(test_01 pico_stdlib module_serial)

# enable usb output, disable uart output
pico_enable_stdio_usb(test_01 1)
pico_enable_stdio_uart(test_01 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(test_01)

# add url via pico_set_program_url
example_auto_set_url(test_01)