Utilities for user-level device drivers

To avoid common implementation bugs in device drivers, the framework facilitates the declaration of hardware register and bit layouts in the form of C++ types. By subjecting the device driver's interaction with the hardware to the to C++ type system, the consistency of register accesses with the hardware specifications can be maintained automatically. Such hardware specifications are declarative and can be cleanly separated from the program logic of the driver. The actual driver program is relieved from any intrinsics in the form of bit-masking operations.

The MMIO access utilities comes in the form of two header files located at util/register.h and util/mmio.h.

Register declarations

The class templates found in util/register.h provide a means to express register layouts using C++ types. In a way, these templates make up for C++'s missing facility to define accurate bitfields. The following example uses the Register class template to define a register as well as a bitfield within the register:

 struct Vaporizer : Register<16>
 {
   struct Enable : Bitfield<2,1> { };
   struct State  : Bitfield<3,3> {
     enum{ SOLID = 1, LIQUID = 2, GASSY = 3 };
   };

   static void     write (access_t value);
   static access_t read  ();
 };

In the example, Vaporizer is a 16-bit register, which is expressed via the Register template argument. The Register class template allows for accessing register content at a finer granularity than the whole register width. To give a specific part of the register a name, the Register::Bitfield class template is used. It describes a bit region within the range of the compound register. The bit 2 corresponds to true if the device is enabled and bits 3 to 5 encode the State. To access the actual register, the methods read() and write() must be provided as back end, which performs the access of the whole register. Once defined, the Vaporizer offers a handy way to access the individual parts of the register, for example:

 /* read the whole register content */
 Vaporizer::access_t r = Vaporizer::read();

 /* clear a bit field */
 Vaporizer::Enable::clear(r);

 /* read a bit field value */
 unsigned old_state = Vaporizer::State::get(r);

 /* assign new bit field value */
 Vaporizer::State::set(r, Vaporizer::State::LIQUID);

 /* write whole register */
 Vaporizer::write(r);

Bitfields that span multiple registers

The register interface of hardware devices may be designed such that bitfields are not always consecutive in a single register. For example, values of the HDMI configuration of the Exynos-5 SoC are scattered over multiple hardware registers. The problem is best illustrated by the following example of a hypothetical timer device. The bits of the clock count value are scattered across two hardware registers, the lower 6 bits of the 16-bit-wide register 0x2, and two portions of the 32-bit-wide register 0x4. A declaration of those registers would look like this:

 struct Clock_2 : Register<0x2, 16>
 {
   struct Value : Bitfield<0, 6> { };
 };

 struct Clock_1 : Register<0x4, 32>
 {
   struct Value_2 : Bitfield<2, 13> { };
   struct Value_1 : Bitfield<18, 7> { };
 };

Writing a clock value needs consecutive write accesses to both registers with bit shift operations applied to the value:

 write<Clock_1::Value_1>(clk);
 write<Clock_1::Value_2>(clk >> 7);
 write<Clock_2::Value>(clk >> 20);

The new Bitset_2 and Bitset_3 class templates contained in util/register.h allow the user to compose a logical bit field from 2 or 3 physical bit fields. The order of the template arguments expresses the order of physical bits in the logical bit set. Each argument can be a register, a bit field, or another bit set. The declaration of such a composed bit set for the example above looks as follows:

 struct Clock : Bitset_3<Clock_1::Value_1,
                         Clock_1::Value_2,
                         Clock_2::Value> { };

With this declaration in place, the driver code becomes as simple as:

 write<Clock>(clk);

Under the hood, the framework performs all needed consecutive write operations on the registers 0x2 and 0x4.

Memory-mapped I/O

The utilities provided by util/mmio.h use the Register template class as a building block to provide easy-to-use access to memory-mapped I/O registers. The Mmio class represents a memory-mapped I/O region taking its local base address as constructor argument. The following example illustrates its use:

 class Timer : Mmio
 {
   struct Value   : Register<0x0, 32> { };
   struct Control : Register<0x4, 8> {
     struct Enable  : Bitfield<0,1> { };
     struct Irq     : Bitfield<3,1> { };
     struct Method  : Bitfield<1,2>
     {
       enum { ONCE = 1, RELOAD = 2, CYCLE = 3 };
     };
   };

   public:

     Timer(addr_t base) : Mmio(base) { }

     void enable();
     void set_timeout(Value::access_t duration);
     bool irq_raised();
 };

The memory-mapped timer device consists of two registers: The 32-bit Value register and the 8-bit Control register. They are located at the MMIO offsets 0x0 and 0x4, respectively. Some parts of the Control register have specific meanings as expressed by the Bitfield definitions within the Control struct.

Using these declarations, accessing the individual bits becomes almost a verbatim description of how the device is used. For example:

 void enable()
 {
   /* access an individual bitfield */
   write<Control::Enable>(true);
 }

 void set_timeout(Value::access_t duration)
 {
   /* write complete content of a register */
   write<Value>(duration);

   /* write all bitfields as one transaction */
   write<Control>(Control::Enable::bits(1) |
                  Control::Method::bits(Control::Method::ONCE) |
                  Control::Irq::bits(0));
 }

 bool irq_raised()
 {
   return read<Control::Irq>();
 }