30- Punteros y enlaces de Arduino


Punteros arduino.

Los punteros son uno de los temas m谩s dif铆ciles de la programaci贸n, intentar茅 explicarlo de forma m谩s sencilla y cercana a la pr谩ctica. Empecemos por la representaci贸n de los datos en la memoria del microcontrolador: en la lecci贸n sobre operaciones de bits, comentamos que el bloque de memoria m铆nimo direccionable es un byte, es decir, podemos hacer referencia a cualquier byte de la memoria del microcontrolador. Cuando trabajamos con variables, no pensamos en las direcciones y la ubicaci贸n de los datos en la memoria, solo usamos sus nombres para leer / escribir, pasar los nombres como par谩metros en la funci贸n y realizar otras acciones con los datos. Poseer las direcciones de los bloques de datos le permite hacer muchas cosas de manera m谩s r谩pida y eficiente en t茅rminos de memoria. Algunos ejemplos de las posibilidades que ofrecen los punteros:

  • Usando punteros, puede dividir cualquier dato (variables de todo tipo, estructuras) en bits (flujos de bits) para su posterior manipulaci贸n con ellos (transferencia / escritura / lectura);
  • Puede pasar direcciones de bloques de datos como argumentos a funciones, de modo que cuando se llame a la funci贸n, no se creen copias de las variables y el c贸digo se ejecute m谩s r谩pido. En otras palabras, habilita la funci贸n para cambiar el argumento pasado;
  • Trabajar con memoria din谩mica 芦directamente禄, creando matrices din谩micas de cualquier tama帽o con acceso r谩pido a los datos. Lea la lecci贸n sobre memoria din谩mica.

驴Qu茅 es un puntero? Esta es una variable que contiene la direcci贸n del 谩rea de datos (variable / estructura / objeto / funci贸n, etc.) en la memoria del microcontrolador, m谩s precisamente, su primer bloque, un byte. Sabemos que todos los datos constan de bytes, de diferentes n煤meros, conociendo la direcci贸n del primer byte en un bloque de datos, podemos tener control sobre los datos en esta direcci贸n, pero necesitamos saber el tama帽o de este bloque. En realidad, al crear un puntero, indicamos a qu茅 tipo de datos apunta, un puntero puede apuntar a cualquier tipo de datos. Por s铆 mismo, el puntero almacena la direcci贸n, agregando 1 al puntero, obtenemos la direcci贸n del siguiente bloque de memoria.

Pasemos a los operadores que nos permiten trabajar con punteros.

Tenemos algunos operadores, son una de las caracter铆sticas m谩s poderosas del lenguaje C ++:

  • & – devuelve la direcci贸n de los datos en la memoria (la direcci贸n del primer bloque de datos)
  • * – devuelve el valor en la direcci贸n especificada
  • -> – operador de indirecci贸n a miembros y m茅todos (para punteros a estructuras y clases). Es una abreviatura de la construcci贸n mediante un puntero: a –> b equivalente a: ( * a ) .b

驴C贸mo funciona? Podemos crear un puntero al tipo de datos deseado como este:

tipo_datos* nombre_pointer;
tipo_datos * nombre_pointer;
tipo_datos *nombre_pointer;

S铆, se puede confundir con la multiplicaci贸n, pero el compilador no lo confundir谩. Las tres opciones de notaci贸n son equivalentes, en diferentes art铆culos / c贸digos puede encontrar las tres opciones, est茅 preparado para esto. Despu茅s de la declaraci贸n, usted y yo tenemos un puntero, una variable que puede almacenar la direcci贸n de otra 芦variable禄 del tipo de datos especificado: estas pueden ser variables ordinarias de todos los tipos, matrices, cadenas, funciones, estructuras, objetos e incluso vac铆o –void… Echemos un vistazo a los punteros a diferentes tipos de datos por separado para cubrir todas sus posibilidades a la vez.


Punteros a variables 芦regulares禄.

Trabajar con un puntero le permite leer / cambiar el valor de una variable a trav茅s de su direcci贸n. Vea un ejemplo con comentarios:

byte b;    // una variable de tipo byte
b = 10;    // b ahora es 10
byte * ptr; // ptr - variable "puntero a objeto de tipo byte"
ptr = & b;  // el puntero ptr almacena la direcci贸n de la variable b
* ptr = 24; // b ahora tiene 24 (escribe en & b)
byte s;    // variable s
s = * ptr;  // s ahora tambi茅n es 24 (leer en & b)

