Working with external device registers in C, part 3

  • Tutorial
All is well that ends well

Now that we have examined how, using the means of the C language, we can determine the fixed location of the register in the address space of the MK (part 1), how we can determine the individual bit groups in the register (part 2), it's time to consider how we can with these groups work. Working with a group of bits as a whole does not present any problems, it is based on their description in the form of bit fields and has already been demonstrated, however, we may also need to work with individual bits of the field, and for reasons of efficiency or comprehensibility of the program, it is not advisable to divide the group into separate fields .
Suppose we need to separately manipulate the high-order bit of the command field from our example. The first thing that comes to mind is union, however unions cannot have a bit length. There is an option to create two versions of the register description and combine them already, and it works:
#pragma bitfields=reversed
typedef struct {
  unsigned :1;
  unsigned int code:3;
  unsigned :26;
  const unsigned flag1:1;
  unsigned flag:1;
} tIO_STATUS;
typedef struct {
  unsigned :1;
  unsigned int start:1;
  unsigned :30;
} tIO_STATUSA;
#pragma bitfields=default 
typedef union {
  tIO_STATUS;
  tIO_STATUSA;
} tIO_STATUS2;  
#define IO_ADR 0x20000004
volatile tIO_STATUS2 * const pio_device = (tIO_STATUS2 *) (IO_ADR);
  pio_device->code = 3;
  while (pio_device->flag) {};
  pio_device->start=1;
, but creating two additional types is somewhat redundant (in my opinion).
An alternative for manipulating individual bits of a group are all the same bit masks, and we come to costructures of the type:
#define BITNUM 2 // биты в группе нумеруются с 0
#define BITMASK (1<code |= (1<< BITNUM); // устанавливаем бит
  pio_device->code &= ~BITMASK;; // сбрасываем бит
Note that the compiler does not check the validity of the value for such operations (as opposed to assigning a constant). Also note that the bit number is applied, from which a bit mask is created by shifting. I do this because it’s more difficult to make a mistake in dialing than in bitmask (0x40000000), and I still have to take it into my mind, but there is no difference in the code (but this, of course, is a matter of taste). And now a really serious remark - all authors of articles on embedded programming (including myself) strongly DO NOT recommend using such constructions in the text, but define macros for setting and resetting bit fields
#define SETBIT(DEST,MASK) (DEST) |= (MASK)
#define CLRBIT(DEST,MASK) (DEST) &= ~(MASK)
continue to use only them.
 SETBIT(pio_device->code,1 << BITNUM);
 CLRBIT(pio_device->code,BITMASK);
Firstly, you will not make an offensive mistake by forgetting in the second case to put a bitwise negation (~) or instead of putting a logical negation (!) (Those who have never made such a mistake are very attentive people, unfortunately, to them I do not belong). Secondly, by switching to a bitwise addressable MK, you can redefine this macro (only for single bits) taking into account the hardware capabilities and get a significantly faster code. Thirdly, if (when) you have to turn these operations into atomic, it is much easier to do this in defining a macro than chasing them throughout the program.

Here about the last aspect, it makes sense to talk in more detail. As you know, the need for atomic operations arises when there are more than one processes competing for access to a resource. So, to access the registers of external devices, even in the case of a single process (the main While loop), there is implicit competition from the interrupt service routines. Therefore, when accessing the VU registers, the read-modify-write sequence poses a threat from the point of view of ensuring the continuity of actions. The fact is that the mask set / reset bitmap operations on the mask contained in the command set can NOT operate directly on the address space cells and, accordingly, cannot ensure atomic change of the register of the slave. This is not to say that MK developers do not understand the shortcomings of this approach, but there is still no direct solution to the problem, which obviously indicates the presence of deep internal aspects that impede it. Various approaches to the problem are known. The presence of two registers in the address space, writing a unit to one of them sets the value bit, writing a unit to the other resets the value bit. The presence of a bit-banding mechanism, when each bit corresponds to a separate value in the address space (naturally, in addition to the usual mechanism for accessing the entire register as a whole). Well, and the widespread mechanism of disabling pervany before the operation with permission at its end. Maybe I'm not in the know, but for MK there are still no good hardware atomic operations. Various approaches to the problem are known. The presence of two registers in the address space, writing a unit to one of them sets the value bit, writing a unit to the other resets the value bit. The presence of a bit-banding mechanism, when each bit corresponds to a separate value in the address space (naturally, in addition to the usual mechanism for accessing the entire register as a whole). Well, and the widespread mechanism of disabling pervany before the operation with permission at its end. Maybe I'm not in the know, but for MK there are still no good hardware atomic operations. Various approaches to the problem are known. The presence of two registers in the address space, writing a unit to one of them sets the value bit, writing a unit to the other resets the value bit. The presence of a bit-banding mechanism, when each bit corresponds to a separate value in the address space (naturally, in addition to the usual mechanism for accessing the entire register as a whole). Well, and the widespread mechanism of disabling pervany before the operation with permission at its end. Maybe I'm not in the know, but for MK there are still no good hardware atomic operations. when each bit corresponds to a separate value in the address space (naturally, in addition to the usual mechanism of access to the entire register as a whole). Well, and the widespread mechanism of disabling pervany before the operation with permission at its end. Maybe I'm not in the know, but for MK there are still no good hardware atomic operations. when each bit corresponds to a separate value in the address space (naturally, in addition to the usual mechanism of access to the entire register as a whole). Well, and the widespread mechanism of disabling pervany before the operation with permission at its end. Maybe I'm not in the know, but for MK there are still no good hardware atomic operations.

