Documentos de Académico
Documentos de Profesional
Documentos de Cultura
FUNDADOR DE PROGRAMARFACIL.COM
MEMORIAS DE ARDUINO
Uso eficiente de memorias de microcontroladores con C++
1
1. Arquitectura de los ordenadores 3
1.1 Arquitectura de los microcontroladores 4
2. Memorias de Arduino 6
2.1 Memoria Flash 7
2.2 Memoria EEPROM 9
2.2 Memoria SRAM 10
2.2.1 Globals 12
2.2.2 Heap 14
2.2.3 Stack 17
2.2.4 Cómo medir el uso de la memoria SRAM 19
El contenido de este libro y las ilustraciones, a menos que se indique lo contrario, es propiedad de Luis del
Valle de Programarfacil.com. Este libro y todo su contenido tiene todos los derechos reservados. Para
evitar su difusión sin permiso, cada documento es único y lleva cifrado el email de la persona que lo ha
descargado. Libros como este son posibles gracias mucho esfuerzo y tiempo invertido. Te pido por favor
que no lo distribuyas sin consentimiento expreso del autor.
Memorias de Arduino 2
1. Arquitectura de los ordenadores
Si pudiéramos reducir un sistema informático a su mínima expresión tendríamos una unidad
central de procesamiento o CPU (del inglés Central Processing Unit) y una memoria.
Por otro lado tenemos la memoria que es donde se almacenan los datos y los programas.
Esto es una descripción muy simple y reducida de cómo funciona un sistema informático pero
suficiente para entender lo que te quiero contar a continuación.
Hay dos arquitecturas que se suelen utilizar en sistemas informáticos. Está la arquitectura Von
Neumann y la arquitectura Harvard.
La mayor diferencia entre estas dos arquitecturas está en cómo se divide la memoria y dónde se
guardan los datos y los programas.
Mientras que la arquitectura Von Neumann utiliza la misma memoria para almacenar datos y los
programas (el código).
Memorias de Arduino 3
La arquitectura Harvard utiliza memorias físicamente separadas para los datos y para los
programas.
Lo mismo te estás preguntando: si quiero fabricar un sistema informático ¿cuál es la mejor
opción?
Pues tengo que decirte que una no es mejor que otra. Cada una tiene sus ventajas..
Memorias de Arduino 4
Lo importante y lo con lo que quiero que te quedes es que, la arquitectura Von Neumann utiliza
una única memoria y la arquitectura Harvard utiliza dos memorias, una para datos y otra para los
programas.
En la actualidad, la mayoría de los ordenadores (PC, MAC, etc…) son diseños híbridos que
utilizan las dos arquitecturas. Se benefician de lo mejor de cada una de ellas.
Cómo funciona un ordenador es algo complejo y se sale del objetivo de este documento. Lo que
sí que es importante es tener presente qué es lo que sucede en los microcontroladores.
Generalmente tiene una tarea bien definida que debe realizar de manera confiable y eficiente.
Una de las ventajas de los microcontroladores es su precio. Suelen ser muy económicos.
Puedes comprarlos por $1,9.
Memorias de Arduino 5
Son tan económicos porque tienen lo mínimo para que funcionen. No se dan grandes lujos como
los ordenadores personales.
El modelo Harvard (dos memorias separadas físicamente) es la mejor opción para aplicaciones
integradas. Por ejemplo, el ATmega328 del Arduino UNO utiliza una arquitectura Harvard.
Memorias de Arduino 6
Diagrama de bloques del ATmega328
Si lo comparas con un PC de gama baja, tiene unas 100.000 veces menos de memoria.
Antes de meternos de lleno en el tema, tienes que entender cómo funciona la memoria de
Arduino.
2. Memorias de Arduino
Realmente un Arduino no tiene memoria. La memoria está integrada dentro del
microcontrolador. Pero para entendernos todos, voy a hablar de las memorias de Arduino.
● Memoria Flash
● Memoria SRAM
● Memoria EEPROM
No todas las placas de Arduino tienen el mismo microcontrolador. Dependiendo de cada modelo
suelen llevar un microcontrolador u otro.
UNO ATmega328 32 KB 2 KB 1 KB
Memorias de Arduino 7
Leonardo, Micro ATmega32U4 32 KB 2,5 KB 1 KB
Pero como puedes comprobar, la gran mayoría tienen las 3 memorias. A continuación vamos a
ver cuales son las funciones principales de cada memoria.
Desde la memoria Flash se puede ejecutar un código sin embargo, no es posible modificar los
datos de la memoria Flash mientras se ejecuta dicho código.
Esto quiere decir que una vez que cargas el programa a la memoria Flash, ya no puedes
modificar los datos de ese programa hasta que no vuelvas a cargar un programa nuevo.
Memorias de Arduino 8
Este proceso de carga de programa en la memoria Flash se realiza cuando en el IDE de Arduino
das al botón de subir.
Una característica de la memoria Flash es que no es volátil lo que quiere decir que aunque se
apague el microcontrolador, el programa seguirá estando allí cuando se vuelva a encender.
Eso sí, la memoria Flash no es eterna. Tiene una vida útil de aproximadamente 100.000 ciclos
de escritura. Pero no te austes.
Para que puedas gastar una memoria Flash, tendrías que cargar 10 programas al día durante
todos los días de los próximos 27 años :)
Memorias de Arduino 9
Es difícil llegar a los límites de la memoria Flash pero debes llevar especial cuidado en las
librerías que añades a tu programa. Normalmente las librerías consumen memoria Flash y
memoria SRAM.
En principio, con un uso normal es complicado llegar a los límites de la memoria Flash. Por
ejemplo, para obtener el error que te muestro en la siguiente imagen, he tenido que utilizar un
programa de más de 15.000 líneas de código.
Por lo tanto, en principio la memoria Flash es fija. Una vez se ha cargado el programa compilado
a la memoria, no se modificará su tamaño. Luego veremos que hay técnicas que nos permiten
guardar datos en la memoria Flash mientras se ejecuta el código, pero lo normal es que esto no
suceda.
Un gran inconveniente es que se lee byte a byte y esto resulta un tanto incómodo. Tiene una
vida útil de aproximadamente 100.000 ciclos de escritura, igual que la memoria Flash.
Aunque es diferente a la memoria SRAM, hay momentos en las que la memoria EEPROM es
bastante útil.
Si quieres utilizar la memoria EEPROM en algún programa te recomiendo que utilices la librería
de Arduino EEPROM.
Puede ser muy útil si quieres guardar algún tipo de información que necesites utilizar o modificar
en algún momento y que necesites que su valor se mantenga aunque la placa de Arduino se
quede sin alimentación.
Memorias de Arduino 10
Por lo tanto, para medir cuánta memoria está ocupada o cuánta está libre no queda otra que
llevar la cuenta de los bytes que has ido almacenando y liberando.
Sin embargo tiene un gran inconveniente y es que es una memoria volátil. Esto quiere decir que
en el momento que el microcontrolador se queda sin energía, los datos se pierden.
Sirve para almacenar información temporal como el valor de las variables en un determinado
momento. Esto hace que sea uno de los mayores focos de problemas en nuestros códigos.
Por este motivo me voy a centrar en ver cómo podemos optimizar este tipo de memoria.
Para entender cómo funciona la memoria SRAM en C++ (lenguaje con el que se programa
Arduino), imagínate un array gigante que incluye todos los bytes de la memoria. El elemento en
el índice 0 es el primer byte de la SRAM y así sucesivamente.
Memoria de n bytes
La asignación de memoria se realiza de forma consecutiva. Lo que quiero decir es que si quieres
almacenar una variable del tipo int que ocupa 2 bytes, ocupará dos elementos consecutivos.
Si luego quieres reservar memoria para otra variable del tipo long de 4 bytes, esos 4 bytes se
sitúan de forma consecutiva dentro de la memoria.
Memorias de Arduino 11
Solo quiero hacer una puntualización y es que en realidad, el índice del array no empieza en 0.
Esto depende de cada microcontrolador.
En el caso del ATmega328 la memoria RAM empieza en la dirección 256 (en hexadecimal se
escribe como 0x100). Por lo tanto, si quieres leer el byte 42 de la memoria RAM debes utilizar el
índice 256 + 42.
De momento lo único que quiero es que te quedes que la memoria es como un array troceado
de byte en byte y que cada variable se almacena una detrás de otra de forma consecutiva.
El compilador (y las librerías estándar) trocean el array de memoria en tres áreas que se utilizan
para almacenar diferentes tipos de datos.
También hay una zona libre de memoria (NO USADA) entre las zonas heap y stack.
Memorias de Arduino 12
Cada área de la memoria SRAM almacena un tipo de dato diferente. Veamos en detalle cada
una de ellas.
2.2.1 Globals
El área de globals almacena las variables globales que se declaren en el programa o código.
Recuerda que una variable global está accesible desde cualquier parte del
código ya sea dentro de las funciones loop() y setup() o de cualquier
función definida por el usuario.
Como es normal, el tamaño de este área es constante es decir, permanece igual durante la
ejecución del programa.
Cuando el compilador hace el recuento de todas las variables globales, reserva el área globals
para almacenar este tipo de variables.
Todas las variables que estén dentro de este área están siempre presentes en la memoria. Por
lo tanto, debes llevar especial cuidado al declarar una variable global ya que la memoria que
ocupe no se podrá utilizar para otra cosa.
Pregúntate siempre si la variable global podría ser una variable local. En caso afirmativo, utiliza
siempre variables locales para disminuir el área de memoria globals.
void setup() {
// Uso de la variable local
pinMode(pinLed, OUTPUT);
}
void loop() {
int tiempo = 2000; // Variable local
Memorias de Arduino 13
delay(tiempo);
}
Por ejemplo, el tiempo de espera que solo se utiliza en la función loop() debe ser local como
muestro en el código.
Los textos y literales son un gran enemigo de la memoria SRAM ya que normalmente se utilizan
en variables globales y ocupan mucho espacio del área globals.
Es muy importante que evites, en la medida de tus posibilidades, utilizar muchos textos en tus
programas ya que reducen significativamente la memoria SRAM.
Hay métodos para evitar que los textos se coman toda la memoria SRAM. La más sencilla es
decir al compilador que guarde los datos en la memoria Flash usando las sentencias PROGMEM
y F().
Todo esto lo veremos más adelante cuando veamos cómo tratar los textos o literales de forma
eficiente.
Como resumen, el área de memoria globals almacena todas las variables globales de tu código.
Cuantas más variables de este tipo tengas, más grande será el área globals y no podrás
utilizarlo para otros fines.
2.2.2 Heap
El área heap contiene las variables que asignan la memoria dinámicamente. A diferencia del
área globals, su tamaño varía durante la ejecución del programa.
Para que entiendas el concepto de memoria dinámica, te voy a poner una analogía. Imagínate
que eres el dueño de un cine y que tienes dos salas con el mismo número de butacas.
Memorias de Arduino 14
En la sala 1 se proyectan películas con mucho éxito. El Señor de los Anillos, Regreso al Futuro,
Star Wars, etc...
En la sala 2 se proyecta cine de autor que, aunque no tiene tanto éxito, tiene su público.
Normalmente la sala 1 siempre está llena y en la sala 2 siempre hay huecos libres. Si tuvieras
más espacio en la sala 1 los llenarías seguro.
Este sería un claro ejemplo de reserva de memoria estática. Las butacas de cada sala no se
pueden ni aumentar ni disminuir, son las que son.
Pero imagínate que pudieras coger butacas de la sala 2 para ampliar la sala 1 cuando te hiciera
falta. Por ejemplo, en el estreno de Joker.
Memorias de Arduino 15
Aprovecharías mucho mejor las butacas de las dos salas y sacarías más rentabilidad. Pero
cuando no te hagan falta las devuelves a la sala 2 o las dejas libres.
Más o menos así es como funciona la memoria dinámica. Te permite reservar la memoria
necesaria en tiempo de ejecución del programa es decir, una vez ya se ha compilado y cargado
a la memoria.
Por ejemplo, en la primera iteración del bucle loop() necesitas 10 bytes de memoria, luego
necesitas 8 bytes, luego 15 bytes y así haces que la memoria vaya aumentando y disminuyendo.
Sin embargo, en otras ocasiones, esto se hace de forma automática. Por ejemplo, cuando se
utiliza un cierto tipo de dato para guardar cadenas de texto como la clase String. Luego veremos
la implicación que tiene todo esto.
Por lo tanto, la memoria dinámica crece y disminuye según se reserve o libere memoria.
Memorias de Arduino 16
Debido a esto, existe un problema asociado a la memoria dinámica que se conoce como
fragmentación heap que consiste en que a veces, cuando se libera la memoria, no se liberan las
celdas adyacentes y se produce una fragmentación.
Imagínate que hay una memoria de 30 bytes y se asignan tres bloques de 10 bytes.
Esto hace que ahora quedan 20 bytes de memoria libre en dos bloques separados de 10 bytes.
Se podrían asignar por separado 10 bytes y 10 bytes pero si tienes una variable que ocupa 20
bytes, no podrías almacenarla en la memoria.
Memorias de Arduino 17
Efecto de la fragmentación heap en la memoria
2.2.3 Stack
El área de memoria stack contiene el resto de datos que se almacena en la SRAM:
El tamaño del área de la memoria stack cambia continuamente durante la ejecución del
programa al igual que cambia el área heap.
Cuando en un programa se declara una variable local en una función, el área de memoria stack
aumenta en la misma proporción que el tamaño de la variable.
Lo mismo ocurre cuando se llama a una función con parámetros ya que los parámetros
funcionan como variables locales a la función.
Cuando termina de ejecutarse la función, se libera la memoria de las variables locales y de los
parámetros. Todo esto hace que el área de memoria stack cambie continuamente de tamaño.
Memorias de Arduino 18
El siguiente código muestra una función con un parámetro y una variable local. Estas variables
se almacenan en el área de memoria stack.
return temperatura;
}
No sucede este tipo de fragmentación o no es tan problemática porque el área de memoria stack
funciona de forma diferente al área de memoria heap.
Lo que sucede es que cuando tu llamas a una función, se reserva toda la memoria de golpe: los
parámetros y las variables locales.
Si seguimos con el ejemplo del código anterior lo que sucederá es que reservará 3 bytes de
memoria: 1 byte del parámetro tipo byte y 2 bytes de la variable local tipo int.
Cuando la función devuelva el valor, liberará esos 3 bytes de memoria y no dejará huecos en
medio.
Esa es la diferencia.
Memorias de Arduino 19
Una buena forma de saber si hay algún tipo de problema con la memoria es controlar su tamaño.
Ahora veremos que esto no es tan sencillo como parece sobre todo con las áreas de memoria
heap y stack.
En la consola se muestra información sobre las variables globales que se utilizan tanto en tu
programa como en las librerías que hayas añadido a tu código.
Esta zona de memoria está acotada y no varía durante la ejecución del programa. Sin embargo,
el área heap y stack son dos áreas que varían durante la ejecución y por lo tanto, es más difícil
de medir.
Para saber la memoria SRAM libre y usada puedes utilizar una librería. Normalmente son
complejas ya que utilizan programación de bajo nivel. Una de las que mejor funciona es
MemoryUsage.
Si no se trabajan de una forma correcta, la memoria SRAM deja de ser eficiente y empiezan a
surgir problemas.
La siguiente sección la dedicaré exclusivamente a mostrarte cómo trabajar con las cadenas de
texto de una forma eficiente.
Memorias de Arduino 20
Si por ejemplo quieres utilizar una pantalla OLED o un LCD para mostrar textos, debes prestar
especial cuidado a la hora de almacenar cadenas en la memoria SRAM del microcontrolador.
Las cadenas de texto suelen ser uno de los mayores focos de problemas en cuanto a la
optimización de memoria SRAM.
Una cadena de texto es una secuencia de caracteres terminada por un cero. El cero marca el
final de la cadena y se llama terminador nulo. Normalmente ese último carácter no se muestra.
Por lo tanto, una cadena como “hola” que tiene cuatro caracteres en realidad ocupa cinco bytes.
String s = "hola";
char s[] = "hola";
const char* s = "hola";
La forma de declarar una cadena de texto más perjudicial para la memoria SRAM es utilizando la
clase String.
Y la verdad, la gestión de las cadenas con la clase String en Arduino es un poco complicada.
Memorias de Arduino 21
Esta clase se creó para que la gente que se está iniciando en la programación con Arduino
tuviera una forma fácil de trabajar con cadenas.
Sin embargo, puede ser un arma de doble filo por varios motivos.
Pero antes de meternos de lleno en todo esto volvamos a recordar cómo se divide la memoria
SRAM.
Se divide en 3 áreas:
Esto lo producen las variables dinámicas y precisamente la clase String utiliza la reserva de
memoria dinámica de forma interna, sin que tu te enteres.
Memorias de Arduino 22
De vez en cuando una pieza desaparece y deja un hueco libre.
Si la siguiente pieza cabe en el hueco lo utiliza. No importa si es más pequeña y deja un hueco
desaprovechado.
Y a esto se le llama fragmentación. Imaginate hacer esto cientos de veces en la memoria SRAM.
Puedes llegar a desaprovechar mucha memoria.
Cada vez que creas un objeto de la clase String reservas memoria de forma dinámica que
depende del tamaño de la cadena de texto. Si utilizas muchos objetos de esta clase, puedes
llegar a desaprovechar mucha memoria del área heap.
Memorias de Arduino 23
String s1 = "hola";
String s2 = "Luis";
// El operador + con objetos String concatena las dos cadenas
String s3 = s1 + " " + s2;
A primera vista ves que hay 3 cadenas. La primera que almacena “hola”, la segunda que
almacena “Luis” y la tercera que es el resultado de concatenar las otras dos cadenas.
Sin embargo, hay una cuarta cadena. Para poder crear la cadena s3 hay que hacerlo por etapas.
Partimos de las dos primeras cadenas s1 y s2 que ocupan sus posiciones de memoria. En esta
representación no voy a tener en cuenta el carácter nulo del final.
Para crear la cadena s3, primero coge la cadena s1 cuyo valor es “hola” y concatena un espacio
al final de la misma creando un nuevo objeto String temporal al que voy a llamar s3’.
Luego, en ese String, s e agrega la cadena s2 cuyo valor es “Luis” creando la cadena s3 final.
La cadena String intermedia s3’, es una cadena temporal que se crea en el proceso pero luego
se elimina. Sin embargo, es una variable dinámica y por lo tanto, si no se rellena con otro objeto
String u otra variable dinámica del mismo tamaño, genera un hueco en el área heap.
Memorias de Arduino 24
Entonces, si quieres concatenar varios textos a un objeto String ¿cómo evitar estos objetos
temporales?
Una opción es utilizar la función concat() de la clase String. Esta función te permite concatenar
varias cadenas de texto en un mismo objeto String.
String s1 = "hola";
s1.concat(" ");
s1.concat("Luis");
Pero aún utilizando la función concat(), corres el riesgo de que se queden huecos. Si según
vamos añadiendo cadenas a la cadena original, el hueco en el que estaba situada la cadena es
demasiado pequeño, se moverá a otro hueco más grande dejando vacío el hueco pequeño.
La mejor solución es utilizar la clase String lo menos posible en un programa. Por contra, debes
utilizar las cadenas nativas de C que luego veremos.
void setup() {
Serial.begin(9600);
String sTemp = "Temperatura";
String sVal = "25,5ºC";
imprimirValor(sTemp, sVal);
}
void loop() {}
Memorias de Arduino 25
En el código anterior hay 3 funciones. Las típicas setup() y loop() y una tercera función definida
por el usuario que muestra por el monitor serie dos cadenas de texto que se pasan como
parámetro.
Antes de llamar a la función se crean dos cadenas String: sTemp y sVal. Luego, al llamar a la
función, se crean otros dos objetos String, los parámetros.
Aunque sean variables locales y parámetros, los objetos de la clase String, se almacenan como
variables de reserva de memoria dinámica en el área heap.
Esto hace que de repente, tengas el doble de variables String. Y todas ellas se almacenan en el
área de memoria heap con el riesgo de fragmentación que conlleva todo esto.
Si no llevas cuidado, cuando quieras echar un vistazo al área de memoria heap, parecerá un
queso suizo.
Para evitar esta duplicidad el truco está en pasar las variables por referencia. Esto lo puedes
hacer de la siguiente manera.
void setup() {
Serial.begin(9600);
String sTemp = "Temperatura";
String sVal = "25,5ºC";
Memorias de Arduino 26
imprimirValor(sTemp, sVal);
}
void loop() {}
Al poner el modificador & antes del nombre del parámetro lo que estamos haciendo es pasar la
propia variable y no crear una variable nueva. Es decir, es exactamente el mismo objeto String.
Es como la función copiar y mover que tienen los sistemas operativos y programas. Cuando
pasas una variable de forma normal, sin que sea por referencia, realmente lo que haces es una
copia y por lo tanto, estás utilizando el doble de memoria.
Sin embargo, cuando pasas una variable por referencia es como si movieras esa variable y la
pasaras tal cual a la función. No se hace ninguna copia.
Esto se sale fuera de este tutorial y va muy ligado a cómo funcionan los punteros de memoria.
Más adelante profundizaremos más sobre los punteros.
Lo que quiero que entiendas es que aún así, pasando los objetos String por referencia, lo mejor
que puedes hacer es no utilizar la clase String para almacenar cadenas de texto.
Entonces ¿cómo guardar cadenas de texto de forma óptima? En la siguiente sección te hablaré
de las cadenas nativas en C.
Esto significa que el último carácter tiene que tener el código ASCII 0.
Todas las funciones internas que manipulan las cadenas de caracteres buscan el carácter NULL
final como marcador para mostrar dónde está el final de la cadena.
Memorias de Arduino 27
Crear una cadena en C es tan simple como escribir el siguiente código.
char cadena[30];
Esta sentencia crea un array de hasta 30 caracteres. Si es una variable global se almacena en el
área globals. Si se declara en una función va directamente al área stack. En ningún caso va al
área heap.
Gracias a que no utiliza el área de memoria dinámica, evitamos que se produzca la poco
deseada fragmentación del heap.
Sin embargo, no es todo tan bonito como parece. Manipular una array de caracteres no es tan
sencillo como manipular un objeto String. Y esto es debido a que no deja de ser un array de
elementos que en este caso, son caracteres.
Pero ¿qué pasa si quieres modificar el valor del array en mitad del código? Con un objeto String
es bastante sencillo.
Pero con un array de caracteres no puedes hacer lo mismo. Para cambiar un array tienes que ir
elemento por elemento.
Memorias de Arduino 28
cadena[0] = 'A';
cadena[1] = 'h';
cadena[2] = 'o';
cadena[3] = 'r';
cadena[4] = 'a';
cadena[5] = ' ';
cadena[6] = 'e';
cadena[7] = 's';
cadena[8] = ' ';
cadena[9] = 'o';
cadena[10] = 't';
cadena[11] = 'r';
cadena[12] = 'a';
cadena[13] = ' ';
cadena[14] = 'c';
cadena[15] = 'a';
cadena[16] = 'd';
cadena[17] = 'e';
cadena[18] = 'n';
cadena[19] = 'a';
Esto hace que sea complicado modificar su valor en mitad del código, pero no te preocupes, hay
una solución para hacer esto. La función strcpy() permite copiar una cadena en un array de una
forma muy sencilla.
La función strcpy(...) se puede utilizar para copiar tanto literales como otros arrays de caracteres.
Esta función elimina todos los caracteres anteriores y copia carácter a carácter la nueva cadena
de texto.
Memorias de Arduino 29
Para concatenar o unir cadenas sucede algo parecido. El ejemplo que hemos visto
anteriormente con la clase String no funciona con arrays de caracteres.
String s1 = "hola";
String s2 = "Luis";
// El operador + con objetos String concatena las dos cadenas
String s3 = s1 + " " + s2;
Par concatenar dos arrays de caracteres hay que utilizar la función strcat().
strcat(s3, s1);
strcat(s3, " ");
strcat(s3, s2);
Hasta ahora, en los dos casos que hemos visto de strcpy() y strcat(), no hemos tenido en cuenta
los tamaños de los arrays.
Es decir, en ningún momento se hace una comprobación de si la nueva cadena o la cadena que
se va a contestar, cabe en el array destino.
Imagínate que se declara un array de 5 elementos. Este array podrá almacenar una cadena de 4
caracteres. No olvides el carácter NULL del final del array.
Pues al contrario de lo que puedas pensar, no da ningún error. El código anterior lo puedes
ejecutar perfectamente en un microcontrolador de un Arduino sin errores de compilación.
¿Qué es lo que sucede en la memoria? Pues algo increíble y que se suele utilizar los
delincuentes para ejecutar código malicioso en el sistema. A esto se le conoce como
desbordamiento de buffer (en inglés lo encontrarás como overflow) .
Memorias de Arduino 30
Lo que sucede es que al necesitar más memoria la coge sin previo aviso.
Da lo mismo si esa posición de memoria está libre o está ocupada por otra variable,
sobreescribe la posición de memoria.
Esto es muy peligroso ya que estás modificando el valor de otra variable sin darte cuenta
provocando que el comportamiento del programa sea impredecible.
Esto, por el contrario, no sucede con otras variables del tipo int, byte, boolean, long, float, … En
el caso de todas estas variables numéricas también se produce un desbordamiento de buffer
pero lo que sucede es que la variable se da la vuelta.
Al llegar al límite inferior o superior del tipo de dato, comienza por el máximo o mínimo
respectivamente.
Para evitar esto existen dos versiones más seguras de las funciones strcpy() y strcat(). Se
llaman igual pero con una n intercalada.
Las dos añaden un nuevo parámetro que indica el número máximo de caracteres que se copian
o concatenan de la cadena2.
Con estas dos funciones podrás evitar el tan peligroso desbordamiento de buffer.
Otro problema que nos encontramos con los arrays de caracteres es al compararlos.
Hola";
char s1[5] = "
char s2[5] = " Hola";
if(s1 == s2){
Serial.println("Son iguales");
}
Memorias de Arduino 31
Utilizar el operador de igualdad siempre será false porque lo que realmente estás comparando
son direcciones de memoria.
Volvamos un poco hacia atrás. Cuando he empezado a hablar de la memoria SRAM he dicho
que es como un array gigante. Cada elemento de ese array es un byte es decir, una posición de
memoria.
De hecho, los índices del array no empiezan por cero. En el caso del ATmega328 el primer
elemento al que se puede acceder es el 256 y el último elemento es el 256 + 2048 (2304) ya que
la memoria SRAM del ATmega328 tiene 2048 bytes.
En otros microcontroladores, el índice del primer elemento será otro diferente ya que depende
de cada microcontrolador.
Una dirección de memoria es precisamente eso, el índice dentro del array de memoria. Puede
ser un valor entre 256 y 2304 en el caso del ATmega328.
Cuando se declara un array de caracteres o de otro tipo de dato, lo que realmente se almacena
es la dirección de memoria para saber dónde empieza el array.
Memorias de Arduino 32
Y esto tiene que ver mucho con los punteros que veremos más adelante. De momento lo único
que quiero que entiendas es que cuando estás comparando dos arrays de caracteres lo que
realmente comparas son sus direcciones de memoria.
if(s1 == s2){
Serial.println("Son iguales");
}
Y como es normal, no coinciden ya que cada array está almacenado en una dirección diferente.
Hola";
char s1 = "
char s2 = " Hola";
La función strcmp(cadena1, cadena2) compara las dos cadenas y devuelve un número entero:
Por ejemplo, con la función strcasecmp() el array “hola” es igual al array “HOLA” e igual al array
“Hola”.
Estas dos funciones también tienen su versión mejorada para comparar un número determinado
de caracteres.
Las dos funciones comparan los primeros numCaracteres de las cadenas cadena1 y cadena2.
Por último, para terminar esta sección, quiero mostrarte otra forma de crear una array de
caracteres donde la longitud del array viene determinada por la longitud de la cadena.
Memorias de Arduino 33
char cadena[] = "Esto es un texto";
En este ejemplo puedes ver que no se indica el tamaño del array. El tamaño lo determina la
longitud de la cadena. En concreto esta declaración reserva 17 bytes de memoria.
16 bytes para el texto y uno más para el carácter NULL de final de cadena.
La tercera forma que tenemos de declarar una cadena es a través de un puntero. Pero esto, lo,
veremos a continuación.
3.3 Punteros
Antes de ver como se declara una cadena a través de un puntero, tengo que aclarar qué es eso
de los punteros.
Estoy seguro que en alguna ocasión has oído o leído el término puntero y que, probablemente,
creas que es algo complicado de entender.
Sin embargo, debes quitarte ese miedo ya que el concepto de puntero es algo muy simple.
Ahora mismo verás que no es nada complejo.
Lo primero que tienes que entender antes de ver los punteros, es cómo funcionan internamente
las variables.
Hay variables que almacenan números enteros (int) , otras variables almacenan una cadena de
caracteres (String o array de caracteres) y algunas almacenan datos con decimales (float).
Más o menos debes ver las variables como una taquilla donde se guardan cosas. En cada
taquilla se guarda una variable concreta.
Memorias de Arduino 34
Hasta ahora esto nos ha servido para entender cómo funcionan las variables pero en realidad,
es un proceso un poco más complejo.
En la tabla de símbolos se crearían los siguientes registros correspondientes a las dos variables.
Memorias de Arduino 35
Tabla de símbolos
temperatura = 23;
En la tabla de símbolos se ve reflejado ese nuevo valor. Pero la dirección de memoria que se
almacena en lvalue no se modifica.
Tabla de símbolos
Y así es como funciona internamente una variable. Por un lado tenemos la dirección de memoria
(lvalue) y por otro tenemos el valor real (rvalue) .
Esto se suele representar con un diagrama que se conoce como diagrama lvalue-rvalue.
Con esta aclaración, ya podemos pasar a ver qué son los punteros.
Memorias de Arduino 36
3.3.2 ¿Qué es un puntero?
Una vez que tenemos claro qué son las variables y cómo funcionan realmente, estamos listos
para entender qué son los punteros.
En pocas palabras, un puntero no es más que una variable que almacena la dirección de
memoria de otra variable.
Según la terminología que acabamos de ver, un puntero es una variable cuyo rvalue es el lvalue
de otra variable.
Como ves es muy sencillo. Un puntero almacena la dirección de memoria de otra variable. A la
vez, un puntero también tiene su propio lvalue que indica la posición de memoria donde se
guarda ese dato.
int *puntero;
Lo primero que se indica es el tipo de dato. Ojo, no indica es el tipo de dato que se va
almacenar, es el tipo de dato de la variable cuya dirección de memoria vamos a almacenar. Son
cosas totalmente diferentes.
Por ejemplo, si quieres almacenar la dirección de memoria de una variable del tipo float utilizarás
el siguiente código.
float *puntero;
Memorias de Arduino 37
Y si es de tipo char será este otro código.
char *puntero;
Por otra parte, el asterisco (*) indica al compilador que esa variable es un puntero. Lo
encontrarás también con el nombre de operador de indirección.
int *puntero;
int* puntero;
int * puntero;
Por sí solo, un puntero que no apunta a ninguna dirección de memoria no es muy útil. Lo normal
es que el puntero guarde la dirección de memoria de otra variable.
Pero ¿cómo obtenemos la dirección de memoria de una variable? Cuando quieres obtener el
valor de la variable lo único que necesitas hacer es llamar a la propia variable.
Por ejemplo, si quieres mostrar por el monitor serie el valor de la variable numero solo tendrías
que escribir el nombre de la variable.
Serial.println(numero);
Sin embargo, para obtener la dirección de memoria se utiliza el operador de dirección (&).
Cuando se utiliza el operador de dirección sobre una variable, devuelve la dirección de memoria
es decir, lvalue.
Memorias de Arduino 38
Entonces, por un lado tenemos un puntero que almacena la dirección de memoria y por otro lado
tenemos el operador de dirección que obtiene la dirección de memoria de cualquier variable.
Para asignar una dirección de memoria de una variable a un puntero se utiliza el operador igual
(=). En el siguiente código tienes un ejemplo.
void setup(){
Serial.begin(9600);
// Asignar dirección de memoria a puntero
puntero = &variable;
}
void loop(){
}
Sencillo ¿no?
Lo que debe quedar claro es que con el operador de dirección se obtiene el lvalue de cualquier
variable. Cuando digo cualquier variable también se incluyen los punteros.
Memorias de Arduino 39
La asignación de la dirección de memoria al puntero se conoce como
referencia. Por esta misma razón el operador de dirección (&) se conoce
también como operador de referencia.
void setup() {
Serial.begin(9600);
Serial.println("variable");
Serial.print("lvalue (dirección de memoria): ");
Serial.println((long)&variable);
Serial.print("rvalue (valor real): ");
Serial.println(variable);
Serial.println("puntero");
Serial.print("lvalue (dirección de memoria): ");
Serial.println((long)&puntero);
Serial.print("rvalue (valor real): ");
Serial.println((long)puntero);
}
void loop() {
}
La primera es que cuando vas a mostrar por el monitor serie una dirección de memoria con el
operador de dirección, debes hacer una conversión a un tipo de dato que admita las funciones
Serial.pirnt(...) y Serial.println(...).
Serial.println((long)&variable);
Memorias de Arduino 40
Cuando utilizas el operador &variable lo que está devolviendo es un tipo de dato int* que indica
que es un puntero a un int.
Por eso hay que hacer la conversión a un tipo de dato que admiten las funciones de la clase
Serial como Serial.print y Serial.println. Esta conversión se hace poniendo entre paréntesis el
tipo de dato al que quieres convertir.
En este caso se convierte a tipo long. También se podría convertir a tipo int y funcionaría.
Serial.println((long)puntero);
Hay que hacer una conversión a un tipo de dato que admita la función Serial.println(...).
Si lo cargas en una placa de Arduino o en un ESP8266 y abres el monitor serie, verás algo
parecido a la siguiente imagen.
e la variable
Ojo, que las direcciones de memoria pueden variar. Lo interesante es que el lvalue d
es el mismo del rvalue del puntero.
Memorias de Arduino 41
La indirección de un puntero va más allá. Si sabemos la dirección de memoria de una variable
¿por qué no cambiar el valor de la variable a través de la dirección de memoria?
¿Cómo se hace eso? Pue a través del operador de indirección que se representa con un
asterisco (*).
void setup(){
Serial.begin(9600);
// Indirección
*puntero = 20;
Serial.print("variable = ");
Serial.println(variable);
}
void loop(){
}
¿Qué valor crees que se mostrará en el monitor serie para variable?
Si cargas este código y abres el monitor serie comprobarás que ahora variable vale 20. Eso sí,
sin haber asignado el valor a través de la propia variable como se suele hacer.
Esta vez lo hemos hecho a través del puntero, con el operador de indirección.
Memorias de Arduino 42
Y este es el poder de los punteros y de la indirección, que puedes cambiar valores de variables
sin utilizar la propia variable.
Ahora seguramente te estés pensando: bien, muy chulos los punteros pero ¿por qué demonios
quería utilizar un puntero en mis programas?
Todos lo que hemos visto anteriormente no son más que ejemplos simples de cómo trabajar con
punteros. Sin embargo, hay razones muy potentes por las que querrías utilizar los punteros.
Sobre todo cuando estás trabajando con microcontroladores (como los que utilizan las placas de
Arduino) y tienes que manejar muchas operaciones de bajo nivel.
Pero para que realmente sepas el valor que tienen los punteros, es imprescindible que conozcas
cómo funcionan las funciones en el lenguaje C/C++.
Ya hemos visto algo en una sección anterior cuando te hablaba de la clase String y lo que
sucede cuando se utiliza como parámetro de una función.
Básicamente, para no entrar en mucho detalle, hay dos formas de llamar a una función: por valor
y por referencia.
Memorias de Arduino 43
int suma(int x, int y){
// Sumar los dos números
int resultado = x + y;
return resultado;
}
void setup(){
Serial.begin(9600);
// Sumar dos números
int a = 3;
int b = 4;
int resultadoSuma = suma(a, b);
Serial.println(resultadoSuma);
}
void loop(){
}
El código es bastante sencillo, una función que suma dos números y una llamada a la función
desde setup().
Lo primero es declarar dos variables del tipo int que se llaman a y b a las que se asignan los
valores 3 y 4 respectivamente.
Luego se declara otra variable del tipo int con nombre resultadoSuma que se iguala a la llamada
a la función suma(). Esta función admite dos parámetros.
Al llamar a la función con las variables a y b en realidad se sustituye por sus valores (rvalue) y
por lo tanto la llamada a la función quedaría de la siguiente forma suma(3, 4).
En la función suma() se establecen los valores x=3 e y=4 dentro de la función ya que son los
argumentos que se han pasado.
A esto se le llama pasar por valor porque lo que realmente hemos hecho es pasar el valor de las
variables. Dentro de la función, los valores que se pasan (los valores de a y b en este caso) se
copian en las variables (x e y en este ejemplo).
Memorias de Arduino 44
Hasta ahora esto nos ha funcionado. Pasamos un valor a la función y dentro hace unas
operaciones para luego devolver un valor. Es algo simple.
Pero esto es realmente un problema si, por ejemplo, queremos que la función haga algo con las
variables originales. Por ejemplo cambiar de valor de esas variables originales.
Al pasar los argumentos por valor a la función, no se pueden cambiar las variables originales. Lo
que vengo a decir es que a y b son variables diferentes a x e y. Cada una está en una dirección
diferente de memoria aunque tengan los mismos valores.
x = 8;
y = 6 ;
Las variables a y b seguirán valiendo lo mismo. Su valor no ha variado aunque hayan variado x e
y porque son variables diferentes.
Por lo tanto, puedes cambiar los valores de x e y pero esto no afecta a las variables originales a
y b por que están en una región diferente en la memoria.
Además, las variables x e y dejan de existir en cuanto se ejecuta la sentencia return resultado.
Memorias de Arduino 45
Si lo que realmente quieres es modificar el valor de las variables originales, existe una forma de
hacerlo. Haciendo una llamada por referencia.
void setup(){
Serial.begin(9600);
int numeroA = 5;
sumaUno(&numeroA);
Serial.println(numeroA);
}
void loop(){
}
Lo primero es declarar una variable del tipo int que se llama numeroA a
la que asignamos el
valor de 5.
Esa dirección de memoria se asigna al parámetro de la función sumaUno que se llama numero y
que además es un puntero. Fíjate en la declaración, se hace igual que como hemos visto antes,
con el operador de indirección (*).
Gracias a que el parámetro numero e s un puntero, tenemos acceso directo a la variable original
numeroA a través de su dirección de memoria. Por lo tanto, con el operador de indirección (*)
podemos cambiar su valor directamente.
Memorias de Arduino 46
*numero = *numero + 1;
Memorias de Arduino 47
Y estas son las dos formas de pasar parámetros a una función: por valor y por referencia.
Ahora, vamos a seguir viendo más temas con punteros en concreto, con arrays.
void setup() {
Serial.begin(9600);
Memorias de Arduino 48
void loop() {
}
Para acceder a un elemento del array lo único que tienes que hacer es poner el nombre del
array y entre corchetes el número del elemento.
numArray[2]
Si cargas el código anterior y abres el monitor serie, verás algo parecido a la siguiente imagen.
Pero ¿qué pasa si te digo que hay otra forma de acceder a los elementos a través de su
dirección en memoria?
Antes, quiero que entiendas cómo se almacena un array en memoria. La representación gráfica
sería la siguiente.
Memorias de Arduino 49
Dos cosas a tener en cuenta en esta representación. Cada elemento del array ocupa 2
posiciones de memoria. Eso es debido a que el array almacena datos el tipo int que ocupan 2
bytes.
Y si te fijas, todos los elementos van en posiciones de memoria consecutivas. Una vez conoces
la primera posición, la del elemento 0, podrías acceder a los demás elementos con solo sumar 2
a la dirección de memoria.
Vuelvo a recalcar que la diferencia de 2 en las direcciones de memoria es debido a que se trata
de un tipo de dato int ( 2 bytes). Si fuera un array de tipo byte por ejemplo, habría que sumar de
uno en uno (1 byte).
Entonces ¿cómo obtenemos el valor de la dirección de memoria del primer elemento del array?
Pues es más sencillo de lo que parece ya que cuando se declara un array, lo que estamos
haciendo es declarar un puntero. Y la dirección de memoria a la que apunta es el primer
elemento del array.
void setup() {
Memorias de Arduino 50
Serial.begin(9600);
void loop() {
}
Justo después de declarar el array he puesto el siguiente código para mostrar por el monitor
serie la dirección de memoria del primer elemento del array.
Serial.println((long)numArray);
Recuerda que hay que convertir al tipo long poniendo entre paréntesis el tipo de dato al que se
quiere convertir la variable.
En la siguiente línea de código, con el operador de indirección (*), se accede al valor de esa
posición de memoria.
Serial.println(*numArray);
Si cargas el código a la placa verás el siguiente resultado con la diferencia que la dirección de
memoria puede no coincidir.
Memorias de Arduino 51
El primer número, el 2292, es la dirección de memoria. El segundo número, el 5, es el valor del
primer elemento.
Por lo tanto, si sabemos la dirección de memoria del primer elemento, podríamos recorrer el
array si vamos sumando de 2 en 2.
Por ejemplo, si el primer elemento está en la dirección 2292, el segundo elemento estará en la
posición 2294. El tercer elemento en la posición 2296 y el cuarto elemento en la posición 2298.
Con estas indicaciones, el siguiente código recorre un array a través de sus direcciones de
memoria.
void setup() {
Serial.begin(9600);
void loop() {
}
El código
Serial.print((long)(numArray + i));
Y la línea de código
Serial.println(*(numArray + i));
Muestra el valor real (rvalue) en esa posición de memoria utilizando el operador de indirección.
Carga el código a la placa y abre el monitor serie. Aparecerá algo parecido a la siguiente
imagen.
Memorias de Arduino 52
Vamos a analizar más de cerca el código anterior sobre todo donde se imprimen los valores
Serial.print((long)(numArray + i));
Serial.println(*(numArray + i));
La primera iteración del bucle i es igual a cero eso quiere decir que la dirección de memoria es
2292 + 0 = 2292. Esto corresponde con el primer elemento del array.
Memorias de Arduino 53
En la segunda iteración del bucle i = 1 y aquí es donde empieza a no cuadrar la cosa. Si sumas
2292 + 1 = 2293 sin embargo, la posición de memoria a la que se accede es la 2294.
La razón de todo esto se debe a cómo el compilador maneja los punteros. Cuando defines un
array, el compilador es lo suficientemente inteligente como para asignar la memoria en función
del tamaño de tipo de dato utilizado.
Memorias de Arduino 54
En este caso se utiliza un tipo de dato int que como he venido diciendo, en Arduino ocupa 2
bytes.
En este caso es dos pero si cambias el tipo de dato del array de int a byte, puedes comprobar
cómo en ese caso avanza de uno en uno
void setup() {
Serial.begin(9600);
void loop() {
}
El resultado es muy parecido pero ahora las direcciones de memoria avanzan de una en una.
Memorias de Arduino 55
Ahora que comprendes qué son los punteros, cómo funcionan y cómo se declaran los arrays, es
momento de pasar a ver cómo utilizar los puntos con cadenas de texto
Mientras que el objeto String u tiliza memoria dinámica que se almacena en el área heap y puede
producir fragmentación, la cadena de caracteres utiliza memoria estática que se almacena en el
área globals o en el área stack.
Pero hay otra forma de almacenar un texto a través de un puntero a una cadena. Es algo muy
común que verás en muchos sitios. La sintaxis sería la siguiente.
Las sintaxis es muy parecida a la declaración del puntero. Primero se pone el tipo de dato, char,
y a continuación el espacio y el operador de indirección que ya hemos visto (*).
En este puntero se almacena la dirección de memoria (rvalue) del primer carácter de la cadena.
Algo parecido a lo que sucede con los arrays.
Memorias de Arduino 56
Otra cosa a tener en cuenta es que en ningún momento se indica el tamaño de la cadena.
Cuando se inicializa con una cadena, el compilador reserva la memoria correspondiente más
una posición extra para el carácter NULL.
Si se declara como variable global se almacena en el área globals y si se declara como variable
local se almacena en el área stack.
La diferencia entre declarar un puntero a una cadena y un array de caracteres es muy sutil.
// Array de caracteres
char cadenaC[15] = "Otro texto";
// Puntero a cadena
char *cadena = "Esto es un texto";
Ya hemos visto que con un array de caracteres es fácil cambiar su valor de la cadena con las
funciones strcpy() o strcat().
Sin embargo, si intentas cambiar el valor de un puntero a una cadena puedes causar problemas
ya que es de sólo lectura.
Por eso, en muchas ocasiones verás que un puntero a una cadena va prefijado siempre como
constante con la palabra reservada const.
Esto lo que le dice al compilador es: nunca voy a cambiar el valor de esta variable, ni me dejes
intentarlo.
Por último quiero hablarte de cómo puedes recorrer un puntero a una cadena carácter a
carácter. Se hace igual que hemos visto con los arrays, a través de un bucle for.
Memorias de Arduino 57
void setup() {
Serial.begin(9600);
const char *cadena = "Esto es un texto";
void loop() {
}
Gracias al bucle recorro todas las posiciones de memoria empezando por la primera que está
almacenada en el puntero cadena. El resultado es que muestra la cadena por el monitor serie.
Y con esto damos por finalizado cómo utilizar un puntero a una cadena. En la siguiente lección
te voy a enseñar dos formas que existen de guardar cadenas en la memoria Flash en vez de
utilizar la SRAM.
Memorias de Arduino 58
3.5 Almacenar cadenas en memoria Flash
Una técnica muy utilizada para reducir el tamaño ocupado de la memoria SRAM es almacenar
las variables en la memoria Flash, la misma que se utiliza para almacenar el programa.
Utilizar este tipo de técnicas es más complejo que las diferentes formas que hemos estado
viendo a lo largo de este libro para almacenar una cadena de texto en la memoria SRAM.
Un caso típico es cuando tienes un montón de textos que quieres mostrar en una pantalla o
display. Estos textos normalmente son estáticos es decir, no se van a modificar y por lo tanto
sería interesantes almacenarlos en la memoria Flash.
Otro caso típico es cuando se muestran muchos mensajes por el monitor serie. Cualquier
llamada a las funciones Serial.print() y Serial.println() consumen memoria SRAM.
Algo parecido ocurre con PROGMEM. Este modificador le dice al compilador que guarde la
información en la memoria Flash en lugar de la SRAM, donde normalmente iría.
Este modificador forma parte de la librería pgmspace.h. No hace falta que añadas esta librería al
IDE de Arduino o que la incluyas al programa ya que se hace de forma automática.
Memorias de Arduino 59
cómo acceder a la información guardada en la memoria Flash.
Una forma muy común de utilizar PROGMEM e s con un array de datos en vez de con una
variable. Así es más sencillo y mucho más útil. Se utiliza de la misma forma.
Para usar PROGMEM s e debe hacer en dos pasos. Ya hemos visto cómo almacenar los datos
en la memoria Flash. El siguiente paso es obtener esos datos de la memoria Flash y eso se
hace a través de funciones definidas en la librería pgmspace.h.
En el enlace anterior verás muchas funciones para obtener los datos almacenados en la
memoria Flash con PROGMEM. Yo me voy a centrar en la función para obtener los datos de una
cadena de texto.
Si por ejemplo quieres trabajar con números debes utilizar la función pgm_read_byte_near.
Tenlo en cuenta.
Este sería el proceso que debemos seguir con los textos que queremos almacenar en la
memoria Flash y luego recuperarlos para utilizarlos en cualquier sitio. Mostrar por el monitor
serie o en algún display.
Memorias de Arduino 60
Un código de ejemplo sería el siguiente.
void setup() {
Serial.begin(9600);
void loop() {
}
Una variable con el modificador PROGMEM siempre tiene que ser una
variable global. No utilizar nunca como variable local.
Memorias de Arduino 61
En la función setup() se declara una variable local que es un array de caracteres que se llama
cadenaLocal. El tamaño de este array depende del tamaño de la cadena miCadena.
Para saber qué tamaño tiene se utiliza la función strlen_P(miCadena) que devuelve el número
de caracteres sin contar el carácter nulo. Por eso, a la hora de declarar el array se suma uno.
La idea es recorrer carácter a carácter de la cadena que está almacenada en la memoria Flash y
guardarlos en la variable local cadenaLocal.
Luego con esta cadena podemos hacer lo que queramos ya sea mostrar por el monitor serie o
mostrarlo a través de un display.
Realmente lo que se consigue es pasar de la memoria Flash a la memoria SRAM solo cuando
haga falta. Y al tratarse de una variable local, cuando termina de la ejecución de la función, se
libera la memoria.
A continuación te voy a mostrar otra técnica que te ayudará a no saturar la memoria SRAM con
mensajes a través del monitor serie.
Memorias de Arduino 62
3.5.2 Evitar consumir memoria con el macro F() para la librería Serial
Cuando estás programando con un Arduino o con un ESP8266 lo típico es que tengas en varios
puntos de tu código llamadas a las funciones Serial.print() y Serial.println() de la librería Serial.
Si eres un obseso como es mi caso de saber qué está sucediendo dentro del código, llenarás
todo el programa de llamadas a estas funciones.
En estos casos corremos el riesgo de quedarnos sin memoria SRAM y que el programa se
comporte de forma extraña.
Para evitar esto se utiliza el macro F() que básicamente lo que hace es almacenar una cadena
de texto en la memoria Flash en vez de almacenarlo en la memoria SRAM.
Se puede utilizar tanto con la librería Serial como con cualquier función de otra librería que
admita una cadena de texto.
Digamos que es una forma sencilla de guardar un texto en la memoria Flash sin complicaciones.
Lo bueno es que cuando se necesita acceder a esos datos, automáticamente copia la
información en la memoria SRAM para poder utilizarla.
Al igual que con el modificador PROGMEM, no se pueden cambiar los datos una vez llames al
macro F().
Ya hemos visto que PROGMEM puede almacenar cualquier tipo de dato pero el macro F() solo
funciona para cadenas de texto.
Por ejemplo, si quieres almacenar un HTML o un texto muy grande, no te recomiendo que
utilices el macro F(). Utiliza el modificador PROGMEM.
Al contrario de lo que ocurre con una variable que se declara como global y que puedes utilizar
una y otra vez sin ocupar más memoria, el macro F() siempre ocupa memoria aunque lo utilices
con la misma cadena de texto.
Memorias de Arduino 63
Mi recomendación es que lo utilices con las funciones como Serial.print y Serial.println a parte de
las funciones específicas de las librerías como LCD y OLED.
void setup() {
Serial.begin(9600);
void loop() {
}
Y con esto doy por finalizado este ebook donde hemos visto muchos temas relacionados con la
memoria de Arduino y su optimización.
Memorias de Arduino 64