Parece no ser nada complicado: cre贸 un puntero ptr en byte, es decir byte * ptr y escrib铆 la direcci贸n de la variable b en 茅l: ptr = & b… Ahora tenemos acceso sobre la variable b, a trav茅s del puntero ptr, podemos cambiar su valor de esta manera: * ptr = valor;

Intentemos pasar una direcci贸n a una funci贸n y cambiar el valor de una variable por su direcci贸n dentro de la funci贸n. Tengamos una funci贸n que tome la direcci贸n de una variable de tipo int y eleva al cuadrado esta variable.

void square(int* val) {
  *val = *val * *val;
}

As铆 es como lo usaremos:

int value = 7;  cre贸 una variable
square(&value);  // pas贸 su direcci贸n a la funci贸n
// aqu铆 el valor ya es == 49

驴Por qu茅 es bueno este enfoque? No creamos una copia de la variable, como hicimos en la lecci贸n sobre funciones, pasamos la direcci贸n y cambiamos los valores directamente. Esto es de lo que estoy hablando:

void setup () {  
  valor int = 7; // cre贸 una variable
  valor = cuadrado ( valor ) ;
  // aqu铆 el valor ya es == 49
}
int cuadrado ( int val ) {  
  return val * val;
}

Aqu铆, se crea una copia de la variable en RAM, interactuamos con esta copia, y luego la devolvemos y la asignamos. 隆Este c贸digo corre mucho m谩s lento!


Punteros a matriz en Arduino.

Tuvimos una lecci贸n separada sobre matrices, y all铆 no te cargu茅 y te dije 芦de d贸nde vienen las matrices禄, porque las matrices son en realidad un puntero y sus amigos. 驴Qu茅 es una matriz en general y c贸mo funciona? El nombre de la matriz es un puntero al primer elemento de esta matriz (establecemos el tipo de elemento al declarar la matriz), es decir myArray [ 0 ] == * myArray, o as铆: myArray == & myArray [ 0 ]… Para que sea m谩s f谩cil escribir y leer el c贸digo, se introducen corchetes, pero de hecho funciona as铆: a [ b ] == * ( a + b )! Una matriz es un 谩rea de memoria llena de 芦variables禄 del tipo especificado, y podemos referirnos a ellas. Un par de ejemplos sobre c贸mo trabajar con una matriz sin usar corchetes:

void setup() {
  Serial.begin(9600);
  //  trabajar sin [] corchetes
  byte myArray[] = {1, 2, 3, 4, 5};
  //  imprime 1 2 3 4 5
  for (byte i = 0; i < 5; i++) {
    Serial.print(*(myArray + i));
    Serial.print(' ');
  }
  Serial.println();
  //  trabajar con un puntero separado
  int myArray2[] = {10, 20, 30, 40, 50};
  int* ptr2 = myArray2; //  puntero a la matriz
  // imprime 10 20 30 40 50
  for (byte i = 0; i < 5; i++) {
    Serial.print(*ptr2);
    ptr2++;
    Serial.print(' ');
  }
}

Preste atenci贸n al segundo ejemplo: el bucle aumenta el puntero en uno, ptr2 ++, por lo tanto, se lleva a cabo el 芦cambio禄 al siguiente elemento de la matriz.

Esta disposici贸n de matrices le permite pasarlas como argumentos a funciones sin ning煤n problema. Ejemplo: una funci贸n que devuelve la suma de todos los elementos de una matriz:

void setup() {
  Serial.begin(9600);
  int myArray[] = {1, 2, 3, 4, 5, 6};
  Serial.println(sumArray(myArray));
}
int sumArray(int* arrayPtr) {  
  int sum = 0;
  / suma la matriz
  for (byte i = 0; i < 6; i++) { 
    sum += arrayPtr[i];
  }
  return sum; // regresa
}

Un punto importante: la matriz 鈥渘o sabe鈥 de qu茅 tama帽o es, es solo un 谩rea de memoria asignada. Para la universalidad de este enfoque, es necesario conocer el tama帽o de la matriz de antemano o pasarlo como un argumento:

