Reputation: 3637
In embedded system firmware, it's common to represent multiple memory-mapped registers of a peripheral using a struct, under the assumption that compiler's code generation will follow a particular memory layout on a given platform. For example, the following code is standard:
typedef struct
{
volatile uint32_t CRL; /* bytes 0 to 4 */
volatile uint32_t CRH; /* bytes 4 to 8 */
volatile uint32_t IDR; /* bytes 8 to 12 */
volatile uint32_t ODR; /* bytes 12 to 16 */
} GPIO_t;
#define GPIOA ((GPIO_t *) 0xDEADBEEF)
#define GPIOB ((GPIO_t *) 0xDEADCODE)
void loop(void)
{
GPIOB->CRL = 0x01;
GPIOA->CRH = 0x01;
}
The justifications of this method are:
It creates a simulated namespace in plain C code. Different GPIO ports can have the same registers, but they're clearly distinguished without using ugly prefixes, and the peripheral structs can be directly passed around as a whole.
Repeated register layouts only need to be defined once.
Although the C standard has few requirements (if any) on the exact memory layout of structs, they're usually predictable on a given platform. Since all registers are MCU-specific anyway, portability is unimportant.
Thus, it's my impression that this technique is common.
To represent fields within a physical MMIO register, using bit fields in a struct is a closely related technique that can achieve similar results. For example, the bit fields in the CRH
register may be represented using the following struct:
typedef struct
{
/*
* 00: Analog Input, 01: Floating Input
* 10: Pull-up/down, 11: Reserved
*/
volatile unsigned int CTL7 : 2;
/* 00: Pull Pull, 01: Open Drain */
volatile unsigned int MODE7 : 2;
volatile unsigned int CTL6 : 2;
volatile unsigned int MODE6 : 2;
volatile unsigned int CTL5 : 2;
volatile unsigned int MODE5 : 2;
volatile unsigned int CTL4 : 2;
volatile unsigned int MODE4 : 2;
volatile unsigned int CTL3 : 2;
volatile unsigned int MODE3 : 2;
volatile unsigned int CTL2 : 2;
volatile unsigned int MODE2 : 2;
volatile unsigned int CTL1 : 2;
volatile unsigned int MODE1 : 2;
volatile unsigned int CTL0 : 2;
volatile unsigned int MODE0 : 2;
} CRH_t;
In low-level embedded firmware, is it considered acceptable to represent multiple MMIO registers using a struct?
In low-level embedded firmware, is it considered acceptable to represent multiple options within the same register using bit fields in structs?
Both techniques are related, and both rely on potentially unportable assumptions. But it appears to me that the answer to the first question is often "YES", but the answer to the second question is often "NO" - or at least with considerable disagreements. For example, in question Is it ok to use bit-fields in embedded system firmware?, many objections can be found:
Lundin: It's naive to think you'll never have to port the program. Quite a few programs will have to get ported to a different compiler at some point, even if the hardware stays the same. Especially true for embedded systems. In addition, it is quite nice to be able to re-use already written code in new projects. I can mention at least 10 more [compilers]. And then different hardware ports of all those compilers. They do not implement padding, endianess or even bit order the same. They are all quite different with how they handle bit padding when you mix different types in the same struct.
Another aspect: I've written plenty of simulators and test cases for my MCU programs on a 64 bit PC. For example at one point I designed a radio spectrum allocation algorithm and needed a graphical simulator to visualize and debug the algorithm. The target platform was a 8 bit MCU but I ran all tests on 64 bit x86. To do the same by hacking together some LCD graphics library + fixing hardware for it would have been way more time consuming than writing portable code and then hack together a simple Windows app in a RAD tool.
Why is it considered okay to use structs to represent MMIO registers in embedded system firmware, but not bit fields?
Upvotes: 1
Views: 170
Reputation: 347
bit fields representation is different from one machine to another, especially regarding endianness (the order in which bytes are stored), some machines write numerals with the most significant digits on the left, so we might consider bit 31 to be the leftmost and bit 0 to be the rightmost, but when you use structs to represent MMIO registers in embedded systems, the compiler ensures that the structure's fields are arranged correctly according to the target machine's architecture.
for example:
let's suppose we have this struct:
struct Data {
int number1; //(4 bytes)
int number2; //(4 bytes)
};
and we have two machines first one is Big Endian second one is Little Endian
sending a struct:-
1- Sender Machine (Big Endian):
Data dataToSend = {123, 456};
2- Receiver Machine (Little Endian):
Data dataReceived = {123, 456};
sending a bits:-
1- Sender Machine (Big Endian):
sends (00 00 00 7B 00 00 01 C8)
number1 (4 bytes): 00 00 00 7B (hexadecimal for 123)
number2 (4 bytes): 00 00 01 C8 (hexadecimal for 456)
1- Receiver Machine (Little Endian):
received (00 00 00 7B 00 00 01 C8)
number1 (4 bytes): 00 00 00 7B (hexadecimal for 2063597568)
number2 (4 bytes): 00 00 01 C8 (hexadecimal for 3355508736)
note:
if you want the receiver to get the bits correctly it should be sent like this:
7B 00 00 00 C8 01 00 00
Also, note that if both machines are big-endian or both are little-endian, then bits sending is valid. or if you perform a transformation from big-endian to little-endian when you receive the data with adding an extra parameter indicating your machine type, it can also work. or send a struct and let the compiler do the work.
Upvotes: 0