Ch 29 輸出入埠 (2) 8253/8254

這一章堙A小木偶將介紹 PC 堶悸滬p時晶片,8253/8254 晶片。然後以 8253/8254 晶片推動喇叭使喇叭發出聲音。這個喇叭是隱藏在主機堛滿A不是主機外面接在音效卡後面的喇叭。早期的電腦沒有很強的聲音功能,僅僅靠主機板堛熙漭z發出『嗶』的一聲,例如當電腦啟動時,如果沒有問題,它會發出一短聲『嗶』,或者當您一下子輸入太多按鍵,電腦來不及處理,也就鍵盤緩衝區填入太多資料來不及處理時,電腦喇叭也會發出『嗶』的一聲通知您。這一章所指的只是讓這個喇叭發出這種單調的聲音,不是您想的像流行歌曲的那種聲音。

本章末了,再以 8253/8254 晶片為基礎,設計一個精密計時器程式,此計時器可以延遲一段時間,精密度達一微秒,利用此特性,小木偶再撰寫一個可以彈奏一首曲子的程式。


8253/8254 計時器

8253/8254 結構

IBM 在推出 PC/XT 時,堶戚t責計時的是 8253 晶片,到了 IBM 推出 AT 級電腦時改用 8254 晶片,不過這兩個晶片幾乎完全相容,差別有二:8254 有讀回模式,8253 則無此模式;另外 8253 可接受的最大時脈為 2.6MHz,而 8254 為 10MHz,不過在 PC 上,它們都是接收主機板上的一個石英震盪器所產生的時脈,此震盪器每秒震盪 1193180 次,所以對程式設計師來說幾乎是相同的。此外它們的計時方式可以藉由程式的規劃而改變,所以也稱為可程式計時晶片 ( programmable interval timer ),也可縮寫為 PIT。

8253/8254 內部由三個計時通道 ( 或者說 8253/8254 內部有三個計時器 ) 及一個模式控制暫存器 ( mode control register ) 組成。這三個計時器編號是 0、1、2,分別對應 I/O 埠 40H、41H、42H。這三個計時器,每一個計時器都有六種計時模式,可以藉由改變 8253/8254 模式控制暫存器重新規劃計時模式 ( 但最好不要這樣做,因為系統中所有的計時工作都由 8253/8254 負責,一旦改變計時模式很容易當機 )。模式控制暫存器在 I/O 埠編號 43H,其長度僅一個位元組,也就是八個位元長,這八個位元所代表意義如下表:

位元 位元值 意  義
7、6 00 選擇計時通道 0
01 選擇計時通道 1
10 選擇計時通道 2
11 指定讀回命令 ( 僅 8254 可用 )
5、4 00 保留住目前數值
01 僅讀寫較高的位元組
10 僅讀寫較低的位元組
11 先讀寫較低的位元組,再讀寫較高的位元組
1、2、3 000 計時模式 0
001 計時模式 1
010 計時模式 2
011 計時模式 3
100 計時模式 4
101 計時模式 5
0 0 使用二進位格式的數值
1 使用 BCD 格式的數值

8253/8254 內的這三個計時器,每一個計時器上由三部份組成:

  1. 兩個 16 位元的暫存器:計數暫存器 ( counter register ) 和閂暫存器 ( latch register )。
  2. 兩個輸入信號裝置:控制閘 ( gate ) 和時序脈衝 ( clock )。
  3. 一個輸出信號:輸出端 ( out )。

每當計時器重新被規劃後,由 I/O 埠寫入的數值會被保留在閂暫存器堙A然後複製一份到計數暫存器中,計數暫存器依時序脈衝訊號開始遞減倒數到零時,便會有一方波經過控制閘,假如控制閘開啟,此方波訊號便會通過輸出端而到達周邊裝置,周邊裝置便收到一個訊號,假如控制閘關閉,則輸出端便無作用,周邊裝置便收不到訊號。

