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 at a time 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
- First look: Pimoroni’s PicoSystem hackable handheld games console
- How to build a cellular IoT device with the Raspberry Pi Pico — part two, the code
- How to build a cellular IoT device with the Raspberry Pi Pico — part one, the hardware
- Raspberry Pi Pico proxies: the Pimoroni Tiny 2040 and the Adafruit QT Py RP2040
- Introducing C++ programming on the Raspberry Pi Pico
- Enjoy some old school 3D arcade action — courtesy of the Raspberry Pi Pico
- Play Hunt the Wumpus, Raspberry Pi Pico style
- How to Debug a Raspberry Pi Pico with a Mac, SWD and… another Pico
- How to program the Raspberry Pi Pico in C on a Mac