6

While working on an embedded systems project using an Atmel SAM3X8E, I noticed the following bit of code in some of the CMSIS header files.

#ifndef __cplusplus
typedef volatile const uint32_t RoReg; /**< Read only 32-bit register (volatile const unsigned int) */
#else
typedef volatile       uint32_t RoReg; /**< Read only 32-bit register (volatile const unsigned int) */
#endif

Why does the typedef for C++ not include const? I saw somewhere a mention that C++ does not store integer const variables in runtime memory, which if true would mean the const would need to be removed because of how microcontroller registers are memory-mapped, but I can't seem to find anything else saying that C++ does that (though my search was admittedly pretty brief). Not having much experience with C++, I also thought it might be that C++ doesn't allow const struct members, as those typedefs are mostly used in struct typedefs for collections of registers, but that doesn't seem to be the case either.

JAB
  • 20,783
  • 6
  • 71
  • 80
  • 3
    ... and why did the original author feel that "if not/else" was a good way to structure the logic? Aaargh. – unwind Mar 14 '13 at 19:42
  • @unwind Many people advise always putting the positive part first. The author of that code considered `#ifndef __cplusplus` positive. – Daniel Fischer Mar 14 '13 at 20:13
  • Personally, there are several ways I use to decide which block of code to put first; I generally base it on a mix of which version of the conditional looks better/makes more sense in context, which one is more likely to occur if that can be estimated, and which block of code is larger (if statements with small main blocks and big else blocks tend to look unbalanced to me). – JAB Mar 14 '13 at 22:35
  • `Reputation += LARGE_CONST; /* Ask some bike shed language question, that every idiot (including me) will have an opinion on. */` However, perhaps this link is actually helpful. http://www.embedded.com/electronics-blogs/programming-pointers/4025609/Place-volatile-accurately Why is this question tagged with ARM? – artless noise Mar 15 '13 at 01:32
  • @BillPringlemeir Because the header file the code snippet was encountered in is part of a library for an ARM processor, specifically CMSIS (hence the CMSIS tag). I figured that was enough reason to add those tags. – JAB Mar 15 '13 at 13:52
  • @BillPringlemeir: Not all compilers are created equal, it's possible that his ARM compiler could have some unusual behavior that explained the issue. – Ben Voigt Mar 15 '13 at 14:14
  • @BenVoigt: Exactly. If I were working with multiple different embedded systems (ARM, non-ARM, etc.) and encountered the code snippet in my question in various places, then I would have left out [tag:arm] and [tag:cmsis], but as it was encountered in this specific context, I specified that context in the tags. – JAB Mar 15 '13 at 14:15
  • @JAB: Although in that case, specifying which compiler would be more useful than which CPU architecture. – Ben Voigt Mar 15 '13 at 14:16
  • @BenVoigt: True enough. I'm not actually sure what compiler it is, though, since I'm using the Arduino "IDE" to take care of most of that. Looking through the directory structure, I think it might be "Sourcery C++ Lite"? – JAB Mar 15 '13 at 14:23
  • @JAB: Arduino uses AVR, not ARM. Perhaps you are using an Armuino environment? – Ben Voigt Mar 15 '13 at 14:24
  • @BenVoigt: I'm working with the Arduino Due, actually, which has an ARM processor (the one mentioned in my question). My question isn't really related to the Arduino library, though, just the underlying abstraction layer (or whatever it would be called). – JAB Mar 15 '13 at 14:29

3 Answers3

5

If you declare with const, C++ standard will obligate you to initialize the contents of the variable. In the case of micro-controller register, you do not want to do that.

