Skip to content

Communcication Device Class (CDC)

The easiest way to provide a serial interface over USB is to use the stdio support provided by the Pico SDK. Just by calling pico_enable_stdio_usb() in your CMakeLists.txt, your firmware will appear as a serial port on the host computer, and you can send and receive data through functions like printf() and getchar(). However, when using USB stdio, you can not provide multiple serial ports or use USB port for other purposes such as mass storage or HID. You can use the USB Device library provided by pico-jxglib to overcome these limitations.

Single Serial Port

Here, we will create a USB device that implements the Communication Device Class (CDC) and echos back any data sent from the host computer.

Building and Flashing the Program

Create a new Pico SDK project named usbdev-cdc.

Create Pico SDK Project
  1. In VSCode, run >Raspberry Pi Pico: New Pico Project in the command palette.
  2. In the dialog below, select C/C++. new-project-dialog

  3. Create a project with the following settings:

    • Name ... Enter the project name.
    • Board type ... Select your board type.
    • Location ... Select the parent directory where the project directory will be created.
    • Stdio support ... Leave Console over USB unchecked when you use LABOPlatform or other USB features because they conflict with each other. You can enable it by editing the CMakeLists.txt file later.
    • Code generation options ... Check Generate C++ code.

new-project

Open Existing Pico SDK Project

Open the project folder in VSCode using one of the following methods:

  • In a command prompt, change the current directory to the project folder and execute code ..
  • In a Explorer, choose the project folder, push Alt+D to focus the address bar, and execute code ..

    vscode-from-explorer

If the folder is already prepared as a Pico SDK project, just proceed with editing and building the project.

If not, you will see the following message at the bottom right corner of VSCode.

do-you-want-to-import

Click Yes and you will see the following window.

import-project

Click Import and the project will be prepared as a Pico SDK project.

When the Do you want to import this project as Raspberry Pi Pico project? message disappears before you click Yes, you can reveal it by clicking the icon notify-icon at the bottom right corner of VSCode.

Clone the pico-jxglib repository from GitHub so the direcory structure looks like this:

├── pico-jxglib/
└── usbdev-cdc/
    ├── CMakeLists.txt
    ├── usbdev-cdc.cpp
    └── ...
Clone the Repository

Change the current directory to the parent directory where you want to clone the pico-jxglib repository and run the following commands:

$ git clone https://github.com/ypsitau/pico-jxglib.git
$ cd pico-jxglib
$ git submodule update --init --recursive

pico-jxglib is updated almost daily. If you've already cloned it, run the following command in the pico-jxglib directory to get the latest version:

$ git pull

A directory from a Git repository can safely be moved to another location even after cloning.

Add the following lines to the end of CMakeLists.txt:

CMakeLists.txt
1
2
3
4
target_link_libraries(usbdev-cdc
    jxglib_USBDevice)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
jxglib_configure_USBDevice(usbdev-cdc CFG_TUD_CDC 1)

Edit usbdev-cdc.cpp as follows:

usbdev-cdc.cpp
#include "pico/stdlib.h"
#include "jxglib/USBDevice/CDC.h"

using namespace jxglib;

int main(void)
{
    ::stdio_init_all(); 
    USBDevice::Controller deviceController({
            bcdUSB:             0x0200,
            bDeviceClass:       TUSB_CLASS_MISC,
            bDeviceSubClass:    MISC_SUBCLASS_COMMON,
            bDeviceProtocol:    MISC_PROTOCOL_IAD,
            bMaxPacketSize0:    CFG_TUD_ENDPOINT0_SIZE,
            idVendor:           0xcafe,
            idProduct:          USBDevice::GenerateSpecificProductId(0x4000),
            bcdDevice:          0x0100,
        },
        0x0409,
        "pico-jxglib sample",
        "CDC Echo Back",
        "01234567"
    );
    USBDevice::CDC cdc(deviceController, "EchoBack Normal", 0x81, 8, 0x02, 0x82, 64);
    deviceController.Initialize();
    cdc.Initialize();
    for (;;) {
        if (cdc.cdc_available()) {
            char buff[64];
            int bytes = cdc.cdc_read(buff, sizeof(buff));
            cdc.cdc_write(buff, bytes);
            cdc.cdc_write_flush();
        }
        Tickable::Tick();
    }
}

Build and flash the program to the board.

Build and Flash

The simplest way to build and flash the program is to use the UF2 file generated by the build process. Pico board, a USB cable, and a computer are all you need to get started. Here are the steps to build and flash the program:

  1. Pressing F7 on VSCode will build the project. If this is the first time you build the project, you will see the dialog shown below. Select Pico Using compilers: ... and the build process will start.

    select-a-kit

  2. After building, you can find the generated UF2 file in the build directory.

  3. Connect your Pico to the computer using a USB cable while holding the BOOTSEL button, and it will appear as a mass storage device. Copy the generated UF2 file to this device to flash it. No need to mind the destination directory, just copy it to the root directory of the device.

If you have a debug probe like this, you can also flash the program using OpenOCD and GDB. This is the recommended method for development, as it allows you to debug the program while running it on the board!

Running the Program

Multiple Serial Ports

Here, we will create a USB device that implements multiple CDC interfaces, which appear as multiple serial ports on the host computer. Each serial port will echo back data sent from the host in different manners such as transforming the characters to uppercase and encrypting them.

Building and Flashing the Program

Create a new Pico SDK project named usbdev-cdc-multi.

