Page 1 of 1

PCI IDE bus mastering DMA question

Posted: Mon Feb 15, 2016 12:40 pm
by yr
I've implemented a PCI IDE driver that supports bus mastering DMA, following the specification here. The instructions on page 99 are quite clear and allowed me to get things up and running under Bochs fairly quickly.

However, I noticed something odd yesterday: adding a line of trace logging between submitting the READ DMA command to the disk and engaging the bus master function on the PCI controller caused things to not work (the drive would never signal an interrupt). Without the logging, things worked perfectly. After some investigation I've determined that it has nothing to do with logging; adding a simple delay between submitting the READ DMA command and engaging bus mastering causes things to not work.

Now the above linked spec is quite clear that the sequence of actions should be submitting the READ DMA command first, and then engaging bus mastering. However, this seems to be problematic under Bochs. So I added a bit of additional logging to the Bochs source (in iodev/harddrv.cc and iodev/pci_ide.cc) to track what's going on. Here's an extract from the Bochs log showing a successful DMA transfer:

Code: Select all

00065821496d[PIDE  ] BM-DMA read command register, channel 0, value = 0x08
00065821498d[PIDE  ] BM-DMA write command register, channel 0, value = 0x09
00065821571d[HD    ] READ DMA command for channel 0, device 0
00065831498d[PIDE  ] READ DMA to addr=0x001cd000, size=0x00000200
00065831498d[HD    ] raising interrupt 14 {DISK}
And here's an extract from when the transfer hangs:

Code: Select all

00068106692d[HD    ] READ DMA command for channel 0, device 0
00068106897d[PIDE  ] BM-DMA read command register, channel 0, value = 0x08
00068106899d[PIDE  ] BM-DMA write command register, channel 0, value = 0x09
00068116899d[PIDE  ] Data is not ready for channel 0 READ DMA
Note that in the successful case, the actions sequence shows up as re-ordered so that the bus mastering is engaged before the device command--despite the fact that my code is actually submitting the commands in the opposite order (following the spec).

In the problematic case, the READ DMA and bus mastering commands appear in the intended order. And that doesn't seem to work. I've also checked that re-ordering the commands in my driver source allows things to work. However, I don't want to do that since the intended ordering is stated quite clearly in the spec.

So I'm wondering: is this a bug in Bochs? Or am I missing something? Would appreciate any thoughts/suggestions. Also happy to share more details from the code if it would be helpful.

Re: PCI IDE bus mastering DMA question

Posted: Mon Feb 15, 2016 2:42 pm
by yr
Digging around the Bochs source a bit more, it does seem to be implemented under the assumption that the device transfer command runs after the bus mastering command. So the question is whether this behavior is correct?

Here's the code that handles the device READ DMA command (from iodev/harddrv.cc):

Code: Select all

