【问题标题】:How to convince avr-gcc, that the memory position of a global byte array is a constant如何说服avr-gcc,全局字节数组的内存位置是一个常数
【发布时间】:2014-10-30 08:36:33
【问题描述】:

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

  • 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 编译。

首先我创建了一个反向字节的全局查找表(大小: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 转换成某种东西(我尝试了我能想到的任何东西)。也许我需要另一个约束(“g”是我的最后一个测试)。我确信我使用了任何可能和不可能的东西。 我编写了一个汇编程序版本,它工作正常,但不是内联汇编代码,而是一个适当的函数,它增加了另外 6 个周期(用于调用和 ret)。

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

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

【问题讨论】:

  • 您是要反转表中的值,还是使用表反转值?如果是后者,为什么不将表声明为常量:uint8_t const BitReverseTable[]?另外,为什么不简单地使用BitReverseTable[25] 而不是宏呢?
  • 1.它必须很快,所以表格位置的计算是关闭的。 2.它是哈佛架构,几乎没有内存,所以表必须在prgmem中。我会使用 pgm_read_byte,但我需要对齐技巧来加快速度。
  • 您也可以查看此解决方案(更简单且少于 20 个周期):stackoverflow.com/questions/61179846/…

标签: c gcc avr-gcc gcc4 winavr


【解决方案1】:

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

已编辑以修复 cmets 中的 OP (Thomas) 指出的问题:

  • Z寄存器的高字节是r31(我有r30r31交换)
  • 较新的 AVR,如 ATmega2560 也支持 lpm r,Z(仅限较旧的 AVR lpm r0,Z

感谢您的修复,Thomas!我确实有一块 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 中的参数,以及r31USING_REVERSE_BITS; 设置的表地址高字节。

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

avr-gcc-4.8.2 -O2 -fomit-frame-pointer -mmcu=atmega2560 -S 编译上面的代码会产生汇编源代码。 (我推荐使用-O2 -fomit-frame-pointer。) 省略 cmets 和普通指令:

    .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 没有害处(即使它是@ 的低部分987654351@ 寄存器),因为它将在下一次迭代开始时从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

这开始让我印象深刻:每个字节十个周期,四个周期用于设置,三个周期清理(brne 如果没有跳转,则只需要一个周期)。我在脑海中列出了循环计数,因此它们中可能存在小错误(这里或那里的循环)。 r26:r27X,函数的第一个指针参数在r24:r25 中提供,长度在r22

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

虽然 GCC 以多余的寄存器移动而臭名昭著,但我真的很喜欢它上面生成的代码。当然,总是有技巧的空间。对于重要的代码序列,我建议尝试不同的变体,甚至更改函数参数的顺序(或在本地范围内声明循环变量)等等,然后使用-S 进行编译以查看生成的代码。 AVR instruction timings 很简单,所以很容易比较代码序列,看看是否明显更好。我喜欢先删除指令和 cmets;它使程序集更易于阅读。


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

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

就个人而言,我建议在更新 avr-gcc 时重新编译上述测试代码,并查看生成的程序集(也许使用 sed 去除 cmets 和标签,并与已知的好版本进行比较?)。

问题?

【讨论】:

  • +1 - 这看起来很聪明。特别是拆分宏以加快数组转换。我仍然不确定你为什么使用 r0。我知道旧的 Atmel 处理器只有 LPM r0,Z,但后来的处理器(如 ATmega2560)也有 LPM rd,Z。所以你也可以使用 LPM r24,Z。或者我错过了什么(除了向下兼容性,我不需要)?我明天试试这个。
  • 根据您的建议,我注意到我的内联汇编存在一个根本缺陷。我误用了asmSymbolicName。它需要放在方括号 [] 中,以 % 开头。所以我误解了编译器的输出。我认为您在解决方案中混淆了r31r30r31ZH),这表明您必须即时编写和测试它。如果您更正此问题,我愿意接受您的回答,也许可以使用LPM r24, Z 来加快速度。我准备了自己的版本,我想这会给编译器更多的优化机会。如果我的测试成功,我会发布它。
  • @Thomas:我以为 ATmega2560 只支持lpm r0,Z,我不小心把r30r31 互换了;感谢您注意到这些。我只对代码进行了编译测试,抱歉之前没有提及。感谢cmets;我编辑了我的答案以反映它们。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-12-19
  • 1970-01-01
  • 1970-01-01
  • 2016-03-12
  • 1970-01-01
  • 2020-11-10
  • 2017-04-18
相关资源
最近更新 更多