【问题标题】:Loading 16-bit (or bigger) immediate with a Arm inline GCC assembly使用 Arm 内联 GCC 程序集立即加载 16 位(或更大)
【发布时间】:2021-08-17 17:58:29
【问题描述】:

注意:为了简洁起见,这里的示例被简化了,因此它们不能证明我的意图。如果我只是像示例中一样写入内存位置,那么 C 将是最好的方法。但是,我正在做的事情在这个例子中我不能使用 C,即使通常最好留在 C 中。

我正在尝试使用值加载寄存器,但我坚持使用 8 位立即数。

我的代码:

https://godbolt.org/z/8EE45Gerd

#include <cstdint>

void a(uint32_t value) {
    *(volatile uint32_t *)(0x21014) = value;
}

void b(uint32_t value) {
    asm (
        "push ip                                \n\t"
        "mov ip,       %[gpio_out_addr_high]    \n\t"
        "lsl ip,       ip,                   #8 \n\t"
        "add ip,       %[gpio_out_addr_low]     \n\t"
        "lsl ip,       ip,                   #2 \n\t"
        "str %[value], [ip]                     \n\t"
        "pop ip                                 \n\t"
        : 
        : [gpio_out_addr_low]  "I"((0x21014 >> 2)     & 0xff),
          [gpio_out_addr_high] "I"((0x21014 >> (2+8)) & 0xff),
          [value] "r"(value)
    );
}

// adding -march=ARMv7E-M will not allow 16-bit immediate
// void c(uint32_t value) {
//     asm (
//         "mov ip,       %[gpio_out_addr]    \n\t"
//         "str %[value], [ip]                     \n\t"
//         : 
//         : [gpio_out_addr]  "I"(0x1014),
//           [value] "r"(value)
//     );
// } 


int main() {
    a(20);
    b(20);
    return 0;
}

当我编写 C 代码(参见 a())时,它会在 Godbolt 中组装成:

a(unsigned char):
        mov     r3, #135168
        str     r0, [r3, #20]
        bx      lr

我认为它使用MOV 作为伪指令。当我想在汇编中做同样的事情时,我可以将值放入某个内存位置并使用LDR 加载它。我认为这就是我使用 -march=ARMv7E-M 时 C 代码的组装方式(MOVLDR 替换),但是在许多情况下这对我来说并不实用,因为我会用它来做其他事情.

在 0x21014 地址的情况下,前 2 位为零,因此当我正确移位时,我可以将这个 18 位数字视为 16 位,这就是我在 b() 中所做的,但仍然我必须用 8 位立即数传递它。但是,在 Keil 文档中,我注意到提到了 16 位立即数:

https://www.keil.com/support/man/docs/armasm/armasm_dom1359731146992.htm

https://www.keil.com/support/man/docs/armasm/armasm_dom1361289878994.htm

在 ARMv6T2 及更高版本中,ARM 和 Thumb 指令集都包括:

A MOV instruction that can load any value in the range 0x00000000 to 0x0000FFFF into a register.
A MOVT instruction that can load any value in the range 0x0000 to 0xFFFF into the most significant half of a register, without altering

最低有效一半的内容。

我认为我的 CortexM4 应该是 ARMv7E-M 并且应该满足这个“ARMv6T2 及更高版本”的要求并且应该能够使用 16 位立即数。

但是从 GCC 内联汇编文档中我没有看到这样的提及:

https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html

当我启用 ARMv7E-M 架构并取消注释 c() 时,我使用常规“I”立即然后我得到一个编译错误:

<source>: In function 'void c(uint8_t)':
<source>:29:6: warning: asm operand 0 probably doesn't match constraints
   29 |     );
      |      ^
<source>:29:6: error: impossible constraint in 'asm'

所以我想知道有没有办法在 GCC 内联汇编中使用 16 位立即数,还是我遗漏了什么(这会使我的问题无关紧要)?

