DMC

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
DMC
by on (#111336)
Hey guys, sorry if this has been covered, but I'm a little stumped on my DMC implementation and wanted to see if any of you guys have any insight. I'm trying to get the APU "just right", and seem unable to get my DMC to pass even the "basic operations" test, so obviously I'm misunderstanding something. Here's what I believe happens:

* The frequency is how many CPU cycles between DMC "ticks"
* Every time the DMC "ticks", if there are "bytes remaining" in the sample, then a DMA read might happen
* A DMA read happens if there are bytes remaining and the sample buffer is empty (AKA, every 8 ticks)
* After a read, bytes remaining is decremented, if it hits 0, we either STOP, IRQ, or LOOP.

Based on this, here's my current attempt at implementing the rules above, this function is called every CPU cycle:

Code:
void DMC::tick() {

   if(enabled()) {
      if(frequency_ != 0) {
         if(--frequency_ == 0) {         
            frequency_ = reload_frequency_;            

            if(bytes_remaining_ != 0) {
            
               // read the 8-bit sample
               if(bit_ == 0) {
                  // TODO: hijack the CPU for appropriate number of cycles
                  sample_buffer_ = nes::cpu.read(current_address_);
                  if(current_address_ == 0xffff) {
                     current_address_ = 0x8000;
                  } else {
                     ++current_address_;               
                  }
               }
               
               // TODO: grab appropriate bit from sample_buffer and update
               bit_ = (bit_ + 1) % 8;
               
            
               if(--bytes_remaining_ == 0) {
                  if(loop_) {
                     bytes_remaining_ = sample_length_;
                     current_address_ = sample_address_;
                  } else if(irq_enabled_) {
                     nes::cpu.irq(CPU::APU_IRQ);
                     nes::apu.status_ |= APU::STATUS_DMC_IRQ;
                  }
               }
            }
         }
      }
   }
}


Any thoughts on where I am going wrong?
Re: DMC
by on (#111339)
Could you describe what basic operations it's failing? What do you expect it to do when you don't even make use of your sample_buffer at all?
Re: DMC
by on (#111342)
I am trying to run it against blargg's "7-dmc_basics.nes". It was failing the first test with the message:
Quote:
DMC isn't working well enough to test further
.

However, I added some tracing and it seems that the test was expecting things to happen exactly 8 times slower than I was doing it. So i made one change, when the frequency is set, I multiply it by 8.

Now it passes some of the tests in that ROM (but not all).

Why do I need to multiply it by 8? I thought the frequency was the number of CPU cycles between DMC "ticks" (what is the right word there, "clocks"?). When the DMC's timer decides it's time to fire, does it not reset until all 8 bits of the sample byte have been loaded?
Re: DMC
by on (#111344)
I presume you're looking up the DPCM frequency from the appropriate table: Wiki

Yes, the DPCM unit should tick once every n CPU cycles, where n is a value looked up from that table. During this tick it should consume one bit of data from the sample buffer. If the sample buffer is empty it will need to reload a new byte.

Multiplying your frequency by 8 does not sound like the correct thing to do. It's probably because you're decrementing bytes_remaining_ every time, even though you're only fetching a new sample when bit_ is 0.
Re: DMC
by on (#111346)
Ah, good call, that was precisely it. I am using the frequency table of course and things are closer to passing the test suite now :-).

For reference, here is the new code which doesn't depend on me multiplying the frequency from the table by 8:

Code:
void DMC::tick() {
   if(enabled()) {
      if(frequency_ != 0) {
         if(--frequency_ == 0) {         
            frequency_ = reload_frequency_;            

            if(bytes_remaining_ != 0) {
            
               // read the 8-bit sample
               if(bit_ == 0) {               
                  // TODO: hijack the CPU for appropriate number of cycles, not hardcoded to 4
                  nes::cpu.burn(4);
                  sample_buffer_ = nes::cpu.read(current_address_);
                  if(current_address_ == 0xffff) {
                     current_address_ = 0x8000;
                  } else {
                     ++current_address_;               
                  }
               }
               
               // TODO: grab appropriate bit from sample_buffer and update
               
               bit_ = (bit_ + 1) % 8;
               if(bit_ == 0) {
                  if(--bytes_remaining_ == 0) {
                     if(loop_) {
                        bytes_remaining_ = sample_length_;
                        current_address_ = sample_address_;
                     } else if(irq_enabled_) {
                        nes::cpu.irq(CPU::APU_IRQ);
                        nes::apu.status_ |= APU::STATUS_DMC_IRQ;
                     }
                  }
               }
            }
         }
      }
   }
}


Now it passes all but the last of the DMC basics test
Quote:
#19 - There should be a one-byte buffer that's filled immediately if empty
. I am honestly not sure what I am doing wrong as far as that last test.
Re: DMC
by on (#111347)
Could you link to the source code for the test you're using?
Re: DMC
by on (#111348)
sure thing, i'm gonna include the whole test suite for context. The test in particular's source code is:

apu_test/source/7-dmc_basics.s

Thanks for taking a look.
Re: DMC
by on (#111369)
The test is seeing if $4015 reports that the DPCM unit has read the last byte of the sample with correct timing.

The DPCM flag in $4015 (bit 4) clears immediately when the last byte of the sample is read. It will still play all the bits of the sample (so it will be affecting sound for 7 more ticks, as it empties the bits from its 1-byte buffer), but the flag in $4015 is based on bytes read, not bits.

So, what this test does is first empties the DPCM unit (i.e. turns it off, and delays long enough for the bits to finish), then it triggers a 1-byte sample to play, expecting bit 4 of $4015 to immediately clear, since that one byte will be read on the first tick, and it's the last byte of the sample.

You should also note that setting bit 4 of $4015 doesn't discard any bits remaining in the sample buffer. If another sample still had bits in there, they will finish playing before it loads the first byte of the new sample. I don't think there is a test for this in blargg's tests here, but it's still something to be aware of.
Re: DMC
by on (#111382)
This test was failing me because I was doing things in the wrong order on writes to $4015, had nothing to do with my DMC operation at all.

Code:
        private void Poke4015(uint address, byte data)
        {
            cpu.Interrupt(dmc.IrqPending = false);

            sq1.Enabled = (data & 0x01) != 0;
            sq2.Enabled = (data & 0x02) != 0;
            tri.Enabled = (data & 0x04) != 0;
            noi.Enabled = (data & 0x08) != 0;
            dmc.Enabled = (data & 0x10) != 0; // this property will set IRQ on blargg's test, so it must be AFTER clearing the DMC IRQ flag.
        }
Re: DMC
by on (#111408)
Awesome info guys. With your help, I was able to get my DMC code to pass the basic operations tests :-). Turns out my issue was two fold:

1. As the test states, enabling the DMC should process a 1 byte sample immediately if the sample buffer is empty.
2. (This was the tricky part). I was only draining the sample buffer if there was at least one byte remaining! Once I figured that out, the rest was easy :-).

For reference of anyone who reads this later, the final (I believe correct code) ended up looking like this:

Code:
        // timer_.tick() will return true every N ticks where N is the frequency of the channel
   if(enabled() && timer_.tick()) {
      if(bytes_remaining_ != 0) {
      
         // read the 8-bit sample
         if(sample_shift_counter_ == 0) {
            sample_shift_counter_ = 8;

            // TODO: hijack the CPU for appropriate number of cycles,
            //       not hardcoded to 4
            nes::cpu.burn(4);
            sample_buffer_ = nes::cpu.read(sample_pointer_);

            sample_pointer_ = ((sample_pointer_ + 1) & 0xffff) | 0x8000;

            if(--bytes_remaining_ == 0) {
               if(loop_) {
                  bytes_remaining_ = sample_length_;
                  sample_pointer_ = sample_address_;
               } else if(irq_enabled_) {
                  nes::cpu.irq(CPU::APU_IRQ);
                  nes::apu.status_ |= APU::STATUS_DMC_IRQ;
               }
            }
         }
      }
      
      if(sample_shift_counter_ != 0) {
         // TODO: process a bit from the sample
         --sample_shift_counter_;
      }
   }


Now I have to figure out the next DMC test ;-).

Thanks!
Re: DMC
by on (#111410)
Haven't followed the topic closely, but it would be nice to have the details of this stuff on the wiki if it isn't there already. (Feel free to disregard this as a misplaced semi-rant if it's already there. :))

Seems some info is hidden away on the forums or in test roms, which imo isn't ideal (lots of "rediscovery" needed by newcomers).
Re: DMC
by on (#111412)
Well, if you think something from this thread is missing from the wiki, tell me what it is and I'll add it, or you can add it if you have a wiki account.
Re: DMC
by on (#111413)
I'm pretty sure I put all this in my APU doc and on the Wiki.
Re: DMC
by on (#111414)
Ok, no worries then. :)