assembly 使用程序集将数据移动到__uint24

tzcvj98z  于 2023-04-21  发布在  其他
关注(0)|答案(2)|浏览(92)

我最初有以下C代码:

volatile register uint16_t counter asm("r12");

__uint24 getCounter() {
  __uint24 res = counter;
  res = (res << 8) | TCNT0;
  return res;
}

这个函数在一些热门的地方运行,并且是内联的,我试图将很多东西塞进一个ATtiny13中,所以是时候优化它了。
该函数编译为:

getCounter:
        movw r24,r12
        ldi r26,0
        clr r22
        mov r23,r24
        mov r24,r25
        in r25,0x32
        or r22,r25
        ret

我想出了这个组合:

inline __uint24 getCounter() {
  //__uint24 res = counter;
  //res = (res << 8) | TCNT0;
  
  uint32_t result;
  asm(
    "in %A[result],0x32" "\n\t"
    "movw %C[result],%[counter]" "\n\t"
    "mov %B[result],%C[result]" "\n\t"
    "mov %C[result],%D[result]" "\n\t"
    : [result] "=r" (result)
    : [counter] "r" (counter)
    :
  );
  return (__uint24) result;
}

使用uint32_t的原因是为了“分配”第四个连续的寄存器,编译器可以理解它是被破坏的(因为我不能在破坏列表中做类似"%D[result]"的事情)
我的汇编是正确的吗?从我的测试来看似乎是正确的。有没有一种方法可以让编译器更好地优化getCounter(),这样就不需要混淆汇编了?有没有更好的方法在汇编中做到这一点?

EDITmovw的整个想法是保持读取原子,因为counter变量在中断内部递增。

kmbjn2e3

kmbjn2e31#

从我在GodBolt中的实验来看,即使使用-O3标志,avr-gcc优化器也不够复杂。我怀疑是否有任何其他标志可以欺骗它进行更多优化(我尝试了一些,但没有帮助)。因此,您使用内联汇编的方法确实看起来是合理的。

原代码分析

  1. counter变量存储在r12(LSB)和r13(MSB)寄存器中。
  2. TCNT0从I/O空间地址0x 32读取(通过in Rd, 0x32指令)。
    1.根据avr-gcc ABI,在r22(LSB):r23:r24(MSB)中返回24位值。
    1.因此,总结一下,我们希望发生以下转移:
r24 <-- r13
r23 <-- r12
r22 <-- TCNT0

更新方案

查看代码,我猜你有某种定时器中断,当定时器达到某个上限阈值时,它会递增counter。如果是这种情况,即使在纯C版本中,代码也有更深层次的问题。重要的是,TCNT0counter读取应该是原子的!否则,如果中断发生在movwin指令之间,则您的结果将不准确。例如:

counter = 0x0010, TCNT0 = 0xff
MOVW copies 0x0010
Interrupt occurs => handler sets counter = 0x0011 and TCNT0 = 0 
IN instruction reads TCNT0 = 0
result = 0x0010_00 (instead of 0x0010_ff)

有两种方法可以解决此问题:
1.将CLI / SEI Package 在两个读取周围,以将它们放在一起,而不可能在中间中断。
1.读取TCNT0两次,在阅读计数器之前和之后。如果第二次读取的结果较小,则意味着发生了中断,我们不能信任这些值,请重复读取。
因此,没有bug的正确解决方案可能是这样的(根据需要添加内联规范):

__uint24 getCounter() {
  union
  {
    __uint24 result;

    struct {
      uint8_t lo;
      uint16_t hi;
    } parts;
  } u;

  __builtin_avr_cli();
  u.parts.hi = counter;
  u.parts.lo = TCNT0;
  __builtin_avr_sei();

  return u.result;
}

制作:

getCounter:
        cli
        mov r23,r12
        mov r24,r13
        in r22,0x32
        sei
        ret

雷霆:https://godbolt.org/z/YrWrT8sT4

新方案

添加了原子性要求后,我们必须使用movw指令。下面是一个版本,它最大限度地减少了内联汇编的数量,并尽可能多地使用C:

__uint24 getCounter() {
  union
  {
    __uint24 result;

    struct {
      uint8_t lo;
      uint16_t hi;
    } parts;
  } u;

  uint16_t tmp;

  // Ensure the counter is read atomically with movw instruction
  asm("movw %C[tmp],%[counter]  \n\t" : [tmp] "=r" (tmp) : [counter] "r" (counter));

  u.parts.hi = tmp;
  u.parts.lo = TCNT0;

  return u.result;
}

雷霆:https://godbolt.org/z/P9a9K6n76

旧方案(无原子性)

提供了组装分析

