← Índice del curso

Sistemas Embebidos · Tema 10

Capas de abstracción: HAL y drivers

En el tema anterior escribiste registros uno por uno. Hacerlo en todo el programa es una trampa: el día que cambie el chip, o que quieras probar la lógica en tu PC, habrás pegado la aplicación entera al hardware. Una buena capa de abstracción es lo que separa lo que el producto hace de la máquina concreta que lo hace.

01 · Punto de partida

El problema del código pegado al chip

Imaginá un termostato cuyo main() contiene, mezcladas, la lógica («si la temperatura baja de la consigna, encender el calefactor») y las operaciones de hardware (GPIOC_BSRR = 1u << 29;, USART1_DR = c;). Funciona. Pero ese programa tiene tres problemas ocultos:

  • No se puede cambiar de chip. Si el chip se discontinúa o el costo manda un cambio de familia (tema 6), hay que revisar cada línea del programa para encontrar las que tocan el hardware.
  • No se puede probar sin la placa. La lógica no corre en tu PC porque cada decisión está atada a un registro que no existe ahí (tema 5).
  • No se entiende. Quien lee GPIOC_BSRR = 1u << 29; tiene que reconstruir en la cabeza que eso significa «encender el LED de estado».

La solución es una idea vieja de la ingeniería de software: poner una interfaz entre lo que cambia por razones distintas. La lógica del producto cambia cuando cambian los requisitos; el acceso al hardware cambia cuando cambia el chip. Si se mezclan, cualquiera de los dos cambios obliga a tocar todo.

02 · Estructura

La pila de capas y su regla

En el tema 4 dimos un primer vistazo a las capas de un firmware. Ahora las miramos como un diseño: cada una ofrece una interfaz a la superior y se apoya en la inferior.

Pila de capas de un firmware Cinco capas apiladas: aplicación, servicios y bibliotecas, drivers de dispositivo, capa de abstracción de hardware y hardware. Las flechas apuntan hacia abajo e indican que cada capa solo depende de la inferior. Un recuadro lateral indica qué cambia cuando se cambia el chip o cuando se cambia un componente externo. Aplicaciónla lógica del producto Servicios y bibliotecascomunicaciones, archivos, control PID Drivers de dispositivosensor, pantalla, módem: usan la HAL HALgpio, uart, spi, i2c, timer, adc Hardware: registros del chip Cambia el chip →se reescribe la HAL Cambia un componente →se reescribe su driver Cambia el producto →se toca la aplicación

La regla que hace útil a toda la pila es una sola: las dependencias apuntan hacia abajo. La aplicación puede llamar a un servicio, a un driver o a la HAL; la HAL nunca llama a la aplicación ni conoce a un driver. Cuando esa regla se cumple:

  • cada capa se prueba por separado, reemplazando la inferior por una versión de prueba;
  • un cambio queda contenido en la capa que corresponde;
  • se pueden reutilizar capas completas en otro producto.

Una forma práctica de vigilarla es mirar los #include. Si un archivo de la aplicación incluye stm32f1xx.h, la capa de arriba conoce los detalles de la de abajo y la separación se rompió. Los archivos de cabecera del fabricante solo deberían aparecer dentro de la HAL.

Otra regla complementaria: no saltarse capas sin motivo. Si la aplicación llama directamente a la HAL en lugar de usar el driver del sensor, ahora conoce qué bus usa el sensor, y cambiarlo exige tocar la aplicación. A veces conviene saltar (para algo trivial como un LED), pero que sea una decisión consciente.

03 · La capa que aísla el chip

La HAL: una interfaz común para los periféricos

La HAL (Hardware Abstraction Layer, capa de abstracción de hardware) es el conjunto de funciones que oculta los registros de un chip detrás de operaciones con nombre: configurar un pin, enviar por una UART, leer un conversor. Quien la usa no necesita saber en qué registro se escribe. Existe en varios niveles, y conviene distinguirlos porque la palabra se usa para todos:

