Making things do stuff – Part 6

As code designers we tend to eschew specific ‘stove-pipe’ code in favour of reusable code elements.  Up until now we’ve been coding some very specific examples so it’s probably worth looking at some more generic solutions.

In this article we’ll look at building generic register manipulation classes (or, as one commenter referred to them, ‘register proxy’ classes).  Here, we’re really exploring code design rather than coding ‘mechanics’.  I’m using this to explore some factors like the balance between efficiency, performance and flexibility.

I’ll be making some design choices that you may not agree with.  That’s fine.  Leave your discussion points in the comments below.

If you’re new to hardware manipulation in C++, I’d recommend going back to the basics article for a refresher of what we’re trying to achieve.

A first-pass design

Our previous designs have focussed on building abstractions of I/O devices – GPIO ports, UARTs, etc.  This time we will focus on a class that represents a single hadware register.  Here’s a first-pass design.  This is not a complete interface by any stretch.  I’m deliberately ignoring most of the API for clarity.  We discuss interface design in more detail in this article.

class Register
{
public:
  explicit Register(std::uint32_t address);

  Register& operator=(std::uint32_t bit_mask);
  operator uint32_t();

  inline Register& operator|=(std::uint32_t bit_mask);
  inline Register& operator&=(std::uint32_t bit_mask);
  inline Register& operator^=(std::uint32_t bit_mask);

  // etc...

private:
  volatile std::uint32_t* raw_ptr;
};

 

The constructor maps the internal raw_ptr onto the supplied address.

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

 

Operations on the Register class are mapped directly onto the hardware

Register& Register::operator|=(std::uint32_t bit_mask)
{
  *raw_ptr |= bit_mask;
  return *this;
}

 

Clients can now use Register objects as proxies for hardware registers

int main()
{
  Register mode   { 0x40020C00 };
  Register output { 0x40020C14 };

  mode   |= (1 << (15 * 2));
  output |= (1 << 15);
  output &= ~(1 << 15);

  ...
}

 

There’s a limitation with this class at the moment: it only handles 32-bit registers.  For flexibility we’d like to be able to support 8-bit, 16-bit and 32-bit registers.  We could provide multiple classes, for example:

class Register_32
{
  // As above.
};

class Register_16
{
  // As above, but for uint16_t.
};

class Register_8
{
  // You get the idea...
};

 

There’s a huge amount of code repetition here.  The design is crying out for a generic solution.

Second attempt:  template-based solution

Let’s make our Register class a template.  But what’s the template parameter?  Let’s start by making it the underlying type

template <typename T>
class Register
{
public:
  explicit Register(std::uint32_t address);

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

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

  // etc...

private:
  volatile T* raw_ptr;
};

 

Our client code changes, now

int main()
{
  Register<std::uint32_t> mode   { 0x40020C00 };
  Register<std::uint32_t> output { 0x40020C14 };

  mode   |= (1 << (15 * 2));
  output |= (1 << 15);
  output &= ~(1 << 15);

  ...
}

 

This is fine; but it doesn’t prevent awkward client code like this

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

  // What happens when you perform bitwise
  // operations on signed numbers?
  //
  mode   |= (1 << (15 * 2));
  output |= (1 << 15);
  output &= ~(1 << 15);

  ...
}

 

Third attempt: Using a template trait class

An alternative approach is to encapsulate the type of the pointer and just allow the client to specify the number of bits in the register.

#include <cstddef>

template <std::size_t sz>
class Register
{
  // We’ll come back to this...
};


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

  mode   |= (1 << (15 * 2));
  output |= (1 << 16);       // Should this be allowed on
                             // on a 16-bit register?
  ...
}

 

This gives us an implementation problem:  what’s the type of the underlying raw pointer?

template <std::size_t sz>
class Register
{
public:
  explicit Register(std::uint32_t address);

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

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

  // etc...

private:
  volatile ???* raw_ptr;
};

 

For an 8-bit register the pointer-type should be (something like) std::uint8_t; for a 16-bit register it should be std::uint16_t; and so on.  How can we deduce the type just from the number of bits?

