Making things do stuff – Part 8

We’ve been using templates to provide a hardware register abstraction for use with hardware manipulation problems, typical of what you’d find in a deeply-embedded (“bare-metal”) system.

Previously, we looked at using trait classes to establish pointers and tag-dispatch to handle special-case registers, such as read-only or write-only register.

In this article we’re going to add some syntactic sugar to our Register class, to allow the developer to access individual bits in the register using array-index ([]) notation.  This will allow us to explore the concept of proxy objects with a concrete and practical use case.

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.

If you’re familiar with hardware manipulation, I’d highly recommend going back to article 2 to familiarise yourself with our problem domain.

The story so far

If you just arrived at this article, I’d highly recommend reading the previous two to familiarise yourself with the code so far.

I’m not going to repeat a lot of the code from earlier articles to avoid cluttering-up the topic at hand.

Accesssing individual bits

Whilst it’s useful to be able to access registers as whole words in many instances it is much more convenient to access individual bits.

One (very common) model for doing this is to conceptually treat a Register object as an array of bits.  We can now use the index operator to access individual bits

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

  mode |= ( 1 << (15 * 2));  // Word-based API 

  output[15] = 1;            // Access individual bit
}

 

This is elegant for the client but presents us with some implementation problems.  Consider:  when you use the index operator on an array it returns an array element

int main()
{
  int arr[10];

  int i = arr[0];  // arr[0] yields an int
}

 

In the case of the Register class we can’t return an individual bit – there is no ‘bit’ type in C++.

The solution is to create a type that represents a bit in the hardware – a proxy for a bit.  This proxy type should be created as a nested type within the Register class

// Tag classes, as previously...

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

  // bit_proxy represents one of the
  // individual bits in the hardware
  // register.
  //
  class bit_proxy
  {
  public:
    // To be defined

  protected:
    bit_proxy(Register<sz, Register_Ty>* reg, unsigned int num) :
      owner   { reg },
      bit_num { num }
    {
    }

  private:
    friend class Register<sz, Register_Ty>;

    Register<sz, Register_Ty>* owner;
    unsigned int  bit_num;
  };

public:
  // API as before...

  bit_proxy operator[](unsigned int index)
  {
    // Check that index is valid...

    return bit_proxy { this, index };
  }
};

 

The bit_proxy stores information about the particular bit it represents; but it must also store the identity of the actual Register object it is manipulating.

We’ve made the constructor of the bit_proxy protected: we don’t want clients to create proxy objects.  Constructing a bit_proxy is delegated to the index operator.  The index operator does no actual bit manipulation – that is all handed off to the proxy.

The client receives a bit_proxy object that can be used to indirectly manipulate the original Register object.

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

  uint32_t value = output[15];  // Read via proxy

  output[15] = 1;               // Write via proxy
}

 

To support the code above our bit_proxy requires two methods:

  • Read – Convert to an (unsigned) integer (specifically, whatever reg_type is declared as in the owning class).
  • Write – Set a particular bit, using an (unsigned) integer value

These functions must indirectly manipulate the owning Register.  Furthermore:

  • A bit_proxy for a read-only Register must allow reading, but not writing
  • A bit_proxy for a write-only Register must allow writing, but not reading
  • A bit_proxy for a read-write Register must allow reading and writing

To continue our previous pattern we will use tag-dispatch.  As the bit_proxy is a nested class of the Register we can use the Register_Ty template parameter (as before).

class bit_proxy
{
public:
  // Read
  //
  operator Register::reg_type() const
  {
    return read(Register_Ty { });
  }
 
  // Write
  //
  void operator=(Register::reg_type val)
  {
    write(val, Register_Ty { });
  }

protected:
  bit_proxy(Register<sz, Register_Ty>* reg, unsigned int num) :
    owner   { reg },
    bit_num { num }
  {
  }

  // Tag-dispatched read and write implementations
  //
  Register::reg_type read(read_only) const
  {
    return (*(owner->raw_ptr) & (1 << bit_num))? 1 : 0;
  }

  void write(Register::reg_type val, write_only)
  {
    if(val == 0) *(owner->raw_ptr) &= ~(1 << bit_num);
    else         *(owner->raw_ptr) |=  (1 << bit_num);
  }

private:
  friend class Register<sz, Register_Ty>;

  Register<sz, Register_Ty>* owner;
  unsigned int  bit_num;
};

 

Bit proxy copy-assignment

What about the following code?

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

  output[15] = 1;           // Call 1
  output[14] = output[15];  // Call 2
}

 

The first call is invoking our assignment operator as specified above.  However, the second call is the copy-assignment operator; that is

bit_proxy bit_proxy::operator=(const bit_proxy&);

 

We haven’t written this function, the compiler supplied it; but it’s not going to do what we want.  The default behaviour of the copy-assignment operator is to set all the attributes of the left-hand bit_proxy to those of the right-hand.  This could lead to a number of (disastrous) effects:

  • The bit_proxy could change its owner to another Register object
  • The bit number could be changed to represent another bit

In other words, our proxy object would no longer represent its original bit, but some completely other bit!

Remember, a proxy is just a stand-in for a real bit, somewhere in (hardware) memory.  What we actually want to do is transfer the state represented by the right-hand proxy object onto the state represented by the left-hand proxy object.

Thus there are two operations we need to perform:

  • Read the state of the right-hand object
  • Change the state of the left-hand object to match.

Luckily, we already have two functions to allow us to do this (although the syntax for calling them in this context is far from pretty)

bit_proxy& bit_proxy::operator=(const bit_proxy& rhs)
{
  operator=(rhs.operator Register::reg_type());
  return *this;
}

 

