1mod cfg;
3
4use core::str::FromStr;
5use std::{fmt::Write, sync::OnceLock};
6
7use anyhow::{Context, Result, bail, ensure};
8use cfg::PeriConfig;
9use indexmap::IndexMap;
10pub use proc_macro2::TokenStream;
11use quote::{format_ident, quote};
12use strum::IntoEnumIterator;
13
14use crate::cfg::{SupportItem, SupportStatus, Value};
15
16macro_rules! include_toml {
17 (Config, $file:expr) => {{
18 static LOADED_TOML: OnceLock<Config> = OnceLock::new();
19 LOADED_TOML.get_or_init(|| {
20 let config: Config = basic_toml::from_str(include_str!($file))
21 .with_context(|| format!("Failed to load device configuration: {}", $file))
22 .unwrap();
23
24 config
25 .validate()
26 .with_context(|| format!("Failed to validate device configuration: {}", $file))
27 .unwrap();
28
29 config
30 })
31 }};
32}
33
34#[derive(
36 Debug,
37 Clone,
38 Copy,
39 PartialEq,
40 Eq,
41 PartialOrd,
42 Ord,
43 serde::Deserialize,
44 serde::Serialize,
45 strum::Display,
46 strum::EnumIter,
47 strum::EnumString,
48 strum::AsRefStr,
49)]
50#[serde(rename_all = "lowercase")]
51#[strum(serialize_all = "lowercase")]
52pub enum Arch {
53 RiscV,
55 Xtensa,
57}
58
59#[derive(
61 Debug,
62 Clone,
63 Copy,
64 PartialEq,
65 Eq,
66 PartialOrd,
67 Ord,
68 serde::Deserialize,
69 serde::Serialize,
70 strum::Display,
71 strum::EnumIter,
72 strum::EnumString,
73 strum::AsRefStr,
74)]
75pub enum Cores {
76 #[serde(rename = "single_core")]
78 #[strum(serialize = "single_core")]
79 Single,
80 #[serde(rename = "multi_core")]
82 #[strum(serialize = "multi_core")]
83 Multi,
84}
85
86#[derive(
88 Debug,
89 Clone,
90 Copy,
91 PartialEq,
92 Eq,
93 PartialOrd,
94 Ord,
95 Hash,
96 serde::Deserialize,
97 serde::Serialize,
98 strum::Display,
99 strum::EnumIter,
100 strum::EnumString,
101 strum::AsRefStr,
102)]
103#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
104#[serde(rename_all = "kebab-case")]
105#[strum(serialize_all = "kebab-case")]
106pub enum Chip {
107 Esp32,
109 Esp32c2,
111 Esp32c3,
113 Esp32c6,
115 Esp32h2,
117 Esp32s2,
119 Esp32s3,
121}
122
123impl Chip {
124 pub fn from_cargo_feature() -> Result<Self> {
125 let all_chips = Chip::iter().map(|c| c.to_string()).collect::<Vec<_>>();
126
127 let mut chip = None;
128 for c in all_chips.iter() {
129 if std::env::var(format!("CARGO_FEATURE_{}", c.to_uppercase())).is_ok() {
130 if chip.is_some() {
131 bail!(
132 "Expected exactly one of the following features to be enabled: {}",
133 all_chips.join(", ")
134 );
135 }
136 chip = Some(c);
137 }
138 }
139
140 let Some(chip) = chip else {
141 bail!(
142 "Expected exactly one of the following features to be enabled: {}",
143 all_chips.join(", ")
144 );
145 };
146
147 Ok(Self::from_str(chip.as_str()).unwrap())
148 }
149
150 pub fn target(&self) -> String {
151 Config::for_chip(self).device.target.clone()
152 }
153
154 pub fn has_lp_core(&self) -> bool {
155 use Chip::*;
156
157 matches!(self, Esp32c6 | Esp32s2 | Esp32s3)
158 }
159
160 pub fn lp_target(&self) -> Result<&'static str> {
161 match self {
162 Chip::Esp32c6 => Ok("riscv32imac-unknown-none-elf"),
163 Chip::Esp32s2 | Chip::Esp32s3 => Ok("riscv32imc-unknown-none-elf"),
164 _ => bail!("Chip does not contain an LP core: '{self}'"),
165 }
166 }
167
168 pub fn name(&self) -> &str {
169 match self {
170 Chip::Esp32 => "Esp32",
171 Chip::Esp32c2 => "Esp32c2",
172 Chip::Esp32c3 => "Esp32c3",
173 Chip::Esp32c6 => "Esp32c6",
174 Chip::Esp32h2 => "Esp32h2",
175 Chip::Esp32s2 => "Esp32s2",
176 Chip::Esp32s3 => "Esp32s3",
177 }
178 }
179
180 pub fn pretty_name(&self) -> &str {
181 match self {
182 Chip::Esp32 => "ESP32",
183 Chip::Esp32c2 => "ESP32-C2",
184 Chip::Esp32c3 => "ESP32-C3",
185 Chip::Esp32c6 => "ESP32-C6",
186 Chip::Esp32h2 => "ESP32-H2",
187 Chip::Esp32s2 => "ESP32-S2",
188 Chip::Esp32s3 => "ESP32-S3",
189 }
190 }
191
192 pub fn is_xtensa(&self) -> bool {
193 matches!(self, Chip::Esp32 | Chip::Esp32s2 | Chip::Esp32s3)
194 }
195
196 pub fn is_riscv(&self) -> bool {
197 !self.is_xtensa()
198 }
199
200 pub fn list_of_check_cfgs() -> Vec<String> {
201 let mut cfgs = vec![];
202
203 cfgs.push(String::from("cargo:rustc-check-cfg=cfg(not_really_docsrs)"));
205 cfgs.push(String::from("cargo:rustc-check-cfg=cfg(semver_checks)"));
206
207 let mut cfg_values: IndexMap<String, Vec<String>> = IndexMap::new();
208
209 for chip in Chip::iter() {
210 let config = Config::for_chip(&chip);
211 for symbol in config.all() {
212 if let Some((symbol_name, symbol_value)) = symbol.split_once('=') {
213 let symbol_name = symbol_name.replace('.', "_");
216 let entry = cfg_values.entry(symbol_name).or_default();
217 if !entry.contains(&symbol_value.to_string()) {
219 entry.push(symbol_value.to_string());
220 }
221 } else {
222 let cfg = format!("cargo:rustc-check-cfg=cfg({})", symbol.replace('.', "_"));
224
225 if !cfgs.contains(&cfg) {
226 cfgs.push(cfg);
227 }
228 }
229 }
230 }
231
232 for (symbol_name, symbol_values) in cfg_values {
234 cfgs.push(format!(
235 "cargo:rustc-check-cfg=cfg({symbol_name}, values({}))",
236 symbol_values.join(",")
237 ));
238 }
239
240 cfgs
241 }
242}
243
244#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
245pub struct MemoryRegion {
246 name: String,
247 start: u32,
248 end: u32,
249}
250
251#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
252pub struct PeripheralDef {
253 name: String,
255 #[serde(default, rename = "pac")]
257 pac_name: Option<String>,
258 #[serde(default, rename = "virtual")]
260 is_virtual: bool,
261 #[serde(default)]
263 interrupts: IndexMap<String, String>,
264}
265
266impl PeripheralDef {
267 fn symbol_name(&self) -> String {
268 format!("soc_has_{}", self.name.to_lowercase())
269 }
270}
271
272#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
273struct Device {
274 name: String,
275 arch: Arch,
276 target: String,
277 cores: usize,
278 trm: String,
279
280 peripherals: Vec<PeripheralDef>,
281 symbols: Vec<String>,
282
283 #[serde(flatten)]
285 peri_config: PeriConfig,
286}
287
288fn number(n: impl std::fmt::Display) -> TokenStream {
291 TokenStream::from_str(&format!("{n}")).unwrap()
292}
293
294#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
296pub struct Config {
297 device: Device,
298 #[serde(skip)]
299 all_symbols: OnceLock<Vec<String>>,
300}
301
302impl Config {
303 pub fn for_chip(chip: &Chip) -> &Self {
305 match chip {
306 Chip::Esp32 => include_toml!(Config, "../devices/esp32.toml"),
307 Chip::Esp32c2 => include_toml!(Config, "../devices/esp32c2.toml"),
308 Chip::Esp32c3 => include_toml!(Config, "../devices/esp32c3.toml"),
309 Chip::Esp32c6 => include_toml!(Config, "../devices/esp32c6.toml"),
310 Chip::Esp32h2 => include_toml!(Config, "../devices/esp32h2.toml"),
311 Chip::Esp32s2 => include_toml!(Config, "../devices/esp32s2.toml"),
312 Chip::Esp32s3 => include_toml!(Config, "../devices/esp32s3.toml"),
313 }
314 }
315
316 pub fn empty() -> Self {
318 Self {
319 device: Device {
320 name: String::new(),
321 arch: Arch::RiscV,
322 target: String::new(),
323 cores: 1,
324 trm: String::new(),
325 peripherals: Vec::new(),
326 symbols: Vec::new(),
327 peri_config: PeriConfig::default(),
328 },
329 all_symbols: OnceLock::new(),
330 }
331 }
332
333 fn validate(&self) -> Result<()> {
334 for instance in self.device.peri_config.driver_instances() {
335 let (driver, peri) = instance.split_once('.').unwrap();
336 ensure!(
337 self.device
338 .peripherals
339 .iter()
340 .any(|p| p.name.eq_ignore_ascii_case(peri)),
341 "Driver {driver} marks an implementation for '{peri}' but this peripheral is not defined for '{}'",
342 self.device.name
343 );
344 }
345
346 Ok(())
347 }
348
349 pub fn name(&self) -> String {
351 self.device.name.clone()
352 }
353
354 pub fn arch(&self) -> Arch {
356 self.device.arch
357 }
358
359 pub fn cores(&self) -> Cores {
361 if self.device.cores > 1 {
362 Cores::Multi
363 } else {
364 Cores::Single
365 }
366 }
367
368 pub fn peripherals(&self) -> &[PeripheralDef] {
370 &self.device.peripherals
371 }
372
373 pub fn symbols(&self) -> &[String] {
375 &self.device.symbols
376 }
377
378 pub fn all(&self) -> &[String] {
380 self.all_symbols.get_or_init(|| {
381 let mut all = vec![
382 self.device.name.clone(),
383 self.device.arch.to_string(),
384 match self.cores() {
385 Cores::Single => String::from("single_core"),
386 Cores::Multi => String::from("multi_core"),
387 },
388 ];
389 all.extend(self.device.peripherals.iter().map(|p| p.symbol_name()));
390 all.extend_from_slice(&self.device.symbols);
391 all.extend(
392 self.device
393 .peri_config
394 .driver_names()
395 .map(|name| name.to_string()),
396 );
397 all.extend(self.device.peri_config.driver_instances());
398
399 all.extend(
400 self.device
401 .peri_config
402 .properties()
403 .filter_map(|(name, optional, value)| {
404 let is_unset = matches!(value, Value::Unset);
405 let mut syms = match value {
406 Value::Boolean(true) => Some(vec![name.to_string()]),
407 Value::NumberList(_) => None,
408 Value::String(value) => Some(vec![format!("{name}=\"{value}\"")]),
409 Value::Generic(v) => v.cfgs(),
410 Value::StringList(values) => Some(
411 values
412 .iter()
413 .map(|val| {
414 format!(
415 "{name}_{}",
416 val.to_lowercase().replace("-", "_").replace("/", "_")
417 )
418 })
419 .collect(),
420 ),
421 Value::Number(value) => Some(vec![format!("{name}=\"{value}\"")]),
422 _ => None,
423 };
424
425 if optional && !is_unset {
426 syms.get_or_insert_default().push(format!("{name}_is_set"));
427 }
428
429 syms
430 })
431 .flatten(),
432 );
433 all
434 })
435 }
436
437 pub fn contains(&self, item: &str) -> bool {
439 self.all().iter().any(|i| i == item)
440 }
441
442 pub fn generate_metadata(&self) -> TokenStream {
443 let properties = self.generate_properties();
444 let peris = self.generate_peripherals();
445 let gpios = self.generate_gpios();
446
447 quote! {
448 #properties
449 #peris
450 #gpios
451 }
452 }
453
454 fn generate_properties(&self) -> TokenStream {
455 let chip_name = self.name();
456
457 let arch = self.device.arch.as_ref();
459 let cores = number(self.device.cores);
460 let trm = &self.device.trm;
461
462 let mut macros = vec![];
463
464 let peripheral_properties =
465 self.device
466 .peri_config
467 .properties()
468 .flat_map(|(name, _optional, value)| match value {
469 Value::Number(value) => {
470 let value = number(value); quote! {
472 (#name) => { #value };
473 (#name, str) => { stringify!(#value) };
474 }
475 }
476 Value::Boolean(value) => quote! {
477 (#name) => { #value };
478 },
479 Value::String(value) => quote! {
480 (#name) => { #value };
481 },
482 Value::NumberList(numbers) => {
483 let numbers = numbers.into_iter().map(number).collect::<Vec<_>>();
484 macros.push(generate_for_each_macro(
485 &name.replace(".", "_"),
486 &[("all", &numbers)],
487 ));
488 quote! {}
489 }
490 Value::Generic(v) => {
491 if let Some(for_each) = v.macros() {
492 macros.push(for_each);
493 }
494 v.property_macro_branches()
495 }
496 Value::Unset | Value::StringList(_) => {
497 quote! {}
498 }
499 });
500
501 quote! {
502 #[doc = concat!("assert_eq!(chip_name, ", chip!(), ")")]
510 #[macro_export]
512 #[cfg_attr(docsrs, doc(cfg(feature = "_device-selected")))]
513 macro_rules! chip {
514 () => { #chip_name };
515 }
516
517 #[macro_export]
519 #[cfg_attr(docsrs, doc(cfg(feature = "_device-selected")))]
520 macro_rules! property {
521 ("chip") => { #chip_name };
522 ("arch") => { #arch };
523 ("cores") => { #cores };
524 ("cores", str) => { stringify!(#cores) };
525 ("trm") => { #trm };
526 #(#peripheral_properties)*
527 }
528
529 #(#macros)*
530 }
531 }
532
533 fn generate_gpios(&self) -> TokenStream {
534 let Some(gpio) = self.device.peri_config.gpio.as_ref() else {
535 return quote! {};
537 };
538
539 cfg::generate_gpios(gpio)
540 }
541
542 fn generate_peripherals(&self) -> TokenStream {
543 let mut tokens = TokenStream::new();
544
545 if let Some(peri) = self.device.peri_config.i2c_master.as_ref() {
547 tokens.extend(cfg::generate_i2c_master_peripherals(peri));
548 };
549 if let Some(peri) = self.device.peri_config.uart.as_ref() {
550 tokens.extend(cfg::generate_uart_peripherals(peri));
551 }
552 if let Some(peri) = self.device.peri_config.spi_master.as_ref() {
553 tokens.extend(cfg::generate_spi_master_peripherals(peri));
554 };
555 if let Some(peri) = self.device.peri_config.spi_slave.as_ref() {
556 tokens.extend(cfg::generate_spi_slave_peripherals(peri));
557 };
558
559 tokens.extend(self.generate_peripherals_macro());
560
561 tokens
562 }
563
564 fn generate_peripherals_macro(&self) -> TokenStream {
565 let mut stable = vec![];
566 let mut unstable = vec![];
567 let mut all_peripherals = vec![];
568
569 let mut stable_peris = vec![];
570
571 for item in PeriConfig::drivers() {
572 if self.device.peri_config.support_status(item.config_group)
573 == Some(SupportStatus::Supported)
574 {
575 for p in self.device.peri_config.driver_peris(item.config_group) {
576 if !stable_peris.contains(&p) {
577 stable_peris.push(p);
578 }
579 }
580 }
581 }
582
583 if let Some(gpio) = self.device.peri_config.gpio.as_ref() {
584 for pin in gpio.pins_and_signals.pins.iter() {
585 let pin = format_ident!("GPIO{}", pin.pin);
586 let tokens = quote! {
587 #pin <= virtual ()
588 };
589 all_peripherals.push(quote! { #tokens });
590 stable.push(tokens);
591 }
592 }
593
594 for peri in self.device.peripherals.iter() {
595 let hal = format_ident!("{}", peri.name);
596 let pac = if peri.is_virtual {
597 format_ident!("virtual")
598 } else {
599 format_ident!("{}", peri.pac_name.as_deref().unwrap_or(peri.name.as_str()))
600 };
601 let mut interrupts = peri.interrupts.iter().collect::<Vec<_>>();
603 interrupts.sort_by_key(|(k, _)| k.as_str());
604 let interrupts = interrupts.iter().map(|(k, v)| {
605 let pac_interrupt_name = format_ident!("{v}");
606 let bind = format_ident!("bind_{k}_interrupt");
607 let enable = format_ident!("enable_{k}_interrupt");
608 let disable = format_ident!("disable_{k}_interrupt");
609 quote! { #pac_interrupt_name: { #bind, #enable, #disable } }
610 });
611 let tokens = quote! {
612 #hal <= #pac ( #(#interrupts),* )
613 };
614 if stable_peris
615 .iter()
616 .any(|p| peri.name.eq_ignore_ascii_case(p))
617 {
618 all_peripherals.push(quote! { #tokens });
619 stable.push(tokens);
620 } else {
621 all_peripherals.push(quote! { #tokens (unstable) });
622 unstable.push(tokens);
623 }
624 }
625
626 generate_for_each_macro("peripheral", &[("all", &all_peripherals)])
627 }
628
629 pub fn active_cfgs(&self) -> Vec<String> {
630 let mut cfgs = vec![];
631
632 for symbol in self.all() {
634 cfgs.push(symbol.replace('.', "_"));
635 }
636
637 cfgs
638 }
639
640 pub fn list_of_cfgs(&self) -> Vec<String> {
642 self.active_cfgs()
643 .iter()
644 .map(|cfg| format!("cargo:rustc-cfg={cfg}"))
645 .collect()
646 }
647}
648
649type Branch<'a> = (&'a str, &'a [TokenStream]);
650
651fn generate_for_each_macro(name: &str, branches: &[Branch<'_>]) -> TokenStream {
652 let macro_name = format_ident!("for_each_{name}");
653
654 let flat_branches = branches.iter().flat_map(|b| b.1.iter());
655 let repeat_names = branches.iter().map(|b| TokenStream::from_str(b.0).unwrap());
656 let repeat_branches = branches.iter().map(|b| b.1);
657
658 quote! {
659 #[macro_export]
663 #[cfg_attr(docsrs, doc(cfg(feature = "_device-selected")))]
664 macro_rules! #macro_name {
665 (
666 $($pattern:tt => $code:tt;)*
667 ) => {
668 macro_rules! _for_each_inner {
669 $(($pattern) => $code;)*
670 ($other:tt) => {}
671 }
672
673 #(_for_each_inner!(( #flat_branches ));)*
683
684 #( _for_each_inner!( (#repeat_names #( (#repeat_branches) ),*) ); )*
694 };
695 }
696 }
697}
698
699pub fn generate_build_script_utils() -> TokenStream {
700 let check_cfgs = Chip::list_of_check_cfgs();
701
702 let chip = Chip::iter()
703 .map(|c| format_ident!("{}", c.name()))
704 .collect::<Vec<_>>();
705 let feature_env = Chip::iter().map(|c| format!("CARGO_FEATURE_{}", c.as_ref().to_uppercase()));
706 let name = Chip::iter()
707 .map(|c| c.as_ref().to_string())
708 .collect::<Vec<_>>();
709 let all_chip_features = name.join(", ");
710 let config = Chip::iter().map(|chip| {
711 let config = Config::for_chip(&chip);
712 let symbols = config.active_cfgs();
713 let arch = config.device.arch.to_string();
714 let target = config.device.target.as_str();
715 let cfgs = config.list_of_cfgs();
716 quote! {
717 Config {
718 architecture: #arch,
719 target: #target,
720 symbols: &[
721 #(#symbols,)*
722 ],
723 cfgs: &[
724 #(#cfgs,)*
725 ],
726 }
727 }
728 });
729
730 let bail_message = format!(
731 "Expected exactly one of the following features to be enabled: {all_chip_features}"
732 );
733
734 quote! {
735 #[cfg(docsrs)]
737 macro_rules! println {
738 ($($any:tt)*) => {};
739 }
740
741 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
742 #[cfg_attr(docsrs, doc(cfg(feature = "build-script")))]
743 pub enum Chip {
744 #(#chip),*
745 }
746
747 impl core::str::FromStr for Chip {
748 type Err = ();
749
750 fn from_str(s: &str) -> Result<Self, Self::Err> {
751 match s {
752 #( #name => Ok(Self::#chip),)*
753 _ => Err(()),
754 }
755 }
756 }
757
758 impl Chip {
759 pub fn from_cargo_feature() -> Result<Self, &'static str> {
763 let all_chips = [
764 #(( #feature_env, Self::#chip )),*
765 ];
766
767 let mut chip = None;
768 for (env, c) in all_chips {
769 if std::env::var(env).is_ok() {
770 if chip.is_some() {
771 return Err(#bail_message);
772 }
773 chip = Some(c);
774 }
775 }
776
777 match chip {
778 Some(chip) => Ok(chip),
779 None => Err(#bail_message)
780 }
781 }
782
783 pub fn is_xtensa(self) -> bool {
785 self.config().architecture == "xtensa"
786 }
787
788 pub fn target(self) -> &'static str {
790 self.config().target
791 }
792
793 pub fn name(self) -> &'static str {
801 match self {
802 #( Self::#chip => #name ),*
803 }
804 }
805
806 pub fn contains(self, symbol: &str) -> bool {
816 self.all_symbols().contains(&symbol)
817 }
818
819 pub fn define_cfgs(self) {
821 self.config().define_cfgs()
822 }
823
824 pub fn all_symbols(&self) -> &'static [&'static str] {
832 self.config().symbols
833 }
834
835 pub fn iter() -> impl Iterator<Item = Chip> {
843 [
844 #( Self::#chip ),*
845 ].into_iter()
846 }
847
848 fn config(self) -> Config {
849 match self {
850 #(Self::#chip => #config),*
851 }
852 }
853 }
854
855 struct Config {
856 architecture: &'static str,
857 target: &'static str,
858 symbols: &'static [&'static str],
859 cfgs: &'static [&'static str],
860 }
861
862 impl Config {
863 fn define_cfgs(&self) {
864 emit_check_cfg_directives();
865 for cfg in self.cfgs {
866 println!("{cfg}");
867 }
868 }
869 }
870
871 pub fn emit_check_cfg_directives() {
873 #(println!(#check_cfgs);)*
874 }
875 }
876}
877
878pub fn generate_lib_rs() -> TokenStream {
879 let chips = Chip::iter().map(|c| {
880 let feature = format!("{c}");
881 let file = format!("_generated_{c}.rs");
882 quote! {
883 #[cfg(feature = #feature)]
884 include!(#file);
885 }
886 });
887
888 quote! {
889 #![cfg_attr(docsrs, feature(doc_cfg))]
985 #![cfg_attr(not(feature = "build-script"), no_std)]
986
987 #(#chips)*
988
989 #[cfg(any(feature = "build-script", docsrs))]
990 include!( "_build_script_utils.rs");
991 }
992}
993
994pub fn generate_chip_support_status(output: &mut impl Write) -> std::fmt::Result {
995 let nothing = "";
996
997 let driver_col_width = std::iter::once("Driver")
999 .chain(
1000 PeriConfig::drivers()
1001 .iter()
1002 .filter(|i| !i.hide_from_peri_table)
1003 .map(|i| i.name),
1004 )
1005 .map(|c| c.len())
1006 .max()
1007 .unwrap();
1008
1009 write!(output, "| {:driver_col_width$} |", "Driver")?;
1011 for chip in Chip::iter() {
1012 write!(output, " {} |", chip.pretty_name())?;
1013 }
1014 writeln!(output)?;
1015
1016 write!(output, "| {nothing:-<driver_col_width$} |")?;
1018 for chip in Chip::iter() {
1019 write!(
1020 output,
1021 ":{nothing:-<width$}:|",
1022 width = chip.pretty_name().len()
1023 )?;
1024 }
1025 writeln!(output)?;
1026
1027 for SupportItem {
1029 name,
1030 config_group,
1031 hide_from_peri_table,
1032 } in PeriConfig::drivers()
1033 {
1034 if *hide_from_peri_table {
1035 continue;
1036 }
1037 write!(output, "| {name:driver_col_width$} |")?;
1038 for chip in Chip::iter() {
1039 let config = Config::for_chip(&chip);
1040
1041 let status = config.device.peri_config.support_status(config_group);
1042 let status_icon = match status {
1043 None => " ",
1044 Some(status) => status.icon(),
1045 };
1046 let support_cell_width = chip.pretty_name().len() - status.is_some() as usize;
1049 write!(output, " {status_icon:support_cell_width$} |")?;
1050 }
1051 writeln!(output)?;
1052 }
1053
1054 writeln!(output)?;
1055
1056 writeln!(output, " * Empty cell: not available")?;
1058 for s in [
1059 SupportStatus::NotSupported,
1060 SupportStatus::Partial,
1061 SupportStatus::Supported,
1062 ] {
1063 writeln!(output, " * {}: {}", s.icon(), s.status())?;
1064 }
1065
1066 Ok(())
1067}