以計時器零來說,它被 PC 用來作為系統時脈,它的閂暫存器一開機時填入 0,然後把這個 0 複製到計數暫存器中,計數暫存器的 0 開始遞減變成 0FFFFH、0FFFEH……一直到再度變為零時,便發出一個訊號,此訊號先後通過控制閘與輸出端到達 8259 晶片,8259 產生一個 INT 8 中斷。因為 8253/8254 接收每秒 1193180 次的震盪,而由 0 減到 0 共計數 65536 次才產生一個方波,所以每秒 8253/8254 產生 18.2 次的第八號中斷。

1193180÷65536=18.2

常常有許多高手就是利用此一方式檢查 INT 8,來達成常駐的目的,因為硬體每秒會產生 18.2 次中斷八。只要知道中斷八的起點,常駐程式很容易就能掌握控制權。至於計時器一與計時器二是分別用來定時更新 DRAM 記憶體內的資料與推動喇叭。由上面說明可以知道,這三個計時器都已有了固定的用途,而且計時器零與計時器一都和系統的計時有關,其控制閘不能經程式關閉,而且閂暫存器內的計數值如果任意篡改的話,很容易當機,所以只剩下計時器二可供程式設計師使用不致有當機的危險,當然經驗豐富的程式設計師不在此限。底下的表是列出各計時器內定的用途及設定值:

名稱I/O 埠 內定計
時模式
用  途
計時器 0 40H 3 系統計時
計時器 1 41H 2 DRAM 刷新
計時器 2 42H   PC 喇叭
模式控制暫存器 43H   設定計時器之計時模式及輸入格式

8253/8254 的計時器二

計時器 2 與電腦藉由埠 42H 溝通,而計時器 2 又與 PC 喇叭相連,因此吾人改變埠 42H 之計數值,便可以改變喇叭的頻率。當然要使喇叭發出聲音還得打開計時器 2 的控制閘,此控制閘在埠 61H 的位元 0。計時器 2 與喇叭的連接示意圖如下:

8253、I/O埠61H與喇叭
從圖中看來,每當計時器 2 輸出一次震盪,也藉由埠 61H 的第一位元傳給喇叭。而埠 61H 的第零及第一位元再和一個『且閘』( AND 閘 ) 相連,所以實際撰寫程式時,必須把埠 61H 的第零及第一位元均設成一。

至於計數值應該填入什麼數值呢?答案顯然不是聲音的頻率。因為聲音頻率越大,每秒振動越多次,計數暫存器應該要很快的變為零,所以計數暫存器所填入的數值要小才行,也就是說我們所填入的數值是某數除以頻率。某數又是多少呢?以 C 大調的 Do 為例,其頻率是 262Hz,也就是說每秒要振動 262 次,計數暫存器每一秒要歸零 262 次,而 8253/8254 每秒接收到石英震盪器 1193180 次的振動,所以計時器應填入 4554:

1193180÷262=4554

而 Re 頻率為 294Hz,所以計數暫存器應填入 4058:

1193180÷294=4058

其餘依此類推。換句話說,計數暫存器內的數值應為 1193180 除以頻率以後的商數。

PIANO.ASM 原始程式

底下是 PIANO.ASM 的原始程式,把它組譯、連結好並轉換成 PIANO.COM ,即可在 DOS 或 Win 9x DOS 模式下執行。

;把鍵盤模擬成鋼琴的程式:
;1:Do 2:Re 3:Mi 4:Fa 5:Sol 6:La 7:Si 8:Do

;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
message db      'PAINO v 1.0',0dh,0ah
        db      '以鍵盤模擬鋼琴的程式',0dh,0ah,0dh,0ah
        db      '鍵盤 1、2……8 表示 Do、Re……Do。',0dh,0ah
        db      '按 Esc 鍵退出程式。$'
freq    dw      262,294,330,347,392,440,494,524 ;14 頻率
begin:  mov     ah,9
        mov     dx,offset message
        int     21h

