use alloc::string::{String, ToString};
use alloc::vec::Vec;
use onerom_config::chip::{CHIP_TYPE_NAMES, ChipType};
use onerom_config::hw::Board;
#[cfg(test)]
use onerom_config::hw::Model;
use onerom_metadata::BitModes;
use crate::image::{ChipSetType, CsConfig, CsLogic};
use crate::v2::addr_layout::{LayoutError, derive_addr_layout};
use crate::v2::alg_config::bit_mode_for;
use crate::v2::alg_preference::{
AddrAlgPreference, CsAlgPreference, DataAlgPreference, cs_alg_preference,
};
use crate::v2::cs_data_layout::derive_cs_data_layout;
use crate::v2::multi_cs_config::derive_multi_cs_config;
use crate::v2::slot_context::{SlotContext, socket_pin_offset};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompatResult {
pub num_addr_pins: u8,
pub slot_size_bytes: u32,
pub pin_offset: i16,
pub num_fly_lead_pins: u8,
pub hole_gpios: u64,
}
impl CompatResult {
pub fn excess_addr_bits(&self) -> u32 {
self.hole_gpios.count_ones()
}
pub fn hole_gpio_list(&self) -> Vec<u8> {
(0..u64::BITS)
.filter(|bit| self.hole_gpios & (1u64 << bit) != 0)
.map(|bit| bit as u8)
.collect()
}
pub fn is_native(&self) -> bool {
self.pin_offset == 0
}
pub fn is_overhang(&self) -> bool {
self.pin_offset > 0
}
pub fn requires_fly_leads(&self) -> bool {
self.pin_offset < 0
}
pub fn fit_description(&self) -> String {
if self.is_native() {
"native".to_string()
} else if self.requires_fly_leads() {
match self.num_fly_lead_pins {
0 => "larger socket (no fly-leads)".to_string(),
1 => "fly-lead to X1".to_string(),
2 => "fly-lead to X1 and X2".to_string(),
n => alloc::format!("fly-lead ({n} pins)"),
}
} else {
"overhang".to_string()
}
}
}
pub fn format_size(bytes: u32) -> String {
if bytes >= 1024 * 1024 {
alloc::format!("{}MB", bytes / (1024 * 1024))
} else if bytes >= 1024 {
alloc::format!("{}KB", bytes / 1024)
} else {
alloc::format!("{bytes}B")
}
}
pub fn is_v2_chip(chip_type: ChipType) -> bool {
!chip_type.is_plugin() && crate::SUPPORTED_CHIP_TYPES_V2.contains(&chip_type)
}
pub fn default_cs_config(chip_type: ChipType) -> CsConfig {
let logic = |name: &str| {
chip_type
.control_lines()
.iter()
.any(|l| l.name == name)
.then_some(CsLogic::ActiveLow)
};
CsConfig::from_chip_type(
&chip_type,
logic("cs1"),
logic("cs2"),
logic("cs3"),
logic("cs4"),
logic("ce"),
logic("oe"),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServingAlgInfo {
pub addr_window_base: u8,
pub addr_window_pins: u8,
pub cs_alg: CsAlgPreference,
pub addr_alg: AddrAlgPreference,
pub data_alg: DataAlgPreference,
}
impl ServingAlgInfo {
pub fn samples_gpio(&self, gpio: u8) -> bool {
gpio >= self.addr_window_base && (gpio - self.addr_window_base) < self.addr_window_pins
}
}
pub fn serving_alg_info(
board: Board,
chip_type: ChipType,
set_type: ChipSetType,
num_chips: usize,
cs_config: CsConfig,
secondary_cs_config: Option<CsConfig>,
force_16_bit: bool,
) -> Result<ServingAlgInfo, crate::Error> {
let pin_offset = socket_pin_offset(chip_type.chip_pins(), board.chip_pins())
.ok_or(crate::Error::UnsupportedBoardChipType { board, chip_type })?;
let bit_mode = bit_mode_for(chip_type, board);
let multi_cs_config = match set_type {
ChipSetType::Multi => Some(derive_multi_cs_config(
chip_type,
&cs_config,
&secondary_cs_config.unwrap_or_else(|| multi_secondary_config(chip_type)),
)),
ChipSetType::Single | ChipSetType::Banked => None,
};
let ctx = SlotContext {
board,
set_type,
chip_types: alloc::vec![chip_type; num_chips],
cs_config,
bit_mode,
pin_offset,
force_16_bit,
multi_cs_config,
};
let addr_layout = derive_addr_layout(&ctx)?;
let cs_data_layout = derive_cs_data_layout(&ctx, Some(&addr_layout))?;
Ok(ServingAlgInfo {
addr_window_base: addr_layout.gpio_base,
addr_window_pins: addr_layout.num_addr_pins,
cs_alg: cs_alg_preference(
cs_data_layout.cs_ignore_index,
cs_data_layout.alg_cs2.as_ref(),
),
addr_alg: AddrAlgPreference::AlgAddr0,
data_alg: match (bit_mode, force_16_bit) {
(BitModes::BitMode16, false) => DataAlgPreference::AlgData1,
_ => DataAlgPreference::AlgData0,
},
})
}
fn multi_secondary_config(chip_type: ChipType) -> CsConfig {
let has = |name: &str| chip_type.control_lines().iter().any(|l| l.name == name);
if has("ce") && has("oe") {
CsConfig::CeOeExplicit {
ce: CsLogic::ActiveLow,
oe: CsLogic::Ignore,
}
} else {
default_cs_config(chip_type)
}
}
const MAX_MULTI_CHIPS: usize = 3;
pub fn check_chip_set_on_board(
board: Board,
chip_type: ChipType,
set_type: ChipSetType,
num_chips: usize,
cs_config: CsConfig,
) -> Result<CompatResult, crate::Error> {
if !is_v2_chip(chip_type) {
return Err(crate::Error::UnsupportedBoardChipType { board, chip_type });
}
match set_type {
ChipSetType::Single if num_chips == 1 => {}
ChipSetType::Banked if num_chips >= 2 && board.supports_banked_roms() => {}
ChipSetType::Multi
if (2..=MAX_MULTI_CHIPS).contains(&num_chips) && board.supports_multi_chip_sets() => {}
ChipSetType::Single | ChipSetType::Banked | ChipSetType::Multi => {
return Err(crate::Error::UnsupportedBoardConfig {
board,
reason: alloc::format!("board cannot serve a {num_chips}-chip {set_type:?} set"),
});
}
}
let pin_offset = socket_pin_offset(chip_type.chip_pins(), board.chip_pins())
.ok_or(crate::Error::UnsupportedBoardChipType { board, chip_type })?;
let bit_mode = bit_mode_for(chip_type, board);
let multi_cs_config = match set_type {
ChipSetType::Multi => Some(derive_multi_cs_config(
chip_type,
&cs_config,
&multi_secondary_config(chip_type),
)),
ChipSetType::Single | ChipSetType::Banked => None,
};
let ctx = SlotContext {
board,
set_type,
chip_types: alloc::vec![chip_type; num_chips],
cs_config,
bit_mode,
pin_offset,
force_16_bit: false,
multi_cs_config,
};
let addr_layout = derive_addr_layout(&ctx)?;
derive_cs_data_layout(&ctx, Some(&addr_layout))?;
let bytes_per_word: u32 = if matches!(bit_mode, BitModes::BitMode16) {
2
} else {
1
};
let num_fly_lead_pins = if pin_offset < 0 {
let addr_line_start = if matches!(bit_mode, BitModes::BitMode16) {
1
} else {
0
};
chip_type.address_pins()[addr_line_start..]
.iter()
.filter(|&&ap| {
let sp = ap as i16 + pin_offset;
sp < 1 || sp > board.chip_pins() as i16
})
.count() as u8
} else {
0
};
let slot_size_bytes = (1u32 << addr_layout.num_addr_pins) * bytes_per_word;
if slot_size_bytes as usize > crate::MAX_IMAGE_SIZE {
return Err(LayoutError::RomTableTooLarge {
board,
chip_type,
set_type,
num_chips,
num_addr_pins: addr_layout.num_addr_pins,
table_size: slot_size_bytes as usize,
}
.into());
}
let live: u64 = addr_layout
.addr_pin_gpios
.iter()
.chain(addr_layout.x1_gpio.iter())
.chain(addr_layout.x2_gpio.iter())
.fold(0u64, |acc, &gpio| acc | (1u64 << gpio));
let window_end = addr_layout.gpio_base + addr_layout.num_addr_pins;
let hole_gpios = (addr_layout.gpio_base..window_end)
.filter(|gpio| live & (1u64 << gpio) == 0)
.fold(0u64, |acc, gpio| acc | (1u64 << gpio));
Ok(CompatResult {
num_addr_pins: addr_layout.num_addr_pins,
slot_size_bytes,
pin_offset,
num_fly_lead_pins,
hole_gpios,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChipCompat {
pub chip_type: ChipType,
pub alias: &'static str,
pub rom_size_bytes: u32,
pub result: CompatResult,
}
pub fn pin_offset_order(pin_offset: i16) -> i32 {
match pin_offset {
0 => 0,
n if n > 0 => 1,
_ => 2,
}
}
pub fn supported_chips(board: Board, set_type: ChipSetType, num_chips: usize) -> Vec<ChipCompat> {
let mut entries: Vec<ChipCompat> = CHIP_TYPE_NAMES
.iter()
.filter_map(|alias| {
let chip_type = ChipType::try_from_str(alias)?;
let cs_config = default_cs_config(chip_type);
let result =
check_chip_set_on_board(board, chip_type, set_type, num_chips, cs_config).ok()?;
Some(ChipCompat {
chip_type,
alias,
rom_size_bytes: chip_type.size_bytes() as u32,
result,
})
})
.collect();
entries.sort_by_key(|e| {
(
pin_offset_order(e.result.pin_offset),
e.result.pin_offset.abs(),
e.rom_size_bytes,
e.alias,
)
});
entries
}
#[cfg(test)]
mod tests {
use super::*;
fn find(board: Board, alias: &str) -> ChipCompat {
*supported_chips(board, ChipSetType::Single, 1)
.iter()
.find(|e| e.alias == alias)
.unwrap_or_else(|| panic!("{alias} should be listed for {}", board.name()))
}
#[test]
fn format_size_picks_whole_units() {
assert_eq!(format_size(512), "512B");
assert_eq!(format_size(1024), "1KB");
assert_eq!(format_size(48 * 1024), "48KB");
assert_eq!(format_size(1024 * 1024), "1MB");
}
#[test]
fn reports_the_documented_image_sizes() {
let native = find(Board::Fire24F, "2364");
assert_eq!(native.rom_size_bytes, 8 * 1024);
assert_eq!(native.result.slot_size_bytes, 8 * 1024);
assert_eq!(native.result.fit_description(), "native");
let overhang = find(Board::Fire28C, "2364");
assert_eq!(overhang.rom_size_bytes, 8 * 1024);
assert_eq!(overhang.result.slot_size_bytes, 256 * 1024);
assert_eq!(overhang.result.fit_description(), "overhang");
let fly_lead = find(Board::Fire24F, "2764");
assert_eq!(fly_lead.rom_size_bytes, 8 * 1024);
assert_eq!(fly_lead.result.slot_size_bytes, 32 * 1024);
assert_eq!(fly_lead.result.fit_description(), "fly-lead to X1");
}
#[test]
fn larger_socket_without_fly_leads_says_so() {
let entry = find(Board::Fire28C, "28C512");
assert!(entry.result.requires_fly_leads());
assert_eq!(entry.result.num_fly_lead_pins, 0);
assert!(!entry.result.is_native());
assert_eq!(
entry.result.fit_description(),
"larger socket (no fly-leads)"
);
}
#[test]
fn orders_by_fit_class_without_interleaving() {
for board in [Board::Fire24F, Board::Fire28C, Board::Fire32B] {
let entries = supported_chips(board, ChipSetType::Single, 1);
assert!(!entries.is_empty(), "{} lists no chips", board.name());
let classes: Vec<i32> = entries
.iter()
.map(|e| pin_offset_order(e.result.pin_offset))
.collect();
assert!(
classes.windows(2).all(|w| w[0] <= w[1]),
"{} entries are not ordered by fit class: {classes:?}",
board.name()
);
let mut offsets: Vec<i16> = entries.iter().map(|e| e.result.pin_offset).collect();
offsets.dedup();
let unique = offsets.len();
offsets.sort_unstable();
offsets.dedup();
assert_eq!(
unique,
offsets.len(),
"{} has a pin offset split across sections",
board.name()
);
}
}
#[test]
fn lists_each_alias_separately() {
let entries = supported_chips(Board::Fire24F, ChipSetType::Single, 1);
for alias in ["2316", "9316", "9316A"] {
assert!(
entries.iter().any(|e| e.alias == alias),
"{alias} missing from the fire-24-f listing"
);
}
}
#[test]
fn omits_unsupported_chips() {
let entries = supported_chips(Board::Fire24F, ChipSetType::Single, 1);
assert!(entries.iter().all(|e| e.alias != "27C400"));
assert!(
check_chip_set_on_board(
Board::Fire24F,
ChipType::Chip27C400,
ChipSetType::Single,
1,
default_cs_config(ChipType::Chip27C400)
)
.is_err()
);
}
fn single(board: Board, chip_type: ChipType) -> CompatResult {
check_chip_set_on_board(
board,
chip_type,
ChipSetType::Single,
1,
default_cs_config(chip_type),
)
.expect("chip should be servable on this board")
}
#[test]
fn wasted_bits_name_the_pin_responsible() {
let result = single(Board::Fire28D, ChipType::Chip23128);
assert_eq!(result.slot_size_bytes, 32 * 1024);
assert_eq!(result.excess_addr_bits(), 1);
assert_eq!(result.hole_gpio_list(), alloc::vec![18]);
assert_eq!(Board::Fire28D.socket_pin_for_gpio(18), Some(1));
}
#[test]
fn a_chip_at_its_floor_has_no_holes() {
let result = single(Board::Fire28D, ChipType::Chip27512);
assert_eq!(result.slot_size_bytes, 64 * 1024);
assert_eq!(result.excess_addr_bits(), 0);
assert_eq!(result.hole_gpios, 0);
assert!(result.hole_gpio_list().is_empty());
}
#[test]
fn excess_bits_account_for_the_whole_image() {
for board in Model::Fire.boards().iter().filter(|b| b.mcu_pio()) {
for entry in supported_chips(*board, ChipSetType::Single, 1) {
let floor = entry.result.slot_size_bytes >> entry.result.excess_addr_bits();
assert_eq!(
floor << entry.result.excess_addr_bits(),
entry.result.slot_size_bytes,
"{} {}: {} excess bits does not account for a {}B image",
board.name(),
entry.alias,
entry.result.excess_addr_bits(),
entry.result.slot_size_bytes,
);
let bound = entry.rom_size_bytes.min(crate::MAX_IMAGE_SIZE as u32);
assert!(
floor >= bound,
"{} {}: floor {floor}B is below the {bound}B this chip needs",
board.name(),
entry.alias,
);
}
}
}
#[test]
fn a_banked_set_is_measured_separately_from_the_single() {
let alone = single(Board::Fire24F, ChipType::Chip2364);
assert_eq!(alone.slot_size_bytes, 8 * 1024);
assert_eq!(alone.excess_addr_bits(), 0);
let banked = check_chip_set_on_board(
Board::Fire24F,
ChipType::Chip2364,
ChipSetType::Banked,
2,
default_cs_config(ChipType::Chip2364),
)
.expect("Fire24F should serve a banked pair of 2364s");
assert_eq!(banked.slot_size_bytes, 32 * 1024);
assert_eq!(banked.excess_addr_bits(), 1);
}
#[test]
fn a_set_beyond_the_image_limit_is_unsupported() {
for (num_chips, servable) in [(2, true), (4, false)] {
let result = check_chip_set_on_board(
Board::Fire28D,
ChipType::Chip23QL384,
ChipSetType::Banked,
num_chips,
default_cs_config(ChipType::Chip23QL384),
);
assert_eq!(
result.is_ok(),
servable,
"banked x{num_chips} of 23QL384 on Fire28D"
);
}
}
#[test]
fn shapes_that_are_not_slot_shapes_are_declined() {
for (set_type, num_chips) in [
(ChipSetType::Single, 2),
(ChipSetType::Banked, 1),
(ChipSetType::Multi, 1),
] {
assert!(
check_chip_set_on_board(
Board::Fire28D,
ChipType::Chip27512,
set_type,
num_chips,
default_cs_config(ChipType::Chip27512),
)
.is_err(),
"{set_type:?} x{num_chips} is not a slot shape"
);
}
}
#[test]
fn a_homogeneous_multi_set_is_checkable() {
let result = check_chip_set_on_board(
Board::Fire24F,
ChipType::Chip2364,
ChipSetType::Multi,
2,
default_cs_config(ChipType::Chip2364),
)
.expect("Fire24F should serve two 2364s as a multi set");
assert_eq!(result.slot_size_bytes, 32 * 1024);
assert_eq!(result.excess_addr_bits(), 1);
}
#[test]
fn a_ce_oe_secondary_selects_on_ce_alone() {
assert_eq!(
multi_secondary_config(ChipType::Chip27512),
CsConfig::CeOeExplicit {
ce: CsLogic::ActiveLow,
oe: CsLogic::Ignore,
}
);
assert!(
check_chip_set_on_board(
Board::Fire28D,
ChipType::Chip27512,
ChipSetType::Multi,
2,
default_cs_config(ChipType::Chip27512),
)
.is_ok(),
"a CE/OE chip should be servable as a multi pair"
);
}
}