The solution is to use a template trait class.  A trait class acts as a compile-time lookup for type-specific (or, in our case, value-specific) characteristics.  For a more detailed explanation of trait classes have a look here.

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* const raw_ptr;
};

 

When a Register template class is instantiated the sz template parameter is used to select an appropriate trait class specialisation.  The trait class’s internal_type alias is used to provide the reg_type for the Register class.

int main()
{
  Register<32> mode   { 0x40020C00 }; // <= std::uint32_t
  Register<16> output { 0x40020C14 }; // <= std::uint16_t
  Register<17> odd    { 0x40021000 }; // FAIL – No trait for 17-bit
}

 

Thus, a Register<32> will have its reg_type set as std::uint32_t; a Register<16> will have its reg_type declared as std::uint16_t.

If an arbitrary number is selected, for example Register<17>, the code will fail to compile as there is no appropriate trait class.

Under the hood

What’s the cost of this template complexity in terms of run-time performance?  We’ll use a variation on code we’ve used before

#include “Register.h”

namespace STM32F407
{
  enum device
  {
    GPIO_A, GPIO_B, GPIO_C,
    GPIO_D, GPIO_E, GPIO_F,
    GPIO_G, GPIO_H, GPIO_I
  };


  inline void enable_device(device dev)
  {
    Register<32> rcc_enable { 0x40023830 };
    rcc_enable |= (1 << dev);
  }

}  // namespace STM32F407

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

  STM32F407::enable_device(STM32F407::GPIO_D);

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

  while(true)
  {
    output |= (1 << 15);
    sleep(1000);

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

 

Here’s the assembler output:

; main() {
;
08000d44:   ldr     r1, [pc, #36]       ; r1 = 0x40020C00 <mode>
08000d46:   ldr     r3, [pc, #40]       ; r3 = 0x40020C14 <output>

; STM32F407::enable_device(STM32F407::GPIO_D);
;
08000d48:   ldr     r0, [pc, #40]       ; r0  = 0x40023830 <rcc_enable>
08000d4a:   ldr     r2, [r0, #0]        ; r2  = *r0
08000d4c:   orr.w   r2, r2, #8          ; r2  = r2 | 0x08
08000d50:   str     r2, [r0, #0]        ; *r0 = r2

; mode |= (0b01 << (15 * 2));
;
08000d52:   ldr     r2, [r1, #0]        ; r2  = *r1 <mode>
08000d54:   orr.w   r2, r2, #1073741824 ; r2  = r2 | 0x40000000
08000d58:   str     r2, [r1, #0]        ; *r1 = r2

; while(true) {
; output |= (1 << 15);
loop:
08000d5a:   ldr     r2, [r3, #0]        ; r2  = *r3 <output>
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]        ; *r3 = r2

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

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

 

A quick comparison with our earlier examples reveals almost identical code.  This is not a huge surprise as most of the template ‘magic’ is being done at compile-time, not run-time.

Summary

We’ve only just begun to explore the use of templates for hardware access.  At the moment we haven’t gained a huge amount of benefit over ‘raw’ hardware access; except losing the pain of integer-to-pointer casts.

In the next article we’ll extend our design and look at a mechanism for dealing with read-only and write-only registers.

Glennan Carnie
Dislike (0)
Website | + posts

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.

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.

3 Responses to Making things do stuff – Part 6

  1. Mli Mber says:

    Have you tried out the Kvasir for a model approach to low-level interaction? See https://kvasir.io or the CppCast episode with Odin Holmes.

    Like (0)
    Dislike (0)
  2. Looks interesting! Thanks!

    Like (0)
    Dislike (0)
  3. I'm not sure size_t is the right choice for template parameter type. Yes, the number of bits in the registry is somewhat related with the number of bits in a pointer. But it is hard to imagine more than say 2^32 bits in a registry. When you use size_t your binary layout will be different for different platforms, which is not what you usually want.

    Also, since this is the _number_ of bits in the registry, consider using signed typed for it, to make things more clear. See this excellent talk about where to use signed and unsigned types from Jon Kalb: https://www.youtube.com/watch?v=wvtFGa6XJDU&t=82s

    Like (0)
    Dislike (0)

Leave a Reply