gt_key: mov     ah,7
        int     21h     ;20 讀取按鍵
        cmp     al,1bh
        je      exit    ;22 若為 Esc 鍵,則退出程式
        sub     al,'1'
        cbw
        mov     bx,ax
        shl     bx,1
        mov     ax,34dch
        mov     cx,freq[bx]     ;28 取得按鍵所代表的頻率
        mov     dx,12h          ;29 DX:AX=1234DCH=1193180D
        div     cx
        mov     bx,ax           ;31 BX=(1193180/頻率)之商數

        mov     al,10110110b    ;33 準備把 BX 寫入埠 42H 當作計數暫存器
        out     43h,al
        mov     ax,bx
        out     42h,al          ;36 先傳出 BX 之低位元組
        mov     al,ah
        out     42h,al          ;38 再傳出 BX 之高位元組
        in      al,61h
        or      al,00000011b
        out     61h,al          ;41 打開喇叭發出聲音

        mov     cx,0ffffh       ;43
delay:  mov     dx,400h
dec_dx: dec     dx
        jnz     dec_dx
        loop    delay           ;47 使聲音延續一段時間

        in      al,61h
        and     al,11111100b    ;50 遮掉位元 0 及位元 1
        out     61h,al          ;51 關掉喇叭
        jmp     gt_key

exit:   int     20h
;---------------------------------------
code    ends
;***************************************
        end     start

容小木偶稍做解說。程式第 33、34 行,把 10110110B 填入埠 43H。為何填入 10110110B 這個數呢?請參考前表可知,因為選擇計時器 2,所以第 6、7 位元是 10;要先讀寫較低的位元組再讀寫較高的位元組,所以第 4、5 位元為 11;以計時模式 3 計時,所以第 1、2、3 位元為 011;輸入數值以二進位格式 ( 即十六進位 ) 表示,所以第 0 位元為 0。

此程式為模擬鋼琴鍵盤發出聲音,所以當使用者每按下一鍵,就使喇叭發出該鍵所對應頻率的聲音,並使聲音持續一段時間而後停止發音。要使喇叭無聲,只要使埠 61H 的第 0 或第 1 位元任何一個為零即可,但又使其他位元不更動,所以在 50 行使用 AND 指令迫使位元 0 及位元 1 變為零。

程式第 43 行到第 47 行是用來延遲喇叭發出聲音的。這是因為現在電腦執行速度很快,假如沒有這段延遲時間程式,一瞬間就會執行到第 49 行使聲音太短,短到您聽不見,所以要一段延遲時間使喇叭發出的聲音夠長。至於要延遲多少,必須視電腦速度調整,速度越快的電腦必須使 DX 更大才行。小木偶的電腦是 AMD K6-2-500,也就是 500MHz 等級的,假如您的電腦是 2GHz 級的,DX 應當要更大。

底下小木偶介紹如何實作一個計時器,這個計時器也可以用在修改 PIANO 程式,不必再用嘗試錯誤的方法延遲時間。


精密計時器

原理

8253 晶片雖能推動喇叭,但這只是它附屬的功能,它主要的功能其實是計時。這堜瓵蛌滬p時是指兩事件發生相距多少時間,稱為時間間隔。有關計時器的方法,一般是利用 AH=2CH/INT 21H 或 CMOS 晶片分別讀取兩事件發生的時間,然後計算時間差就能求出時間間隔,這樣的計時方法精密度僅能到達百分之一秒。此處小木偶以讀取 8253 內含的計數暫存器來計時,這樣的計時方法精密度可接近微秒 ( 一微秒等於 10-6 秒 )。不過在慢速的電腦,如 8088、80286,執行一道指令的時間比一微秒稍小,所以實際上有一些限制無法真正達到如此精密度,但還是比 AH=2CH/INT 21H 或讀取 CMOS 系統時間來得精密。

