【问题标题】:How does a person reverse the order of the lower 8 bits of an "int" value and leave the upper 8 bits the same?一个人如何颠倒“int”值的低 8 位的顺序并保持高 8 位相同?
【发布时间】:2019-04-30 17:45:55
【问题描述】:

我的应用程序要求我将值存储在 16 位计数器中,但由于 pcb 问题,它需要反转计数器的低 8 位(01001110 到 01110010)。代码是用 C (GCC) 编写的,计数器寄存器是“int”类型(16 位)。我的应用程序使用的是 Atmel ATtiny 8 位 MCU。我知道如果我将计数器寄存器声明为“int”类型,编译器将分配 2 个 RAM 位置。我只是用掩码提取低字节,然后重新排列这些位,然后用类似的东西将它们粘贴回去;

counter = counter & 0x00       clear lower byte value
counter = counter + (register with the reversed 8 bits)   

// Then, Replace lower byte value with new value

这应该有效吗? 谢谢

【问题讨论】:

  • 我就是这样做的。如果您的处理器具有反向位操作,那可能是最快的方法,否则,反向操作还不错。关于如何做到这一点,有多个关于 SO 的示例。
  • 仅供参考,您的按位和语句将清除所有位。如果是 16 位值,它应该类似于 counter &= 0xFF00
  • 和 0xFF00。加号有效,但与 OR 有太多相关性感觉更好。加号没有错……
  • 不要依赖 int 的任何特定大小 - 或使用有符号类型,使用 uint16_t
  • 不要在 SO 上发布“谢谢”答案(甚至是 cmets),这是一个问答,而不是讨论论坛。您通过接受和/或支持答案来表达对 SO 的赞赏。

标签: gcc int embedded microcontroller atmel


【解决方案1】:

您对过程的文字描述是正确的,但您的伪代码说明不准确且不完整。

你需要复制counter的LSB,然后再清除它;否则您将丢失需要反转的位。您需要正确清除 LSB,可以将 LSB 位反转回计数器 LSB,如下所示:

// Copy counter LSB
uint8_t lsb = (uint8_t)(counter & 0xFFu) ;

// Clear counter LSB
counter &= 0xff00u ;

// Reverse LSB bits and mask into counter LSB
for( uint8_t mask = 0x80u;  
     mask != 0; 
     lsb >>= 1, mask >>= 1 )
{
    counter |= ((lsb & 0x01u) != 0) ? mask : 0 ;
}

您还应该使用 stdint.h 类型 uint16_tuint8_t 进行此操作,而不是依赖 int 是任何特定大小 - 这将使代码在 int 的系统上更具可移植性和可测试性不是 16 位。通常在执行按位运算时应该使用无符号类型。

一种更快的方法是使用查找表,尽管可能需要更多的 ROM 空间。一个 256 字节的查找表生成起来相当麻烦,并且在 ATtiny 上在内存使用方面相当禁止。相反,它可以使用 16 字节查找几乎同样有效地完成,如下所示:

// Copy counter LSB
uint8_t lsb = (uint8_t)(counter & 0xFFu) ;

// Clear counter LSB
counter &= 0xff00u ;

static const uint8_t lookup[] = { 0x0, 0x8, 0x4, 0xC, 
                                  0x2, 0xA, 0x6, 0xE, 
                                  0x1, 0x9, 0x5, 0xD, 
                                  0x3, 0xB, 0x7, 0xF } ;

counter |= lookup[lsb & 0xf] << 4 | lookup[lsb >> 4] ;

您甚至可以打包查找表并仅使用 8 个字节(0x80、0xC4 等):

static const uint8_t lookup[] = { 0x80, 0xC4, 
                                  0xA2, 0xE6,  
                                  0x91, 0xD5,  
                                  0xB3, 0xF7 } ;

uint8_t msnib = ( lsb & 0x01 ) ? lookup[(lsb & 0xf) >> 1] >> 4 : 
                                 lookup[(lsb & 0xf) >> 1] & 0xf ;

uint8_t lsnib = ( lsb & 0x10 ) ? lookup[(lsb & 0xf0) >> 5] >> 4 : 
                                 lookup[(lsb & 0xf0) >> 5] & 0xf ;
counter |= (lsnib | msnib << 4) ;

但是查找表大小的减少不太可能通过代码大小的增加来证明其产生的额外位操作的合理性 - 而且它有点“太聪明了” - 需要一段时间才能做到正确!

第一种方法的优点是它可以应用于任意数量的位。两种查找表解决方案都可以扩展为 4 位的倍数的任何字长,而无需更改查找表大小,因此可以很好地扩展。


基准测试

