*(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. #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). 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. #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. 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. #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 #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.Source: https://habr.com/ru/post/220717/
All Articles