跳轉到內容

序列埠程式設計/DOS 程式設計

華夏公益教科書,開放書籍,開放世界

現在是時候建立我們在前面已經建立的一切了。雖然您不太可能將 MS-DOS 用於大型應用程式,但它是一個很好的作業系統,可以演示與 8250 UART 的軟體訪問和驅動程式開發相關的許多想法。與 Linux、OS-X 或 Windows 等現代作業系統相比,MS-DOS 幾乎不能稱之為作業系統。它真正提供的只是對硬碟的基本訪問和一些小的實用程式。這對於我們在這裡處理的內容來說並不重要,這是一個很好的機會來了解我們如何直接操作 UART 以獲得計算機所有方面的全部功能。我正在使用的工具都是免費的(如啤酒),可以在模擬軟體(如 VMware 或 Bochs)中使用,也可以嘗試這些想法。序列裝置的模擬通常是這些程式的弱點,因此如果您從 DOS 的軟盤引導或在另一臺舊的計算機上工作,它可能更容易,否則該計算機將被丟棄,因為它已經過時了。

對於 Pascal,您可以檢視這裡

  • Turbo Pascal [1] 版本 5.5 - 這是我實際用於這些示例的軟體,也是大多數網路上較舊文件支援的編譯器(通常)。
  • Free Pascal [2] - *注意* 這是一個 32 位版本,儘管有一個用於 DOS 開發的移植版本。與 Turbo Pascal 不同,它還在不斷開發中,對於在 DOS 中執行的嚴肅專案更有價值。

對於 MS-DOS 替代(如果您碰巧沒有 MS-DOS 6.22)

  • FreeDOS [3] 專案 - 現在微軟已經放棄了 DOS 的開發,這幾乎是唯一剩下的純粹命令列驅動的並遵循 DOS 架構的作業系統。

Hello World,序列資料版本

[編輯 | 編輯原始碼]

簡介 中,我提到編寫實現 RS-232 序列通訊的計算機軟體非常困難。一個非常簡短的程式表明,至少一個基本的程式並不難。事實上,比典型的“Hello World”程式只多三行。

 program HelloSerial;
 var
   DataFile: Text;
 begin
   Assign(DataFile,'COM1');
   Rewrite(DataFile);
   Writeln(DataFile,'Hello World');
   Close(DataFile);
 end.

所有這些都行得通,因為在 DOS(以及所有版本的 Windows 也是如此……在這點上)有一個名為 COM1 的“保留”檔名,它是作業系統連線到序列通訊埠的。雖然這看起來很簡單,但它具有欺騙性地簡單。您仍然無法訪問能夠控制波特率或調變解調器的任何其他設定。然而,使用上一章中討論的有關 UART 的知識 程式設計 8250 UART,這是一個相當簡單的事情。

為了嘗試更簡單的事情,您甚至不需要編譯器。這利用了 DOS 中的保留“裝置名”,並且可以在命令提示符下完成。

C:\>COPY CON COM1

您在這裡所做的是從 CON(控制檯或您在計算機上使用的標準鍵盤)獲取輸入,並將其“複製”到 COM1。您還可以使用此方法的變體來完成一些有趣的檔案傳輸,但它有一些重要的限制。最重要的是,您無法訪問 UART 設定,這只是使用 UART 的任何預設設定,或者您上次使用序列終端程式更改設定時使用的設定。

查詢 UART 的埠 I/O 地址

[編輯 | 編輯原始碼]

我們必須處理的下一個重大任務是嘗試找到埠 I/O 的基本“地址”,以便我們可以直接與 UART 晶片通訊(請參閱 典型的 RS232 硬體配置 模組中關於介面邏輯的部分,瞭解有關此內容的資訊)。對於“典型的”PC 系統,以下通常是您需要處理的地址

序列埠名稱 基本 I/O 埠地址 IRQ(中斷)號
COM1 3F8 4
COM2 2F8 3
COM3 3E8 4
COM4 2E8 3

在 RAM 中查詢 UART 基地址

[編輯 | 編輯原始碼]

我們將在稍後回到 IRQ 號的問題,但現在我們需要知道從哪裡開始訪問有關每個 UART 的資訊。如前所述,DOS 還跟蹤序列 I/O 埠的位置,以便自身使用,因此您可以嘗試在 DOS 使用的記憶體表中“查詢”,以嘗試找到正確的地址。這並不總是有效,因為我們超出了正常的 DOS API 結構。其他與 MS-DOS 相容的作業系統(FreeDOS 在這裡工作正常)可能不會以這種方式工作,因此請注意,這可能只是完全給您錯誤的結果。

序列 I/O 埠的地址可以在 RAM 中的以下位置找到


偏移量
COM1 $0040 $0000
COM2 $0040 $0002
COM3 $0040 $0004
COM4 $0040 $0006


