use super::{AtaDisk, DEV_HEAD, DEV_OBSOLETE, SECTOR, ST_DRQ, Volatile};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Taskfile {
pub command: u8,
pub feature: u16,
pub count: u16,
pub lba: u64,
pub device: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Registers {
pub status: u8,
pub error: u8,
pub count: u16,
pub lba: u64,
pub device: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Done,
Data {
out: bool,
dma: bool,
block: u64,
},
}
impl Phase {
#[must_use]
pub fn is_data(self) -> bool {
matches!(self, Phase::Data { .. })
}
}
impl AtaDisk {
fn phase_of(state: &Volatile) -> Phase {
if state.status & ST_DRQ == 0 {
return Phase::Done;
}
let (out, dma) = match state.xfer.as_ref() {
Some(xfer) => (xfer.out, xfer.dma),
None => (false, false),
};
Phase::Data {
out,
dma,
block: (state.buf.len() - state.pos) as u64,
}
}
pub fn taskfile_start(&self, tf: &Taskfile) -> Phase {
let mut state = self.state.lock();
state.device = tf.device;
state.selected = true;
if state.in_reset {
return AtaDisk::phase_of(&state);
}
state
.features
.load(tf.feature as u8, (tf.feature >> 8) as u8);
state.count.load(tf.count as u8, (tf.count >> 8) as u8);
state.lba_low.load(tf.lba as u8, (tf.lba >> 24) as u8);
state
.lba_mid
.load((tf.lba >> 8) as u8, (tf.lba >> 32) as u8);
state
.lba_high
.load((tf.lba >> 16) as u8, (tf.lba >> 40) as u8);
self.command(&mut state, tf.command);
AtaDisk::phase_of(&state)
}
#[must_use]
pub fn taskfile_phase(&self) -> Phase {
AtaDisk::phase_of(&self.state.lock())
}
#[must_use]
pub fn taskfile_registers(&self) -> Registers {
let state = self.state.lock();
Registers {
status: state.status,
error: state.error,
count: u16::from(state.count.current) | (u16::from(state.count.previous) << 8),
lba: u64::from(state.lba_low.current)
| (u64::from(state.lba_mid.current) << 8)
| (u64::from(state.lba_high.current) << 16)
| (u64::from(state.lba_low.previous) << 24)
| (u64::from(state.lba_mid.previous) << 32)
| (u64::from(state.lba_high.previous) << 40),
device: state.device | DEV_OBSOLETE,
}
}
#[must_use]
pub fn taskfile_head(&self) -> u8 {
self.state.lock().device & DEV_HEAD
}
pub fn taskfile_acknowledge(&self) -> bool {
let mut state = self.state.lock();
let had = state.irq;
state.irq = false;
had
}
pub fn taskfile_read(&self, dst: &mut [u8]) -> u64 {
let mut state = self.state.lock();
if state.status & ST_DRQ == 0 {
return 0;
}
if state.xfer.as_ref().is_some_and(|x| x.out) {
return 0;
}
let at = state.pos;
let n = core::cmp::min(dst.len(), state.buf.len().saturating_sub(at));
if n == 0 {
return 0;
}
dst[..n].copy_from_slice(&state.buf[at..at + n]);
state.pos = at + n;
if state.pos >= state.buf.len() {
self.block_consumed(&mut state);
}
n as u64
}
pub fn taskfile_write(&self, src: &[u8]) -> u64 {
let mut state = self.state.lock();
if state.status & ST_DRQ == 0 {
return 0;
}
if !state.xfer.as_ref().is_some_and(|x| x.out) {
return 0;
}
let at = state.pos;
let n = core::cmp::min(src.len(), state.buf.len().saturating_sub(at));
if n == 0 {
return 0;
}
state.buf[at..at + n].copy_from_slice(&src[..n]);
state.pos = at + n;
if state.pos >= state.buf.len() {
self.block_filled(&mut state);
}
n as u64
}
#[must_use]
pub fn sector_bytes(&self) -> u64 {
SECTOR
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::RamStore;
use crate::dev::ata::disk::{
CTL_SRST, ERR_ABRT, ERR_IDNF, Geometry, Identity, Medium, Position, Reg, ST_BSY, ST_DRDY,
ST_ERR, cmd, default_geometry,
};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
const SECTORS: u64 = 128;
const BYTES: u64 = SECTORS * SECTOR;
fn stamp(lba: u64) -> Vec<u8> {
let mut out = vec![0u8; SECTOR as usize];
out[0] = lba as u8;
out[1] = 0xc3;
out[511] = !(lba as u8);
out
}
fn drive(dma: bool) -> (AtaDisk, Arc<RamStore>) {
let store = Arc::new(RamStore::new(BYTES));
for lba in 0..SECTORS {
RamStore::write_at(&store, lba * SECTOR, &stamp(lba)).expect("it fits");
}
let mut id =
Identity::new(SECTORS, default_geometry(SECTORS), true, 16).expect("an identity");
id.dma = dma;
let disk =
AtaDisk::with_medium(id, Position::Device0, Arc::clone(&store) as Arc<dyn Medium>)
.expect("the medium fits");
(disk, store)
}
fn drain(disk: &AtaDisk) -> Vec<u8> {
let mut out = Vec::new();
let mut chunk = [0u8; 100];
while disk.taskfile_phase().is_data() {
let n = disk.taskfile_read(&mut chunk);
assert!(n > 0, "a data phase that moved nothing");
out.extend_from_slice(&chunk[..n as usize]);
}
out
}
fn by_registers(disk: &AtaDisk, command: u8, lba: u32, count: u8) -> Vec<u8> {
disk.write_reg(
Reg::Device,
u16::from(DEV_OBSOLETE | 0x40 | ((lba >> 24) as u8 & 0x0f)),
);
disk.write_reg(Reg::SectorCount, u16::from(count));
disk.write_reg(Reg::LbaLow, u16::from(lba as u8));
disk.write_reg(Reg::LbaMid, u16::from((lba >> 8) as u8));
disk.write_reg(Reg::LbaHigh, u16::from((lba >> 16) as u8));
disk.write_reg(Reg::Command, u16::from(command));
let want = u64::from(if count == 0 { 256u16 } else { u16::from(count) }) * SECTOR;
let mut out = Vec::new();
while (out.len() as u64) < want {
let word = disk.read_reg(Reg::Data, false);
out.push(word as u8);
out.push((word >> 8) as u8);
}
out
}
#[test]
fn a_taskfile_read_and_a_register_read_return_the_same_bytes() {
let (disk, _store) = drive(false);
let by_ports = by_registers(&disk, cmd::READ_SECTORS, 7, 3);
let phase = disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS,
count: 3,
lba: 7,
device: 0x40,
..Taskfile::default()
});
assert!(matches!(
phase,
Phase::Data {
out: false,
dma: false,
block: 512
}
));
let by_taskfile = drain(&disk);
assert_eq!(by_taskfile, by_ports);
assert_eq!(&by_taskfile[..512], &stamp(7)[..]);
assert_eq!(&by_taskfile[1024..], &stamp(9)[..]);
assert_eq!(disk.taskfile_registers().status, ST_DRDY | 0x10);
}
#[test]
fn identify_device_is_the_same_512_bytes_either_way() {
let (disk, _store) = drive(false);
disk.write_reg(Reg::Command, u16::from(cmd::IDENTIFY));
let mut by_ports = Vec::new();
while by_ports.len() < 512 {
let word = disk.read_reg(Reg::Data, false);
by_ports.push(word as u8);
by_ports.push((word >> 8) as u8);
}
let phase = disk.taskfile_start(&Taskfile {
command: cmd::IDENTIFY,
..Taskfile::default()
});
assert!(matches!(
phase,
Phase::Data {
out: false,
dma: false,
block: 512
}
));
assert_eq!(drain(&disk), by_ports);
assert_eq!(disk.taskfile_phase(), Phase::Done);
}
#[test]
fn a_forty_eight_bit_taskfile_reaches_the_high_halves_of_the_registers() {
let (disk, _store) = drive(false);
let phase = disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS_EXT,
count: 2,
lba: 100,
device: 0x40,
..Taskfile::default()
});
assert!(phase.is_data());
let got = drain(&disk);
assert_eq!(&got[..512], &stamp(100)[..]);
assert_eq!(&got[512..], &stamp(101)[..]);
let regs = disk.taskfile_registers();
assert_eq!(regs.lba, 101);
assert_eq!(regs.count, 0);
assert_eq!(regs.status & ST_ERR, 0);
let phase = disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS_EXT,
count: 1,
lba: 0x0000_5566_7788_99aa,
device: 0x40,
..Taskfile::default()
});
assert_eq!(phase, Phase::Done, "an address past the end has no data");
let regs = disk.taskfile_registers();
assert_eq!(regs.status & ST_ERR, ST_ERR);
assert_eq!(regs.error, ERR_IDNF);
}
#[test]
fn a_taskfile_write_reaches_the_medium() {
let (disk, store) = drive(false);
let payload: Vec<u8> = (0..SECTOR as usize).map(|i| (i as u8) ^ 0x77).collect();
let phase = disk.taskfile_start(&Taskfile {
command: cmd::WRITE_SECTORS,
count: 1,
lba: 20,
device: 0x40,
..Taskfile::default()
});
assert!(matches!(phase, Phase::Data { out: true, .. }));
let mut at = 0usize;
while disk.taskfile_phase().is_data() {
let end = (at + 300).min(payload.len());
let n = disk.taskfile_write(&payload[at..end]);
assert!(n > 0);
at += n as usize;
}
assert_eq!(at, payload.len());
let mut got = vec![0u8; SECTOR as usize];
Medium::read_at(&*store, 20 * SECTOR, &mut got).expect("the medium reads");
assert_eq!(got, payload);
Medium::read_at(&*store, 21 * SECTOR, &mut got).expect("the medium reads");
assert_eq!(got, stamp(21));
}
#[test]
fn a_read_and_a_write_will_not_move_each_others_blocks() {
let (disk, _store) = drive(false);
disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS,
count: 1,
lba: 1,
device: 0x40,
..Taskfile::default()
});
assert_eq!(disk.taskfile_write(&[0u8; 512]), 0, "a data-in block");
disk.taskfile_start(&Taskfile {
command: cmd::WRITE_SECTORS,
count: 1,
lba: 1,
device: 0x40,
..Taskfile::default()
});
let mut buf = [0u8; 512];
assert_eq!(disk.taskfile_read(&mut buf), 0, "a data-out block");
disk.taskfile_start(&Taskfile {
command: cmd::FLUSH_CACHE,
..Taskfile::default()
});
assert_eq!(disk.taskfile_phase(), Phase::Done);
assert_eq!(disk.taskfile_read(&mut buf), 0);
assert_eq!(disk.taskfile_write(&buf), 0);
}
#[test]
fn the_dma_family_answers_only_on_a_drive_that_has_dma() {
let (plain, _store) = drive(false);
let phase = plain.taskfile_start(&Taskfile {
command: cmd::READ_DMA_EXT,
count: 1,
lba: 3,
device: 0x40,
..Taskfile::default()
});
assert_eq!(phase, Phase::Done);
let regs = plain.taskfile_registers();
assert_eq!(regs.status & ST_ERR, ST_ERR);
assert_eq!(
regs.error, ERR_ABRT,
"aborted, as a device without a command must"
);
let (fast, _store) = drive(true);
let phase = fast.taskfile_start(&Taskfile {
command: cmd::READ_DMA_EXT,
count: 1,
lba: 3,
device: 0x40,
..Taskfile::default()
});
assert!(matches!(
phase,
Phase::Data {
out: false,
dma: true,
block: 512
}
));
assert_eq!(drain(&fast), stamp(3));
}
#[test]
fn the_two_protocols_read_the_same_sector() {
let (disk, _store) = drive(true);
disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS_EXT,
count: 1,
lba: 11,
device: 0x40,
..Taskfile::default()
});
let by_pio = drain(&disk);
disk.taskfile_start(&Taskfile {
command: cmd::READ_DMA_EXT,
count: 1,
lba: 11,
device: 0x40,
..Taskfile::default()
});
let by_dma = drain(&disk);
assert_eq!(by_pio, by_dma);
assert_eq!(by_pio, stamp(11));
}
#[test]
fn a_drive_held_in_software_reset_starts_nothing() {
let (disk, _store) = drive(false);
disk.write_device_control(CTL_SRST);
let phase = disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS,
count: 1,
lba: 0,
device: 0x40,
..Taskfile::default()
});
assert_eq!(phase, Phase::Done);
assert_eq!(disk.taskfile_registers().status, ST_BSY);
disk.write_device_control(0);
let regs = disk.taskfile_registers();
assert_eq!(regs.status, ST_DRDY | 0x10);
assert_eq!(regs.count, 1);
assert_eq!(regs.lba, 1);
assert!(
!disk.taskfile_acknowledge(),
"a software reset raises no INTRQ"
);
}
#[test]
fn the_interrupt_is_taken_once() {
let (disk, _store) = drive(false);
disk.taskfile_start(&Taskfile {
command: cmd::FLUSH_CACHE,
..Taskfile::default()
});
assert!(disk.taskfile_acknowledge(), "the command completed");
assert!(!disk.taskfile_acknowledge(), "and the line dropped");
}
#[test]
fn a_chs_taskfile_names_the_same_sector_an_lba_one_does() {
let (disk, _store) = drive(false);
let geometry: Geometry = disk.current_geometry();
let lba = 40u64;
let head = (lba / u64::from(geometry.sectors)) % u64::from(geometry.heads);
let sector = lba % u64::from(geometry.sectors) + 1;
let cylinder = lba / (u64::from(geometry.sectors) * u64::from(geometry.heads));
disk.taskfile_start(&Taskfile {
command: cmd::READ_SECTORS,
count: 1,
lba: sector | (cylinder << 8),
device: head as u8,
..Taskfile::default()
});
assert_eq!(drain(&disk), stamp(lba));
assert_eq!(disk.sector_bytes(), SECTOR);
assert_eq!(disk.taskfile_head(), head as u8);
}
}