Está en la página 1de 64

LUIS DEL VALLE 

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

3. Cadenas de texto en Arduino 19


3.1 Clase String 20
3.2 Cadenas nativas en C 26
3.3 Punteros 32
3.3.1 Funcionamiento interno de las variables 33
3.3.2 ¿Qué es un puntero? 35
3.3.3 Cómo declarar un puntero 35
3.3.4 La indirección de un puntero 39
3.3.5 Llamada a una función por valor 41
3.3.5 Llamada a una función por referencia 44
3.3.5 Arrays y punteros 46
3.4 Puntero a una cadena 53
3.5 Almacenar cadenas en memoria Flash 55
3.5.1 Reducir área globals con PROGMEM 56
3.5.2 Evitar consumir memoria con el macro F() para la librería Serial 59

Copyright 2020 Luis del Valle, programarfacil.com

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.

La CPU es el hardware encargado de interpretar las instrucciones de un programa informático


mediante operaciones matemáticas. Vamos, es el cerebro. El que manda las órdenes a las
entradas y salidas.

Por otro lado tenemos la memoria que es donde se almacenan los datos y los programas.

En todo momento la CPU y la memoria están en comunicación directa.

La arquitectura de los ordenadores establece cómo se diseña y configura este sistema


informático es decir, donde deben guardarse los programas, donde los datos y cómo debe ser la
comunicación entre la CPU y la memoria.

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.

Se ve claramente cuál es la diferencia ¿no?

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..

Mientras que la arquitectura Harvard tiene un rendimiento excelente, la arquitectura Von


Neumann es más flexible.

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.

1.1 Arquitectura de los microcontroladores


A estas alturas ya sabrás más o menos qué es un microcontrolador. No deja de ser una versión
reducida de un ordenador donde hay una CPU y una memoria.

Generalmente tiene una tarea bien definida que debe realizar de manera confiable y eficiente.

Por ejemplo, el Arduino UNO utiliza el microcontrolador ATmega328. Es un microcontrolador de


8-bit muy conocido y popular de la empresa Microchip Technology.

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.

Los programas se almacenan en la memoria Flash y los datos en la memoria SRAM.

Memorias de Arduino 6
Diagrama de bloques del ATmega328

Sin embargo, aunque estemos hablando de que el ATmega328 utilice la arquitectura de


sistemas informáticos Harvard, existen grandes diferencias entre un microcontrolador y un
ordenador personal.

La mayor diferencia es que trabajan a escalas diferentes. El ATmega328 tiene solo 32 KB de


memoria Flash y 2 KB de SRAM.

Si lo comparas con un PC de gama baja, tiene unas 100.000 veces menos de memoria.

Por lo tanto, un punto crítico en los microcontroladores es la optimización de la memoria. De esto


es de lo que te voy a hablar en este documento.

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.

Normalmente los microcontroladores que utiliza Arduino tienen 3 memorias:

● Memoria Flash
● Memoria SRAM
● Memoria EEPROM

No todos los microcontroladores tienen 3 memorias. Por ejemplo, el


ESP8266 no tiene memoria Flash, aunque parezca raro. Solo dispone de
memoria SRAM. La memoria Flash se añade posteriormente en los
módulos como el ESP-01 y el ESP-12.

No todas las placas de Arduino tienen el mismo microcontrolador. Dependiendo de cada modelo
suelen llevar un microcontrolador u otro.

Cada microcontrolador tiene sus propias características técnicas. En la siguiente tabla te


muestro la información sobre la memoria de los microcontroladores más usados por Arduino.

Arduino Microcontrolador Flash SRAM EEPROM

UNO ATmega328 32 KB 2 KB 1 KB

Memorias de Arduino 7
Leonardo, Micro ATmega32U4 32 KB 2,5 KB 1 KB

Mega ATmega5260 256 KB 8 KB 4 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.

2.1 Memoria Flash


La memoria Flash se utiliza principalmente para almacenar el programa ya compilado y algunos
datos. Es el equivalente al disco duro de tu ordenador.

Aquí también se guarda el bootloader.

El bootloader o gestor de arranque es un pequeño programa que se


ejecuta cuando se enciende el Arduino o se pulsa el botón de ​reset.​ La
función principal es esperar a que el IDE de Arduino envíe un nuevo
código compilado y guardarlo en la memoria Flash. Gracias al bootloader
nos permite programar un Arduino con solo un cable USB.

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.

Una memoria volátil es aquella memoria que se pierde al interrumpirse el


