rlvgl 0.2.4

A modular, idiomatic Rust reimplementation of the LVGL graphics library for embedded and simulator use.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Rendering pipeline for Microchip SAM BSP code generation.
//!
//! Consumes a [`MicrochipIr`] produced by [`super::merge`], builds a
//! MiniJinja context enriched with precomputed pin routing and peripheral
//! usage helpers, then renders each of the six PAC-style templates
//! (`mod.rs`, `pac.rs`, `clocks.rs`, `io_mux.rs`, `peripherals.rs`,
//! `board.rs`) into `out_dir/<board_stem>/` per CHIPS-MICROCHIP-00 §6
//! INV-MC6. Two linker fragments (`memory.x` and `<chip_link_stem>.x`,
//! concretely `atsamd51j19a.x` for ATSAMD51J19A) are emitted alongside
//! when the chip yaml carries a `linker:` block, per CHIPS-MICROCHIP-05
//! §5.1. The `<chip_link_stem>.x` slot is intentionally empty in v0
//! (CHIPS-MICROCHIP-05 §5.3); it reserves the linker file name so
//! future chips that need additional `SECTIONS` directives can
//! populate the template without an emission-shape Standards Action.
//!
//! Templates are embedded via `include_str!` so the rendered BSP does not
//! depend on the filesystem layout of the creator crate.

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");

/// A resolved per-pad routing decision for the render templates.
///
/// One [`PadRoute`] is produced per entry in [`MicrochipIr::pins`]. The
/// `pmux_letter` is the function-letter looked up against the chip yaml's
/// `io_mux:` table (e.g. signal `SERCOM5_PAD0` on pad `PB16` matches
/// `fn_c: SERCOM5_PAD0` → letter `"C"`). `pmux_bits` is the 4-bit field
/// value (`A=0, B=1, ..., H=7, N=0xf`) the template writes into the PMUX
/// register.
#[derive(Serialize, Debug, Clone)]
pub struct PadRoute {
    /// Pad name in `PAxx` form.
    pub pad: String,
    /// PORT group index (0=A, 1=B, 2=C, 3=D).
    pub group: u8,
    /// Pin index within the group (0..31).
    pub pin: u8,
    /// Signal name from the board spec.
    pub signal: String,
    /// Owning peripheral instance if any.
    pub peripheral: Option<String>,
    /// Pin direction lower-cased for template matching.
    pub direction: String,
    /// Optional pull configuration (`"up"`, `"down"`, or null).
    pub pull: Option<String>,
    /// Optional label used for generated `pub const` names.
    pub label: Option<String>,
    /// PMUX function letter ("A".."H", "N") or null for plain GPIO pins.
    pub pmux_letter: Option<String>,
    /// PMUX 4-bit field value, or null for plain GPIO.
    pub pmux_bits: Option<u8>,
    /// True when this pad should have PMUX enabled (peripheral function).
    pub pmux_enable: bool,
    /// True when the board YAML claims this pad carries a peripheral
    /// signal (`SERCOMn_PADm`, `USB_DM`, etc.) but the chip YAML's
    /// `io_mux.fn_<letter>:` columns do not list that signal — i.e. the
    /// chipdb is internally inconsistent for this pad. The renderer
    /// emits a fallback PINCFG-only sequence flagged with a
    /// `// MISMATCH:` comment so reviewers can spot it.
    pub unmatched_peripheral: bool,
    /// True when `pin` is odd → write `pmuxo` field; false → write `pmuxe`.
    pub pmux_odd: bool,
    /// Index for `port.group(g).pmux(half)`, equal to `pin / 2`.
    pub pmux_half: u8,
}

/// Render a full PAC-style BSP for the given [`MicrochipIr`] under `out_dir`.
///
/// Creates `out_dir/<board_stem>/{mod,pac,clocks,io_mux,peripherals,board}.rs`
/// (always six files), plus the linker fragments `memory.x` and
/// `<chip_link_stem>.x` when the chip yaml carries a `linker:` block
/// (per CHIPS-MICROCHIP-05 §5.1). `board_stem` is the snake-cased
/// board name; `chip_link_stem` is the lowercase-no-separator form of
/// the chip name (concretely `atsamd51j19a` for `ATSAMD51J19A`).
///
/// # Errors
/// Returns any I/O failure creating the output directory or writing
/// files, and any MiniJinja rendering failure.
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)
}

