01 · Punto de partida
De un archivo a cincuenta
Los ejemplos de los temas anteriores cabían en una pantalla. Un producto real, no. Un termostato con sensor, pantalla, comunicaciones, control y almacenamiento tiene varias decenas de archivos, escritos durante meses por más de una persona. En ese tamaño aparecen problemas que no existían a escala pequeña: nadie sabe dónde está cada cosa, un cambio de pines obliga a revisar diez archivos, compilar tarda cada vez más y dos módulos terminan dependiendo uno del otro en círculo.
La mayor parte de esos problemas se evitan con decisiones tempranas y baratas: cómo se divide el código en módulos, qué muestra cada archivo de cabecera, cómo se ordenan las carpetas y dónde viven los parámetros de configuración. Corregirlas más tarde es posible, pero cuesta mucho más, porque hay que mover código que ya está en uso.
Este tema es el puente entre la teoría de capas del anterior y el taller del tema 12, donde vas a construir un esqueleto de firmware completo. Todo lo que se explica acá se usa en ese taller.
02 · La unidad básica
El módulo: interfaz y implementación
En C, la unidad natural de organización es el módulo: un par de archivos, uno .h con la interfaz pública y uno .c con la implementación. El .h responde «¿qué se puede pedirle a este módulo?»; el .c contiene el «¿cómo lo hace?». Quien usa el módulo incluye solo el .h.
/* tmp102.h — driver del sensor de temperatura TMP102 (interfaz pública) */
#ifndef TMP102_H
#define TMP102_H
#include <stdint.h>
/* Lee la temperatura en décimas de grado (250 = 25,0 °C).
* Devuelve 0 si salió bien y -1 si falló el bus. No se puede llamar desde una ISR. */
int tmp102_leer(int16_t *decimas);
#endif /* TMP102_H */
/* tmp102.c — implementación */
#include "tmp102.h"
#include "hal/hal_i2c.h"
#define TMP102_DIR 0x48 /* privado: no aparece en el .h */
static int16_t ultima_lectura; /* privada del módulo: static */
static int16_t convertir(uint8_t alto, uint8_t bajo) { /* función privada */
int16_t crudo = (int16_t)((alto << 8) | bajo) >> 4;
return (int16_t)((crudo * 5) / 8);
}
int tmp102_leer(int16_t *decimas) { /* función pública, declarada en el .h */
uint8_t b[2];
if (i2c_leer_registro(TMP102_DIR, 0x00, b, 2) != 0) return -1;
ultima_lectura = convertir(b[0], b[1]);
*decimas = ultima_lectura;
return 0;
}
Lo que importa es la asimetría. El .h es corto y estable; el .c puede cambiar por completo sin que nadie lo note. La palabra static aplicada a una función o variable global la vuelve privada del archivo: nadie más puede verla ni usarla por error. Es la herramienta que hace cumplir el ocultamiento de información en C.
Algunas reglas que hacen que un módulo sea bueno:
- Una responsabilidad. «Maneja el sensor de temperatura» es un módulo; «maneja el sensor, la pantalla y un poco de red» es una fuente de problemas.
- Prefijo del módulo en todo lo público (
tmp102_leer,pid_calcular). En C no hay espacios de nombres; el prefijo evita choques y dice de dónde viene cada función. - Tamaño manejable. Si un
.cpasa de unos pocos cientos de líneas, probablemente son dos módulos. - Estado privado. Las variables del módulo son
static; se accede a ellas con funciones. Una variable global pública es un acuerdo tácito entre todos los archivos, y los acuerdos tácitos se rompen.
03 · El lado público
Qué va (y qué no va) en una cabecera
Un archivo de cabecera no es compilado por sí mismo: el preprocesador lo pega, tal cual, en cada archivo .c que lo incluye. Esa idea simple, texto copiado, explica casi todas las reglas.
| Va en la cabecera | No va en la cabecera |
|---|---|
| Declaraciones de funciones públicas (prototipos) | El cuerpo de las funciones (salvo static inline cortas) |
Tipos (typedef, struct, enum) que forman parte de la interfaz | Definiciones de variables globales |
| Constantes y macros públicas | Detalles internos: registros, constantes privadas |
Declaraciones extern de las variables que sí deben ser públicas | Inclusión de más cabeceras de las necesarias |
| Un include guard | Código que se ejecuta |
El include guard
Si una cabecera se incluye dos veces en el mismo archivo (algo muy fácil de provocar: main.c incluye A.h y B.h, y B.h también incluye A.h), las definiciones de tipos se repiten y el compilador se queja de redefiniciones. La solución estándar es el include guard: las tres líneas que envuelven el contenido de toda cabecera:
#ifndef HAL_IO_H /* si todavía no se definió esta marca... */
#define HAL_IO_H /* ...definirla, para que la segunda vez no entre */
/* contenido de la cabecera */
#endif /* HAL_IO_H */
/* Alternativa no estándar pero muy extendida: una sola línea, #pragma once, al comienzo */
Variables en cabeceras: el error del enlazador
Otra consecuencia del «texto pegado»: una variable definida en una cabecera se define una vez por cada archivo .c que la incluye. Cada archivo objeto la contiene, y al enlazar aparecen duplicadas.
/* MAL — contador.h */
int contador = 0; /* se define en cada .c que incluya este archivo */
/* error al enlazar: multiple definition of `contador' */
/* BIEN — contador.h */
extern int contador; /* solo se declara: "existe en algún lado" */
/* BIEN — contador.c */
int contador = 0; /* se define una única vez */
La diferencia es la misma que entre declarar y definir: la declaración dice que algo existe; la definición lo crea y le reserva memoria. En una cabecera solo debe haber declaraciones. Y mejor aún: evitar la variable pública y exponer funciones contador_leer() y contador_incrementar(), que mantienen el estado privado.
Dos hábitos más. Incluir lo mínimo: si una cabecera solo necesita saber que existe un tipo, a veces alcanza con una declaración adelantada en lugar de incluir otra cabecera entera; cada #include de más es una dependencia más. Y una cabecera debe compilar sola: si para usarla hay que incluir otra antes, esa otra falta dentro de ella.
04 · El árbol de archivos
La estructura de carpetas
Una buena estructura de carpetas hace visible la arquitectura. Quien abre el proyecto por primera vez debería poder ver las capas del tema 10 en el propio árbol:
termostato/
├── src/
│ ├── app/ main.c, termostato.c, termostato.h (la aplicación)
│ ├── servicios/ pid.c, pid.h (lógica reutilizable)
│ ├── drivers/ tmp102.c, tmp102.h, pantalla.c ... (componentes externos)
│ └── hal/
│ ├── hal_io.h, hal_i2c.h (interfaces)
│ ├── stm32/ hal_io.c, hal_i2c.c (implementación para el chip)
│ └── pc/ hal_io.c, hal_i2c.c (simulación para pruebas)
├── vendor/ CMSIS y HAL del fabricante (no se modifican)
├── config/ config.h, config_pines.h
├── linker/ stm32f103.ld, startup_stm32f103.c
├── test/ pruebas que corren en el PC
├── build/ objetos, .elf, .map (no se versiona)
├── docs/ requisitos, diseño, decisiones
├── Makefile
└── README.md cómo compilar, grabar y probar
Cada carpeta responde a una pregunta y tiene una dependencia permitida:
src/apppuede incluir lo deservicios,driversy las interfaces dehal; nuncavendorni los archivos dehal/stm32.src/hal/stm32ysrc/hal/pcson dos implementaciones de las mismas interfaces. La compilación elige cuál; el resto del proyecto no cambia. La del PC permite correr la lógica en la computadora (tema 5).vendor/contiene código ajeno. Se trata como una biblioteca de terceros: no se edita, se actualiza de forma controlada, y solo la HAL lo incluye.config/concentra los parámetros del producto en un lugar conocido.build/se genera y se borra sin riesgo; jamás se guarda en el control de versiones.test/ydocs/viven al lado del código, no en otro lado, para que se mantengan actualizados.
No existe una estructura única correcta. Lo que importa es que sea predecible (el nombre de la carpeta dice qué hay), coherente con las capas y que se cumpla. Un proyecto chico puede fusionar carpetas; uno grande las subdivide. Lo grave es el árbol que crece sin criterio hasta que ya nadie sabe dónde poner un archivo nuevo.
05 · Un solo lugar para los parámetros
Configuración centralizada
Cada producto tiene valores que lo definen: frecuencia del reloj, velocidad de la consola, tamaños de buffers, períodos de control, pines usados. Si esos valores están desperdigados por el código, cambiarlos exige buscar y es fácil olvidarse de uno. La práctica es reunirlos en archivos de configuración, con nombre, en la carpeta config/:
/* config.h — parámetros del producto */
#ifndef CONFIG_H
#define CONFIG_H
#define F_CPU_HZ 72000000UL
#define UART_BAUDIOS 115200UL
#define BUFFER_RX_TAM 128 /* bytes; potencia de 2 para usar máscara */
#define PERIODO_CONTROL_MS 100
/* Verificaciones en tiempo de compilación: un valor imposible no llega a la placa */
_Static_assert(BUFFER_RX_TAM >= 64 && (BUFFER_RX_TAM & (BUFFER_RX_TAM - 1)) == 0,
"BUFFER_RX_TAM debe ser una potencia de 2 y al menos 64");
#endif
/* config_pines.h — asignación de pines (un solo lugar) */
#ifndef CONFIG_PINES_H
#define CONFIG_PINES_H
#define LED_ESTADO_PUERTO C
#define LED_ESTADO_PIN 13
#define LED_ESTADO_ACTIVO_BAJO 1 /* 1: el LED enciende con nivel bajo */
#define BOTON_PUERTO A
#define BOTON_PIN 0
#endif
Ventajas que ya se ven: un cambio de placa toca un archivo; nadie escribe «13» en medio de una función sin que se sepa qué es; y _Static_assert convierte un requisito en algo que el compilador comprueba por vos. Algunas recomendaciones:
- Nada de números mágicos. Un
128suelto en el código es una promesa implícita de que dos lugares coinciden. ConBUFFER_RX_TAM, o coincide por construcción, o falla la compilación. - Un nombre, un lugar. Si un parámetro aparece definido en dos archivos con valores distintos, el comportamiento depende de cuál se incluya primero; es de los errores más silenciosos (el laboratorio lo muestra).
- Constantes con tipo cuando se puede.
static constoenumdan chequeo de tipos que una macro no tiene; las macros quedan para lo que debe resolverse en el preprocesador (tamaños de arreglos en C, selección de código). - Variantes por construcción. Para versiones del producto (otra placa, otro mercado), se elige el archivo de configuración o se pasa una marca al compilador (
-DPLACA_V2), y el código usa#ifen el archivo de configuración, no desparramado por el programa. - Separar configuración de compilación y de ejecución. Lo que se fija al compilar va aquí. Lo que el usuario puede cambiar en campo (una consigna, un horario) es un dato, no una macro, y se guarda en memoria no volátil (tema 40).
06 · Construir el árbol
El sistema de construcción
El Makefile del tema 5 listaba los objetos a mano. Con decenas de archivos eso no escala. Un esquema más general usa comodines para encontrar los fuentes, separa los objetos por plataforma y genera las dependencias de las cabeceras automáticamente:
PLATAFORMA ?= stm32 # stm32 (placa) o pc (pruebas): make PLATAFORMA=pc
ifeq ($(PLATAFORMA),stm32)
CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m3 -mthumb -Os -ffunction-sections -fdata-sections
LDFLAGS = -T linker/stm32f103.ld -nostartfiles -Wl,--gc-sections
else
CC = gcc
CFLAGS = -O0 -g
LDFLAGS =
endif
CFLAGS += -Wall -Wextra -Isrc -Iconfig -MMD -MP # -MMD -MP: genera las dependencias de cabeceras
SRC = $(wildcard src/app/*.c src/servicios/*.c src/drivers/*.c src/hal/$(PLATAFORMA)/*.c)
OBJ = $(SRC:%.c=build/$(PLATAFORMA)/%.o)
build/$(PLATAFORMA)/firmware: $(OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) $(OBJ) -o $@
build/$(PLATAFORMA)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
-include $(OBJ:.o=.d) # reutiliza las dependencias generadas
Tres ideas valen la pena:
PLATAFORMAelige la implementación de la HAL. Conmakese compila para la placa consrc/hal/stm32; conmake PLATAFORMA=pcse compila para la computadora consrc/hal/pc. La aplicación y los drivers son los mismos archivos en ambos casos. Es la separación en capas, hecha operativa.- Las opciones
-Isrc -Iconfigagregan esas carpetas a la búsqueda de cabeceras, para escribir#include "hal/hal_i2c.h"y#include "config.h"sin ruta relativa. - Las opciones
-MMD -MPhacen que el compilador escriba, para cada objeto, un archivo.dcon las cabeceras de las que depende. Si se modifica una cabecera,makerecompila solo los.cque la incluyen. Sin esto, cambiar un.hdeja objetos viejos y produce errores de enlace o comportamientos fantasma que desaparecen con unmake clean: una fuente de horas perdidas.
A medida que el proyecto crece se pasa a CMake o a herramientas equivalentes, que automatizan más, pero la idea es la misma: una descripción declarativa de qué fuentes forman cada versión.
07 · Orden compartido
Convenciones y herramientas de calidad
Cuando más de una persona toca el código, los acuerdos sobre estilo importan tanto como la arquitectura. Un conjunto mínimo:
- Nombres. Archivos en minúscula; funciones y variables públicas con el prefijo del módulo; constantes y macros en mayúsculas; tipos terminados en
_t. Nombres que dicen qué hacen (temperatura_decimasy not2). - Formato automático. Una herramienta como
clang-formatcon un archivo de configuración compartido elimina las discusiones de estilo y los cambios ruidosos en las revisiones. - Avisos como errores. Compilar con
-Wall -Wextray, en integración continua,-Werror. Un aviso ignorado hoy es un defecto mañana. - Análisis estático. Herramientas como
cppchecko los analizadores del propio compilador detectan desbordes, variables sin inicializar y fugas sin ejecutar el programa. Para sistemas críticos existen guías de codificación como MISRA C, que restringen las construcciones del lenguaje más propensas a errores. - Comentarios que explican el porqué. «Espera 20 ms: el sensor tarda eso en estabilizarse tras el encendido» sirve; «espera 20 ms» no. Las cabeceras documentan el contrato de cada función (bloqueo, errores, uso desde interrupciones).
Y las convenciones del repositorio: un .gitignore que excluya build/; vendor/ con una versión fija (copiada o como submódulo) y una nota de cuál es; etiquetas en cada versión entregada (tema 5); y un README.md que diga en pocas líneas cómo compilar, grabar y probar. Si un colega nuevo no puede compilar el proyecto en media hora leyendo el README, la documentación está incompleta.
08 · Analogía con programación
Un revisor de dependencias en Python
Vigilar las dependencias a mano no escala. El programa siguiente representa un proyecto como un diccionario (archivo → capa e includes) y revisa dos reglas: que ningún archivo dependa de una capa superior ni de las cabeceras del fabricante salvo la HAL, y que no haya inclusiones circulares. Se corre sobre un proyecto limpio y sobre uno con dos defectos.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
CAPAS = {"app": 0, "servicios": 1, "drivers": 2, "hal": 3, "vendor": 4}
def proyecto(defectos=()):
p = { # archivo: (capa, [archivos que incluye])
"app/main.c": ("app", ["app/termostato.h", "hal/hal_io.h"]),
"app/termostato.h": ("app", ["servicios/pid.h", "hal/hal_io.h"]),
"app/termostato.c": ("app", ["app/termostato.h", "drivers/tmp102.h"]),
"servicios/pid.h": ("servicios", []),
"servicios/pid.c": ("servicios", ["servicios/pid.h"]),
"drivers/tmp102.h": ("drivers", ["hal/hal_i2c.h"]),
"drivers/tmp102.c": ("drivers", ["drivers/tmp102.h"]),
"hal/hal_io.h": ("hal", []),
"hal/hal_i2c.h": ("hal", []),
"hal/stm32/hal_io.c": ("hal", ["hal/hal_io.h", "vendor/stm32f1xx.h"]),
"hal/stm32/hal_i2c.c":("hal", ["hal/hal_i2c.h", "vendor/stm32f1xx.h"]),
"vendor/stm32f1xx.h": ("vendor", []),
}
if "vendor_en_app" in defectos:
p["app/termostato.c"][1].append("vendor/stm32f1xx.h")
if "pid_conoce_app" in defectos:
p["servicios/pid.h"][1].append("app/termostato.h")
return p
def revisar(p):
hallazgos = []
for archivo, (capa, incs) in p.items():
for h in incs:
capa_h = p[h][0]
if capa_h == "vendor" and capa != "hal":
hallazgos.append(f"{archivo} incluye {h}: solo la HAL puede incluir cabeceras del fabricante")
elif CAPAS[capa_h] < CAPAS[capa]:
hallazgos.append(f"{archivo} ({capa}) incluye {h} ({capa_h}): la dependencia apunta hacia arriba")
estado, camino = {}, []
def visitar(x):
estado[x] = 1
camino.append(x)
for h in p[x][1]:
if estado.get(h) == 1: # volvimos a un archivo de la ruta actual
ciclo = camino[camino.index(h):] + [h]
hallazgos.append("inclusión circular: " + " -> ".join(ciclo))
elif h not in estado:
visitar(h)
camino.pop()
estado[x] = 2
for x in p:
if x not in estado:
visitar(x)
return hallazgos
for titulo, defectos in (("Proyecto limpio", ()), ("Proyecto con defectos", ("vendor_en_app", "pid_conoce_app"))):
print(titulo)
problemas = revisar(proyecto(defectos))
print(" sin hallazgos" if not problemas else "\n".join(" - " + t for t in problemas))
Resultado: el proyecto limpio no tiene hallazgos. El que tiene defectos informa tres: la aplicación incluye la cabecera del fabricante, servicios/pid.h incluye una cabecera de la aplicación (dependencia hacia arriba) y, como consecuencia, la inclusión circular app/termostato.h -> servicios/pid.h -> app/termostato.h. Un solo defecto de diseño (la biblioteca que conoce a la aplicación) produce dos síntomas distintos. Probá con solo "pid_conoce_app" o agregá una línea que haga que hal/hal_io.h incluya drivers/tmp102.h.
Este tipo de revisión se automatiza en la integración continua: cada cambio al repositorio corre un revisor y rechaza los que rompen la estructura. Herramientas reales como include-what-you-use, las reglas de dependencias de CMake o simples grep sobre los #include cumplen la misma función. Lo importante es la idea: una regla de arquitectura que nadie comprueba es un deseo.
09 · Exploración interactiva
Analizar un proyecto
El laboratorio carga el proyecto del termostato (12 archivos en las cinco capas) y aplica el mismo tipo de revisión del apartado anterior, ampliada con tres reglas más: cabeceras sin include guard incluidas dos veces, variables definidas en cabeceras y macros duplicadas con valores distintos. Cada casilla introduce un defecto en el proyecto; mirá cómo cambian la lista de includes y los hallazgos.
Defectos que se pueden introducir
Qué incluye cada archivo
—
Hallazgos del revisor
—
Marcá una casilla para introducir un defecto y ver cómo lo detecta el revisor.
Recorrido guiado
- Sin ninguna casilla: 12 archivos, ningún hallazgo. Mirá la lista de includes: cada flecha apunta hacia una capa inferior o igual, y
stm32f1xx.hsolo es incluido por los dos archivos de la HAL. - Marcá La aplicación incluye stm32f1xx.h: error de capa. La aplicación ya conoce el chip; el día que cambie la familia habrá que reescribirla.
- Desmarcala y marcá pid.h incluye termostato.h: aparecen dos errores, una dependencia hacia arriba y una inclusión circular. Un solo defecto de diseño, dos síntomas.
- Probá hal_io.h sin include guard:
main.cincluyehal_io.hdirectamente y también a través determostato.h, y lo recibe dos veces. Es un error de compilación que no se debe a nada que escribiste mal enmain.c. - Con tmp102.h define una variable, el error no sale al compilar sino al enlazar: cada archivo .c compila bien por separado, pero ambos contienen su propia copia de la variable y el enlazador no sabe con cuál quedarse.
- Marcá LED_PIN definido en dos archivos: la única entrada del análisis es un aviso, no un error. El proyecto compila y se ejecuta, pero con una configuración ambigua. Ese tipo de defecto es el que llega al cliente.
- Marcalas todas: más de media docena de hallazgos, de causas independientes. Probablemente un proyecto real que nació sin estructura se vea así.
Desafío: ¿por qué no basta con que compile?
De los seis defectos, solo dos impiden compilar o enlazar: el include guard faltante y la variable en la cabecera. Esos se descubren enseguida. Los otros cuatro (la aplicación que incluye al fabricante, la HAL que incluye a un driver, la biblioteca que conoce a la aplicación y la macro duplicada) compilan sin quejarse: nada falla hoy, y el costo aparece al cambiar de chip, al probar en el PC, al reutilizar un módulo o cuando el LED se enciende en el pin equivocado. Son defectos de diseño, no de código, y solo los encuentra una revisión que mire la estructura. Por eso esa revisión se automatiza, como en el revisor del apartado anterior.
10 · Método de trabajo
Método para estructurar un proyecto
- Dibujá las capas y los módulos en una página antes de crear archivos: qué hace cada uno, de quién depende. Si no entra en una hoja, el diseño es demasiado enredado.
- Creá el árbol de carpetas con las capas, aunque al principio tenga un solo archivo por carpeta.
- Escribí primero las cabeceras. Cada módulo comienza con su
.h: nombre, funciones, contrato. Si una cabecera es difícil de escribir, el módulo todavía no está bien pensado. - Hacé privado todo lo que se pueda con
static, y exponé funciones, no variables. - Centralizá la configuración y ponele verificaciones en tiempo de compilación.
- Armá la compilación temprano: un comando para la placa y uno para el PC, con dependencias de cabeceras automáticas y avisos activados.
- Automatizá las reglas de arquitectura: un revisor de
#includeen la integración continua, para que la estructura no se degrade sin que nadie lo vea. - Documentá en el README cómo compilar, grabar, probar y dónde va cada cosa, y mantenelo al día cuando el proyecto cambie.
Un criterio de salud simple: agregar un componente nuevo (un sensor, una pantalla) debería implicar crear archivos nuevos y tocar muy pocos existentes. Si cada cambio obliga a editar quince archivos, la estructura no está cumpliendo su trabajo.
11 · Comprensión
Confusiones frecuentes
«Poner todo en pocos archivos grandes es más simple»
Es más simple hoy y mucho más caro después: un archivo de tres mil líneas no se puede probar por partes, genera conflictos cuando dos personas lo editan y esconde dependencias. Dividir en módulos con interfaces cortas cuesta poco al principio y evita la maraña.
«Un include guard evita todos los problemas de inclusión»
Evita las redefiniciones por incluir dos veces, pero no resuelve las dependencias circulares ni las que apuntan en el sentido equivocado: un ciclo protegido por guards puede compilar y dejar un tipo sin declarar según el orden de inclusión.
«Puedo definir una variable global en la cabecera si la necesitan varios archivos»
Se define una vez por cada archivo que incluye la cabecera y el enlazador se queja. Lo correcto es extern en la cabecera y la definición en un único .c; mejor todavía, que el módulo oculte la variable y ofrezca funciones.
«Los #define de configuración pueden estar donde se usen»
Si el mismo parámetro vive en dos lugares, tarde o temprano divergen. Un valor, un nombre, un archivo, sobre todo para pines, tamaños y tiempos.
«La estructura de carpetas es cuestión de gusto»
Hay libertad en los nombres, pero la estructura expresa la arquitectura: dónde vive el código que toca el chip, cuál se puede probar en el PC, qué no se modifica. Una estructura que no refleja las capas obliga a descubrirlas leyendo los #include.
«Con una buena estructura no hace falta revisarla»
La estructura se degrada con cada atajo: un «solo esta vez» que incluye una cabecera del fabricante en la aplicación. La única defensa es una comprobación automática en cada cambio.
12 · Práctica guiada
Predecí antes de comprobar
1. ¿Declaración o definición?
Indicá cuáles de estas líneas pueden estar en una cabecera incluida por varios archivos: (a) extern int modo;; (b) int modo = 0;; (c) void led_encender(void);; (d) static inline int doble(int x) { return 2 * x; }; (e) const uint8_t tabla[4] = {1, 2, 3, 4};.
Ver solución
Pueden estar: (a) declaración; (c) prototipo; (d) función static inline, que cada archivo tiene como copia privada. No deben estar: (b) define una variable en cada archivo (error de enlace «multiple definition»), y (e) define una tabla en cada archivo: ocupa flash por cada inclusión y, según el compilador, produce el mismo error. La tabla se define en un solo .c y se declara con extern const uint8_t tabla[4]; en la cabecera.
2. Armar el include guard
Escribí las tres líneas de protección para una cabecera llamada driver_pantalla.h y explicá qué ocurre al incluirla dos veces.
Ver solución
#ifndef DRIVER_PANTALLA_H, #define DRIVER_PANTALLA_H al comienzo, y #endif al final. La primera vez, la marca no existe: se define y se procesa el contenido. La segunda vez, la marca ya existe: el preprocesador salta todo hasta el #endif, por lo que los tipos no se declaran dos veces.
3. Ubicar archivos
Ubicá en el árbol de carpetas: (a) la rutina que escribe en GPIOB->BSRR; (b) un controlador PID; (c) la interfaz hal_uart.h; (d) la tabla de pines del producto; (e) la biblioteca CMSIS descargada del fabricante; (f) un banco de pruebas que corre en el PC.
Ver solución
(a) src/hal/stm32/. (b) src/servicios/. (c) src/hal/. (d) config/config_pines.h. (e) vendor/. (f) test/. Notá que (a) es la única que menciona un registro del chip, y está en la carpeta específica de la plataforma.
4. Compilar para el PC
Con el Makefile del apartado 6, ¿qué cambia al ejecutar make PLATAFORMA=pc? ¿Qué archivos se compilan que antes no, y cuáles dejan de compilarse?
Ver solución
Se usa gcc en lugar del compilador cruzado y se prescinde de las opciones del linker script. De la HAL se compilan los archivos de src/hal/pc/ (la simulación) en lugar de los de src/hal/stm32/. La aplicación, los servicios y los drivers son exactamente los mismos. El resultado es un ejecutable para la computadora que corre la lógica real del producto.
5. Un cambio de pin
El LED de estado pasa del pin PC13 al PB5. ¿Cuántos archivos debería tocar el cambio en un proyecto bien estructurado? ¿Y qué indica si tocás más?
Ver solución
Uno solo: config/config_pines.h (y, si la asignación del pin usa registros distintos, la función de inicialización de la HAL de la plataforma). Si hay que editar la aplicación o varios drivers, es señal de que el pin está escrito en el código disperso (un número mágico) o de que el hardware se filtra por las capas.
6. Detectar el defecto
En tmp102.c (un driver) encontrás #include "app/termostato.h" para usar un tipo de la aplicación. ¿Qué regla se rompe y cómo lo resolverías?
Ver solución
Se rompe la regla de que las dependencias apuntan hacia abajo: un driver (capa baja) conoce la aplicación (capa alta). Consecuencias: el driver no se puede reutilizar en otro producto y se corre el riesgo de ciclos de inclusión. Se resuelve definiendo el tipo en el propio driver (o en una cabecera común de tipos que ambos incluyan) y haciendo que la aplicación use el del driver, no al revés.
13 · Síntesis
Ideas para recordar
- La estructura de archivos y carpetas es la forma concreta del diseño por capas; las decisiones tempranas son baratas y corregirlas después es caro.
- Un módulo es un par
.h(interfaz pública, corta y estable) y.c(implementación); lo privado se declarastatic, lo público lleva el prefijo del módulo y las variables se exponen mediante funciones. - La cabecera se pega como texto en cada archivo que la incluye: lleva declaraciones, tipos y macros públicas, nunca definiciones de variables; todas llevan include guard.
- El árbol de carpetas refleja las capas (app, servicios, drivers, hal con una carpeta por plataforma, vendor, config, test, build) y cada carpeta tiene dependencias permitidas.
- La configuración vive en un lugar, sin números mágicos, con verificaciones en tiempo de compilación (
_Static_assert) y variantes por construcción. - El sistema de construcción encuentra los fuentes, elige la plataforma (placa o PC), recompila lo mínimo con las dependencias de cabeceras (
-MMD -MP) y activa los avisos. - Las convenciones de nombres, el formato automático, el análisis estático y los avisos como errores mantienen el orden entre varias personas; el repositorio ignora
build/, fijavendor/y se documenta en el README. - Las reglas de arquitectura se automatizan (por ejemplo, un revisor de
#include): una regla que nadie comprueba es un deseo, y los defectos de diseño compilan sin quejarse.
¿Listo para el tema 12?
Antes de avanzar, explicá por qué una variable definida en una cabecera produce un error de enlace; dibujá el árbol de carpetas de un proyecto con sensor, pantalla y comunicaciones indicando qué puede incluir cada carpeta; y decí qué haría falta cambiar para compilar la lógica en tu PC. Si podés justificar las tres respuestas, tenés la estructura sobre la que construir.
En el próximo tema cerramos el bloque con un taller: vas a armar un esqueleto de firmware desde cero, con arranque, reloj, GPIO y un módulo de aplicación sobre una placa real, aplicando la estructura de este tema.