¿Cómo escribir un programa Arduino?


Epílogo de las lecciones básicas.

Ese es el final del curso básico de lecciones de programación de Arduino. Hemos estudiado los conceptos más básicos, recordado (o estudiado) parte del plan de estudios de informática de la escuela, hemos estudiado la mayor parte de la sintaxis y herramientas del lenguaje C ++ y, al parecer, todo el conjunto de funciones de Arduino que nos ofrece la plataforma. Hago hincapié en que estudiamos las funciones de C++ y Arduino, porque no hay un «lenguaje Arduino», este es un concepto falso. Arduino está programado en C o ensamblador, y la plataforma nos proporciona solo unas pocas docenas de funciones estándar para trabajar con un microcontrolador, es decir, funciones, no un lenguaje. Ahora que tenemos una hoja en blanco del cuaderno Arduino IDE y el deseo de crear y programar, ¡intentémoslo!


Estructura del programa Arduino.

Antes de pasar a los problemas reales, hay algunas cosas fundamentales de las que hablar. El microcontrolador, como comentamos al comienzo del viaje, es un dispositivo complejo que consta de un núcleo de proceso, una memoria de acceso permanente y aleatorio y varios dispositivos periféricos (temporizadores / contadores, ADC, etc.). Es el núcleo del microcontrolador el que se ocupa del procesamiento de nuestro código; emite comandos al resto del hardware, que luego puede funcionar de forma independiente. El kernel ejecuta varios comandos, impulsados ​​por el generador de reloj: la mayoría de las placas Arduino tienen un reloj de 16 MHz. Cada pulsación del generador de reloj obliga al núcleo computacional a ejecutar el siguiente comando, por lo que Arduino realiza 16 millones de operaciones por segundo.… Es mucho. Para la mayoría de las tareas, más que suficiente, lo principal es utilizar esta velocidad con prudencia.

¿Por qué estoy hablando de esto? El microcontrolador puede realizar solo una tarea a la vez, ya que solo tiene un núcleo de computación, por lo que no hay una «multitarea» real y no puede ser, pero debido a la alta velocidad de ejecución, el kernel puede realizar cambios de tarea rápidamente, y para una persona parecerá una multitarea, porque para nosotros “ un segundo ”, para un microcontrolador – ¡16 millones de acciones! Solo hay dos opciones para organizar su código:

  • El principal paradigma para trabajar con un microcontrolador es el llamado superciclo, es decir, el ciclo principal del programa, que se ejecuta de arriba a abajo (si miras el código) y comienza desde el principio, hasta el final. En el IDE de Arduino, nuestro super bucle es loop()… En el bucle principal, podemos interrogar sensores, controlar dispositivos externos, enviar datos a pantallas, realizar cálculos, etc., pero en cualquier caso, estas acciones se producirán una tras otra, de forma secuencial. Este es el mecanismo principal del paralelismo de tareas: de hecho, todas se ejecutan secuencialmente una tras otra, pero al mismo tiempo lo suficientemente rápido como para parecer «paralelas».
  • Además del bucle principal, tenemos interrupciones que nos permiten implementar algún tipo de «enhebrado» de tareas, especialmente en situaciones donde la velocidad es importante. La interrupción le permite detener la ejecución del bucle principal en cualquier lugar, distraerse con la ejecución de algún bloque de código, y luego de su finalización exitosa, regresar al bucle principal y continuar trabajando. Algunas tareas se pueden resolver solo con interrupciones, sin escribir una sola línea en un bucle loop()! Ya estudiamos las interrupciones de hardware que le permiten interrumpir el desarrollo del programas. Tales interrupciones son externas, es decir, son provocadas por factores externos (una persona presionó un botón, se activó un sensor, etc.). Además, el microcontrolador tiene interrupciones internas que son causadas por los periféricos del microcontrolador, ¡y puede haber más de una docena de estas interrupciones! Una de estas interrupciones es una interrupción del temporizador: según el período configurado, el programa interrumpirá y ejecutará el código especificado. Hablaremos de esto a continuación, y también hay una lección separada sobre cómo trabajar con interrupciones del temporizador en el curso avanzado. Este enfoque es bueno para las tareas que deben realizarse con frecuencia y con alta frecuencia; para todo lo demás, puede configurar un temporizador por cuenta de tiempo y trabajar con este tiempo.
  • Por defecto, el IDE de Arduino establece uno de los temporizadores (el cero) en la cuenta en tiempo real, gracias a lo cual tenemos funciones como millis () y micros (). Son estas funciones las que son una herramienta lista para usar para la gestión del tiempo de nuestro código y nos permiten crear trabajo en un horario. El punto más importante y crítico: las tareas no deben ralentizar la ejecución del programa durante un período más largo que el período de la tarea más corta, de lo contrario, todas las tareas se realizarán con el período más largo. Por eso es necesario abandonar retrasos y esperas: el retraso siempre se puede reemplazar comprobando el temporizador durante las próximas iteraciones del bucle, y lo mismo con esperar algo, por ejemplo, la respuesta de un sensor. Las tareas deben ser asincrónicas tanto como sea posible y no bloquear el código, desafortunadamente no todas las bibliotecas tienen funciones análogas sin bloqueo. Incluso un bloqueador nativo analogRead () se puede hacer sin bloqueo, pero Arduino decidió no complicar la vida a los principiantes.

