VSCode+ラズピコ拡張 USB-CDCでPCと通信してみる

  • 2025.08.24
  • USB
VSCode+ラズピコ拡張 USB-CDCでPCと通信してみる

皆さん こんにちは

ラズピコ研究員の moon です。

この記事は「初心者必見! Raspberry Pi Picoの C/C++デバッグ環境を容易に構築する」によって環境構築していることを前提にしています。

皆さんはUSBで通信したことがありますか?

私はプログラムの書き込み等のツールとしてUSBを使うことは多いのですが、実際のシステムにおいては使ったことがありません。

今回はUSBの通信を使ってPCからラズピコに簡単なコマンドを送ってLEDをOn/Off制御してみます。

C/C++ SDKの中にTinyUSBというライブラリがあるので今回はその中のCDCという機能を使ってみます。

USB-CDC

CDCは Communication Device Class の略です。
インターフェースは USB ですが内容は UART のシリアル通信に似ています。
実際にプログラムを動かすとデバイスマネージャーでは仮想COMポートが2つ見えるようです。

通信するPC側の端末はVSCodeのシリアルモニタを使えば良いでしょう。

用途とか条件等:

UARTと違って USB の信号をつかうため、半二重通信になります。(同時に送受信できません)

CDCでは送受信する最大パケット長が64バイトになるので注意が必要です。
(Full-Speed device(12Mbps)の場合は最大64バイトで、ラズピコのマイコンRP2040はこの仕様に当たります)

これを超えるサイズの電文を送る際には、分割して送るなどの処理が必要になります。

後はUSBコネクタの特徴から、常時ケーブルを接続して使うのは難しいかも知れません。
一時的につないで設定を行う等の使い方が一般的かも知れません。

接続

Pico Probe に付属のケーブルを使って、以下のようにつなぎます。
USBケーブル2本は両方ともにPCに接続します。

信号は同じピン番号にアサインされているのでストレートにつなぎます。

クロック(SWCLK) GND データ(SWDIO)
Raspberry Pi Pico H コネクタ 1 2 3
Debug Probeのデバッグ用コネクタ 1 2 3

ただしDebug Probe の方はコネクタが2つあるので注意が必要です。
透明のケースには上から見ると、UとDの文字が刻印されています。

U : UARTのコネクタ
D : Debug用のコネクタ

Dの方にケーブルをつなぎます。

Debug Probe(左)とラズピコ(右)を接続した様子を以下の画像で確認してください。

今回は UART を使わないので UART の接続は不要です。

プロジェクトを作成する

何度かプロジェクトを作成しているので詳細は省略します。
VSCodeからRaspberry Pi Pico Projectを選択し、New C/C++ Project を選択します。

Name に usb と入力し、 Board type で Pico を選択します。
それ以外はデフォルト設定のままで構いません。
選択後、右下の Create ボタンを押します。

キットの選択

キットはビルドツールと考えれば良いでしょう。
usb のキットを選択してください と言われるので、その下に出て来る候補の一番下の 「Pico コンパイラの使用: C = C:\Users…」を選択します。

実行とデバッグ

アクティビティーバーの実行とデバッグを選択します。

メニューの 実行 – デバッグの開始を選択します。

usb の起動対象を選択します と言われるので、その下の候補から usb を選択します。

デバッガーが起動し、main()関数のところでプログラムが停止します。

F5キーを押してプログラムが動作することを確認できたら、Shift + F5キーを押してプログラムを停止します。

参考にしたサンプルコード

今回は SDK のサンプルコードではなく TinyUSB の中にあるものを選びました。

C:\Users\m3925\.pico-sdk\sdk\2.2.0\lib\tinyusb\examples\device\cdc_dual_ports\src

ここにある main.c を改造して、先ほどのプロジェクトでつくられた usb.c と置き換えます。
以下にコードを掲示しておきます。

while(1)ループの中で、tud_task()を動かしておく必要があるようです。
これはTinyUSBのデバイスのタスクです。
それからもうひとつ、cdc_task()があります。
今回はこの関数の中のコードに手を加えました。

少しやり過ぎた感があるのですが、先頭の文字’L’の前にゴミがあったら捨てたり、小分けにして送られてきた場合でも結合して電文の判断が付くようにしたつもりです。
このへんは主旨からはずれる部分ですからバグ等があってもご容赦ください。

仕様としては
PCから”Led 1″ の文字列を受け取るとLEDを点灯します。(LFの終端をつけて電文長を6にして送信します)
そして”Led On”の文字列を返信します。

PCから”Led 0″ の文字列を受け取るとLEDを消灯します。(LFの終端をつけて電文長を6にして送信します)
そして”Led Off”の文字列を返信します。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>

#include "bsp/board_api.h"
#include "tusb.h"

#include "cdc_device.h"
#include "tusb_fifo.h"

/* Blink pattern
 * - 250 ms  : device not mounted
 * - 1000 ms : device mounted
 * - 2500 ms : device is suspended
 */
