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.
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;
}