我使用三种不同的优化设置在 https://godbolt.org/ 设置为 AVR GCC 4.6.4 测试了每个实现。指令计数不包括为使其可编译而添加的函数进入/退出代码,并且仅表示从该答案中的源代码生成的指令。

|          |  Instruction Count |               |
|Algorithm | No Opt | -O3 | -Os | + Data (bytes)|
|----------|:------:|:---:|:---:|:-------------:|
| Loop     |   38   |  88 |  23 |       0       |
| LookUp16 |   59   |  38 |  37 |      16       |
| LookUp8  |  137   |  65 |  62 |       8       |

测试几乎没有说明执行时间,但如果代码大小很关键,那么具有空间优化的循环算法 (-Os) 可能是最佳选择。

无论优化级别如何,查找表无疑更快,并且具有任一优化的 16 字节查找表可能是一个合理的平衡。对于-O3,它总体上比 88 指令展开循环更小、更快。它还有一个明显的优势,即代码大小的可变性要小得多,优化设置可以最大限度地减少在调试和发布版本之间切换时的意外。

8 字节查找没有什么优点,也许除了很有趣之外。

【讨论】:

  • 我同意 LUT 可能不是正确的方法。在大型微控制器上,它很少是适当的通用优化,因此在内存严重受限的 MCU 上几乎可以肯定是个坏主意。检查以确保您的编译器将优化位反转循环。可能,但不能保证。如果没有,您最好使用某种 SWAR 算法自己编写位操作代码。
  • @CodyGray 说:“我同意 LUT 可能不是正确的方法。”,但我很确定我没有这么说,所以这是一种观点而不是协议。我不会那么强调。它无疑会比“walking-one”算法更快,如果编译器通过展开循环来优化循环,查找表解决方案可能会更小。各有其可能的优势,解决方案应考虑到应用程序的空间/时间限制和要求,Richard 建议的 256 元素查找肯定令人望而却步。
  • @CodyGray :我添加了一些代码大小指标,表明 LUT 并非没有优点。
【解决方案2】:

你有一个错字:

counter = counter & 0x00       clear lower byte value

应该是

counter = counter & 0xFF00;

counter &= 0xFF00;

清除低字节。您可以通过一次将位旋转到另一个变量来反转位。如果时间很关键,您需要在汇编中执行此操作,因为 C 没有旋转运算符,并且必须模拟该特征,例如

new_byte = 0;

if (orig_byte & 0x80)
    new_byte |= 0x01;

if (orig_byte & 0x40)
    new_byte |= 0x02;
...

等等。可能是 C 语言中最快的方法之一。 或者如果您可以节省 256 字节的闪存,那么只需使用一个表格,例如

__flash unsigned char rotated_bytes[] = { 0x00, 0x80, 0x40, 0xC0, 0x20, ... };

new_byte = rotated_byte[orig_byte];

(将 __flash 替换为编译器的扩展关键字以表示“程序内存”)

【讨论】:

  • 你的查表值不正确,需要的操作无论如何都不是“轮换”。当问题中没有提到任何要求时,您还强调了性能 - 这是一个过早的优化。
  • 你说得对,我在写表格时脑子有问题。我开始写 0b10000000、0b01000000、0b11000000 等,不知何故我说,“让我们用小数来写,把它弄得一团糟。哎呀。ATTiny 有 2K 到 16K 的闪存,所以我相信优化最终会变得很重要,即使不是马上。此外,将空间与代码的权衡考虑为嵌入式编程中的“技巧包”是很好的。是的,我的意思是写“移出”位。
  • 也许,但内存限制建议 space 而不是 time 优化更有可能,并且您的展开循环或查找都不是代码空间高效的。您可能已经更正了查找 - 我已经为您修复了。
  • 一个好的折衷方案是对半字节(4 位)值进行 16 字节查找。
  • 使用八个ifs 的简单方法是最好的。 Atmel 带有很好的位操作指令。我的 gcc 将if (orig_byte &amp; 0x40) new_byte |= 0x02; 翻译成两个汇编指令! (SBRC:如果寄存器中的位被清除则跳过下一条指令,然后是OR指令)
【解决方案3】:

这是我的方法:

uint16_t Flipper(uint8_t hi, uint8_t reversed_lo)
{
    uint8_t lo=0;
    if (reversed_lo & 0x01) lo |= 0x80;
    if (reversed_lo & 0x02) lo |= 0x40;
    if (reversed_lo & 0x04) lo |= 0x20;
    if (reversed_lo & 0x08) lo |= 0x10;
    if (reversed_lo & 0x10) lo |= 0x08;
    if (reversed_lo & 0x20) lo |= 0x04;
    if (reversed_lo & 0x40) lo |= 0x02;
    if (reversed_lo & 0x80) lo |= 0x01;
    return (hi<<8) | lo;
}