enum {
  BLINK_NOT_MOUNTED = 250,
  BLINK_MOUNTED = 1000,
  BLINK_SUSPENDED = 2500,
};

enum result {
  LED_ON,
  LED_OFF,
  FAILURE,
};

static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;

static void led_blinking_task(void);
static void cdc_task(void);

static bool search_and_replace_stx(char *buffer, uint32_t *count);
static void move_data(char *dst, char *src, uint32_t count);
static enum result parse(char *buffer, int total);

int main(void) {
  board_init();

  // init device stack on configured roothub port
  tusb_rhport_init_t dev_init = {
    .role = TUSB_ROLE_DEVICE,
    .speed = TUSB_SPEED_AUTO
  };
  tusb_init(BOARD_TUD_RHPORT, &dev_init);

  if (board_init_after_tusb) {
    board_init_after_tusb();
  }

  while (1) {
    tud_task(); // tinyusb device task
    cdc_task();
  }
}

// Invoked when device is mounted
void tud_mount_cb(void) {
  blink_interval_ms = BLINK_MOUNTED;
}

// Invoked when device is unmounted
void tud_umount_cb(void) {
  blink_interval_ms = BLINK_NOT_MOUNTED;
}

//--------------------------------------------------------------------+
// USB CDC
//--------------------------------------------------------------------+

// Led 1 CR を受信すると LED On
// Led 0 CR を受信すると LED Off

// 文字列の先頭は大文字の 'L' でなければならない

const char START_OF_TEXT = 'L';

const int COMMAND_LENGTH = 6;

uint8_t buf[2][64];

int wp[2] = {0};
int total[2] = {0};

const char* led_on = "Led On";
const char* led_off = "Led Off";

static void cdc_task(void) {
  uint8_t itf;
  char buffer[64];
  for (itf = 0; itf < CFG_TUD_CDC; itf++) {
    while (tud_cdc_n_available(itf)) {

      uint32_t count = tud_cdc_n_read(itf, buffer, sizeof(buffer));

      // さすがに tud_cdc_n_read() はそのまま使いたい
      // もし途中に'L'の文字があった場合でも、'L'の文字から受信するし始めるように調整する
      bool b = search_and_replace_stx(buffer, &count);

      if (b) {
        move_data(&buf[itf][0], buffer, count); // 先頭 or 仕切り直し
      } else {
        move_data(&buf[itf][wp[itf]], buffer, count);
      }

      if (buf[itf][0] != START_OF_TEXT) {
        wp[itf] = 0;
        continue;
      }
      wp[itf] += count;
      total[itf] += count;
      if (6 <= total[itf]) {
        enum result r = parse(&buf[itf][0], total[itf]);
        if (r == LED_ON) {
          board_led_write(1);
          tud_cdc_n_write_str(itf, led_on);
        } else if (r == LED_OFF) {
          board_led_write(0);
          tud_cdc_n_write_str(itf, led_off);
        }
        tud_cdc_n_write_flush(itf);

        wp[itf] = 0;
        total[itf] = 0;
      }
    }
  }
}

// もしバッファの途中に先頭を示す文字'L'を見つけたら、やり直しする
static bool search_and_replace_stx(char *buffer, uint32_t *count) {
  char buf2[64];
  
  for (int i = 0; i < *count; i++) {
    if (i == 0) {
      if (buffer[0] == START_OF_TEXT) {
        if (*count < COMMAND_LENGTH) {
          if (buffer[--*count] == '\r') {
              *count--;   // 文中の改行コードは捨てる
          }
        }
        return true;
      }
    } else {
      if (buffer[i] == START_OF_TEXT) {
        for (int j = 0; j < *count - i; j++) {
          buf2[j] = buffer[i + j];
        }
        *count = *count - i;
        for (int k = 0; k < *count; k++) {
          buffer[k] = buf2[k];
          if (buffer[k] == '\r') {
            if (k < COMMAND_LENGTH) {
              *count = k;   // 文中の改行コードは捨てる
            }
            return true;
          }
        }
        return true;
      }
    }
  }
  return false;
}

// 単純コピー
static void move_data(char *dst, char *src, uint32_t count) {
  for (int i = 0; i < count; i++) {
    dst[i] = src[i];
  }
}

// 解析
static enum result parse(char *buffer, int total) {
  if (buffer[0] != START_OF_TEXT) {
    return FAILURE;
  }
  buffer[5] = '\0';
  if (strcmp("Led 1", buffer) == 0) {
    return LED_ON;
  } else {
    if (strcmp("Led 0", buffer) == 0) {
      return LED_OFF;
    }
  }
  return FAILURE;
}

