Capítulo 4. Entradas, salidas y temporizadores

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Las entradas, salidas y temporizadores forman la base de casi todo lo que hacen los sistemas embebidos. Incluso los métodos de comunicación para hablar con otros componentes están formados por ellos (ver controladores bit-bang en el Capítulo 7). En este capítulo, voy a recorrer un ejemplo de desarrollo de producto. Los objetivos cambiarán constantemente para que se parezca a la vida real. Eso también nos permitirá explorar cómo pasar de un sistema sencillo de una luz parpadeante a botones de desbobinado y LEDs de atenuación. Por el camino, veremos mucho sobre temporizadores y cómo hacen mucho más que medir el tiempo.

Sin embargo, antes de empezar con la dominación del mundo, err, quiero decir, con el desarrollo de productos, necesitas saber un poco sobre los registros, la interfaz de tu software con el procesador.

Manejo de registros

Para hacer cualquier cosa con una línea de E/S, tenemos que hablar con el registro apropiado. Como se describe en "Tu procesador es un lenguaje", puedes pensar en los registros como una API para el hardware. Descritos en el manual de usuario del chip, los registros vienen en todos los sabores para configurar el procesador y controlar los periféricos. Están mapeados en memoria, por lo que puedes escribir en una dirección específica para modificar un registro concreto. A menudo, cada bit del registro significa algo específico, lo que significa que tenemos que empezar a utilizar números binarios para ver los valores de bits concretos.

Matemáticas binarias y hexadecimales

Familiarizarte con las matemáticas básicas binarias y hexadecimales (hex) hará que tu carrera en sistemas embebidos sea mucho más agradable. Desplazar bits individuales es estupendo cuando sólo necesitas modificar uno o dos lugares. Pero si necesitas modificar toda la variable, el hexadecimal es muy útil porque cada dígito en hexadecimal corresponde a un nibble (cuatro bits) en binario (ver Tabla 4-1). (Sí, por supuesto que un nibble es la mitad de un byte. A los incrustados les gustan los juegos de palabras).

Tabla 4-1. Matemáticas binarias y hexadecimales
Binario Hex Decimal Recuerda este número
0000 0 0 Esta es fácil.
0001 1 1 Esto es (1 << 0).
0010 2 2 Esto es (1 << 1). Desplazar es lo mismo que multiplicar por 2desplazarValor.
0011 3 3 Observa que en binario es la suma de uno y dos.
0100 4 4 (1 << 2) es un 1 desplazado dos ceros a la izquierda.
0101 5 5 Es un número interesante porque todos los demás bits están activados.
0110 6 6 ¿Ves cómo parece que podrías desplazar los tres hacia la izquierda en uno? Esto podría reunirse como ((1 << 2)|(1 << 1)), o ((1 << 2) + (1 << 1)), o, lo que es más común, (3 << 1).
0111 7 7 Fíjate en el patrón de los bits binarios. Son muy repetitivos. Aprende el patrón y podrás generar esta tabla si lo necesitas.
1000 8 8 (1 << 3). ¿Ves cómo se relacionan el desplazamiento y el número de ceros? Si no, mira la representación binaria de 2 y 4.
1001 9 9 Vamos a ir más allá de los números decimales normales. Como hay más dígitos en hexadecimal, tomaremos prestados algunos del alfabeto. Mientras tanto, 9 es sólo 8 + 1.
1010 A 10 Este es otro número especial con todos los demás bits activados.
1011 B 11 ¿Ves cómo el último bit va y viene de 0 a 1? Significa par e impar.
1100 C 12 ¿Notas cómo C es sólo 8 y 4 combinados en binario? Así que, por supuesto, es igual a 12.
1101 D 13 El segundo bit de la derecha va y viene de 0 a 1 a la mitad de velocidad que el primero: 0, luego 0, luego 1, luego 1, luego repite.
1110 E 14 El tercer bit también va y viene, pero a la mitad de velocidad que el segundo bit.
1111 F 15 Todos los bits están activados. Es importante recordar esto.

Ten en cuenta que con cuatro bits (un dígito hexadecimal) puedes representar 16 números, pero no puedes representar el número 16. Muchas cosas en los sistemas embebidos están basadas en ceros, incluidas las direcciones, por lo que se asignan bien a números binarios o hexadecimales.

Un byte son dos nibbles, el izquierdo desplazado hacia arriba (izquierda) cuatro espacios respecto al otro. Así que 0x80 es (0x8 << 4).

Una palabra de 16 bits está formada por dos bytes. Podemos obtenerla desplazando el byte más significativo 8 bits hacia arriba, y luego sumándolo al byte inferior:

0x1234 = (0x12 << 8) + (0x34)

Una palabra de 32 bits tiene 8 caracteres en hexadecimal, pero 10 caracteres en decimal. Aunque esta representación densa puede hacer útiles las impresiones de depuración, la verdadera razón por la que utilizamos el hexadecimal es para facilitar el binario.

Consejo

Como la memoria se ve generalmente en hexadecimal, algunos valores se utilizan para identificar anomalías en la memoria. En particular, espera ver (y utilizar) 0xDEADBEEF o 0xDEADC0DE como indicadores (son mucho más fáciles de recordar en hexadecimal que en decimal). Otros dos bytes importantes son 0xAA y 0x55. Como los bits de estos números se alternan, son fáciles de ver en un osciloscopio y buenos para hacer pruebas cuando quieras ver muchos cambios en tus valores.

Operaciones bit a bit

Cuando trabajes con registros, en tendrás que pensar en las cosas a nivel de bits. A menudo, activarás y desactivarás bits específicos. Si nunca has utilizado operaciones a nivel de bit, ahora es el momento de aprender. Mientras que el operador lógico && suele significar Y, ! significa NO, y || significa O, éstos se basan en la idea de que todo lo que es cero es falso, y todo lo que es distinto de cero es verdadero.

Las operaciones bit a bit funcionan bit a bit (ver Tabla 4-2). En bit a bit AND, si las dos entradas tienen un bit activado, la salida también lo tendrá. Los demás bits de la salida serán cero. Para OR, si cualquiera de las dos entradas tiene un bit activado, la salida también lo tendrá.

Tabla 4-2. Operaciones bit a bit
Operación bit a bit Significado Sintaxis Ejemplos
Y Si las dos entradas tienen un bit activado, la salida también lo tendrá. & 0x01 & 0x02 = 0x00
0x01 & 0x03 = 0x01
0xF0 & 0xAA = 0xA0
O Si alguna de las dos entradas tiene un bit activado, la salida también será . | 0x01 | 0x02 = 0x03
0x01 | 0x03 = 0x03
0xFF | 0x00 = 0xFF
XOR Si sólo una de las dos entradas tiene un bit activado, la salida también lo tendrá. ^ 0x01 ^ 0x02 = 0x03
0x01 ^ 0x03 = 0x02
0xAA ^ 0xF5 = 0x5F
NO Cada bit se configura en su opuesto. ~ ~0x01 = 0xFE
~0x00 = 0xFF
~0x55 = 0xAA

Probar, Fijar, Borrar y Alternar

Las operaciones hexadecimales y bit a bit son sólo un paso previo para poder interactuar con los registros. Si quieres saber si un bit está activado, tienes que hacer un AND bit a bit con el registro:

test = register & bit;
test = register & (1 << 3); // check 3rd bit in the register
test = register & 0x08;     // same, different syntax

Ten en cuenta que la variable test será igual a cero (el bit no está activado en el registro) o 0x08 (el bit está activado). No es verdadero ni falso, aunque puedes utilizarla en condicionales normales, ya que sólo comprueban si es cero o distinto de cero.

Si quieres que active un bit en un registro, ORéalo con el registro:

register = register | bit;
register = register | (1 << 3); // turn on the 3rd bit in the register
register |= 0x08;               // same, different syntax

Borrar un bit es un poco más confuso porque quieres dejar los demás bits del registro sin cambios. La estrategia típica es invertir el bit que queremos borrar utilizando NOT (~) a nivel de bit. A continuación, une el valor invertido con el registro. De esta forma, los demás bits permanecen inalterados:

register = register & ~bit;
register = register & ~(1 << 3); // turn off the 3rd bit in the register
register &= ~0x08;               // same, different syntax

Si quisieras conmutar un bit, podrías comprobar su valor y luego activarlo o desactivarlo según fuera necesario. Por ejemplo

test = register & bit;
if (test) {                      // bit is set, need to clear it
    register = register & ~bit;
} else {                         // bit is not set, need to set it
    register = register | bit;
}

Sin embargo, hay otra forma de conmutar un bit utilizando la operación XOR:

register = register ^ bit;
register = register ^ (1 << 3);

Si el registro tiene activado el bit 0x08, el XOR verá dos unos y emitirá un cero para ese bit. Si el registro no tiene el bit activado, el XOR verá un uno y un cero, emitiendo un uno para ese bit. Los demás bits del registro no cambiarán porque sólo está activado el único bit en la segunda entrada, y los demás son cero.

Suficiente repaso. Necesitarás conocer estas operaciones para utilizar los registros. Si esto no es algo con lo que te sientas cómodo, hay recursos al final del capítulo para que aprendas más o practiques.

Conmutar una salida

El departamento de marketing ha acudido a ti con una idea para un producto. Cuando ves a través del humo y los espejos, te das cuenta de que lo único que necesitan es que parpadee una luz.