我的编译器为此函数生成 25 条指令,成本为 50 字节:

;reversed_lo sits in R22
;hi          sits in R21
;lo          goes to R18
000007DF 60.fd                SBRC R22,0        Skip if bit in register cleared 
000007E0 02.c0                RJMP PC+0x0003        Relative jump 
000007E1 20.e0                LDI R18,0x00      Load immediate 
000007E2 01.c0                RJMP PC+0x0002        Relative jump 
000007E3 20.e8                LDI R18,0x80      Load immediate 
000007E4 61.fd                SBRC R22,1        Skip if bit in register cleared 
000007E5 20.64                ORI R18,0x40      Logical OR with immediate 
000007E6 62.fd                SBRC R22,2        Skip if bit in register cleared 
000007E7 20.62                ORI R18,0x20      Logical OR with immediate 
000007E8 63.fd                SBRC R22,3        Skip if bit in register cleared 
000007E9 20.61                ORI R18,0x10      Logical OR with immediate 
000007EA 64.fd                SBRC R22,4        Skip if bit in register cleared 
000007EB 28.60                ORI R18,0x08      Logical OR with immediate 
000007EC 65.fd                SBRC R22,5        Skip if bit in register cleared 
000007ED 24.60                ORI R18,0x04      Logical OR with immediate 
000007EE 66.fd                SBRC R22,6        Skip if bit in register cleared 
000007EF 22.60                ORI R18,0x02      Logical OR with immediate 
000007F0 66.23                TST R22       Test for Zero or Minus 
000007F1 0c.f4                BRGE PC+0x02      Branch if greater or equal, signed 
000007F2 21.60                ORI R18,0x01      Logical OR with immediate 
000007F3 30.e0                LDI R19,0x00      Load immediate 
000007F4 a9.01                MOVW R20,R18      Copy register pair 
000007F5 58.2b                OR R21,R24        Logical OR 
000007F6 ca.01                MOVW R24,R20      Copy register pair 
000007F7 08.95                RET       Subroutine return 

【讨论】:

    【解决方案4】:

    在字节中反转位的简单方法是使用联合和位域,如下所示。

    > struct ST_BYTE {
        unsigned char b0    :   1;
        unsigned char b1    :   1;
        unsigned char b2    :   1;
        unsigned char b3    :   1;
        unsigned char b4    :   1;
        unsigned char b5    :   1;
        unsigned char b6    :   1;
        unsigned char b7    :   1;
    
    } ;
    
    union  U_BYTE
    {
        struct ST_BYTE byteflag;
        unsigned char charflag;
    };
    unsigned char Reverse_Bits_in_Byte(U_BYTE local_byte)
    {
        U_BYTE Byte_Var2;
    
        Byte_Var2.byteflag.b0 =local_byte.byteflag.b7;
        Byte_Var2.byteflag.b1 =local_byte.byteflag.b6;
        Byte_Var2.byteflag.b2 =local_byte.byteflag.b5;
        Byte_Var2.byteflag.b3 =local_byte.byteflag.b4;
        Byte_Var2.byteflag.b4 =local_byte.byteflag.b3;
        Byte_Var2.byteflag.b5 =local_byte.byteflag.b2;
        Byte_Var2.byteflag.b6 =local_byte.byteflag.b1;
        Byte_Var2.byteflag.b7 =local_byte.byteflag.b0;
    
        return (Byte_Var2.charflag);
    
    }
    void main()
    {
    
        int i;
        for(i=0;i<8;i++)
        {
            Byte_Var1.charflag = pow(2,i);
            printf("\nBefore Reverse %02X\n", Byte_Var1.charflag);
            Byte_Var1.charflag = Reverse_Bits_in_Byte(Byte_Var1);
            printf("\nAfter Reverse %02X\n", Byte_Var1.charflag);
        }
    
    
    }
    

    请注意,虽然很简单,但不推荐给自己的reasons

    是否采用它是程序员的选择。

    【讨论】:

    • 使用位域的主要问题是位顺序是实现定义的。幸运的是,在这种特殊情况下,当 OP 要求反转位时,位顺序是大端还是小端都无关紧要:-)
    • 偶位访问也是实现定义的。有些可能只访问一个位,有些可能访问一个字节来修改一个位。我为简单的基本代码提出了这个逻辑。作者必须接听电话。
    猜你喜欢
    • 2021-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-11
    • 2021-06-10
    • 1970-01-01
    • 2017-04-15
    • 2013-12-01
    相关资源
    最近更新 更多