diff --git a/Cargo.lock b/Cargo.lock index 3e7d0045c2..00a3b1ade4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -215,6 +215,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "deranged" version = "0.4.0" @@ -242,7 +263,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -321,7 +342,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -593,7 +614,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -655,7 +676,7 @@ checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -754,7 +775,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -797,7 +818,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -846,7 +867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.106", ] [[package]] @@ -961,22 +982,32 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -1076,7 +1107,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1130,7 +1161,7 @@ dependencies = [ "serde_yaml 0.9.34+deprecated", "svd-parser", "svd-rs", - "syn", + "syn 2.0.106", "thiserror 2.0.15", "url", ] @@ -1177,6 +1208,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1185,7 +1227,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1223,7 +1265,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1234,7 +1276,7 @@ checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1580,11 +1622,15 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "csv", "env_logger 0.11.8", "form", "log", "rayon", + "regex", "semver", + "serde", + "serde_yaml 0.9.34+deprecated", "strum", "svd2rust", "svdtools", @@ -1631,7 +1677,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "synstructure", ] @@ -1652,7 +1698,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "synstructure", ] @@ -1686,5 +1732,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 78e3f6884a..7abb0ec7d9 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,11 +7,15 @@ publish = false [dependencies] anyhow = "1.0.99" clap = { version = "4.5.45", features = ["derive"] } +csv = "1.3.1" env_logger = "0.11.8" form = "0.13.0" log = "0.4.27" rayon = "1.11.0" +regex = "1.11.1" semver = "1.0.26" +serde = { version = "1.0.219", features = ["derive"] } +serde_yaml = "0.9.34" strum = { version = "0.27.2", features = ["derive"] } svd2rust = { git = "https://github.com/MabezDev/svd2rust", branch = "skip-peripherals-struct-0.37.1", features = ["serde", "yaml"], default-features = false } svdtools = "0.5.0" diff --git a/xtask/regdesc/peripheral_descriptions.yml b/xtask/regdesc/peripheral_descriptions.yml new file mode 100644 index 0000000000..0e1d57755b --- /dev/null +++ b/xtask/regdesc/peripheral_descriptions.yml @@ -0,0 +1,175 @@ +peripheral_descriptions: + - peripheral: AES + description: AES (Advanced Encryption Standard) Accelerator + - peripheral: ADC + description: ADC (Analog to Digital Converter) + - peripheral: APB_CTRL + description: APB (Advanced Peripheral Bus) Controller + - peripheral: APB_SARADC + description: SAR (Successive Approximation Register) Analog-to-Digital Converter + - peripheral: ATOMIC + description: Atomic Locker + - peripheral: ASSIST_DEBUG + description: Debug Assist + - peripheral: DMA + description: DMA (Direct Memory Access) Controller + - peripheral: DS + description: Digital Signature + - peripheral: ECC + description: ECC (ECC Hardware Accelerator) + - peripheral: ECDSA + description: ECDSA (Elliptic Curve Digital Signature Algorithm) Accelerator + - peripheral: EFUSE + description: eFuse Controller + - peripheral: EMAC_DMA + description: "Ethernet DMA configuration and control registers" + - peripheral: EMAC_EXT + description: "Ethernet Clock, PHY type, and SRAM configuration registers" + - peripheral: EMAC_MAC + description: "Ethernet MAC configuration and control registers" + - peripheral: EXTMEM + description: External Memory + - peripheral: GPIO + description: General Purpose Input/Output + - peripheral: GPIO_SD + description: Sigma-Delta Modulation + - peripheral: HMAC + description: HMAC (Hash-based Message Authentication Code) Accelerator + - peripheral: HP_SYS + description: High-Power System + - peripheral: H264 + description: H264 Encoder (Core) + - peripheral: H264_DMA + description: H264 Encoder (DMA) + - peripheral: I2C0 + description: I2C (Inter-Integrated Circuit) Controller 0 + - peripheral: I2C1 + description: I2C (Inter-Integrated Circuit) Controller 1 + - peripheral: I2S0 + description: I2S (Inter-IC Sound) Controller 0 + - peripheral: I2S1 + description: I2S (Inter-IC Sound) Controller 1 + - peripheral: I2S2 + description: I2S (Inter-IC Sound) Controller 2 + - peripheral: I3C_MST + description: I3C Controller (Master) + - peripheral: I3C_SLV + description: I3C Controller (Slave) + - peripheral: INTERRUPT_CORE0 + description: Interrupt Controller (Core 0) + - peripheral: INTERRUPT_CORE1 + description: Interrupt Controller (Core 1) + - peripheral: IO_MUX + description: Input/Output Multiplexer + - peripheral: JPEG + description: JPEG Codec + - peripheral: LCD_CAM + description: Camera/LCD Controller + - peripheral: LEDC + description: LED Control PWM (Pulse Width Modulation) + - peripheral: LP_ADC + description: Low-power Analog to Digital Converter + - peripheral: LP_APM + description: Low-power Access Permission Management Controller + - peripheral: LP_GPIO + description: Low-power General Purpose Input/Output + - peripheral: LP_INTR + description: Low-power Interrupt Controller + - peripheral: LP_IO_MUX + description: Low-power Input/Output Multiplexer + - peripheral: LP_I2C0 + description: Low-power I2C (Inter-Integrated Circuit) Controller 0 + - peripheral: LP_I2S0 + description: Low-power I2S (Inter-IC Sound) Controller 0 + - peripheral: LP_TEE + description: Low-power Trusted Execution Environment + - peripheral: LP_TIMER + description: Low-power Timer + - peripheral: LP_TSENS + description: Low-power Temperature Sensor + - peripheral: LP_UART + description: Low-power UART (Universal Asynchronous Receiver-Transmitter) Controller + - peripheral: LP_WDT + description: Low-power Watchdog Timer + - peripheral: MCPWM0 + description: Motor Control Pulse-Width Modulation 0 + - peripheral: MCPWM1 + description: Motor Control Pulse-Width Modulation 1 + - peripheral: MIPI_CSI_BRIDGE + description: MIPI Camera Interface Bridge + - peripheral: MIPI_CSI_HOST + description: MIPI Camera Interface Host + - peripheral: MIPI_DSI_BRIDGE + description: MIPI Camera Interface Bridge + - peripheral: MIPI_DSI_HOST + description: MIPI Display Interface Host + - peripheral: PARL_IO + description: Parallel IO Controller + - peripheral: PCNT + description: Pulse Count Controller + - peripheral: PMS + description: Permissions Controller + - peripheral: RMT + description: Remote Control + - peripheral: RNG + description: Hardware Random Number Generator + - peripheral: RSA + description: RSA (Rivest Shamir Adleman) Accelerator + - peripheral: RTC_CNTL + description: Real-Time Clock Control + - peripheral: RTC_IO + description: Low-power Input/Output + - peripheral: RTC_I2C + description: Low-power I2C (Inter-Integrated Circuit) Controller + - peripheral: SDHOST + description: SD/MMC Host Controller + - peripheral: SHA + description: SHA (Secure Hash Algorithm) Accelerator + - peripheral: SOC_ETM + description: Event Task Matrix + - peripheral: SPI0 + description: SPI (Serial Peripheral Interface) Controller 0 + - peripheral: SPI1 + description: SPI (Serial Peripheral Interface) Controller 1 + - peripheral: SPI2 + description: SPI (Serial Peripheral Interface) Controller 2 + - peripheral: SPI3 + description: SPI (Serial Peripheral Interface) Controller 3 + - peripheral: SPI4 + description: SPI (Serial Peripheral Interface) Controller 4 + - peripheral: SYSTEM + description: System Configuration Registers + - peripheral: SYSTIMER + description: System Timer + - peripheral: TIMG0 + description: Timer Group 0 + - peripheral: TIMG1 + description: Timer Group 1 + - peripheral: TRACE + description: RISC-V Trace Encoder + - peripheral: TWAI0 + description: Two-Wire Automotive Interface + - peripheral: TWAI1 + description: Two-Wire Automotive Interface + - peripheral: TWAI2 + description: Two-Wire Automotive Interface + - peripheral: UART0 + description: UART (Universal Asynchronous Receiver-Transmitter) Controller 0 + - peripheral: UART1 + description: UART (Universal Asynchronous Receiver-Transmitter) Controller 1 + - peripheral: UART2 + description: UART (Universal Asynchronous Receiver-Transmitter) Controller 2 + - peripheral: UART3 + description: UART (Universal Asynchronous Receiver-Transmitter) Controller 3 + - peripheral: UART4 + description: UART (Universal Asynchronous Receiver-Transmitter) Controller 4 + - peripheral: UHCI0 + description: Universal Host Controller Interface 0 + - peripheral: UHCI1 + description: Universal Host Controller Interface 1 + - peripheral: USB0 + description: USB OTG (On-The-Go) + - peripheral: USB_DEVICE + description: Full-speed USB Serial/JTAG Controller + - peripheral: XTS_AES + description: XTS-AES-128 Flash Encryption diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 987bccf84a..07a45cc9b0 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -19,6 +19,8 @@ use svd2rust::{ use svdtools::{html::html_cli::svd2html, patch::Config as PatchConfig}; use toml_edit::DocumentMut; +mod regdesc; + #[derive(Debug, Clone, Display, EnumIter, ValueEnum)] #[strum(serialize_all = "kebab-case")] enum Chip { @@ -105,7 +107,7 @@ enum Commands { chips: Vec, }, - /// Published the specified package(s) + /// Publish the specified package(s) Publish { /// Perform all checks without uploading #[arg(long)] @@ -115,6 +117,38 @@ enum Commands { #[arg(value_enum, default_values_t = Chip::iter())] chips: Vec, }, + + /// Generate a base CMSIS-SVD from GDVS register CSVs + /// + /// Requires `--chip`, `--csv-dir`, `--interrupts`, and `--reg-base`. + /// Peripheral bases come from `--reg-base` (IDF `reg_base.h`) and IRQs from + /// `--interrupts` (IDF `interrupts.h`). CSV files are discovered in + /// `--csv-dir`. Output defaults to `target/generated_svds/{chip}.svd`. + GenerateBaseSvd { + /// Chip to generate + #[arg(long, value_enum)] + chip: Chip, + + /// Directory containing GDVS register CSV files + #[arg(long)] + csv_dir: PathBuf, + + /// Path to IDF `soc/interrupts.h` (required) + #[arg(long)] + interrupts: PathBuf, + + /// Path to IDF `register/soc/reg_base.h` (required) + #[arg(long = "reg-base", visible_alias = "reg_base")] + reg_base: PathBuf, + + /// Output SVD file path + #[arg(long)] + output: Option, + + /// SVD version number + #[arg(long, default_value_t = 1)] + version: u32, + }, } // ---------------------------------------------------------------------------- @@ -152,6 +186,23 @@ fn main() -> Result<()> { Commands::Publish { dry_run, chips } => chips .par_iter() .try_for_each(|chip| publish_package(&workspace, chip, dry_run)), + + Commands::GenerateBaseSvd { + chip, + csv_dir, + interrupts, + reg_base, + output, + version, + } => { + let chip = chip.to_string(); + let output = output.unwrap_or_else(|| regdesc::default_output_path(&workspace, &chip)); + let sources = regdesc::GenerateSources { + interrupts, + reg_base, + }; + regdesc::generate_base_svd(&chip, &csv_dir, &output, version, &sources) + } } } diff --git a/xtask/src/regdesc/config.rs b/xtask/src/regdesc/config.rs new file mode 100644 index 0000000000..fba435ed72 --- /dev/null +++ b/xtask/src/regdesc/config.rs @@ -0,0 +1,102 @@ +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +use super::model::{ + ChipInfo, + Peripheral, + PeripheralInstance, + PeripheralInterrupt, + RegdescFragment, +}; + +#[derive(Debug, Deserialize)] +struct DescriptionsFile { + peripheral_descriptions: Vec, +} + +#[derive(Debug, Deserialize)] +struct PeripheralDescription { + peripheral: String, + description: String, +} + +/// Lists `*.csv` files in `csv_dir` (sorted by file name). +pub fn discover_csv_files(csv_dir: &Path) -> Result> { + let mut files = Vec::new(); + for entry in fs::read_dir(csv_dir) + .with_context(|| format!("reading CSV directory {}", csv_dir.display()))? + { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("csv") { + files.push(path); + } + } + files.sort(); + Ok(files) +} + +/// Loads the shared peripheral description blurbs from YAML. +pub fn load_descriptions(path: &Path) -> Result> { + let content = + fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?; + let file: DescriptionsFile = + serde_yaml::from_str(&content).with_context(|| format!("parsing {}", path.display()))?; + Ok(file + .peripheral_descriptions + .into_iter() + .map(|entry| (entry.peripheral, entry.description)) + .collect()) +} + +/// Fills in each instance's description, or falls back to a generic one. +pub fn apply_instance_descriptions( + instances: &mut [PeripheralInstance], + descriptions: &HashMap, +) { + for instance in instances.iter_mut() { + instance.description = descriptions.get(&instance.name).cloned().or_else(|| { + log::warn!("{} has no description", instance.name); + Some(format!("{} Peripheral", instance.name)) + }); + } +} + +/// Bundles everything into one struct that's ready for SVD writing. +pub fn build_fragment( + chip: &str, + peripherals: Vec, + instances: Vec, + interrupts: Vec, +) -> RegdescFragment { + RegdescFragment { + peripherals, + peripheral_instances: instances, + peripheral_interrupts: interrupts, + chip_info: ChipInfo::from_chip_id(chip), + } +} + +/// Path to `xtask/regdesc/`. +pub fn regdesc_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("regdesc") +} + +/// Path to the shared `peripheral_descriptions.yml`. +pub fn peripheral_descriptions_path() -> PathBuf { + regdesc_root().join("peripheral_descriptions.yml") +} + +/// Default SVD output: `target/generated_svds/{chip}.svd`. +pub fn default_output_path(workspace: &Path, chip: &str) -> PathBuf { + workspace + .join("target") + .join("generated_svds") + .join(format!("{chip}.svd")) +} diff --git a/xtask/src/regdesc/csv.rs b/xtask/src/regdesc/csv.rs new file mode 100644 index 0000000000..2c11bd271a --- /dev/null +++ b/xtask/src/regdesc/csv.rs @@ -0,0 +1,521 @@ +use std::sync::LazyLock; + +use anyhow::{bail, Context, Result}; +use regex::Regex; + +use super::{ + model::{ExpandContext, Field, Peripheral, Register, RegisterGroup}, + util::{parse_verilog_number, trim}, +}; + +static BITPOS_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\[((\d+):)?(\d+)\]").unwrap()); +static NONREG_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"([0-9]+)\*([0-9]+)").unwrap()); +static C_IDENT_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9_$]+$").unwrap()); +static REPEAT_LOWER: LazyLock = LazyLock::new(|| Regex::new(r"\$([A-Z])").unwrap()); + +const CSV_VERSION_1_0: i32 = 100; +const CSV_VERSION_1_1: i32 = 110; +const CSV_VERSION_1_2: i32 = 120; + +type CsvLine = std::collections::HashMap; + +/// Trimmed column value, or `""` if the column is missing. +fn csv_field<'a>(line: &'a CsvLine, key: &str) -> &'a str { + trim(line.get(key).map(String::as_str).unwrap_or("")) +} + +/// Feed it CSV text, get back a parsed `Peripheral`. +pub fn read_peripheral_csv(content: &str, peripheral_name: Option<&str>) -> Result { + let mut parser = CsvParser::new(content, peripheral_name)?; + parser.parse() +} + +/// Probe the peripheral type name from CSV contents without a full parse. +pub fn probe_peripheral_name_from_content(content: &str) -> Result> { + let (_version, records) = parse_csv_records(content)?; + Ok(probe_peripheral_name(&records, "RegName_Chg_0") + .or_else(|| probe_peripheral_name(&records, "RegName"))) +} + +struct CsvParser { + lines: Vec, + peripheral_name: String, + csv_version: i32, + last_repeat_num: Option, +} + +impl CsvParser { + /// Sets up the parser and figures out the peripheral name if you didn't + /// override it. + fn new(content: &str, peripheral_name: Option<&str>) -> Result { + let (csv_version, records) = parse_csv_records(content)?; + let peripheral_name = match peripheral_name { + Some(name) => name.to_owned(), + None => probe_peripheral_name(&records, "RegName_Chg_0") + .or_else(|| probe_peripheral_name(&records, "RegName")) + .context("Failed to determine peripheral name")?, + }; + + Ok(Self { + lines: records, + peripheral_name, + csv_version, + last_repeat_num: None, + }) + } + + /// Walks the whole CSV and builds register groups. + fn parse(&mut self) -> Result { + let mut groups: Vec = Vec::new(); + let mut line_idx = 0usize; + + while line_idx < self.lines.len() { + let (register, group, consumed) = self.parse_register(line_idx)?; + line_idx += consumed; + + if let Some(idx) = groups + .iter() + .position(|existing| groups_match(existing, &group)) + { + groups[idx].add_register(register); + } else { + let mut new_group = group; + new_group.add_register(register); + groups.push(new_group); + } + } + + for group in &mut groups { + group.visible = group.registers.iter().any(|r| r.visible); + } + + Ok(Peripheral { + name: self.peripheral_name.clone(), + register_groups: groups, + description: String::new(), + }) + } + + /// Parses one register line (and the field rows that follow it). + fn parse_register(&mut self, start: usize) -> Result<(Register, RegisterGroup, usize)> { + let line = &self.lines[start]; + let reg_name = csv_field(line, "RegName"); + if reg_name.is_empty() { + bail!("Expected a register definition at line {}", start + 2); + } + + let field_name = csv_field(line, "Signal"); + if !field_name.is_empty() { + bail!("Field {field_name} cannot be defined on the same line as register {reg_name}"); + } + + let mut reg_name = reg_name.to_owned(); + if let Some(alt) = extract_alt_names(line).into_iter().next() { + reg_name = alt; + } + + let description = extract_description(line); + let visible = extract_visible(line); + let register_group = extract_register_group(line, self.csv_version); + + let mem_size = extract_mem_size(line, self.csv_version, ®_name); + if mem_size != 0 { + self.validate_mem_region_name(®_name)?; + let register = Register { + name: reg_name, + addr: extract_reg_addr(line)?, + fields: Vec::new(), + description, + visible: true, + size: mem_size, + repeat: None, + repeat_name_hint: None, + repeat_index_hint: None, + is_mem_region: true, + expand_context: ExpandContext::default(), + }; + return Ok((register, register_group, 1)); + } + + self.validate_register_name(®_name)?; + + let repeat_info = self.extract_repeat(line); + self.last_repeat_num = repeat_info.as_ref().map(|(_, n)| *n); + + let (fields, consumed) = self.parse_fields(start + 1)?; + if fields.is_empty() { + bail!("Register {reg_name} doesn't have any fields defined"); + } + + // A register is hidden if the CSV marks it so, or every field is hidden. + let visible = visible && fields.iter().any(|f| f.visible); + + let mut register = Register { + name: reg_name, + addr: extract_reg_addr(line)?, + fields, + description, + visible, + size: 4, + repeat: None, + repeat_name_hint: None, + repeat_index_hint: None, + is_mem_region: false, + expand_context: ExpandContext::default(), + }; + + if let Some((repeat_name, repeat_num)) = repeat_info { + register.set_repeat_hint(repeat_name, repeat_num); + } + + Ok((register, register_group, consumed + 1)) + } + + /// Grabs all field rows belonging to the current register. + fn parse_fields(&self, start: usize) -> Result<(Vec, usize)> { + let mut fields = Vec::new(); + let mut idx = start; + + while idx < self.lines.len() { + let line = &self.lines[idx]; + if !csv_field(line, "RegName").is_empty() { + break; + } + + let field_name = csv_field(line, "Signal"); + if field_name.is_empty() { + // Reserved/gap rows sometimes keep BitPos but no Signal name. + if extract_field_shift_mask(line).is_some() { + idx += 1; + continue; + } + break; + } + + let mut field_name = field_name.to_ascii_uppercase(); + if let Some(alt) = extract_alt_names(line).into_iter().next() { + field_name = alt; + } + field_name = self.validate_fix_field_name(&field_name)?; + + let (shift, mask) = + extract_field_shift_mask(line).context("Field missing bit position")?; + let (min_val, max_val) = extract_min_max(line); + let default_raw = csv_field(line, "Default"); + let (default, _, _) = parse_verilog_number(if default_raw.is_empty() { + "0" + } else { + default_raw + }); + + let mut field = Field { + name: field_name, + shift, + mask, + access: csv_field(line, "SW(R/W)").to_owned(), + default, + description: extract_description(line), + visible: extract_visible(line), + min_val, + max_val, + repeat: None, + repeat_name_hint: None, + repeat_index_hint: None, + }; + + if let Some((repeat_name, repeat_num)) = self.extract_repeat(line) { + field.set_repeat_hint(repeat_name, repeat_num); + } + + fields.push(field); + idx += 1; + } + + Ok((fields, idx - start)) + } + + /// Pulls out repeat info like `$n` and how many times it repeats. + fn extract_repeat(&self, line: &CsvLine) -> Option<(String, i32)> { + let repeat_name = csv_field(line, "RepeatName"); + if repeat_name.is_empty() { + return None; + } + + // GDVS writes `$N`; normalize to `$n` so merge/expand share one index var. + let mut repeat_name = repeat_name.to_ascii_uppercase(); + repeat_name = REPEAT_LOWER + .replace_all(&repeat_name, |caps: ®ex::Captures| { + format!("${}", caps[1].to_ascii_lowercase()) + }) + .into_owned(); + + let num_str = csv_field(line, "RepeatNum"); + if num_str.is_empty() { + return self.last_repeat_num.map(|n| (repeat_name, n)); + } + + Some((repeat_name, num_str.parse().ok()?)) + } + + /// Warns if a register name looks wrong (doesn't stop parsing). + fn validate_register_name(&self, reg_name: &str) -> Result<()> { + if !C_IDENT_REGEX.is_match(reg_name) { + log::warn!("Invalid register name: {reg_name}"); + } + let prefix = format!("{}_", self.peripheral_name); + if !reg_name.starts_with(&prefix) || !reg_name.ends_with("_REG") { + log::warn!("Invalid register name: {reg_name}"); + } + Ok(()) + } + + /// Same as `validate_register_name`, but for `*_MEM` memory regions. + fn validate_mem_region_name(&self, name: &str) -> Result<()> { + if !C_IDENT_REGEX.is_match(name) { + log::warn!("Invalid memory region name: {name}"); + } + let prefix = format!("{}_", self.peripheral_name); + if !name.starts_with(&prefix) || !name.ends_with("_MEM") { + log::warn!("Invalid memory region name: {name}"); + } + Ok(()) + } + + /// Tries to fix/normalize field names; warns if the prefix looks off. + fn validate_fix_field_name(&self, field_name: &str) -> Result { + if !C_IDENT_REGEX.is_match(field_name) { + log::warn!("Invalid field name: {field_name}"); + } + + let prefix_variants = if self.csv_version < CSV_VERSION_1_1 { + vec![ + format!("REG_{}_REG_", self.peripheral_name), + format!("REG_{}_", self.peripheral_name), + ] + } else { + vec![format!("{}_", self.peripheral_name)] + }; + + for prefix in &prefix_variants { + if let Some(rest) = field_name.strip_prefix(prefix) { + return Ok(format!("{}_{rest}", self.peripheral_name)); + } + } + + let prefix_list = prefix_variants.join(" or "); + log::warn!( + "Invalid field name: {field_name}, expected prefix {prefix_list}; keeping original" + ); + Ok(field_name.to_owned()) + } +} + +/// Checks if two register groups should be merged into one. +fn groups_match(existing: &RegisterGroup, group: &RegisterGroup) -> bool { + if existing.name.is_none() && group.name.is_none() { + return existing.description == group.description; + } + existing.name == group.name && existing.description == group.description +} + +/// Low-level CSV reader (semicolon-delimited, GDVS style). +fn parse_csv_records(content: &str) -> Result<(i32, Vec)> { + let mut reader = csv::ReaderBuilder::new() + .delimiter(b';') + .trim(csv::Trim::All) + .flexible(true) + .from_reader(content.as_bytes()); + + let headers: Vec = reader + .headers() + .context("CSV missing headers")? + .iter() + .map(|h| trim(h).to_owned()) + .collect(); + + let mut records = Vec::new(); + for result in reader.records() { + let record = result.context("invalid CSV record")?; + let mut line = CsvLine::new(); + for (header, value) in headers.iter().zip(record.iter()) { + line.insert(header.clone(), value.to_owned()); + } + records.push(line); + } + + let csv_version = if headers.iter().any(|h| h == "CSV_Version") { + match records.first().and_then(|line| line.get("CSV_Version")) { + Some(v) => match trim(v) { + "1.0" => CSV_VERSION_1_0, + "1.1" => CSV_VERSION_1_1, + "1.2" => CSV_VERSION_1_2, + other => bail!("Unknown CSV version: {other}"), + }, + None => CSV_VERSION_1_0, + } + } else { + CSV_VERSION_1_0 + }; + + Ok((csv_version, records)) +} + +/// Guesses the peripheral name from common prefixes in register names. +fn probe_peripheral_name(records: &[CsvLine], column: &str) -> Option { + let names: Vec = records + .iter() + .filter(|line| !csv_field(line, "Address").is_empty()) + .filter_map(|line| line.get(column).cloned()) + .filter(|name| !name.is_empty()) + .collect(); + + if names.len() < 2 { + return None; + } + + let common_prefix = common_prefix(&names); + let pos = common_prefix.rfind('_')?; + Some(common_prefix[..pos].to_owned()) +} + +/// Finds the shared start of a bunch of strings (used for name probing). +fn common_prefix(values: &[String]) -> String { + if values.is_empty() { + return String::new(); + } + let mut prefix = values[0].clone(); + for value in &values[1..] { + while !value.starts_with(&prefix) { + prefix.pop(); + if prefix.is_empty() { + return String::new(); + } + } + } + prefix +} + +/// Parses the address column from a CSV row. +fn extract_reg_addr(line: &CsvLine) -> Result { + let addr = csv_field(line, "Address"); + let hex = addr + .strip_prefix("0x") + .or_else(|| addr.strip_prefix("0X")) + .unwrap_or(addr); + u32::from_str_radix(hex, 16).with_context(|| format!("Invalid address: {addr}")) +} + +/// Figures out memory region size from the `NonReg` column. +fn extract_mem_size(line: &CsvLine, csv_version: i32, reg_name: &str) -> u32 { + let nonreg_val = csv_field(line, "NonReg"); + if nonreg_val.is_empty() { + return 0; + } + + if csv_version < CSV_VERSION_1_2 { + return u32::from_str_radix(nonreg_val.trim_start_matches("0x"), 16).unwrap_or(0); + } + + if let Some(caps) = NONREG_REGEX.captures(nonreg_val) { + let depth = caps[1].parse::().unwrap_or(0); + let width_bits = caps[2].parse::().unwrap_or(0); + if width_bits != 32 { + log::warn!( + "Memory region {reg_name} bit width {width_bits} isn't implemented, assuming 32" + ); + } + return depth * 4; + } + + 0 +} + +/// Grabs the description text from a row. +fn extract_description(line: &CsvLine) -> String { + csv_field(line, "Description").to_owned() +} + +/// Checks if a row is marked public/visible (empty `Public` column = yes). +fn extract_visible(line: &CsvLine) -> bool { + csv_field(line, "Public").is_empty() +} + +/// Pulls alternate register/field names from `RegName_Chg_0`. +fn extract_alt_names(line: &CsvLine) -> Vec { + let alt = csv_field(line, "RegName_Chg_0"); + if alt.is_empty() { + Vec::new() + } else { + alt.split_whitespace() + .map(|s| s.to_ascii_uppercase()) + .collect() + } +} + +/// Reads min/max value columns if present. +fn extract_min_max(line: &CsvLine) -> (Option, Option) { + let parse = |key| { + let value = csv_field(line, key); + if value.is_empty() { + None + } else { + value.parse().ok() + } + }; + (parse("Min_Value"), parse("Max_Value")) +} + +/// Builds a register group from `GroupName` / `GroupIdentifier`. +fn extract_register_group(line: &CsvLine, csv_version: i32) -> RegisterGroup { + let group_desc = csv_field(line, "GroupName"); + let description = if group_desc.is_empty() { + "Default".to_owned() + } else { + group_desc.to_owned() + }; + let name = if csv_version < CSV_VERSION_1_2 { + None + } else { + let id = csv_field(line, "GroupIdentifier"); + if id.is_empty() { + None + } else { + Some(id.to_owned()) + } + }; + + RegisterGroup { + name, + description, + registers: Vec::new(), + visible: true, + repeat: None, + offset: 0, + } +} + +/// Parses `[7:0]`-style bit positions into (shift, mask). +fn extract_field_shift_mask(line: &CsvLine) -> Option<(u32, u32)> { + let bitpos = csv_field(line, "BitPos"); + if bitpos.is_empty() { + return None; + } + let caps = BITPOS_REGEX.captures(bitpos)?; + if caps.get(2).is_none() { + let bit = caps[3].parse().ok()?; + return Some((bit, 0x1)); + } + let high = caps[2].parse::().ok()?; + let low = caps[3].parse::().ok()?; + if high < low { + return None; + } + let width = high - low + 1; + let mask = if width >= 32 { + u32::MAX + } else { + (1u32 << width) - 1 + }; + Some((low, mask)) +} diff --git a/xtask/src/regdesc/idf.rs b/xtask/src/regdesc/idf.rs new file mode 100644 index 0000000000..717c4b868d --- /dev/null +++ b/xtask/src/regdesc/idf.rs @@ -0,0 +1,213 @@ +//! Parse Espressif IDF headers used as SVD metadata sources. + +use std::{fs, path::Path, sync::LazyLock}; + +use anyhow::{bail, Context, Result}; +use regex::Regex; + +use super::model::{PeripheralInstance, PeripheralInterrupt}; + +static DR_REG_BASE: LazyLock = LazyLock::new(|| { + Regex::new(r#"(?m)^#define\s+DR_REG_([A-Z0-9_]+)_BASE\s+(0x[0-9a-fA-F]+)"#).unwrap() +}); +static IEEE802154_BASE: LazyLock = LazyLock::new(|| { + Regex::new(r#"(?m)^#define\s+IEEE802154_REG_BASE\s+(0x[0-9a-fA-F]+)"#).unwrap() +}); +static ETS_ENUM_ENTRY: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*(ETS_[A-Z0-9_]+)\s*(?:=\s*([^,/]+))?\s*,").unwrap()); + +/// GDVS CSV filename → peripheral type when probing would not match. +pub fn csv_type_overrides() -> &'static [(&'static str, &'static str)] { + &[ + ("apb_saradc_reg.csv", "APB_SARADC"), + ("ecc_mult_reg.csv", "ECC"), + ("efuse_mem_reg.csv", "EFUSE"), + ("gpio_ext_reg.csv", "GPIOSD"), + ("spi_mem_reg.csv", "SPI0"), + ("spi1_mem_reg.csv", "SPI1"), + ("spi2_reg.csv", "SPI2"), + ("usb_serial_jtag_reg.csv", "USB_DEVICE"), + ("usb_device_reg.csv", "USB_DEVICE"), + ] +} + +/// IDF `DR_REG__BASE` stem → (instance name, peripheral type). +fn reg_base_alias(stem: &str) -> Option<(&'static str, &'static str)> { + Some(match stem { + "SPIMEM0" => ("SPI0", "SPI0"), + "SPIMEM1" => ("SPI1", "SPI1"), + "GPSPI2" => ("SPI2", "SPI2"), + "GDMA" => ("DMA", "DMA"), + "USB_SERIAL_JTAG" => ("USB_DEVICE", "USB_DEVICE"), + "INTMTX" => ("INTERRUPT_CORE0", "INTMTX_CORE0"), + "TIMERG0" => ("TIMG0", "TIMG"), + "TIMERG1" => ("TIMG1", "TIMG"), + "GPIO_EXT" => ("GPIO_SD", "GPIOSD"), + "HP_SYSTEM" => ("HP_SYS", "HP_SYS"), + "LP_ANA_PERI" => ("LP_ANA", "LP_ANA"), + "MCPWM" => ("MCPWM0", "MCPWM"), + "TWAI" => ("TWAI0", "TWAI"), + "I2S0" => ("I2S0", "I2S"), + "UHCI0" => ("UHCI0", "UHCI"), + "UART0" => ("UART0", "UART"), + "UART1" => ("UART1", "UART"), + "I2C0" => ("I2C0", "I2C"), + "I2C1" => ("I2C1", "I2C"), + "LPPERI" => ("LP_PERI", "LPPERI"), + _ => return None, + }) +} + +/// Reads `soc/interrupts.h` and returns a flat IRQ list (name + hardware source +/// id). +/// +/// Names are normalized toward historical esp-pacs interrupt identifiers. +pub fn load_interrupts_h(path: &Path, chip: &str) -> Result> { + let content = fs::read_to_string(path) + .with_context(|| format!("reading interrupts header {}", path.display()))?; + parse_interrupts_h(&content, chip) + .with_context(|| format!("parsing interrupts header {}", path.display())) +} + +/// Reads `register/soc/reg_base.h` into peripheral instances. +pub fn load_reg_base_h(path: &Path) -> Result> { + let content = fs::read_to_string(path) + .with_context(|| format!("reading reg_base header {}", path.display()))?; + parse_reg_base_h(&content) + .with_context(|| format!("parsing reg_base header {}", path.display())) +} + +pub fn parse_interrupts_h(content: &str, chip: &str) -> Result> { + let mut next_value: i64 = 0; + let mut out = Vec::new(); + let mut in_enum = false; + + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("typedef enum") { + in_enum = true; + next_value = 0; + continue; + } + if !in_enum { + continue; + } + if trimmed.starts_with('}') { + break; + } + + let Some(caps) = ETS_ENUM_ENTRY.captures(line) else { + continue; + }; + let ets = &caps[1]; + if ets == "ETS_MAX_INTR_SOURCE" { + break; + } + if let Some(explicit) = caps.get(2) { + let expr = explicit.as_str().trim(); + // Skip symbolic aliases like `= ETS_FOO_INTR_SOURCE`; keep counting. + if let Ok(v) = parse_c_int(expr) { + next_value = v; + } + } + if next_value < 0 || next_value > u32::MAX as i64 { + bail!("interrupt value out of range for {ets}: {next_value}"); + } + out.push(PeripheralInterrupt { + name: pacify_irq_name(chip, &irq_name_from_ets(ets)), + value: next_value as u32, + description: None, + }); + next_value += 1; + } + + if out.is_empty() { + bail!("no ETS_* interrupt sources found"); + } + Ok(out) +} + +pub fn parse_reg_base_h(content: &str) -> Result> { + let mut out = Vec::new(); + + for caps in DR_REG_BASE.captures_iter(content) { + let stem = &caps[1]; + let addr = u32::from_str_radix(caps[2].trim_start_matches("0x"), 16) + .with_context(|| format!("invalid base address for DR_REG_{stem}_BASE"))?; + let (name, peripheral) = match reg_base_alias(stem) { + Some((n, p)) => (n.to_owned(), p.to_owned()), + None => (stem.to_owned(), stem.to_owned()), + }; + out.push(PeripheralInstance { + name, + peripheral, + base_addr: addr, + description: None, + }); + } + + if let Some(caps) = IEEE802154_BASE.captures(content) { + let addr = u32::from_str_radix(caps[1].trim_start_matches("0x"), 16) + .context("invalid IEEE802154_REG_BASE")?; + out.push(PeripheralInstance { + name: "IEEE802154".to_owned(), + peripheral: "IEEE802154".to_owned(), + base_addr: addr, + description: None, + }); + } + + if out.is_empty() { + bail!("no DR_REG_*_BASE definitions found"); + } + + out.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(out) +} + +fn irq_name_from_ets(ets: &str) -> String { + let mut name = ets.strip_prefix("ETS_").unwrap_or(ets).to_owned(); + if let Some(rest) = name.strip_suffix("_INTR_SOURCE") { + name = rest.to_owned(); + } else if let Some(rest) = name.strip_suffix("_SOURCE") { + // e.g. ETS_BT_BB_NMI_SOURCE → BT_BB_NMI + name = rest.to_owned(); + } + name +} + +/// Map IDF enum stems onto names historically used in esp-pacs PACs. +fn pacify_irq_name(chip: &str, name: &str) -> String { + // Cross-chip exact renames. + let name = match name { + "USB_SERIAL_JTAG" => return "USB_DEVICE".to_owned(), + "GPIO_INTERRUPT_PRO" => return "GPIO".to_owned(), + "GPIO_INTERRUPT_PRO_NMI" => return "GPIO_NMI".to_owned(), + "PWM" | "PWM0" => return "MCPWM0".to_owned(), + "PWM1" => return "MCPWM1".to_owned(), + "TWAI" => return "TWAI0".to_owned(), + "WIFI_MAC_NMI" if matches!(chip, "esp32" | "esp32s2" | "esp32s3") => { + return "WIFI_NMI".to_owned(); + } + "GSPI2" if chip.contains("c6") => return "SPI2".to_owned(), + "GSPI2" => return "GPSPI2".to_owned(), + other => other, + }; + + if let Some(idx) = name.strip_prefix("CPU_INTR_FROM_CPU_") { + return format!("FROM_CPU_INTR{idx}"); + } + + // Timer IRQs stay as IDF names (`TG0_T0`). Chips that want `TG0_T0_LEVEL` + // in the PAC do that in svd patches (e.g. esp32p4 / esp32s31). + + name.to_owned() +} + +fn parse_c_int(expr: &str) -> Result { + let expr = expr.trim(); + if let Some(hex) = expr.strip_prefix("0x").or_else(|| expr.strip_prefix("0X")) { + return i64::from_str_radix(hex, 16).context("hex literal"); + } + expr.parse().context("decimal literal") +} diff --git a/xtask/src/regdesc/merge.rs b/xtask/src/regdesc/merge.rs new file mode 100644 index 0000000000..792d0eca23 --- /dev/null +++ b/xtask/src/regdesc/merge.rs @@ -0,0 +1,345 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use regex::Regex; + +use super::{ + model::{ExpandContext, ExpandValue, Field, Register, Repeat}, + util::remove_index_from_strings, +}; + +static INDEX_VAR_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\$([a-zA-Z])").unwrap()); + +#[derive(Debug, Clone)] +pub struct MergeError(pub String); + +/// Collapses repeated field rows (CH0, CH1, …) into one field with a dim array. +pub fn merge_fields(fields: Vec) -> (Vec, Vec) { + let mut items_by_repeat_name: HashMap> = HashMap::new(); + let mut result = Vec::new(); + let mut merge_errors = Vec::new(); + + for mut field in fields { + match (&field.repeat_name_hint, field.repeat_index_hint) { + (Some(name), Some(_)) => { + items_by_repeat_name + .entry(name.clone()) + .or_default() + .push(field); + } + (Some(name), None) => { + field.name = name.clone(); + field.repeat_name_hint = None; + result.push(field); + } + _ => result.push(field), + } + } + + for (repeat_name, mut items_list) in items_by_repeat_name { + items_list.sort_by_key(|f| f.repeat_index_hint.unwrap_or(0)); + match merge_field_group(&mut items_list, &repeat_name) { + Ok(merged) => result.push(merged), + Err(err) => { + merge_errors.push(err); + result.extend(items_list); + } + } + } + + result.sort_by_key(|f| f.shift); + (result, merge_errors) +} + +/// Same as `merge_fields`, but for registers. +pub fn merge_registers(registers: Vec) -> (Vec, Vec) { + let mut items_by_repeat_name: HashMap> = HashMap::new(); + let mut result = Vec::new(); + let mut merge_errors = Vec::new(); + + for mut register in registers { + match (®ister.repeat_name_hint, register.repeat_index_hint) { + (Some(name), Some(_)) => { + items_by_repeat_name + .entry(name.clone()) + .or_default() + .push(register); + } + (Some(name), None) => { + register.name = name.clone(); + register.repeat_name_hint = None; + result.push(register); + } + _ => result.push(register), + } + } + + for (repeat_name, mut items_list) in items_by_repeat_name { + items_list.sort_by_key(|r| r.repeat_index_hint.unwrap_or(0)); + match merge_register_group(&mut items_list, &repeat_name) { + Ok(merged) => result.push(merged), + Err(err) => { + merge_errors.push(err); + result.extend(items_list); + } + } + } + + result.sort_by_key(|r| r.addr); + (result, merge_errors) +} + +/// Actually merges one group of repeated fields into a single dim'd field. +fn merge_field_group(fields: &mut [Field], repeat_name: &str) -> Result { + let start = merge_get_start(fields, repeat_name)?; + for field in &fields[1..] { + if field.mask != fields[0].mask + || field.access != fields[0].access + || field.default != fields[0].default + || field.visible != fields[0].visible + || field.min_val != fields[0].min_val + || field.max_val != fields[0].max_val + { + return Err(MergeError(format!( + "Field {} differs from {}", + field.name, fields[0].name + ))); + } + } + let stride = merge_get_stride(repeat_name, fields, |f| f.shift)?; + let index_var = merge_get_index_var(repeat_name)?; + merge_validate_names(fields, &index_var, repeat_name, |f| &f.name)?; + merge_validate_descriptions( + fields, + &index_var, + |f| &f.name, + |f| f.description.clone(), + |f, d| f.description = d, + )?; + + let mut merged = fields[0].clone(); + merged.name = repeat_name.to_owned(); + merged.repeat = Some(Repeat::new(fields.len() as u32, stride, index_var, start)); + merged.repeat_name_hint = None; + merged.repeat_index_hint = None; + Ok(merged) +} + +/// Actually merges one group of repeated registers into a single dim'd +/// register. +fn merge_register_group( + registers: &mut [Register], + repeat_name: &str, +) -> Result { + let start = merge_get_start(registers, repeat_name)?; + for register in ®isters[1..] { + if register.visible != registers[0].visible { + return Err(MergeError(format!( + "Register {} visible differs from {}", + register.name, registers[0].name + ))); + } + } + let stride = merge_get_stride(repeat_name, registers, |r| r.addr)?; + let index_var = merge_get_index_var(repeat_name)?; + merge_validate_names(registers, &index_var, repeat_name, |r| &r.name)?; + merge_validate_descriptions( + registers, + &index_var, + |r| &r.name, + |r| r.description.clone(), + |r, d| r.description = d, + )?; + + let mut merged = registers[0].clone(); + merged.name = repeat_name.to_owned(); + merged.repeat = Some(Repeat::new( + registers.len() as u32, + stride, + index_var, + start, + )); + merged.repeat_name_hint = None; + merged.repeat_index_hint = None; + Ok(merged) +} + +/// Checks repeat indices are consecutive (0, 1, 2 — not 0, 2, 5). +fn merge_get_start(items_list: &[T], repeat_name: &str) -> Result +where + T: RepeatHint, +{ + let value_0 = items_list[0].repeat_index_hint().unwrap_or(0); + for (idx, item) in items_list.iter().enumerate() { + if item.repeat_index_hint() != Some(value_0 + idx as i32) { + let indices: Vec<_> = items_list + .iter() + .map(|i| i.repeat_index_hint().unwrap_or(-1)) + .collect(); + return Err(MergeError(format!( + "Repeat indices for {repeat_name} should be consecutive integers starting from {value_0}. Got {indices:?}" + ))); + } + } + Ok(value_0) +} + +/// Computes the address/bit spacing between repeated items. +fn merge_get_stride( + repeat_name: &str, + items_list: &[T], + get_offset: F, +) -> Result +where + F: Fn(&T) -> u32, +{ + if items_list.len() <= 1 { + return Ok(0); + } + let offsets: Vec = items_list.iter().map(get_offset).collect(); + let offset_0 = offsets[0]; + let mut offsets_from_0 = Vec::with_capacity(offsets.len()); + for &offset in &offsets { + let Some(delta) = offset.checked_sub(offset_0) else { + return Err(MergeError(format!( + "Repeat offsets for {repeat_name} are not monotonically increasing. Got {offsets:?}" + ))); + }; + offsets_from_0.push(delta); + } + let stride = offsets_from_0[1]; + if !offsets_from_0 + .iter() + .enumerate() + .all(|(idx, val)| *val == idx as u32 * stride) + { + return Err(MergeError(format!( + "Repeat offsets for {repeat_name} should be equally spaced. Got {offset_0} + {offsets_from_0:?}" + ))); + } + Ok(stride) +} + +/// Pulls the `$n` (or `$a`) variable out of a repeat name. +fn merge_get_index_var(repeat_name: &str) -> Result { + let index_vars: Vec<_> = INDEX_VAR_REGEX + .captures_iter(repeat_name) + .map(|c| c[1].to_owned()) + .collect(); + if index_vars.is_empty() { + return Err(MergeError(format!( + "No index vars found in name: {repeat_name}" + ))); + } + if index_vars.len() > 1 { + return Err(MergeError(format!( + "Multiple index vars not supported yet ({repeat_name})" + ))); + } + Ok(index_vars[0].clone()) +} + +/// Makes sure each item's real name matches what the repeat pattern expects. +fn merge_validate_names( + items_list: &[T], + index_var: &str, + repeat_name: &str, + get_name: F, +) -> Result<(), MergeError> +where + T: RepeatHint, + F: Fn(&T) -> &str, +{ + let start = items_list[0].repeat_index_hint().unwrap_or(0); + for (idx, item) in items_list.iter().enumerate() { + let mut ctx = ExpandContext::default(); + ctx.insert(index_var.to_owned(), ExpandValue::Int(start + idx as i32)); + let expanded_name = ctx.replace(repeat_name); + let real_name = get_name(item); + if !expanded_name.contains(real_name) { + return Err(MergeError(format!( + "Name {real_name} doesn't match repeat name {repeat_name} with index {}", + start + idx as i32 + ))); + } + } + Ok(()) +} + +/// Tries to merge descriptions that only differ by the index number. +fn merge_validate_descriptions( + items_list: &mut [T], + index_var: &str, + get_name: impl Fn(&T) -> &str, + get_desc: impl Fn(&T) -> String, + set_desc: impl Fn(&mut T, String), +) -> Result<(), MergeError> +where + T: RepeatHint + Clone, +{ + if items_list.len() < 2 { + return Ok(()); + } + + let item_0 = items_list[0].clone(); + let item_0_index = items_list[0].repeat_index_hint().unwrap_or(0); + let item_0_value = get_desc(&item_0); + let mut new_value = String::new(); + let mut replaced = vec![false; items_list.len()]; + + for (i, item) in items_list.iter().enumerate() { + let item_value = get_desc(item); + let item_index = item.repeat_index_hint().unwrap_or(i as i32); + if item_value == item_0_value { + continue; + } + let candidate = remove_index_from_strings( + &item_0_value, + &item_value, + item_0_index, + item_index, + &format!("${index_var}"), + ) + .map_err(MergeError)?; + if !new_value.is_empty() && new_value != candidate { + return Err(MergeError(format!( + "Failed to merge description of {} and {}", + get_name(&item_0), + get_name(item) + ))); + } + new_value = candidate; + replaced[i] = true; + } + + if replaced.iter().skip(1).all(|&x| !x) { + return Ok(()); + } + if replaced.iter().skip(1).all(|&x| x) { + for item in items_list.iter_mut() { + set_desc(item, new_value.clone()); + } + return Ok(()); + } + + Err(MergeError(format!( + "Failed to merge description of {}", + get_name(&item_0) + ))) +} + +/// Lets merge helpers read the repeat index from a field or register. +trait RepeatHint { + fn repeat_index_hint(&self) -> Option; +} + +impl RepeatHint for Field { + fn repeat_index_hint(&self) -> Option { + self.repeat_index_hint + } +} + +impl RepeatHint for Register { + fn repeat_index_hint(&self) -> Option { + self.repeat_index_hint + } +} diff --git a/xtask/src/regdesc/mod.rs b/xtask/src/regdesc/mod.rs new file mode 100644 index 0000000000..4fce263f0d --- /dev/null +++ b/xtask/src/regdesc/mod.rs @@ -0,0 +1,139 @@ +mod config; +mod csv; +mod idf; +mod merge; +mod model; +mod svd; +mod util; + +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +pub use config::default_output_path; +use config::{discover_csv_files, load_descriptions, peripheral_descriptions_path}; +use model::RegdescFragment; + +/// IDF headers that supply peripheral bases and the IRQ list. +#[derive(Debug, Clone)] +pub struct GenerateSources { + /// Path to `soc/interrupts.h`. + pub interrupts: PathBuf, + /// Path to `register/soc/reg_base.h` (or chip-equivalent). + pub reg_base: PathBuf, +} + +/// Main entry: load IDF metadata + CSVs, build the chip model, write an SVD +/// file. +pub fn generate_base_svd( + chip: &str, + csv_dir: &Path, + output: &Path, + version: u32, + sources: &GenerateSources, +) -> Result<()> { + log::info!( + "Generating base SVD for {chip} (version {version}) using {} and {}", + sources.reg_base.display(), + sources.interrupts.display() + ); + + let fragment = load_fragment(chip, csv_dir, sources)?; + write_svd(output, &fragment, version) +} + +fn load_fragment(chip: &str, csv_dir: &Path, sources: &GenerateSources) -> Result { + let mut instances = idf::load_reg_base_h(&sources.reg_base)?; + let interrupts = idf::load_interrupts_h(&sources.interrupts, chip)?; + log::info!( + "loaded {} interrupts from {}", + interrupts.len(), + sources.interrupts.display() + ); + + let mut csv_overrides = HashMap::new(); + for &(csv, peri) in idf::csv_type_overrides() { + csv_overrides.insert(csv.to_owned(), peri.to_owned()); + } + + let descriptions = load_descriptions(&peripheral_descriptions_path())?; + config::apply_instance_descriptions(&mut instances, &descriptions); + + let required_types: HashSet<&str> = instances.iter().map(|i| i.peripheral.as_str()).collect(); + + let mut peripherals = Vec::new(); + for csv_path in discover_csv_files(csv_dir)? { + let csv_file = csv_path + .file_name() + .and_then(|n| n.to_str()) + .with_context(|| format!("invalid CSV path {}", csv_path.display()))? + .to_owned(); + + let content = fs::read_to_string(&csv_path) + .with_context(|| format!("reading CSV {}", csv_path.display()))?; + + let name_override = csv_overrides.get(&csv_file).map(String::as_str); + let probed = csv::probe_peripheral_name_from_content(&content)?; + + let peripheral_name = match name_override { + Some(name) => name, + None => match probed.as_deref() { + Some(name) if required_types.contains(name) => name, + _ => { + log::debug!("skipping {csv_file} (not referenced by peripheral instances)"); + continue; + } + }, + }; + + if !required_types.contains(peripheral_name) { + log::warn!( + "CSV {csv_file} maps to '{peripheral_name}' but no instance uses that type; skipping" + ); + continue; + } + + let mut peripheral = csv::read_peripheral_csv(&content, Some(peripheral_name)) + .with_context(|| format!("parsing CSV {}", csv_path.display()))?; + + for err in peripheral.merge_registers_fields() { + log::warn!("merge warning in {csv_file}: {}", err.0); + } + + log::info!("parsed {csv_file} -> {}", peripheral.name); + peripherals.push(peripheral); + } + + for (csv_file, peri_type) in &csv_overrides { + if required_types.contains(peri_type.as_str()) + && !peripherals.iter().any(|p| p.name == *peri_type) + { + log::warn!( + "CSV override '{csv_file}' → '{peri_type}' was not found in {}", + csv_dir.display() + ); + } + } + + Ok(config::build_fragment( + chip, + peripherals, + instances, + interrupts, + )) +} + +fn write_svd(output: &Path, fragment: &RegdescFragment, version: u32) -> Result<()> { + let xml = svd::write_svd(fragment, version); + + if let Some(parent) = output.parent() { + fs::create_dir_all(parent)?; + } + fs::write(output, xml).with_context(|| format!("writing SVD to {}", output.display()))?; + + log::info!("Wrote {}", output.display()); + Ok(()) +} diff --git a/xtask/src/regdesc/model.rs b/xtask/src/regdesc/model.rs new file mode 100644 index 0000000000..3ab008d14a --- /dev/null +++ b/xtask/src/regdesc/model.rs @@ -0,0 +1,414 @@ +use std::collections::HashMap; + +use super::merge::{merge_fields, merge_registers, MergeError}; + +#[derive(Debug, Clone)] +pub struct Repeat { + pub count: u32, + pub stride: u32, + pub index_var: String, + pub start: i32, +} + +impl Repeat { + /// Creates a repeat spec (count, stride, index var, start index). + pub fn new(count: u32, stride: u32, index_var: impl Into, start: i32) -> Self { + Self { + count, + stride, + index_var: index_var.into(), + start, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct ExpandContext { + values: HashMap, +} + +#[derive(Debug, Clone)] +pub enum ExpandValue { + Int(i32), + Str(String), +} + +impl ExpandContext { + /// Stores a value for a `$placeholder`. + pub fn insert(&mut self, key: impl Into, value: ExpandValue) { + self.values.insert(key.into(), value); + } + + /// Substitutes `$n` etc. in a string with actual values. + pub fn replace(&self, text: &str) -> String { + let mut text = text.to_owned(); + for (placeholder, value) in &self.values { + let token = format!("${placeholder}"); + match value { + ExpandValue::Str(subst) => { + text = text.replace(&token, subst); + } + ExpandValue::Int(n) => { + text = text.replace(&token, &n.to_string()); + } + } + } + text + } +} + +#[derive(Debug, Clone)] +pub struct Field { + pub name: String, + pub shift: u32, + pub mask: u32, + pub access: String, + pub default: u64, + pub description: String, + pub visible: bool, + pub min_val: Option, + pub max_val: Option, + pub repeat: Option, + pub repeat_name_hint: Option, + pub repeat_index_hint: Option, +} + +impl Field { + /// How many bits this field covers (counts ones in the mask). + pub fn bit_width(&self) -> u32 { + self.mask.count_ones() + } + + /// Marks this field as "item N of a repeat group". + pub fn set_repeat_hint(&mut self, name: String, index: i32) { + self.repeat_name_hint = Some(name); + self.repeat_index_hint = Some(index); + } + + /// Spreads a repeated field into individual copies for SVD output. + pub fn expand(&self, ctx: &ExpandContext) -> Vec { + let repeat = self + .repeat + .clone() + .unwrap_or_else(|| Repeat::new(1, 0, "n", 0)); + let mut result = Vec::new(); + for i in 0..repeat.count { + let mut field = self.clone(); + field.repeat = None; + field.shift += repeat.stride * i; + let mut expand_ctx = ctx.clone(); + if self.repeat.is_some() && !expand_ctx.values.contains_key(&repeat.index_var) { + expand_ctx.insert( + repeat.index_var.clone(), + ExpandValue::Int(i as i32 + repeat.start), + ); + } + field.name = expand_ctx.replace(&field.name); + field.description = expand_ctx.replace(&field.description); + result.push(field); + } + result + } +} + +#[derive(Debug, Clone)] +pub struct Register { + pub name: String, + pub addr: u32, + pub fields: Vec, + pub description: String, + pub visible: bool, + pub size: u32, + pub repeat: Option, + pub repeat_name_hint: Option, + pub repeat_index_hint: Option, + pub is_mem_region: bool, + pub expand_context: ExpandContext, +} + +impl Register { + /// Computes the register reset value from all field defaults OR'd together. + pub fn reset_value(&self) -> u64 { + self.fields + .iter() + .fold(0u64, |acc, field| acc | (field.default << field.shift)) + } + + /// Marks this register as "item N of a repeat group". + pub fn set_repeat_hint(&mut self, name: String, index: i32) { + self.repeat_name_hint = Some(name); + self.repeat_index_hint = Some(index); + } + + /// Runs field merging on this register. + pub fn merge_fields(&mut self) -> Vec { + let (merged, errors) = merge_fields(std::mem::take(&mut self.fields)); + self.fields = merged; + errors + } + + /// Returns all fields after repeat expansion. + pub fn expanded_fields(&self) -> Vec { + let ctx = &self.expand_context; + let mut fields = Vec::new(); + for field in &self.fields { + fields.extend(field.expand(ctx)); + } + fields.sort_by_key(|f| f.shift); + fields + } + + /// Spreads a repeated register into individual copies. + pub fn expand(&self, ctx: &ExpandContext) -> Vec { + let repeat = self + .repeat + .clone() + .unwrap_or_else(|| Repeat::new(1, 0, "n", 0)); + let mut result = Vec::new(); + for i in 0..repeat.count { + let mut reg = self.clone(); + reg.repeat = None; + reg.addr += repeat.stride * i; + let mut expand_ctx = ctx.clone(); + if self.repeat.is_some() && !expand_ctx.values.contains_key(&repeat.index_var) { + expand_ctx.insert( + repeat.index_var.clone(), + ExpandValue::Int(i as i32 + repeat.start), + ); + } + reg.name = expand_ctx.replace(®.name); + reg.description = expand_ctx.replace(®.description); + result.push(reg); + } + result + } + + /// Swaps `$n` for `%s` (SVD dim syntax) before writing. + pub fn replace_placeholders(&self, sub_expr: &str) -> Register { + let mut reg = self.clone(); + if let Some(repeat) = &self.repeat { + let mut ctx = ExpandContext::default(); + ctx.insert( + repeat.index_var.clone(), + ExpandValue::Str(sub_expr.to_owned()), + ); + reg.expand_context = ctx.clone(); + reg.name = ctx.replace(®.name); + reg.description = ctx.replace(®.description); + } + reg + } +} + +#[derive(Debug, Clone)] +pub struct RegisterGroup { + pub name: Option, + pub description: String, + pub registers: Vec, + pub visible: bool, + pub repeat: Option, + pub offset: u32, +} + +impl RegisterGroup { + /// Adds a register and keeps them sorted by address. + pub fn add_register(&mut self, reg: Register) { + self.registers.push(reg); + self.registers.sort_by_key(|r| r.addr); + } + + /// Runs register merging within this group. + pub fn merge_registers(&mut self) -> Vec { + let (merged, errors) = merge_registers(std::mem::take(&mut self.registers)); + self.registers = merged; + errors + } + + /// Returns all registers after group + register repeat expansion. + pub fn expanded_registers(&self) -> Vec { + let groups = if self.repeat.is_some() { + self.expand(&ExpandContext::default()) + } else { + vec![self.clone()] + }; + + let mut result = Vec::new(); + for group in groups { + for register in &group.registers { + let expanded = register.expand(&ExpandContext::default()); + for mut reg in expanded { + reg.addr += group.offset; + result.push(reg); + } + } + } + result.sort_by_key(|r| r.addr); + result + } + + /// Expands a repeated register group into copies. + fn expand(&self, ctx: &ExpandContext) -> Vec { + let repeat = self + .repeat + .clone() + .unwrap_or_else(|| Repeat::new(1, 0, "n", 0)); + let mut result = Vec::new(); + for i in 0..repeat.count { + let mut group = self.clone(); + group.repeat = None; + group.offset += repeat.stride * i; + let mut expand_ctx = ctx.clone(); + if self.repeat.is_some() && !expand_ctx.values.contains_key(&repeat.index_var) { + expand_ctx.insert( + repeat.index_var.clone(), + ExpandValue::Int(i as i32 + repeat.start), + ); + } + if let Some(name) = &group.name { + group.name = Some(expand_ctx.replace(name)); + } + result.push(group); + } + result + } +} + +#[derive(Debug, Clone)] +pub struct Peripheral { + pub name: String, + pub register_groups: Vec, + #[allow(dead_code)] + pub description: String, +} + +impl Peripheral { + /// Runs merge on all registers and fields in this peripheral. + pub fn merge_registers_fields(&mut self) -> Vec { + let mut errors = Vec::new(); + for group in &mut self.register_groups { + errors.extend(group.merge_registers()); + for register in &mut group.registers { + errors.extend(register.merge_fields()); + } + } + errors + } + + /// Flat list of all registers, sorted by address. + pub fn sorted_registers(&self) -> Vec { + let mut all: Vec<_> = self + .register_groups + .iter() + .flat_map(|g| g.expanded_registers()) + .collect(); + all.sort_by_key(|r| r.addr); + all + } +} + +#[derive(Debug, Clone)] +pub struct PeripheralInstance { + pub name: String, + pub peripheral: String, + pub base_addr: u32, + pub description: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PeripheralInterrupt { + pub name: String, + pub value: u32, + pub description: Option, +} + +#[derive(Debug, Clone)] +pub struct ChipInfo { + pub name: String, + pub description: String, + pub cpu_name: String, + pub cpu_revision: String, + pub cpu_endian: String, + pub mpu_present: bool, + pub fpu_present: bool, + pub nvic_prio_bits: u32, + pub vendor_systick_config: bool, +} + +impl ChipInfo { + /// Builds chip metadata (CPU type, description, FPU, etc.) from + /// `"esp32s31"`. + pub fn from_chip_id(chip: &str) -> Self { + let formal = super::util::pretty_chip_name(chip); + let description = match formal.as_str() { + "ESP32" => "32-bit MCU & 2.4 GHz Wi-Fi & Bluetooth/Bluetooth LE", + "ESP32-C2" => "32-bit RISC-V MCU & 2.4 GHz Wi-Fi & Bluetooth 5 (LE)", + "ESP32-C3" => "32-bit RISC-V MCU & 2.4 GHz Wi-Fi & Bluetooth 5 (LE)", + "ESP32-C5" => { + "32-bit RISC-V MCU & 2.4 and 5 GHz Wi-Fi 6 & Bluetooth 5 (LE) & IEEE 802.15.4" + } + "ESP32-C6" => "32-bit RISC-V MCU & 2.4 GHz Wi-Fi 6 & Bluetooth 5 (LE) & IEEE 802.15.4", + "ESP32-C6-LP" => "32-bit RISC-V MCU", + "ESP32-C61" => "32-bit RISC-V MCU & 2.4 GHz Wi-Fi 6 & Bluetooth 5 (LE)", + "ESP32-H2" => "32-bit RISC-V MCU & Bluetooth 5 (LE) & IEEE 802.15.4", + "ESP32-P4" => "32-bit RISC-V MCU", + "ESP32-S2" => "32-bit MCU & 2.4 GHz Wi-Fi", + "ESP32-S2-ULP" => "32-bit RISC-V MCU", + "ESP32-S3" => "32-bit MCU & 2.4 GHz Wi-Fi & Bluetooth 5 (LE)", + "ESP32-S3-ULP" => "32-bit RISC-V MCU", + "ESP32-S31" => "32-bit RISC-V MCU & 2.4 GHz Wi-Fi & Bluetooth 5 (LE) & IEEE 802.15.4", + _ => "No Chip Description", + } + .to_owned(); + + let cpu_name = match formal.as_str() { + "ESP32" => "Xtensa LX6", + "ESP32-S2" | "ESP32-S3" => "Xtensa LX7", + "ESP32-C2" | "ESP32-C3" | "ESP32-S2-ULP" | "ESP32-S3-ULP" => "RV32IMC", + "ESP32-P4" | "ESP32-S31" => "RV32IMAFC", + _ => "RV32IMAC", + } + .to_owned(); + + let fpu_present = matches!( + formal.as_str(), + "ESP32" | "ESP32-P4" | "ESP32-S3" | "ESP32-S31" + ); + + Self { + name: formal, + description, + cpu_name, + cpu_revision: "r0p0".to_owned(), + cpu_endian: "little".to_owned(), + mpu_present: false, + fpu_present, + nvic_prio_bits: 0, + vendor_systick_config: false, + } + } + + /// Returns the marketing series string like `"ESP32 S-Series"`. + pub fn series(&self) -> &str { + if self.name.contains("ULP") { + "RISC-V ULP" + } else if self.name.starts_with("ESP32-C") { + "ESP32 C-Series" + } else if self.name.starts_with("ESP32-H") { + "ESP32 H-Series" + } else if self.name.starts_with("ESP32-P") { + "ESP32 P-Series" + } else if self.name.starts_with("ESP32-S") { + "ESP32 S-Series" + } else { + "ESP32 Series" + } + } +} + +#[derive(Debug)] +pub struct RegdescFragment { + pub peripherals: Vec, + pub peripheral_instances: Vec, + pub peripheral_interrupts: Vec, + pub chip_info: ChipInfo, +} diff --git a/xtask/src/regdesc/svd.rs b/xtask/src/regdesc/svd.rs new file mode 100644 index 0000000000..e5681a2e4d --- /dev/null +++ b/xtask/src/regdesc/svd.rs @@ -0,0 +1,455 @@ +use std::{fmt::Write as _, sync::LazyLock}; + +use regex::Regex; + +use super::{ + model::{ + ChipInfo, + Peripheral, + PeripheralInstance, + PeripheralInterrupt, + RegdescFragment, + Register, + Repeat, + }, + util::{guess_field_access, simplify_name}, +}; + +static FIELD_DESC_INDEX: LazyLock = LazyLock::new(|| Regex::new(r"(?i)\$[a-z]").unwrap()); + +/// Turns a chip model into a full SVD XML string. +pub fn write_svd(fragment: &RegdescFragment, version: u32) -> String { + let mut out = String::new(); + let chip = &fragment.chip_info; + + writeln!(out, "").unwrap(); + writeln!( + out, + "" + ) + .unwrap(); + + write_device_preamble(&mut out, chip, version); + write_cpu(&mut out, chip); + write_peripherals(&mut out, fragment); + + writeln!(out, "").unwrap(); + out +} + +/// Writes vendor, chip name, version, description, and the license block. +fn write_device_preamble(out: &mut String, chip: &ChipInfo, version: u32) { + let year = time_year(); + writeln!( + out, + " ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD." + ) + .unwrap(); + writeln!(out, " ESPRESSIF").unwrap(); + writeln!(out, " {}", xml_escape(&chip.name)).unwrap(); + writeln!(out, " {}", xml_escape(chip.series())).unwrap(); + writeln!(out, " {version}").unwrap(); + writeln!( + out, + " {}", + xml_escape(&chip.description) + ) + .unwrap(); + writeln!(out, " ").unwrap(); + writeln!( + out, + " Copyright {year} Espressif Systems (Shanghai) PTE LTD" + ) + .unwrap(); + writeln!(out).unwrap(); + writeln!( + out, + " Licensed under the Apache License, Version 2.0 (the "License");" + ) + .unwrap(); + writeln!( + out, + " you may not use this file except in compliance with the License." + ) + .unwrap(); + writeln!(out, " You may obtain a copy of the License at").unwrap(); + writeln!(out).unwrap(); + writeln!(out, " http://www.apache.org/licenses/LICENSE-2.0").unwrap(); + writeln!(out).unwrap(); + writeln!( + out, + " Unless required by applicable law or agreed to in writing, software" + ) + .unwrap(); + writeln!( + out, + " distributed under the License is distributed on an "AS IS" BASIS," + ) + .unwrap(); + writeln!( + out, + " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." + ) + .unwrap(); + writeln!( + out, + " See the License for the specific language governing permissions and" + ) + .unwrap(); + writeln!(out, " limitations under the License.").unwrap(); + writeln!(out, " ").unwrap(); +} + +/// Writes the `` section and global width/reset settings. +fn write_cpu(out: &mut String, chip: &ChipInfo) { + writeln!(out, " ").unwrap(); + writeln!(out, " {}", chip.cpu_name).unwrap(); + writeln!(out, " {}", chip.cpu_revision).unwrap(); + writeln!(out, " {}", chip.cpu_endian).unwrap(); + writeln!(out, " {}", chip.mpu_present).unwrap(); + writeln!(out, " {}", chip.fpu_present).unwrap(); + writeln!( + out, + " {}", + chip.nvic_prio_bits + ) + .unwrap(); + writeln!( + out, + " {}", + chip.vendor_systick_config + ) + .unwrap(); + writeln!(out, " ").unwrap(); + writeln!(out, " 32").unwrap(); + writeln!(out, " 32").unwrap(); + writeln!(out, " 0x00000000").unwrap(); + writeln!(out, " 0xFFFFFFFF").unwrap(); +} + +/// Loops all peripherals, matches them to instances, writes each block. +fn write_peripherals(out: &mut String, fragment: &RegdescFragment) { + writeln!(out, " ").unwrap(); + + let mut indices: Vec<_> = (0..fragment.peripherals.len()).collect(); + indices.sort_by_key(|&i| fragment.peripherals[i].name.as_str()); + + for i in indices { + let peripheral = &fragment.peripherals[i]; + let mut instances: Vec<_> = fragment + .peripheral_instances + .iter() + .filter(|instance| instance.peripheral == peripheral.name) + .cloned() + .collect(); + + if instances.is_empty() { + log::warn!( + "no instances found for peripheral '{}', skipping", + peripheral.name + ); + continue; + } + + if instances.len() > 1 { + instances.sort_by(|a, b| a.name.cmp(&b.name)); + write_derived_peripheral(out, fragment, peripheral, &instances); + } else { + write_peripheral(out, fragment, peripheral, &instances[0]); + } + } + + writeln!(out, " ").unwrap(); +} + +/// Writes extra instances as `derivedFrom` stubs (same layout, different base +/// addr). +fn write_derived_peripheral( + out: &mut String, + fragment: &RegdescFragment, + peripheral: &Peripheral, + instances: &[PeripheralInstance], +) { + let (first, rest) = instances.split_first().unwrap(); + write_peripheral(out, fragment, peripheral, first); + + for instance in rest { + writeln!(out, " ", first.name).unwrap(); + writeln!(out, " {}", xml_escape(&instance.name)).unwrap(); + writeln!( + out, + " {:#x}", + instance.base_addr + ) + .unwrap(); + if let Some(description) = &instance.description { + writeln!( + out, + " {}", + xml_escape(description) + ) + .unwrap(); + } + + // Interrupts are emitted once on the IRQ host peripheral only. + writeln!(out, " ").unwrap(); + } +} + +/// Writes one full peripheral block (name, address, interrupts, registers). +fn write_peripheral( + out: &mut String, + fragment: &RegdescFragment, + peripheral: &Peripheral, + instance: &PeripheralInstance, +) { + let description = instance + .description + .clone() + .unwrap_or_else(|| format!("Peripheral {}", instance.name)); + + writeln!(out, " ").unwrap(); + writeln!(out, " {}", xml_escape(&instance.name)).unwrap(); + writeln!( + out, + " {}", + xml_escape(&description) + ) + .unwrap(); + writeln!( + out, + " {}", + xml_escape(&peripheral.name) + ) + .unwrap(); + writeln!( + out, + " {:#x}", + instance.base_addr + ) + .unwrap(); + + write_address_block(out, peripheral); + + if is_interrupt_host(fragment, &instance.name) { + write_instance_interrupts(out, fragment); + } + + write_registers(out, peripheral); + writeln!(out, " ").unwrap(); +} + +/// CMSIS-SVD requires interrupts under some ``; we park the flat +/// list on `INTERRUPT_CORE0` (or the first instance if that is missing). +fn interrupt_host_name(fragment: &RegdescFragment) -> Option<&str> { + let names: Vec<&str> = fragment + .peripheral_instances + .iter() + .map(|i| i.name.as_str()) + .collect(); + if names.iter().any(|&n| n == "INTERRUPT_CORE0") { + Some("INTERRUPT_CORE0") + } else { + names.first().copied() + } +} + +fn is_interrupt_host(fragment: &RegdescFragment, instance_name: &str) -> bool { + interrupt_host_name(fragment) == Some(instance_name) +} + +fn write_instance_interrupts(out: &mut String, fragment: &RegdescFragment) { + let mut interrupts = fragment.peripheral_interrupts.clone(); + interrupts.sort_by_key(|i| i.value); + for interrupt in &interrupts { + write_interrupt(out, interrupt); + } +} + +/// Writes the `` size summary. +fn write_address_block(out: &mut String, peripheral: &Peripheral) { + let size: u32 = peripheral.sorted_registers().iter().map(|r| r.size).sum(); + writeln!(out, " ").unwrap(); + writeln!(out, " 0x0").unwrap(); + writeln!(out, " {:#x}", size).unwrap(); + writeln!(out, " registers").unwrap(); + writeln!(out, " ").unwrap(); +} + +/// Writes one `` entry. +fn write_interrupt(out: &mut String, interrupt: &PeripheralInterrupt) { + writeln!(out, " ").unwrap(); + writeln!(out, " {}", xml_escape(&interrupt.name)).unwrap(); + writeln!(out, " {}", interrupt.value).unwrap(); + if let Some(description) = &interrupt.description { + writeln!( + out, + " {}", + xml_escape(description) + ) + .unwrap(); + } + writeln!(out, " ").unwrap(); +} + +/// Writes all registers for a peripheral (name cleanup, dim handling, etc.). +fn write_registers(out: &mut String, peripheral: &Peripheral) { + writeln!(out, " ").unwrap(); + + let mut registers: Vec = peripheral + .register_groups + .iter() + .flat_map(|g| g.registers.clone()) + .collect(); + registers.sort_by_key(|r| r.addr); + + for mut register in registers { + register = register.replace_placeholders("%s"); + register.name = simplify_name(&peripheral.name, ®ister.name); + + if register.is_mem_region { + register.name = format!("{}[%s]", register.name); + register.repeat = Some(Repeat::new(register.size, 1, "n", 0)); + register.size = 1; + } + + write_register(out, peripheral, ®ister); + } + + writeln!(out, " ").unwrap(); +} + +/// Writes one `` element. +fn write_register(out: &mut String, peripheral: &Peripheral, register: &Register) { + writeln!(out, " ").unwrap(); + if let Some(repeat) = ®ister.repeat { + if repeat.count > 1 { + writeln!(out, " {}", repeat.count).unwrap(); + writeln!( + out, + " {:#x}", + repeat.stride + ) + .unwrap(); + // Emit dimIndex whenever the sequence doesn't start at 0 so two + // arrays that share a `%s` name (e.g. RMT TX `$n` vs RX `$m`) + // expand to distinct register names (CH0… vs CH2…). + if repeat.start != 0 { + let indices: String = (0..repeat.count) + .map(|i| (repeat.start + i as i32).to_string()) + .collect::>() + .join(","); + writeln!(out, " {indices}").unwrap(); + } + } + } + writeln!(out, " {}", xml_escape(®ister.name)).unwrap(); + writeln!( + out, + " {}", + xml_escape(register.description.trim()) + ) + .unwrap(); + writeln!( + out, + " {:#x}", + register.addr + ) + .unwrap(); + writeln!(out, " {}", register.size * 8).unwrap(); + + let reset = register.reset_value(); + if reset != 0 { + writeln!(out, " {:#x}", reset).unwrap(); + } + + if !register.fields.is_empty() { + write_fields(out, peripheral, register); + } + + writeln!(out, " ").unwrap(); +} + +/// Writes all `` elements inside a register. +fn write_fields(out: &mut String, peripheral: &Peripheral, register: &Register) { + writeln!(out, " ").unwrap(); + + for mut field in register.expanded_fields() { + field.name = simplify_name(&peripheral.name, &field.name); + if field.name.starts_with(®ister.name) && field.name != register.name { + field.name = simplify_name(®ister.name, &field.name); + } + + if field.name.contains("%s") { + field.name = field + .name + .replace("%s", "") + .trim_end_matches('_') + .to_owned(); + } + + let mut description = field.description.clone(); + if FIELD_DESC_INDEX.is_match(&description) { + description = FIELD_DESC_INDEX + .replace_all(&description, field.shift.to_string()) + .into_owned(); + } + + writeln!(out, " ").unwrap(); + writeln!( + out, + " {}", + xml_escape(&field.name) + ) + .unwrap(); + let desc = if description.trim().is_empty() { + " ".to_owned() + } else { + description + }; + writeln!( + out, + " {}", + xml_escape(&desc) + ) + .unwrap(); + writeln!(out, " {}", field.shift).unwrap(); + writeln!( + out, + " {}", + field.bit_width() + ) + .unwrap(); + if let Some(access) = guess_field_access(&field.access) { + writeln!(out, " {access}").unwrap(); + } else { + log::warn!( + "unrecognized field access value '{}', ignoring", + field.access + ); + } + writeln!(out, " ").unwrap(); + } + + writeln!(out, " ").unwrap(); +} + +/// Escapes `&`, `<`, `>`, `"` so the XML doesn't break. +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +/// Gets the current year for the license header (no chrono dependency). +fn time_year() -> i32 { + use std::time::{SystemTime, UNIX_EPOCH}; + // Good enough for license header; avoids pulling in chrono. + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + 1970 + (secs / 31_536_000) as i32 +} diff --git a/xtask/src/regdesc/util.rs b/xtask/src/regdesc/util.rs new file mode 100644 index 0000000000..d993f30292 --- /dev/null +++ b/xtask/src/regdesc/util.rs @@ -0,0 +1,164 @@ +use std::sync::LazyLock; + +use regex::Regex; + +static VERILOG_NUMBER: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+)'([hdb])([0-9a-fA-F]+)").unwrap()); +static ACCESS_SUFFIX: LazyLock = + LazyLock::new(|| Regex::new(r"(/?SC|/?SS|/?WTC|/?WTS)+$").unwrap()); +static REG_SUFFIX: LazyLock = LazyLock::new(|| Regex::new(r"(?i)(_REG)+$").unwrap()); +static LP_GPIO: LazyLock = LazyLock::new(|| Regex::new(r"(?i)^LP_GPIO[\d]*_").unwrap()); + +/// Parses Verilog-style literals like `32'hDEAD` into (value, width, base). +pub fn parse_verilog_number(s: &str) -> (u64, u32, u32) { + let Some(caps) = VERILOG_NUMBER.captures(s) else { + return (s.parse().unwrap_or(0), 0, 10); + }; + + let width = caps[1].parse().unwrap_or(0); + let base = match &caps[2] { + "b" => 2, + "d" => 10, + "h" => 16, + _ => 10, + }; + let value = u64::from_str_radix(&caps[3], base).unwrap_or(0); + (value, width, base) +} + +/// Finds where two descriptions differ only by an index and replaces it with +/// `$n`. +pub fn remove_index_from_strings( + a: &str, + b: &str, + idx_a: i32, + idx_b: i32, + placeholder: &str, +) -> Result { + if a.is_empty() && b.is_empty() { + return Ok(String::new()); + } + if a == b { + return Ok(a.to_owned()); + } + + let idx_a_str = idx_a.to_string(); + let idx_b_str = idx_b.to_string(); + let len_common_prefix = idx_a_str + .chars() + .zip(idx_b_str.chars()) + .take_while(|(x, y)| x == y) + .count(); + + let mut pos_a = 0usize; + let mut pos_b = 0usize; + let mut res = String::new(); + let a_chars: Vec = a.chars().collect(); + let b_chars: Vec = b.chars().collect(); + + while pos_a < a_chars.len() && pos_b < b_chars.len() { + if a_chars[pos_a] == b_chars[pos_b] { + res.push(a_chars[pos_a]); + pos_a += 1; + pos_b += 1; + continue; + } + + let a_suffix: String = a_chars[pos_a.saturating_sub(len_common_prefix)..] + .iter() + .collect(); + let b_suffix: String = b_chars[pos_b.saturating_sub(len_common_prefix)..] + .iter() + .collect(); + + if a_suffix.starts_with(&idx_a_str) && b_suffix.starts_with(&idx_b_str) { + if len_common_prefix > 0 { + res.truncate(res.len().saturating_sub(len_common_prefix)); + } + pos_a += idx_a_str.len() - len_common_prefix; + pos_b += idx_b_str.len() - len_common_prefix; + res.push_str(placeholder); + continue; + } + + return Err(format!( + "Strings '{a}' and '{b}' differ at positions {pos_a} and {pos_b}" + )); + } + + if pos_a != a_chars.len() || pos_b != b_chars.len() { + return Err(format!( + "Strings '{a}' and '{b}' differ at positions {pos_a} and {pos_b}" + )); + } + + Ok(res) +} + +/// Trims spaces — GDVS CSVs love trailing whitespace. +pub fn trim(s: &str) -> &str { + s.trim_matches(' ') +} + +/// Case-insensitive ASCII strip of `prefix_` from the start of `name`. +fn strip_prefix_ci<'a>(name: &'a str, prefix: &str) -> Option<&'a str> { + let needle = format!("{prefix}_"); + let (head, rest) = name.split_at_checked(needle.len())?; + head.eq_ignore_ascii_case(&needle).then_some(rest) +} + +/// Strips peripheral prefixes so SVD names are shorter (`UART0_TX` → `TX`). +pub fn simplify_name(prefix: &str, name: &str) -> String { + let mut name = match strip_prefix_ci(name, prefix) { + Some(rest) => rest.to_owned(), + None => name.to_owned(), + }; + + if prefix.eq_ignore_ascii_case("LP_IO") { + name = LP_GPIO.replace(&name, "").into_owned(); + } + + name = REG_SUFFIX.replace(&name, "").into_owned(); + + if name.chars().next().is_some_and(|c| c.is_ascii_digit()) { + name = format!("_{name}"); + } + + name +} + +/// Maps CSV access strings (`RW`, `RO`, …) to CMSIS ones (`read-write`, etc.). +pub fn guess_field_access(access: &str) -> Option<&'static str> { + const READ_WRITE: &[&str] = &["RW", "RW1C", "RW1S", "W/R", "W1C"]; + const READ_ONLY: &[&str] = &["HRO", "R", "RC", "RO"]; + const WRITE_ONLY: &[&str] = &["W", "WO", "WS", "WT", "WOD", "WOR"]; + + /// Does the actual access-string matching (also tries stripping suffixes). + fn guess(access: &str) -> Option<&'static str> { + if access.contains("R/W") || READ_WRITE.contains(&access) { + return Some("read-write"); + } + if READ_ONLY.contains(&access) { + return Some("read-only"); + } + if WRITE_ONLY.contains(&access) { + return Some("write-only"); + } + None + } + + if let Some(access) = guess(access) { + return Some(access); + } + + let stripped = ACCESS_SUFFIX.replace(access, ""); + guess(&stripped) +} + +/// Turns `esp32s31` into `ESP32-S31` for the SVD header. +pub fn pretty_chip_name(chip: &str) -> String { + chip.to_ascii_uppercase() + .replace("ESP32", "ESP32-") + .trim_matches('-') + .to_owned() +}