Practical Guide to Bare Metal C++
Once the interrupt of "TX available" occurs, the Device must let the Driver know. There must
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
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.
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).
class CharacterDriver { public: typedef ... CharType;
template <typename TCallbackFunc> void asyncRead( CharType* buf, std::size_t bufSize, TCallbackFunc&& func); };
Stage2 - Reading data into the buffer.
The callback’s implementation will be something like:
void canReadCallback() { while(device_.canRead(InterruptContext())) { if ((readBufStart_ + readBufSize_) <= currentReadBufPtr_) { break; }
auto ch = device_.read(InterruptContext()); *currentReadBufPtr_ = ch; ++currentReadBufPtr_; } }
Stage3 - Notifying caller about completion:
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:
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:
class MyDevice { public: bool cancelRead(embxx::device::context::EventLoop) {...} bool cancelRead(embxx::device::context::Interrupt) {...} };
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.
class MyDriver { public: template <typename TPred, typename TFunc> void asyncReadUntil( CharType* buf, std::size_t size, TPred&& pred, TFunc&& func) { ... } };
It allows using complex conditions in evaluating the character. For example, stopping when either '\r' or '\n' is encountered:
typedef embxx::error::ErrorStatus EmbxxErrorStatus;
driver_.asyncReadUntil( buf, bufSize, [](CharType ch) -> bool { return (ch == '\r') || (ch == '\n'); }, [](const EmbxxErrorStatus& es, std::size_t bytesTransferred) { ... });
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:
class MyDevice { public: typedef std::uint8_t CharType; };
The Driver layer will reuse the definition of the character in its internal functions:
template<typename TDevice, ...> class MyDriver { public: typedef typename TDevice::CharType CharType;
void asyncRead(CharType* buf, std::size_t bufSize, ...) {} };
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.
class MyDevice { public: template <typename TFunc> void setCanReadHandler(TFunc&& func) { canReadHandler_ = std::forward<TFunc>(func); }
template <typename TFunc> void setCanWriteHandler(TFunc&& func) { canWriteHandler_ = std::forward<TFunc>(func); }
template <typename TFunc> void setReadCompleteHandler(TFunc&& func) { readCompleteHandler_ = std::forward<TFunc>(func); }
template <typename TFunc> void setWriteCompleteHandler(TFunc&& func) { writeCompleteHandler_ = std::forward<TFunc>(func); }
private: typedef ... OpAvailableHandler; typedef ... OpCompleteHandler;
OpAvailableHandler canReadHandler_; OpCompleteHandler readCompleteHandler_;
OpAvailableHandler canWriteHandler_; OpCompleteHandler writeCompleteHandler_;
};
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.
class MyDevice { public: void configBaud(unsigned value) { ... } ... };
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 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:
template <typename TDevice, typename TEventLoop, typename TReadCompleteCallback, typename TWriteCompleteCallback> class MyDriver { public: ...
typedef embxx::device::context::EventLoop EventLoopContext;
template <typename TFunc> void asyncRead( CharType* buf, std::size_t bufSize, TFunc&& func) { readBufStart_ = buf; currentReadBufPtr = buf; readBufSize_ = bufSize; readCompleteCallback_ = std::forward<TFunc>(func); driver_.startRead(bufSize, EventLoopContext()); }
template <typename TFunc> void asyncWrite( const CharType* buf, std::size_t bufSize, TFunc&& func) { writeBufStart_ = buf; currentWriteBufPtr = buf; writeBufSize_ = bufSize; writeCompleteCallback_ = std::forward<TFunc>(func); driver_.startWrite(bufSize, Event
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。