«Multitarea» con millis ().

La mayoría de los ejemplos de diferentes módulos / sensores utilizan un retardo delay() como un programa de «frenado», por ejemplo, para enviar datos desde un sensor a un puerto serie. Son estos ejemplos los que estropean la percepción del principiante, y también comienza a utilizar delays. ¡Y los retrasos no te llevarán muy lejos!

Recordemos la construcción del temporizador en millis () de la lección sobre funciones de tiempo: tenemos una variable que almacena el tiempo del último «ajuste» del temporizador. Restamos este tiempo del tiempo actual, esta diferencia aumenta constantemente, y por condición podemos capturar el momento en que ha pasado el tiempo que necesitamos. Aprenderemos a deshacernos delay()! Comencemos simple: un parpadeo clásico:

void setup() {
  pinMode(13, OUTPUT);  // pin 13 salida
}
void loop() {
  digitalWrite(13, HIGH); // habilita
  delay(1000);            // espera
  digitalWrite(13, LOW);  // apagar
  delay(1000);            // espera
}

El programa se detiene completamente en el comando delay(), espera el tiempo especificado y luego continúa la ejecución. ¿Por qué es tan malo? (¿Sigues preguntando?) Durante esta parada, no podemos hacer nada en el ciclo loop(), por ejemplo, no podremos sondear el sensor 10 veces por segundo: el retraso no permitirá que el código avance. Puede usar interrupciones (por ejemplo, un temporizador), pero hablaremos de ellas en las lecciones avanzadas. Por ahora, eliminemos el retraso en el boceto más simple.

El primer paso es hacer la siguiente optimización: cortar el código a la mitad y deshacerse de un retraso usando una bandera:

boolean LEDflag = false;
void setup() {
  pinMode(13, OUTPUT);
}
void loop() {
  digitalWrite(13, LEDflag); // on/off
  LEDflag = !LEDflag; // bandera invertida
  delay(1000);        // espera
}

Movimiento complicado, ¡recuérdalo! Este algoritmo le permite alternar el estado de cada llamada. Ahora nuestro código todavía está atascado con un retraso de 1 segundo, eliminémoslo:

boolean LEDflag = false;
uint32_t myTimer; 
void setup() {
  pinMode(13, OUTPUT);
}
void loop() {
  if (millis() - myTimer >= 1000) {
    myTimer = millis(); // reiniciar el temporizador
    digitalWrite(13, LEDflag);// encendido apagado
    LEDflag = !LEDflag; // bandera invertida
  }
}

Qué está pasando aquí: el bucle loop() se ejecuta varios cientos de miles de veces por segundo, como debería, porque eliminamos el retraso. En cada una de nuestras iteraciones, comprobamos si es hora de cambiar el LED, si ha pasado un segundo. Con la ayuda de este diseño, se crea la multitarea deseada, que es suficiente para el 99% de todos los proyectos imaginables, ¡porque hay muchos de esos «temporizadores»!