Felipe Lavratti
  • 2,887
  • 16
  • 34
  • 1
    That looks like a quote - where is it from? – us2012 Mar 14 '13 at 17:08
  • 5
    While this may well be part of the answer, in the situation raised here however, I doubt the compiler is ever tasked with "reserving space" since the register in question is likely to a fixed-location hardware feature of the processor/on chip peripherals. Likely the typedef is only every used to declare a pointer to such a register. – Chris Stratton Mar 14 '13 at 17:17
  • I'd also agree that typdef'ing read only memory types that are used with specific hardware addresses as `volatile const` is perfectly OK in C++. The question itself doesn't show any global constant definition at all, so I don't see the relevance of your answer ... – πάντα ῥεῖ Mar 14 '13 at 17:35
  • 1
    https://github.com/arduino/Arduino/blob/ide-1.5.x/hardware/arduino/sam/system/CMSIS/Device/ATMEL/sam3xa/include/component/component_spi.h and https://github.com/arduino/Arduino/blob/ide-1.5.x/hardware/arduino/sam/system/CMSIS/Device/ATMEL/sam3xa/include/instance/instance_spi0.h for examples, struct member and pointer respectively. – Chris Stratton Mar 14 '13 at 17:47
  • 2
    Of course, the reason given by @fanl may _still_ be the exact reason, even if it does not apply because merely a pointer-to-type is ever used. It's quite possible that the author of that header had that exact reasoning (whether it applies in practice or not). It is a plausible reasoning, at least. The fact that the compiler could still work it out doesn't necessarily mean that a human won't get it wrong. – Damon Mar 14 '13 at 18:19
  • @ChrisStratton For fixed-address registers, some use the linker to assign the addresses to objects rather than using pointer definitions in the code. It enhances debugging because the registers then have symbol associations. That could be the situation here. – D Krueger Mar 14 '13 at 18:42
  • 2
    @DKrueger - plausible perhaps in some other case, but CMSIS defines the addresses explicitly in header files. – Chris Stratton Mar 14 '13 at 18:46
  • And continuing on ChrisStratton's SPI example, here's where the struct typedef is used: https://github.com/arduino/Arduino/blob/ide-1.5.x/hardware/arduino/sam/system/CMSIS/Device/ATMEL/sam3xa/include/sam3x8e.h#L458 – JAB Mar 14 '13 at 19:09
  • 1
    Another good point, but dropping the `const` is still not the best solution. – Ben Voigt Mar 14 '13 at 19:50
  • The cases where the register addresses are assigned to variables all seem to be within struct assignments/class constructors, and #defines are used for the bare addresses themselves, so the required-initializer-for-consts doesn't seem to really matter here. As far as I can tell the codebase never declares a variable of that `const` type so no initialization will be forced. – JAB Mar 14 '13 at 23:02
3

Because no RoReg object is ever instantiated, there is no good reason to omit the const qualifier in the typedef.

Every use of RoReg is in either a macro that defines a pointer to the type...

#define REG_WDT_SR (*(RoReg*)0x400E1A58U) /**< \brief (WDT) Status Register */

...or a struct declaration that is accessed using a similar macro.

typedef struct {
  WoReg WDT_CR; /**< \brief (Wdt Offset: 0x00) Control Register */
  RwReg WDT_MR; /**< \brief (Wdt Offset: 0x04) Mode Register */
  RoReg WDT_SR; /**< \brief (Wdt Offset: 0x08) Status Register */
} Wdt;

#define WDT        ((Wdt    *)0x400E1A50U) /**< \brief (WDT) Base Address */

Even with the const qualifier, the code should behave the same in both C and C++.

Perhaps the author misinterpreted the standard. To guarantee that a C++ struct has the same layout as in C, it requires that the class "has the same access control (Clause 11) for all non-static data members." The author may have mistaken const and volatile for access control specifiers. If they were, then you would want all the struct members to have the same cv-qualifiers in order to ensure compatibility between the C and C++ (and hardware) layouts. But it's public, protected, and private that define access control.

D Krueger
  • 2,446
  • 15
  • 12
  • This and Ben Voigt's answer are both good ones, so it was a little hard to choose, but I ended up going with this one as I feel it more strictly applies to the specific question asked and thus works better as the "official" one, so to speak. – JAB Mar 19 '13 at 14:41
2

As mentioned by @fanl, const does indeed change the default linkage of globals in C++, and does prevent defining a variable without initialization.

But there are better ways to get external linkage than removing const. The usage of reserved arrays in the header file Chris linked is also very fragile. I would say this code leaves a lot of room for improvement -- don't emulate it.

And furthermore these variables don't get defined (that would cause the compiler and linker to select an address), they are always accessed via pointers, with the address fixed according to the memory map.

For headers intended purely for use by C++, this is how I do it (memory map matching a TI Stellaris chip).

Looks complicated, but the optimizing compiler reduces it down to a single instruction per access. And the address offsets are coded in, not dependent on the order and padding of fields inside a structure, so it's much less fragile and easier to verify against the datasheet.

template<uintptr_t extent>
struct memory_mapped_peripheral
{
    char data[extent];
    volatile       uint32_t* offset( uintptr_t off )       { return reinterpret_cast<volatile       uint32_t*>(data+off); }
    volatile const uint32_t* offset( uintptr_t off ) const { return reinterpret_cast<volatile const uint32_t*>(data+off); }
};

struct LM3S_SYSTICK : private memory_mapped_peripheral<0x1000>
{
    volatile       uint32_t& CTRL   (void)             { return offset(0x010)[0]; }
    volatile       uint32_t& RELOAD (void)             { return offset(0x014)[0]; }
    volatile       uint32_t& CURRENT(void)             { return offset(0x018)[0]; }
}* const SYSTICK = reinterpret_cast<LM3S_SYSTICK*>(0xE000E000);