Niveles de abstracción del hardware, de menos a más alto nivel.
NivelEjemploControlPortabilidad
Registros directosGPIOC_BSRR = ...TotalNula
CMSIS (núcleo)Definiciones del núcleo Cortex-M: NVIC_EnableIRQ(), __disable_irq()AltoEntre todos los Cortex-M
Biblioteca de bajo nivel del fabricanteLL de STM32: funciones casi 1 a 1 con los registrosAltoDentro de la familia del fabricante
HAL del fabricanteHAL_GPIO_WritePin(), gpio_put() del SDK de Pico, gpio_set_level() de ESP-IDFMedioDentro de la familia o del SDK
Marcos multiplataformaArduino, mbed, ZephyrMenorMuchas placas y familias
HAL propia del productoled_encender(), consola_enviar()El que defina el diseñoLa que el proyecto necesite

Observá que las HAL de los fabricantes no son portables entre sí: HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, ...) de ST y gpio_put(25, 1) de Raspberry Pi hacen lo mismo con nombres, argumentos y conceptos distintos (puerto y máscara contra número de pin). Si la aplicación llama a una de ellas, queda atada a ese fabricante, aunque no toque un solo registro. Por eso, en un producto serio, conviene una HAL propia, pequeña, que use por dentro la del fabricante: es el mismo patrón del tema 6, ahora con rigor.

El resultado es una interfaz expresada en el vocabulario del producto, no del chip:

// hal_io.h — lo único que la aplicación conoce del hardware
#ifndef HAL_IO_H
#define HAL_IO_H

#include <stdbool.h>

void io_iniciar(void);            // configura relojes, pines y la consola
void led_encender(void);
void led_apagar(void);
bool boton_presionado(void);      // true mientras el botón está apretado (lógica ya corregida)
void consola_enviar(char c);      // envía un carácter; bloquea hasta que se pueda

#endif

Fijate en dos decisiones de diseño. Una, boton_presionado() devuelve «true si está apretado» aunque eléctricamente el botón sea activo en bajo: la inversión de nivel es un detalle del hardware y se resuelve adentro. Dos, el archivo no incluye nada del fabricante: ni tipos de puerto, ni constantes de modo. El hardware no se filtra por la interfaz.

04 · Componentes externos

Drivers de dispositivo

La HAL abstrae el chip. Un driver de dispositivo abstrae un componente externo conectado al chip: un sensor de temperatura, una pantalla, una memoria, un módem. Su trabajo es traducir entre el vocabulario del producto («dame la temperatura en décimas de grado») y el del componente («escribí el puntero de registro 0, leé dos bytes, ajustá el formato de 12 bits»).

La clave es qué usa el driver por debajo: la HAL, nunca los registros del chip. Un driver para un sensor I²C usa funciones genéricas de bus («escribir», «leer») y por eso funciona igual en cualquier microcontrolador que ofrezca ese bus. Un driver de ese tipo es reutilizable entre productos y plataformas sin cambiar una línea.

Una técnica útil para lograrlo es pasarle al driver el bus como un argumento, en forma de una estructura de punteros a función (a veces llamada «inyección de dependencias»). El driver no sabe qué bus es: solo que sabe escribir y leer.

#include <stdint.h>
#include <stddef.h>

typedef struct {
    int (*escribir)(uint8_t dir, const uint8_t *datos, size_t n);   // devuelven 0 si salió bien
    int (*leer)(uint8_t dir, uint8_t *datos, size_t n);
} bus_i2c_t;

#define TMP102_DIR   0x48     // dirección I²C del sensor de temperatura (pin ADD0 a masa)
#define TMP102_TEMP  0x00     // registro de temperatura

