您对过程的文字描述是正确的,但您的伪代码说明不准确且不完整。
你需要复制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_t 和 uint8_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 字节查找没有什么优点,也许除了很有趣之外。