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
//! Rendering pipeline for Silicon Labs BSP code generation.
//!
//! Consumes a [`SilabsIr`] produced by [`super::merge`], builds a
//! MiniJinja context enriched with precomputed pin routing and
//! peripheral-usage helpers, then renders each of the eight templates
//! (`mod.rs`, `pac.rs`, `clocks.rs`, `io_mux.rs`, `peripherals.rs`,
//! `board.rs`, plus `memory.x` and `<chip>.x` linker scripts) into
//! `out_dir/<board_stem>/`.
//!
//! Templates are embedded via `include_str!` so the rendered BSP does
//! not depend on the filesystem layout of the creator crate.
//!
//! Linker emission was deferred at slate 1 per CHIPS-SILABS-00 §11
//! and ratified in CHIPS-SILABS-05 (chipdb/rlvgl-chips-silabs/docs/
//! CHIPS-SILABS-05-LINKER.md). The two linker files only render when
//! the chip yaml carries a `linker:` block (always true for EFM32GG11
//! after slate 1; reserved as optional for forward Series 2 work).

use super::ir::{SilabsDir, SilabsIr, SilabsRoutingKind};
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");

/// Kind of routing applied to one board pin.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PinRouteKind {
    /// Series 1 ROUTELOC fast path: write `routeloc` into
    /// `<peripheral>.<route_reg>.<route_field>` and set
    /// `<peripheral>.<pen_reg>.<pen_field>` to 1.
    Routeloc {
        /// PAC path of the ROUTELOC register
        /// (e.g. `"usart4.routeloc0"`).
        route_reg: String,
        /// Field name within the ROUTELOC register
        /// (e.g. `"txloc"`).
        route_field: String,
        /// PAC path of the ROUTEPEN register
        /// (e.g. `"usart4.routepen"`).
        pen_reg: String,
        /// Field name within the ROUTEPEN register
        /// (e.g. `"txpen"`).
        pen_field: String,
        /// ROUTELOC integer (0..31) from the board YAML.
        loc: u8,
    },
    /// Plain software-driven GPIO — no peripheral signal route.
    Plain,
}

/// A resolved per-pin routing decision for the render templates.
#[derive(Serialize, Debug, Clone)]
pub struct PinRoute {
    /// GPIO port letter (`"A"..="I"`).
    pub port: String,
    /// GPIO pin within the port (`0..15`).
    pub pin: u8,
    /// Signal name from the board spec (e.g. `"USART4_TX"`).
    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 `None`).
    pub pull: Option<String>,
    /// Optional initial drive state for output pins
    /// (`"high"` / `"low"`).
    pub initial: Option<String>,
    /// Optional label used for generated `pub const` names.
    pub label: Option<String>,
    /// Routing decision.
    pub route: PinRouteKind,
}

/// Render a full PAC-style BSP for the given [`SilabsIr`] under
/// `out_dir`.
///
/// Creates
/// `out_dir/<board_stem>/{mod,pac,clocks,io_mux,peripherals,board}.rs`
/// plus the linker scripts `memory.x` and `<chip>.x` when the chip
/// yaml has a `linker:` block. `board_stem` is the snake-cased board
/// name; `<chip>.x` is named after the snake-cased chip family
/// (e.g. `efm32_gg11.x` for the EFM32GG11 family). Linker emission
/// matches the TI shape per CHIPS-SILABS-05 §5.1 (commit ratifying
/// this dropped the §11 deferral).
///
/// # Errors
/// Returns any I/O failure creating the output directory or writing
/// files, and any MiniJinja rendering failure.
pub fn render_silabs_pac(ir: &SilabsIr, 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 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_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),
        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)
}

/// 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: &SilabsIr) -> 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 pin assignment into a routing decision.
///
/// For each board pin we try, in order:
/// 1. ROUTELOC fast path — the owning peripheral exists, the pin
///    carries a `routeloc:` integer, and the peripheral has a signal
///    whose role matches the pin's signal name (after stripping the
///    peripheral prefix). The chip YAML names the route_reg /
///    route_field / pen_reg / pen_field paths; the board YAML
///    supplies the LOC integer.
/// 2. Plain GPIO — fallback for pins owned by `gpio` or pins without a
///    matching ROUTELOC entry.
fn resolve_pin_routes(ir: &SilabsIr) -> Vec<PinRoute> {
    ir.pins
        .iter()
        .map(|pin| {
            let route = match (
                ir.chip.routing_kind,
                pin.peripheral.as_deref(),
                pin.routeloc,
            ) {
                (SilabsRoutingKind::Routeloc, Some(periph_name), Some(loc))
                    if periph_name != "gpio" =>
                {
                    let role_hint = pin_role_hint(&pin.signal, Some(periph_name));
                    ir.chip
                        .peripherals
                        .get(periph_name)
                        .and_then(|periph| {
                            pick_routeloc_signal(periph, pin.direction, role_hint.as_deref(), loc)
                        })
                        .unwrap_or(PinRouteKind::Plain)
                }
                _ => PinRouteKind::Plain,
            };
            PinRoute {
                port: pin.port.clone(),
                pin: pin.pin,
                signal: pin.signal.clone(),
                peripheral: pin.peripheral.clone(),
                direction: dir_to_str(pin.direction).to_string(),
                pull: pin.pull.clone(),
                initial: pin.initial.clone(),
                label: pin.label.clone(),
                route,
            }
        })
        .collect()
}

