Since Mbed 6 was released, there’s been a lot of confusion about printf. This is understandable, as the Mbed developers removed the previous method of printing stuff (Serial) that people have been using for years. What’s more, the developers have been very tight-lipped about what exactly to use as an equivalent. There are several possible replacements (none of which is a 1:1 port), and it’s often unclear which to use.
I have worked with Mbed OS for years, and did extensive research and testing into all of the current methods, analyzing how to implement them correctly, how they perform, and what they can’t do. In this guide I will present my results and my recommendations on which to use.
TL;DR
If you just want to print things to the serial port normally and performantly in Mbed 6, just put this code into mbed_app.json:
{
"target_overrides": {
"*": {
"platform.stdio-baud-rate": 115200,
"platform.stdio-buffered-serial": 1
}
}
}
You no longer need to create an object to call printf() on, like Serial in Mbed 5. You can just use the default printf() function and it will work fine. You can select whatever baudrate you need by changing the baud-rate option, and this baudrate will be used from bootup.
Also, if you need ISO C compliant floating point formatting in printf(), add this line into the target overrides block:
"target.printf_lib": "std"
Otherwise, by default, floating point numbers cannot be printed at all.
The Whole Story
Printf() in mbed is a complicated story, and we’ll need to go through a bit of background before I can explain how the current methods work.
Mbed File Handles
Mbed OS interoperates with the C library’s IO functions in a somewhat complicated way. Essentially, Mbed OS provides the abstraction of file descriptors. These are specific integers that represent I/O streams that can be read to and/or written from – just like in Unix C. These include the standard streams: stdin, stdout, and stderr (which have file descriptors 0, 1, and 2 respectively). You can also create a file descriptor for any object that extends FileHandle (such as BufferedSerial) using the mbed::bind_to_fd()
function.
In contrast, the C library provides struct FILE
. This is essentially an opaque struct that provides information about an I/O stream to the C library – for example, it might contain function pointers for writing to it, a pointer to the buffer, etc. However, all that you as a user can do with it is pass it to functions that take a FILE *
. This includes things like fwrite()
and fprintf()
.
To work with the C library, Mbed OS provides a key function:
std::FILE *fdopen(mbed::FileHandle *fh, const char *mode)
This is like the Unix fdopen(), except that it produces a C FILE *
for a FileHandle object. This allows you to use the FileHandle with all C IO functions that take a FILE *
. Since FileHandle lacks printf() itself, using FILE *
plus fprintf()
is the recommended way to call printf() on a FileHandle in Mbed 6.
Overriding the Console
Thankfully, the Mbed devs realized that carrying around FILE*
variables for every print statement would get annoying. So, they created a way to override the behavior of the default printf(). Since printf() prints to stdout, it is equivalent to writing to file descriptor 1 (stdout). Similarly, scanf() is equivalent to reading from file descriptor 0 (stdin). Mbed allows you to define a specific function to configure the stream that is referred to by those file descriptors. This looks like:
namespace mbed
{
FileHandle *mbed_override_console(int fd)
{
return /* some file handle pointer */;
}
}
Defining this function in your code overrides a weak symbol (it’s complicated…) in Mbed OS, and your implementation will be used instead of the default one. This function will get called three times at bootup, once each with fd
equal to 0, 1, and 2. The FileHandle that you return will then be connected to that file descriptor.
Note: 99% of the time you will want to return the same FileHandle for all three, as FileHandles like BufferedSerial support both input and output in the same object.
Direct and Buffered Serial
If you don’t provide mbed_override_console(), Mbed OS provides code equivalent to this:
namespace mbed
{
FileHandle *mbed_override_console(int fd)
{
static DirectSerial console(USBTX, USBRX, MBED_CONF_PLATFORM_STDIO_BAUD_RATE);
return &console;
}
}
DirectSerial is an undocumented Mbed OS class that works similarly to UnbufferedSerial. Since it’s the default stream, by default printfs won’t be buffered – they will block until the entire string has been transmitted. If you try to print a 100 character line at 115200 baud, your code is going to block for 8.7 ms until every single character has been transmitted out of the device. This is, understandably, not desirable in most situations.
This is why, in Mbed 6, ARM created the BufferedSerial class. This class (similar to the popular MODSERIAL community library for Mbed 5) keeps a software buffer of characters to be transmitted. It then uses an interrupt to detect when the serial port peripheral in the CPU can accept more data, then gradually feeds in characters in the background. This means, effectively, that you can have the serial port send characters at a low rate in the background while your code executes in the forground. Much better than just waiting, right?
To use BufferedSerial instead of DirectSerial, you need to add the following line to your mbed_app.json:
"platform.stdio-buffered-serial": 1
This will cause the console override to be declared equivalent to:
namespace mbed
{
FileHandle *mbed_override_console(int fd)
{
static BufferedSerial console(USBTX, USBRX, MBED_CONF_PLATFORM_STDIO_BAUD_RATE);
return &console;
}
}
In most cases, this is what you will want for printf() support in your own apps.
Note that the baud rate can be configured by this setting in mbed_app.json:
"platform.stdio-baud-rate": 115200
The default, for some ungodly reason, is 9600 baud, so you will probably want to increase this to escape dialup speeds.
Porting Code that Uses Serial
In Mbed 5, the Serial class acted as a one-stop-shop for all serial port communication needs. But when porting to Mbed 6, you will need to divide your usage of the Serial class into two categories:
- Usage of Serial to provide standard output (e.g.
pc.printf("Hello World\n")
) - Usage of Serial to communicate with external devices (GPSs, modems, radios, etc.)
If Serial is being used as standard output, just change the calls to regular printfs (and scanfs), and use the JSON method discussed above. Bam, done.
If Serial is being used for I/O with a device, then you will need to replace it with calls to UnbufferedSerial or BufferedSerial. UnbufferedSerial is a direct equivalent to old Serial and is not buffered, but, as discussed above, BufferedSerial can provide better performance in many situations. For most devices, BufferedSerial in blocking mode should be a straight upgrade from Serial with few gotchas.
For example, if you only need to read and write fixed numbers of bytes, you can simply use (Un)BufferedSerial Directly:
BufferedSerial myRadio(PIN_RADIO_TX, PIN_RADIO_RX);
char* data = "TEST";
myRadio.write(data, strlen(data));
However, if you need more advanced IO functions like printf, you have to use the FILE *
API:
FILE* myRadioFile = fdopen(&myRadio, "r+");
fprintf(myRadioFile, "Test #%d\n", 2);
NOTE: A while ago I released the SerialStream library, which provides a Stream subclass that sends data to a BufferedSerial class. This is also an option for porting code, but the Mbed developers have indicated that the Stream class as a whole is on the chopping block for deprecation so it’s probably best to avoid too much reliance on this system.
Blocking vs Non-Blocking
One final topic to cover: In Mbed 6, the developers introduced the extremely useful option for serial ports to be non-blocking. Consider the problem of checking if a char has been received on the serial port. If one was received, we want to save it to a buffer, but if not, we want to continue with the program. In Mbed 5, this was very difficult: reading from Serial was blocking, so getc() would wait forever until someone typed a character. So, the only way to implement this was using interrupts on Serial, e.g. calling pc.attach(<...>, Serial::RxIrq)
and then reading chars in the interrupt.
In Mbed 6, this functionality can be implemented much more easily using BufferedSerial::set_blocking() (which affects both the class functions and the FILE *
API).
myRadio.set_blocking(false);
while(true)
{
<do other stuff...>
char received;
while(myRadio.read(&received, 1) > 0)
{
addToBuffer(received);
}
}
BufferedSerial::readable() is also a convenient way of doing the same thing.
while(true)
{
<do other stuff...>
char received;
while(myRadio.readable())
{
myRadio.read(&received, 1); // guaranteed to return because the port is readable
addToBuffer(received);
}
}
Note: both of these methods only work for BufferedSerial, not UnbufferedSerial.
A Note about Performance
Before publishing this guide, I ran a benchmark of all 3 methods of printing characters (console override, FILE *
, and SerialStream). Using BufferedSerial, I tested both long (>100 chars) and short (1 char) strings, as well as medium (115200) and fast (921600) baudrates. I found that there was hardly any significant difference between the three methods. The console override was a few percent faster than the others for one character strings, but the difference was barely noticable, and all three methods were able to reach the maximum transfer rate for their baudrates.
Anyway, hopefully this guide provides all you ever wanted (and didn’t want) to know about serial ports in Mbed 6. Let me know in the comments if there are any questions.