// Devuelve 0 y la temperatura en décimas de grado (250 = 25,0 °C); -1 si falló el bus
int tmp102_leer(const bus_i2c_t *bus, int16_t *decimas) {
    uint8_t reg = TMP102_TEMP;
    uint8_t b[2];
    if (bus->escribir(TMP102_DIR, &reg, 1) != 0) return -1;   // apuntar al registro de temperatura
    if (bus->leer(TMP102_DIR, b, 2) != 0) return -1;            // leer sus dos bytes
    int16_t crudo = (int16_t)((b[0] << 8) | b[1]) >> 4;        // dato de 12 bits, justificado a la izquierda
    *decimas = (int16_t)((crudo * 5) / 8);                      // 0,0625 °C por paso = 0,625 décimas
    return 0;
}

Este driver no menciona un solo registro del microcontrolador: solo el sensor (su dirección, el registro de temperatura y su formato de 12 bits con 0,0625 °C por paso). Tiene dos ventajas notables. Funciona en cualquier chip cuya HAL sepa llenar una bus_i2c_t. Y se puede probar en el PC con un «bus falso» que devuelva bytes conocidos (por ejemplo 0x19 0x00, que significa 25,0 °C), sin tener el sensor ni la placa (tema 47).

Es también el lugar adecuado para decisiones que no son de la aplicación: tiempos de espera tras el encendido, comprobación del identificador del chip, reintentos ante un error del bus, conversión de unidades y formato de los datos. La aplicación pide «temperatura» y el driver se encarga de que sea confiable.

05 · Diseñar

Diseñar una buena interfaz

La calidad de una capa se juega en su interfaz. Unas pautas que evitan los errores más comunes:

  • Hablar el idioma del que llama, no del hardware. led_encender() y no gpio_escribir(PUERTO_C, 13, 0). Si el nombre contiene un pin, un registro o un modo, el hardware se está filtrando.
  • Pequeña y completa. Pocas funciones que cubran lo que la capa superior necesita. Cada función extra es algo más que mantener y que ata a los clientes.
  • Sin tipos del fabricante en los encabezados públicos. Si hal_io.h incluye stm32f1xx.h, toda la aplicación arrastra esa dependencia.
  • Un solo responsable por recurso. Un pin o un periférico lo maneja un único módulo. Si dos módulos escriben el mismo registro, los errores de interferencia son difíciles de ver.
  • Errores explícitos. Las funciones que pueden fallar devuelven un código (0 correcto, negativo error) o un tipo enumerado; nunca fallan en silencio. Un bus que no responde es una situación normal que el código debe poder manejar.
  • Documentar el contrato. Si la función bloquea, cuánto puede tardar, si se puede llamar desde una interrupción, qué precondiciones tiene. En firmware, estas propiedades son tan importantes como el resultado (tema 16).
  • Configuración por parámetros, no por magia. Una estructura con los parámetros (velocidad, paridad, pines) en lugar de constantes enterradas en el código.
typedef enum { UART_OK = 0, UART_ERR_PARAM = -1, UART_ERR_OCUPADA = -2 } uart_err_t;

typedef struct {
    uint32_t baudios;        // por ejemplo 115200
    uint8_t  bits_datos;     // 8
    char     paridad;        // 'N', 'E' u 'O'
} uart_config_t;

uart_err_t uart_iniciar(const uart_config_t *cfg);
uart_err_t uart_enviar(const uint8_t *datos, size_t n);   // bloquea; no usar desde una ISR

Una última pauta es la más importante: abstraer donde hay variabilidad real. No hace falta una capa para lo que nunca cambiará; sí la hace falta donde ya sabés que hay dos opciones (dos chips candidatos, un sensor y un simulador) o donde necesitás poder probar. Una abstracción sin variación detrás es solo una indirección más.

06 · Sin almuerzo gratis

El costo de abstraer

Cada capa tiene un precio, y en un microcontrolador pequeño se nota:

  • Tiempo. Una llamada a función agrega decenas de ciclos entre guardar registros, pasar parámetros y volver; si la HAL además verifica parámetros o recorre una estructura de configuración, más. Un cambio de nivel de un pin que tarda 2 ciclos escribiendo el registro puede pasar a 40 con una HAL genérica. Para señales rápidas (un protocolo por software, una interrupción crítica) eso importa.
  • Memoria. Las HAL genéricas de los fabricantes, que soportan todas las opciones, ocupan miles de bytes de flash por periférico aunque uses una sola opción. Una HAL propia, a medida, puede ocupar una fracción.
  • Complejidad. Más archivos, más interfaces, más nombres. Una abstracción mal pensada oculta lo que pasa y complica la depuración.

