Cross-Platform Console Input

After we got BTstack running on the STM32 F4 Discovery board, next on our plan was to play music received by A2DP via the built-in Cypress Audio Codec.

However, it turned out that it’s not possible to output audio via the I2S interface at exactly 44100 Hz. It can be configured for 44108 Hz though, which is close enough for our human perception, but if we receive audio at the nominal rate of 44100 Hz, our buffer will underflow after a while – even if there are only 8 samples missing per second. So, while we need to work on an algorithm that keeps the audio in sync by adding or dropping a single sample occasionally, we’ll look at another practical problem.

Most of BTstack’s audio related examples, e.g. HFP HF/AG, or the upcoming A2DP examples, provide a basic console interface for control. While this was fine for Posix systems, and has been extended for Window as well, it hasn’t been supported on embedded systems until now.

In preparation for the A2DP demos running on the STM32 F4 Discovery, we’re going to generalize the existing solution and add support for it in the STM32 port.

Let’s look at the current situation. The examples test if the port-specific HAVE_POSIX_STDIN is defined. If yes, they include “btstack_stdin.h” found in platform/posix. The “btstack_stdin.h” defines following API:

// setup handler for command line interface
void btstack_stdin_setup(void (*stdin_handler)(char c));

// gets called by main.c to restore console
void btstack_stdin_reset(void);

The current interface only requires to trigger the stdin callback from the BTstack Run Loop / main thread. The API is very simple: we register a stdin handler, which is called with the character from the console when there is one.

On Posix, stdin is just a file descriptor as any other, so the original implementation did just register stdin as a data source and called the stdin_handler from there.

While it’s possible to treat stdin as some kind of data source on Windows as well, we did manage to get asynchronous notifications for console input. Instead, we added another thread that could do a blocking read on stdin, and perform the callback from the BTstack Run Loop.

Neither of these two approaches will work on an embedded target where we usually retarget printf to a free UART. To call the stdin callback from the BTstack Run Loop / main thread on an embedded target, we need to:
– setup an IRQ handler for received console bytes,
– provide a data source to perform the callback on the main thread (not from ISR),
– evaluate if we can extract a new btstack_stdin_embedded.c helper to separate the data source handling from the embedded code.

On the STM32F4 Discovery board:
– We’ve used the USART2 for the debug output via printf. The debug output was sent on USART2_TX, which is routed to pin A2.
– Incoming console input arrives on USART2_RX, which is routed to pin A3. We did already configure it before, but didn’t use it yet.

Now, ideally, we would just like to get an IRQ for each received byte. Looking at the stm32f4xx_hal_uart.h API, the provided options however are based on receiving a block of bytes – either polling, via per byte IRQ or via DMA. This has been a perfect fit for the hal_uart_dma.h implementation that communicates with the CC256x, but it’s not optimal for reading individual characters. Nevertheless, console input isn’t exactly a high-performance task, so we decided to go with the HAL_UART_Receive_IT function and re-trigger it after we have executed the stdin_callback.

To integrate it with the STM32CubeMX code, we first add the USART2 IRQ handler to stm32f4xx_it.c.

/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart2);
}

Then, the USART2 IRQ needs to be enabled in HAL_UART_MspInit() of usart.c.

...
/* Peripheral interrupt init */
HAL_NVIC_SetPriority(USART2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
...

For a quick test, we just call the receive function with a buffer for a single byte in main().

static uint8_t stdin_buffer[1];
..
HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);

Then, we extend our current UART IRQ handler:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
    if (huart == &huart3){
        (*rx_done_handler)();
    }
}

to call the new stdin_rx_complete() handler:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
    if (huart == &huart3){
        (*rx_done_handler)();
    }
    if (huart == &huart2){
        stdin_rx_complete
    }
}

In the stdin_rx_complete() handler, we just echo it back.

void stdin_rx_complete(void){
    printf("Received: %c\n", stdin_buffer[0]);
    HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);        
}

Let’s try with picocom:

$ picocom --baud 115200 /dev/tty.usbserial-A9GN71D5
picocom v1.7

Terminal ready
Received: H
Received: e
Received: l
Received: l
Received: o
Received:
Received: W
Received: o
Received: r
Received: l
Received: d
Received: !

Great. Works as expected.

To integrate it properly, we need to register a data source with the BTstack Run Loop. In the IRQ handler, we only have to set a flag and poll this flag in the data source process. The complete code looks like this:

// btstack_stdin.h
#include "btstack_stdin.h"

static uint8_t stdin_buffer[1];
volatile int stdin_character_received;
static void  (*stdin_handler)(char c);
static btstack_data_source_t stdin_data_source;

static void stdin_rx_complete(void){
    stdin_character_received = 1;
}