附带问题,是否可以在 Godbolt 中禁用这些伪指令?我已经看到它们也与 RISC-V 程序集一起使用,但我更愿意查看反汇编的真实字节码,以了解这些伪/宏程序集指令产生的确切指令。

【问题讨论】:

  • 可以使用小写的i约束。这不会进行范围检查,但对于您的目的应该没问题。此外,如果您需要寄存器中的值,您可以将该寄存器用作输入并将加载留给编译器。如果你使用"r"(0x1014),gcc 会很乐意为你生成一个movw r3, #4116
  • @Jester 非常感谢你,它们都工作得很好:godbolt.org/z/84ME1hnar 所以我可以使用更合适的那个,我认为在 C 中加载它而不是强迫太多的手编译器可能最适合我的大多数情况。
  • 使用输入值并让编译器这样做的另一个好处是编译器知道寄存器中的值。如果它需要再次使用它,它不必重新加载它。如果你修改你的godbolt链接做d32(20); d32(20); d32(20);,你会得到movw r3, #4116 ; movs r2, #20 ; str r2, r3 ; str r2, r3 ; str r2, r3,而如果你在asm中做mov,它总是必须做负载。
  • 是的,我明白你的意思。谢谢。
  • 回复您对您现在已删除的答案的评论:未来的内联 asm 初学者读者不会意识到它是如何简化的/放回哪个重要部分。GNU C inline asm在不被不安全的坏例子误导的情况下很难学习,所以我反对或编辑任何不安全的 SO 答案。完全有可能使用内联汇编编写一个将字节、半字或字正确存储到 MMIO 寄存器的小型包装函数。如果您进行了一些小的更改来修复错误,我会删除我的反对票,也许还会反对。

标签: assembly gcc arm inline-assembly immediate-operand


【解决方案1】:

cmets 中的@Jester 建议使用i 约束来传递更大的立即数或使用真正的C 变量,用所需的值初始化它并让内联汇编接受它。这听起来像是最好的解决方案,在内联汇编中花费的时间越少越好,人们想要更好的性能往往低估了 C/C++ 工具链在给定正确代码时的优化能力,对于许多重写 C/C++ 代码是回答而不是在汇编中重做所有事情。 @Peter Cordes 提到不要使用内联汇编,我同意。但是,在这种情况下,某些指令的确切时序至关重要,我不能冒险让工具链稍微不同地优化某些指令的时序。

Bit-banging 协议并不理想,在大多数情况下,答案是避免 bit-banging,但在我的情况下,它并不是那么简单,其他方法也不起作用:

  • SPI 不能用于流式传输数据,因为我需要推送更多信号并且具有任意长度,而我的硬件仅支持 8 位/16 位。
  • 尝试使用 DMA2GPIO 时遇到了抖动问题。
  • 尝试了 IRQ 处理程序,它的开销太大并且我的性能下降(如下所示,只有 2 个 nop,因此空闲时间没有太多空间可做)。
  • 尝试了预烘焙位流(包括时间),但是对于 1 字节的真实数据,我最终保存了 64 字节的流数据,并且从内存中读取的整体速度要慢得多。
  • 每个写入值的预支持函数(并且对于每个写入值都有一个函数查找表)工作得非常好,实际上速度太快了,因为现在工具链具有编译时已知值并且能够很好地优化它,我的 TCK 在 40MHz 以上。问题是我必须添加很多延迟才能将其减慢到所需的速度(8MHz),并且必须为每个输入值完成,当长度为 8 位或更短时它很好,但是对于 32-位长度无法放入闪存 (2^32 => 4294967296),并且将单个 32 位访问拼接成四个 8 位访问会在 TCK 信号上引入大量抖动。
  • 在 FPGA 结构中实现这个外设,可以让我控制一切,通常这是正确的答案,但我想尝试在没有结构的设备上实现它。

长话短说,bit-banging 是不好的,而且大多数情况下有更好的方法来解决它,而且使用内联汇编的不必要实际上可能会在不知不觉中产生更糟糕的结果,但就我而言,我需要它。在我之前的代码中,我试图专注于一个关于立即数的简单问题,而不是讨论切线或 X-Y 问题。

