【问题标题】:How do I safely pass around an IO address that eventually gets passed into inline assembly?如何安全地传递最终传递到内联汇编的 IO 地址?
【发布时间】:2020-08-23 14:36:50
【问题描述】:

我有一个来自 AVR 项目的简短程序集 sn-p:

    uint8_t high = _BV(0);
    uint8_t low = ~high;
    uint8_t port_value = 0;
    asm volatile (
        "in %0, %1   \n\t"
        "or %0, %3   \n\t"
        "out %1, %0  \n\t"
        T1H_NOOP
        "and %0, %2   \n\t"
        "out %1, %0   \n\t"
        T1L_NOOP
    : "=r" (port_value)
    : "I" (_SFR_IO_ADDR(PORTB)), "r" (low), "r" (high));

此块背后的想法是在短时间内启用特定引脚(实际物理微处理器引脚)(T1H_NOOP),然后将其关闭。上面的代码实际上完美无缺。

但是,在上面的代码中,确切的引脚是硬编码的:PORTB,引脚 0 (_BV(0))。我想要的是传递这样一个地址:

struct IO_ADDR {
    volatile uint8_t *port;
    uint8_t pin
}

只要我保留在 C 代码中,这确实有效。

    struct IO_ADDR addr = { .port = &PORTB, .bit = 0 };
    latch(&addr);

void latch(struct IO_ADDR *addr) {
    if (addr->bit >= 8) return;
    *(addr->port) &= ~(_BV(addr->bit));
    _delay_us(50);
}

当我这么说的时候,我的意思是我已经通过模拟器运行它并且看到引脚按预期启动,另外我已经将这个 sn-p 与上面的组件配对并在硬件上运行它。所以,很明显,*(addr->port) &= ... 正在寻址引脚本身,而不是指针。很酷。

但是当我这样做时,我得到一个组装错误:

        asm volatile (
            "in %0, %1   \n\t"
            "or %0, %3   \n\t"
            "out %1, %0  \n\t"
            T1H_NOOP
            "and %0, %2   \n\t"
            "out %1, %0   \n\t"
            T1L_NOOP
        : "=r" (port_value)
        : "I" (_SFR_IO_ADDR(*(addr->port))), "r" (low), "r" (high));

这个错误:

/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c: In function 'write_value':
/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c:29:9: warning: asm operand 1 probably doesn't match constraints
   29 |         asm volatile (
      |         ^~~
/nix/store/j31yaksw2dh82by2lgz1ysgh494cz6j2-src/neopixels.c:29:9: error: impossible constraint in 'asm'

如果我将 addr->port 参数替换为 _SFR_IO_ADDR(addr->port),也会发生这种情况。

SFR_IO_ADDR(*(addr->port)) 对此进行预处理:

        : "I" (
# 38 "src/neopixels.c" 3 4
              (((uint16_t) &(
# 38 "src/neopixels.c"
              *(addr->port)
# 38 "src/neopixels.c" 3 4
              )) - 0x20)
# 38 "src/neopixels.c"
              )

PORTB 的情况下,最终程序集应该是这个特定硬件上的地址 0x24(并忽略编译器选择的确切寄存器):

    in r18, 24   
    or r18, r21   
    out 24, r18  

我需要做什么才能将该特定 IO 地址传递给我的汇编代码?

【问题讨论】:

  • 您的基本问题是您使用的 IO 地址和指令需要立即操作数,因此需要在编译时进行评估,但一般只能在运行时确定地址.您要么需要使 IO 地址成为始终可以在编译时评估的常量,要么使用将寄存器或内存操作数作为 IO 地址的指令。请注意,IN 和 OUT 指令的地址空间不同于 C 指针使用的地址空间。需要减去 0x20 才能将内存地址转换为 IO 地址。
  • 注意这里真的没有必要使用内联汇编。当使用的内存地址是编译时常数时,GCC可以生成IN和OUT指令,并在转换后映射到有效的IO地址。
  • 确实编译器可以生成比你更好的汇编代码:gcc.godbolt.org/z/rKshaM
  • 如果您需要对每条指令进行精确控制,除了我在上面链接中给出的示例 t1h_noop 延迟函数之外,那么您应该使用常规汇编。
  • 您可以查看 GCC 为您展示的使用指针的 C 代码 sn-p 生成的程序集。然后你可以在汇编中编写相同类型的东西。

标签: assembly gcc avr


【解决方案1】:

"I" 装配约束 requires it's operand to be a constant(或者,使用 -O1/-O2,一个常量表达式)很遗憾,您将无法将其作为参数传递。

【讨论】:

    【解决方案2】:

    经过一天的研究,我找到了这个答案:

            asm volatile (
                "ld %0, %1  \n\t"
                "or %0, %3  \n\t"
                "st %1, %0 \n\t"
                T1H_NOOP
                "and %0, %2 \n\t"
                "st %1, %0 \n\t"
                T1L_NOOP
            : "=r" (port_value)
            : "X" (*(addr->port)), "r" (low), "r" (high));
    

    涉及两个关键点。其中之一是提供X 约束而不是I 约束,这实际上意味着“操作数可以是任何东西”。这是次优的,但汇编器不接受一些更明显的(“操作数是不可重定位的内存地址”)。

    此外,我从 inout 汇编指令切换到 ld 和 @987654329 @ 指令,接受内存地址。

    最后一点,我不得不更改某些 T1H_NOOP、T1L_NOOP、T0H_NOOP 和 T0L_NOOP 宏中的 NOP 指令数量,以维持neopixel signalling protocol 所需的时序约束。

    所有这些都非常痛苦,以至于我阅读了一些关于脉冲宽度调制和带中断的定时器/时钟电路的文章,因为感觉它会比我的活跃繁忙循环产生更可靠的时序。但是,添加中断处理程序会以我现在不准备处理的方式增加我的代码复杂性。

    【讨论】:

    • 如果地址可以是一个常数,你可以把这段代码包装在if __builtin_constant_p中。然后有两种变体,一种是I,另一种是X。由于此条件在编译时已解决,因此它应该为您提供两全其美的效果。请注意查看docs,它指出了诸如内联函数如何无法正确检测常量之类的事情,除非您启用优化(这是有道理的)。
    • 我相信在某些情况下这肯定是编译时常量。我碰巧有一个情况,它没有。你认为这个选项会节省任何程序空间吗?
    • 很难绝对地谈论优化器可能会做什么。但我希望像if __builtin_constant_p do I else do X 这样的构造会优化您所期望的方式。没有理由(在优化的版本中)将这两种情况都包括在内。使用“I”是否比使用“X”“更小”?不能说,我不太了解 AVR。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多