void setup() {
  Serial.begin(9600);
  int myArray[] = {1, 2, 3, 4, 5, 6};
  //muestra la suma de la matriz
  Serial.println( sumArray(myArray, sizeof(myArray)) );
  // pas贸 la matriz y su tama帽o (en bytes !!!)
}
int sumArray(int* arrayPtr, int arrSize) {  
  int sum = 0;
 // encuentra el tama帽o de la matriz en el n煤mero de elementos,
  // dividiendo el tama帽o en bytes por el peso de cualquier miembro de la matriz
  arrSize = arrSize / sizeof(arrayPtr[0]);
  
  // suma la matriz
  for (byte i = 0; i < arrSize; i++) {  sum += arrayPtr[i];}
  return sum; // regreso
}

Puntero a funci贸n en Arduino.

La funci贸n tambi茅n tiene su propia direcci贸n en la memoria, en la que se puede acceder. Una funci贸n puede ser llamada simplemente por un puntero, o puede pasarla como argumento a otra funci贸n, y esto se puede hacer en otros archivos, en clases y bibliotecas. Un puntero de funci贸n se declara as铆:

return_dattype ( * nombre ) ( argumentos )

Luego, al puntero se le puede pasar la direcci贸n de cualquier funci贸n (solo con el nombre, el operador de direcci贸n, como en el caso de las matrices, no es necesario):

void setup() {
  Serial.begin(9600);
  void (*ptrF)(byte a); //puntero de funci贸n (se declara a continuaci贸n) 
  ptrF = printByte;     //da la direcci贸n de la funci贸n printByte
  ptrF(125);    //  llamar a printByte a trav茅s de un puntero (salidas 125)
  int (*ptrFunc)(byte a, byte b); //  hacer otro puntero 
  ptrFunc = sumFunc;    //para la funci贸n sumFunc
  
 // llama a printByte, que pasaremos el resultado a sumFunc 
  // a trav茅s del puntero ptrFunc
  ptrF(ptrFunc(10, 30));  // imprimir谩 40
}
void printByte(byte b) {
  Serial.println(b);
}
int sumFunc(byte a, byte b) {
  return (a + b);
}
void loop() {}

De esta manera, puede implementar una caracter铆stica de estilo 芦attachInterrupt()禄 en la biblioteca: almacenar la direcci贸n de la funci贸n en la clase y llamarla desde la clase.


Puntero a estructuras y clases en Arduino.

Las estructuras y clases (tambi茅n hay enumeraciones) son tipos de datos compuestos, el mecanismo de interacci贸n a trav茅s de punteros es ligeramente diferente aqu铆. Creemos una estructura, un puntero a ella y accedamos a la estructura a trav茅s de 茅l:

struct myStruct {
  byte myByte;
  int myInt;
} ;
// crear estructura someStruct
myStruct someStruct;
// puntero de tipo myStruct * para estructurar someStruct
myStruct * p = & someStruct;
// escribe en la direcci贸n en someStruct.myInt
p- > myInt = -666;
//(*p).myInt = -666; // m谩s o menos, vea el comienzo de la lecci贸n

De esta manera, puede pasar grandes estructuras sin hacer una copia en variables formales, 隆mucho m谩s r谩pido! Todo ser谩 exactamente igual con las clases.


Puntero a Void.

En todos los ejemplos anteriores, creamos un puntero a un tipo de datos conocido. Pero, 驴qu茅 sucede si desea transferir una direcci贸n a un tipo de datos desconocido? Se puede hacer void * ptr – puntero a vac铆o, para cualquier tipo! Pero esto solo se suma a los problemas, 驴c贸mo trabajamos con este puntero? Para empezar, el puntero 芦void禄 se puede convertir m谩s tarde a cualquier tipo deseado mediante una conversi贸n:

float Fval = 0,254 ;  // variable flotante
void * ptrV = & Fval;  // puntero a ?, (le dio un float, no le importa)
// creado Fptr - puntero para float
// y convirti贸 el ptrV desconocido en float
float * Fptr = ( float * ) ptrV;
// ahora * Fptr es 0.254

Aqu铆 hemos transformado ptrV, que era void * (puntero a void), en un puntero a float con ayuda ( float* ). A veces, esto puede resultar conveniente, por ejemplo, cuando se transfieren datos de diferentes formatos utilizando una 芦funci贸n universal禄. Tambi茅n puede ver la conversi贸n del tipo de puntero a trav茅s de cast (para obtener m谩s detalles, consulte la lecci贸n sobre tipos de datos ):

