hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot/ata.zig
blob: da1c4cb9026151be26267b3bcaf46d43a9484bd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// ata pio driver, primary bus master drive, 28 bit lba
//
// this is the dumbest most simple most retartded possible block device driver
// it busy polls the status port and shovels one sector at a time through the 16
// bit data port. no dma no interrupts no second drive. its slow but the whole point
// is that revo builds a real filesystem on top of these two calls (read/write a 512 byte sector) so
// i need to rewrite this when im not drunk (never gonna happen)
//
// see: https://wiki.osdev.org/ATA_PIO_Mode

const port = @import("port.zig");
const serial = @import("serial.zig");

pub const SECTOR = 512;

// primary bus io ports
const DATA = 0x1F0;
const FEATURES = 0x1F1; // error on read
const SECCOUNT = 0x1F2;
const LBA_LO = 0x1F3;
const LBA_MID = 0x1F4;
const LBA_HI = 0x1F5;
const DRIVE = 0x1F6;
const CMD = 0x1F7; // status on read
const CTRL = 0x3F6; // alt status / device control

// status bits
const ST_ERR = 0x01;
const ST_DRQ = 0x08; // data request, drive wants a word moved
const ST_DRDY = 0x40;
const ST_BSY = 0x80;

const CMD_READ = 0x20;
const CMD_WRITE = 0x30;
const CMD_FLUSH = 0xE7;
const CMD_IDENTIFY = 0xEC;

const DRIVE_MASTER = 0xA0; // chs mode (for identify)
const DRIVE_LBA = 0xE0; // lba mode (top nibble holds lba 24..27)
const FLOAT_BUS = 0xFF; // status read on a bus with no drive
const SPIN_LIMIT = 1_000_000; // busy poll cap before we call the bus dead
const IDENTIFY_WORDS = 256; // identify returns one sector of 16 bit words
const ID_SECTORS_LO = 60; // identify words 60/61 hold the 28 bit sector count
const ID_SECTORS_HI = 61;

var g_present = false;
var g_sectors: u64 = 0;

pub fn present() bool {
    return g_present;
}

pub fn sectorCount() u64 {
    return g_sectors;
}

// reading the alt status 4 times burns ~400ns, the spec wants that gap after a
// command before the status bits mean anything
fn delay400() void {
    _ = port.inb(CTRL);
    _ = port.inb(CTRL);
    _ = port.inb(CTRL);
    _ = port.inb(CTRL);
}

// spin until bsy drops
// returns false if it never does (dead bus)
fn waitNotBusy() bool {
    var spins: u32 = 0;
    while (spins < SPIN_LIMIT) : (spins += 1) {
        if (port.inb(CMD) & ST_BSY == 0) return true;
    }
    return false;
}

// spin until the drive is ready to move a word (drq set)
// false on error/timeout
fn waitDrq() bool {
    var spins: u32 = 0;
    while (spins < SPIN_LIMIT) : (spins += 1) {
        const st = port.inb(CMD);
        if (st & ST_ERR != 0) return false;
        if (st & ST_BSY == 0 and st & ST_DRQ != 0) return true;
    }
    return false;
}

// select master and load the lba + a one sector count
fn selectSector(lba: u28) void {
    port.outb(DRIVE, DRIVE_LBA | @as(u8, @truncate((lba >> 24) & 0x0F)));
    port.outb(FEATURES, 0);
    port.outb(SECCOUNT, 1);
    port.outb(LBA_LO, @truncate(lba & 0xFF));
    port.outb(LBA_MID, @truncate((lba >> 8) & 0xFF));
    port.outb(LBA_HI, @truncate((lba >> 16) & 0xFF));
}

// probe the primary master with identify, the floating bus reads status
// 0xFF, a present drive clears bsy and coughs up 256 words of identify data,
// words 60/61 hold the 28 bit sector count
pub fn init() void {
    // a nonexistent bus floats high, bail before we wait a million spins on it
    if (port.inb(CMD) == FLOAT_BUS) {
        serial.write("ata: no drive on primary bus\r\n");
        return;
    }

    port.outb(DRIVE, DRIVE_MASTER);
    delay400();
    port.outb(SECCOUNT, 0);
    port.outb(LBA_LO, 0);
    port.outb(LBA_MID, 0);
    port.outb(LBA_HI, 0);
    port.outb(CMD, CMD_IDENTIFY);
    delay400();

    if (port.inb(CMD) == 0) {
        serial.write("ata: primary master absent\r\n");
        return;
    }
    if (!waitDrq()) {
        serial.write("ata: identify failed\r\n");
        return;
    }

    var id: [IDENTIFY_WORDS]u16 = undefined;
    for (&id) |*word| word.* = port.inw(DATA);

    g_sectors = (@as(u64, id[ID_SECTORS_HI]) << 16) | id[ID_SECTORS_LO];
    g_present = true;
    serial.write("ata: primary master, sectors=");
    serial.writeDec(g_sectors);
    serial.write("\r\n");
}

pub fn read(lba: u28, buf: *[SECTOR]u8) bool {
    if (!g_present) return false;
    if (!waitNotBusy()) return false;
    selectSector(lba);
    port.outb(CMD, CMD_READ);
    if (!waitDrq()) return false;
    var i: usize = 0;
    while (i < SECTOR) : (i += 2) {
        const word = port.inw(DATA);
        buf[i] = @truncate(word & 0xFF);
        buf[i + 1] = @truncate(word >> 8);
    }
    return true;
}

// write one 512 byte sector from buf to lba, then flush the drive cache so it
// actually hits the image and survives a reboot, yk, like an actual file system
pub fn write(lba: u28, buf: *const [SECTOR]u8) bool {
    if (!g_present) return false;
    if (!waitNotBusy()) return false;
    selectSector(lba);
    port.outb(CMD, CMD_WRITE);
    if (!waitDrq()) return false;
    var i: usize = 0;
    while (i < SECTOR) : (i += 2) {
        const word = @as(u16, buf[i]) | (@as(u16, buf[i + 1]) << 8);
        port.outw(DATA, word);
    }
    port.outb(CMD, CMD_FLUSH);
    _ = waitNotBusy();
    return true;
}