/// Return the ordered list of peripheral instances this board uses.
///
/// Deduplicated while preserving first-seen order so snapshot output is
/// stable across runs.
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
}

/// Pre-resolve every board pin assignment into a [`PadRoute`].
///
/// Looks up each pin's owning pad in the chip's `io_mux:` table, then
/// scans the eight function columns (A..H plus N) for the one whose
/// signal name matches `pin.signal`. Records the letter + 4-bit field
/// value for the template to emit. Pads with `signal: GPIO` (or a signal
/// that does not appear on any letter) get `pmux_enable = false` and no
/// PMUX letter — the template emits a plain-GPIO PINCFG instead.
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);
            // Board pins that claim a peripheral role but for which no
            // matching PMUX column exists land here as `pmux_enable=false`.
            // Per CHIPS-MICROCHIP-00 §9 the snapshot test (-03 acceptance
            // gate) is the strictness boundary; the -02 generator
            // (this code) emits a comment-flagged fallback so the
            // pipeline renders end-to-end even when the chip YAML and
            // board YAML disagree on a single pad. The mismatch is
            // surfaced in the rendered `io_mux.rs` via the
            // `unmatched_peripheral` flag.
            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()
}

/// Look up the PMUX function letter for `signal` on `pad`.
///
/// Returns `(letter, bits, enable)` where `enable` is true iff the
/// signal name matches one of the eight letter columns. Signals like
/// `"GPIO"` or `"LED"` will not match any column and yield
/// `(None, None, false)`.
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)
}

/// Heuristic: does `signal` look like a peripheral alternate-function name?
///
/// Used to differentiate `signal: SERCOM5_PAD0` (peripheral — must land
/// on a PMUX letter) from `signal: GPIO` / `signal: LED` (plain — no
/// PMUX needed). Anything matching one of the known SAM AF prefixes is
/// treated as peripheral; everything else is plain GPIO.
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",
    }
}

/// Format a u32 as `0xXXXXXXXX` for linker MEMORY blocks.
fn hex32_filter(value: u32) -> String {
    format!("0x{value:08X}")
}

/// Convert a spec-level dotted PAC path like `mclk.apbamask` into the
/// svd2rust form `MCLK.apbamask()`. The first segment is the peripheral
/// instance — in svd2rust-generated PAC crates that's an uppercase field
/// on `Peripherals`, not a method. Subsequent segments are registers or
/// blocks within the instance and stay as method calls.
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
}

/// Convert a chip name into the lowercase-no-separator form used as the
/// `<chip_link_stem>.x` linker file name per CHIPS-MICROCHIP-05 §5.1.
///
/// Distinct from [`snake_case`] in that no underscore separators are
/// inserted on case transitions — the Microchip PAC crate names on
/// crates.io are themselves all-lowercase without separators (e.g.
/// `atsamd51j19a`, `atsamd21j18a`), so the linker file name matches
/// the PAC crate name for grep-ability against documentation. Any
/// non-alphanumeric input character is dropped.
fn chip_link_stem(input: &str) -> String {
    input
        .chars()
        .filter(|c| c.is_ascii_alphanumeric())
        .flat_map(|c| c.to_lowercase())
        .collect()
}

/// Convert an arbitrary board/chip name into a snake_case file stem.
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() {
        // ATSAMD51J19A → atsamd51j19a (matches the PAC crate name on
        // crates.io for grep-ability per CHIPS-MICROCHIP-05 §5.1).
        assert_eq!(chip_link_stem("ATSAMD51J19A"), "atsamd51j19a");
        // D21 family member: ATSAMD21J18A → atsamd21j18a.
        assert_eq!(chip_link_stem("ATSAMD21J18A"), "atsamd21j18a");
        // L21 family member with mixed case stays alphanumeric only.
        assert_eq!(chip_link_stem("ATSAML21J18B"), "atsaml21j18b");
        // Non-alphanumeric separators get dropped.
        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));
    }
}