summaryrefslogtreecommitdiff
path: root/kernel/drivers/ahci.c
diff options
context:
space:
mode:
authorAnton Kling <anton@kling.gg>2023-11-12 20:55:33 +0100
committerAnton Kling <anton@kling.gg>2023-11-12 20:58:08 +0100
commit23b8815e8ca1bc8fbc745fc3073be21f5a813c9e (patch)
tree0499342477c7df1452907851ce29ec1ff6135f6c /kernel/drivers/ahci.c
parent4e34a5f97786b3bab82f08297ead63e1618744ec (diff)
AHCI: Add very basic AHCI and some additional helper functions to mmu.c
Currently certain parts of the code are taken from osdev wiki as there seems to be very little documentation that I can find on how H2D FIS is structured. Those parts, especially some of the uglier parts of the example code will be rewritten/changed.
Diffstat (limited to 'kernel/drivers/ahci.c')
-rw-r--r--kernel/drivers/ahci.c435
1 files changed, 435 insertions, 0 deletions
diff --git a/kernel/drivers/ahci.c b/kernel/drivers/ahci.c
new file mode 100644
index 0000000..dfb9395
--- /dev/null
+++ b/kernel/drivers/ahci.c
@@ -0,0 +1,435 @@
+#include <assert.h>
+#include <drivers/pci.h>
+#include <mmu.h>
+#include <stdio.h>
+
+#define ATA_DEV_BUSY 0x80
+#define ATA_DEV_DRQ 0x08
+
+#define HBA_PxIS_TFES (1 << 30)
+
+// https://wiki.osdev.org/ATA_Command_Matrix
+#define ATA_CMD_READ_DMA_EX 0x25
+
+typedef enum {
+ FIS_TYPE_REG_H2D = 0x27, // Register FIS - host to device
+ FIS_TYPE_REG_D2H = 0x34, // Register FIS - device to host
+ FIS_TYPE_DMA_ACT = 0x39, // DMA activate FIS - device to host
+ FIS_TYPE_DMA_SETUP = 0x41, // DMA setup FIS - bidirectional
+ FIS_TYPE_DATA = 0x46, // Data FIS - bidirectional
+ FIS_TYPE_BIST = 0x58, // BIST activate FIS - bidirectional
+ FIS_TYPE_PIO_SETUP = 0x5F, // PIO setup FIS - device to host
+ FIS_TYPE_DEV_BITS = 0xA1, // Set device bits FIS - device to host
+} FIS_TYPE;
+
+struct HBA_PRDT_ENTRY {
+ u32 dba; // Data base address
+ u32 dbau; // Data base address upper 32 bits
+ u32 rsv0; // Reserved
+
+ // DW3
+ u32 dbc : 22; // Byte count, 4M max
+ u32 rsv1 : 9; // Reserved
+ u32 i : 1; // Interrupt on completion
+};
+
+struct HBA_CMD_TBL {
+ // 0x00
+ u8 cfis[64]; // Command FIS
+
+ // 0x40
+ u8 acmd[16]; // ATAPI command, 12 or 16 bytes
+
+ // 0x50
+ u8 rsv[48]; // Reserved
+
+ // 0x80
+ struct HBA_PRDT_ENTRY
+ prdt_entry[1]; // Physical region descriptor table entries, 0 ~ 65535
+};
+
+// Host to device
+struct FIS_REG_H2D {
+ // DWORD 0
+ u8 fis_type; // FIS_TYPE_REG_H2D
+
+ u8 pmport : 4; // Port multiplier
+ u8 rsv0 : 3; // Reserved
+ u8 c : 1; // 1: Command, 0: Control
+
+ u8 command; // Command register
+ u8 featurel; // Feature register, 7:0
+
+ // DWORD 1
+ u8 lba0; // LBA low register, 7:0
+ u8 lba1; // LBA mid register, 15:8
+ u8 lba2; // LBA high register, 23:16
+ u8 device; // Device register
+
+ // DWORD 2
+ u8 lba3; // LBA register, 31:24
+ u8 lba4; // LBA register, 39:32
+ u8 lba5; // LBA register, 47:40
+ u8 featureh; // Feature register, 15:8
+
+ // DWORD 3
+ u8 countl; // Count register, 7:0
+ u8 counth; // Count register, 15:8
+ u8 icc; // Isochronous command completion
+ u8 control; // Control register
+
+ // DWORD 4
+ u8 rsv1[4]; // Reserved
+};
+
+struct HBA_PORT {
+ u32 clb; // 0x00, command list base address, 1K-byte aligned
+ u32 clbu; // 0x04, command list base address upper 32 bits
+ u32 fb; // 0x08, FIS base address, 256-byte aligned
+ u32 fbu; // 0x0C, FIS base address upper 32 bits
+ u32 is; // 0x10, interrupt status
+ u32 ie; // 0x14, interrupt enable
+ u32 cmd; // 0x18, command and status
+ u32 rsv0; // 0x1C, Reserved
+ u32 tfd; // 0x20, task file data
+ u32 sig; // 0x24, signature
+ u32 ssts; // 0x28, SATA status (SCR0:SStatus)
+ u32 sctl; // 0x2C, SATA control (SCR2:SControl)
+ u32 serr; // 0x30, SATA error (SCR1:SError)
+ u32 sact; // 0x34, SATA active (SCR3:SActive)
+ u32 ci; // 0x38, command issue
+ u32 sntf; // 0x3C, SATA notification (SCR4:SNotification)
+ u32 fbs; // 0x40, FIS-based switch control
+ u32 rsv1[11]; // 0x44 ~ 0x6F, Reserved
+ u32 vendor[4]; // 0x70 ~ 0x7F, vendor specific
+};
+
+struct HBA_MEM {
+ // 0x00 - 0x2B, Generic Host Control
+ u32 cap; // 0x00, Host capability
+ u32 ghc; // 0x04, Global host control
+ u32 is; // 0x08, Interrupt status
+ u32 pi; // 0x0C, Port implemented
+ u32 vs; // 0x10, Version
+ u32 ccc_ctl; // 0x14, Command completion coalescing control
+ u32 ccc_pts; // 0x18, Command completion coalescing ports
+ u32 em_loc; // 0x1C, Enclosure management location
+ u32 em_ctl; // 0x20, Enclosure management control
+ u32 cap2; // 0x24, Host capabilities extended
+ u32 bohc; // 0x28, BIOS/OS handoff control and status
+
+ // 0x2C - 0x9F, Reserved
+ u8 rsv[0xA0 - 0x2C];
+
+ // 0xA0 - 0xFF, Vendor specific registers
+ u8 vendor[0x100 - 0xA0];
+
+ // 0x100 - 0x10FF, Port control registers
+ struct HBA_PORT ports[1]; // 1 ~ 32
+};
+
+struct HBA_CMD_HEADER {
+ // DW0
+ u8 cfl : 5; // Command FIS length in DWORDS, 2 ~ 16
+ u8 a : 1; // ATAPI
+ u8 w : 1; // Write, 1: H2D, 0: D2H
+ u8 p : 1; // Prefetchable
+
+ u8 r : 1; // Reset
+ u8 b : 1; // BIST
+ u8 c : 1; // Clear busy upon R_OK
+ u8 rsv0 : 1; // Reserved
+ u8 pmp : 4; // Port multiplier port
+
+ u16 prdtl; // Physical region descriptor table length in entries
+
+ // DW1
+ volatile u32 prdbc; // Physical region descriptor byte count transferred
+
+ // DW2, 3
+ u32 ctba; // Command table descriptor base address
+ u32 ctbau; // Command table descriptor base address upper 32 bits
+
+ // DW4 - 7
+ u32 rsv1[4]; // Reserved
+};
+
+#define SATA_SIG_ATA 0x00000101 // SATA drive
+#define SATA_SIG_ATAPI 0xEB140101 // SATAPI drive
+#define SATA_SIG_SEMB 0xC33C0101 // Enclosure management bridge
+#define SATA_SIG_PM 0x96690101 // Port multiplier
+
+#define AHCI_DEV_NULL 0
+#define AHCI_DEV_SATA 1
+#define AHCI_DEV_SEMB 2
+#define AHCI_DEV_PM 3
+#define AHCI_DEV_SATAPI 4
+
+#define HBA_PORT_IPM_ACTIVE 1
+#define HBA_PORT_DET_PRESENT 3
+
+u32 check_type(volatile struct HBA_PORT *port) {
+ u32 ssts = port->ssts;
+
+ u8 ipm = (ssts >> 8) & 0x0F;
+ u8 det = ssts & 0x0F;
+
+ if (det != HBA_PORT_DET_PRESENT) // Check drive status
+ return AHCI_DEV_NULL;
+ if (ipm != HBA_PORT_IPM_ACTIVE)
+ return AHCI_DEV_NULL;
+
+ switch (port->sig) {
+ case SATA_SIG_ATAPI:
+ return AHCI_DEV_SATAPI;
+ case SATA_SIG_SEMB:
+ return AHCI_DEV_SEMB;
+ case SATA_SIG_PM:
+ return AHCI_DEV_PM;
+ default:
+ return AHCI_DEV_SATA;
+ }
+}
+
+void ahci_start_command_execution(volatile struct HBA_PORT *port) {
+ // Wait for it to stop running.
+ // TODO: Figure out if this is really required.
+ for (; port->cmd & (1 << 14);)
+ ;
+
+ // Start recieving FIS into PxFB
+ port->cmd |= ((u32)4 << 0);
+ // Start processing of commands
+ port->cmd |= ((u32)1 << 0);
+}
+
+void ahci_stop_command_execution(volatile struct HBA_PORT *port) {
+ // Disable processing of commands
+ port->cmd &= ~((u32)1 << 0);
+ // Disable recieving FIS into PxFB
+ port->cmd &= ~((u32)1 << 4);
+
+ // Check the CR and FR registers to make sure they are no longer running.
+ for (; (port->cmd & (1 << 14)) || (port->cmd & (1 << 15));)
+ ;
+}
+
+// clb_address: size has to be 1024 and byte aligned to 1024
+// fb_address: size has to be 256 and byte aligned to 256
+// command_table_array: size has to be 256*32
+// They are both physical addresses
+void ahci_set_base(volatile struct HBA_PORT *port, u32 virt_clb_address,
+ u32 virt_fb_address, u32 virt_command_table_array) {
+ u32 clb_address = (u32)virtual_to_physical((void *)virt_clb_address, NULL);
+ u32 fb_address = (u32)virtual_to_physical((void *)virt_fb_address, NULL);
+ u32 command_table_array =
+ (u32)virtual_to_physical((void *)virt_command_table_array, NULL);
+
+ ahci_stop_command_execution(port);
+
+ // Command List Base Address (CLB): Indicates the 32-bit base physical address
+ // for the command list for this port. This base is used when fetching
+ // commands to execute. This address must be 1K-byte aligned as indicated by
+ // bits 09:00 being read only.
+ assert(0 == (clb_address & (0x3FF)));
+ assert(0 == (fb_address & (0xFF)));
+ port->clb = clb_address & (~(u32)(0x3FF));
+ port->clbu = 0;
+
+ port->fb = fb_address;
+ port->fbu = 0;
+
+ // Command table offset: 40K + 8K*portno
+ // Command table size = 256*32 = 8K per port
+ struct HBA_CMD_HEADER *cmdheader =
+ (struct HBA_CMD_HEADER *)(virt_clb_address);
+ for (u8 i = 0; i < 32; i++) {
+ cmdheader[i].prdtl = 8; // 8 prdt entries per command table
+ // 256 bytes per command table, 64+16+48+16*8
+ cmdheader[i].ctba = command_table_array + i * 256;
+ cmdheader[i].ctbau = 0;
+ }
+
+ ahci_start_command_execution(port);
+}
+
+void ahci_sata_setup(volatile struct HBA_PORT *port) {
+ // clb_address: size has to be 1024 and byte aligned to 1024
+ // fb_address: size has to be 256 and byte aligned to 256
+ // command_table_array: size has to be 256*32
+ u32 clb_address = (u32)ksbrk(1024);
+ u32 fb_address = (u32)ksbrk(256);
+ u32 command_table_array = (u32)ksbrk(256 * 32);
+ create_physical_to_virtual_mapping(
+ virtual_to_physical((void *)clb_address, NULL), (void *)clb_address,
+ 1024);
+ create_physical_to_virtual_mapping(
+ virtual_to_physical((void *)fb_address, NULL), (void *)fb_address, 256);
+ create_physical_to_virtual_mapping(
+ virtual_to_physical((void *)command_table_array, NULL),
+ (void *)command_table_array, 256 * 32);
+
+ // TODO: Should it be the responsiblity of the caller to make sure these are
+ // clear?
+ memset((void *)clb_address, 0, 1024);
+ memset((void *)fb_address, 0, 256);
+ memset((void *)command_table_array, 0, 256 * 32);
+
+ ahci_set_base(port, clb_address, fb_address, command_table_array);
+}
+
+// Returns the command slot.
+// Sets err if no free slot was found.
+u8 get_free_command_slot(volatile struct HBA_PORT *port, u8 *err) {
+ u32 slots = (port->sact | port->ci);
+ for (u8 i = 0; i < 32; i++) {
+ if (!((slots >> i) & 1)) {
+ *err = 0;
+ return i;
+ }
+ }
+ *err = 1;
+ return 0;
+}
+
+u8 ahci_read(volatile struct HBA_PORT *port, u32 startl, u32 starth, u32 count,
+ u16 *outbuffer) {
+ port->is = -1; // Clear pending interrupts
+ u8 err;
+ u32 command_slot = get_free_command_slot(port, &err);
+ if (err) {
+ klog("No command slot found", LOG_WARN);
+ return 0;
+ }
+ struct HBA_CMD_HEADER *cmdheader =
+ (struct HBA_CMD_HEADER *)physical_to_virtual((void *)port->clb);
+ cmdheader += command_slot;
+ cmdheader->w = 0; // No write(aka read)
+ cmdheader->cfl = sizeof(struct FIS_REG_H2D) / sizeof(u32);
+ cmdheader->prdtl = (u16)((count - 1) / 16 + 1); // Number of PRDT
+
+ // Write to the prdtl
+ struct HBA_CMD_TBL *cmdtbl =
+ (struct HBA_CMD_TBL *)(physical_to_virtual((void *)cmdheader->ctba));
+
+ memset((void *)cmdtbl, 0,
+ sizeof(struct HBA_CMD_TBL) +
+ (cmdheader->prdtl - 1) * sizeof(struct HBA_PRDT_ENTRY));
+
+ // 8K bytes (16 sectors) per PRDT
+ u16 i = 0;
+ for (; i < cmdheader->prdtl - 1; i++) {
+ cmdtbl->prdt_entry[i].dba = (u32)virtual_to_physical(outbuffer, NULL);
+ cmdtbl->prdt_entry[i].dbc =
+ 8 * 1024 - 1; // 8K bytes (this value should always be set to 1 less
+ // than the actual value)
+ cmdtbl->prdt_entry[i].i = 1;
+ outbuffer += 4 * 1024; // 4K words
+ count -= 16; // 16 sectors
+ }
+ // FIXME: Edge case if the count does not fit. This should not be here it is
+ // ugly. Find a more general case.
+ cmdtbl->prdt_entry[i].dba = (u32)virtual_to_physical(outbuffer, NULL);
+ cmdtbl->prdt_entry[i].dbc = count * 512 - 1;
+ cmdtbl->prdt_entry[i].i = 1;
+
+ struct FIS_REG_H2D *cmdfis = (struct FIS_REG_H2D *)(&cmdtbl->cfis);
+
+ cmdfis->fis_type = FIS_TYPE_REG_H2D;
+ cmdfis->c = 1;
+ cmdfis->command = ATA_CMD_READ_DMA_EX;
+
+ cmdfis->lba0 = (u8)startl;
+ cmdfis->lba1 = (u8)(startl >> 8);
+ cmdfis->lba2 = (u8)(startl >> 16);
+ cmdfis->device = 1 << 6; // LBA mode
+
+ cmdfis->lba3 = (u8)(startl >> 24);
+ cmdfis->lba4 = (u8)starth;
+ cmdfis->lba5 = (u8)(starth >> 8);
+
+ cmdfis->countl = count & 0xFF;
+ cmdfis->counth = (count >> 8) & 0xFF;
+
+ // The below loop waits until the port is no longer busy before issuing a new
+ // command
+ u32 spin = 0;
+ while ((port->tfd & (ATA_DEV_BUSY | ATA_DEV_DRQ)) && spin < 1000000) {
+ spin++;
+ }
+ if (spin == 1000000) {
+ kprintf("Port is hung\n");
+ return 0;
+ }
+
+ port->ci = 1 << command_slot; // Issue command
+
+ // Wait for completion
+ for (;;) {
+ // In some longer duration reads, it may be helpful to spin on the DPS bit
+ // in the PxIS port field as well (1 << 5)
+ if ((port->ci & (1 << command_slot)) == 0)
+ break;
+ if (port->is & HBA_PxIS_TFES) // Task file error
+ {
+ kprintf("Read disk error\n");
+ return 0;
+ }
+ }
+
+ // Check again
+ if (port->is & HBA_PxIS_TFES) {
+ kprintf("Read disk error\n");
+ return 0;
+ }
+
+ return 1;
+}
+
+void ahci_test(volatile struct HBA_PORT *port) {
+ char buffer[1024];
+ memset(buffer, 1, 1024);
+ assert(ahci_read(port, 0, 0, 2, (u16 *)buffer));
+ for (u32 i = 0; i < 1024; i++) {
+ kprintf("%x", buffer[i]);
+ }
+}
+
+void ahci_init(void) {
+ struct PCI_DEVICE device;
+ if (!pci_devices_by_id(0x01, 0x06, &device))
+ return;
+ kprintf("vendor: %x\n", device.vendor);
+ kprintf("device: %x\n", device.device);
+ kprintf("header_type: %x\n", device.header_type);
+
+ struct PCI_BaseAddressRegister bar;
+ pci_get_bar(&device, 5, &bar);
+
+ u8 *HBA_base = mmu_map_frames((void *)bar.address, bar.size);
+ volatile struct HBA_MEM *hba = (volatile struct HBA_MEM *)(HBA_base);
+ for (u8 i = 0; i < 32; i++) {
+ if (!((hba->pi >> i) & 1))
+ continue;
+ u32 type = check_type(&hba->ports[i]);
+ switch (type) {
+ case AHCI_DEV_SATA:
+ ahci_sata_setup(&hba->ports[i]);
+ ahci_test(&hba->ports[i]);
+ kprintf("SATA drive found at port %d\n", i);
+ break;
+ case AHCI_DEV_SATAPI:
+ kprintf("SATAPI drive found at port %d\n", i);
+ break;
+ case AHCI_DEV_SEMB:
+ kprintf("SEMB drive found at port %d\n", i);
+ break;
+ case AHCI_DEV_PM:
+ kprintf("PM drive found at port %d\n", i);
+ break;
+ default:
+ kprintf("No drive found at port %d\n", i);
+ break;
+ }
+ }
+}