Perhaps a cleaner way to do this is to get the compiler to deduce the calls to make:

bit_proxy& bit_proxy::operator=(const bit_proxy& rhs)
{
  *this = static_cast<Register::reg_type>(rhs);
  return *this;
}

 

What about const Registers?

Register objects are just like any other object type.  In the example below we’re passing a (read-write) Register object to a function as an input parameter.  If we follow the idioms for parameter passing in C++ it should be passed as a reference-to-const.

 

// Alias to simplify code
//
using Register_t = Register<32, read_write>;


// process() takes a Register_t as 
// an input parameter
//
void process(const Register_t& in_reg)
{
 in_reg[15] = 1;     // <= Write should fail

 if(in_reg[15] == 1) // <= Read is OK
 {
   ...
 }
}


int main()
{
  // Create a read-write Register
  //
  Register_t mode { 0x40020C10 };

  process(input)
}

 

Notice in the process() function we have a constant read-writable Register object!

A const read-write Register object is effectively a read-only object.  As you might expect only read-only operations should compile – even if the Register is declared as a read-write type.

(And yes, you can make a write-only Register const; although it makes no sense to do so – you can never read from, or write to, it.  Not so useful, then)

We want reads of a const Register object to succeed, but writes should fail.

Our first-pass is to be make a read-only version of the index operator that returns a const bit_proxy.

const bit_proxy operator[](unsigned int index) const
{
  // Check that index is valid...

  return bit_proxy { const_cast<Register*>(this), index };
}

 

Note the const_cast<>  This is required because the bit_proxy class stores a pointer to a non-const Register object internally and that cannot be initialised with a const-object (remember, a const member function changes the type of the ‘this’ pointer).

This code works (ugly const_cast notwithstanding) but is really not necessary.  Since we cannot modify the Register object we don’t need a proxy object.  It is simply enough to return the value of the selected bit.  To ensure we don’t allow reads of write-only objects we must also called our (tag-dispatched) read() function.

template <std::size_t sz, typename Register_Ty>
unsigned int
Register<sz, Register_Ty>::operator[](unsigned int index) const
{
  return ((read(Register_Ty { }) & (1 << index)) != 0 ? 1 : 0);
}

Under the hood (again)

Of course, nothing ever comes for free; there must be a cost to our syntactic sugar.   We’ve created a lot more template code now.

Here’s our perennial example – flashing the blue LED – now updated for our bit-access operator

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

  rcc_enable[3] = 1;

  // Set the GPIO mode to output
  // The blue LED is on pin 15
  //
  mode[30] = 1;
  mode[31] = 0;

  while(true)
  {
    output[15] = 1;
    sleep(1000);

    output[15] = 0;
    sleep(1000);
  }
}

 

And, as usual, let’s examine the output opcodes

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

; rcc_enable[3] = 1;
;
08000d48:   ldr     r0, [pc, #48]       ; r0  = 0x40023830 <rcc_enable>
08000d4a:   ldr     r1, [r0, #0]        ; r1  = *r0
08000d4c:   orr.w   r1, r1, #8          ; r1  = r1 | 0x08
08000d50:   str     r1, [r0, #0]        ; *r0 = r1

; mode[30] = 1;
;
08000d52:   ldr     r1, [r2, #0]        ; r1  = *r2 <mode>
08000d54:   orr.w   r1, r1, #1073741824 ; r1  = r1 | 0x40000000
08000d58:   str     r1, [r2, #0]        ; *r2 = r1

; mode[31] = 0;
;
08000d5a:   ldr     r1, [r2, #0]        ; r1  = *r2 <mode>
08000d5c:   bic.w   r1, r1, #2147483648 ; r1  = r1 & ~0x80000000
08000d60:   str     r1, [r2, #0]        ; *r2 = r1

; while(true) {
; output[15] = 1;
;
loop:
08000d62:   ldr     r2, [r3, #0]        ; r2  = *r3 <output>
08000d64:   orr.w   r2, r2, #32768      ; r2  = r2 | 0x8000
08000d68:   str     r2, [r3, #0]        ; *r3 = r2

; output[15] = 0;
;
08000d6a:   ldr     r2, [r3, #0]        ; r2  = *r3 <output>
08000d6c:   bic.w   r2, r2, #32768      ; r2  = r2 & ~0x8000
08000d70:   str     r2, [r3, #0]        ; *r3 = r2

; }
08000d72:   b.n     0x8000d62           ; goto loop

                                        ; Register addresses:
08000d74:   dcd     1073875968          ; 0x40020C00
08000d78:   dcd     1073875988          ; 0x40020C14
08000d7c:   dcd     1073887280          ; 0x40023830

 

This code is – to all practical intents and purposes – identical to the original code we started with in article 2.

Summary

There is a fear that more complex C++ constructs – like templates – should be avoided in embedded code for fear of significant memory and performance issues.  Used carefully and judiciously, though, these constructs can produce very flexible abstractions at low (almost zero) additional cost.

If you want to explore these aspects in more detail have a look at our training courses

 

C++-501

AC++-501

AC++11-401

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 General and tagged , , , . Bookmark the permalink.

2 Responses to Making things do stuff – Part 8

  1. Now how about a future look at using ranges (or something else more appropriate?) to represent multi-bit register fields? 😉 Of different signage than the containing register (i.e. uint32_t register containing signed or enumeration fields)? That would be c++17 bare-metal nirvana...

    Like (0)
    Dislike (0)
  2. Ooh, interesting! I'll have a think...

    Although, I might have to wait a while for my cross-compiler to catch up! 🙂

    Like (0)
    Dislike (0)

Leave a Reply