Search code examples
cusbmicrocontrollerstdioraspberry-pi-pico

Read strings from USB-CDC; how to use stdio_set_chars_available_callback()?


I try to read strings transmitted over USB to the Raspberry Pi Pico using stdio within a callback function as soon as characters are available. I don't want to poll the interface or use a repeating timer. Because UART0 and UART1 are needed for other purposes and all PIO-State machines are toll used for even more UART interfaces, USB is the only option. PICO_SDK provides void stdio_set_chars_available_callback(void(*)(void *) fn, void *param) to get notified when characters are available.

I don't attempt to read strings, just a single character (as an integer) then echo the integer. When I can't read a char I can't read a string either. The real project is over 3000 lines long and build entirely on the concepts of interrupts and callbacks. Besides UART0 and UART1 I implemented UART2, UART3, UART4 and UART5 using the PIO-Statemachines. getchar_timeout_us(0); is not an option it would make parsing, checking byte spacing (time between bytes) harder.

#include <stdio.h>
#include "pico/stdlib.h"

void callback(void *ptr){

    int *i = (int*) ptr;  // cast void pointer back to int pointer
    // read the character which caused to callback (and in the future read the whole string)
    *i = getchar_timeout_us(100); // length of timeout does not affect results
}

int main(){
    stdio_usb_init(); // init usb only. uart0 and uart1 are needed elsewhere
    while(!stdio_usb_connected()); // wait until USB connection
    int i = 0;
    stdio_set_chars_available_callback(callback, (void*)  &i); //register callback

    // main loop
    while(1){
        if(i!=0){
            printf("%i\n",i); //print the integer value of the character we typed
            i = 0; //reset the value
        }
        sleep_ms(1000);
    }
    return 0;
}

The callback works. I tested it by enabling the LED inside the callback. When I typed in the terminal the LED turned on. The passing of the void pointer works too. The value is changed by the callback. I assume the issue is *i = getchar_timeout_us(100);. The function returns -1, not the ASCII (integer) value of the character I typed. get_char_timeout_us() returns PICO_ERROR_TIMEOUT macro when timeout occurs. I timeout reading a char within the callback notifying characters are available to read.

The 100 μs in getchar_timeout_us(100); is chosen because it is not much greater than the minimum time between bytes at a baudrate of 115200. The minimum time would be 86,805 μs. Maybe too fast to type but I'll write a Python script sending the strings to the Raspberry Pi Pico.

I expect reading a character within the callback notifying that characters are available would return the send character and not PICO_ERROR_TIMEOUT. My assumption was timeout of 100 μs in getchar_timeout_us(100); was too short to type a character. Although there should be at least a single character in buffer because else the callback shouldn't have triggered. I increased the timeout to 1,000,000 μs, so 1 second. Still PICO_ERROR_TIMEOUT.

I tried multiple terminals, usually I use PySerial serial.tools.miniterm on Windows. I also tried Minicom and screen on Fedora Linux 38 (different computer), no different result. Getting rid of the timeout by using getchar(); leads to callback getting blocked indefinitely. It appears characters typed disappear into the void when I enter the callback. My assumption is getchar_timeout_us(100); and getchar(); are the wrong functions. What are the correct functions to call? Or how do I use the existing function to read the characters in the buffer?

CMakeList.txt:

# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "../../pico-sdk")

set(PICO_BOARD pico CACHE STRING "Board type")

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

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

project(foo 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(foo src/foo.cpp )

target_include_directories(foo
  PUBLIC
    include/
)

pico_set_program_name(foo "foo")
pico_set_program_version(foo "0.1")

pico_enable_stdio_uart(foo 0) # disable stdio over UART we need it
pico_enable_stdio_usb(foo 1)  # enable stdio over USB

# Add the standard library to the build
target_link_libraries(foo
        pico_stdlib)

# Add the standard include files to the build
target_include_directories(foo PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)

pico_add_extra_outputs(foo)

Terminal output:

py -m  serial.tools.miniterm COM14 115200
--- Miniterm on com14  115200,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
-1
-1
-1
-1
-1

--- exit ---

I typed Hello (72, 101, 108, 108, 111) but got (-1, -1, -1, -1, -1). This:

#include <stdio.h>
#include "pico/stdlib.h"

int main()
{
    //stdio_init_all();
    stdio_usb_init(); // init usb only. uart0 and uart1 are needed elsewhere
    while(!stdio_usb_connected()); // wait until USB connection
    while(1){
        int i = getchar_timeout_us(100);
        if(i >= 0){
            printf("%i", i);
        }
    }
    return 0;
}

works. When I type Hello I get (72, 101, 108, 108, 111) as expected. Making i volatile does not change the output and because void pointer is not supposed to be volatile it throws a warning. Replacing i with a global volatile int g = 0 (no example) variable either.

#include <stdio.h>
#include "pico/stdlib.h"

void callback(volatile void *ptr){

    volatile int *i = (volatile int*) ptr;  // cast void pointer back to int pointer
    // read the character which caused to callback (and in the future read the whole string)
    *i = getchar_timeout_us(100); // length of timeout does not affect results
}

int main(){
    stdio_usb_init(); // init usb only. uart0 and uart1 are needed elsewhere
    while(!stdio_usb_connected()); // wait until USB connection
    volatile int i = 0;
    stdio_set_chars_available_callback(callback, (volatile void*)  &i); //register callback

    // main loop
    while(1){
        if(i!=0){
            printf("%i\n",i); //print the integer value of the character we typed
            i = 0; //reset the value
        }
        sleep_ms(1000);
    }
    return 0;
}

Putting gpio_put(PICO_DEFAULT_LED_PIN, 1) after *i = getchar_timeout_us(100); in the callback will turn on the LED. Putting gpio_put(PICO_DEFAULT_LED_PIN, 0) after sleep_ms(1000); in the main loop will not turn off the LED, as would be expected.


Solution

  • The issue is due to the Mutex handling in stdio_usb.c https://github.com/raspberrypi/pico-sdk/blob/6a7db34ff63345a7badec79ebea3aaef1712f374/src/rp2_common/pico_stdio_usb/stdio_usb.c

    Commenting the Mutex code out in stdio_usb_in_chars(), as shown below, will "fix" the issue.

    Seems like there is a bug when attempting to get a Mutex in the callback function. Looks like stdio_usb_out_chars() blocks at mutex_try_enter_block_until() waiting for the Mutex that's held (and never released) in low_priority_worker_irq()

    int stdio_usb_in_chars(char *buf, int length) {
    // note we perform this check outside the lock, to try and prevent possible deadlock conditions
    // with printf in IRQs (which we will escape through timeouts elsewhere, but that would be less graceful).
    //
    // these are just checks of state, so we can call them while not holding the lock.
    // they may be wrong, but only if we are in the middle of a tud_task call, in which case at worst
    // we will mistakenly think we have data available when we do not (we will check again), or
    // tud_task will complete running and we will check the right values the next time.
    //
    int rc = PICO_ERROR_NO_DATA;
    if (stdio_usb_connected() && tud_cdc_available()) {
        //if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) {
        //    return PICO_ERROR_NO_DATA; // would deadlock otherwise
        //}
        if (stdio_usb_connected() && tud_cdc_available()) {
            int count = (int) tud_cdc_read(buf, (uint32_t) length);
            rc = count ? count : PICO_ERROR_NO_DATA;
        } else {
            // because our mutex use may starve out the background task, run tud_task here (we own the mutex)
            tud_task();
        }
        //mutex_exit(&stdio_usb_mutex);
    }
    return rc;
    }