UART
Last updated
Was this helpful?
Last updated
Was this helpful?
Our next stage will be to support debug logging via UART interface. In conventional C++ logging is performed using either function or (such as or ).
If printf
is used the compilation may fail at the linking stage with following errors:
Once these functions are stubbed with empty bodies, the compilation will succeed, but the image size will be quite big (around 45KB).
The _sbrk
function is required to support dynamic memory allocation. The printf
function probably uses malloc()
to allocate some temporary buffers. If we open the assembly listing file we will see calls to <malloc>
and <free>
.
The _write
function is used to write characters into the standard output consol, which doesn't exist in embedded product. The developer must use this function implementation to write all the provided characters to UART serial interface. Many developers implement this function in a straightforward synchronous way with busy loop:
In this case the call to printf
function will be blocking and won't return until all the characters are written one by one to UART, which takes a lot of execution time. This approach is suitable for quick and dirty debugging, but will quickly become impractical when the project grows.
In order to make the execution of printf
quick, there must be some kind of interrupt driven component that is responsible to buffer all the provided characters and forward it to UART asynchronously one by one using "TX buffer register is free" kind of interrupts.
One of disadvantages in using printf
for logging is a necessity to specify an output format of the printed variables:
In case the type of the printed variable changes, the developer must remember to update type in the format string too. This is the reason why many C++ developers prefer using streams instead of printf
:
Stage1 - Sending asynchronous buffer write request from the Component layer to Driver in event loop (non-interrupt) context.
Stage2 - Writing provided data.
Once the interrupt of "TX available" occurs, the Device must let the Driver know. There must obviously be some kind of callback involved, which Driver must provide during its construction / initialisation stage. Let's assume at this moment that such assignment was successfully done, and Device is capable of successfully notifying the Driver, that there is an ability to write character to TX FIFO of the peripheral.
When the Driver receives such notification, it attempts to write as many characters as possible:
This is because when "TX available" interrupt occurs, there may be a place for multiple characters to be sent, not just one. Doing checks and writes in a loop may save many CPU cycles.
Please note, that all these calls are performed in interrupt context. They are marked in red in the picture above.
Once the Tx FIFO of the underlying Device is full or there are no more characters to write, the callback returns. The whole cycle described above is repeated on every "TX available" interrupt until the whole provided buffer is sent to the Device for writing.
Stage3 - Notifying caller about completion:
The reading from UART is done in a very similar manner.
Stage1 - Sending asynchronous buffer read request from the Component layer to Driver in event loop (non-interrupt) context.
The asyncRead()
member function of the Driver should allow callback to be callable object of any type (but one that exposes predefined signature of course).
Stage2 - Reading data into the buffer.
The callback's implementation will be something like:
Stage3 - Notifying caller about completion:
If the cancellation is successful, the callback must be invoked with error code indicating that the operation was aborted (embxx::error::ErrorCode::Aborted
).
One possible case of unsuccessful cancellation is when callback was posted for execution in event loop, but hasn't been executed yet when cancellation is attempted. In this case Driver is aware that there is no pending asynchronous operation and can return false
immediately.
Another possible case of unsuccessful cancellation is when completion interrupt occurs in the middle of cancellation request:
There may be a case, when partial read needs to be performed, for example until specific character is encountered. In this case the Driver is responsible to monitor incoming characters and cancel the read into the buffer operation before its completion:
Note, that previously Driver called cancelRead()
member function of the Device in event loop (non-interrupt) context, while in "read until" situation the cancellation happens in interrupt mode. That requires Device to implement these functions for both modes:
The asyncReadUntil()
member function of the Driver should be able to receive any stateless predicate object that defines bool operator()(CharType ch) const
. The predicate invocation should return true when expected character is received and reading operation must be stopped.
It allows using complex conditions in evaluating the character. For example, stopping when either '\r' or '\n' is encountered:
In this section I will try to describe in more details what Device class needs to provide for the Driver to work correctly. First of all it needs to define the type of characters used:
The Driver layer will reuse the definition of the character in its internal functions:
There is a need for Device to be able to record callback objects from the Driver in order to notify the latter about an ability to read/write next character and about operation completion.
The OpAvailableHandler
and OpCompleteHandler
type may be either hard coded to be std::function<void ()>
and std::function<void (const embxx::error::ErrorStatus&)>
respectively or passed as template parameters:
Choosing the "template parameters option" is useful when the same Device class is reused between multiple applications for the same product line.
The next stage would be implementing all the required functions:
Driver must be a generic piece of code, that can be reused with any Device control object (as long as it exposed right public interface) and in any application, including ones without dynamic memory allocation.
We will also need to store callbacks provided with any asynchronous operation. Note that the "read" and "write" are independent operations and it should be possible to perform asyncRead()
and asyncWrite()
calls at the same time.
The only way to make Driver generic is to move responsibility of specifying callback storage type up one level, i.e. we must put them as template parameters:
The example code above may work, but it contradicts to one of the basic principles of C++: "You should pay only for what you use". In case of using UART for logging, there is no input from the peripheral and it is a waist to keep data members for "read" required to manage "read" operations. Let's try to improve the situation a little bit by using template specialisation as well as reduce number of template parameters by using "Traits" aggregation struct.
Now, the template specialisation based on queue size should do the job:
In order to support this extension, the Device class must implement some extra functionality too: 1. The new read/write request can be issued by the Driver in interrupt context, after previous operation reported completion.
When new asynchronous read/write request is issued to the Driver it must be able to prevent interrupt context callbacks from being invoked to avoid races on the internal data structure:
Please pay attention to the boolean return value of suspend*()
functions. They are like cancel*()
ones, there is an indication whether the invocation of the callbacks is suspended or there is no operation currently in progress.
Note that UartSocket
uses default "TTraits" template parameter of embxx::driver::Character
, which is defined to be:
It allows usage of both "read" and "write" operations at the same time. Having the definitions in place it is quite easy to implement the "echo" functionality:
As was mentioned earlier, our ultimate goal would be having standard output stream like interface for debug output, which works asynchronously without any blocking busy waits. Such interface must be a generic Component, which works in non-interrupt context, while using recently covered generic "Character" Driver in conjunction with platform specific "Uart" Device.
Such Component should be implemented as two sub-Components. One is "Stream Buffer" which is responsible to maintain circular buffer of written characters and flush them to the peripheral using "Character" Driver when needed. The characters, that have been successfully written, are removed from the internal buffer. The second one is "Stream" itself, which is responsible to convert various values into characters and write them to the end of the "Stream Buffer".
Let's start with "Output Stream Buffer" first. It needs to receive reference to the Driver it's going to use:
The "Output Stream Buffer" needs to support two main operations: 1. Pushing new single character at the end of the buffer. 2. Flushing all (or part of) written characters, i.e. activate asynchronous write with Driver.
When pushing a new character, there may be a case when the internal buffer is full. In this case, the pushed character needs to be discarded and there must be an indication whether "push" operation was successful. The function may return either bool
to indicate success of the operation or std::size_t
to inform the caller how may characters where written. If 0
is returned, the character wasn't written.
This limited number of operations is enough to implement "Output Stream" - like interface. However, "Output Stream Buffer" can be useful in writing any serialised data into the peripheral, not only the debug output. For example using standard algorithms:
There also may be a need to iterate over written, but still not flushed, characters and update some of them before the call to flush()
. In other words the "Output Stream Buffer" must be treated as random access container:
The next stage would be defining the "Output Stream" class, which will allow printing of null terminated strings as well as various integral values.
We will also require the numeric base representation and manipulator. Unfortunately, usage of std::oct
, std::dec
or std::hex
manipulators will require inclusion of standard library header , which in turn includes other standard stream related headers, which define some static objects, which in turn are defined and instantiated in standard library. It contradicts our main goal of writing generic code that doesn't require standard library to be used. It is better to define such manipulators ourselves:
The value of the numeric base representation must be taken into account when creating string representation of numeric values. The usage is very similar to standard:
It may be convenient to support a little bit of formatting, such as specifying minimal width of the output as well as fill character:
The usage is very similar to the base manipulator:
Another useful manipulator is adding '\n' at the end as well as calling flush()
, just like std::endl
does when using standard output streams:
Then usage example may be changed to:
To summarise: The "Output Stream" object converts given integer value into the printable characters and uses pushBack()
member function of "Output Stream Buffer" to pass these characters further. The request to flush()
is also passed on. When "Output Stream Buffer" receives a request to flush internal buffer it activates the "Character" Driver, which it turn uses "UART" Device to write characters to serial interface one by one. As the result of such cooperation, the "printing" statement is very quick, there is no wait for all the characters to be written before the function returns, like it is usually done with printf()
. All the characters are written at the background using interrupts, while the main thread of the application continues its execution without stalling.
In general, debug logging should be under conditional compilation, for example only in DEBUG mode, while the printing code is excluded when compiling in RELEASE mode.
Sometimes there is a need to easily change the amount of debug messages being printed. For that purpose, the concept of logging levels is widely used:
The logging statement becomes a macro:
In this case all the logging attempts for level below log::Info
get optimised away by the compiler, because the if
statement known to evaluate to false
at compile time:
It would be nice to be able to add some automatic formatting to the logged statements, such as printing the log level and/or adding '\n' and flushing at the end. For example, the code below
to produce the following output
with '\n' character and call to flush()
at the end.
It is easy to achieve when using some kind of wrapper logging class around the output stream as well as relevant formatters. For example:
The logging macro will look like this:
A formatter can be defined by exposing the same interface, but wraps the original StreamLogger
or another formatter. For example let's define formatter that calls flush()
member function of the stream when output is complete:
The definition of such logger would be:
The same SLOG()
macro will work for this logger with extra formatting:
Let's also add a formatter that capable of printing any value (and '\n' in particular) at the end of the output.
The definition of the logger that adds '\n' character and then calls flush()
member function of the underlying stream would be:
While the construction will require to specify the character which is going to be printed at the end, but before call to flush()
.
As the last formatter, let's do the one that prefixes the output with log level information:
The definition of the logger that prints such a prefix at the beginning and '\n' at the end together with call to flush()
would be:
This application will produce the following output to the UART interface with new line appearing every second:
In many systems the UART interfaces are also used to communicate between various microcontrollers on the same board or with external devices. When there are incoming messages, the characters must be stored in some buffer before they can be processed by some Component. Just like we had "Output Stream Buffer" for buffering outgoing characters, we must have "Input Stream Buffer" for buffering incoming ones.
It must obviously have an access to the Character Driver and will probably have a circular buffer to store incoming characters.
The Driver won't perform any read operations unless it is explicitly requested to do so with its asyncRead()
member function. Sometimes, there is a need to keep characters flowing in and being stored in the buffer, even when the Component responsible for processing them is not ready. In order to make this happen, the "Input Stream Buffer" must be responsible for constantly requesting the Driver to perform asynchronous read while providing space where these characters are going to be stored.
Most of the times the responsible Component will require some number of characters to be accumulated before their processing can be started. There is a need to provide asynchronous notification callback request when appropriate number of characters becomes available. The callback must be stored in the internal data structures of the "Input Stream Buffer" and invoked when needed. Due to the latter being developed as a generic class, there is a need to provide callback storage type as a template parameter.
Once the required number of characters is accumulated, the Component must be able to access and process them. It means that "Input Stream Buffer" must also be a container with random access iterators.
Please note, that all the access to the characters are done using const iterator. It means we do not allow external and uncontrolled update of the characters inside of the buffer.
When the characters inside the buffer got processed and aren't needed any more, they need to be discarded to free the space inside the buffer for new ones to come.
First of all there is a need to have an access to the led to flash, input buffer to store the incoming characters and timer manager to allocate a timer to measure timeouts.
Second, there is a need to define a Morse code sequences in terms of dots and dashes duration as well as mapping an incoming character to the respective sequence.
Now, the code that is responsible to flash a led is quite simple:
The nextLetter()
member function waits until one character becomes available in the buffer, then maps it to the sequence and removes it from the buffer. If the mapping exists it calls the nextSyllable()
member function to start the flashing sequence. The function activates the led and waits the relevant amount of time, based on the provided dot or dash duration. After the timeout, the led goes off and new wait is activated. However if the end of sequence is reached, the wait will be of InterSpacing
duration and nextLetter()
member function will be called again, otherwise the wait will be of Spacing
duration and nextSyllable()
will be called again to activate the led and wait for the next period in the sequence.
After this quite a significant effort we've created a full generic stack to perform asynchronous input/output operations over serial interface, such as UART. It may be reused in multiple independent projects while providing platform specific low level device control object at the bottom of this stack.
Even if type of printed variable changes the compiler will generate a call to appropriate overloaded operator<<
of and the value will be printed correctly. The developer will also have to implement the missing _write
function to write provided characters somewhere (UART interface in our case).
However using C++ streams in bare metal development is often not an option. They use exceptions to handle error cases as well as for formatting. The compilation of simple output statement with streams above created image of more than 500KB using compiler.
To summarise all the stated above, there may be a problem to use standard function or for debug logging, especially in systems with small memory and where dynamic memory allocations and exceptions mustn't be used. Our ultimate goal will be creation of standard output stream like interface for debug logging while using asynchronous event handling with model and where most of the code is generic and only smal part of managing write of a single character to the UART interface is platform specific.
Asyncrhonous read and write operations on the UART interface are very similar to the generic way of programming and handling asynchronous events described earlier in chapter.
The Component calls asyncWrite()
member function of the Driver and provides pointer to the buffer, size of the buffer and the callback object to invoke when the write is complete. The asyncWrite()
function needs to be able to receive any type of callable object, such as expression or . To achieve this the function must be templated:
According to the convention mentioned , the callback must receive an error status of whether the operation is successful as its first parameter. When performing asynchronous operation on the buffer, it can be required to know how many characters have been read / written before the error occurred, in case the operation wasn't successful. For this purpose such callback object must receive number of bytes written as the second parameter, i.e. expose the void (const embxx::error::ErrorStatus& err, std::size_t bytesTransferred)
signature.
When the Driver receives the asynchronous operation request, it forwards it to the Device, letting the latter know how many bytes will be written during the whole process. Please note that Driver uses tag parameter to specify that startWrite()
member function of Device is invoked in event loop (non-interrut) context. The job of the Device object is to enable appropriate interrupts and return immediately. Once the interrupt occurs, the stage of writing the data begins.
Once the whole buffer is sent to the Device for writing, the Driver is aware that there will be no more writes performed. However it doesn't report completion until the Device itself calls appropriate callback indicating that the operation has been indeed completed. Shifting the responsibility of identifying when the operation is complete to Device will be needed later when we will want to reuse the same Driver for and peripherals. It will be important to know when internal Tx FIFO of the peripheral becomes empty after all the characters from previous operation have been written.
Once the Driver receives notification from the Device (still in interrupt context), that the write operation is complete, it bundles the callback object, provided with initial asyncWrite()
request, together with error status and number of actual bytes transferred using expression and sends the callable object to for execution in event loop (non-interrupt) context.
The cancellation flow is very similar to the one described in chapter:
Note, that there may be extra configuration functions specific for the peripheral being controlled. For example baud rate, parity, flow control for UART. Such configuration is almost always platform and/or product specific and usually performed at application startup. It is irrelevant to the model introduced in this book.
The project has multiple applications that use UART1 interface for logging. The peripheral control code is the same for all of them and is implemented in .
First of all, we will need references to Device as well as objects:
As it was mentioned earlier in section, there is quite often a need to stop reading characters into the provided buffer when some condition evaluates to true. It means there is also a need to provide storage for the character evaluation predicate:
Please note, that allowed number of pending "read" requests is specified as 0 in the traits struct above, i.e. the read operations are not allowed. The "read complete" and "read until predicate" types are irrelevant and specified as . The instantiation of the Driver object must take it into account and not include any "read" related functionality. In order to achieve this the Driver class needs to have two independent sub-functionalities of "read" and "write". It may be achieved by inheriting from two base classes.
Note, that it is possible to implement general case when read/write queue size is greater than 1. It will require some kind of request queuing (using for example) and will allow issuing multiple asynchronous read/write requests at the same time.
Such generic Driver is already implemented in file of library. The Driver is called "Character", because it reads/writes the provided buffer one character at a time. The documentation can be found .
Now, it is time to do something practical. The application in project implements simple single character echo.
The System
class in file defines the Device and Driver layers:
There is also a need to have a buffer, where characters are stored before they are written to the device. Remember that we are trying to create a Component, which can be reused in multiple independent projects, including ones that do not support dynamic memory allocation. Hence, may be a good choice for it. It means, there is a need to provide size of the buffer as one of the template arguments:
In the example above, requires a container to define push_back()
member function:
As was mentioned earlier, the OutStreamBuf
uses as its internal buffer and any characters pushed beyond the capacity gets discarded. There must be a way to identify available capacity as well as request asynchronous notification via callback when requested capacity becomes available:
Such "Output Stream Buffer" is already implemented in file of library and documentation can be found .
Such "Output Stream" is already implemented in file of library and documentation can be found .
Such StreamLogger
together with multiple formatters is already implemented in file of library and documented .
The application in project implements logging of simple counter that gets incremented once a second:
The file defines the whole output stack:
The application in project implements buffering of incoming characters in the "Input Stream Buffer" and uses the method to display them by flashing the on-board led.