usb serial

本章では、usbシリアル通信を用いてrp2040とPCで通信できるようにします。 必要なツールは以下です。

用途ツール名
文字入出力ツールTera Term VT
デバッグツールBaker link. Dev
ターゲットRaspbery Pi pico

動作の内容は、Tera Termで入力した小文字アルファベットが大文字アルファベットとして返信されます。 以下は、Tera Termでの動作動画です。

本チャプターでは、Baker link. Devを外部マイコンへの書き込み機能で用います。

外部マイコンへの書き込み手順(Rasberry Pi Pico)

  1. Baker link. Envを起動し、プロジェクト名、createクリック、プロジェクト保存先を選択すると、数秒後にVisual Studio Codeが開きます。

  1. Visual Studio Codeの左下に表示される「コンテナ―で再度開く」をクリックしてください。すると、Dockerイメージのダウンロード&ビルド処理が開始されます。この処理が数分程度かかりますので、しばらくお待ちください。

  1. src/main.rsを開き、以下のプログラムに書き換えてください。 コードにはcopilotでコメントを添えています。
#![no_std] // 標準ライブラリを使用しないことを指定
#![no_main] // 標準のエントリーポイント(main関数)を使用しないことを指定

// RTT(リアルタイムトレース)を使用するための設定
use defmt_rtt as _;
// パニック時のプローブ設定
use panic_probe as _;
// RP2040のハードウェア抽象化レイヤーをインポート
use rp2040_hal as hal;
// スタートアップ関数のマクロ
use hal::entry;
// Peripheral Access Crateの短縮エイリアス、低レベルのレジスタアクセスを提供
use hal::pac;
// USBデバイスサポート
use usb_device::{class_prelude::*, prelude::*};
// USB通信クラスデバイスサポート
use usbd_serial::SerialPort;
// フォーマットされた文字列を書き込むために使用
use core::fmt::Write;
use heapless::String;

/// ベアメタルアプリケーションのエントリーポイント。
///
/// `#[entry]`マクロは、Cortex-Mのスタートアップコードがすべてのグローバル変数を初期化した後にこの関数を呼び出すことを保証します。
///
/// この関数はRP2040の周辺機器を設定し、USBシリアル経由で受信した文字をエコーします。
/// 
#[link_section = ".boot2"] // ブートローダーセクションを指定
#[used] // コンパイラにこの静的変数が使用されることを明示
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; // ブートローダーのデータ

