Interrupt-Driven DMA PWM on the 32X

Chilly Willy

Established Member
This is how you do interrupt driven DMA PWM audio on the 32X. The first thing to ask is, why? If you wish to do more than just audio on the slave sh2, you need to make your audio code interrupt driven. That way when you do something that takes a long time, the audio will interrupt the task as needed to generate audio. Otherwise you are stuck trying to break the tasks into small enough pieces that it doesn't interfere with your polled audio. With DMA'd buffers, that is a decent amount of time, but not enough for some things.

This method works on real hardware and on Fusion 3.64 (remember that 3.63 sounds like crap). If you use Gens/GS release 7 with my DMA PWM modifications, you need to make one more change to pwm.c to use int-driven dma pwm:

Change this

Code:
        if (PWM_Mode & 0x0080)

        {

            // RPT => generate DREQ1 as well as INT

            SH2_DMA1_Request(&M_SH2, 1);

            SH2_DMA1_Request(&S_SH2, 1);

        }

to this

Code:
        if (PWM_Mode & 0x0080)

        {

            // RPT => generate DREQ1 as well as INT

            SH2_DMA1_Request(&M_SH2, 1);

            SH2_DMA1_Request(&S_SH2, 1);

            if ((SH2_Read_Long(&S_SH2, 0xFFFFFF9C) & 7) == 7)

                SH2_Interrupt_Internal(&S_SH2, (SH2_Read_Long(&S_SH2, 0xFFFFFFA8)<<8) | ((SH2_Read_Word(&S_SH2, 0xFFFFFEE2) >> 8) & 0x000F));

        }

So how do we do interrupt driven dma pwm audio? First, assign the dma an exception entry in the exception table... I suggest the exception right after the autovectors since it's the first free entry not used by anything else, and easy to find. So your table looks like this at the end:

Code:
        .long   slave_pwm       /* PWM interupt (Level 6 & 7) */

        .long   slave_cmd       /* Command interupt (Level 8 & 9) */

        .long   slave_hbi       /* H Blank interupt (Level 10 & 11 */

        .long   slave_vbi       /* V Blank interupt (Level 12 & 13) */

        .long   slave_rst       /* Reset Button (Level 14 & 15) */

        .long   slave_dma1      /* DMA1 TE INT */

Make sure you're using the slave table, not the master. Now you need the code for that exception.

Code:
!-----------------------------------------------------------------------

! Slave DMA 1 TE INT handler

!-----------------------------------------------------------------------

slave_dma1:

        ! save registers

        sts.l   pr,@-r15

        mov.l   r0,@-r15

        mov.l   r1,@-r15

        mov.l   r2,@-r15

        mov.l   r3,@-r15

        mov.l   r4,@-r15

        mov.l   r5,@-r15

        mov.l   r6,@-r15

        mov.l   r7,@-r15

        mov.l   sd1_handler,r0

        jsr     @r0

        nop

        ! restore registers

        mov.l   @r15+,r7

        mov.l   @r15+,r6

        mov.l   @r15+,r5

        mov.l   @r15+,r4

        mov.l   @r15+,r3

        mov.l   @r15+,r2

        mov.l   @r15+,r1

        mov.l   @r15+,r0

        lds.l   @r15+,pr

        rte

        nop

        .align  2

sd1_handler:

        .long   _slave_dma1_handler

We push the registers that aren't saved by C, then call the C function, slave_dma1_handler(). Before I talk about that function, we need to see how to set up the dma in the slave code.

Code:
void slave(void)

{

    uint16_t sample, ix;

    // init DMA

    SH2_DMA_SAR0 = 0;

    SH2_DMA_DAR0 = 0;

    SH2_DMA_TCR0 = 0;

    SH2_DMA_CHCR0 = 0;

    SH2_DMA_DRCR0 = 0;

    SH2_DMA_SAR1 = 0;

    SH2_DMA_DAR1 = 0x20004034; // storing a long here will set left and right

    SH2_DMA_TCR1 = 0;

    SH2_DMA_CHCR1 = 0;

    SH2_DMA_DRCR1 = 0;

    SH2_DMA_DMAOR = 1; // enable DMA

    SH2_DMA_VCR1 = 72; // set exception vector for DMA channel 1

    SH2_INT_IPRA = (SH2_INT_IPRA & 0xF0FF) | 0x0F00; // set DMA INT to priority 15

    // init the sound hardware

    MARS_PWM_MONO = 1;

    MARS_PWM_MONO = 1;

    MARS_PWM_MONO = 1;

    if (MARS_VDP_DISPMODE & MARS_NTSC_FORMAT)

        MARS_PWM_CYCLE = (((23011361 << 1)/SAMPLE_RATE + 1) >> 1) + 1; // for NTSC clock

    else

        MARS_PWM_CYCLE = (((22801467 << 1)/SAMPLE_RATE + 1) >> 1) + 1; // for PAL clock

    MARS_PWM_CTRL = 0x0185; // TM = 1, RTP, RMD = right, LMD = left

    sample = SAMPLE_MIN;

    /* ramp up to SAMPLE_CENTER to avoid click in audio (real 32X) */

    while (sample < SAMPLE_CENTER)

    {

        for (ix=0; ix<(SAMPLE_RATE*2)/(SAMPLE_CENTER - SAMPLE_MIN); ix++)

        {

            while (MARS_PWM_MONO & 0x8000) ; // wait while full

            MARS_PWM_MONO = sample;

        }

        sample++;

    }

    // initialize mixer

    MARS_SYS_COMM6 = MIXER_UNLOCKED; // sound subsystem running

    fill_buffer(&snd_buffer[0]); // fill first buffer

    slave_dma1_handler(); // start DMA

    SetSH2SR(2);

    while (1)

    {

        if (MARS_SYS_COMM4 == SSH2_WAITING)

            continue; // wait for command

        // do command in COMM4

        // done

        MARS_SYS_COMM4 = SSH2_WAITING;

    }

}