利用 8253 計時的原理其實很簡單。前面提到,8253/8254 計時器零每秒發出 18.2 次的 INT 8H 中斷,此中斷次數會被 BIOS 記錄在記憶體位址 0000:046C 處,以雙字組的長度表示。電腦一開機後,BIOS 便開始使該雙字組由零逐漸增加,此後以每秒增加 18.2,換句話說,每 0.055 秒,0000:046C 處的雙字組會增加一:

1÷18.2=0.055

所以如果讀取兩次位於記憶體 0000:046C 雙字組,並求出兩者之差值,再除以 18.2 就是兩次讀取的時間間隔,其單位為『秒』。不過這樣的計時精密度僅僅 0.055 秒。

如果要再增加精密度,還可以再讀取 8253/8254 計時器零堶悸滬p數暫存器數值。該數值在 0.055 秒內,會由 0FFFFH 逐次遞減至 0,所以精密度為 8.4×10-7 秒,大約一微秒:

0FFFFH=65536
0.055÷65536=8.4×10-7

說了這麼多,整理一下。我們的計時器是三個字組組成的,前兩個字組是讀取 0000:046C 的雙字組,此雙字組是以 0.055 秒為單位;後一個字組是讀取計時器 0 內的計數值,此計數值為一字組長度,以 8.4×10-7 秒為單位。

當某事件發生時,因為計數暫存器內的計數值遞減至零時,0000:046C 之中斷次數才增一,所以我們應先讀取計數暫存器內的計數值,再讀取 0000:046C 內的中斷次數,這樣才更接近事件發生的時間。但是當我們讀取計數暫存器後,其計數器內的數值仍不斷地倒數,有可能在讀取計數暫存器之後,但是尚未讀取 0000:046C 的這段時間內,計數暫存器就已歸零,這樣讀取的中斷次數其實是需要減少一的。這個問題會發生在計數暫存器內的數值已經很接近零時,為避免這個問題發生,小木偶的做法是先禁止中斷發生,待讀取 0000:046C 之中斷次數後再使中斷能發生。

PLAY.ASM 與歌曲檔

底下小木偶利用上述原理撰寫一程式,PLAY.ASM,此程式可以利用 PC 喇叭彈奏一首曲子。所彈奏的曲子以純文字檔形式描述,小木偶稱之為歌曲檔,其格式第一行為歌曲名稱,第二行以後每 6 個位元組表示五線譜中的一個音符,前兩個位元組表示音高,這兩個位元組的第一個位元組表高音或低音,高音用『+』表示,低音以『-』表示,『0』表示正常,第二個位元組表示音階,C 表示 Do、D 表示 Re、E 表示 Mi、F 表示 Fa……、B 表示 Si,如果是 0 表示休止符。

之後所接的一個位元組是分隔符號,可用『,』或空白表示,其實在 PLAY.ASM 堥癡S有檢查一定要用『,』或空白。接下來的兩個位元組表示此音符的拍子,01 表示十六分音符 ( 四分之一拍 )、02 表示八分音符 ( 半拍 )、04 表示四分音符 ( 一拍 )、06 表示附點四分音符 ( 一拍半 )、08 表示二分音符 ( 兩拍 )、16 表示全音符 ( 四拍 )。拍子完後,接下來的一個位元組是分隔符號,『,』。例如底下是一首老歌,『祝你幸福』,的歌曲檔,檔名是 bless_you_happy.txt,您可用文書軟體編輯:

祝你幸福
0G,06,0A,01,0G,01,0E,04,0G,04
0C,04,0D,02,0C,01,0D,01,0E,08
0G,06,0A,02,+C,04,0A,02,0E,02
0G,16
0C,06,0C,02,0D,02,0C,01,0D,01,0E,02,0G,02
0G,02,0A,02,+C,02,+D,02,0A,01,0G,01,0E,04
00,02,0G,02,0G,02,0E,03,0D,02,0E,02,0E,02,0D,02
0C,16
0A,06,0G,01,0A,01,+C,04,0A,02,+C,02
+C,02,+D,02,+C,02,+C,02,+E,08
00,02,+D,02,+C,02,0A,02,0G,02,+C,02,0A,01,0G,01,0E,02
0G,16
0C,06,0C,02,0D,02,0C,01,0D,01,0E,02,0G,02
0G,02,0G,02,0E,02,0G,02,0A,02,0A,01,0G,01,0A,04
00,02,0G,02,0G,02,+E,02,+D,02,+C,02,0G,02,+D,02
+C,16

