UART
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:
1
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-sbrkr.o): In function `_sbrk_r':
2
sbrkr.c:(.text._sbrk_r+0x18): undefined reference to `_sbrk'
3
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-writer.o): In function `_write_r':
4
writer.c:(.text._write_r+0x20): undefined reference to `_write'
5
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-closer.o): In function `_close_r':
6
closer.c:(.text._close_r+0x18): undefined reference to `_close'
7
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-fstatr.o): In function `_fstat_r':
8
fstatr.c:(.text._fstat_r+0x1c): undefined reference to `_fstat'
9
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-isattyr.o): In function `_isatty_r':
10
isattyr.c:(.text._isatty_r+0x18): undefined reference to `_isatty'
11
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-lseekr.o): In function `_lseek_r':
12
lseekr.c:(.text._lseek_r+0x20): undefined reference to `_lseek'
13
/usr/bin/../lib/gcc/arm-none-eabi/4.8.3/../../../../arm-none-eabi/lib/libc.a(lib_a-readr.o): In function `_read_r':
14
readr.c:(.text._read_r+0x20): undefined reference to `_read'
15
collect2: error: ld returned 1 exit status
Copied!
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:
1
extern "C" int _write(int file, char *ptr, int len)
2
{
3
int count = len;
4
if (file == 1) { // stdout
5
while (count > 0) {
6
while (... /* poll the status bit */) {} // just wait
7
TX_REG = *ptr;
8
++ptr;
9
--count;
10
}
11
}
12
return len;
13
}
Copied!
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:
1
std::int32_t i = ...; // some value
2
printf("Value = %d\n");
Copied!
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:
1
std::int32_t i = ...; // some value
2
std::cout << "Value = " << i << std::endl;
Copied!
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.
Image: Asyncrhonous write request
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:
1
class CharacterDriver
2
{
3
public:
4
typedef ... CharType;
5
6
template <typename TCallbackFunc>
7
void asyncWrite(
8
const CharType* buf,
9
std::size_t bufSize,
10
TCallbackFunc&& func);
11
};
Copied!
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.
Image: 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:
1
typedef embxx::device::context::Interrupt InterruptContext;
2
3
void canWriteCallback()
4
{
5
// Executed in interrupt context, must be quick
6
while(device_.canWrite(InterruptContext())) {
7
if ((writeBufStart_ + writeBufSize_) <= currentWriteBufPtr_) {
8
break;
9
}
10
11
device_.write(*currentWriteBufPtr_, InterruptContext());
12
++currentWriteBufPtr_;
13
}
14
}
Copied!
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.
Image: Notifying caller about completion
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.
Image: Asynchronous read request
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).
1
class CharacterDriver
2
{
3
public:
4
typedef ... CharType;
5
6
template <typename TCallbackFunc>
7
void asyncRead(
8
CharType* buf,
9
std::size_t bufSize,
10
TCallbackFunc&& func);
11
};
Copied!
Stage2 - Reading data into the buffer.
Image: Writing provided data
The callback's implementation will be something like:
1
void canReadCallback()
2
{
3
while(device_.canRead(InterruptContext())) {
4
if ((readBufStart_ + readBufSize_) <= currentReadBufPtr_) {
5
break;
6
}
7
8
auto ch = device_.read(InterruptContext());
9
*currentReadBufPtr_ = ch;
10
++currentReadBufPtr_;
11
}
12
}
Copied!
Stage3 - Notifying caller about completion:
Image: Notifying caller about completion

Cancelling Asynchronous Operations

The cancellation flow is very similar to the one described in Device-Driver-Component chapter:
Image: Cancel read
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.
Image: Cancel read
Another possible case of unsuccessful cancellation is when completion interrupt occurs in the middle of cancellation request:
Image: Cancel read

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:
Image: Notifying caller about 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:
1
class MyDevice
2
{
3
public:
4
bool cancelRead(embxx::device::context::EventLoop) {...}
5
bool cancelRead(embxx::device::context::Interrupt) {...}
6
};
Copied!
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.
1
class MyDriver
2
{
3
public:
4
template <typename TPred, typename TFunc>
5
void asyncReadUntil(
6
CharType* buf,
7
std::size_t size,
8
TPred&& pred,
9
TFunc&& func)
10
{
11
...
12
}
13
};
Copied!
It allows using complex conditions in evaluating the character. For example, stopping when either '\r' or '\n' is encountered:
1
typedef embxx::error::ErrorStatus EmbxxErrorStatus;
2
3
driver_.asyncReadUntil(
4
buf,
5
bufSize,
6
[](CharType ch) -> bool
7
{
8
return (ch == '\r') || (ch == '\n');
9
},
10
[](const EmbxxErrorStatus& es, std::size_t bytesTransferred)
11
{
12
...
13
});
Copied!

