Lesson 15. Pointers in C++ for Arduino. Conversion of different data types to bytes.

Pointers in C++ for Arduino

In the lesson we learn what pointers are, and how they allow us to optimize the program code, learn how to convert complex data types (int, long, float ...) into a sequence of bytes.

Previous lesson     List of lessons     Next lesson

In principle, you can develop programs without using pointers. But they significantly speed up the execution of the program code, which is especially important when working with microcontrollers. Below will be shown how effective the use of pointers to convert complex data types into bytes.

The topic of pointers in C ++ is important and extensive. Now I will very briefly talk about pointers. Details of their use will be discussed in the following lessons as needed.

 

Pointers in C++ for Arduino.

When developing a program, we work with variables of different types, arrays, objects, functions ...

  • We address all of them by the names given during the declaration.
  • All of them are stored in memory, divided into cells - bytes.
  • All occupy a different number of bytes in memory.
  • But all objects have the address of the beginning of the memory block in which they are located.

A pointer is a variable that contains the address of a memory cell. A pointer refers to the beginning of a block of memory that contains a variable or function.

  • Pointers can be used to pass data by reference. This speeds up data processing, since there is no need to copy the data, as is done when transferring using the variable name.
  • Pointers are used for dynamic memory allocation, for example, for an array of unlimited size.
  • Pointers are useful for converting various types of data into byte streams.

 

Indirect Addressing.

You can find out the address of a particular variable in C ++ by the operation of obtaining the address &. It gives the address of the variable in front of which the character & is written.

And to access a variable by address (pointer) there is an indirect addressing operation *. It reads the value of the memory cell at the address referenced by the pointer.

cd = 15; // variable cd = 15
ptrCd = & cd; // variable ptrCd = address of the variable cd
vl = * ptrCd; // variable vl = value at the address from ptrCd, i.e. vl = cd = 15

It should be understood that & cd is a number, a specific address. And ptrCd is a pointer type variable, i.e. address variable.

 

Types of pointers.

There are pointers:

  • to main types;
  • to arrays;
  • to composite objects (described by classes);
  • to functions;
  • to pointers;
  • to void.

 

Pointers to main types.

Like any other variable, the pointer must be declared before use. When declaring a pointer, the symbol * is placed before its name.

int * ptrdt; // pointer to a variable of type int
float * ptrx, * ptry, * ptrz; // pointers to float variables

To distinguish pointers from ordinary variables, it is customary to add ptr characters to the name. But this condition is optional and many do not adhere to it.

When declaring pointers, the required number of bytes of memory is allocated, depending on the type of data. For example, for dt (int) the compiler will allocate 2 bytes, and for x (float) 4 bytes will be reserved.

 

Pointers to arrays.

Using pointers is very efficient when working with arrays. The array name is already a hidden form of applying pointers.

The array name is a pointer to its first element.

int weights [10]; // array weights
weights == & weights [0]; // name is the address of the first element of the array

It is convenient to use pointers to arrays, i.e. array names as function arguments.

int weights [10]; // array weights
calculateAll (weights); // function uses the array name as an argument

 

Pointers to Functions.

Like an array name, the function name itself is a pointer. A pointer to a function stores the memory address of the program where its code is located. At this address, control is transferred when the function is called. Such pointers are used:

  • to call a function by accessing a variable with its address, and not through a name;
  • to pass the name of a function as an argument to another function.

In lesson 10, we used the MsTimer2 :: set() function. As the second argument, we set the name of another function (timerInterrupt) - the interrupt handler.

MsTimer2 :: set (2, timerInterrupt); // set the interrupt period and the name of the interrupt handler

A function pointer is declared like this:

type (* name) (arguments).

Compared with the function declaration, brackets and * were added.

int (* ptrCalc) (int, float); // declaration of a pointer to a function with int and float arguments
ptrCalc = calculate; // assign the pointer function ptrCalc address
Serial.printf (ptrCalc (x, 2.345)); // function call via pointer

int calculate (int, float) {
  // function body calculate
}

 

Pointers to void.

Pointers to void are used to refer to an object whose type is not defined. For example, to store different types of data in the same memory cells.

Before using a pointer to void to store a specific data type, you must perform an explicit conversion to this type.

 

Dynamic variables.

When we declare a variable, the compiler allocates for it the right amount of memory cells. These cells cannot be used for other purposes, even if this variable is no longer necessary. Therefore, this variable is called static. If a static variable requires a significant amount of memory (for example, a large array), then it may be necessary to remove it and free up memory for new variables.

This can be done only when using dynamic variables. Dynamic variables can be created and deleted during program execution. Access to dynamic variables is made only through pointers.

Memory allocation for a dynamic variable is performed using operator new:

data_type * pointer_name = new datatype;

For example:

long * dt = new long;

Memory is allocated for type long (4 bytes). The start address is written to dt.

int * weights = new int [50];

Memory is allocated for 50 int values (100 bytes). The start address is written to pointer weights , which can be used as an array name.