La mayoría de los procesadores tienen clavijas cuyos estados digitales pueden ser leídos (entrada) o configurados (salida) por el software. Reciben el nombre de clavijas de E/S, E/S de propósito general (GPIO), E/S digital (DIO) y, ocasionalmente, E/S general (GIO). El caso de uso básico suele ser sencillo, al menos cuando se conecta un LED:

  1. Inicializa el pin para que sea una salida (como pin de E/S, podría ser de entrada o de salida).

  2. Pon el pin alto cuando quieras que el LED se encienda. Pon la patilla baja cuando quieras que el LED se apague. (Aunque el LED también puede conectarse para que esté encendido cuando la patilla esté baja, este ejemplo evitará la lógica invertida).

A lo largo de este capítulo, te daré ejemplos de tres manuales de usuario diferentes para que te hagas una idea de lo que puedes esperar de la documentación de tu procesador. El manual del microcontrolador ATtiny AVR de Microchip describe un microcontrolador de 8 bits con multitud de periféricos. La guía del usuario del MSP430x2xx de TI describe un procesador RISC de 16 bits diseñado para consumir muy poco. El manual de referencia STM32F103xx de STMicroelectronics describe un microcontrolador Arm Cortex de 32 bits. No necesitarás estos documentos para seguir adelante, pero pensé que te gustaría conocer los procesadores en los que se basan los ejemplos.

Configurar la clavija para que sea una salida