struct LM3S_NVIC : private memory_mapped_peripheral<0x1000>
{
    volatile       uint32_t& EN    (uintptr_t i)       { return offset(0x100)[i]; }
    volatile       uint32_t& DIS   (uintptr_t i)       { return offset(0x180)[i]; }
    volatile       uint32_t& PEND  (uintptr_t i)       { return offset(0x200)[i]; }
    volatile       uint32_t& UNPEND(uintptr_t i)       { return offset(0x280)[i]; }
    volatile const uint32_t& ACTIVE(uintptr_t i) const { return offset(0x300)[i]; }
    volatile       uint32_t& PRI   (uintptr_t i)       { return offset(0x400)[i]; }
    volatile       uint32_t& SWTRIG(void)              { return offset(0xF00)[0]; }
}* const NVIC = reinterpret_cast<LM3S_NVIC*>(0xE000E000);
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • The full code is here: http://www.coocox.org/repo/bf7c3c91-96ed-11df-80ae-001d7d723e56/src/cmsis/core_cm3.h.htm What would be the proper way of doing it ? – Felipe Lavratti Mar 14 '13 at 19:55
  • @fanl: Really, you can just put the `const` back in. Maintainability of that code isn't your problem. – Ben Voigt Mar 14 '13 at 20:02
  • @fanl: But I added my approach just for reference. – Ben Voigt Mar 14 '13 at 20:08
  • 1
    I find interesting your approach. Is there a way to avoid data allocation in the `memory_mapped_peripheral` struct? – Felipe Lavratti Mar 14 '13 at 20:18
  • 1
    A suggestion that such a roundabout scheme is best would seem to contribute a lot of ammunition to the on-again/off-again argument about the suitability of C++ for hardware-level embedded work. It would seem a lot cleaner to just suggest doing all the I/O manipulation in C, and save C++ for your higher level tasks (if you feel compelled to use it). – Chris Stratton Mar 14 '13 at 21:06
  • 1
    "[T]he address offsets are coded in, not dependent on the order and padding of fields inside a structure[.]" The padding issue makes sense, but for a system where the register size is known and all registers in a group are contiguous, it seems to me that using a more conventional struct would be clearer/easier to look through for a code-writer. Your version may be more flexible, but it's also slightly more obtuse as you could easily change around the order of offsets while keeping the order of variables, which could cause confusion for someone referencing the structs directly. – JAB Mar 14 '13 at 23:22
  • Rather, order of registers. – JAB Mar 14 '13 at 23:33
  • @fanl: There is no allocation, because no object of this type is allocated. Instead, it's used via pointer. (C++ allows an object with trivial constructor to begin its lifetime merely by possessing a memory area of sufficient size and alignment.) – Ben Voigt Mar 15 '13 at 00:58
  • @JAB: One particular place the flexibility comes in handy is when the mapping contains numbered sets of registers which are non-contiguous (Yes, the Stellaris does this, although the snippet I extracted doesn't have an example). Another thing is that different chips in the family have different registers. The offsets never change... but surrounding fields in a struct with `#if` will tend to throw off subsequent fields. My way, you can conditionally enable registers without any side effects. – Ben Voigt Mar 15 '13 at 01:01
  • @BenVoigt That does sound quite useful. And now that I think about it, it probably wouldn't be too hard to put in a macro or two to make your setup seem less boilerplatey (something like `#define RoReg(type, name, off) volatile const type & name (void) const {return offset(off)[0];}` and `#define RoRegArray(type, name, off) volatile const type & name (uintptr_t i) const {return offset(off)[i];}`), and then maybe one or two more to make the structs a little less mucky. – JAB Mar 15 '13 at 14:08
  • @JAB: Yes, pretty much (but drop the type argument, it's always `uint32_t`). And when the array is non-contiguous you won't be able to use the macros. – Ben Voigt Mar 15 '13 at 14:11
  • @BenVoigt: How would a non-contiguous array work in the context you've given? Would you not need a class that overrode the [] operator anyway, to have it work the same way as contiguous array access? – JAB Mar 15 '13 at 14:27
  • (Also, what if it's an environment that supports multiple registers to a word? Then you could have `uint16_t` and `uint8_t` registers. And then there could be systems with `uint64_t` registers, perhaps.) – JAB Mar 15 '13 at 14:35
  • @JAB: Just calculate the offset based on `i`, instead of using subscripting. And while it's possible that systems exist with different register sizes, ARM doesn't have them (many of the 32-bit registers have reserved or unimplemented bits, but a 32-bit read or write is still used) – Ben Voigt Mar 15 '13 at 14:57