优化此C(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个样本,然后使振幅位置变量保持递增计数,以便当它达到2 ^ n时将溢出并重置为零。 我确实尝试了完全没有if的情况下模拟代码,虽然它确实提高了速度,但只花了大约10个周期,因此一次执行大约需要55个周期,但不幸的是,它的速度还不够快,所以我做到了需要进一步优化代码,如果没有这只有两行的话,这是很难考虑的!! 我唯一真正想到的是,是否可以将静态查找表存储在需要较少时钟周期访问的地方?我认为它用来访问阵列的LDS指令全都需要2个周期,所以我可能真的不会在那里节省很多时间,但是在这个阶段我愿意尝试任何事情。 我完全不知该从何处去。我看不到如何提高C代码的效率,但是我对这种事情还很陌生,所以我可能会丢失一些东西。我很乐意提供任何帮助。.我意识到这是一个非常特殊且涉及到的问题,通常我会尽量避免在这里提出此类问题,但是我已经为此工作了很长时间了,总的来说损失,所以我将竭尽所能。     
已邀请:
我可以看到一些要开始研究的领域,没有特别的顺序: 1.减少要推送的寄存器数量,因为每个推送/弹出对都需要四个周期。例如,“ 2”允许您从其寄存器分配器中删除一些寄存器,因此您可以将它们用于该单个ISR中的寄存器变量,并确保它们仍包含上次的值。如果您的程序从未将
r1
设置为除anything6ѭ以外的任何值,您可能还摆脱了
r1
eor 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位的,而以前的简单数组索引的大小是8位。如果设计阵列地址以使地址的高8位为常量(在汇编代码中为
hi8(array)
),并且仅处理ISR中实际变化的低8位,则可能会帮助解决这一问题。但这确实意味着编写汇编代码。从上面生成的汇编代码可能是在汇编中编写该版本的ISR的良好起点。 5.如果从时序角度来看可行,则将样本缓冲区的大小调整为2的幂,以简单的ѭ12代替if-reset-zero到零部分。如果您想使用4.中建议的8位/ 8位地址拆分,甚至以256的幂乘以2(并根据需要复制示例数据以填充256字节缓冲区)甚至可以节省您添加后的AND指令。 6.如果ISR仅使用不影响状态寄存器的指令,则停止推入并弹出
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。它是非常简短的代码,并且您已有现有的反汇编程序来指导您。但是对于这种性质的东西,您应该能够做得更好:使用更少的寄存器,以节省ѭ15和
pop
;对其进行重构,以使其不会在三个不同的时间从内存中加载
amplitudePlace
,等等。     
您是否必须与程序的其余部分共享所有这些变量?由于您共享的每个此类变量都必须是易失性的,因此不允许编译器对其进行优化。至少振幅幅值看起来好像可以更改为局部静态变量,然后编译器才能够进一步对其进行优化。     
为了澄清,您的中断应该是这样的:
ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace++];
    amplitudePlace &= 63;
}
这将要求您的表长度为64个条目。如果可以选择表的地址,则可以用一个指针离开,增加它的指针,然后用0xffBf递增它。 如果使用变量而不是固定常量会使速度变慢,则可以将指针变量替换为特定的数组:
PORTD = amplitudes13[amplitudePlace++];
然后,您更改中断指针以对每个波形使用不同的功能。这不太可能节省很多,但我们的总周期数已降至10个周期。 寄存器使用事。一旦获得了这样一个非常简单的ISR,就可以检查ISR的序言和结语,以推入并弹出处理器状态。如果ISR仅使用1个寄存器,则可以在汇编器中进行操作,并且仅保存和恢复该1个寄存器。这将减少中断开销,而不会影响程序的其余部分。一些编译器可能会为您执行此操作,但我对此表示怀疑。 如果有时间和空间,您还可以创建一个长表,并用+ = freq替换++,其中freq将通过跳过而使波形为基本频率的整数倍(2x,3x,4x等)。这么多的样本。     
您是否考虑过解决问题并以固定的中断频率以可变的速率步进,而不是一次通过变化的中断速率单步执行一个表?这样,ISR本身就会更重,但是您可以负担得起以较低的速度运行它。此外,只需一点定点算法,就可以轻松生成更宽的频谱范围,而不会弄乱多个表格。 无论如何,如果您有能力稍微调整一下需求以适应硬件,那么有一百零一种作弊方法可以节省此类问题的周期。例如,您可以将计时器的输出链接起来,为另一个硬件计时器计时,并使用第二个计时器的计数器作为表索引。您可能会保留全局寄存器或滥用未使用的I / O来存储变量。您可以在COMPA中断中一次查找(或内插)两个条目,并在它们之间设置一个微小的第二个COMPB中断以发出缓冲的条目。等等等等。 稍有硬件滥用和精心设计的汇编代码,您应该可以在15个左右的周期内完成此操作,而不会遇到太多麻烦。您是否可以使其在系统的其余部分上正常运行是另一个问题。     
也许可以通过使用算术表达式将所有条件和比较全部消除:
ISR(TIMER1_COMPA_vect) 
{
        PORTD = amplitudes[amplitudePlace];

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}
如果您的CPU以合理的速度执行模运算,那么它应该快得多。如果仍然不够用,请尝试在汇编器中编写此版本。     

要回复问题请先登录注册