它看起来是正确的,并提供了正确的结果。然而,有两件事我可以建议改进:
1.它有3条mov指令,需要3个周期来执行。gcc生成了类似的代码,因为movw只在均匀对齐的寄存器上运行。但是你可以用2条mov指令来替换这些指令,并且它也将消除对更大的uint32_t变量的需要。
1.我会避免硬编码TCNT0地址,以获得更好的代码可移植性。

建议组装

下面是代码的一个稍微修改的版本:

inline __uint24 getCounter() {
  __uint24 result;
  asm(
    "in   %A[result], %[tcnt0]"    "\n\t"
    "mov  %B[result], %A[counter]" "\n\t"
    "mov  %C[result], %B[counter]" "\n\t"
    : [result] "=r" (result)
    : [counter] "r" (counter)
    , [tcnt0] "I" (_SFR_IO_ADDR(TCNT0))
  );
  return result;
}

然而,请注意这个解决方案的一个缺点-我们在阅读计数器时失去了原子性。如果两个mov指令之间发生中断,并且counter在中断中被修改,我们可能会得到正确的结果。但是如果counter从未被中断修改过,我宁愿使用两个单独的mov指令来提高性能。

Godbolt:https://godbolt.org/z/h3nT4or97(我删除了inline关键字以显示生成的程序集)

afdcj2ne

afdcj2ne2#

你将在R13:R12中读取counter的值,所以你需要两个MOV和一个IN来读取TCNT0。所以使用内联汇编的工作版本是:

#include <avr/io.h>

register uint16_t counter asm("r12");

static inline __attribute__((__always_inline__))
__uint24 getCounter (void)
{
    __uint24 result;

    __asm ("mov %B0, %A1" "\n\t"
           "mov %C0, %B1"
           : "=r" (result)
           : "r" (counter), "0" (TCNT0));

    return result;
}

关于该解决方案的一些说明:

  • 最大内联是通过static inline和always_inline实现的。
  • TCNT0是在C/C++代码中读取的,而不是在汇编中读取的,因此编译器可以选择最佳指令来读取该SFR(INLDS,具体取决于arch)。
  • GCC会将读取TCNT0的reg分配给与result相同的寄存器。由于avr-gcc ABI是小端字节序,因此它会分配给result的LSB。这在GCC内联汇编中没有问题,即使TCNT0result具有不兼容的类型。
  • count这样的全局寄存器变量不能是volatile的,GCC会发出警告:
warning: optimization may eliminate reads and/or writes to register variables [-Wvolatile-register-var]
volatile register uint16_t counter asm("r12");
^~~~~~~~

Reason是历史表示,REG的内部表示甚至没有volatile属性。因此,您可能需要重新考虑代码。例如,像while (counter != 0) ...这样的循环可能无法实现预期的功能。

  • 使用像counter这样的全局寄存器变量有一些注意事项:对于每个模块/编译单元,编译器必须知道它不能将变量分配给一些可以自由使用的寄存器。因此,您可以在每个模块中包含counter的decl,包括那些甚至不使用counter的模块。或者更好的是,使用-ffixed-12 -ffixed-13编译所有模块。为了减少对调用约定的干扰,最好使用R2而不是R12。请注意,R12可能用于传递参数,libc / libgcc中的代码也可能使用R12,因为这些库无法知道R12(或R2)是禁止的。

使用上面的代码并显示生成的程序集的示例是使用-Os -save-temps编译以下代码。

void f (int, __int24);

int main (void)
{
    f (0, getCounter() /* in R22:R20 */);
}
  • .s将读取:
main:
    in r20,0x32
/* #APP */
    mov r21, r12
    mov r22, r13
/* #NOAPP */
    ldi r25,0
    ldi r24,0
    rcall f
    ...

原子阅读counter

正如在评论中提到的,counter应该被原子地读取。使用movw是1个tick,因此比cli/sei序列更快。使用24位变量就足够了。虽然我不确定少一个寄存器是否会有区别。无论如何,下面是一个使用movw的解决方案。SFR在程序集中读取,因此它变得不稳定:

static inline __attribute__((__always_inline__))
__uint24 getCounter (void)
{
    __uint24 result;
    __asm volatile ("movw %A0, %A1" "\n\t" // Atomic read of counter.
                    "mov  %C0, %B0" "\n\t"
                    "mov  %B0, %A0" "\n\t"
                    "in   %A0, %i2"
                    : "=r" (result)
                    : "r" (counter), "n" (&TCNT0));
    return result;
}

请注意,内联汇编操作数打印修饰符i是在v4.7中引入的,与__uint24的版本相同;所以不要对%i挠头。

相关问题