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.
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:
| Nivel | Ejemplo | Control | Portabilidad |
|---|---|---|---|
| Registros directos | GPIOC_BSRR = ... | Total | Nula |
| CMSIS (núcleo) | Definiciones del núcleo Cortex-M: NVIC_EnableIRQ(), __disable_irq() | Alto | Entre todos los Cortex-M |
| Biblioteca de bajo nivel del fabricante | LL de STM32: funciones casi 1 a 1 con los registros | Alto | Dentro de la familia del fabricante |
| HAL del fabricante | HAL_GPIO_WritePin(), gpio_put() del SDK de Pico, gpio_set_level() de ESP-IDF | Medio | Dentro de la familia o del SDK |
| Marcos multiplataforma | Arduino, mbed, Zephyr | Menor | Muchas placas y familias |
| HAL propia del producto | led_encender(), consola_enviar() | El que defina el diseño | La 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, ®, 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 nogpio_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.hincluyestm32f1xx.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
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- Agrupalas por recurso. Cada grupo es candidato a un módulo: salidas de indicación, entradas de usuario, sensores, comunicaciones.
- Escribí primero la interfaz (el
.h), sin pensar en el chip. Verificá que nada del fabricante aparezca en él. - Escribí la aplicación contra la interfaz. Si ya podés escribir la lógica completa, la interfaz es suficiente.
- Implementá la interfaz para el chip elegido, usando la HAL del fabricante o los registros, dentro de un único archivo por plataforma.
- Implementá una versión de prueba para el PC que registre las llamadas, y probá la lógica con ella.
- Vigilá las dependencias mirando los
#include: nada del fabricante fuera de la HAL; nada de la aplicación dentro de la HAL. - Medí el costo. Tamaño y tiempo de las rutas críticas; si algún acceso no tolera la indirección, resolvelo con
static inlineo 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 inliney 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.