const XTAL_FREQ_HZ: u32 = 12_000_000u32; // 外部クリスタルの周波数を定義
#[entry]
fn main() -> ! {
    // シングルトンオブジェクトを取得
    let mut pac = pac::Peripherals::take().unwrap(); // 周辺機器のハンドルを取得

    // ウォッチドッグドライバを設定 - クロック設定コードに必要
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); // ウォッチドッグタイマーを初期化

    // クロックを設定
    //
    // デフォルトでは125 MHzのシステムクロックを生成
    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ, // 外部クリスタルの周波数
        pac.XOSC, // 外部オシレータ
        pac.CLOCKS, // クロック制御
        pac.PLL_SYS, // システムPLL
        pac.PLL_USB, // USB PLL
        &mut pac.RESETS, // リセット制御
        &mut watchdog, // ウォッチドッグタイマー
    )
    .ok()
    .unwrap(); // クロック設定が成功したか確認

    let timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); // タイマーを初期化

    #[cfg(feature = "rp2040-e5")] // コンパイル時の条件付きコンパイル
    {
        let sio = hal::Sio::new(pac.SIO); // シリアル入出力を初期化
        let _pins = hal::Pins::new(
            pac.IO_BANK0, // IOバンク0
            pac.PADS_BANK0, // パッドバンク0
            sio.gpio_bank0, // GPIOバンク0
            &mut pac.RESETS, // リセット制御
        );
    }

    // USBドライバを設定
    let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
        pac.USBCTRL_REGS, // USBコントローラのレジスタ
        pac.USBCTRL_DPRAM, // USBコントローラのDPRAM
        clocks.usb_clock, // USBクロック
        true, // VBUS検出を有効にする
        &mut pac.RESETS, // リセット制御
    ));

    // USB通信クラスデバイスドライバを設定
    let mut serial = SerialPort::new(&usb_bus); // シリアルポートを初期化

    // 偽のVIDとPIDでUSBデバイスを作成
    let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd)) // USBデバイスを構築
        .strings(&[StringDescriptors::default()
            .manufacturer("Fake company") // メーカー名
            .product("Serial port") // 製品名
            .serial_number("TEST")]) // シリアル番号
        .unwrap()
        .device_class(2) // デバイスクラスコード(2は通信デバイス)
        .build();

    let mut said_hello = false; // ウェルカムメッセージを表示したかどうかのフラグ
    loop {
        // 最初にウェルカムメッセージを表示
        if !said_hello && timer.get_counter().ticks() >= 2_000_000 { // タイマーが2秒以上経過したか確認
            said_hello = true; // フラグを更新
            let _ = serial.write(b"Hello, World!\r\n"); // ウェルカムメッセージを送信

            let time = timer.get_counter().ticks(); // 現在のタイマー値を取得
            let mut text: String<64> = String::new(); // フォーマットされた文字列を格納するバッファ
            writeln!(&mut text, "Current timer ticks: {}", time).unwrap(); // タイマー値を文字列に書き込む

            // これは、シリアルポートに書き込まれるバイト数がUSBペリフェラルに利用可能なバッファよりも小さいため、信頼性があります。
            // 一般的には、転送されていないバイトが失われないように、戻り値を処理する必要があります。
            let _ = serial.write(text.as_bytes()); // タイマー値をシリアルポートに送信
        }

        // 新しいデータをチェック
        if usb_dev.poll(&mut [&mut serial]) { // USBデバイスのポーリング
            let mut buf = [0u8; 64]; // データを格納するバッファ
            match serial.read(&mut buf) { // シリアルポートからデータを読み込む
                Err(_e) => {
                    // 何もしない
                }
                Ok(0) => {
                    // 何もしない
                }
                Ok(count) => {
                    // 大文字に変換
                    buf.iter_mut().take(count).for_each(|b| {
                        b.make_ascii_uppercase(); // 文字を大文字に変換
                    });
                    // ホストに送り返す
                    let mut wr_ptr = &buf[..count]; // 書き込みポインタを設定
                    while !wr_ptr.is_empty() { // バッファが空になるまでループ
                        match serial.write(wr_ptr) { // シリアルポートに書き込む
                            Ok(len) => wr_ptr = &wr_ptr[len..], // 書き込んだ分だけポインタを進める
                            // エラーが発生した場合、未書き込みデータを破棄します。
                            // 一つの可能なエラーはErr(WouldBlock)で、これはUSB書き込みバッファがいっぱいであることを意味します。
                            Err(_) => break, // エラーが発生したらループを抜ける
                        };
                    }
                }
            }
        }
    }
}

// ファイルの終わり
  1. Baker link. EnvのRunをクリックしてください。するとバックグラウンドで、probe-rsのDAP Serverが起動します。

  1. Baker link. DevとRasberry Pi PicoをJST-SH型3ピンコネクタケーブルで接続してください。

  1. Raspberry Pi Picoに電源を供給するために、Raspberry Pi PicoとPCをUSBケーブルで接続します。

  1. 次のBaker link. Devを外部マイコン書き込みモードで接続するために、真ん中のボタンを押しながら、Baker link. DevとPCをUSBケーブルで接続してください。

外部マイコン書き込みモードで接続されると緑のLEDが点灯します。

  1. Visual Studio CodeでF5キーを押してください。すると、以下のようなアイコンが表示されます。

  1. もう一度、F5キーを押すと、プログラムが動作します。Tera Termの設定は、新しい接続でシリアル、ポートは書き込んだマイコンボードのポート番号を選択してください。また、設定->端末->ローカルエコーにチェックを入れると文字を記入できます。

rp-rsを参考にしました。