4
\$\begingroup\$

Back at it again.

STM32L43KC Datasheet:

The question this time is I am finally starting to implement my digital filter which can been see as:

Z - Transform: $$LPF_D=\frac{Y}{U}=\frac{0.3957z^2+0.7914z+0.3957}{z^2+0.3848z+0.1979}$$

Difference Equation:

$$Y_i = 0.3957*U_i + 0.7914*U_{i-i} + 0.3957*U_{i-2} - 0.3848*Y_{i-1} - 0.1979*Y_{i-2}$$

With a sample of \$T_s = \$ 15uS. Created with the Tustin Method. With a Pre-Warp frequency of 20kHz.

Code architecture:

The ADC is sampling at 66kHz.

System clocked at 4MHz

Using a DMA to tunnel the ADC's sample into a short type array that can store up to 2000 samples called:

  • "short adcValues[2000]".

I have two interrupts:

  1. Triggers when the DMA fills half of the array "The Half Transfer Interrupt"
  2. Triggers when the DMA complete the transfer to the array " The Complete Transfer Interrupt"

This allows me to process the top half first while the bottom half is being processed and vice versa.

I have measured the time I am given to be in each interrupt which is 15mS for both the half transfer interrupt and the complete transfer interrupt. So essentially I would need my difference equation to be executed within 15mS.

With the current solution It gets executed in 188mS. I know you can do a lot of things such as used fixed point, DSP, FPU, however I am very new to this and have no idea which approach should I try.

Is my implementation even correct without the optimizations? Is it even possible to execute the code within 15mS when it needs to run 1000 times?

Code updated: Thanks to awjlogan. I got the code down to 25mS still not quite < 15mS. I guess the only thing to do is to increase the clock speed?

Second Update: Changing the clock speed to 80MHz got the code to 1.1mS! Thank you everyone for helping and giving their input.

short adcValue[2048];

uint32_t U_i[3] = {0,0,0};
uint32_t Y_i[3] = {0,0,0};

//16.16 Format
const uint32_t COEFF_0 = 0x654C;
const uint32_t COEFF_1 = 0xCA99;
const uint32_t COEFF_2 = 0x6282;
const uint32_t COEFF_3 = 0x32A9;

    void DMA2_Channel3_IRQHandler(void){

    if (((DMA2->ISR) & (1<<10)) != 0){
        halfTransferComplete = 1;
        DMA2->IFCR |= (1<<10);
    } else if (((DMA2->ISR) & (1<<9)) != 0){
        transferComplete = 1;
        DMA2->IFCR |= (1<<9);
    }
}


   while (1) {
    if (halfTransferComplete == 1){

    for (int i = 0; i < 1024; i++){

         U_i[2] = U_i[1];
         U_i[1] = U_i[0];
         U_i[0] = adcValue[i];
         Y_i[2] = Y_i[1];
         Y_i[1] = Y_i[0];
         Y_i[0] = COEFF_0*U_i[0] + COEFF_1*U_i[1] + COEFF_0*U_i[2] - COEFF_2*Y_i[1] - COEFF_3*Y_i[2];
         DAC1->DHR12R1 = Y_i[0] >> 16;
    }
        halfTransferComplete = 0;

}

    if (transferComplete == 1){

     for (int i = 1024; i < 2048; i++){

         U_i[2] = U_i[1];
         U_i[1] = U_i[0];
         U_i[0] = adcValue[i];
         Y_i[2] = Y_i[1];
         Y_i[1] = Y_i[0];
         Y_i[0] = COEFF_0*U_i[0] + COEFF_1*U_i[1] + COEFF_0*U_i[2] - COEFF_2*Y_i[1] - COEFF_3*Y_i[2];
         DAC1->DHR12R1 = Y_i[0] >> 16;
    }
         transferComplete = 0;
      }
  }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

A few general suggestions first:

  • Recommend using defined types for your declarations, e.g. uint32_t rather than int for U_i. The u in uint32_t stands for unsigned, and I assume you're not using a bipolar ADC. These allow portability across different architectures, as int is compiler dependent.
  • short is also compiler dependent, it's only guaranteed to be at least 16bits wide. The natural width for an Arm micro is 32bit, so you might get better performance using uint32_t adcValue. Again, compiler dependent, I'm not sure how the M4 handles packing two 16bit values into a single 32bit value.
  • Your two loops are identical, so just manipulate the indexing to select between the top and bottom.
  • There are often optimisations to be done if you use powers of 2 for the number of iterations. Can you work on 1024 samples rather than 1000?

To run your loop faster, consider this fixed point approach. Your microcontroller has:

  • A 12bit ADC
  • A 12bit DAC