Las herramientas para pagar menos son conocidas. Las funciones cortas de la HAL se declaran static inline en el encabezado, de modo que el compilador las expande en el lugar de la llamada y el costo en tiempo desaparece. Con la optimización en tiempo de enlace (-flto, tema 7) el compilador puede incluso eliminar las capas intermedias que no hacen nada. Lo que no se elimina es el costo de diseño, y por eso la regla es la del apartado anterior: abstraer lo que varía.

07 · Analogía con programación

La misma aplicación, dos plataformas, en Python

Este programa implementa la aplicación del termostato de juguete (si el botón está apretado, enciende el LED y manda una «A»; si no, apaga el LED) una sola vez, y la ejecuta sobre dos «plataformas» distintas: un AVR y un STM32. Cada una es una clase con las mismas cinco operaciones de la HAL, que en lugar de tocar un chip anotan la operación de bajo nivel que harían. La aplicación no sabe con cuál está hablando.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

class HalAVR:
    nombre = "AVR"
    def __init__(self, pulsaciones):
        self.pulsaciones = list(pulsaciones)     # lo que "ve" el pin del botón
        self.bajo_nivel = []                     # operaciones de registro que se harían
    def led_encender(self): self.bajo_nivel.append("PORTB |= (1 << PB5)")
    def led_apagar(self):   self.bajo_nivel.append("PORTB &= ~(1 << PB5)")
    def boton_presionado(self):
        self.bajo_nivel.append("leer PIND bit 2")
        return self.pulsaciones.pop(0)
    def consola_enviar(self, c): self.bajo_nivel.append(f"UDR0 = {c!r}")


class HalSTM32:
    nombre = "STM32"
    def __init__(self, pulsaciones):
        self.pulsaciones = list(pulsaciones)
        self.bajo_nivel = []
    def led_encender(self): self.bajo_nivel.append("GPIOC_BSRR = 1 << 29")
    def led_apagar(self):   self.bajo_nivel.append("GPIOC_BSRR = 1 << 13")
    def boton_presionado(self):
        self.bajo_nivel.append("leer GPIOA_IDR bit 0")
        return self.pulsaciones.pop(0)
    def consola_enviar(self, c): self.bajo_nivel.append(f"USART1_DR = {c!r}")


def app_paso(hal, traza):                        # la aplicación: no menciona ningún registro
    if hal.boton_presionado():
        hal.led_encender()
        hal.consola_enviar("A")
        traza.append("LED encendido, 'A' enviada")
    else:
        hal.led_apagar()
        traza.append("LED apagado")


PULSACIONES = [False, True, False]
trazas = {}
for plataforma in (HalAVR, HalSTM32):
    hal = plataforma(PULSACIONES)
    traza = []
    for _ in PULSACIONES:
        app_paso(hal, traza)
    trazas[hal.nombre] = traza
    print(f"{hal.nombre}")
    print("  aplicación :", " | ".join(traza))
    print("  bajo nivel :", "; ".join(hal.bajo_nivel))

print("\nMisma conducta en ambas plataformas:", trazas["AVR"] == trazas["STM32"])

Resultado: la traza de la aplicación es idéntica en las dos plataformas (LED apagado | LED encendido, 'A' enviada | LED apagado) y la última línea imprime True. Lo único que cambia es la lista de operaciones de bajo nivel: PORTB |= (1 << PB5) y UDR0 = 'A' en el AVR; GPIOC_BSRR = 1 << 29 y USART1_DR = 'A' en el STM32. Probá agregar una tercera clase con otras operaciones y comprobá que app_paso sigue sin cambios, o cambiá PULSACIONES por otra secuencia.

