¿Cómo puede un lenguaje que no tiene objetos heredar propiedades? La respuesta está en cómo el compilador acomoda los datos en la memoria RAM. Si colocas un `struct Base` como el **primer elemento exacto** de un `struct Derivado`, el compilador garantiza que ambas estructuras compartirán la misma dirección de inicio en memoria.
Esto significa que puedes tomar un puntero al objeto Derivado (Caudalímetro), hacerle un "Casteo Fuerte" (Base*), y pasárselo a una función genérica. ¡El compilador se lo tragará perfectamente sin corromper la memoria!
El Polimorfismo significa "muchas formas". Quieres que todos los sensores tengan un método llamado `Actualizar()`, pero que la forma en que lo hagan sea diferente. En C, agregamos un **Puntero a Función** dentro del `struct Base`. Al crear el objeto específico (ej. Termómetro), conectamos ese puntero a la función real `Leer_ADC()`. Así, el Súper Bucle solo llama a `sensor->Actualizar()`, y el lenguaje enruta la llamada dinámicamente a la función correcta de hardware.
Ejercicio 1: Vamos a aplicar OOP al Hardware. Crearemos una clase genérica Sensor_t. Luego derivaremos dos sensores específicos: SensorCaudal_t (que usa timers/EXTI) y SensorTemperatura_t (que usa ADC). Construiremos un Arreglo de Punteros en el Súper Bucle para recorrerlos todos ciegamente, invocando sus funciones de hardware específicas mediante el poder de los punteros a función.
/**
* @file oop_sensors.c
* @brief Implementación de OOP, Herencia y Polimorfismo en C Bare-Metal
*/
#include "stm32f401xe.h"
#include <stdint.h>
// =========================================================================
// 1. LA CLASE BASE (Sensor Genérico)
// =========================================================================
// Usamos un typedef adelantado para poder referenciar el puntero en la misma estructura
typedef struct Sensor_Base_t Sensor_Base_t;
struct Sensor_Base_t {
uint32_t id_nodo;
float valor_actual;
// MÉTODO VIRTUAL (Polimorfismo)
// Un puntero a una función que recibe su propio objeto como argumento (simulando el 'this' de C++)
void (*Metodo_Actualizar)(Sensor_Base_t *self);
};
// =========================================================================
// 2. CLASES DERIVADAS (Herencia Estructural)
// =========================================================================
// ---> Derivado 1: Sensor de Temperatura (Vía ADC)
typedef struct {
// REGLA DE ORO DE LA HERENCIA C: La Base DEBE ser el primer miembro absoluto.
Sensor_Base_t super;
// Propiedades exclusivas de este hijo
uint8_t canal_adc;
float offset_calibracion;
} Sensor_TempADC_t;
// ---> Derivado 2: Caudalímetro (Vía Interrupciones/Timers)
typedef struct {
Sensor_Base_t super;
// Propiedades exclusivas de este hijo
uint32_t *registro_hardware_timer;
float factor_k_litros;
} Sensor_Caudal_t;
// =========================================================================
// 3. IMPLEMENTACIÓN DE MÉTODOS ESPECÍFICOS DEL HARDWARE
// =========================================================================
// Función real de Hardware para Temperatura
void Leer_Temperatura_Hardware(Sensor_Base_t *self) {
// 1. "Downcasting" explícito (Revertimos el polimorfismo para acceder a las propiedades del hijo)
Sensor_TempADC_t *pt100 = (Sensor_TempADC_t *)self;
// 2. Ejecutamos la lógica específica (Ej. Lectura simulada del registro ADC)
uint32_t lectura_cruda = ADC1->DR; // (En un código real, pedirías la conversión del canal pt100->canal_adc)
float grados = (lectura_cruda * 0.08f) + pt100->offset_calibracion;
// 3. Guardamos el resultado en la Base para que el resto del sistema lo vea
pt100->super.valor_actual = grados;
}
// Función real de Hardware para Caudal
void Leer_Caudal_Hardware(Sensor_Base_t *self) {
Sensor_Caudal_t *caudal = (Sensor_Caudal_t *)self;
// Leer el registro de cuenta física del Timer directamente
uint32_t pulsos_totales = *(caudal->registro_hardware_timer);
// Matematica específica del caudalímetro
caudal->super.valor_actual = (float)pulsos_totales / caudal->factor_k_litros;
}
// =========================================================================
// 4. EL SÚPER BUCLE POLIMÓRFICO
// =========================================================================
int main(void) {
// INSTANCIACIÓN DE OBJETOS
// Creamos el Sensor Termómetro y lo configuramos (El Constructor)
Sensor_TempADC_t mi_termometro;
mi_termometro.super.id_nodo = 101;
mi_termometro.super.valor_actual = 0.0f;
mi_termometro.canal_adc = 5; // Pin PA5
mi_termometro.offset_calibracion = -1.2f;
// BINDING POLIMÓRFICO: Le conectamos su función real al puntero base
mi_termometro.super.Metodo_Actualizar = Leer_Temperatura_Hardware;
// Creamos el Sensor de Litros (Caudalímetro)
Sensor_Caudal_t mi_medidor_flujo;
mi_medidor_flujo.super.id_nodo = 201;
mi_medidor_flujo.super.valor_actual = 0.0f;
mi_medidor_flujo.registro_hardware_timer = (uint32_t *)&(TIM2->CNT);
mi_medidor_flujo.factor_k_litros = 100.0f; // 100 pulsos = 1 litro
mi_medidor_flujo.super.Metodo_Actualizar = Leer_Caudal_Hardware;
// EL PODER DEL POLIMORFISMO: El Arreglo Genérico
// Guardamos las direcciones de nuestros hijos distintos en una lista de punteros de clase Base.
// Esto es seguro gracias a que 'super' es el primer elemento de la memoria.
Sensor_Base_t *lista_sensores[2];
lista_sensores[0] = (Sensor_Base_t *)&mi_termometro;
lista_sensores[1] = (Sensor_Base_t *)&mi_medidor_flujo;
while(1) {
// ¡Magia! El Súper Bucle ya no sabe ni le importa cuántos sensores hay,
// ni si son I2C, UART, ADC o Timers. Solo los recorre e invoca la actualización.
for (int i = 0; i < 2; i++) {
// Esta llamada invoca 'Leer_Temperatura_Hardware' en la vuelta 0
// y 'Leer_Caudal_Hardware' en la vuelta 1.
// El propio objeto provee su función oculta.
lista_sensores[i]->Metodo_Actualizar( lista_sensores[i] );
// Y luego podemos usar los datos genéricamente:
// if (lista_sensores[i]->valor_actual > LIMITE) { ALARMA! }
}
}
return 0;
}
Objetivo del día: Escalabilidad Industrial (Open/Closed Principle).
¿Qué pasa si mañana el cliente de tu gasolinera te exige que tu controlador maneje 4 mangueras al mismo tiempo, en lugar de 1? Y no solo eso, te exige que una manguera sea de Flujo Másico (por RS485) y las otras tres sean de Pulsos (por Timers). En una programación procedimental "espagueti", tu archivo main.c colapsaría intentando manejar miles de condicionales if (tipo_manguera == MASICO)... else if (tipo == PULSOS)... para cada lectura de datos y cada cálculo en la pantalla LCD.
Al introducir **OOP en C Bare-Metal**, has "encapsulado" el problema. Creaste la clase base `Dispensador`, y la heredaste para crear el `Dispensador_Masico` y el `Dispensador_Pulsos`. El Súper Bucle de tu máquina se quedó inmaculado. Solo tienes un arreglo maestro de `Dispensadores`, e invocas `dispensadores[i]->Manejar_Venta()`. Si quieres agregar una 5ta manguera de otro fabricante, no modificas el núcleo de tu Tesis; simplemente creas un nuevo archivo `.c` con un nuevo "objeto hijo" y lo conectas a la lista maestra. Esto es arquitectura de clase mundial.
struct Base como el **primer elemento** de un struct Derivado. El estándar de C garantiza que no hay relleno (padding) al inicio de una estructura, asegurando que ambos compartan exactamente la misma dirección física en la RAM.Sensor *self). Dentro de la función específica, este puntero se realiza un Downcast de regreso a la clase derivada para acceder a las propiedades exclusivas del hardware.