flujo eléctrico. La memoria Flash es una memoria NO volátil.

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 :)

Al compilar o cargar un programa a la memoria Flash de Arduino, en la consola de Arduino te


muestra información de la memoria libre y ocupada. Este puede ser un buen indicio para
detectar si tienes algún problema.

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.

2.2 Memoria EEPROM


La memoria EEPROM es otra forma de memoria no volátil, como la memoria Flash. Sin
embargo, esta memoria se puede leer y escribir desde un programa en ejecución al contrario
que la memoria Flash.

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.

La gestión de la memoria EEPROM depende exclusivamente de ti. Tu eres el encargado de


guardar, modificar y eliminar los datos.

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.

2.2 Memoria SRAM


La memoria estática de acceso aleatorio o SRAM (del inglés ​Static Random Access Memory)​
puede leerse y escribirse desde un programa en ejecución. Es como la memoria EEPROM.

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

Si cogemos como ejemplo el microcontrolador ATmega328 que utiliza el Arduino UNO, el


tamaño de la memoria SRAM es de 2 KB. Por lo tanto, el array tendrá 2.048 elementos (1 KB es
igual a 1.024 bytes ).

Aunque esté poniendo el ATmega328 como ejemplo, todo lo que vamos a


ver a continuación, se puede aplicar a cualquier microcontrolador
compatible con Arduino.

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.

¿Que hay en la memoria RAM entre el 0 y el 256?


Ha estas direcciones de memoria se les suele llamar “mágicas” y se
asignan al registro y los dispositivos del microcontrolador. Funciones
internas de Arduino como ​digitalWrite()​, utilizan estas direcciones.

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.

● Área ​globals​ o globales.


● Área ​heap​ o montículo.
● Área ​stack​ o pila.

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.

Las librerías que añades a tu código también utilizan variables globales y


harán que aumente este área.

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.​

const byte​ pinLed = ​4​; ​// Variable global

void​ ​setup​() {
​// Uso de la variable local
​pinMode​(pinLed, ​OUTPUT​);
}

