ATA DMA Mode Troubles [Solved]

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
User avatar
piranha
Member
Member
Posts: 1391
Joined: Thu Dec 21, 2006 7:42 pm
Location: Unknown. Momentum is pretty certain, however.
Contact:

ATA DMA Mode Troubles [Solved]

Post by piranha »

Okay, I have been battling with my ATA driver for a couple of weeks. First I noticed that sometimes PIO doesn't seem to work properly (Occasionally if I read the same sector twice I get different data [one set of data is usually half correct and half zero]). So I decided that that would be a perfect time to start getting DMA mode to actually work. So, in my spare time I've been staring at specs, staring at my code, and staring at the buggy behavior. Currently I'm getting Drive Fault errors returned in the status byte of the ATA drive (inb 0x1F7 returns 0x50). I've been unable to locate why this is, I have looked through the wiki pages on ATA stuffs and I don't see it, etc, etc.

My code:

Code: Select all

typedef struct {
	unsigned addr;
	unsigned short size;
	unsigned short last;
}__attribute__((packed)) prdtable_t;

int ata_dma_init(struct ata_controller *cont, struct ata_device *dev, int rw, unsigned char *buffer)
{
	prdtable_t *t = (prdtable_t *)cont->prdt_virt;
	t->addr = cont->dma_buf_phys;
	t->size = 512;
	t->last = 0x8000;
	outl(cont->port_bmr_base + BMR_PRDT, (unsigned)cont->prdt_phys);
	if (rw == WRITE)
		memcpy(cont->dma_buf_virt, buffer, 512);
	return 1;
}

int ata_start_command(struct ata_controller *cont, struct ata_device *dev, unsigned long long addr, char rw)
{
	unsigned char cmd=0;
	if(rw == READ)
		cmd = 0x25;
	else
		cmd = 0x35;
	outb(cont->port_cmd_base+REG_DEVICE, 0x40 | (dev->id << 4));
	ATA_DELAY(cont);
	outb(cont->port_cmd_base+REG_SEC_CNT, 0x00);
	
	outb(cont->port_cmd_base+REG_LBA_LOW, (unsigned char)(addr >> 24));
	outb(cont->port_cmd_base+REG_LBA_MID, (unsigned char)(addr >> 32));
	outb(cont->port_cmd_base+REG_LBA_HIG, (unsigned char)(addr >> 40));
	
	outb(cont->port_cmd_base+REG_SEC_CNT, 0x01);
	outb(cont->port_cmd_base+REG_LBA_LOW, (unsigned char)addr);
	outb(cont->port_cmd_base+REG_LBA_MID, (unsigned char)(addr >> 8));
	outb(cont->port_cmd_base+REG_LBA_HIG, (unsigned char)(addr >> 16));
	
	outb(cont->port_cmd_base+REG_COMMAND, cmd);
	return 1;
}

int ata_dma_rw(struct ata_controller *cont, struct ata_device *dev, int rw, unsigned blk, char *buf)
{
	ata_start_command(cont, dev, blk, rw);
	ATA_DELAY(cont);
	
	inb(cont->port_bmr_base + BMR_STATUS);
	ata_dma_init(cont, dev, rw, (unsigned char *)buf);
	inb(cont->port_bmr_base + BMR_STATUS);
	
	uint8_t cmd = inb(cont->port_bmr_base + BMR_COMMAND);
	cmd = 0x1 | (rw == READ ? 8 : 0);
	outb(cont->port_bmr_base, cmd);
	
	unsigned char st;
	while(1) {
		st = inb(cont->port_bmr_base + BMR_STATUS);
		uint8_t sus = ata_reg_inb(cont, REG_STATUS);
		
		printk(1, "poll %x %x\n", sus, st);
		if(!(st & 0x4))
			continue;
		if(st & 0x2)
			panic("DMA err");
		if(sus & STATUS_ERR)
			panic("Error in reg_stat");
		if(!(sus & STATUS_BSY) && (sus & STATUS_DRQ))
			break;
	}
	
	st = inb(cont->port_bmr_base + BMR_COMMAND);
	outb(cont->port_bmr_base + BMR_COMMAND, st);
	st = inb(cont->port_bmr_base + BMR_STATUS);
	outb(cont->port_bmr_base + BMR_STATUS, st);
	
	uint8_t status = ata_reg_inb(cont, REG_STATUS);
	if (!(status & STATUS_ERR)) {
		if(st & 0x2)
			panic("B");
	} else
		panic("A");
	if (rw == READ)
		memcpy(buf, cont->dma_buf_virt, 512);
	return 512;
}
Yeah, I will convert stuff to uint16_t and the likes once I get it working.