现在回到“将更大的立即数传递给程序集”的主题,这里是一个更真实的示例的实现:

https://godbolt.org/z/5vbb7PPP5

#include <cstdint>

const uint8_t TCK = 2;
const uint8_t TMS = 3;
const uint8_t TDI = 4;
const uint8_t TDO = 5;

template<uint8_t number>
constexpr uint8_t powerOfTwo() {
    static_assert(number <8, "Output would overflow, the JTAG pins are close to base of the register and you shouldn't need PIN8 or above anyway");
    int ret = 1;
    for (int i=0; i<number; i++) {
        ret *= 2;
    }
    return ret;
}

template<uint8_t WHAT_SIGNAL>
__attribute__((optimize("-Ofast")))
uint32_t shiftAsm(const uint32_t length, uint32_t write_value) {
    uint32_t addressWrite = 0x40021014; // ODR register of GPIO port E (normally not hardcoded, but just for godbolt example it's like this)
    uint32_t addressRead  = 0x40021010; // IDR register of GPIO port E (normally not hardcoded, but just for godbolt example it's like this)

    uint32_t count     = 0;
    uint32_t shift_out = 0;
    uint32_t shift_in  = 0;
    uint32_t ret_value = 0;

    asm volatile (
    "cpsid if                                                  \n\t"  // Disable IRQ
    "repeatForEachBit%=:                                       \n\t"

    // Low part of the TCK
    "and.w %[shift_out],   %[write_value],    #1               \n\t"  // shift_out = write_value & 1
    "lsls  %[shift_out],   %[shift_out],      %[write_shift]   \n\t"  // shift_out = shift_out << pin_shift
    "str   %[shift_out],   [%[gpio_out_addr]]                  \n\t"  // GPIO = shift_out

    // On the first cycle this is redundant, as it processed the shift_in from the previous iteration.
    // First iteration is safe to do extraneously as it's just doing zeros
    "lsr   %[shift_in],    %[shift_in],       %[read_shift]    \n\t"  // shift_in = shift_in >> TDI
    "and.w %[shift_in],    %[shift_in],       #1               \n\t"  // shift_in = shift_in & 1
    "lsl   %[ret_value],   #1                                  \n\t"  // ret = ret << 1
    "orr.w %[ret_value],   %[ret_value],      %[shift_in]      \n\t"  // ret = ret | shift_in

    // Prepare things that are needed toward the end of the loop, but can be done now
    "orr.w %[shift_out],   %[shift_out],      %[clock_mask]    \n\t"  // shift_out = shift_out | (1 << TCK)
    "lsr   %[write_value], %[write_value],    #1               \n\t"  // write_value = write_value >> 1
    "adds  %[count],       #1                                  \n\t"  // count++
    "cmp   %[count],       %[length]                           \n\t"  // if (count != length) then ....

    // High part of the TCK + sample
    "str   %[shift_out],   [%[gpio_out_addr]]                  \n\t"  // GPIO = shift_out
    "nop                                                       \n\t"
    "nop                                                       \n\t"
    "ldr   %[shift_in],    [%[gpio_in_addr]]                   \n\t"  // shift_in = GPIO
    "bne.n repeatForEachBit%=                                  \n\t"  // if (count != length) then  repeatForEachBit

    "cpsie if                                                  \n\t"  // Enable IRQ - the critical part finished

    // Process the shift_in as normally it's done in the next iteration of the loop
    "lsr   %[shift_in],    %[shift_in],       %[read_shift]    \n\t"  // shift_in = shift_in >> TDI
    "and.w %[shift_in],    %[shift_in],       #1               \n\t"  // shift_in = shift_in & 1
    "lsl   %[ret_value],   #1                                  \n\t"  // ret = ret << 1
    "orr.w %[ret_value],   %[ret_value],      %[shift_in]      \n\t"  // ret = ret | shift_in

    // Outputs
    : [ret_value]       "+r"(ret_value),
      [count]           "+r"(count),
      [shift_out]       "+r"(shift_out),
      [shift_in]        "+r"(shift_in)

    // Inputs
    : [gpio_out_addr]   "r"(addressWrite),
      [gpio_in_addr]    "r"(addressRead),
      [length]          "r"(length),
      [write_value]     "r"(write_value),
      [write_shift]     "M"(WHAT_SIGNAL),
      [read_shift]      "M"(TDO),
      [clock_mask]      "I"(powerOfTwo<TCK>())

    // Clobbers
    : "memory"
    );

    return ret_value;
}

