use std::fmt::Write;
use std::str::FromStr;
use nucleus_db::{Database, Pin};
use crate::config::{Config, Peripheral};
use crate::model;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Generated {
pub config_h: String,
pub init_c: String,
}
struct Lowered {
instance: String,
handle: String,
handle_type: &'static str,
config_type: String,
kind: Kind,
pins: Vec<(Pin, u8, &'static str)>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Usart,
Spi,
I2c,
Tim,
}
pub fn generate(config: &Config, db: &Database) -> Generated {
let lowered: Vec<Lowered> = config
.peripherals
.iter()
.filter_map(|(instance, table)| lower(instance, table, db))
.collect();
Generated {
config_h: config_header(&lowered),
init_c: init_source(config, &lowered),
}
}
fn kind_of(instance: &str) -> Option<Kind> {
let prefix = instance.trim_end_matches(|c: char| c.is_ascii_digit());
match prefix {
"usart" | "uart" => Some(Kind::Usart),
"spi" => Some(Kind::Spi),
"i2c" | "fmpi2c" => Some(Kind::I2c),
"tim" => Some(Kind::Tim),
_ => None,
}
}
fn lower(instance: &str, table: &Peripheral, db: &Database) -> Option<Lowered> {
let kind = kind_of(instance)?;
let roles = model::roles_for(instance)?;
let name = model::peripheral_name(instance);
let digits: String = {
let rev: String = instance
.chars()
.rev()
.take_while(char::is_ascii_digit)
.collect();
rev.chars().rev().collect()
};
let (handle_prefix, handle_type) = match kind {
Kind::Usart => ("huart", "UART_HandleTypeDef"),
Kind::Spi => ("hspi", "SPI_HandleTypeDef"),
Kind::I2c => ("hi2c", "I2C_HandleTypeDef"),
Kind::Tim => ("htim", "TIM_HandleTypeDef"),
};
let mut pins = Vec::new();
for role in roles {
if let Some(value) = table.pin_str(role.key) {
if let Ok(pin) = Pin::from_str(value) {
if let Some(af) = db.find_af(pin, &name, role.signal) {
pins.push((pin, af, role.signal));
}
}
}
}
Some(Lowered {
config_type: format!("Nucleus_{name}_Config"),
handle: format!("{handle_prefix}{digits}"),
handle_type,
instance: name,
kind,
pins,
})
}
fn config_header(lowered: &[Lowered]) -> String {
let mut s = String::new();
s.push_str(GENERATED_BANNER);
s.push_str(
"#ifndef NUCLEUS_CONFIG_H\n\
#define NUCLEUS_CONFIG_H\n\n\
#include \"stm32f4xx_hal.h\"\n\n\
#ifdef __cplusplus\n\
extern \"C\" {\n\
#endif\n\n",
);
for p in lowered {
let _ = writeln!(s, "/* {} — resolved configuration */", p.instance);
let _ = writeln!(s, "typedef struct {{");
for field in p.kind.config_fields() {
let _ = writeln!(s, " uint32_t {field};");
}
let _ = writeln!(s, "}} {};", p.config_type);
let _ = writeln!(s, "extern {} {};\n", p.handle_type, p.handle);
}
s.push_str(
"/* Initializes every peripheral declared in stm32.toml. Call once after\n\
\x20 HAL_Init() and the system clock configuration. */\n\
void Nucleus_Init(void);\n\n\
#ifdef __cplusplus\n\
}\n\
#endif\n\n\
#endif /* NUCLEUS_CONFIG_H */\n",
);
s
}
fn init_source(config: &Config, lowered: &[Lowered]) -> String {
let mut s = String::new();
s.push_str(GENERATED_BANNER);
s.push_str("#include \"nucleus_config.h\"\n\n");
for p in lowered {
let _ = writeln!(s, "{} {};", p.handle_type, p.handle);
}
s.push('\n');
for p in lowered {
emit_config_instance(&mut s, config, p);
}
s.push_str("void Nucleus_Init(void)\n{\n");
s.push_str(" GPIO_InitTypeDef GPIO_InitStruct = {0};\n\n");
emit_gpio_clock_enables(&mut s, lowered);
for p in lowered {
let _ = writeln!(s, " /* ---- {} ---- */", p.instance);
emit_gpio_config(&mut s, p);
emit_peripheral_init(&mut s, p);
s.push('\n');
}
s.push_str("}\n");
s
}
fn emit_config_instance(s: &mut String, config: &Config, p: &Lowered) {
let var = format!("{}_config", p.instance.to_ascii_lowercase());
let table = &config.peripherals[&p.instance.to_ascii_lowercase()];
let _ = writeln!(s, "static const {} {} = {{", p.config_type, var);
match p.kind {
Kind::Usart => {
let baud = table
.0
.get("baud")
.and_then(toml::Value::as_integer)
.unwrap_or(115_200);
let _ = writeln!(s, " .BaudRate = {baud}u,");
}
Kind::Spi => {
let mode = table
.0
.get("mode")
.and_then(toml::Value::as_integer)
.unwrap_or(0);
let (cpol, cpha) = spi_mode(mode);
let _ = writeln!(s, " .CLKPolarity = {cpol},");
let _ = writeln!(s, " .CLKPhase = {cpha},");
}
Kind::I2c => {
let speed = table
.0
.get("speed")
.and_then(toml::Value::as_str)
.unwrap_or("standard");
let hz = if speed.eq_ignore_ascii_case("fast") {
400_000
} else {
100_000
};
let _ = writeln!(s, " .ClockSpeed = {hz}u,");
}
Kind::Tim => {
let (psc, arr) = tim_timing(config, table);
let _ = writeln!(s, " .Prescaler = {psc}u,");
let _ = writeln!(s, " .Period = {arr}u,");
}
}
let _ = writeln!(s, "}};\n");
}
fn emit_gpio_clock_enables(s: &mut String, lowered: &[Lowered]) {
let mut ports: Vec<char> = lowered
.iter()
.flat_map(|p| p.pins.iter().map(|(pin, _, _)| pin.port.letter()))
.collect();
ports.sort_unstable();
ports.dedup();
if ports.is_empty() {
return;
}
s.push_str(" /* GPIO port clocks */\n");
for port in ports {
let _ = writeln!(s, " __HAL_RCC_GPIO{port}_CLK_ENABLE();");
}
s.push('\n');
}
fn emit_gpio_config(s: &mut String, p: &Lowered) {
for (pin, af, _signal) in &p.pins {
let port = pin.port.letter();
let pull = if p.kind == Kind::I2c {
"GPIO_PULLUP"
} else {
"GPIO_NOPULL"
};
let mode = if p.kind == Kind::I2c {
"GPIO_MODE_AF_OD"
} else {
"GPIO_MODE_AF_PP"
};
let _ = writeln!(s, " GPIO_InitStruct.Pin = GPIO_PIN_{};", pin.number);
let _ = writeln!(s, " GPIO_InitStruct.Mode = {mode};");
let _ = writeln!(s, " GPIO_InitStruct.Pull = {pull};");
let _ = writeln!(s, " GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;");
let _ = writeln!(
s,
" GPIO_InitStruct.Alternate = GPIO_AF{af}_{};",
p.instance
);
let _ = writeln!(s, " HAL_GPIO_Init(GPIO{port}, &GPIO_InitStruct);");
}
}
fn emit_peripheral_init(s: &mut String, p: &Lowered) {
let h = &p.handle;
let cfg = format!("{}_config", p.instance.to_ascii_lowercase());
let _ = writeln!(s, " __HAL_RCC_{}_CLK_ENABLE();", p.instance);
let _ = writeln!(s, " {h}.Instance = {};", p.instance);
match p.kind {
Kind::Usart => {
let _ = writeln!(s, " {h}.Init.BaudRate = {cfg}.BaudRate;");
for (field, val) in [
("WordLength", "UART_WORDLENGTH_8B"),
("StopBits", "UART_STOPBITS_1"),
("Parity", "UART_PARITY_NONE"),
("Mode", "UART_MODE_TX_RX"),
("HwFlowCtl", "UART_HWCONTROL_NONE"),
("OverSampling", "UART_OVERSAMPLING_16"),
] {
let _ = writeln!(s, " {h}.Init.{field} = {val};");
}
let _ = writeln!(s, " HAL_UART_Init(&{h});");
}
Kind::Spi => {
let _ = writeln!(s, " {h}.Init.CLKPolarity = {cfg}.CLKPolarity;");
let _ = writeln!(s, " {h}.Init.CLKPhase = {cfg}.CLKPhase;");
for (field, val) in [
("Mode", "SPI_MODE_MASTER"),
("Direction", "SPI_DIRECTION_2LINES"),
("DataSize", "SPI_DATASIZE_8BIT"),
("NSS", "SPI_NSS_SOFT"),
("BaudRatePrescaler", "SPI_BAUDRATEPRESCALER_16"),
("FirstBit", "SPI_FIRSTBIT_MSB"),
("TIMode", "SPI_TIMODE_DISABLE"),
("CRCCalculation", "SPI_CRCCALCULATION_DISABLE"),
] {
let _ = writeln!(s, " {h}.Init.{field} = {val};");
}
let _ = writeln!(s, " HAL_SPI_Init(&{h});");
}
Kind::I2c => {
let _ = writeln!(s, " {h}.Init.ClockSpeed = {cfg}.ClockSpeed;");
for (field, val) in [
("DutyCycle", "I2C_DUTYCYCLE_2"),
("OwnAddress1", "0"),
("AddressingMode", "I2C_ADDRESSINGMODE_7BIT"),
("DualAddressMode", "I2C_DUALADDRESS_DISABLE"),
("OwnAddress2", "0"),
("GeneralCallMode", "I2C_GENERALCALL_DISABLE"),
("NoStretchMode", "I2C_NOSTRETCH_DISABLE"),
] {
let _ = writeln!(s, " {h}.Init.{field} = {val};");
}
let _ = writeln!(s, " HAL_I2C_Init(&{h});");
}
Kind::Tim => {
let _ = writeln!(s, " {h}.Init.Prescaler = {cfg}.Prescaler;");
let _ = writeln!(s, " {h}.Init.Period = {cfg}.Period;");
for (field, val) in [
("CounterMode", "TIM_COUNTERMODE_UP"),
("ClockDivision", "TIM_CLOCKDIVISION_DIV1"),
("AutoReloadPreload", "TIM_AUTORELOAD_PRELOAD_ENABLE"),
] {
let _ = writeln!(s, " {h}.Init.{field} = {val};");
}
let _ = writeln!(s, " HAL_TIM_PWM_Init(&{h});");
}
}
}
impl Kind {
fn config_fields(self) -> &'static [&'static str] {
match self {
Kind::Usart => &["BaudRate"],
Kind::Spi => &["CLKPolarity", "CLKPhase"],
Kind::I2c => &["ClockSpeed"],
Kind::Tim => &["Prescaler", "Period"],
}
}
}
fn spi_mode(mode: i64) -> (&'static str, &'static str) {
match mode {
1 => ("SPI_POLARITY_LOW", "SPI_PHASE_2EDGE"),
2 => ("SPI_POLARITY_HIGH", "SPI_PHASE_1EDGE"),
3 => ("SPI_POLARITY_HIGH", "SPI_PHASE_2EDGE"),
_ => ("SPI_POLARITY_LOW", "SPI_PHASE_1EDGE"),
}
}
fn tim_timing(config: &Config, table: &Peripheral) -> (u32, u32) {
let bits = table
.0
.get("duty_resolution_bits")
.and_then(toml::Value::as_integer)
.unwrap_or(16)
.clamp(1, 31) as u32;
let arr: u32 = (1u32 << bits) - 1;
let freq = table
.0
.get("frequency_hz")
.and_then(toml::Value::as_integer)
.unwrap_or(1000)
.max(1) as u64;
let timer_clk = config.device.clock_hz.unwrap_or(180_000_000).max(1);
let divisor = freq * (arr as u64 + 1);
let psc = (timer_clk / divisor).saturating_sub(1);
(psc.min(u32::MAX as u64) as u32, arr)
}
const GENERATED_BANNER: &str = "\
/* Generated by Nucleus — do not edit by hand.\n\
\x20* Regenerate with `nucleus build`. Source of truth: stm32.toml. */\n\n";
#[cfg(test)]
mod tests {
use super::*;
use crate::config;
fn gen(text: &str) -> Generated {
let cfg = config::parse(text).unwrap();
generate(&cfg, &Database::f446re())
}
const EXAMPLE: &str = r#"
[device]
family = "STM32F446RE"
clock_hz = 180_000_000
[peripherals.usart2]
tx = "PA2"
rx = "PA3"
baud = 115200
[peripherals.spi1]
mosi = "PA7"
miso = "PA6"
sck = "PA5"
nss = "PA4"
mode = 0
[peripherals.i2c1]
sda = "PB9"
scl = "PB8"
speed = "fast"
[peripherals.tim2]
channel1 = "PA0"
frequency_hz = 1000
duty_resolution_bits = 16
"#;
#[test]
fn header_declares_handles_and_prototype() {
let g = gen(EXAMPLE);
assert!(g.config_h.contains("extern UART_HandleTypeDef huart2;"));
assert!(g.config_h.contains("typedef struct {"));
assert!(g.config_h.contains("void Nucleus_Init(void);"));
assert!(g.config_h.contains("#ifndef NUCLEUS_CONFIG_H"));
}
#[test]
fn init_calls_stock_hal_init_functions() {
let g = gen(EXAMPLE);
for call in [
"HAL_UART_Init(&huart2);",
"HAL_SPI_Init(&hspi1);",
"HAL_I2C_Init(&hi2c1);",
"HAL_TIM_PWM_Init(&htim2);",
] {
assert!(g.init_c.contains(call), "missing {call}\n{}", g.init_c);
}
assert_eq!(g.init_c.matches("void Nucleus_Init(void)").count(), 1);
}
#[test]
fn gpio_uses_af_numbers_from_database() {
let g = gen(EXAMPLE);
assert!(g.init_c.contains("GPIO_InitStruct.Pin = GPIO_PIN_2;"));
assert!(g.init_c.contains("GPIO_AF7_USART2;"));
assert!(g.init_c.contains("GPIO_AF5_SPI1;"));
assert!(g.init_c.contains("GPIO_AF4_I2C1;"));
}
#[test]
fn enables_each_gpio_port_clock_once() {
let g = gen(EXAMPLE);
assert_eq!(g.init_c.matches("__HAL_RCC_GPIOA_CLK_ENABLE();").count(), 1);
assert_eq!(g.init_c.matches("__HAL_RCC_GPIOB_CLK_ENABLE();").count(), 1);
}
#[test]
fn i2c_pins_are_open_drain_with_pullups() {
let g = gen(EXAMPLE);
assert!(g.init_c.contains("GPIO_MODE_AF_OD"));
assert!(g.init_c.contains("GPIO_PULLUP"));
}
#[test]
fn resolved_params_land_in_config_structs() {
let g = gen(EXAMPLE);
assert!(g.init_c.contains(".BaudRate = 115200u,"));
assert!(g.init_c.contains(".ClockSpeed = 400000u,")); assert!(g.init_c.contains(".CLKPolarity = SPI_POLARITY_LOW,")); }
#[test]
fn output_is_deterministic() {
assert_eq!(gen(EXAMPLE), gen(EXAMPLE));
}
#[test]
fn empty_config_still_emits_valid_init() {
let g = gen("[device]\nfamily = \"STM32F446RE\"\n");
assert!(g.init_c.contains("void Nucleus_Init(void)"));
assert!(g.config_h.contains("void Nucleus_Init(void);"));
}
}