void​ ​loop​() {
​int​ tiempo = ​2000​; ​// Variable local

​// Una variable global se puede utilizar en cualquier sitio


​digitalWrite​(pinLed, ​HIGH​);

​// La variable local solo se puede utilizar en su ámbito

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.

// Esta cadena ocupa 11 bytes en el área globals


const ​char* ​cadena = ​"Hola mundo"​;

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.

¿Qué es esto de asignar la memoria de forma dinámica?

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.

A esto se le llama memoria dinámica.

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.

La memoria dinámica se almacena en el área ​heap​.

En ocasiones, el propio programador se encarga de la gestión de memoria dinámica (reservar y


liberar la memoria). Lo hace a través de funciones como ​malloc(...)​ o ​free(...).​

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.

Y ahora se liberan el primer y tercer bloque.

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

El fenómeno de fragmentación ​heap​ es muy perjudicial para los sistemas integrados o


microcontroladores ya que desperdicia mucha memoria SRAM algo de lo que escasean los
microcontroladores.

No es obligatorio usar el área de memoria ​heap


Si el microcontrolador que estás utilizando tiene una memoria SRAM
pequeña (menos de 16 KB), te recomiendo no utilizar la memoria dinámica
ya que se desaprovecha mucha memoria SRAM.

2.2.3 Stack
El área de memoria ​stack​ contiene el resto de datos que se almacena en la SRAM:

1. Las variables locales es decir, las que su alcance es una función.


2. Los parámetros que se pasan a una función.
3. La propia llamada a la función.

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.​

int​ obtenerTemperatura(​byte​ pin){


​// Variable local
​int​ temperatura = ​analogRead​(pin);

​return​ temperatura;
}

Tamaño de la memoria ​stack


Muchos microcontroladores limitan el tamaño del área de memoria ​stack.​
No es el caso del ATmega328 pero por ejemplo, el ESP8266, restringe
este área a 4KB.

Cuando se bloquea el código y no sabes qué pasa


Si a veces el programa que estás ejecutando en un microcontrolador se
bloquea de una manera muy impredecible, es muy probable que el área de
memoria ​stack​ esté corrupta. Para solucionar este problema debes reducir
el consumo de este tipo de memoria o pasar a un microcontrolador con
una memoria SRAM mayor.

Y te estarás preguntando: ​en el área de memoria stack ¿no se produce fragmentación?

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.​

2.2.4 Cómo medir el uso de la memoria SRAM


Ya hemos visto que el tamaño del área de memoria ​globals​ se puede medir cuando se compila
el programa en el IDE de Arduino.

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​.

Para instalar la librería debes descargar el archivo .zip de GitHub e instalarla.

Lo realmente importante a la hora de trabajar con microcontroladores es optimizar al máximo el


uso de la memoria SRAM. Y uno de los mayores focos de problemas que me he encontrado son
las cadenas de texto.

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.

3. Cadenas de texto en Arduino


Trabajar con cadenas en un código puede ser una auténtica tortura.

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.

Recuerda que cada carácter ocupa un byte y se puede representar por su


código ASCII que no es más que un código numérico del 0 al 255.

Por ejemplo, la cadena “hola” se traduce en la siguiente secuencia de bytes:

Por lo tanto, una cadena como “hola” que tiene cuatro caracteres en realidad ocupa cinco bytes.

Hay tres formas de declarar una cadena de texto.

String​ s = ​"hola"​;
char​ s[] = ​"hola"​;
const​ ​char​* s = ​"hola"​;

Aunque todas se almacenan en la memoria SRAM, no todas afectan por igual.

La forma de declarar una cadena de texto más perjudicial para la memoria SRAM es utilizando la
clase ​String.​

Y es, precisamente, la primera que vamos a ver.

3.1 Clase String


Cuando empiezas con Arduino encuentras en la clase ​String ​un aliado para trabajar con las
cadenas de texto de una forma sencilla.

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:

● Globals,​ que es un área fija y almacena las variables globales.


● Heap​ que almacena la memoria dinámica.
● Stack​ que almacena las variables locales.

También te he hablado de la fragmentación del área de memoria ​heap.​ Básicamente lo que


sucede es que quedan huecos vacíos sin poder ser ocupados y eso produce que el área de
memoria ​heap​ esté desaprovechado.

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.

Para que entiendas cómo afecta la utilización de la clase ​String​ a la fragmentación de la


memoria, imagínate que tienes un montón de piezas puestas una detrás de otra.

Y según llegan nuevas piezas se añaden al final de la fila

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​.

Vamos a verlo con un ejemplo.

Memorias de Arduino 23
String​ s1 = ​"hola"​;
String​ s2 = ​"Luis"​;
// El operador + con objetos String concatena las dos cadenas
String​ s3 = s1 + ​" "​ + s2;

¿Cuántas cadenas de texto cuentas aquí?

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.

Por lo tanto, tampoco es una buena opción.

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.

Vamos a ver otro ejemplo que te va a sorprender.

void​ imprimirValor(​String​ s1, ​String​ s2) {


​Serial​.​print​(s1);
​Serial​.​print​(​" = "​);
​Serial​.​println​(s2);
}

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​ imprimirValor(​String​ &s1, ​String​ &s2) {


​Serial​.​print​(s1);
​Serial​.​print​(​" = "​);
​Serial​.​println​(s2);
}

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.

3.2 Cadenas nativas en C


Una cadena nativa de C no es más que un array de caracteres. Es como cualquier otro array la
única diferencia es que debe obedecer ciertas reglas. La regla más importante es que el último
elemento del array tiene que ser el carácter nulo (​NULL)​ .

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​.

Aunque se haya reservado el espacio para 30 caracteres en realidad solo


vas a poder utilizar 29. No olvides que siempre hay que dejar el último
hueco para el carácter NULL.

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.

Asignar un valor inicial al array no es complejo.

char​ cadena[​30​] = ​"Esto es una cadena"​;

Pero ¿qué pasa si quieres modificar el valor del array en mitad del código? Con un objeto ​String
es bastante sencillo.

​ Esto es una cadena"​;


String​ cadena = "
cadena = ​"Ahora es otra cadena"​;

Pero con un array de caracteres no puedes hacer lo mismo. Para cambiar un array tienes que ir
elemento por elemento.

char​ cadena[​30​] = ​"Esto es una cadena"​;

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.

char​ cadena[​30​] = ​"Esto es una cadena"​;


strcpy(cadena, ​"Ahora es otra cadena"​);
// Ahora cadena vale Ahora es otra cadena

char​ cadena2[​3​] = ​"Y otra cadena mas"​;


strcpy(cadena, cadena2);
// Ahora cadena vale Y otra cadena mas

La función ​strcpy(cadena1, cadena2)​ admite dos parámetros:

● cadena1​: es el array de caracteres donde se va a copiar el nuevo texto


● cadena2​: es el array de caracteres o literal de donde se va a copiar el texto

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().

char​ s1[​5​] = ​"hola"​;


char​ s2[​5​] = ​"Luis"​;
char​ s3[​10​] = ​""​;

strcat(s3, s1);
strcat(s3, ​" "​);
strcat(s3, s2);

La función ​strcat(cadena1, cadena2)​ admite dos parámetros:

● cadena1​: cadena final donde se concatena cadena2


● cadena2​: cadena originen para concatenar en cadena1

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.

¿Qué pasa si intento sustituir la cadena por una cadena mayor?

char​ cadena[​5​] = ​"Hola"​;


strcpy(cadena, ​"Adios"​);

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.

strncpy(cadena1, cadena2, numCaracteres);


strncat(cadena1, cadena2, numCaracteres);

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.

¿Cómo?¿Direcciones de memoria? Sé que te acaba de explotar la cabeza pero no te


preocupes, es más fácil de lo que parece.

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.

Para poder comparar carácter a carácter se utiliza la función ​strcmp().​

​ Hola"​;
char​ s1 = "
char​ s2 = "​ Hola"​;

if​(strcmp(s1, s2) == 0){


​Serial​.​println​(​"Son iguales"​);
}

La función ​strcmp(cadena1, cadena2)​ compara las dos cadenas y devuelve un número entero:

● Si el resultado es 0 es que las cadenas son idénticas.


● Si el resultado es diferente de cero es que las cadenas son diferentes.

Otra función extremadamente útil es ​strcasecmp(cadena1, cadena2)​. Hace exactamente lo


mismo que la función ​strcmp(cadena1, cadena2)​ pero no importa si los caracteres del array
están en mayúsculas o minúsculas.

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.

strncmp(cadena1, cadena2, numCaracteres);


strncasecmp(cadena1, cadena2, numCaracteres);

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.

3.3.1 Funcionamiento interno de las variables


Las variables no son más que contenedores de datos. Una variable sirve para almacenar un tipo
de dato concreto.

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.

Cuando se declara una variable en un programa, el compilador comprueba en la tabla de


símbolos si esa variable con ese nombre existe y de no ser así la añade a la tabla de símbolos.

La tabla de símbolos es básicamente una lista o tabla de las variables que


se declaran en un programa.

Cada variable tiene 4 campos que hay que conocer:

● Identificador​: es el identificador o nombre de la variable. Recuerda que dos variables no


pueden tener el mismo nombre.
● Tipo de dato​: indica el tipo de dato que almacena la variable.
● lvalue​: es la dirección de memoria donde se almacena la variable.
● rvalue​: es el valor real de la variable.

Imagínate que se declaran estas dos variables.

int​ temperatura = ​0​;


byte​ pinLed = ​7​;

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

Identificador Tipo de dato lvalue rvalue

temperatura int 2298 0

pinLed byte 2296 7

Si por ejemplo actualizas el valor de una de las variables.

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

Identificador Tipo de dato lvalue rvalue

temperatura int 2298 23

pinLed byte 2296 7

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.

3.3.3 Cómo declarar un puntero


Un puntero se declara como cualquier otra variable salvo una particularidad que ahora veremos.
En el siguiente código te muestro un ejemplo.

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;

En los dos casos se almacena un número que indica la dirección de memoria.

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.

Debido a que el espacio en blanco no influye en el lenguaje C++, puedes encontrar la


declaración de un puntero de las siguientes formas.

int​ *puntero;
int​* puntero;
int​ * puntero;

Estos tres casos en una declaración correcta de un 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.

Ahora sólo queda saber cómo juntar estas dos partes.

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.

int​ variable = ​5​;


int​ *puntero;

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.

Siempre que se aprende un nuevo concepto es interesante ponerlo en práctica. El siguiente


código muestra por el monitor serie ​lvalue​ y ​rvalue​ de una variable y un puntero.

int​ variable = ​5​;


int​ *puntero;

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);