這些地址是由 BIOS 在啟動時寫入記憶體的。如果其中一個埠不存在,BIOS 會將零寫入相應的地址。請注意,地址以段:偏移量格式給出,您需要將段地址乘以 16 並加上偏移量才能得到記憶體中的物理地址。這就是 DOS “查詢”埠地址的地方,這樣您就可以執行本章中的第一個示例程式。

在彙編器中,您可以這樣獲取地址

; Data Segment
.data
Port  dw 0
...

; Code Segment
.code
mov ax,40h
mov es,ax
mov si,0
mov bx,Port ; 0 - COM1 , 1 - COM2 ...
shl bx,1
mov Port, es:[si+bx]

在 Turbo Pascal 中,您可以幾乎以相同的方式獲取這些地址,而且在某些方面甚至更容易,因為它是一種“高階語言”。您所要做的就是新增以下行以訪問 COM 埠位置作為簡單陣列

 var
   ComPort: array [1..4] of Word absolute $0040:$0000;

保留的、非標準的詞語 absolute 是一個用於編譯器的標記,它指示編譯器不要“分配”記憶體,而是使用您已經指定的記憶體位置。除非您訪問諸如始終儲存在該記憶體位置的這些 I/O 埠地址之類的元素,否則程式設計師很少會這樣做。

對於一個簡單的程式,它只是打印出所有四個標準 COM 埠的 I/O 埠地址表,您可以使用這個簡單的程式

 program UARTLook;
 const
   HexDigits: array [$0..$F] of Char = '0123456789ABCDEF';
 var
   ComPort: array [1..4] of Word absolute $0040:$0000;
   Index: Integer;
 function HexWord(Number:Word):String;
 begin
   HexWord := '$' + HexDigits[Hi(Number) shr 4] +
                    HexDigits[Hi(Number) and $F] +
                    HexDigits[Lo(Number) shr 4] +
                    HexDigits[Lo(Number) and $F];
 end;
 begin
   writeln('Serial COMport I/O Port addresses:');
   for Index := 1 to 4 do begin
     writeln('COM',Index,' is located at ',HexWord(ComPort[Index]));
   end;
 end.

搜尋 BIOS 設定

[編輯 | 編輯原始碼]

假設標準 I/O 地址似乎不適用於您的計算機,並且您也無法透過搜尋 RAM 找到正確的 I/O 埠偏移量地址,那麼仍然沒有絕望。假設您之前沒有意外更改這些設定,您也可以嘗試在計算機的 BIOS 設定頁面中查詢這些數字。您可能需要進行一些操作才能找到這些資訊,但如果您在計算機上擁有傳統的序列資料埠,它將存在。

如果您使用的是透過 USB 連線的序列資料埠(在較新的計算機上很常見),那麼您就無法(輕鬆地)在 DOS 中進行直接的序列資料通訊。相反,您需要使用更高階的作業系統,例如 Windows 或 Linux,這超出了本章的範圍。我們將在後續章節中介紹如何在這些作業系統中訪問序列通訊例程。我們在此討論的基本原理仍然值得回顧,因為它涉及基本的 UART 結構。

雖然嘗試使 IRQ 可選並且不假設上述資訊在所有情況下都是正確的可能很有用,但需要注意的是,大多數與 PC 相容的計算機裝置通常使用這種方式使用這些 IRQ 和 I/O 埠地址,因為存在遺留支援。令人驚訝的是,隨著計算機變得越來越複雜,即使是配備了 USB 裝置等更先進裝置,這些遺留連線仍然適用於大多數裝置。

修改 UART 暫存器

[編輯 | 編輯原始碼]

現在我們已經知道在哪裡記憶體中查詢以修改 UART 暫存器,讓我們將這些知識付諸實踐。我們現在還將對本章前面列出的表格進行一些實際應用 8250 UART 程式設計

首先,讓我們重新執行之前的“Hello World”應用程式,但這次我們將 RS-232 傳輸引數設定為 1200 波特率、7 個數據位、偶校驗和 2 個停止位。我選擇這個設定引數是因為它不符合大多數調變解調器應用程式的標準,作為演示。如果您能夠更改這些設定,那麼其他傳輸設定將變得微不足道。

首先,我們需要設定一些軟體常量來跟蹤記憶體中的位置。這主要是為了讓將來嘗試更改我們軟體的人員清楚地瞭解情況,而不是因為編譯器需要它。

 const
   LCR = 3;
   Latch_Low = $00;
   Latch_High = $01;

接下來,我們需要將 DLAB 設定為邏輯“1”,以便我們可以設定波特率

 Port[ComPort[1] + LCR] := $80;

在這種情況下,我們忽略了線路控制暫存器 (LCR) 的其餘設定,因為我們將在稍後進行設定。請記住,這僅僅是一種“快速簡便”的方法,現在就完成了。稍後我們將使用此模組演示設定波特率等內容的更“正式”方法。