In Qemu it just flat out fails every time. It gets into the polling loop and spits out 'poll 50 4' until the end of time. In bochs, it seems to sometimes work, at least the first time. Sometimes it will correctly read the first sector, but when it tries to read another sector it freezes, spitting out 'poll 58 0' occasionally interspersed with 'poll 5A 0'. Sometimes it has the same behavior, except that it reads the sector as all zeros. Most of the time its the first one though.

I'm sure that its something really simple, but I've been staring at the code for a long time with no results, so I figured I'd ask here.
Thanks,
-JL

EDIT: I Feel like its a timing issue, since I never had the problems with PIO mode until I switched to a different (slower) computer, but I could be wildly wrong about that.
Last edited by piranha on Thu Nov 10, 2011 1:14 pm, edited 1 time in total.
SeaOS: Adding VT-x, networking, and ARM support
dbittman on IRC, @danielbittman on twitter
https://dbittman.github.io
gerryg400
Member
Member
Posts: 1801
Joined: Thu Mar 25, 2010 11:26 pm
Location: Melbourne, Australia

Re: ATA DMA Mode Troubles

Post by gerryg400 »

Just had a quick look. Aren't you issuing the command before (in ata_start_command) setting up your DMA ?
If a trainstation is where trains stop, what is a workstation ?
User avatar
piranha
Member
Member
Posts: 1391
Joined: Thu Dec 21, 2006 7:42 pm
Location: Unknown. Momentum is pretty certain, however.
Contact:

Re: ATA DMA Mode Troubles

Post by piranha »

As far as I can tell, it makes no difference where I put the call the ata_start_command, be it at the beginning, right before polling, or right before issuing the DMA command to the BMR.

If I'm supposed to issue it after I set up DMA (which is how it was originally, I was just playing around with the code), I'll do that.

-JL
SeaOS: Adding VT-x, networking, and ARM support
dbittman on IRC, @danielbittman on twitter
https://dbittman.github.io
gerryg400
Member
Member
Posts: 1801
Joined: Thu Mar 25, 2010 11:26 pm
Location: Melbourne, Australia

Re: ATA DMA Mode Troubles

Post by gerryg400 »

I need to go now, but I'll have a closer look later. My code also waits after setting the register before issuing the read/write command. It looks a little like this

Code: Select all

    set up DMA
    wait for !ATA_BUSY
    setup control and lba regs
    wait for !ATA_BUSY && ATA_READY   <<<==== Add this 
    send read/write command
    
If a trainstation is where trains stop, what is a workstation ?
User avatar
piranha
Member
Member
Posts: 1391
Joined: Thu Dec 21, 2006 7:42 pm
Location: Unknown. Momentum is pretty certain, however.
Contact:

Re: ATA DMA Mode Troubles

Post by piranha »

Okay, I changed it so that in the main function its:

Code: Select all

	inb(cont->port_bmr_base + BMR_STATUS);
	ata_dma_init(cont, dev, rw, (unsigned char *)buf);
	inb(cont->port_bmr_base + BMR_STATUS);
	uint8_t cmdReg = inb(cont->port_bmr_base + BMR_COMMAND);
	cmdReg = 0x1 | (rw == READ ? 8 : 0);
	outb(cont->port_bmr_base, cmdReg);
	while(1)
	{
		int x = ata_reg_inb(cont, REG_STATUS);
		if(!(x & STATUS_BSY))
			break;
	}
	ata_start_command(cont, dev, blk, rw);
And in ata_start_command its:

Code: Select all

	outb(cont->port_cmd_base+REG_LBA_HIG, (unsigned char)(addr >> 16));
	
	while(1)
	{
		int x = ata_reg_inb(cont, REG_STATUS);
		if(!(x & STATUS_BSY) && (x & STATUS_DRDY))
			break;
	}
	outb(cont->port_cmd_base+REG_COMMAND, cmd);
Unfortunately, I get the exact same behavior.

-JL
SeaOS: Adding VT-x, networking, and ARM support
dbittman on IRC, @danielbittman on twitter
https://dbittman.github.io
User avatar
piranha
Member
Member
Posts: 1391
Joined: Thu Dec 21, 2006 7:42 pm
Location: Unknown. Momentum is pretty certain, however.
Contact:

Re: ATA DMA Mode Troubles

Post by piranha »

Okay, it would seem that I'm not supposed to poll for DRQ after starting the command for DMA transfers. At least, I assume thats right. It seems to be working now...

-JL
SeaOS: Adding VT-x, networking, and ARM support
dbittman on IRC, @danielbittman on twitter
https://dbittman.github.io
gerryg400
Member
Member
Posts: 1801
Joined: Thu Mar 25, 2010 11:26 pm
Location: Melbourne, Australia

Re: ATA DMA Mode Troubles

Post by gerryg400 »

Do you mind posting the working version ? Just the important part. TIA.
If a trainstation is where trains stop, what is a workstation ?
User avatar
piranha
Member
Member
Posts: 1391
Joined: Thu Dec 21, 2006 7:42 pm
Location: Unknown. Momentum is pretty certain, however.
Contact:

