Kitabı oxu: «Arduino. Trucos y secretos.»
Arduino
Trucos y secretos
120 ideas para resolver
cualquier problema
Arduino
Trucos y secretos
120 ideas para resolver
cualquier problema
Paolo Aliverti
Edición original publicada en italiano por Edizioni LSWR
con el título: Arduino trucchi e segreti, © 2018 Edizioni Lswr*
Título de la edición en español:
Arduino. Trucos y secretos
Primera edición en español, año 2019
© 2019 MARCOMBO, S.A.
www.marcombo.com
Traducción: Sònia Llena Hurtado
Corrección: Genís Monraba
Maquetación: Giancarlo Salinas
Directora de producción: Mª Rosa Castillo
Cualquier forma de reproducción, distribución, comunicación pública o transformación de esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción prevista por la ley.
Esta publicación contiene opiniones del autor y tiene por objeto proporcionar información clara y precisa. El procesamiento de textos, incluso si se realiza con una atención escrupulosa, no puede asumir la responsabilidad específica del autor y/o editor de los posibles errores o imprecisiones.
El editor ha hecho todo lo posible para obtener y citar las fuentes exactas de las ilustraciones. Si en algunos casos no ha sido posible encontrar alguna con derechos de autor, está disponible para hacer frente a las omisiones involuntarias o errores en las referencias citadas.
ISBN: 978-84-267-2727-5
D.L.: B-13448-2019
Impreso en Prodigitalk
Printed in Spain
Sumario
1.Para empezar
1. Instalar Arduino
2. Cargar un sketch en Arduino
3. Programar Arduino con Arduino Web Editor
4. Cargar un sketch con Arduino Web Editor
5. Simular Arduino con TinkerCAD Circuits
6. Programar Arduino por bloques con Bitbloq
2.Programar Arduino
7. Crear un sketch vacío
8. Utilizar una variable
9. Definir una constante
10. Manipular cadenas de caracteres
11. Utilizar las cadenas
12. Convertir una variable de un tipo a otro
13. Repetir una operación
14. Utilizar un array
15. Obtener la longitud de un array
16. Copiar o ampliar un array
17. Utilizar arrays bidimensionales (matrices)
18. Definir una prueba simple
19. Definir una prueba con más de una alternativa
20. Definir una función
21. Crear clases y objetos
22. Generar números aleatorios
23. Encontrar el número más alto en un array
24. Ordenar una matriz de números
25. Rellenar un array de números aleatorios
26. Transformar una cadena en una matriz (tokenizar)
27. Gestionar índices múltiples y periódicos
28. Trabajar con bits
29. Crear un módulo
30. Utilizar librerías externas
31. Crear librerías propias
3.Los básicos: LED y botones
32. Encender un LED
33. Controlar la luminosidad de un LED
34. Utilizar un LED RGB
35. Crear un PWM en todos los pines (PWM software)
36. Generar una secuencia aleatoria de númerospara encender varios LED
37. Conectar botones
38. Debouncing
39. Mantener el estado de un botón
40. Identificar la pulsación y la liberación de un botón (bordes ascendentes y descendentes)
41. Medir el tiempo de pulsación de un botón
42. Obtener clics múltiples sobre un botón
43. Utilizar una entrada analógica
44. Botón en línea analógica
45. Utilizar un trimmer para configurar parámetros
46. Insertar pausas «largas»
47. Ganar seis pines digitales
48. Utilizar la consola para enviar y recibir comandos
49. Encender cuatro LED en secuencia con un botón
50. Convertir un valor de un intervalo al otro
51. Adivina el LED (programa por estados)
53. Controlar el llenado de una bañera (máquina con estados)
54. Encender una caldera
55. Simon
56. Marcar un número pulsando varios botones
4.Los sensores
57. Alimentar a Arduino
58. Medir una tensión
59. Medir una corriente
60. Conectar un sensor de temperatura (termistor)
61. Conectar un sensor de temperatura LM35
62. Conectar un sensor de luz
63. Conectar un sensor para la humedad
64. Utilizar un sensor digital para humedad y temperatura
65. Detectar un gas
66. Detectar campos magnéticos
67. Detectar la inclinación con un sensor tilt
68. Detectar el movimiento con un PIR
69. Crear un botón táctil o touch sensor
70. Utilizar un sensor de infrarrojos para descodificar un mando a distancia
71. Encender la TV con Arduino
72. Detectar un obstáculo con un sensor infrarrojo
73. Medir la distancia con un sensor a ultrasonidos
74. Utilizar la fecha y la hora actuales
75. Utilizar un teclado numérico
76. Detectar la orientación o el movimiento con acelerómetros y giroscopios
78. Encontrar el norte en una brújula
79. Conocer la posición geográfica con un GPS
80. Conectar un micrófono y realizar un clap control
81. Conectar una fuente de audio
82. Leer etiquetas RFID
83. Aislar una entrada
5.Los actuadores y la visualizaciónde información
84. Encender una bombilla de 12 V con un transistor
85. Controlar un relé con un transistor
86. Encender una bombilla de 12 V con un MOSFET
87. Controlar la velocidad de un motor
88. Controlar la dirección de rotación de un motor c.c.
89. Utilizar un servomotor
90. Encender una bombilla a 220 V
91. Interruptor crepuscular para una bombilla de 220 V
92. Utilizar un motor paso a paso (stepper)
93. Aislar las salidas
94. Aumentar las salidas con un registro de desplazamiento
95. Utilizar una pantalla de siete segmentos
96. Utilizar una pantalla LCD
97. Controlar una pantalla con solo dos cables (I2C)
98. Diseñar caracteres personalizados en una pantalla LCD
99. Crear un terminal de pantalla LCD
100. Crear un menú multinivel con una pantalla LCD
101. Conectar una pantalla gráfica TFT
102. Generar sonidos
103. LED NeoPixel
104. Generar una señal analógica
105. Conectar un amplificador
106. Controlar un potenciómetro digital
107. Reproducir un archivo de audio mp3 o wav
6.Comunicar e intercambiar datos
108. Añadir un nuevo puerto serie (SoftwareSerial)
109. Conectarse a un puerto RS232
110. Conectarse con un puerto RS485
111. Comunicar dos arduinos con el protocolo I2C
112. Comunicar dos arduinos con el protocolo SPI
113. Controlar Arduino con Firmata
114. Utilizar Arduino como consola para Processing
115. Controlar un LED por Bluetooth con un smartphone
116. Encender un LED con Python
117. Transmitir datos de Arduino a Python
118. Conectarse a Internet y ver una página web
119. Arduino como servidor web
120. Publicar una página web dinámica
121. Encender un LED con el navegador
122. Enviar un e-mail con Arduino pulsando un botón
123. Enviar SMS con Arduino
124. Recibir SMS
125. Leer y escribir un archivo desde una tarjeta SD
126. Generar una señal de TV
127. Emular un ratón USB
128. Emular un teclado USB
7.Índice analítico