Volviendo a la petición de marketing de conmutar un LED, la mayoría de los pines de E/S pueden ser entradas o salidas. El primer registro que tendrás que configurar controlará la dirección de la patilla para que sea una salida. En primer lugar, determina qué patilla vas a modificar. Para modificar la patilla, necesitarás saber el nombre de la patilla (por ejemplo, "patilla de E/S 2", no su número en el procesador (por ejemplo, patilla 12). Los nombres suelen estar dentro del procesador en los esquemas, mientras que el número de pin está en el exterior de la caja (como se muestra en la Figura 4-2).

Figura 4-2. Esquema de un procesador con un LED conectado

Las patillas pueden tener varios números en su nombre, indicando un puerto (o banco) y una patilla en ese puerto. (Los puertos también pueden ser letras en lugar de números.) En la figura, el LED está conectado al pin 10 del procesador, que se llama "SCLK/IO1_2". Esta patilla se comparte entre el puerto SPI (recuerda que se trata de un método de comunicación, del que hablaremos en el Capítulo 7) y el subsistema de E/S (IO1_2). El manual de usuario te indicará si el pin es un pin de E/S por defecto o un pin SPI (y cómo cambiar entre ambos). Puede ser necesario otro registro para indicar la finalidad de la patilla. La mayoría de los vendedores son buenos a la hora de indicar la configuración del pin, pero si se comparte entre periféricos, puede que tengas que buscar en la sección de periféricos para desactivar la funcionalidad no deseada. En nuestro ejemplo, diremos que el pin es una E/S por defecto.

En el subsistema de E/S, es el segundo pin (2) del primer banco (1). Tendremos que recordarlo y asegurarnos de que el pin se utiliza como pin de E/S y no como pin SPI. En el manual de usuario de tu procesador encontrarás más información. Busca una sección con un nombre como "Configuración de E/S", "Introducción a las E/S digitales" o "Puertos de E/S". Si tienes problemas para encontrar el nombre, busca la palabra "dirección", que suele utilizarse para describir si quieres que la patilla sea una entrada o una salida.

Una vez que encuentres el registro en el manual, podrás determinar si necesitas activar o desactivar un bit en el registro de dirección. En la mayoría de los casos, necesitas activar el bit para que la patilla sea una salida. Podrías determinar la dirección y codificar el resultado:

*((int*)0x0070C1) |= (1 << 2);

Sin embargo, por favor, no lo hagas.

El proveedor del procesador o compilador casi siempre proporcionará un archivo de cabecera o una capa de abstracción de hardware(HAL, que incluye un archivo de cabecera) que oculta el mapa de memoria del chip para que puedas tratar los registros como variables globales. Si el proveedor no te ha proporcionado un archivo de cabecera, crea uno tú mismo, para que tu código se parezca más a una de estas líneas:

Procesador STM32F103
GPIOA->CRL |= 1 << 2;  // set IOA_2 to be an output
Procesador MSP430
P1DIR |= BIT2;         // set IO1_2 to be an output
Procesador ATtiny
DDRB |= 0x4;           // set IOB_2 to be an output

Ten en cuenta que los nombres de los registros son diferentes para cada procesador, pero el efecto del código en cada línea es el mismo. Cada procesador tiene diferentes opciones para fijar el segundo bit del byte (o palabra).

En cada uno de estos ejemplos, el código lee el valor actual del registro, lo modifica y, a continuación, vuelve a escribir el resultado en el registro. Este ciclo de lectura-modificación-escritura tiene que producirse en trozos atómicos, lo que significa que tienen que ejecutarse sin interrupciones ni ningún otro procesamiento entre los pasos. Si lees el valor, lo modificas y luego haces otras cosas antes de escribir el registro, corres el riesgo de que el registro haya cambiado y el valor que estás escribiendo esté desfasado. La modificación del registro cambiará el bit previsto, pero también podría tener consecuencias no deseadas.

Encender el LED

El siguiente paso es encender el LED. De nuevo, tendremos que encontrar el registro adecuado en el manual de usuario:

Procesador STM32F103
GPIOA->ODR |= (1 << 2);    // IOA_2 high
Procesador MSP430
P1OUT |= BIT2;             // IO1_2 high
Procesador ATtiny
PORTB |= 0x4;              // IOB_2 high

El archivo de cabecera de la capa de abstracción de hardware GPIO proporcionado por el proveedor del procesador muestra cómo las direcciones en bruto quedan enmascaradas por algunas sutilezas de programación. En el STM32F103x6, se accede a los registros de E/S en una dirección a través de una estructura (he reorganizado y simplificado el archivo):

typedef struct
{
  __IO uint32_t CRL;  // Port configuration (low)
  __IO uint32_t CRH;  // Port configuration (high)
  __IO uint32_t IDR;  // Input data register
  __IO uint32_t ODR;  // Output data register
  __IO uint32_t BSRR; // Bit set/reset register
  __IO uint32_t BRR;  // Bit reset register
  __IO uint32_t LCKR; // Port configuration lock
} GPIO_TypeDef;

#define PERIPH_BASE       0x40000000UL
#define APB2PERIPH_BASE   (PERIPH_BASE + 0x00010000UL)
#define GPIOA_BASE        (APB2PERIPH_BASE + 0x00000800UL)
#define GPIOA             ((GPIO_TypeDef *)GPIOA_BASE)

El archivo de cabecera describe muchos registros que no hemos visto. También están en la sección del manual del usuario, con muchas más explicaciones. Prefiero acceder a los registros a través de la estructura porque agrupa las funciones relacionadas y, a menudo, te permite trabajar con los puertos indistintamente.

Una vez que tengamos el LED encendido, tendremos que volver a apagarlo. Sólo tienes que borrar esos mismos bits, como se muestra en "Probar, configurar, borrar y alternar":

Procesador STM32F103
GPIOA->ODR &= ~(1 << 2);       // IO1_2 low
Procesador MSP430
P1OUT &= ~(BIT2);              // IO1_2 low
Procesador ATtiny
PORTB &= ~0x4;                 // IOB_2 low

Parpadeo del LED

Para terminar nuestro programa, sólo tenemos que unirlo todo. El pseudocódigo para ello es el siguiente:

main:
  initialize the direction of the I/O pin to be an output
loop:
  set the LED on
  do nothing for some period of time
  set the LED off
  do nothing for the same period of time
  repeat loop

Una vez que lo hayas programado para tu procesador, debes compilarlo, cargarlo y probarlo. Quizá quieras ajustar el bucle de retardo para que el LED se vea más o menos bien. Probablemente necesitarás varias decenas de miles de ciclos de procesador, o el LED parpadeará más rápido de lo que puedas percibirlo.

Solución de problemas

Si tiene configurado un sistema de depuración como JTAG, averiguar por qué no se enciende tu LED probablemente sea sencillo. De lo contrario, puede que tengas que utilizar el proceso de eliminación.

En primer lugar, comprueba dos veces tus cálculos. Aunque te sientas completamente cómodo con el hexadecimal y el desplazamiento de bits, siempre es posible que se produzca una errata. En mi experiencia, las erratas son los errores más difíciles de detectar, a menudo más que las corrupciones de memoria. Comprueba que estás utilizando el pin correcto en el esquema. Y asegúrate de que hay corriente en la placa. (Puede que este consejo te parezca gracioso, pero te sorprendería la frecuencia con que esto influye).

A continuación, comprueba si un pin está compartido entre distintos periféricos. Aunque dijimos que la patilla era una E/S por defecto, si tienes problemas, comprueba si hay una función alternativa que pueda establecerse en el registro de configuración.

Mientras tengas el manual abierto, comprueba que el pin está configurado correctamente. Si el LED no responde, tendrás que leer el capítulo de periféricos del manual de usuario. Los procesadores son diferentes, así que comprueba que el pin no necesita una configuración adicional (por ejemplo, una salida de control de potencia) o tiene una función activada por defecto (no conviertas el GPIO en una interrupción por accidente).

La mayoría de los procesadores tienen E/S por defecto porque es la forma más sencilla de que sus usuarios (¡nosotros!) verifiquen que el procesador está conectado correctamente. Sin embargo, sobre todo con los procesadores de bajo consumo, querrás mantener desactivados todos los subsistemas que no utilices para evitar el consumo de energía. (Y otros procesadores de propósito especial pueden tener otra funcionalidad por defecto.) El manual del usuario te dirá más sobre la configuración por defecto y cómo cambiarla. A menudo hay otros registros que necesitan que se active (o desactive) un bit para que una patilla actúe como E/S.

A veces hay que configurar los relojes para que el subsistema de E/S funcione correctamente (o para que una función de retardo vaya lo suficientemente lenta como para que puedas ver la alternancia). Si sigues teniendo problemas, busca los ejemplos del proveedor e identifica las diferencias.

A continuación, asegúrate de que el sistema está ejecutando tu código. ¿Tienes otra forma de verificar que el código que se está ejecutando es el código que has compilado? Si tienes un puerto serie de depuración, prueba a incrementar la revisión para verificar que el código se está cargando. Si eso no funciona, asegúrate de que tu sistema de compilación está utilizando el procesador correcto para el objetivo.

Haz que el código sea lo más sencillo posible para tener la certeza de que el procesador está ejecutando la función que maneja los LEDs. Elimina cualquier inicialización no crítica de los periféricos por si el sistema se está retrasando a la espera de un dispositivo externo inexistente. Desactiva las interrupciones y aserciones, y asegúrate de que el perro guardián está desactivado (ver "Perro guardián").

En muchos microcontroladores, las patillas pueden absorber más corriente de la que pueden suministrar. Por lo tanto, no es raro que la patilla esté conectada al cátodo en lugar de al ánodo del LED. En estos casos, enciendes un LED escribiendo un cero en lugar de un uno. Esto se llama lógica invertida. Comprueba tu esquema para ver si éste es el caso.

Si la salida sigue sin funcionar, considera si hay algún problema relacionado con el hardware. En primer lugar, vuelve a comprobar que el LED está conectado al pin del chip que crees que está. Después, si es posible, ejecuta el software en varias placas para descartar un defecto relacionado con el montaje. Incluso en el hardware, puede ser algo sencillo (instalar los LED al revés es bastante fácil). Puede ser un problema de diseño, como que la patilla del procesador no pueda suministrar suficiente corriente para accionar el LED; la hoja de datos (o el manual del usuario) podría indicártelo. Puede haber un problema en la placa, como un componente o una conexión rotos. Con las patillas de alta densidad de la mayoría de los procesadores, es muy fácil cortocircuitar unas patillas con otras. Pide ayuda o saca tu multímetro (u osciloscopio).

Separar el hardware de la acción

A Marketing le gustó tu primer prototipo, aunque quizá haya que retocarlo un poco más adelante. El sistema pasó de una placa prototipo a una PCB. Durante este proceso, de algún modo cambió el número de pin (a IO1_3). Ambos sistemas tienen que poder funcionar.

Es trivialmente sencillo arreglar el código de este proyecto, pero para un sistema más grande, los pines pueden estar revueltos para dar paso a una nueva función. Veamos cómo simplificar las modificaciones.

Archivo de cabecera específico de la placa

Utilizar un archivo de cabecera específico de la placa te permite evitar la codificación rígida del pin. Si tienes un archivo de cabecera, sólo tienes que cambiar un valor allí en lugar de recorrer tu código para cambiarlo en todos los lugares a los que se hace referencia. El archivo de cabecera podría tener este aspecto:

#define LED_SET_DIRECTION  (P1DIR)
#define LED_REGISTER       (P1OUT)
#define LED_BIT            (1 << 3)

Las líneas de código para configurar y hacer parpadear el LED pueden ser independientes del procesador:

LED_SET_DIRECTION |= LED_BIT; // set the I/O to be output
LED_REGISTER |= LED_BIT;      // turn the LED on
LED_REGISTER &= ~LED_BIT;     // turn the LED off

Eso podría resultar un poco engorroso si tienes muchas líneas de E/S o necesitas los otros registros. Estaría bien poder dar sólo el puerto (1) y la posición en el puerto (3) y dejar que el código lo resuelva. El código podría ser más complejo, pero probablemente ahorraría tiempo (y errores). Para ello, el archivo de cabecera tendría este aspecto:

// ioMapping_v2.h
#define LED_PORT 1
#define LED_PIN 3

Si queremos recompilar para utilizar distintas compilaciones para distintas placas, podemos utilizar tres archivos de cabecera. El primero es la antigua asignación de patillas de la placa(ioMapping_v1.h). A continuación, crearemos uno para la nueva asignación de patillas(ioMapping_v2.h). Podríamos incluir el que necesitamos en nuestro archivo .c principal, pero eso desvirtúa el objetivo de modificar menos ese código. Si hacemos que el archivo principal incluya siempre un ioMapping .h genérico, podemos cambiar las versiones en el archivo principal incluyendo el archivo de cabecera correcto:

// ioMapping.h
#if COMPILING_FOR_V1
#include "ioMapping_v1.h"
#elif COMPILING_FOR_V2
#include "ioMapping_v2.h"
#else
#error "No I/O map selected for the board. What is your target?"
#endif /* COMPILING_FOR_*/

Utilizar un archivo de cabecera específico de la placa refuerza tu proceso de desarrollo frente a futuros cambios de hardware. Al separar la información específica de la placa de la funcionalidad del sistema, estás creando una base de código más flexible y con menos vínculos.

Consejo

Mantener el mapa de E/S en Excel es una forma bastante común de asegurarse de que los ingenieros de hardware y software están de acuerdo en las definiciones de las patillas. Con un poco de guión creativo, puedes generar tu mapa de E/S específico de la versión a partir de un archivo CSV para asegurarte de que los identificadores de las patillas coinciden con los del esquema.

Código de gestión de E/S

En lugar de escribir directamente en los registros del código, tendremos que manejar los múltiples puertos de forma genérica. Hasta ahora tenemos que inicializar el pin para que sea una salida, poner el pin alto para que el LED esté encendido, y poner el pin bajo para que el LED esté apagado. Curiosamente, tenemos un gran número de opciones para montar esto, incluso para una interfaz tan sencilla.

En la implementación, la función de inicialización configura el pin para que sea una salida (y lo configura para que sea un pin de E/S en lugar de un periférico, si es necesario). Con varias patillas, podrías inclinarte por agrupar toda la inicialización, pero eso rompería la modularidad de los sistemas.

Aunque el código ocupe un poco más de espacio, es mejor hacer que cada subsistema inicialice las E/S que necesita. Así, si eliminas o reutilizas un módulo, tendrás todo lo que necesitas en una sola zona. Sin embargo, hemos visto una situación en la que no debes separar las interfaces en subsistemas: el archivo de cabecera de mapeo de E/S, donde se reúnen todos los pines para que la interfaz con el hardware sea más fácil de comunicar.

Siguiendo con la interfaz del subsistema de E/S, el establecimiento de un pin alto y bajo podría hacerse con una sola función: IOWrite(port, pin, high/low). Otra posibilidad es dividirla en dos funciones: IOSet(port, pin) y IOClear(port, pin). Ambos métodos funcionan. Imagina cómo será nuestra función principal en ambos casos.

El objetivo es hacer que el LED conmute. Si utilizamos IOWrite, podemos tener una variable que conmute entre alto y bajo. En el caso de IOSet y IOClear, tendríamos que guardar esa variable y comprobarla en el bucle principal para determinar a qué función llamar. Como alternativa, podríamos ocultar IOSet y IOClear dentro de otra función llamada IOToggle. No tenemos ninguna restricción particular con nuestro hardware, así que no necesitamos considerar la optimización del código en este ejemplo. Sin embargo, en aras de la educación, considera las opciones que nos estamos dando con estas posibles interfaces.

La opción IOWrite lo hace todo en una sola función, por lo que ocupa menos espacio de código. Sin embargo, tiene más parámetros, por lo que ocupa más espacio en la pila, que sale de la RAM. Además, tiene que mantener alrededor una variable de estado (también RAM).

Con la opción IOSet/IOClear/IOToggle, hay más funciones (más espacio de código), pero menos parámetros y posiblemente ninguna variable necesaria (menos RAM). Ten en cuenta que la función alternar no es más cara en términos de ciclos de procesador que las funciones establecer y borrar.

Este tipo de evaluación requiere que pienses en la interfaz a lo largo de otra dimensión. En el Capítulo 11 se explicará con más detalle cómo optimizar cada área. Durante la fase de creación del prototipo, es demasiado pronto para optimizar el código, pero nunca es demasiado pronto para considerar cómo se puede diseñar el código para permitir su optimización más adelante.

Lazo principal

Las modificaciones de las secciones anteriores colocan el código de gestión de E/S en su propio módulo, aunque los fundamentos del bucle principal no cambian. La implementación podría tener el siguiente aspecto:

void main(void){
  IOSetDir(LED_PORT, LED_PIN, OUTPUT);
  while (1) {    // spin forever
    IOToggle(LED_PORT, LED_PIN);
    DelayMs(DELAY_TIME);
  }
}

La función principal ya no depende directamente del procesador. Con este nivel de desacoplamiento, es más probable que el código se reutilice en otros proyectos. En (a) de la Figura 4-3, se muestra la versión original de la arquitectura de software, cuya única dependencia es la HAL del procesador. La del medio, etiquetada (b), es nuestra versión actual. Es más complicada, pero la separación de intereses es más evidente. Observa que los archivos de cabecera están apartados a un lado para mostrar que alimentan las dependencias.

Nuestra próxima reorganización creará una arquitectura aún más flexible y reutilizable, ilustrada por (c).

Figura 4-3. Comparación de diferentes arquitecturas en orden de abstracción creciente

Patrón de fachada

Como puedes imaginar , nuestra interfaz de E/S es va a hacerse más compleja a medida que se amplíen las funciones del producto. (Actualmente sólo tenemos un pin de salida, así que no puede simplificarse más.) A largo plazo, queremos ocultar los detalles de cada subsistema. Existe un patrón de diseño de software estándar llamado fachada que proporciona una interfaz simplificada a un fragmento de código. El objetivo del patrón de fachada es facilitar el uso de una biblioteca de software. Siguiendo la metáfora que he estado utilizando en este libro, de que interactuar con el procesador es similar a trabajar con una biblioteca de software, tiene sentido utilizar el patrón de fachada para ocultar algunos detalles del procesador y del hardware.

En "Diseñar para el cambio", vimos el patrón adaptador, que es una versión más general del patrón fachada. Mientras que el patrón adaptador actuaba como una traducción entre dos capas, la fachada lo hace simplificando la capa inferior. Si actuaras como intérprete entre científicos y extraterrestres, se te podría pedir que tradujeras "x = y + 2, donde y = 1". Si fueras un patrón adaptador, volverías a expresar la misma información sin ningún cambio. Si fueras un patrón fachada, probablemente dirías "x = 3" porque es más sencillo y los detalles no son críticos para utilizar la información.

Ocultar detalles en un subsistema es una parte importante de un buen diseño. Hace que el código sea más legible y más fácil de probar. Además, el código de llamada no depende de las partes internas del subsistema, por lo que el código subyacente puede cambiar, dejando intacta la fachada.

Una fachada para nuestro LED parpadeante ocultaría la idea de los pines de E/S del código de llamada creando un subsistema LED, como se muestra en la parte derecha de la Figura 4-3. Dado lo poco que el usuario necesita saber sobre el subsistema LED, la fachada podría implementarse con sólo dos funciones:

LEDInit()

Llama a la función de inicialización de E/S para el pin LED (sustituye a IOSetDir(…))

LEDBlink()

Parpadea el LED (sustituye a IOToggle(…))

Añadir una fachada hace que tu código sea más fácil de ampliar y modificar cuando los requisitos cambien inevitablemente.

La entrada en E/S

Marketing quiere cambiar la forma en que el sistema parpadea en respuesta a un botón. Ahora, cuando se mantenga pulsado el botón, el sistema dejará de parpadear por completo.

Nuestro esquema no es mucho más complejo si añadimos un botón (ver Figura 4-4). Observa que el botón utiliza IO2_2 (puerto de E/S 2, patilla 2), que en el esquema se denota con S1 (interruptor 1). El icono de un interruptor tiene cierto sentido; cuando lo pulsas, conduce a través de la zona indicada. Aquí, cuando pulses el interruptor, la patilla se conectará a masa.

Muchas patillas de E/S del procesador tienen resistencias internas de pull-up. Cuando una patilla es una salida, los pull-ups no hacen nada. Sin embargo, cuando la patilla es una entrada, el pull-up le da un valor constante (1), incluso cuando no hay nada conectado. La existencia y la intensidad del pull-up pueden ser configurables, pero esto depende de tu procesador (y posiblemente de la patilla concreta). La mayoría de los procesadores tienen incluso la opción de permitir pull-downs internos en una patilla. En ese caso, nuestro interruptor podría haberse conectado a alimentación en lugar de a masa.

Figura 4-4. Esquema con un LED y un botón
Consejo

Las entradas con pull-ups internos consumen un poco de energía, así que si tu sistema necesita conservar unos microamperios, puedes acabar desactivando los pull-ups (o pull-downs) innecesarios.

El manual de usuario de tu procesador describirá las opciones de patillas. Los pasos básicos para la configuración son los siguientes:

  1. Añade la patilla al archivo de cabecera del mapa de E/S.

  2. Configúralo como entrada. Comprueba que no forma parte de otro periférico.

  3. Configura explícitamente un pull-up (si es necesario).

Una vez que tengas tu pin de E/S configurado como entrada, tendrás que añadir una función para utilizarlo: una que pueda devolver el estado del pin como alto (verdadero) o bajo (falso):

boolean IOGet(uint8_t port, uint8_t pin);

Cuando se pulse el botón, se conectará a masa. Esta señal es activa baja, lo que significa que cuando el botón se mantiene pulsado activamente, la señal es baja.

Para mantener ocultos los detalles del sistema, querremos hacer un subsistema de botones que pueda utilizar nuestro módulo de gestión de E/S. Encima de la función de E/S, podemos poner otra fachada, para que el subsistema de botones tenga una interfaz sencilla.

La función E/S devuelve el nivel del pin. Sin embargo, queremos saber si el usuario ha realizado una acción. En lugar de que la interfaz del botón devuelva el nivel, puedes invertir la señal para determinar si el botón está pulsado en ese momento. La interfaz podría ser

void ButtonInit()

Llama a la función de inicialización de E/S del botón

boolean ButtonPressed()

Devuelve verdadero cuando el botón está pulsado

Como se muestra en la Figura 4-5, tanto el subsistema LED como el de botones utilizan el subsistema de E/S y el archivo de cabecera de mapa de E/S. Esto es una simple ilustración de cómo la modularización que hicimos anteriormente en el capítulo permite la reutilización.

Figura 4-5. Arquitectura con el botón recién añadido

A un nivel superior, hay algunas formas de implementar la función principal. He aquí una posibilidad:

main:
  initialize LED
  initialize button
loop:
  if button pressed, turn LED off
  else toggle LED
  do nothing for a period of time
  repeat

Con este código, el LED no se apagará inmediatamente, sino que esperará hasta que haya transcurrido el retardo. El usuario puede notar cierto retardo entre la pulsación del botón y el apagado del LED.

Consejo

Un sistema que no responde a la pulsación de un botón en menos de un cuarto de segundo (250 ms) parece lento y difícil de usar. Un tiempo de respuesta de 100 ms es mucho mejor, pero sigue siendo perceptible para las personas impacientes. Un tiempo de respuesta inferior a 50 ms parece muy ágil.

Para disminuir el tiempo de respuesta, podríamos comprobar constantemente si se ha pulsado el botón:

loop:
  if button pressed, turn LED off
  else
    if enough time has passed,
      toggle LED
      clear how much time has passed
  repeat

Ambos métodos comprueban el botón para determinar si está pulsado. Esta consulta continua se denomina sondeo y es fácil de seguir en el código. Sin embargo, si es necesario apagar el LED lo más rápido posible, puede que quieras que el botón interrumpa el flujo normal.

Espera, esa palabra(interrumpir) es muy importante. Seguro que la has oído antes, y pronto entraremos en más detalles (y de nuevo en el Capítulo 5). Antes de eso, mira lo sencillo que puede ser el bucle principal si utilizas una interrupción para capturar y manejar la pulsación del botón:

loop:
  if button not pressed, toggle LED
  do nothing for a period of time
  repeat

El código de interrupción (también conocido como ISR, o rutina de servicio de interrupción) llama a la función para apagar el LED. Sin embargo, esto hace que los subsistemas del botón y del LED dependan el uno del otro, acoplando los sistemas de una forma poco obvia. Hay ocasiones en las que tendrás que hacer esto para que un sistema embebido sea lo suficientemente rápido como para manejar un evento.

El Capítulo 5 describirá cómo y cuándo utilizar las interrupciones con más detalle. En este capítulo seguiremos examinándolas sólo a alto nivel.

Pulsación momentánea del botón

En lugar de utilizar el botón para detener el LED, el marketing quiere probar diferentes velocidades de parpadeo pulsando el botón. Por cada pulsación del botón, el sistema debe disminuir la duración del retardo en un 50% (hasta que llegue casi a cero, momento en el que debe volver al retardo inicial).

En la tarea anterior, lo único que tenías que comprobar era si el botón estaba pulsado. Esta vez tienes que saber tanto cuándo se pulsará el botón como cuándo se soltará. Lo ideal sería que el interruptor tuviera el aspecto de la parte superior de la Figura 4-6. Si así fuera, podríamos hacer que el sistema tomara nota del perímetro ascendente de la señal y actuara en ese momento.

Interrumpir al pulsar un botón

Ésta podría ser otra área en la que una interrupción puede ayudarnos a captar la entrada del usuario para que el bucle principal no tenga que sondear el pin de E/S tan rápidamente. El bucle principal se vuelve sencillo si utiliza una variable global para conocer las pulsaciones de los botones:

interrupt when the user presses the button:
  set global button pressed = true

loop:
  if global button pressed,
    set the delay period (reset or decrease it)
    set global button pressed = false
    turn LED off
  if enough time has passed,
    toggle LED
    clear how much time has passed
  repeat

Los pines de entrada de muchos procesadores pueden configurarse para que se interrumpan cuando la señal del pin esté en un determinado nivel (alto o bajo) o haya cambiado (perímetro ascendente o descendente). Si la señal del botón tiene el aspecto de la señal del botón ideal de la parte superior de la Figura 4-6, ¿dónde querrías interrumpir? Interrumpir cuando la señal está baja puede provocar múltiples activaciones si el usuario mantiene pulsado el botón. Yo prefiero interrumpir en el perímetro ascendente para que, cuando el usuario pulse el botón, no ocurra nada hasta que lo suelte.

Advertencia

Para comprobar con precisión una variable global en esta situación, necesitarás la palabra clave volatile C, que quizás nunca antes habías necesitado al desarrollar software en C y C++. Esta palabra clave indica al compilador que el valor de la variable u objeto puede cambiar inesperadamente y nunca debe optimizarse. Todos los registros y todas las variables globales compartidas entre las interrupciones y el código normal deben marcarse como volátiles. Si tu código funciona bien sin optimizaciones y luego falla cuando las optimizaciones están activadas, comprueba que las globales y los registros apropiados están marcados como volátiles.

Configurar la interrupción

La configuración de una patilla para activar una interrupción suele ser independiente de la configuración de la patilla como entrada. Aunque ambos cuentan como pasos de inicialización, queremos mantener la configuración de la interrupción separada de nuestra función de inicialización existente. De este modo, puedes ahorrarte la complejidad de la configuración de la interrupción para las patillas que la requieran (más información en el Capítulo 5).

Configurar un pin para interrumpir el procesador añade tres funciones más a nuestro subsistema de E/S:

IOConfigureInterrupt(port, pin, trigger type, trigger state)

Configura un pin para que sea una interrupción. La interrupción se disparará cuando vea un determinado tipo de disparo, como perímetro o nivel. Para un tipo de activación por flanco, la interrupción puede producirse en el perímetro ascendente o descendente. Para un tipo de disparo por nivel, la interrupción puede producirse cuando el nivel es alto o bajo. Algunos sistemas también proporcionan un parámetro para una llamada de retorno, que es una función que se llamará cuando se produzca la interrupción; otros sistemas codificarán la llamada de retorno con un nombre de función determinado, y tendrás que poner allí tu código.

IOInterruptEnable(port, pin)

Activa la interrupción asociada a un pin.

IOInterruptDisable(port, pin)

Desactiva la interrupción asociada a un pin.

Si las interrupciones no son por pin (podrían ser por banco), el procesador puede tener una interrupción de E/S genérica para cada banco, en cuyo caso el ISR tendrá que desentrañar qué pin causó la interrupción. Depende de tu procesador. Si cada patilla de E/S puede tener su propia interrupción, los módulos pueden acoplarse más libremente.

Conmutadores Desconectadores

Muchos botones no proporcionan la señal limpia que se muestra en la señal de botón ideal de la parte superior de la Figura 4-6. En su lugar, se parecen más a los etiquetados como "Señal de botón digital con glitches". Si interrumpieras en esa señal, tu sistema podría malgastar ciclos de procesador interrumpiendo en los rebotes al principio y al final de la pulsación del botón. El rebote del interruptor puede deberse a un efecto mecánico o eléctrico.

La Figura 4-6 también muestra una vista analógica de lo que podría ocurrir cuando se pulsa un botón y sólo entra en acción lentamente. (Lo que ocurre realmente puede ser mucho más complicado, dependiendo de si el rebote se debe principalmente a una causa mecánica o eléctrica). Ten en cuenta que hay partes de la señal analógica en las que la señal no es ni alta ni baja, sino algo intermedio. Como tu línea de E/S es una señal digital, no puede representar este valor indeterminado y puede comportarse mal. Una señal digital llena de perímetros haría que el código creyera que se han producido varias pulsaciones por acción del usuario. El resultado sería incoherente y frustrante para el usuario. Peor aún, interrumpir en una señal así puede provocar inestabilidad en el procesador, así que volvamos al sondeo de la señal.

Figura 4-6. Diferentes vistas de las señales de los botones

El rebote es la técnica utilizada para eliminar los perímetros espurios. Aunque puede hacerse por hardware o por software, nos centraremos en el software. Consulta el excelente artículo web de Jack Ganssle sobre el debouncing (en "Lecturas complementarias") para conocer las soluciones de hardware.

Nota

Muchos interruptores modernos tienen un periodo de incertidumbre muy corto. Los interruptores también tienen hojas de datos; consulta la tuya para ver qué recomienda el fabricante. Ten en cuenta que intentar determinar empíricamente si necesitas desconectar puede no ser suficiente, ya que los distintos lotes de interruptores pueden actuar de forma diferente.

Sigue buscando el perímetro ascendente de la señal, donde el usuario suelta el botón. Para evitar la basura cerca de los flancos ascendente y descendente, tendrás que buscar un periodo relativamente largo de señal constante. Lo largo que sea depende de tu interruptor y de lo rápido que quieras responder al usuario.

Para eliminar el rebote del interruptor, haz varias lecturas (también llamadas muestras) de la clavija a un intervalo periódico varias veces más rápido de lo que te gustaría que respondiera. Cuando haya varias muestras consecutivas y coherentes, avisa al resto del sistema de que el pulsador ha cambiado. Mira la Figura 4-6; el objetivo es leer lo suficiente como para que los niveles lógicos inciertos no provoquen pulsaciones espurias del botón.

Necesitarás tres variables:

  • La lectura en bruto actual de la línea de E/S

  • Un contador para determinar cuánto tiempo ha sido constante la lectura bruta

  • El valor del botón rebotado utilizado por el resto del código

El tiempo que tarda la desbobinación (y el tiempo que tarda tu sistema en responder a un usuario) depende de lo alto que tenga que incrementarse el contador antes de que la variable del botón desbobinado pase al estado bruto actual. El contador debe configurarse de modo que la eliminación de la denuncia se produzca en un tiempo razonable para ese interruptor.

Si no hay una especificación para ello en tu producto, considera la velocidad a la que se pulsan los botones en un teclado. Si los mecanógrafos avanzados pueden escribir 120 palabras por minuto, suponiendo una media de cinco caracteres por palabra, están pulsando las teclas (botones) unas 10 veces por segundo. Calculando que un botón está pulsado la mitad del tiempo, tienes que buscar que el botón esté pulsado durante unos 50 ms. (Si realmente estás fabricando un teclado, probablemente necesites una tolerancia más ajustada porque hay mecanógrafos más rápidos).

Para nuestro sistema, el interruptor mítico tiene una hoja de datos imaginaria que establece que el interruptor sonará durante no más de 12,5 ms al pulsarlo o soltarlo. Si el objetivo es responder a un botón pulsado durante 50 ms o más, podemos muestrear a 10 ms (100 Hz) y buscar cinco muestras consecutivas.

Utilizar cinco muestras consecutivas es bastante conservador. Quizá quieras ajustar la frecuencia con la que sondeas el nivel de la patilla, de modo que sólo necesites tres muestras consecutivas para indicar que el estado del botón ha cambiado.

Consejo

Consigue un equilibrio en tu metodología de denuncia: considera el coste de equivocarte (¿molestia o catástrofe?) y el coste de responder más lentamente al usuario.

En el método anterior de interrupción de perímetro descendente para tratar la pulsación del botón, el estado del botón no era tan interesante como el cambio de estado. Para ello, añadiremos una cuarta variable para simplificar el bucle principal:

read button:
  if raw reading is the same as debounced button value,
    reset the counter
  else
    decrement the counter
    if the counter is zero,
      set debounced button value to raw reading
      set changed to true
      reset the counter

main loop:
  if time to read button,
    read button
    if button changed and button is no longer pressed,
      set button changed to false
      set the delay period (reset or halve it)
  if time to toggle the LED,
    toggle LED
  repeat

Ten en cuenta que ésta es la forma básica del código. Hay muchas opciones que pueden depender de los requisitos de tu sistema. Por ejemplo, ¿quieres que reaccione rápidamente al pulsar un botón, pero lentamente al soltarlo? No hay ninguna razón por la que el contador de rebotes tenga que ser simétrico.

En el pseudocódigo anterior, el bucle principal vuelve a sondear el botón en lugar de utilizar interrupciones. Sin embargo, muchos procesadores tienen temporizadores que pueden configurarse para interrumpir. La lectura del botón podría hacerse en un temporizador para simplificar la función principal. La conmutación del LED también podría realizarse en un temporizador. Pronto hablaremos más sobre los temporizadores, pero antes, marketing tiene otra petición.

Incertidumbre de tiempo de ejecución

Marketing tiene varios LED para probar. Los LEDs están conectados a diferentes clavijas. Utiliza el botón para recorrer las posibilidades.

Hemos manejado la pulsación del botón, pero el subsistema LED sólo conoce la salida en el pin 1_2 de la placa v1 o 1_3 de la placa v2. Una vez que hayas inicializado todos los LEDs como salidas, podrías poner una sentencia condicional (o switch) en tu bucle principal:

if count button presses == 0, toggle blue LED
if count button presses == 1, toggle red LED
if count button presses == 2, toggle yellow LED

Para ponerlo en práctica, tendrás que tener tres subsistemas LED diferentes, o (lo que es más probable) tu función de conmutación del LED tendrá que tomar un parámetro. Lo primero representa un montón de código copiado (casi siempre algo malo), mientras que lo segundo significa que la función LED tendrá que asignar el color al pin de E/S cada vez que conmute el LED (lo que consume ciclos de procesador).

Aumentar la flexibilidad del código

Nuestro objetivo aquí es crear un método para utilizar una opción concreta de una lista de varios objetos posibles. En lugar de hacer la selección cada vez (en el bucle principal o en la función LED), puedes seleccionar el LED deseado al pulsar el botón. Entonces, la función de conmutación del LED es agnóstica respecto al LED que está cambiando:

main loop:
  if time to read button,
    read button
    if button changed and button is no longer pressed,
      set button changed to false
      change LED variable

  if time to toggle the LED,
    toggle LED
  repeat

Al añadir una variable de estado, utilizamos un poco de RAM para ahorrar unos cuantos ciclos de procesador. Las variables de estado tienden a hacer confuso un sistema, especialmente cuando la sección change LED variable del código está separada de toggle LED. Desentrañar el código para mostrar cómo una variable de estado controla la opción puede resultar tedioso para alguien que intente solucionar un error (¡comentar ayuda!). La variable de estado simplifica considerablemente la función LED toggle, por lo que hay ocasiones en las que una variable de estado merece la pena por las complicaciones que crea.

Inyección de dependencia

Sin embargo, podemos ir más allá de una variable de estado y hacer algo aún más flexible. Antes vimos que abstraer los pines de E/S de la placa nos ahorra tener que reescribir el código cuando cambia la placa. También podemos utilizar la abstracción para tratar los cambios dinámicos (como qué LED se va a utilizar). Para ello, utilizaremos una técnica llamada inyección de dependencias.

Antes, escondíamos el pin de E/S en el código del LED (creando una jerarquía de funciones que sólo dependen de los niveles inferiores). Con la inyección de dependencia, eliminaremos esa dependencia pasando un manejador de E/S como parámetro al código de inicialización del LED. El manejador de E/S sabrá qué pin cambiar y cómo cambiarlo, pero el código del LED sólo sabrá cómo llamar al manejador de E/S. Mira la Figura 4-7.

Figura 4-7. Arquitectura de inyección de dependencias para la incertidumbre en tiempo de ejecución

Un ejemplo muy utilizado para ilustrar la inyección de dependencia relaciona los motores con los coches. El coche, el producto final, depende de un motor para moverse. El fabricante fabrica el coche y el motor. Aunque el coche no puede elegir qué motor instalar, el fabricante puede inyectar cualquiera de las opciones de dependencia que el coche puede utilizar para desplazarse (por ejemplo, el motor de 800 caballos o el de 20 caballos).

Volviendo al ejemplo del LED, el código del LED es como el del coche y depende del pin de E/S para funcionar, igual que el coche depende del motor. Sin embargo, el código del LED puede hacerse lo suficientemente genérico como para evitar la dependencia de un pin de E/S concreto. Esto permite que la función principal (nuestro fabricante) instale un pin de E/S adecuado a las circunstancias, en lugar de codificar la dependencia en tiempo de compilación. Esta técnica te permite componer el funcionamiento del sistema en tiempo de ejecución.

En C++ u otros lenguajes orientados a objetos, para inyectar la dependencia, pasamos un nuevo objeto manejador de patillas de E/S al LED cada vez que se pulsa un botón. El módulo LED nunca sabría nada sobre qué pin está cambiando ni cómo lo está haciendo. Las variables para ocultar esto se establecen en el momento de la inicialización (pero recuerda que se trata de variables, que consumen RAM y desordenan el código).

Consejo

En C se suele utilizar una estructura de punteros a funciones para conseguir el mismo objetivo.

La inyección de dependencia es una técnica muy potente, sobre todo si tu módulo LED tuviera que hacer algo mucho más complicado, por ejemplo, emitir código Morse. Si pasaras tu manejador de patillas de E/S, podrías reutilizar la rutina de salida de código Morse del LED para cualquier procesador. Además, durante las pruebas, tu manejador de patillas de E/S podría imprimir cada llamada que le hiciera el módulo LED en lugar de (o además de) cambiar la patilla de salida.

Sin embargo, el ejemplo del motor del coche ilustra uno de los principales problemas de la inyección de dependencia: la complejidad. Funciona bien cuando sólo necesitas cambiar el motor. Pero una vez que estás inyectando las ruedas, la columna de dirección, las fundas de los asientos, la transmisión, el salpicadero y la carrocería, el módulo del coche se complica bastante, con poca utilidad intrínseca propia.

El objetivo de la inyección de dependencias es permitir la flexibilidad. Esto es contrario al objetivo del patrón de fachada, que reduce la complejidad. En un sistema embebido, la inyección de dependencias necesitará más RAM y unos cuantos ciclos de procesador adicionales. El patrón de fachada casi siempre ocupará más espacio de código. Tendrás que considerar las necesidades y recursos de tu sistema para encontrar un equilibrio razonable.

Utilizar un temporizador

Utilizar el botón para cambiar la velocidad de parpadeo era útil, pero marketing ha encontrado un problema en la incertidumbre introducida en la velocidad de parpadeo. En lugar de reducir la velocidad del LED a la mitad, marketing quiere utilizar el botón para recorrer una serie de velocidades de parpadeo precisas: 6,5 veces por segundo (Hz), 8,5 Hz y 10 Hz.

Esta petición parece sencilla, pero es la primera vez que necesitamos hacer algo con precisión horaria. Antes, el sistema podía manejar los botones y conmutar el LED generalmente cuando le convenía. Ahora el sistema necesita manejar el LED en tiempo real. Lo cerca que estés de la "precisión" depende de los parámetros de tu sistema, principalmente de la exactitud y precisión del reloj de entrada de tu procesador. Empezaremos utilizando un temporizador en el procesador para hacerlo más preciso de lo que era antes, y luego veremos si el marketing puede aceptarlo.

Piezas con temporizador

En principio, un temporizador es un simple contador que mide el tiempo acumulando un número de pulsaciones de reloj. Cuanto más determinista sea el reloj maestro, más preciso podrá ser el temporizador. Los temporizadores funcionan independientemente de la ejecución del software, actuando en segundo plano sin ralentizar en absoluto el código. Para ser técnicos, ocurren en las puertas de silicio que forman parte del microcontrolador.

Para ajustar la frecuencia del temporizador, tendrás que determinar la entrada del reloj. Puede ser el reloj de tu procesador (también conocido como reloj del sistema, o reloj maestro), o puede ser un reloj diferente de otro subsistema (por ejemplo, muchos procesadores tienen un reloj periférico).

Por ejemplo, el ATtiny45 tiene un reloj de procesador máximo de 4 MHz. Queremos que el LED pueda parpadear a 10 Hz, pero eso significa interrumpir a 20 Hz (interrumpir para encenderlo, volver a interrumpir para apagarlo). Esto significa que necesitaremos una división de 200.000. El ATtiny45 es un procesador de 8 bits; tiene dos temporizadores de 8 bits y uno de 16 bits. Ninguno de los dos temporizadores contará tan alto (consulta la barra lateral "Estadísticas del sistema"). Sin embargo, los diseñadores del chip se dieron cuenta de este problema y nos proporcionaron otra herramienta: el registro de preescalado, que divide el reloj para que el contador se incremente a un ritmo más lento.

Advertencia

Muchos temporizadores se basan en cero en lugar de en uno, así que para un preescalador que divide por 2, pon un 1 en el registro del preescalador. Todo esto de los temporizadores ya es bastante complicado sin llevarlo a cabo con las matemáticas anteriores. Consulta el manual de tu procesador para ver qué registros de temporizador están basados en cero.

El efecto del registro de preescalado se ve en la Figura 4-8. El reloj del sistema cambia regularmente. Con un valor de preescalado de dos, el reloj preescalado (la entrada a nuestro subsistema de temporizador) alterna a la mitad de la velocidad del reloj del sistema. El temporizador cuenta hacia arriba. El procesador observa cuando el temporizador coincide con el registro de comparación (ajustado a 3 en el diagrama). Cuando el temporizador coincide, puede seguir contando o reiniciarse, dependiendo del procesador y de los ajustes de configuración.

Antes de volver al temporizador del ATtiny45, ten en cuenta que los registros necesarios para que funcione un temporizador suelen ser los siguientes:

Contador temporizador

Contiene el valor cambiante del temporizador (el número de ticks desde que el temporizador se reinició por última vez).

Registro de comparación (o registro de comparación de captura o registro de coincidencia)

Cuando el contador del temporizador es igual a este registro, se realiza una acción. Puede haber más de un registro de comparación para cada temporizador.

Registro de acción (o registro de auto-recarga)

Este registro establece una acción a realizar cuando el temporizador y el registro de comparación son iguales. (En algunos temporizadores, estas acciones también están disponibles cuando el temporizador se desborda, que es como tener un registro de comparación establecido en el valor máximo del contador del temporizador). Hay cuatro tipos de acciones posibles de configurar (pueden ocurrir una o varias):

  • Interrumpe

  • Detener o continuar el recuento

  • Recarga el contador

  • Poner un pin de salida en alto, bajo, conmutar o sin cambios

Registro de configuración del reloj (opcional)

Este registro indica a un subsistema qué fuente de reloj debe utilizar, aunque por defecto puede ser el reloj del sistema. Algunos procesadores tienen temporizadores que incluso permiten conectar un reloj a un pin de entrada. A menudo puedes elegir si quieres que la cuenta sea ascendente o descendente; en estos ejemplos utilizaré la cuenta ascendente, pero el proceso es similar para la cuenta descendente.

Registro preescalador

Como se muestra en la Figura 4-8, esto reduce el reloj rápido entrante para que funcione más lentamente, permitiendo que los temporizadores sucedan para eventos más lentos.

Registro de control

Esto hace que el temporizador empiece a contar una vez configurado. A menudo, el registro de control también tiene una forma de reiniciar el temporizador.

Registro de interrupción (puede ser múltiple)

Si tienes interrupciones de temporizador, tendrás que utilizar el registro de interrupción adecuado para activar, desactivar y comprobar el estado de cada interrupción de temporizador.

Figura 4-8. Preescalado y ajuste del temporizador

La configuración de un temporizador es específica del procesador; por lo general, el manual del usuario te guiará en la configuración de cada uno de estos registros. El manual de usuario de tu procesador puede dar a los registros nombres ligeramente diferentes. Una buena forma de entender cómo utilizar un temporizador es mirar el código de ejemplo de un proveedor de chips: normalmente incluye unos cuantos ejemplos que configuran los temporizadores de varias formas. Deberías recorrer el código de ejemplo línea por línea, con la hoja de datos delante, para asegurarte de que entiendes cómo se configura el temporizador.

Nota

En lugar de un registro de comparación, puede que tu procesador te permita activar las acciones del temporizador sólo cuando éste se desborde. Se trata de un valor de coincidencia implícito de dos al número de bits del registro del temporizador, menos 1 (por ejemplo, para un temporizador de 8 bits, (28) - 1 = 255). Ajustando el preescalador, se pueden alcanzar la mayoría de los valores del temporizador sin demasiados errores.

Haciendo cuentas

Voy a mostrar las matemáticas necesarias para configurar un temporizador. Siéntete libre de hojear o saltar a "Uso de la modulación por ancho de pulso" y vuelve cuando necesites las matemáticas. Además, echa un vistazo al repositorio GitHub de este libro para ver algunas calculadoras que te permitirán seguir estas matemáticas sin seguir mi álgebra aquí.

Los temporizadores están hechos para tratar con escalas de tiempo físicas, por lo que necesitas relacionar una serie de registros con un tiempo real. Recuerda que la frecuencia (por ejemplo, 10 Hz) es inversamente proporcional al periodo (por ejemplo, 0,1 segundos).

La ecuación básica para la relación entre la frecuencia de interrupción, la entrada de reloj, el preescalador y el registro de comparación es:

interruptFrequency = clockIn / (prescaler * compare)

Se trata de un problema de optimización. Conoces el clockIn y el objetivo, interrupt​Frequency. Tienes que ajustar el preescalador y los registros de comparación hasta que la frecuencia de interrupción se acerque lo suficiente al objetivo. Si no hubiera otras limitaciones, éste sería un problema fácil de resolver (pero no siempre es tan fácil).

Volviendo al temporizador de 8 bits del ATtiny45, al reloj del sistema de 4 MHz y a la frecuencia objetivo de 20 Hz, podemos exportar las restricciones que tendremos que utilizar para resolver la ecuación:

  • Son valores enteros, por lo que el preescalador y el registro de comparación tienen que ser números enteros. Esta restricción es válida para cualquier procesador.

  • El registro de comparación tiene que estar entre 0 y 255 (porque el registro del temporizador tiene un tamaño de ocho bits).

  • El preescalador del ATtiny45 es de 10 bits, por lo que el preescalador máximo es 1.023. (El tamaño de tu preescalador puede ser diferente.)

El preescalador de este subsistema no se comparte con otros periféricos, por lo que no tenemos que preocuparnos por esta posible limitación para nuestra solución (todavía).

Existen varias heurísticas para encontrar un preescalador y un registro de comparación que proporcionen la frecuencia de interrupción necesaria (ver Figura 4-9).

Advertencia

Pregunté a dos profesores de matemáticas cómo resolver este problema de forma genérica. Las respuestas que obtuve fueron interesantes. Lo más interesante fue enterarme de que este problema es NP-completo por dos razones: intervienen números enteros y es un problema no lineal de dos variables. ¡Gracias, profesor Ross y profesor Patton!

Figura 4-9. Heurística para encontrar los valores de registro para una frecuencia de interrupción objetivo

Podemos determinar el preescalador mínimo reordenando la ecuación y ajustando el registro de comparación a su valor máximo:

prescaler = clockIn / (compare * interruptFrequency)
      = 4 MHz / (255 * 20 Hz)

Por desgracia, el valor del preescalador resultante es un número en coma flotante (784,31). Si redondeas (785), acabas con una frecuencia de interrupción de 19,98 Hz, lo que supone un error de menos de una décima de porcentaje.

Si redondeas a la baja el preescalador (784), la frecuencia de interrupción estará por encima del objetivo, y podrás disminuir el registro de comparación para que el temporizador esté más o menos bien. Con el preescalador = 784 y el comparador = 255, el error es del 0,04%. Sin embargo, el marketing pedía alta precisión, y existen algunos métodos para encontrar un preescalador mejor.

En primer lugar, ten en cuenta que quieres que el producto del preescalador y el registro de comparación sea igual a la entrada de reloj dividida por la frecuencia objetivo:

prescaler * compare = clockIn / interruptFrequency = 4 MHz/20 Hz = 200,000

Es un número redondo y bonito, fácilmente factorizable en 1.000 (preescalador) y 200 (registro de comparación). Esta es la mejor solución y la más fácil para optimizar los prescaler y compare: determinar los factores de (clockIn/interruptFrequency) y ordenarlos en prescaler y compare. Sin embargo, esto requiere que (clockIn/interrupt​Fre⁠quency) sea un número entero y que los factores se dividan fácilmente en los tamaños permitidos para los registros. No siempre es posible utilizar este método.

Más Matemáticas: Objetivo Difícil Frecuencia

A medida que avanzamos hacia otra frecuencia de parpadeo solicitada por marketing (8,5 Hz, o una frecuencia de interrupción de 17 Hz), el nuevo objetivo es:

prescaler * compare = clockIn / interruptFrequency = 4 MHz/17 Hz = 235294.1

No existe una factorización sencilla de este número en coma flotante. Podemos comprobar que el resultado es posible calculando el preescalador mínimo (lo hicimos antes poniendo el registro de comparación en su valor máximo). El resultado (923) cabrá en nuestro registro de 10 bits. Podemos calcular el porcentaje de error utilizando lo siguiente:

error = 100 * (goal interrupt frequency - actual) / goal

Con el preescalador mínimo, obtenemos un error del 0,03%. Está bastante cerca, pero podemos acercarnos más.

Ajusta el preescalador a su valor máximo, y mira cuáles son las opciones. En este caso, un preescalador de 1.023 conduce a un valor de comparación de 230 y a un error inferior al 0,02%, lo que es un poco mejor. Pero, ¿podemos reducir aún más el error?

Para temporizadores más grandes, podrías intentar una búsqueda binaria de un buen valor, empezando por el preescalador mínimo. Duplícalo, y luego mira los valores del preescalador que sean +/- 1 para encontrar un registro de comparación que sea el número entero más cercano. Si la frecuencia resultante no se aproxima lo suficiente, repite la duplicación del preescalador modificado. Por desgracia, con nuestro ejemplo, no podemos duplicar nuestro preescalador y mantenernos dentro de los límites del número de 10 bits.

Por último, otra forma de encontrar la solución es utilizar un script o programa (por ejemplo, MATLAB o Excel) y utilizar la fuerza bruta para probar las opciones, como se muestra en el procedimiento siguiente. Empieza por encontrar los valores mínimo y máximo del preescalador (poniendo el registro de comparación a 1). Limita el mínimo y el máximo para que sean números enteros y quepan en el número correcto de bits. Luego, para cada número entero de ese rango, calcula el registro de comparación para la frecuencia de interrupción objetivo. Redondea el registro de comparación al número entero más próximo y calcula la frecuencia de interrupción real. Este método dio como resultado un preescalador de 997, un registro de comparación de 236 y un pequeño error del 0,0009%. Una solución de fuerza bruta como ésta te dará el menor error, pero probablemente te llevará el mayor tiempo de desarrollo. Determina con qué error puedes vivir, y pasa a otras cosas una vez hayas alcanzado ese objetivo.

Método de fuerza bruta para encontrar el valor más bajo del registro de frecuencia de interrupción de error:

  1. Calcula los valores mínimo y máximo del preescalador:

    m i n P r e s c a l e r = c l o c k I n p u t g o a l F r e q u e n c y × m a x C o m p a r e = 4 M H z 17 H z × 255 = 922 m a x P r e s c a l e r = c l o c k I n p u t g o a l F r e q u e n c y × m a x C o m p a r e = 4 M H z 17 H z × 1 = 235,294 supera el máx. = 2 10 - 1 = 1,023
  2. Para cada valor del preescalador de 922 a 1.023, calcula un valor de comparación:

    c o m p a r e = c l o c k I n p u t g o a l F r e q u e n c y × p r e s c a l e r = 4 M H z 20 H z × 922 = 255.2 = r o u n d ( 255.2 ) 255

    Utiliza el valor de comparación calculado para determinar la frecuencia de interrupción con estos valores:

    i n t e r r u p t F r e q u e n c y = clockInput prescaler*compare = 4mHz 923*255 = 17 . 013 H z

    Calcula el error:

    e r r o r % = 100 × a b s ( g o a l F r e q u e n c y - i n t e r r u p t F r e q u e n c y ) g o a l F r e q u e n c y = 100 × a b s ( 17 H z - 17.013 ) 17 H z = 0.08 %
  3. Encuentra el preescalador y los registros de comparación con el menor error.

Una larga espera entre las pulsaciones del temporizador

La fuerza bruta funciona bien para 17 Hz, pero cuando consigues la salida objetivo de 13 Hz (el nuevo objetivo de marketing de 2 × 6,5 Hz), el preescalador mínimo que puedes calcular es de más de 10 bits. El temporizador no cabe en el temporizador de 8 bits. Esto se muestra como una excepción en el diagrama de flujo(Figura 4-9). La solución más sencilla es utilizar un temporizador mayor si puedes. El temporizador de 16 bits del ATtiny45 puede aliviar este problema porque su valor máximo de comparación es de 65.535 en lugar de los 255 de 8 bits, por lo que podemos utilizar un preescalador más pequeño.

Si no se dispone de un temporizador más grande, otra solución es desconectar la línea de E/S del temporizador y llamar a una interrupción cuando expire el temporizador. La interrupción puede incrementar una variable y actuar cuando la variable sea lo suficientemente grande. Por ejemplo, para llegar a 13 Hz, podríamos tener un temporizador de 26 Hz y conmutar el LED cada dos veces que se llame a la interrupción. Este método es menos preciso porque podría haber retrasos debidos a otras interrupciones.

Utilizar un temporizador

Una vez que hayas determinado tus ajustes, la parte difícil ha terminado, pero hay algunas cosas más que hacer:

  • Elimina el código de la función principal para conmutar el LED. Ahora el bucle principal sólo necesitará un conjunto de registros de preescalado y comparación para recorrerlos cuando se pulse el botón.

  • Escribe el controlador de interrupción que activa el LED.

  • Configura la patilla. Algunos procesadores conectarán cualquier temporizador a cualquier salida, mientras que otros permitirán que un temporizador cambie de pin sólo con una configuración especial. Para el procesador que no admita un temporizador en el pin, necesitarás tener un manejador de interrupciones en el código que conmute sólo el pin de interés.

  • Configura los ajustes del temporizador e inícialo.

Uso de la modulación por ancho de pulsos

La investigación de mercado ha demostrado que a los clientes potenciales les molesta el brillo del LED, diciendo que su intensidad, similar a la de un láser, les ciega. Marketing quiere que prueben diferentes ajustes de brillo (100%, 80%, 70% y 50%), utilizando el botón para cambiar entre las opciones.

Esta tarea nos ofrece una buena oportunidad para explorar la modulación por ancho de pulsos (PWM), que determina el tiempo que un pin permanece alto o bajo antes de conmutar. Los PWM funcionan de forma continua, encendiendo y apagando un periférico según un programa regular. El ciclo suele ser muy rápido, del orden de milisegundos (o cientos de Hz).

Las señales PWM a menudo accionan motores y LED (aunque los motores requieren un poco más de soporte de hardware). Utilizando PWM, el procesador puede controlar la cantidad de energía que recibe el hardware. Utilizando alguna electrónica barata, la salida de una patilla PWM puede suavizarse para que sea la señal media. En el caso de los LEDs, sin embargo, no es necesaria ninguna electrónica adicional. El brillo es relativo a la cantidad de tiempo que el LED está encendido durante cada ciclo.

Un temporizador es un conjunto de pulsos que son todos iguales, por lo que la señal del temporizador es 50% alta y 50% baja (esto se conoce como ciclo de trabajo del 50%). En el PWM, la anchura de los pulsos cambia en función de la situación. Así que un PWM puede tener una relación diferente. Un PWM con un ciclo de trabajo del 100% está siempre encendido, como un nivel alto de un pin de salida. Y un ciclo de trabajo del 0% representa un pin que ha sido conducido (o tirado) hacia abajo. El ciclo de trabajo representa el valor medio de la señal, como muestra la línea discontinua de la Figura 4-10.

Figura 4-10. Ciclos de trabajo PWM: 20%, 50% y 80%.

Teniendo en cuenta el temporizador de "Utilizar un temporizador", podríamos implementar un PWM con una interrupción. Para nuestro ejemplo del LED de 20 Hz, teníamos un registro de comparación de 200, de modo que cada 200 ticks, la interrupción del temporizador haría algo (conmutar el LED). Si quisiéramos que el LED estuviera encendido el 80% del tiempo con un temporizador de 20 Hz (en lugar del 50% que hacíamos antes), podríamos hacer ping-pong entre dos interrupciones que activaran el registro de comparación en cada pasada:

  • Interrupción temporizador 1

    • Enciende el LED.

    • Pon el registro de comparación a 160 (80% de 200).

    • Reinicia el temporizador.

  • Interrupción del temporizador 2

    • Apaga el LED.

    • Pon el registro de comparación a 40 (20% de 200).

    • Reinicia el temporizador.

Con un temporizador de 20 Hz, esto probablemente se vería como una serie muy rápida de parpadeos en lugar de un LED tenue. El problema es que el temporizador de 20 Hz es demasiado lento. Cuanto más aumentes la frecuencia, más tenue parecerá el LED en lugar de parpadear. Sin embargo, una frecuencia más rápida significa más interrupciones.

Existe una forma de llevar a cabo este procedimiento en el procesador. En el apartado anterior, las acciones configurables incluían si se reiniciaba el contador, así como la forma de fijar el pin. Muchos temporizadores tienen varios registros de comparación y permiten acciones diferentes para cada uno. Así, una salida PWM puede configurarse con dos registros de comparación, uno para controlar la frecuencia de conmutación y otro para controlar el ciclo de trabajo.

Por ejemplo, la parte inferior de la Figura 4-10 muestra un temporizador que cuenta hacia arriba y se reinicia. Esto representa la frecuencia de conmutación establecida por un registro de comparación. Llamaremos A a este registro de comparación y lo pondremos a 100. Cuando se alcanza este valor, el temporizador se reinicia y el LED se enciende. El ciclo de trabajo se ajusta con un registro diferente (registro de comparación B, ajustado a 80) que apaga el LED pero permite que el temporizador siga contando.

Qué patillas pueden actuar como salidas PWM depende de tu procesador, aunque a menudo son un subconjunto de las patillas que pueden actuar como salidas del temporizador. La sección PWM del manual de usuario del procesador puede estar separada de la sección del temporizador. Además, hay diferentes configuraciones de controladores PWM, a menudo para aplicaciones concretas (los motores suelen ser quisquillosos con el tipo de PWM que necesitan).

Para nuestro LED, una vez configurado el PWM, el código sólo necesita modificar el ciclo de trabajo cuando se pulsa el botón. Al igual que con los temporizadores, la función principal no controla directamente el LED en absoluto.

Aunque atenuar el LED es lo que pedía el marketing, hay otras aplicaciones interesantes que puedes probar. Para conseguir un efecto de ronquido, en el que el LED se desvanece, tendrás que modificar el ciclo de trabajo en incrementos. Si tienes LED tricolores, puedes utilizar el control PWM para ajustar los tres colores del LED a distintos niveles, lo que proporciona toda una paleta de opciones.

Envío del producto

Marketing ha encontrado el LED (azul), la frecuencia de parpadeo (8 Hz) y el brillo (100%) perfectos, y está listo para enviar el producto en cuanto establezcas los parámetros.

Todo parece tan sencillo como establecer los parámetros y enviar el código. Sin embargo, ¿qué aspecto tiene el código ahora? ¿Se ha transformado el código del temporizador en código PWM, o sigue existiendo la interrupción del temporizador? Con una luminosidad del 100%, el código PWM ya no es necesario. De hecho, el código del botón puede desaparecer. La posibilidad de elegir un LED en tiempo de ejecución ya no es necesaria. El antiguo diseño de la placa puede olvidarse en el futuro. Antes de enviar el código y congelar el desarrollo, intentemos reducir el espagueti en algo menos enmarañado.

Un producto empieza como una idea y a menudo tarda unas cuantas iteraciones en solidificarse hasta convertirse en realidad. Los ingenieros suelen tener una buena imaginación sobre cómo funcionarán las cosas. No todo el mundo tiene tanta suerte, así que un buen prototipo puede ayudar mucho a definir el objetivo.

Sin embargo, mantener código innecesario desordena la base de código (véase la parte izquierda de la Figura 4-11). El código no utilizado (o peor aún, el código que ha sido comentado) es frustrante para la siguiente persona que no sepa por qué se han eliminado cosas. Evita eso en la medida de lo posible. En su lugar, confía en que tu sistema de control de versiones pueda recuperar el código antiguo. No tengas miedo de etiquetar, ramificar o liberar internamente una versión de desarrollo. Te ayudará a encontrar las funciones eliminadas después de podar el código para su envío.

Figura 4-11. Comparación del código espagueti del prototipo con un diseño más sencillo

En este ejemplo, muchas cosas son fáciles de eliminar porque simplemente no son necesarias. Algo sobre lo que es más difícil decidir es la inyección de dependencias. Aumenta la flexibilidad para futuros cambios, lo que es una buena razón para dejarla. Sin embargo, cuando tienes que asignar un temporizador concreto a una patilla de E/S concreta, la configuración del sistema se vuelve más dependiente del procesador y más rígida. El coste de forzarlo a ser flexible puede ser alto si intentas construir un archivo para manejar todas las contingencias. En este caso, consideré la idea y sopesé el coste de hacer un archivo que nunca querría mostrar a nadie con el beneficio de reducir la posibilidad de escribir errores en el subsistema de E/S. Opté por hacer archivos más legibles, aunque ello supusiera algunos fallos iniciales, pero respeto cualquiera de las dos opciones.

En la parte derecha de la Figura 4-11, se recorta la base de código, utilizando sólo los módulos que necesita. Mantiene el archivo de cabecera de mapeado de E/S, incluso con definiciones para la placa antigua, porque el coste es bajo (está en un archivo independiente para facilitar el mantenimiento y no requiere ciclos de procesador adicionales). Los ingenieros de sistemas embebidos tendemos a acabar con el hardware más antiguo (retribución kármica por el tiempo al principio del proyecto, cuando teníamos el hardware más nuevo). Puede que necesites el archivo de cabecera de la placa antigua para futuros desarrollos. Casi todo lo demás que no sea crítico puede pasar al control de versiones y luego eliminarse del proyecto.

Duele ver el esfuerzo tirado por la borda. ¡Pero no es así! Todo el resto del código era necesario para hacer los prototipos que se requerían para fabricar el producto. El código ayudó a la comercialización, y aprendiste mucho mientras lo escribías y lo probabas. Lo mejor de desarrollar el código del prototipo es que el código final puede parecer limpio porque has explorado las opciones.

Necesitas equilibrar la flexibilidad de dejar todo el código dentro con la mantenibilidad de una base de código que sea fácil de entender. Ahora que lo has escrito una vez, confía en que tu yo futuro pueda escribirlo de nuevo (si es necesario).

Otras lecturas

Get Creación de sistemas empotrados, 2ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.