ARM VFP的一點體會

ARM VFP的一點體會
關鍵字: VFP arm1136JF-S MCIMX31 gcc linux
參考文獻:<<ARM1136JF-S and ARM1136J-S Technical Reference Manual r1p3>>、<<ARM Architecture Reference Manual>>、<<VFP11 Vector Floating-point Coprocessor for ARM1136JF-S processor r1p3 Technical Reference Manual>>
前言:
MCIMX31 是一款基於ARM1136JF-S的多媒體處理器。他適合用來做智能手機,手持式遊戲終端,多媒體播放器等智能手持式設備。有關這款CPU的參數特性可以參考Freescale的DataSheet。
最近使用MCIMX31的VFP有些收穫,寫出來與大家分享。
調試環境:
成都萊得科技有限公司([url=http://www.nidetech.com/]http://www.nidetech.com[/url])的I.MX31開發板。
1. VFP的功能特點
在我看來VFP除了提供浮點數基本運算(加、減、乘、除、開方、比較、取反)提供支持之外,最有特點是它向量(vectors)功能。它同時支持最多8組單精度4組雙精度浮點數的運算。有關這部分的敘述請參考<<ARM Architecture Reference Manual>> Chapter C5 VFP Addressing Modes。下面看一個程序實例,程序是用arm-none-linux-gnueabi-gcc 4.1.2編譯,運行在MCIMX31 Linux 2.6.24.5平臺下。
#include <unistd.h>
#include <stdio.h>
void vfp_regs_load(float arrays[32])
{
    asm volatile("fldmias %0, {s0-s31}\n"
            :
            :"r"(arrays));
}
void vfp_regs_save(float arrays[32])
{
    asm volatile ("fstmias %0, {s0-s31}"
            :
            :"r"(arrays));
}
void print_array(float array[32])
{
    int i;
    for(i=0; i<32; i++)
    {
        if(i%8==0)
            printf("\n");
        printf("%f ",i, array[i]);
    }
    printf("\n");
}
int main()
{
    unsigned int fpscr;
    float f1=1.0, f2=1.0;
    float farrays[32], farrays2[32];
    int i;
    fpscr = 0x130000;
    asm volatile ("fmxr fpscr, %0\n"
            :
            :"r"(fpscr));
    asm volatile ("fmrx %0, fpscr\n"
            :"=r"(fpscr));
    vfp_regs_save(farrays2);
    for(i=0; i<32; i++)
        farrays[i] = f1+f2*(float) i;
    vfp_regs_load(farrays);
    vfp_regs_save(farrays2);
    printf("\n1:ScalarA op ScalarB->ScalarD");
    vfp_regs_load(farrays);
    asm volatile("fadds s0, s1, s2");
    vfp_regs_save(farrays2);
    print_array(farrays2);
    printf("\n2:VectorA[?] op ScalarB->VectorD[?]");
    vfp_regs_load(farrays);
    asm volatile("fadds s8,  s24, s0");
    vfp_regs_save(farrays2);
    print_array(farrays2);
    printf("\n3:VectorA[?] op VectorB[?]->VectorD[?]");
    vfp_regs_load(farrays);
    asm volatile("fadds s8,  s16, s24");
    vfp_regs_save(farrays2);
    print_array(farrays2);
}

運行結果:
1:ScalarA op ScalarB->ScalarD
5.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 
9.000000 10.000000 11.000000 12.000000 13.000000 14.000000 15.000000 16.000000 
17.000000 18.000000 19.000000 20.000000 21.000000 22.000000 23.000000 24.000000 
25.000000 26.000000 27.000000 28.000000 29.000000 30.000000 31.000000 32.000000 
2:VectorA[?] op ScalarB->VectorD[?]
1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 
26.000000 10.000000 28.000000 12.000000 30.000000 14.000000 32.000000 16.000000 
17.000000 18.000000 19.000000 20.000000 21.000000 22.000000 23.000000 24.000000 
25.000000 26.000000 27.000000 28.000000 29.000000 30.000000 31.000000 32.000000 
3:VectorA[?] op VectorB[?]->VectorD[?]
1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 
42.000000 10.000000 46.000000 12.000000 50.000000 14.000000 54.000000 16.000000 
17.000000 18.000000 19.000000 20.000000 21.000000 22.000000 23.000000 24.000000 
25.000000 26.000000 27.000000 28.000000 29.000000 30.000000 31.000000 32.000000 
第一種情況是最簡單的兩個浮點數相加(fadds s0, s1, s2) 我們看到的結果是s0(5.00)=s1(2.00)+s2(3.00)。
第二種情況是一組Vector和一個Scalar相加(fadds s8,  s24, s0),我們看到的結果就是:
S8(26.00)=S24(25.00)+S0(1.00)
S10(28.00)=S26(27.00)+S0(1.00)
S12(30.00)=S28(29.00)+S0(1.00)
S14(32.00)=S30(31.00)+S0(1.00)
第三種情況是兩組Vectors相加(fadds s8,  s16, s24),我們看到的結果就是:
S8(42.00)=S24(25.00)+S16(17.00)
S10(46.00)=S26(27.00)+S18(19.00)
S12(50.00)=S28(29.00)+S20(21.00)
S14(54.00)=S30(31.00)+S22(23.00)
至於爲什麼是有4組結果,並且相鄰結果間隔一個。有興趣的可以參考<<VFP11? Vector Floating-point Coprocessor for ARM1136JF-S processor r1p3 Technical Reference Manual>>有關FPSCR的敘述。
2. 硬件支持
ARM1136JF-S通過兩個協處理器CP10和CP11來實現VFP。其中CP10支持單精度浮點操作,CP11支持雙精度浮點操作。所以所有的VFP指令其實就是一些協處理器的指令比如FADDS其實就是一個CDP指令,一個FLDS就是一個LDC指令。理論上講只要採用了ARM1136JF-S的CPU就應該能夠支持VFP。

3. 編譯器對VFP的支持
一個浮點數操作最後是翻譯成VFP指令,還是翻譯成fpa,或者是softfloat是編譯器決定的。實例:
[sjl@sjl vfp]$ cat f.c
int main()
{
    float f1=1.2,f2=1.3;
    f1 = f2*f1;
}
[sjl@sjl vfp]$ arm-linux-gcc -v
....
gcc version 3.4.4
[sjl@sjl vfp]$ arm-linux-gcc -c f.c
[sjl@sjl vfp]$ arm-linux-objdump -d f.o
f.o:     file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
   0:   e1a0c00d        mov     ip, sp
   4:   e92dd800        stmdb   sp!, {fp, ip, lr, pc}
   8:   e24cb004        sub     fp, ip, #4      ; 0x4
   c:   e24dd008        sub     sp, sp, #8      ; 0x8
  10:   e59f3024        ldr     r3, [pc, #36]   ; 3c <.text+0x3c>
  14:   e50b3010        str     r3, [fp, #-16]
  18:   e59f3020        ldr     r3, [pc, #32]   ; 40 <.text+0x40>
  1c:   e50b3014        str     r3, [fp, #-20]
  20:   ed1b1104        ldfs    f1, [fp, #-16]
  24:   ed1b0105        ldfs    f0, [fp, #-20]
  28:   ee910100        fmls    f0, f1, f0
  2c:   ed0b0104        stfs    f0, [fp, #-16]
  30:   e1a00003        mov     r0, r3
  34:   e24bd00c        sub     sp, fp, #12     ; 0xc
  38:   e89da800        ldmia   sp, {fp, sp, pc}
  3c:   3f99999a        swicc   0x0099999a
  40:   3fa66666        swicc   0x00a66666
我們用arm-linux-gcc 3.4.4編譯明顯,生成的不是VFP指令。

[sjl@sjl vfp]$ arm-none-linux-gnueabi-gcc -v
....
gcc version 4.1.2
[sjl@sjl vfp]$ arm-none-linux-gnueabi-gcc -c f.c
[sjl@sjl vfp]$ arm-none-linux-gnueabi-objdump -d f.o
f.o:     file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
   0:   e1a0c00d        mov     ip, sp
   4:   e92dd800        stmdb   sp!, {fp, ip, lr, pc}
   8:   e24cb004        sub     fp, ip, #4      ; 0x4
   c:   e24dd008        sub     sp, sp, #8      ; 0x8
  10:   e59f3024        ldr     r3, [pc, #36]   ; 3c <.text+0x3c>
  14:   e50b3014        str     r3, [fp, #-20]
  18:   e59f3020        ldr     r3, [pc, #32]   ; 40 <.text+0x40>
  1c:   e50b3010        str     r3, [fp, #-16]
  20:   e51b0014        ldr     r0, [fp, #-20]
  24:   e51b1010        ldr     r1, [fp, #-16]
  28:   ebfffffe        bl      0 <__aeabi_fmul>
  2c:   e1a03000        mov     r3, r0
  30:   e50b3014        str     r3, [fp, #-20]
  34:   e24bd00c        sub     sp, fp, #12     ; 0xc
  38:   e89da800        ldmia   sp, {fp, sp, pc}
  3c:   3f99999a        svccc   0x0099999a
  40:   3fa66666        svccc   0x00a66666
我們用arm-none-linux-gnueabi-gcc 4.1.2 默認也不是生成VFP指令。
[sjl@sjl vfp]$ arm-none-linux-gnueabi-gcc -mfpu=vfp -mfloat-abi=softfp -c f.c
[sjl@sjl vfp]$ arm-none-linux-gnueabi-objdump -d f.o
f.o:     file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
   0:   e1a0c00d        mov     ip, sp
   4:   e92dd800        stmdb   sp!, {fp, ip, lr, pc}
   8:   e24cb004        sub     fp, ip, #4      ; 0x4
   c:   e24dd008        sub     sp, sp, #8      ; 0x8
  10:   e59f3020        ldr     r3, [pc, #32]   ; 38 <.text+0x38>
  14:   e50b3014        str     r3, [fp, #-20]
  18:   e59f301c        ldr     r3, [pc, #28]   ; 3c <.text+0x3c>
  1c:   e50b3010        str     r3, [fp, #-16]
  20:   ed1b7a05        flds    s14, [fp, #-20]
  24:   ed5b7a04        flds    s15, [fp, #-16]
  28:   ee677a27        fmuls   s15, s14, s15
  2c:   ed4b7a05        fsts    s15, [fp, #-20]
  30:   e24bd00c        sub     sp, fp, #12     ; 0xc
  34:   e89da800        ldmia   sp, {fp, sp, pc}
  38:   3f99999a        svccc   0x0099999a
  3c:   3fa66666        svccc   0x00a66666
用arm-none-linux-gnueabi-gcc 4.1.2指定-mfpu=vfp -mfloat-abi=softfp參數之後生成VFP指令。
好像是從GCC 4之後才支持VFP,如果你要在原有GCC 3裏面使用VFP,如何解決,大家可以一起思考這個問題。

4. 操作系統對VFP的支持
應用程序要使用VFP指令,還需要操作系統配合。
在ARM1136JF-S裏面有幾個重要的協處理器與VFP有關。
CP15 c1 協處理器訪問控制寄存器,這個寄存器規定了用戶模式和特權對協處理器的訪問權限。我們要使用VFP當然要運行用戶模式訪問CP10和CP11。
另外一個寄存器是VFP的FPEXC Bit30這是VFP功能的使用位。
其實操作系統在做了這兩件事情之後,用戶程序就可以使用VFP了。
例子:
編譯內核取消VFP的支持,編寫一個內核驅動,加入以下代碼:
void enable_vfp(void)
{
    int ret = 0;
    unsigned int value;
    asm  volatile ("mrc p15, 0, %0, c1, c0, 2"
            :"=r"(value)
            :);
    value |= 0xf00000;/*enable CP10, CP11 user access*/
    asm volatile("mcr p15, 0, %0, c1, c0, 2"
            :
            :"r"(value));
    asm volatile("fmrx %0, fpexc"
               :"=r"(value));
    value |=(1<<30);
    asm volatile("fmxr fpexc, %0"
               :
               :"r"(value));
}
編寫一個應用程序:
int main()
{
    float f1=1.2,f2=1.3;
    f1 = f2*f1;
    printf("%f\n", f1);
}
adsdebian:/dev/shm# ./a.out 
1.560000
看見了吧,結果正確。
但,我們看內核的VFP支持代碼,還是幹了很多其他的事情。
讓我們來想想這個問題:
    8374:       ed1b7a05        flds    s14, [fp, #-20]
    8378:       ed5b7a04        flds    s15, [fp, #-16]
    837c:       ee677a27        fmuls   s15, s14, s15
    8380:       ed4b7a05        fsts    s15, [fp, #-20]
    8384:       ed5b7a05        flds    s15, [fp, #-20]
我們知道s14,s15這些是協處理器的寄存器,屬於所有進程共有的資源。所以當上述例子執行到0x8378時程序切換到其他進程,恰好這個其他進程也訪問了s14那結果就可想而知了。
實例:
加載上面我們提到的禁用VFP的Linux kernel並且加載一個驅動修改對應的協處理器寄存器運行用戶程序執行VFP指令。
[sjl@sjl vfp]$ cat fork.c 
#include <unistd.h>
#include <stdio.h>
int main()
{
    float f1=1.0,f2=1.0;
    pid_t pid;
    fork();
    pid = getpid();
    while(1)
    {
        f1 +=f2;
        printf("%d %f\n", pid, f1);
    }
}
[sjl@sjl vfp]$ arm-none-linux-gnueabi-gcc -O2 -mfpu=vfp -mfloat-abi=softfp fork.c
[sjl@sjl vfp]$ arm-none-linux-gnueabi-objdump -d a.out|less
...
000083b4 <main>:
    83b4:       e92d4010        stmdb   sp!, {r4, lr}
    83b8:       ed2d8b03        fstmdbx sp!, {d8}
    83bc:       e24dd004        sub     sp, sp, #4      ; 0x4
    83c0:       ebffffc7        bl      82e4 <.text-0x18>
    83c4:       ebffffc3        bl      82d8 <.text-0x24>
    83c8:       ed9f8a08        flds    s16, [pc, #32]
    83cc:       eef08a48        fcpys   s17, s16
    83d0:       e1a04000        mov     r4, r0
    83d4:       ee388a28        fadds   s16, s16, s17
    83d8:       eeb77ac8        fcvtds  d7, s16
    83dc:       e1a01004        mov     r1, r4
    83e0:       ec532b17        fmrrd   r2, r3, d7
    83e4:       e59f0008        ldr     r0, [pc, #8]    ; 83f4 <.text+0xf8>
    83e8:       ebffffc0        bl      82f0 <.text-0xc>
    83ec:       eafffff8        b       83d4 <main+0x20>
    83f0:       3f800000        svccc   0x00800000
    83f4:       0000847c        andeq   r8, r0, ip, ror r4
...
執行結果:
.....
1382 915.000000
1382 916.000000
1381 2.000000
1381 917.000000
1381 918.000000
....
我們看到進程1381的執行結果與我們程序設計的意圖不同。
經過我們上面的例子,我們知道爲了讓應用程序能夠正常的執行,操作系統要保存進程的VFP現場,當然爲支持VFP操作系統還要做不少其他的事情。
看來操作系統至少要在進程切換的時候保存應用程序保存VFP的寄存器值。來看看Linux是怎麼做的。
5、VFP代碼情景
進程在切換的時候,內核的函數調用過程是這樣的(ARM Arch):
__schedule()->context_switch()->switch_to()->__switch_to()
__switch_to()在Arch/arm/kernel/entry-armv.S中實現,這段代碼不長,這裏我比較關心的是
...
mov r5, r0
add r4, r2, #TI_CPU_SAVE
ldr r0, =thread_notify_head
mov r1, #THREAD_NOTIFY_SWITCH
bl atomic_notifier_call_chain
mov r0, r5
...
翻譯成C就是atomic_notifier_call_chain(thread_notify_head,THREAD_NOTIFY_SWITCH,next->cpu_context);
到我們的VFP代碼中去看,
static struct notifier_block vfp_notifier_block = {
.notifier_call = vfp_notifier,
};
vfp_init()
{
...
thread_register_notifier(&vfp_notifier_block);
...
}
很明顯進程在切換的時候會執行到vfp_notifier()中去。仔細研究vfp_notifier()的代碼,沒有發現保存VFP寄存器的代碼。倒是這段代碼比較可疑:
...
fmxr(FPEXC, fpexc & ~FPEXC_EN);
...
FPEXC的FPEXC_EN是VFP功能的使能位,如果這位未被設置,CPU在執行到VFP指令時會產生“未定義指令”中斷。好了,每次一個新進程被切換到CPU的時候FPEXC_EN總是未被設置,此時一個VFP指令就產生了一個“未定義指令”中斷。
好了。程序這下跑到__und_usr()->call_fpe()->do_vfp(),這個過程其實還是很複雜的,有興趣可以仔細閱讀代碼。
do_vfp:
...
  ldr r4, .LCvfp
ldr r11, [r10, #TI_CPU] @ CPU number
add r10, r10, #TI_VFPSTATE @ r10 = workspace
ldr pc, [r4]  @ call VFP entry point
...
.LCvfp:
.word vfp_vector
好嗎跑到vfp_vector,vfp_vector這個函數指針在vfp_init()裏面賦值 vfp_vector = vfp_support_entry;
關鍵就在vfp_support_entry裏面。vfp_support_entry不僅負責保存/恢復進程的VFP寄存器值,還負責處理VFP的異常(例如除0等等)。
具體的實現過程有興趣的可以慢慢的看了。[/i][/i]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章