如何反转一个字节

时间:2013-03-15 10:35:46

标签: assembly byte reverse avr atmega

我目前正在开发一个项目,而且我必须颠倒一个字节的顺序。我目前正在使用AVR Studio Mega32微控制器。

例如:

0000 0001 becomes 1000 0000
0001 0110 becomes 0110 1000
1101 1001 becomes 1001 1011

首先,我有这个:

ldi r20,0b00010110

反转字节的最简单方法是什么,以便r20变为01101000?

5 个答案:

答案 0 :(得分:3)

这是一个片段 - 它是为GNU工具链(avr-gcc,binutils,avr-libc等)编写的 - 但它应该很容易适应:

static inline __attribute__ ((always_inline))
uint8_t avr_reverse_byte (uint8_t x)
{
    x = ((x & 0x55) << 1) | ((x & 0xaa) >> 1);
    x = ((x & 0x33) << 2) | ((x & 0xcc) >> 2);

    /* x = ((x & 0x0f) << 4) | ((x & 0xf0) >> 4); */

    __asm__ ("swap %0" : "=r" (x) : "0" (x)); /* swap nibbles. */

    return x;
}

因此,除了使用swap指令实现的最终hi-lo半字节交换外,对'C'代码的改进并不大。

答案 1 :(得分:2)

我刚才不能提供AVR代码。但一般的位反转技术如下:

abcd efgh   p
badc fehg   p = ((p and 0AAh) shr 1) or ((p shl 1) and 0AAh)
dcba hgfe   p = ((p and 033h) shr 2) or ((p shl 2) and 033h)
hgfe dcba   p = ((p and 00Fh) shr 4) or ((p shl 4) and 0F0h)

答案 2 :(得分:2)

另一个简单的方法是使用进位标志:

重复8x:

lsl r20 ; shift one bit into the carry flag
ror r0  ; rotate carry flag into result 

r20中的输入,r0中的输出,r20的内容已被销毁;寄存器可以自由更改。)

这使用16个指令@ 2个字节,每个1个周期= 32个字节的程序存储器,16个周期在完全“展开”时反转一个字节。包装在一个循环中,代码大小可以减少,但执行时间会增加。

答案 3 :(得分:1)

针对字节的两半的4位(16项)查找表似乎是一个不错的折衷(就像@Aki在评论中指出的那样)。

AVR指令每个为2个字节,因此一个16字节的表与8个指令占用相同的空间。 (事实证明,速度或大小不值得,除非您可以将数组对齐256个字节以使索引比gcc便宜得多。)

可以使用每个字节的高低一半来打包LUT。但这会比在表大小(8字节= 4条指令)方面节省更多的钱(在索引的第4位上使用分支在屏蔽前有条件地进行SWAP)要花费更多的钱。


让我们比较一下AVR GCC的功能。在编译Brett的代码时,gcc4.6令人惊讶地错过了优化(实际上是提升为int,而没有充分利用将结果截断为uint8_t的优势)。具有讽刺意味的是,它使用SWAP指令确实x<<4 | x>>4优化为旋转。 (AVR没有多计数轮换指令,正常轮换是随身携带。移位仅可用于每条指令一次计数。)

#include <stdint.h>

uint8_t reverse_byte_alu (uint8_t x)
{
    uint8_t xeven = x & 0x55,  xodd = x & 0xaa;
    x = (xeven << 1) | (xodd >> 1);  // swap adjacent bit pairs

    xeven = x & 0x33,  xodd = x & 0xcc;
    x = (xeven << 2) | (xodd >> 2);  // swap adjacent 2-bit chunks

    x = ((x << 4) | (x >> 4));  // 4-bit rotate is recognized as SWAP
    return x;
}

compiles into this asm with gcc4.6 -O3 (Godbolt compiler explorer) 。我没有看到任何错过的优化。

reverse_byte_alu:
    mov r25,r24
    andi r25,lo8(85)
    lsl r25
    andi r24,lo8(-86)
    lsr r24
    or r25,r24              # swap of adjacent bits done

    mov r24,r25
    andi r24,lo8(51)
    lsl r24
    lsl r24                 # << 2
    andi r25,lo8(-52)
    lsr r25
    lsr r25                 # >> 2
    or r24,r25              # swap pairs of bits done

    swap r24                # swap nibbles
    ret

16条指令,每条指令2个字节,全部1个周期。 (ret除外)https://www.microchip.com/webdoc/avrassembler/avrassembler.wb_instruction_list.html

因此,这比@JimmyB的答案稍好。@ JimmyB的答案需要16个单周期指令,而不包含ret。 (但是可以将其汇总为一个小循环)。


