Clean Fracanto

Clean Fracanto #1 — Clean Architecture

Clean Architecture — The Dependency Rule in Pure C

The Dependency Rule in Pure C

Robert C. Martin's Clean Architecture formulates one central rule: dependencies may only point from outer layers to inner layers. Outer layers — drivers, UI, databases — depend on inner layers, never the reverse. The inner layers know nothing of the outer ones and remain stable even as hardware, protocols, or peripherals change.

The Three Layers as Concentric Rings

The fracanto framework implements this rule in three layers that can be read as concentric rings:

The innermost ring: CAN Protocol (Layer 1). This is where the system's entities reside — the data structures and encoding rules that define what a CAN message is. The can_frame module encodes 11-bit CAN IDs from priority, node ID, and subtype. The can_msg module translates 25 message types into CAN-FD frames. The signal_types module assigns each signal a semantic hint (CV, Gate, Trigger, CC). This layer has no outward dependencies — it defines the language in which modules speak to each other, regardless of which hardware transports that language.

The middle ring: HAL (Hardware Abstraction Layer). The HAL is the boundary layer between framework logic and hardware. Through a vtable with eight operations (init, deinit, start, stop, send, set_filter, set_rx_callback, get_tick_ms), it defines what a CAN backend must be capable of — not how it does it. The SocketCAN implementation for Linux and the STM32-FDCAN implementation for microcontrollers are interchangeable adapters that serve this interface. The HAL depends on the protocol (it transports CAN frames), but the protocol knows nothing of the HAL.

The outermost ring: Node Management (Layer 2). This is where the application logic of a module resides: the state machine (INIT → ANNOUNCING → RUNNING → ERROR), the frame dispatch, the CV/Gate routing, the parameter system, the panel abstraction, and the audio pipeline. This layer uses the HAL to send and receive frames and the protocol to encode them. But neither the HAL nor the protocol knows that node management exists.

The driver layer sits at the very outside — it implements the vtable interfaces for concrete hardware (ADS8866, DAC8552, PCM5102A, Encoder, SSD1306) and is injected from outside during initialization. No framework code references a concrete driver.

The Dependency Rule in Practice

The decisive consequence: A concrete module repository like fragesymo implements only the 16 callbacks of the module_ops_t table. It knows neither CAN frames nor HAL details nor SPI registers. The dispatch bridge in module.c translates raw CAN messages into type-safe callbacks — the module receives on_cv(channel, value), not a byte buffer with CAN ID parsing. The dependency always points inward: Driver → Framework Interface → Protocol.

Boundaries and the Role of the vtable

In Clean Architecture, boundaries separate the layers. In object-oriented languages, these are typically abstract classes or interfaces. In fracanto, the _ops_t structs assume this role. Each vtable is a boundary: fracanto_hal_ops_t separates protocol from hardware, panel_ops_t separates UI logic from GPIO access, io_ops_t separates signal processing from ADC/DAC hardware, audio_ops_t separates the audio pipeline from DMA streaming. The boundaries are explicit, machine-readable, and verifiable at compile time.