use super::ir::{EspDir, EspIr};
use anyhow::{Context, Result};
use minijinja::{Environment, Value, context};
use serde::Serialize;
use std::path::Path;
const TPL_MOD: &str = include_str!("templates/mod.rs.jinja");
const TPL_PAC: &str = include_str!("templates/pac.rs.jinja");
const TPL_CLOCKS: &str = include_str!("templates/clocks.rs.jinja");
const TPL_IO_MUX: &str = include_str!("templates/io_mux.rs.jinja");
const TPL_PERIPHS: &str = include_str!("templates/peripherals.rs.jinja");
const TPL_BOARD: &str = include_str!("templates/board.rs.jinja");
const TPL_MEMORY_X: &str = include_str!("templates/memory.x.jinja");
const TPL_CHIP_X: &str = include_str!("templates/chip.x.jinja");
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PinRouteKind {
Direct {
fn_slot: u8,
},
Matrix {
signal_id: u16,
},
Plain,
}
#[derive(Serialize, Debug, Clone)]
pub struct PinRoute {
pub gpio: u8,
pub signal: String,
pub peripheral: Option<String>,
pub direction: String,
pub pull: Option<String>,
pub label: Option<String>,
pub route: PinRouteKind,
}
pub fn render_esp_pac(ir: &EspIr, out_dir: &Path) -> Result<Vec<std::path::PathBuf>> {
let board_stem = snake_case(&ir.board.name);
let chip_stem = snake_case(&ir.chip.name);
let target = out_dir.join(&board_stem);
std::fs::create_dir_all(&target).with_context(|| format!("create {}", target.display()))?;
let peripherals_used = peripherals_used(ir);
let pin_routes = resolve_pin_routes(ir);
let shim_instances = shim_instances(ir, &peripherals_used);
let mut env = Environment::new();
env.add_filter("pac_path", pac_path_filter);
env.add_filter("hex32", hex32_filter);
env.add_template("mod.rs", TPL_MOD)?;
env.add_template("pac.rs", TPL_PAC)?;
env.add_template("clocks.rs", TPL_CLOCKS)?;
env.add_template("io_mux.rs", TPL_IO_MUX)?;
env.add_template("peripherals.rs", TPL_PERIPHS)?;
env.add_template("board.rs", TPL_BOARD)?;
let emit_linker = ir.chip.linker.is_some() && ir.chip.arch.starts_with("rv32");
let chip_x_name = format!("{chip_stem}.x");
if emit_linker {
env.add_template("memory.x", TPL_MEMORY_X)?;
env.add_template("chip.x", TPL_CHIP_X)?;
}
let ctx = context! {
ir => Value::from_serialize(ir),
peripherals_used => Value::from_serialize(&peripherals_used),
pin_routes => Value::from_serialize(&pin_routes),
shim_instances => Value::from_serialize(&shim_instances),
board_stem => board_stem.clone(),
chip_stem => chip_stem,
};
let mut files: Vec<String> = [
"mod.rs",
"pac.rs",
"clocks.rs",
"io_mux.rs",
"peripherals.rs",
"board.rs",
]
.iter()
.map(|s| s.to_string())
.collect();
if emit_linker {
files.push("memory.x".to_string());
files.push("chip.x".to_string());
}
let mut written = Vec::new();
for name in &files {
let tmpl = env.get_template(name)?;
let rendered = tmpl
.render(&ctx)
.with_context(|| format!("render {name}"))?;
let out_name: &str = if name == "chip.x" { &chip_x_name } else { name };
let path = target.join(out_name);
std::fs::write(&path, rendered).with_context(|| format!("write {}", path.display()))?;
written.push(path);
}
Ok(written)
}
fn peripherals_used(ir: &EspIr) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for pin in &ir.pins {
if let Some(p) = pin.peripheral.as_deref() {
if !out.iter().any(|s| s == p) {
out.push(p.to_string());
}
}
}
out
}
fn shim_instances(ir: &EspIr, peripherals_used: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let push = |name: String, out: &mut Vec<String>| {
if !out.iter().any(|s| s == &name) {
out.push(name);
}
};
push("IO_MUX".to_string(), &mut out);
push("GPIO".to_string(), &mut out);
for p in peripherals_used {
push(p.to_ascii_uppercase(), &mut out);
}
for name in peripherals_used {
if let Some(gate) = ir.chip.clock_tree.system_gates.get(name) {
for reg_path in [
Some(gate.clk_en_reg.as_str()),
Some(gate.rst_reg.as_str()),
gate.clk_sel_reg.as_deref(),
]
.into_iter()
.flatten()
{
if let Some(first) = reg_path.split('.').next() {
push(first.to_ascii_uppercase(), &mut out);
}
}
}
}
out
}
fn resolve_pin_routes(ir: &EspIr) -> Vec<PinRoute> {
ir.pins
.iter()
.map(|pin| {
let role_hint = pin_role_hint(&pin.signal, pin.peripheral.as_deref());
let route = match pin.peripheral.as_deref() {
Some(periph_name) => ir
.chip
.peripherals
.get(periph_name)
.map(|periph| {
pick_route_for_signal(periph, pin.gpio, pin.direction, role_hint.as_deref())
})
.unwrap_or(PinRouteKind::Plain),
None => PinRouteKind::Plain,
};
PinRoute {
gpio: pin.gpio,
signal: pin.signal.clone(),
peripheral: pin.peripheral.clone(),
direction: dir_to_str(pin.direction).to_string(),
pull: pin.pull.clone(),
label: pin.label.clone(),
route,
}
})
.collect()
}
fn pin_role_hint(signal: &str, peripheral: Option<&str>) -> Option<String> {
if signal.is_empty() {
return None;
}
let lowered = signal.to_ascii_lowercase();
if let Some(p) = peripheral {
let prefix = format!("{}_", p.to_ascii_lowercase());
if let Some(rest) = lowered.strip_prefix(&prefix) {
return Some(rest.to_string());
}
}
if let Some(idx) = lowered.rfind('_') {
return Some(lowered[idx + 1..].to_string());
}
Some(lowered)
}
fn pick_route_for_signal(
periph: &super::ir::EspPeripheral,
gpio: u8,
direction: EspDir,
role_hint: Option<&str>,
) -> PinRouteKind {
for sig in &periph.signals {
if !direction_compatible(direction, sig.direction) {
continue;
}
if sig.iomux_pin == Some(gpio) {
if let Some(slot) = sig.iomux_fn {
return PinRouteKind::Direct { fn_slot: slot };
}
}
}
if let Some(hint) = role_hint {
for sig in &periph.signals {
if !direction_compatible(direction, sig.direction) {
continue;
}
if sig.role.eq_ignore_ascii_case(hint) {
if let Some(id) = sig.gpio_matrix_id {
return PinRouteKind::Matrix { signal_id: id };
}
}
}
}
for sig in &periph.signals {
if !direction_compatible(direction, sig.direction) {
continue;
}
if let Some(id) = sig.gpio_matrix_id {
return PinRouteKind::Matrix { signal_id: id };
}
}
PinRouteKind::Plain
}
fn direction_compatible(pin: EspDir, sig: EspDir) -> bool {
match (pin, sig) {
(EspDir::Inout, _) | (_, EspDir::Inout) => true,
(a, b) => a == b,
}
}
fn dir_to_str(d: EspDir) -> &'static str {
match d {
EspDir::In => "in",
EspDir::Out => "out",
EspDir::Inout => "inout",
}
}
fn hex32_filter(value: u32) -> String {
format!("0x{value:08X}")
}
fn pac_path_filter(value: String) -> String {
let mut segments = value.split('.');
let mut out = match segments.next() {
Some(first) => first.to_ascii_uppercase(),
None => return String::new(),
};
for rest in segments {
out.push('.');
out.push_str(rest);
if !rest.ends_with(')') {
out.push_str("()");
}
}
out
}
fn snake_case(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut prev_was_lower = false;
for ch in input.chars() {
if ch.is_ascii_alphanumeric() {
if ch.is_ascii_uppercase() {
if prev_was_lower {
out.push('_');
}
out.extend(ch.to_lowercase());
prev_was_lower = false;
} else {
out.push(ch);
prev_was_lower = true;
}
} else {
if !out.ends_with('_') && !out.is_empty() {
out.push('_');
}
prev_was_lower = false;
}
}
while out.ends_with('_') {
out.pop();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_case_handles_mixed_separators() {
assert_eq!(snake_case("ESP32-C3-DevKitM-1"), "esp32_c3_dev_kit_m_1");
assert_eq!(snake_case("ESP32-C3"), "esp32_c3");
assert_eq!(snake_case("uart0"), "uart0");
}
#[test]
fn pac_path_filter_uppercases_instance_and_method_chains_registers() {
assert_eq!(
pac_path_filter("system.perip_clk_en0".into()),
"SYSTEM.perip_clk_en0()"
);
assert_eq!(pac_path_filter("gpio".into()), "GPIO");
assert_eq!(pac_path_filter("uart0.conf0".into()), "UART0.conf0()");
}
#[test]
fn pac_path_filter_preserves_cluster_accessor_parens() {
assert_eq!(
pac_path_filter("pcr.uart(0).conf".into()),
"PCR.uart(0).conf()"
);
assert_eq!(
pac_path_filter("pcr.uart(0).clk_conf".into()),
"PCR.uart(0).clk_conf()"
);
}
}