接下來,我們需要輸入調變解調器的波特率。在 除數鎖存位元組表 中查詢 1200 波特率,得到以下值

 Port[ComPort[1] + Latch_High] := $00;
 Port[ComPort[1] + Latch_Low] := $60;

現在,我們需要根據我們所需的 7-2-E 通訊設定來設定 LCR 的值。我們還需要“清除”DLAB,我們也可以同時進行。

 Clearing DLAB = 0 * 128
 Clearing "Set Break" flag = 0 * 64
 Even Parity = 2 * 8
 Two Stop bits = 1 * 4
 7 Data bits = 2 * 1

 Port[ComPort[1] + LCR] := $16  {8*2 + 4 + 2 = 22 or $16 in hex}

到目前為止,事情都清楚了嗎?我們剛剛做了一些位運算,我試圖在這裡保持事情非常簡單,並試圖詳細解釋每個步驟。讓我們把所有這些都整合在一起,作為快速簡便的“Hello World”,但也要調整傳輸設定

 program HelloSerial;
 const
   LCR = 3;
   Latch_Low = $00;
   Latch_High = $01;
 var
   ComPort: array [1..4] of Word absolute $0040:$0000;
   DataFile: Text;
 begin
   Assign(DataFile,'COM1');
   Rewrite(DataFile);
   {Change UART Settings}
   Port[ComPort[1] + LCR] := $80;
   Port[ComPort[1] + Latch_High] := $00;
   Port[ComPort[1] + Latch_Low] := $60;
   Port[ComPort[1] + LCR] := $16
   Writeln(DataFile,'Hello World');
   Close(DataFile);
 end.

這正在變得越來越複雜,但還不算太複雜。不過,到目前為止,我們只做了將資料寫入序列埠。從序列資料埠讀取資料會更加棘手。

基本序列輸入

[編輯 | 編輯原始碼]

理論上,您可以使用標準 I/O 庫,就像從硬碟上的檔案讀取資料一樣,從 COM 埠讀取資料。就像這樣

 Readln(DataFile,SomeSerialData);

但是,大多數軟體在執行此操作時都會遇到一些問題。要記住的一點是,使用標準輸入例程將停止您的軟體,直到輸入完成,以“Enter”字元(ASCII 碼 13 或十六進位制 $0D)結束。

通常,您希望使用接收序列資料的程式來允許使用者在軟體等待資料輸入時執行其他操作。在多工作業系統中,這將簡單地放在另一個“執行緒”上,但由於這是 DOS,我們通常沒有執行緒功能,也沒有必要。為了將序列資料輸入到您的軟體中,我們還有其他一些替代方法。

輪詢 UART

[編輯 | 編輯原始碼]

除了簡單地讓標準 I/O 例程獲取輸入之外,最容易的做法可能是對 UART 進行軟體輪詢。這樣做有效的原因之一是,序列通訊通常比 CPU 速度慢得多,因此您可以在每個字元傳輸到計算機之間執行許多工。此外,我們正在嘗試使用 UART 晶片進行實際應用,因此這是一個很好的方法來演示晶片的功能,而不僅僅是簡單地輸出資料。

序列回顯程式

[編輯 | 編輯原始碼]

檢視線路狀態暫存器 (LSR),有一個名為 Data Ready 的位,它指示 UART 中有一些資料可供您的軟體使用。我們將利用該位,並開始直接從 UART 進行資料訪問,而不是依賴標準 I/O 庫。我們將在此處演示的程式將被稱為 Echo,因為它只做一件事,就是接收透過序列資料埠傳送到計算機的任何資料,並將其顯示在您的螢幕上。我們還將配置 RS-232 設定為更常見的 9600 波特率、8 個數據位、1 個停止位和無校驗。要退出程式,您所要做的就是按下鍵盤上的任何鍵。

 program SerialEcho;
 uses
   Crt;
 const
   RBR = 0;
   LCR = 3;
   LSR = 5;
   Latch_Low = $00;
   Latch_High = $01;
 var
   ComPort: array [1..4] of Word absolute $0040:$0000;
   InputLetter: Char;
 begin
   Writeln('Serial Data Terminal Character Echo Program.  Press any key on the keyboard to quit.');
   {Change UART Settings}
   Port[ComPort[1] + LCR] := $80;
   Port[ComPort[1] + Latch_High] := $00;
   Port[ComPort[1] + Latch_Low] := $0C;
   Port[ComPort[1] + LCR] := $03;
   {Scan for serial data}
   while not KeyPressed do begin
     if (Port[ComPort[1] + LSR] and $01) > 0 then begin
       InputLetter := Chr(Port[ComPort[1] + RBR]);
       Write(InputLetter);
     end; {if}
   end; {while}
 end.

簡單終端

[編輯 | 編輯原始碼]

