Tuesday, May 5, 2020

Implementing printf() on a UART

When developing on a new embedded (bare metal) platform, one of the first things I look for (after getting an LED to flash) is the ability to output text to a display. Usually the simplest path is to commandeer one of the UARTs and to start bashing characters out the TX pin and receiving them at the host end through a PuTTY terminal. So how to implement this efficiently?

The first question you'll need to resolve is how to get characters from your code into the UART. Hopefully the frawework you are using provides a simple setup process and wrapper to allow you to initialize the UART and send a single character. In the STM32CubeIDE environment, the UART properties can be setup fairly simply:

115200 is a pretty safe baud rate to start with. I've also shown the corresponding PuTTY settings too. Once you've proved that basic communications are working, you will probably be able to set the baud rate much higher; although take care when doing this. Even though the STM32 might be able to do 8Mbaud, FTDI-based USB Serial devices will top out at 3Mbaud: (https://www.ftdichip.com/Support/Knowledgebase/index.html?whatbaudratesareachieveabl.htm)
In any case, let's assume you've correctly setup the comms basics (don't forget to make sure the parity and stop bits match). Next job is to send something simple.

Once again, using the STM32CubeIDE environment as an example, ST offers a simple function call as part of the HAL (Hardware Abstraction Layer) interface:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
Assuming STM32CubeIDE has instantiated the UART handle as "huart1", you can make a simple call from your code like this:
char msg[] = "Hello World";
hres = HAL_UART_Transmit(huart1, msg, strlen(msg), 100);

Tying into printf()

Sending out "Hello World" might be initially very gratifying, but eventually you'll need to send out more advanced information such as variable values. One tempting option is to jump in and write your own MyPrintf() function, but that would involve a whole lot of re-inventing the wheel. A much better option is to tie into the existing functionality that already exists. Fortunately, the authors of most embedded frameworks understand that developers will want to redirect their output through various peripherals so they provide the infrastructure to readily support it.
If you dig deep enough, you'll find that after several layers of code, the standard printf() function distills down to one or two low-level functions. Take a look in the syscalls.c file for the _write() function:
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
Take note that this function is prefaced by __attribute__((weak)). This is a message to the compiler to look elsewhere in the code for a _write() function and use that one first, but if it can't find one then use this one. In effect, it allows you to implement your own _write() function within your codebase without needing to modify the library source files. A UART-based implementation would therefore be:
int _write(int file, char *ptr, int len)
{
HAL_StatusTypeDef hres;
hres = HAL_UART_Transmit(huart1, (uint8_t*)ptr, len, TX_BLOCKING_TIMEOUT);
if (hres == HAL_OK)
return len;
else
return -1;
}
With this function now in your code, you can access all of the wonderful formatting capabilities that come with printf() and your output will be magically directed to the serial port.

Concluding Remarks

The implementation above gets you up and running with basic character output capabilities but there's a hidden problem you may face further down the track. This implementation is 'blocking'; which means that it waits until all characters have been successfully piped out the serial port before it returns control back to the calling function. In timing critical applications or when sending large volumes of data, this could have an impact on the performance of your system, so in another blog entry, I'll talk about how to implement a ring-buffer and interrupts so your code doesn't have to wait around for the UART to finish its thing.


No comments:

Post a Comment