​// Asignar dirección de memoria a puntero


puntero = &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​() {
}

De este código quiero hacer varias puntualizaciones.

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(...).​

Es lo que hecho en la sentencia

​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.

Lo mismo sucede cuando intentas mostrar por el monitor serie un puntero.

​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.

3.3.4 La indirección de un puntero


Ya hemos visto que un puntero almacena la dirección de memoria de otra variable gracias al
operador de dirección o referencia (&).

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?

Básicamente lo que hace la indirección es acceder a la posición de memoria que está


almacenada en ​rvalue​ del puntero, y cambia el valor que haya guardado allí.

¿Cómo se hace eso? Pue a través del operador de indirección que se representa con un
asterisco (*).

Vamos a verlo con un código muy sencillo.

int​ variable = ​5​;


int​ *puntero;

void​ ​setup​(){
​Serial​.​begin​(​9600​);

​// Asignar dirección de memoria a puntero


puntero = &variable;

​// 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.

3.3.5 Llamada a una función por valor


Vamos a empezar por lo más fácil que es hacer una llamada a una función por valor. En el
siguiente código te pongo un ejemplo.

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.

Si por ejemplo dentro de la función haces

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.

3.3.5 Llamada a una función por referencia


La llamada por referencia a una función es muy útil si quieres cambiar el valor del parámetro que
pasas a una función o si quieres devolver más de un valor desde una función.

Esto se consigue gracias a los punteros.

Vamos a ver un ejemplo.

void​ sumaUno (​int​ *numero){


*numero = *numero + ​1​;
}

void​ ​setup​(){
​Serial​.​begin​(​9600​);
​int​ numeroA = ​5​;
sumaUno(&numeroA);
​Serial​.​println​(numeroA);
}

void​ ​loop​(){
}

Piensa en lo que está sucediendo aquí.

Lo primero es declarar una variable del tipo ​int​ que se llama ​numeroA a
​ la que asignamos el
valor de 5.

Luego se llama a la función ​sumaUno()​ a la que se pasa como parámetro la dirección de


memoria (​lvalue​) de la variable ​numeroA​. Recuerda que para obtener la dirección de memoria se
debe utilizar el operador de dirección o referencia (&).

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 (*).

void​ sumaUno (​int​ *numero){

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​;

Si cargas el código anterior en tu placa de Arduino o ESP8266, comprobarás como la variable


numeroA​ aumenta en uno sin hacer una asignación directa.

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.

3.3.5 Arrays y punteros


Creo que ya estarás familiarizado con los arrays. Sobre todo con la forma habitual de utilizar los
arrays.

Veamos un ejemplo muy sencillo.

void​ ​setup​() {
​Serial​.​begin​(​9600​);

​int​ numArray[​4​] = {​5​, 9


​ ​, ​12​, ​98​};
​// Indices 0, 1, 2, 3

​for​ (​int​ i = ​0​; i < ​4​; i++) {


​Serial​.​print​(​"Elemento["​);
​Serial​.​print​(i);
​Serial​.​print​(​"] = "​);
​Serial​.​println​(numArray[i]);
}
}

Memorias de Arduino 48
void​ ​loop​() {
}

Es un ejemplo típico. Se declara un array de 4 elementos que contiene los números 5, 9, 12 y


98. Para recorrer el array se utiliza un bucle ​for​ que recorre los índices que van del 0 al 3.

Recuerda que los índices de un array empiezan en 0 y no en 1.

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.

Lo puedes comprobar con el siguiente código.

void​ ​setup​() {

Memorias de Arduino 50
​Serial​.​begin​(​9600​);

​int​ numArray[​4​] = {​5​, ​9​, ​12​, ​98​};


​Serial​.​println​((​long​)numArray);
​Serial​.​println​(*numArray);
}

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​);

​int​ numArray[​4​] = {​5​, ​9​, ​12​, ​98​};

​for​ (​int​ i = ​0​; i < ​4​; i++) {


​Serial​.​print​(​"Direccion["​);
​Serial​.​print​((​long​)(numArray + i));
​Serial​.​print​(​"] = "​);
​Serial​.​println​(*(numArray + i));
}
}

