Making things do stuff – Part 7

In our previous article we explored using templates to build a generic ‘register’ type to allow programmers to access hardware without all the nasty syntax of integer-to-pointer casting, etc.

At the moment, this class gives us little extra functionality beyond cleaning up the syntax (although, in its favour, it also doesn’t incur any additional run-time cost/performance).

In this article we’re going to extend our design to consider special hardware register types – notably read-only and write-only registers – and see how we can add extra functionality to our Register type.

Although there is more than one way to solve this problem we’re going to use a simple function-overload selection mechanism called ‘tag dispatch’.

A quick review

Our Register class at the moment is a simple wrapper around a raw pointer for accessing hardware memory.   We’ve used a trait class to allow us to use appropriate pointer types for different-sized hardware registers.

template <unsigned int sz>
struct Register_traits { };

template <>
struct Register_traits<8>  { using internal_type = std::uint8_t; };

template <>
struct Register_traits<16> { using internal_type = std::uint16_t };

template <>
struct Register_traits<32> { using internal_type = std::uint32_t; };

template <>
struct Register_traits<64> { using internal_type = std::uint64_t; };

 
template <std::size_t sz>
class Register
{
public:
  // Alias for convenience
  //
  using reg_type = typename Register_traits<sz>::internal_type;

  explicit Register(std::uint32_t address);

  Register& operator=(reg_type bit_mask);
  operator reg_type();

  inline Register& operator|=(reg_type bit_mask);
  inline Register& operator&=(reg_type bit_mask);
  inline Register& operator^=(reg_type bit_mask);

  // etc...

private:
  volatile reg_type* raw_ptr;
};

 

This allows us to create Register<> objects for accessing hardware; and we can use them like ‘normal’ objects.

(If you’ve been reading along this code should look familiar.  If not, I’d recommend going back to our second article to see what’s going on in the code below)

int main()
{
  Register<32> rcc_enable { 0x40023830 };
  Register<32> mode       { 0x40020C00 };
  Register<32> output     { 0x40020C14 };

  // Enable GPIO D clock
  //
  rcc_enable |= (1 << 3);

  // Set the GPIO mode to output
  // The blue LED is on pin 15
  //
  mode |= (0b01 << (15 * 2));

  // Flash the LED
  //
  while(true)
  {
    output |= (1 << 15);
    sleep(1000);

    output &= ~(1 << 15);
    sleep(1000);
  }
}

Read- and Write-only registers

As we discussed in the first article of this series a read-only register is a register than cannot be written to; a write-only register is one that may be written to, but if it is read its contents are undefined.

In C it is simple to deal with read-only and read-write registers:  we can do so with the const qualifier

#define READ_WRITE volatile
#define READ_ONLY  volatile const

int main(void)
{
  READ_WRITE uint32_t * const rw_reg = (uint32_t*)0x40020000;
  READ_ONLY  uint32_t * const ro_reg = (uint32_t*)0x40020004;

  uint32_t val_a = *rw_reg;  // OK   - read
  *rw_reg = 0x01;            // OK   - write

  uint32_t val_b = *ro_reg;  // OK   - read
  *ro_reg = 0x01;            // FAIL - write
}

 

But what about write-only registers?  There’s no way in C of qualifying an object as write-only.  Most implementations simply use the same qualification as read-write and rely on the programmer (or code reviews) to find errors

#define READ_WRITE volatile
#define WRITE_ONLY volatile
#define READ_ONLY  volatile const

int main(void)
{
  READ_WRITE uint32_t * const rw_reg = (uint32_t*)0x40020000;
  READ_ONLY  uint32_t * const ro_reg = (uint32_t*)0x40020004;
  WRITE_ONLY uint32_t * const wo_reg = (uint32_t*)0x40020008;

  uint32_t val_a = *rw_reg;  // OK   - read
  *rw_reg = 0x01;            // OK   - write

  uint32_t val_b = *ro_reg;  // OK   - read
  *ro_reg = 0x01;            // FAIL – write

  uint32_t val_c = *wo_reg;  // Works, but is not valid
  *wo_reg = 0x01;            // OK – write
}

 

To solve this problem in C++ we’re going to use a technique known as ‘tag dispatch’

Tag dispatch

Tag dispatch is a function overloading and dispatch mechanism.  The aim is to categorise classes with particular ‘characteristics’ and dispatch function overloads based on the particular characteristics on an object.

Class characterstics 

First we need to establish what characteristics our classes should have.  This is used to define a class hierarchy.  The idea is that classes ‘accumulate’ characteristics through inheritance.  These characteristics are defined through a set of ‘tag’ classes

Let’s establish our tag classes:

