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.
Contents
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.
- Practice makes perfect, part 3 – Idiomatic kata - February 27, 2020
- Practice makes perfect, part 2– foundation kata - February 13, 2020
- Practice makes perfect, part 1 – Code kata - January 30, 2020
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.
Have you tried out the Kvasir for a model approach to low-level interaction? See http://kvasir.io or the CppCast episode with Odin Holmes.
Looks interesting! Thanks!
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