Now let's talk about constants. As a rule, for communicating with VU registers there is a certain set of valid field values ​​and a good programming style is to consider the description of these features as a set of constants with meaningful names and check for validity of the value when assigning (so far I have used magic number 3, but this is exclusively in training purposes). What opportunities does the C language provide for solving this problem? There are two of them - defining constants via #define and creating enumerated types. We will analyze each of these alternatives. Suppose that our device is capable of accepting only 2 commands - “start work” with code 3 and “stop work” with code 2. Then we can write:
#define IO_DEVICE_START 3
#define IO_DEVICE_STOP 2
 pio_device->code=IO_DEVICE_START;
, which is most often done. So the magic number disappeared, even there is a check for compliance with the size of the bit field, but the expression
 pio_device->code=1;
the compiler will skip as valid. That is, the task of controlling the value of admissibility falls on the shoulders of the developer and is implemented by ASSERT. The method is quite workable, we often use it and it is quite acceptable if it weren’t more convenient, namely the use of an enumerated type:
#pragma bitfields=reversed
typedef struct {
  unsigned :1;
  enum {
    O_DEVICE_START=3,
    IO_DEVICE_STOP=2,
  }  code:3;
  unsigned :26;
  const unsigned flag1:1;
  unsigned flag:1;
} tIO_STATUS;
#pragma bitfields=default 
 pio_device->code=IO_DEVICE_START;
 SETBIT(pio_device->code,BITMASK); 
 pio_device->code |= BITMASK;
 pio_device->code=pio_device | BITMASK;
Pay attention to the fact that in the last line we get a warning about incompatibility of types, and in the two previous ones that do the same, we don’t get (this is not a bug, this is such a feature). Why is this method more convenient? First, we can place the enumeration of possible values ​​directly in the body of the structure description, which is more readable. Secondly, the compiler will check the values ​​in the definitions and will not allow us to go beyond the scope of the field. Third, and most importantly, the compiler will not allow us to assign an invalid (not listed) value to the field, although it leaves us with a loophole shown in the penultimate line (if anyone knows how to close it, write). In short, everything is wonderful and wonderful, BUT you can not use such a construction in any compiler, since the C standard does not allow using anything other than int for bit fields. Moreover, even the IAR will require an additional compiler directive --enum_is_int to ensure proper alignment. But if you are not afraid of compiler dependency, then the method is very beautiful, transparent and convenient (I agree in advance with those who write in comments that this will greatly reduce the portability).

Well, in conclusion, a few thoughts about the functions and wrappers for them. Often when viewing libraries you will find something similar to the following:
dev_data_r_w (int n, int data_command, int r_w, int *adr) { ... };
int dev_data(int n, int data_comand, iint *adr) { return dev_data_r_w (n, 1, 1, int *adr);
int read_dev(int n, int *adr) { return dev_data(n,1,adr); };
int ch_read_dev( int *adr) { return read_dev(1,adr); };
, and it is easy to see that the first function does the real work, and all the others create wrappers for it, so as not to write the corresponding constant parameters. In C ++ (and in a number of others), a similar problem is removed by default parameter values, but for C it is still relevant. My personal opinion is that this should not be done. If dynamic type conversion is not required, then use macros to create convenient (easy to use) synonyms for a common function:
#define dev_data(N,DC,ADR) dev_data_r_w ((N),(DC),1,(ADR))
#define read_dev(N,ADR) dev_data((N),1,(ADR))
#define ch_read_dev(ADR) read_dev(1,(ADR))
Such a definition is not more complicated, it loses slightly in terms of code size, but wins in terms of execution time and the size of memory used. Such multi-link constructs are especially touched in interrupt routines. And one more observation - for some reason, some programmers (if there are such among the readers, write why) believe that creating their own enumerated type
enum { SET=1, RESET=0 } ACTIVE;
- that's cool. I can still understand when this type is used to write a value to bits, but when to control its value? It seems to me that the bool type completely replaces this type, although who knows, it is ready to listen to other opinions.

To summarize the third part of the article, the goal was to agree on some general rules in the description of access methods to the VU registers, to develop some vocabulary, because I was thinking of writing several posts in which, step by step, I would consider building the maintenance routines of MK peripherals from the simplest ones (SPI , UART) (although with a deep examination there are not so many very simple devices) up to quite complex (USB, Ethernet). In principle, the task has been completed, there are still a number of comments on the design of the programs, but I will present them already along the way.

Also popular now: