FPU (4) IEEE 與 BCD

来源:互联网 发布:js radio 选中的值 编辑:程序博客网 时间:2024/06/05 09:13

 

Ch 25 FPU (4) IEEE 與 BCD

這一章小木偶將介紹兩個有用的副程式,一個是可以把 FPU 的堆疊頂直接印在螢幕上 (註一),另一個是可以接受 ASCII 阿拉伯數字字串並存入 FPU 堆疊頂。

為了了解把堆疊頂數值印在螢幕上,必須先說明什麼是『科學記號』?『科學記號』是一種可以表示很大或很小數的一種表示方法,任何數均可以化成

a×10x

這個記號,a×10x,就稱為『科學記號』,其中 a 必須在 1 和 10 之間,可以等於 1 但不能等於 10,而 x 是整數,可以是正數、負數或零。至於 a 與 x 中文如何稱呼?小木偶不知道,但下文要用到,姑且稱它們為『有效數』與『次方數』。例如:

       1 = 1×100
54000000 = 5.4×107
0.000022 = 2.2×10-5

第三個例子,2.2 稱為『有效數』,-5稱為『次方數』。『科學記號』用來表示很大或很小的數時,很方便而且一目了然,所以小木偶前面所提到的兩個副程式就是把 FPU堆疊頂以科學記號表示,另一個副程式是輸入科學記號字串,轉換成 IEEE 格式存於堆疊頂。而實際輸入時 10 的次方數用『E』表示,例如2.3E2 即 2.3×102


IEEE 轉換成 ASCII 字串:ieee_to_ascii_5c

原理

要把 FPU 堆疊頂以 ASCII 文字字串的科學記號表示出來,必須求出『有效數』和『次方數』。在 FPU 指令中有一個 FBSTP指令可以把 IEEE 格式的整數轉換成聚集 BCD 數,本程式就是想辦法把『有效數』轉換成整數,而次方數必定是整數,再用 FBSTP指令存成聚集 BCD 數,再調整有效數的小數點位置就成了。

次方數的求法並不難,利用以 10 為底的對數很容易就算出來了。

log10(a×10x) = log10a + log1010x = log10a + x

其整數部份就是次方數,但是 x87 指令裏並沒有求以 10 為底的對數值,只有求以 2 為底的對數值,即 FYL2X,不過還好小木偶還記得國中老師教的,用『換底公式』可以計算出來,換底公式是:

換底公式

換句話說,只要先使 ST(1) 與 ST 之值分別是 1 與要在螢幕上顯示的數,再用 FYL2X 指令把 ST 變成以 2 為底的對數值,再除以 Log210 ,最後再把結果自整數捨去就可得到次方數。求出次方數後再求有效數就不難了。其實您可以想像,有效數就是原來的值 (要顯示在螢光幕的數) 除以 10 的次方數。小木偶想舉個例子說明,可能會更清楚一些。

假如有一個數 57923.8912 要把它變成科學記號,顯然答案是 5.79238912×104,在小木偶的程式裏是先計算 log1057923.8912,這個對數值是 4.762857729,只要取整數部份即為次方數。把原來的數除以 104 就得到有效數 5.79238912 了。只不過這樣算出來的有效數仍是經過編碼的 IEEE 格式,但是如果乘以 1017,就可以變成整數,再用 FBSTP 這個指令直接把它以 packed BCD 的格式存入記憶體中,這樣就很容易變成用 ASCII 格式了。

為何要乘以 1017 呢?原來是以 FBSTP 存入的整數最大有 18 位,而扣掉個位數,所以乘以 1017 就足以應付了。

原始碼

和前一章一樣,要組譯成 COM 檔的程式碼和 EXE 不同。而組譯給 EXE 用的副程式稱為 ieee_to_ascii_5x,只能被EXE 檔呼叫,組譯給 COM 檔用的程式稱為 ieee_to_ascii_5c,能被 EXE 檔或 COM 檔呼叫。此處的 5 是指給Pentium 等級或及其以上的 CPU 使用的副程式。底下是 ieee_to_ascii_5c 的原始碼:

        .286
.8087
;***************************************
code segment byte
assume cs:code,ds:code
public ieee_to_ascii_5c
extrn x_p_y_5c:near
;---------------------------------------
;目的:此副程式是將 8087 ST 暫存器內之實數,轉換成
; ASCII 數字字串之格式,並存於DS:BX所指定的位址
; 建檔於1990.05.15 20:21 修改於 1998.03.25 am
;輸入:ST - 欲轉換之實數
; DS:BX - 字串儲存位址,必需有 26 bytes,大位數於低位址
;輸出:ST - 欲轉換之實數
; DS:BX - ASCII 格式的數字字串,大位數於低位址
ieee_to_ascii_5c proc near
push si
call realad
addrad equ this word
realad: pop si
sub si,offset addrad
ftst ;022 因 Log2 0 會造成無意義,故先測試是否為0
push ax
fstsw cs:sw[si]
fwait
mov ax,cs:sw[si] ;--st0--;--st1--;--st2--;026
sahf
fld st ; x ; x ; ;028 假設要印出的數為 x
jne non_z
;030 ST為0時,exp和mans均為0
fbstp cs:exp[si] ; x ;
fld st ; x ; x ;
jmp short ok_z
;034 ST 不為零時,先計算出指數為何
non_z: fabs ; |x| ; x ;
fld1 ; 1 ; |x| ; x ;039
fxch st(1) ; |x| ; 1 ; x ;037
fyl2x ;log2|x|; x ;
fldl2t ;log2 10;log2|x|; x ;039
fdivp st(1),st ;e=log x; x ; 040 指數為『e的整數部份』
fstcw cs:cw[si] ;041 保存現有捨入方法
fwait
push cs:cw[si]
and cs:cw[si],not 0c00h ;044 更改捨入方法為『向負無限大』捨入
or cs:cw[si],0400h
fldcw cs:cw[si] ;046 載入新的捨入方法
frndint ;i=int e; x ; ;047 把 e 捨入成 i
pop cs:cw[si] ; i 即為指數部份
fldcw cs:cw[si] ;049 存回舊的捨入方法
fld st ; i ; i ; x
fbstp cs:exp[si] ; i ; x ; ;051 把 i 存入 exp 變數裏
fchs ; -i ; x ;
fild cs:ten[si] ; 10 ; -i ; x ;053 載入 10
call x_p_y_5c ; 10-i ; x ;
fmul st,st(1) ; x/10i ; x ;
fmul cs:ten17[si] ; sig ; x ; ;056 sig 為『有效數』
ok_z: fbstp cs:mans[si] ; x ;

;059 將mans,exp之packed BCD轉換成ASCII數字字串
cld
push di ;061 存入原暫存器
push bx
push es
mov di,bx ;065 DI=IEEE數轉換成ASCII後的存放位址
push ds
pop es ;066 使 ES=DS
mov bx,offset mans+9
add bx,si ;068 BX=『有效數』最高位址
mov al,cs:[bx] ;069 AL=『有效數』的正負號
call set_sign
dec bx ;071 BX=『有效數』的次高位址,此位址為數值開始
mov al,cs:[bx] ;072 AL=『有效數』的第一位與第二位
mov ah,al
and al,0f0h ;074 先處理 AL 的較大位數
shr al,4
call set_nibble
mov al,'.' ;077 存入小數點
stosb
mov al,ah ;079 再處理 AL 的較小位數
call set_nibble
push cx
dec bx ;082 再處理其他 16 位數
mov cx,8
agin0: call set_byte
loop agin0
mov al,'E' ;086 存入『E』
stosb
mov bx,offset exp+9
add bx,si ;089 BX=指數最高位址
mov al,cs:[bx] ;090 AL=指數的符號
call set_sign
mov cx,2 ;092 指數最多位數為四位數
sub bx,8 ;093 BX=跳過零後的指數最高位址
agin1: call set_byte
loop agin1
pop cx
pop es
pop bx
pop di
pop ax
pop si
ret

;104 設定正負號
set_sign: or al,al
mov al,'+'
jz positive
mov al,'-'
positive: stosb
ret

;112 解開 AL 的 packed BCD 數,並存入 DI 所指的位址
set_byte: mov al,cs:[bx]
mov ah,al
shr al,4
call set_nibble
mov al,ah
call set_nibble
dec bx
ret

set_nibble: and al,0fh
add al,'0'
stosb
ret

;127 資料區
mans dt 0
exp dt 0
ten17 dq 1.0E17
ten dw 10
cw dw ? ;控制字組
sw dw ? ;狀態字組
ieee_to_ascii_5c endp
;---------------------------------------
code ends
;***************************************
end ieee_to_ascii_5c

這個程式可以把它存成 IEEE2A5C.ASM 然後組譯並加入程式庫,MYASMLIB.LIB。


新的指令

.286

這個指令是表示可以使用 80286 新增的指令集。在這個程式裏第 75 行出現『shr al,4』這一條指令只能在 80286 及其以上等級的 CPU 使用,如果是在 8087 使用必須改成

mov     cl,4
shr al.cl

NOT

這是一個邏輯運算指令,它的意思是『非』運算,意思是把 0 變成 1,1 變成零,在組譯時,MASM就會自動算出真實數值是多少,而寫入目的檔。例如本程式的第 44 行,not 0c00h,小木偶為說明方便先把 0c00h 換成二進位是0000 1100 0000 0000,not 運算是把 0 變成 1,1 變成零,所以 not 0c00h 就變成 1111 00111111 1111,即 0f3ffh。( 其實 not 0c00h= 0f3ffh )

FSTCW/FLDCW 儲存/載入控制字組

先說說 FSTCW 指令,這個指令是把 FPU 上的『控制字組』的內容儲存到一個 16 位元的記憶體變數中,有關控制字組的詳細說明請參閱附錄二控制字組。

FLDCW 是把一個 16 位元記憶體變數載入到 FPU 的控制字組中。這個 16 位元記憶體變數之數值將會改變 FPU 的例外條件的處理、如何捨入、控制實數精確度等等操作。


ieee_to_ascii_5c 關鍵過程說明

小木偶將說明這個程式的幾個關鍵過程:求次方數的捨去、求有效數。

求次方數的捨去

在本程式中,要求某數的變成科學記號時,10 的次方數是多少?最簡單的方法是取其常用對數值。舉例來說:1234=1.234×103次方數是 3,而 Log 1234=3.091315,取其整數部份就是10 的次方數了。再看另一個例子,0.00222=2.22×10-3,次方數是 -3,而 Log 0.00222=-2.653647,如果只取整數部份,顯然是不對的,但是如果『向負無限大捨去』結果就成為 -3,也就是十的次方數了。而前一個例子,向負無限大捨去也得到正確的結果,所以本程式中捨去方式是『向負無限大捨去』。

要使 FPU 向負無限大捨去,必須改變控制字組。第 41 行先由 FSTCW 取得控制字組之資料存於 cw 變數中,這是因為我們沒有變法對控制字組直接運算。再查附錄二控制字組得知,要使 FPU 向負無限大捨去應使第 10、11 位元分別為 0、1,所以在第 44 行先使 cw 的第 10、11 位元均別為 0 (對原控制字組之資料做 and 0f3ffh 運算 ),再使第 10 位元變為 1 ( 對控制字組之資料做 or 400h 運算 )。最後再用FLDCW 指令使新的 cw 載入到 FPU 裏的控制字組,這樣 FPU 做捨去動作時就會向負無限大捨去了。

求有效數

小木偶想還是舉例說明好了。例如 1234.567=1.234567×103,有效數是 1.234,雖然 FBSTP可以把 IEEE 但是前提必須是使堆疊頂為整數,否則會自小數點以下捨去。如何使其為整數呢?當然就是乘以10、100、1000、10000……,但是要乘以那一個得看小數位數而定,這樣很麻煩,小木偶有個間接的方法,就是先除以該數的 10的次方數,再乘以 1017,為何乘以 1017 呢?原因是 FBSTP 可以有 18 位數。例如這個例子,1234.567 先除以 1000,變成 1.234567,再乘以 1017 變成 123456700000000000,這是一個很大的整數,把它用 FBSTP 指令以聚集 BCD 數存入 mans 變數中,而小數點的位置在第一位數字之後。

FBSTP 存入時,必須先設一個 10 位元組的記憶體變數,也就是 mans,最高位址的那一個位元組只有第 7 位元是表示符號,其他第0 到第 6 位元都沒有使用。而後共有九個位元組,可以表示 18 位的聚集 BCD 數。原始程式的第 59 行到第 125行就是根據上述編碼方式,解開這個聚集的 BCD 數的程式。


ASCII 字串轉換 IEEE:ascii_to_ieee_5c

這個副程式是用來把一個字串變成 IEEE 實數格式,並存於 ST 暫存器中。而這個字串可以是 ASCII 編碼的阿拉伯數字,或是非聚集的 BCD 數字,但是都必須以歸位字元 (0DH) 做為結尾,且大位數在低位址。

小木偶為什麼要寫這個程式呢?原來當使用者以鍵盤輸入數字時,其實每按下一個鍵,電腦將其掃描碼與 ASCII碼記錄於記憶體中成為字串,但這些字串無法為 FPU 立即運算,即使是 CPU 也只能做聚集或非聚集的 BCD 運算,所以小木偶依照AH=0AH/INT 21H 所輸入字串格式,寫了這個副程式,把字串化成實數。

這個副程式所能接受的字串包含 ASCII 字串與非聚集的 BCD 數,換句話說 ASCII 字元只要在 30H 到 39H 或是數字在00H 到 09H 都可以,此外字串包含小數點『.』(2EH) 或是『E』、『e』也可以被接受的。『E』、『e』是表示十的幾次方的意思。

原始程式

cr      equ     0dh
;***************************************
code segment byte
assume cs:code,ds:code
;---------------------------------------
;目的:將 ASCII 數字格式轉換成 IEEE 實數格式並存於 ST 堆疊暫存器
;日期:建立於1990.12.01 1:19,修改於1998.04.07,2003.01.01
;輸入:DS:BX - ASCII 數字字串或非聚集 BCD 數起始位址,此數字字串的較大數在低位址,
; 且以 0dh 字串結束符號,例如:01 02 03 2E 04 45 02 0D 表示 123.4E2
; 或 31 32 33 2E 34 65 32 0D 表示 123.4E2,都是可以接受的格式
;輸出:CF - CY 錯誤;NC 正常
; ST - 若正常則 IEEE 實數;若錯誤則重設 FPU
extrn x_p_y_5c:near
public ascii_to_ieee_5c
ascii_to_ieee_5c proc near
push si ;016 保存 SI
call real2 ;017 SI=此副程式所需資料之真正位址
addr2 equ this word
real2: pop si
sub si,offset addr2

push ax ;022 保存 AX、BX
mov cs:byte ptr status[si],0 ;023 設定沒有小數點、E、有效數
push bx
cmp byte ptr [bx],cr ;025 檢查是否為空字串,及使用者
jz zero ; 僅輸入一個 0DH
call sign ; s ; ;027 取得有效數正負號
call get_num ; m ; ;028 計算有效數數值
jc error
cmp al,cr
jz ok

and cs:byte ptr status[si],0fdh
call sign ; s ; m ;034 取得 10 的次方數之正負號
call get_num ; e ; m ;035 計算 10 的次方數數值
jc error
fild cs:ten[si] ; 10 ; e ; m ;
call x_p_y_5c ; 10^e ; m ;
fmulp st(1),st ;m*10^e ; ;039 得到 m*10^e 於 ST
ok: clc ;040 正常結束
exit: pop bx
pop ax
pop si
ret

zero: fldz ;046 使用者只輸入一個 0DH
jmp ok

error: stc ;044 錯誤結束
finit
jmp exit
;---------------------------------------
;053 取得正負號之副程式
sign: mov al,[bx]
fld1 ; s=+1 ;
cmp al,'-'
jz sign_m
cmp al,'+'
jz sign_p
ret
sign_m: fchs ; s=-1 ;
sign_p: inc bx
ret
;---------------------------------------
;065 檢查 AL 是否在 0-9 或 30H-39H 之間,若是則 NC 並使 AL 在 0-9;若否則 CY
check: cmp al,9
jb num
sub al,'0'
jb non_n
cmp al,9
ja non_n
num: clc
ret
non_n: stc ;074 輸入字串含非數字,錯誤
ret
;076 資料區---------------------------------------------------------------------
x dw ? ;FPU 只能由記憶體載入數值,每一位數暫時存放此處再載入 FPU
ten dw 10 ;常數 10
status db ? ;bit0=0 無E符號 bit1=0 無小數點 bit2=0 無有效數
; 1 有E符號 1 有小數點 1 有有效數
;-------------------------------------------------------------------------------
get_num: ;082 計算數值之副程式
fldz ; i=0 ; s ;083 先載入 0
n0: cbw ;084 使 AH=0,以便存入 x 變數
mov al,[bx]
cmp al,cr
jz n2
cmp al,'E'
je n1
cmp al,'e'
je n1
cmp al,'.'
je n6
call check
jnc n7
jmp short n5

;098 處理輸入字串中含科學記號 E 之程式片段
n1: test cs:status[si],1 ;099 檢查是否已經有『E』了
jnz n5
or cs:status[si],1 ;101 還未有『E』,正確

;103 處理結尾符號 0DH
n2: test cs:status[si],0fh
jz na
test cs:status[si],2
jz n3 ;107 如有小數點須先將 t 彈出
fcomp st(1) ; i ; s ;108 彈出 t
n3: test cs:status[si],4 ;109 檢查字串是否沒有有效數
jnz short n4
fcomp st ; s ; ;111 若無有效數,則設有效數為 1
fld1 ; i=1 ; s ;
n4: fmulp st(1),st ; m=i*s ; ;113 使有效數值與正負號相乘
inc bx
clc
ret
n5: stc
ret

;120 處理字串中含小數點的程式片段
n6: test cs:status[si],2 ;121 檢查是否已經有小數點
jnz n5
or cs:status[si],6 ;123 還未有小數點
fld1 ; 1 ; i ; s ;124 ST=0.1,以後每增一
fidiv cs:ten[si] ; t=0.1 ; i ; s ; 位小數,t 變為原來
jmp short n9 ; 的十分之一

;128 處理字串中的『數』的程式片段
n7: mov cs:x[si],ax ;129 存入暫時存放處
or cs:status[si],4 ;130 設定有有效數旗標
test cs:status[si],2 ;131 檢查此位數為整數部份或小數部份
jnz n8
;133 沒輸入小數點,故為整數部分
fimul cs:ten[si] ; i=10i ; s ; ;134 每增加一位數,
fild cs:x[si] ; x ; i ; s ; 原數變 10 倍
faddp st(1),st ;i=10i+x; s ; ;136 加上新增的一位數
jmp short n9
;138 已輸入小數點,故為小數部分
n8: fld st ; t ; t ; i ; s
fimul cs:x[si] ; x=x*t ; t ; i ; s ;140 新增一位小數
faddp st(2),st ; t ; i=x+i ; s ;
fidiv cs:ten[si] ;t=t/10 ; i ; s
n9: inc bx
jmp n0

na: fcomp st
fldz
jmp n4
ascii_to_ieee_5c endp
;---------------------------------------
code ends
;***************************************
end ascii_to_ieee_5c

把它寫好後存入 A2IEEE5C.ASM,組譯好並加入程式庫,MYASMLIB.LIB。

ascii_to_ieee_5c 程式說明

假想,您現在是一個使用者,要輸入一個數字,這個數字可能是像『12345』這樣的整數,也可能是像『0.003』或是像『-88.88』或是像『6.02×1023』這樣的數,所以這個副程式得考慮到所有使用者可能輸入的情況。

為了簡化程式,當使用者輸入像『6.02×1023』這樣的數時,10的幾次方是用『E』來表示,而在『E』之前與『E』之後都應該是數字,所以小木偶設立一個副程式,get_num,來取得數字部份,這樣在取得有效數或10 的次方數都只呼叫 get_num 就可以了。此外在數字之前也有可能包含正負號,所以在呼叫 get_num 之前先呼叫 sign副程式來設定正負號,如果為正號或使用者沒有輸入則會在 ST 存入 +1,反之則為 -1。

接下來是這個程式最重要的部份,即數值的取得,也就是 get_num 那段程式片段 (第 82 行到第 148行)。這段程式主要可分為兩部份,所輸入的數字字元是在整數部份或是在小數部份,假如是在整數部份,則原先的數必須變成 10倍,然後再加上該數字;假如是在小數部份,則原先的數值不變,而新加入的數字要比變成原數值的最小位數還小十分之一。

舉例來說,如果使用者輸入『1234.567』這個字串,在記憶體中位址由低而高是『31 32 33 34 2E 35 36 370D』,最先處理的是整數部份的『1』,當處理到第二位,2,時原先的 1 變成 10 再加上 2,於是成 12,當處理到 3 時,原先的 12變 120 再加上 3 成為 123……一直到整數數結束,在程式第 134 行就是使原數乘以 10。處理小數時,第一位,5,應該看成0.1*5,第二位,6,應該看成 0.01*6……,所以小木偶在第 124、125 行設定 0.1,當每新增一位時,先乘以新增的數 (第140 行),再加到原數就可算出數值部份了 (第 141 行),最後為了下一位數運算,還要把 0.1 再除以 10(第 142 行)。

其他值得提一提的大概是 status 變數了,這個變數是小木偶用來記錄『旗標』的。它的功用就類似 CPU 的旗標。status的第零位元代表使用者輸入的字串是否包含『E』字元,小木偶先假設沒有『E』字元,該位元設為零(第23行),當程式讀取到『E』時再設該位元為一(第101 行),但是如果使用者輸入兩個或兩個以上的『E』顯然是不合法的數字,於是再讀取到『E』時就得檢查是否已經有讀過『E』了(第 99行),於是產生錯誤。

status的第一位元是用來檢查小數點的,方法同上,也是先假設沒有輸入小數點,當遇到小數點時設定此位元為一,再遇到小數點時就產生錯誤。status的第二位元是用來檢查使用者是否輸入『次方符號前毫無任何數字的字串』,例如『E02』這樣的字串,這類字串應該是省略有效數,1,這個副程式將它看成100。小木偶先假設該位元為零,表示使用者所輸入的字串就是類似『E02』的這樣字串,但是只要有輸入任何有效數值,就會使該位元為一(第123、130行);假如都沒有遇到有效數字,那該位元會保持為零,所以程式 109 到 112行檢查是否是這樣的情況,假如是這種情形,則有效數必須設為一。


測試 ieee_to_ascii_5c 與 ascii_to_ieee_5c

寫好這兩個副程式後,小木偶再寫個程式來測試它們,稱為 TSTIEEE.ASM (TST 是 test 之意)

;***************************************
code segment
assume cs:code,ds:code
org 100h
extrn ascii_to_ieee_5c:near
extrn ieee_to_ascii_5c:near
;---------------------------------------
start: jmp begin
string db 50,0,50 dup (?)
num db 0dh,0ah,26 dup (?),'$'
two dw 2
mes db 0dh,0ah,'Error!$'
begin: mov ah,0ah
mov dx,offset string
int 21h
finit
mov bx,offset string+2
call ascii_to_ieee_5c
jc error
mov bx,offset num+2
call ieee_to_ascii_5c
mov dx,offset num
jmp short pnt
error: mov dx,offset mes
pnt: mov ah,9
int 21h
int 20h
;---------------------------------------
code ends
;***************************************
end start

寫好之後,進行下面組譯、連結:

H:/HomePage/SOURCE>masm tstieee; [Enter]
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987. All rights reserved.


51498 + 365446 Bytes symbol space free

0 Warning Errors
0 Severe Errors

H:/HomePage/SOURCE>link tstieee [Enter]

Microsoft (R) Personal Computer Linker Version 2.40
Copyright (C) Microsoft Corp 1983, 1984, 1985. All rights reserved.

Run File [TA2I5C.EXE]: [Enter]
List File [NUL.MAP]: [Enter]
Libraries [.LIB]: myasmlib [Enter]
Warning: no stack segment

H:/HomePage/SOURCE>exe2bin tstieee tstieee.com [Enter]

H:/HomePage/SOURCE>ta2i5c [Enter]
123456 [Enter] →輸入數字
+1.23456000000000000E+0005 → TSTIEEE 印出的科學記號
H:/HomePage/SOURCE>ta2i5c [Enter]
-0.000355 [Enter]→再測試一次
-3.55000000000000000E-0004

註一:除了本章的程式之外,當然也有其他的方法把 IEEE 格式的暫時實數轉換成 ASCII 字串,例如 SYMDEB 可以顯示各種實數格式,顯然其內必定包含此種程式的程式碼,小木偶也嘗試過追蹤它,也找到了這段程式碼,請參考附錄八。


回到首頁,到第二十四章,到第二十六章