Esta forma, una interfaz común con distintas implementaciones intercambiables, es lo que en otros lenguajes se llama polimorfismo. En C se logra con archivos de implementación distintos para cada plataforma (se compila uno solo) o con tablas de punteros a función. Y el truco más útil: una tercera «plataforma», la del PC, que en lugar de registros anota las llamadas en una lista, convierte la aplicación en algo que se puede probar con un programa automático.

08 · Exploración interactiva

Migrar de plataforma

La aplicación del apartado anterior (leer un botón, encender un LED y enviar una «A» por la consola) está escrita acá de dos maneras: directo contra el hardware y con una capa propia. Elegí la plataforma de origen y la de destino. El laboratorio genera el código de ambas versiones, cuenta cuántas líneas de la aplicación hay que reescribir al migrar y muestra el archivo de la capa que habría que escribir para la plataforma nueva.

Origen, destino y dos maneras de escribir la aplicación

Líneas de la aplicación (origen)—
Líneas que tocan el hardware—
A reescribir, sin capa propia—
A reescribir, con capa propia—
Líneas de la capa del destino—

Aplicación directa, plataforma de destino

—

Aplicación con capa propia (igual en todas)

—

Capa propia de la plataforma de destino

—

Elegí origen y destino para ver cuánto trabajo evita una capa propia.

Recorrido guiado

  1. Con AVR → STM32: la aplicación tiene 16 líneas y 10 tocan el hardware. Sin capa, esas 10 líneas se reescriben dentro de la aplicación. Con la capa propia, la aplicación (la del medio) no cambia.
  2. Mirá el código de la aplicación con capa: es la misma lectura de siempre, con funciones de nombre claro. Ninguna línea nombra un registro, un pin ni una plataforma.
  3. Mirá la capa del destino: 14 líneas que contienen exactamente las mismas operaciones de hardware que había que reescribir antes, pero agrupadas en un solo archivo.
  4. Cambiá a Pico como destino: la capa tiene solo 11 líneas, porque el SDK del Pico ya trae funciones de alto nivel. En un AVR o un STM32 sin biblioteca, la capa es más larga.
  5. Con origen en Pico o en ESP32, la aplicación directa tiene 13 y 14 líneas: menos que con registros. El SDK reduce el trabajo, pero no lo elimina, porque los nombres siguen siendo propios de cada fabricante.
  6. Una aplicación real tiene miles de líneas y decenas de usos del hardware. Con la capa, el costo de migrar sigue siendo el tamaño de un archivo, no el del programa. El ahorro crece con el proyecto.
Desafío: el número que no sale en la pantalla

El laboratorio cuenta líneas porque es fácil de medir, pero el verdadero ahorro es de otro tipo: las pruebas. Cuando la lógica no toca el hardware, la misma batería de pruebas que validó la aplicación en la plataforma de origen sigue valiendo para la de destino; solo hay que probar la capa nueva. Sin capa, cada línea reescrita pierde la validación acumulada y hay que volver a probar todo el producto en el chip nuevo.

09 · Método de trabajo

Método para separar en capas

  1. Listá las operaciones de hardware que el producto necesita, en palabras del producto: encender el LED de estado, leer la temperatura, enviar un mensaje, medir la batería.
  2. Agrupalas por recurso. Cada grupo es candidato a un módulo: salidas de indicación, entradas de usuario, sensores, comunicaciones.
  3. Escribí primero la interfaz (el .h), sin pensar en el chip. Verificá que nada del fabricante aparezca en él.
  4. Escribí la aplicación contra la interfaz. Si ya podés escribir la lógica completa, la interfaz es suficiente.
  5. Implementá la interfaz para el chip elegido, usando la HAL del fabricante o los registros, dentro de un único archivo por plataforma.
  6. Implementá una versión de prueba para el PC que registre las llamadas, y probá la lógica con ella.
  7. Vigilá las dependencias mirando los #include: nada del fabricante fuera de la HAL; nada de la aplicación dentro de la HAL.
  8. Medí el costo. Tamaño y tiempo de las rutas críticas; si algún acceso no tolera la indirección, resolvelo con static inline o con una excepción documentada.

