Ch 29 輸出入埠 (2) 8253/8254
這一章裏,小木偶將介紹 PC 裏面的計時晶片,8253/8254 晶片。然後以 8253/8254 晶片推動喇叭使喇叭發出聲音。這個喇叭是隱藏在主機裏的,不是主機外面接在音效卡後面的喇叭。早期的電腦沒有很強的聲音功能,僅僅靠主機板裏的喇叭發出『嗶』的一聲,例如當電腦啟動時,如果沒有問題,它會發出一短聲『嗶』,或者當您一下子輸入太多按鍵,電腦來不及處理,也就鍵盤緩衝區填入太多資料來不及處理時,電腦喇叭也會發出『嗶』的一聲通知您。這一章所指的只是讓這個喇叭發出這種單調的聲音,不是您想的像流行歌曲的那種聲音。
本章末了,再以 8253/8254 晶片為基礎,設計一個精密計時器程式,此計時器可以延遲一段時間,精密度達一微秒,利用此特性,小木偶再撰寫一個可以彈奏一首曲子的程式。
8253/8254 計時器
8253/8254 結構
IBM 在推出 PC/XT 時,裏面負責計時的是 8253 晶片,到了 IBM 推出 AT 級電腦時改用 8254 晶片,不過這兩個晶片幾乎完全相容,差別有二:8254 有讀回模式,8253 則無此模式;另外 8253 可接受的最大時脈為 2.6MHz,而 8254 為 10MHz,不過在 PC 上,它們都是接收主機板上的一個石英震盪器所產生的時脈,此震盪器每秒震盪 1193180 次,所以對程式設計師來說幾乎是相同的。此外它們的計時方式可以藉由程式的規劃而改變,所以也稱為可程式計時晶片 ( programmable interval timer ),也可縮寫為 PIC。
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 內的這三個計時器,每一個計時器上由三部份組成:
- 兩個 16 位元的暫存器:計數暫存器 ( counter register ) 和閂暫存器 ( latch register )。
- 兩個輸入信號裝置:控制閘 ( gate ) 和時序脈衝 ( clock )。
- 一個輸出信號:輸出端 ( out )。
每當計時器重新被規劃後,由 I/O 埠寫入的數值會被保留在閂暫存器裏,然後複製一份到計數暫存器中,計數暫存器依時序脈衝訊號開始遞減倒數到零時,便會有一方波經過控制閘,假如控制閘開啟,此方波訊號便會通過輸出端而到達周邊裝置,周邊裝置便收到一個訊號,假如控制閘關閉,則輸出端便無作用,周邊裝置便收不到訊號。
以計時器零來說,它被 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 與喇叭的連接示意圖如下:

至於計數值應該填入什麼數值呢?答案顯然不是聲音的頻率。因為聲音頻率越大,每秒振動越多次,計數暫存器應該要很快的變為零,所以計數暫存器所填入的數值要小才行,也就是說我們所填入的數值是某數除以頻率。某數又是多少呢?以 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 晶片雖能推動喇叭,但這只是它附屬的功能,它主要的功能其實是計時。這裏所謂的計時是指兩事件發生相距多少時間,稱為時間間隔。有關計時器的方法,一般是利用 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 計時器零裏面的計數暫存器數值。該數值在 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 裏並沒有檢查一定要用『,』或空白。接下來的兩個位元組表示此音符的拍子,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] 演奏『祝你幸福』 →程式回應歌曲名
就可以看見螢幕上顯示歌名,並發出聲音。在這個程式裏,您也可以使用長檔名,自從 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 | 雙字組 |
程式第 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』表示結束,這幾行就是做這樣的事。
開啟這個歌曲檔之後,先得到此檔的檔案資訊,由檔案資訊裏求出檔案大小。此程式最後會讀取整個檔案內容,並存放在程式尾端的 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 行是定義音符頻率,一首歌裏通常會有少部份低音與高音。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秒 )