我如何安全地传递最终传递到内联汇编的 IO 地址?

How do I safely pass around an IO address that eventually gets passed into inline assembly?

我有一个来自 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) 启用特定引脚(实际物理微处理器引脚),然后将其关闭。上面的代码实际上可以完美运行。

但是,在上面的代码中,确切的引脚是硬编码的: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);
}

当我这么说时,我的意思是我已经通过模拟器 运行 看到引脚按预期动作,另外我已经将这个片段与上面的组件配对 运行 它在硬件上。所以,很明显,*(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 地址传递给我的汇编代码?

"I" 程序集约束 requires it's operand to be a constant(或者,使用 -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 约束,这实际上意味着“操作数可以是任何东西”。这是sub-optimal,但是汇编器没有接受一些比较明显的(“操作数是一个non-relocatable内存地址”)。

此外,我从 inout 汇编程序指令切换,正如 Ross Ridge 在上面的评论中指出的那样,需要一个必须在编译时知道的立即地址,并且到ldst 指令,接受内存地址。

作为最后的说明,我必须按顺序更改某些 T1H_NOOP、T1L_NOOP、T0H_NOOP 和 T0L_NOOP 宏中的 NOP 指令数维持 neopixel signalling protocol 要求的时序约束。

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