Notice that the VCR for DMA1 is set to 72. That's that exception vector we added to the slave table. Notice that IPRA bits 12 to 8 are set to 0xF (15). That's the exception priority. Tailor that to your needs knowing that CMD is 8, HBlank is 10, VBlank is 12, and the reset button is 14. Other than that, we do a "standard" set up of the audio. We fill the first buffer and call the handler to start off the dma. After that, the transfer-end dma interrupt will continue the process. We then fall in a loop where we look at COMM4 for a command from the Master SH2 or 68K. That's where you stick any tasks that take a long time for the slave to do. Those tasks may/will be interrupted by the dma interrupt as needed to fill buffers and start the next dma operation.

So how about that exception function?

Code:
void slave_dma1_handler(void)

{

    static int32_t which = 0;

    while (MARS_SYS_COMM6 == MIXER_LOCK_MSH2) ; // locked by MSH2

    SH2_DMA_CHCR1; // read TE

    SH2_DMA_CHCR1 = 0; // clear TE

    if (which)

    {

        // start DMA on first buffer and fill second

        SH2_DMA_SAR1 = ((uint32_t)&snd_buffer[0]) | 0x20000000;

        SH2_DMA_TCR1 = num_samples; // number longs

        SH2_DMA_CHCR1 = 0x18E5; // dest fixed, src incr, size long, ext req, dack mem to dev, dack hi, dack edge, dreq rising edge, cycle-steal, dual addr, intr enabled, clear TE, dma enabled

        fill_buffer(&snd_buffer[MAX_NUM_SAMPLES * 2]);

    }

    else

    {

        // start DMA on second buffer and fill first

        SH2_DMA_SAR1 = ((uint32_t)&snd_buffer[MAX_NUM_SAMPLES * 2]) | 0x20000000;

        SH2_DMA_TCR1 = num_samples; // number longs

        SH2_DMA_CHCR1 = 0x18E5; // dest fixed, src incr, size long, ext req, dack mem to dev, dack hi, dack edge, dreq rising edge, cycle-steal, dual addr, intr enabled, clear TE, dma enabled

        fill_buffer(&snd_buffer[0]);

    }

    which ^= 1; // flip audio buffer

}

Note that read TE/clear TE set of lines - those are REQUIRED for dma interrupts to occur properly. Took a while to figure that out. Then it's merely a matter of starting the next DMA on the proper buffer, then calling fill_buffer on the other buffer. We're then done until the next interrupt.

See how easy that is?
laugh.gif


Here is the full source code. There is an arc with my current linker scripts for the compiler, an arc with the xm player library and converter tool, and an arc with the 32X code:

ldscripts-32X.7z

libxmp-v1.1.7z

xmplayer-v1.1

The resultant binaries:

XMP-Doom-Wolf3D.zip

UP/DOWN immediately goes to the previous/next songs, LEFT/RIGHT changes the volume down/up (it starts at max volume), and START pauses/resumes.

If you have any questions, be sure to ask. Note, this could TOTALLY be applied to the Saturn. I intend to make an XM player driver for the Saturn here sometime this fall.
 
Know this post is a little dated but I gotta say: Chilly Willy is the man! Seriously! This stuff is geat. If it wasn't for people like you we'd have bad programs; thank you for all your C examples and talk on the forums (really enjoy reading your discussions with kool kitty). It's really great to see all the work you've done on the 32X and, seeing that someone gets good use out of a dated system is really great; the 32X wasn't put to good use by gamers but it's definately a prime target for programmers. Cheers!

BTW, how is that OGG player coming along? That's suprising you got the 32X to play 22khz 30kbps mono ogg on only the slave cpu (it's on a 16-bit bus=WTH?!).

Regards
 
Back
Top