執行時,在 DOS 模式下輸入

D:\HomePage\SOURCEG>play bless_~1.txt [Enter]
演奏『祝你幸福』  →程式回應歌曲名

就可以看見螢幕上顯示歌名,並發出聲音。在這個程式堙A您也可以使用長檔名,自從 Win 95 上市以來,檔名就不在受限於 8.3 格式,所以程式也不應該限制只能使用 8.3 格式的檔名,事實上在 Win 9X DOS 模式堛 AH=71H/INT 21H 有一系列有關處理長檔名的服務中斷,請參考第 15 章 Ralf Brown's Home Page 的說明。所以執行 PLAY.COM 時,您也可以輸入

D:\HomePage\SOURCEG>play bless_you_happy.txt [Enter]  →使用長檔名
演奏『祝你幸福』

PLAY.ASM 原始程式

底下看看 PLAY.ASM。把 PLAY.ASM 組譯、連結好,可得 PLAY.EXE,再用 EXE2BIN.EXE 轉換成 PLAY.COM 可執行檔。

file_info       struc       ;01 定義 file_info 結構體
attributes                  dd   ?	
creation_time               dq   ?	
last_access_time            dq   ?	
last_write_time             dq   ?	
volume_serial_number        dd   ?	
file_size_high              dd   ?	
file_size_low               dd   ?	
number_of_links_to_file     dd   ?
unique_file_identifier_high dd   ?	
unique_file_identifier_low  dd   ?	
file_info       ends        ;12 結構體結束

;***************************************
play    segment
        assume  cs:play,ds:play
        org     100h
;---------------------------------------
start:  jmp     begin
song_info       file_info       <?>
handle  dw      ?
msg1    db      '檔案開啟或讀取錯誤。$'
msg2    db      '演奏『$'
msg3    db      '』',0dh,0ah,'$'
msg4    db      '檔案太大。$'
freq1   dw      131,147,165,174,196,220,247         ;26 低音頻率
freq2   dw      262,294,330,347,392,440,494
freq3   dw      524,588,660,694,784,880,988         ;28 高音頻率
lst_buf dw      ?
time1   dw      ?,?,?,0
time2   dw      ?,?,?,0

begin:  mov     si,81h          ;33 指向 PLAY.COM 參數位址
        cld
nxt0:   lodsb
        cmp     al,' '
        je      nxt0
        mov     dx,si
        dec     dx              ;39 歌曲檔名起始位址
nxt1:   lodsb
        cmp     al,0dh
        jne     nxt1
        dec     si
        mov     byte ptr [si],0 ;44 歌曲檔名結束位址

        mov     si,dx
        sub     bx,bx
        mov     dx,1
        mov     cx,dx
        mov     ax,716ch
        int     21h             ;50 開啟檔案
        jc      exit0
        mov     handle,ax

        mov     bx,ax
        mov     dx,offset song_info
        mov     ax,71a6h
        int     21h             ;57 取得檔案資訊

        mov     bx,handle
        mov     cx,word ptr song_info.file_size_low
        cmp     cx,0f000h
        ja      exit2
        mov     dx,offset buffer
        mov     ah,3fh
        int     21h             ;63 讀取檔案
        jnc     ok0

exit0:  mov     dx,offset msg1  ;59 開啟檔案或讀取錯誤
exit3:  mov     ah,9
        int     21h
exit1:  mov     ax,4c00h
        int     21h
        
