How to send data to a Raspberry Pi Pico via USB

Do you need to transfer data to and from a Raspberry Pi Pico, or similar RP2040-based board, connected to your computer by USB? Here’s a neat way to achieve it without any tedious mucking about with the USB stack. Apart from a couple of questions on the Raspberry Pi Forum, there’s not much in the way of documentation, so here’s a write-up.

One of the projects I’m working on could be improved by allowing bulk data to be sent from a program running on my Mac to a connected Pico. Transmission is ad hoc: it only takes place when the user needs to send something. Because the two devices are connected by USB, that’s the medium I wanted to use for data transfer, rather than take the easier but clunkier approach of feeding one of the Pico’s UARTs from a USB-to-Serial device.

But how to achieve this without having to suss out how to work explicitly with USB? The trick is to use the Pico SDK’s built-in debugging mechanism. The SDK lets you route stdin and stdout over UART. It also lets you route this UART over USB.

You may very well have used this already: call stdio_init_all() from your code, or include pico_enable_stdio_usb(my_project 1) and/or pico_enable_stdio_uart(my_project 1) in your CMakeLists.txt file, and you can issue debugging statements from your running code simply by calling C’s standard printf() function. Run a terminal emulator at the other end, like screen or Minicom, and you can view the debugging output.

Here’s the trick, such as it is: don’t use this method for logging, use it for data transfer. Here’s how.

The Pico SDK defines the function getchar_timeout_us(). It returns the next character in the Pico’s stdin FIFO (First In, First Out) buffer as a signed integer. It takes a timeout period in microseconds and it’ll wait that long for a character to appear. Or you can pass zero to get any waiting character or return immediately.

Here’s the code I use:

uint16_t get_block(uint8_t *buffer) {
  uint16_t buffer_index= 0;
  while (true) {
    int c = getchar_timeout_us(100);
    if (c != PICO_ERROR_TIMEOUT && buffer_index < BUFFER_LENGTH) {
      buffer[buffer_index++] = (c & 0xFF);
    } else {
      break;
    }
  }
  return buffer_index;
}

PICO_ERROR_TIMEOUT is a constant set by the SDK; BUFFER_LENGTH is mine.

I call get_block() from another function that loads the data a block so that I can parse the contents and take action based on the block type, which is just a byte in the block. Another byte is a simple checksum of all the block’s contents but the head, tail and checksum bytes. My application calls for different block types, but yours might not, of course. You get the idea, though.

Once a block has been processed, I just use printf() to send back an ACK (ACKnowledge) so the program that’s sending the data knows it should send the next block. When the data has been sent, the source sends an tail block, and that’s the transmission done.

Why blocks? Partly because of the nature of the application suggested it, but mostly because I ran into problems when squirting more than around 15KB of data en masse. I started out including a two-byte data size field which the code read and attempted to store up to that number of bytes as they arrived. That worked for small payloads, but ran into issues with large ones. Using blocks proved to work well whatever the overall data size. My data blocks carry 256 bytes, but there’s a lot of scope for experimentation to find an optimal block size.

The data source is a Python script that I call from the command line and pass the Pico’s device file name and the name of the binary file to be sent. The script builds the blocks and sends them out using the PySerial library: first a header block, then the data blocks, and finally a tail block. Between blocks it samples the serial port for the ACK signal from the code running on the Pico.

Here’s the code to send a data block:

def send_data_block(uart, bytes, counter):
    length = len(bytes) - counter
    if length > 255: length = 255
    out = bytearray(length + 6)
    out[0] = 0x55                   # Head
    out[1] = 0x3C                   # Sync
    out[2] = 0x01                   # Block Type
    out[3] = length                 # Data length
    # Set the data
    for i in range(0, length):
        out[i + 4] = bytes[counter + i]
    # Compute checksum
    cs = 0
    for i in range (2, length + 6 - 2):
        cs += out[i]
    cs &= 0xFF
    out[length + 6 - 2] = cs        # Checksum
    out[length + 6 - 1] = 0x55      # Trailer
    counter += length
    r = uart.write(out)
    return counter

And this is the ACK checker:

def await_ack(uart, timeout=2000):
    buffer = bytes()
    now = (time_ns() // 1000000)
    while ((time_ns() // 1000000) - now) < timeout:
        if uart.in_waiting > 0:
            buffer += uart.read(uart.in_waiting)
            if "\n" in buffer.decode():
                show_verbose("RX: " + buffer[:-1].decode())
                return True
    # Error -- No Ack received
    return False

You can read more about using stdio at the Pico SDK documentation site.

More Raspberry Pi Pico

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s