use alloc::collections::BTreeSet;
use alloc::string::ToString;
use alloc::vec::Vec;
use onerom_config::chip::ChipType;
use onerom_config::hw::Board;
use onerom_metadata::BitModes;
use crate::image::ChipSetType;
use crate::{Error, MAX_IMAGE_SIZE};
use super::alg_preference::AddrAlgPreference;
use super::gpio_window::fits_pio_window;
use super::slot_context::SlotContext;
const MIN_ADDR_PINS: u8 = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddrLayout {
pub gpio_base: u8,
pub num_addr_pins: u8,
pub x1_gpio: Option<u8>,
pub x2_gpio: Option<u8>,
pub addr_pin_gpios: Vec<u8>,
pub excess_addr_pin_gpios: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LayoutError {
UnmappedPin {
board: Board,
chip_type: ChipType,
phys_pin: u8,
},
MissingXPin { board: Board, x_pin: u8 },
NoValidLayout { board: Board },
NoSelectLine { board: Board, chip_type: ChipType },
NonContiguousSelect {
board: Board,
chip_type: ChipType,
gpios: Vec<u8>,
},
MissingAddrPinGpios { board: Board, chip_type: ChipType },
RomTooLargeNoCsConfig { board: Board, chip_type: ChipType },
RomTableTooLarge {
board: Board,
chip_type: ChipType,
set_type: ChipSetType,
num_chips: usize,
num_addr_pins: u8,
table_size: usize,
},
IncompatiblePinCount { board: Board, chip_type: ChipType },
InteriorIgnoredLine {
board: Board,
chip_type: ChipType,
gpio: u8,
},
}
impl From<LayoutError> for Error {
fn from(err: LayoutError) -> Self {
match err {
LayoutError::UnmappedPin {
board,
chip_type,
phys_pin,
} => Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"{} physical pin {} has no GPIO mapping on this board",
chip_type.name(),
phys_pin
),
},
LayoutError::NoSelectLine { board, chip_type } => Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"no select line found for {} — the chip type must have at least one \
control line (CE/OE/CS1) with a GPIO mapping on this board",
chip_type.name()
),
},
LayoutError::NonContiguousSelect {
board,
chip_type,
gpios,
} => Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"{} select/control GPIOs {:?} are not contiguous on this board; \
Multi and Banked sets require all select, commoned, and X-pin GPIOs \
to form a contiguous range within a single PIO window",
chip_type.name(),
gpios
),
},
LayoutError::MissingXPin { board, x_pin } => Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"Board does not define X{x_pin} pin, required for Multi/Banked sets"
),
},
LayoutError::NoValidLayout { board } => Error::UnsupportedBoardConfig {
board,
reason: "No valid GPIO layout found for this chip set on this board".to_string(),
},
LayoutError::MissingAddrPinGpios { board, chip_type } => Error::InvalidConfig {
error: alloc::format!(
"derive_cs_data_layout requires addr_layout for {chip_type:?} on \
{board:?} (ALG_CS_2 chip), but none was provided"
),
},
LayoutError::RomTooLargeNoCsConfig { board, chip_type } => Error::InvalidConfig {
error: alloc::format!(
"{chip_type:?} on {board:?}: ROM exceeds MAX_IMAGE_SIZE; cs1 \
(active_low/active_high) is required to select a half but was not configured"
),
},
LayoutError::RomTableTooLarge {
board,
chip_type,
set_type,
num_chips,
num_addr_pins,
table_size,
} => {
let size_str = if table_size.is_multiple_of(1024 * 1024) {
alloc::format!("{}MB", table_size / (1024 * 1024))
} else {
alloc::format!("{}KB", table_size / 1024)
};
let max_str = if MAX_IMAGE_SIZE.is_multiple_of(1024 * 1024) {
alloc::format!("{}MB", MAX_IMAGE_SIZE / (1024 * 1024))
} else {
alloc::format!("{}KB", MAX_IMAGE_SIZE / 1024)
};
let hint = if matches!(set_type, ChipSetType::Banked | ChipSetType::Multi)
&& num_chips > 1
{
alloc::format!(
" Try reducing to {} chip{}.",
num_chips - 1,
if num_chips - 1 == 1 { "" } else { "s" }
)
} else {
alloc::string::String::new()
};
Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"a {num_chips}-chip {set_type:?} set of {chip_type} requires \
{num_addr_pins} address bits, producing a {size_str} ROM table. \
Maximum supported is {max_str}.{hint}"
),
}
}
LayoutError::IncompatiblePinCount { board, chip_type } => {
Error::UnsupportedBoardChipType { board, chip_type }
}
LayoutError::InteriorIgnoredLine {
board,
chip_type,
gpio,
} => Error::UnsupportedBoardConfig {
board,
reason: alloc::format!(
"{} has an ignored control line at GPIO {gpio} sitting between \
this set's select lines; a truly-ignored line interior to the \
CS-detect span is not currently supported on this board (only \
edge-positioned ignored lines can be excluded and forced low)",
chip_type.name()
),
},
}
}
}
fn addr_line_candidates(
board: Board,
chip_type: ChipType,
n: usize,
pin_offset: i16,
) -> Result<&'static [u8], LayoutError> {
let socket_pin = (chip_type.address_pins()[n] as i16 + pin_offset) as u8;
let gpios = board.gpios_for_socket_pin(socket_pin);
if gpios.is_empty() {
return Err(LayoutError::UnmappedPin {
board,
chip_type,
phys_pin: socket_pin,
});
}
Ok(gpios)
}
pub fn derive_addr_layout(ctx: &SlotContext) -> Result<AddrLayout, LayoutError> {
let board = ctx.board;
let set_type = ctx.set_type;
let chip_types = &ctx.chip_types;
let bit_mode = ctx.bit_mode;
let multi_cs_config = ctx.multi_cs_config.as_ref();
let pin_offset = ctx.pin_offset;
let chip0 = chip_types[0];
let addr_line_start = if matches!(bit_mode, BitModes::BitMode16) {
1
} else {
0
};
let num_addr_lines = chip0.num_addr_lines() - addr_line_start;
let bytes_per_word: usize = if matches!(bit_mode, BitModes::BitMode16) {
2
} else {
1
};
let max_useful_addr_lines = (MAX_IMAGE_SIZE / bytes_per_word).ilog2() as usize;
let num_in_range = num_addr_lines.min(max_useful_addr_lines);
let num_excess = num_addr_lines - num_in_range;
let mut candidates: Vec<&'static [u8]> = Vec::with_capacity(num_addr_lines + 2);
let mut fly_lead_x_count = 0u8;
for n in 0..num_addr_lines {
let addr_pin_idx = addr_line_start + n;
let socket_pin = chip0.address_pins()[addr_pin_idx] as i16 + pin_offset;
if socket_pin < 1 || socket_pin > board.chip_pins() as i16 {
if matches!(set_type, ChipSetType::Multi | ChipSetType::Banked) {
return Err(LayoutError::NoValidLayout { board });
}
fly_lead_x_count += 1;
if fly_lead_x_count > 2 {
return Err(LayoutError::NoValidLayout { board });
}
let x_gpios = board.gpios_for_x_pin(fly_lead_x_count);
if x_gpios.is_empty() {
return Err(LayoutError::MissingXPin {
board,
x_pin: fly_lead_x_count,
});
}
candidates.push(x_gpios);
} else {
candidates.push(addr_line_candidates(
board,
chip0,
addr_pin_idx,
pin_offset,
)?);
}
}
let mut x1_idx: Option<usize> = None;
let mut x2_idx: Option<usize> = None;
if matches!(set_type, ChipSetType::Multi | ChipSetType::Banked) {
let x1 = board.gpios_for_x_pin(1);
if x1.is_empty() {
return Err(LayoutError::MissingXPin { board, x_pin: 1 });
}
x1_idx = Some(candidates.len());
candidates.push(x1);
if chip_types.len() >= 3 {
let x2 = board.gpios_for_x_pin(2);
if x2.is_empty() {
return Err(LayoutError::MissingXPin { board, x_pin: 2 });
}
x2_idx = Some(candidates.len());
candidates.push(x2);
}
}
let multi_span_gpios: Vec<u8> = if matches!(set_type, ChipSetType::Multi) {
if let Some(mcc) = multi_cs_config {
core::iter::once(mcc.per_chip_select)
.chain(mcc.commoned_lines.iter().copied())
.filter_map(|kind| {
let name = kind.name();
let line = chip0.control_lines().iter().find(|l| l.name == name)?;
let socket_pin = line.pin as i16 + pin_offset;
if socket_pin < 1 || socket_pin > board.chip_pins() as i16 {
return None;
}
board
.gpios_for_socket_pin(socket_pin as u8)
.first()
.copied()
})
.collect()
} else {
Vec::new()
}
} else {
Vec::new()
};
let two_option_slots: Vec<usize> = candidates
.iter()
.enumerate()
.filter(|(_, opts)| opts.len() > 1)
.map(|(i, _)| i)
.collect();
let num_combos: u32 = 1 << two_option_slots.len();
let mut best: Option<(AddrLayout, (AddrAlgPreference, u32))> = None;
for combo in 0..num_combos {
let resolved_vec: Vec<u8> = candidates
.iter()
.enumerate()
.map(|(i, opts)| {
let choice = two_option_slots
.iter()
.position(|&s| s == i)
.map(|bit| ((combo >> bit) & 1) as usize)
.unwrap_or(0);
opts[choice]
})
.collect();
let mut span_gpios: BTreeSet<u8> = resolved_vec[..num_in_range].iter().copied().collect();
for extra_idx in [x1_idx, x2_idx].into_iter().flatten() {
span_gpios.insert(resolved_vec[extra_idx]);
}
for &gpio in &multi_span_gpios {
span_gpios.insert(gpio);
}
let min = *span_gpios.iter().next().unwrap();
let max = *span_gpios.iter().last().unwrap();
let span = max - min + 1;
let num_addr_pins = span.max(MIN_ADDR_PINS);
let gpio_base = min;
if !fits_pio_window(gpio_base, num_addr_pins) {
continue;
}
let score = (
AddrAlgPreference::AlgAddr0,
(num_addr_pins as u32) * 1000 + gpio_base as u32,
);
if best.as_ref().is_none_or(|(_, s)| score < *s) {
best = Some((
AddrLayout {
gpio_base,
num_addr_pins,
x1_gpio: x1_idx.map(|i| resolved_vec[i]),
x2_gpio: x2_idx.map(|i| resolved_vec[i]),
addr_pin_gpios: resolved_vec[..num_in_range].to_vec(),
excess_addr_pin_gpios: if num_excess > 0 {
resolved_vec[num_in_range..num_addr_lines].to_vec()
} else {
Vec::new()
},
},
score,
));
}
}
best.map(|(layout, _)| layout)
.ok_or(LayoutError::NoValidLayout { board })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{CsConfig, CsLogic};
use super::super::slot_context::SlotContext;
fn ctx_single_8bit(board: Board, chip_type: ChipType) -> SlotContext {
SlotContext {
board,
set_type: ChipSetType::Single,
chip_types: alloc::vec![chip_type],
cs_config: CsConfig::new(Some(CsLogic::ActiveLow), None, None),
bit_mode: BitModes::BitMode8,
pin_offset: 0,
force_16_bit: false,
multi_cs_config: None,
}
}
#[test]
fn fire24a_2364_single() {
let layout = derive_addr_layout(&ctx_single_8bit(Board::Fire24A, ChipType::Chip2364))
.expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 0,
num_addr_pins: 16,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![7, 6, 5, 4, 3, 2, 1, 0, 10, 11, 14, 15, 12],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire28c_27128_banked_2chip() {
let ctx = SlotContext {
board: Board::Fire28C,
set_type: ChipSetType::Banked,
chip_types: alloc::vec![ChipType::Chip27128, ChipType::Chip27128],
cs_config: CsConfig::new(Some(CsLogic::ActiveLow), None, None),
bit_mode: BitModes::BitMode8,
pin_offset: 0,
force_16_bit: false,
multi_cs_config: None,
};
let layout = derive_addr_layout(&ctx).expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 13,
num_addr_pins: 16,
x1_gpio: Some(28),
x2_gpio: None,
addr_pin_gpios: alloc::vec![27, 26, 25, 24, 23, 22, 21, 20, 16, 15, 13, 14, 19, 17],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire40a_27c400_bitmode16() {
let ctx = SlotContext {
board: Board::Fire40A,
set_type: ChipSetType::Single,
chip_types: alloc::vec![ChipType::Chip27C400],
cs_config: CsConfig::new(Some(CsLogic::ActiveLow), None, None),
bit_mode: BitModes::BitMode16,
pin_offset: 0,
force_16_bit: false,
multi_cs_config: None,
};
let layout = derive_addr_layout(&ctx).expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 19,
num_addr_pins: 18,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![
36, 35, 34, 33, 32, 31, 30, 29, 27, 26, 25, 24, 23, 22, 21, 20, 19, 28
],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire32b_27c010_single() {
let layout = derive_addr_layout(&ctx_single_8bit(Board::Fire32B, ChipType::Chip27C010))
.expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 18,
num_addr_pins: 17,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![
34, 33, 32, 31, 30, 29, 28, 27, 23, 22, 20, 21, 26, 24, 25, 19, 18
],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire32b_27c040_single() {
let layout = derive_addr_layout(&ctx_single_8bit(Board::Fire32B, ChipType::Chip27C040))
.expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 16,
num_addr_pins: 19,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![
34, 33, 32, 31, 30, 29, 28, 27, 23, 22, 20, 21, 26, 24, 25, 19, 18, 17, 16
],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire32b_sst39sf040_single() {
let layout = derive_addr_layout(&ctx_single_8bit(Board::Fire32B, ChipType::ChipSST39SF040))
.expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 17,
num_addr_pins: 19,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![
34, 33, 32, 31, 30, 29, 28, 27, 23, 22, 20, 21, 26, 24, 25, 19, 18, 17, 35
],
excess_addr_pin_gpios: alloc::vec![],
}
);
}
#[test]
fn fire32b_27c080_single_excess_a19() {
let layout = derive_addr_layout(&ctx_single_8bit(Board::Fire32B, ChipType::Chip27C080))
.expect("layout derivation should succeed");
assert_eq!(
layout,
AddrLayout {
gpio_base: 16,
num_addr_pins: 19,
x1_gpio: None,
x2_gpio: None,
addr_pin_gpios: alloc::vec![
34, 33, 32, 31, 30, 29, 28, 27, 23, 22, 20, 21, 26, 24, 25, 19, 18, 17, 16
],
excess_addr_pin_gpios: alloc::vec![13],
}
);
}
}