6

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.
  • 2
    Here's an article I believe you may find to be quite helpful about template-based register access, and I think it will help you answer each of your questions. – J Trana Oct 31 '15 at 0:11
  • The user can use more typedef, using, namespace alias, and declare aliases for constant enum values inside the function with const 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. – rwong Nov 1 '15 at 0:04
  • @JTrana: Wow, that is a really nice article! Thank You very much! Just one more question though: If I want to change multiple bits/fields in the same register with one access, is there a way to do so? And if no, why not? I have seen some examples using the bits (instead of the registers) as first class citizens but so far, they all seemed to lack that possibility. Rationale: Say I want to completely reconfigure a peripheral regularly, or some settings are mutaly exclusive (e.g. if exactly one of DIS or EN had to be set in PERIPH_CR in the example). – mox Nov 1 '15 at 12:25
  • I think it just comes down to how you implement write(). I don't think there's any reason that you can't support arbitrary values rather than single bits. – J Trana Nov 5 '15 at 6:52
  • Why don't you use C bit fields? They are safe enough even without C++ encapsulation features. – Netch Nov 30 '15 at 10:35
2

One your questions raised has an easy answer: retyping the prefix.

I teach that such prefixes should be a sematic portion not just a concatenation with the name: prefix::x, not prefix_x.

For the flags, put them into their own nested namespace. You can use that within the function or even the specific scope where a bunch of them are used together. (This is a case where using directives are OK, because it is targeted and local.)

{ // in some inner scope
    using namespace someperiphial::reg1;
    Blah blah foo, bar, am  // instead of somperiphial::reg1::foo etc.
}
1

Well, obviously uint16_t is the wrong type, and you wanted std::underlying_type<T> in the binary operators. They're rather greedy, after all, and you can't assume all enums are unsigned 16 bits.

One alternative to make them less greedy is to put your own enums and these overloads in their own dedicated namespace. Argument-dependent lookup will then only include these overloads when applied to your enums.

As for the efficiency question, pretty much everything that the compiler does is free at runtime. Even better, because of type aliasing rules, your typesafe C++ constructs are sometimes even more efficient. A uint16_t* can't be pointing to your enums, which reduces aliasing risks.

The problem with

some_peripheral1.reg1 =
       some_peripheral<some_peripheral_base>::reg1_bits::AM |
       some_peripheral<some_peripheral_base>::reg1_bits::BAR;

is that the C++ parse tree is analyzed bottom-up. Roughly speaking, C++ sees A = B | C which are two operator calls, quite possibly overloaded. And in your code they are actual overloads. In particular, it needs to figure out the type of (B|C) before it does overload resolution of =. So it's pretty clear that the type of A cannot reasonably affect B|C, because A isn't even considered at that time.

The argument is weaker for

some_peripheral1.reg1  = some_peripheral<some_peripheral_base>::reg1_bits::AM;
some_peripheral1.reg1 |= some_peripheral<some_peripheral_base>::reg1_bits::BAR;

but even there the types on both sides need to be determined separately before overload resolution is done, and once overload resolution is done it's too late to affect the name lookup.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.