exit2:  mov     dx,offset msg4  ;75 檔案太大
        jmp     exit3

ok0:    add     dx,word ptr song_info.file_size_low
        mov     lst_buf,dx      ;79 計算歌曲檔最後位址

        call    print_song_name ;81 顯示歌曲名
        mov     si,di
nxt2:   inc     si              ;83 指向音高
nxt3:   lodsw
        cmp     al,0ah          ;85 檢查是否換行
        jne     nt_lf           ;86 若不是換行,到 nt_lf
        dec     si              ;87 若換行,將 SI 減一
        cmp     si,lst_buf      ;88 ,再查是否已到檔案尾結束
        je      t_off           ;89 若已到檔案尾,到 t_off 處
        jmp     nxt3
nt_lf:  cmp     ax,3030h        ;91 檢查是否為休止符
        jne     nt_rst          ;92 若不是休止符,跳到 nt_rst 處
        call    turn_off_speaker;93 若是休止符,則關閉喇叭
        jmp     short last_t

nt_rst: call    get_freq_addr   ;96 取得應彈奏頻率位址
        call    sound

last_t: mov     di,offset time2 ;99 取得 INT 8 中斷次數,並存
        call    get_time        ;100 於 time2 雙字組變數

        inc     si
        lodsw                   ;103 取得拍子長度
        sub     ax,3030h        ;104 拍子長度的十位數在 AL,個位數在 AH
        mov     bl,ah
        cbw
        mov     bh,10
        mul     bh
        add     al,bl           ;109 AX= 拍子長度的十六進位數
        shl     ax,1            ;110 假設一拍的時間為 16/18.2 秒
        shl     ax,1            ;111 AX= 在拍子時間內的中斷次數
        mov     di,offset time2+2
        add     [di],ax         ;113 timer2= 拍子結束時的中斷次數
        adc     word ptr [di+2],0

gt_tm:  mov     di,offset time1 ;116 取得 INT 8H 及計數器數值於 time1
        call    get_time

        mov     bx,offset time1+2
        mov     di,offset time2+2
        mov     ax,[bx+2]
        cmp     ax,[di+2]       ;122 比較 time1 是否大於或等於 time2
        jb      gt_tm           ;123 若否,則再讀取 INT 8H 中斷次數及計數值
        mov     ax,[bx]
        cmp     ax,[di]
        jb      gt_tm

        cmp     si,lst_buf      ;128 若是,表示拍子已結束,檢查是否到檔案尾
        jne     nxt2
t_off:  call    turn_off_speaker
        mov     bx,handle
        mov     ah,3eh
        int     21h                 ;133 關閉檔案
        jmp     exit1
;---------------------------------------
;印出歌曲名
;輸入-buffer 之資料
print_song_name proc    near
        mov     di,offset buffer
        mov     al,0dh              ;140 尋找第一行結束位址
        repne   scasb
        mov     byte ptr [di-1],'$' ;142 加上『$』當作 AH=9/INT 21H
        mov     ah,9                ;143 所印出字串結束記號
        mov     dx,offset msg2
        int     21h
        mov     dx,offset buffer
        mov     ah,9
        int     21h
        mov     dx,offset msg3
        mov     ah,9
        int     21h
        ret
print_song_name endp
;---------------------------------------
;取得頻率位址
;輸入-AL:0、+、-等高低音
;      AH:CDEFGAB 等音階
;輸出-BX:指向 freq1、freq2、freq3 其中一個頻率位址
get_freq_addr   proc    near
        mov     dx,offset freq2 ;160 假設中音
        cmp     al,'+'          ;161 檢查是否高音
        jne     if_low          ;162 若否,跳到 if_low 檢查是否低音
        mov     dx,offset freq3 ;163 若為高音,使 DX 指向 freq3
        jmp     short ok1
if_low: cmp     al,'-'          ;165 檢查是否低音
        jne     ok1             ;166 若否,跳到 ok1
        mov     dx,offset freq1 ;167 若使低音,使 DX 指向 freq1

