使用 Arm 内联 GCC 程序集加载 16 位(或更大)立即数
Loading 16-bit (or bigger) immediate with a Arm inline GCC assembly
注意:这里只是为了简洁起见,对示例进行了简化,因此它们并不能证明我的意图。如果我只是写入与示例中完全相同的内存位置, 然后 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 代码的组装方式(MOV
被 LDR
替换),但是在很多情况下这对我来说并不实用,因为我会用.
做其他事情
在 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
In ARMv6T2 and later, both ARM and Thumb instruction sets include:
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
the contents of the least significant half.
我认为我的 CortexM4 应该是 ARMv7E-M 并且应该满足这个“ARMv6T2 及更高版本”的要求并且应该能够使用 16 位立即数。
但是从 GCC 内联汇编文档中我没有看到这样的提及:
https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html
当我启用 ARMv7E-M arch 并取消注释 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'
所以我想知道有没有一种方法可以将 16 位立即数与 GCC 内联汇编一起使用,或者我是否遗漏了什么(这会使我的问题变得无关紧要)?
附带问题,是否可以在 Godbolt 中禁用这些伪指令?我已经看到它们也与 RISC-V 程序集一起使用,但我更愿意看到反汇编的真实字节码以查看这些 pseudo/macro 汇编指令产生的确切指令。
@Jester 在评论中建议使用 i
约束来传递更大的立即数或使用真正的 C 变量,用所需的值初始化它并让内联程序集接受它。这听起来像是最好的解决方案,在内联汇编上花费的时间越少越好,人们想要更好的性能往往低估了 C/C++ 工具链在给定正确代码的情况下在优化方面的强大能力,并且许多人重写了 C/C++ 代码是答案,而不是重做汇编中的所有内容。 @Peter Cordes 提到不使用内联汇编,我同意。然而,在这种情况下,某些指令的准确时序至关重要,我不能冒着工具链略微不同的风险来优化某些指令的时序。
Bit-banging 协议并不理想,在大多数情况下,答案是避免 bit-banging,但在我的情况下,它并不是那么简单,其他方法也不起作用:
- SPI 无法用于传输数据,因为我需要推送更多信号,并且具有任意长度,而我的硬件仅支持 8-bit/16-bit.
- 尝试使用 DMA2GPIO 并遇到抖动问题。
- 尝试了 IRQ 处理程序,它的开销太大而且我的性能下降了(正如您在下面看到的,只有 2 个 nop,所以在空闲时间没有太多 space 可做)。
- 尝试预烘焙比特流(包括时间),但是对于 1 字节的真实数据,我最终保存了 64 字节的流数据,并且从内存中读取的整体速度要慢得多。
- 每个写入值的预支持函数(并查找 table 函数,每个值写入)工作得很好,实际上太快了,因为现在工具链具有编译时已知值并且是能够很好地优化它,我的TCK在40MHz以上。问题是我不得不添加很多延迟以将其减慢到所需的速度(8MHz)并且必须为每个输入值完成,当长度为 8 位或更少时它很好,但对于 32-位长度无法放入闪存 (2^32 => 4294967296) 并将单个 32 位访问拼接为四个 8 位访问在 TCK 信号上引入了很多抖动。
- 在 FPGA 结构中实现这个外设,可以让我控制一切,通常这是正确的答案,但我想尝试在没有结构的设备上实现它。
长话短说,bit-banging 不好,大多数情况下有更好的解决方法,使用内联汇编的不必要的使用实际上可能会在不知不觉中产生更糟糕的结果,但在我的情况下我需要它。在我之前的代码中,我试图专注于一个关于立即数的简单问题,而不是进入切线或 X-Y 问题讨论。
现在回到 'passing bigger immediates to the assembly' 的主题,这里是一个更真实的示例的实现:
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 关于减少汇编的评论将为工具链提供更多机会进一步优化 'load of addresses into the registers',在内联的情况下它不应该再次加载地址(所以它们只完成一次) reads/writes 有多个调用)。这里启用了内联:
https://godbolt.org/z/K8GYYqrbq
问题是,这值得吗?我认为是的,我的 TCK 是死点 8MHz,我的占空比接近 50%,而我对保持原样的占空比更有信心。并且在我期望完成采样时完成了采样,而不用担心它会因不同的工具链设置而得到不同的优化。
注意:这里只是为了简洁起见,对示例进行了简化,因此它们并不能证明我的意图。如果我只是写入与示例中完全相同的内存位置, 然后 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 代码的组装方式(MOV
被 LDR
替换),但是在很多情况下这对我来说并不实用,因为我会用.
在 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
In ARMv6T2 and later, both ARM and Thumb instruction sets include:
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
the contents of the least significant half.
我认为我的 CortexM4 应该是 ARMv7E-M 并且应该满足这个“ARMv6T2 及更高版本”的要求并且应该能够使用 16 位立即数。
但是从 GCC 内联汇编文档中我没有看到这样的提及:
https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html
当我启用 ARMv7E-M arch 并取消注释 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'
所以我想知道有没有一种方法可以将 16 位立即数与 GCC 内联汇编一起使用,或者我是否遗漏了什么(这会使我的问题变得无关紧要)?
附带问题,是否可以在 Godbolt 中禁用这些伪指令?我已经看到它们也与 RISC-V 程序集一起使用,但我更愿意看到反汇编的真实字节码以查看这些 pseudo/macro 汇编指令产生的确切指令。
@Jester 在评论中建议使用 i
约束来传递更大的立即数或使用真正的 C 变量,用所需的值初始化它并让内联程序集接受它。这听起来像是最好的解决方案,在内联汇编上花费的时间越少越好,人们想要更好的性能往往低估了 C/C++ 工具链在给定正确代码的情况下在优化方面的强大能力,并且许多人重写了 C/C++ 代码是答案,而不是重做汇编中的所有内容。 @Peter Cordes 提到不使用内联汇编,我同意。然而,在这种情况下,某些指令的准确时序至关重要,我不能冒着工具链略微不同的风险来优化某些指令的时序。
Bit-banging 协议并不理想,在大多数情况下,答案是避免 bit-banging,但在我的情况下,它并不是那么简单,其他方法也不起作用:
- SPI 无法用于传输数据,因为我需要推送更多信号,并且具有任意长度,而我的硬件仅支持 8-bit/16-bit.
- 尝试使用 DMA2GPIO 并遇到抖动问题。
- 尝试了 IRQ 处理程序,它的开销太大而且我的性能下降了(正如您在下面看到的,只有 2 个 nop,所以在空闲时间没有太多 space 可做)。
- 尝试预烘焙比特流(包括时间),但是对于 1 字节的真实数据,我最终保存了 64 字节的流数据,并且从内存中读取的整体速度要慢得多。
- 每个写入值的预支持函数(并查找 table 函数,每个值写入)工作得很好,实际上太快了,因为现在工具链具有编译时已知值并且是能够很好地优化它,我的TCK在40MHz以上。问题是我不得不添加很多延迟以将其减慢到所需的速度(8MHz)并且必须为每个输入值完成,当长度为 8 位或更少时它很好,但对于 32-位长度无法放入闪存 (2^32 => 4294967296) 并将单个 32 位访问拼接为四个 8 位访问在 TCK 信号上引入了很多抖动。
- 在 FPGA 结构中实现这个外设,可以让我控制一切,通常这是正确的答案,但我想尝试在没有结构的设备上实现它。
长话短说,bit-banging 不好,大多数情况下有更好的解决方法,使用内联汇编的不必要的使用实际上可能会在不知不觉中产生更糟糕的结果,但在我的情况下我需要它。在我之前的代码中,我试图专注于一个关于立即数的简单问题,而不是进入切线或 X-Y 问题讨论。
现在回到 'passing bigger immediates to the assembly' 的主题,这里是一个更真实的示例的实现:
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 关于减少汇编的评论将为工具链提供更多机会进一步优化 'load of addresses into the registers',在内联的情况下它不应该再次加载地址(所以它们只完成一次) reads/writes 有多个调用)。这里启用了内联:
https://godbolt.org/z/K8GYYqrbq
问题是,这值得吗?我认为是的,我的 TCK 是死点 8MHz,我的占空比接近 50%,而我对保持原样的占空比更有信心。并且在我期望完成采样时完成了采样,而不用担心它会因不同的工具链设置而得到不同的优化。