// Invoked when cdc when line state changed e.g connected/disconnected
// Use to reset to DFU when disconnect with 1200 bps
void tud_cdc_line_state_cb(uint8_t instance, bool dtr, bool rts) {
  (void)rts;

  // DTR = false is counted as disconnected
  if (!dtr) {
    // touch1200 only with first CDC instance (Serial)
    if (instance == 0) {
      cdc_line_coding_t coding;
      tud_cdc_get_line_coding(&coding);
      if (coding.bit_rate == 1200) {
        if (board_reset_to_bootloader) {
          board_reset_to_bootloader();
        }
      }
    }
  }
}

CMakeLists.txtを編集する

このファイルの中でSDKのパスは PICO_SDK_PATH として認識されています。
値は ${PICO_SDK_PATH} とすれば取り出すことができます。
TinyUSB のパスはこれを使って ${PICO_SDK_PATH}/lib/tinyusb となります。

皆さんの環境でもパスが正しいことを確認しておくと良いでしょう。

そこで私は TINYUSB_DIR を以下のように定義しました。

if(NOT DEFINED TINYUSB_DIR)
    set(TINYUSB_DIR ${PICO_SDK_PATH}/lib/tinyusb)
    message(STATUS "TINYUSB_DIR = ${TINYUSB_DIR}")
endif()

また、CDCを使うのに必要と思われるソース(*.c)を TARGET_FILES にまとめました。

set(TARGET_FILES
    ${CMAKE_CURRENT_LIST_DIR}/usb.c
    ${TINYUSB_DIR}/examples/device/cdc_dual_ports/src/usb_descriptors.c
    ${TINYUSB_DIR}/src/tusb.c
)

更に、必要と思われるヘッダは試行錯誤しながら以下のように指示しました。

target_include_directories(usb PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}
    ${TINYUSB_DIR}/examples/device/cdc_dual_ports/src
    ${TINYUSB_DIR}/hw
    ${TINYUSB_DIR}/src/class/cdc
    ${TINYUSB_DIR}/src/common
)

ライブラリの追加方法はSDKの公式ドキュメントを見ても詳しく書いてありませんでした。

examplesの CMakeLists.txt を見ながら以下のライブラリをリンクして動作するようになりました。

target_link_libraries(usb
    pico_stdlib
    tinyusb_device
    tinyusb_board
) 

ソースコードよりも、こちらの依存関係をマッチさせてビルドを通すまでに時間がかかったかも知れません。
以下にファイルの内容を掲示しておきます。

# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

set(FAMILY rp2040)
set(BOARD pico_sdk)

if(NOT DEFINED TINYUSB_DIR)
    set(TINYUSB_DIR ${PICO_SDK_PATH}/lib/tinyusb)
    message(STATUS "TINYUSB_DIR = ${TINYUSB_DIR}")
endif()

set(TARGET_FILES
    ${CMAKE_CURRENT_LIST_DIR}/usb.c
    ${TINYUSB_DIR}/examples/device/cdc_dual_ports/src/usb_descriptors.c
    ${TINYUSB_DIR}/src/tusb.c
)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)

# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
    set(USERHOME $ENV{USERPROFILE})
else()
    set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
    include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico CACHE STRING "Board type")

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

project(usb C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(usb ${TARGET_FILES})

target_compile_options(usb PRIVATE -O0)

pico_set_program_name(usb "usb")
pico_set_program_version(usb "0.1")

# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(usb 0)
pico_enable_stdio_usb(usb 0)

# Add the standard library to the build
target_link_libraries(usb
    pico_stdlib
    tinyusb_device
    tinyusb_board
)

# Add the standard include files to the build
target_include_directories(usb PRIVATE
        ${CMAKE_CURRENT_LIST_DIR}
        ${TINYUSB_DIR}/examples/device/cdc_dual_ports/src
        ${TINYUSB_DIR}/hw
        ${TINYUSB_DIR}/src/class/cdc
        ${TINYUSB_DIR}/src/common
)

pico_add_extra_outputs(usb)

ビルドしてみる

試行錯誤を繰り返した後、なんとかビルドが通るようになりました。
そのあと、usb.c に手を加えていきました。

動作させてみる

F5キーを押してプログラムを動作させておきます。
PCからの送信はVSCodeのシリアルモニターを使ってみました。

エディター下の、下部パネルの右の方にあるシリアルモニターを選択します。

ポートでUSBの仮想シリアルポートを選択します。
USBの仮想シリアルポート(2つ)はプログラムを動かすと認識されます。
プログラムを動作させた後、追加されたポートのいずれかを選択します。

ボーレート:115200
行の終わり:CRを選択します。

監視の開始ボタンを押して、下のテキストボックスに Led 1 と入力し、メッセージの送信ボタンを押します。
下図の緑色で囲った部分のボタンです。

LEDが点灯すれば成功です。
点灯した場合、"Led On"の文字列を返信します。

Led 0 と入力し、メッセージの送信ボタンを押すとLEDが消灯します。
消灯した場合、"Led Off"の文字列を返信します。

いかがでしたか皆さんの環境では USB-CDC が動きましたか?

GitHubの こちら にプロジェクトを置きましたので参考になさってください。

お疲れさまでした。

USBカテゴリの最新記事