class read_only { };
class write_only { };
class read_write : public read_only, public write_only { };

 

These are just empty classes.  Their only purpose is as a function dispatch selector.

Notice the use of multiple inheritance in our tags.  A read_write tag is a type-of read_only and a type-of write_only.  That is, it supports both reading and writing.  More accurately, a read_write tag object could be substituted for both a read_only object and a write_only object.  We will exploit these feature in a moment.

We now need to extend our Register class with a new template parameter

template <std::size_t sz, typename Register_Ty = read_write>
class Register
{
  // As previously...
};

 

The Register_Ty template parameter defines the class’s read-write characteristics.  I’ve defaulted it to read_write (the most generic)

We can use this now in Register object construction

int main()
{
  Register<32>             rw_reg { 0x4002000 };
  Register<32, read_only>  ro_reg { 0x4002004 };
  Register<32, write_only> wo_reg { 0x4002008 };
}

 

Tag dispatch overloads

The next step is to define behaviour (functions) appropriate to each object’s characteristics. For our Register class this means we want to:

  • Enable ‘read’ functions for read-only Register types
  • Enable ‘write’ functions for write-only Register types
  • Enable ‘read’ and ‘write’ for read-write Register types

To do this we mark each function based on the Register type that can call it.  This is done by adding another parameter to the function – the tag class.

for an operator overload (for example, |=) this causes us a problem – there’s no way we can supply an extra (that is, third) parameter to the call.  And, in reality, we don’t want to have to do that anyway.

The solution is to use nested calls:  the operator overload function calls on a (protected) implementation that implements the tag overload

template <std::size_t sz, typename Register_Ty = read_write>
class Register
{
public:
  using reg_type = typename Register_traits<sz>::internal_type;

  explicit Register(std::uint32_t address) :
    raw_ptr { reinterpret_cast<std::uint32_t*>(address) }
  {
  }

  // Operator overloads.  The public API
  //
  void operator=(reg_type bit_mask)
  {
    write(bit_mask, Register_Ty { });
  }

  operator reg_type() const
  {
    return read(Register_Ty { });
  }

  void operator|=(std::uint32_t bit_mask)
  {
    or_assign(bit_mask, Register_Ty { });
  }

  void operator&=(std::uint32_t bit_mask)
  {
    and_assign(bit_mask, Register_Ty { });
  }

protected:
  // Tag-dispatched implementation
  // functions
  //
  void write(reg_type bit_mask, write_only)
  {
    *raw_ptr = bit_mask;
  }

  reg_type read(read_only) const
  {
    return *raw_ptr;
  }

  void or_assign(std::uint32_t bit_mask, read_write)
  {
    *raw_ptr |= bit_mask;
  }

  void and_assign(std::uint32_t bit_mask, read_write)
  {
    *raw_ptr &= bit_mask;
  }

private:
  volatile reg_type* raw_ptr;
};

 

Let’s look at the public API functions (the operator overloads).  These functions simply call the implementation function, passing on any parameters.  In addition, they pass an object of type Register_Ty.  This is the tag dispatch.  The function that will be called will be the function with the best-match signature.  If no signature matches, there will be a compiler error.

For example, If the Register object is tagged as read_only, a call to operator reg_type() will call read(read_only { }):

// For a read_only-tagged Register...
//
operator reg_type() const
{
  return read(Register_Ty { }); // read(read_only{})
}

 

That is an exact match to the implementation function read().  However, if a call is made to operator=

// For a read_only-tagged Register...
//
void operator=(reg_type bit_mask)
{
  write(bit_mask, Register_Ty { }); // write(uint32_t, read_only{})
}

 

There is no overload for write that matches this signature; and the call fails at compile-time. (Unfortunately, the failure will be flagged inside the Register class, not at the point-of-call.  Such is the nature of templates)

What about objects marked as read_write?  For functions like operator&= there is a direct match.  (Note:  since &= is a read-modify-write operation the Register must support both read and write to work)

For functions like operator= this is where tag dispatch gets useful.  Since a read_write tag class inherits from read_only and write_only it can be substituted for either of them (note: but not the other way round!).

So a call to write() will work, even though the function signature is not an exact match.

// For a read_write-tagged Register...
//
void operator=(reg_type bit_mask)
{
  write(bit_mask, Register_Ty { }); // write(uint32_t, read_write{})
                                    // read_write object is sliced
                                    // into write_only object.
}

 

The result of this tag dispatch is that we can check for – at compile-time – both read-only and write-only registers, without having to explicitly overload for all combinations of tag value.

