AHCI/ATAPI Read/Write commands in DMA mode

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
tretornesp
Posts: 18
Joined: Mon Aug 15, 2022 12:30 pm

AHCI/ATAPI Read/Write commands in DMA mode

Post by tretornesp »

Hello everyone.

I'm facing some trouble trying to perform reads from a cdrom device (qemu emulated).

As i have read, it is possible to tell the fis to operate in ATAPI mode as explained in:

https://wiki.osdev.org/AHCI#AHCI_.26_ATAPI

The steps i'm following (starting from a working read ata function) are:

1. Changing the ATA DMA READ command (0x25) to ATA PACKET (0xA0).
2. Setting the ATAPI command (READ (0xA8)) in the command table.
3. Setting the ATAPI mode bit on the command header.
4. Changing the FIS type from REG_H2D (0x27) to PIO_SETUP(0x5f).

My code so far is:

Code: Select all

uint8_t read_atapi_port(uint8_t port_no, uint64_t sector, uint32_t sector_count) {
    struct ahci_port* port = &ahci_ports[port_no];

    uint32_t sector_low = (uint32_t)sector;
    uint32_t sector_high = (uint32_t)(sector >> 32);

    port->hba_port->interrupt_status = (uint32_t)-1;
    int spin = 0;
    int slot = (int)find_cmd_slot(port);
    if (slot == -1) {
        printf("No free command slots\n");
        return 0;
    }

    struct hba_command_header* command_header = (struct hba_command_header*)(uint64_t)(port->hba_port->command_list_base);
    command_header += slot;
    command_header->command_fis_length = sizeof(struct hba_command_fis) / sizeof(uint32_t);
    command_header->write = 0;
    command_header->atapi = 1;
    command_header->prdt_length = (uint16_t)((sector_count - 1) >> 4) + 1;

    struct hba_command_table* command_table = (struct hba_command_table*)(uint64_t)(command_header->command_table_base_address);
    memset(command_table, 0, sizeof(struct hba_command_table) + (command_header->prdt_length - 1) * sizeof(struct hba_prdt_entry));

    command_table->atapi_command[0] = ATAPI_READ_CMD;
    command_table->atapi_command[1] = 0;
    command_table->atapi_command[2] = (uint8_t)(sector >> 0x18);
    command_table->atapi_command[3] = (uint8_t)(sector >> 0x10);
    command_table->atapi_command[4] = (uint8_t)(sector >> 0x08);
    command_table->atapi_command[5] = (uint8_t)(sector >> 0x00);

    void* buffer = port->buffer;
    int i;
    for (i = 0; i < command_header->prdt_length - 1; i++) {
        command_table->prdt_entry[i].data_base_address = (uint32_t)(uint64_t)buffer;
        command_table->prdt_entry[i].data_base_address_upper = (uint32_t)((uint64_t)buffer >> 32);
        command_table->prdt_entry[i].byte_count = 8 * 1024 - 1;
        command_table->prdt_entry[i].interrupt_on_completion = 1;
        buffer = (void*)((uint64_t*)buffer+0x1000);
        sector_count -= 16;
    }

    command_table->prdt_entry[i].data_base_address = (uint32_t)(uint64_t)buffer;
    command_table->prdt_entry[i].data_base_address_upper = (uint32_t)((uint64_t)buffer >> 32);
    command_table->prdt_entry[i].byte_count = (sector_count << 9) - 1;
    command_table->prdt_entry[i].interrupt_on_completion = 1;

    struct hba_command_fis* command_fis = (struct hba_command_fis*)command_table->command_fis;

    command_fis->fis_type = FIS_TYPE_PIO_SETUP;
    command_fis->command_control = 1;
    command_fis->command = ATA_CMD_PACKET;

    command_fis->lba0 = (uint8_t)sector_low;
    command_fis->lba1 = (uint8_t)(sector_low >> 8);
    command_fis->lba2 = (uint8_t)(sector_low >> 16);

    command_fis->device_register = 1 << 6;

    command_fis->lba3 = (uint8_t)(sector_low >> 24);
    command_fis->lba4 = (uint8_t)(sector_high);
    command_fis->lba5 = (uint8_t)(sector_high >> 8);

    command_fis->count_low = sector_count & 0xFF;
    command_fis->count_high = (sector_count >> 8);

    while (port->hba_port->task_file_data & (ATA_DEV_BUSY | ATA_DEV_DRQ) && spin < 1000000) {
        spin++;
    };
    if (spin == 1000000) {
        printf("Port is hung\n");
        return 0;
    }

    port->hba_port->command_issue = (1 << slot);

    while(1) {
        if ((port->hba_port->command_issue & (1<<slot)) == 0) break;
        if (port->hba_port->interrupt_status & HBA_PxIS_TFES) {
            return 0;
        }
    }

    if (port->hba_port->interrupt_status & HBA_PxIS_TFES) {
        return 0;
    }

    return 1;
}
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by Octocontrabass »