Device Implementation

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:
1
class MyDevice
2
{
3
public:
4
typedef std::uint8_t CharType;
5
};
Copied!
The Driver layer will reuse the definition of the character in its internal functions:
1
template<typename TDevice, ...>
2
class MyDriver
3
{
4
public:
5
typedef typename TDevice::CharType CharType;
6
7
void asyncRead(CharType* buf, std::size_t bufSize, ...) {}
8
};
Copied!
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.
1
class MyDevice
2
{
3
public:
4
template <typename TFunc>
5
void setCanReadHandler(TFunc&& func)
6
{
7
canReadHandler_ = std::forward<TFunc>(func);
8
}
9
10
template <typename TFunc>
11
void setCanWriteHandler(TFunc&& func)
12
{
13
canWriteHandler_ = std::forward<TFunc>(func);
14
}
15
16
template <typename TFunc>
17
void setReadCompleteHandler(TFunc&& func)
18
{
19
readCompleteHandler_ = std::forward<TFunc>(func);
20
}
21
22
template <typename TFunc>
23
void setWriteCompleteHandler(TFunc&& func)
24
{
25
writeCompleteHandler_ = std::forward<TFunc>(func);
26
}
27
28
private:
29
typedef ... OpAvailableHandler;
30
typedef ... OpCompleteHandler;
31
32
OpAvailableHandler canReadHandler_;
33
OpCompleteHandler readCompleteHandler_;
34
35
OpAvailableHandler canWriteHandler_;
36
OpCompleteHandler writeCompleteHandler_;
37
38
};
Copied!
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:
1
template <typename TCanReadHandler,
2
typename TCanWriteHandler,
3
typename TReadCompleteHandler,
4
typename TWriteCompleteHandler>
5
class MyDevice
6
{
7
public:
8
... // setters are as above
9
10
private:
11
12
TCanReadHandler canReadHandler_;
13
TReadCompleteHandler readCompleteHandler_;
14
15
TCanWriteHandler canWriteHandler_;
16
TWriteCompleteHandler writeCompleteHandler_;
17
};
Copied!
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:
1
class MyDevice
2
{
3
public:
4
5
typedef embxx::device::context::EventLoop EventLoopContext;
6
typedef embxx::device::context::Interrupt InterruptContext;
7
8
// Start read operation - enables interrupts
9
void startRead(std::size_t length, EventLoopContext context);
10
11
// Cancel read in event loop context
12
bool cancelRead(EventLoopContext context);
13
14
// Cancel read in interrupt context - used only if
15
// asyncReadUntil() function was used in Device
16
bool cancelRead(InterruptContext context);
17
18
// Start write operation - enables interrupts
19
void startWrite(std::size_t length, EventLoopContext context);
20
21
// Cancell write operation
22
bool cancelWrite(EventLoopContext context);
23
24
// Check whether there is a character available to be read.
25
bool canRead(InterruptContext context);
26
27
// Check whether there is space for one character to be written.
28
bool canWrite(InterruptContext context);
29
30
// Read the available character from Rx FIFO of the peripheral
31
CharType read(InterruptContext context);
32
33
// Write one more character to Tx FIFO of the peripheral
34
void write(CharType value, InterruptContext context);
35
};
Copied!
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.
1
class MyDevice
2
{
3
public:
4
void configBaud(unsigned value) { ... }
5
...
6
};
Copied!
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:
1
template <typename TDevice, typename TEventLoop>
2
class MyDriver
3
{
4
public:
5
// Reuse definition of character type from the Device
6
typedef TDevice::CharType CharType;
7
8
// During the construction store references to Device
9
// and Event Loop objects.
10
MyDriver(TDevice& device, TEventLoop& el)
11
: device_(device),
12
el_(el)
13
{
14
// Register appropriate callbacks with device
15
device_.setCanReadHandler(
16
std::bind(
17
&MyDriver::canReadInterruptHandler, this));
18
device_.setReadCompleteHandler(
19
std::bind(
20
&MyDriver::readCompleteInterruptHandler,
21
this,
22
std::placeholders::_1));
23
24
device_.setCanWriteHandler(
25
std::bind(
26
&MyDriver::canWriteInterruptHandler, this));
27
device_.setWriteCompleteHandler(
28
std::bind(
29
&MyDriver::writeCompleteInterruptHandler,
30
this,
31
std::placeholders::_1));
32
33
}
34
35
...
36
37
private:
38
39
void canReadInterruptHandler() {...}
40
void readCompleteInterruptHandler(
41
const embxx::error::ErrorStatus& es) {...}
42
43
void canWriteInterruptHandler() {...}
44
void writeCompleteInterruptHandler(
45
const embxx::error::ErrorStatus& es) {...}
46
47
TDevice& device_;
48
TEventLoop& el_;
49
};
Copied!
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:
1
template <typename TDevice,
2
typename TEventLoop,
3
typename TReadCompleteCallback,
4
typename TWriteCompleteCallback>
5
class MyDriver
6
{
7
public:
8
...
9
10
typedef embxx::device::context::EventLoop EventLoopContext;
11
12
template <typename TFunc>
13
void asyncRead(
14
CharType* buf,
15
std::size_t bufSize,
16
TFunc&& func)
17
{
18
readBufStart_ = buf;
19
currentReadBufPtr = buf;
20
readBufSize_ = bufSize;
21
readCompleteCallback_ = std::forward<TFunc>(func);
22
driver_.startRead(bufSize, EventLoopContext());
23
}
24
25
template <typename TFunc>
26
void asyncWrite(
27
const CharType* buf,
28
std::size_t bufSize,
29
TFunc&& func)
30
{
31
writeBufStart_ = buf;
32
currentWriteBufPtr = buf;
33
writeBufSize_ = bufSize;
34
writeCompleteCallback_ = std::forward<TFunc>(func);
35
driver_.startWrite(bufSize, EventLoopContext());
36
}
37
38
private:
39
...
40
41
// Read info
42
CharType* readBufStart_;
43
CharType* currentReadBufPtr_;
44
std::size_t readBufSize_;
45
TReadCompleteCallback readCompleteCallback_;
46
47
// Write info
48
const CharType* writeBufStart_;
49
const CharType* currentWriteBufPtr_;
50
std::size_t writeBufSize_;
51
TWriteCompleteCallback writeCompleteCallback_;
52
};
Copied!
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:
1
template <typename TDevice,
2
typename TEventLoop,
3
typename TReadCompleteCallback,
4
typename TWriteCompleteCallback,
5
typename TReadUntilPred>
6
class MyDriver
7
{
8
public:
9
...
10
11
typedef embxx::device::context::EventLoop EventLoopContext;
12
13
template <typename TPred, typename TFunc>
14
void asyncReadUntil(
15
CharType* buf,
16
std::size_t bufSize,
17
TPred&& pred,
18
TFunc&& func)
19
{
20
readBufStart_ = buf;
21
currentReadBufPtr = buf;
22
readBufSize_ = bufSize;
23
readCompleteCallback_ = std::forward<TFunc>(func);
24
readUntilPred_ = std::forward<TPred>(pred)
25
driver_.startRead(bufSize, EventLoopContext());
26
}
27
28
private:
29
...
30
31
// Read info
32
CharType* readBufStart_;
33
CharType* currentReadBufPtr_;
34
std::size_t readBufSize_;
35
TReadCompleteCallback readCompleteCallback_;
36
TReadUntilPred readUntilPred_;
37
38
...
39
};
Copied!
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.
1
struct MyOutputTraits
2
{
3
// The "read" handler storage type.
4
typedef std::nullptr_t ReadHandler;
5
6
// The "write" handler storage type.
7
// The valid handler must have the following signature:
8
// "void handler(const embxx::error::ErrorStatus&, std::size_t);"
9
typedef embxx::util::StaticFunction<
10
void(const embxx::error::ErrorStatus&, std::size_t)> WriteHandler;
11
12
// The "read until" predicate storage type
13
typedef std::nullptr_t ReadUntilPred;
14
15
// Read queue size
16
static const std::size_t ReadQueueSize = 0;
17
18
// Write queue size
19
static const std::size_t WriteQueueSize = 1;
20
};
Copied!
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.
1
template <typename TDevice,
2
typename TEventLoop,
3
typename TTraits = MyOutputTraits>
4
class MyDriver :
5
public ReadSupportBase<
6
TDevice,
7
TEventLoop,
8
typename TTraits::ReadHandler,
9
typename TTraits::ReadUntilPred,
10
TTraits::ReadQueueSize>,
11
public WriteSupportBase<
12
TDevice,
13
TEventLoop,
14
typename TTraits::WriteHandler,
15
TTraits::WriteQueueSize>
16
{
17
typedef ReadSupportBase<...> ReadBase;
18
typedef WriteSupportBase<...> WriteBase;
19
public:
20
template <typename TPred, typename TFunc>
21
void asyncRead(
22
CharType* buf,
23
std::size_t bufSize,
24
TFunc&& func)
25
{
26
ReadBase::asyncRead(buf, bufSize, std::forward<TFunc>(func);
27
}
28
29
template <typename TPred, typename TFunc>
30
void asyncWrite(
31
const CharType* buf,
32
std::size_t bufSize,
33
TFunc&& func)
34
{
35
WriteBase::asyncWrite(buf, bufSize, std::forward<TFunc>(func);
36
}
37
};
Copied!
Now, the template specialisation based on queue size should do the job:
1
template <typename TDevice,
2
typename TEventLoop,
3
typename TReadHandler,
4
typename TReadUntilPred,
5
std::size_t ReadQueueSize>;
6
class ReadSupportBase;
7
8
9
template <typename TDevice,
10
typename TEventLoop,
11
typename TReadHandler,
12
typename TReadUntilPred>;
13
class ReadSupportBase<TDevice, TEventLoop, TReadHandler, TReadUntilPred, 1>
14
{
15
public:
16
ReadSupportBase(TDevice& device, TEventLoop& el) {...}
17
... // Implements the "read" related API
18
private:
19
... // Read related data members
20
};
21
22
template <typename TDevice,
23
typename TEventLoop,
24
typename TReadHandler,
25
typename TReadUntilPred>;
26
class ReadSupportBase<TDevice, TEventLoop, TReadHandler, TReadUntilPred, 0>
27
{
28
public:
29
ReadSupportBase(TDevice& device, TEventLoop& el) {}
30
// No need for any "read" related API and data members
31
};
32
33
template <typename TDevice,
34
typename TEventLoop,
35
typename TWriteHandler,
36
std::size_t WriteQueueSize>;
37
class WriteSupportBase;
38
39
40
template <typename TDevice,
41
typename TEventLoop,
42
typename TReadHandler>;
43
class WriteSupportBase<TDevice, TEventLoop, TWriteHandler, 1>
44
{
45
public:
46
WriteSupportBase(TDevice& device, TEventLoop& el) {...}
47
... // Implements the "write" related API
48
private:
49
... // Write related data members
50
};
51
52
template <typename TDevice,
53
typename TEventLoop,
54
typename TWriteHandler>;
55
class WriteSupportBase<TDevice, TEventLoop, TWriteHandler, 0>
56
{
57
public:
58
WriteSupportBase(TDevice& device, TEventLoop& el) {}
59
// No need for any "write" related API and data members
60
};
Copied!
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.
1
class MyDevice
2
{
3
public:
4
void startRead(std::size_t length, InterruptContext context);
5
void startWrite(std::size_t length, InterruptContext context);
6
};
Copied!
  1. 1.
    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:
    1
    class MyDevice
    2
    {
    3
    public:
    4
    bool suspendRead(EventLoopContext context);
    5
    void resumeRead(EventLoopContext context)
    6
    bool suspendWrite(EventLoopContext context);
    7
    void resumeWrite(EventLoopContext context);
    8
    };
    Copied!
    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:
1
class System
2
{
3
public:
4
static const std::size_t EventLoopSpaceSize = 1024;
5
typedef embxx::util::EventLoop<
6
EventLoopSpaceSize,
7
device::InterruptLock,
8
device::WaitCond> EventLoop;
9
10
typedef device::InterruptMgr<> InterruptMgr;
11
12
typedef device::Uart1<InterruptMgr> Uart;
13
14
typedef embxx::driver::Character<Uart, EventLoop> UartSocket;
15
16
...
17
18
private:
19
20
...
21
EventLoop el_;
22
Uart uart_;
23
UartSocket uartSocket_;
24
};
Copied!
Note that UartSocket uses default "TTraits" template parameter of embxx::driver::Character, which is defined to be:
1
struct DefaultCharacterTraits
2
{
3
typedef embxx::util::StaticFunction<
4
void(const embxx::error::ErrorStatus&, std::size_t)> ReadHandler;
5
typedef embxx::util::StaticFunction<
6
void(const embxx::error::ErrorStatus&, std::size_t)> WriteHandler;
7
typedef std::nullptr_t ReadUntilPred;
8
static const std::size_t ReadQueueSize = 1;
9
static const std::size_t WriteQueueSize = 1;
10
};
Copied!
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:
1
// Forward declaration
2
void writeChar(System::UartSocket& uartSocket, System::Uart::CharType& ch);
3
4
void readChar(System::UartSocket& uartSocket, System::Uart::CharType& ch)
5
{
6
uartSocket.asyncRead(&ch, 1,
7
[&uartSocket, &ch](const embxx::error::ErrorStatus& es, std::size_t bytesRead)
8
{
9
GASSERT(!es);
10
GASSERT(bytesRead == 1);
11
static_cast<void>(es);
12
static_cast<void>(bytesRead);
13
writeChar(uartSocket, ch);
14
});
15
}
16
17
void writeChar(System::UartSocket& uartSocket, System::Uart::CharType& ch)
18
{
19
uartSocket.asyncWrite(&ch, 1,
20
[&uartSocket, &ch](const embxx::error::ErrorStatus& es, std::size_t bytesWritten)
21
{
22
GASSERT(!es);
23
GASSERT(bytesWritten == 1);
24
static_cast<void>(es);
25
static_cast<void>(bytesWritten);
26
readChar(uartSocket, ch);
27
});
28
}
29
30
int main() {
31
auto& system = System::instance();
32
auto& uart = system.uart();
33
34
// Configure serial interface
35
uart.configBaud(115200);
36
uart.setReadEnabled(true);
37
uart.setWriteEnabled(true);
38
39
// Start with asynchronous read
40
auto& uartSocket = system.uartSocket();
41
System::Uart::CharType ch = 0;
42
readChar(uartSocket, ch);
43
44
// Run the event loop
45
device::interrupt::enable();
46
auto& el = system.eventLoop();
47
el.run();
48
49
GASSERT(0); // Mustn't exit
50
return 0;
Copied!

Stream-like Printing Interface

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".
Image: Stream
Let's start with "Output Stream Buffer" first. It needs to receive reference to the Driver it's going to use:
1
template <typename TDriver>
2
class OutStreamBuf
3
{
4
public:
5
OutStreamBuf(TDriver& driver)
6
: driver_(driver)
7
{
8
}
9
10
private:
11
TDriver& driver_;
12
...
13
};
Copied!
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:
1
template <typename TDriver,
2
std::size_t TBufSize>
3
class OutStreamBuf
4
{
5
public:
6
typedef typename TDriver::CharType CharType;
7
typedef embxx::container::StaticQueue<CharType, BufSize> Buffer;
8
9
private:
10
...
11
Buffer buf_;
12
};
Copied!
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.
1
template <...>
2
class OutStreamBuf
3
{
4
public:
5
// Add new character at the end of the buffer
6
std::size_t pushBack(CharType ch);
7
8
// Get number of written, not-flushed characters
9
std::size_t size();
10
11
// Flush number of characters
12
void flush(std::size_t count = size());
13
...
14
};
Copied!
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:
1
OutStreamBuf<...> outStreamBuf(...);
2
std::array<std::uint8_t, 128> data = {{.../* some data*/}};
3
4
std::copy(data.begin(), data.end(), std::back_inserter(outStreamBuf));
5
outStreamBuf.flush();
Copied!
In the example above, std::back_inserter requires a container to define push_back() member function:
1
template <...>
2
class OutStreamBuf
3
{
4
public:
5
// Wrap pushBack()
6
void push_back(CharType ch)
7
{
8
pushBack(ch);
9
}
10
...
11
};
Copied!
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:
1
template <...>
2
class OutStreamBuf
3
{
4
public:
5
typedef embxx::container::StaticQueue<CharType, BufSize> Buffer;
6
typedef typename Buffer::Iterator Iterator;
7
typedef typename Buffer::ConstIterator ConstIterator;
8
typedef typename Buffer::ValueType ValueType;
9
typedef typename Buffer::Reference Reference;
10
typedef typename Buffer::ConstReference ConstReference;
11
12
bool empty() const;
13
void clear();
14
void resize(std::size_t newSize);
15
16
Iterator begin();
17
Iterator end();
18
19
ConstIterator begin() const;
20
ConstIterator end() const;
21
22
ConstIterator cbegin() const;
23
ConstIterator cend() const;
24
25
Reference operator[](std::size_t idx);
26
ConstReference operator[](std::size_t idx) const;
27
...
28
};
Copied!
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:
1
template <typename TDriver,
2
std::size_t TBufSize,
3
typename TWaitHandler =
4
embxx::util::StaticFunction<void (const embxx::error::ErrorStatus&)> >
5
class OutStreamBuf
6
{
7
public:
8
std::size_t availableCapacity() const;
9
10
template <typename TFunc>
11
void asyncWaitAvailableCapacity(
12
std::size_t capacity,
13
TFunc&& func)
14
{
15
if (capacity <= availableCapacity()) {
16
... // invoke callback via post() member function of Event Loop
17
}
18
waitAvailableCapacity_ = capacity;
19
waitHandler_ = std::forward<TFunc>(func);
20
21
// The Driver is writing some portion of flushed characters,
22
// evaluate the capacity again when Driver reports completion.
23
}
24
25
private:
26
...
27
std::size_t waitAvailableCapacity_;
28
WaitHandler waitHandler_;
29
};
Copied!
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.
1
template <typename TStreamBuf>
2
class OutStream
3
{
4
public:
5
typedef typename TStreamBuf::CharType CharType;
6
7
explicit OutStream(TStreamBuf& buf)
8
: buf_(buf)
9
{
10
}
11
12
OutStream(OutStream&) = delete;
13
~OutStream() = default;
14
15
void flush()
16
{
17
buf_.flush();
18
}
19
20
OutStream& operator<<(const CharType* str)
21
{
22
while (*str != '\0') {
23
buf_.pushBack(*str);
24
++str;
25
}
26
return *this;
27
}
28
29
OutStream& operator<<(char ch)
30
{
31
buf_.pushBack(ch);
32
return *this;
33
}
34
35
OutStream& operator<<(std::uint8_t value)
36
{
37
// Cast std::uint8_t to unsigned and print.
38
return (*this << static_cast<unsigned>(value));
39
}
40
41
OutStream& operator<<(std::int16_t value)
42
{
43
... // Cast std::int16_t to int type and print.
44
return *this;
45
}
46
47
OutStream& operator<<(std::uint16_t value)
48
{
49
// Cast std::uint16_t to unsigned and print
50
return (*this << static_cast<std::uint32_t>(value));
51
}
52
53
OutStream& operator<<(std::int32_t value)
54
{
55
... // Print signed value
56
return *this;
57
}
58
59
OutStream& operator<<(std::uint32_t value)
60
{
61
... // Print unsigned value
62
return *this;
63
}
64
65
OutStream& operator<<(std::int64_t value)
66
{
67
... // Print 64 bit signed value
68
return *this
69
}
70
71
OutStream& operator<<(std::uint64_t value)
72
{
73
... // Print 64 bit signed value
74
return *this
75