When declaring, you can initialize the value at the pointer address:

long * dt = new long (102345); // value of memory at adress dt = 102345

You can free up the memory allocated by the new operator using the delete operator.

// memory allocation
long * dt = new long;
int * weights = new int [50];

// code

// freeing memory
delete [] weights;
delete dt;

Note that to free the memory of arrays, use the operator delete []. If you forget about parentheses, only the first element of the array will be deleted, and the rest will be inaccessible.

 

Pointers to composite objects (described by classes).

The operator new is also used to create dynamic objects.

Button * buttonPlus = new Button; // memory allocation for buttonPlus object of type Button

When creating a static object, access to its properties and methods is done by the direct conversion operation - ".".

buttonPlus.scanState ();
buttonPlus.flagClick = false;

To work with a dynamic object through a pointer, to access its properties and methods, use the operator of indirect conversion ”->”.

buttonPlus-> scanState ();
buttonPlus-> flagClick = false;

 

Pointer operations.

You can perform simple operations with pointers:

  • indirect access;
  • assignment;
  • addition with a constant;
  • subtraction;
  • increment;
  • decrement;
  • comparison;
  • explicit type conversion.

The access operation allows access to the value whose address is stored in the index. The construction * pointer_name can be considered a variable name. It allows all actions that are allowed for the type specified when the pointer is declared.

Arithmetic operations with pointers automatically take into account the size of the type of variable. Those for the char data type, the increment of the pointer will increase the memory address by 1 byte, and for the long type, adding 1 to the pointer will add 4 bytes to the real memory address. Arithmetic operations are mainly used when working with data placed in memory sequentially, for example, with arrays.

 

Conversion of different data types to bytes.

In the last lesson, we saved the data in the Arduino's EEPROM. We kept the data type byte. EEPROM stores data in bytes and our data in bytes. It's simple. But, for exampe, we need to save a variable of type int in the EEPROM. It takes 2 bytes in memory and must be converted to individual bytes in order to write to non-volatile memory. You can do this:

int dt = 0x1234; // variable of type int that needs to be converted to bytes
byte byteEeprom1 = (byte) (dt & 0xff); // low byte = 0x34
byte byteEeprom2 = (byte) (dt >> 8); // high byte = 0x12

We received two bytes, but had to perform several operations, including a shift of 8 bits. To handle a variable of type long shifts and other operations will be much more. But I don’t have any idea what to do with a float variable. Convert it to a fixed-point number? Probably, something can be thought up, but it will be a difficult decision and will require significant resources from the microcontroller. But all variables are stored in memory, divided into bytes. We must take them from memory in the form of bytes. You can do this with a pointer.

Let's convert an int variable to bytes in this way.

int dt = 0x1234; // variable of type int that needs to be converted to bytes
byte byteEeprom1 = * ((byte *) (& dt)); // low byte = 0x34
byte byteEeprom2 = * ((byte *) (& dt) + 1); // high byte = 0x12

To read a byte, we:

  • received the address of the variable: & dt;
  • explicitly converted the address to a pointer to the type byte: (byte *);
  • applied the operation of indirect addressing: *;
  • for reading the second byte added 1 to the pointer.

In this example, we did not declare a pointer at all. In the lines of code for reading bytes, the operation of receiving and translating the address was repeated. To convert data with large sizes, it is better to declare a pointer. This will greatly speed up the program.

An example of converting a float variable into bytes:

float dt = 2.58901; // variable of type float, which must be converted to bytes
byte * ptrdt; // pointer to byte type
ptrdt = (byte *) (& dt); // get the address of the variable dt
byte byteEeprom1 = * ptrdt; // read bytes
byte byteEeprom2 = * (ptrdt + 1);
byte byteEeprom3 = * (ptrdt + 2);
byte byteEeprom4 = * (ptrdt + 3);

It remains to write bytes to the EEPROM. Of course, this can be done without intermediate variables.

// write bytes to EEPROM
EEPROM.write (0, * ptrdt);
EEPROM.write (1, * (ptrdt + 1));
EEPROM.write (2, * (ptrdt + 2));
EEPROM.write (3, * (ptrdt + 3));

To read data of type float from EEPROM, we read the bytes and write them sequentially to the memory area by the pointer ptrdt:

// read bytes from the EEPROM and write to the memory area for dt
* ptrdt = EEPROM.read (0);
* (ptrdt + 1) = EEPROM.read (1);
* (ptrdt + 2) = EEPROM.read (2);
* (ptrdt + 3) = EEPROM.read (3);

Converting variables of different types into bytes is required in many other cases with byte streams, for example, when communicating on a serial port.

Pointers are a very important topic for practical programming of microcontrollers. In future lessons, as necessary, we will consider ways to use them with concrete examples. Then you may have to return to this lesson.

In the next lesson we will talk about the reliability of programs for Arduino, learn how to use the watchdog timer.

Previous lesson     List of lessons     Next lesson

Leave a Reply

Your email address will not be published. Required fields are marked *