tretornesp wrote:4. Changing the FIS type from REG_H2D (0x27) to PIO_SETUP(0x5f).
That's not right. You need a Register H2D FIS to tell the drive you're sending the PACKET command. The HBA will automatically handle the PIO Setup FIS.

You also need to change the values you write into the task file registers in your Register H2D FIS, since the PACKET command assigns different meanings to those registers.

The ATA Features register (which you're not setting) is the ATAPI Features register. In the ATAPI Features register, you may need to use bits 0 and 2. Bit 0 controls whether the data transfer is PIO or DMA, and some drives may require you to set bit 2 when performing device-to-host DMA. Use IDENTIFY PACKET DEVICE to check your drive's DMA capabilities.

The ATA LBA1 and LBA2 registers are the ATAPI Byte Count Low and Byte Count High registers. When using DMA transfers, these registers are ignored. When using PIO transfers, these registers indicate the number of bytes to transfer per DRQ block. AHCI does not support more than 0x2000 bytes per DRQ block, and may not support multiple DRQ blocks per command, so you should use DMA transfers if your drive supports them.

The rest of the task file registers are either unused or don't exist.
tretornesp
Posts: 18
Joined: Mon Aug 15, 2022 12:30 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by tretornesp »

After a few days trying to implement your suggestions I can't seem to get it right. I'm an absolute newbie on device driver development.

If you don't mind I would really appreciate if you could confirm if the following structure values apply all your suggestions or if i'm missing something:

Code: Select all

command_fis->fis_type = FIS_TYPE_REG_H2D; //0x27
command_fis->feature_low |= 1;
command_fis->feature_low |= (1 << 2); //Setting feature bits 0 and 2
command_fis->command_control = 1;
command_fis->command = ATA_CMD_PACKET; //0xA0
command_fis->device_register = 1 << 6;
command_fis->count_low = sector_count & 0xFF;
command_fis->count_high = (sector_count >> 8);
sector_count is an uint32 with the number of sectors to read
sector_high is an uint32 with the highest 32 bits of the sector number.
sector_low is an uint32 with the lowest 32 bits of the sector number.

The rest of the values are set to 0.

also my atapi command:

Code: Select all

command_table->atapi_command[0] = 0xA8;
command_table->atapi_command[1] = 0x00;
command_table->atapi_command[2] = (uint8_t)(sector_high >> 8);
command_table->atapi_command[3] = (uint8_t)sector_high;
command_table->atapi_command[4] = (uint8_t)(sector_low >> 24);
command_table->atapi_command[5] = (uint8_t)(sector_low >> 16);
command_table->atapi_command[6] = (uint8_t)(sector_low >> 8);
command_table->atapi_command[7] = (uint8_t)sector_low;
command_table->atapi_command[8] = 0x00;
command_table->atapi_command[9] = 0x00;
command_table->atapi_command[10] = 0x00;
command_table->atapi_command[11] = 0x00;
command_table->atapi_command[12] = 0x00;
command_table->atapi_command[13] = 0x00;
command_table->atapi_command[14] = 0x00;
command_table->atapi_command[15] = 0x00;
if this is correct, i'm guessing DMA may be unavailable... I guess it would be a good idea to spend the time writing support for the IDENTIFY PACKET DEVICE command.

Related structure (FIS_REG_H2D)

Code: Select all

struct hba_command_fis {
    uint8_t fis_type;

    uint8_t pm_port : 4;
    uint8_t reserved0 : 3;
    uint8_t command_control : 1;

    uint8_t command;
    uint8_t feature_low;

    uint8_t lba0;
    uint8_t lba1;
    uint8_t lba2;
    uint8_t device_register;

    uint8_t lba3;
    uint8_t lba4;
    uint8_t lba5;
    uint8_t feature_high;

    uint8_t count_low;
    uint8_t count_high;
    uint8_t icc;
    uint8_t control;

    uint8_t reserved1[4];
} __attribute__ ((packed));
Thank you so much for your time and knowledge!
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by Octocontrabass »

tretornesp wrote:If you don't mind I would really appreciate if you could confirm if the following structure values apply all your suggestions or if i'm missing something:
All of the ATA task file registers/bits I didn't list in my previous post are unused and should be zero.

Code: Select all

command_fis->feature_low |= 1;
command_fis->feature_low |= (1 << 2); //Setting feature bits 0 and 2
Unused bits of the features register should be zero. Drives that don't use bit 2 may reject commands with bit 2 set, but QEMU's emulated drive probably doesn't care.

Code: Select all

command_fis->device_register = 1 << 6;
ATAPI devices are supposed to ignore this bit, but I'd set the whole register to 0 just in case.

Code: Select all

command_fis->count_low = sector_count & 0xFF;
command_fis->count_high = (sector_count >> 8);
This register is unused in ATAPI and should be zero.

Code: Select all

command_table->atapi_command[2] = (uint8_t)(sector_high >> 8);
command_table->atapi_command[3] = (uint8_t)sector_high;
command_table->atapi_command[4] = (uint8_t)(sector_low >> 24);
command_table->atapi_command[5] = (uint8_t)(sector_low >> 16);
READ(12) uses 32-bit LBA. You should use only sector_low to fill these four bytes.

Code: Select all

command_table->atapi_command[6] = (uint8_t)(sector_low >> 8);
command_table->atapi_command[7] = (uint8_t)sector_low;
command_table->atapi_command[8] = 0x00;
command_table->atapi_command[9] = 0x00;
You should use sector_count to fill these four bytes.
tretornesp wrote:I guess it would be a good idea to spend the time writing support for the IDENTIFY PACKET DEVICE command.
You should be able to get QEMU to work without it.
tretornesp
Posts: 18
Joined: Mon Aug 15, 2022 12:30 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by tretornesp »

Quick update: After a lot of tinkering i'm now able to read the first n sectors of a CD with the following code:

Code: Select all

uint8_t read_atapi_port(uint8_t port_no, uint64_t sector, uint32_t sector_count) {
    printf("Atapi read issued (port %d, sector %d, sector_count %d)\n", port_no, sector, sector_count);

    struct ahci_port* port = &ahci_ports[port_no];
    void* buffer = port->buffer;

    port->hba_port->interrupt_status = (uint32_t)-1;
    int spin = 0;
    int slot = (int)find_cmd_slot(port);
    if (slot == -1) {
        printf("No free command slots\n");
        return 0;
    }

    struct hba_command_header* command_header = (struct hba_command_header*)(uint64_t)(port->hba_port->command_list_base);
    command_header += slot;

    command_header->command_fis_length = sizeof(struct hba_command_fis) / sizeof(uint32_t);
    command_header->write = 0;
    command_header->atapi = 1;
    command_header->prdt_length = 1;
    
    struct hba_command_table* command_table = (struct hba_command_table*)(uint64_t)(command_header->command_table_base_address);
    memset(command_table, 0, sizeof(struct hba_command_table) + (command_header->prdt_length - 1) * sizeof(struct hba_prdt_entry));

    command_table->prdt_entry[0].data_base_address = (uint32_t)(uint64_t)buffer;
    command_table->prdt_entry[0].data_base_address_upper = (uint32_t)((uint64_t)buffer >> 32);
    command_table->prdt_entry[0].byte_count = (sector_count << 9) - 1;
    command_table->prdt_entry[0].interrupt_on_completion = 1;

    struct hba_command_fis* command_fis = (struct hba_command_fis*)command_table->command_fis;
    memset(command_fis, 0, sizeof(struct hba_command_fis));

    command_fis->fis_type = FIS_TYPE_REG_H2D;
    command_fis->command_control = 1;
    command_fis->feature_low = 5;
    command_fis->command = ATA_CMD_PACKET;

    command_table->atapi_command[0] = ATAPI_READ_CMD;
    command_table->atapi_command[1] = 0;
    command_table->atapi_command[2] = (uint8_t)((sector >> 24)& 0xff);
    command_table->atapi_command[3] = (uint8_t)((sector >> 16)& 0xff);
    command_table->atapi_command[4] = (uint8_t)((sector >> 8)& 0xff);
    command_table->atapi_command[5] = (uint8_t)((sector >> 0)& 0xff);
    command_table->atapi_command[6] = 0;
    command_table->atapi_command[7] = 0;
    command_table->atapi_command[8] = 0;
    command_table->atapi_command[9] = (uint8_t)(sector_count & 0xff);
    command_table->atapi_command[10] = 0;
    command_table->atapi_command[11] = 0;
    command_table->atapi_command[12] = 0;
    command_table->atapi_command[13] = 0;
    command_table->atapi_command[14] = 0;
    command_table->atapi_command[15] = 0;

    while (port->hba_port->task_file_data & (ATA_DEV_BUSY | ATA_DEV_DRQ) && spin < 1000000) {
        spin++;
    };
    if (spin == 1000000) {
        printf("Port is hung\n");
        return 0;
    }

    port->hba_port->command_issue = (1 << slot);

    while(1) {
        if ((port->hba_port->command_issue & (1<<slot)) == 0) break;
        if (port->hba_port->interrupt_status & HBA_PxIS_TFES) {
            return 0;
        }
    }

    if (port->hba_port->interrupt_status & HBA_PxIS_TFES) {
        return 0;
    }

    return 1;
}
Sadly when i specify an starting LBA different than 0. ex:

Code: Select all

command_table->atapi_command[5] = 1;
The program returns a buffer full of zeros.

If anybody has an idea of what may be happening here, i'll appreciate the help. I will update this with fully working code once i find out what is going on.

Thank you so much and have a nice day!
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by Octocontrabass »

tretornesp wrote:Sadly when i specify an starting LBA different than 0. ex:

Code: Select all

command_table->atapi_command[5] = 1;
The program returns a buffer full of zeros.
That sounds normal. Most optical discs don't have any data in LBA 0 through 15. Did you try LBA 16?
tretornesp
Posts: 18
Joined: Mon Aug 15, 2022 12:30 pm

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by tretornesp »

LOL, you are right, it works. Thank you!
startrail
Posts: 12
Joined: Tue Jan 05, 2016 9:10 am

Re: AHCI/ATAPI Read/Write commands in DMA mode

Post by startrail »

A small suggestion, the byte_count for CDROM should be a multiple of 2048 as compared to 512 of HDDs.

So the part

Code: Select all

command_table->prdt_entry[0].byte_count = (sector_count << 9) - 1;
becomes

Code: Select all

#define CDROM_SECTOR_SIZE 2048
command_table->prdt_entry[0].byte_count = (sector_count * CDROM_SECTOR_SIZE) - 1;
I was facing this problem when running on VirtualBox 6.1 (Ubuntu 22.04 HostOS)

Found the solution in this thread viewtopic.php?f=1&t=33511 (User zity's 1st reply)
Post Reply