Re: ATA DMA Mode Troubles

Post by piranha »

Here is the working version of the code. At least, it appears to work, though it wouldn't surprise me if there were still issue with it.

Code: Select all

typedef struct {
	unsigned addr;
	unsigned short size;
	unsigned short last;
}__attribute__((packed)) prdtable_t;

int ata_dma_init(struct ata_controller *cont, struct ata_device *dev, int size, int rw, unsigned char *buffer)
{
	prdtable_t *t = (prdtable_t *)cont->prdt_virt;
	t->addr = cont->dma_buf_phys;
	t->size = size;
	t->last = 0x8000;
	outl(cont->port_bmr_base + BMR_PRDT, (unsigned)cont->prdt_phys);
	if (rw == WRITE)
		memcpy(cont->dma_buf_virt, buffer, size);
	unsigned char st = inb(cont->port_bmr_base + BMR_STATUS);
	st |= BMR_STATUS_ERROR | BMR_STATUS_IRQ;
	outb(cont->port_bmr_base + BMR_STATUS, st);
	return 1;
}

int ata_start_command(struct ata_controller *cont, struct ata_device *dev, unsigned block, char rw)
{
	unsigned long long addr = block;
	unsigned char cmd=0;
	if(rw == READ)
		cmd = 0x25;
	else
		cmd = 0x35;
	if(cont->selected != dev) {
		outb(cont->port_cmd_base+REG_DEVICE, 0x40 | (dev->id << 4));
		cont->selected = dev;
		ATA_DELAY(cont);
	}
	outb(cont->port_cmd_base+REG_SEC_CNT, 0x00);
	outb(cont->port_cmd_base+REG_LBA_LOW, (unsigned char)(addr >> 24));
	outb(cont->port_cmd_base+REG_LBA_MID, (unsigned char)(addr >> 32));
	outb(cont->port_cmd_base+REG_LBA_HIG, (unsigned char)(addr >> 40));
	outb(cont->port_cmd_base+REG_SEC_CNT, 0x01);
	outb(cont->port_cmd_base+REG_LBA_LOW, (unsigned char)addr);
	outb(cont->port_cmd_base+REG_LBA_MID, (unsigned char)(addr >> 8));
	outb(cont->port_cmd_base+REG_LBA_HIG, (unsigned char)(addr >> 16));
	
	while(1)
	{
		int x = ata_reg_inb(cont, REG_STATUS);
		if(!(x & STATUS_BSY) && (x & STATUS_DRDY))
			break;
	}
	outb(cont->port_cmd_base+REG_COMMAND, cmd);
	return 1;
}

int ata_dma_rw(struct ata_controller *cont, struct ata_device *dev, int rw, unsigned blk, char *buf)
{
	unsigned size=512;
	ata_dma_init(cont, dev, size, rw, (unsigned char *)buf);
	uint8_t cmdReg = inb(cont->port_bmr_base + BMR_COMMAND);
	cmdReg = (rw == READ ? 8 : 0);
	outb(cont->port_bmr_base, cmdReg);
	while(1)
	{
		int x = ata_reg_inb(cont, REG_STATUS);
		if(!(x & STATUS_BSY))
			break;
	}
	ata_start_command(cont, dev, blk, rw);
	ATA_DELAY(cont);
	cmdReg = inb(cont->port_bmr_base + BMR_COMMAND);
	cmdReg |= 0x1 | (rw == READ ? 8 : 0);
	outb(cont->port_bmr_base, cmdReg);
	unsigned char st;
	while(1) {
		st = inb(cont->port_bmr_base + BMR_STATUS);
		uint8_t sus = ata_reg_inb(cont, REG_STATUS);
		if(!(st & 0x4))
			continue;
		if(st & 0x2)
			panic("DMA err");
		if(sus & STATUS_ERR)
			panic("Error in reg_stat");
		if(!(sus & STATUS_BSY))
			break;
		
	}
	uint8_t status = ata_reg_inb(cont, REG_STATUS);
	if (!(status & STATUS_ERR)) {
		if(st & 0x2)
			panic("B");
	} else
	{
                panic("A");
	}
	if (rw == READ)
		memcpy(buf, cont->dma_buf_virt, 512);
	
	st = inb(cont->port_bmr_base + BMR_COMMAND);
	outb(cont->port_bmr_base + BMR_COMMAND, st & ~0x1);
	st = inb(cont->port_bmr_base + BMR_STATUS);
	outb(cont->port_bmr_base + BMR_STATUS, st | BMR_STATUS_ERROR | BMR_STATUS_IRQ);
	return 512;
}
Hope this helps someone!

-JL
SeaOS: Adding VT-x, networking, and ARM support
dbittman on IRC, @danielbittman on twitter
https://dbittman.github.io
Post Reply