如何说服avr-gcc,全局字节数组的内存位置是一个常量

时间:2014-09-05 11:37:22

标签: c gcc avr-gcc gcc4 winavr

我为一个带有ATmega2560处理器的avr项目编写了一个快速" 8位反向" -routine。 我正在使用

  • GNU C(WinAVR 20100110)版本4.3.3(avr)/由GNU C版本3.4.5(mingw-vista special r3)编写,GMP版本4.2.3,MPFR版本2.4.1。< / LI>

首先,我创建了一个反向字节的全局查找表(大小:0x100):

uint8_t BitReverseTable[]
        __attribute__((__progmem__, aligned(0x100))) = {
    0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,
    0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
    [...]
    0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};

这可以按预期工作。这是我打算使用的宏,这应该只花费我5个圆柱:

#define BITREVERSE(x) (__extension__({                                        \
    register uint8_t b=(uint8_t)x;                                            \
    __asm__ __volatile__ (                                                    \
        "ldi r31, hi8(table)"                                          "\n\t" \
        "mov r30, ioRegister"                                          "\n\t" \
        "lpm ioRegister, z"                                            "\n\t" \
        :[ioRegister] "+r" (b)                                                \
        :[table] "g" (BitReverseTable)                                        \
        :"r30", "r31"                                                         \
    );                                                                        \
}))

编译(或不编译)的代码。

int main() /// Test for bitreverse
{
    BITREVERSE(25);
    return 0;
}

这是我从编译器得到的错误:

c:/winavr-20100110/bin/../lib/gcc/avr/4.3.3/../../../../avr/bin/as.exe -mmcu=atmega2560 -o bitreverse.o C:\Users\xxx\AppData\Local\Temp/ccCefE75.s
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s: Assembler messages:
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s:349: Error: constant value required
C:\Users\xxx\AppData\Local\Temp/ccCefE75.s:350: Error: constant value required

我想问题出在这里:

    :[table] "g" (BitReverseTable)                                        \

