Skip to main content

nucleus_compiler/
codegen.rs

1//! HAL code generation.
2//!
3//! Turns a validated [`Config`] into two C files:
4//!
5//! - `nucleus_config.h` — a typed config struct per peripheral plus `extern`
6//!   HAL handle declarations and the `Nucleus_Init()` prototype.
7//! - `nucleus_init.c` — the resolved config struct instances, the handle
8//!   definitions, and a single `Nucleus_Init()` that enables GPIO clocks,
9//!   configures the alternate-function muxing (using AF numbers resolved from
10//!   [`nucleus_db`]), and calls the stock ST HAL `HAL_*_Init` functions.
11//!
12//! **Architectural rule (README):** the generated code never reimplements the
13//! HAL. It only calls `Init` functions with resolved parameters, so a HAL
14//! point-release that changes internals does not break Nucleus output. The
15//! tested HAL family is STM32F4 (`stm32f4xx_hal.h`).
16//!
17//! Codegen assumes the config already passed [`crate::solver::solve`]; it skips
18//! unmodelled peripheral kinds and pins it cannot resolve rather than failing.
19
20use std::fmt::Write;
21use std::str::FromStr;
22
23use nucleus_db::{Database, Pin};
24
25use crate::config::{Config, Peripheral};
26use crate::model;
27
28/// The generated C sources for a project.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Generated {
31    pub config_h: String,
32    pub init_c: String,
33}
34
35/// One peripheral lowered to everything codegen needs.
36struct Lowered {
37    /// HAL instance name, e.g. `USART2`.
38    instance: String,
39    /// Handle variable, e.g. `huart2`.
40    handle: String,
41    /// HAL handle type, e.g. `UART_HandleTypeDef`.
42    handle_type: &'static str,
43    /// Per-instance config struct type, e.g. `Nucleus_USART2_Config`.
44    config_type: String,
45    kind: Kind,
46    /// Resolved pin uses: `(pin, af, signal)`.
47    pins: Vec<(Pin, u8, &'static str)>,
48}
49
50#[derive(Clone, Copy, PartialEq, Eq)]
51enum Kind {
52    Usart,
53    Spi,
54    I2c,
55    Tim,
56}
57
58/// Generate `nucleus_config.h` and `nucleus_init.c` for `config`.
59pub fn generate(config: &Config, db: &Database) -> Generated {
60    let lowered: Vec<Lowered> = config
61        .peripherals
62        .iter()
63        .filter_map(|(instance, table)| lower(instance, table, db))
64        .collect();
65
66    Generated {
67        config_h: config_header(&lowered),
68        init_c: init_source(config, &lowered),
69    }
70}
71
72fn kind_of(instance: &str) -> Option<Kind> {
73    let prefix = instance.trim_end_matches(|c: char| c.is_ascii_digit());
74    match prefix {
75        "usart" | "uart" => Some(Kind::Usart),
76        "spi" => Some(Kind::Spi),
77        "i2c" | "fmpi2c" => Some(Kind::I2c),
78        "tim" => Some(Kind::Tim),
79        _ => None,
80    }
81}
82
83fn lower(instance: &str, table: &Peripheral, db: &Database) -> Option<Lowered> {
84    let kind = kind_of(instance)?;
85    let roles = model::roles_for(instance)?;
86    let name = model::peripheral_name(instance);
87    // The instance index is the *trailing* digit run, so `i2c1` -> "1" (not
88    // "21" from the embedded "2" in "i2c").
89    let digits: String = {
90        let rev: String = instance
91            .chars()
92            .rev()
93            .take_while(char::is_ascii_digit)
94            .collect();
95        rev.chars().rev().collect()
96    };
97
98    let (handle_prefix, handle_type) = match kind {
99        Kind::Usart => ("huart", "UART_HandleTypeDef"),
100        Kind::Spi => ("hspi", "SPI_HandleTypeDef"),
101        Kind::I2c => ("hi2c", "I2C_HandleTypeDef"),
102        Kind::Tim => ("htim", "TIM_HandleTypeDef"),
103    };
104
105    let mut pins = Vec::new();
106    for role in roles {
107        if let Some(value) = table.pin_str(role.key) {
108            if let Ok(pin) = Pin::from_str(value) {
109                if let Some(af) = db.find_af(pin, &name, role.signal) {
110                    pins.push((pin, af, role.signal));
111                }
112            }
113        }
114    }
115
116    Some(Lowered {
117        config_type: format!("Nucleus_{name}_Config"),
118        handle: format!("{handle_prefix}{digits}"),
119        handle_type,
120        instance: name,
121        kind,
122        pins,
123    })
124}
125
126fn config_header(lowered: &[Lowered]) -> String {
127    let mut s = String::new();
128    s.push_str(GENERATED_BANNER);
129    s.push_str(
130        "#ifndef NUCLEUS_CONFIG_H\n\
131         #define NUCLEUS_CONFIG_H\n\n\
132         #include \"stm32f4xx_hal.h\"\n\n\
133         #ifdef __cplusplus\n\
134         extern \"C\" {\n\
135         #endif\n\n",
136    );
137
138    for p in lowered {
139        let _ = writeln!(s, "/* {} — resolved configuration */", p.instance);
140        let _ = writeln!(s, "typedef struct {{");
141        for field in p.kind.config_fields() {
142            let _ = writeln!(s, "    uint32_t {field};");
143        }
144        let _ = writeln!(s, "}} {};", p.config_type);
145        let _ = writeln!(s, "extern {} {};\n", p.handle_type, p.handle);
146    }
147
148    s.push_str(
149        "/* Initializes every peripheral declared in stm32.toml. Call once after\n\
150         \x20  HAL_Init() and the system clock configuration. */\n\
151         void Nucleus_Init(void);\n\n\
152         #ifdef __cplusplus\n\
153         }\n\
154         #endif\n\n\
155         #endif /* NUCLEUS_CONFIG_H */\n",
156    );
157    s
158}
159
160fn init_source(config: &Config, lowered: &[Lowered]) -> String {
161    let mut s = String::new();
162    s.push_str(GENERATED_BANNER);
163    s.push_str("#include \"nucleus_config.h\"\n\n");
164
165    // Handle definitions.
166    for p in lowered {
167        let _ = writeln!(s, "{} {};", p.handle_type, p.handle);
168    }
169    s.push('\n');
170
171    // Resolved config struct instances (the "typed config" the HAL init reads).
172    for p in lowered {
173        emit_config_instance(&mut s, config, p);
174    }
175
176    s.push_str("void Nucleus_Init(void)\n{\n");
177    s.push_str("    GPIO_InitTypeDef GPIO_InitStruct = {0};\n\n");
178
179    emit_gpio_clock_enables(&mut s, lowered);
180
181    for p in lowered {
182        let _ = writeln!(s, "    /* ---- {} ---- */", p.instance);
183        emit_gpio_config(&mut s, p);
184        emit_peripheral_init(&mut s, p);
185        s.push('\n');
186    }
187
188    s.push_str("}\n");
189    s
190}
191
192fn emit_config_instance(s: &mut String, config: &Config, p: &Lowered) {
193    let var = format!("{}_config", p.instance.to_ascii_lowercase());
194    let table = &config.peripherals[&p.instance.to_ascii_lowercase()];
195    let _ = writeln!(s, "static const {} {} = {{", p.config_type, var);
196    match p.kind {
197        Kind::Usart => {
198            let baud = table
199                .0
200                .get("baud")
201                .and_then(toml::Value::as_integer)
202                .unwrap_or(115_200);
203            let _ = writeln!(s, "    .BaudRate = {baud}u,");
204        }
205        Kind::Spi => {
206            let mode = table
207                .0
208                .get("mode")
209                .and_then(toml::Value::as_integer)
210                .unwrap_or(0);
211            let (cpol, cpha) = spi_mode(mode);
212            let _ = writeln!(s, "    .CLKPolarity = {cpol},");
213            let _ = writeln!(s, "    .CLKPhase = {cpha},");
214        }
215        Kind::I2c => {
216            let speed = table
217                .0
218                .get("speed")
219                .and_then(toml::Value::as_str)
220                .unwrap_or("standard");
221            let hz = if speed.eq_ignore_ascii_case("fast") {
222                400_000
223            } else {
224                100_000
225            };
226            let _ = writeln!(s, "    .ClockSpeed = {hz}u,");
227        }
228        Kind::Tim => {
229            let (psc, arr) = tim_timing(config, table);
230            let _ = writeln!(s, "    .Prescaler = {psc}u,");
231            let _ = writeln!(s, "    .Period = {arr}u,");
232        }
233    }
234    let _ = writeln!(s, "}};\n");
235}
236
237fn emit_gpio_clock_enables(s: &mut String, lowered: &[Lowered]) {
238    let mut ports: Vec<char> = lowered
239        .iter()
240        .flat_map(|p| p.pins.iter().map(|(pin, _, _)| pin.port.letter()))
241        .collect();
242    ports.sort_unstable();
243    ports.dedup();
244    if ports.is_empty() {
245        return;
246    }
247    s.push_str("    /* GPIO port clocks */\n");
248    for port in ports {
249        let _ = writeln!(s, "    __HAL_RCC_GPIO{port}_CLK_ENABLE();");
250    }
251    s.push('\n');
252}
253
254fn emit_gpio_config(s: &mut String, p: &Lowered) {
255    for (pin, af, _signal) in &p.pins {
256        let port = pin.port.letter();
257        let pull = if p.kind == Kind::I2c {
258            "GPIO_PULLUP"
259        } else {
260            "GPIO_NOPULL"
261        };
262        let mode = if p.kind == Kind::I2c {
263            "GPIO_MODE_AF_OD"
264        } else {
265            "GPIO_MODE_AF_PP"
266        };
267        let _ = writeln!(s, "    GPIO_InitStruct.Pin = GPIO_PIN_{};", pin.number);
268        let _ = writeln!(s, "    GPIO_InitStruct.Mode = {mode};");
269        let _ = writeln!(s, "    GPIO_InitStruct.Pull = {pull};");
270        let _ = writeln!(s, "    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;");
271        let _ = writeln!(
272            s,
273            "    GPIO_InitStruct.Alternate = GPIO_AF{af}_{};",
274            p.instance
275        );
276        let _ = writeln!(s, "    HAL_GPIO_Init(GPIO{port}, &GPIO_InitStruct);");
277    }
278}
279
280fn emit_peripheral_init(s: &mut String, p: &Lowered) {
281    let h = &p.handle;
282    let cfg = format!("{}_config", p.instance.to_ascii_lowercase());
283    let _ = writeln!(s, "    __HAL_RCC_{}_CLK_ENABLE();", p.instance);
284    let _ = writeln!(s, "    {h}.Instance = {};", p.instance);
285    match p.kind {
286        Kind::Usart => {
287            let _ = writeln!(s, "    {h}.Init.BaudRate = {cfg}.BaudRate;");
288            for (field, val) in [
289                ("WordLength", "UART_WORDLENGTH_8B"),
290                ("StopBits", "UART_STOPBITS_1"),
291                ("Parity", "UART_PARITY_NONE"),
292                ("Mode", "UART_MODE_TX_RX"),
293                ("HwFlowCtl", "UART_HWCONTROL_NONE"),
294                ("OverSampling", "UART_OVERSAMPLING_16"),
295            ] {
296                let _ = writeln!(s, "    {h}.Init.{field} = {val};");
297            }
298            let _ = writeln!(s, "    HAL_UART_Init(&{h});");
299        }
300        Kind::Spi => {
301            let _ = writeln!(s, "    {h}.Init.CLKPolarity = {cfg}.CLKPolarity;");
302            let _ = writeln!(s, "    {h}.Init.CLKPhase = {cfg}.CLKPhase;");
303            for (field, val) in [
304                ("Mode", "SPI_MODE_MASTER"),
305                ("Direction", "SPI_DIRECTION_2LINES"),
306                ("DataSize", "SPI_DATASIZE_8BIT"),
307                ("NSS", "SPI_NSS_SOFT"),
308                ("BaudRatePrescaler", "SPI_BAUDRATEPRESCALER_16"),
309                ("FirstBit", "SPI_FIRSTBIT_MSB"),
310                ("TIMode", "SPI_TIMODE_DISABLE"),
311                ("CRCCalculation", "SPI_CRCCALCULATION_DISABLE"),
312            ] {
313                let _ = writeln!(s, "    {h}.Init.{field} = {val};");
314            }
315            let _ = writeln!(s, "    HAL_SPI_Init(&{h});");
316        }
317        Kind::I2c => {
318            let _ = writeln!(s, "    {h}.Init.ClockSpeed = {cfg}.ClockSpeed;");
319            for (field, val) in [
320                ("DutyCycle", "I2C_DUTYCYCLE_2"),
321                ("OwnAddress1", "0"),
322                ("AddressingMode", "I2C_ADDRESSINGMODE_7BIT"),
323                ("DualAddressMode", "I2C_DUALADDRESS_DISABLE"),
324                ("OwnAddress2", "0"),
325                ("GeneralCallMode", "I2C_GENERALCALL_DISABLE"),
326                ("NoStretchMode", "I2C_NOSTRETCH_DISABLE"),
327            ] {
328                let _ = writeln!(s, "    {h}.Init.{field} = {val};");
329            }
330            let _ = writeln!(s, "    HAL_I2C_Init(&{h});");
331        }
332        Kind::Tim => {
333            let _ = writeln!(s, "    {h}.Init.Prescaler = {cfg}.Prescaler;");
334            let _ = writeln!(s, "    {h}.Init.Period = {cfg}.Period;");
335            for (field, val) in [
336                ("CounterMode", "TIM_COUNTERMODE_UP"),
337                ("ClockDivision", "TIM_CLOCKDIVISION_DIV1"),
338                ("AutoReloadPreload", "TIM_AUTORELOAD_PRELOAD_ENABLE"),
339            ] {
340                let _ = writeln!(s, "    {h}.Init.{field} = {val};");
341            }
342            let _ = writeln!(s, "    HAL_TIM_PWM_Init(&{h});");
343        }
344    }
345}
346
347impl Kind {
348    fn config_fields(self) -> &'static [&'static str] {
349        match self {
350            Kind::Usart => &["BaudRate"],
351            Kind::Spi => &["CLKPolarity", "CLKPhase"],
352            Kind::I2c => &["ClockSpeed"],
353            Kind::Tim => &["Prescaler", "Period"],
354        }
355    }
356}
357
358/// SPI mode (0–3) → `(CLKPolarity, CLKPhase)` HAL macros.
359fn spi_mode(mode: i64) -> (&'static str, &'static str) {
360    match mode {
361        1 => ("SPI_POLARITY_LOW", "SPI_PHASE_2EDGE"),
362        2 => ("SPI_POLARITY_HIGH", "SPI_PHASE_1EDGE"),
363        3 => ("SPI_POLARITY_HIGH", "SPI_PHASE_2EDGE"),
364        _ => ("SPI_POLARITY_LOW", "SPI_PHASE_1EDGE"),
365    }
366}
367
368/// Resolve a PWM timer's `(Prescaler, Period)` from the requested frequency and
369/// duty resolution. Uses the device `clock_hz` as the timer clock estimate
370/// (an approximation — full clock-tree solving is explicitly out of scope).
371fn tim_timing(config: &Config, table: &Peripheral) -> (u32, u32) {
372    let bits = table
373        .0
374        .get("duty_resolution_bits")
375        .and_then(toml::Value::as_integer)
376        .unwrap_or(16)
377        .clamp(1, 31) as u32;
378    let arr: u32 = (1u32 << bits) - 1;
379
380    let freq = table
381        .0
382        .get("frequency_hz")
383        .and_then(toml::Value::as_integer)
384        .unwrap_or(1000)
385        .max(1) as u64;
386    let timer_clk = config.device.clock_hz.unwrap_or(180_000_000).max(1);
387
388    // freq = timer_clk / ((PSC + 1) * (ARR + 1))  =>  PSC = timer_clk/(freq*(ARR+1)) - 1
389    let divisor = freq * (arr as u64 + 1);
390    let psc = (timer_clk / divisor).saturating_sub(1);
391    (psc.min(u32::MAX as u64) as u32, arr)
392}
393
394const GENERATED_BANNER: &str = "\
395/* Generated by Nucleus — do not edit by hand.\n\
396\x20* Regenerate with `nucleus build`. Source of truth: stm32.toml. */\n\n";
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use crate::config;
402
403    fn gen(text: &str) -> Generated {
404        let cfg = config::parse(text).unwrap();
405        generate(&cfg, &Database::f446re())
406    }
407
408    const EXAMPLE: &str = r#"
409[device]
410family = "STM32F446RE"
411clock_hz = 180_000_000
412
413[peripherals.usart2]
414tx = "PA2"
415rx = "PA3"
416baud = 115200
417
418[peripherals.spi1]
419mosi = "PA7"
420miso = "PA6"
421sck = "PA5"
422nss = "PA4"
423mode = 0
424
425[peripherals.i2c1]
426sda = "PB9"
427scl = "PB8"
428speed = "fast"
429
430[peripherals.tim2]
431channel1 = "PA0"
432frequency_hz = 1000
433duty_resolution_bits = 16
434"#;
435
436    #[test]
437    fn header_declares_handles_and_prototype() {
438        let g = gen(EXAMPLE);
439        assert!(g.config_h.contains("extern UART_HandleTypeDef huart2;"));
440        assert!(g.config_h.contains("typedef struct {"));
441        assert!(g.config_h.contains("void Nucleus_Init(void);"));
442        assert!(g.config_h.contains("#ifndef NUCLEUS_CONFIG_H"));
443    }
444
445    #[test]
446    fn init_calls_stock_hal_init_functions() {
447        let g = gen(EXAMPLE);
448        for call in [
449            "HAL_UART_Init(&huart2);",
450            "HAL_SPI_Init(&hspi1);",
451            "HAL_I2C_Init(&hi2c1);",
452            "HAL_TIM_PWM_Init(&htim2);",
453        ] {
454            assert!(g.init_c.contains(call), "missing {call}\n{}", g.init_c);
455        }
456        // Exactly one init entry point.
457        assert_eq!(g.init_c.matches("void Nucleus_Init(void)").count(), 1);
458    }
459
460    #[test]
461    fn gpio_uses_af_numbers_from_database() {
462        let g = gen(EXAMPLE);
463        // PA2 = USART2_TX is AF7; PA7 = SPI1_MOSI is AF5; PB9 = I2C1_SDA is AF4.
464        assert!(g.init_c.contains("GPIO_InitStruct.Pin = GPIO_PIN_2;"));
465        assert!(g.init_c.contains("GPIO_AF7_USART2;"));
466        assert!(g.init_c.contains("GPIO_AF5_SPI1;"));
467        assert!(g.init_c.contains("GPIO_AF4_I2C1;"));
468    }
469
470    #[test]
471    fn enables_each_gpio_port_clock_once() {
472        let g = gen(EXAMPLE);
473        assert_eq!(g.init_c.matches("__HAL_RCC_GPIOA_CLK_ENABLE();").count(), 1);
474        assert_eq!(g.init_c.matches("__HAL_RCC_GPIOB_CLK_ENABLE();").count(), 1);
475    }
476
477    #[test]
478    fn i2c_pins_are_open_drain_with_pullups() {
479        let g = gen(EXAMPLE);
480        assert!(g.init_c.contains("GPIO_MODE_AF_OD"));
481        assert!(g.init_c.contains("GPIO_PULLUP"));
482    }
483
484    #[test]
485    fn resolved_params_land_in_config_structs() {
486        let g = gen(EXAMPLE);
487        assert!(g.init_c.contains(".BaudRate = 115200u,"));
488        assert!(g.init_c.contains(".ClockSpeed = 400000u,")); // fast
489        assert!(g.init_c.contains(".CLKPolarity = SPI_POLARITY_LOW,")); // mode 0
490    }
491
492    #[test]
493    fn output_is_deterministic() {
494        assert_eq!(gen(EXAMPLE), gen(EXAMPLE));
495    }
496
497    #[test]
498    fn empty_config_still_emits_valid_init() {
499        let g = gen("[device]\nfamily = \"STM32F446RE\"\n");
500        assert!(g.init_c.contains("void Nucleus_Init(void)"));
501        assert!(g.config_h.contains("void Nucleus_Init(void);"));
502    }
503}