My question is focused on design, and the code included in this question is meant to clarify the problem I'm experiencing. I'm interested in conceptual level answers and not code in order to answer my question.
Background:
When programming bare-metal systems, one often has to manipulate bits in memory-mapped register controlling the behaviour of on-chip or external peripherals. Usually, there are software libraries defining these bits and registers. A minimal example might look like this:
#include <stdio.h>
#include <stdint.h>
/* Might be in some library like this: */
#define SOME_PERIPHERAL_BASE_ADDRESS 0x8fce0000
#define SOME_PERIPHERAL_REG1 (*((uint16_t*)(SOME_PERIPHERAL_BASE_ADDRESS + 0x8e)))
#define SOME_PERIPHERAL_REG1_AM (1 << 3)
#define SOME_PERIPHERAL_REG1_FOO (1 << 4)
#define SOME_PERIPHERAL_REG1_BAR (1 << 5)
/* User code: */
int main()
{
/* How to use these definitions on a bare metal system: */
/* SOME_PERIPHERAL_REG1 is initialies to the reset value by hardware, so
* there is no need to initialise it. */
/* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */
/* This will of course segfault when not on a bare metal system. */
/* SOME_PERIPHERAL_REG1 = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; */
/* printf("reg1 = %i\n", SOME_PERIPHERAL_REG1); */
/* Ersatz dummy code for testing on a pc: */
uint16_t value=0;
printf("reg1 = %i\n", value);
value = SOME_PERIPHERAL_REG1_AM | SOME_PERIPHERAL_REG1_BAR; /* <- No type-safty! */
printf("reg1 = %i\n", value);
return 0;
}
Issues with this approach:
While this code is reasonably readable and short, it lacks in certain ways:
- A programmer will tediously have to retype the prefix (SOME_PERIPHERAL_REG1_ in the example) for each register value
- No type safety: A programmer can easily set a register to a value that was intended for another register (e.g. set reg1 of Some_peripheral to a value intended for the control register of Another_peripheral) or not to a register value at all (e.g. set reg1 to 15 (an integer)).
- Another problem with this approach is, that some registers are not accessible with simple assigns. For example, I use an SSD1289 display controller that is connected to the external memory interface of my microcontroller. To set the registers of the SSD1289, I will need to first write the address of the register to a certain address in the memory region followed by a write of the new register value to another address in that region. With plain C, there is no way of encapsulating this except of using a setter function.
Potential Solution:
To overcome this, I tried creating a register object in C++ that is parameterised with an enum containing the bit definitions and a pointer to the address of the register. A minimal example might look like this:
#include <iostream>
#include "enum_binary_operators.hpp"
// Code that should go in the library:
/**
* \brief An object ancapsulating a memory-mapped peripheral control register.
*
* \param reg_bits An enum defining the bitfields for that register.
* \param ret_address The memory-mapped address where the register lies in
* memory
*/
template < typename reg_bits, uintptr_t reg_address >
class Register
{
private:
// On the intended pare metal target, this wouldn't be neccesary, as
// the value would be stored in the memory-mapped address of the register.
uint16_t value = 0;
public:
// Write a new value to the register
inline Register& operator= (reg_bits const& val)
{
// This is what is intended, but of course segfaults when not on bare metal.
//*reinterpret_cast<uint16_t*>(reg_address) = static_cast < uint16_t > (val);
// dummy replacement of the above line.
value = static_cast < uint16_t > (val);
// possibly do some more things like send the value to a remote register.
return *this;
}
operator uint16_t() // Read register
{
// This is what is intended, but of course segfaults when not on bare metal.
//return *reinterpret_cast<uint16_t*>(reg_address);
// dummy replacement of the above line.
return value;
}
[[deprecated("Please only use the appropriate register bit enum values to manipulate the register!")]] Register& operator= (int val) {}
};
/**
* \brief An Object encapsulating a memory-mapped peripheral
*
* \param base_address The base address of the registers belonging to that peripheral.
*/
template < uintptr_t base_address>
class Some_peripheral
{
public:
// This enum should not be in global namespace to avoid name conflicts.
// For example many peripherals might have a register named
// "control_register".
enum reg1_bits : uint16_t
{
AM = (1 << 3),
FOO = (1 << 4),
BAR = (1 << 5),
};
// more register bit definitions
Register < reg1_bits, base_address + 0x8e > reg1;
// more registers
// ...
// possibly more high-level functions for the peripheral like
// initialisation routines, transmission routines, etc.
};
// More peripherals
// The base address should be provided by some file defining the peripheral addresses for a given chip.gg
//extern constexpr some_peripheral_base;
constexpr uintptr_t some_peripheral_base = 0x8fce0000;
Some_peripheral<some_peripheral_base> some_peripheral1;
// And some more peripherals
// User code:
int main()
{
// Get the current value of the register.
std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;
// Each register can only be written with the bits that belong to it.
some_peripheral1.reg1 = Some_peripheral<some_peripheral_base>::reg1_bits::AM
| Some_peripheral<some_peripheral_base>::reg1_bits::BAR;
// This is the intended usage:
// Somehow, operator= should be able to deduce the scope resolution automatically at runtime.
//some_peripheral1.reg1 = AM | BAR;
std::cout<<"reg1 = "<<static_cast < uint16_t > (some_peripheral1.reg1)<<std::endl;
return 0;
}
With enum_binary_operators.hpp:
#ifndef ENUM_BINARY_OPERATORS_HPP
#define ENUM_BINARY_OPERATORS_HPP
#include <type_traits>
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator|(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) | static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator&(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) & static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator^(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) ^ static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(T const& x, T const& y) {
return static_cast < T > (static_cast < uint16_t > (x) << static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(unsigned int const x, T const& y) {
return static_cast < T > (x << static_cast < uint16_t > (y));
}
template < typename T >
inline typename std::enable_if < std::is_enum < T > ::value, const T > ::type
operator<<(int const x, T const& y) {
return static_cast < T > (x << static_cast < uint16_t > (y));
}
#endif /* ENUM_BINARY_OPERATORS_HPP */
Questions:
Now, my questions are:
- Am I correct in assuming this should come at negligible run time overhead? Compile time will be increased, of course.
- Is there a better way of doing this?
- And is there any way of getting rid of the long scope specifiers in the assignment? The idea here is, that the compiler already knows, where the enum members will have to come from (since the enum is a template argument to the register class), so maybe one could get it to include the proper scope resolution on its own in some way.
typedef
,using
,namespace alias
, and declare aliases for constant enum values inside the function withconst auto my_enum_value_alias = some_enum_scope::some_enum_value;
Also, in some situations, I find that perhaps some things (e.g. utility functions) that are common to all derived classes or template specializations are better put in a non-template class. In other words, decide what should go into the template class; put everything else in a non-template class.