优化此C(AVR)代码

时间:2011-04-13 07:52:42

标签: c optimization assembly avr

我有一个中断处理程序,运行速度不够快,无法完成我想做的事情。基本上我用它来生成正弦波,通过从AVR微控制器上的查找表输出值到PORT,但不幸的是,这并没有足够快地让我获得我想要的波的频率。我被告知我应该考虑在汇编中实现它,因为编译器生成的汇编可能效率稍低,并且可以进行优化但是在查看汇编代码之后我真的看不到我能做得更好。

这是C代码:

const uint8_t amplitudes60[60] = {127, 140, 153, 166, 176, 191, 202, 212, 221, 230, 237, 243, 248, 251, 253, 254, 253, 251, 248, 243, 237, 230, 221, 212, 202, 191, 179, 166, 153, 140, 127, 114, 101, 88, 75, 63, 52, 42, 33, 24, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 24, 33, 42, 52, 63, 75, 88, 101, 114};
const uint8_t amplitudes13[13] = {127, 176,  221, 248,  202, 153, 101, 52, 17,  1, 6,  33,  75};
const uint8_t amplitudes10[10] = {127, 176,   248,  202, 101, 52, 17,  1,  33,  75};

volatile uint8_t numOfAmps = 60;
volatile uint8_t *amplitudes = amplitudes60;
volatile uint8_t amplitudePlace = 0; 

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace];

    amplitudePlace++; 

    if(amplitudePlace == numOfAmps)
    {
        amplitudePlace = 0;
    }

}

幅度和numOfAmps都被另一个比这个运行速度慢得多的中断例程改变了(它基本上用于改变正在播放的频率)。在一天结束时,我将不会使用那些确切的数组,但它将是一个非常相似的设置。我很可能有一个60个值的数组,另一个只有30个。这是因为我正在构建一个频率扫描器,在较低的频率下我可以给它更多的样本,因为我有更多的时钟周期可以使用但是在更高的频率下,我非常紧张。

我确实意识到我可以以较低的采样率工作,但我不希望每个时期不到30个样本。我不认为拥有指向数组的指针会使它变得更慢,因为程序集从数组中获取值并且程序集从指向数组的指针获取值似乎是相同的(这是有意义的)。

在我必须产生的最高频率我被告知我应该能够在每个正弦波周期内使用大约30个样本。目前有30个样本,它运行的最快速度大约是所需最大频率的一半,我认为这意味着我的中断需要以两倍的速度运行。

因此,模拟时的代码需要65个周期才能完成。再说一遍,我被告知我应该能够将它降到最多约30个周期。

这是生成的ASM代码,我想到了每一行在它旁边的作用:

ISR(TIMER1_COMPA_vect) 
{
push    r1
push    r0
in      r0, 0x3f        ; save status reg
push    r0
eor     r1, r1      ; generates a 0 in r1, used much later
push    r24
push    r25
push    r30
push    r31         ; all regs saved


PORTD = amplitudes[amplitudePlace];
lds     r24, 0x00C8     ; r24 <- amplitudePlace I’m pretty sure
lds     r30, 0x00B4 ; these two lines load in the address of the 
lds     r31, 0x00B5 ; array which would explain why it’d a 16 bit number
                    ; if the atmega8 uses 16 bit addresses


add     r30, r24            ; aha, this must be getting the ADDRESS OF THE element 
adc     r31, r1             ; at amplitudePlace in the array.  

ld      r24, Z              ; Z low is r30, makes sense. I think this is loading
                            ; the memory located at the address in r30/r31 and
                            ; putting it into r24

out     0x12, r24           ; fairly sure this is putting the amplitude into PORTD

amplitudePlace++; 
lds     r24, 0x011C     ; r24 <- amplitudePlace
subi    r24, 0xFF       ; subi is subtract imediate.. 0xFF = 255 so I’m
                        ; thinking with an 8 bit value x, x+1 = x - 255;
                        ; I might just trust that the compiler knows what it’s 
                        ; doing here rather than try to change it to an ADDI 

sts     0x011C, r24     ; puts the new value back to the address of the
                        ; variable

if(amplitudePlace == numOfAmps)
lds     r25, 0x00C8 ; r24 <- amplitudePlace
lds     r24, 0x00B3 ; r25 <- numOfAmps 

cp      r24, r24        ; compares them 
brne    .+4             ; 0xdc <__vector_6+0x54>
        {
                amplitudePlace = 0;
                    sts     0x011C, r1 ; oh, this is why r1 was set to 0 earlier
        }


}

