rlvgl 0.2.4

A modular, idiomatic Rust reimplementation of the LVGL graphics library for embedded and simulator use.
Documentation
//! Per-peripheral initialization for {{ ir.board.name }}.
//!
//! Only the peripherals actually used by the board are referenced. The
//! generator emits real register writes for UART0 (when it is the console),
//! I2C (full timing setup), SPI master (clock + mode), and timer groups
//! (watchdog disable). Other peripherals get TODO stubs.

use super::pac;

#[allow(unused_imports)]
use super::board::{APB_HZ, XTAL_HZ};

/// Initialize every board peripheral in dependency order.
///
/// Called by [`crate::pac::init`] after clocks and IO MUX are configured.
pub fn init() {
    {% for name in peripherals_used %}
    init_{{ name }}();
    {% endfor %}
}

{% for name in peripherals_used %}
{% set periph = ir.chip.peripherals[name] %}
{% if periph %}
{% if name == "uart0" and ir.board.console and ir.board.console.peripheral == "uart0" %}
/// Bring up UART0 as the console at {{ ir.board.console.baud }} baud, 8N1.
///
/// Clock divider is computed from `APB_HZ` ({{ ir.clocks.apb_hz }} Hz) and
/// the console baud rate ({{ ir.board.console.baud }} baud).
pub fn init_uart0() {
    let p = unsafe { pac::Peripherals::steal() };
    const BAUD: u32 = {{ ir.board.console.baud }};
    // UART0 CLKDIV on ESP32-C3 is Q20.4 fixed point: integer part in bits
    // [19:0], fractional part in bits [23:20]. Compute `(APB << 4) / BAUD`,
    // then write the integer and fractional halves as raw register bits to
    // sidestep field-width type errors in svd2rust 0.31.
    let clkdiv: u32 = (APB_HZ << 4) / BAUD;
    let integer = clkdiv >> 4;
    let frag = clkdiv & 0xF;
    p.UART0
        .clkdiv()
        .write(|w| unsafe { w.bits((integer & 0xF_FFFF) | (frag << 20)) });
    // 8 data bits, no parity, 1 stop bit.
    p.UART0.conf0().modify(|_, w| unsafe {
        w.bit_num().bits(0b11) // 8 data bits
         .parity_en().clear_bit()
         .stop_bit_num().bits(0b01) // 1 stop bit
    });
    // Reset FIFOs. Skipping the TX-idle poll: C3's UART0_STATUS layout
    // differs across PAC revisions and the FIFO reset alone is enough for
    // a cold bring-up.
    p.UART0.conf0().modify(|_, w| w.rxfifo_rst().set_bit().txfifo_rst().set_bit());
    p.UART0.conf0().modify(|_, w| w.rxfifo_rst().clear_bit().txfifo_rst().clear_bit());
}
{% elif periph.class == "i2c" %}
/// Bring up {{ name }} as an I2C master at {{ ir.board.i2c_configs[name].scl_hz | default(100000) }} Hz.
///
/// Targets the `{{ ir.chip.pac_crate }}::{{ name | upper }}` register block.
/// Clock gating is already done by `crate::clocks::init` — this function
/// only sets up the peripheral-internal timing and leaves it ready for
/// per-transaction command-list writes.
///
/// TRM reference: ESP32-C3 Chapter 16 "I2C Controller", register map and
/// timing formulas. The template writes all *_period / *_hold / *_sample
/// registers as raw half-period counts derived from the source clock
/// (XTAL_HZ = {{ ir.clocks.xtal_hz }} Hz on C3 — the I2C controller uses
/// XTAL directly when `sclk_sel = 0`).
pub fn init_{{ name }}() {
    let p = unsafe { pac::Peripherals::steal() };
    const SCL_HZ: u32 = {{ ir.board.i2c_configs[name].scl_hz | default(100000) }};
    // Full SCL period in XTAL cycles. Split 50/50 between low and high
    // halves. For 400 kHz @ 40 MHz XTAL that's 100 cycles, 50 per half.
    let period: u32 = XTAL_HZ / SCL_HZ;
    let half: u32 = period / 2;

    // Select XTAL_CLK as the source (sclk_sel = 0) and enable the clock.
    p.{{ name | upper }}.clk_conf().modify(|_, w| unsafe {
        w.sclk_sel().clear_bit()
         .sclk_active().set_bit()
         .sclk_div_num().bits(0)
    });
    // Master mode, MSB-first on both directions.
    p.{{ name | upper }}.ctr().modify(|_, w| {
        w.ms_mode().set_bit()
         .tx_lsb_first().clear_bit()
         .rx_lsb_first().clear_bit()
         .clk_en().set_bit()
    });
    // SCL low/high periods (raw register writes — field widths vary across
    // PAC revisions, so stay register-level).
    p.{{ name | upper }}.scl_low_period().write(|w| unsafe { w.bits(half) });
    p.{{ name | upper }}.scl_high_period().write(|w| unsafe { w.bits(half) });
    // SDA hold/sample timings — conservative quarter-period defaults.
    let quarter: u32 = half / 2;
    p.{{ name | upper }}.sda_hold().write(|w| unsafe { w.bits(quarter) });
    p.{{ name | upper }}.sda_sample().write(|w| unsafe { w.bits(quarter) });
    // START / repeated-START / STOP setup + hold times.
    p.{{ name | upper }}.scl_start_hold().write(|w| unsafe { w.bits(half) });
    p.{{ name | upper }}.scl_rstart_setup().write(|w| unsafe { w.bits(half) });
    p.{{ name | upper }}.scl_stop_hold().write(|w| unsafe { w.bits(half) });
    p.{{ name | upper }}.scl_stop_setup().write(|w| unsafe { w.bits(half) });
    // Reset TX + RX FIFOs so subsequent transactions start clean.
    p.{{ name | upper }}.fifo_conf().modify(|_, w| {
        w.tx_fifo_rst().set_bit().rx_fifo_rst().set_bit()
    });
    p.{{ name | upper }}.fifo_conf().modify(|_, w| {
        w.tx_fifo_rst().clear_bit().rx_fifo_rst().clear_bit()
    });
    // Publish the config — `conf_upgate` latches the new timings.
    p.{{ name | upper }}.ctr().modify(|_, w| w.conf_upgate().set_bit());
}
{% elif periph.class == "spi_master" %}
/// Bring up {{ name }} as an SPI master.
///
/// Targets the `{{ ir.chip.pac_crate }}::{{ name | upper }}` register block.
/// Clock gating is already done by `crate::clocks::init`.
/// Default: 1 MHz, mode 0 (CPOL=0, CPHA=0). Override via `spi_configs`
/// in the board YAML.
///
/// CHIPS-ESP-12: the prior template wrote `wr_byte_order` /
/// `rd_byte_order` on the USER register. Those fields don't exist on any
/// of the six in-tree ESP32 PACs — SPI byte ordering on these chips is
/// fixed at MSB-first. Bit-order lives on the CTRL register
/// (`wr_bit_order` / `rd_bit_order`) and is portable across legacy
/// (esp32c3 0.31, esp32p4 0.2) and modern (svd2rust 0.37+) PACs alike,
/// so the template stays vintage-agnostic.
pub fn init_{{ name }}() {
    let p = unsafe { pac::Peripherals::steal() };
    {% if ir.board.spi_configs is defined and ir.board.spi_configs[name] is defined %}
    const SPI_CLK_HZ: u32 = {{ ir.board.spi_configs[name].clk_hz }};
    const SPI_MODE: u8 = {{ ir.board.spi_configs[name].mode }};
    {% else %}
    const SPI_CLK_HZ: u32 = 1_000_000;
    const SPI_MODE: u8 = 0;
    {% endif %}
    // Clock divider: SPI_CLK = APB_CLK / (clkcnt_n + 1).
    let clkcnt_n: u32 = if SPI_CLK_HZ >= APB_HZ { 0 } else { APB_HZ / SPI_CLK_HZ - 1 };
    let clkcnt_h = clkcnt_n / 2;
    p.{{ name | upper }}.clock().write(|w| unsafe {
        w.bits((clkcnt_n & 0x3F) | ((clkcnt_h & 0x3F) << 6) | ((clkcnt_n & 0x3F) << 12))
    });
    // Full-duplex mode. Byte order is fixed in hardware (MSB-first); bit
    // order also defaults to MSB-first at reset on every in-tree ESP32
    // PAC vintage (CTRL.wr_bit_order / rd_bit_order both reset to 0) so
    // there's nothing to write here. The C3 0.31 PAC types those fields
    // as a 1-bit BitWriter while modern svd2rust-0.37+ PACs (P4 0.2,
    // C6 0.23, H2 0.19, C5 0.2, C61 0.3) type them as a 2-bit
    // FieldWriter; not writing them at all sidesteps the divergence
    // and lets the template stay vintage-agnostic.
    p.{{ name | upper }}.user().modify(|_, w| w.doutdin().set_bit());
    // CPOL via misc.ck_idle_edge, CPHA via user.ck_out_edge.
    let cpol = (SPI_MODE >> 1) & 1 != 0;
    let cpha = SPI_MODE & 1 != 0;
    if cpol {
        p.{{ name | upper }}.misc().modify(|_, w| w.ck_idle_edge().set_bit());
    } else {
        p.{{ name | upper }}.misc().modify(|_, w| w.ck_idle_edge().clear_bit());
    }
    if cpha {
        p.{{ name | upper }}.user().modify(|_, w| w.ck_out_edge().set_bit());
    } else {
        p.{{ name | upper }}.user().modify(|_, w| w.ck_out_edge().clear_bit());
    }
}
{% elif periph.class == "ledc" %}
/// Bring up {{ name }} (LED PWM controller).
///
/// Configures LEDC timers and channels based on board config.
/// Clock gating is already done by `crate::clocks::init`.
pub fn init_{{ name }}() {
    let _p = unsafe { pac::Peripherals::steal() };
    // LEDC is configured per-channel at runtime by application code.
    // Clock gate is enabled; channels are ready for use.
    // See the {{ ir.chip.pac_crate }}::LEDC register block for timer/channel
    // configuration (timer_conf, ch_conf0, ch_duty, ch_hpoint).
}
{% elif periph.class == "timg" %}
/// Bring up {{ name }} (Timer Group).
///
/// Disables the watchdog and leaves the main timer ready for use.
/// Clock gating is already done by `crate::clocks::init`.
pub fn init_{{ name }}() {
    let p = unsafe { pac::Peripherals::steal() };
    // Disable the watchdog timer (MWDT) — it's often enabled by bootrom.
    // Write the WDT key first, then disable.
    p.{{ name | upper }}.wdtwprotect().write(|w| unsafe { w.bits(0x50D8_3AA1) });
    p.{{ name | upper }}.wdtconfig0().write(|w| unsafe { w.bits(0) });
    p.{{ name | upper }}.wdtwprotect().write(|w| unsafe { w.bits(0) });
}
{% else %}
/// TODO: initialize {{ name }} (class `{{ periph.class }}`, base {{ periph.base }}).
///
/// Populate this function by writing into the `{{ ir.chip.pac_crate }}::{{ name | upper }}`
/// register block. Clock gating is already done by `crate::clocks::init`.
pub fn init_{{ name }}() {
    let _ = unsafe { pac::Peripherals::steal() };
    // TODO: {{ periph.class }} init sequence for {{ name }}
}
{% endif %}
{% else %}
/// TODO: unknown peripheral `{{ name }}` (no entry in chip IR).
pub fn init_{{ name }}() {}
{% endif %}

{% endfor %}