use super::ir::{MicrochipDir, MicrochipIoMuxPad, MicrochipIr};
use anyhow::{Context, Result, anyhow};
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/atsamd51j19a.x.jinja");
#[derive(Serialize, Debug, Clone)]
pub struct PadRoute {
pub pad: String,
pub group: u8,
pub pin: u8,
pub signal: String,
pub peripheral: Option<String>,
pub direction: String,
pub pull: Option<String>,
pub label: Option<String>,
pub pmux_letter: Option<String>,
pub pmux_bits: Option<u8>,
pub pmux_enable: bool,
pub unmatched_peripheral: bool,
pub pmux_odd: bool,
pub pmux_half: u8,
}
pub fn render_microchip_pac(ir: &MicrochipIr, 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 chip_link_stem = chip_link_stem(&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 pad_routes = resolve_pad_routes(ir)?;
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();
let chip_x_name = format!("{chip_link_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),
pad_routes => Value::from_serialize(&pad_routes),
board_stem => board_stem.clone(),
chip_stem => chip_stem,
chip_link_stem => chip_link_stem.clone(),
};
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: &MicrochipIr) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for pin in &ir.pins {
if let Some(p) = pin.peripheral.as_deref()
&& !out.iter().any(|s| s == p)
{
out.push(p.to_string());
}
}
out
}
fn resolve_pad_routes(ir: &MicrochipIr) -> Result<Vec<PadRoute>> {
ir.pins
.iter()
.map(|pin| {
let pad_entry = ir
.chip
.io_mux
.iter()
.find(|p| p.pad == pin.pad)
.ok_or_else(|| {
anyhow!(
"board '{}' pad '{}' not found in chip '{}' io_mux table",
ir.board.name,
pin.pad,
ir.chip.name
)
})?;
let (pmux_letter, pmux_bits, pmux_enable) =
pmux_lookup_for_signal(pad_entry, &pin.signal);
let unmatched_peripheral =
!pmux_enable && pin.peripheral.is_some() && is_peripheral_signal(&pin.signal);
Ok(PadRoute {
pad: pin.pad.clone(),
group: pad_entry.group,
pin: pad_entry.pin,
signal: pin.signal.clone(),
peripheral: pin.peripheral.clone(),
direction: dir_to_str(pin.direction).to_string(),
pull: pin.pull.clone(),
label: pin.label.clone(),
pmux_letter,
pmux_bits,
pmux_enable,
unmatched_peripheral,
pmux_odd: pad_entry.pin % 2 == 1,
pmux_half: pad_entry.pin / 2,
})
})
.collect()
}
fn pmux_lookup_for_signal(
pad: &MicrochipIoMuxPad,
signal: &str,
) -> (Option<String>, Option<u8>, bool) {
let columns: [(&str, u8, &Option<String>); 9] = [
("A", 0x0, &pad.fn_a),
("B", 0x1, &pad.fn_b),
("C", 0x2, &pad.fn_c),
("D", 0x3, &pad.fn_d),
("E", 0x4, &pad.fn_e),
("F", 0x5, &pad.fn_f),
("G", 0x6, &pad.fn_g),
("H", 0x7, &pad.fn_h),
("N", 0xf, &pad.fn_n),
];
for (letter, bits, slot) in columns {
if let Some(name) = slot.as_deref()
&& name.eq_ignore_ascii_case(signal)
{
return (Some(letter.to_string()), Some(bits), true);
}
}
(None, None, false)
}
fn is_peripheral_signal(signal: &str) -> bool {
const PERIPHERAL_PREFIXES: &[&str] = &[
"SERCOM", "TCC", "TC", "ADC", "DAC", "USB", "EIC", "GCLK_IO", "CM4_", "AC_",
];
PERIPHERAL_PREFIXES.iter().any(|p| signal.starts_with(p))
}
fn dir_to_str(d: MicrochipDir) -> &'static str {
match d {
MicrochipDir::In => "in",
MicrochipDir::Out => "out",
MicrochipDir::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);
out.push_str("()");
}
out
}
fn chip_link_stem(input: &str) -> String {
input
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.flat_map(|c| c.to_lowercase())
.collect()
}
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("Adafruit Feather M4 Express"),
"adafruit_feather_m4_express"
);
assert_eq!(snake_case("ATSAMD51J19A"), "atsamd51_j19_a");
assert_eq!(snake_case("sercom0"), "sercom0");
}
#[test]
fn chip_link_stem_strips_separators_and_lowercases() {
assert_eq!(chip_link_stem("ATSAMD51J19A"), "atsamd51j19a");
assert_eq!(chip_link_stem("ATSAMD21J18A"), "atsamd21j18a");
assert_eq!(chip_link_stem("ATSAML21J18B"), "atsaml21j18b");
assert_eq!(chip_link_stem("ATSAMD51-J19A"), "atsamd51j19a");
}
#[test]
fn pac_path_filter_uppercases_instance_and_methods_registers() {
assert_eq!(pac_path_filter("mclk.apbamask".into()), "MCLK.apbamask()");
assert_eq!(pac_path_filter("gclk".into()), "GCLK");
assert_eq!(
pac_path_filter("port.group0.pmux0".into()),
"PORT.group0().pmux0()"
);
}
#[test]
fn pmux_lookup_matches_letter() {
let pad = MicrochipIoMuxPad {
pad: "PB16".into(),
group: 1,
pin: 16,
fn_a: Some("EIC_EXTINT_0".into()),
fn_b: None,
fn_c: Some("SERCOM5_PAD0".into()),
fn_d: None,
fn_e: Some("TC6_WO0".into()),
fn_f: None,
fn_g: None,
fn_h: None,
fn_n: Some("GCLK_IO2".into()),
analog: None,
};
assert_eq!(
pmux_lookup_for_signal(&pad, "SERCOM5_PAD0"),
(Some("C".to_string()), Some(0x2), true)
);
assert_eq!(
pmux_lookup_for_signal(&pad, "GCLK_IO2"),
(Some("N".to_string()), Some(0xf), true)
);
assert_eq!(pmux_lookup_for_signal(&pad, "GPIO"), (None, None, false));
}
}