如何使avr gcc确信全局字节数组的内存位置是常量
我正在为一个带有处理器的avr项目编写一个快速的“8位反转”例程。 我正在使用如何使avr gcc确信全局字节数组的内存位置是常量,c,gcc,avr-gcc,gcc4,winavr,C,Gcc,Avr Gcc,Gcc4,Winavr,我正在为一个带有处理器的avr项目编写一个快速的“8位反转”例程。 我正在使用 GNU C(20100110)版本4.3.3(avr)/由GNU C版本3.4.5(mingw vista special r3)、GMP版本4.2.3、MPFR版本2.4.1编译 首先,我创建了一个反向字节的全局查找表(大小:0x100): 这正如预期的那样有效。这就是我打算使用的宏,只需花费5个周期: #define BITREVERSE(x) (__extension__({
- GNU C(20100110)版本4.3.3(avr)/由GNU C版本3.4.5(mingw vista special r3)、GMP版本4.2.3、MPFR版本2.4.1编译
#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" \
); \
}))
使其编译(或不编译)的代码
这就是我从编译器中得到的错误:
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个周期(用于call和ret)
欢迎任何意见或建议
上bitreverse.c的完整源代码。
详细的编译器输出也在avr gcc(gcc)4.8.2上运行,但对我来说,它确实有一种独特的hacky回味 编辑以修复OP(Thomas)在评论中指出的问题:
寄存器的高位字节是Z
(我交换了r31
和r30
)r31
- 较新的AVR(如ATmega2560)也支持lpm r,Z(仅限较旧的AVR
)lpm r0,Z
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; })
我们的想法是使用r31
,以保持Z
寄存器对的高位字节。使用反向位的
macro在当前作用域中定义它,使用内联汇编有两个目的:避免不必要地将表地址的低位加载到寄存器中,并确保GCC知道我们已经在其中存储了一个值(因为它是一个输出操作数),而不知道该值应该是什么,因此,希望在整个范围内保留它
REVERSE\u BITS()
宏生成结果,告诉编译器它需要寄存器r30
中的参数,以及使用\u REVERSE\u BITS设置的表地址高位字节代码>在r31
中
听起来有点复杂,但那只是因为我不知道如何更好地解释它。这真的很简单
使用avr-gcc-4.8.2-O2-fomit帧指针-mmcu=atmega2560-S编译上述代码将生成程序集源代码。(我建议使用-O2-fomit帧指针
)
省略注释和常规指令:
.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的添加,情况不再如此。寄存器改变了,就是这样
如果您确定始终提供大于零的长度
,请使用
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:r27
是X
,函数的第一个指针参数在r24:r25
中提供,长度为r22
反向位表
位于正确的部分,并正确对齐。(.p2align 8
不对齐256字节;它指定低8位为零的对齐。)
尽管GCC因多余的寄存器移动而臭名昭著,但我真的很喜欢它上面生成的代码。当然,总有改进的余地;对于重要的代码序列,我建议尝试不同的变体,甚至更改函数参数的顺序(或在局部范围内声明循环变量),等等,然后使用-S
编译以查看生成的代码。AVR很简单,因此比较代码序列非常容易,以确定其中一个是否明显更好。我想先删除指令和评论;这样可以更容易地阅读部件
令人回味无穷的原因是,明确地说“定义这样一个寄存器变量不会保留寄存器;在流控制确定变量的值不活动的地方,它仍然可以用于其他用途”,我只是
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