float* Fptr = static_cast<float*>(ptrV);

Desglose en Bytes.

A veces es necesario transferir algunos datos y luego recibirlos del otro lado. O escribir estos datos en alg煤n medio externo (EEPROM, tarjeta de memoria, etc.) y luego volver a leerlos. Necesitamos una herramienta universal que anote cualquier fecha y luego la lea correctamente. Para resolver este problema, puede usar punteros, esto se hace de la siguiente manera: cree un puntero al tipo byte, y as铆gnele la direcci贸n de un bloque de datos de cualquier tipo realizando la transformaci贸n  ( byte * ), solo obtenemos un puntero al primer byte de datos. Sabiendo la longitud (tama帽o en bytes) de nuestro fragmento de datos, podemos leerlo byte a byte simplemente agregando uno a la direcci贸n. Veamos un ejemplo simple con divisi贸n de n煤meros de 4 bytes de largo usando punteros:

// N煤mero grande
uint32_t bigVal = 123456789;
// puntero ptrB a la direcci贸n & bigVal
// convertir a (byte *)
byte * ptrB = ( byte * ) & bigVal;
// dividir en bytes
byte bigVal_1 = * ptrB;
byte bigVal_2 = * ( ptrB + 1 ) ;
byte bigVal_3 = * ( ptrB + 2 ) ;
byte bigVal_4 = * ( ptrB + 3 ) ;
// intenta volver a armarlo
// necesita una nueva variable 
// mismo tipo que el primero (uint32_t)
uint32_t newBig;
// toma su direcci贸n
byte * ptrN = ( byte * ) & newBig;
// 隆y recupera 4 bytes!
* ptrN = bigVal_1;  
* ( ptrN + 1 ) = bigVal_2;
* ( ptrN + 2 ) = bigVal_3;
* ( ptrN + 3 ) = bigVal_4;
// en este punto newBig es 123456789

Por lo tanto, puede 芦analizar禄 y 芦recopilar禄 cualquier dato (matriz de cualquier tipo, estructura), conociendo su tama帽o. 

El problema se puede resolver de una manera m谩s bella utilizando una matriz de bytes para leer y escribir. Considere un ejemplo con la conversi贸n de un tipo de puntero a trav茅s de( byte * ), mediante void* y mediante template:

Ejemplo v铆a (byte*)

// buffer
byte buffer[20];
// estructura para la prueba
struct myStruct {
  byte val1;
  int val2;
  long val3;
  float val4;
} ;
void setup () {  
  // === prueba con variables ===
  long a = 123456789;
  long b = 0;
  // dividir el bloque de datos a en bytes
  // y almacenar en b煤fer
  writeData (( byte * ) & a, sizeof ( a )) ;
  // recopilar el bloque de datos b
  // desde el b煤fer del b煤fer
  readData (( byte * ) & b, sizeof ( b )) ;
  // aqu铆 b == 123456789
  // === prueba con estructuras ===
  // crear estructura
  myStruct transmit;
  // asigna un valor al miembro val4
  transmit. val4 = 3,1415 ;
  // estructura "receptora"
  myStruct recieve;
  // dividir el bloque de datos de transmisi贸n en bytes
  // y almacenar en b煤fer
  writeData (( byte * ) & transmit, sizeof ( transmit )) ;
  // recopila el bloque de datos de recepci贸n
  // desde el b煤fer del b煤fer
  readData (( byte * ) & recieve, sizeof ( recieve )) ;
  
  // recieve.val4 aqu铆 == 3.1415
}
void writeData ( byte * datos, int longitud ) {  
  int i = 0;
  while ( longitud-- ) {  
    buffer [ i ] = * ( datos + i ) ;
    i ++;
  }
}
void readData ( byte * datos, int longitud ) {  
  int i = 0;
  while ( longitud-- ) {  
    * ( datos + i ) = buffer [ i ] ;
    i ++;
  }
}
void loop ()

Ejemplo a trav茅s de void*

