Close

Of ISRs and DMA

A project log for Synthesizer LFO / Function Generator

Building an arbitrary waveform generator to serve as an LFO in a Eurorack synthesizer

steven-gannSteven Gann 08/26/2022 at 05:350 Comments

So what do you do when your main loop gets in the way of running a specific function fast and regularly? You turn that function into an ISR triggered by a timer interrupt, of course!

Interrupting 

I moved the LUT code into the TMR0 ISR and set that timer to go about as fast as possible. This didn't quite work because it seems the interrupt was tripping again before the ISR actually finished. No problem, I'll just pause the timer and disable interrupts at the start of the ISR and put them back at the end. Timekeeping is a little off but at least it is regular.

But... now the logic in the main loop doesn't seem to be running, or at least it is running very, very slowly. Why? Well when the ISR is running the main loop is paused and resumes when the ISR finishes, but this ISR is taking long enough that it usually triggers again almost immediately after returning. The main loop includes a NOP-based delay loop for simple button debouncing, so a loop that previously took 2ms to iterate now takes several minutes. I could slow down the TMR0 period so the ISR triggers less often, but if I slow it down enough to make a real difference my DAC update rate is WAY too slow. So maybe I can make the ISR faster? Why is it slow? Within the ISR I check a "mode" variable with an IF statement, then copy the value from the appropriate LUT array into the DAC's output register. And there's where it hangs. When I write directly to the DAC out register the CPU appears to hang out on that instruction for the DAC's settling time, way too long. If I read the ADC's input register it takes even longer!

 I need to eliminate this, but how?

Let's try DMA

Confession time: I've never used DMA before. I've read about it, I understand it in theory, but never actually used it in a project. The principle is pretty simple, a peripheral in the MCU is triggered by something and copies data from one region of memory to another, and that memory can be RAM or a peripheral's register. It is the closest thing you can get to async programming on a single-core processor.

So I already have TMR0 configured to be nice and fast, so I set up DMA channel 0 to copy a single byte from a general purpose register (GPR) to the DAC's output register every time TMR0's interrupt triggers. I removed my TMR0 ISR and moved the LUT logic back into the main loop. Incredibly, I can let the main loop run as fast as it can and the DAC output keep s up without slowing it down! Well, not to slow. If I speed up TMR0 enough the main loop starts running noticeably slower so there's a tiny bit of overhead whenever the DMA is triggered, but an order of magnitude less than before.

Next step is reading the ADC. This time I configured DMA channel 1 to copy two bytes from the ADC's input register to a GPR. The ADC takes a bit longer than the DAC, and frankly I don't need to read the ADC nearly as often as the DAC. Every 100ms is plenty fast! I configured TMR1 to trigger every 100ms and now I had zero-overhead ADC reads!

This is a pretty big step forward. For testing, I set the ADC's value to adjust the rate of advancing the LUT's index for a primitive frequency control. I am able to hit over 5kHz before the sine wave is too aliased, 500% of my target frequency!

Next step is going to be the analog parts, figuring out how to interface a 3.3v MCU to Eurorack voltage levels.

Discussions