Our next stage will be to support debug logging via UART interface. In conventional C++ logging is performed using either printf function or output streams (such as std::cout or std::cerr).
If printf is used the compilation may fail at the linking stage with following errors:
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-sbrkr.o): In function `_sbrk_r':
sbrkr.c:(.text._sbrk_r+0x18): undefined reference to `_sbrk'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-writer.o): In function `_write_r':
writer.c:(.text._write_r+0x20): undefined reference to `_write'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-closer.o): In function `_close_r':
closer.c:(.text._close_r+0x18): undefined reference to `_close'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-fstatr.o): In function `_fstat_r':
fstatr.c:(.text._fstat_r+0x1c): undefined reference to `_fstat'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-isattyr.o): In function `_isatty_r':
isattyr.c:(.text._isatty_r+0x18): undefined reference to `_isatty'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-lseekr.o): In function `_lseek_r':
lseekr.c:(.text._lseek_r+0x20): undefined reference to `_lseek'
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-readr.o): In function `_read_r':
readr.c:(.text._read_r+0x20): undefined reference to `_read'
collect2: error: ld returned 1 exit status
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:
extern "C" int _write(int file, char *ptr, int len)
{
int count = len;
if (file == 1) { // stdout
while (count > 0) {
while (... /* poll the status bit */) {} // just wait
TX_REG = *ptr;
++ptr;
--count;
}
}
return len;
}
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:
std::int32_t i = ...; // some value
printf("Value = %d\n");
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:
std::int32_t i = ...; // some value
std::cout << "Value = " << i << std::endl;
Even if type of printed variable changes the compiler will generate a call to appropriate overloaded operator<< of std::ostream 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 locales for formatting. The compilation of simple output statement with streams above created image of more than 500KB using GNU Tools for ARM Embedded Processors compiler.
To summarise all the stated above, there may be a problem to use standard printf function or output streams 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 Device-Driver-Component model and Event Loop 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 Device-Driver-Component chapter.
Writing to UART
Stage1 - Sending asynchronous buffer write request from the Component layer to Driver in event loop (non-interrupt) context.
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 std::bind expression or lambda function. To achieve this the function must be templated:
According to the convention mentioned earlier, 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 embxx::device::context::EventLoop 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.
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:
typedef embxx::device::context::Interrupt InterruptContext;
void canWriteCallback()
{
// Executed in interrupt context, must be quick
while(device_.canWrite(InterruptContext())) {
if ((writeBufStart_ + writeBufSize_) <= currentWriteBufPtr_) {
break;
}
device_.write(*currentWriteBufPtr_, InterruptContext());
++currentWriteBufPtr_;
}
}
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:
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 I2C and SPI 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 std::bind expression and sends the callable object to Event Loop for execution in event loop (non-interrupt) context.
Reading from UART
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).
The cancellation flow is very similar to the one described in Device-Driver-Component chapter:
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:
Reading "Until"
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.
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:
class MyDevice
{
public:
typedef std::uint8_t CharType;
};
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:
template <typename TCanReadHandler,
typename TCanWriteHandler,
typename TReadCompleteHandler,
typename TWriteCompleteHandler>
class MyDevice
{
public:
... // setters are as above
private:
TCanReadHandler canReadHandler_;
TReadCompleteHandler readCompleteHandler_;
TCanWriteHandler canWriteHandler_;
TWriteCompleteHandler writeCompleteHandler_;
};
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:
class MyDevice
{
public:
typedef embxx::device::context::EventLoop EventLoopContext;
typedef embxx::device::context::Interrupt InterruptContext;
// Start read operation - enables interrupts
void startRead(std::size_t length, EventLoopContext context);
// Cancel read in event loop context
bool cancelRead(EventLoopContext context);
// Cancel read in interrupt context - used only if
// asyncReadUntil() function was used in Device
bool cancelRead(InterruptContext context);
// Start write operation - enables interrupts
void startWrite(std::size_t length, EventLoopContext context);
// Cancell write operation
bool cancelWrite(EventLoopContext context);
// Check whether there is a character available to be read.
bool canRead(InterruptContext context);
// Check whether there is space for one character to be written.
bool canWrite(InterruptContext context);
// Read the available character from Rx FIFO of the peripheral
CharType read(InterruptContext context);
// Write one more character to Tx FIFO of the peripheral
void write(CharType value, InterruptContext context);
};
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 Device-Driver-Component model introduced in this book.
The embxx_on_rpi 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 src/device/Uart1.h.
Driver Implementation
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.
First of all, we will need references to Device as well as Event Loop objects:
template <typename TDevice, typename TEventLoop>
class MyDriver
{
public:
// Reuse definition of character type from the Device
typedef TDevice::CharType CharType;
// During the construction store references to Device
// and Event Loop objects.
MyDriver(TDevice& device, TEventLoop& el)
: device_(device),
el_(el)
{
// Register appropriate callbacks with device
device_.setCanReadHandler(
std::bind(
&MyDriver::canReadInterruptHandler, this));
device_.setReadCompleteHandler(
std::bind(
&MyDriver::readCompleteInterruptHandler,
this,
std::placeholders::_1));
device_.setCanWriteHandler(
std::bind(
&MyDriver::canWriteInterruptHandler, this));
device_.setWriteCompleteHandler(
std::bind(
&MyDriver::writeCompleteInterruptHandler,
this,
std::placeholders::_1));
}
...
private:
void canReadInterruptHandler() {...}
void readCompleteInterruptHandler(
const embxx::error::ErrorStatus& es) {...}
void canWriteInterruptHandler() {...}
void writeCompleteInterruptHandler(
const embxx::error::ErrorStatus& es) {...}
TDevice& device_;
TEventLoop& el_;
};
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:
As it was mentioned earlier in Reading "Until" 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:
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.
struct MyOutputTraits
{
// The "read" handler storage type.
typedef std::nullptr_t ReadHandler;
// The "write" handler storage type.
// The valid handler must have the following signature:
// "void handler(const embxx::error::ErrorStatus&, std::size_t);"
typedef embxx::util::StaticFunction<
void(const embxx::error::ErrorStatus&, std::size_t)> WriteHandler;
// The "read until" predicate storage type
typedef std::nullptr_t ReadUntilPred;
// Read queue size
static const std::size_t ReadQueueSize = 0;
// Write queue size
static const std::size_t WriteQueueSize = 1;
};
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 std::nullptr_t. 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.
Now, the template specialisation based on queue size should do the job:
template <typename TDevice,
typename TEventLoop,
typename TReadHandler,
typename TReadUntilPred,
std::size_t ReadQueueSize>;
class ReadSupportBase;
template <typename TDevice,
typename TEventLoop,
typename TReadHandler,
typename TReadUntilPred>;
class ReadSupportBase<TDevice, TEventLoop, TReadHandler, TReadUntilPred, 1>
{
public:
ReadSupportBase(TDevice& device, TEventLoop& el) {...}
... // Implements the "read" related API
private:
... // Read related data members
};
template <typename TDevice,
typename TEventLoop,
typename TReadHandler,
typename TReadUntilPred>;
class ReadSupportBase<TDevice, TEventLoop, TReadHandler, TReadUntilPred, 0>
{
public:
ReadSupportBase(TDevice& device, TEventLoop& el) {}
// No need for any "read" related API and data members
};
template <typename TDevice,
typename TEventLoop,
typename TWriteHandler,
std::size_t WriteQueueSize>;
class WriteSupportBase;
template <typename TDevice,
typename TEventLoop,
typename TReadHandler>;
class WriteSupportBase<TDevice, TEventLoop, TWriteHandler, 1>
{
public:
WriteSupportBase(TDevice& device, TEventLoop& el) {...}
... // Implements the "write" related API
private:
... // Write related data members
};
template <typename TDevice,
typename TEventLoop,
typename TWriteHandler>;
class WriteSupportBase<TDevice, TEventLoop, TWriteHandler, 0>
{
public:
WriteSupportBase(TDevice& device, TEventLoop& el) {}
// No need for any "write" related API and data members
};
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 Static (Fixed Size) Queue for example) and will allow issuing multiple asynchronous read/write requests at the same time.
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.
Such generic Driver is already implemented in embxx/driver/Character.h file of embxx library. The Driver is called "Character", because it reads/writes the provided buffer one character at a time. The documentation can be found here.
Character Echo Application
Now, it is time to do something practical. The app_uart1_echo application in embxx_on_rpi project implements simple single character echo.
The System class in System.h file defines the Device and Driver layers:
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:
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, Static (Fixed Size) Queue 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:
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.
template <...>
class OutStreamBuf
{
public:
// Add new character at the end of the buffer
std::size_t pushBack(CharType ch);
// Get number of written, not-flushed characters
std::size_t size();
// Flush number of characters
void flush(std::size_t count = size());
...
};
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:
OutStreamBuf<...> outStreamBuf(...);
std::array<std::uint8_t, 128> data = {{.../* some data*/}};
std::copy(data.begin(), data.end(), std::back_inserter(outStreamBuf));
outStreamBuf.flush();
In the example above, std::back_inserter requires a container to define push_back() member function:
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:
As was mentioned earlier, the OutStreamBuf uses Static (Fixed Size) Queue 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:
template <typename TDriver,
std::size_t TBufSize,
typename TWaitHandler =
embxx::util::StaticFunction<void (const embxx::error::ErrorStatus&)> >
class OutStreamBuf
{
public:
std::size_t availableCapacity() const;
template <typename TFunc>
void asyncWaitAvailableCapacity(
std::size_t capacity,
TFunc&& func)
{
if (capacity <= availableCapacity()) {
... // invoke callback via post() member function of Event Loop
}
waitAvailableCapacity_ = capacity;
waitHandler_ = std::forward<TFunc>(func);
// The Driver is writing some portion of flushed characters,
// evaluate the capacity again when Driver reports completion.
}
private:
...
std::size_t waitAvailableCapacity_;
WaitHandler waitHandler_;
};
Such "Output Stream Buffer" is already implemented in embxx/io/OutStreamBuf.h file of embxx library and documentation can be found here.
The next stage would be defining the "Output Stream" class, which will allow printing of null terminated strings as well as various integral values.
template <typename TStreamBuf>
class OutStream
{
public:
typedef typename TStreamBuf::CharType CharType;
explicit OutStream(TStreamBuf& buf)
: buf_(buf)
{
}
OutStream(OutStream&) = delete;
~OutStream() = default;
void flush()
{
buf_.flush();
}
OutStream& operator<<(const CharType* str)
{
while (*str != '\0') {
buf_.pushBack(*str);
++str;
}
return *this;
}
OutStream& operator<<(char ch)
{
buf_.pushBack(ch);
return *this;
}
OutStream& operator<<(std::uint8_t value)
{
// Cast std::uint8_t to unsigned and print.
return (*this << static_cast<unsigned>(value));
}
OutStream& operator<<(std::int16_t value)
{
... // Cast std::int16_t to int type and print.
return *this;
}
OutStream& operator<<(std::uint16_t value)
{
// Cast std::uint16_t to unsigned and print
return (*this << static_cast<std::uint32_t>(value));
}
OutStream& operator<<(std::int32_t value)
{
... // Print signed value
return *this;
}
OutStream& operator<<(std::uint32_t value)
{
... // Print unsigned value
return *this;
}
OutStream& operator<<(std::int64_t value)
{
... // Print 64 bit signed value
return *this
}
OutStream& operator<<(std::uint64_t value)
{
... // Print 64 bit signed value
return *this
}
private:
TStreamBuf& buf_;
};
We will also require the numeric base representation and manipulator. Unfortunately, usage of std::oct, std::decor 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:
enum Base
{
bin, ///< Binary numeric base stream manipulator
oct, ///< Octal numeric base stream manipulator
dec, ///< Decimal numeric base stream manipulator
hex, ///< Hexadecimal numeric base stream manipulator
Base_NumOfBases ///< Must be last
};
template <typename TStreamBuf>
class OutStream
{
public:
explicit OutStream(TStreamBuf& buf)
: buf_(buf)
base_(dec)
{
}
OutStream& operator<<(Base value)
{
base_ = value;
return *this
}
private:
TStreamBuf& buf_;
Base base_;
};
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:
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.
Such "Output Stream" is already implemented in embxx/io/OutStream.h file of embxx library and documentation can be found here.
Logging
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.
#ifndef NDEBUG
stream << "Some info massage" << endl;
#endif
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:
namespace log
{
enum Level
{
Trace, ///< Use for tracing enter to and exit from functions.
Debug, ///< Use for debugging information.
Info, ///< Use for general informative output.
Warning, ///< Use for warning about potential dangers.
Error, ///< Use to report execution errors.
NumOfLogLevels ///< Number of log levels, must be last
};
} // namespace log
The logging statement becomes a macro:
const auto MinLogLevel = log::Info;
#define LOG(stream__, level__, output__) \
do { \
if (MinLevel <= (level__)) { \
(stream__).stream() << output__; \
} \
} while (false)
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:
LOG(stream, log::Debug, "This message is not printed." << endl);
LOG(stream, log::Info, "This message IS printed." << endl);
LOG(stream, log::Warning, "This message IS printed also." << endl);
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
LOG(stream, log::Debug, "This is DEBUG message.");
LOG(stream, log::Info, "This is INFO message.");
LOG(stream, log::Warning, "This is WARNING message.");
to produce the following output
[DEBUG]: This is DEBUG message.
[INFO]: This is INFO message.
[WARNING]: This is WARNING message.
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:
template <log::Level TLevel, typename TStream>
class StreamLogger
{
public:
typedef TStream Stream;
static const log::Level MinLevel = TLevel;
explicit StreamLogger(Stream& outStream)
: outStream_(outStream)
{
}
Stream& stream()
{
return outStream_;
}
// Begin output. This function is called before requested
// output is redirected to stream. It does nothing.
void begin(log::Level level)
{
static_cast<void>(level);
}
// End output. This function is called after requested
// output is redirected to stream. It does nothing.
void end(log::Level level)
{
static_cast<void>(level);
}
private:
Stream& outStream_;
};
The logging macro will look like this:
#define SLOG(log__, level__, output__) \
do { \
if ((log__).MinLevel <= (level__)) { \
(log__).begin(level__); \
(log__).stream() << output__; \
(log__).end(level__); \
} \
} while (false)
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:
template <typename TNextLayer>
class StreamFlushSuffixer
{
public:
// Constructor, forwards all the other parameters to the constructor
// of the next layer.
template<typename... TParams>
StreamFlushSuffixer(TParams&&... params)
: nextLavel_(std::forward<TParams>(params)...)
{
}
Stream& stream()
{
return nextLavel_.stream();
}
void begin(log::Level level)
{
nextLavel_.begin(level);
}
void end(log::Level level)
{
nextLavel_.end(level);
stream().flush();
}
private:
TNextLavel nextLavel_;
};
The definition of such logger would be:
typedef ... OutStream; // type of the output stream
typedef
StreamFlushSuffixer<
StreamLogger<
log::Debug,
OutStream
>
> Log;
The same SLOG() macro will work for this logger with extra formatting:
OutStream stream(... /* construction params */);
Log log(stream);
SLOG(log, log::Debug, "This is DEBUG message.\n");
Let's also add a formatter that capable of printing any value (and '\n' in particular) at the end of the output.
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.
template <typename TDriver, std::size_t TBufSize>
class InStreamBuf
{
public:
// Start data accumulation in the internal buffer.
void start();
// Stop data accumulation in the internal buffer.
void stop();
// Inquire whether characters are being accumulated.
bool isRunning() const;
};
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.
template <typename TDriver, std::size_t TBufSize, typename TWaitHandler>
class InStreamBuf
{
public:
typedef typename Buffer::ConstIterator ConstIterator;
typedef ConstIterator const_iterator;
typedef typename Buffer::ValueType ValueType;
typedef ValueType value_type;
typedef typename Buffer::ConstReference ConstReference;
typedef ConstReference const_reference;
// Get size of available for read data.
std::size_t size() const;
// Check whether number of available characters is 0.
bool empty() const;
//Get full capacity of the buffer.
constexpr std::size_t fullCapacity() const;
ConstIterator begin() const;
ConstIterator end() const;
ConstIterator cbegin() const;
ConstIterator cend() const;
ConstReference operator[](std::size_t idx) const;
};
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.
template <typename TDriver, std::size_t TBufSize, typename TWaitHandler>
class InStreamBuf
{
public:
// Consume part or the whole buffer of the available data for read.
void consume(std::size_t consumeSize = size());
};
Morse Code Application
The app_uart1_morse application in embxx_on_rpi project implements buffering of incoming characters in the "Input Stream Buffer" and uses the Morse Code method to display them by flashing the on-board led.
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.
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.
Summary
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.