Un criterio práctico para decidir cuándo vale la pena todo esto: si el producto va a vivir más de unos meses, si hay más de una persona trabajando o si existe la posibilidad real de cambiar de chip, la capa se paga sola. Para un experimento descartable de una tarde, no hace falta.

10 · Comprensión

Confusiones frecuentes

«Usar la HAL del fabricante ya me hace portable»

Te protege de los registros, pero te ata a un fabricante: la HAL de ST y el SDK de Raspberry Pi no se parecen. La portabilidad real exige una interfaz propia, con el vocabulario del producto, que las use por dentro.

«Abstraer es siempre mejor»

Cada capa cuesta memoria, tiempo y complejidad. Se abstrae donde hay una variación real o una necesidad de prueba. Una capa sin nada que varíe detrás es solo un nombre más para aprender.

«Un driver y la HAL son lo mismo»

La HAL abstrae los periféricos del chip (UART, SPI, I²C, GPIO). Un driver abstrae un componente externo (un sensor, una pantalla) y usa la HAL para hablarle. Cambiar de chip toca la HAL; cambiar de sensor, el driver.

«Puedo dejar un acceso directo a un registro solo esta vez»

Un acceso directo en la aplicación es una grieta: el hardware se filtra, aparece en otro lugar y en un año hay veinte. Si un acceso rápido es imprescindible, va dentro de la HAL, con un nombre y un comentario que justifiquen la excepción.

«Con una HAL no hace falta saber lo que hay debajo»

Cuando algo no funciona, el problema suele estar en lo que la HAL hace (un reloj sin habilitar, un pin mal configurado). Sin conocer los registros no se puede diagnosticar. La capa evita escribir lo repetitivo, no evita entenderlo.

«Las capas hacen el programa más lento sí o sí»

Las funciones pequeñas declaradas static inline, junto con la optimización en tiempo de enlace, hacen que el compilador elimine casi todo el costo. El costo real está en las HAL genéricas muy parametrizadas y en las rutas críticas, que se miden.

11 · Práctica guiada

Predecí antes de comprobar

1. ¿En qué capa va cada archivo?

Ubicá cada archivo en una capa (aplicación, servicios, driver de dispositivo, HAL): (a) termostato.c; (b) tmp102.c; (c) hal_i2c_stm32.c; (d) pid.c (un controlador genérico); (e) pantalla_ssd1306.c; (f) mqtt_cliente.c.

Ver solución

(a) aplicación. (b) driver de dispositivo (el sensor). (c) HAL (la implementación del bus I²C para STM32). (d) servicios y bibliotecas: es lógica reutilizable sin hardware. (e) driver de dispositivo (la pantalla). (f) servicios: una pila de comunicaciones, que a su vez usa una HAL de red o serie.

2. Una dependencia que sobra

Revisando termostato.c encontrás #include "stm32f1xx.h" y una línea GPIOB->BSRR = .... ¿Qué problema indica y cómo lo corregirías?

Ver solución

La aplicación conoce el chip: si cambia la familia, hay que reescribir termostato.c, y no se puede probar en el PC. Se corrige creando (o usando) una función de la interfaz, por ejemplo calefactor_encender() en hal_io.h, implementándola en el archivo de la HAL de STM32 y reemplazando la línea. El #include del fabricante se mueve al archivo de la HAL.

3. El costo de una llamada

Un bucle que invierte un pin usando una función de la HAL de 40 ciclos más 4 de bucle, a 72 MHz. ¿Cuál es la frecuencia máxima de la señal generada? ¿Y si el acceso es directo en 4 ciclos por vuelta?

Ver solución

Con la función: 44 ciclos por inversión → 72.000.000 / 44 ≈ 1,64 millones de inversiones por segundo. Con el acceso directo: 4 ciclos → 18 millones por segundo, once veces más rápido. (Una onda completa necesita dos inversiones, así que las frecuencias de señal son la mitad.) Para un LED es irrelevante; para un protocolo por software puede decidir si funciona. Ahí conviene una función static inline.