void​ ​loop​() {
}

El código

Serial​.​print​((​long​)(numArray + i));

Muestra la dirección de memoria (​lvalue​) del elemento correspondiente.

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.

Y precisamente eso es lo que muestra en el monitor serie.

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.

Una de dos, o hace magia o el compilador no sabe sumar.

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.

Cuando se recorre un puntero de un array, el compilador es lo suficientemente inteligente como


para multiplicar el iterador (variable ​i​ del bucle ​loop)​ por el tamaño del tipo de datos que utiliza el
bucle.

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​);

​byte​ numArray[​4​] = {​5​, ​9​, ​12​, ​98​};

​for​ (​int​ i = ​0​; i < ​4​; i++) {


​Serial​.​print​(​"Direccion["​);
​Serial​.​print​((​long​)(numArray + i));
​Serial​.​print​(​"] = "​);
​Serial​.​println​(*(numArray + i));
}
}

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

3.4 Puntero a una cadena


Hasta ahora lo que hemos visto es que para almacenar una cadena de texto lo podemos hacer a
​ a través de un array de caracteres.
través del objeto ​String o

String​ cadenaS = " ​ Esto es una cadena"​;


char​ cadenaC[​15​] = ​"Otra cadena"​;

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.

char​ *cadena = ​"Esto es un texto"​;

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.​

