use structs for hw-registers

Frank B

Senior Member
Hi,

it is a little late for the existing libraries, but it might be worth considering for future-developments:

On ARM it is more efficient to use structs instead of constants to acces registers, as soon you access more than one in a function.

For example, to access the Hardware-Serial, you have several direct reads and writes to the hardware. GCC needs to load the adress of each register indepently.
More efficient (faster - and less code) is to use structs. This way, GCC need the adress of the struct only, and can use the offsets/displacements for subsequent accesses.

A short example for GPIO:
Code:
struct sGPIO {
  uint32_t PDOR; //Port Data Output Register
  uint32_t PSOR; //Port Set Output Register
  uint32_t PCOR; //Port Clear Output Register
  uint32_t PTOR; //Port Toggle Output Register
  uint32_t PDIR; //Port Data Input Register
  uint32_t PDDR; //Port Data Direction Register
};

volatile struct sGPIO * const GPIO_D = (struct sGPIO *)0x400FF0C0;

Now, an registeraccess looks like :
GPIO_D->PDDR = 1;
GPIO_D->PDOR = 1;

This way, it loads the 32-Bit address only one time (and uses the offset to 0x400FF0C0), instead of loading it twice.

In some cases, this can lead to huge speed-improvements, too.
 
Last edited:
I am learning C++ at the moment, and I am no au fait with hw registers (I would like to learn though).

Can I ask you some basic questions? Your code creates a pointer to a GPIO struct called GPIO_D. You use 'pointer access' syntax to access the struct members ... GPIO_D->PDOR = 1;

Your code uses syntax i am not entirely familiar with .... = (struct sGPIO *)0x400FF0C0; .... I take this is a type cast of the HEX number to a GPIO pointer type, and then an assignment of that to the GPIO pointer GPIO_D?

This is where I am asking for help .... the GPIO pointer GPIO_D has a base memory location of 0x400FF0C0?? so when using pointer access syntax, what actually happens? ...

GPIO_D->PDDR = 1;

Does this 'add' 20 BYTES to 0x400FF0C0 (PDDR is 5 +1 on the list, and each member is 4 bytes long) and then assign '1' to the 32bit wide memory location?? In other words, because each member of GPIO is 4 bytes long, you are effectively poking a 1 into a known byte a certain distance from 0x400FF0C0?

Am I close??!!?

.... what exactly is 'PDDR'? just a 'spacer' / label?... What exactly is an offset? a memory location, I guess, but a very specific one?

Thanking you in advance. I don't quite get the C struct syntax, i guess...and the offset / PDOR ...PDDR language is a bit of a mystery
 
Yes, adds 20 bytes. A struct is (like) an array of items that may or not be the same size - in this case they are. The compiler understands the layout of the struct where each element exists for a reference from the single point, with the needed byte offset to get to that element of whatever type it was declared from the 'pointer' assigned to the struct when it was declared.

Structs are really cool - and Frank makes the point that given a known ADDRESS [ GPIO_D = (struct sGPIO *)0x400FF0C0 ] any contiguous elements can be accessed from that single address with just an offset rather than a full address. So once a processor register is holding the base address - it won't have to change that address - but merely load the offset from that address to refer to subsequent values in the same 'space'.

In the case of GPIO_D it is some specific 'register' space in the processor defined for the Teensy that allows hardware control for that area of the address space and whatever it relates to.

Adrian - if you BING/google search for "C struct" in some fashion you should find a set of examples or tutorials that will give more detail. Then you can learn about UNION - a way to selectively remap dissimilar items in the same struct.
 
Most of the professional compilers (IAR, Keil) come with a set of header files for the on-chip peripherals/ports for the hundreds of MCUs they support. These typically use bitfields and structs to define every bit in every device.
#define usage is minimal.
 
thanks ....I had had a quick look at C structs, and wondered how they worked in Frank's code (I understand classes in C++ a lot better, and they are similar) I will look up union ... absolutely no idea what that is (apart from your brief description!)

Anyway, thanks for your time, defragster, and stevech.

Defragster, I am rewriting the voltmeter example from Talkie to convert a numerical value into an array of 'words' to spool through your sayQ function ... the current approach is elegant but doesn't output an array....I think I have cracked it, so will test over the next day or two.

Stevech, I guess you are saying that PDOR ...PDDR would be 'predefined' under some compilers, but that they are merely labels under Frank's example, and indeed he is creating the 'definition' for them! Is that right?
 
I expect that you can also use a class and then "placement new" to create it. Just don't define any methods.
 
Like this??

Code:
class sGPIO {
public:
  uint32_t PDOR; //Port Data Output Register
  uint32_t PSOR; //Port Set Output Register
  uint32_t PCOR; //Port Clear Output Register
  uint32_t PTOR; //Port Toggle Output Register
  uint32_t PDIR; //Port Data Input Register
  uint32_t PDDR; //Port Data Direction Register
};