4. Probar un driver sin el sensor

Con el tmp102_leer del apartado 4, ¿qué bytes debería devolver un bus falso para que el resultado sea 20,5 °C (205 décimas)?

Ver solución

El sensor entrega 12 bits justificados a la izquierda con 0,0625 °C por paso: 20,5 / 0,0625 = 328 = 0x148. Desplazado 4 bits a la izquierda en 16 bits: 0x1480. Los bytes son 0x14, 0x80. Comprobación: (0x1480) >> 4 = 0x148 = 328; 328 × 5 / 8 = 205 ✓. Con un bus falso que devuelva esos dos bytes se prueba el cálculo sin tocar hardware.

5. Diseñar una interfaz

Proponé cuatro funciones (sin tipos ni nombres del fabricante) para controlar un motor de corriente continua con velocidad y sentido.

Ver solución

Por ejemplo: void motor_iniciar(void);, void motor_velocidad(int8_t porcentaje); (con signo: negativo = sentido inverso, de −100 a +100), void motor_detener(void); (con parada libre) y void motor_frenar(void);. Nada menciona un temporizador, un pin PWM ni un puente H: eso queda en el archivo de la plataforma. Conviene documentar cuánto tarda en detenerse y si motor_velocidad se puede llamar desde una interrupción.

6. ¿Vale la pena la capa?

Un compañero va a construir un parpadeador de LED para una demostración de una tarde, en un único chip. ¿Le aconsejarías armar una HAL propia?

Ver solución

No. No hay variación posible (un chip), no hay más personas ni vida útil, y no se necesita probar en el PC. El código directo es más simple y suficiente. La capa se justifica cuando el producto va a vivir, cuando hay un equipo, cuando puede cambiar el chip o cuando se quiere probar la lógica sin la placa. Aplicar la misma disciplina a un experimento de una tarde es sobreingeniería.

12 · Síntesis

Ideas para recordar

  • Mezclar la lógica con el acceso al hardware impide cambiar de chip, probar sin la placa y entender el código; la solución es una interfaz entre lo que cambia por motivos distintos.
  • Un firmware se organiza en capas (aplicación, servicios, drivers, HAL, hardware) con una regla: las dependencias apuntan hacia abajo; se vigila mirando los #include.
  • La HAL abstrae los periféricos del chip; un driver abstrae un componente externo y se apoya en la HAL, nunca en los registros.
  • Las HAL de los fabricantes no son portables entre sí; para una portabilidad real conviene una HAL propia, mínima, en el vocabulario del producto, que las use por dentro.
  • Una buena interfaz es pequeña, completa, sin tipos del fabricante, con errores explícitos, un responsable por recurso y un contrato documentado (bloqueo, uso desde interrupciones).
  • Pasar el bus a un driver como una estructura de punteros a función lo hace reutilizable en cualquier chip y probable en el PC con un bus falso.
  • Abstraer cuesta tiempo, memoria y complejidad; se reduce con funciones static inline y con la optimización en tiempo de enlace, y se justifica solo donde hay variación real o necesidad de prueba.
  • Con una capa propia, migrar de plataforma cuesta escribir un archivo de implementación en lugar de reescribir la aplicación, y las pruebas de la lógica siguen valiendo.

¿Listo para el tema 11?

Antes de avanzar, explicá la diferencia entre una HAL y un driver con un ejemplo, escribí el encabezado de una interfaz mínima para un buzzer (sin nada del fabricante) y decí cómo detectarías, mirando solo los #include, que una dependencia apunta en el sentido equivocado. Si podés justificar las tres respuestas, ya diseñás el firmware en capas.

En el próximo tema bajamos a lo concreto: organizar un proyecto de firmware. Cómo repartir módulos, archivos de cabecera y configuración en carpetas, y cómo estructurar la compilación para que el esquema de capas sea visible en el propio árbol de archivos.