這個程式並不複雜。實際上,可以從此程式中改編一個非常簡單的“終端”程式,以允許傳送和接收字元。在這種情況下,將使用 Escape 鍵退出程式,實際上,程式的大部分更改都將發生在此處。我們還首次引入了直接輸出到 UART,而不是透過標準 I/O 庫,使用以下程式碼行

 Port[ComPort[1] + THR] := Ord(OutputLetter);

傳輸保持暫存器 (THR) 是您想要傳輸的資料首先進入 UART 的方式。DOS 之前已經處理了詳細資訊,因此現在我們不需要開啟“檔案”來發送資料。為了保持事情非常簡單,我們將假設您無法以 9600 波特率輸入,或者大約每分鐘 11000 個字。只有當您處理非常慢的波特率(例如 110 波特率)時,這才會成為問題(仍然超過每分鐘 130 個字的打字速度……這確實是一位非常快的打字員)。

 program SimpleTerminal;
 uses
   Crt;
 const
   THR = 0;
   RBR = 0;
   LCR = 3;
   LSR = 5;
   Latch_Low = $00;
   Latch_High = $01;
   {Character Constants}
   NullLetter = #0;
   EscapeKey = #27;
 var
   ComPort: array [1..4] of Word absolute $0040:$0000;
   InputLetter: Char;
   OutputLetter: Char;
 begin
   Writeln('Simple Serial Data Terminal Program.  Press "Esc" to quit.');
   {Change UART Settings}
   Port[ComPort[1] + LCR] := $80;
   Port[ComPort[1] + Latch_High] := $00;
   Port[ComPort[1] + Latch_Low] := $0C;
   Port[ComPort[1] + LCR] := $03;
   {Scan for serial data}
   OutputLetter := NullLetter;
   repeat
     if (Port[ComPort[1] + LSR] and $01) > 0 then begin
       InputLetter := Chr(Port[ComPort[1] + RBR]);
       Write(InputLetter);
     end; {if}
     if KeyPressed then begin
       OutputLetter := ReadKey;
       Port[ComPort[1] + THR] := Ord(OutputLetter); 
     end; {if}
   until OutputLetter = EscapeKey;
 end.

DOS 中的中斷驅動程式

[編輯 | 編輯原始碼]

軟體輪詢方法可能足以滿足大多數簡單任務,如果您想測試一些序列資料概念,而無需編寫大量軟體,它可能就足夠了。只使用這種資料輸入方法就可以完成很多事情。

但是,當您編寫更完整的軟體時,重要的是要考慮軟體的效率。雖然計算機正在“輪詢”UART 以檢視是否已透過序列通訊埠傳送了字元,但它花費了相當多的 CPU 週期來完全不做任何事情。將像上面演示的那個程式擴充套件為非常大型程式的一小部分也會變得非常困難。如果您想從軟體中獲得最後一點 CPU 效能,我們需要轉向中斷驅動程式以及如何編寫它們。

我坦率地說,從上面列出的簡單輪詢應用程式過渡到這種複雜程度,是一個很大的飛躍,但總的來說,這是一個重要的程式設計主題。我們還將揭示 8086 晶片系列的低階行為,這些知識也可以應用於更新的作業系統,至少可以作為背景資訊。

回到之前關於 8259 可程式設計中斷控制器 (PIC) 晶片的討論,像 UART 這樣的外部裝置可以向 8086 訊號,告知需要發生一個重要任務,該任務會**中斷**當前在計算機上執行的軟體的流程。然而,並非所有計算機都這樣做,有時軟體輪詢裝置是獲取來自其他裝置的資料輸入的唯一方法。中斷事件的真正優勢在於,你可以非常快地處理來自 UART 等裝置的資料採集,而本來用來測試是否有資料可用的 CPU 時間可以用於其他任務。在設計**事件驅動**的作業系統時,它也非常有用。

中斷請求 (IRQ) 被標記為 IRQ0 到 IRQ15。UART 晶片通常使用 IRQ 3 或 IRQ 4。當 PIC 向 CPU 訊號中斷已發生時,CPU 會自動開始執行一個非常小的子程式,該子程式之前已在 RAM 中的**中斷表**中設定。啟動的具體程式取決於哪個 IRQ 被觸發。我們將在本文中演示編寫自己的軟體的能力,該軟體可以從作業系統中“接管”中斷髮生時應該發生的事情。實際上,至少對於我們正在重寫的那些部分,我們正在編寫自己的“作業系統”。

事實上,這就是作業系統作者在嘗試製作新作業系統時所做的事情……處理中斷並編寫控制連線到計算機的裝置所需的子程式。