pop     r31             ; restores the registers
pop     r30
pop     r25
pop     r24
pop     r19
pop     r18
pop     r0
out     0x3f, r0        ; 63
pop     r0
pop     r1
reti

除了可能在中断中使用更少的寄存器以便我有更少的推/弹,我真的看不出这个汇编代码效率低的地方。

我唯一的另一个想法是,如果我可以解决如何在C中获取n位int数据类型以便数字将在到达结束时回绕,那么if语句可能会被删除?我的意思是我会有2 ^ n - 1个样本然后让amplitudePlace变量继续计数,这样当它达到2 ^ n时它会溢出并重置为零。

我确实尝试在没有完全if位的情况下模拟代码,虽然它确实提高了速度,但它只需要大约10个周期关闭,因此一次执行大约需要55个周期,但不幸的是,这仍然不够快我确实需要进一步优化代码,这是很难考虑的,而不是只有2行!!

我唯一真正的想法是看看我是否可以在需要较少时钟周期的地方存储静态查找表?它用来访问阵列的LDS指令我认为所有这些都需要2个周期,所以我可能不会在那里节省太多时间,但在这个阶段我愿意尝试任何东西。

我完全不知道从哪里开始。我无法看到如何使我的C代码更有效率,但我只是对这类事情相当新,所以我可能会遗漏一些东西。我会喜欢任何帮助..我意识到这是一个非常特殊和涉及的问题,通常我会尽量避免在这里问这些问题,但我已经在这个问题上工作了很长时间而且完全失去了所以我真的会接受任何帮助。

6 个答案:

答案 0 :(得分:6)

我可以看到一些开始工作的领域,没有按特定顺序列出:

1。减少要推送的寄存器数量,因为每个推/弹对需要四个周期。例如,avr-gcc允许您从其寄存器分配器中删除一些寄存器,因此您可以将它们用于该单个ISR中的寄存器变量,并确保它们仍包含上次的值。如果您的程序从未将r1设置为除eor r1,r1以外的任何内容,您也可以摆脱r10的推动。

2. 使用本地临时变量作为数组索引的新值,以便为该volatile变量保存不必要的加载和存储指令。像这样:

volatile uint8_t amplitudePlace;

ISR() {
    uint8_t place = amplitudePlace;
    [ do all your stuff with place to avoid memory access to amplitudePlace ]
    amplitudePlace = place;
}

3. 向后计数从59到0而不是从0到59,以避免单独的比较指令(在减法中无论如何都会发生0的比较)。伪代码:

     sub  rXX,1
     goto Foo if non-zero
     movi rXX, 59
Foo:

而不是

     add  rXX,1
     compare rXX with 60
     goto Foo if >=
     movi rXX, 0
Foo:

4。也许使用指针和指针比较(使用预先计算的值!)而不是数组索引。需要检查而不是向后计算哪一个更有效。也许可以将数组对齐到256字节边界,并且只使用8位寄存器作为指针,以节省加载和保存地址的高8位。 (如果你的SRAM用完了,你仍然可以将这60个字节数组中的4个数据的内容放入一个256字节数组中,并且仍然可以获得所有地址的优势,包括8个常量高位和8个可变低位。)< / p>

uint8_t array[60];
uint8_t *idx = array; /* shortcut for &array[0] */
const uint8_t *max_idx = &array[59];

ISR() {
    PORTFOO = *idx;
    ++idx;
    if (idx > max_idx) {
        idx = array;
    }
}

问题是指针是16位而你的简单数组索引以前是8位大小。如果你设计你的数组地址使得地址的高8位是常量(在汇编代码hi8(array)中),并且你只处理实际改变的低8位,那么帮助它可能是一个技巧。 ISR。但这确实意味着编写汇编代码。上面生成的汇编代码可能是在汇编中编写ISR版本的一个很好的起点。

5. 如果从定时的角度来看可行,请将样本缓冲区大小调整为2的幂,以使用简单的i = (i+1) & ((1 << POWER)-1);替换if-reset-to-zero部分。如果你想使用 4。中提出的8位/ 8位地址分割,或许甚至可以使用256来获得2的幂(并根据需要复制样本数据以填充256字节缓冲区)甚至会在ADD后保存AND指令。

