
哈Ha! 在本文中,我想谈谈具有ARM体系结构的处理器的浮点工作。 我认为本文主要对将其操作系统移植到ARM体系结构的人有用,同时他们需要对硬件浮点的支持(这是我们对
Embox所做的,
Embox以前使用浮点运算的软件实现)。
因此,让我们开始吧。
编译器标志
要支持浮点,必须将正确的标志传递给编译器。 快速浏览我们的想法是,有两个选项特别重要:-mfloat-abi和-mfpu。 -mfloat-abi选项为浮点操作设置
ABI ,并且可以具有以下三个值之一:“ soft”,“ softfp”和“ hard”。 顾名思义,“ soft”选项告诉编译器使用内置函数调用对浮点进行编程(以前使用此选项)。 在考虑了-mfpu选项之后,稍后再考虑其余两个'softfp'和'hard'。
-Mfpu标志和VFP版本
如
gcc联机文档中所述,-mfpu选项允许您指定硬件类型,并且可以采用以下选项:
'自动','vfpv2','vfpv3','vfpv3-fp16','vfpv3-d16','vfpv3-d16-fp16','vfpv3xd','vfpv3xd-fp16','neon-vfpv3','neon -fp16','vfpv4','vfpv4-d16','fpv4-sp-d16','neon-vfpv4','fpv5-d16','fpv5-sp-d16','fp-armv8','霓虹灯-fp-armv8'和'crypto-neon-fp-armv8'。 “ neon”与“ neon-vfpv3”相同,“ vfp”为“ vfpv2”。
我的编译器(arm-none-eabi-gcc(15:5.4.1 + svn241155-1)5.4.1 20160919)产生了一个略有不同的列表,但这并没有改变问题的本质。 无论如何,我们需要了解this或that标志如何影响编译器,当然,还应该了解何时使用该标志。
我开始了解基于imx6处理器的平台,但是我们将其推迟一会儿,因为霓虹灯协处理器具有我稍后将要讨论的功能,因此我们将以更简单的情况开始-从
集成商/ cp平台开始,
我没有开发板,所以调试是在qemu仿真器上完成的。 在qemu中,Interator / cp平台基于
ARM926EJ-S处理器,而后者又支持
VFP9-S协处理器。 该协处理器符合矢量浮点体系结构版本2(VFPv2)。 因此,您需要设置-mfpu = vfpv2,但是此选项不在我的编译器的选项列表中。 在
Internet上,我遇到了带有-mcpu = arm926ej-s -mfpu = vfpv3-d16标志的编译选项,安装了它,一切都为我编译。 开始时,我收到了一个
未定义的指令异常 ,这是可以预见的,因为协处理器已
关闭 。
为了使协处理器能够工作,您需要将
FPEXC寄存器中的EN位[30]
置1 。 这是使用VMSR命令完成的。
asm volatile ("VMSR FPEXC, %0" : : "r" (1 << 30);
实际上,VMSR命令由协处理器处理,并且如果未打开协处理器,则会引发异常,但是访问此寄存器
不会导致该异常。 的确,与其他寄存器不同,只有在
特权模式下才可以访问该寄存器。
在协处理器被允许工作之后,我们对数学函数的测试开始通过。 但是,当我打开优化(-O2)时,引发了前面提到的未定义指令异常。 它起源于先前在代码中调用过的
vmov指令,但已成功执行(无异常)。 最后,我在页面末尾找到了短语“复制立即常量的指令在VFPv3中可用”(也就是说,从VFPv3开始支持使用常量的操作)。 我决定检查模拟器中发布了哪个版本。 版本记录在
FPSID寄存器中。 从文档中可以得出,寄存器的值必须为0x41011090。 这对应于体系结构字段[19..16]中的1,即VFPv2。 其实,在启动时打印出了一个
unit: initializing embox.arch.arm.fpu.vfp9_s: VPF info: Hardware FP support Implementer = 0x41 (ARM) Subarch: VFPv2 Part number = 0x10 Variant = 0x09 Revision = 0x00
仔细阅读“ vfp”是别名“ vfpv2”之后,我设置了正确的标志,它起作用了。 返回到我看到标志-mcpu = arm926ej-s -mfpu = vfpv3-d16的组合的
页面 ,我注意到我不够谨慎,因为-mfloat-abi = soft出现在标志列表中。 也就是说,在这种情况下没有硬件支持。 更准确地讲,-mfpu仅在将“ soft”以外的值设置为-mfloat-abi时才有意义。
组装工
现在该谈论汇编程序了。 毕竟,我需要提供运行时支持,例如,编译器当然不了解上下文切换。
寄存器
让我们从寄存器的描述开始。 VFP允许您使用32位(s0..s31)和64位(d0..d15)浮点数执行操作,这些寄存器之间的对应关系如下图所示。

Q0-Q15是来自旧版本的128位寄存器,可用于SIMD,稍后将对其进行详细介绍。
指令系统
当然,大多数情况下应该将VFP寄存器的工作交给编译器,但至少您必须手动编写上下文切换。 如果您已经对用于通用寄存器的汇编指令的语法有了大致的了解,那么处理新指令应该不会很困难。 大多数情况下,仅添加前缀“ v”。
vmov d0, r0, r1 /* r0 r1, .. d0 64 , r0-1 32 */ vmov r0, r1, d0 vadd d0, d1, d2 vldr d0, r0 vstm r0!, {d0-d15} vldm r0!, {d0-d15}
依此类推。 可以
在ARM网站上找到完整的命令列表。
当然,不要忘记VFP版本,这样就不会出现上述情况。
标记-mfloat-abi'softfp'和'hard'
返回-mfloat-abi。 如果您阅读
文档 ,我们将看到:
“ softfp”允许使用硬件浮点指令生成代码,但仍使用软浮点调用约定。 “硬”允许生成浮点指令并使用FPU特定的调用约定。
也就是说,我们正在谈论将参数传递给函数。 但是至少我对“软浮动”和“特定于FPU的”调用约定之间的区别不清楚。 假设硬情况使用浮点寄存器,而softfp情况使用整数寄存器,我在
debian wiki上找到了确认信息。 而且,尽管这是针对NEON协处理器的,但这并不重要。 另一个有趣的一点是,通过softfp选项,编译器可以但不是必须使用硬件支持:
“编译器可以根据选择的FPU类型(-mfpu =)做出明智的选择,决定何时以及是否生成仿真的或实际的FPU指令”
为了获得更好的清晰度,我决定进行实验,并且感到非常惊讶,因为在关闭-O0优化的情况下,差异很小,并且不适用于实际使用浮点的位置。 猜测编译器只是将所有内容压入堆栈,而不是使用寄存器,因此我打开了-O2优化,并再次感到惊讶,因为通过该优化,编译器开始对hard和sotffp选项都使用硬件浮点寄存器,区别在于在-O0的情况下,它是微不足道的。 结果,对于我自己,我通过一个事实来解释这一点,即编译器解决了以下
问题 :如果在浮点寄存器和整数之间复制数据,性能将大大下降。 并且,编译器在进行优化时开始使用所有可用的资源。
当被问及使用“ softfp”或“ hard”的哪个标志时,我对自己回答如下:无论何时已经有使用“ softfp”标志编译的部分,都应使用“ hard”。 如果有,则需要使用“ softfp”。
上下文切换
由于Embox支持抢占式多任务处理,因此要在运行时正常工作,自然需要实现上下文切换。 为此,必须保存协处理器寄存器。 有一些细微差别。 首先:事实证明,
浮点(vstm / vldm)的堆栈操作命令不支持所有模式 。 第二:这些操作不支持使用超过16个64位寄存器。 如果需要一次加载/保存更多寄存器,则需要使用两条指令。
我将再进行一次小型优化。 实际上,根本不需要每次都保存和恢复256字节的VFP寄存器(通用寄存器仅占用64字节,因此差异很大)。 仅当过程原则上使用这些寄存器时,显而易见的优化才会执行这些操作。
正如我已经提到的,当VFP协处理器关闭时,执行相应指令的尝试将导致“未定义指令”异常。 在此异常的处理程序中,您需要检查异常是由什么引起的,并且如果是使用VPF协处理器的问题,则该进程将标记为使用VFP协处理器。
结果,已经写入的保存/恢复上下文被补充了宏。
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ vmrs tmp, FPEXC ; \ stmia stack!, {tmp}; \ ands tmp, tmp, #1<<30; \ beq fpu_out_save_inc; \ vstmia stack!, {d0-d15}; \ fpu_out_save_inc: #define ARM_FPU_CONTEXT_LOAD_INC(tmp, stack) \ ldmia stack!, {tmp}; \ vmsr FPEXC, tmp; \ ands tmp, tmp, #1<<30; \ beq fpu_out_load_inc; \ vldmia stack!, {d0-d15}; \ fpu_out_load_inc:
为了检查浮点条件下上下文切换操作的正确性,我们编写了一个测试,其中我们在一个线程中将一个线程相乘,然后在另一个线程中相除,然后比较结果。
EMBOX_TEST_SUITE("FPU context consistency test. Must be compiled with -02"); #define TICK_COUNT 10 static float res_out[2][TICK_COUNT]; static void *fpu_context_thr1_hnd(void *arg) { float res = 1.0f; int i; for (i = 0; i < TICK_COUNT; ) { res_out[0][i] = res; if (i == 0 || res_out[1][i - 1] > 0) { i++; } if (res > 0.000001f) { res /= 1.01f; } sleep(0); } return NULL; } static void *fpu_context_thr2_hnd(void *arg) { float res = 1.0f; int i = 0; for (i = 0; i < TICK_COUNT; ) { res_out[1][i] = res; if (res_out[0][i] != 0) { i++; } if (res < 1000000.f) { res *= 1.01f; } sleep(0); } return NULL; } TEST_CASE("Test FPU context consistency") { pthread_t threads[2]; pthread_t tid = 0; int status; status = pthread_create(&threads[0], NULL, fpu_context_thr1_hnd, &tid); if (status != 0) { test_assert(0); } status = pthread_create(&threads[1], NULL, fpu_context_thr2_hnd, &tid); if (status != 0) { test_assert(0); } pthread_join(threads[0], (void**)&status); pthread_join(threads[1], (void**)&status); test_assert(res_out[0][0] != 0 && res_out[1][0] != 0); for (int i = 1; i < TICK_COUNT; i++) { test_assert(res_out[0][i] < res_out[0][i - 1]); test_assert(res_out[1][i] > res_out[1][i - 1]); } }
关闭优化后,该测试成功通过,因此我们在测试说明中指出应使用优化来编译它,即EMBOX_TEST_SUITE(“ FPU上下文一致性测试。必须使用-02”); 尽管我们知道测试不应该依赖于此。
NEON和SIMD协处理器
现在该告诉我为什么推迟有关imx6的故事了。 事实是,它基于Cortex-A9内核,并包含更高级的NEON协处理器(https://developer.arm.com/technologies/neon)。 NEON不仅是VFPv3,而且还是SIMD协处理器。 VFP和NEON使用相同的寄存器。 VFP使用32位和64位寄存器进行操作,NEON使用64位和128位寄存器,后者仅称为Q0-Q16。 除整数值和浮点数外,NEON还可以与模数为16或8的多项式环一起使用。
NEON的vfp模式与拆卸的vfp9-s协处理器几乎没有区别。 当然,最好为-mfpu指定vfpv3或vfpv3-d32选项以获得更好的优化,因为它具有32个64位寄存器。 要启用协处理器,必须授予对c10和c11协处理器的访问权限。 这是使用命令完成的
asm volatile ("mrc p15, 0, %0, c1, c0, 2" : "=r" (val) :); val |= 0xf << 20; asm volatile ("mcr p15, 0, %0, c1, c0, 2" : : "r" (val));
但没有其他根本差异。
如果指定-mfpu = neon,则另一件事,在这种情况下,编译器可以使用SIMD指令。
在C中使用SIMD
为了通过寄存器手动<<登记>>值,可以包含“ arm_neon.h”并使用相应的数据类型:
对于一个寄存器中的四个32位浮点数,float32x4_t;对于八个8位整数,uint8x8_t等等。 要访问单个值,我们将其称为数组,加法,乘法,赋值等。 至于普通变量,例如:
uint32x4_t a = {1, 2, 3, 4}, b = {5, 6, 7, 8}; uint32x4_t c = a * b; printf(“Result=[%d, %d, %d, %d]\n”, c[0], c[1], c[2], c[3]);
当然,使用自动矢量化会更容易。 对于自动矢量化,将-ftree-vectorize标志添加到GCC。
void simd_test() { int a[LEN], b[LEN], c[LEN]; for (int i = 0; i < LEN; i++) { a[i] = i; b[i] = LEN - i; } for (int i = 0; i < LEN; i++) { c[i] = a[i] + b[i]; } for (int i = 0; i < LEN; i++) { printf("c[i] = %d\n", c[i]); } }
加法循环生成以下代码:
600059a0: f4610adf vld1.64 , [r1 :64] 600059a4: e2833010 add r3, r3, #16 600059a8: e28d0a03 add r0, sp, #12288 ; 0x3000 600059ac: e2811010 add r1, r1, #16 600059b0: f4622adf vld1.64 , [r2 :64] 600059b4: e2822010 add r2, r2, #16 600059b8: f26008e2 vadd.i32 q8, q8, q9 600059bc: ed430b04 vstr d16, [r3, #-16] 600059c0: ed431b02 vstr d17, [r3, #-8] 600059c4: e1530000 cmp r3, r0 600059c8: 1afffff4 bne 600059a0 <foo+0x58> 600059cc: e28d5dbf add r5, sp, #12224 ; 0x2fc0 600059d0: e2444004 sub r4, r4, #4 600059d4: e285503c add r5, r5, #60 ; 0x3c
在对并行化代码进行测试之后,我们发现,只要变量是独立的,则在循环中进行简单加法就可以使加速达到7倍。 此外,我们决定查看并行化对实际任务有多大影响,并使用MESA3d对其软件进行仿真,并测量带有不同标志的fps数量,我们获得了每秒2帧的增益(15 vs. 13),即加速度约为15-20% 。
我将
给出另一个
使用NEON命令 (不是我们的
命令 ,而是ARM的
命令)进行加速的示例 。
复制内存比正常情况下快50%。 汇编器中有真实的示例。
正常复制周期:
WordCopy LDR r3, [r1], #4 STR r3, [r0], #4 SUBS r2, r2, #4 BGE WordCopy
用霓虹灯命令和寄存器循环:
NEONCopyPLD PLD [r1, #0xC0] VLDM r1!,{d0-d7} VSTM r0!,{d0-d7} SUBS r2,r2,#0x40 BGE NEONCopyPLD
显然,64字节的复制比4字节的复制要快,这种复制将增加10%,但是剩下的40%似乎可以完成协处理器的工作。
皮质
在Cortex-M中使用FPU与上述方法没有太大区别。 例如,上面的宏就是这样保存fpu-shny上下文的
#define ARM_FPU_CONTEXT_SAVE_INC(tmp, stack) \ ldr tmp, =CPACR; \ ldr tmp, [tmp]; \ tst tmp, #0xF00000; \ beq fpu_out_save_inc; \ vstmia stack!, {s0-s31}; fpu_out_save_inc:
此外,vstmia命令仅使用寄存器s0-s31,并且以不同方式访问控制寄存器。 因此,我将不做过多介绍,仅解释diff。 因此,我们分别使用
cortex-m7支持STM32F7discovery,我们需要设置-mfpu = fpv5-sp-d16标志。 请注意,在移动版本中,您需要更仔细地查看协处理器版本,因为相同的cortex-m可能具有不同的选择。 因此,如果您的选项不是双精度的,而是单精度的,则可能没有
stm32f4discovery中的 D0-D16寄存器,这就是为什么使用带有寄存器S0-S31的变量的原因。 对于此控制器,我们使用-mfpu = fpv4-sp-d16。
主要区别在于访问控制器的控制寄存器,它们直接位于主内核的地址空间中,对于不同的类型,它们对于
cortex-m7而言是不同的
cortex-m4 。
结论
至此,我将结束关于ARM浮点的简短故事。 我注意到,现代微控制器非常强大,不仅适用于控制,还适用于处理信号或各种多媒体信息。 为了有效利用所有这些功能,您需要了解它的工作原理。 我希望本文有助于弄清楚这一点。