从我的观点来看,BitReverseTable是数组的内存位置,它在编译时是固定的并且是已知的。因此它是不变的。 也许我需要将BitReverseTable转换成某种东西(我尝试过任何我能想到的东西)。也许我需要另一个约束(&#34; g&#34;是我的最后一次测试)。我确定我用过任何可能而且不可能的东西。 我编写了一个汇编程序版本,它工作正常,但它不是一个内联汇编代码,而是一个适当的函数,它增加了另外6个循环(用于调用和返回)。

非常欢迎任何建议或建议!

pastebin上的bitreverse.c的完整来源。 详细编译器输出也在pastebin

1 个答案:

答案 0 :(得分:1)

以下似乎适用于avr-gcc(GCC)4.8.2,但对我来说确实有一种明显的hacky回味。

编辑修复OP(Thomas)在评论中指出的问题:

  • Z注册的高字节为r31(我已r30交换r31
  • 较新的AVR,如ATmega2560也支持lpm r,Z(仅限较早的AVR lpm r0,Z

感谢您的修复,托马斯!我有一个ATmega2560主板,但我更喜欢Teensies(部分原因是因为原生USB),所以我只对代码进行了编译测试,没有运行它来验证。我应该提到那个;道歉。

const unsigned char reverse_bits_table[256] __attribute__((progmem, aligned (256))) = {
      0, 128,  64, 192,  32, 160,  96, 224,  16, 144,  80, 208,  48, 176, 112, 240,
      8, 136,  72, 200,  40, 168, 104, 232,  24, 152,  88, 216,  56, 184, 120, 248,
      4, 132,  68, 196,  36, 164, 100, 228,  20, 148,  84, 212,  52, 180, 116, 244,
     12, 140,  76, 204,  44, 172, 108, 236,  28, 156,  92, 220,  60, 188, 124, 252,
      2, 130,  66, 194,  34, 162,  98, 226,  18, 146,  82, 210,  50, 178, 114, 242,
     10, 138,  74, 202,  42, 170, 106, 234,  26, 154,  90, 218,  58, 186, 122, 250,
      6, 134,  70, 198,  38, 166, 102, 230,  22, 150,  86, 214,  54, 182, 118, 246,
     14, 142,  78, 206,  46, 174, 110, 238,  30, 158,  94, 222,  62, 190, 126, 254,
      1, 129,  65, 193,  33, 161,  97, 225,  17, 145,  81, 209,  49, 177, 113, 241,
      9, 137,  73, 201,  41, 169, 105, 233,  25, 153,  89, 217,  57, 185, 121, 249,
      5, 133,  69, 197,  37, 165, 101, 229,  21, 149,  85, 213,  53, 181, 117, 245,
     13, 141,  77, 205,  45, 173, 109, 237,  29, 157,  93, 221,  61, 189, 125, 253,
      3, 131,  67, 195,  35, 163,  99, 227,  19, 147,  83, 211,  51, 179, 115, 243,
     11, 139,  75, 203,  43, 171, 107, 235,  27, 155,  91, 219,  59, 187, 123, 251,
      7, 135,  71, 199,  39, 167, 103, 231,  23, 151,  87, 215,  55, 183, 119, 247,
     15, 143,  79, 207,  47, 175, 111, 239,  31, 159,  95, 223,  63, 191, 127, 255,
};

#define USING_REVERSE_BITS \
    register unsigned char r31 asm("r31"); \
    asm volatile ( "ldi r31,hi8(reverse_bits_table)\n\t" : [r31] "=d" (r31) )

#define REVERSE_BITS(v) \
    ({ register unsigned char r30 asm("r30") = v; \
       register unsigned char ret; \
       asm volatile ( "lpm %[ret],Z\n\t" : [ret] "=r" (ret) : [r30] "d" (r30), [r31] "d" (r31) ); \
       ret; })

unsigned char reverse_bits(const unsigned char value)
{
    USING_REVERSE_BITS;
    return REVERSE_BITS(value);
}

void reverse_bits_in(unsigned char *string, unsigned char length)
{
    USING_REVERSE_BITS;

    while (length-->0) {
        *string = REVERSE_BITS(*string);
        string++;
    }
}

对于仅支持lpm r0,Z的旧版AVR,请使用

#define REVERSE_BITS(v) \
    ({ register unsigned char r30 asm("r30") = v; \
       register unsigned char ret asm("r0"); \
       asm volatile ( "lpm %[ret],Z\n\t" : [ret] "=t" (ret) : [r30] "d" (r30), [r31] "d" (r31) ); \
       ret; })

我们的想法是使用local reg var r31来保留Z寄存器对的高字节。 USING_REVERSE_BITS;宏在当前作用域中定义它,使用内联汇编有两个目的:避免将表地址的低部分不必要地加载到寄存器中,并确保GCC知道我们已将值存储到它(因为它是一个输出操作数)没有任何方式知道该值应该是什么,因此希望将其保留在整个范围内。

REVERSE_BITS()宏产生结果,告诉编译器它需要寄存器r30中的参数,以及USING_REVERSE_BITS;r31设置的表地址高字节。 / p>

听起来有点复杂,但那只是因为我不知道如何更好地解释它。这真的很简单。

使用avr-gcc-4.8.2 -O2 -fomit-frame-pointer -mmcu=atmega2560 -S编译上面的内容会产生汇编源。 (我建议使用-O2 -fomit-frame-pointer。) 省略评论和正常指令:

    .text

reverse_bits:
    ldi r31,hi8(reverse_bits_table)
    mov r30,r24
    lpm r24,Z
    ret

reverse_bits_in:
    mov r26,r24
    mov r27,r25
    ldi r31,hi8(reverse_bits_table)
    ldi r24,lo8(-1)
    add r24,r22
    tst r22
    breq .L2
.L8:
    ld r30,X
    lpm r30,Z
    st X+,r30
    subi r24,1
    brcc .L8
.L2:
    ret

    .section    .progmem.data,"a",@progbits
    .p2align    8
reverse_bits_table:
    .byte    0
    .byte    -128
    ; Rest of data omitted for brevity

如果您想知道,在ATmega2560上,GCC将第一个8位参数和8位函数结果放在寄存器r24中。

据我所知,第一个功能是最佳的。 (对于仅支持lpm r0,Z的旧版AVR,您可以添加一项动作,将结果从r0复制到r24。)

对于第二个函数,设置部分可能不是最佳的(对于一个,你可以先做tst r22 breq .L2来加速零长度数组检查),但我我不确定自己是否可以写一个更快/更短的一个;这对我来说当然是可以接受的。

第二个函数中的循环对我来说是最佳的。它使用r30的方式我起初发现奇怪和可怕,但后来我意识到这很有道理 - 使用的寄存器更少,以这种方式重用r30没有任何害处(即使它是Z的低部分也注册了),因为它将在下一次迭代开始时从string加载一个新值。

请注意,在我之前的编辑中,我提到交换函数参数的顺序产生了更好的代码,但是随着Thomas的添加,不再是这种情况。寄存器改变了,就是这样。

如果您确定始终提供大于零length,请使用

void reverse_bits_in(unsigned char *string, unsigned char length)
{
    USING_REVERSE_BITS;
    do {
        *string = REVERSE_BITS(*string);
        string++;
    } while (--length);
}

产量

reverse_bits_in:
    mov r26,r24                      ; 1 cycle
    mov r27,r25                      ; 1 cycle
    ldi r31,hi8(reverse_bits_table)  ; 2 cycles
.L4:
    ld r30,X                         ; 2 cycles
    lpm r30,Z                        ; 3 cycles
    st X+,r30                        ; 2 cycles
    subi r22,lo8(-(-1))              ; 1 cycle
    brne .L4                         ; 2 cycles
    ret                              ; 4 cycles

开始让我印象深刻:每个字节10个周期,4个设置周期,3个周期清理(如果没有跳转,brne只需一个周期)。循环计数我列在我的头顶,所以可能有小错误(这里或那里的一个循环)。 r26:r27X,函数的第一个指针参数在r24:r25中提供,长度为r22

reverse_bits_table位于正确的部分,并且正确对齐。 (.p2align 8确实对齐到256个字节;它指定低8位为零的对齐。)

尽管GCC因多余的寄存器动作而臭名昭着,但我真的很喜欢它上面生成的代码。当然,总有空间去做;对于重要的代码序列,我建议尝试不同的变体,甚至更改函数参数的顺序(或在本地作用域中声明循环变量),等等,然后使用-S进行编译以查看生成的代码。 AVR instruction timings很简单,因此比较代码序列非常容易,看看是否有一个明显更好。我想首先删除指令和注释;它使组装更容易阅读。


令人讨厌的余味的原因是GCC documentation明确表示“定义这样的寄存器变量不会保留寄存器;它仍然可用于流量控制确定变量值的地方的其他用途是不是“,我只是不相信这对GCC开发人员来说意味着和我一样。即使它现在做了,也可能在未来;没有标准的GCC开发人员应该遵守这里,因为这是GCC特有的功能。

另一方面,我只依赖于上面记录的GCC行为,虽然“hacky”,但它确实从直接的C代码生成有效的汇编。

就我个人而言,无论何时更新avr-gcc,我都建议重新编译上面的测试代码,并查看生成的程序集(可能使用sed删除注释和标签,并与已知的正常版本进行比较?)。 / p>

有问题吗?