ok1:    and     ah,0dfh ;169 使小寫變大寫
        cmp     ah,'A'  ;170 檢查是否為 La 或 Si
        je      ra_ton
        cmp     ah,'B'
        je      si_ton
        sub     ah,'C'
ton:    mov     bl,ah   ;175 以 freq? 的位址為基準計算音高頻
        sub     bh,bh   ;176 率相對於freq? 之位址,
        shl     bx,1    ;177 並存於 BX
        add     bx,dx   ;178 再加上 DX,則 BX 為將彈奏音符之頻率位址
        ret

ra_ton: mov     ah,5
        jmp     ton
si_ton: mov     ah,6
        jmp     ton
get_freq_addr   endp
;---------------------------------------
;發出聲音
;輸入-BX:指向聲音頻率位址
;輸出-發出聲音
sound   proc    near
        mov     ax,34dch
        mov     cx,[bx]
        mov     dx,12h
        div     cx
        mov     bx,ax

        mov     al,10110110b
        out     43h,al
        mov     ax,bx
        out     42h,al          ;200 先傳出低位元
        mov     al,ah
        out     42h,al          ;202 再傳出高位元
        in      al,61h
        or      al,00000011b
        out     61h,al          ;205 打開喇叭發出聲音
        ret
sound   endp
;---------------------------------------
;取得滴答次數,存於 DI 所指的三個字組內
;輸入-DS:DI 指向三字組位址
;輸出-ES:DI 所指位址將存入 TIMER0 計數值及 INT 8H 中斷次數
;      AX、BX 之值會被破壞
get_time        proc    near
        push    ds      ;214 保存 DS、SI
        push    si      
        sub     ax,ax   ;216 使 DS:SI 指向 0000:046C
        mov     si,46ch
        mov     ds,ax

        cli             ;220 禁止硬體中斷

        mov     al,0    ;222 要求保留計時器零的計數值
        out     43h,al

        in      al,40h  ;225 讀取計數器之值,並存於 BX
        mov     bl,al
        in      al,40h
        mov     bh,al
        not     bx      ;229 使 BX 改為遞增

        mov     ax,bx
ok:     stosw           ;232 存入計數計數值
        movsw           ;233 存入 INT 8H 中斷次數
        movsw

        sti             ;236 恢復禁止的硬體中斷 
        pop     si
        pop     ds
        ret
get_time        endp
;---------------------------------------
turn_off_speaker        proc    near
        in      al,61h
        and     al,0fch
        out     61h,al  ;245 關閉喇叭
        ret
turn_off_speaker        endp
;---------------------------------------
buffer:                 ;249 歌曲檔資料存放處
;---------------------------------------
play    ends
;***************************************
        end     start

依慣例,小木偶稍作解釋。1∼12 行是定義一個結構體,此結構體表示檔案資訊,在 57 行取得檔案資訊時,系統會把 bless_yuo_happy.txt 的資訊填入此結構體。

AX=71A6H/INT 21H

這個中斷服務程式是用來獲得某個檔案的資訊,其用法是使 AX 填入 71A6H,BX 填入檔案代碼,DS:DX 指向一個結構體。當成功地取得該檔資訊時,進位旗標會被清除,並在 DS:DX 所指定的結構體填入該檔的檔案資訊,檔案資訊的結構如下:

偏移位址大小 意  義
0 雙字組 檔案屬性
04 四字組 建立時間
0C 四字組 最後讀取時間
14 四字組 最後更動時間
1C 雙字組 卷序號
20 雙字組 檔案大小 ( 較高的 32 位元 )
24 雙字組 檔案大小 ( 較低的 32 位元 )
28 雙字組
2C 雙字組
30 雙字組
在這個程式堙A最重要的是檔案大小,得到檔案大小之後,程式才知道要演奏到那兒才結束。

