Interrupt driven DMA PWM audio
Posted: Wed Sep 19, 2012 9:16 pm
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.
Here's our example binary for folks to try. It 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
to this
Now that you can play the example, UP/DOWN immediately goes to the previous/next songs, RIGHT/LEFT changes the volume up/down (it starts at max volume), and START pauses/resumes.
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:
Make sure you're using the slave table, not the master. Now you need the code for that exception.
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.
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?
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?
I'll be releasing the full code for the example, along with my XM player, later in the week... maybe the week end.
Here's our example binary for folks to try. It 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: Select all
if (PWM_Mode & 0x0080)
{
// RPT => generate DREQ1 as well as INT
SH2_DMA1_Request(&M_SH2, 1);
SH2_DMA1_Request(&S_SH2, 1);
}
Code: Select all
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: Select all
.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 */
Code: Select all
!-----------------------------------------------------------------------
! 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
Code: Select all
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;
}
}
So how about that exception function?
Code: Select all
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
}
See how easy that is?
I'll be releasing the full code for the example, along with my XM player, later in the week... maybe the week end.