19

In the embedded world for ages people wrote hardware(-configuration)-register-mappings as structures, a really simple example for a 32-bit hardware:

#define hw_baseaddr ((uintptr_t) 0x10000000)

struct regs {
     uint32_t reg1;
     uint32_t reg2;
};

#define hw_reg ((volatile struct regs *) hw_baseaddr)

void f(void)
{
    hw_reg->reg1 = 0xdeadcafe;
    hw_reg->reg2 = 0xc0fefe;
}

This works very well, the compiler (gcc at least on our platform) recognizes that the hw_reg is referencing the same address (which is known and constant at compile-time) and is ld'ing it only once. The second st (store) is done with a 4-byte-offset with a single instruction - again on our platform.

How to reproduce this behavior with modern C++ (post C++11) without using #defines?

We tried a lot of things: static const inside and outside classes and constexpr. They both don't like (implicit) reinterprest_cast<>'s .

Responding to a comment as to why changing it: I'm afraid it's mostly fame and glory. But not only. With this C code debugging can be hard. Imagine you'd want to log all write-accesses, this approach would require you to rewrite everything everywhere. However, here I'm not looking for a solution which will simplify a specific situation, I'm looking for inspiration.

EDIT Just to clarify as per some comments: I'm asking this question not to change any code which is working (and was written in the 1990s). I'm looking for a solution for future projects, because I'm not totally happy with the define-implementation, and was asking myself whether modern C++ has a superior possibility.

T.C.
  • 133,968
  • 17
  • 288
  • 421
Patrick B.
  • 11,773
  • 8
  • 58
  • 101
  • To re-frame your question slightly, what is the advantage of achieving this another way? – Ed King Sep 20 '17 at 09:23
  • `volatile struct regs * const hw_reg = ((volatile struct regs *)) hw_baseaddr)` doesn't give the required behavior? – Ajay Brahmakshatriya Sep 20 '17 at 09:29
  • @AjayBrahmakshatriya Typically `hw_reg` would be defined in an include-file, shared between several object-files. `hw_reg` would then be defined multiple times. – Patrick B. Sep 20 '17 at 09:30
  • @PatrickB. Add a static to it. Most compilers would optimize it and not actually allocate memory unless you take address of `hw_reg` – Ajay Brahmakshatriya Sep 20 '17 at 09:33
  • @AjayBrahmakshatriya adding `static` to variables placed in an include-file is discouraged. Among others because of the warning it creates when not used. (hiding the tree in the wood) – Patrick B. Sep 20 '17 at 09:34
  • 1
    @EdKing I answered to your comment inside my question. – Patrick B. Sep 20 '17 at 09:37
  • @PatrickB In that case the only option I can think of is wrap `hw_reg` into a template class and take the address as a template argument. – Ajay Brahmakshatriya Sep 20 '17 at 09:37
  • @PatrickB. A `const` variable implicitly has static linkage, which means each source file including that header will get its own copy local to that file. No problem with multiple definitions. – Angew is no longer proud of SO Sep 20 '17 at 10:02
  • 1
    The real question is _why do you need_ to reproduce this behavior with modern C++? Changing things for the sake of changing? Meta programming for the sake of it? You don't need to use `#defines` even in C, you can use `const uintptr_t` or a pointer type. – Lundin Sep 20 '17 at 12:42
  • they have been doing it for ages, but at their own risk, more of a dumb luck thing. a specific compiler will continue to behave the same way for quite a while, if not forever, depends on design and how often they refactor or re-write. Might take you most of your career to see a failure. – old_timer Sep 20 '17 at 13:15
  • @Lundin: `const` is C has very different semantics. The `#define` way is ideomatic and fine for both languages. So yes, stick with that and use proper macro names. – too honest for this site Sep 20 '17 at 19:22
  • @old_timer: It is implementation defined. For a specific target and ABI, it is (and has to be) well defined. Problem is more when they don't use the correct types for literals and pointers. – too honest for this site Sep 20 '17 at 19:24
  • 2
    "fame and glory" is the worst reason to change a running system/working code. THere is none of both in trashing everything. – too honest for this site Sep 20 '17 at 20:11
  • exactly it is implementation defined...so not something you can use habitually, but something you have to either never use, or use and verify (disassemble) regularly – old_timer Sep 20 '17 at 20:17
  • @old_timer: Hardware peripheral registers are not portable by definition. Whatever STmicroelectronics might claim. And for ARM Cortex-M, etc. , the ABI is well defined. Heck, this is low-level embedded programming. If someone would use that at application level or access hardware without a proper driver in-between on anything C++ makes sense at all, I'd be happy to help you kick Glutei maximi. – too honest for this site Sep 20 '17 at 20:54
  • I'm not looking for how to change existing code. As said in my question, I'm looking for inspiration which might help me to write better things in the future. That's all. – Patrick B. Sep 21 '17 at 06:50
  • I would just stick to the original code, it's simple and everyone knows what it means – M.M Sep 22 '17 at 04:04

2 Answers2

8

I think variable templates make for an elegant solution here.

// Include this in some common header
template <class Impl>
volatile Impl& regs = *reinterpret_cast<volatile Impl*>(Impl::base_address);

template <std::uintptr_t BaseAddress>
struct HardwareAt {
    static const std::uintptr_t base_address = BaseAddress;

    // can't be instantiated
    ~HardwareAt() = delete; 
};

// This goes in a certain HW module's header
struct MyHW : HardwareAt<0x10000000> {
    std::uint32_t in;
    std::uint32_t out;
};