boolean LEDflag = false;
// variables de tiempo
uint32_t myTimer, myTimer1, myTimer2;
uint32_t myTimer3;
void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
}
void loop() {
   // cada segundo
  if (millis() - myTimer >= 1000) {
    myTimer = millis(); // reiniciar el temporizador
    digitalWrite(13, LEDflag); /// encendido apagado
    LEDflag = !LEDflag; // bandera invertida
  }
  //  3 veces por segundo
  if (millis() - myTimer1 >= 333) {
    myTimer1 = millis(); // reinicia
    Serial.println("timer 1");
  }
  //cada 2 segundos
  if (millis() - myTimer2 >= 2000) {
    myTimer2 = millis(); // reinicia
    Serial.println("timer 2");
  }
  // cada 5 segundos
  if (millis() - myTimer3 >= 5000) {
    myTimer3 = millis(); // reinicia
    Serial.println("timer 3");
  }
}

Esto significa que 4 temporizadores con diferentes períodos de respuesta funcionan silenciosamente para nosotros, funcionan «en paralelo», lo que nos proporciona multitarea: podemos mostrar datos en la pantalla una vez por segundo, y al mismo tiempo sondear el sensor 10 veces por segundo promediar sus lecturas. ¡Un buen ejemplo para tu primer proyecto!

Asegúrese de volver a la lección sobre funciones de tiempo, ¡allí desarmamos varias construcciones del temporizador de tiempo de actividad!

Paralelismo con interrupciones de temporizador

Para tareas de tiempo crítico, puede utilizar la ejecución de interrupciones del temporizador. Qué tareas puede ser:

  • Indicación dinámica;
  • Generación de un protocolo de señal / comunicación específico;
  • Software PWM;
  • «Reloj» de motores paso a paso;
  • Cualquier otro ejemplo de ejecución después de un tiempo estrictamente especificado o simplemente ejecución periódica durante un período estricto (varios microsegundos). Dado que se trata de una interrupción, la tarea se procesará con prioridad sobre el resto del código en el super bucle.

Configurar el temporizador en la frecuencia y el modo de funcionamiento deseados es una tarea abrumadora para un principiante, aunque se puede resolver en 2-3 líneas de código, por lo que sugiero usar bibliotecas. Existen bibliotecas TimerOne y TimerTwo para configurar las interrupciones de temporizadores 1 y 2. 

Ahora veamos un ejemplo simple en el que los datos se enviarán al puerto «en paralelo» a un Blink en ejecución. El ejemplo está divorciado de la realidad, no puede hacer esto, pero es importante para comprender la esencia misma: el código en la interrupción se ejecutará en cualquier caso, no se preocupa por los retrasos y los bucles muertos en el código principal.

#include "TimerTwo.h"
void setup() {
  Serial.begin(9600);
  // Establecer el período del temporizador 333000 μs -> 0.333 s (3 veces por segundo)
  Timer2.setPeriod(300000);
  Timer2.enableISR();   // inicia la interrupción en el canal A del temporizador 2
  pinMode(13, OUTPUT);  // parpadeará
}
void loop() {
  // parpadea
  digitalWrite(13, 1);
  delay(1000);
  digitalWrite(13, 0);
  delay(1000);
}
// Interrumpir un temporizador 2
ISR(TIMER2_A) {
  Serial.println("isr!");
}

Las interrupciones del temporizador son una herramienta muy poderosa, pero no tenemos muchos temporizadores y solo deben usarse cuando realmente se necesitan. El 99% de las tareas se pueden resolver sin interrumpir el temporizador escribiendo el bucle principal óptimo y aplicando correctamente millis()

Cambiar de tarea

La herramienta más importante para organizar la lógica del programa es la llamada máquina de estados, un valor que tiene un conjunto predeterminado de estados. Suena complicado, pero en realidad estamos hablando del operador swith y una variable que se cambia mediante un botón o un temporizador. Por ejemplo:

if ( hace clic en el botón 1) mode++;
if ( hace clic en el botón 2) mode--;
switch (mode) {
  case 0:
    // tarea 0
    break;
  case 1:
    // tarea 1
    break;
  case 2:
    // tarea 2
    break;
  .........
}

La variable de mode debe ser signed (int o int8_t) para evitar un desbordamiento en la dirección opuesta al recibir un valor negativo.