以下是捕獲鍵盤中斷並在每次按下按鍵時在揚聲器中產生“咔嗒”聲的非常簡單的程式。關於整個部分的一個有趣的事情是,雖然它稍微偏離了主題,但這正是在與序列裝置進行通訊。典型 PC 上的鍵盤透過 RS-232 序列協議傳輸關於您按下的每個鍵的資訊,該協議通常以 300 到 1200 波特率執行,並且具有自己的自定義 UART 晶片。通常情況下,這不是您需要處理的事情,而且很少會有其他型別的裝置連線到鍵盤埠,但有趣的是,您可以通過了解序列資料程式設計來“入侵”鍵盤的功能。

 program KeyboardDemo;
 uses
   Dos, Crt;
 const
   EscapeKey = #27;
 var
   OldKeybrdVector: Procedure;
   OutputLetter: Char;
 {$F+}
 procedure Keyclick; interrupt;
 begin
   if Port[$60] < $80 then begin
     Sound(5000);
     Delay(1);
     Nosound;
   end;
   inline($9C) { PUSHF - Push the flags onto the stack }
   OldKeybrdVector;
 end;
 {$F-}
 begin
   GetIntVec($9,@OldKeybrdVector);
   SetIntVec($9,Addr(Keyclick));
   repeat
    if KeyPressed then begin
      OutputLetter := ReadKey;
      Write(OutputLetter);
    end; {if}
   until OutputLetter = EscapeKey;
   SetIntVec($9,@OldKeybrdVector);
 end.

這個程式做了很多事情,我們還需要探索 16 位 DOS 軟體的領域。為了與當時設計時可用的計算機技術合作,8086 晶片的設計者不得不做出很多妥協。與計算機的總體成本相比,計算機記憶體非常昂貴。大多數 IBM-PC 競爭的早期微型計算機只有 64K 或 128K 的主 CPU RAM,因此龐大的程式並不重要。事實上,最初的 IBM-PC 旨在僅在 128K 的 RAM 上執行,儘管它確實成為標準,通常最多使用 640K 的主 RAM,特別是在 IBM PC-XT 釋出以及 PC“克隆”市場推出被普遍認為是“標準 PC”的計算機之後。

設計提出了一種稱為**分段記憶體**的東西,其中 CPU 地址由一個記憶體“段”指標和一個 64K 記憶體塊組成。這就是為什麼這些計算機上的一些早期軟體只能在 64K 的記憶體中執行,並且為 8086 上的編譯器作者帶來了噩夢。奔騰計算機通常沒有這個問題,因為“保護模式”下的記憶體模型不使用這種分段設計方法。

遠過程呼叫

[edit | edit source]
 
{$F+}
{$F-}

此程式有兩個“編譯器開關”,它們通知編譯器需要使用所謂的遠過程呼叫。通常情況下,對於小型程式和簡單子程式,您可以使用所謂的相對索引,這樣軟體就可以讓 CPU 透過一些簡單的數學運算並“新增”一個數字到當前 CPU 地址來“跳轉”到帶有該程式的 RAM 部分,從而找到正確的指令。這樣做尤其是因為它使用相當少的記憶體來儲存所有這些指令。

但是,有時必須從與當前 CPU 記憶體地址“指令指標”完全不同的 RAM 中訪問程式。中斷程式就是其中之一,因為它甚至不必與儲存在中斷向量表中的相同程式相同。這引出了接下來要討論的部分

中斷程式

[edit | edit source]
procedure Keyclick; interrupt;

此過程名稱後面的“中斷”一詞在這裡是一個關鍵項。這告訴編譯器在組織此函式時,它必須做一些與正常函式呼叫行為略有不同的事情。通常情況下,對於計算機上的大多數軟體,您都有很多簡單指令,然後是(在彙編程式中)一個稱為

RET

這是從過程呼叫返回的助記符彙編指令。中斷的處理方式略有不同,通常應以不同的 CPU 指令結尾,該指令在彙編中稱為

IRET

或者簡稱為中斷返回。任何中斷服務例程都應該發生的一件事是,在執行任何其他操作之前“保留”CPU 資訊。您在軟體中編寫的每個“命令”都會修改 CPU 的內部暫存器。請記住,中斷可能發生在執行另一個程式的一些計算的中間,例如渲染圖形影像或進行工資計算。我們需要保留這些資訊並在子程式結束時“恢復”所有 CPU 暫存器的值。這通常透過將所有暫存器值“壓入”CPU 堆疊、執行 ISR,然後恢復 CPU 暫存器來完成。

在這種情況下,Turbo Pascal(以及其他具有這種編譯器標誌的編寫良好的編譯器)會使用此簡單標誌為您處理這些底層細節。如果您使用的編譯器沒有此功能,則必須“手動”新增這些功能並將其顯式地放入您的軟體中。這並不意味著編譯器會為您做所有事情來建立一箇中斷程式。還有更多步驟才能使它正常工作。

過程變數

[edit | edit source]
var
   OldKeybrdVector: Procedure;

這些指令使用的是所謂的過程變數。請記住,所有軟體都位於與變數和您的軟體使用的其他資訊相同的記憶體中。本質上,一個變數程式,您不必擔心它做什麼,直到軟體執行,而且您可以在程式執行時更改此變數。這是一個強大的概念,不常使用,但它可以用於許多不同的用途。在本例中,我們跟蹤了之前的中斷服務例程,並將這些例程“連結”在一起。

