Un teclado matricial de 4x4 tiene 16 botones mecánicos, pero solo 8 cables de salida. Internamente, los botones no están conectados a GND o VCC; están conectados en intersecciones. Hay 4 cables de Filas (Rows) y 4 cables de Columnas (Columns). Un botón simplemente actúa como un puente temporal entre una fila y una columna.
El algoritmo de escaneo consiste en:
Si configuras las salidas de las Filas en modo Push-Pull (Inyectando 3.3V o 0V de manera forzada), corres un peligro enorme. ¿Qué pasa si el cliente presiona el botón '1' (Fila 1) y el botón '4' (Fila 2) exactamente al mismo tiempo y están en la misma columna? Crearás un cortocircuito directo entre el pin de la Fila 1 (que está inyectando 3.3V) y el pin de la Fila 2 (que está drenando a 0V), destruyendo los pines del STM32.
La solución Bare-Metal es configurar las Filas en modo Open-Drain (Drenador Abierto). En este modo, el pin solo sabe "jalar" hacia 0V. Cuando le escribes un `1`, el pin no inyecta voltaje, simplemente se desconecta (Alta impedancia). Así, si presionan dos botones, un pin a 0V se conecta con un pin desconectado... ¡Cero cortocircuitos!
Ejercicio 1: Integraremos el teclado al Puerto B de tu microcontrolador. Las Filas serán salidas (PB4 a PB7) configuradas expresamente como Open-Drain. Las Columnas serán entradas (PB0 a PB3) configuradas con Pull-Up interno. Construiremos la Máquina de Estados para escanear y hacer el "Debounce" (Antirrebote mecánico) de 20 milisegundos sin utilizar la función delay().
/**
* @file keypad_async.c
* @brief Escaneo de Teclado Matricial 4x4 (No Bloqueante / Open-Drain)
*/
#include "stm32f401xe.h"
// Variable global de tiempo (Del SysTick, Día 98)
extern volatile uint32_t milisegundos_globales;
// Mapa de caracteres de la matriz
const char keypad_map[4][4] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
void inicializar_teclado_matricial(void) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
// --- COLUMNAS (Entradas PB0 a PB3) ---
// Limpiamos los bits para forzar el modo entrada (00)
GPIOB->MODER &= ~(0xFF << 0);
// Habilitamos Pull-Up interno (01) en PB0, PB1, PB2, PB3
GPIOB->PUPDR &= ~(0xFF << 0);
GPIOB->PUPDR |= (0x55 << 0);
// --- FILAS (Salidas PB4 a PB7) ---
// Modo Salida General (01)
GPIOB->MODER &= ~(0xFF << (4*2));
GPIOB->MODER |= (0x55 << (4*2));
// IMPORTANTE: Modo OPEN-DRAIN para las filas (Evita cortocircuitos)
GPIOB->OTYPER |= (GPIO_OTYPER_OT4 | GPIO_OTYPER_OT5 | GPIO_OTYPER_OT6 | GPIO_OTYPER_OT7);
// Por defecto, apagar todas las filas (Alta impedancia en Open-Drain escribiendo 1)
GPIOB->BSRR = (0xF << 4);
}
/**
* @brief Cambia el estado de una fila específica a GND (0). Las demás a Hi-Z (1).
*/
void activar_fila(uint8_t fila) {
// Apagamos todas primero (Escribir 1 en BSRR Set)
GPIOB->BSRR = (0xF << 4);
// Activamos solo la solicitada (Escribir 1 en BSRR Reset, lo que pone un 0 físico)
GPIOB->BSRR = (1 << ((fila + 4) + 16));
}
/**
* @brief Máquina de Estados del Teclado (No Bloqueante)
* Debe llamarse en cada ciclo del Súper Bucle.
* @return char Retorna el caracter si hubo una pulsación válida, de lo contrario 0.
*/
char escanear_teclado_async(void) {
static uint8_t fila_actual = 0;
static uint32_t tiempo_debounce = 0;
static char tecla_candidata = 0;
static uint8_t estado = 0; // 0=Escaneando, 1=Debounce Presionado, 2=Esperando liberación
switch(estado) {
case 0: // ESCANEO RÁPIDO
activar_fila(fila_actual);
// Le damos unos ciclos de reloj para que el voltaje eléctrico baje físicamente a cero
__NOP(); __NOP(); __NOP();
// Leemos el puerto B y aislamos los primeros 4 bits (Columnas)
uint16_t lecturas = GPIOB->IDR & 0x0F;
// Si la lectura NO es 0x0F (1111), significa que un 0 se coló (Botón presionado)
if (lecturas != 0x0F) {
// Buscamos cuál columna es la responsable
for (int col = 0; col < 4; col++) {
if ((lecturas & (1 << col)) == 0) {
tecla_candidata = keypad_map[fila_actual][col];
tiempo_debounce = milisegundos_globales;
estado = 1; // Pasamos a validar el rebote mecánico
break;
}
}
} else {
// Si no hay nada presionado, saltamos a la siguiente fila para la próxima iteración
fila_actual = (fila_actual + 1) & 3; // Bitwise equivalente a % 4
}
break;
case 1: // DEBOUNCE (Esperar 20ms sin bloquear el CPU)
if ((milisegundos_globales - tiempo_debounce) >= 20) {
// Volvemos a leer. Si sigue en 0, es una pulsación real y no estática.
if ((GPIOB->IDR & 0x0F) != 0x0F) {
estado = 2; // Esperar a que suelte el botón
return tecla_candidata; // ¡Disparo exitoso!
} else {
estado = 0; // Falsa alarma (Ruido)
}
}
break;
case 2: // ESPERAR LIBERACIÓN (Anti-Repetición)
if ((GPIOB->IDR & 0x0F) == 0x0F) {
// Todos los botones se han soltado
tiempo_debounce = milisegundos_globales;
estado = 3; // Debounce de liberación
}
break;
case 3: // DEBOUNCE LIBERACIÓN (20ms)
if ((milisegundos_globales - tiempo_debounce) >= 20) {
estado = 0; // Máquina reseteada, lista para un nuevo escaneo
}
break;
}
return 0; // Ninguna pulsación completada en este ciclo
}
int main(void) {
// SysTick_Init();
inicializar_teclado_matricial();
while(1) {
// El teclado escanea una fracción de la matriz por cada ciclo de reloj.
// No cuesta recursos y no utiliza la prohibida función delay().
char tecla = escanear_teclado_async();
if (tecla != 0) {
// procesar_tecla_presionada(tecla);
// Ej: Si presiona 'A', se activa Despacho por Importe ($200.00 MXN)
}
// ... Tareas de caudalímetro y telemetría siguen corriendo al 100%
}
return 0;
}
Objetivo del día: Interacción Industrial Segura y Tolerancia a Fallos de Usuario.
Un usuario impaciente podría presionar el botón de "$500" y la tecla "Aceptar" al mismo tiempo. En un equipo mal diseñado, eso generaría un cortocircuito quemando los pines del microcontrolador. Al diseñar la etapa de filas del STM32 con arquitectura Open-Drain, tu hardware es ahora inmune al pánico del usuario y a los teclados de membrana defectuosos que cierran circuitos incorrectos por desgaste de la mica interna.
Aún más importante, el algoritmo del Despachador de Gas LP requiere leer pulsos de la turbina magnética con el EXTI cada pocos microsegundos. Si hubieras usado una librería de Arduino estándar para el teclado (que usa `delay()` para el debounce mecánico), el microcontrolador se quedaría ciego durante 20 milisegundos completos cada vez que el despachador tocara un botón. Con tu nueva **Máquina de Estados de 4 fases**, la matriz se escanea de forma 100% asíncrona. El usuario siente una interfaz responsiva, y el contador metrológico no pierde ni un solo mililitro de gas.