使用汇编将数据移动到 __uint24

问题描述 投票:0回答:2

我原来有如下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()
,这样就不需要混淆汇编了? 有没有更好的方法在装配中做到这一点?

编辑

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

c assembly avr inline-assembly avr-gcc
2个回答
2
投票

从我在 GodBolt 中的实验来看,即使有

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

原码分析

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

提供装配分析

它看起来正确并提供正确的结果。但是,我可以建议改进两件事:

  1. 它有3条
    mov
    指令,需要3个周期来执行。 gcc 生成了类似的代码,因为
    movw
    仅在均匀对齐的寄存器上运行。但是你可以用 2
    mov
    指令替换它们,它也将消除对更大的
    uint32_t
    变量的需要。
  2. 我会避免硬编码
    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
关键字以显示生成的程序集)


2
投票

你会读取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;
}

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

  • 最大内联是通过静态内联和 always_inline 实现的。

  • TCNT0
    在 C/C++ 代码中读取,而不是在程序集中读取,因此编译器可以选择最佳指令来读取该 SFR(
    IN
    LDS
    取决于架构)。它也更方便,因为不需要来自 AVR-LibC 的
    __SFR_IO_ADDR
    gobbledegook。

  • GCC 会将读取

    TCNT0
    的寄存器分配给与
    result
    相同的寄存器。由于avr-gcc ABI是little endian,所以会分配到
    result
    的LSB。即使
    TCNT0
    result
    具有不兼容的类型,这对于 GCC 内联汇编来说都很好。

  • 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
    的声明,包括那些甚至不使用
    counter
    的模块。或者更好的是,使用
    -ffixed-12 -ffixed-13
    编译所有模块。为了减少对 calling convention 的干扰,最好使用 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
    ...
© www.soinside.com 2019 - 2024. All rights reserved.