有一些程式稱為終止並駐留 (TSR),它們被載入到您的計算機中。其中一些被稱為驅動程式,作業系統本身也會插入子程式來執行基本功能。如果您想與所有這些其他軟體“友好相處”,確保每個人都有機會檢視中斷中的資料的既定協議是將每個新的中斷子程式連結到以前儲存的中斷向量。當我們完成了我們想用中斷做的事情後,我們就會讓所有其他程式也有機會使用中斷。還有一種可能是我們剛剛編寫的中斷服務例程 (ISR) 不是鏈中的第一個,而是一個被另一個 ISR 呼叫的 ISR。

獲取/設定中斷向量

[edit | edit source]
  GetIntVec($9,@OldKeybrdVector);
  SetIntVec($9,Addr(Keyclick));
  SetIntVec($9,@OldKeybrdVector);

再說一次,這是 Turbo Pascal 以一種方便的方式“隱藏”細節。您可以直接訪問一個“向量表”,但此向量表並不總是位於 RAM 中的相同位置。相反,如果您透過 BIOS 使用軟體中斷,則可以“保證”中斷向量會被正確替換。

硬體中斷表

[edit | edit source]
中斷 硬體 IRQ 用途
$00 CPU 除零
$01 CPU 單步指令處理
$02 CPU 不可遮蔽中斷
$03 CPU 斷點指令
$04 CPU 溢位指令
$05 CPU 邊界異常
$06 CPU 無效操作碼
$07 CPU 未找到數學協處理器
$08 IRQ0 系統計時器
$09 IRQ1 鍵盤
$0A IRQ2 來自 IRQ8 - IRQ15 的級聯
$0B IRQ3 序列埠 (COM2)
$0C IRQ4 序列埠 (COM1)
$0D IRQ5 音效卡
$0E IRQ6 軟盤控制器
$0F IRQ7 並行埠 (LPT1)
$10 - $6F 軟體中斷
$70 IRQ8 即時時鐘
$71 IRQ9 傳統 IRQ2 裝置
$72 IRQ10 保留(通常是 PCI 裝置)
$73 IRQ11 保留(通常是 PCI 裝置)
$74 IRQ12 PS/2 滑鼠
$75 IRQ13 數學協處理器結果
$76 IRQ14 硬碟驅動器
$77 IRQ15 保留
$78 - $FF 軟體中斷

此表簡要概述了中斷的一些用途及其相關的中斷號。請記住,IRQ 號主要是參考號,CPU 使用一組不同的編號。例如,鍵盤 IRQ 是 IRQ1,但在 CPU 內部編號為中斷 $09。

CPU 本身也會“生成”一些中斷。雖然從技術上講是硬體中斷,但這些中斷是由 CPU 內部 的條件生成的,有時基於軟體或作業系統設定的條件。當我們開始編寫序列通訊埠的中斷服務例程時,將使用中斷 11 和 12(十六進位制為 $0B 和 $0C)。正如您所見,大多數中斷都分配給特定任務。我省略了軟體中斷,主要是為了專注於序列程式設計和硬體中斷。

其他功能

[編輯 | 編輯原始碼]

這個程式還有其他幾個部分,不需要過多解釋。請記住,我們正在討論序列程式設計,而不是中斷驅動程式。I/O 埠 $60 很有趣,因為它是鍵盤 UART 的接收緩衝區 (RBR)。它返回鍵盤“掃描碼”,而不是實際按下的字元。事實上,當您在 PC 上使用鍵盤時,鍵盤實際上會為每個您使用的鍵傳輸兩個字元。一個字元是在您按下鍵時傳輸的,另一個字元是在“釋放”鍵向上移動時傳輸的。在這種情況下,DOS 中的中斷服務例程通常會將掃描碼轉換為軟體可以使用的 ASCII 碼。實際上,像 Shift 鍵這樣的簡單鍵也被視為另一個掃描碼。

聲音例程訪問的是 PC 內部揚聲器,而不是音效卡上的揚聲器。現在唯一使用此揚聲器的可能是 BIOS“嗶聲程式碼”,您只會在計算機出現硬體故障時聽到這些程式碼,或者在啟動或重啟計算機時聽到短暫的“嗶”聲。它從未設計用於語音合成或音樂播放等用途,驅動程式嘗試將其用於這些用途的聲音非常糟糕。儘管如此,它仍然是一個值得嘗試的東西,也是一個遺留的計算機部件,令人驚訝地,它仍然被許多當前的計算機使用。

終端程式再探

[編輯 | 編輯原始碼]