Así, se organiza la selección y ejecución de las secciones de código seleccionadas. Alternar la variable mode también debe hacerse por una razón, como en el ejemplo anterior, aquí hay dos opciones:

  • Limitar el rango de la variable modo por código de tarea mínimo (generalmente 0) y máximo (número de tareas menos 1).
  • Cambiar de la última tarea a la primera y viceversa, es decir «Bucle hacia atrás» de cambio.

Hay varias formas de limitar el rango. Los métodos son absolutamente iguales en esencia, pero se pueden escribir de diferentes maneras:

// limitar el modo a 10
// Método 1 
mode++; 
if (mode > 10) mode = 10; 
// Método 2 
mode = min(mode++, 10); 
// Método 3 
if (++mode > 10) mode = 10;

Del mismo modo al disminuir:

// Método 1 
mode--; 
if (mode < 0) mode = 0; 
// Método 2 
mode = max(mode--, 0);
// Método 3 
if (--mode < 0) mode = 0;

El cambio del primero al último y viceversa se realiza de la misma manera:

// cambiar de modo en el rango 0-10 (11 modos)
//  sobrepasar valores extremos
// MÉTODO 1
// incrementar
mode++;
if (mode > 10) mode = 0;
//disminuir
mode--;
if (mode < 0) mode = 10;
// MÉTODO 2
// incrementar
if (++mode > 10) mode = 0;
// нdisminuir
if (--mode < 0) mode = 10;

Banderas

Las variables booleanas, o banderas, son una herramienta muy importante para organizar la lógica de un programa. La bandera global puede almacenar el «estado» de los componentes del programa, y ​​serán conocidos en todo el programa, y ​​en todo el programa se pueden cambiar. Un ejemplo un poco exagerado:

boolean flag = false;
void loop() {
  // si hubo un clic en el botón, levante la bandera
  if (buttonClick()) flag = true;
  if (flag) {
    // algún código
  }
}

El estado de la bandera global se puede leer en cualquier otra función y lugar del programa, lo que simplifica enormemente el código y elimina las llamadas innecesarias.

Usando una bandera, puede organizar una sola ejecución de un bloque de código para algún evento:

boolean flag = false;
void loop() {
  // si hubo un clic en el botón, levante la bandera
  if (buttonClick()) flag = true;
  if (flag) {
    flag = false;
    // se ejecutará una vez
  }
}

La bandera también se puede invertir, lo que le permite generar una secuencia 10101010 para cambiar algunos estados:

boolean flag = false;
void loop() {
  // Supongamos que el período del temporizador cumple la condición
  if (timerElapsed()) {
    flag = !flag; //  bandera invertida
        // por ejemplo, necesitas pasar dos valores a la función,
    	// alternándolos en un temporizador
    setSomeValue(flag ? 10 : 200);
  }
}

Las banderas son una herramienta muy poderosa, ¡no te olvides de ellas!

Deshacerse de ciclos y retrasos

Hablamos anteriormente sobre cómo hacer parpadear un LED sin demora. ¿Cómo deshacerse del ciclo? Es muy simple: el bucle se reemplaza con un contador y una condición. Digamos que tenemos un bucle for que genera el valor del contador:

for (int i = 0; i < 10; i++) {
  Serial.println(i);
}

Para deshacernos del bucle, necesitamos crear nuestra propia variable de contador, poner todo en otro bucle (por ejemplo, en loop() ) e independientemente aumentar la variable y verificar la condición:

int counter = 0;
void loop() {
  Serial.println(counter);
  counter++;
  if (counter >= 10) counter = 0;
}

Y eso es todo.

Pero, ¿y si hubiera un retraso en el bucle? Aquí hay un ejemplo:

for (int i = 0; i < 30; i++) {
  // ejemplo, enciende el i-ésimo LED
  delay(100);
}

Es necesario deshacerse del ciclo como de delay(). Introduzcamos un temporizador en millis (), y trabajaremos en ello:

int counter = 0;      // reemplazar i
uint32_t timer = 0;   //  variable de temporizador
#define T_PERIOD 100  //período de cambio
void loop() {
  
  if (millis() - timer >= T_PERIOD) { //temporizador en millis ()  
    timer = millis(); // Reiniciar
    // acción con contador - nuestro i-ésimo LED por ejemplo
    counter++;  // incrementa contador
    if (counter > 30) counter = 0;  //  repite el cambio 
  }
  
}