int main() {
    shiftAsm<TMS>(7,  0xff);                  // reset the target TAP controler
    shiftAsm<TMS>(3,  0x12);                  // go to state some arbitary TAP state
    shiftAsm<TDI>(32, 0xdeadbeef);            // write to target

    auto ret = shiftAsm<TDI>(16, 0x0000);     // read from the target

    return 0;
}

@David Wohlferd 关于减少汇编的评论将为工具链提供更多机会来进一步优化“将地址加载到寄存器中”,以防内联它不应该再次加载地址(因此它们只完成一次但是有多个读/写调用)。这里启用了内联:

https://godbolt.org/z/K8GYYqrbq

还有一个问题,值得吗?我想是的,我的 TCK 是死点 8MHz,我的占空比接近 50%,而我对保持原样的占空比更有信心。并且采样是在我预期的时候完成的,不用担心它会因不同的工具链设置而得到不同的优化。

【讨论】:

  • 试图专注于一个关于即时的简单问题而不是切线,但看起来这不是这里所希望的。 - 这不是我的原因对您之前的答案投反对票,这是您之前的答案实际上被破坏了,例如在不告诉编译器的情况下编写寄存器。只要答案实际上是完全安全地使用内联汇编,简单的示例就可以很好地解决基本问题,例如如何使用立即输入操作数。
  • 如果您的实际用例如此简单,那么您实际上不应该这样做,应该在问答中提及这一点,以使未来陷入 XY 问题的读者受益他们认为这是他们想要的,但实际上并非如此。但这不值得被否决,你的其他答案被否决的原因是编译为str reg, reg而不是str reg, [reg]的损坏代码,并且缺少clobber声明,所以它没有达到它的最低目标为了。 (我期待你解决这个答案,然后我会推翻我的投票。)
  • 这里这个更大的激励示例作为将所有内容保存在 asm 中的原因是有道理的,顺便说一句,并且很好地表明让编译器在寄存器中为您构造常量通常是好的。 volatile uint32_t addressWrite 没有意义,但是:地址值本身不需要存储和重新加载到内存,只需要指向 MMIO 寄存器。您没有其他线程或中断修改该 C 变量,所以它不应该是 volatile
  • 如果它是一个指针,你可以告诉C编译器它是一个指向易失性的指针,比如volatile uint32_t *aw = ...;。然后使用两个内存输出操作数(如"=m"(*addressWrite))并让编译器选择一种寻址模式,而不是手动强制[ %[reg] ] 甚至是有意义的。它可能只将一个常量放在一个 reg 中,并将 [r1] 用于一个,[r1 + 4] 用于另一个。
  • 而且由于您的回答开始提到我的 cmets,因此您误解了我所说的话。我并不是说示例必须是完整的现实生活中复杂的用例。我的意思是,当你编写一个简单的示例时,不要遗漏任何使该简单示例安全的部分,这一点至关重要。例如asm ( "mov ip, %0\n" "str %0, [ip]" ::"i"(0x1014), "r"(value) :"ip"); 作为 4 字节存储的一种方式是可以的,但没有 "ip" clobber 对于未来复制它的读者来说是非常危险的。 这就是我所说的不简化重要部分的意思。
猜你喜欢
  • 1970-01-01
  • 2019-12-12
  • 2019-01-28
  • 2016-06-15
  • 1970-01-01
  • 1970-01-01
  • 2012-12-06
  • 2015-09-07
  • 1970-01-01
相关资源
最近更新 更多