我將回到序列終端程式,這次將使用中斷服務例程重新編寫應用程式。我還想介紹一些其他概念,所以我會在這個示例程式中嘗試加入它們。從使用者的角度來看,我想新增從命令列更改終端特性的功能,並允許“終端使用者”更改波特率、停止位和奇偶校驗檢查等內容,並允許這些內容成為變數,而不是硬編碼的常量。我會解釋每個部分,然後在完成後將它們全部整合在一起。

序列 ISR

[編輯 | 編輯原始碼]

這是一個我們可以使用的序列 ISR 示例

 {$F+}
 procedure SerialDataIn; interrupt;
 var
  InputLetter: Char;
 begin     
   if (Port[ComPort[1] + LSR] and $01) > 0 then begin
     InputLetter := Chr(Port[ComPort[1] + RBR]);
  end; {if}
 end;
 {$F-}

這與我們之前使用的輪詢方法並沒有太大區別,但請記住,透過將檢查放入 ISR 中,CPU 僅在有資料可用時才進行檢查。為什麼還要檢查 LSR 以檢視是否有資料位元組可用?讀取傳送到 UART 的資料不是 UART 觸發中斷的唯一原因。我們將在後面的部分詳細介紹這一點,但現在這是一個良好的程式設計實踐,可以確認資料是否在那裡。

透過將此檢查移到 ISR 中,CPU 可以有更多時間執行其他任務。我們甚至可以將鍵盤輪詢也放入 ISR 中,但現在我們先保持簡單。

停用 FIFO

[編輯 | 編輯原始碼]

我們編寫這個 ISR 的方式存在一個小的問題。我們假設 UART 中沒有 FIFO。這個 ISR 當前編寫方式可能出現的“錯誤”是 FIFO 緩衝區中可能存在多個字元。通常,當這種情況發生時,UART 只發送單箇中斷,由 ISR 負責完全“清空”FIFO 緩衝區。

相反,我們只需完全停用 FIFO。這可以使用 FCR(FIFO 控制暫存器)完成,並明確停用 FIFO。作為額外的預防措施,我們還將在程式的初始化部分“清除”UART 中的 FIFO 緩衝區。清除 FIFO 看起來像這樣

  Port[ComPort[1] + FCR] := $07; {clearing the FIFOs}

停用 FIFO 看起來像這樣

  Port[ComPort[1] + FCR] := $00; {disabling FIFOs}

我們將在下一節使用 FIFO,所以到目前為止,這只是一個關於該暫存器的簡要介紹。

使用 PIC

[編輯 | 編輯原始碼]

到目前為止,我們不必擔心使用可程式設計中斷控制器 (PIC)。現在我們必須使用它。不需要執行 PIC 的所有潛在指令,但我們需要啟用和停用 UART 使用的中斷。每個 PC 上通常有兩個 PIC,但由於典型的 UART IRQ 向量,我們實際上只需要處理主 PIC。

PIC 函式 I/O 埠地址
PIC 命令 0x20
中斷標誌 0x21

這將以下兩個常量新增到軟體中

 {PIC Constants}
 MasterPIC = $20;
 MasterOCW1 = $21;

參考PIC IRQ 表,我們需要在軟體中新增以下行才能啟用 IRQ4(通常用於 COM1)

 Port[MasterOCW1] := Port[MasterOCW1] and $EF;

當我們完成程式時進行“清理”時,我們還需要使用以下軟體行停用此 IRQ

 Port[MasterOCW1] := Port[MasterOCW1] or $10;

請記住,COM2 位於另一個 IRQ 向量上,因此您必須使用不同的常量才能使用該 IRQ。稍後將進行演示。我們使用邏輯與或對該 PIC 暫存器中的現有值進行運算,因為我們不想更改 PC 上其他軟體和驅動程式可能使用的其他中斷向量的值。

我們還需要修改中斷服務例程 (ISR),使其與 PIC 協同工作。您可以傳送給 PIC 的一個命令稱為中斷結束 (EOI)。這向 PIC 發出訊號,表明它可以清除此中斷訊號並處理較低優先順序的中斷。如果您沒有清除 PIC,中斷訊號將保留,並且 CPU 無法處理任何其他“較低優先順序”的中斷。這是 CPU 如何與 PIC 通訊以結束中斷週期的。

以下行新增到 ISR 中以實現這一點

 Port[MasterPIC] := EOI;

調變解調器控制暫存器

[編輯 | 編輯原始碼]

這可能是您在嘗試獲取 UART 中斷時可能犯的最不明顯的錯誤。調變解調器控制暫存器實際上是 UART 與 PC 其餘部分通訊的方式。由於大多數計算機主機板上的電路設計方式,您通常必須開啟輔助輸出 2 訊號才能使中斷“連線”到 CPU。此外,在這裡我們將開啟 序列資料線 上的 RTS 和 DTS 訊號以確保裝置能夠傳輸。我們將在後面的部分介紹軟體和硬體流控制。

要在 MCR 中開啟這些值,我們需要在軟體中新增以下行

 Port[ComPort[1] + MCR] := $0B;