Para empezar
En esta sección, encontrarás respuesta a los problemas más comunes de instalación y configuración de Arduino. Además, verás distintas alternativas para escribir proyectos y cargarlos en la placa, escribiendo código o colocando bloques.
La primera vez que nos acercamos a un nuevo producto, todo nos parece difícil y oscuro. Incluso Arduino, aunque pueda parecer sencillo, puede mostrar dificultades al inicio. Los problemas mayores surgen durante la fase de instalación, si bien con el tiempo el proceso de setup() ha sido mejorado y, actualmente, no es habitual tener ningún problema. Con las primeras placas, como Arduino Diecimila o Duemilanove, siempre surgía el drama de la instalación de los controladores por el puerto serie, caso que, hoy en día, es muy extraño que ocurra.
En esta primera sección encontrarás algunos «trucos» para resolver los problemas que podrías encontrar en la fase de instalación y configuración del producto. El método clásico para programar Arduino es el de utilizar su entorno de desarrollo (IDE). Hoy en día es posible programar Arduino también con plataformas en la nube (online) e, incluso, simularlo, de manera que la mayoría de los proyectos o sketch y de los ejemplos se pueden probar aun sin disponer de una placa real.
El gran éxito que ha obtenido Scratch (https://scratch.mit.edu), un sistema desarrollado por el MIT para escribir código colocando elementos gráficos, también ha llegado a Arduino, definiendo casi un estándar para este tipo de interfaz de programación. Por esta razón se pueden encontrar sistemas como TinkerCAD Circuits y Bitbloq con los cuales la programación de Arduino se lleva a cabo mediante bloques. Con estos métodos es complicado escribir código complejo, pero son muy eficaces para que niños y principiantes se acerquen con éxito a la programación.
1. Instalar Arduino
Descarga Arduino IDE desde el sitio web http://www.arduino.cc/en/Main/Software y sigue las instrucciones que te proporciona el sistema (Windows, Mac o Linux). Es imprescindible que instales Arduino IDE en tu ordenador para poder programar Arduino.

Figura 1.1 – Descarga del software para Arduino.
Al detalle
Visita la dirección web http://www.arduino.cc/en/Main/Software y descarga la última versión del programa. En el momento de escribir este libro, la versión publicada es la 1.8.5 y está disponible para Windows, Mac OS X y Linux. Elige la versión compatible con tu sistema y descárgala en una carpeta.
Windows
El procedimiento de instalación de Arduino es en general bastante sencillo.
Si trabajas con Windows, puedes consultar las instrucciones publicadas en el sitio web de Arduino (http://www.arduino.cc/en/Guide/Windows).
Para empezar, consigue una placa Arduino y un cable para conectarla al ordenador. El cable depende de la versión de Arduino que hayas comprado. Para la más habitual, Arduino Uno, necesitarás un cable USB tipo A-B, de los que se utilizan para las impresoras (Figura 1.2). El conector USB macho tipo B es de forma rectangular, mientras que el conector macho de tipo A tiene forma cuadrada, con dos bordes redondeados.
Si dispones de un Arduino Mega, un Due o un Leonardo, deberás utilizar un cable micro-USB: el cable que se utiliza normalmente para cargar los teléfonos móviles Android.

Figura 1.2 – Cable USB de tipo A-B, adecuado para Arduino Uno.
Si la placa es nueva, conecta el cable y comprueba que el conector encaja bien en la clavija. Si utilizas un cable micro-USB, fíjate porque, aunque la toma micro-USB se encuentra soldada sobre el circuito impreso, es muy fácil romperla con una pequeña flexión.
Conecta Arduino al ordenador: la placa debería encenderse. Todas las placas Arduino cuentan, como mínimo, con un LED que empezará a parpadear, es decir, indicará algún tipo de actividad. Probablemente1 Windows detectará la presencia de nuevo hardware y solicitará la búsqueda o instalación de los nuevos controladores. Con las últimas versiones del programa, el procedimiento de instalación se ha simplificado notablemente y resulta muy raro tener que instalar dichos controladores de forma manual, si bien no lo es para algunas placas en concreto.
La descripción oficial del procedimiento de instalación, en el sitio web de Arduino, dice:
1.Conecta Arduino al ordenador y espera a que Windows inicie el proceso de instalación de los controladores hasta que la instalación falle.
2.Abre el Panel de control, pulsa en «Sistema y seguridad». Luego, en «Sistema».
3.En la ventana «Sistema», abre «Administrador de dispositivos».
4.En la lista de hardware, busca la opción «Puertos (COM y LPT)». Entre las opciones, deberías ver un puerto denominado «Arduino Uno (COMxx)». Si no encuentras el periférico, busca en «Otros dispositivos» o «Dispositivos desconocidos».
5.Pulsa con el botón derecho del ratón sobre «Arduino Uno (COMxx)» y elige «Actualizar el software de controlador».
6.Elige «Examinar las carpetas para buscar el controlador».
7.Navega por las carpetas donde has descargado y desempaquetado el programa de Arduino y localiza el archivo arduino.inf que encontrarás en la carpeta «Driver» (¡no en la carpeta «FTDI USB Drivers»!). En las versiones más antiguas de Arduino IDE (hasta la 1.0.3) este archivo de denomina Arduino Uno.inf.
8.Windows reconocerá el controlador y la instalación finalizará.
Ya puedes iniciar Arduino pulsando sobre el icono que aparece en el Escritorio o en el menú de inicio.
Mac OS X
Para OS X, el procedimiento de instalación es aún más sencillo. Puedes consultar las instrucciones en el sitio web de Arduino (http://www.arduino.cc/en/Guide/MacOSX).
Debes descargar un archivo comprimido (.zip) en tu ordenador. Normalmente, dicho archivo acaba en la carpeta «Descargas» o en el Escritorio. Descomprime el archivo con un doble clic y verás otro archivo con el nombre Arduino.app: arrástralo a la carpeta «Aplicaciones».
Por lo general, con las nuevas placas no deberás hacer nada más, ni tendrás que instalar ningún tipo de controlador. Los controladores FTDI te servirán en el caso de que quieras utilizar una placa «Duemilanove». Puedes encontrar estos controladores en el sitio web de FTCI (http://www.ftdichip.com/Drivers/VCP.htm).
Inicia Arduino con un doble clic sobre el icono situado dentro de la carpeta «Aplicaciones».
GNU/Linux
Desde la versión diez de Ubuntu, puedes encontrar Arduino en los repositorios de sistema («Universe»). Utiliza synaptic para localizar el paquete e instalarlo. Si no, puedes abrir un terminal y escribir:
sudo apt-get update && sudo apt-get install arduino arduino-core
Deberás introducir tu contraseña porque estás utilizando el comando sudo. Cuando finalice la instalación, Arduino estará entre los programas instalados.
Si el software no se inicia, quizás tengas que instalar Java. Instala el paquete open-jdk utilizando synaptic o desde un terminal.
Comprueba si tu usuario forma parte del grupo «dialout», para que tenga los permisos para utilizar los puertos serie. De no ser así (o en caso de duda), únete al grupo con:
sudo usermod -aG dialout pablo
Sustituye pablo por el nombre de tu usuario. De forma alternativa, puedes lanzar Arduino desde un terminal utilizando el comando sudo:
sudo ./arduino
2. Cargar un sketch en Arduino
Para cargar un sketch en Arduino, pulsa el botón «Verify» situado en la barra de herramientas del IDE y, después, haz clic en «Upload». También se puede cargar directamente pulsando simplemente en «Upload».
Al detalle
Mientras trabajas con Arduino, tendrás que cargar continuamente nuevos sketch en la placa. La operación de carga es muy sencilla y rápida, y ha caracterizado el éxito de Arduino. Hace unos años, el procedimiento para programar un microcontrolador era bastante complejo, pero actualmente es cuestión de segundos. Para cargar un nuevo sketch en la placa, solo necesitas un cable USB del tipo adecuado. El cable depende del tipo de placa y, por desgracia, ¡cada modelo necesita uno distinto! Para programar Arduino Uno, la placa más utilizada, se necesita un cable USB de tipo A-B, de los que se utilizan habitualmente para conectar una impresora al ordenador (Figura 1.2). Si es la primera vez que cargas un sketch, antes debes instalar el software (consulta el truco núm. 1 para ver cómo hacerlo).
Para cargar un sketch en Arduino:
1.Inicia el entorno de programación de Arduino.
2.Crea tu prototipo y cabléalo a Arduino.
3.Escribe el sketch.
4.Conecta Arduino al ordenador.
5.En Tools > Port, comprueba que se encuentra seleccionado el puerto correcto. En Windows, debería aparecer una opción del tipo COMx (Arduino Uno); en Mac, verás algo como /dev/tty.usbmodemXXX, mientras que en GNU/Linux encontrarás /dev/ttyACM0 o /dev/ttyS0 o S1.
6.Comprueba también que el hardware configurado en Tools > Board corresponde a tu placa (por ejemplo, Arduino Uno).

Figura 1.3 – Ventana de Arduino con el sketch Blink cargado. En la barra de herramientas, puedes ver los botones «Verify», para controlar el código (1), y «Upload» para cargarlo en la placa (2).
7.Ahora, pulsa el botón «Verify» (el primero de la barra de herramientas).
8.El ordenador trabajará durante unos segundos mientras controla el código. Si todo ha ido bien y no hay errores, en la parte inferior de la ventana, en la consola, deberías ver solo texto de color blanco.
9.Pulsa «Upload», el segundo botón de la barra de herramientas, para transferir el programa a la placa.
10.Si no surge ningún problema de comunicación (normalmente, debido a la selección errónea del puerto), tras unos instantes el firmware será transferido y, en la parte inferior de la ventana, debería aparecer el texto «Done Uploading».
Si algo va mal:
•Comprueba el puerto serie: ¿es el correcto? ¿Se ha instalado correctamente?
•¿El cable está bien introducido en la placa? ¿Se encienden los LED?
•¿Has elegido la placa correcta?

Figura 1.4 – Ventana de Arduino al finalizar la carga de un sketch.
3. Programar Arduino con Arduino Web Editor
Arduino Web Editor es la versión online del clásico Arduino IDE. Puedes programar Arduino utilizando simplemente un navegador con conexión a Internet. Para utilizar Arduino Web Editor, conéctate al sitio web https://create.arduino.cc/editor (es obligatorio registrarse).
Al detalle
Desde hace poco tiempo, se encuentra disponible también la versión online, llamada Arduino Web Editor, que permite programar online y guardar todos los proyectos en la nube, es decir, en los servidores de Arduino. Arduino Web Editor forma parte de la nueva plataforma Arduino Create, nacida para ayudar a los makers a escribir código, colaborar y encontrar soporte para sus proyectos. Con esta solución utilizarás siempre la versión más actualizada del IDE, completa con todas las librerías. Puedes empezar a utilizar la versión online de Arduino visitando la página de descarga (http://www.arduino.cc/en/Main/Software). Para poder acceder al editor, es necesario, obviamente, registrarse. El acceso se lleva a cabo desde una página donde hay que introducir los datos de inicio de sesión y una contraseña. Una vez te hayas registrado, recibirás un correo electrónico con un enlace para activar tu cuenta y completar el proceso de registro. Puedes acceder a Arduino Web Editor en la dirección web https://create.arduino.cc/editor.

Figura 1.5 – La página web de acceso al Web Editor.
Aun así, para utilizar Arduino Web Editor, necesitarás instalar un pequeño programa, denominado plugin, que funciona en el ordenador y se comunica con la placa y el Web Editor. Este plugin está disponible para Windows, Linux y Mac y se instala con un proceso similar al de Arduino IDE. El plugin reconoce la placa que se encuentra conectada al ordenador y administra la carga del firmware que has escrito online.

Figura1.6 – La página web de acceso al Web Editor.
El Web Editor se puede utilizar con los navegadores más comunes (Chrome, Firefox, Safari y Edge), aunque se recomienda utilizar con Chrome. Cuando te hayas identificado, verás a la izquierda, en la primera columna, el menú principal con las opciones:
•Sketchbook: el lugar donde guardarás todos tus proyectos.
•Examples: una sección con varios ejemplos que puedes descargar para entender el funcionamiento de Arduino.
•Libraries: la sección de gestión de las librerías.
•Serial Monitor: una sección para activar el Serial Monitor online y recibir y transmitir datos con Arduino conectado al ordenador.
•Help: enlace y páginas de ayuda.
•Preferences: las opciones de configuración del Web Editor.
La segunda columna solo contiene las opciones relativas a cada menú, mientras que la tercera es el área principal donde se escribe el código.
El Web Editor es muy cómodo porque te permite tener siempre disponibles tus proyectos estés donde estés y con la última versión del compilador y de las librerías. Desgraciadamente, a veces es más lento respecto al IDE de Arduino, puesto que trabaja online y los proyectos que creas se compilan en un servidor remoto y se descargan en tu ordenador.
4. Cargar un sketch con Arduino Web Editor
Después de haber escrito el sketch online, en Arduino Web Editor, pulsa «Verify» y, después, «Upload», igual que en el IDE de Arduino.
Al detalle
El procedimiento de carga de un sketch con Arduino Web Editor no es muy distinto al procedimiento clásico. Para utilizar Arduino Web Editor, debes registrarte en el sitio web e instalar el plugin en el ordenador (consulta el truco núm. 1).
Para cargar un sketch en Arduino Web Editor:
1.Entra en Arduino Web Editor (https://create.arduino.cc/editor) con tus datos de inicio de sesión y tu contraseña.
2.Crea tu primer prototipo y conéctalo a Arduino.
3.Escribe el sketch online.
4.Conecta Arduino a tu ordenador.
5.Comprueba que se encuentre seleccionada tu placa y el puerto serie correcto (Figura 1.7) mediante el menú desplegable situado al inicio del sketch.
6.Seguidamente, pulsa el botón «Verify» (muestra un signo con forma de «V» y está situado justo debajo del título del sketch).
7.Pulsa «Upload», el botón situado junto a «Verify», para transferir el programa desde los servidores de Arduino a tu placa (Figura 1.8).
8.Tras unos instantes, el firmware será transferido y, en la parte inferior de la ventana, debería aparecer el texto «Success: Done Uploading» (Figura 1.9).

Figura 1.7 – Antes de cargar el sketch con Arduino Web Editor, selecciona la placa y el puerto serie que hay que utilizar.

Figura 1.8 – Los botones «Verify» y «Upload» se encuentran justo debajo del título del sketch.

Figura 1.9 – Cuando finaliza la carga, en la parte inferior de la ventana, aparecerá el mensaje: «Success: Done Uploading».
5. Simular Arduino con TinkerCAD Circuits
Es posible montar y simular un circuito electrónico con Arduino utilizando TinkerCAD Circuits (http://www.tinkercad.com/), un software online que surgió, en un principio, para el modelado 3D pero que, desde hace poco tiempo, también incluye un módulo de diseño y simulación electrónica. Para poder utilizar TinkerCAD Circuits es preciso registrarse y funciona directamente desde el navegador.
Al detalle
¿Existe un simulador para Arduino? Mucha gente se hace esta pregunta y, de hecho, hace unos años, había algún software, aunque las distintas soluciones existentes presentaban todas limitaciones o algún que otro defecto para ser consideradas útiles. Hace poco, Autodesk ha preparado un módulo online para realizar y simular cualquier tipo de circuito electrónico.
Los simuladores existen desde hace mucho tiempo, aunque suelen ser programas difíciles de usar. Autodesk ha conseguido crear un programa integrado dentro de TinkerCAD, un conocido CAD online, muy sencillo de usar y bastante fiable. El simulador de Autodesk se denomina TinkerCAD Circuits y permite no solo simular circuitos analógicos, formados por LED, resistencias, condensadores y transistores, e integrados, sino también añadir Arduino, programarlo y depurarlo. Una auténtica maravilla.
Para utilizar TinkerCAD Circuits, es obligatorio registrarse gratuitamente en TinkerCAD (http://www.tinkercad.com/) introduciendo tu correo electrónico y una contraseña segura. También es posible activar un proceso de inicio de sesión en dos pasos con el envío de un código numérico para el acceso. Una vez registrado, ya se puede acceder a TonkerCAD. Para entrar en la sección Circuits, hay que pulsar sobre el enlace situado en la barra de herramientas de la izquierda. En el escritorio verás todos tus circuitos, con una previsualización y el título. Para crear un nuevo circuito, pulsa en «Create new Circuit». Los circuitos pueden ser privados o públicos. Para modificar la visibilidad, pulsa sobre el icono que muestra un engranaje en una esquina de la imagen de la previsualización y elige la opción «properties». Si decides hacer público un circuito, asegúrate de completar todas las informaciones necesarias y de introducir etiquetas y una descripción adecuada. Al situar el puntero del ratón sobre la previsualización aparece el texto «Tinker this»: pulsa sobre él para modificar el circuito.

Figura 1.10 – El escritorio de TinkerCAD Circuits.
Tanto el diseño como la modificación de un circuito son muy sencillos y rápidos. La configuración del área de trabajo es muy parecida a la de Fritzing. Los componentes se agrupan en la columna de la izquierda, dentro de un panel que cuenta con filtros y un campo de búsqueda. Para crear un circuito, empieza a arrastrar elementos hasta el área principal. Si te falta algo, intenta buscarlo. Por ejemplo, puedes pedir una bread- board escribiendo «breadboard» en el campo de búsqueda. Las conexiones eléctricas se trazan pulsando sobre un terminal libre y desplazando el ratón. El algoritmo de trazado de los cables es muy flexible y permite trazar líneas ordenadas y con curvas precisas.

Figura 1.11 – TinkerCAD permite crear circuitos electrónicos, así como utilizar Arduino.
Insertando en el área de trabajo una placa Arduino (hasta el momento solo está disponible Arduino Uno), podrás editar el código e, incluso, probarlo. Para acceder al código, hay que pulsar el botón «Code» situado en la barra de herramientas superior. Aparecerá un nuevo panel a la derecha, en el cual podrás empezar a escribir el código para Arduino. Cuando veas por primera vez el panel para escribir el código te sorprenderá: el código se encuentra definido por bloques, como en Scratch. ¡Incluso los más pequeños pueden empezar a experimentar con Arduino! Para cambiar a la vista «clásica» del sketch, abre el menú desplegable situado sobre el panel que contiene los bloques y elige «Code».
Prueba a escribir el código para que un LED parpadee:
void setup() { pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH);
delay(1000); // Wait for 1000 millisecond(s)
digitalWrite(13, LOW);
delay(1000); // Wait for 1000 millisecond(s)
}
Comprueba que el código sea correcto y, después, pulsa «Start Simulation» para iniciar la simulación. Tras unos instantes, si no hay errores, ¡el LED de Arduino empezará a parpadear! Sencillo, ¿no?

Figura 1.12 – Para acceder al código de Arduino hay que pulsar el botón «Code».
El simulador también permite depurar paso a paso el código (debug). Insertando breakpoint, es decir, puntos de parada donde la ejecución del programa se detendrá, podrás analizar los valores memorizados en las variables y entender mejor lo que ocurre. Cuando el programa está en depuración, puedes hacerlo avanzar de breakpoint a break- point o bien línea por línea. Para activar el modo debug basta con pulsar sobre el botón que muestra el dibujo de una cucaracha estilizada. Los breakpoint se insertan simplemente pulsando al inicio de la línea.

Figura 1.13 – TinkerCAD Circuits incluye también el Serial Monitor y el debug.

Figura 1.14 – Incluso los más pequeños pueden escribir código para Arduino utilizando la programación por bloques estilo Scratch.
6. Programar Arduino por bloques con Bitbloq
Bitbloq es un entorno de programación online, por bloques, para Arduino. Para utilizarlo, hay que acceder desde Chrome a la siguiente dirección web: http://bitbloq.bq.com.
Al detalle
Bitbloq 2 es un lenguaje de programación por bloques que se puede utilizar para diseñar proyectos para Arduino. Se utiliza online y puede programar directamente las placas Arduino Uno mediante un plugin que debe instalarse en el ordenador. La forma de los bloques es muy parecida a la de Scratch y de TinkerCAD Circuits. A diferencia de este último, los circuitos y los proyectos no pueden ser simulados, sino solo creados. Para probarlos, es preciso cargarlos en una placa real, que puede ser un Arduino Uno, un Arduino Mega o algunos kits robóticos propuestos por Bitbloq. Las placas y el hardware son limitados y solo se pueden utilizar los componentes incluidos en los kits. No encontrarás bread- boards, resistencias ni transistores. Para programar con Bitbloq debes instalar el navegador Chrome y es recomendable registrarse, si bien se puede utilizar también como invitado.
El área de trabajo está dividida en tres columnas principales:
•A la izquierda puedes ver un selector para activar las vistas «hardware», «software» y «project info».
•En el centro, el área principal, donde se diseñan los circuitos y los bloques de código.
•A la derecha, un panel con las piezas de hardware o los distintos tipos de bloques (según el modo de trabajo activo).
Para programar Arduino con Bitbloq:
1.Conecta una placa Arduino Uno al ordenador con un cable USB.
2.Abre Bitbloq 2 (http://bitbloq.bq.com) desde el navegador Chrome.
3.Entra introduciendo los datos de inicio de sesión y la contraseña, o bien trabaja como invitado.
4.Si has iniciado sesión, crea un nuevo proyecto.
5.Pulsa sobre la pestaña «Boards» y arrastra una placa Arduino Uno al área de trabajo (Figura 1.15).

Figura 1.15 – El área principal de trabajo de Bitbloq: arrastra una placa Arduino Uno para empezar a diseñar el código.
6.Añade los componentes de hardware necesarios arrastrándolos hasta el área de trabajo y conectándolos entre sí.
7.Pasa a la vista «Software» y crea un sketch arrastrando los bloques desde la columna lateral.
8.Pulsa en «Probar» (el botón con forma de «V» situado sobre el menú/barra de herramientas superior) y espera a que el programa se compile correctamente.
9.Pulsa sobre el botón «Subir» (muestra un símbolo en forma de flecha y está situado sobre el menú/barra de herramientas superior) y aparecerá una ventana que solicitará la descarga de la aplicación Web2Board. Selecciona el sistema operativo correcto e instala el plugin. Es posible que tengas que reiniciar el ordenador. Esta operación es necesaria solo la primera vez que se utiliza Bitbloq.
10.Tras este reinicio, vuelve a abrir Bitbloq 2, abre de nuevo el proyecto y pulsa una vez más sobre el botón «Cargar».
11.Cuando termine la carga, aparecerá un mensaje en la pantalla que informa de que todo ha ido bien: «Programa cargado con éxito».
Verás que las conexiones eléctricas están simplificadas. Para conectar un LED a Arduino, se indica un único cable; esto es así porque se prevé el uso de placas sencillas y kits robóticos que no necesitan un montaje complejo de los circuitos.
La sección de programación por bloques se divide en tres partes: una zona para definir variables globales (funciones y clases con visibilidad global), una zona Setup y una zona Loop.
Los bloques están agrupados en la columna de la derecha y organizados según su funcionalidad. Para añadir un bloque al sketch, basta con arrastrarlo hasta una de las tres zonas de trabajo, exactamente dentro del rectángulo «Drag a block here to start your program».
Los bloques insertados en la zona Setup se ejecutan al inicio del programa y se utilizan para determinar el comportamiento de los pines y de otras acciones que deben ejecutarse solo una vez al inicio del programa. Los bloques insertados en la zona Loop se repiten continuamente.
Si sientes curiosidad por ver cómo se crea el sketch, puedes pasar de la vista de bloques a la de código pulsando sobre «Blocks» o «Code».
Para hacer que un LED parpadee, primero debes conectar un LED a la placa (Figura 1.16) y, después, pasar a la sección «Software» para componer los bloques (Figura 1.17).

Figura 1.16 – Añade los componentes electrónicos arrastrándolos desde el panel lateral.

Figura 1.17 – En la sección Software puedes crear un sketch arrastrando los bloques desde el panel lateral.
No es necesario que especifiques el pin utilizado en la sección Setup. Para realizar el blink, arrastra un bloque «Switch on» desde el área «Components» (de color rojo), después, un bloque «Delay» desde la sección «Control» (color azul), un bloque «switch off» y otro «Delay» (Figura 1.18). Al mismo tiempo que el sketch gráfico, Bitbloq genera el correspondiente código, visible en la sección «Code» (Figura 1.19).
Completa el proyecto añadiendo un nombre y una descripción en la sección «Project Info».

Figura 1.18 – El código de un simple blink.

Figura 1.19 – Si sientes curiosidad, puedes echar un vistazo al código generado por Bitbloq.
1 El condicional es obligatorio dada la heterogénea variedad de configuraciones de hardware y las versiones de Windows.

Programar Arduino
Para crear prototipos, es preciso escribir código. En esta sección, encontrarás soluciones a los casos más habituales que pueden surgir al programar Arduino: trabajar con listas, matrices, gestionar bucles, crear funciones y objetos, generar números aleatorios y desarrollar operaciones lógicas.
Arduino fue creado para ser un sistema simple y de fácil acceso incluso para gente sin experiencia. Nació como plataforma de desarrollo y prototipado donde lo normal es experimentar. El equipo de Massimo Banzi ha hecho todo lo posible para reducir cualquier dificultad. Uno de los aspectos más complicados se encuentra en la escritura del código. No todos son capaces de hacerlo y, por desgracia, las tecnologías modernas no invitan a aventurarse con la programación, ¡aunque todos tengamos en nuestras manos un smartphone y, con él, un pequeño ordenador! Antes, los ordenadores estaban hechos para ser programados y se vendían con un manual de programación, al cual tarde o temprano se accedía para echar un vistazo o para realizar algún que otro experimento y aprender a programar.
Programar un microcontrolador requiere conocer el lenguaje C/C++ y, en ocasiones, incluso código máquina, todavía más complicado. La programación de Arduino se ha simplificado notablemente añadiendo librerías que hacen que la aplicación de todas las operaciones fundamentales sea más sencilla. Para configurar un pin como entrada o salida no se debe modificar ningún registro: basta con llamar al pinMode(). Además de estas dificultades, los principiantes se encuentran con el hecho de tener que escribir un programa. La programación requiere el análisis del problema por resolver y la elaboración de una solución. En esta sección encontrarás una serie de trucos para afrontar algunos de los problemas de programación más «comunes» que pueden surgir al crear prototipos con Arduino.
7. Crear un sketch vacío
Para crear un sketch vacío, accede a la opción de menú File > Nuevo o bien utiliza el atajo de teclado Ctrl + N (en Mac, Cmd + N). Entre los ejemplos, verás la estructura del sketch vacío: File > Ejemplos > 01.Basics > BareMinimum. Un sketch siempre debe tener las dos secciones denominadas setup() y loop().
Al detalle
Cada vez que desees empezar un nuevo proyecto, deberás abrir un nuevo sketch. ¡Así de sencillo! Inicia Arduino IDE y pulsa las teclas Ctrl + N (Cmd + N en Mac) para abrir una ventana nueva que contiene la estructura de un nuevo sketch. Normalmente, no será necesario que escribas la estructura de un sketch desde cero. También puedes encontrar la estructura básica del sketch de Arduino entre los ejemplos, siguiendo la ruta de menú File > Ejemplos > 01.Basics > BareMinimum.
El sketch contiene dos secciones denominadas setup() y loop(). La sección setup() contiene las instrucciones de inicialización de la placa, que se ejecutan cuando esta se enciende; en la parte loop() verás las instrucciones que se repetirán hasta el infinito. Las secciones están delimitadas entre llaves. Este sería el código mínimo para crear un sketch:
void setup() {
//aquí iría el código de inicialización
}
void loop() {
//aquí iría el código que se repite
}
En el teclado no aparece el símbolo de la llave. Para escribirla, deberás pulsar una combinación de teclas: Alt Gr + Shift + [ y Alt Gr + Shift + ]. En Mac las teclas que debes pulsar son: Alt + Shift + [ y Alt Gr + Shift + ].
El código puede tener comentarios, es decir, notas que Arduino ignorará pero que sirven para insertar información de uso en el programa o la descripción de su funcionamiento. Los comentarios en Arduino se muestran de color gris. Si un comentario ocupa una sola línea, va precedido de una doble barra //. Un comentario que ocupa varias líneas empieza con /* y termina con */. Todo el texto incluido entre los dos delimitadores será ignorado.
A continuación, como ejemplo, puedes ver el comentario de varias líneas situado al inicio del ejemplo Blink:
/*
Blink
Turns on an LED on for one second, then off for one second, repeatedly.
Most Arduinos have an on-board LED you can control. On the Uno and
Leonardo, it is attached to digital pin 13. If you΄re unsure what
pin the on-board LED is connected to on your Arduino model, check
the documentation at http://arduino.cc
This example code is in the public domain.
modified 8 May 2014
by Scott Fitzgerald
*/
Un poco más adelante, puedes ver un comentario de una sola línea:
// the setup function runs once when you press reset or power the board
Inserta siempre muchos comentarios en tus sketch: te ayudarán a recordar cómo funciona. Te aseguro que, sin comentarios, al cabo de un tiempo, incluso el código que ahora te parece sencillísimo te parecerá complicado.
8. Utilizar una variable
Una variable es un espacio de la memoria de Arduino para contener datos. Para crear una variable, en primer lugar debes escribir el tipo de datos que contendrá, seguido del nombre que desees. Una variable puede estar «vacía» o sin asignar, o puede contener un valor inicial, que se puede especificar escribiendo un signo igual seguido del valor inicial.
El nombre de una variable puede estar formado por mayúsculas y minúsculas, números y signos especiales, como «_» o «-». El nombre de la variable debe empezar por una letra y no puede contener espacios.
Una variable entera sin asignar sería:
int temperatura;
Una variable entera de tipo long con valor inicial sería:
long presión = 12345;
Una declaración múltiple de variables sería:
int x, y;
Una declaración múltiple de variables con asignación sería:
float m = 1.2, n = 3.4;
Al detalle
Los programas almacenan datos dentro de las celdas de memoria. ¿Cómo se inserta un dato en una celda de memoria? Dicho así, parece incluso intimidatorio, aunque el modo de hacerlo es muy sencillo: se utilizan variables. Una variable es como un pequeño cajón donde se guardan informaciones. Son cajones «tipificados», es decir, que pueden contener un tipo muy concreto de dato. Un cajón (una variable) tiene un nombre que debe respetar unas normas: no puede empezar por números o por caracteres especiales (por ejemplo *, +, -, etc.). Habitualmente se pueden utilizar de forma indiferente mayúsculas y minúsculas.
Nombres de variables válidos serían:
temperatura
i
parametro123
Nivel_de_gasolina
VOLUMEN
Nombres de variables no permitidos serían:
12gama
nivel del gas
*pipa
Para utilizar una variable, solo tienes que escribir su nombre. Cuando Arduino encuentre alguna, irá a buscarla en su memoria y la sustituirá por el valor correcto. Veamos un ejemplo sencillo, muy conocido:
int led = 13;
void setup(){
pinMode(led, OUTPUT);
}
void loop() {
digitalWrite(led, HIGH);
delay(1000);
digitalWrite(led, LOW);
delay(1000);
}
En esta versión del sketch blink se utiliza una variable para memorizar el número del pin al cual se encuentra conectado el LED que debe parpadear. Y esto es así porque es posible que hoy quieras que parpadee el pin 13, pero mañana, el pin 2. La variable led, situada al inicio del sketch, contiene el número del pin que quieres que parpadee, el número 13. Cuando Arduino ejecute el sketch, creará una variable de nombre led que contiene memorizado el número 13. Dentro del setup(), al encontrar el pinMode(), en lugar de un número verá led e irá a buscar en su memoria la variable led para leer el valor memorizado, 13, que utilizará para configurar el pin. En el loop(), cuando Arduino se encuentre con digitalWrite(), volverá a encontrarse con led, que sustituirá por el valor memorizado: 13. La ventaja de utilizar una variable es que, en el caso de que desees modificar el pin al cual se encuentra conectado el LED, ¡solo tendrás que realizar un cambio al inicio del sketch!
Dentro de un sketch no puede haber dos declaraciones de variables con el mismo nombre. Esta regla, sin embargo, no tiene en cuenta el scope de las variables. Cada variable tiene su propio ámbito o su propio alcance o visibilidad (scope). Una variable definida al inicio de un sketch, fuera de loop() y setup() tiene el alcance máximo, porque es visible en todas partes y, por tanto, puede ser utilizada dentro de loop(), de setup() y de cualquier otro bloque de código que exista.
int x = 0;
void setup(){
x = 10;
}
void loop() {
x = x + 2;
}
En este sketch, la variable x ha sido definida al inicio del sketch, fuera de loop() y de setup() y, por tanto, es visible en todas partes. Se puede utilizar tanto dentro de loop() como de setup(), sin que surjan errores de compilación. Piensa en el sketch como si fuera una habitación que contiene dos habitaciones más denominadas loop() y setup(). En cambio, si se define una variable dentro de loop() o de setup(), esta tendrá visibilidad solo dentro del bloque de código en el cual ha sido definida. Una variable creada dentro de setup() no será visible dentro de loop().
void setup(){
char cmd = ΄a΄;
}
void loop() {
cmd = ΄s΄;
}
Si tratas de compilar este ejemplo, se producirá un error, porque la variable cmd, definida dentro de setup(), no es visible también dentro de loop(). Una variable muy utilizada y a menudo redefinida es i, que se utiliza tradicionalmente para crear contadores:
void setup(){
}
void loop() {
for (int i = 0; i < 10; i++){
//código que hace algo…
}
for (int i = 0; i < 200; i++){
//código que hace otras cosas…
}
}
En este sketch, la variable i ha sido redefinida dos veces dentro de los dos bucles for. Si compilas el sketch no se producirá ningún error porque ambas variables se encuentran dentro del scope del bucle for. Es como si el bucle for fuera una especia de habitación dentro de la habitación del loop(): de este modo, no existe ningún conflicto.
Una vez elegido el nombre, deberás definir qué tipo de información podrá contener la variable. Una variable creada para almacenar números enteros podrá contener solo números sin coma, positivos o negativos: 0, 1, 2, -1…
Para definir una variable en C/C++ deberás escribir lo siguiente:
int nivel_gasolina = 10;
En primer lugar, debes indicar el tipo de número que podrá contener la variable, que, en este caso, es int, es decir, números enteros. Seguidamente, asigna a la variable el valor 10, utilizando el signo igual. Si lo deseas, puedes crear una variable sin preasignar ningún valor:
int nivel_agua;
El compilador asignará un valor predeterminado que podría ser igual a cero, pero siempre es mejor no confiar en ello y asignar a las variables un valor inicial conocido (simplemente para no llevarse una sorpresa). Es posible inicializar más de una variable a la vez:
float x,y,x;
y asignarles, si se desea, su valor inicial:
float x=0.1, y=10.2, z=-5.6;
Además del tipo int, que engloba valores desde –32.768 hasta 32.767, existen también long y byte. Un byte es un entero pequeño que puede contener valores que van del 0 al 255. En cambio, el tipo long contiene números muy altos que van del –2.147.483.648 al 2.147.483.647. También se pueden utilizar números sin signo, en cuyo caso tenemos:
•Unsigned char - de 0 a 255 y equivale al tipo byte.
•Unsigned int - de 0 a 65.535.
•Unsigned long – de 0 a 4.294.967.295.
¿Se pueden utilizar números con coma? Sí, pero no es recomendable en los programas escritos para Arduino porque consumen mucha memoria. Para definir un número con coma, se utiliza la palabra clave float seguida del nombre de la variable y, si procede, del valor que se desea asignar. Para indicar la coma, recuerda que debes utilizar el punto (.), como en el sistema anglosajón.
float tension_sensor = 1.12;
Los números float requieren cuatro bytes y asumen valores del 3,4028235 ⋅ 10+38 al –3,4028235 ⋅ 10+38. En lenguaje C, si se necesita una mayor precisión, se pueden utilizar los números con coma de tipo double, pero en Arduino Uno, double y float son equivalentes.
Textos y caracteres
Es posible memorizar tanto textos como caracteres utilizando variables. Puedes incluir en una variable un único carácter o bien un texto entero o «cadena». Por carácter se entiende un único símbolo, como el símbolo utilizado para la «a» o el símbolo que representa un número (por ejemplo «1»).1 Una cadena, en cambio, es una secuencia de caracteres. En el lenguaje C, los caracteres se indican mediante comillas simples. Esta es la definición de una variable de tipo carácter:
char un_carácter = ΄a΄;
En lugar del tipo int, en esta ocasión debes utilizar el tipo char.
Una secuencia de caracteres forma un texto, que los programadores también llaman cadena. Al principio, el concepto de «cadena» no existía en el lenguaje C, que utilizaba (y todavía utiliza) secuencias de caracteres: imagina un cajón con muchos compartimentos ordenados. Una cadena se define de este modo:
char texto[] = "No tengo mucho que decir.";
Inmediatamente después del nombre de la cadena, hay que introducir un par de corchetes: el texto debe escribirse entre comillas dobles. Así, para mostrar una cadena en el Serial Monitor, puedes escribir un programa como el siguiente:
void setup() {
Serial.begin(9600);
char txt[] = "avg vgv gvg x";
Serial.println(txt);
}
void loop() {
}
Carga el programa en Arduino y, después, abre el Serial Monitor para leer el resultado.
También se puede declarar una cadena indicando previamente su longitud:
char texto[5] = "hola";
La longitud útil siempre debe contener un carácter más respecto a los necesarios. Esto se debe a que la última posición siempre debe estar ocupada por el carácter especial ΄\0΄ (denominado «carácter nulo» o «terminador»). Si no se indica de forma explícita la longitud de una cadena, será el compilador quien inserte el carácter nulo al final de la secuencia de caracteres. El carácter nulo ayuda al ordenador a saber cuándo finaliza la cadena.
Dado que una cadena es un conjunto ordenado de caracteres, puedes acceder a cada uno de los caracteres individuales indicando su posición. Puedes acceder a un único carácter escribiendo lo siguiente:
void setup() {
Serial.begin(9600);
char saludo[5] = "hola";
for (int i = 0; i < 5; i++){
Serial.println(saludo[i]);
}
}
void loop(){}
En este programa hemos creado una cadena de cinco caracteres llamada saludo. El bucle for crea un índice que se puede utilizar para leer los caracteres uno a uno y mostrarlo en el Serial Monitor con Serial.println().
El índice para acceder a los caracteres individuales de una cadena empieza de cero y llega al valor máximo, menos uno. En el ejemplo anterior, el índice va del cero al cuatro.
Arduino cuenta con un tipo String que facilita mucho las operaciones: no es necesario utilizar paréntesis o funciones raras para modificarlas. Así es como se define una cadena:
String texto = "¡Así todo es más fácil!";
Para combinar dos cadenas en Arduino, basta con sumarlas:
String txt1 = "Hello";
String txt2 = "World!";
String texto = txt1 + " " + txt2;
9. Definir una constante
Se pueden definir constantes mediante el modificador const:
const int led = 13;
o bien con la directiva #define:
#define nombre valor
Al detalle
En muchos sketch puede resultar útil definir una constante, es decir, asignar un nombre conveniente o un alias que se utilice en lugar de un número o una cadena. El uso de constantes mejora la legibilidad y el mantenimiento del código. Existen dos posibilidades para definir una constante:
•con el modificador const;
•con la directiva #define.
El modificador const se utiliza en el momento en que se declara una variable y sirve para decir a Arduino (o, mejor dicho, al compilador) que la variable es de tipo constante y que, por tanto, no sufrirá ningún cambio durante la ejecución del sketch. El compilador reservará una zona de la memoria específica para mantener este valor y, así, optimizará la escritura del código.
const int led = 13;
El clásico sketch para hacer parpadear un LED se puede reescribir utilizando una constante. Esta reescritura tiene la ventaja de simplificar el cambio de pin: si mañana quieres que parpadee un LED conectado al pin 10, no tendrás que repasar todo el sketch para ir sustituyendo el número del pin (esta ventaja también la tenemos cuando utilizamos variables); además, el uso de una constante optimiza el uso de la memoria y la velocidad del sketch:
const int led = 13;
void setup(){
pinMode(led, OUTPUT);
}
void loop() {
digitalWrite(led, HIGH);
delay(1000);
digitalWrite(led, LOW);
delay(1000);
}
El uso de la directiva #define precisa un poco más de atención, porque tiene una sintaxis diferente de la que se utiliza para las variables y es fácil cometer algún error. Con #define se pueden definir «etiquetas» que se sustituirán antes de que el sketch sea compilado, por lo que no se llega a crear una auténtica variable y el espacio de memoria consumido es solo el que ocupa el valor sustituido. Las directivas son examinadas y procesadas por el preprocesador, un programa que se ejecuta antes de la compilación real y que prepara el sketch para ser compilado.
Una #define debería permitir ahorrar un poco de memoria y agilizar la ejecución del sketch. Es recomendable insertar las constantes creadas con #define al inicio del sketch:
#define ledPin 13
Así es como sería el sketch blink utilizando una directiva en lugar de una constante:
#define LED 13
void setup(){
pinMode(LED, OUTPUT);
}
void loop() {
digitalWrite(LED, HIGH);
delay(1000);
digitalWrite(LED, LOW);
delay(1000);
}
Una #define no necesita un «tipo», sino solo una etiqueta seguida por el valor, que puede ser numérico, textual e, incluso, ¡un bloque de código! Si utilizas #define no escribas ningún signo igual ni el punto y coma final, puesto que si no estos símbolos serán sustituidos en el sketch y se producirán resultados impredecibles. La directiva #define se puede utilizar para crear macros, es decir, fragmentos de código, similares a las funciones, que implementan operaciones simples y repetidas con frecuencia dentro del sketch. Esta sería una macro que calcula la longitud de una circunferencia desde el radio:
#define cfz(R) 2*3.14*R
Una macro que seguramente encontrarás más útil es la que muestro a continuación y que sirve para simplificar la impresión sobre el Serial Monitor. En lugar de escribir cada vez Serial.println() puedes sustituir esta línea de código por un prt() más breve:
#define prt(S) Serial.println(S);
Aquí puedes ver un código completo donde se han utilizado constantes y macros:
#define PI 3.14
#define cfz(R) 2*PI*R
#define prt(S) Serial.println(S);
void setup(){
Serial.begin(9600);
int radio = 12;
String str = "circunferencia=" + String(cfz(radio));
prt(str);
}
void loop(){}
10. Manipular cadenas de caracteres
Una secuencia de caracteres forma una cadena. Arduino puede trabajar tanto con estructuras clásicas de C como las cadenas de caracteres, que no son más que matrices de char, como utilizar los objetos String que ofrecen una mayor flexibilidad. Las matrices de caracteres son muy frecuentes y, a menudo, son utilizadas por las distintas funciones y librerías. Las funciones principales para manipular este tipo de datos son:
•strcpy(dest, src): se utiliza para copiar una cadena src en una cadena dest, con las mismas dimensiones.
•strncpy(dest, src, número_de_caracteres): como strcpy copia una cadena en otra, pero pudiendo especificar el número de caracteres que hay que copiar.
•strcat(dest, src): es útil para enlazar una cadena src con otra dest.
•strcmp(str1, str2): compara una cadena con otra y, si son iguales, devuelve el valor numérico «0».
Por lo general, para desplazar las cadenas se utilizan bucles for y el acceso a los caracteres individuales se lleva a cabo mediante un índice y tratando la cadena como un simple array. Para extraer el i-ésimo carácter de la cadena str se puede utilizar:
char ch = str[i];
Al detalle
Al programar con Arduino, se puede definir una cadena, es decir, una secuencia de caracteres, de dos maneras: utilizando el método más moderno con String, un objeto que facilita muchas operaciones, o tratando las cadenas como matrices de caracteres. El segundo método es el «histórico» y estándar de C y en muchas ocasiones es preciso trabajar con estas estructuras, a veces extraídas de alguna librería.
Ya hemos visto que para definir una cadena de caracteres basta con declarar un array de caracteres con la longitud indicada o no:
char cadena[] = "Una simple secuencia de caracteres";
Si bien es posible trabajar con estas cadenas tratándolas como matrices y, por tanto, actuando sobre los elementos individuales, existen funciones más cómodas que agilizan algunas de las operaciones más frecuentes. Una función imprescindible es strlen(), que proporciona la longitud de la cadena, entendida como el número de caracteres contenidos hasta el carácter final de la cadena, ΄\0΄. Estas son algunas líneas de código que extraen la dimensión de la cadena y la muestran en el Serial Monitor:
char str[] = "Hello computer";
int n = strlen(str);
Serial.print("longitud de str: ");
Serial.println(n);
Para copiar una cadena se puede utilizar un bucle for que recorre la cadena de origen y la copia sobre la de destino. C ofrece un sistema más eficaz para la copia de cadenas de caracteres: la función strcpy(), que acepta como parámetros la cadena de destino y la «fuente». El comando strcpy() admite un tercer parámetro, opcional, que sirve para indicar el número de caracteres que hay que copiar. Este sería un ejemplo de uso de strcpy():
char str[] = "Hello computer";
char dest[n];
strcpy(dest, str);
Serial.print("copia de str: ");
Serial.println(dest);
La cadena de caracteres str se copia en dest con un solo comando. El uso de estas funciones es mucho más eficaz que un bucle for creado para la ocasión. Aquí tienes otro ejemplo, que utiliza strncpy() para copiar solo una parte de la cadena:
char str[] = "Hello computer";
char saludo[] = " ";
strncpy(saludo, str, 5);
Serial.print("copia de una parte de str: ");
Serial.println(saludo);
En ocasiones, puede ser necesario comparar dos cadenas. Trabajando con caracteres individuales, esta operación podría ser realmente imprescindible: deberías disponer de una cadena con la longitud adecuada, capaz de contener las dos cadenas que hay que copiar. Las dos cadenas se copian en el orden correcto dentro de la cadena final. También en esta ocasión C ofrece una solución cómoda que resuelve este problema en una única línea de código. Con strcat() se puede «pegar» la segunda cadena a la primera. Así es cómo se hace:
char a[] = "Hello ";
char b[] = "World";
strcat(a, b);
Serial.print("a + b = ");
Serial.println(a);
¿Cómo se comparan dos cadenas? C cuenta con la función strcmp() que compara carácter a carácter la primera cadena proporcionada con la segunda. Para ser idénticas, dos cadenas deben contener los mismos caracteres. La función strcmp() recorre los caracteres de las dos cadenas, los compara y devuelve «0» como resultado en el caso en que sean idénticas. Así es cómo se utiliza:
char key[] = "123";
char value[] = "123";
if (strcmp(value, key) == 0) {
Serial.print("Key y value son idénticos");
} else {
Serial.print("Key y value no son idénticos");
}
A continuación, puedes ver un sketch de resumen que contiene todas las operaciones mostradas en las líneas anteriores:
// Manipular cadenas de caracteres
void setup() {
Serial.begin(9600);
//una cadena de caracteres
char str[] = "Hello computer";
//la longitud de la cadena
int n = strlen(str);
Serial.print("longitud de str: ");
Serial.println(n);
//copia de una cadena
char dest[n];
strcpy(dest, str);
Serial.print("copia de str: ");
Serial.println(dest);
//copia de n caracteres de una cadena a otra
char saludo[] = " ";
strncpy(saludo, str, 5);
Serial.print("copia de una parte de str: ");
Serial.println(saludo);
//concatenación de dos cadenas
char a[] = "Hello ";
char b[] = "World";
strcat(a, b);
Serial.print("a + b = ");
Serial.println(a);
//comparación de dos cadenas char key[] = "123";
char value[] = "123";
if (strcmp(value, key) == 0) {
Serial.print("Key y value son idénticos");
} else {
Serial.print("Key y value no son idénticos");
}
}
void loop(){}
Habitualmente, para manipular las cadenas se utilizan bucles for o while con los cuales se escanean carácter a carácter. Para acceder a cada carácter individual, basta con tratar una cadena de caracteres como un simple array y utilizar la notación con los corchetes y un indicador str[i], como en el siguiente fragmento de código:
char str[] = "Hello!";
for (int i = 0; i < strlen(str); i++){
char ch = str[i];
Serial.println(ch);
}
11. Utilizar las cadenas
Para utilizar un objeto de tipo String, basta con definirlo mediante su constructor:
String str = String("Esto es una cadena");
O bien asignándole directamente un valor, como se hace con las cadenas de caracteres:
String str = "Una segunda cadena";
Las cadenas disponen de múltiples métodos para su manipulación que las hacen potentes y flexibles. Una de las características más apreciadas y utilizadas es la posibilidad de enlazarlas entre ellas e, incluso, con tipos de datos distintos, simplemente con un símbolo de adición, como si fuera una operación matemática común:
String a = "Hello";
String b = "World";
String c = a + " " + b;
Al detalle
En Arduino es posible definir una cadena de dos maneras: con una secuencia de caracteres o bien con el elemento String, que es un tipo de dato particular, implementado con una clase (véase 2.21), que ofrece mayores ventajas y flexibilidad respecto a la clásica secuencia de caracteres de C. Se puede definir un objeto de tipo String de varios modos. El método más natural es utilizarlo como si fuera un tipo de dato común y, después, asignarle directamente un valor:
String str = "Hello World";
Por lo general, los objetos se crean (o se implementan) llamando a su constructor, es decir, al método encargado de su creación. Se puede crear una cadena también de este modo:
String str = String("Hello Moon!");
Mediante el constructor, se pueden llevar a cabo varias posibilidades, entre las cuales la de transformar directamente varios tipos de datos en una cadena. Si el argumento del constructor es un entero, será transformado en la correspondiente cadena:
String str = String(123);
El constructor de String acepta un segundo parámetro, que según el caso puede asumir distintos significados: por ejemplo, puede servir para convertir un número de un formato a otro y mostrarlo correctamente en pantalla. Las conversiones de tipo se pueden hacer proporcionando como segundo parámetro la «base» que se debe utilizar para representar el número. Las posibilidades son: HEX, BIN y DEC. Las siguientes instrucciones transforman el número decimal «45» en una cadena con su representación hexadecimal:
String str = String(45, HEX);
Serial.println(str);
El número mostrado será «2D». De la misma forma, para obtener conversiones en binario, se utilizará BIN:
String str = String(12, BIN);
Serial.println(str);
DEC producirá un número decimal. Prueba a utilizarlo proporcionando como primer parámetro un número en notación hexadecimal:
String str = String(0xB, DEC);
Serial.println(str);
El número de caracteres incluido en una cadena se obtiene desde el método length(). Este es un ejemplo de su uso:
String str = String("Abracadabra");
int n = str.length();
Serial.print("longitud de str: ");
Serial.println(n);
Las cadenas de tipo String se pueden combinar fácilmente utilizando el símbolo de adición. Enlazar varias cadenas para componer un mensaje se convierte en una operación sencilla y rápida:
String a = "ábrete";
String b = "sésamo";
String c = a + " " + b;
Serial.println(c);
La concatenación puede incluir también variables de tipo numérico, como en el siguiente ejemplo, que permite «ahorrar» en Serial.println():
String d = "X vale ";
String e = " puntos";
int x = random(0, 10);
String f = d + x + e;
Serial.println(f);
El tipo String cuenta con muchos otros métodos que me limito a mostrar a continuación con una breve descripción de su funcionamiento. Puedes encontrar más información y detalles de su uso en la siguiente dirección: https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/.
•charAt(n): devuelve el carácter a la posición indicada.
•compareTo(str): compara las dos cadenas y devuelve 0 si son iguales.
•concat(parámetro): enlaza el parámetro proporcionado, que puede ser de distintos tipos, con la cadena sobre la cual se ha llamado este método.
•endsWith(str): comprueba si la cadena termina con el carácter o la cadena proporcionada.
•equals(str): compara la cadena proporcionada y devuelve true si coinciden.
•equalsIgnoreCase(): compara dos cadenas ignorando las diferencias entre mayúsculas y minúsculas.
•getBytes(buff, n): copia n caracteres de la cadena en el búfer de byte buff.
•indexOf(str, from): busca una subcadena dentro de una cadena.
•lastIndexOf(str): devuelve la última posición del carácter o de la cadena indicados.
•length(): la longitud de la cadena.
•remove(n): elimina los caracteres de una cadena a partir de la posición n.
•replace(str1, str2): sustituye las coincidencias de str1 con str2.
•reserve(len): asigna suficiente espacio de memoria para procesar la cadena.
•setCharAt(i, ch): modifica el carácter a la posición indicada.
•startsWith(str): comprueba si una cadena empieza con la subcadena indicada.
•substring(from, to): recorta una subcadena desde la posición from hasta to.
•toCharArray(): devuelve una cadena de caracteres char[] equivalente a la actual.
•toInt(): transforma la cadena en entero.
•toFloat(): transforma la cadena en decimal.
•toLowerCase(): transforma los caracteres de la cadena de mayúsculas a minúsculas.
•toUpperCase(): transforma los caracteres de la cadena de minúsculas a mayúsculas.
•trim(): elimina los caracteres vacíos que pueden aparecer al inicio y al final de la cadena.
12. Convertir una variable de un tipo a otro
Para convertir una variable de un tipo a otro (type cast), se coloca el tipo deseado, entre paréntesis, antes de la variable a transformar:
(tipo) expresión
Al detalle
Dentro de un programa, es necesario utilizar diferentes tipos de datos. Si bien, por simplicidad, se intenta usar solo variables de tipo entero, es inevitable que se encuentren otros tipos de datos, producidos por determinadas funciones o como resultado de la realización de ciertas operaciones.
La conversión más simple es la explícita, en la cual se coloca, entre paréntesis, antes de la variable o número a transformar el tipo deseado. Para transformar la variable n, de tipo byte, en un entero:
byte n = 3;
int x = (int)n;
En este tipo de conversiones, denominadas en la jerga del sector type casting, se puede perder información. Si se pasa de un tipo de dato más pequeño a otro más grande no hay peligro, como en el caso que acabamos de ver: un byte ocupa solo ocho bits, mientras que un entero puede ocupar dieciséis. En una conversión de entero a byte, en cambio, podrían producirse pérdidas de datos, porque el ordenador cortaría por completo todos los bits sobrantes, manteniendo solo los primeros ocho, que se deberían copiar dentro de la nueva variable de tipo byte. Veamos un ejemplo de ello:
void setup() {
Serial.begin(9600);
int n = 12;
byte b = (byte)n;
Serial.println(b);
}
void loop(){}
La variable de tipo entero, n, ha sido convertida en bytes y, después, mostrada con Serial.println. Si cargas el sketch y abres el Serial Monitor verás que aparece el número 12. Es normal. Modifica el sketch asignando a n el valor 257.
void setup() {
Serial.begin(9600);
int n = 257;
byte b = (byte)n;
Serial.println(b);
}
void loop(){}
¡Ahora el Serial Monitor muestra «1»! Para explicar este comportamiento extraño, es aconsejable que escribas los números en bits. Así se puede escribir el número doce:
12: 0000 1100
Ocupa un único byte, porque tiene ocho bits. Si lo convertimos a tipo int, no hay ningún problema. El tipo int está formado por 2 bytes, es decir, 16 bits y los espacios vacíos se rellenarán con ceros:
12: 0000 0000 0000 1100 Ahora intentaremos hacer lo contrario, escribir el número entero 257, transformado en bits:
257: 0000 0001 0000 0001
La operación de type cast de int a byte corta por completo la variable, manteniendo solo los primeros ocho bits y dejando, por tanto, lo siguiente:
1: 0000 0001
que transformado en números decimales equivale a 1.
Podrías optar por hacer caso omiso a las conversiones de tipo, pero en ocasiones son realmente necesarias. Una conversión inevitable se da cuando se realiza una división. Al dividir dos números enteros, se puede obtener un resultado con decimales. Para no perder estos decimales, se podría guardar el resultado en una variable de tipo float:
void setup() {
Serial.begin(9600);
int n = 15;
int m = 2;
float res = n/m;
Serial.println(res);
}
void loop(){}
Ejecuta este sketch y abre el Serial Monitor. El número que aparece es 7 y no 7,5. Arduino ha realizado una división entre enteros, sin mantener los decimales. Para obtener el resultado correcto, es preciso insertar un type cast a número decimal:
void setup() {
Serial.begin(9600);
int n = 15;
int m = 2;
float res = (float)n/m;
Serial.println(res);
}
void loop(){}
Ahora la división se ejecuta correctamente y, en el Serial Monitor, se mostrará 7,5.
Otros tipos de conversiones «interesantes» son aquellas entre cadenas y números, y viceversa. A veces se reciben números en forma de texto, como cuando se lee un puerto serie o algún módulo en concreto, como un GPS que proporciona las coordenadas como cadenas de caracteres. Un número expresado en caracteres no se puede utilizar directamente para realizar cuentas, porque las cifras que vemos son solo símbolos ASCII. Para utilizarlos, se necesitan conversiones particulares. Sin duda, es más sencillo transformar un número en una cadena. Existen conversiones explícitas e implícitas. Una conversión implícita se puede obtener utilizando la concatenación de un objeto String con un número. Los objetos String son más versátiles que las cadenas de caracteres y ofrecen distintos métodos para su manipulación. Una de las propiedades de este tipo de cadenas es la de poder concatenarse utilizando el signo de adición (véase la sección dedicada a ello). Por tanto, es posible utilizar esta propiedad para transformar «implícitamente» un número en texto:
String str = String(123);
txt = String("a + " + str);
Serial.print("txt = ");
Serial.println(txt);
Sumando una cadena vacía a un número se obtendrá una cadena que contiene los caracteres que representan el número. También se puede inicializar un objeto String con un entero. El número se transformará automáticamente en su representación textual:
int n = 100;
String txt = String(n);
Convertir una cadena en un número puede ser más complicado, a menos que se utilicen los objetos String que proporcionan los prácticos métodos toInt() y toFloat() para transformar sin problemas una cadena que contiene cifras en un número. Este sería un ejemplo:
String n1 = "201";
int n = n1.toInt();
n = n + 1;
Serial.println(n);
La cadena 201 ha sido transformada en entero llamando a toInt() y después guardada en la variable, de tipo int, n. Para demostrar que la transformación ha tenido efecto, a n le sumamos «1» y después mostramos el resultado, que será 202.
String cuenta también con el método toFloat() para transformar una cadena en un número con decimales. A continuación, puedes ver un ejemplo como el anterior:
n1 = "12.3";
float x = n1.toFloat();
x = x * 2.0f;
Serial.println(x);
Con las cadenas más clásicas formadas por caracteres también es posible realizar conversiones de texto a int y float. C cuenta con las funciones atoi() y atof(), que toman como parámetro una cadena que contiene un número y la transforman en entero o decimal. Este sería un ejemplo de conversión en número entero cuyo resultado de atoi() se suma después a 100 para comprobar la efectiva conversión en número:
char n2[] = "567";
int z = atoi(n2);
z = z + 100;
Serial.println(z);
A continuación, puedes ver el correspondiente ejemplo de uso de atof():
char n3[] = "56.7";
float q = atof(n3);
q = q * 10.0f;
Serial.println(q);
13. Repetir una operación
Para repetir un grupo de instrucciones, existen varias posibilidades. Si la repetición se produce hasta que se comprueba un evento aleatorio o sin determinar, se utiliza el bucle while:
while (condición) {
//operación a repetir…
}
El bucle while puede repetir operaciones un número predeterminado de veces, utilizando un contador «externo», realizado con una variable:
int i = 0;
while (i < 10) {
//instrucciones a repetir 10 veces
i++;
}
Si se desea que las instrucciones incluidas dentro del bucle while se ejecuten como mínimo una vez se puede utilizar la forma do...while:
do {
//instrucciones a repetir
} while (condición);
Si utilizas un bucle while, debes asegurarte de que siempre haya una condición que ponga fin al bucle. En caso contrario, se repetirá sin fin.
Para repetir operaciones durante un número predeterminado de veces, se utiliza el bucle for, que necesita una variable que se utilice como contador, una regla para establecer la finalización del bucle y el método para aumentar el valor de la variable en cada iteración:
for (int i = 0; i < 10; i++) {
//instrucciones a repetir 10 veces
}
Además de estos bucles «explícitos», también es posible utilizar el mismo loop(), que Arduino repite a la mayor velocidad posible. El bucle loop() no tiene fin.
Se puede modificar la ejecución de un bucle for y while utilizando continue y break:
•break interrumpe inmediatamente el bucle y sale.
•continue interrumpe la ejecución de la iteración actual y pasa a la siguiente.
Al detalle
Para no escribir sketch kilométricos, resulta del todo necesario introducir bucles que repitan las operaciones. Los bucles permiten escribir código más compacto, legible y de fácil mantenimiento. Los principiantes creen que no supone ningún problema repetir fragmentos de código dentro de un programa. Este inócuo comportamiento puede llevar a situaciones ingestionables y a un código complicado. Si repites varias veces fragmentos de código, en caso de tener que modificarlo, te verás obligado a revisar cada fragmento uno a uno. Los ordenadores son especialistas en repetir operaciones largas y aburridas, y es inteligente aprovecharse de esta capacidad. Existen distintos tipos de bucles que pueden repetir un grupo de instrucciones durante un determinado número de veces o bien hasta que no se verifica una determinada condición.
El tipo más sencillo de repetición es la que implica la comprobación de una determinada condición, donde el número de repeticiones no está predefinido. Puedes realizar un bucle de este tipo con la instrucción while. Tras la palabra clave while, se añade, entre paréntesis, la condición por evaluar y después, entre corchetes, las instrucciones por repetir. El bucle se repite hasta que la condición indicada entre los dos paréntesis es verdadera. Este sería un ejemplo para mostrar diez veces una cadena en el Serial Monitor:
void setup() {
Serial.begin(9600);
int i = 0;
while (i < 10) {
Serial.print("i: ");
Serial.println(i);
i++;
}
}
void loop(){}
Todo el código está incluido en el setup(). Al inicio de la sección puedes ver la inicialización del puerto serie. Para poder contar los diez pasos, el bucle while necesita una variable. Por esta razón, inmediatamente antes de entrar en el bucle, se ha definido la variable i, inicializada a 0.
El bucle while empieza y repetirá las instrucciones incluidas entre corchetes hasta que la variable i sea menor que 10. Dentro del cuerpo de while, la variable i debe aumentar en cada iteración, sino el bucle no se detendría nunca porque la condición i < 10 siempre sería verdadera.
Por brevedad, en ocasiones verás la operación de aumento de la variable i en una forma «compactada» que tanto gusta a los programadores:
i++
Con un bucle while el número de repeticiones no está prefijado. Por este motivo, a veces se introduce una variable de contador que memoriza el número de repeticiones, aunque esta solo es necesaria si se precisa llevar la cuenta de las repeticiones.
Vamos a intentar realizar un bucle while que se interrumpe cuando se verifica un evento aleatorio. En este caso, deberás realizar un simple circuito, contectando un botón a Arduino. Coloca el botón sobre la breadboard y añade una resistencia de 10 kΩ (Figura 2.1). El evento «aleatorio» será la pulsación del botón, que interrumpirá la ejecución del bucle.

Figura 2.1 – Conecta un botón al pin 8 de Arduino.
void setup() {
Serial.begin(9600);
pinMode(8, INPUT);
}
void loop(){
bool RUN = true;
while (RUN) {
Serial.print(".");
if (digitalRead(8)){
RUN = false;
}
delay(200);
}
Serial.println("X");
}
En el setup() del sketch vemos la inicialización del puerto serie a 9600 baudios (símbolos por segundo) y del pin 8 de Arduino, que utilizaremos como entrada donde conectar el botón. En el loop() encontramos un bucle while que está controlado por la variable RUN, de tipo booleano. Una variable booleana puede contener solo un valor que puede ser verdadero o falso (true o false). El bucle while repetirá las instrucciones incluidas en su cuerpo hasta que la variable RUN sea igual a true. Para modificar el estado de la variable, es necesario pulsar el botón, controlado por una instrucción de tipo if. A la primera pulsación del botón, el valor de RUN pasa de true a false y el bucle while podrá finalizar. Si ejecutas el programa y abres el Serial Monitor podrás observar una secuencia de puntos que se interrumpen con una «X» cada vez que pulsas el botón.
En ocasiones es preferible ejecutar un grupo de instrucciones y, después, valorar si repetirlas o no cuando finaliza la ejecución. En estos casos, puedes utilizar el bucle do...while:
int i = 1;
do {
i++;
Serial.print("i vale:");
Serial.println(i);
} while (i <= 5);
Esta vez el programa entra de inmediato en el cuerpo del bucle (el bloque que se repetirá), aumenta la variable i y muestra un mensaje. Después, comprueba si ejecutar de nuevo el grupo de instrucciones.
Otro constructo muy utilizado es for, con el cual puedes repetir operaciones durante un número predefinido de veces. Para utilizar el bucle for debes especificar lo siguiente:
•la variable que se utilizará como contador.
•una prueba para saber cuándo deben finalizar las repeticiones.
•un modo para incrementar la variable contador.
Estas tres informaciones se «codifican» en C y van detrás de la palabra clave for:
for (int i = 1; i < 3; i++) {
//instrucciones que se repetirán 3 veces
}
Inmediatamente después de for, puedes ver un par de paréntesis donde, separadas por puntos y coma, se encuentran las tres informaciones necesarias para que el bucle funcione. En primer lugar, se define una variable i que se utilizará como contador y a la cual se asigna el valor inicial uno; después, se añade la condición de final de bucle: las repeticiones continuarán hasta que el contador i sea menor que tres. Por último, puedes ver el modo de incremento: la variable i se incrementará en una unidad por cada vuelta. Las instrucciones que hay que repetir están delimitadas por llaves.
El programa llega a la instrucción for, crea una variable i2 y en ella memoriza el valor «uno». Después, identifica la condición de fin de bucle (terminará cuando i sea mayor o igual que tres) y el modo de incremento de la variable (i++). Dado que i es menor que tres, el bloque de instrucciones situado entre llaves puede ser ejecutado (cuerpo del bucle). Después de esto, el programa incrementa la variable i, que ahora valdrá «dos», comprueba la condición (2 < 3) y ejecuta de nuevo el cuerpo del bucle. En el siguiente paso, i valdrá «tres» y la condición ya no se volverá a comprobar (3 < 3), por lo que el programa saltará hasta el final del bucle for y seguirá adelante.
Puedes modificar el «paso» sustituyendo i++ con el modo de incremento que prefieras. Puedes avanzar por pasos de dos unidades mediante:
for (int i = 1; i < 10; i = i + 2) {
//instrucciones a repetir
}
Ahora el modo de incremento es más comprensible: la variable i en cada paso será calculada añadiendo «dos» a su valor.
La variable «contador» también puede ser una variable preexistente y no es necesario definirla dentro de for, sino que debes inicializarla siempre insertando un valor inicial. Por ejemplo, puedes escribir:
int i = 0;
for (i = 1; i < 10; i = i + 2) {
//cuerpo del bucle for
}
Puedes modificar las repeticiones de un bucle for o while utilizando los comandos break y continue. El primero, break, interrumpe el bucle, mientras que continue interrumpe la ejecución de la iteración actual.
Prueba a ejecutar el siguiente programa:
void setup() {
Serial.begin(9600);
int i = 0;
while (i < 10) {
Serial.print("i: ");
Serial.println(i);
if (i == 5) break;
i++;
}
Serial.print("salido con i: ");
Serial.println(i);
}
void loop(){}
El bucle for debería mostrar el valor de la variable i diez veces.
Dentro del cuerpo del bucle, hay una prueba que verifica si i vale cinco y, después, llama a break. De este modo, las diez iteraciones se interrumpirán a la quinta vez y, en pantalla, podrás leer:
i:4
i:5
Salido con i = 5
La instrucción continue interrumpe solo la ejecución de la interacción actual, sin interrumpir por completo la ejecución del bucle. Prueba ahora este ejemplo:
void setup() {
Serial.begin(9600);
for (int j = 0; j < 10; j++){
if (j == 5) continue;
Serial.print("j: ");
Serial.println(j);
}
}
void loop(){}
Cuando j sea igual a cinco, el programa saltará al inicio del bucle y continuará con las iteraciones que faltan. El resultado en pantalla será el siguiente:
i: 0
i: 1
i: 2
i: 3
i: 4
i: 6
i: 7
i: 8
i: 9
Las instrucciones break y continue funcionan con cualquier tipo de bucle: for, while y do...while.
14. Utilizar un array
Un array es una variable dotada de varias partes a las cuales es posible acceder con un índice. En un array con n posiciones, el índice va desde 0 hasta n-1. Se puede definir un array de enteros del siguiente modo:
int números[6] = {0, 10, 2, 3, 1, 9};
Para acceder y utilizar un valor contenido en un array se debe indicar, utilizando un índice, el valor que se desea utilizar. Para acceder al cuarto elemento del array deberías escribir:
números[3]
Al detalle
A veces, una simple variable no es suficiente y podría ser que necesitaras tener que crear muchos «cajones» para memorizar informaciones distintas, pero muy similares entre sí en cuanto al significado.
Veamos un caso sencillo, pero concreto: calcular la media de cinco números. Para calcular la media, debes definir cinco variables distintas, una para cada número, después las sumas y divides el resultado entre cinco. Puedes llamar a las variables n1, n2, n3, n4, n5.
float n1 = 1.1;
float n2 = 2.3;
float n3 = 3.7;
float n4 = 4.1;
float n5 = 5.8;
float media = (n1 + n2 + n3 + n4 + n5) / 5.0;
Es cierto que no resulta demasiado práctico tener que crear cinco variables distintas. Un array es como un cajón con compartimentos. El número de compartimentos está predefinido. Crea un cajón denominado «temperaturas» para memorizar tres valores diferentes:
float temperaturas[3];
Para indicar el número de posiciones disponibles (o compartimentos), debes añadir el número, entre corchetes, inmediatamente después del nombre de la variable. También puedes indicar el número de posiciones y dejar vacíos los corchetes; en este caso, después del signo igual, deberás listar los elementos del array entre llaves y separados por comas (,).
float temperaturas[] = {32.1, 30.0, 33.0};
int estados[] = {0, 10, 2, 3, 1, 9};
Así es como calculamos la media de cinco números utilizando un array:
void setup() {
Serial.begin(9600);
float nn[] = {12.2, 1.2, 324.5, 34.8, 45.9};
float media = (nn[0]+nn[1]+nn[2]+nn[3]+nn[4])/5.0;
Serial.println(media);
}
void loop() {}
Para acceder a un elemento del array, debes indicar su posición dentro del «cajón» utilizando el índice que empieza desde cero. Si el array tiene cinco posiciones, el índice irá de cero a cuatro. Para especificar el elemento deseado, se utilizan corchetes, situados después del nombre del array.
void setup() {
Serial.begin(9600);
float nn[] = {12.2, 1.2, 324.5, 34.8, 45.9};
//para mostrar el tercer elemento
Serial.println (nn[2]);
//para mostrar el primer elemento, el índice es 0
Serial.println (nn[0]);
//para mostrar el último elemento
Serial.println (nn[4]);
}
void loop() {}
15. Obtener la longitud de un array
Arduino no dispone de una función para calcular la longitud de un array. Sin embargo, se puede utilizar el operador sizeof(), que cuenta los bytes ocupados por la estructura o por cualquier variable. Para obtener el tamaño, es decir, el número de posiciones, de un array de enteros, se puede utilizar una redacción como esta:
int dimensión_array = sizeof(array_a_medir)/sizeof(int);
Al detalle
Cuando trabajes con arrays y estructuras dinámicas, puede ocurrir que necesites calcular su longitud, es decir, el número de elementos. En Arduino, no existe una función específica para obtener el número de elementos de un array. C tiene el operador sizeof(), que cuenta el número de bytes que ocupa el array. La definición de un array va precedida por el tipo de variable que será memorizada y este devolverá resultados distintos según si los elementos son de tipo byte, int o long.
Prueba el siguiente sketch:
void setup() {
Serial.begin(9600);
int números[] = {1,2,3,4};
int posiciones = sizeof(números);
Serial.println (posiciones);
}
void loop(){}
Si lo ejecutas y abres el Serial Monitor verás: ¡8! ¿Cómo es posible si el array «números» tiene solo 4 posiciones? El operador sizeof() cuenta el número de bytes y no «las casillas» efectivas del array. El tipo int, en Arduino, ocupa dos bytes y, por tanto, un array de cuatro posiciones ocupa, en total, ocho bytes. Para mostrar correctamente el número de posiciones del array, debes corregir el cálculo de la variable «posiciones»:
int posiciones = sizeof(números)/2;
Al corregir el sketch, obtendrás el valor correcto. Sin embargo, de este modo, deberás conocer cada vez el tamaño en bytes del tipo utilizado. Esto podría ser un problema en el caso de tener que compilar el sketch para una plataforma distinta, donde quizás los int ocupan cuatro bytes. Para hacer frente a futuros problemas, podemos adaptar automáticamente el cálculo solicitando a Arduino que especifique el tamaño del tipo «base» utilizado en el array. Así, el sketch pasa a ser:
void setup() {
Serial.begin(9600);
int números[] = {1,2,3,4};
int posiciones = sizeof(números)/sizeof(int);
Serial.println (posiciones);
}
void loop(){}
El cálculo dinámico del tamaño de un array se puede utilizar para gestionar de forma dinámica y automática series de pines. Con cierta frecuencia tendrás que configurar y controlar largas series de pines. Puedes ocuparte de ello administrando un pin cada vez y repitiendo todo el código necesario, o bien listando todos los números de los pines en un array que se recorrerá con un bucle for. Aquí tienes un ejemplo para inicializar un determinado número de pines como salidas:
int pinout[] = {5,10,11,13};
void setup() {
Serial.begin(9600);
for (int i = 0; i < sizeof(pinout)/sizeof(int); i++) {
pinMode(pinout[i], OUTPUT);
Serial.print("inic del pin ");
Serial.println(pinout[i]);
}
}
void loop() {
//código del sketch…
}
Al inicio del sketch se encuentra la definición de un array de tipo int que contiene la lista de los pines a utilizar. En el setup() inicializamos la serie para poder probar el sketch a través del Serial Monitor. La inicialización de los pines se lleva a cabo desde un bucle for que utiliza la variable i. La variable empieza desde cero e irá incrementando de manera que indique todas las celdas del array. El número de celdas no se indica de forma explícita, sino que se calcula «en línea» con la fórmula indicada anteriormente. Dentro del bucle for los pines que hay que inicializar se obtienen leyendo el valor de pinout[i] «indicado» por i.
16. Copiar o ampliar un array
Los valores contenidos en un array no se pueden copiar con una simple reasignación. El mejor método para copiar un array es utilizar el comando memcpy():
memcpy( lista_destino, lista_origen, número_bytes_a_copiar );
El único problema que presenta esta instrucción es que se debe especificar el número de bytes que hay que copiar, número que no se puede expresar en «posiciones», sino en bytes; una información que, a menudo, los neófitos desconocen y que depende del tipo de datos utilizados en el array. Para obtener fácilmente el número de bytes, es aconsejable utilizar el operador sizeof() aplicándolo al tipo de datos contenidos en el array. Para copiar un array de int formado por cuatro elementos podríamos hacer lo siguiente:
memcpy( list2, list1, 4*sizeof(int) );
No existe ningún operador que permita ampliar dinámicamente la longitud de un array. Por este motivo, el único sistema es crear un nuevo array con una longitud superior y, después, copiar los elementos de la estructura más pequeña a la más grande.
Al detalle
Desafortunadamente, en C y en la mayoría de los lenguajes de programación, los array están considerados estructuras complejas y no se pueden copiar directamente como si fueran simples variables. No es posible realizar esta operación:
int lista1[4] = lista2;
Intuitivamente es posible, si entendemos bien cómo está formado un array, realizar la copia con un bucle for. Para copiar un array de tres elementos, podríamos utilizar este código:
int nuevo_array[3];
for (int i = 0; i < 3; i++) {
nuevo_array[i] = array_a_copiar[i];
}
Si bien esta solución funciona, desgraciadamente no es muy eficaz, ni elegante. El lenguaje C dispone del operador memcpy() para copiar directamente un array en otro. La sintaxis es la siguiente:
memcpy( lista_destino, lista_origen, número_bytes_a_copiar );
El problema de memcpy() es definir bien el número de bytes que hay que copiar; de hecho, como no es una función «específica» para la copia de arrays, sino más bien una herramienta para copiar fragmentos de bytes, requiere que se especifique no el número de posiciones a copiar, ¡sino el número de bytes! Este simple requerimiento intimida, sin duda, a cualquier maker principiante. El número de bytes se puede obtener conociendo el tipo de dato memorizado en el array y ayudándose de otro operador: sizeof(). Este devuelve la dimensión en bytes de una variable o de un tipo de datos. Si tenemos un array de int, la dimensión de cada celda es igual al número de bytes ocupado por el tipo int, que se obtiene con sizeof(int). El número de bytes ocupado por un array de enteros de n posiciones será, por tanto, n multiplicado por sizeof(int). Veamos un ejemplo práctico:
int list1[4] = {5,10,11,13};
int list2[4];
void setup()
{
Serial.begin(9600);
memcpy( list2, list1, 4*sizeof(int) );
for (int i = 0; i < 4; i++) {
Serial.println(list2[i]);
}
}
void loop(){}
Las primeras instrucciones del sketch crean dos arrays. El primero, denominado list1, se inicializa con cuatro elementos, mientras que el segundo tiene el mismo tamaño pero está vacío. Para este ejemplo, no es necesario repetir operaciones, por lo que utilizaremos solo el bloque setup(). En el setup() inicializamos el puerto serie para poder escribir en él desde el sketch. La operación de copia con memcpy() es inmediata. Los argumentos del comando son la lista de destino, la lista de origen y el número de bytes a copiar. Si sabemos que los arrays son de tipo int y que el número de celdas a copiar es igual a cuatro, el cálculo del número de bytes se obtiene utilizando el operador sizeof(int),que devolverá el tamaño efectivo, en bytes, de una celda. El bucle for muestra en el Serial Monitor el contenido del segundo array, para comprobar la copia resultante.
La instrucción memcpy() también se puede utilizar para ampliar un array. Entre las instrucciones de Arduino no existe un comando para crear y gestionar matrices dinámicas y, por tanto, para ampliar un array, es preciso crear uno más grande y copiar en él el contenido del más pequeño. A continuación, puedes ver un sketch igual que el anterior pero donde el tamaño del segundo array es cinco.
int list1[4] = {5,10,11,13};
int list2[5];
void setup()
{
Serial.begin(9600);
memcpy( list2, list1, 4*sizeof(int) );
for (int i = 0; i < sizeof(list2)/sizeof(int); i++) {
Serial.println(list2[i]);
}
}
void loop(){}
El sketch es completamente igual que el anterior, con la única diferencia de que el tamaño de list2 ahora es cinco. La copia realizada con memcpy() se lleva a cabo siempre sobre cuatro celdas. Esta vez, el bucle for calcula dinámicamente el número de informaciones que hay que escribir en el Serial Monitor, utilizando para ello sizeof(). El número de celdas del array a recorrer es igual a las dimensiones en bytes de todo el array (list2), dividido entre el tamaño en bytes de cada celda (sizeof(int)). El resultado de este cálculo es, por tanto, cinco.
Veamos ahora otra variante, donde el segundo array se determina de forma dinámica y solo cuando se necesita. En los sketch anteriores conocíamos de antemano las dimensiones del array, pero a menudo esta información no se sabe y se necesita crear un array con un número de celdas calculado al instante. El sketch es este:
int list1[4] = {5,10,11,13};
void setup() {
Serial.begin(9600);
//creo un array con una posición más respecto a list1
//el tamaño de list1
int posiciones_list1 = sizeof(list1)/sizeof(int);
//defino el nuevo array:
int list2[ posiciones_list1 + 1 ];
//lo inicializo:
for (int i = 0; i < sizeof(list2)/sizeof(int); i++) {
list2[i] = 0;
}
//copio list1 en el nuevo array que acabo de crear
memcpy( list2, list1, 4*sizeof(int) );
for (int i = 0; i < sizeof(list2)/sizeof(int); i++) {
Serial.println(list2[i]);
}
}
void loop(){}
En este fragmento tenemos solo la definición inicial de list1 que contiene cuatro números. Dentro del setup() crearemos un nuevo array con una posición más respecto a las de list1. Para crear correctamente este array, en primer lugar, se debe calcular el tamaño de list1 con:
int posiciones_list1 = sizeof(list1)/sizeof(int);
Inmediatamente después, ya se puede crear el nuevo array, list2, con una celda más:
int list2[ posiciones_list1 + 1 ];
Para tener la certeza de que list2 no contiene números aleatorios, debemos inicializarlo escribiendo en cada una de las celdas el número «0» y, después, realizar la copia de list1 utilizando memcpy().
17. Utilizar arrays bidimensionales (matrices)
El lenguaje C define matrices como arrays de arrays. Una matriz de enteros se puede definir con la siguiente línea de texto:
int matriz [filas][columnas];
Las matrices pueden tener también más de dos dimensiones. Para acceder a los elementos se utilizan bucles for anidados.
Al detalle
Para gestionar mapas o conjuntos de datos de dos o más dimensiones, es preciso utilizar matrices. Una matriz es una tabla de datos, organizados en filas y columnas. Algunos lenguajes de programación cuentan con sistemas de gestión de alto nivel de este tipo de datos; en C, a menos que no escribamos nosotros una librería (o que la busquemos), debemos trabajar con arrays de arrays. Una matriz sería un array que contiene otros arrays. Para evitar confusiones, es recomendable que los arrays contenidos tengan todos el mismo tamaño. El lenguaje C permite definir un array multidimensional simplemente añadiendo el nuevo tamaño después de la declaración del array:
int matriz [4][4];
El primer índice especifica el número de filas y el segundo, el número de columnas. El array denominado matriz es una tabla de números con cuatro columnas y cuatro filas. La matriz también se puede inicializar con valores. Se escribe de la siguiente manera:
int matriz[4][4] = {
{1 ,2 ,3 , 4},
{11,12,13,14},
{21,22,23,24},
{31,32,33,34}
};
Es muy importante insertar correctamente los pares de llaves y las comas. Para evitar confusiones, es oportuno aplicar una sangría en varias líneas y añadir, si es necesario, espacios vacíos. Un array dimensional puede tener también más de dos dimensiones. Una matriz tridimensional se puede declara de este modo:
int cubo[3][3][3];
El acceso a los elementos se lleva a cabo mediante índices que siempre empiezan desde 0 y alcanzan una longitud máxima de menos 1. Para acceder al valor que ocupa la segunda fila, tercera columna, escribiremos:
int val = matriz[1][2];
Para trabajar con estructuras multidimensionales, se recomienda utilizar bucles for anidados que recorren filas y columnas. A continuación, puedes ver un sketch de ejemplo que recorre una matriz y muestra su contenido en el Serial Monitor:
int matriz[4][4] = { //[filas][columnas]
{1 , 2, 3, 4},
{11,12,13,14},
{21,22,23,24},
{31,32,33,34}
};
void setup(){
Serial.begin(9600);
for(int r = 0; r < 4; r++){ //fila
for(int c = 0; c < 4; c++){ //columna
int celda = matriz[r][c];
String str = String(celda) + " ";
Serial.print(str);
}//close for j
Serial.println("\n");
}//close for i
}
void loop(){}
Al inicio del sketch podemos ver la definición de una matriz de enteros con dieciséis elementos organizados en cuatro filas y cuatro columnas. La matriz se inicializa con valores numéricos. Todo el código se encuentra en la parte setup() y el loop() no se utiliza. El primer bucle for utiliza un contador denominado r que recorre las filas de la matriz y va de cero a tres. Dentro del cuerpo del primer bucle for, un segundo bucle gestiona el contador para las columnas, denominado c, que va de cero a tres. Dentro del cuerpo del segundo bucle for podemos utilizar las dos variables c y r para recorrer e indicar cada vez las celdas de la matriz que leeremos con:
int celda = matriz[r][c];
El valor leído se compone en una cadena y se muestra en el Serial Monitor. Al final del cuerpo del segundo bucle for encontramos una instrucción:
Serial.println("\n");
que sirve para escribir un «y a parte» (carácter "\n") y separar las filas mostradas por el bucle for interno.
18. Definir una prueba simple
La instrucción para realizar una prueba y evaluar si una determinada expresión es cierta es if. La forma más sencilla que puede tener un bloque de decisión es la siguiente:
if (expresión) {
//código a ejecutar
}
En el caso de que exista otra alternativa, el modo de escribirlo es: «Si la expresión es cierta haces una cosa; si no, haces otra», y la correspondiente sintaxis, que utiliza la instrucción else, es la siguiente:
if (expresión) {
//código a ejecutar
} else {
//código a ejecutar como alternativa
}
Una estructura de decisión puede incluir varias pruebas que confirman si una expresión es cierta o si lo es otra. Estos bloques pueden tener otras ramas creadas con else if y contar (o no) con un bloque final del tipo else para gestionar los casos restantes:
if (expresión1) {
//código para gestionar el caso 1
} else if (expresión2) {
//código para gestionar el caso 2
} else if (expresión3) {
//código para gestionar el caso 3
} else {
//código para los casos restantes
}
Al detalle
Resulta extraño que un programa tenga un flujo continuo y regular de principio a fin: es habitual que se tengan que medir variables o evaluar condiciones para, después, tomar decisiones. Una de las funciones principales y más importantes de un lenguaje de programación es la de poder «tomar decisiones», es decir, «realizar pruebas» evaluando si una determinada expresión es verdadera o falsa. No se admiten situaciones intermedias, porque estamos hablando de lógica «de cálculos» o «booleana». Existen muchos tipos de lógicas, entre las cuales algunas de particulares, más parecidas a nuestra manera de razonar, denominadas lógicas Fuzzi, donde no solo existe lo verdadero o lo falso, sino que también se admiten situaciones intermedias o con matices. Pero Arduino no dispone de ellas.
La forma más simple de prueba es el uso de la instrucción if, seguida del código por ejecutar en el caso de que la expresión que le ha sido proporcionada sea cierta. El código puede estar encerrado entre llaves, o bien escrito «en línea» inmediatamente después de if. Esta sería una sencilla instrucción if que escribe un mensaje en el Serial Monitor:
if (expresión) Serial.println("la expresión es verdadera");
La instrucción if puede «activar» otras operaciones, utilizando para ello un par de llaves:
if (expresión) {
Serial.println("la expresión es verdadera");
}
La expresión por evaluar puede ser cualquier cosa que devuelva un valor verdadero o falso. Por tanto, podría ser una variable de tipo booleano, una condición lógica o matemática a evaluar o bien el resultado de una función o de un comando, como el valor devuelto por una digitalRead(). Una variable booleana se define con:
bool prueba;
y puede ser inicializada o contener solo los tres valores true y false:
bool prueba = false;
prueba = true;
Los términos true y false son, en realidad, nombres de conveniencia o marcadores de posición para dos valores numéricos. El valor true corresponde, de hecho, a 1 y false a 0. Una variable de tipo bool puede ser utilizada directamente por una if:
bool prueba = true;
if (prueba) {
Serial.println("prueba es true");
//otras instrucciones…
}
En C existe el modificador ! que niega, es decir, invierte el valor de una expresión booleana. Para mostrar una cadena en el Serial Monitor, en el caso en que una determinada expresión no sea verdadera, se puede escribir así:
if (!prueba) {
Serial.println("prueba es false");
//otras instrucciones…
}
También se pueden llevar a cabo pruebas evaluando el valor de una determinada expresión. Estas valoraciones también pueden ser de tipo matemático y son muy frecuentes en los sketch de Arduino. Quizás deseas encender un LED solo cuando una variable asume un determinado valor: si el nivel es igual a 100, enciende el LED. La traducción de esta expresión presenta una pequeña dificultad. Cuando se evalúa la semejanza de una expresión en C, se debe utilizar un doble signo de igual, para distinguir una prueba de una asignación de valor a una variable:
if (nivel == 100) {
//instrucción a ejecutar
}
Esta escritura devuelve true solo cuando la variable nivel asume el valor «cien». En programación te encontrarás con varios tipos de condiciones. Por ejemplo, puedes tener que comprobar si el valor contenido en una variable es mayor que un número:
if (temperatura > 30) {
//enciende el ventilador
}
El ventilador se encenderá cuando la temperatura sea superior a treinta y, por tanto, a partir de treinta y uno.
Puedes comprobar cuando una variable es mayor o igual que un número colocando el símbolo igual (=) después del de mayor que (>):
if (temperatura >= 30) {
//enciende el ventilador
}
En este caso, el ventilador se encenderá cuando la temperatura sea igual o mayor que treinta. Un operador muy útil es el módulo (%), que se puede utilizar para obtener el resto de una división. Escrito así podría parecerte que no tiene nada de particular, pero es extremadamente práctico para distinguir números pares e impares, o bien para realizar recuentos periódicos. Así es como se identifica si un número es par o impar:
void setup() {
Serial.begin(9600);
for(int i = 0; i < 10; i++) {
Serial.print(i);
if ((i%2) == 0) {
Serial.println(" = par");
} else {
Serial.println(" = impar");
}
}
}
void loop(){
}
Dentro del bucle for, con i%2 calculamos el resto de la división de i entre 2. El resto de esta división podrá valer 0 o 1 y será 0 si el número es par. De este modo, reconocer si un número es par o impar es inmediato. El operador de módulo se puede utilizar también para crear recuentos que se repiten. Imagina que tienes un contador y quieres que un LED se encienda cada cuatro pasos. Una solución bastante intuitiva, aunque no demasiado elegante, es la de crear un contador secundario que vaya de uno a cuatro y que, después, vuelva a empezar. Cuando el contador es igual a cuatro, el LED se enciende.
int i = 0;
void setup() {
Serial.begin(9600);
}
void loop(){
i++;
Serial.print("i: ");
Serial.println(i);
if (i == 4) {
Serial.println("LED on");
i = 0;
}
}
Para mantener el recuento se necesita un índice principal, externo, creado con una variable i que aumenta en la sección loop() del sketch. Una instrucción if comprueba si el contador es igual a cuatro y, en el caso que lo sea, escribe en el Serial Monitor «LED on». Antes de salir del bloque de código del if, la variable i debe reiniciarse para poder retomar el recuento desde el principio. Con el operador módulo cuatro todo es mucho más sencillo porque no hay que reiniciar el contador cada cuatro pasos. El operador % realiza la división entre i y 4 y devuelve el resto, que será un número que va, periódicamente, del 0 al 3. Mira cómo modificar el sketch:
int i = 0;
void setup() {
Serial.begin(9600);
}
void loop(){
i++;
Serial.print("i: ");
Serial.println(i);
if ((i%4) == 3) {
Serial.println("LED on");
}
}
Ahora i seguirá aumentando, pero cada cuatro pasos aparecerá el texto «LED on».
A menudo tendrás que evaluar varias condiciones y la expresión será más complicada. En C es posible combinar varias expresiones, encerrándolas entre paréntesis y utilizando los operadores lógicos «y» (&&) y «o» (||). Para mostrar un determinado mensaje, si la variable i está comprendida entre tres y cinco deberás escribir:
if ((i >= 3) && (i <= 5)) {
Serial.println("i está en el intervalo");
}
Utiliza los paréntesis para separar las condiciones y poner orden.
Para realizar una operación si una entre varias condiciones es verdadera, haz esto:
int i = 10;
int j = 5;
int k = 6;
if ((i == 9) || (j == 5) || (k == 2)) {
Serial.println("¡OK!");
}
El programa mostrará «¡OK!» si i es igual a nueve, cinco o dos. Obviamente, puedes combinar entre sí distintos operadores lógicos.
Una prueba podría incluir una alternativa: si se verifica una determinada cosa, hago una cosa; si no, hago otra. Este comportamiento se traduce en la estructura if...then y es como si tu programa estuviera en una encrucijada: si ocurre una cosa, voy hacia un lado, y si no, voy hacia el otro.
int i = 10;
if (i == 5) {
Serial.println("i vale 5");
} then {
Serial.println("i no vale 5");
}
Si if...then es comparable a una encrucijada, puedes crear más alternativas con else if:
int i =5;
if (i == 0) {
Serial.println ("A \n");
} else if (i == 5) {
Serial.println ("B \n");
} else {
Serial.println ("C \n");
}
19. Definir una prueba con más de una alternativa
Para controlar el flujo de un programa muy complejo y ramificado, y evitar el uso de muchos if, resulta aconsejable utilizar switch, que evalúa una variable, de tipo int o char, y después elige entre las distintas posibilidades proporcionadas. Un switch es normalmente más rápido que una serie de if. Esta es la sintaxis:
switch(variable) {
case label1:
//código a ejecutar
break;
case label2:
//código a ejecutar
break;
case label3 :
case label4 :
case label5 :
//código a ejecutar para distintos valores unificados
break;
default :
// código a ejecutar en los casos no previstos
}
Al detalle
Un programa complejo puede requerir una larga lista de if para gestionar distintas situaciones. En el caso en que los if se refieran a una única variable, es oportuno utilizar el constructo switch, que es más elegante y más eficaz que una lista de if. switch evalúa una variable que puede ser de tipo int o char y, después, según el valor, salta a una de las secciones marcadas con «etiquetas». Las secciones están delimitadas por la etiqueta y por un break. La etiqueta es, en el caso de variables enteras, un número, mientras que en el caso de variables de tipo char, el símbolo del carácter, encerrado entre comillas simples (por ejemplo, ‘a’). Veamos un ejemplo para evaluar el valor de una variable entera:
int marcha = 1;
switch(marcha) {
case 1 :
Serial.println("Empieza");
break;
case 2 :
Serial.println("has puesto segunda");
break;
case 3 :
case 4 :
case 5 :
Serial.println("¡Brooom!");
break;
default :
Serial.println("¿Qué marcha llevas puesta?");
}
El constructo switch ejecuta una prueba inicial sobre la variable marcha y, después, hace que la ejecución del programa salte hasta la sección adecuada. Cada sección está delimitada por case...break. Como puedes ver, se pueden agrupar varias secciones, manteniendo break (como es el caso de 3, 4 y 5). La última sección del bloque switch es default, que se utilizaría en el caso de situaciones no previstas. Si marcha valiera «-1», terminarías en la sección default. El constructo switch puede parecer completamente igual que un listado de if...then pero, a diferencia de if, es más rápido porque ejecuta una única prueba, mientras que la lista de if requiere tantas pruebas como if presentes.
Si la variable de control de switch fuera de tipo char, no habría ninguna diferencia, solo habría que prestar atención en definir correctamente las etiquetas. Este es un ejemplo que descodifica un comando de tipo char recibido por una hipotética función leeComandoDesdeBluetooth():
char cmd;
cmd = leeComandoDesdeBluetooth();
switch (cmd){
case ΄a΄:
Serial.println("a");
break;
case ΄s΄:
Serial.println("s");
break;
default:
Serial.println("??");
}
20. Definir una función
Para crear una función debes:
•Indicar el tipo de dato que devolverá dicha función.
•Asignar a la función un nombre «unívoco» y significativo.
•Declarar los parámetros que se utilizarán como si fueran variables y encerrarlos entre paréntesis después del nombre.
•Añadir el cuerpo de la función, es decir, las operaciones que deberá llevar a cabo.
•Devolver el cálculo ejecutado mediante la palabra clave return.
A continuación, puedes ver el código de una función genérica que recibe dos parámetros de tipo int, los utiliza para realizar cálculos y, después, devuelve un tipo int:
int función(int a, int b) {
int res = 0;
//hace algo con los parámetros
return res;
}
Una función que no devuelve nada es un procedimiento y se define anteponiéndole la palabra clave void:
void procedimiento() {
//código a ejecutar
}
Un procedimiento también puede recibir parámetros:
void procedimiento(int param) {
//hace algo con param
}
Los parámetros de las funciones normalmente se pasan por valor, lo que significa que los valores de las variables se copiarán en el espacio de memoria de la función. Para permitir que la función modifique directamente las variables pasadas como parámetros, se utiliza el paso por referencia:
void función(int* n) {
*n = *n + 1;
}
Aquí, la llamada a la función se lleva a cabo anteponiendo la & al nombre de la variable:
fiunción(&variable);
De este modo se pasa a la función la dirección de la variable en lugar de su valor.
Al detalle
Los principiantes tienden a escribir todo el código en una secuencia. Este método instintivo funciona, pero produce un código confuso y difícil de mantener. Escribir programas como largas listas de instrucciones es una práctica poco eficaz, que puede funcionar para casos muy sencillos. Sin embargo, en cuanto las cosas se complican, gestionar un programa con esta estructura resulta imposible. Sin duda, tendrás partes que se repiten muchas veces que no podrás gestionar con un simple «copiar y pegar». Imagina que has duplicado varias veces un grupo de líneas que realizan una operación repetitiva: leen la fecha del sistema y la muestran de un modo en particular (año/mes/día). Si al cabo de un tiempo necesitas modificar el modo en que se muestra esta información (año/mes/día), deberás repasar el código de arriba a abajo, modificando todas las secciones donde aparece la fecha. Para evitar este tipo de problemas y organizar el código en partes aisladas y destinadas a llevar a cabo una tarea en concreto, se utilizan las funciones. Una función permite aislar un grupo de instrucciones, atribuirles un nombre conveniente, definir qué necesita para que funcione (los parámetros) y el tipo de resultados que producirá. Una función para sumar dos números tendrá esta forma:
int suma(int a, int b) {
int res = 0;
res = a + b;
return res;
}
Puedes insertar una función en cualquier parte del sketch. Yo, normalmente, prefiero insertarlas justo después de setup() y loop(). Al leer el código de la función anterior, obtenemos informaciones. La primera palabra con que te encuentras es «int», que especifica el tipo de dato devuelto. Inmediatamente después, tenemos el nombre de la función, que debería ser explicativo. Se recomienda utilizar nombres compuestos que expliquen para qué sirve la función: f012() no dice demasiado, pero calcula_fecha()
o leeSensorTemperatura() seguramente sí. Puedes utilizar varias letras separándolas con la inicial en mayúsculas, o con un guion. Las palabras deben estar unidas, no se pueden utilizar espacios. La función suma acepta dos parámetros, a y b, de tipo int. Los parámetros se utilizan para realizar cálculos y devolver un resultado de tipo int con la palabra clave return.
En resumen, para crear una función es preciso:
•indicar el tipo de dato que devolverá la función.
•bautizar la función con un nombre «unívoco» y significativo.
•declarar los parámetros que se utilizarán como si fueran variables y encerrarlos entre paréntesis inmediatamente después del nombre.
•añadir el cuerpo de la función, es decir, las operaciones que deberá llevar a cabo.
•devolver el cálculo ejecutado utilizando la palabra clave return.
En el caso de que algunas partes no se utilicen, estas se pueden excluir. Es posible tener una función que no devuelva ningún resultado y que se denomina procedimiento y su nombre va precedido por la palabra clave void, que significa «vacío». Un procedimiento puede tener o no tener parámetros. Dentro de un procedimiento, no encontraremos la palabra return. Este sería un procedimiento que no recibe ningún parámetro:
void errorFatal() {
Serial.println("¡¡¡¡¡Error fatal!!!!!");
}
loop() y setup() también tienen esta forma: son procedimientos que no devuelven nada y no disponen de parámetros. Aquí puedes ver el código de un procedimiento que recibe un único parámetro, una secuencia de caracteres que se mostrarán en el Serial Monitor:
void prt(char msg[]) {
Serial.println(msg);
}
Para llamar una función o un procedimiento, basta con escribir su nombre, proporcionando, si los tiene, los parámetros y recogiendo el resultado obtenido. Este sería un ejemplo de un sketch completo con funciones y procedimientos:
void setup() {
Serial.begin(9600);
errorFatal();
prt("hello world");
int n = suma(10, 20);
Serial.println(n);
}
void loop(){}
int suma(int a, int b) {
int res = 0;
res = a + b;
return res;
}
void prt(char msg[]) {
Serial.println(msg);
}
void errorFatal() {
Serial.println("¡¡¡¡¡Error fatal!!!!!");
}
Hasta ahora hemos utilizado las funciones proporcionando los parámetros, simplemente escribiéndolos entre paréntesis. Normalmente, los parámetros se pasan «por valor», lo que significa que la función recibe una copia de las variables proporcionadas y no modifica las variables originales. Este sistema sirve para evitar cambios en las variables de quien llama y puede evitar muchos problemas en el caso de que haya muchas modificaciones a la vez en una variable. Todas las modificaciones que se realizan sobre la copia de una variable desaparecen y se pierden si no se restituyen con return. Este comportamiento no siempre es el deseado y, en algunos casos, en cambio, sería conveniente poder modificar directamente las variables suministradas para que también se modifiquen en el entorno de quien llama. En C existe esta posibilidad y se denomina paso de parámetros «por referencia» o «por dirección».
Veamos un ejemplo. Crea un sketch y declara las dos funciones:
void fnz1(int n) {
n = n + 1;
}
void fnz2(int* n) {
*n = *n + 1;
}
Seguramente, has visto que Fnz2() es de tipo particular. El parámetro de entrada es de tipo int, pero va seguido de un asterisco *. Cada vez que se utiliza la variable n, delante de su nombre aparece el asterisco. Esta notación es necesaria para poder utilizar el paso de parámetros por referencia. Una variable es un pequeño espacio de memoria dentro de un ordenador. Para poder encontrarla, cada celda debe tener una dirección. Si se conoce la dirección de memoria de la celda resulta inmediato encontrarla dentro de la memoria. ¡Pero ten en cuenta que la dirección es diferente que el valor! La escritura int* sirve para definir un indicador de la celda y es un tipo especial que no contiene un valor, sino la dirección de la celda de memoria de tipo int. Si conoces la dirección, puedes acceder al valor de la celda con una escritura particular, es decir: *n. Normalmente, tratamos solo con los valores de las variables, pero podemos conocer su dirección insertando un símbolo especial: la «et» &. Dada la variable «temperatura», para conocer la dirección de la correspondiente celda de memoria, se utiliza: &temperatura.
Veamos cómo utilizar las dos funciones que acabamos de definir:
int m = 10;
fnz1(m);
Serial.print("m vale: ");
Serial.println(m);
fnz2(&m);
Serial.print("m vale: ");
Serial.println(m);
Utilizamos una variable m, que pasamos a la función fnz1(). Durante la llamada a fnz1() se crea una copia de la variable denominada n. La función utiliza n en su interior y la modifica. La variable «temporal» n, antes de volver al programa principal, vale 11. Cuando la función termina, todas las variables internas son destruidas y las modificaciones se pierden. Por esta razón, cuando el sketch escribe el valor de m, este es aún igual a 10. Con fnz2() las cosas son distintas porque le pasamos la dirección de la variable m (&m) en vez de su valor. Dentro de la función, para trabajar sobre el valor de la variable utilizaremos *n. Sin embargo, cuando la función termine, ¡los cambios permanecerán y la variable m valdrá 11! Si ejecutas el sketch, en el Serial monitor podrás leer:
m vale: 10
m vale: 11
¡Utiliza siempre con atención el paso de variables por dirección y solo si es estrictamente necesario! ¡Recuerda que trabajar directamente con las direcciones de memoria podría ser muy peligroso y producir efectos impredecibles!
Las variables definidas dentro de una función tienen una duración limitada: se crean en el momento en que se invoca la función y, cuando termina su ejecución, se destruyen. Esto significa que no mantienen su valor entre llamada y llamada a menos que se definen fuera de la función (cosa no muy elegante y, sobre todo, arriesgada). El lenguaje de programación C cuenta con el modificador static (que podría confundirse con const), que se utiliza para crear variables permanentes, memorizadas en porciones de memoria especiales. Una variable de este tipo, definida dentro de una función, mantiene su valor entre llamada y llamada porque cuando finaliza su ejecución no se destruye.
Prueba a ejecutar este sketch:
void setup(){
Serial.begin(9600);
}
void loop(){
int n = paso();
Serial.println(n);
delay(500);
}
int paso(){
static int movimientos;
movimientos++;
return movimientos;
}
La función paso utiliza una variable estática denominada movimientos. La variable se crea en la primera llamada de la función y después no se destruye. Al llamar a la función por segunda vez, volverá a encontrarse la variable ya inicializada y valorizada. En el Serial Monitor podrás ver, después, una larga secuencia de números crecientes.
21. Crear clases y objetos
Para escribir programas más complejos, Arduino también permite utilizar las clases y la programación orientada a objetos (OOP). Se puede instanciar una clase con una escritura similar a la que se utiliza para crear una variable:
Led led1 = Led();
La escritura de una clase utiliza una declaración en la cual se insertan los métodos y las variables que pueden ser de tipo privado, público o protegido:
class Led {
private:
int status; //propiedad
public:
Led(); //constructor
void turnOn(); //método
};
La implementación de los métodos es similar a la definición de una función. El nombre del método siempre va precedido del nombre de la clase:
void Led::setState (int s) {
status = s;
}
Al detalle
Hace unas décadas se descubrió que, para escribir programas de un modo más fácil, se podía utilizar un nuevo método: la programación orientada a objetos (OOP – Object Oriented Programming). En nuestro mundo interactuamos continuamente con objetos. Los objetos tienen propiedades, como color, forma, peso, precio, etc. Interactuamos con los objetos realizando acciones: los movemos, nos los comemos, los creamos, los destruimos, los cambiamos de color...
Alguien pensó que todo esto se podía reproducir también en la programación ofreciendo a los programadores la posibilidad de definir objetos de tipo software dotados de métodos y propiedades.
Las propiedades son simples variables, mientras que los métodos son funciones. Los objetos esconden en su interior los detalles del funcionamiento y exponen «métodos» para poder utilizarlos e interactuar con ellos.
Cada objeto debe, antes que nada, estar definido en su «forma»: es necesario establecer qué métodos y propiedades posee. Esta definición se lleva a cabo escribiendo una clase. La clase es como si fuera el proyecto por seguir para construir el objeto. Para poder utilizar el objeto, es preciso crear una instancia de la clase. Cada instancia tiene una existencia propia que desarrolla independientemente de las de las otras instancias creadas a partir de la misma clase.
Podemos definir un objeto que duplica un automóvil. Para ello, voy a escribir una serie de instrucciones que definen la clase. En la Fiat de Turín tienen un cajón con los proyectos del «Fiat 500». Cuando alguien pide un 500 rojo, toman los proyectos y construyen un coche pintándolo de color rojo. Así es como, en el cajón, tenemos la definición de la clase «auto Fiat 500» y, por la calle, en cambio, encontramos distintas instancias del Fiat 500: rojos, azules, a topos o a rallas. La programación orientada a objetos funciona del mismo modo. El programador ha escrito la definición de la clase de un objeto y, cada vez que lo necesita, crea una instancia de ella que es totalmente distinta a las otras y que tiene una vida propia.
Para crear un ejemplo con Arduino podríamos pensar que tenemos una clase «Led» que contiene variables típicas para un componente electrónico: modelo, color, estado (encendido o apagado) e, incluso, corriente y tensión aplicada. Las variables normalmente no se modifican directamente, sino mediante «métodos». Antes que nada, es necesario crear una nueva instancia de «Led» a partir de su clase. La escritura que se utiliza para instanciar un objeto a partir de una clase es similar a la que se utiliza para crear una nueva variable:
Led led1 = Led();
En primer lugar, encontramos el nombre de la clase «Led», seguido del nombre que queremos dar a esta instancia (led1). La operación es similar a la creación de una variable y el nombre del objeto utiliza las mismas reglas para la nomenclatura. Tras el signo igual, vemos, de nuevo, el nombre de la clase seguido de dos paréntesis: es el constructor, es decir, el método principal de la clase, que permite construir la instancia inicial del objeto. En este caso, el constructor no tiene parámetros, aunque a veces es posible añadirlos (depende de cómo se ha escrito la clase y si esta los admite). Para la clase «Led» podríamos tener un parámetro que defina el color del componente:
Led led1 = Led("ROJO");
O bien el pin al cual está conectado:
Led led1 = Led(13);
Las informaciones pasadas al constructor se mantienen dentro de la estructura del objeto utilizando simples variables. Para modificar las variables se pueden utilizar los métodos, que son funciones especiales definidas dentro de la clase a la que pertenece el objeto. Esta particularidad, la de mantener «en privado» algunas informaciones respecto a los usuarios, se conoce como encapsulamiento. Para encender y apagar el LED hago lo siguiente:
led1.turnOn();
led1.turnOff();
Los métodos han sido definidos por el programador cuando ha escrito la definición de clase. Quien utiliza la clase no tiene ni idea (ni le interesa saberlo) de cómo se han realizado estos métodos. Por tanto, los objetos son como «cajas negras» de las cuales ignoramos el funcionamiento y la organización interna. El único modo para interactuar lo proporciona un conjunto de métodos que definen la interfaz del objeto.
La definición de una clase en C++ empieza con la palabra clave class, seguida del nombre de la clase y de un par de llaves. Dentro del bloque delimitado por las llaves insertaremos las variables y la declaración de los métodos presentes.
class Led {
private:
int status;
int led;
public:
Led();
Led(int);
void turnOn();
void turnOff();
void setState(int);
int getState();
String toString();
};
En el cuerpo de la definición puedes ver unas palabras especiales: public y private. Estas palabras clave sirven para indicar qué partes serán visibles externamente y cuáles no. Las propiedades del objeto Led son status y led (el pin al cual estará conectado) y no nos interesa que los usuarios puedan acceder directamente a ellos, por lo que las protegemos con la palabra clave private, la cual oculta su visibilidad. Inmediatamente después de la palabra clave public encontramos el constructor Led() y los métodos que queremos que sean públicos o que estén expuestos a los usuarios. El constructor tiene una sintaxis particular respecto a las funciones, como el hecho de que no utiliza ningún tipo de retorno ni void. En la clase Led encontramos dos constructores. El segundo utiliza un parámetro de tipo int que servirá para especificar el pin al cual se encuentra conectado el LED.
Después de haber declarado los métodos que están presentes en la clase, pasamos a su implementación. Para que el compilador reconozca los métodos que pertenecen a la clase, la definición siempre va precedida del nombre de la clase seguido de un par de «dos puntos». Esta es la definición del constructor simple:
Led::Led(){
status = LOW;
pin = 13;
}
La escritura es similar a la que se utiliza para definir una función, solo que el nombre del método o del constructor siempre va precedido del nombre de la clase. Este constructor utiliza las variables internas status y pin, asignándoles valores iniciales. El estado status se define en LOW y el pin predefinido es el número 13. El segundo constructor espera que se indique el pin al que se debe conectar el LED; su implementación será:
Led::Led(int n){
status = LOW;
pin = n;
}
El pin, pasado como parámetro n, se copiará en la variable interna y privada pin. Las variables internas siempre se deberían modificar solo utilizando los métodos previstos por la clase. Así, para modificar el estado del LED utilizaremos los siguientes métodos:
void Led::turnOn() {
status = HIGH;
}
void Led::turnOff() {
status = LOW;
}
También he utilizado un método para definir el estado según convenga:
void Led::setState (int s) {
status = s;
}
Y otro para comprobar en qué estado se encuentra el LED y que devuelve el valor de status (HIGH o LOW):
int Led::getState () {
return status;
}
Por último, resulta conveniente tener un método que ayude a mostrar en pantalla, por ejemplo, en el Serial Monitor, las informaciones del LED:
String Led::toString(){
String s = "led is ";
if (status == LOW) s += "LOW";
else s += "HIGH";
s += " connected on pin ";
s += String(pin);
return s;
}
Para utilizar la clase, instanciar un objeto y encender el LED, haremos lo siguiente:
Led led = Led(13);
led.turnOn();
Serial.println(led.toString());
A continuación, muestro el sketch completo:
class Led {
private:
int status;
int pin;
public:
Led();
Led(int);
void turnOn();
void turnOff();
void setState(int);
int getState();
String toString();
};
void setup(){
Serial.begin(9600);
Led led = Led(13);
Serial.println(led.toString());
led.turnOn();
Serial.println(led.toString());
}
void loop(){}
Led::Led(){
status = LOW;
}
Led::Led(int n){
pin = n;
status = LOW;
}
void Led::turnOn() {
status = HIGH;
}
void Led::turnOff() {
status = LOW;
}
void Led::setState (int s) {
status = s;
}
int Led::getState () {
return status;
}
String Led::toString(){
String s = "led is ";
if (status == LOW) s += "LOW";
else s += "HIGH";
s += " connected on pin ";
s += String(pin);
return s;
}
Los objetos también pueden crear jerarquías. Podríamos tener un primer objeto más sencillo que podríamos utilizar como «apoyo» para crear algo más complejo. Por ejemplo, podríamos tener un objeto de tipo ComponenteElectrónico que utiliza propiedades como corriente, tensión y potencia. Este objeto podría estar «especializado» por otros objetos «derivados», como los objetos «Led» o «Resistor». Cada uno de estos objetos especializados contará con las propiedades heredadas como tensión y corriente y, sin embargo, podrá añadir otras más específicas. El objeto «Led» podría incluir la propiedad color y el objeto «Resistor» podría tener una propiedad resistencia. En la definición de una clase, se puede utilizar la palabra clave protected para que algunas propiedades y métodos sean privados para aquel que utiliza directamente la clase, pero siguen siendo accesibles, en cambio, para quien la desea ampliar.
La información incluida en esta sección es muy limitada. La programación orientada a objetos es un método complejo y articulado. Si deseas profundizar en este tema, deberás consultar un libro que trate explícitamente sobre ello.
22. Generar números aleatorios
Para generar números aleatorios, podemos utilizar la función random():
long n = random(max);
Esta función genera números comprendidos entre 0 y, como máximo, –1. Es posible generar números comprendidos en un intervalo que va desde un mínimo hasta un máximo -1:
long n = random(min, max);
Además, random() genera números pseudoaleatorios y, si deseamos tener números aleatorios de verdad, es recomendable inicializar el algoritmo con:
randomSeed(analogRead(0));
Al detalle
Para generar un número aleatorio, existe una función random(), incluida en las librerías de Arduino, que proporciona un número aleatorio extraído de una secuencia de números generados por un algoritmo. Para generar un número entero de forma aleatoria entre 0 y 99 se utiliza el siguiente texto:
int n = random(100);
También se puede indicar un rango de valores a partir del cual se deben extraer los números. Para obtener números entre 1 y 4, deberás escribir lo siguiente:
int n = random(1,5);
El límite superior siempre se excluye del campo de los valores generados. Los números objetivos son números pseudoaleatorios, porque los genera un algoritmo. ¡Si no tomas las precauciones adecuadas obtendrás siempre las mismas secuencias! Antes de llamar a random() siempre es recomendable inicializar la secuencia con randomSeed(), que también requiere un número aleatorio para ser inicializada. El mejor método para obtener un parámetro aleatorio para inicializar randomSeed() es realizar una lectura analógica. Si las entradas analógicas no están conectadas, solo registran ruido, óptimo para obtener un número aleatorio real. Por tanto, la inicialización de random() se puede realizar así:
randomSeed(analogRead(0));
Aquí puedes ver un sketch completo para mostrar en el Serial Monitor una secuencia de números aleatorios:
long numero;
void setup(){
Serial.begin(9600);
randomSeed(analogRead(0));
}
void loop() {
numero = random(100);
Serial.println(número);
delay(100);
}
Al inicio del sketch preparamos una variable long para guardar los números generados. En el setup() encontramos la inicialización del puerto serie y de la serie de números aleatorios, mediante randomSeed() a la cual hemos proporcionado una lectura analógica tomada de A0. Esta operación «mezcla» los números generados por el algoritmo. En la sección loop(), random(100) genera números aleatorios entre 0 y 99. Los números se muestran en el Serial Monitor.
23. Encontrar el número más alto en un array
Para encontrar el número más alto contenido en un array hay que crear una variable que guarde el resultado y, después, recorrer todos los elementos del array. Cada vez que el elemento actual es mayor que el valor guardado, se sobreescribe.
Al detalle
Los algoritmos de búsqueda siempre son críticos debido al tiempo necesario para recorrer las estructuras de datos. Un caso muy sencillo que ocurre a menudo en programación es uno donde se debe buscar el valor máximo dentro de un array. El método más intuitivo consiste en recorrer la secuencia de valores en busca de aquel que debería ser el número más alto. Para los seres humanos, es sencillo localizar el número más alto dentro de una lista. Si la lista no es muy extensa, basta con un vistazo; si no, nos toca recorrer todos los elementos, uno a uno, hasta que encontramos el que tiene el valor más alto. Un ordenador no puede echar un vistazo a toda una lista y, por tanto, el algoritmo de búsqueda debe «razonar» como si el array fuera parcialmente visible. Imagina que solo puedes leer una casilla del array y que, por tanto, el único elemento visible es el actual. La mejor estrategia para afrontar esta situación de «visibilidad limitada» es disponer de una hoja de papel donde anotar el que, para nosotros, hasta el momento actual, podría ser el número más alto de la lista. Así, imagina que estás sentado frente a una ventana, con tu hoja de papel y un lápiz. Al otro lado de la ventana, un amigo te va mostrando números, uno a uno. Cuando el juego termina, debes indicar cuál es el número más alto entre todos los presentados. No sabes cuántos números te enseñará, pero tu amigo te avisará cuando se terminen. ¿Preparado? ¡Empezamos! El primer número que aparece es un cero, por lo que escribes «0» en tu hoja de papel. Llega el segundo número: «10», que es sin ninguna duda mayor que 0, por lo que este podría ser el número más alto. Borras el 0 y escribes un gran 10. Después aparece un 5, que es inferior a 10 y, por tanto, no puede ser el número más alto. Dejas escrito el 10. Ahora aparece el 56... y lo apuntas de inmediato en el papel. El siguiente número es un 20, por lo que no haces nada. Tu amigo te avisa de que los números han terminado y tú ya puedes afirmar que el número más alto de la serie es el 56.
Veamos cómo traducir en código este algoritmo. Preparamos un array donde insertar la secuencia de números enteros:
int sec[] = {0, 10, 5, 56, 20};
Ahora necesitamos una variable en la cual se memorice el resultado, es decir, el número con el valor más alto de todo el array. Definimos una variable max de tipo entero:
int max = 0;
Como no disponemos de la versión «arduinizada» de un amigo que pasa los números desde el otro lado de la ventana, nos contentaremos con utilizar un bucle for para recorrer los números de la lista y leerlos uno a uno. El bucle parte de 0 y llega al final del array. Podemos hacer que el programa calcule automáticamente el tamaño del array así:
sizeof(seq)/sizeof(int)
Un array se define especificando el número de posiciones, mientras sizeof(), una función predefinida de C, mide las dimensiones de una variable o de un array en bytes. ¡El operador sizeof(seq) no devolverá el número de celdas del array, sino su tamaño en bytes! Si tenemos en cuenta que un int, en Arduino, ocupa dos bytes, podemos utilizar sizeof() para calcular el número de celdas reales del array.
for (int i = 0; i < (sizeof(seq)/sizeof(int)); i++){
//código…
}
En cada iteración, tendremos que comprobar si el valor actual (seq[i]) es mayor que el valor memorizado dentro de la variable max. Para ello, utilizamos un simple if.
if (seq[i] > max) {
//guardo el valor actual…
}
Si la condición se verifica, sobreescribimos el valor de max con el actual de la secuencia.
max = seq[i];
Este es el código completo:
int seq[] = {0, 10, 5, 56, 20};
void setup() {
Serial.begin(9600);
//la variable con el número máximo:
int max = 0;
for (int i = 0; i < (sizeof(seq)/sizeof(int)); i++){
if (seq[i] > max) {
//si el número actual es mayor que el
//guardado dentro de max, lo copio
max = seq[i];
}
}
Serial.println("El max: ");
Serial.println(max);
}
void loop(){}
El valor encontrado aparece en el Serial Monitor. Con pequeñas modificaciones, el mismo algoritmo se puede utilizar para buscar un valor determinado, así como caracteres dentro de una cadena.
24. Ordenar una matriz de números
BubbleSort u ordenamiento de burbuja es un algoritmo de ordenación muy conocido. Una de sus implementaciones para Arduino para ordenar una lista de números enteros es esta:
int a[] = {1,20,15,2,7};
int c = 0;
while (c < sizeof(a)/sizeof(int) ){
//muestra matriz
muestraMatriz();
int i = 0;
while (i < (sizeof(a)/sizeof(int) - 1) ){
if (a[i] > a[i+1]){
//realizo el intercambio
int n = a[i];
a[i] = a[i+1];
a[i+1] = n;
}
i++;
}
c++;
}
Al detalle
Ordenar datos es una de las especialidades de ordenadores y microcontroladores. Existen muchos algoritmos para ordenar listas de números que se diferencian por su rapidez y efectividad. Un buen algoritmo realiza su tarea con pocas iteraciones y, sobre todo, con un número de pasos predecible con anterioridad. Algunos lenguajes de programación incluyen en sus librerías algoritmos para ordenar directamente los tipos de datos tratados, para que no sea necesario escribir el código desde cero cada vez que se necesite. Un algoritmo de ordenación muy conocido es BubbleSort, cuyo nombre evoca las burbujas que se elevan hacia arriba mientras se lleva a cabo la ordenación. Para comprender su funcionamiento, nos basamos en una lista de números:
20,15,1,7
El BubbleSort recorre el array varias veces y cada vez que encuentra una situación anómala intenta solucionarla «localmente». Es decir, analiza dos números adyacentes y, si no están en orden, los cambia de posición. Si se repite esta operación durante un determinado número de pasos, se consigue ordenar todo el array porque los números «se desplazan» en un sentido o en el opuesto, ordenándose.
Empecemos a analizar la lista de números. Vamos a imaginar que cogemos el primer número de la lista y lo comparamos con el siguiente. Si el siguiente es menor, los cambiamos de posición.
Nuestro array inicial:
20,15,1,7
pasará a ser:
15,20,1,7
El 15 y el 20 han intercambiado sus posiciones y parece que el 20 «se desplace» hacia la derecha. Ahora, tomamos el número 20, que está en segunda posición, y lo comparamos con el número de su derecha: 1. Si 1 es menor que 20, los cambiamos de posición.
15,1,20,7
Repetimos la comparación una última vez: 20 > 7 y, por tanto, de nuevo los cambiamos de posición. El número 20 ha acabado en último lugar.
15,1,7,20
Ahora volvamos a empezar. El primer número es 15 , que es mayor que 1. Los cambiamos de posición y seguimos adelante.
1,15,7,20
En el siguiente paso encontramos 15 y 7, y los cambiamos de posición.
1,7,15,20
Ahora tenemos 15 y 20, pero 20 es mayor que 15, por lo que nos detenemos y empezamos de nuevo. Esta vez realizaremos una vuelta inútil porque 1 < 7, 7 < 15 y 15 < 20 y, por tanto, no haremos ningún cambio. La lista está ordenada.
Para estar seguros de que la hemos ordenado, en el peor de los casos, con una lista de N elementos se necesitan N-1 pasos. Con nuestra lista necesitamos solo cuatro pasos. A continuación, puedes ver el sketch completo:
int a[] = {1,20,15,2,7};
void setup(){
Serial.begin(9600);
int c = 0;
while (c < sizeof(a)/sizeof(int) ){
//muestra matriz
muestraMatriz();
int i = 0;
while (i < (sizeof(a)/sizeof(int) - 1) ){
if (a[i] > a[i+1]){
//realizo el intercambio
int n = a[i];
a[i] = a[i+1];
a[i+1] = n;
}
i++;
}
c++;
}
Serial.println("Resultado final:");
muestraMatriz();
}
void loop(){}
void muestraMatriz(){
Serial.print("[");
for (int j = 0; j < sizeof(a)/sizeof(int); j++) {
Serial.print(a[j]);
if (j < sizeof(a)/sizeof(int) - 1) Serial.print(",");
}
Serial.println("]");
}
El array está situado al inicio del sketch. He añadido una función de conveniencia, para simplificar la escritura del código, que muestra en el Serial Monitor los elementos del array. La función se denomina muestraMatriz() y se encuentra al final del sketch. Todo el código forma parte del setup(). El recorrido del array se lleva a cabo con dos bucles while. El bucle más interno utiliza la variable-índice i y, en cada paso, compara el elemento a[i] con el siguiente a[i+1]. Por este motivo, el bucle while no llega hasta el final de la lista, sino que se detiene un paso antes. En el cuerpo del while, si el valor que contiene a[i+1] es menor que el que contiene a[i], los dos números intercambian sus posiciones. Para realizar el intercambio se necesita una variable de apoyo, n, en la cual se copia el valor de a[i] que, después, será sobreescrito por el de a[i+1]. El bucle while externo sirve para recorrer el array el número suficiente de veces para garantizar la ordenación.
Si ejecutas el algoritmo verás que el array quedará ordenado tras unos pocos pasos y que, sin embargo, el algoritmo lo recorrerá otras veces sin hacer nada. Podríamos mejorar el sketch insertando una prueba que detecte el momento en que la lista de números ya está ordenada y, por tanto, interrumpa la ejecución.
Aquí tienes el código final al cual he añadido una variable m que aumenta cada vez que se produce un intercambio de números:
int a[] = {1,20,15,2,7};
int c = 0;
void setup(){
Serial.begin(9600);
while (c < sizeof(a)/sizeof(int) ){
//muestra matriz
muestraMatriz();
int m = 0; //sirve para interrumpir la ejecución
//cuando la lista ya está ordenada
int i = 0;
while (i < (sizeof(a)/sizeof(int) - 1) ){
if (a[i] > a[i+1]){
//realizo el intercambio
int n = a[i];
a[i] = a[i+1];
a[i+1] = n;
//si he intercambiado números, incremento m
m++;
}
i++;
}
//si m = 0 el array está ordenado y, por tanto, me detengo
if (m == 0) break;
c++;
}
Serial.println("Resultado final:");
muestraMatriz();
}
void loop(){}
void muestraMatriz(){
Serial.print("[");
for (int j = 0; j < sizeof(a)/sizeof(int); j++) {
Serial.print(a[j]);
if (j < sizeof(a)/sizeof(int) - 1) Serial.print(",");
}
Serial.println("]");
}
Si m no recibe ningún incremento y, por tanto, vale «0», significa que el array ya está ordenado y se puede interrumpir el proceso con un break.
25. Rellenar un array de números aleatorios
Para generar un array de números aleatorios basta con definir el array que hay que rellenar y, después, asignar los valores, uno tras otro, llamando a la función random(). Este es el fragmento de código que hay que utilizar para rellenar un array de diez posiciones con números aleatorios entre 0 y 99:
//defino un array con 10 posiciones
int seq[10];
//inicializo random
randomSeed(analogRead(A0));
//recorro el array
for (int i = 0; i < sizeof(seq)/sizof(int); i++){
//asigno los valores aleatorios
seq[i] = random(0,100);
}
Al detalle
Arduino dispone de la función random() con la cual se pueden generar números enteros pseudoaleatorios: random() utiliza un algoritmo para sortear los números, razón por la cual se denominan pseudoaleatorios. ¡Si no se inicializa el algoritmo, los números se presentarán siempre en el mismo orden! El mejor método para inicializar el algoritmo es utilizar randomSeed(analogRead(A0)). Llamando a analogRead() sobre un pin analógico «desconectado», se obtendrá un número aleatorio, suficiente para inicializar el algoritmo de random().
//inicializo random
randomSeed(analogRead(A0));
Para insertar los valores dentro del array se puede utilizar un bucle for que empieza por el valor «0» y llega hasta el tamaño máximo del array, que puede ser calculado cada vez con sizeof(). Recuerda que sizeof() devuelve el tamaño del array en bytes y que el número de «posiciones» se corrige dividiendo el valor proporcionado entre el tamaño de una celda: sizeof(int). Por tanto, el bucle for será así:
for (int i = 0; i < sizeof(seq)/sizeof(int); i++){
//asigno los valores aleatorios
seq[i] = random(0,100);
}
La función random() genera solo números de tipo entero pero, a veces, podría ser necesario obtener números decimales. ¿Qué podemos hacer en estos casos? Una solución puede ser dividir el resultado proporcionado por random entre 10, 100 o 1000, según las necesidades. El siguiente sketch asigna diez números decimales aleatorios al array seq[], que en esta ocasión es de tipo float:
void setup(){
Serial.begin(9600);
randomSeed(analogRead(A0));
float seq[10];
for (int i = 0; i < sizeof(seq)/sizeof(float); i++) {
float r = random(0,101)/100.0f;
seq[i] = r;
}
//muestra valores para su comprobación
for (int i = 0; i < sizeof(seq)/sizeof(float); i++) {
Serial.println(seq[i]);
}
}
void loop(){}
El sketch no presenta ninguna dificultad en concreto, pero es muy importante calcular correctamente el tamaño del array. En el bucle for recuerda dividir sizeof(seq) entre sizeof(float); en caso contrario, el sketch podría producir algún comportamiento inesperado. El número aleatorio generado dentro del cuerpo del bucle for está comprendido entre 0 y 100, aunque después se divide entre 100 para llevarlo al intervalo entre 0 y 1. Esta división también es muy importante puesto que hay que dividir el número entre 100.0f y no simplemente entre 100. Si escribimos 100.0f estamos especificando a Arduino que utilice para la división un número de tipo float y, por tanto, con coma. Así, no se pierden los decimales, que, de otro modo, se cortarían.
26. Transformar una cadena en una matriz (tokenizar)
Para transformar una cadena compuesta por elementos separados por comas u otro carácter especial, es preciso recorrer el texto con dos índices, identificando cada vez la posición del carácter separador.
Al detalle
A veces es necesario conectarse con sistemas o shields que proporcionan información en un formato de texto. No es extraño el caso en el cual se reciba una larga lista de datos, separados por comas o por otro carácter especial, y es necesario separarlos para poder analizarlos. Esta operación se denomina «tokenización». El token es el elemento que compone la cadena y, habitualmente, se desea tener en una forma más accesible, por ejemplo, en un array.
Los datos recibidos en forma de cadena pueden tener también irregularidades, por ejemplo, espacios situados antes o después del token. Una vez separado el token, resulta sencillo corregir estos problemas, por ejemplo, aplicando el método trim() de los objetos String. Imaginemos que recibimos un mensaje de este tipo:
String msg = "manzanas,peras , fresas, bananas";
Queremos separar todos sus elementos y guardarlos en una lista:
String lista[4];
En este caso, ya indicamos el tamaño, pero no es difícil utilizar un array dinámico.
Para recortar un token, se puede utilizar el método indexOf() de los objetos String, que devuelve el índice de la cadena proporcionada, a partir de una posición determinada. Empezaremos desde «0» y buscaremos la cadena «,»:
int pos = msg.indexOf(",", 0);
Como conocemos la posición de la coma, podemos recortar el token con:
token = msg.substring(0, posición_coma);
El token también podría contener espacios vacíos que podemos eliminar fácilmente con:
token.trim();
Tras haber recortado el primer token, podemos guardarlo dentro de un array y buscar el siguiente. Esta vez no empezaremos desde «0» sino desde la última posición donde hemos encontrado el carácter separador. Para ello, nos situamos en la casilla después de pos. Después de identificar un nuevo token, debemos actualizar la posición de inicio. Cuando lleguemos al último token, el programa no encontrará más comas y pos valdrá «-1». Podemos utilizar este valor para controlar un bucle while e interrumpirlo cuando hayamos identificado también el último token. El sketch completo es este:
String msg = "manzanas,peras , fresas, bananas";
String lista[4];
void setup(){
Serial.begin(9600);
int ini = 0; //punto de inicio
int pos = 0;
String token = "";
int i = 0;
while (pos >= 0) {
//posición de la coma
pos = msg.indexOf(",", ini);
token = msg.substring(ini, pos);
token.trim();
lista[i] = token;
i++;
Serial.println(token);
ini = pos + 1;
}
//muestro la lista
for (int i = 0; i < 4; i++) {
Serial.print(lista[i]);
Serial.print(" - ");
}
}
void loop(){}
Los tokens se guardan en la «lista» que después, al final del setup(), se muestra para su comprobación en el Serial Monitor.
27. Gestionar índices múltiples y periódicos
Para gestionar más de un índice dentro de un bucle for o while basta con definir una segunda variable que sea calculada en función de la primera. En cambio, para crear índices secundarios y repetitivos, es preferible utilizar el operador módulo.
Al detalle
Hace un tiempo, Paolo Grisanti, uno de mis seguidores en YouTube, me preguntó, tras haber realizado una videolección sobre el bucle for, qué debía hacer para obtener un segundo índice, dentro del bucle for, que decreciera mientras el principal aumentaba de valor. La solución a este problema es sencilla: basta con definir un segundo índice, memorizado en una variable, que se calcule en cada iteración. Imagina que tienes un bucle for que va de 0 a 255 y que quieres añadir un índice j que, al mismo tiempo, pase de 255 a 0. Así es como se hace:
void setup(){
Serial.begin(9600);
for (int i = 0; i < 255; i++) {
int j = 255-1-i;
Serial.print(i);
Serial.print("\t");
Serial.println(j);
delay(10);
}
}
void loop(){}
El bucle for principal utiliza la variable i como índice. La variable va de 0 a 254. En el cuerpo del bucle añadimos una segunda variable j cuyo valor se calcula a cada paso. Para que su valor decrezca cuando i aumente, bastará con extraer i al máximo valor menos uno que i puede alcanzar. Los dos índices se muestran en el Serial Monitor para ser comprobados.
Otras veces podemos necesitar un índice secundario para identificar iteraciones pares o impares. Existen distintos métodos para identificar un número par o impar, pero el más eficaz y elegante es utilizar el resto de la división entre dos. En C, existe un operador denominado «módulo» que devuelve al instante dicho resto. Para identificar un número par, se dividirá el número entre «2» y se comprobará que el valor del módulo, es decir, el resto de la división, sea igual a «0». Para los números impares, el resto será «1». Este es un ejemplo realizado con un bucle for que muestra en cada línea «pares» o «impares»:
for (int i = 0; i < 10; i++) {
int pd = i%2;
Serial.print(i);
if (pd == 0) {
Serial.println(": pares");
} else {
Serial.println(": impares");
}
}
El módulo también se puede utilizar para crear índices repetitivos. Imagina que deseas gestionar un contador periódico dentro del loop() de un sketch que va de 0 a 4. De forma intuitiva, lo realizarías con una variable que incrementaría a cada paso. El problema de esta solución es que cada cuatro pasos tendrías que restablecer la variable para que empezara desde cero. Utilizando el módulo «cuatro», el sketch sería del siguiente modo:
int c = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
int step = c%4;
Serial.print("step: ");
Serial.println(step);
delay(100);
c++;
}
Para activar una acción específica, por ejemplo, el último de los cuatro incrementos periódicos, se puede insertar un if que «salte» cuando el valor del módulo sea igual a 3:
int c = 0;
void setup(){
Serial.begin(9600);
}
void loop(){
if ((c%4) == 3){
Serial.println("¡4!");
} else {
Serial.print(".");
}
c++;
}
28. Trabajar con bits
Si bien el lenguaje C dispone de operadores para trabajar con bits, Arduino proporciona una serie de funciones que simplifican las operaciones a «bajo nivel»:
•highByte(n): devuelve el byte más alto de un número.
•lowByte(n): extrae el byte más alto del número proporcionado.
•bitRead(número, bit_a_leer): lee un único bit del número proporcionado.
•bitWrite(número, bit_a_leer, bit): escribe el bit indicado.
•bitSet(número, bit_a_configurar): configura a 1 el bit indicado.
•bitClear(número, bit_a_eliminar): elimina el bit indicado.
C ofrece también operadores para intervenir directamente sobre bits individuales:
•& es el operador para AND.
•| es el operador para OR.
•^ es el operador para ExOR.
•! es el operador de negación.
Al detalle
Si trabajas con microcontroladores, de vez en cuando puede que tengas que tratar directamente con bits y bytes. Las configuraciones de los pines para el ATMega328, el microcontrolador que utiliza Arduino, se realiza ajustando los bits de algunos registros especiales. Un registro es una variable especial situada dentro de una CPU. Por suerte, Arduino cuenta con muchas funciones de alto nivel que enmascaran el uso de bits y bytes y, para configurar los puertos de entrada, de salida y el estado de un pin, existen métodos adecuados que evitan actuar directamente sobre los registros de los microcontroladores: operación inútilmente complicada y bastante críptica para un principiante. Sin embargo, en algunas ocasiones podría ser necesario actuar sobre los bits, por ejemplo, cuando se utilizan registros deslizantes o determinados chips externos.
Antes de continuar, es mejor que hagamos alguna aclaración sobre los bits y los bytes: un bit es una pequeña unidad de memoria que puede asumir solo dos estados: «0» o «1». Un conjunto de ocho bits se denomina byte. Contar en bits no es muy difícil: se empieza desde cero y, añadiendo una unidad, tenemos «1». Si añadimos otra unidad tendremos diez y no dos. Esto es así porque los números que utilizamos habitualmente utilizan cifras del «0» al «9» y cuando, contando, llegamos al «9», eliminamos las unidades, añadimos una decena y tenemos un «10». Los bits normalmente se escriben de derecha a izquierda.
Este sería un recuento binario:
0 vale 0, 1 vale 1, 10 vale 2, 11 vale 3, 100 vale 4, 101 vale 5, 110 vale 6, etc.
Para convertir un número de binario a decimal, se puede utilizar una tabla donde cada posición tiene un peso. Escribiendo el número que deseamos convertir en las celdas de la tabla y sumando los correspondientes pesos se puede convertir en decimal.
Tabla 2.1 – Conversión de binario a decimal.
| Pesos | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | Número |
| 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 73 | |
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 10 |
Un número compuesto por ocho bits forma un byte que, después, puede asumir todos los valores desde 00000000 hasta 11111111, es decir, de 0 a 255. Para evitar tener que escribir todos estos «1» y «0», muchas veces se utiliza la notación hexadecimal, otro sistema de numeración que emplea 16 símbolos en lugar de las habituales diez cifras del 0 al 9. En el sistema hexadecimal, las cifras posibles son: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Los símbolos de la A a la F valen 10, 11, 12, 13, 14 y 15. Contando en hexadecimal, al llegar a F, se sigue eliminando las unidades y añadiendo una decena: tras la F está el «10», que vale «16».
Cada símbolo hexadecimal puede ser escrito con cuatro bits, por lo que:
0 corresponderá a 0000, 1 corresponderá a 0001, 2 corresponderá a 0010, 3 corresponderá a 0011, hasta llegar a F, que será igual a 1111.
Dado que un byte está formado por ocho bits, puedes indicarlo con dos cifras hexadecimales que equivalen exactamente a ocho bits:
•01 en hexadecimal es igual a 0000 0001, es decir, a 1 en decimal.
•0A en hexadecimal es igual a 0000 1010, es decir, a 10 en decimal.
•0F en hexadecimal es igual a 0000 1111, es decir, a 15 en decimal.
•10 en hexadecimal es igual a 0001 0000, es decir, a 16 en decimal.
•A0 en hexadecimal es igual a 1010 0000, es decir, a 160 en decimal.
•FF en hexadecimal es igual a 1111 1111, es decir, a 255 en decimal.
Arduino puede tratar directamente números binarios anteponiendo la letra «B» (en mayúsculas) a la secuencia de 1 y 0: B00110101. En C también puedes trabajar con valores hexadecimales. Para inicializar una variable con un número hexadecimal haces lo siguiente:
int n = 0x0BAF;
Un número entero en Arduino está formado por dos bytes. Por ejemplo, el número 6660 está formado por dos bytes 1A04, es decir, por los bits 0001 1010 0000 0100. El byte «alto» de este número es igual a los primeros cuatro bits empezando por la izquierda: 0001 1010 y, por tanto, «26» (en decimal).
Con Arduino, se pueden utilizar las funciones lowByte() y highByte(), las cuales extraen el byte más alto o más bajo de un número o de una variable. Puedes extraer el byte más alto utilizando la función highByte():
byte val = highByte(n);
El byte más bajo es, en cambio, 0000 0100, que vale 4 (decimal) y que puedes extraer con:
byte val = lowByte(n);
A continuación, puedes ver un sketch que lo resume todo:
void setup() {
Serial.begin(9600);
int n = 6660;
Serial.print("número: ");
Serial.println(n);
byte val = highByte(n);
Serial.print("high byte: ");
Serial.println(val);
val = lowByte(n);
Serial.print("low byte: ");
Serial.println(val);
}
void loop(){}
Arduino proporciona también funciones para leer los bits individuales de un número. Con:
byte val = bitRead(número, bit_a_leer);
Puedes leer el bit en la posición n de la variable x. La posición se lee a partir del bit menos importante, el situado a la derecha de la secuencia de bits. El valor de n empieza de cero.
Para configurar un bit individual, en cambio, puedes utilizar:
bitWrite(número, bit_a_leer, bit);
La posición del bit dentro del número está indicada por bit_a_leer. El valor que se debe configurar es especificado por bit y puede valer 1 o 0.
La función:
bitSet(número, bit_a_configurar);
Es igual a bitWrite(), con la diferencia que configura en 1 el bit indicado. En cambio, la función:
bitClear(número, bit_a_restablecer);
Restablece el bit indicado, llevándolo a 0.
Este sería un sketch de resumen con el cual probar estas funciones.
void setup() {
Serial.begin(9600);
int n = 42258; //A512
byte lo = lowByte(n);
Serial.println(lo); //muestra 18... es decir 12H
byte hi = highByte(n);
Serial.println(hi); //muestra 165 es decir A5H
for (int i = 15; i >= 0; i = i - 1) {
byte b = bitRead(n, i);
Serial.print(b);
} //muestra 1010010100010010
}
void loop() {
}
Vuelve a copiar el sketch y cárgalo en Arduino. Para este ejemplo no necesitas ningún tipo de hardware. Después de haber cargado el sketch, abre el Serial Monitor para observar los resultados.
La función bit, en cambio, calcula el valor de un bit y es un atajo para ayudarte a transformar un bit en un valor decimal. La función equivale a multiplicar el número «2» tantas veces como indica el parámetro proporcionado.
int val = bit(n);
bit(0) es igual a 1, bit(1) vale 2, bit(3) vale 8, es decir, es como realizar la siguiente multiplicación: 2 x 2 x 2, o bien 23. Con esta función puedes convertir un número binario en formato decimal. Si tienes el número 1010, la conversión se realizará con este cálculo:
1 x 23 + 0 x 22 + 1 x 21 + 0 x 20 = 1 x 8 + 0 + 1 x 2 + 0 = 10
En un sketch deberás escribir:
int val = 1 * bit(3) + 0 * bit(2) + 1 * bit(1) + 0 * bit(0);
Operaciones bit a bit de C
El lenguaje C utiliza operadores de bajo nivel para trabajar directamente con bits. La sintaxis de estos operadores puede parecer un poco más complicada que las funciones proporcionadas por Arduino pero, en cambio, ganan en rapidez. Una operación utilizada con mucha frecuencia, sobre todo cuando se utilizan registros o búfers externos, es el desplazamiento de bits. Los pines y muchas de las funciones de Arduino también están controlados por registros que se pueden activar configurando los bits oportunos. El desplazamiento de un valor dentro de un byte se obtiene con los operadores >> y << seguidos del número de posiciones que debe saltar. Imagina que tienes un simple bit configurado al inicio de un byte:
0000 0001
Si llamas a << 1 es posible que se desplace una posición hacia la izquierda, obteniendo:
0000 0010
Este es un fragmento de código para desplazar un bit primero hacia la izquierda y después, hacia la derecha:
byte n = 1;
String str = "";
//desplazamiento hacia la izquierda
for (int i = 0; i < 7; i++){
n = n << 1;
str = String(n, BIN);
Serial.println(str);
}
//desplazamiento hacia la derecha
for (int i = 0; i < 7; i++){
n = n >> 1;
str = String(n, BIN);
Serial.println(str);
}
El bit se configura dentro de la variable n, de tipo byte. Asignar directamente el valor «1» equivale a escribir: 0000 0001. La operación de desplazamiento se repite siete veces para que el bit se desplace hasta la última posición del byte: 1000 0000. El segundo bucle for sitúa el bit a la posición inicial.
El lenguaje C es capaz de realizar operaciones lógicas bit a bit. Las lógicas fundamentales son: AND, OR, ExOR y NOT. AND es una especie de suma que devuelve 1 si ambos operandos son 1. Su comportamiento se muestra en la Tabla 2.2.
Tabla 2.2 – El operador AND.
| A | B | Resultado |
| 1 | 1 | 1 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 0 |
En el lenguaje C se puede llamar utilizando el operador &. El operador AND actúa, no solo sobre los bits contenidos en un tipo byte, sino también sobre números enteros y float. Este es un ejemplo de uso del operador:
0000 1010 &
0000 1100 =
---------
0000 1000
El código equivalente se muestra a continuación:
int a = 1 * bit(3) + 0 * bit(2) + 1 * bit(1) + 0 * bit(0);
int b = 1 * bit(3) + 1 * bit(2) + 0 * bit(1) + 0 * bit(0);
Serial.println(a);
Serial.println(b);
int c = a & b;
String str = String(c, BIN);
Serial.println("AND");
Serial.println(str);
Los dos operandos han sido calculados sumando los bits con la función que calcula su peso según la posición indicada. Es posible indicar su valor directamente anteponiendo la letra «B» a la secuencia de bits:
int a = B00001010; //en hexadecimal 0x10
int b = B00001100; // en hexadecimal 0x12;
El operador AND se puede utilizar también para extraer y evaluar bits individuales dentro de un byte. De hecho, gracias a sus propiedades, permite eliminar, mediante una máscara concreta, todos los bits que no interesan. Imagina que tienes un número (1010 1010) y deseas evaluar el bit situado en cuarta posición. Para eliminar todos los bits, excepto el cuarto, le aplicaremos la siguiente máscara de bits:
0000 1000
Por tanto, la operación de «extracción» será:
1010 1010 & (el número)
0000 1000 = (la máscara)
---------
0000 1000
Ya hemos aislado el bit. Si el bit situado en la cuarta posición fuera un «0», obtendríamos igualmente «0». Aquí tienes un segundo ejemplo:
0110 0010 & (el número)
0000 1000 = (la máscara)
---------
0000 0000
Y este es un ejemplo escrito en C:
//extracción de un bit
int a = 255; //equivale a 1111 1111
int b = 1 * bit(1) + 0 * bit(0); //la máscara 0000 0010
Serial.println(a);
Serial.println(b);
int c = a & b;
String str = String(c, BIN);
Serial.println("Máscara 0000 0010");
Serial.println(str);
El operador OR devuelve 1 si, como mínimo, uno de sus operandos vale «1». Su comportamiento se muestra en la Tabla 2.3.
Tabla 2.3 – El operador OR.
| A | B | Resultado |
| 1 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
En el lenguaje C se puede llamar con el operador |. El operador OR actúa, no solo sobre los bits contenidos en un tipo byte, sino también sobre números enteros y float. Este es un ejemplo de uso del operador:
0000 1010 |
0000 1100 =
---------
0000 1110
El código equivalente se muestra a continuación:
int a = 1 * bit(3) + 0 * bit(2) + 1 * bit(1) + 0 * bit(0);
int b = 1 * bit(3) + 1 * bit(2) + 0 * bit(1) + 0 * bit(0);
Serial.println(a);
Serial.println(b);
int c = a | b;
String str = String(c, BIN);
Serial.println("OR");
Serial.println(str);
Otro operador muy utilizado es la negación (NOT), que devuelve siempre el bit contrario al proporcionado. Su comportamiento se muestra en la Tabla 2.4.
Tabla 2.4 – El operador NOT.
| A | Resultado |
| 1 | 0 |
| 0 | 1 |
En el lenguaje C se puede llamar con el operador «!». El operador NOT actúa no solo sobre los bits contenidos en un tipo byte, sino también sobre números enteros y float. Este es un ejemplo de uso del operador:
! 0000 1010 =
---------
1111 0101
A continuación, puedes ver un código de ejemplo:
byte a = 1 * bit(0);
byte c = !a;
String str = String(c, BIN);
Serial.println("NOT");
Serial.println(str);
El último operador, más raro de encontrar, es ExOR (o exclusivo), que devuelve 1 solo si los dos operandos son distintos. Su comportamiento se muestra en la Tabla 2.5. El símbolo que se utiliza en el lenguaje C es «^».
Tabla 2.5 – El operador ExOR.
| A | B | Resultado |
| 1 | 1 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
Pulsuz fraqment bitdi.