-
Notifications
You must be signed in to change notification settings - Fork 40
Home
Kvasir is a collection of tools for writing extremely optimized embedded software with all the static checking and powerful abstraction which modern C++ style enables. In the sum of its parts Kvasir aims to provide similar functionality to Arduino, mbed or LPCOpen but with a few stark differences:
- Kvasir is header only, linking against precompiled libraries is an optimizer killer
- Kvasir is typesafe and contains a great deal of static checking of invariants
- Kvasir is efficient, very efficient, often several times smaller than an equivalent using another framework
- Kvasir is modular, great care has been taken to limit the dependencies between modules
- Kvasir allows better encapsulation of user code so that hardware changes are less painful
- Kvasir provides powerful synchronization primitives like atomic blocks to handle race conditions efficiently
- Kvasir compiles to analyzable assembler (although Kvasir is not compliant with MISRA or similar security related guidelines because of the metaprogramming involved the resulting assembler does not contain recursion or indirect function calls so that static analysis tools like those from AbsInt will work)
Using a great deal of meta programming and modern C++ techniques kvasir provides out of the box hardware abstractions to a growing number of peripherals. If there is no abstraction yet for your Core or your peripheral kvasir provides the tools to make your own.
The library currently consists of the following stand alone pieces:
- Kvasir::MPL is a stand alone meta programming library similar to the boost.MPL but without reliance on the standard library and in a modern c++ style.
- Kvasir::Register provides Special Function Register (SFR) abstraction with lazy evaluation and many optimization tricks. It only depends on Kvasir::MPL.
- Kvasir::IO provides GPIO abstraction and relies on Kvasir::Register.
- Kvasir::StartUp provides start up code, automatic optimized initialization and automatic ISR connections.
- Kvasir also supports a growing number of peripheral abstractions and Register::BitLocation definitions for peripherals on differenct chips.
Eventually a much more efficient high level abstraction could be implemented on to op the peripheral abstractions.
Sometimes its easiest to show a code example, take the following excerpt from LPCOpen:
void Chip_IOCON_PinMux(LPC_IOCON_T *pIOCON, uint8_t port, uint8_t pin, uint32_t mode, uint8_t func)
{
uint8_t reg, bitPos;
uint32_t temp;
bitPos = IOCON_BIT_INDEX(pin);
reg = IOCON_REG_INDEX(port,pin);
temp = pIOCON->PINSEL[reg] & ~(0x03UL << bitPos);
pIOCON->PINSEL[reg] = temp | (func << bitPos);
temp = pIOCON->PINMODE[reg] & ~(0x03UL << bitPos);
pIOCON->PINMODE[reg] = temp | (mode << bitPos);
}
//... used somewhere
Chip_IOCON_PinMux(LPC_IOCON, 0, 7, IOCON_MODE_INACT, IOCON_FUNC2);
Chip_IOCON_PinMux(LPC_IOCON, 0, 6, IOCON_MODE_INACT, IOCON_FUNC2);
Chip_IOCON_PinMux(LPC_IOCON, 0, 8, IOCON_MODE_INACT, IOCON_FUNC2);
Chip_IOCON_PinMux(LPC_IOCON, 0, 9, IOCON_MODE_INACT, IOCON_FUNC2);
Does this code meet modern quality standards? I would argue no, first of all IOCON_MODE_INACT and IOCON_FUNC2 are macros which are evil right? Actually the main problem I see, besides having to look up what IOCON_FUNC2 is in the LPC17xx documentation, this code violates the Scott Meyers "most important guideline" Make interfaces easy to use correctly and hard to use incorrectly found here. All of the parameters except the first are ints, I could easily switch them around or use garbage data like port number 142 and not get a compiler error. I don't want to pick on the LPCOpen guys specifically here, this is actually pretty common practice in the embedded world. So why do we do things this way? First of all compatibility, which is a fair point, if you are not using a C++11 conforming compiler then I am sorry to say the Kvasir library is not for you. The second reason is speed and code size, we are in the embedded world here, we need to make sacrifices right? I say wrong and hope to prove that claim with this library.
So far I have explained why I find the status quo unsafe due to lack of static checking. It is actually much less efficient than it needs to be as well. We are asking a lot of the optimizer here, it needs to figure out that all parameters passed to this function are literals and it can therefore in-line everything and reduce it down to two read-and-or-write routines. The Amazing thing is that, in some cases, it can do that! Sadly in most cases the pointer passed in if from another compilation unit or for some other reason the optimizer cannot prove locally that everything is constant and known at compile time. Even in the rare case where the optimizer can unleash its full potential there is still waste. Notice that all four function calls actually manipulate bits in the same two registers and in this particular case we do not care about the order of these operations. However since we usually do care about the order of reads and writes to registers LPCOpen has correctly marked all register representations as volatile. This forbids the optimizer from consolidating everything down to one single read modify write for PINSEL and PINMODE rather than four (one for each function call).
In order to make the interface better we need to have a better way to encapsulate information about bit combinations in registers. Kvasir solves this with a sort of DSL (Domain Specific Language), a Register::Action template can be used to associate specific bit combinations and register address information with the name which they represent. Using the variadic function apply (in the Register namepace) you can apply any number of register Options to their respective registers. Here is the equivalent code to the above snippet:
namespace Sspcfg = Hardware::SSP1::PinCfg;
using namespace Kvasir::IO;
constexpr IO::PinModes::Normal normalMode;
apply(Sspcfg::sselIsP0_6,
Sspcfg::sckIsP0_7,
Sspcfg::misoIsP0_8,
Sspcfg::mosiIsP0_9,
setMode(makePinLocation(port0,pin6),normalMode),
setMode(makePinLocation(port0,pin7),normalMode),
setMode(makePinLocation(port0,pin8),normalMode),
setMode(makePinLocation(port0,pin9),normalMode)
);
No more accidentally switching parameters, the order does not matter any way. No more using unsupported parameters either, if they are in the namespace they are supported. We don't need to look up what IOCON_FUNC2 is either because its right in the name. If you are wondering what Hardware::SSP1::PinCfg::SselIsP0_6 actually is its a constexpr variable who's type in a specialization of Register::Action, an empty struct, which contains the necessary address and bitmask information we need. The apply function sorts its parameters, merges any bit manipulations on the same address and applies the bit manipulations to the respective registers. With optimizer turned on this will result in two read-and-or-write's, one for each register which is at least 4 times more efficient and smaller than the LPCOpen equivalent above. If you are wondering how this meta programming magic stuff works, well its complex, here is a tutorial for the highly motivated. Otherwise just use it, most programmers don't understand whats going on under the hood of the C++ Standard Library either. If you understand meta programming and work in the small processor embedded domain please contact us, we would love to hear your experience and input.