int main()
{
  Register<32>             rw_reg { 0x4002000 };
  Register<32, read_only>  ro_reg { 0x4002004 };
  Register<32, write_only> wo_reg { 0x4002008 };

  rw_reg         = 0x01;      // OK   – write supported
  uint32_t val_a = rw_reg;    // OK   – read supported

  ro_reg         = 0x01;      // FAIL – write not supported
  uint32_t val_b = rw_reg;    // OK   – read supported

  wo_reg         = 0x01;      // OK   – write supported
  uint32_t val_c = rw_reg;    // FAIL – read not supported
}

 

Under the hood

Given all the extra functions we’ve written it feels as if there will be a run-time price to pay for all this code.

However – the tag classes are only used for function dispatch.  Since the tag-dispatched functions are inlined, and the tag classes are empty and never used, the compiler will optimise them away.

To confirm this here’s our code from earlier, updated with the tag dispatch tags.

int main()
{
  Register<32, read_write> rcc_enable { 0x40023830 };
  Register<32, read_write> mode       { 0x40020C00 };
  Register<32, write_only> output     { 0x40020C14 };

  // Enable GPIO D clock
  //
  rcc_enable |= (1 << 3);

  // Set the GPIO mode to output
  // The blue LED is on pin 15
  //
  mode |= (0b01 << (15 * 2));

  // Flash the LED
  //
  while(true)
  {
    output |= (1 << 15);
    sleep(1000);

    output &= ~(1 << 15);
    sleep(1000);
  }
}

 

And here’s the assembly output:

; main() {
;
08000d44:   ldr     r2, [pc, #36]       ; r2  = 0x40023830 <rcc_enable>
08000d46:   ldr     r3, [r2, #0]        ; r3  = *r2
08000d48:   orr.w   r3, r3, #8          ; r3  = r3 | 0x08
08000d4c:   str     r3, [r2, #0]        ; *r2 = r3

; mode |= (0b01 << (15 * 2);
;
08000d4e:   ldr     r2, [pc, #32]       ; r2  = 0x40020C00 <mode>
08000d50:   ldr     r3, [r2, #0]        ; r3  = *r2
08000d52:   orr.w   r3, r3, #1073741824 ; r3  = r3 | 0x40000000
08000d56:   str     r3, [r2, #0]        ‘ *r2 = r3

; while(true) {
; output |= (1 << 15);
;
loop:
08000d58:   ldr     r3, [pc, #24]       ; r3  = 0x40020C14 <output>
08000d5a:   ldr     r2, [r3, #0]        ; r2  = *r3
08000d5c:   orr.w   r2, r2, #32768      ; r2  = r2 | 0x8000
08000d60:   str     r2, [r3, #0]        ; *r3 = r2

; output &= ~(1 << 15);
;
08000d62:   ldr     r2, [r3, #0]        ; r2  = *r3 <output>
08000d64:   bic.w   r2, r2, #32768      ; r2  = r2 & ~0x8000
08000d68:   str     r2, [r3, #0]        ; *r2 = r2

; }
;
08000d6a:   b.n     0x8000d58           ; goto loop

                                        ; Register addresses:
08000d6c:   dcd     1073887280          ; 0x40023830
08000d70:   dcd     1073875968          ; 0x40020C00
08000d74:   dcd     1073875988          ; 0x40020C14

You can see from the assembly that we’re paying no additional price at run-time for these checks.

Summary

Tag dispatch is a simple mechanism for achieving something – compile-time checking for write-only objects – that cannot be done in C.  We can also add to that the fact that this has no impact on run-time performance.

In our next article we’ll add another enhancement to our Register type – bitwise access

Glennan Carnie

Glennan Carnie

Technical Consultant at Feabhas Ltd
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.

He specialises in C++, UML, software modelling, Systems Engineering and process development.
Glennan Carnie

Latest posts by Glennan Carnie (see all)

Dislike (0)

About Glennan Carnie

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry. He specialises in C++, UML, software modelling, Systems Engineering and process development.
This entry was posted in C/C++ Programming and tagged , , , , , , . Bookmark the permalink.

2 Responses to Making things do stuff – Part 7

  1. Veit says:

    Have you considered using std::enable_if for the return type of the operators? I believe it would work (can't test right now) and communicate the intended functionality better than the additional indirection.

    Like (0)
    Dislike (0)
  2. Veit,

    You're absolutely correct - SFINAE (enable_if) is a valid way of achieving the same result.

    I'm going to come back and revisit the read-only and write-only Register in the last article of this series, and look at SFINAE and C++17's constepxr-if as alternative approaches.

    (SPOILERS: I prefer the SFINAE approach for this problem 🙂 )

    Like (0)
    Dislike (0)

Leave a Reply