/// Extract a lower-case role hint from a board pin's signal name.
///
/// Board YAMLs label pins like `USART4_TX`, `I2C2_SCL`. Strip the
/// peripheral-name prefix and lowercase the remainder so we can match
/// it against a peripheral signal's `role` field (e.g. `tx`, `scl`).
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)
}

/// Pick the matching ROUTELOC signal for this pin.
///
/// Looks at the peripheral's `signals` list and returns the first
/// direction-compatible signal whose role matches `role_hint`. If no
/// hint is supplied (or no role matches), falls back to the first
/// direction-compatible signal.
fn pick_routeloc_signal(
    periph: &super::ir::SilabsPeripheral,
    direction: SilabsDir,
    role_hint: Option<&str>,
    loc: u8,
) -> Option<PinRouteKind> {
    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) {
                return Some(PinRouteKind::Routeloc {
                    route_reg: sig.route_reg.clone(),
                    route_field: sig.route_field.clone(),
                    pen_reg: sig.pen_reg.clone(),
                    pen_field: sig.pen_field.clone(),
                    loc,
                });
            }
        }
    }
    for sig in &periph.signals {
        if !direction_compatible(direction, sig.direction) {
            continue;
        }
        return Some(PinRouteKind::Routeloc {
            route_reg: sig.route_reg.clone(),
            route_field: sig.route_field.clone(),
            pen_reg: sig.pen_reg.clone(),
            pen_field: sig.pen_field.clone(),
            loc,
        });
    }
    None
}

fn direction_compatible(pin: SilabsDir, sig: SilabsDir) -> bool {
    match (pin, sig) {
        (SilabsDir::Inout, _) | (_, SilabsDir::Inout) => true,
        (a, b) => a == b,
    }
}

fn dir_to_str(d: SilabsDir) -> &'static str {
    match d {
        SilabsDir::In => "in",
        SilabsDir::Out => "out",
        SilabsDir::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 `cmu.hfperclken0` into
/// the svd2rust form `CMU.hfperclken0`. The first segment is the
/// peripheral instance — in svd2rust-generated PAC crates that's an
/// uppercase field on `Peripherals`. Subsequent segments are
/// registers or sub-blocks within the instance.
///
/// The pinned `efm32gg11b-pac 0.1.4` predates svd2rust's method-style
/// register accessors (svd2rust 0.28.0 / Dec 2020): register blocks
/// expose each register as a direct `#[repr(C)]` struct field
/// (`p.CMU.hfperclken0`), not a method (`p.CMU.hfperclken0()`). This
/// filter therefore emits the field-direct form. Structurally
/// identical to the `atsamd51j19a 0.7.1` divergence handled by
/// CHIPS-MICROCHIP-02.
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
}

/// 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_lowercases_silabs_names() {
        // The shared snake_case algorithm inserts a `_` between a
        // lowercase character and a following uppercase one. After
        // chars are lowered, the trailing `A` after `1` is treated as
        // a fresh segment, so `SLSTK3701A` ends up `slstk3701_a`.
        // This matches the shape every sibling vendor renderer emits.
        assert_eq!(snake_case("SLSTK3701A"), "slstk3701_a");
        assert_eq!(snake_case("EFM32GG11"), "efm32_gg11");
    }

    #[test]
    fn pac_path_filter_uppercases_instance_and_emits_field_direct_registers() {
        // `efm32gg11b-pac 0.1.4` exposes register-block accessors as
        // direct struct fields rather than methods (pre-method-accessor
        // svd2rust era — same vintage as `atsamd51j19a 0.7.1`).
        assert_eq!(pac_path_filter("cmu.hfperclken0".into()), "CMU.hfperclken0");
        assert_eq!(pac_path_filter("gpio".into()), "GPIO");
        assert_eq!(
            pac_path_filter("usart4.routeloc0".into()),
            "USART4.routeloc0"
        );
    }

    #[test]
    fn role_hint_strips_peripheral_prefix() {
        assert_eq!(
            pin_role_hint("USART4_TX", Some("usart4")),
            Some("tx".into())
        );
        assert_eq!(pin_role_hint("I2C2_SCL", Some("i2c2")), Some("scl".into()));
        assert_eq!(pin_role_hint("GPIO", Some("gpio")), Some("gpio".into()));
    }
}