We can represent this in something similar Q12 format (1 integer bit, and 12 fractional), but we'll just normalise things and say 1 > MAX(ADC) >= 0. This means the integer bit is implicitly zero and we will treat all the bits as fractional. When you do a multiplication of a Qm and a Qn fixed point, the product is in Q(m+n) format; we can handle this later. To convert your floating point coefficients to Q12 simply multiply them by \$2^{12}\$. Your coefficients therefore map as:

  • 0.3957 -> 0x0000_0654
  • 0.7914 -> 0x0000_0CA9
  • 0.384 -> 0x0000_0624
  • 0.1979 -> 0x0000_032A

When you multiply your ADC values with these coefficients, you implicitly get a Q24 value out. You can do a right shift by 12 to convert this back to Q12. Your loop now looks something like this:

const uint32_t COEFF_0 = 0x00000654;
const uint32_t COEFF_1 = 0x00000CA9;
const uint32_t COEFF_2 = 0x00000624;
const uint32_t COEFF_3 = 0x0000032A;

for (uint32_t idx = 0; idx < 1024; idx++) {
    U_i[2] = U_i[1];
    U_i[1] = U_i[0];
    U_i[0] = adcValues[idx];
    Y_i[2] = Y_i[1];
    Y_i[1] = Y_i[0];
    Y_i[0] = COEFF_0 * U_i[0] + COEFF_1 * U_i[1] + COEFF_0 * U_i[2] - COEFF_2 * Y_i[1] - COEFF_3 * Y_i[2];
    DAC1->DHR12R1 = Y_i[0] >> 12;
}

Each iteration through the loop is now (naively estimating) something like 6 assignments and 5 multiply-accumulates (which I believe is a single instruction, MLA). As an estimate, let's say 20 instructions per iteration - even at the 4 MHz clock rate you've quoted, that's about 5 ms for 1000 samples. Hope that's helpful!

Addition

I've put some code up on Godbolt, you can play with the compiler options there. It produces roughly what I expected, around 35 instructions per iteration. It shuffles things around registers a bit more than necessary, so it would be a good exercise for you to understand that and perhaps tighten it up by writing the assembly. Try changing the compiler options; for example, swapping -march=armv7-m for -mcpu=cortex-m4.

You said the time is around 25 ms to do all the samples - the accesses to memory and the DAC will slow things down so the rough estimate of time is of the right order of magnitude. I would anticipate that the write to the DAC register takes a relatively long time. Speeding up your clock will certainly make it run faster, and really with something as capable as an M4, you should be running much faster than 4 MHz.

\$\endgroup\$
18
  • 1
    \$\begingroup\$ Hey, its gonna take a couple of reads to digest this! But thank you so much for the response. Just a few questions. The uint32_t I understand I read somewhere that 32 bit arithmetic is faster in 32 bit processors cause its supported.I am confused as how 1024 samples works faster than 1000 samples? Just how the compiler will see it? a base of 2? Processor can be maxed out to 80MHz but having troubles switching clock speed not a direct process. Not using the HAL. \$\endgroup\$
    – Leoc
    Apr 30, 2020 at 17:47
  • \$\begingroup\$ I'll probably put the DAC into another buffer and have a DMA write it to the DAC at the same samples. The biggest concept I dont get is fixed point. I get the whole you can choose your decimal place by multiplying or shifting it for example using an INT if you want a 16.16 then * 2^24. What I am confused about is when you revert it back to the value by shifting it 2^x. How does the code know its a decimal number? isnt it in a INT data type thus let say 0.4132 * 3 = 1.2396 then 1 is going to be in the variable? \$\endgroup\$
    – Leoc
    Apr 30, 2020 at 17:52
  • 1
    \$\begingroup\$ @Pllsz In fixed point, the code doesn't know the position of the radix. YOU know. So you shift things to line it up prior to additions and subtractions and you know where it is in the result. For multiplications and divisions, you know where the radix is for both operands so you know where it is in the result. You manually keep track of it and adjust for it. Every. Single. Damn. Time. \$\endgroup\$
    – DKNguyen
    Apr 30, 2020 at 18:34
  • 1
    \$\begingroup\$ What I noticed, I just tried uploading the code with Keil IDE instead of STM32Cube and the code got reduced to 17mS from 25mS. I am not sure if Keil IDE already implements the optimized flags such as "-mcpu=cortex-m4" or STM32Cube has very big overhead? \$\endgroup\$
    – Leoc
    May 1, 2020 at 20:42
  • 1
    \$\begingroup\$ And maybe perhaps the STM32CUBEIDE generates the makefile with the -mcpu=cortex-m4 flag in it already. I say this because when I take a peak in the make file theres a line that says "$(USER_OBJS) $(LIBS) -mcpu=cortex-m4 -T" \$\endgroup\$
    – Leoc
    May 1, 2020 at 20:45

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.