📜 ⬆️ ⬇️

Work with external device registers in C language, part 1

Inspired by the undoubted success of the previous post (no one wrote that the article was uninteresting and was not intended for Habr, this was already a success, and many people read, wrote comments and gave advice on design - more success, by the way, thanks to all), decided to continue sharing their thoughts about programming MK. Today's notes are devoted to general programming issues in the C language, namely, working with bit fields irrespective of specific MCs and programming environments (although examples will be given for a specific CORTEX-M1 and IAR). It seems that the topic is not new, but I would like to show the disadvantages and advantages of different methods. So we start ...

In programming MK in a high-level language, there is a constantly arising task of interacting with registers of external devices (it seems to me that the embedded one is characterized). First of all, in order to organize this interaction, these registers need to be somehow designated by the means of the language used (let's assume that this is C). Any register of VU is characterized by its address and composition, which must be expressed by means of language. Immediately, we note that the C standard does not represent any possibilities for specifying a specific address in the variable memory (at least I don’t know about such), so either you need to use standard extensions or use tricks. Suppose that we need to write to the 32-bit register of an external device located at addresses 0x40000004, value 3. The following small crutch will allow us to do this using means of the language:
*(uint32_t *) (0x40000004)=3; 
Consider this line more closely. Somewhere above (in the stdint.h file) there is a definition
 typedef unsigned int uint32_t; 
which allows us not to think further about the representation of 32-bit numbers in our version C of the compiler. If we have to switch to another version of the compiler, then it will have its own stdint file and we will not have any issues with portability. This practice is very useful, and I can only join the authors, strongly recommending its use in embedded programming.
Now let's sort this line from the right to the left; we create a constant, suggest that the compiler consider it a reference to a 32-bit number and perform a detour, referring to the memory area pointed to by the constant, obtaining the desired result. The resulting design is not very beautiful: firstly, the magic number is used, secondly, some art is striking. Rewrite a little prettier:
 #define IO_DATA_ADRESS 0x40000004 #define WORD(ADR) *(uint32_t *) (ADR) WORD(IO_DATA_ADRESS)=3; 
Everything is almost well here, the only thing that is not healthy is the need to use a macro in the text, so (naturally) we will add another macro:
 #define IO_DATA WORD(IO_DATA_ADRESS) IO_DATA=3; 
Immediately I will answer those who wish to roll these macros into one, especially considering my dislike for wrapping functions - the macros DO NOT STAY ANYTHING during execution. You can put as many macros as possible inside of each other, and at the same time everything will be processed by the compiler and a single constant will fall into the resulting code - the result of the macro convolution. Well, the increase in compile time is so insignificant that you will never notice it. Of course, this circumstance should not be abused (as they say, without fanaticism), but if the use of nested macros makes the code clearer, use them without thinking, otherwise you risk looking into your code in a year or two and frantically trying to understand what is happening in it (and the patch with new features must be submitted to the customer tomorrow).
We got quite a workable code, all implemented with standard language tools, which would seem better? Nevertheless, it is possible and better (well, I like it better) - if we look at the code after the preprocessor, then any call to our register will turn in expanded form into the same ugly string with two asterisks. And here we come to the rescue (no, not Chip and Dale) pointers. Consider the following code
 volatile uint32_t *pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); *pIO_DATA=3; 
Now, when accessing the register, there are no macros at all, everything is expressed by means of the language, the code is absolutely transparent and (in my opinion) more logical. The only thing left is not very necessary asterisk, but more on that later.
So far I will note one drawback of both variants of this implementation - no one and nothing can stop us from writing
 #define IO_DATA_ADRESS 0x40000003 
and get an exception during the execution of the program, as the compiler does NOT check the type conversion and does not interfere with keeping up (this is C, baby, not ADA, believe me). It is possible to reduce the length of the rope with the help of ASSERTs, but, to be honest, they are not always written, not everywhere and in insufficient quantity.
As for the performance of both constructions (those who read my posts, they already understood that this is my point), then on my compiler (IAR C / C ++ Compiler for ARM 6.60.1.5097) the version with the pointer is longer (due to excessive indexing ) that is treated using the following construct
 volatile uint32_t * const pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); 
, after which the results of the compiler become indistinguishable.
 LDR.N R0, DATA_TABLE1 MOVS R1,#3 STR R1,[R0] ... DATA_TABLE: DC32 0x40000004 
By the way, adding the keyword const corresponds to a good programming style, since our pointer is obviously unchanged, and also saves us from offensive (and long-searched) errors like:
 pIO_DATA=&i; 
In this form, our method of working with registers is quite good, and if it were not for the lack of a lack of validation of values, it is almost perfect (as we know, there are no perfect things, but almost). Nevertheless, there is a problem and I will be happy to show how it is solved (an excellent reason to show off knowledge). In extensions of the C language oriented to working with the MC, means are introduced for indicating the absolute values ​​of the address. In my case, this is the @ operator (and the #pragma location directive), which can be demonstrated with the following example
 volatile uint32_t io_data @ IO_DATA_ADRESS; volatile uint32_t * const pIO_DATA = &io_data; i0_data=3; *pIO_DATA=3; 
Here, in this variant, we manage to use the compiler to check the address, and when we try to enter a value not aligned with the word, we get (tadam!) Error message (trifle, but nicely). The effectiveness of this design is the same as the previous one, and if it were not for compiler dependency (an interesting word came out), then it should be recommended for use. And so all the same, reluctantly, choose the option with type conversion and pointer. The reader is invited to write a macro that will implement one or another variant, depending on a certain flag.
Now consider its only drawback, namely, an extra star, and turn the disadvantage into an indisputable advantage (watch your hands). As is known to MK programmers, devices that interact with only one register do not exist are extremely rare in nature. As a rule, there is a whole set of registers for controlling the device and reporting its state, and they are usually located side by side in the address space of the MC. Suppose that our device has a status register at the address 0x40000008, and before recording data, you must make sure that there is a zero in this register. Of course, no one bothers us to define each register separately and work with them as with unrelated objects:
 #define IO_DATA_ADRESS 0x40000004 #define IO_STATUS_ADRESS 0x40000008 ( - #define IO_STATUS_ADRESS IO_DATA_ADRESS +4) volatile uint32_t pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); volatile uint32_t pIO_STATUS = (uint32_t *) (IO_STATUS_ADRESS); while {*pIO_STATUS) {}; *pIO_DATA=3; 
However, there is a more interesting and logical way, namely, to create a structure whose members are separate registers. In this case, we already at the code level understand the connection between the registers, because they are not just gathered together (if the author of the program is not an idiot - but this version will be left for later when the other explanations are folded back), which contributes to understanding the logic of the program. So, what happens:
 #define IO_DATA_ADRESS 0x40000004 typedef struct { uint32_t data; uint32_t status; } IO_DEVICE; volatile IO_DEVICE * const pio_device = (IO_DEVICE *) (IO_DATA_ADRESS); while (pio_device->status==0) {}; pio_device->data=3; 
, and the performance again did not suffer, but even slightly increased, since the compiler keeps the pointer in the register for and for the second command does not load it. The only drawback of this method is that the addresses of registers should really be near, ideally closely followed, although the pass can be arranged by inserting empty fields into the structure. Another drawback is that we fully rely on the compiler to pack our fields into real addresses and must clearly represent the data alignment requirements.
Something happened a lot about addressing, so work with bit fields in part 2, consider if the topic is interesting.

')

Source: https://habr.com/ru/post/220717/


All Articles