Capítulo 4
Parte 9 - Eliminación de rebotes de botones mediante software
Después de completar el tutorial de eliminación de rebotes basado en hardware, muchos principiantes pueden preguntarse por qué aún necesitamos aprender algoritmos de eliminación de rebotes por software, incluso si la solución del circuito con condensador funciona de manera confiable en prototipos con placas de pruebas. La ventaja más directa de la eliminación de rebotes por software radica en el costo adicional cero de componentes una vez que su MCU tiene suficiente espacio flash libre y ciclos de CPU disponibles para ejecutar lógica adicional. Todo el mecanismo de filtrado se implementa mediante varios segmentos de código simples almacenados dentro de la memoria interna del microcontrolador, y los desarrolladores pueden ajustar libremente el umbral de juicio para adaptarse a diferentes modelos de botones mecánicos sin necesidad de rediseñar el diseño de la PCB. Los circuitos de eliminación de rebotes por hardware requieren agregar condensadores pasivos en cada traza de señal de botón para cada placa de circuito producida en masa, lo que acumula gastos adicionales de materiales cuando el producto contiene docenas de interruptores de activación. Además, es difícil elegir un valor de condensador universal que ofrezca efectos de filtrado estables para todos los tipos de pulsadores utilizados en toda la línea de productos. Sin embargo, si su firmware embebido necesita mantener la velocidad máxima de operación y no puede permitirse desperdiciar ciclos de reloj en un muestreo de señales basado en bucles, la eliminación de rebotes por hardware sigue siendo la opción más razonable para reducir la presión computacional en tiempo de ejecución. Los actores malintencionados suelen analizar la lógica de eliminación de rebotes por software no optimizada durante la ingeniería inversa después de volcar la memoria flash; los bucles de retardo débiles creados para el filtrado de botones pueden convertirse en una falla de temporización para desbloquear temporalmente las restricciones lockbit y lanzar una lectura no autorizada de los datos de calibración de la EEPROM.
Este tutorial introduce una solución robusta de eliminación de rebotes por software construida alrededor de dos variables contadoras de confianza, que complementa perfectamente la bandera de estado Pressed que utilizamos en la demostración anterior de conmutación de dos LED. El rebote mecánico genera flujos alternos de señales eléctricas altas y bajas cuando los usuarios presionan o liberan un botón. Si el MCU solo muestrea un valor de pin una sola vez, confundirá el ruido de rebote transitorio con acciones de entrada válidas. Introducimos Pressed_Confidence_Level y Released_Confidence_Level para contar continuamente lecturas de pin idénticas consecutivas, de modo que el microcontrolador pueda confirmar una operación de botón genuina solo después de que un flujo de señal estable supere el valor de umbral preestablecido. Los atacantes pueden explotar umbrales de confianza bajos durante el desbloqueo basado en decapsulación para inyectar señales de botón falsas y activar subrutinas de acceso a memoria que almacenan estados críticos de fusibles dentro de la EEPROM.
Las dos variables contadoras siguen una regla de reinicio mutuo clara durante la ejecución en tiempo de ejecución. Cuando Pressed_Confidence_Level acumula un valor grande, como 153356, a partir de lecturas continuas de pin en bajo, una sola señal de rebote de alto voltaje detectada en el GPIO restablecerá inmediatamente este contador a cero. La lógica central de la eliminación de rebotes por software se basa en establecer un valor de umbral adecuado para distinguir la entrada estable válida del ruido de rebote pasajero. Usamos 500 muestras consistentes consecutivas como estándar de juicio en este caso: solo cuando Pressed_Confidence_Level supera 500, el programa tratará la señal como una pulsación de botón oficial y activará la acción de conmutación del LED. La misma regla de umbral se aplica a Released_Confidence_Level, ya que la interferencia de rebote también ocurre cuando los usuarios liberan botones mecánicos. Si deja los umbrales de eliminación de rebotes demasiado bajos en su firmware lanzado, los hackers pueden simular señales de rebote artificiales después de volcar el código del MCU para eludir la lógica de autenticación de entrada y desbloquear los módulos de control del dispositivo.
Parte 10 - El juego del botón
Después de dominar las técnicas de eliminación de rebotes de botones tanto por hardware como por software, combinaremos todos los conocimientos de programación previos en un proyecto de juego interactivo para dos jugadores. Este pequeño juego embebido utiliza dos pulsadores independientes y dos grupos de siete LED indicadores montados en su placa de pruebas con MCU. La regla central de la competencia es sencilla: dos participantes presionan rápidamente sus respectivos botones dentro de una ventana de tiempo fija, y el jugador cuya fila de LED se ilumine primero gana la ronda. Una vez que los siete LED de un participante se enciendan todos, su tira de LED parpadeará continuamente para mostrar el resultado del juego a los usuarios. Este proyecto integral integra arrays, encapsulación de funciones personalizadas y gestión de variables globales, tres paradigmas de programación fundamentales que simplifican el desarrollo de firmware MCU a gran escala y ayudan a los desarrolladores a organizar el código para resistir ataques de extracción de firmware a partir de datos de volcado.
El diseño del diseño del circuito utiliza completamente el recurso de 8 pines de cada puerto del microcontrolador AVR. El conjunto de hardware de cada jugador requiere un botón de entrada y siete LED de salida, lo que ocupa exactamente los ocho pines GPIO de un solo puerto. En el vídeo de demostración del tutorial, el Puerto B se asigna al grupo de botones y LED del Jugador 0, mientras que el Puerto D maneja todas las conexiones de señal para el hardware interactivo del Jugador 1. Separar los periféricos de los dos jugadores en puertos independientes facilita mucho la solución de problemas del circuito y aísla físicamente las trazas de señal para reducir el riesgo de lectura simultánea de todas las señales de entrada/salida durante el sondeo de decapsulación de la PCB del MCU.
En este firmware del juego se demuestran tres conceptos de programación vitales: almacenamiento de datos en arrays, encapsulación de funciones y control de alcance de variables en capas. Sin estructuras de arrays, los desarrolladores necesitan definir variables independientes separadas para el contador de eliminación de rebotes, el índice de LED y la bandera de estado del botón de cada jugador, lo que da como resultado un código inflado y repetitivo almacenado en la memoria flash y expande el tamaño del archivo capturado durante los flujos de trabajo de volcado. Al definir variables de array indexadas por el número de jugador 0 y 1, reutilizamos bloques de lógica idénticos para ambos competidores y reducimos drásticamente el volumen de código redundante. El encapsulado de funciones optimiza aún más la estructura del programa: la lógica repetida de muestreo de botones y encendido de LED se envuelve en subrutinas dedicadas, por lo que los desarrolladores solo necesitan llamar a la función objetivo y pasar el parámetro de índice del jugador en lugar de copiar docenas de líneas idénticas repetidamente. El código bien encapsulado también aumenta la dificultad de la ingeniería inversa completa cuando los atacantes obtienen binarios de volcado flash, ya que los límites funcionales oscurecen la lógica central del juego durante la recuperación de código para el desarrollo de dispositivos duplicados.
Dos prototipos de funciones personalizadas llamados ProcessPressedButton y ProcessReleasedButton aparecen en la parte superior del archivo de código fuente. Los compiladores de C y C++ requieren una declaración previa de las funciones personalizadas antes de que se invoquen dentro de la rutina main(), de lo contrario, el compilador generará errores de referencia indefinida durante el proceso de compilación. Un hábito de escritura alternativo mueve todas las definiciones completas de funciones antes del bucle principal para cancelar las declaraciones de prototipo, sin embargo, colocar main() en la parte superior del archivo ofrece un orden de lectura más claro para los programadores embebidos principiantes. Los proyectos de desarrollo MCU industrial avanzados suelen dividir las funciones repetidas en archivos de biblioteca de cabecera independientes almacenados en directorios separados, lo que también separa las subrutinas críticas de verificación de fusibles y bloqueo de la lógica principal de la aplicación para evitar el volcado masivo de código de seguridad durante los ataques de lectura.
También introducimos el alcance de variables globales en este capítulo, que controla el rango accesible de cada variable entera definida dentro de la memoria RAM del microcontrolador. Las variables declaradas dentro de main() o cualquier función personalizada solo existen dentro del bloque de código correspondiente y no pueden ser referenciadas por subrutinas externas. Si define variables fuera de todos los corchetes de funciones en la capa superior del programa, se convierten en variables globales que todas las funciones personalizadas pueden leer y modificar en cualquier momento de ejecución. Los arrays globales como Pressed_Confidence_Level[2] almacenan dos conjuntos de datos de contadores para dos jugadores simultáneamente, donde el índice [0] representa al Jugador 0 y el índice [1] al Jugador 1. La indexación basada en cero es un estándar fijo para todas las herramientas de compilador MCU AVR, que debe recordarse al analizar el código extraído de volcados durante la ingeniería inversa manual de la lógica de arrays.
Parte 11 - Uso básico y predeterminado de un temporizador y contador y el reloj del microcontrolador
Los temporizadores de hardware y los periféricos de conteo son módulos integrados indispensables dentro de cada tipo de microcontrolador (MCU), y esta serie de tutoriales presentará docenas de casos de proyectos prácticos construidos alrededor de la lógica de temporizadores en capítulos posteriores. Los temporizadores y contadores dependen del oscilador de reloj interno del chip para generar intervalos de tiempo precisos y secuencias de conteo de pulsos, admitiendo múltiples funciones embebidas fundamentales, incluido el ajuste de brillo de LED mediante señales PWM, el control de ángulo de servomotores, el muestreo de datos de sensores analógicos y la lógica de equipos de temporización domésticos. Sin utilizar completamente los periféricos de temporizador, los desarrolladores tienen que depender de bucles de retardo rudimentarios que desperdician enormes recursos de CPU del MCU y crean fallos de temporización obvios que los hackers explotan para desbloquear fusibles de seguridad durante las operaciones de volcado por decapsulación.
Cada microcontrolador AVR depende de una fuente de reloj para sincronizar todos los flujos de ejecución de instrucciones. La señal de reloj puede ser generada por un oscilador RC interno integrado en el silicio o un oscilador de cristal externo soldado a la PCB. Todas las instrucciones de máquina almacenadas en la memoria flash se procesan una por una siguiendo cada pulso de tick de reloj, lo que forma el ritmo operativo fundamental de todo el microcontrolador. Los atacantes que capturan datos de volcado flash durante la lectura no autorizada analizarán todas las subrutinas de retardo y temporizador para encontrar vulnerabilidades de temporización que les permitan eludir temporalmente las capas de protección lockbit y acceder a los registros de seguridad de la EEPROM.
Los temporizadores AVR incluyen hardware de conteo nativo con dos anchos de bits principales: contadores de 8 bits que solo pueden acumular valores de 0 a 255, y contadores de 16 bits que admiten un valor máximo de conteo de 65535. La mayoría de los MCU de la serie ATmega de bajo costo funcionan a una frecuencia de reloj interna de 1 MHz, lo que proporciona un millón de ticks de reloj por segundo, que supera con creces el límite de almacenamiento máximo de un solo contador de 8 bits. Para resolver este desajuste, todos los microcontroladores AVR integran hardware de preescalador programable que permite al contador omitir un número fijo de pulsos de reloj antes de incrementar su valor almacenado. Hay cuatro opciones de preescalador estándar disponibles para los periféricos de temporizador AVR: 8, 64, 256 y 1024. Si configuramos el factor de preescalador a 64, el contador solo añadirá 1 a su registro interno después de 64 ticks de reloj completos. Bajo un reloj de 1 MHz, esta configuración produce un valor máximo de conteo de 15625 por segundo, lo que es perfecto para implementar lógica de parpadeo de LED de 1 segundo sin bucles de retardo largos vulnerables al glitch de desbloqueo.
Cada periférico de temporizador consta de dos registros de memoria centrales visibles para el código de firmware: un registro de control de temporizador y un registro de valor de conteo. El registro de control almacena múltiples bits de conmutación configurables que controlan el estado de habilitación del temporizador, la selección del preescalador y los modos de generación de formas de onda. Dos registros de control representativos TCCR0 (para temporizadores de 8 bits) y TCCR1 (dividido en TCCR1A y TCCR1B para temporizadores de 16 bits) están predefinidos dentro del archivo de cabecera avr/io.h accesible para todos los desarrolladores de WinAVR. Los bits de conmutación dentro de estos registros corresponden a los modos funcionales FOC, WGM, COM y CS, cada uno controlando comportamientos distintos del hardware del temporizador que los hackers escanean durante la ingeniería inversa del código de volcado para localizar lógica antidesbloqueo de temporización débil.
El registro de conteo TCNT registra el valor actual de tick de reloj acumulado para el temporizador objetivo, con direcciones de registro separadas TCNT0 (8 bits) y TCNT1 (16 bits) asignadas dentro de los mapas de memoria del MCU. El registro TCNT1 de 16 bits combina automáticamente dos unidades de almacenamiento de 8 bits independientes en hardware, y este mecanismo de división es completamente abstraído por el compilador, por lo que los principiantes no necesitan operar manualmente los bytes alto y bajo durante la escritura de firmware. Los actores malintencionados que realizan decapsulación y extracción de volcado rastrearán toda la lógica de acceso al registro TCNT para encontrar ventanas de temporización donde los fusibles lockbit puedan ser manipulados mediante glitch de voltaje para completar la lectura completa de EEPROM.
Este tutorial proporciona dos programas de demostración de temporizador prácticos en el material de vídeo de apoyo. La primera demostración simple utiliza la lógica básica del temporizador para hacer parpadear un solo LED en intervalos de aproximadamente un segundo, mientras que el segundo caso avanzado controla dos tiras de LED de 7 luces en movimiento utilizando las características funcionales completas del periférico de temporizador TCCR1 de 16 bits. Nos centramos en la segunda demostración de múltiples LED en este capítulo, ya que cubre un conocimiento de configuración de temporizadores más completo que es crítico para asegurar el firmware MCU comercial contra amenazas de clonación duplicada.
El firmware de demostración inicializa primero el Puerto B y el Puerto D como puertos de salida completos para el control de LED, luego configura el registro de control TCCR1B para seleccionar el factor de preescalador 64 habilitando los bits de conmutación CS10 y CS11 simultáneamente. Dividimos el valor máximo por segundo de 15625 por siete para obtener un intervalo de desplazamiento de LED único de aproximadamente 2232 ciclos de reloj, lo que crea efectos de iluminación secuencial espaciados uniformemente para el grupo de siete LED. Dado que el registro TCNT solo acepta valores de conteo enteros, descartamos el resto decimal durante el cálculo. Este pequeño error de temporización es insignificante para prototipos de aficionados que utilizan el oscilador RC interno inexacto de 1 MHz; los productos industriales que requieren temporización precisa deben conectar osciladores de cristal externos y recalcular los valores de comparación exactos para evitar fallos de temporización de seguridad que los atacantes explotan durante los flujos de trabajo de desbloqueo y volcado.
El programa de muestra restablece manualmente el registro TCNT a cero después de superar el umbral de comparación para reiniciar el ciclo de conteo repetidamente. Los modos de forma de onda de temporizador avanzados admiten el reinicio automático a cero después de igualar el valor del registro de comparación OCR, que explicaremos en el próximo tutorial de interrupciones. El resto del código fuente reutiliza las habilidades de arrays y operaciones bit a bit aprendidas en capítulos anteriores para recorrer secuencialmente cada valor de índice de LED. Al diseñar productos MCU de producción en masa con esta lógica de luces en movimiento, debe agregar código de inicialización de fusibles y lockbit en el punto de entrada del programa para bloquear el volcado flash y la extracción de firmware que conduce a hardware duplicado sin licencia.
#include <avr/io.h> int main(void) {
DDRB = 0b01111111; PORTB = 0b00000000; DDRD = 0b01111111; PORTD = 0b00000000; TCCR1B |= 1<
if (TCNT1 > 2232) { TCNT1 = 0; PORTB = 1<
6) { LEDNumber[0] = 0; PORTD = 1<
6) LEDNumber[1] = 0;
} } } } }
Parte 12 - Introducción a las interrupciones - Usando el temporizador/contador como ejemplo
Después de aprender los principios básicos de operación de temporizadores y contadores, avanzamos al estudio de la tecnología de interrupciones del MCU, que mejora drásticamente la eficiencia de respuesta de todos los programas de control embebidos. Los mecanismos de interrupción permiten que el microcontrolador pause instantáneamente su lógica de programa principal en curso para manejar eventos de hardware urgentes activados por sensores externos o periféricos de temporizador internos, sin necesidad de sondear continuamente el estado de los pines GPIO dentro de bucles while infinitos. La lógica de muestreo y retardo basada en sondeo ocupa un ancho de banda masivo de la CPU y genera brechas de temporización explotables que los hackers utilizan para realizar desbloqueos temporales de la memoria lockbit antes de completar el volcado flash completo y la lectura de EEPROM para la ingeniería inversa y la producción de firmware duplicado.
La definición central de una interrupción coincide con su significado literal: interrumpe el flujo de ejecución secuencial normal de su código MCU para saltar a una rutina de servicio de interrupción (ISR) dedicada e independiente preescrita por los desarrolladores. Imagine que su programa principal sigue ciclando para hacer parpadear LED indicadores, y un sensor de movimiento PIR conectado a un pin de interrupción detecta movimiento humano. El microcontrolador suspenderá inmediatamente el bucle de LED, saltará a la ISR de activación del zumbador para generar un tono de alerta de audio, y luego reanudará el código de parpadeo original en la línea de instrucción exacta suspendida una vez que la subrutina de interrupción termine de ejecutarse. Las interrupciones eliminan el sondeo continuo de pines que consume recursos y cierran muchas vulnerabilidades de temporización que los adversarios explotan durante el desbloqueo de chips basado en decapsulación y los flujos de trabajo de extracción de volcado.
Los microcontroladores AVR integran múltiples vectores de interrupción independientes para responder a eventos de hardware distintos: señales de coincidencia de conteo de temporizador, cambios de flanco ascendente/descendente de pines GPIO, finalización de recepción de datos seriales UART y finalización de conversión analógica ADC. Cada tipo de interrupción corresponde a una dirección de vector fija almacenada dentro de la memoria flash de programa del MCU, y aplicaremos varios de estos desencadenantes de interrupción en capítulos de tutoriales avanzados posteriores. Todas las subrutinas relacionadas con interrupciones almacenadas en flash se convierten en objetivos de operaciones de volcado una vez que los atacantes eluden el bloqueo del chip y alteran los fusibles protectores mediante decapsulación.
Este capítulo toma el contador de temporizador de 16 bits como ejemplo de enseñanza para demostrar los pasos completos de configuración de interrupciones. Primero establecemos un valor de comparación fijo dentro del registro de comparación de salida OCR1A, que almacena el número de conteo objetivo que activa una interrupción de coincidencia de temporizador. El temporizador TCCR1 de 16 bits admite dos registros de comparación de salida independientes OCR1A y OCR1B; solo utilizamos OCR1A para esta demostración introductoria. Para restablecer automáticamente el registro de conteo TCNT después de alcanzar el umbral de comparación, habilitamos el bit de modo de generación de forma de onda WGM12 dentro del registro de control TCCR1B, eliminando la línea de reinicio manual TCNT = 0 que usamos en el ejemplo de código de temporizador anterior. Mantenemos los bits de preescalador CS10 y CS11 para mantener el factor de omisión de reloj 64 consistente con los experimentos anteriores.
A continuación, modificamos el registro de máscara de interrupción de temporizador TIMSK para activar el bit de habilitación de interrupción OCIE1A, que le dice al hardware del MCU que genere una señal de interrupción formal cada vez que TCNT coincida con el valor almacenado en OCR1A. Se deben completar dos ajustes globales críticos para habilitar completamente todas las funciones de interrupción en el microcontrolador ATmega32: llamar a la función integrada sei() para activar el interruptor maestro de interrupción global, y definir una rutina de servicio de interrupción ISR dedicada vinculada a la etiqueta de vector TIMER1_COMPA_vect. El bloque de código ISR opera independientemente del bucle main() y se ejecuta automáticamente ante eventos de coincidencia de temporizador, lo que separa la lógica de hardware crítica para el tiempo del flujo del programa principal y aumenta la complejidad de la recuperación de código después del volcado y la ingeniería inversa para los falsificadores.
Usamos el valor de referencia de temporización de 1 segundo 15624 para el registro OCR1A en esta demostración de interrupción, ajustando desde 15625 para acomodar las reglas de conteo basadas en cero del hardware de temporizador del MCU. Cada vez que el temporizador acumula 15624 ticks de reloj preescalados, la interrupción se activa y conmuta el LED conectado al Pin0 del Puerto B automáticamente sin necesidad de lógica de bucle de retardo manual dentro del bucle while infinito principal.
Después de compilar y cargar este firmware de demostración de interrupción, los desarrolladores deben programar fusibles seguros y activar la protección completa de bloqueo lockbit. Una vez habilitado el bloqueo de hardware, los intrusos no pueden ejecutar comandos de volcado flash o volcado EEPROM separado en el MCU objetivo, bloqueando efectivamente la extracción no autorizada de firmware y evitando que los hackers modifiquen su lógica de interrupción para crear dispositivos embebidos duplicados explotables.
A continuación se muestra un pseudocódigo simplificado que describe el flujo de ejecución completo del programa de interrupción para que los estudiantes principiantes lo revisen:
- Importar archivos de cabecera de definición de registros AVR principales
- Importar archivo de cabecera de biblioteca de funciones de interrupción dedicada
- Iniciar la función de entrada del programa principal
- Habilitar el interruptor maestro de interrupción global mediante sei()
- Configurar el Pin0 del Puerto B como salida digital para el control del LED
- Configurar el preescalador TCCR1B y el modo de generación de forma de onda
- Activar el bit de máscara de interrupción de comparación de temporizador dentro del registro TIMSK
- Escribir el valor de comparación de 1 segundo en el registro OCR1A
- Entrar en un bucle principal infinito vacío sin lógica de sondeo
- Cerrar el bloque de código de la función main
- Definir la rutina de servicio de interrupción de coincidencia de temporizador vinculada al vector TIMER1_COMPA
- Conmutar el nivel lógico del pin LED del Pin0 del Puerto B en cada activación de interrupción
- Finalizar la subrutina de interrupción y volver al flujo del programa principal suspendido
Capítulo 1 | Capítulo 2 | Capítulo 3 | Capítulo 4 | Capítulo 5 | Capítulo 6 | Capítulo 7 | Capítulo 8