中斷使能暫存器

[編輯 | 編輯原始碼]

我們還沒有完全完成。我們還需要啟用 UART 本身的中斷。這很簡單,目前我們只希望 UART 在接收到資料時觸發中斷。這只是一行簡單的程式碼需要新增

 Port[ComPort[1] + IER] := $01;

到目前為止,將這些內容整合在一起

[編輯 | 編輯原始碼]

以下是使用 ISR 輸入的完整程式

 program ISRTerminal;
 uses
   Crt, Dos;
 const
   {UART Constants}
   THR = 0;
   RBR = 0;
   IER = 1;
   FCR = 2;
   LCR = 3;
   MCR = 4;
   LSR = 5;
   Latch_Low = $00;
   Latch_High = $01;
   {PIC Constants}
   MasterPIC = $20;
   MasterOCW1 = $21;
   {Character Constants}
   NullLetter = #0;
   EscapeKey = #27;
 var
   ComPort: array [1..4] of Word absolute $0040:$0000;
   OldSerialVector: procedure;
   OutputLetter: Char;
 {$F+}
 procedure SerialDataIn; interrupt;
 var
   InputLetter: Char;
 begin     
   if (Port[ComPort[1] + LSR] and $01) > 0 then begin
     InputLetter := Chr(Port[ComPort[1] + RBR]);
     Write(InputLetter);
   end; {if}
   Port[MasterPIC] := EOI;
 end;
 {$F-}
 begin
   Writeln('Simple Serial ISR Data Terminal Program.  Press "Esc" to quit.');
   {Change UART Settings}
   Port[ComPort[1] + LCR] := $80;
   Port[ComPort[1] + Latch_High] := $00;
   Port[ComPort[1] + Latch_Low] := $0C;
   Port[ComPort[1] + LCR] := $03;
   Port[ComPort[1] + FCR] := $07; {clearing the FIFOs}
   Port[ComPort[1] + FCR] := $00; {disabling FIFOs}
   Port[ComPort[1] + MCR] := $0B;
   {Setup ISR vectors}
   GetIntVec($0C,@OldSerialVector);
   SetIntVec($0C,Addr(SerialDataIn));
   Port[MasterOCW1] := Port[MasterOCW1] and $EF;
   Port[ComPort[1] + IER] := $01;
   {Scan for keyboard data}
   OutputLetter := NullLetter;
   repeat
     if KeyPressed then begin
       OutputLetter := ReadKey;
       Port[ComPort[1] + THR] := Ord(OutputLetter); 
     end; {if}
   until OutputLetter = EscapeKey;
   {Put the old ISR vector back in}
   SetIntVec($0C,@OldSerialVector);
   Port[MasterOCW1] := Port[MasterOCW1] or $10;
 end.

此時,您開始理解序列資料程式設計的複雜性。我們還沒有完成,但如果您已經走到了這一步,希望您已經理解了上面列出的程式的每個部分。我們將嘗試一步一步地進行,但此時,您應該能夠編寫一些使用序列 I/O 的簡單自定義軟體。

命令列輸入

[編輯 | 編輯原始碼]

您可以用多種不同的方式“掃描”啟動程式的引數。例如,如果您在 DOS 中啟動一個簡單的終端程式,可以使用以下命令開始

C:> terminal COM1 9600 8 1 None

或者可能是

C:> terminal COM4 1200 7 2 Even

顯然,如果使用者想要更改諸如波特率之類的簡單內容,就不需要重新編譯軟體。我們試圖在這裡完成的是獲取用於啟動程式的其他專案。在 Turbo Pascal 中,有一個函式可以返回一個字串

 ParamStr(index)

它包含命令列中的每個專案。這些專案透過字串傳遞給程式。有關如何提取這些引數的快速示例程式,請參見此處

 program ParamTst;
 var
   Index: Integer;
 begin
   writeln('Parameter Test -- displays all command line parameters of this program');
   writeln('Parameter Count = ',ParamCount);
   for Index := 0 to ParamCount do begin
     writeln('Param # ',Index,' - ',ParamStr(Index));
   end;
 end.

一個有趣的“引數”是引數編號 0,它是正在處理命令的程式的名稱。我們不會使用此引數,但它在許多其他程式設計情況下很有用。

獲取終端引數

[編輯 | 編輯原始碼]

為了簡單起見,我們將要求所有引數都以波特率、位大小、停止位、奇偶校驗的格式存在;或者根本沒有引數。此示例主要用於演示如何使用變數透過軟體使用者而不是程式設計師來更改 UART 設定。由於新增的部分不言自明,我將直接提供完整的程式。這裡將進行一些字串操作,超出了本書的範圍,但僅用於解析命令。為了保持使用者介面簡單,我們僅使用命令列引數來更改 UART 引數。我們可以構建一個花哨的介面,允許在程式執行時更改這些設定,但這留給讀者作為練習。

華夏公益教科書