aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/db.rs258
-rw-r--r--src/main.rs69
-rw-r--r--src/table.rs235
3 files changed, 562 insertions, 0 deletions
diff --git a/src/db.rs b/src/db.rs
new file mode 100644
index 0000000..41ef85e
--- /dev/null
+++ b/src/db.rs
@@ -0,0 +1,258 @@
+// Copyright (C) 2019 Nicolas Schodet
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+//! This module handles loading parts information from database.
+use flate2::read::GzDecoder;
+use roxmltree::{Document, Node};
+use std::collections::HashMap;
+use std::error::Error;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+static EXT: &str = ".xml.gz";
+
+type Result<T> = std::result::Result<T, Box<Error>>;
+
+/// Information about a part.
+#[derive(Debug)]
+pub struct PartInfo<'a> {
+ /// Part.
+ pub part: &'a str,
+ /// Product line.
+ pub line: String,
+ /// Package.
+ pub package: String,
+ /// GPIO mapping mode.
+ pub gpio_mode: GpioMode,
+ /// Information for all pins.
+ pub pins: Vec<PinInfo>,
+}
+
+/// Information about one pin.
+#[derive(Debug)]
+pub struct PinInfo {
+ /// Name.
+ pub name: String,
+ /// Position in package. This can be a number or a letter with a number.
+ pub position: String,
+ /// Signals.
+ pub signals: Vec<SignalInfo>,
+}
+
+/// Information about one signal.
+#[derive(Debug)]
+pub struct SignalInfo {
+ /// Name.
+ pub name: String,
+ /// Mapping information.
+ pub map: SignalMap,
+}
+
+/// Information on how to map a signal to a pin.
+#[derive(Clone, Debug)]
+pub enum SignalMap {
+ /// Alternate function, with its AF number.
+ AF(u8),
+ /// Additional function, no AF setup to do.
+ AddF,
+ /// Remap, used on older parts without the AF system. The signal can be available on several
+ /// remaps.
+ Remap(Vec<u8>),
+}
+
+/// Mode of GPIO mapping.
+#[derive(Clone, Copy, Debug)]
+pub enum GpioMode {
+ /// Alternate function based mapping.
+ AF,
+ /// Remap based mapping.
+ Remap,
+}
+
+/// Map pins and signals to mapping information. This is used temporarily when loading it from a
+/// separated file.
+type GpiosInfo = HashMap<String, HashMap<String, SignalMap>>;
+
+impl<'a> PartInfo<'a> {
+ /// Extract information from XML file in database.
+ pub fn new(database: &Path, part: &'a str) -> Result<PartInfo<'a>> {
+ // Read XML.
+ let xml_name = database.join(["mcu/", part, EXT].concat());
+ let xml = read_gziped(&xml_name)?;
+ let doc = Document::parse(&xml)?;
+ let doc_root = doc.root_element();
+ // Basic attributes.
+ let line = attribute_or_error(&doc_root, "Line")?;
+ let package = attribute_or_error(&doc_root, "Package")?;
+ // GPIO.
+ let gpio_ip = doc_root
+ .children()
+ .find(|n| n.has_tag_name("IP") && n.attribute("Name") == Some("GPIO"))
+ .ok_or("missing GPIO")?;
+ let gpio_version = attribute_or_error(&gpio_ip, "Version")?;
+ let (gpio_mode, gpios_info) = load_gpios(database, &gpio_version)?;
+ // Pins.
+ fn parse_signal(
+ signals_map: Option<&HashMap<String, SignalMap>>,
+ s: Node,
+ ) -> Result<SignalInfo> {
+ let name = attribute_or_error(&s, "Name")?;
+ let map = match signals_map {
+ None => SignalMap::AddF,
+ Some(signals_map) => signals_map.get(&name).unwrap_or(&SignalMap::AddF).clone(),
+ };
+ Ok(SignalInfo { name, map })
+ };
+ fn parse_pin(gpios_info: &GpiosInfo, n: Node) -> Result<PinInfo> {
+ let name = attribute_or_error(&n, "Name")?;
+ let position = attribute_or_error(&n, "Position")?;
+ let signals = n
+ .children()
+ .filter(|s| s.has_tag_name("Signal") && s.attribute("Name") != Some("GPIO"))
+ .map(|s| {
+ let signals_map = gpios_info.get(&name);
+ parse_signal(signals_map, s)
+ })
+ .collect::<Result<_>>()?;
+ Ok(PinInfo {
+ name,
+ position,
+ signals,
+ })
+ };
+ let pins = doc_root
+ .children()
+ .filter(|n| n.has_tag_name("Pin"))
+ .map(|n| parse_pin(&gpios_info, n))
+ .collect::<Result<_>>()?;
+ // Done.
+ Ok(PartInfo {
+ part,
+ line,
+ package,
+ gpio_mode,
+ pins,
+ })
+ }
+ /// Produce a one-line part summary.
+ pub fn summary(self: &Self) -> String {
+ format!("{}: {} {}", self.part, self.line, self.package)
+ }
+}
+
+/// List all parts in database matching a given regex.
+pub fn list_parts(database: &Path, pattern: &str) -> Result<Vec<String>> {
+ let re = regex::Regex::new(pattern)?;
+ let mut list = Vec::new();
+ for entry in database.join("mcu").read_dir()? {
+ if let Some(name) = entry?.file_name().to_str() {
+ if name.ends_with(EXT) {
+ let part = &name[..(name.len() - EXT.len())];
+ if re.is_match(part) {
+ list.push(part.to_owned());
+ }
+ }
+ }
+ }
+ Ok(list)
+}
+
+/// Load information on GPIOs from XML file in database. Return a hash indexed by pin and signal,
+/// giving signal mapping information.
+fn load_gpios(database: &Path, gpio_version: &str) -> Result<(GpioMode, GpiosInfo)> {
+ // Read XML.
+ let xml_name = database.join(["mcu/IP/GPIO-", gpio_version, "_Modes", EXT].concat());
+ let xml = read_gziped(&xml_name)?;
+ let doc = Document::parse(&xml)?;
+ let doc_root = doc.root_element();
+ // Decode document.
+ fn parse_af(signal: Node) -> Result<SignalMap> {
+ let af = signal
+ .descendants()
+ .find(|n| n.has_tag_name("PossibleValue"))
+ .ok_or("no AF found")?
+ .text()
+ .ok_or("no AF text")?;
+ let k = "GPIO_AF";
+ if !af.starts_with(k) {
+ return Err("not an AF".into());
+ }
+ let i = af[k.len()..].find('_').ok_or("not an AF")?;
+ let af = &af[k.len()..k.len() + i];
+ let af = af.parse::<u8>()?;
+ Ok(SignalMap::AF(af))
+ }
+ fn parse_remaps(signal: Node) -> Result<SignalMap> {
+ let remap_blocks = signal.children().filter(|n| n.has_tag_name("RemapBlock"));
+ fn parse_remap(n: Node) -> Result<u8> {
+ let name = attribute_or_error(&n, "Name")?;
+ let k = "REMAP";
+ let i = name.rfind(k).ok_or("missing REMAP")?;
+ let remap = name[i + k.len()..].parse::<u8>()?;
+ Ok(remap)
+ }
+ let remaps = remap_blocks.map(parse_remap).collect::<Result<_>>()?;
+ Ok(SignalMap::Remap(remaps))
+ }
+ let mut gpios = HashMap::new();
+ let pins = doc_root.children().filter(|n| n.has_tag_name("GPIO_Pin"));
+ let mut mode = None;
+ for pin in pins {
+ let pin_name = attribute_or_error(&pin, "Name")?;
+ let signals = pin.children().filter(|n| n.has_tag_name("PinSignal"));
+ let mut signals_map = HashMap::new();
+ for signal in signals {
+ // First try to parse a remap until this fails once.
+ if mode.is_none() {
+ let one_remap_block = signal.children().find(|n| n.has_tag_name("RemapBlock"));
+ mode = if one_remap_block.is_some() {
+ Some(GpioMode::Remap)
+ } else {
+ Some(GpioMode::AF)
+ }
+ }
+ let map = match mode.unwrap() {
+ GpioMode::AF => parse_af(signal),
+ GpioMode::Remap => parse_remaps(signal),
+ }?;
+ let signal_name = attribute_or_error(&signal, "Name")?;
+ signals_map.insert(signal_name, map);
+ }
+ gpios.insert(pin_name, signals_map);
+ }
+ Ok((mode.unwrap_or(GpioMode::AF), gpios))
+}
+
+/// Read gziped file to string.
+fn read_gziped(path: &Path) -> Result<String> {
+ let mut gunzip = GzDecoder::new(File::open(path)?);
+ let mut xml = String::new();
+ gunzip.read_to_string(&mut xml)?;
+ Ok(xml)
+}
+
+/// Factorize attribute getter, return an error if not found.
+fn attribute_or_error(node: &Node, name: &str) -> Result<String> {
+ match node.attribute(name) {
+ Some(v) => Ok(v.to_owned()),
+ None => {
+ let tag = node.tag_name().name();
+ Err(format!("{} missing a {} attribute", tag, name).into())
+ }
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..3ac9d28
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,69 @@
+// Copyright (C) 2019 Nicolas Schodet
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+//! Help with pin assignment on STM32.
+//!
+//! This reads database extracted from CubeMX and produce a table of all signals that can be mapped
+//! to the microcontroller pins. This table can be open with a spreadsheet.
+use std::error::Error;
+use std::io;
+use std::path::PathBuf;
+use structopt::StructOpt;
+
+mod db;
+mod table;
+
+/// MCU pins mapper.
+#[derive(StructOpt, Debug)]
+struct Opt {
+ /// Database path
+ #[structopt(short = "d", long, default_value = "db", parse(from_os_str))]
+ database: PathBuf,
+ /// Exclude component
+ #[structopt(short = "x", long, number_of_values = 1)]
+ exclude: Vec<String>,
+ #[structopt(subcommand)]
+ command: OptCommand,
+}
+
+#[derive(StructOpt, Debug)]
+enum OptCommand {
+ /// Search the database for MCUs matching the given regex.
+ #[structopt(name = "parts")]
+ Parts { pattern: String },
+ /// Output a pin out table for a given part.
+ #[structopt(name = "table")]
+ Table { part: String },
+}
+
+fn main() -> Result<(), Box<Error>> {
+ let opt = Opt::from_args();
+ match opt.command {
+ OptCommand::Parts { pattern } => {
+ for part in db::list_parts(&opt.database, &pattern)? {
+ let part_info = db::PartInfo::new(&opt.database, &part)?;
+ println!("{}", part_info.summary());
+ }
+ }
+ OptCommand::Table { part } => {
+ let part_info = db::PartInfo::new(&opt.database, &part)?;
+ let filter = table::SignalFilter::new(&opt.exclude)?;
+ table::write_pin_out(&part_info, io::stdout(), &filter)?;
+ }
+ }
+ Ok(())
+}
diff --git a/src/table.rs b/src/table.rs
new file mode 100644
index 0000000..efdc23d
--- /dev/null
+++ b/src/table.rs
@@ -0,0 +1,235 @@
+// Copyright (C) 2019 Nicolas Schodet
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+// and associated documentation files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+//! Handle table output.
+use crate::db;
+use itertools::Itertools;
+use regex::{Regex, RegexSet};
+use std::collections::hash_set::HashSet;
+use std::collections::HashMap;
+use std::error::Error;
+use std::io::Write;
+use std::result::Result as StdResult;
+
+type Result<T> = StdResult<T, Box<Error>>;
+
+/// Filter signals to reduce pin out table size.
+pub struct SignalFilter {
+ /// Signals to exclude from table.
+ excludes: RegexSet,
+ /// Substitutions to shorten signal names.
+ subs: Vec<Regex>,
+ /// Factorizations to reduce the number of similar signals, with the associated separator.
+ facts_sep: Vec<(Regex, &'static str)>,
+}
+
+/// Produce a pin out table.
+pub fn write_pin_out(
+ part_info: &db::PartInfo,
+ writer: impl Write,
+ filter: &SignalFilter,
+) -> Result<()> {
+ match part_info.gpio_mode {
+ db::GpioMode::AF => write_pin_out_af(part_info, writer, filter),
+ db::GpioMode::Remap => write_pin_out_remap(part_info, writer, filter),
+ }
+}
+
+/// Produce a pin out table for AF based parts.
+fn write_pin_out_af(
+ part_info: &db::PartInfo,
+ writer: impl Write,
+ filter: &SignalFilter,
+) -> Result<()> {
+ let mut writer = csv::Writer::from_writer(writer);
+ for pin in &part_info.pins {
+ let mut signals: [Vec<_>; 17] = Default::default();
+ for signal in &pin.signals {
+ let index = match signal.map {
+ db::SignalMap::AF(af) => af as usize,
+ db::SignalMap::AddF => signals.len() - 1,
+ _ => panic!("Bad signal map"),
+ };
+ signals[index].push(signal.name.as_str());
+ }
+ let signals = filter.signal_filter(&pin.name, &pin.position, &signals);
+ let mut row = Vec::new();
+ row.push(pin.name.clone());
+ row.push(pin.position.clone());
+ for i in &signals {
+ row.push(i.join(" "));
+ }
+ writer.write_record(row)?;
+ }
+ Ok(())
+}
+
+/// Produce a pin out table for Remap based parts.
+fn write_pin_out_remap(
+ part_info: &db::PartInfo,
+ writer: impl Write,
+ filter: &SignalFilter,
+) -> Result<()> {
+ let mut lines = Vec::new();
+ let mut allcats = HashSet::new();
+ for pin in &part_info.pins {
+ let signals = pin
+ .signals
+ .iter()
+ .map(|signal| {
+ if let db::SignalMap::Remap(remaps) = &signal.map {
+ let remaps = remaps.iter().sorted().map(|x| x.to_string()).join(",");
+ format!("{}({})", signal.name, remaps)
+ } else {
+ signal.name.clone()
+ }
+ })
+ .collect::<Vec<_>>();
+ let signals = filter
+ .signal_filter(&pin.name, &pin.position, &[signals])
+ .into_iter()
+ .next()
+ .unwrap();
+ let mut signals_hash = HashMap::new();
+ for signal in signals {
+ let cat = signal.split('_').next().unwrap().to_owned();
+ allcats.insert(cat.clone());
+ signals_hash.entry(cat).or_insert(Vec::new()).push(signal);
+ }
+ lines.push((&pin.name, &pin.position, signals_hash));
+ }
+ let mut writer = csv::Writer::from_writer(writer);
+ let mut allcats = allcats.into_iter().collect::<Vec<_>>();
+ allcats.sort();
+ for (name, position, signals_hash) in lines {
+ let mut row = Vec::new();
+ row.push(name.clone());
+ row.push(position.clone());
+ for cat in &allcats {
+ let col = signals_hash.get(cat);
+ if let Some(col) = col {
+ row.push(col.iter().join(" "));
+ } else {
+ row.push(String::from(""));
+ }
+ }
+ writer.write_record(row)?;
+ }
+ Ok(())
+}
+
+impl SignalFilter {
+ /// Prepare a new filter.
+ pub fn new(exclude: &Vec<String>) -> StdResult<SignalFilter, regex::Error> {
+ let excludes = RegexSet::new(exclude.iter().map(|x| format!(r"^(?:{})[0-9_]", x)))?;
+ let subs = [
+ "((?:HR|LP)?T)IM",
+ "((?:LP)?U)S?ART",
+ "(D)FSDM",
+ "(F)S?MC",
+ "(Q)UADSPI(?:_BK)?",
+ "(S)PI",
+ "(SW)PMI",
+ "I2(S)",
+ "(SD)MMC",
+ "(SP)DIFRX",
+ "FD(C)AN",
+ "USB_OTG_([FH]S)",
+ r"(T\d_B)KIN",
+ ]
+ .iter()
+ .map(|x| Regex::new(&format!(r"^{}([0-9_])", x)))
+ .collect::<StdResult<_, _>>()?;
+ let facts_sep = [
+ (r"T\d_B\d?_COMP(\d+)", ""),
+ (r"ADC(\d)_IN[NP]?\d+", ""),
+ (r"ADC\d+_IN([NP]?\d+)", ""),
+ (r"[SUT]\d_(.+)", "/"),
+ ]
+ .iter()
+ .map(|(fact, sep)| Ok((Regex::new(fact)?, *sep)))
+ .collect::<StdResult<_, _>>()?;
+ Ok(SignalFilter {
+ excludes,
+ subs,
+ facts_sep,
+ })
+ }
+ /// Filter a list of signal.
+ fn signal_filter<'a, I, J, S>(
+ self: &Self,
+ _name: &str,
+ _position: &str,
+ cols: I,
+ ) -> Vec<Vec<String>>
+ where
+ S: ToString,
+ J: IntoIterator<Item = S>,
+ I: IntoIterator<Item = J>,
+ {
+ let mut res = Vec::new();
+ for signals in cols {
+ let signals = signals
+ .into_iter()
+ .map(|s| {
+ self.subs
+ .iter()
+ .fold(s.to_string(), |s, re| re.replace(&s, "$1$2").to_string())
+ })
+ .collect();
+ let signals = self.facts_sep.iter().fold(signals, |signals, (fact, sep)| {
+ factorize(&signals, &fact, sep)
+ });
+ let signals = signals
+ .into_iter()
+ .filter(|s| !self.excludes.is_match(s))
+ .collect();
+ res.push(signals);
+ }
+ res
+ }
+}
+
+/// For a given iterable, match each items with the given regex, if there are several matches they
+/// are factorized on the first subgroup.
+fn factorize<I, S>(it: I, re: &Regex, sep: &str) -> Vec<String>
+where
+ S: ToString,
+ I: IntoIterator<Item = S>,
+{
+ let mut others = Vec::new();
+ let mut facts: HashMap<_, Vec<_>> = HashMap::new();
+ for i in it {
+ let i = i.to_string();
+ if let Some(c) = re.captures(&i) {
+ let g = c.get(1).expect("first group should match");
+ let termout = (&i[..g.start()], &i[g.end()..]);
+ let termout = (termout.0.to_owned(), termout.1.to_owned());
+ let term = g.as_str().to_owned();
+ facts.entry(termout).or_insert(Vec::new()).push(term);
+ } else {
+ others.push(i);
+ }
+ }
+ let mut r = Vec::new();
+ for (termout, terms) in facts {
+ let terms = terms.join(sep);
+ r.push(format!("{}{}{}", termout.0, terms, termout.1));
+ }
+ r.extend(others);
+ r
+}