static void stdin_process(struct btstack_data_source *ds, btstack_data_source_callback_type_t callback_type){
    if (!stdin_character_received) return;
    if (stdin_handler){
        (*stdin_handler)(stdin_buffer[0]);
    }
    stdin_character_received = 0;
    HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);
}

void btstack_stdin_setup(void (*handler)(char c)){
    // set handler
    stdin_handler = handler;

    // set up polling data_source
    btstack_run_loop_set_data_source_handler(&stdin_data_source, &stdin_process);
    btstack_run_loop_enable_data_source_callbacks(&stdin_data_source, DATA_SOURCE_CALLBACK_POLL);
    btstack_run_loop_add_data_source(&stdin_data_source);

    // start receiving
    HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);
}

While that’s not much code, it still mixes the USART HAL code with the code to call the main application. Let’s clean up that and try to come up with a minimal IRQ-driven hal_stdin.h:

hal_stdin_setup(void (*handler)(char c));

With this, we can create a btstack_stdin_embedded.c that can be used with all embedded ports.

#include "btstack_stdin.h"
volatile int stdin_character_received;
volatile char stdin_character;
static void (*stdin_handler)(char c);
static btstack_data_source_t stdin_data_source;

static void btstack_stdin_handler(char c){
    stdin_character = c;
    stdin_character_received = 1;
    btstack_run_loop_embedded_trigger();
}

static void btstack_stdin_process(struct btstack_data_source *ds, btstack_data_source_callback_type_t callback_type){
    if (!stdin_character_received) return;
    if (stdin_handler){
        (*stdin_handler)(stdin_character);
    }
    stdin_character_received = 0;
}

void btstack_stdin_setup(void (*handler)(char c)){
    // set handler
    stdin_handler = handler;

    // set up polling data_source
    btstack_run_loop_set_data_source_handler(&stdin_data_source, &stdin_process);
    btstack_run_loop_enable_data_source_callbacks(&stdin_data_source, DATA_SOURCE_CALLBACK_POLL);
    btstack_run_loop_add_data_source(&stdin_data_source);

    // start receiving
    hal_stdin_setup(&btstack_stdin_handler);
}

That’s almost the code we had before, minus the STM32F4 specific code. This leaves only very little platform-specific code and allows for quick porting to new targets.

#include "hal_stdin.h"
static uint8_t stdin_buffer[1];
static void (*stdin_handler)(char c);
void hal_stdin_setup(void (*handler)(char c)){
    stdin_handler = handler;
    // start receiving
      HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);
}

static void hal_stdin_rx_complete(void){
    if (stdin_handler){
        (*stdin_handler)(stdin_buffer[0]);
    }
    HAL_UART_Receive_IT(&huart2, &stdin_buffer[0], 1);
}

As an additional goodie, this hal_stdin.h implementation could be used with an RTOS as well, if we provide a suitable btstack_stdin.h implementation for that like with the btstack_uart_block_embedded.c.

With the code in place, all existing examples that provide a console interface now also work on the STM32 F4 Discovery port, and it’s easy to support other embedded targets quickly.

$ picocom --baud 115200 /dev/tty.usbserial-A9GN71D5
picocom v1.7

Terminal ready

--- Bluetooth HFP Audiogateway (AG) unit Test Console D0:39:72:CD:83:45 ---

a - establish HFP connection to PTS module 00:15:83:5F:9D:46
b - establish AUDIO connection          | B - release AUDIO connection
c - simulate incoming call from 1234567 | C - simulate call from 1234567 dropped
d - report AG failure
e - answer call on AG                   | E - reject call on AG
r - disable in-band ring tone           | R - enable in-band ring tone
f - Disable cellular network            | F - Enable cellular network
g - Set signal strength to 0            | G - Set signal strength to 5
h - Disable roaming                     | H - Enable roaming
i - Set battery level to 3              | I - Set battery level to 5
j - Answering call on remote side
k - Clear memory #1                     | K - Set memory #1
l - Clear last number                   | L - Set last number
m - simulate incoming call from 7654321
n - Disable Voice Recognition           | N - Enable Voice Recognition
o - Set speaker volume to 0  (minimum)  | O - Set speaker volume to 9  (default)
p - Set speaker volume to 12 (higher)   | P - Set speaker volume to 15 (maximum)
q - Set microphone gain to 0  (minimum) | Q - Set microphone gain to 9  (default)
s - Set microphone gain to 12 (higher)  | S - Set microphone gain to 15 (maximum)
t - terminate connection
u - join held call
v - discover nearby HF units
w - put incoming call on hold (Response and Hold)
x - accept held incoming call (Response and Hold)
X - reject held incoming call (Response and Hold)
BTstack Port for STM32 F4 Discovery Board with CC256x