volatile sGPIO * const GPIO_D = new ((uint32_t *)0x400FF0C0) sGPIO();

is the pointer 0x400FF0C0 size right? When I read about placement new there were all sorts of warnings about pointer sizes , memory locations and things....the teensy is a '32 bit machine'??/ Is that what this means??? size of memory chunk/register?
 
Yes Adrian - I suppose a class is a struct of function and data pointers. You've learned to run before you could walk it seems. A struct is it's own powerful construct, part of the original C - like an array - only formalized to allow the compiler to resolve named reference/offset points from a single given address in memory - which is what Frank is declaring in the OP.

Pushing it to a class might work like that and help you see it - but struct is a fundamental building block of C - and destroys the use of the code in a "C" way.

There is great effort to avoid that overhead and the kraap that can come with it throughout the system using things like this in Kinetis.h where the "system" is defined:
Code:
#ifdef __cplusplus
extern "C" {
#endif
 
Legless is a better description of my C/C++. Thankyouse for taking the time to teach me things when I should have been working ...;) Back to the grindstone...
 
Yes Adrian - I suppose a class is a struct of function and data pointers. You've learned to run before you could walk it seems. A struct is it's own powerful construct, part of the original C - like an array - only formalized to allow the compiler to resolve named reference/offset points from a single given address in memory - which is what Frank is declaring in the OP.

Pushing it to a class might work like that and help you see it - but struct is a fundamental building block of C - and destroys the use of the code in a "C" way.

There is great effort to avoid that overhead and the kraap that can come with it throughout the system using things like this in Kinetis.h where the "system" is defined:
Code:
#ifdef __cplusplus
extern "C" {
#endif

Using extern "C" just prevents the compiler from renaming the external name to include the return type and argument types in the actual name passed to the linker. You are still programming in C++, just the interface names are C compatible.
 
Thanks MM - as I said C++ kraap - that keeps the C function names from being mangled to C++ standards. It keeps the functions C style accessible. In this Kinetis.h case it includes the isr functions, where everything outside that is impervious to the fun that is C++, just #define's and struct's and enum's.
 
The IAR and KEIL both use a scheme like this:

#defines for all the I/O registers and I/O bits - just to give them unchanging names but different values per MCU within a family.

struct typedefs for all the I/O config and state parameters.

All plain C so that these system definitions will compile with a program that is all C or a program that is a mix of C and C++.
The device drivers are commonly all C.
The typedef'd structs are simply declared at compile time - e.g., for an MCU with 4 USARTS, there'd be say USART1_config, USART2_config, USART3_config, USART4_config. Stuct would be initialized with run-time code for, say, baud, char format, GPIO pins mapped, etc. Anytime later, the array contents are altered, say, to change baud rates. Then the one driver is called, with the address of the struct for one of the UARTS - to change the baud rate. The structs also contain the DMA channel numbers, and pointers to callbacks, where the default callback is __weak void nothing() and a user can override with his own callback.

Rather than creating a C++ class instance for each (uses more RAM, hard to share class instance between ISR and non-ISR), etc.

I have code to recalculate the UART and SPI dividers whenever someone changes the CPU frequency (PLL speed, HSI/HSE choice). So within limits, the UARTS and SPI follow along. This is used to speed up for compute-intensive code then slow down for normal use, but some UARTs and the SPIs work the same at both speeds.

I find C++ in drivers and I/O to be more than a PITA. It makes sense in some other use cases, though I use very little C++ due to the obtuse code that it can lead to if one does not comment well about tricky things during inheritance and virtual methods. But mostly, its the dreaded use of free() and C++ object deletes (OMG in the String class), that can make an embedded system with small RAM unreliable. Of course, a well experienced and careful C++ embedded engineer can do the right thing.
 
Last edited:
You can see in kinetis.h that I started (quite some time ago) converting some of the hardware registers to using this faster approach.

One of the earliest I did was SPI, and initially I made the mistake of calling it "SPI0" (which causes all sorts of conflicts). As far as I know, ST7735 and FastLED were the only 2 libs to use this struct, rather than just the original register names. But even now, old copies of those 2 libs still turn up every now and then.

Eventually I want to convert all of kinetis.h. Contributions are welcome. But anyone contributing needs to understand this is a low priority, slow-track feature, and I've been burned before by my own poor decision to use the short name SPI0. The names need to be carefully chosen. Please understand the review time for these sorts of contributions might be weeks or even many months, so be patient and expect a long, paranoid path before anything gets merged. We need to be extremely careful to avoid more problems like my little SPI0 debacle! Almost all these structs should be typedefs beginning with "KINETIS_" or "KINETISL_" or "KINETISK_" and ending with "_t".
 
Back
Top