¡Eso es todo! En lugar de una variable de bucle » i «ahora tenemos nuestro propio contador global counter que va de 0 a 30 (en este ejemplo) con un período de 100 ms.


¿Cómo combino varios bocetos?

Para combinar varios proyectos en uno, debe abordar todos los posibles conflictos:

  • ¿Los proyectos se basan en el mismo tablero / plataforma?
    • ¡Sí, es bueno!
    • No, debe asegurarse de que la placa «común» pueda funcionar con las piezas de hardware que se encuentran en los proyectos combinados, y que también tenga los periféricos necesarios.
  • ¿Hay algún hardware en los proyectos conectado a interfaces de comunicación?
    • No, ¡genial!
    • Sí, I2C: todas las piezas de hardware están conectadas al I2C de la placa común. ¡Asegúrese de que las direcciones del dispositivo no coincidan (esto ocurre muy raramente)!
    • Sí, SPI: el bus SPI tiene todos los pines “comunes”, excepto CS (Selección de chip), este pin puede ser cualquiera digital. Puedes leer más detalles aquí.
    • Sí, la UART es un problema, solo se puede conectar un dispositivo a la UART. Puede colgar una pieza de hardware en el hardware UART y la segunda en SoftwareSerial. O molestarse con multiplexores.
  • ¿Se utilizan pines en ambos proyectos?
    • No, ¡genial!
    • Sí, hay que averiguar qué función realiza el pin en cada uno de los proyectos y elegir un reemplazo, tanto en el hardware como en el programa:
      • Si se trata de una entrada-salida digital normal, puede sustituirla por cualquier otra
      • Si se trata de una medición de señal analógica, sustitúyala por otro pin analógico
      • Si se trata de una generación PWM, conéctese en consecuencia a otro pin PWM y corrija el programa
      • Si se trata de una interrupción, tenga cuidado
  • ¿Se utilizan los mismos bloques periféricos de microcontroladores? Para hacer esto, necesita estudiar las piezas y módulos externos y sus bibliotecas:
    • No, ¡EXCELENTE!
    • Sí, la situación requiere una buena experiencia con Arduino …
    • Se utiliza el mismo temporizador: no puede utilizar simultáneamente PWM en las patas del primer temporizador y controlar los servoaccionamientos mediante la biblioteca Servo.h
    • Generación de sonido usando tono (): no puede usar PWM en las patas del segundo temporizador
    • Se utilizan interrupciones del temporizador y generación de PWM en el temporizador correspondiente, una situación difícil
    • Etc., puede haber infinitas situaciones …

Puede realizar todas las ediciones en los diagramas y programas de los proyectos combinados para que no entren en conflicto. A continuación, comenzamos a ensamblar el programa general :

  • Conectamos todas las bibliotecas . Algunas bibliotecas pueden entrar en conflicto, como Servo y Timer1, como se discutió anteriormente.
  • Comparamos los nombres de las variables globales y las definiciones en los programas combinados: no deben ser iguales. Cambiamos las coincidencias reemplazando por código ( Editar / Buscar ) con otros. A continuación, copie y pegue todas las variables globales y las definiciones en un programa común
  • Fusionando el contenido del bloque setup()
  • Copie y pegue todas las funciones «personalizadas» en el programa general
  • Sólo tenemos loop() y esta es la tarea mas difícil

Solíamos tener dos (o más) proyectos de trabajo por separado. Ahora nuestra tarea como programador es pensar y programar el trabajo de estos varios proyectos en uno, y aquí hay una infinidad de situaciones:

  • El código principal (que está en loop ()) de diferentes proyectos debe ejecutarse a su vez en un temporizador
  • Un conjunto de acciones de diferentes proyectos debe cambiarse con un botón o de alguna manera
  • Un sensor de otro proyecto se agrega a un proyecto: los datos deben procesarse y su movimiento adicional programado (visualización, envío, etc.)
  • Todos los «proyectos» deben funcionar simultáneamente en un Arduino
  • Etc.

En la mayoría de los casos, no se puede simplemente tomar y combinar el contenido de loop() de diferentes programas, espero que todos entiendan esto. Incluso una luz intermitente y un zumbador no se pueden combinar de esta manera si el código se escribió originalmente con retrasos o bucles cerrados.


Deja un comentario