En ningún caso se almacena en el área ​heap.​

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​.

const​ ​char​ *cadena = ​"Esto es un texto"​;

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"​;

​for​ (​int​ i = ​0​; i < strlen(cadena); i++) {


​Serial​.​print​(*(cadena + i));
}
}

void​ ​loop​() {
}

En el código anterior he utilizado la función ​strlen(cadena)​ para obtener la longitud de una


cadena de texto.

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.

El objetivo es liberar lo máximo posible 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.

Así se evitaría la saturación de la memoria SRAM.

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.

A continuación, vamos a ver soluciones para los dos casos.

3.5.1 Reducir área globals con PROGMEM


La palabra reservada ​PROGMEM ​es un modificador de variables. Es como cuando utilizamos la
palabra reservada ​const​ o ​volatile​.

Lo que hacen todos estos modificadores es precisamente eso, modificar el comportamiento de la


variable. Si por ejemplo pones ​const​ indicas al compilador que esa variable no se puede
mdificar.

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.

La sintaxis sería la siguiente.

const​ ​char​ miCadena[] PROGMEM = ​"Esto es un texto"​;

Es importante que este tipo de variables vayan marcadas con ​const


(constantes) ya que la memoria Flash es de sólo lectura. Luego veremos

Memorias de Arduino 59
cómo acceder a la información guardada en la memoria Flash.

Bueno en realidad el modificador ​PROGMEN​ puede ir en cualquier sitio.

const​ PROGMEM ​char​ miCadena[] = "


​ Esto es un texto"​;
const​ ​char​ PROGMEM miCadena[] = "​ Esto es un texto"​;

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.

const​ PROGMEM ​int​ arrayNumeros[] = {​610​, ​256​, ​128​, ​64​};

El modificador PROGMEM puede ser utilizado con cualquier tipo de


variable. No tiene que ser sólo con textos.

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.

const​ ​char​ miCadena[] PROGMEM = ​"Esto es un texto que se almacena en Flash"​;

void​ ​setup​() {
​Serial​.​begin​(​9600​);

​// La función strlen_P devuelve el tamaño de la cadena


​Serial​.​print​(​"Tamaño cadena: "​);
​Serial​.​println​(strlen_P(miCadena));

​// Variable local del mismo tamaño


​char​ cadenaLocal[strlen_P(miCadena) + ​1​];

​for​ (​byte​ i = ​0​; i < strlen_P(miCadena) + ​1​; i++) {


cadenaLocal[i] = pgm_read_byte_near(miCadena + i);
}

​// Mostrar en el monitor serie o en una pantalla


​Serial​.​print​(cadenaLocal);
}

void​ ​loop​() {
}

El código comienza declarando un array de caracteres, ​miCadena,​ constante con el modificador


PROGMEM​ que almacena el texto en la memoria Flash.

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.

char​ cadenaLocal[strlen_P(miCadena) + ​1​];

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.

Si ejecutas el código anterior obtendrás un resultado parecido a este.

Esta técnica nos puede ayudar a optimizar la memoria SRAM al máximo.

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.

Debes tener en cuenta varias cosas cuando usas el macro F().

No se pueden cambiar los datos

Al igual que con el modificador ​PROGMEM,​ no se pueden cambiar los datos una vez llames al
macro F().

Solo funciona con cadenas

Ya hemos visto que ​PROGMEM​ puede almacenar cualquier tipo de dato pero el macro F() solo
funciona para cadenas de texto.

No es muy óptimo para grandes bloques 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​.

Optimización de memoria RAM

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.

A continuación te dejo un ejemplo del uso del macro F().

void​ ​setup​() {
​Serial​.​begin​(​9600​);

​Serial​.​println​(F(​"Este texto se almacena en la memoria Flash"​));


​Serial​.​println​(F(​"Otro texto más a la memoria Flash"​));
​Serial​.​println​(F(​"Y este otro también"​));
}

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.

La memoria SRAM de un microcontrolador es un factor muy crítico debido a su tamaño reducido.


Siempre que puedas intenta ocupar lo mínimo posible y optimizarla.

Memorias de Arduino 64

También podría gustarte