use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{AccessConstraints, MemAttrs, MemOps, MemResult, Region, RegionRef};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::dev::ata::bays::{self, Bay};
use crate::dev::ata::disk::{AtaDisk, Reg};
use crate::machine::realize::Instance;
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
pub const CLASS_NAME: &str = "pc.ide";
const STATE_VERSION: u32 = 1;
pub const COMMAND_WINDOW_LEN: u64 = 8;
pub const CONTROL_WINDOW_LEN: u64 = 1;
pub const DEFAULT_MASTER_BAY: &str = "ata0";
pub const DEFAULT_SLAVE_BAY: &str = "ata1";
#[must_use]
pub fn register_at(offset: u64) -> Reg {
match offset & 7 {
0 => Reg::Data,
1 => Reg::Feature,
2 => Reg::SectorCount,
3 => Reg::LbaLow,
4 => Reg::LbaMid,
5 => Reg::LbaHigh,
6 => Reg::Device,
_ => Reg::Command,
}
}
const PIN_RANK: LockRank = LockRank::LEAF;
struct Channel {
bays: [Arc<Bay>; 2],
names: [String; 2],
irq_out: Mutex<Option<WireSource>>,
}
impl fmt::Debug for Channel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Channel")
.field("master", &self.names[0])
.field("slave", &self.names[1])
.field("master_present", &self.bays[0].is_occupied())
.field("slave_present", &self.bays[1].is_occupied())
.finish()
}
}
impl Channel {
fn drives(&self) -> [Option<Arc<AtaDisk>>; 2] {
[self.bays[0].drive(), self.bays[1].drive()]
}
fn answering(drives: &[Option<Arc<AtaDisk>>; 2]) -> Option<&Arc<AtaDisk>> {
drives.iter().flatten().find(|drive| drive.is_selected())
}
fn read_reg(&self, reg: Reg, debug: bool) -> u16 {
let drives = self.drives();
let value = match Channel::answering(&drives) {
Some(drive) => drive.read_reg(reg, debug),
None => nobody_home(&drives),
};
drop(drives);
if !debug {
self.refresh();
}
value
}
fn write_reg(&self, reg: Reg, value: u16) {
for drive in self.drives().iter().flatten() {
drive.write_reg(reg, value);
}
self.refresh();
}
fn read_alt_status(&self) -> u8 {
let drives = self.drives();
match Channel::answering(&drives) {
Some(drive) => drive.read_alt_status(),
None => nobody_home(&drives) as u8,
}
}
fn write_control(&self, value: u8) {
for drive in self.drives().iter().flatten() {
drive.write_device_control(value);
}
self.refresh();
}
fn refresh(&self) {
let drives = self.drives();
let level = Channel::answering(&drives).is_some_and(|drive| drive.irq_asserted());
drop(drives);
let pin = self.irq_out.lock().clone();
if let Some(pin) = pin {
pin.set(Level::from_bool(level));
}
}
}
fn nobody_home(drives: &[Option<Arc<AtaDisk>>; 2]) -> u16 {
if drives.iter().any(Option::is_some) {
0x0000
} else {
0xffff
}
}
#[derive(Debug)]
struct CommandBlock(Arc<Channel>);
impl MemOps for CommandBlock {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let reg = register_at(offset);
match (reg, dst.len()) {
(Reg::Data, 2) => {
let word = self.0.read_reg(Reg::Data, attrs.debug);
dst.copy_from_slice(&word.to_le_bytes());
Ok(())
}
(Reg::Data, 4) => {
let lo = self.0.read_reg(Reg::Data, attrs.debug);
let hi = self.0.read_reg(Reg::Data, attrs.debug);
dst[..2].copy_from_slice(&lo.to_le_bytes());
dst[2..].copy_from_slice(&hi.to_le_bytes());
Ok(())
}
(Reg::Data, 1) => {
dst[0] = self.0.read_reg(Reg::Data, attrs.debug) as u8;
Ok(())
}
(reg, 1) => {
dst[0] = self.0.read_reg(reg, attrs.debug) as u8;
Ok(())
}
_ => Err(BusError::BadAccess),
}
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
if attrs.debug {
return Err(BusError::BadAccess);
}
let reg = register_at(offset);
match (reg, src.len()) {
(Reg::Data, 2) => {
self.0
.write_reg(Reg::Data, u16::from_le_bytes([src[0], src[1]]));
Ok(())
}
(Reg::Data, 4) => {
self.0
.write_reg(Reg::Data, u16::from_le_bytes([src[0], src[1]]));
self.0
.write_reg(Reg::Data, u16::from_le_bytes([src[2], src[3]]));
Ok(())
}
(reg, 1) => {
self.0.write_reg(reg, u16::from(src[0]));
Ok(())
}
_ => Err(BusError::BadAccess),
}
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U32)
}
}
#[derive(Debug)]
struct ControlBlock(Arc<Channel>);
impl MemOps for ControlBlock {
fn read(&self, _offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
*byte = self.0.read_alt_status();
Ok(())
}
fn write(&self, _offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
self.0.write_control(*value);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::word(Width::U8, Endian::Little)
}
}
#[derive(Debug)]
pub struct Ide {
channel: Arc<Channel>,
command: RegionRef,
control: RegionRef,
}
impl Ide {
pub fn new(props: &Props) -> Result<Ide> {
let mut r = props.reader();
let master = r.or_str("master", DEFAULT_MASTER_BAY)?.to_string();
let slave = r.or_str("slave", DEFAULT_SLAVE_BAY)?.to_string();
r.finish()?;
if master == slave {
return Err(Error::Config {
at: String::from(CLASS_NAME),
message: alloc::format!(
"`master` and `slave` are two positions on one cable and cannot both be \
`{master}`"
),
});
}
let bays = [bays::attach(props, &master)?, bays::attach(props, &slave)?];
Ok(Ide::with_bays(bays, [master, slave]))
}
#[must_use]
pub fn with_bays(bays: [Arc<Bay>; 2], names: [String; 2]) -> Ide {
let channel = Arc::new(Channel {
bays,
names,
irq_out: Mutex::with_rank(PIN_RANK, None),
});
let command: RegionRef = Arc::new(Region::io(
CLASS_NAME,
COMMAND_WINDOW_LEN,
Arc::new(CommandBlock(Arc::clone(&channel))) as Arc<dyn MemOps>,
));
let control: RegionRef = Arc::new(Region::io(
"pc.ide.ctl",
CONTROL_WINDOW_LEN,
Arc::new(ControlBlock(Arc::clone(&channel))) as Arc<dyn MemOps>,
));
Ide {
channel,
command,
control,
}
}
#[must_use]
pub fn drive(&self, position: crate::dev::ata::Position) -> Option<Arc<AtaDisk>> {
let index = usize::from(position == crate::dev::ata::Position::Device1);
self.channel.bays[index].drive()
}
#[must_use]
pub fn bay_names(&self) -> [&str; 2] {
[&self.channel.names[0], &self.channel.names[1]]
}
#[must_use]
pub fn irq_asserted(&self) -> bool {
let drives = self.channel.drives();
Channel::answering(&drives).is_some_and(|drive| drive.irq_asserted())
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: STATE_VERSION,
summary: "one AT IDE channel: the command and control blocks, master/slave selection, INTRQ",
properties: &[
PropertySpec {
name: "master",
kind: ValueKind::Str,
required: false,
summary: "the drive bay device 0 is fitted in (default `ata0`)",
},
PropertySpec {
name: "slave",
kind: ValueKind::Str,
required: false,
summary: "the drive bay device 1 is fitted in (default `ata1`)",
},
],
construct: |props| Ok(Box::new(Ide::new(props)?)),
};
impl Device for Ide {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
self.channel.refresh();
}
fn region(&self, name: &str) -> Option<RegionRef> {
match name {
"" | "regs" | "cmd" => Some(Arc::clone(&self.command)),
"ctl" => Some(Arc::clone(&self.control)),
_ => None,
}
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != "irq" {
return Err(Error::Config {
at: port.to_string(),
message: String::from("an IDE channel drives one pin, `irq`"),
});
}
*self.channel.irq_out.lock() = Some(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == "irq" {
self.channel.refresh();
}
}
}
impl Instance for Ide {}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Ide::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("master", ValueKind::Str))
.prop(PropSchema::new("slave", ValueKind::Str))
.region("regs")
.region("cmd")
.region("ctl")
.port("irq", PortDir::Out)
}
#[cfg(test)]
mod tests;