优化这个C (AVR)代码



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

这是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

除了可能在中断中使用更少的寄存器,以便我有更少的push/pop,我真的看不出这个汇编代码在哪里效率低下。

我唯一的另一个想法是,如果我能弄清楚如何在C中获得n位int数据类型,那么if语句可能会被删除,以便数字在到达末尾时绕起来?我的意思是,我将有2^n - 1个样本,然后让ampludeplace变量不断计数,这样当它达到2^n时,它就会溢出并重置为零。

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

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

我完全不知道从这里该去哪里。我不知道如何让我的C代码更高效,但我只是对这类事情相当陌生,所以我可能会错过一些东西。我希望得到任何帮助。我意识到这是一个非常特殊和复杂的问题,通常我会尽量避免在这里问这类问题,但我已经研究了很长时间,完全不知所措,所以我真的会接受任何我能得到的帮助。

我可以看到一些可以开始工作的领域,排名不分先后:

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

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个可变的低位组成的所有地址的优势。)

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 bit,而你的简单数组索引以前是8 bit的大小。如果您设计的数组地址中较高的8位是常量(在汇编代码中,hi8(array)),并且您只处理在ISR中实际变化的较低的8位,那么帮助解决这个问题可能是一个技巧。不过,这确实意味着要编写汇编代码。从上面生成的汇编代码可能是用汇编编写该版本的ISR的一个很好的起点。

5。如果从时序角度来看可行,将样本缓冲区大小调整为2的幂,用简单的i = (i+1) & ((1 << POWER)-1);替换If复位为零部分。如果您想使用4中建议的8位/8位地址分割。,甚至可能将2的幂调到256(并根据需要复制示例数据以填充256字节缓冲区),甚至可以在ADD之后节省and指令。

6。如果ISR只使用不影响状态寄存器的指令,则停止push和弹出SREG

在手动检查所有其他汇编代码是否存在假设时,以下代码可能会派上用场:

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

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

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

你可能会在C和汇编中写出十几个ISR的变体,把每个变体的循环加起来,然后选择最好的一个。

在不做任何寄存器保留的情况下,我最终为ISR提供了大约31个周期(不包括进入和离开,这增加了另外8或10个周期)。完全摆脱寄存器推送将使ISR降低到15个周期。将样本缓冲区更改为256字节的常量大小,并让ISR独占使用四个寄存器,可以将ISR中花费的周期减少到6个(加上8或10个进入/离开)。

我认为最好是用纯汇编语言编写ISR。它是非常简短和简单的代码,并且您有现有的反汇编器来指导您。但是对于这种性质的东西,你应该能够做得更好:例如,使用更少的寄存器,以节省pushpop;重构它,使它不会从内存中加载amplitudePlace三次,等等

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

说明一下,你的中断应该是这样的:

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

这将要求您的表长度为64个条目。如果你可以选择表的地址,你可以使用单个指针,自增它,&

如果使用变量而不是固定常量会减慢速度,你可以用一个特定的数组替换指针变量:

PORTD = amplitudes13[amplitudePlace++];

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

关于寄存器的使用。一旦您得到这样一个非常简单的ISR,您就可以检查ISR的prolog和epilog,它们推送和弹出处理器状态。如果您的ISR只使用1个寄存器,您可以在汇编程序中完成它,并且只保存和恢复那个寄存器。这将在不影响程序其余部分的情况下减少中断开销。有些编译器可能会为您这样做,但我对此表示怀疑。

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

不是用不同的中断率一次步进表中的一个条目,您是否考虑过扭转问题并以固定的中断频率以可变速率步进?这样ISR本身会更重,但你可以负担得起以较低的速率运行它。此外,使用一点定点运算,您可以轻松生成更宽的频谱,而无需混淆多个表。

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

使用一点硬件滥用和精心编写的汇编代码,您应该能够在15个循环左右完成此任务,而不会有太多麻烦。你能否让它与系统的其他部分很好地配合是另一个问题。

也许用一个算术表达式就足够把条件和比较都去掉了:

ISR(TIMER1_COMPA_vect) 
{
        PORTD = amplitudes[amplitudePlace];
        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

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

最新更新