// buffer
byte buffer[20];
// estructura para la prueba
struct myStruct {
  byte val1;
  int val2;
  long val3;
  float val4;
} ;
void setup() { 
  // === prueba con variables ===
  long a = 123456789;
  long b = 0;
  // dividir el bloque de datos a en bytes
  // y almacenar en b煤fer
  writeData ( & a, sizeof ( a )) ;
  // recopilar el bloque de datos b
  // desde el b煤fer del b煤fer
  readData ( & b, sizeof ( b )) ;
  // aqu铆 b == 123456789
  // === prueba con estructuras ===
  // crear estructura
  myStruct transmit;
  // asigna un valor al miembro val4
  transmit. val4 = 3,1415 ;
  // estructura "receptora"
  myStruct recieve;
  // dividir el bloque de datos de transmisi贸n en bytes
  // y almacenar en b煤fer
  writeData ( & transmit, sizeof ( transmit )) ;
  // recopila el bloque de datos de recepci贸n
  // desde el b煤fer del b煤fer
  readData ( & recieve, sizeof ( recieve )) ;
  
  // recieve.val4 aqu铆 == 3.1415
}
void writeData ( void * data, int length ) {  
  uint8_t * dataByte = ( uint8_t * ) data;
  int i = 0;
  while ( length-- ) {  
    b煤fer [ i ] = * ( dataByte + i ) ;
    i ++;
  }
}
void readData ( void * data, int length ) {  
  uint8_t * dataByte = ( uint8_t * ) data;
  int i = 0;
  while ( length-- ) {  
    * ( dataByte + i ) = buffer [ i ] ;
    i ++;
  }
}
void loop () {}  

Ejemplo mediante template

// buffer
byte buffer[20];
// estructura para la prueba
struct myStruct {
  byte val1;
  int val2;
  long val3;
  float val4;
} ;
void setup() {
  // === prueba con variables ===
  long a = 123456789 ;
  long b = 0 ;
  
  // dividir el bloque de datos a en bytes
  // y almacenar en b煤fer
  writeData ( a ) ;
  
  // recopilar el bloque de datos b
  // desde el b煤fer
  readData ( b ) ;
  
  // aqu铆 b == 123456789
  
  // === prueba con estructuras ===
  // crear estructura
  myStruct transmit;
  
  // asigna un valor al miembro val4
  transmit. val4 = 3,1415 ;
  
  // estructura "receptora"
  myStruct recieve;
  
  // dividir el bloque de datos de transmisi贸n en bytes
  // y almacenar en b煤fer
  writeData ( transmit ) ;
  
  // recopila el bloque de datos de recepci贸n
  // desde el b煤fer del b煤fer
  readData ( recieve ) ;
  // recieve.val4 aqu铆 == 3.1415
}
template < typename T >
void writeData ( T & data ) {  
  const uint8_t * ptr = ( const uint8_t * ) & data;
  for ( uint16_t i = 0 ; i < sizeof ( T ) ; i ++ ) {   
    buffer [ i ] = * ptr ++;
  }
}
plantilla < typename T >
void readData ( T & data ) {  
  uint8_t * ptr = ( uint8_t * ) & data;
  for ( uint16_t i = 0 ; i < sizeof ( T ) ; i ++ ) {   
    * ptr ++ = buffer [ i ] ;
  }
}
bucle vac铆o () {}  

Estos ejemplos difieren solo en la forma en que se pasa y procesa el argumento de la direcci贸n:

  • En el primer caso, lanzamos el puntero a ( byte * ) al pasar un argumento a una funci贸n. Tambi茅n pasamos el tama帽o del bloque de datos usando sizeof ().
  • En el segundo caso, tenemos void* y no le importa qu茅 tipo de datos se le pasar谩n, luego transferimos el puntero a uint8_t mediante reinterpret_cast. Tambi茅n pasamos el tama帽o del bloque de datos usando sizeof ().
  • La tercera opci贸n es a trav茅s de una funci贸n de plantilla, que no importa en absoluto el tipo y tama帽o de los datos: acepta datos por referencia, luego hacemos un puntero al primer byte y justo dentro de la funci贸n calculamos el tama帽o del bloque de datos a trav茅s sizeof (). Esta es la opci贸n m谩s poderosa y vers谩til.

Nota: los tres ejemplos ocupan la misma cantidad de memoria.

Esta lecci贸n es lo m谩s breve y de 芦referencia禄 posible, recomiendo leer con m谩s detalle sobre punteros, enlaces (no los analizamos) y sus caracter铆sticas en la referencia de C ++ en la web C con Clase.


Deja un comentario