Create Pico SDK Project
  1. In VSCode, run >Raspberry Pi Pico: New Pico Project in the command palette.
  2. In the dialog below, select C/C++. new-project-dialog

  3. Create a project with the following settings:

    • Name ... Enter the project name.
    • Board type ... Select your board type.
    • Location ... Select the parent directory where the project directory will be created.
    • Stdio support ... Leave Console over USB unchecked when you use LABOPlatform or other USB features because they conflict with each other. You can enable it by editing the CMakeLists.txt file later.
    • Code generation options ... Check Generate C++ code.

new-project

Open Existing Pico SDK Project

Open the project folder in VSCode using one of the following methods:

  • In a command prompt, change the current directory to the project folder and execute code ..
  • In a Explorer, choose the project folder, push Alt+D to focus the address bar, and execute code ..

    vscode-from-explorer

If the folder is already prepared as a Pico SDK project, just proceed with editing and building the project.

If not, you will see the following message at the bottom right corner of VSCode.

do-you-want-to-import

Click Yes and you will see the following window.

import-project

Click Import and the project will be prepared as a Pico SDK project.

When the Do you want to import this project as Raspberry Pi Pico project? message disappears before you click Yes, you can reveal it by clicking the icon notify-icon at the bottom right corner of VSCode.

Clone the pico-jxglib repository from GitHub so the direcory structure looks like this:

├── pico-jxglib/
└── usbdev-cdc-multi/
    ├── CMakeLists.txt
    ├── usbdev-cdc-multi.cpp
    └── ...
Clone the Repository

Change the current directory to the parent directory where you want to clone the pico-jxglib repository and run the following commands:

$ git clone https://github.com/ypsitau/pico-jxglib.git
$ cd pico-jxglib
$ git submodule update --init --recursive

pico-jxglib is updated almost daily. If you've already cloned it, run the following command in the pico-jxglib directory to get the latest version:

$ git pull

A directory from a Git repository can safely be moved to another location even after cloning.

Add the following lines to the end of CMakeLists.txt:

CMakeLists.txt
1
2
3
4
target_link_libraries(usbdev-cdc-multi
    jxglib_USBDevice)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
jxglib_configure_USBDevice(usbdev-cdc-multi CFG_TUD_CDC 3)

Note

We set CFG_TUD_CDC to 3 in jxglib_configure_USBDevice() since we are using three CDC interfaces.

Edit usbdev-cdc-multi.cpp as follows:

usbdev-cdc-multi.cpp
#include <ctype.h>
#include "pico/stdlib.h"
#include "jxglib/USBDevice/CDC.h"

using namespace jxglib;

int main(void)
{
    ::stdio_init_all(); 
    USBDevice::Controller deviceController({
            bcdUSB:             0x0200,
            bDeviceClass:       TUSB_CLASS_MISC,
            bDeviceSubClass:    MISC_SUBCLASS_COMMON,
            bDeviceProtocol:    MISC_PROTOCOL_IAD,
            bMaxPacketSize0:    CFG_TUD_ENDPOINT0_SIZE,
            idVendor:           0xcafe,
            idProduct:          USBDevice::GenerateSpecificProductId(0x4000),
            bcdDevice:          0x0100,
        },
        0x0409,
        "pico-jxglib sample",
        "CDC Echo Back Multi",
        "0123456"
    );
    USBDevice::CDC cdc1(deviceController, "EchoBack Normal", 0x81, 8, 0x02, 0x82, 64);
    USBDevice::CDC cdc2(deviceController, "EchoBack Upper", 0x83, 8, 0x04, 0x84, 64);
    USBDevice::CDC cdc3(deviceController, "EchoBack Crypt", 0x85, 8, 0x06, 0x86, 64);
    deviceController.Initialize();
    cdc1.Initialize();
    cdc2.Initialize();
    cdc3.Initialize();
    for (;;) {
        char buff[64];
        if (cdc1.cdc_available()) {
            int bytes = cdc1.cdc_read(buff, sizeof(buff));
            cdc1.cdc_write(buff, bytes);
            cdc1.cdc_write_flush();
        }
        if (cdc2.cdc_available()) {
            int bytes = cdc2.cdc_read(buff, sizeof(buff));
            for (int i = 0; i < bytes; i++) buff[i] = toupper(buff[i]);
            cdc2.cdc_write(buff, bytes);
            cdc2.cdc_write_flush();
        }
        if (cdc3.cdc_available()) {
            int bytes = cdc3.cdc_read(buff, sizeof(buff));
            for (int i = 0; i < bytes; i++) buff[i] = buff[i] + 1;
            cdc3.cdc_write(buff, bytes);
            cdc3.cdc_write_flush();
        }
        Tickable::Tick();
    }
}

Build and flash the program to the board.

Build and Flash

The simplest way to build and flash the program is to use the UF2 file generated by the build process. Pico board, a USB cable, and a computer are all you need to get started. Here are the steps to build and flash the program:

  1. Pressing F7 on VSCode will build the project. If this is the first time you build the project, you will see the dialog shown below. Select Pico Using compilers: ... and the build process will start.

    select-a-kit

  2. After building, you can find the generated UF2 file in the build directory.

  3. Connect your Pico to the computer using a USB cable while holding the BOOTSEL button, and it will appear as a mass storage device. Copy the generated UF2 file to this device to flash it. No need to mind the destination directory, just copy it to the root directory of the device.

If you have a debug probe like this, you can also flash the program using OpenOCD and GDB. This is the recommended method for development, as it allows you to debug the program while running it on the board!

Running the Program