26- 驴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 鈥嬧媝or 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 鈥嬧媠er谩n conocidos en todo el programa, y 鈥嬧媏n 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 鈥渃omunes鈥, 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