3
$\begingroup$

Newbie question here.

I'm working on a system that is running a 32khz sampling rate, audio, 16 bit DAC. Previously, our system had a very hard latency requirement of 1 sample. So no problem, we just read 1 sample in, did some processing within the sampling rate, and then read 1 sample out.

Now that this requirement has been relaxed, I think that we can use block processing to do things such as spectrum computation and processing and use more than 1 sample to do the adding and multiplication, etc. Does this buy us blockSize * (1/fs) compute time?

My problem is, being a noob, I don't know how to formulate this new requirement in our chip's main() function.

So let's pretend we have some function SPI_Read(&mic) which is reading a sample, and SPI_Write(&output) which sends to a speaker. How would I transform this into a block based system?

The current system is setup like:

main() {
    while() { // check for interrupt
        // while loop executing on system timer set to 1 / fs
        SPI_Read(&in); // read from adc
        // filters
        SPI_Write(&out); // output to dac

    }
}

This is running on a cortex-M4, no codec.

Thank you for your help! (:

$\endgroup$
  • $\begingroup$ I suggest moving this to stack overflow... $\endgroup$ – pichenettes Jan 30 '15 at 9:36
  • 2
    $\begingroup$ Yes, the CMSIS-DSP library is all block-based, and runs more efficiently that way. This presentation talks about how to unroll loops and make processing more efficient, etc. $\endgroup$ – endolith Jan 30 '15 at 14:48
4
$\begingroup$

First, use a timer and an ISR to get accurate timing (don't forget to configure the NVIC so that this timer interrupt takes over any other ISR that would be running). Only this will ensure a consistant sample rate. Little variations in timing would create noticeable degradations of audio quality. In particular, in your current example, unless the "filters" code has no branches, the time at which the SPI_Write occurs will be jittery depending on the path taken in the code.

For block processing, you need a "ping-pong" buffering scheme.

In your per-sample ISR:

SPI_Write(output[active_buffer][sample_index]);
SPI_Read(input[active_buffer][sample_index]);
++sample_index;
if (sample_index >= BUFFER_SIZE) {
  active_buffer = (active_buffer + 1) % NUM_BUFFERS;
  sample_index = 0;
}

In your main code:

while (render_buffer != active_buffer) {
  // Process samples from input[render_buffer][0] to input[render_buffer][BUFFER_SIZE - 1]
  // And write them to output[render_buffer][0] to output[render_buffer][BUFFER_SIZE - 1]
  render_buffer = (render_buffer + 1) % NUM_BUFFERS;
}

render_buffer and active_buffer are declared as volatile. Start with NUM_BUFFERS equal to 2. Higher values will introduce more latency, but might be necessary if the processing code has exceptionally long code paths.

The benefit of processing samples block by block is that some variables required by your DSP algorithm (such as filter state variables) can be kept in registers within the processing loop, saving load/stores. There are also situations in which you can benefit from batch load/stores. Finally, it makes it easier to schedule computations at "audio rate" (once per audio sample) and "control rate" (once per block).

$\endgroup$
  • $\begingroup$ I feel like this code would lead to dropouts if the main loop gets caught up in something and can't process the samples. Would it be stupid to do the DSP code inside the ISR to guarantee that it runs, and do the control code in the main loop using whatever time is leftover when the ISR completes? $\endgroup$ – endolith Jan 30 '15 at 15:03
  • 1
    $\begingroup$ Yes, this is correct, the main code should visit the "while (render_buffer != active_buffer)" condition frequently enough to avoid drops - so the main loop should not spend too much time on any other tasks. But it's still better than processing each individual sample in the main loop. If long tasks are happening in the main loop, you can schedule the while (render_buffer != active_buffer) check in its own timer ISR. Though having to do this is a symptom it's time to switch to an RTOS :) $\endgroup$ – pichenettes Jan 30 '15 at 15:10
  • $\begingroup$ Doing the processing in the ISR doesn't work if your algorithm works on groups of samples, and if you want to enjoy the benefits of efficient code processing a block of samples with all state variables fitting in registers... $\endgroup$ – pichenettes Jan 30 '15 at 15:11
  • 1
    $\begingroup$ Yes, that's the point. You are allowed a time frame of only 1/fs in the ISR if you don't want to miss the next sample and the stack to pile up. If your DSP code is really light and if you can process a group of several samples in 1/fs then it's fine - but this situation is unlikely. $\endgroup$ – pichenettes Jan 30 '15 at 17:13
  • 1
    $\begingroup$ A situation in which doing the processing in the ISR works much better is when you use DMA to automate the transmission/reception of samples. In this case you can program let the DMA controller do the double buffering, and you configure it to get an interrupt whenver a half-buffer is ready for read/write. It's easier to use this approach when using an I2S codec rather than with raw SPI read/writes (mostly because most MCUs I have played with don't have any facility for toggling the CS line between word transfers on the SPI peripheral). $\endgroup$ – pichenettes Jan 30 '15 at 17:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.