// Example usage
int main()
{
    std::printf("%p\n%p\n", &regs<MyHW>.in, &regs<MyHW>.out);

    // or with alias for backward compatibility:
    auto hw_reg = &regs<MyHW>;
    std::printf("%p\n%p\n", &hw_reg->in, &hw_reg->out);
}

One benefit of using it like this instead of with macros, is that you're type safe, and you can actually refer to registers of different hardware modules from the same source file without mixing it all up.

Yam Marcovic
  • 7,953
  • 1
  • 28
  • 38
  • 3
    Shooting sparrows with cannons. – too honest for this site Sep 20 '17 at 19:20
  • I'd say it is almost artistically elegant :) – zen-cat Sep 20 '17 at 19:58
  • @zen-cat: I'ts hell of templates. I'd like to see an embedded developer rewriting a header with some 1000 registers/defines. But hey, some seem to have spare time anyway. – too honest for this site Sep 20 '17 at 20:29
  • @Olaf Why do you have to rewrite a header? struct MyHW : HardwareAt<0x10000000> { ST_SOME_HW_REGS regs; }; – Alex D Sep 20 '17 at 22:54
  • @Alex: Because you don't want to rewrite the headers provided by the MCU manufacturer which contain typically some 100- some 1000 registers, depending on MCU and vendor. Heck, I wish the profs would teach their CS students at least a bit embedded basics. There is not always black and white! But most of them haven't read a single datasheet/user's guide themselves. – too honest for this site Sep 20 '17 at 23:21
  • @Olaf OP is asking for a different way to do it (he even later specified that it's for future projects) because macros hinder his maintenance ability. If all you're getting from a given vendor is a struct, you can just wrap it, you don't need to rewrite or even modify anything. If you're getting a bunch of macros, then note again the OP is asking specifically about how to avoid that, and therefore saying he simply shouldn't because that's not your own personal preference-- while it might be based on valid points--isn't answering his question. – Yam Marcovic Sep 21 '17 at 11:12
  • @YamMarcovic: He is asking "How to reproduce this behavior with modern C++ (post C++11) without using #defines?" - Which is nonsense. 1) You cannot reproduce the behaviour of macros without them - that's the reason they exist. 2) this standard usage is completely valid in "modern C++" as well, it is well defined (read the standard). 3) No sane developer will rewrite all vendoer headers, read my comments **completely** for more. 4) That's a typical C++ theorists' questions without any practical usage. – too honest for this site Sep 21 '17 at 11:19
  • Finally: there is more than black & white people are taught by those OOP/template fetishists who never wrote even a piece of production embedded code. Form shall **follow** function, not the opposite! – too honest for this site Sep 21 '17 at 11:19
  • 2
    @Olaf Moreover this answer evokes undefined behavior, as was the code in the question. Writing correct code as inside my answer is even simpler than here. I don't understand poeple who prefers UB and overdesign while good coding is simpler and produces the same exact assembly. – Oliv Sep 21 '17 at 13:39
  • @Olaf Exactly! MCU manuf gives you a header, in some cases it's generated by IDE's like Libero or whatever for a HW revision. The header has a structure defined with all the registers. Just include it as a first member of MyHW structure, make sure the packing is 1 and voila! Why the heck do you have to rewrite the header? I still don't get it! Heck, you can even use it as a nameless member (C99) and you won't have to deref an extra name while accessing a register! – Alex D Sep 21 '17 at 23:30
  • @Alex: It will not give you a single struct. For non-CMSIS MCUs, there will be typically no `struct`, but a bunch of defines. For CMSIS, there still is a bunch of defines, `struct`s, etc. None of them uses the stuff above. Just please read actual headers: AVR, ST, NXP (+ Freescale), TI, Renesas, Microchip(+Atmel). All have 10+ **different** CPU families. Welcome to the real world! – too honest for this site Sep 21 '17 at 23:43
  • @Olaf He's not asking how to reproduce the behavior of *macros*, but okay... And he's asking for a non-macro replacement that would replace his macro that actually refers to a *specific struct*. As to UB, if this is UB then so is any malloc(). But at this point it looks like your whole goal is to win the argument regardless of the details, so good luck with that... – Yam Marcovic Sep 22 '17 at 06:31
  • This is not about winning, but good practice and reasonable software development. Try a production project, you might be surprised. (and it was not me to bring up UB - reading competence?). – too honest for this site Sep 22 '17 at 12:20
  • 1
    A valid answer could well be that there is no replacement solution in modern C++ untel now. Stick with defines! @Olaf, for real, do not hesitate to post such an answer. – Patrick B. Sep 22 '17 at 12:56
  • @PatrickB.: Not for the C++ tag. There are way too many academic "programmers" underway thinking "the C++ way" is the new sliced bread. I prefer the proverb: "There are two kinds of j**rks: One who thinks 'this is old, this is good' and the other thinking 'this is new, this is better'." Worked fine for me for >30 years. – too honest for this site Sep 22 '17 at 13:47
4

Since the sole purpose of the #define is to give you access to struct members, you could use a template to do the equivalent. My compiler generates code for the template that is identical to the #define.

// #define hw_reg ((volatile struct regs *) hw_baseaddr)

template <class T, uintptr_t addr>
class RegsPtr
{
public:
  RegsPtr() { ; }
  volatile T* operator->() const { return reinterpret_cast<T*>(addr); }
  volatile T& operator*() const { return *operator->(); }
};

const RegsPtr<struct regs, hw_baseaddr> hw_reg;
D Krueger
  • 2,446
  • 15
  • 12