void bx_hard_drive_c::seek_timer()
{
  Bit8u param = bx_pc_system.triggeredTimerParam();
  Bit8u channel = param >> 1;
  Bit8u device = param & 1;
  controller_t *controller = &BX_CONTROLLER(channel, device);
  if (BX_DRIVE_IS_HD(channel, device)) {
    switch (controller->current_command) {
...
      case 0x25: // READ DMA EXT
      case 0xC8: // READ DMA
        BX_DEBUG( ( "READ DMA command for channel %d, device %d", channel, device ) );
        controller->error_register = 0;
        controller->status.busy  = 0;
        controller->status.drive_ready = 1;
        controller->status.seek_complete = 1;
        controller->status.drq   = 1;
        controller->status.corrected_data = 0;
        DEV_ide_bmdma_start_transfer(channel);  // <=== ***** SETS FLAG *****
        break;
...
The bmdma_start_transfer function called above just sets a flag on the PCI IDE controller to indicate the drive is ready to transfer data:

Code: Select all

void bx_pci_ide_c::bmdma_start_transfer(Bit8u channel)
{
  if (channel < 2) {
    BX_PIDE_THIS s.bmdma[channel].data_ready = 1;
  }
}
However, writing the PCI bus mastering command register then clears the flag (from iodev/pci_ide.cc):

Code: Select all

void bx_pci_ide_c::write(Bit32u address, Bit32u value, unsigned io_len)
{
  Bit8u offset, channel;

  offset = address - BX_PIDE_THIS pci_base_address[4];
  channel = (offset >> 3);
  offset &= 0x07;
  switch (offset) {
    case 0x00:
      BX_DEBUG(("BM-DMA write command register, channel %d, value = 0x%02x", channel, value));
      BX_PIDE_THIS s.bmdma[channel].cmd_rwcon = (value >> 3) & 1;
      if ((value & 0x01) && !BX_PIDE_THIS s.bmdma[channel].cmd_ssbm) {
        BX_PIDE_THIS s.bmdma[channel].cmd_ssbm = 1;
        BX_PIDE_THIS s.bmdma[channel].status |= 0x01;
        BX_PIDE_THIS s.bmdma[channel].prd_current = BX_PIDE_THIS s.bmdma[channel].dtpr;
        BX_PIDE_THIS s.bmdma[channel].buffer_top = BX_PIDE_THIS s.bmdma[channel].buffer;
        BX_PIDE_THIS s.bmdma[channel].buffer_idx = BX_PIDE_THIS s.bmdma[channel].buffer;
        BX_PIDE_THIS s.bmdma[channel].data_ready = 0;  // <=== ***** CLEARS FLAG *****
...
Now Bochs is usually pretty accurate, but the directions in the spec also seem pretty clear (note ordering of steps 3 & 4)...
To initiate a bus master transfer between memory and an IDE DMA slave device, the following steps are required:

1. Software prepares a PRD Table in main memory. Each PRD is 8 bytes long and consists of an address pointer to the starting address and the transfer count of the memory buffer to be transferred. In any given PRD Table, two consecutive PRDs are offset by 8-bytes and are aligned on a 4-byte boundary.

2. Software provides the starting address of the PRD Table by loading the PRD Table Pointer Register . The direction of the data transfer is specified by setting the Read/Write Control bit. Clear the Interrupt bit and Error bit in the Status register.

3. Software issues the appropriate DMA transfer command to the disk device.

4. Engage the bus master function by writing a 1 to the Start bit in the Bus Master IDE Command Register for the appropriate channel. The first entry in the PRD table is fetched by the PIIX. The channel remains masked until the first descriptor is loaded.

5. The controller transfers data to/from memory responding to DMA requests from the IDE device.

6. At the end of the transfer, the IDE device signals an interrupt.

7. In response to the interrupt, software resets the Start/Stop bit in the command register. It then reads the controller status and then the drive status to determine if the transfer completed successfully.
Any thoughts?

Re: PCI IDE bus mastering DMA question

Posted: Mon Feb 15, 2016 4:01 pm
by gerryg400
How did adding a delay cause it to fail. Iow, why did it work before the delay was added ?

Re: PCI IDE bus mastering DMA question

Posted: Mon Feb 15, 2016 4:54 pm
by yr
I think it's because Bochs emulates drive seek time. Basically, my code does things in the following order:
  • Send READ DMA command to drive,
  • Engage bus master function.
If there's no delay between the two steps, the Bochs log shows events appearing in the opposite order (for successful transfers), which makes sense if the first command is delayed by the seek time.

Adding a delay longer than the seek time between the two steps makes the ordering "stronger" (as it ensures that the drive processes its command first). And since the Bochs implementation seems to be written for the opposite ordering, we get a failure.

Here's the Bochs code from iodev/harddrv.cc that handles the seek time emulation (I added the logging):

Code: Select all

void bx_hard_drive_c::start_seek(Bit8u channel)
{
  Bit64s new_pos, prev_pos, max_pos;
  Bit32u seek_time;
  double fSeekBase, fSeekTime;

  if (BX_SELECTED_IS_CD(channel)) {
    max_pos = BX_SELECTED_DRIVE(channel).cdrom.max_lba;
    prev_pos = BX_SELECTED_DRIVE(channel).cdrom.curr_lba;
    new_pos = BX_SELECTED_DRIVE(channel).cdrom.next_lba;
    fSeekBase = 80000.0;
  } else {
    max_pos = (BX_SELECTED_DRIVE(channel).hdimage->hd_size / 512) - 1;
    prev_pos = BX_SELECTED_DRIVE(channel).curr_lsector;
    new_pos = BX_SELECTED_DRIVE(channel).next_lsector;
    fSeekBase = 5000.0;
  }
  fSeekTime = fSeekBase * (double)abs((int)(new_pos - prev_pos + 1)) / (max_pos + 1);
  seek_time = (fSeekTime > 10.0) ? (Bit32u)fSeekTime : 10;
  BX_DEBUG(("Seek time is %d", seek_time));
  bx_pc_system.activate_timer(
    BX_SELECTED_DRIVE(channel).seek_timer_index, seek_time, 0);
}
And here's a sample output from the log showing a successful transfer. Note that the bus mastering command gets processed first (even though it was submitted second) while we're waiting for the seek to complete.

Code: Select all

00143641705d[PIDE  ] BM-DMA read DTP register, channel 0, value = 0x1cb000
00143792506d[PIDE  ] BM-DMA read command register, channel 0, value = 0x08
00143792508d[PIDE  ] BM-DMA write command register, channel 0, value = 0x08
00143792509d[PIDE  ] BM-DMA read command register, channel 0, value = 0x08
00143935506d[PIDE  ] BM-DMA read status register, channel 0, value = 0x04
00143935508d[PIDE  ] BM-DMA write status register, channel 0, value = 0x06
00143935509d[PIDE  ] BM-DMA read status register, channel 0, value = 0x00
00144239695d[HD    ] 8-bit write to 01f6 = e0 {DISK}
00144239698d[HD    ] 8-bit write to 01f2 = 01 {DISK}
00144239698d[HD    ] sector count = 1 {DISK}
00144239701d[HD    ] 8-bit write to 01f3 = 43 {DISK}
00144239701d[HD    ] sector number = 67 {DISK}
00144239705d[HD    ] 8-bit write to 01f4 = 00 {DISK}
00144239705d[HD    ] cylinder low = 00h {DISK}
00144239709d[HD    ] 8-bit write to 01f5 = 00 {DISK}
00144239709d[HD    ] cylinder high = 00h {DISK}
00144239712d[HD    ] 8-bit write to 01f7 = c8 {DISK}
00144239712d[HD    ] Seek time is 10           <========= SEEK TIME
00144239716d[PIDE  ] BM-DMA read command register, channel 0, value = 0x08
00144239718d[PIDE  ] BM-DMA write command register, channel 0, value = 0x09  <====== BM COMMAND
00144239718d[PIDE  ] Clearing the data ready flag for channel 0
00144239812d[HD    ] READ DMA command for channel 0, device 0  <====== DRIVE COMMAND
00144249718d[PIDE  ] READ DMA to addr=0x001cd000, size=0x00000200
00144249718d[HD    ] raising interrupt 14 {DISK}

Re: PCI IDE bus mastering DMA question

Posted: Mon Feb 15, 2016 6:13 pm
by gerryg400
It does look like a bug. One of the maintainers is a forum member and perhaps he will comment.

Re: PCI IDE bus mastering DMA question

Posted: Wed Feb 17, 2016 11:24 pm
by yr
Thanks for the sanity check. Was wondering if I was missing something obvious. Guess I'll hope the Bochs maintainer sees this post and comments...