程式第 33∼44 行是取得使用者在命令提示下輸入 PLAY 之後的參數,也就是歌曲檔名。參數位址會放在 PSP 的 80H 處,但 80H 是輸入參數的長度,所以此程式由 81H 開始。例如您在 DOS 模式下輸入

D:\HomePage\SOURCEG>play bless_you_happy.txt [Enter]

那麼在 80H 處,會有您輸入的參數,如下︰

1825:0080  14 20 62 6C 65 73 73 5F-79 6F 75 5F 68 61 70 70  . bless_you_happ
1825:0090  79 2E 74 78 74 0D 00 00-00 00 00 00 00 00 00 00  y.txt...........
1825:00A0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

為了用 AX=716C/INT 21H 開啟檔案,必須找到歌曲檔的起始位址,並在檔案尾加上『0』表示結束,這幾行就是做這樣的事。

開啟這個歌曲檔之後,先得到此檔的檔案資訊,由檔案資訊堥D出檔案大小。此程式最後會讀取整個檔案內容,並存放在程式尾端的 buffer: 標號處,所以 buffer: 標號位址加上檔案大小就是音樂檔內容的最後位址。也就是說當成是演奏到這個位址完畢,整首歌曲就結束了,這個位址被存在 lst_buf 變數 ( 在程式第 29 行定義)。請看看程式第 52∼79 行就是做這些事情。

等這些事情做完,程式就開始彈奏樂曲了。一般而言,一個音包含音調及拍子,音調指的就是頻率,拍子是指喇叭發出的聲音應持續多久。而底下的程式就是處理這兩個問題。

第 83∼97 行是讀取 buffer: 處的音樂檔資料,此時 SI 指向音樂檔歌曲名之後的位址,用 LODSW 載入至 AX 後檢查是否換行,是否到檔案尾,是否為休止符,如果都不是的話,表示所讀到的是音高,然後到 96 行取得該音的頻率,然後到 97 行發出聲音。

發出聲音完後便是決定這個聲音持續多久,小木偶的做法在發出聲音後立即讀取時間,此時間就是發出聲音的時間,然後讀取歌曲檔的拍子長度,拍子長度就是聲音持續的時間,聲音持續的時間再加上發出聲音的時間就是停止聲音的時間,然後進入一個迴圈,此迴圈的第一步是讀取時間,再比較此時間是否等於停止發出聲音的時間,如果不相等,會回到迴圈的第一步,重複這些動作,一直到讀取的時間等於聲音停止的時間,然後進行下一個音符。

小木偶利用一個副程式,get_time,讀取時間。get_time 在程式第 214∼239 行,它會把時間記錄在 ES:DI 所指的三個字組長度的地方。副程式一開始是保存 SI 並使 DS:SI 指向 0000:046C,因為 SI 在主程式中表示歌曲檔的指標,必須保存,否則就無法得知下一個音在那兒了。接下來禁止硬體中斷,用 CLI 指令。

CLI 指令

然後讀取計時器 0 的計數暫存器內的數值,讀取的方法是先

第 26∼28 行是定義音符頻率,一首歌堻q常會有少部份低音與高音。PLAY.COM 取得某個音符頻率的過程在第 160∼184 行的 get_freq_addr 副程式,


註一:本章的第二部份包含一個延遲時間的程式,此處小木偶用 8253 計時器來達到延遲的目的,讓喇叭能持續發出一段聲音。事實上 AT 級以上的電腦中,BIOS 已經提供了這個服務程式。儘管 BIOS 已經提供這個服務程式,不過自己 DIY 一番,也是很有趣的,不是嗎?

AH=86H/INT 15H 延遲時間

此 BIOS 中斷服務程式可以使電腦延遲一微秒至一小時的時間,使用時 AH 必等於 86H,CX:DX 存放要延遲的時間,此時間以 0.977 微秒為單位。( 一微秒 = 10-6秒 )


回到首頁到第二十八章到第三十章