数组索引在AVR中并不便宜。寻址方式的唯一选择是后递增或递减,无立即移位。因此必须将16位数组地址添加到4位半字节值中。如果您的数组位于地址空间的低256字节中,这可能会更便宜。或者,如果您的数组是256字节对齐的,则只需设置指针寄存器的高字节并将查找值放在低字节即可。 (gcc缺少此优化)。

利用gcc在16个字节的边界上对齐数组可使地址计算更便宜,但指令总数为18,其中一些是多周期指令。

__attribute__((aligned(16)))  // makes indexing cheaper
static const uint8_t reverse_nibble[16] = {
        0,      0b1000, 0b0100, 0b1100,
        0b0010, 0b1010, 0b0110, 0b1110,
        0b0001, 0b1001, 0b0101, 0b1101,
        0b0011, 0b1011, 0b0111, 0b1111
        };

uint8_t reverse_byte_nibble_LUT (uint8_t x) {
    uint8_t hi = reverse_nibble[x>>4];
    hi = ((hi << 4) | (hi >> 4));  // SWAP instead of SWAP+AND for just a shift
    uint8_t lo = reverse_nibble[x & 0x0f];
    return hi | lo;
}

编译为17条指令和LD is a 2-cycle instruction when accessing FLASH with a non-increment/decrement addressing mode。 (在某些CPU上,当不访问闪存或内部SRAM时为1个周期。)

  # gcc4.6 output, not optimal
    mov r26,r24
    swap r26
    andi r26,lo8(15)
    ldi r27,lo8(0)
    subi r26,lo8(-(reverse_nibble))   # AVR doesn't have add-immediate byte, only sub
    sbci r27,hi8(-(reverse_nibble))   # X register = (x>>4) - (-LUT_base)
    ld r25,X
    swap r25
    mov r30,r24
    ldi r31,lo8(0)
    andi r30,lo8(15)
    andi r31,hi8(15)                 # missed opt, this is a double-redundant 0 & 0
    subi r30,lo8(-(reverse_nibble))
    sbci r31,hi8(-(reverse_nibble))
    ld r24,Z
    or r24,r25
    ret

ldi r27, 0 / sbci r27可能是错过的优化。使用16字节的对齐表,我们无法进位高字节。我认为我们可以做到:

    # generate r30 = x&0x0f
    subi r30,lo8(-(reverse_nibble))   # ORI would work, too.  no-op with 256-byte aligned table
    ldi  r31,hi8(reverse_nibble)      # reuse this for hi and lo

因此,通过更好的索引可以在速度上领先一步,但是总大小(代码+表)肯定看起来更糟。

答案 4 :(得分:1)

稍作调整,便可以从注释中的代码片段中获得额外的性能。

当我们确保将LUT对齐到16字节边界时,可以通过异或生成地址。同样,我们可以通过索引对表进行XOR,从而允许对参数x进行就地修改。我已经注释掉了GCC生成的不必要的指令。

__attribute__((aligned(16)))  // makes indexing cheaper
static const uint8_t reverse_nibble_xor[16] = {
        0 ^ 0,  1 ^ 0b1000, 2 ^ 0b0100, 3 ^ 0b1100,
        4 ^ 0b0010, 5 ^ 0b1010, 6 ^ 0b0110, 7 ^ 0b1110,
        8 ^ 0b0001, 9 ^ 0b1001, 10 ^ 0b0101, 11 ^ 0b1101,
        12 ^ 0b0011, 13 ^ 0b1011, 14 ^ 0b0111, 15 ^ 0b1111
        };


uint8_t reverse_ams(uint8_t x)
{
    uint8_t *p = (uint8_t *)((uint16_t)reverse_nibble_xor ^ (x & 15));
    x ^= p[0];
    x = ((x << 4) | (x >> 4));
    uint8_t *q = (uint8_t *)((uint16_t)reverse_nibble_xor ^ (x & 15));
    x ^= q[0];
    return x;
}

reverse_ams:
        ldi r18,lo8(reverse_nibble)
//      ldi r19,hi8(reverse_nibble)
        ldi r31,hi8(reverse_nibble)  // use r31 directly instead of r19
        mov r30,r24
//        ldi r31,lo8(0)
        andi r30,lo8(15)
//        andi r31,hi8(15)
        eor r30,r18
//        eor r31,r19
        ld r25,Z
        eor r25,r24
        swap r25
        mov r30,r25
//        ldi r31,lo8(0)
        andi r30,lo8(15)
//        andi r31,hi8(15)
        eor r30,r18
//        eor r31,r19
        ld r24,Z
        eor r24,r25
        ret