6. 如果ISR仅使用不影响状态寄存器的指令,请停止按下并弹出SREG

一般

以下内容可能会派上用场,尤其是手动检查所有其他汇编代码的假设:

firmware-%.lss: firmware-%.elf
        $(OBJDUMP) -h -S $< > $@

这将生成整个固件映像的注释完整汇编语言列表。您可以使用它来验证寄存器(非)使用情况。请注意,启动代码仅在您首次启用中断之前运行一次,不会干扰您的ISR以后独占使用寄存器。

如果您决定不直接在汇编代码中编写ISR,我建议您在每次编译后编写C代码并检查生成的汇编代码,以便立即观察到您的更改最终会生成。

您最终可能会在C和汇编中编写十几个ISR变体,为每个变体添加循环,然后选择最佳变体。

注意在没有进行任何寄存器预约的情况下,我最终得到了大约31个周期的ISR(不包括进入和离开,这又增加了8或10个周期)。完全摆脱寄存器推动将使ISR降低到15个周期。更改为具有256字节常量的样本缓冲区并使ISR独占使用四个寄存器允许在ISR中花费6个周期(加上8或10进入/离开)。

答案 1 :(得分:3)

我认为最好的方法是在纯汇编程序中编写ISR。它是非常简短的代码,您可以使用现有的反汇编程序来指导您。但对于这种性质的东西,你应该能够做得更好:例如使用较少的寄存器,以保存pushpop;重新计算它,使它不会在内存中加载amplitudePlace三次,等等。

答案 2 :(得分:3)

你必须与程序的其余部分共享所有这些变量吗?由于您共享的每个此类变量都必须是volatile,因此不允许编译器对其进行优化。至少amplitudePlace看起来可以更改为本地静态变量,然后编译器可以进一步优化它。

答案 3 :(得分:2)

澄清一下,你的中断应该是这样的:

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace++];
    amplitudePlace &= 63;
}

这将要求您的表长度为64个条目。如果你可以选择你的桌子的地址,你可以使用一个指针,增加它,&amp;它与0xffBf。

如果使用变量而不是固定常量会降低速度,可以将指针变量替换为特定数组:

PORTD = amplitudes13[amplitudePlace++];

然后更改中断指针,为每个波形使用不同的功能。这可能不是很大的节省,但我们的总周期减少了10个。

登记用法事项。一旦你获得了这样一个非常简单的ISR,你可以检查推送和弹出处理器状态的ISR的prolog和epilog。如果您的ISR仅使用1个寄存器,则可以在汇编程序中执行此操作,并仅保存和恢复该寄存器。这将减少中断开销,而不会影响程序的其余部分。有些编译器可能会为你做这件事,但我对此表示怀疑。

如果有时间和空间你也可以创建一个长表并用+ = freq替换++,其中freq将使波形成为基频的整数倍(2x,3x,4x等... )跳过那么多样本。

答案 4 :(得分:1)

您是否考虑过以问题为中心并以固定的中断频率踩到可变速率,而不是一次一个地进入表中?这样ISR本身会更重,但你可以负担得起以较低的速度运行它。此外,通过一点定点运算,您可以轻松生成更广泛的频率,而无需使用多个表格。

无论如何,如果你能够将你的需求略微提高以适应硬件,那么就有一百零一种方法可以为这类问题节省周期。例如,您可以将计时器的输出链接到另一个硬件计时器的时钟,并使用第二个计时器的计数器作为表索引。您可以保留全局寄存器或滥用未使用的I / O来存储变量。您可以在COMPA中断中一次查找两个条目(或插值),并在其间设置一个微小的第二个COMPB中断以发出缓冲条目。等等,等等。

通过一些硬件滥用和精心设计的汇编代码,您应该能够在15个左右的时间内完成此操作,而不会有太多麻烦。是否可以使其与系统的其余部分保持良好匹配是另一个问题。

答案 5 :(得分:0)

通过使用算术表达式来解除条件和比较可能就足够了:

ISR(TIMER1_COMPA_vect) 
{
        PORTD = amplitudes[amplitudePlace];

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

如果你的CPU以合理的速度执行模运算,这应该快得多。如果仍然不够,请尝试在汇编程序中编写此版本。