Skip to content
Ostoic edited this page Aug 14, 2017 · 6 revisions

Register actions are a sort of DSL (Domain Specific Language) meant to increase the efficiency and type safety when configuring SFRs (Special Function Registers). SFRs are often made up of bit-fields containing many different loosely related configurations in one register.

For example the LPC11xx processors organize pin function, open drain, pull up and pull down etc. all in one register for each pin (starting at address 0x40044000). On the LPC17xx chips however these same functions are organized differently. Pin functions for all pins are grouped into one set of registers, pin modes (pull up/down repeater etc.) in another set and open drain functionality in yet another set. This can be a nightmare for those targeting multiple micro controllers with one code base.

What if one wants to access such the open drain function on one pin from an ISR, this would normally cause a race condition because the ISR access could interrupt an access to the same register. Using a bit banded write would solve the problem, however this can lead to confusing code. With Register actions the code is essentially identical apply(makeOpenDrain(myPin)) vs apply(atomic(makeOpenDrain(myPin))).

Register actions provide a cost free extra layer of abstraction. Essentially every action, setting port 0 pin 4 to open-drain mode for example, is abstracted as a Register::Action. Register::Actions contain the following data in a compile time accessible manor:

  • BitLocation
  • register address
  • register access mode (read only, clear on read etc.)
  • bit mask
  • writable bits mask (defaults to all)
  • abstract data type (defaults to int)
  • data
  • action (read, write literal, write run time, xor etc.)

Register Actions can be executed by calling the variadic Kvasir::Register::apply(...) function. The apply function sorts its parameters by address, merges actions which act on the same address and then executes the actions in the sorted order. Sorting and merging is all done at compile time so there is no run time cost, therefore using register actions is never less efficient than the hand coded equivalent. It can of course be more efficient because by sorting the registers by address the compiler may not need to load the register address as often from memory and actions on the same register are merged.

Initialization

In practice many SFRs are initialized at start up. This is often very messy however, sometimes initialization code is grouped with its corresponding module code for clarity, sometimes it is grouped with other init code which accesses the same registers for efficiency. Often some sort of hybrid is used and maintainability of the code suffers. Kvasir solves this problem in part with register actions. Every module, e.g. struct or class, which requires initialization must specify a static constexpr variable named init and register itself in the KVASIR_START() macro. Kvasir will then group all initialization actions together and merge them where possible for efficiency. This gives the best of both worlds, initialization is grouped with its corresponding module in code and merged with other actions on the same register under the hood for efficiency.

Ordering guaranties

Access to the SFRs is done using the volatile keyword as is common practice, this entails that no other loads or stores may be reordered across a call to apply(). However the order in which apply executes its arguments is undefined (this needs to be the case in order to allow merging access to the same register by multiple actions). If you require one action to be sequenced before another either call apply() twice or use a sequence point.

Sequence points

Kvasir::Register::sequencePoint is essentially a Tag which tells the reordering and merging algorithms not to reorder across this point. In normal user code one could simply call apply twice, or use a sequence point, whatever suits your taste, however in init sequences sequencePoint must be used.

PortPins

Not only is supporting multiple micro controllers with a single piece of code a common requirement but still more common is the requirement of supporting multiple hardware configurations or board revisions. At the most basic level this means associating a hardware function with different ports or pins. For example a status LED could be on port 0 pin 4 in the first design and then be moved to port 2 pin 1 in a later revision. Maintains code which targets both boards is not as easy as one might think, not only do we need to write at a different bit offset into a different register in order to turn the LED on and off but we also need to set the pin to output with open drain mode at different offsets in different registers as well. Sadly it is common practice to litter code with #ifdef BOARD_REVA etc. Kvasir::GPIO::PinLocation allows you to encode you port an pin information into a compile time accessible container. You can then pass this container a compile time factory function which will return you the corresponding register action. This way you only have to define your pins in one place and the proper code will be generated by Kvasir everywhere else.