Assembly 如何安全地传递最终传递到内联程序集中的IO地址?

Assembly 如何安全地传递最终传递到内联程序集中的IO地址?,assembly,gcc,avr,Assembly,Gcc,Avr,我有一个来自AVR项目的简短汇编代码片段: 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

我有一个来自AVR项目的简短汇编代码片段:

    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)启用特定引脚(实际的物理微处理器引脚),然后将其关闭。上面的代码实际上可以完美地工作

但是,在上面的代码中,确切的pin码是硬编码的:端口B,pin码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);
}
当我这么说的时候,我的意思是我已经通过模拟器运行了这段代码,看到了引脚按预期的方式启动,另外我已经将这段代码与上面的程序集配对,并在硬件上运行它。因此,很明显,
*(addr->port)&=…
正在寻址pin本身,而不是指针。酷

但当我执行此操作时,会出现一个汇编错误:

        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参数替换为
\u SFR\u IO\u addr(addr->port)
,也会发生这种情况

SFR\u IO\u 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(忽略编译器选择的确切寄存器),最终组装应为:


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

汇编约束(或者,对于-O1/-O2,一个常量表达式),因此很遗憾,您无法将其作为参数传递。

汇编约束(或者,对于-O1/-O2,一个常量表达式)很遗憾,您无法将其作为参数传递。

经过一天的研究,我找到了以下答案:

        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
约束,这实际上意味着“操作数可以是任何东西”。这是次优的,但汇编程序不接受一些更明显的选项(“操作数是不可重定位的内存地址”)

此外,我从
in
out
汇编指令(正如Ross Ridge在上面的注释中指出的那样)切换到了
ld
st
指令,它们接受内存地址

最后,我必须更改一些T1H_NOOP、T1L_NOOP、T0H_NOOP和T0L_NOOP宏中NOP指令的数量,以保持所需的定时约束


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

经过一天的研究,我发现了以下答案:

        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
约束,这实际上意味着“操作数可以是任何东西”。这是次优的,但汇编程序不接受一些更明显的选项(“操作数是不可重定位的内存地址”)

此外,我从
in
out
汇编指令(正如Ross Ridge在上面的注释中指出的那样)切换到了
ld
st
指令,它们接受内存地址

最后,我必须更改一些T1H_NOOP、T1L_NOOP、T0H_NOOP和T0L_NOOP宏中NOP指令的数量,以保持所需的定时约束


所有这些都很痛苦,以至于我读了一些关于脉冲宽度调制和带中断的定时器/时钟电路的书,因为感觉它会产生比我的忙循环更可靠的定时。但是,添加中断处理程序会增加我的代码复杂性,这是我目前还不准备处理的。

您的基本问题是,使用IO地址的指令采用的是立即操作数,因此需要在编译时进行计算,但地址通常只能在运行时确定。您要么需要将IO地址设置为一个常量,该常量始终可以在编译时进行计算,要么使用一条指令,将寄存器或内存操作数作为IO地址。请注意,IN和OUT指令的地址空间与C指针使用的地址空间不同。您需要减去0x20才能将内存地址转换为IO地址。注意,这里实际上不需要使用内联汇编。当使用的内存地址是编译时常量时,GCC可以生成输入和输出指令,并在转换后映射到有效的IO地址。事实上,编译器可以生成比您更好的汇编代码:如果您需要精确控制每条指令,除了我在上面的链接中给出的示例
t1h\u noop
delay函数之外,您还应该使用常规汇编。您可以查看GCC为使用指针的C代码段生成的汇编。然后,您可以在汇编中编写相同类型的内容。您的基本问题是,您将IO地址与接受立即操作数的指令一起使用,因此需要在编译时进行计算,但通常只能在运行时确定地址。您要么需要将IO地址设置为一个常量,该常量始终可以在编译时进行计算,要么使用一条指令,将寄存器或内存操作数作为IO地址。请注意,IN和OUT指令的地址空间与C指针使用的地址空间不同。您需要减去0x20才能将内存地址转换为IO地址。注意,这里实际上不需要使用内联汇编。GCC可以生成输入和输出指令