10

我有一个中断处理程序,它运行得不够快,无法完成我想做的事情。基本上,我使用它通过将查找表中的值输出到 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

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

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

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

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

我完全不知道从这里去哪里。我看不出如何让我的 C 代码更有效率,但我对这类事情还很陌生,所以我可能会遗漏一些东西。我很想得到任何帮助。我意识到这是一个非常特殊且涉及到的问题,通常我会尽量避免在这里问这些问题,但我已经为此工作了很长时间,而且完全不知所措,所以我真的会接受任何我能得到的帮助。

4

6 回答 6

6

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

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

2.为数组索引的新值使用一个局部临时变量,以将不必要的加载和存储指令保存到该易失性变量。像这样的东西:

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 的幂,以将 if-reset-to-zero 部分替换为简单的i = (i+1) & ((1 << POWER)-1);. 如果您想使用4.中提出的 8 位/8 位地址拆分,甚至可能使用 256 的 2 次方(并根据需要复制样本数据以填充 256 字节缓冲区)甚至可以为您节省ADD 之后的 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 个进入/离开)。

于 2011-04-13T08:12:31.120 回答
3

我想说最好的办法是用纯汇编程序编写你的 ISR。这是非常简短的代码,您可以使用现有的反汇编程序来指导您。但是对于这种性质的东西,你应该能够做得更好:例如使用更少的寄存器,以节省pushand pop; 重构它,使其不会amplitudePlace从内存中加载三个不同的时间,等等。

于 2011-04-13T08:16:09.697 回答
3

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

于 2011-04-13T08:41:54.917 回答
2

为了澄清,你的中断应该是这样的:

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

这将要求您的表格长度为 64 个条目。如果您可以选择表的地址,则可以使用单个指针,将其递增,然后使用 0xffBf 来处理它。

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

PORTD = amplitudes13[amplitudePlace++];

然后更改中断指针以对每个波形使用不同的函数。这不太可能节省大量资金,但我们总共减少了 10 个周期。

至于寄存器使用的东西。一旦你得到一个像这样的非常简单的 ISR,你可以检查 ISR 的序言和结语,它们会推送和弹出处理器状态。如果您的 ISR 仅使用 1 个寄存器,您可以在汇编程序中执行此操作,并且只保存和恢复该一个寄存器。这将减少中断开销而不影响程序的其余部分。一些编译器可能会为您执行此操作,但我对此表示怀疑。

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

于 2011-04-13T13:30:00.540 回答
1

您是否考虑过解决问题并以固定中断频率以可变速率步进,而不是一次以不同的中断频率逐个遍历表?这样 ISR 本身会更重,但您可以负担得起以较低的速率运行它。另外,通过一点定点算法,您可以轻松地生成更广泛的频率谱,而不会弄乱多个表。

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

使用一点硬件滥用和精心编写的汇编代码,您应该可以在 15 个周期左右完成此操作,而不会遇到太多麻烦。您是否可以使它与系统的其余部分配合得很好是另一个问题。

于 2011-04-14T09:10:29.867 回答
0

也许通过使用算术表达式来摆脱条件和比较就足够了:

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

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

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

于 2011-04-13T08:30:16.983 回答