DOS程序员手册(四)

5.4打印机功能

    打印机是能够直接控制的输出设备之外的唯一的重要输出设备。它们的功能比屏幕

 

107页

功能要简单得多,因为它们只涉及字符输出,并最小程度地与打印机的输入有关。

      输出给打印机的最简单的方法是利用DOS层的打印机输出功能(Int 21h,功能

05h),如列表5.9所示。该功能使用户能将字符输送给打印机设备。如果使用该功能出现

了错误,通过引入关键出错处理程序,DOS就可以处理出错情况。

    列表5.9

          /*prtout.c

              Listing 5.9 of DOS Programmer'S ReferenCe*/

          #inClude<Stdio.h>

          #include<dOS.h>

          /*Prototypes*/

                VOid OUtprt(Char*str);

          void main()

          {

                OUtprt("This is a line to the printer\012\015");

          }

          void outprt(str)

                char*Str;

          {

                union REGS regs;

                regs.h.ah = 0x05;

                while(*str){

                    regS.h.dl=*str;

                    intdos(&regs,&regs);

                    str++;

                }

          }

    还可以在更低的级别引入BIOS打印功能(Int 17h)来获得对打印功能的更大控制。

在这个级别上,如果需要,可以在程序中直接检查打印机的状态,并回答打印机错误。列表

5.10显示怎样使用BIOS功能去处理打印机接口的一个实例。

    列表5.10

          /*prtchk.C

              Listing 5.10 of DOS Programmer's Reference*/

          #inClude<Stdio.h>

          #inClude<dOS.h>

          #include<stdlib.h>

          /* prototypes*/

              VOid OUtprt(Char *str);

              int prtrdy(void);

          void main()

          {

              int i;

              for(i=0; i<10;i++){

                    if(!prtrDy())exit(1);

                  outprt("This is a line to the printer\012\015");

              }

        }

                           

108页

      #define PRINTER 0x17

      void outprt(str)

            char*Str;

      {

            union REGS regs;

            regs.X.dX = 0;

            while(*str){

                regS.h.ah=0x00;

                regs.h.al=*str;

                int86(PRINTER,&regs,&regS);

                putchar(*str);

                Str++;

            }

        }

        int prtrdy()

        {

            union REGS regs;

            regs.h.ah=2;

            regs.x.dX=0;

              int86(PRINTER,&regs,&regS);

        printf("printer Status:%x\n",regs.h.ah);

            if(regs.h.ah & 0x20)

                printf("printer out of paper\n");

            if(regs.h.ah & 0x08)

                printf("printer I/O error\n");

          return ((regs.h.ah&0x20)==0 && (regs.h.ah&0x08)==0);

        }

    正如这些例子所示,可以直接从程序执行打印机输出,井同时了解打印机正做些什么

事情。

    复杂的打印机控制功能如图形和字体控制,对于正在使用的打印机,它们是特定的。

除了将字符输送给打印机,BIOS和DOS都没有内在的功能,用于操纵打印机。有关特殊

化的打印机控制功能的讨论,超出了本书的范围。

                      5.5小    结

    本章讨论了输出方面的众多功能,基本上都与视频显示有关。我们已经了解了可用的

视频方式以及可以使用的不同屏幕显示器。

    在DOS中,屏幕控制局限于一些允许向屏幕进行基本输出的简单功能,别的很少使

用。更复杂的控制需要对Bios功能的存取,从而允许光标定位和字符属性控制。在BIOS

级别上,可以进行图形显示。屏蒂定位,甚至窗口功能。产生复杂屏幕的程序几乎总是必

须在BIOS或以下的级别上进行,是借助利用屏幕显示内存的直接访问来完成的。

    打印机功能比屏幕功能受到了更多的限制。除了输出字符给打印机和检查打印机的

状态以外,别的任何事情都没有基本的功能。不能调用BIOS和DOs功能来完成打印机

图形输出,或特殊的打印机功能,如字体改变。任何特别的控制都必须由用户的程序来执

行。

    第6章讨论输入设备,它是对本章内容(输出功能)的补充。

 

 

    在大多数人看来,输入与输出结合得这样紧,以致人们常常说“I/O”或“输入/输出”,
而很少单独提到其中的一个。在这一章里,要补充第5章的内容,我们要讨论输入设备。
本章重点介绍两种使用最普遍的输入设备:键盘和鼠标。键盘由系统所包含的DOS
和BIOS功能操纵,而要使鼠标的功能发挥作用,则必须在系统中添加一个驱动程序。
键盘比大多数人所意识到的要复杂得多。作为整个pc工程的精致片断,键盘能适应
BIOS中的低级功能,并借助缓冲字符输入和提供中断来使键盘操作对于程序几乎是看不
到的,这样就让BIOS能在敲击键盘字符时操纵它们。
    因为中断直到第11章"中断处理程序”才讨论,所以本章仅以简单的方式涉及到鼠
标。这也是为一个有趣的程序才这样作的。通过使用鼠标功能(当它们存在时),可以在许
多程序中建立由鼠标所控制的功能。
    将键盘及鼠标与中断处理功能结合起来能使键盘和鼠标功能都变得更加有趣。使用热
键来启动的TSR程序,或中断鼠标输入的鼠标驱动程序都对用户的需要进行快速反应。
第11章将更详细地讨论中断。但是,目前需要了解基本的输入功能。
                      6.1键    盘
    人们总是依赖于pC机有键盘作为输入设备——每台Pc机都有,并且大多数程序都
使用它们。尽管替代性的输入设备如鼠标已普遍用于一些类型的程序,但很少有程序不需
键盘输入而进行操作。
    本节先看BIOS和DOS功能怎样用于键盘输入。这些功能用来替代高级语言的通常
输入功能时,会带来下列优点:
    .对输入的最大控制。例如,可以添加用户指定的编辑性能和特殊的帮助功能。
    .比使用高级语言所提供的键盘输入例程更乙、的程序。这些例程,用来提供每一种
      可能的输入环境和条件,但执行了大量的“开销代码”。
    .更敏捷、反应更迅速的输入,因为可以使程序对无论怎样的选择都能产生反应。
在讨论编程之前,先看看键盘的工作方式。
6.1.1了解键盘的工作方式
    对程序员来说,键盘可能是计算机上最熟悉但却最不了解的设备。大多数人了解文件
 
110页
系统和磁盘操作,因为阅读了这方面的书籍。但最少讨论键盘。只有很少的专家了解键盘。
    本书不打算描述使键盘工作的硬件,而着重看看如果要使用与键盘相关的DOS和
BIOS设备时用户必须了解的各种事件的顺序。
    首先,想象你在通过联系计算机和键盘的电缆看计算机。按键时,可以看到数字(键盘
“扫描码”)由键盘通过电缆到达计算机。
    每按一次键,就产生一个独一无二的8位数(最明显的位总是0,以示按键和后面的
显示之间的区别),即使分列左边和右边的Shift键也由不同的数字代表。这些扫描码精确
地指示按的是哪一个键。
    无论什么时候放开一个键,都会产生另一个扫描码。这个代码与前面的相同,只是设
置了高序位(即扫描码加上128)。扫描码就以这种方式用信号告诉ROM BIOS:这个键已
经放开了。
    尽管有许多不同类型的键盘配置,但大部分键盘都遵循三种基本设计中的一种。这些
设计则都源于IBM为这些不同的计算机开发的键盘设计。这三种设计是PC机(83键)键
盘、个人机AT(84键)键盘,以及增强(101键)键盘。虽然大多数扫描码在三种设计中保
持相同,但还是有些区别。图6.1、6.2和6.3分别显示了PC机、个人机AT及增强键盘上
的扫描码(十六进制)。注意增强键盘利用多个扫描码来识别它的一些附加键。
111页
                   图6.3十六进制的增强型键盘扫描码
当按住一个键超过半秒钟时,键盘就会产生一系列的扫描码以响应按键动作,并以特
助速率重复出现。 BIOS能够说出哪个键正被按住(不是反复按)。因为在这个系列中键
没有产生任何放开键的代码。
键盘只识别所按的键(不是原符),并且只提供与该键对应的扫描码。(扫描码只是指
示已经按住了某个特定的键)。BIOS翻译扫描码以确定哪个ASCII码字符对应于所按的
键。为旧的85键和84键键盘设计的BIOS ROM忽略了101键键盘上额外的键,所以许
多程序都没有利用这些附加键。
每当按一个键,键盘都会产生扫描码和Int 09h,它告诉ROM BIOS已经按住了一个
键。系统的控制马上传递给中断处理程序,它读端口96(60h)以确定按住了哪个键。中断
服务例程读扫描码,并将它转换成代表所按键的16位代码,该代码的较低字节是所按键
的ASCII码值;较高字节则是扫描码。表6.1就是这些代码的列表。
                              表6.1 AscII码和扫描代码
键        增强型    键      增强型
Esc                01          01        01            ?/              35          35        35
!1                 02          02        02            右shift            36          36        36
@2                 03           03        03            PrtSc *          37          37        E0 12
#3                 04           04        04           左Alt              38          38        38
$4                 05           05        05           空格              39           39        39
%5                 06          06        06            CapsLock          3A          3A        3A
^ 6                 07          07        07           F1                3B          3B        3B 
&7                 08          08        08            F2                  3C        3C        3C
* 8                 09           09       09            F3                  3D        3D        3D
(9                 0A          0A        0A            F4                  3E        3E        3E
)0                 0B         0B        0B            F5                  3F        3F        3F
_-                  0C          0C        0C            F6                  40        40        40
112页
                                                                   (续)
键            增强型        键        增强型
                PC         AT     增强型                  PC         AT   增强型
+=                   00        0D          00          F7                41          41        41
Backspace              0E       0E          0E          F8                42           42        42
Tab                   0F        0F          0F          F9                43          43        43
Qq                    10         10         10          F10                44          44        44
Ww                     11        11·        11          Num Lock        45           45        45
Ee                     12        12          12         ScrollLOck        46           46        46
Rr                     13        13          13          7 Home            47          47        47
Tt                     14        14          14          8向上箭头        48           48        48
Yy                    15         15         15           9 PgUp            49          49        49
Uu                    16        16          16          Gray-             4A          4A        4A
Ii                     17        17          17          4向左箭头        48          48       48
00                    18        18          18          5                 4C          4C        4C
Pp                    19         19          19          6向右箭头        4D          4D         4D
{[             1A       1A        1A      Gray+          4E       4E      4E
}]             1B      1B        1B      1End            4F      4F      4F
Enter            1C      1C        1C      2向下箭头      50       50      50
左Ctrl           1D      1D        1D      3 PsDn          51      51      51
Aa               1E      1E        1E      0 Ins           52     52      52
Ss                  1F        1F        1F         .Del             53        53          53
Dd                   20        20        20         仅AT键盘
Ff                   21        21        21          SysRq            N/A       54        N/A
Gg                  22         22        22         仅增强型键盘
Hh                  23        23        23          GrayEnter         N/A      N/A      E01C
Jj                  24        24        24          右Ctrl             N/A      N/A      E010
Kk                  25        25        25          Gray/            N/A      N/A      E035
Ll                  26        26        26          Gray *           N/A      N/A        37
:;                27        27        27          Right Alt           N/A      N/A      E038
"'                  28        28        28          Gray Home         N/A     N/A      E0 47
~                 29        29        29          GrayC向上         N/A      N/A      E048
Shift              2A        2A        2A          GaryPgUp         N/A      N/A      E049
|                2B      2B        2B      GrayC向左       N/A    N/A    E04B
Zz              2C      2C        2C      GrayC向右       N/A    N/A    E04D
Xx               2D      2D        2D      GrayEnd         N/A    N/A    E04F
Cc              2E      2E        2E      GrayC向下       N/A    N/A    E050
Vv               2F      2F        2F      Gray PgDn       N/A   N/A    E051
Bb               30      30        30      Gray Ins        N/A   N/A     E052
Nn              31      31      31        Gray De          N/A    N/A    E053
Mm              32      32      32        F11             N/A    N/A    57
<,            33      33      33        F12             N/A    N/A    58
>.             34      34      34        Pause/Break N/A    N/A    E1
    
113页
    特殊键,如功能键和数字小键盘上的键,在低字节中有一个零来指示所按的键不是一
个标准的ASCII字符,并且必须被特殊处理。
    当BIOs键盘中断处理程序结束处理所按的键时,它将代码放进键盘缓冲区中保存
起来,直到某个程序需要它时。两个特别的键,Ctrl-Break和Shift,printScreen,不以这种方
法处理;相反它们产生的中断请求马上由BIOS的另外部分处理了。Ctrl-Break调用了Int
1Bh,它是DOS设置来迫使Ctrl-C进入缓冲区,以便进行后面的“正常”处理。 Shift-
PrintScreen则调用了Int 05h,它执行屏幕打印动作。
    在这个水平上,不用书写任何程序去访问键盘数据。相反,本章中的代码只依赖BIOS
或DOS去预处理所有键盘输入数据,以便用户只需要处理通常键的ASCII码和特殊键的
扫描码。
6.1.2用BASIC读键盘
    在开始利用DOS和BIOS调用去读键盘之前,让我们先看看使用标准BASIC功能来
进行键盘输入的一个BASIC程序。 KEYBD.BAS程序使用了BASIC解释程序中有用的
功能(见列表6.1)
列表6.1
      10 REM...KEYBD.BAS Basic keybOard demonstration
        20 c$S=INKEY$:IF C$=" " THEn 20
        30 gosub 100:GOTo 20
        100 REM···Display Keyboard input
        110 IF LEN(C$)>1 THEN GOSUB 200 ELSE GOSUB 300
        120 PRINT USING "Character:! ### ###"; CH$;c;SC
        130 RETURN
        200 REM···KeyStrOke haS SCan code included
        210 CH$=".":C=ASC(MID$(c$,1,1)):SC=Asc(MID$(C$,2,1))
        220 RETURN
        300 REM···Keystroke is Character only
        310 CH$2C$:c=ASC(c$):SC=0
        320 RETURN
    为达到与BASIC解释程序兼容,该程序书写时带有行号。下面逐步显示这个程序的
简单结构:
    1.等待击键。
    2.打印键击字符、ASCII码和扫描码。
    3.从第一步开始重复。
    INKEY$(放在列表6.1第20行的紧密循环中)提供了读键盘的一种方式,并且不
必等待击键。根据字符串是否超过一个字符长,100行的子例程翻译从INKEY$返回的
内容,第一个字符是字符的ASCII值,第二个则是扫描码(起始于第200行的子例程用于
处理所按键的大小写)。通常字符串只有一个字符长。第300行的子例程序则用于翻译字
符串的代码。
    如果运行列表6.1中的程序,就会注意到扫描码只在有限的程度上与单个的键对应
(参见图6.1和6.2)。KEYBD.BAS程序存在以下不足:
                            
114页
 ·INKEY$只返回“有意义的”字符(在这个例子中字符“有意义”只是针对BASIC
        解释程序而言的)。
      ·如果完成的击键不是ASCII字符,则只为此击键返回扫描码,不用看Shift键、Ctrl
        键或Alt键
    从现在开始,本书不继续介绍解释程序兼容的BASIC程序,而只介绍QuickBASIC
编译程序。列表6.2显示了为QuickBASIC而修改的程序,以充分利用编译程序的性能。
    列表6.2
              KEYBD2.BAS
          QUickBASIC Keyboard Demonstration
          Start:
                C$=INKEY$:IF C$=“” THEN GOTO Start
                GOSUB Display:GOTO Start
          Display:
             Display Keyboard Input
            IF LEN( C$) > 1 THEN GOSUB Scan ELSE GOSUB Char
            PRINT USING " Character: ! ### ###"  ; CH$;c; SC
                RETURN
          Scan:
                Keystroke includes scan  code
            CH$=".":c=ASC( MID$(C$,1,1)):SC = ASC( MID$( C$,2,1))
                RETURN
          char :
            Keystroke is Character only
            CH$=c$:C=ASC(c$) : SC=0
                RETURN
    从列表6.2中的样本程序开始,将改而采用BIOS功能调用来执行输入。然后我们就
能在键盘上看到更多的击键。
6.1.3使用Int 16h来访问键盘
    当准备使用BIOS功能时,要注意到的第一件事是Int 16h的功能0(BIOS键盘输入
功能),它的作用是等待击键(见列表6.3)。
    列表 6.3
    /* waitkey.c
        Listing 6.3 of DOS Programmer's Reference*/
    #include<stdio.h>
    #include<dos.h>
    void main()
    { 
          unsigned char scancode;
          unsigned char charcode;
          union REGS         regs;
          printf("Test of Int 16 keyboard services\n");
          printf("press Esc to exit program\n";
          while(Charcode != 27){
              regs.h.ah = 0; 
              int86(0x16, &regs, &regs);
 
115页
       scancode=regs.h.ah;
                    charcode = regs.h.al;
                    printf("Scan: %.3d  ASCII : %.3d [%C] \n",
                        scancode, charcode,charcode);
              }
          }
      仅当有键按下时,读出击键的内容,而不是等待击键。Int 16h,功能1指示一个击键是
否正在等待。如果没有按键在等待,那么就在处理器的进位标记寄存器中设置0进位标
记。可按照列表6.4所示测试该寄存器。
      列表6.4
          /* waitkey2.C
            Listing 6.4 of DOS Programmer'S Reference*/
          #include<stdio.h> 
          #include<dos.h>
          void main()
          { 
              unsigned char scancode;
              unsigned char Charcode;
                union REGS         regs;
                printf("Test of Int 16 keyboard services\n");
                printf("press Esc to exit program\n";
              while(charcode != 27){ 
                    while(1) { 
                        putchar('.');
                        regs.h.ah=1;
                        int86(0x16, &regs, &regs);
                        if((regs.x.flags & 0x40) == 0)
                              break;
                              }
                  regs.h.ah=0;
                    int86(0x16, &regs, &regs);
                    scancode = regs.h.ah;
                    charcode = regs.h.al;
                    printf("\nScan: %.3d  ASCII: %.3d [%C]  \n",
                  scancode, Charcode, charcode);
                }
          } 
      从键盘获取键之前,该程序首先看看是否有键准备好了。若没有,程序只在屏幕上打
印一个点。不论什么时候按键,程序都会停止打印点并显示信息,告诉按的是哪个键。然
后程序又开始打印点。
      当程序有某种途径来检查键盘输入的程序在等待击键时,它们不会“闲”着,而要去完
成其它的任务。例如,可以使用这类例程来移动屏幕上的数据,而在击一个键时,终止此操
作。
      Int 16h功能2用于确定键的状态:Ins、Caps Lock、Num Lock、Scroll Lock、Alt、Ctrl,
以及左边和右边的Shift键。键盘输入程序的下一个版本则让用户检查这些键的状态(见
列表6.5)。
 
116页
列表6.5
/* Keystat.c
Listing 6.5 of DOS Programmer's Reference */
#include <stdio.h>
#include <dos.h>
#define VIDEO 0x10
/* Prototypes */
void gotoxy(int r, int c);
void cls(void);
void main()
{
unsigned char scancode;
unsigned char charcode;
union REGS regs;
int ins, caps, num, scroll;
int alt, ctrl, left, right;
cls();
gotoxy(0,0);
printf("Test of Int 16 keyboard services
");
printf("Press Esc to exit program
");
gotoxy(10, 12);
puts("INS CAPS NUM SCRL ALT CTRL LEFT RiGHT");
while(charcode != 27) {
while(1) {
regs.h.ah = 2;
int86(0x16, &regs, &regs);
ins = regs.h.al&0x80;
caps = regs.h.al&0x40;
num = regs.h.al&0x20;
scroll = regs.h.al&0x10;
alt = regs.h.al&0x08;
 ctrl = regs.h.al&0x04;
left=regs.h.al&0x02;
right = regs.h.al&0x01;
gotoxy(11, 12);
printf("%S %S %s %s %S %S %S %S
" ,
ins ? " ON " : " OFF",
caps ? "ON " :  " OFF",
num  ? " ON " : " OFF",
Scroll ? " ON " : " OFF",
alt ? " ON " : " OFF",
ctrl ? " ON " : " OFF",
left ? " ON " : " OFF",
right ? " ON " : " OFF");
regs.h.ah = 1;
int86(0x16, &regs, &regs);
if((regs.x.flags&0x40)==0) break;
}
regs.h.ah = 0;
int86(0x16, &regs, &regs);
Scancode = regs.h.ah;
charcode = regs.h.al;
printf("Scan: %.3d ASCII: %.3d [%C]
",
scancode, charcode, charcode);
}
cls();
gotoxy(0,0);
 
117页
 } 
    void gotoxy(row,col)
        int row, col;
    {
        union REGS regs;
        if(row<0||row>24)  return;
        if (col<0||col>79)  return;
        regs.h.ah=2;
        regs.h.bh=0;
        regs.h.dh=row; 
        regs.h.dl=col;
        int86(VIDEO,&regs,&regs);
    }
    void cls()
    { 
        union  REGS regs;
        regs.h.ah = 0x06;
        regs.h.al = 0;
        regs.h.bh=7;
        regs.h.Ch=0;
        regs.h.cl=0;
        regs.h.dh = 25;
        regs.h.dl = 80;
          int86(VIDEO,&regs,&regs);
    }
在列表6.5中的程序里,putchar('.')已经被用于检查特殊键的状态,并将此状态显
示在屏幕中心的代码所代替。这种状态信息给用户一个完整的画面,用来说明键盘上正在
发生什么。如果希望自己的程序去检查一个突发事件,如左边和右边的Shift键盘同时被
按住,Int 16h的功能2便能指出这个事件的发生时间。
Int 16h的功能2将单个字节返回到AL寄存器中。每个位都对应于一个特定的击
键。表6.2显示该功能的位赋值。
                          表6.2键标记字节中各位的含义
          位
                            意义
76543210
0······0       没按住右边的Shift键
·······1       按住了右边的Shift键
······0·       没按住左边的Shift键
···. ··1·       按住了左边的Shift键
·····1··       没按住Ctrl键
·····1··       按住了Ctrl键
····0···       没按住Alt键
·. ··1···       按住了Alt键
···0····         Scroll Lock关闭
···1····         Scroll Lock打开
··0·····         Num Lock关闭
··1···.  .         Num Lock打开
·0····· .         Caps Lock关闭
·1····· .         Caps Lock打开
0. ·····.          Insert关闭
1···. ·. ·         Insert打开
处理键盘输入的BIOS功能代表了不用进行机器访问的最基本的途径。对键盘而言,
 
118页
BIOS 功能是能够利用的最低层,并且它也能保证某个程序尽可能地与各种PC机兼容。
6.1.4使用Int 21h来访问键盘
    当以常规方法在DOS水平上获取键盘输入时,会失去对每个可能的击键的立即访
问。 DOS输入功能指示特殊键是否已被按住(如果第1个字节返回的是0,第2个是扫描
码)。但DOS功能不能告诉用户已有ASCII码的键的扫描码,也不报告Alt、shift等键的
状态。另外,最常使用的DOS输入功能要等待一次击键,这会强迫在一些程序中使用
BIOS接口。在本章稍后的部分,我们看到一个DOS输入功能克服了这种局限性。
    Int 21h的功能1(有回显字符输入)是用户可能想用的最基本的DOS字符输入功能
(见列表6.6)。
列表 6.6
      /* Keyin.c
          Listing 6.6 of DOS Programmer's Reference*/
      #include<stdio.h>
      #include<dos.h> 
      #define CTRL_D       0x04
      #define LF     0x0a
      #define ENTER   0x0d
      /*prototypes*/
            Unsigned int keyin(void);
      void main()
      { 
            int c;
        printf("Testing Int 21h input functions\n"); 
        printf("press Ctrl-D to exit program\n");
        while((c=keyin())!=CTRL_D) 
              if(c>=256) 
                  printf("SPECIAL: %d\n",c-256);
              else if(c==ENTER) 
                  putchar(LF);
    }
    unsigned int keyin()
    {
        union REGS regs; 
        int offset;
        offset = 0; 
        regs.h.ah = 0x01;
        intdos(&regs,&regs);
        if(regs.h.al==0){ 
          offset = 256; 
            regs.h.ah = 0x01;
            intdos(&regs,&regs);
        } 
        return (regs.h.al+offset);
    } 
Keyin.c程序采用下列步骤:
 
119页
1.等待来自键盘的一个字符。
    2.如果该字符为特殊字符(例如,功能键或光标键),则打印APECIAt及该键的扫
描码。
    3.如果该字符是Enter键,字符输入功能就会在没有换行的情况下输出一个Re- 
turn;用户必须输出一个换行字符到屏幕上,才会使显示内容换行。
    4.从第1步开始重复。
如果用户在没有能将换行字符打印到屏幕上的专门部分的帮助下运行该程序,那么
就会看到这些字符在敲入时会被DOS功能回显出来。但是,当按Enter键时,光标不会移
向下一行。记住:该功能回显所敲的字符,并且Enter键只代表回车—而不是换行(Line
feed)字符。
    该程序的一个有趣的功能是:它采用的特殊编码能将功能键与普通键区分开。因为
ASCII字符总是小于或等于255,所以这个功能产生一个扩展的字符设置,它是通过将一
个返回0的ASCII码键的扫描码值加上256来达到的。实际上,这个方法常能简单化译出
击键代码的过程,并且不会带来问题。许多程序员不用此方法,因为他们将击键与变量联
系在一起。将返回的字符与整数联系起来,用户就可以有更大范围的特殊代码来代表特殊
击键。
    当需要特殊键(如功能键和箭头)时,上述读字符的方法有一个问题,即当进行第二次
调用想获得特殊键的扫描码时,DOS功能会打印出与扫描码相等的ASCII码,就如象想
获得的是ASCII码而不是扫描码。为防止这种情况发生,可以使用Int 21h的功能8,并且
由用户自己回显字符(见列表6.7)。
    列表6.7
/* Keyin2.c
    Listing 6.7 of DOS Programmer's Reference*/
#include <stdio.h>
#include <dos.h>
#define CTRL_D 0x04
#define LF      0x0a
#define  ENTER      0x0d
/* prototypes*/
        unsigned int keyin(void);
void main()
{ 
        int  c;
      printf("Testing Int 21h input functions\0");
      printf("press Ctrl-D to exit program\n");
      while((c=keyin())!=CTRL_D) 
            if(c>=256){ 
                printf("SPECIAL: %d\n",c-256);
            }else{ 
                putchar(c);
                if(C==ENTER) 
                    putchar(LF);
            } 
} 
unsigned int keyin()
120页
     { 
              union REGS regs;
                int offset;
                offset=0;
                regs.h.ah = 0x08; 
                intdos(&regs, &regs);
                if(regs.h.al==0){
                    offset = 256;
                    regs.h.ah = 0x08;
                    intdos(&regs,&regs);
                } 
                return (regs.h.al+offset);
          }
      这个程序与列表6.6中的程序之间的区别非常细微,如果不仔细看就会忽略。首先,
在Keyin()函数中,设置程序去调用Int 21h的功能8而不是功能1。主例程序必须在敲击
字符是将每个字符回显给屏幕(因为DOS的内核并没为用户回显示字符)。因为特殊键独
立操纵,所以不用为回显假的功能键代码而烦恼—只会回显正在敲的键。
      每个用于本章的DOS功能都等待一次击键,但与BIOS功能一起使用时,就可以周
期性地检查,看看是否有一个字符正在等待读取(这个过程叫做查询)。中断21h的功能
0Bh指示一个字符是否正等着被读取。列表6.8显示了当操作者等待一次击键时,怎样使
用这个功能去执行任务。
      列表6.8
/*  Keyin3.c
      Listing 6.8 of DOS Programmer's Reference*/
#include<stdio.h>
#include<dos.h> 
#define CTRL_D 0x04
#define LF   0x0a
#define ENTER    0x0d
/* prototypes*/
        unsigned int keyin(void);
        unsigned int charwait(void);
void main()
{ 
        int c;
        printf("Testing Int 21h input functions\n");
        printf("press Ctrl-D to exit program\n");
        while((c =keyin())!=CTRL_D)
            if(c>=256) { 
                printf("SPECIAL: %d\n",c-256);
            } else{
                putchar(c);
                if(c==ENTER)
                    putchar(LF);
            } 
 
121页
unsigned int keyin()
{ 
        union REGS regs; 
        int offset;
        while(!charwait())
            putchar('.');
        offset = 0;
        regs.h.ah = 0x08;
        intdos(&regs, &regs);
        if(regs.h.al==0) {
            offset  = 256;
            regs.h.ah = 0x08;
            intdos(&regs, &regs);
        }
        return (regs.h.al+offset);
unsigned int charwait()
{ 
        union REGS regs;
        regs.h.ah=0x0b;
        intdos(&regs, &regs);
        return (regs.h.al);
      这个程序介绍了一个新的函数,charwait(),由它去检查键盘上的字符。如果已有字
符正在等待读取,它就会返回TRUE;否则就返回FALSE。
      该函数以Int 21h的功能0Bh为基础,它设置AL寄存器的值,如果某字符在等待,它
就设置为FFh(255),否则为0。通过返回AL寄存器的值,该函数指示一个字符是否在等
待读取(FALSE即是0,TRUE是非0的值)。
      要估价程序能怎样快速地检查字符,可以运行它,并看看程序等待敲入一个字符时经
过屏幕的时间。该程序对输入反应相当迅速。如果用一个长的、复杂的操作来代替简单的
putchar('.'),程序就会变得极端迟钝了。
      本章的前面提到了一个克服常规DOS输入的许多局限性的特殊DOS输入功能:普
通的I/O功能—Int 21h的功能06h。
      如果调用Int 21h的功能06h时,DL寄存器包含0FFh,那么该功能将从键盘缓冲区
中获得输入(并且如果没有字符可用,就返回0,且进位标志被设置)。
      如果在DL寄存器中是其它的内容,它就被输出给CRT并且不进行检查。于是,这个
单一功能提供了使用直接的BIOS输入和输出时所能获得的大部分功能;并且还保持完
整的DOS兼容性。因为它是原始的“CP/M遗产”功能之一,所以很遗憾它没有象它应该
有的那样得到普及。
      列表6.9是列表6.8从C语言到TurboPascal的翻译。经过修改,可利用Int 21h的
功能06h来输入和输出。功能和过程名没变,行为也是同样的。该程序没有使用Int 21h的
功能0Bh来检查键的就绪状态,而只是使用由Turbo Pascal的DOS单元提供的ZFlag(零
进位标志)常量来简单地测试零进位标志,这样就消除了charwait函数;该程序不使用
putchar来作单字节输出,它加上了putch函数,借助功能06h来执行输出。
 
122页
列表6.9
{keyin4.pas }
program keyin4;
uses DOS; { run-time DOS library }
const
CTRL_D = $04;
LF = $0a;
ENTER = $0d;
var
c : integer;
reg : registers; { declared in DOS unit }
procedure putch( i : integer );
begin
reg.ah := $06; { general I/O function }
reg.dl := byte(i) ; { OUTPUT the character }
MsDos (reg);
end ;
function keyin : integer;
var 
i : integer;
offset : integer;
begin
repeat { wait for keystroke }
putch($2E) ; { put  '.' on CRT }
reg. ah := $06; {general I/O function}
reg.dl := $FF; {flag for INPUT use }
MsDos (reg) ;
i := reg.flags AND FZero;
until i <> FZero; { declared in DOS unit }
if (reg.al = 0) then { flag as.extended key }
begin
offset := 256;
reg.ah := $06; { and go get scan code }
reg.dl := $FF;
MsDos (reg ) ;
end
else { set as normal key }
offset := 0;
keyin := reg.al + offset;
end ;
begin
writelnt('Testing int 21h input functions' ) ;
writeln( ' Press Ctrl-D to exit program' ) ;
c := keyin;
while (keyin <> CTRL_D) do
begin
if (c >= 256) then
begin
writeln (' SPECIAL : ',c-256) ;
end
else
begin
putch (c) ;
if (c= ENTER) then
putch (Lr) ;
 
123页
           end;
            c:= keyin;
          end;
      end. 
(Int 21h,功能0Ah),并让它获得用户的输入,回显它,而且允许进行行编辑。DOS编程方
面的一些书籍指出:该功能通过返回一个两字节序列(包括零字节和扫描码)来返回功能
键、箭头以及其它的特殊键。keyin5.c 程序(见列表6.10)在特殊键返回时会显示它们
—但它们没有返回。 IBM技术手册没有包括两字节的键代码。
    列表6.10
    /* Keyin5.c
      Listing 6.10 of DOS Programmer's Reference*/
    #include <stdio.h>
    #include<dos.h>
    #define CTRL_D 0x04
    #define LF     0x0a
    #define ENTER  0x0d
    /* prototypes*/
        char getline(char*b,int n);
    void main()
    {
        char buffer[11];
      printf("Testing Int 21h Input Functions\n");
      printf("press Enter with no input to exit program\n");
      while(getline(buffer, 10)>0) { 
          printf("<<%>>\n",buffer); 
      } 
} 
char getline(buffer,n) 
      char* buffer;
        int n; 
    {
        union REGS regs;
        char locbuf[514];
        int i,j;
        locbuf[0]=n;
        locbuf[1]=0;
        regs.h.ah = 0x0a;
        regs.x.dx=(int)&locbuf;
        intdos(&regs,&regs);
        for(i=0,j=0; i<locbuf[1];i++,j++) 
              if(locbuf[i+2]==0) {
                  i++; 
                  buffer[j]='x';
              } else{
                  buffer[1] =locbuf[i+2];
              }
          buffer[j]=NULL;
          return (locbuf[1]);
    }
124页
    对已缓冲的输入功能的需求使它变得特殊化。缓冲区的设计使它不能自然地适合
BASIC、C语言或Pascal处理字符串的途径。调用该功能时,必须将它传递给一个大得足
够用来处理输入字符再加上2个额外字符的缓冲区。缓冲区中的第一个字符是允许输入
的字符的最大数目;由DOS装入的第二个字符则代表被读入的字符数目。
6.1.5识别键盘支持的水平
      如果BIOS支持增强键盘,那么有两套BIOS功能可以访问键盘:功能00h、01h和
02h支持旧的键盘,而功能10h、11h和12h支持新键盘。一个程序怎样才能告诉用户它能
否使用增强键盘功能呢?
    可以利用功能05h(向键盘缓冲区写入),来测试增强键盘功能的存在。当向缓冲区写
入FFFFh值时,应该会发现AL设置成了00h或0h。如果是这样,就利用功能10h(获得
击键)去读入该缓冲区。如果存在增强键盘功能,就可以在16个调用的最大值中读
FFFFh值(因为在键盘缓冲区中存在16个字)。
      在DOS 5.0中,探测增强键盘是自动进行的;功能01h、06h、07h、08h、0Ah、0Bh和
0Ch,如果它们存在,都使用增强键盘功能(见列表6.11)。
    列表6.11
    page55,132
    ;Kbdtype.asm
    ;Determine whether the BIOS enhanced keyboard functions are
    ;present. Prints the level supported and returns an errorlevel
    ;of 0 (no enhanced keyboard) or 1(enhanced keyboard). 
            .model tiny
            .code
            .startup
check       proc
          ;   clear keyboard buffer
clear : mov           ah, 1           ;Check for keystroke present
            int      16h
            jz        buffer_clear    ;if  none,proceed...
            mov        ah,0            ; read the keystroke and discard it
            int      16h
            jmp       clear           ;check again
          ;   stuff  0FFFFh into buffer
buffer_clear:
            mov        ax , 5FFh         ; stuff FFFF in
            mov        CX,0FFFFh
            int      16h
            cmp        a1,1           ; success?
            ja        no_ enhanced       ; no , BIOS plainly doesn't support it
          ;   try to read 0FFFFh
            mov        cx, 1           ;16 entries in the standard buffer
tryit: mov            ah,1th           ; use enhanced check for keystroke present
            int      16h
            jz        no_enhanced       ; nothing there,it didn't work 
  mov        ah , 10h          ; use enhanced function to read keyStroke
            int      16h
            cmp       ax , 0FFFFh        ; our keystroke?
            je         enhanced
            loop       tryit
 
125页
   ;     no enhanced keyboard 
    no_enhanced:  
            mov       dx ,offset no   ;print negative message
            mov        al,1          ; Set error code to 1
    printit :
            push       ax              ; save error code
            mov       ah,9          ;print string
                int        21h
            pop     ax               ;  restore error code
            mov      ah ,04CH       ;  exit with error code
            int     21h            ;  exit
        ;    enhanced keyboard
    enhanced:
            mov       dx,offset yes ;print positive message
            xor        al ,al       ;zer error code
            jmp      printit
    check    endp
    no      db        “N0”
    yes      db       " ENHANCED KEYBOARD FUNCTIONS SUPPORTED$" 
                end
                      6.2鼠    标
    鼠标(或其等价的设备,如跟踪球)作为计算机系统的部分,其应用正不断增长。而且
依赖于鼠标而获得有效使用的软件产品也在增长。Microsoft Windows可以只用键盘来操
作但是图形接口的充分便利则只能来自鼠标。尽管一些程序如Microsoft Paint(在Win- 
dows之下运行)能够使用键盘,但要轻松地做到这一,点却是不可能的。而Guide(OW1 In-
ternational的超级文件系统)没有鼠标就不能使用。
    DOS没有包括鼠标驱动程序。读者可能已注意到,DOS没有包含用户程序可能想用
的许多不同类型外设的驱动程序。开发DOS时曾决定让用户能增加设备驱动程序来控制
另外的设备,从而使DOS得到了更广泛的使用。当引导系统时,这些驱动程序之一,Mi- 
crosoft的MOUSE.SYS将被加进操作系统中(只要购买了Microsoft鼠标,它就会提供
MOUSE.SYS)。本节将介绍鼠标软件的工作方式。
6.2.1了解鼠标的工作方式
    鼠标是能够附另到计算机上的一个十分简单的仪器设备。它由一个小球构成,该小球
置于能在平面上滚动的“鼠标”内部。当球转动时,鼠标内的电路就向计算机报告这种移
动,软件则将这种移动解释和翻译成屏幕上的光标移动。除了这个球外,鼠标还包括两个
或三个按钮(开关),可用来发信号告诉计算机。
6.2.2初始化鼠标驱动程序
    根据所拥有的软件,可以采取许多方式中的一种去设置鼠标驱动程序。某些软件包能
产生一个TSR去操纵鼠标;其它的则有专门的驱动程序。如果有了驱动程序,就应该把它
 
126页
安装到系统中,即包含在CONFIG.SYS文件的一行中,这一行告诉系统在引导过程中装入
该驱动程序。如果有存放在C驱动程序器的根目录中的Microsoft的鼠标驱动程序,那
么可以使用下面的行:
      DEVICE=C:MOUSE.SYS
6.2.3鼠标位于何处
    首先,程序必须确定鼠标是否已装入。Int 33h的功能0能告诉使用者:如果AX的返
回值,非零,那么鼠标是可用的(在早于3.0的DOS版本中,Int 33h的向量没有初始化来
指向一个IRET;使用功能0测试之前,应该证明这个向量不指向地点0000:0000)。
    如果已经知道鼠标是可用的,就可以使用它了:可以周期性地查询它有关位置改变和
击键方面的情况,也可以在鼠标移动或按一个按钮时设置一个中断服务例程。为简单起
见,这里只讨论查询技术。
    因为鼠标驱动程序监视着鼠标光标在屏幕上的位置,所以用户程序不必做这样的工
作。用户只需知道鼠标是定位在用户想做某种事情的地方。样本程序MOUSE.C,它是以
Keyin3.c为基础的,它显示出当用户等待其它输入时怎样检查鼠标(见列表6.12)。
    列表6.12
        /*MOuSe.c
            Listing 6.12 of DOS Programmer'S ReferenCe*/
        #include<Stdio.h>
        #include<Stdlib.h> 
        #include<dOS.h>
        #include<String.h>
        #define CTRL_D 0x04
        #define LF        0x0a
        #define ENTER  0x0d
        #define MOUSE     0x33
        #define VIDEO   0x10
        static int Buttons=0;
        /*prototypes*/
              unSigned int keyin(void);
              unSigned int charwait(void);
              void gotoxy(int r,int c);
              void cls(void);
              void mousepos(void);
 
127页
printf("SPECIAL:%d
",C-256);
}else{
putchar(c);
if(C==ENTER)
putchar(LF);
}
mouseoff();
cls();
gotoxy(0,0);
}
unsigned int keyin()
{
union REGS regs;
int offset;
while(!charwait())
mousepos();
offSet=0;
regs.h.ah=0x08;
intdoS(&regs,&regs);
if(regS.h.al==0){
offset = 256;
regS.h.ah=0x08;
intdos(&regs,&regs);
}
return (regs.h.al+offset);
}
unsigned int charwait()
{
union REGS regs;
regS.h.ah=0x0b;
intdos(&regs,&regs);
return (regs.h.al);
}
void chk_mouse()
{
union REGS regs; 
struct SREGS sregs;
regS.x.ax=0x3533;
 intdosx(&regs,&regs,&sregs);
 if((regs.x.bx|sregs.es)==0){
printf("No mouse driver present
");
exit(255);
}
regs.x.ax=0;
int86(MOUSE,&regs,&regs);
if(regs.x.ax!=0){
Buttons=regs.x.bx;
mouseon();
}
}
void mouseon()
{
union REGS regs;
struct SREGS sregs;
if(Buttons){
128页
regs.x.ax=0x01;
int86(MOUSE,&regS,&regs);
}
}
void mouseoff()
{
union REGS regs;
struct SREGS sregs;
if(Buttons){
regs.x.ax=0x02;
int86(MOUSE,&regs,&regs);
}
}
 void mousepos()
{
union REGS regs;
char status[6];
gotoxy(12,10);
if(Buttons){
regS.x.ax=0x03;
int86(MOUSE,&regs,&regs);
switch(regs.x.bx&0x03){
Case 0:/*no buttons*/
strcpy(StatuS,"NONE");
break;
caSe 1:/*left button*/
Strcpy(status,"LEFT");
break;
case 2:/*right button*/
strCpy(StatuS,"RIGHT");
break;
case 3:/*both buttons*/
StrCpy(Status,"BOTH");
break;
}
printf("X=%4.4d Y=%4.4d %S",
regs.x.cx,regs.x.dx,status);
}else{
printf("No mouse");
}
}
void gotoxy(row,col)
int row,col;
{
union REGS regs;
if(row<0||row>24)return;
if(col<0||col>79)return;
regs.h.ah=2;
regs.h.bh=0;
regs.h.dh=row;
regs.h.dl=col;
int86(VIDEO,&regs,&regs);
}
 
129页
           void cls()
          { 
                union REGS regs;
                regs.h.ah=0x06;
                regs.h.al=0;
                regs.h.bh=7;
                regs.h.ch=0;
                regs.h.Cl=0;
                regS.h.dh=25;
                regS.h.dl=80; 
                int86(VIDEO,&regS,&regs);
          } 
    Mouse.C包括Chk_Mouse()函数,它检查鼠标驱动程序的存在,如果存在就检查鼠
标。当该程序等待输入时,它将光标移到屏幕中心并显示有关鼠标位置和按钮的信息,而
不显示等待的时间周期。
      如果已有一个安装好的鼠标,移动它并看看鼠标状态改变,以反映鼠标的移动。有了
这些基本函数便可构成最低水平的可操作的鼠标功能。 gotoxy()函数也加进了Mouse.c
程序中,它能利用BIOS屏幕定位途径在屏幕上定位光标(第5章“输出设备”解释了这个
函数的操作方式)。
    这个样本程序只包括3个基本的鼠标功能。第一个,确定鼠标是否已安装以及它有多
少按钮,该功能在一个全局变量中设置按钮的数目。当调用其它的鼠标功能时,这些功能
会检查这个变量以确定它们是否有事情可做。
      如果鼠标已经装入,就可以调用第二个功能(Int 33h功能1)使其在屏幕上显示鼠标
光标。在有鼠标的情况下,调用Int 33h的功能3,在检查(但不会找到)字符时检查鼠标状
态(位置及按钮状态)。该功能返回被编码并置入BX寄存器两个低位的鼠标按钮的状态。
如果设置了位0,那么左边的按钮已经按过了。如果设置的是位1,则是右边的按钮已经按
下了。
      鼠标在屏幕上的位置,x坐标方向上从0到639,y坐标轴则从0到199。依据当前的
显示方式,可惜助表6.3以列或坐标来确定鼠标位置。
                                    表6.3鼠标坐标
           屏幕方式                    坐标 
           0或1                     行=DX/8,列=CX/16
           2或3                     行=DX/8,列=CX/8
           4或5                     x=CX/2,y=DX
           6                        x=CX,y=DX
           7                        行=DX/8,列=CX/8
           14到16                   x=CX,y=DX
      其他的鼠标功能(在本书接近结束处的一节“中断33h:鼠标功能”中介绍)有助于控
制鼠标光标的大小、形状和它在屏幕上的边界,以及它对鼠标产生反应而在屏幕上移动的
速度。还可以设置一个功能以便任何时候当鼠标事件(如按键或放开键)发生时都能调用
它。我们已经了解过这类例程,叫作中断处理程序,具体请见第11章。
                                                                                          
130页
                 6.3小    结
    本章讨论了键盘和鼠标功能,并介绍了一些基本的,可以用来执行输入功能的操作。
借助BIOS所提供的键盘功能,就可以知道键盘上按下了哪个键并追踪操作行为。可以看
到字符输入时的AscII码(就象从高级语言输入功能返回的那些值一样),还可以看到所
按键的扫描码,尽管它们只从某些键上返回。还能够监视shift、Alt、Ctrl、Scroll Lock、
Num Lock和Caps Lock键的状态。
    通过利用DOS Int 33h的鼠标功能,就能监视鼠标按钮在鼠标上的位置以及鼠标在
屏幕上的位置。甚至还可以设置中断,用于鼠标功能,这样任何时候当鼠标事件(如击键)
发生时,这些功能都会执行特殊的处理程序。
 
    现代计算机世界的串行设备是非常吸引人的。感谢串行设备,借助这样的设备,再利
用如CompuServe或MCI Mial的系统,我们就能与世界各地不同的人们进行通信联系。
本章的主题是有关串行设备为什么能以目前的方式运行,以及我们怎样才能利用它们的
特性。
    在这一章里,都要以与其它计算机进行通信这种方式而与串行设备一起工作——这
是当前这种通信方式最常见的用户。串行通信还可以用于打印机、传感器和许多其它的设
备。并且这种通信不必是双向的。例如,与打印机的通信占主导地位的是单向的:从计算
机到打印机。或者可以建立一个程序来读取Associated Press新闻电信,它是一个1200bps
(每秒位)的单向数据传送,或NOAA的天气信息,它是一个50bps的单向数据传送。
    本章集中介绍一个简单的双向终端通信程序,该程序包括了串行通信的所有方面。但
在编写终端程序之前,应该知道串行接口的工作方式。定义一些基本术语后,就要提供有
关IBM PC串行接口芯片(UART)的工作方式方面的细节。然后讨论上升到硬件水平以
便用户了解怎样直接控制芯片。
    借助一些基本原理,可以编写两个终端程序。第一个利用BIOS功能去访问串行端
口。将会看到:这种类型的程序用于重要的通信时太慢了。如果直接与UART芯片一起工
作,就可以获得一定的速度和控制。尽管第二个终端程序证明了这项技术,但即使是这样,
它对1200bps的实际通信也还是太慢了。一个真正的实用通信程序必须将本章中的技巧
与第11章“中断处理程序”中的内容结合在一起。编写一个中断驱动的通俗读物程序,并
非本书所涉及的主题,它是对任何PC程序员的耐心和他们的理解程度的考验。有关编写
这样的程序的实例可以参看《Advanced Assembly Language》,由Allen L,Wyatt编写(由
Que公司出版)。
    从基本原理上讲,一个串行接口是将计算机内部数据的并行格式(8位字节)转变成
一个串行格式(1位),后者能在单根数据线上进行传递。这种转换能由软件来执行,但在
PC机中,硬件能更有效地完成它。
    图7.1分析了串行接口的基本目的:将信息由并行格式转变成串行格式,或反过来。
数据从该接口的一边进入,从另一边产生和转换。接口把数据的每个字符转变成一个信息
“包”,从而将数据转变成串行格式;这个信息“包”能以发送和接收双方都认可的某种途径
来进行传递(有关串行格式的更详细讨论,见本章稍后部分)。只有在每个串行连接的终端
都使用同样的数据格式和相同的传递速度时,计算机之间才能成功地进行通信。
    常用两种方法来传递串行信息。它们都称作定时方法,它们都是通过串行来传递和接
                                                                                               
132页
         图7.1串行接口将数据形式由并行的(内部的)变成串行的(外部的)
收信息的。第一种方法叫作同步通信,它维持对数据经连接而进行的传递和接收的严格控
制。在这个过程中,数据以精确定时的间隔进行传递。定时信息是与数据一起传递的,以
便接收方计算机能与要接收的信息同步。这类通信通常用于小型机或主机应用程序,有关
它的讨论则超出了本书的范围。
    第二种方法叫做异步通信。在这种方法里,每个信息包放在通信线上,彼此之间并没
有精确限定的时间间隔。这些数据包能够一个挨一个地快速传递或每个传递之间间隔不
同的时间。这种方法对于大多数微机,包括IBM系列都是适合的。
    尽管可以为其它类型的串行通信(如前面提到的同步方法)购买硬件接口设备,但异
步通信应该能满足大多数通常目的的需要。
    图7.2显示了通信线上的串行信息包(可以作为异步通信的时间功能)。该线路通常
保持标记状态(高电压)。当下降到空闲状态(低电压)时,就是给信号说明字符开始了(开
始位)。要确定下一个位是高或低,可以用以位频率为基础的精确时间间隔来对线路进行
抽样。在标记状态时,数据位后会紧跟一个或更多的停止位,从而有足够的时间用于字符
处理和用于系统准备下一个字符。
                                      图7.2串行传递
                      7.1串行接口
    在进行异步通信的更详细讨论之前,应该了解基本的通信术语。我们还要看到,串行
接口将并行格式数据转换成容易经过串行通信连接进行传递的信息包形式。这些信息包
 
133页
油许多特定的信息位构成。每个位都有一个特定的目的:开始位、数据位、停止位以及奇偶
性。下面定义这些和其它几个重要的术语:
      ·开始位在实际字符数据之前送出的一个位,用来警告接收方计算机:一个字符
        就要来了。该位由串行设备自动送出。
      ·数据位该位代表正在传递的单个字符。(数据位的个数通常即是字的长度)。在
      计算机奇偶性的情况下,串行设备的正常通信使用7位的字长度(见下列定义);
        否则就使用8位的字长度。 IBM PC上的串行通信芯片能够操纵5-8个数据位。
      ·奇偶性一个简单的字符水平“真假性”检查,接收方可以利用它来看看是否已正
        确地接收到了字符。计算奇偶性的方法是:首先计数正在传递的信息包中数据部
      分设置为1的位数,然后加上所希望的奇偶性类型位代表。对于EVEN(偶)奇偶
      性,设置为1的数据位的总数再加上奇偶性位必须是一个偶数。相反,ODD(奇)奇
      偶性中就会带来一个奇数。其他的奇偶性位可能的设置包括MARK(总是设置成
      1),SPACE(总是设为0),或NONE(总是忽略)。
      ·停止位该位在数据包的结束处送出,以便给接收方时间,在下一个字符到来之
      前处理已有的一个字符。对于将要操纵的所有通信,正常情况下只有一个停止位
      (只有当通信以极低的速度,如110bps进行时才需要2个停止位)。
      ·波特率这是一个电气学名词,代表一条通信线的传输速率。它常常(尽管不正
      确)用来指位速率。
    ·位速率传输速度,即每秒传送的位数,常常(尽管不正确)被称为波特率。位速率
      是个更精确的词,故在本书中主要使用它。
    ·全双工一种通信手段,传递时,显示在屏幕上的信息就是输送给远处计算机的
      字符的回送。
    ·半双工也是一种通信手段,其中传送给远处计算机的信息并未返回到送出的计
      算机。
    一台计算机与另一台计算机通信时,双方必须根据一系列预先限定的用来定义所传
递的信息格式的参数进行操作。如果两台计算机没有设置为相同值,它们之间的通信就是
不可靠的。计算机之间的通信没有象应有的那样普及,一个原因是程序员还没能在不需用
户去理解专业化的、复杂术语的通信标准上达到共识。
    但许多配置却是相当标准的。通常,8个数据位,没有奇偶性,以及一个停止位或者7
个数据位,EVEN或ODD奇偶性以及一个停止位就可以工作了。对于大多数联机计算机
系统,位速率一般都建立在1200、2400或9600bps上。如果试试这些配置中的一个,那么
几乎总是能与联机计算机系统中的某台计算机匹配上。
    传递信息之后,计时至关重要。通信线在接收到开始位之前是空闲的。开始位到达后,
就要在精确的时间间隔内对通信线进行采样,以接收构成字符的单个位。奇偶性位则用来
计算所传递的字符的正确程度,它紧跟在数据位之后,最后,得到并释放停止位,接收方又
在等待下一个开始位。
    如果这些背景知识使人晕头转向,那么可以放松一下。IBM微机已安装了管理串行
                                                                                         
134页 
    通信的低水平行为的硬件。在确定要使用的通信参数并把它们输入以后,串行转换硬件能
    保证使用户得到所希望的东西。
                      7.2串行转换:UART
      IBM微机(以及大多数兼容tA)都使用一个硬件芯片:它起初以8250 Universal Asyn-
    chronous Receiver/Transmitter(8250通用异步接收/传送器)为基础,由National Semicon-
    ductor(国家半导体)公司生产。在Pc机和Pc/xT的早期,就已经使用了这种芯片的其它
    类型。最近的则是Pc16450和Pc16550。这类芯片在功能上都与8250等同,但能提供更
    高的效率和速度。 Pc16550除了与8250兼容以外,还有一个使用了FIFO(先进、先出)数
    据缓冲的操作方式,该数据缓冲大大地增加了吞吐量。在这一章里,这3个芯片简单地称
    作UART(广泛的异步接收者/运输者)。
      与一些依赖软件来管理通信的系统相比,UART让人迷惑不解。它参与到接收和传
    递信息位的具体活动中,从而使程序员不用完成其它任务。
      假定需穿过一系列电线来送出数据,而此时线上电压水平在不断变化。如果编写了程
    序来管理这条线路,那么就能直接控制这条线,并告知任何需要的东西。听起来很困难,是
    不是?理论上并不是这样,但过程是繁琐的,并易导致细微的出错。有些系统(例如,原始
    的TSR80彩色计算机)只能以这种方式处理串行通信。
      借助UART,就不必添麻烦去编写在通信线上开或关来获得数据的一个软件控制程
序。要花少得多的精力去编写和测试软件uART,UART芯片为使用者提供了大量的控
    制,并允许与其他设备进行快速的、标准的通信。
      8250和16450 UART分别有10个可以编程的1个字节寄存器;16550则有11个。
这些寄存器控制和监视串行端口。大多数寄存器用于初始化,只有几个用得有规律。所有
寄存器都可通过7个I/O端口地址进行访问。这些地址作为来自一个基地址的偏移值来
计算,基地址是随着所使用的通信端口而改变的。表7.1显示了coM1:到coM4:的基地
址;表7.2列举了这些地址的偏移值,它们分别控制每个UART寄存器。
                              表7.1 IBM通信端口的基地址
                通信端口                  基地址
                COM1:                    03F8h
                COM2:                   02F8h
                COM3:                   03E8h
                COM4:                    02E8h
                              表7.2 UART寄存器:来自基地址的值
      偏移值  LST位7                意    义
      0     0       传递保持者寄存器(THR)和接收者数据寄存器(RDR)
      0         1         波特率除数值的低字节(BRDL)
       
135页
                                                                      (续)
偏移值      LST位7        意    义
    1         0           中断允许寄存器(IER)
    1         1           波特率除数值的高字节(BRDH)
    2         x           中断识别寄存器(IIR)和FIFO控制寄存器(FCR一16550UART独有)
    3         x           线控制寄存器(LCR)
    4         X           调制解调器控制寄存器(MCR)
    5         X           线状态寄存器(LSR)
    6         x           调制解调器状态寄存器(MSR)
读者可能已注意到,即使UART有10或11个寄存器去控制操作,但却只有7个端
口地址。这7个地址中的大多数支持一个以上的寄存器。在偏移值为0时,不论什么时候
向端口写,都可以访问THR,而不论什么时候从端口读都可以访问RDR。因为这两个寄
存器中没有一个需要同时的读和写访问,所以这两者的结合很有意思。这种安排与用在
16550 UART上的安排是相同的;IIR和FCT寄存器共享相同的寄存器。IIR是一个只读
的寄存器;FCR则是一个只写的寄存器。
当LSR第7位设置为1时,偏移值为0和1的寄存器则会执行另一项功能。当这种
情况发生时,这两个端口就会访问BRD寄存器(因为只有在芯片的初始化期间才访问
BRD寄存器,所以它们在通常操作期间能安全地保存起来)。
    让我们看看每个UART寄存器都做些什么。
7.2.1发送保持寄存器(THR)
该寄存器保持有将要送出的数据字节。如果LSR的第5位指示该寄存器是空的,那
么就可以向它写入数据。
7.2.2接收数据寄存器(RDR)
该寄存器保持有最近从通信线上接收到的数据韶。如果LSR0位指示已接收到一
个字节,那么就可以读取该寄存器。
7.2.3波特率除数(BRD)
BRD是一个16位数,它指定UART使用的传输率(不是波特率,不考虑UART设计
考所给的正规名字)。它在两个8位端口(BRDt和BRDH)之间划分开来。要确定位传输
速率,就可用UART的内部时钟频率(1.832MHz)来除以BRD,如下所示:
            BRD=时钟速度/16*期望的bps
使用该等式很容易确定不同位速度的设置。例如计算BRD为9,600,等式如下∶
        BRD=1843200/16*1200=1843200/19200=96=0060h
于是,BRDH必须设置为0,而且BRDL必须设置成0Ch。
136页
      可以使用该等式来构造表7.3所列举的典型位速率的BRD。
                                  表7.3 波特率除数
              位速率                             BRDH                                BRDL
              50                                  09h                                 00h
              110                                 04h                                 17h
              300                                  01h                                80h
              1200                                 00h                                60h
              2400                                 00h                                30h
              4800                                00h                                18h
              9600                                00h                                 0Ch
              19200                                00h                                06h
    注意IBM警告其BIOS早期版本用户,不要设置高于9600bps的位速率。但可在
          19200bps(甚至更高)的位速率下安全地驱动UART。
          要设置BRD,必须首先将LCR的第7位设置成1。然后可以安全地将所需除数输
          出给它们的I/O地址(参见表7.2)。设置BRD后,应马上清除LCR的第7位。
    7.2.4中断允许寄存器(IER)
        IER控制UART产生的中断类型,一次可以允许一个或更多的中断,这依据编写中
    断处理程序的方法而定。不论什么时候允许了一个中断,就必须采用一个特定的行为来清
    除它。表7.4显示了寄存器位上所指定的中断以及需用来清除每个中断的合适行为。
                           表7.4中断允许寄存器
            位                      启    动                      操    作
            0                       数据接收                      读RDR
            1                       THR空                         输出到THR
            2                       数据出错或中断                读LSR
            3                       MSR改变                       读MSR
            4~7                    未用:总是设置为0
        当表7.4中所示启动条件之一发生并且相应的IER设置为1时,中断就会产生。
    7.2.5中断识别寄存器(IIR)
        中断发生时通信程序能从IIR的位设置来识别该中断。表7.5列举了这些位的意义。
                                      表7.5中断识别寄存器
            位                  意    义
            0             已发生的中断超过1个
            1~2          中断ID
            3             中断ID(MsB~16550 UART独有;在其它UART上总是设置0)
            4~5          未用;总是设置为0
            6~7        FIFO缓冲允许标志(16550 UART独有;在其它UART上总是设置为0)
       
137页
    如果软件是由中断驱动的,那么首先必须限定希望产生的中断的类型(参考表7.4)。
然后,接到一个中断请求后,必须检查IIR,看看发生的中断是哪种类型。表7.6指示用来
识别中断的三个位(8250和16450 UART只有第1位和第2位)的可能设置。
                                  表7.6中断ID位设置
        第三位           第二位                第一位                    意义
          0               0                     0                   MSR发生变化
          0               0                     1                   THR空
          1               1                     0                   接收FIFO字符超时
          0               1                     0                   数据接收
          0               1                     1                   数据接收出错或中断
7.2.6 FIFO控制寄存器(FCR)
    16550 UART添加了一项功能,用于缓冲正在送出或接收的数据。该缓冲称为FI- 
FO——(先进先出)。在UART的早期型号中,这种缓冲是没有用的。表7.7显示了该寄
存器各位的意义。
                                表7.7 FIFO控制寄存器
           位                     意义
           0                    允许和清除FIFO缓冲
           1                    接收重新设置的FIFO缓冲
           2                    传递重新设置的FIFO缓冲
           3                    DMA模式选择
           4~5                 保留
           6                    接收方触发器(LSB)
           7                    接收方触发器(MSB)
    FCR的第6位及第7位用来指示应该用怎样的触发器水平来产生中断。这种水平即
是指示在中断产生之前,接收缓冲应该有多满。如果有快速中断设备途径,就可以设置高
水平触发器,并且较少中断主程序。表7.8显示了可能的触发器水平。
                                表7.8 FCR中断触发器水平
        位                        水平
        00                       1个字节
        10                       4个字节
        01                       8个字节
        11                       14个字节 
7.2.7线控制寄存器(LCR)
    它是串行线的主要控制寄存器。表7.9介绍该寄存器的位分配情况。
                                  表7.9线控制寄存器
      位            意义                设置            备注
      0-1          字符长度
                      5个位             00
                      6个位             01
                    7个位               10
                    8个位               11
                                                                                                 
138页
      位              意义                设置            备注
      2               停止位
                      1个位               0
                      1.5个位                            如果使用5位字符
                      2个位               1               如果使用6-、7-、8-位字符
      3-5             奇偶性
                      16NORE              000
                      000                 100
                      EVEN                UA
                      MARK                101
                      SPACE               111
      6               中断条件
                      禁止                0
                      许可                1
      7               端口触发器
                      正常                0                使用THR/RDR和IER寄存器
                      可选的              1                使用BRDL和BRDH寄存器
7.2.8调制解调器控制寄存器(MCR)
    MCR将控制线设置给调制解调器,并通过这些线告诉调制解调器计算机将要送出字
符,接收字符,或两者都要做。表7.10显示了该寄存器的位分配。
                            表7.10调制解调器控制寄存器
         位                            意义
         0                        设置DTR线活动
         1                        设置RTS线活动
         2                        用户输出#1(Hayes Reset) 
         3                        用户输出#2(Enable Ints)
         4                        UART环路
         5~7                     未用;设置成零
     数据终端就绪(DTR)线告诉调制解调器,计算机已打开并准备接收来自调制解调器
的信息。请求送出(RTS)线告诉解调器计算机准备向线上送出一些东西。平常情况下,可
以安全地将DTR和RTS都设置成1来打开这些线。一些解调器会忽视(或设置成会忽
视)这些信号,但;日式的解调器不会这样。第2位只能被特别的硬件(如Hayes SmartMo-
dem内部板,它使用第2位来重新设置解调器)所使用,并且应被初始化为0。第3位(用
户输出#2),它与UART的中断设备紧密结合在一起,如果它未设置成1,就会中断处
理。第4位允许对带有线上通信的程序进行测试。在这种环路状态下,送出端口的数据会
作为输入一样再次出现。
7.2.9线状态寄存器(LSR)
    LSR会告知通信线的状态(见表7.11)。借助该寄存器,可以诊断通常的线路问题。
             
139页
                         表7.11线状态寄存器(LSR)
      位                    意  义
      0               接收到的数据;RDR中的字节
      1               因为下一个字节到达前,先前的字节还未被读取,就发生了超出错
      2                  奇偶性出错
      3                     传送不同步(字符读取后未发现停止位)而导致帧差错
      4                     中断探测
      5                   THR是空的;可以向线上输出一个字符
      6                   TSR空;TSR将来自THR的字符放在线上,每次放一个位
      7                   超时(在16450和16550 UART中总是设置成0)
7.2.10调制解调器状态寄存器(MSR)
    调制解调器的状态决定于某个状态线是高还是低,以及自最后一个寄存器读取以后,
特定的线上的状态是否已发生了变化。表7.12显示了MSR中的位是如何分配的。
                              表7.12 MSR各位的意义
      位                                意    义
      0                           清除发送(CTS)发生变化
      1                           数据设置就绪(DsR)发生变化
      2                           环形指示器(RI)发生变化
      3                           数据携带者探测(DCD)发生改变
      4                           CTS设置为高电平
      5                           DSR设置为高电平
      6                           RI设置为高电平
      7                           DCD设置为高电平
调制解调器信号与连结计算机和串行设备的电信号线状态的改变相对应。依设备而
定,这些硬件信号可能用也可能不用。一些调制解调器没有利用它们而只是依赖Hayes命
令设置来处理通信。调制解调器(或其他串行设备)可能使用调制解调器信号,表7.13列
举了它们的意义。
                          表7.13调制解调器信号
      信号                              意  义
      CTS                         调制解调器准备接收来自计算机的字符。
      DSR                         调制解调器已打开并准备操作。
      RI                          电话线是环形的。因为线是环形的,所以RI保持高电
                                  平,以便计算机能探测这些环线
      DCD                         调制解调器之间的联系
                                                                                          
140页
                  7.3将通信端口初始化
      直接与UART进行工作并没有看起来的那么容易。甚至初始化工作,也会变成一个
复杂的操作,它依赖于以正确的顺序来对寄存器排序,以便产生特定的效果。对于许多程
序(甚至那些打算直接访问UART的程序),不必直接初始化此芯片。可以通过目的是使
任务简单化的设计的BIOS功能来控制初始化过程。在这个编程领域(象其他地方一样),
并不需要做多少准备工作。
      要访问对串行端口进行初始化的BIOS功能,将AH寄存器设置0,DX寄存器则设置
成代表将要初始化的通信端口的序号(从零开始计数,于是,0=COM1:,2=COM3:,3=
COM4:)。因为IBM BIOS的一些版本并非内在地支持4个通信端口,所以DX设置会只
限于0或1。而所有的PS/2系列都支持4个通信端口。
      最后,将AL设置成所希望的初始化参数。表7.14列举了可能的AL设置。
                            表7.14 AL的BlOS通信端口初始化设置
      位                      意义                            设置
      0-1                     字长
                              未用                            00
                              未用                            01
                              7个位                           10
                              8个位                           11
      2                       停止位
                              1个位                           0
                              2个位                           1
      3~4                    奇偶性
                              NONE                            00
                              000                             01
                              NONE                            10
                              EVEN                            11
      5~7                    位速率
                              110bps                          000
                              150bps                           001
                              300bps                          010
                              600bps                          011
                             1200Bps                            100
                              2400bps                         101
                             4800bpS                         110
                              9600bps                           111
      在AH、AL和DX设置成必需的值以后,生成一个Int 14h,将通信端口根据所需规格
进行设置(有关BIOS功能所必需的寄存器设置,请见表7.17;有关该功能的进一步情况,
请参见第五部分“BIOS功能参考手册”一节的内容)。
       
141页
    由表7.14可见,不能设置5个或6个位的数据长度,也不能设置低于110bps或高于
9600bps的位速率。对于某些应用程序,初始化选项是有限的。例如,当结合某个特殊化的
应用程序如NOAA天气信息时,它只有50bps和5位字长度,那么唯一的初始化选项就
是通过直接处理UART寄存器来对通信端口进行初始化。
    IBM PS/2系列计算机,有另一个BIOS功能提供对通倍接口的一些附加的控制。要
访问此功能,可以将AH寄存器设置成4,而对于通常的BIOS功能,则可将DX寄存器设
置成代表需初始化的通信端口的值。然后,将AL设置成0或1,依据是否需要线上的中断
条件而定。通常,AL设置为0(没有中断)。BH必须设置成所需的奇偶性,如表7.15所示。
                  表7.15 BH(功能14/4)的奇偶性设置
             设置                      奇偶性意义
              0                            无
              1                            奇
              2                            偶
              3                            标记
              4                            SPACE
    BL必须设置成所需的停止位的数字;0代表1个停止位,1代表1.5个(用于5位数
据长度)或2个停止位(用于6个、7个或8个位数据长度)。
    CH中的数据长度是特定的,为5,比所需的数据位数小。于是0代表5个数据位,3则
等于8个数据位。
    最后,CL应设置成所需的位速率。位速率由表7.16中所列的设置而定。
                          表7.16 CL的位速率设置(功能14/4)
              设置                          位速率
              0                             110bps
              1                             150bps
              2                             300bps
              3                             600bps
              4                             1200bps
              5                             2400bps
              6                             4800bps
              7                             4800bps
              8                             19200bps        
    设置了所有寄存器(AH、AL、BH、BL、CH、CL和DX)的值后,就可调用Int 14h来按
特定的那样设置通信端口。表7.17总结了本节出现的两个BIOS功能必需的寄存器设
置。有关这些功能的更多信息,可参见第五部分的“BIOS功能参考手册”一节。
·                        表7.17 BIOS通信端日初始化功能小结
      功能              参数                        及可能的设置
      AH=0             AL                      根据表7.14表中的数据来设置
                        DX                     所需的通信端口0(COM:)到3(COM4:)
      AH=4                                     (只在PS/2系列中发挥作用)
                        AL                      中断条件设置
                        BH                      奇偶性
                        BL                      停止位
                                                                                           
142页
        功能            参数                  及可能的设置
                        CH                    字长度
                        CL                    位速率
                        DX                    所需的通信端口:0(COM:)到3(COM4:)
      如果不使用BIOS对通信端口进行初始化的功能,那么就不能把某个寄存器设置(通
过BIOS功能)成所需的值,这时就必须直接访问UART。但在假设“BIOS不能做”之前,
先试着用BIOS的初始化功能来进行操作——可能会发现它有更多的用处。
      如果说完成UART的所有配置的想法很吓人的话,那么请记住确实可在任何时候改
变配置的任何部分。这意味着可以使用BIOS程序在其范围内设置所有的参数,然后直接
到UART硬件水平来修改速度、字大小或奇偶性等等的值。
      通过利用直接的UART初始化,可以将任何速度设置到115.2Kbps。这其中的秘密
隐含在本章前面所列的速度等式中;变成下面的格式就略简单一些:
              除数=115.200/期望的bps速率
      这里主要的区别就是时钟速度已经与16位参数结合在一起变成了一个数字常量。可
以看出115.2Kbps率(由某些插板式计算机数据——传输程序达到这么高的速度)是可
能来自串行接口(除数0001)的最大值的。
      从表7.1到表7.10提供了做这件事时所需的所有信息;唯一要小心的是在向某个端
口进行输出后不要马上尝试从它输入,因为UART比许多现代的CPU反应要慢得多。
BIOS程序在每个OUT命令都包含JMP $ +2命令,这样就带来足够的延迟,以便确定
在高水平的时钟速度下不出现问题;如果要添加用户自己的初始化过程,那么这是一个很
好的规则。
                      7.4调制解调器
      调制解调器(modem)是调制器——解调器(modulator,demodulator)的缩写。尽管如
何使用某个特定调制解调器的详细指导,超出了本书的范围,但本章还是要作一些大致的
介绍。
      首先,大多数调制解调器都宣传为与Hayes兼容的,所以调制解调器的控制是在送给
它的命令序列(字符串)中,而不是在它自己的控制线中。许多复杂的终端程序利用这些
Hayes命令序列来与调制解调器通信;这些程序能直接控制调制解调器的许多功能。我们
所要做的就是告诉这个程序想要做什么。
      如果调制解调器利用它自己的控制线而不是字符串命令设置,那我们只需在BIOS
或硬件水平直接操纵解调器的控制线就可以控制它。 BIOS和DOS功能通常能很好地将
这些控制线从程序中隐藏起来,这意味着必须直接由UART去控制这些功能。
      并非所有串行通信都需要调制解调器。例如,在与中心计算机串行连接的大多数事务
中,这种连接可从直接用电线连接到PC机上,如果中心计算机与这台pc相隔较近的话。
 
143页
从技术上讲,存在外界干扰的情况下,400英尺的距离可以工作,但上面两者的距离还是
应该不超过150英尺。另外如果打印机和串行设备能够满足电缆长度的要求,那么也可以
在没有调制解调器的情况下运行它们。
    通过电话线进行通信,必须使用调制解调器。它的电话系统的波段宽度相对有限(从
300到3000Hz)。从UART输出的是一系列矩形波会变形而无法辨认。结果就没法通信。
调制解调器可以将UART输出的矩形波转变成一系列的音调,从而降至电话线的波段之
内。
      旧式的、较慢的调制解调器只能简单地转变输出:一个音调对应0,另一个对应1。新
式的、较快的调制解调器不仅利用音调,而且利用了多路复用技术,相位调整定相以及其
它的电学信号成份来通过一条线传递更大容量的信息。
                  7.5编写一个终端程序
    现在我们已经看到UART如何发挥作用以及调制解调器的工作方式,那就差不多已
有准备去设计一个简单的终端程序,但在开始之前,还应该注意许多别的因素,本节就来
讨论它们。
    通信程序能以两种方式来执行。第一种是使用查询方法:程序周期性地检查一下串行
端口,以确定收到的字符是否有用。如果有用,就可以处理它并继续下去。第二种从计算
机时间来考虑,它更为有效。它以中断为基础:用户与计算机一起工作,直到收到的字符有
用,用户就被UART中断。然后处理该字符,并返回到中断之前进行的工作上。
    本章只讨论第一种方法。但它存在一些问题。尽管用户的计算机能与其它计算机通
信,但可能以高于300bps的速度失去收到的字符。特别是当屏幕填满了而不得不滚动一
行时。这些以查询为基础的程序是用于学习,而不是通常使用的。本章一开始就提到,必
须将这里的知识与第11章的内容结合起来以产生自己的中断驱动通信程序。
7.5.1双工考虑、
    在利用串行控制的查询方法之前,必须决定一下:希望进行的通信是全双工还是半双
工。读者可能回想起这个名词,本章早些时候引入了这个词,即全双工通信,表示每个通过
通信连接送出的字符都从远处的计算机回送回来。屏幕上所看到的字符是被其它计算机
在接收、回送(或传递)的字符。这些字符滚动在两个方向上同时进行——可以将字符输送
给计算机,同时它将别的字符送回来。以下所示为简单的终端程序的全双工执行过程:
    1.如果一个字符在键盘上,那么送出它。
    2,如果字符是在串行端口,就显示它。
    3.回到第一步。
    尽管大多数计算机利用全双工通信,但仍有一些利用半双工(每次字符只向一个方向
滚动)。因为远处的计算机不回送(再传递)它接收到的字符,所以半双工终端程序只在一
小点上与全双工版本不同:
                                                                                       
144页 
      1.如果一个字符在键盘上,那么送出并显示它。
      2.如果字符是在串行端口,就显示它。
      3.回到第一步。
      设中间的区别在于操纵从键盘获得字符的编码节中。下面的小节显示了它的代码怎
样受到了影响。
7.5.2控制程序:Term.C
      从概念上说,通信程序是非常简单的。列表7.1的程序,以C语言编写,它执行本章
讨论的基本概念。
    列表7.1
            /*Term.c
                Listing 7.1 of DOS Programmer'S Reference*/
            #include<Stdio.h>
            #include<COnio.h>
            #include<Stdlib.h>
            #define FALSE 0
            #define TRUE !FALSE
            main()
            {
                    void setup(void);
                    int    keybd(void);
                    void serial(void);
                    ClrSCr();
              printf("Simple Terminal Program\n\n\n");
              setup(); 
              while(TRUE){
                  if(!keybd())
                        exit(0);
                  serial();
              }
        } 
      除了清除终端屏幕(由cls()函数完成)和设置端口(借助setup()函数)以外,这是一
个终端程序的简单应用。它可以一直重复下去,从键盘(用keybd()函数)或从串行端口
(serial()函数)来交替获得字符。
      注意该程序提供了一个方法来结束程序,就象所有负责任的程序应该做到的那样:如
果键盘处理程序返回FALSE,程序就结束了(正如Listing 7.4所示,已经编好了这样的程
序;不论什么时候按下Shift-F1,键盘功能都会返回FALSE)。
7.5.3支持函数
      让我们看看每一个支持函数。keybd()函数有两个版本:一个处理全双工操作,另一个
则处理半双工操作。可以利用任何满足需要的函数。                     
      一、初始化:setup()函数
      setup()是用户开发的第一个函数,可以用它来对串行端口初始化。正如这里所显示
 
145页
的,该函数利用一个简单化的、硬编码的配置:1200bps,8位,无奇偶性和1个停止位。列
表7.2显示了怎样实现这个配置。
    列表7.2
          /* Setup.c
            Listing 7.2 of DOS Programmer'S Reference*/
          #include<Stdio.h>
          #include<dOS.h>
        #define  COM1       0
          #define RS232     0x14
          #define   sETUP       0x83 /* 1200 baud,8bits,no parity,*/
                                        /* 1 stop bit*/
          void setup()
          { 
                union REGS regs;
                printf("Setup the serial port\n");
                regS.h.ah=0;
                regS.x.dx=cOM1;
                regs.h.al=SETUP;
                int86(Rs232,&regs,&regs);
          }
    如上面程序所示,setup()将COM1:初始化为1200bps,并设置8位字长度无奇偶性
和1个停止位。
. setup()还利用了本章前面介绍的基本的BIOS通信端口初始化功能。
      二、一个初始化的替代函数
    setup()函数并不灵活——只能给串行端口设置一个特定的位速率和数据格式。作为
一个替代物,可以产生一个更灵活的setup()函数来处理多元初始化参数(见列表7.3)。
    列表7.3
        /*Setup2.c
        Listing 7.3 of DOS Programmer'S Reference*/
        #include <stdio.h>
        #include <dOS.h>
        #define       RS232             0x14
          #define     B300                0x40
          #define     B1200              0x80
          #define     B2400               0xa0
          #define    NOPARITY            0x00
          #define     EVEN                0x18
          #define     ODD                 0x08
          #define     WORD7               0x02
          #define    WORD8               0x03
          #define     STOP1               0x00
          #define     STOP2               0x40
          int setup(port,bpS,WOrd,parity,Stop)
146页
int port, /*COM port, 0=COM1, 1=COM2, etc.*/
bpS,/*bpS rate*/
wOrd,/*wOrd length*/
parity,/*0=off,1=EVEN,2=ODD*/
stop;/*number of stop bits*/
{
union REGS regs;
unsigned char setup;
Setup=0;
printf("Set up the serial pOrt
");
if(port!=0&&port!=1)
return (-1);/* bad COM port*/
switch(bps){
case 300:
Setup|=B300;
break;
case 1200: 
setup|=B1200;
break;
case 2400:
setup|=B2400;
break;
default:
return (-2);/*not 300/1200/2400 bps*/
}
if(word==7)
setup=WORD7;
else if(word==8)
setup=WORD8;
else
return (-3); /*not 7 or 8 bits*/
if(parity==0)
Setup|=NOPARITY;
elSe if(parity==1)
setup=EVEN;
else if (parity==2)
setup|=ODD;
else
return (-4);/*bad parity code*/
if(stop==1)
Setup=STOP1;
else if(Stop==2)
Setup=STOP2;
else
return (-5);/*not 1 or 2 bits */
regs.h.ah=0;
regs.x.dx=port;
regs.h.al=setup;
int86(RS232, &regs, &regs);/*set up the port*/
return (0);
}
setup()的这个更长版本,允许我们对每个参数进行出错检查,并能更灵活地设置参
数。表7.1中的term.c不能使用这个替代性的setup()函数。它是在一个假设的基础上
编写出来:配置值建立在程序中。如果需要就可以自己添加,但随后必须也提供一种方法,或
 
147页
从命令行,或者就象输入给程序的参数一样,来设置适当的初始化参数。
   如果对coM1以外的端口初始化,就必须也改变其它的支持函数——xmit()chrdy
()rch()和loOpback()——来支持其他的通信端口。以过测试并且直到对基本的串行端
口编程感到满意时,可能想使用Listing7.2中所示的Setup()的功能有限的版本然后与列
表7.3所示的版本一起试验。
三、键盘控制:keybd()函数
该函数管理键盘,通过它可以确定字符是否已由键盘输入,然后,如果是就传递这个
字符(见列表7.4)。
列表7.4
      /*Keybd.c
      Listing 7.4 of DOS Programmer'S Reference*/
      #include <stdio.h>
      #include <dos.h>
      #define     FALSE       0
      #define     TRUE       !FALSE
      #define    SF1         84
      int keybd()
      {
            char c;
            int get_Ch(void);
            void xmit(int ch);
            if((c=get_Ch())>=0){ 
                  /* There has been a keystrOke*/
                  if(c==0){
                  /* The first character was zero*/
                        if((c=get_ch))==SF1)
                                return (FALSE);
                        return (TRUE);
                  }
                  xmit(C);
          }.................................. 
          return (TRUE);
      }
    get_ch()函数的操作是keybd()函数操作的主要部分,这些将在下一节讨论。现在需
要了解的是:如果get_ch()返回0,就说明一个特殊的字符或键组合已从键盘输入了。这
种情况下,get_ch()必须再次被调用,以获得所按键的键盘扫描码。
keybd()专门用来寻找这个特殊字符值(一个0);如果找到了就寻找下一个。如果下
一个是shift-F1,字符就返回FALSE给调用程序;如果不是(例如按下另一个特殊键组合
或非ASCII字符),不会返回TRUE,并且忽略字符项。
    如果从键盘输入一个平常的ASCII值,get_ch()函数不会返回0;字符通过xmit()函
数进行发送(就象输入一样,不加改变)。
    (一)keybd()用于半双工通信
    只需添加一行就能使keybd()函数与半双工操作兼容。在包含xmit()函数的行之后
 
148页
紧跟着插入下面一行:
              putscrn(c);
      基本上,该函数将字符显示到屏幕上(在本章后面你会了解putscrn()的精确开发和
使用技巧)。
      (二)I/O控制:get_ch()和xmit()函数
    这两个函数支持keybd()函数。至于set_ch(),可参见列表7.5,它可以从键盘重新获
得一个字符(如果该字符有效);而xmit()则通过串行端口来传送字符。
      列表7.5
            /*Get_ch.c
            Listing 7.5 Of DoS Programmer's Reference*/
            #include <stdio.h>
          #include<dOS.h>
          #define   MASK         0x7f 
          #define    ZFLAG     0x40
          int get_Ch()
          {
                  union REGS regs;
                  regs.h.ah=6;
                  regS.h.dl=0xff;
              intdos(&regs,&regs);
              if(regs.X.flags&zFLAG)
                  retUrn (-1);
              return (regs.h.al&MASK);
        }
    get_ch()利用DOS直接的控制台I/O功能(见第6章)来输入字符。读者可能回忆起
来了:这个DOs功能设置零进位标志(ZFLAG)来指示是否有一个字符。get_ch()测试
ZFLAG,看看字符是否有用。如果有用,则为AL中的字符;功能就将AL返回到调用程
序,并将最高位设置为0。最高位与屏蔽值相与就可以去除它。这种屏蔽过程能消除传输
8位字符时所带来的任何可能的问题)。
      列表7.6显示的xmit.c(),是完成keybd()必需的另一条途径。
    列表7.6
            /*Xmit.C
            Listing 7.6 of DOS Programmer'S Reference*/
            #include<dos.h> 
            #define   RS232     0x14
            #define   WRITECH   1
            #define    COM1       0
            VOid xmit(ch)
                  char ch; 
            {
                  union REGS regs;
                  regS.h.ah=WRITECH;
                  regs.x.dX=COM1;
                  regS.h.al=ch;
              int86(RS232,&regs,&regs);
        }
 
149页
  xmit()函数只是将字符写给BIOS中的串行端口处理程序。由列表7.6可见,这个
BIOS函数只需使用3个寄存器:AH包含所需的功能号(1),AL是传送的字符(由调用程
序传递给xmit()),以及DX是用于传送的通信端口(0,指定的是COM1)。有关BIOS功
用的更多信息,可参见第五部分的“BIOS功能参考手册”一节。
    四、接收字符:srial()函数
目前已了解了keybd()、get_ch()和xmit(),那么接下来就应准备检查终端程序的其
它重要部分——接收并显示任何输入字符的部分。列表7.7显示了这个函数,它的名字叫
做srial()。
列表7.7
        /* Serial.C
          Listing 7.7 of DOS Programmer'S ReferenCe*/
        #include<Stdio.h>
        #include<dOS.h>
        VOid serial()
        { 
             char c;
              int chrdy(void);
              int rch(void);
              void putSCrn(int c);
              if(chrdy()){ 
                    c=rCh();
                    putSCrn(c);
              }
        }
    让我们考察一下serial()的完整性所必需的3个函数:chrdy()、rch()和putscrn()。
    (一)串行端口状态:chrdy()函数
    该函数检查一下输入的字符是否在串行端口上。它利用BIOS Int 14h的功能03h,能
返回通信端口状态(有关该功能的详细情况,参见“BIOS功能参考手册”一节,见第五部
分)。列表7.8显示了怎样执行chrdy()。
列表7.8
      /*Chrdy.c
          Listing 7.8 of DOS Programmer'S ReferenCe*/
      #include<Stdio.h> 
      #inClude<dOS.h>
      #define   RS232       0x14
      #define  STATUS     3
      #define COm1       0
      #define   DTARDY    0x100
      int ChrdY()
      { 
            union REGS regs;
            regs.h.ah=STATUS;
            regS.x.dX=COM1;
 
150页
          int86(RS232,&regs,&regs);
          return (regs.x.ax&DTARDY);
    }
(二)访问接收到的字符:Rch()函数
当chrdy()返回TRUE,告诉使用者程序一个字符正等着时,rch()函数就用来获得该
字符(见列表7.9)。
列表7.9
      /* Rch.c
        Listing 7.9 of DOS Programmer'S Reference*/
      #include<Stdio.h> 
      #include<doS.h>
      #define    com1         0
      #define     RS232         0x14
      #define    READcH        2
      #define      maSK        0x7f
      int rCh()                  
      { 
              union REGS regs; 
              regs.h.ah=READCH;
              regs.x.dx=COM1;
              int86(RS232, &regs,&regs);
          return (regs.h.al&MASK); /*strip parity bit*/
      } 
所输入字符的最高位已经去掉,就象在get_ch()中一样,将AL中的值与屏蔽值相与
便可去掉最高位。
    (三)屏幕显示:Putscrn()和Put_ch()函数
Serial()的最后部分是用来在屏幕上显示字符的方法。因为只有能打印的字符才能显
示出来,所以计了putscrn()(列表7.10所示)来忽略所有控制字符,只除了一个回车键
和换行码以外。
列表7.10
        /*putscrn.c
        Listing 7.10 of DOS Programmer's ReferenCe*/
      #include <stdio.h>
        #include <dOS.h>
        #define   CR    0x0d
        #define     LF    0x0a
        void putscrn(c)
              char c;
        {
              void put_ch(int);
            if(c>=' '||C==CR||c==LF)  
                put_ch(c);
        }
 
151页
  putscrn()利用基本的字符输出的DOs功能来调用put_ch(),将字符写到显示器上。
(有关这个功能,可参见第五部分“DOs功能参考手册”)。下面显示了put_ch()的工作方
式。
列表7.11
    /*put_ch.c
        Listing 7.11 of DOS Programmer'S Reference*/
        #include <stdio.h>
        #include <dOS.h>
        #define       CHAROUT     2
        void pUt_Ch(c)
            char c;
        {
               union REGS regs;
          regs.h.ah=CHAROUT;
            regS.h.dl=C;
            intdos(&regs,&regs);
      } 
                    7.6使用term.c
    目前已经了解了term.c所必需的每个函数,输入它们并试着来使用term.c不难发
现:尽管你的计算机不能与其它计算机通信,但是如果输入的字符以快速的序列更多地到 
达的话,有可能会失去一些字符。只有一行或两行的短短的信息可能就会使程序超负荷。
毛病在哪儿?
    存在许多问题:
    .因为终端程序并不为效率而编写,所以它包括许多分支调用——每一个都要占用
      时间。如果只将少数几个函数编进程序中,可能会提高其速度。
    . DOs功能被调用用来操纵键盘和屏幕。尽管敲入字符不会比读取它更快,但DOS
      功能调用所花费的时间夺走了串行通信所需的时间。
    .所使用的BIOS功能调用并非那么显著有效。
    不考虑这些问题,我们将不改变该程序的基本设计——它的主要目的是显示怎样在
名序中使用BIOS和DOs功能。但可以改变它使之直接对uART芯片进行操作,从而提
高其速度。这个过程会减少或消除当速度高于300bps时的输入字符丢失;这种丢失大部
可归因于调用BIOS程序花费来翻卷屏幕的时间,并且在查沟程序中没有办法解决这个
问题。
                  7.7直接访问UART
直接访问UART能使程序更紧凑和快速,但需要知道什么时候这种访问是合适和合
理的。需注意一些重要的折衷处理方案。
 
152页
      直接在硬件水平上操作可完成下列工作:
        ·可以带来从计算机所能获得的最大速度增加值。
        ·提供最大的编程灵活性。
        ·当将程序传递给另一种类型的计算机时,它是最依赖于机器的编程类型,也最易
          受兼容性问题的影响。
     IBM承诺保持与UART串行接口的兼容性,如果要与IBM生产的计算机一起工作,
这个承诺是让人欣慰的。但这不是绝对的保证。当与使用DOs的其他计算机一起工作时,
这个承诺也不是一个安全的赌注。尽管大多数计算机目前都使用与8250兼容的UART,
但注意使用不同UART的型号之间仍存在着细微的区别。
      正如前面一节所提到的,列表7.1中的term.c的版本会导致输入字符的丢失,因为
该版本不能踉上字符的稳定流水线。许多程序员不能从DOs或BIOS串行接口功能中获
得所需的性能;但是直接对UART进行操作可以解决这个难题。要做到这一点,必须通过
I/O端口来访问UART。尽管有些编程语言并没提供访问I/O端口的途径,但本书所使用
    的语言都能提供。
    7.7.1汇编语言
      在汇编语言中,可以用iN和OUT指令去访问该端口:IN将一个字或字节读入到
AL或AX寄存器中;ouT则将AL或AX寄存器中的一个字节或字写到I/O端口里。
OUT指令的变种包括ouTS(BL或cx寄存器)、ouTSB(来自DOS[si]的字节)以及
    OUTSW(来自DOS:[SI]中的字)。
    7.7.2C语言
      在Microsoft C++和Borland C++中,inport fo outnort函数可将字输入或输出给端
    口。对字节也是一样。
    7.7.3 BASIC语言
      BASIC功能INP和OUP可从一个端口读入字节或向它写入字节。
    7.7.4 Pascal语言
      Pascal不象其它语言,它没有用来访问端口的函数。但Turbo pascal将把端口当作数
组(port用于字节,portW则于字)。对此数组的读和写就能完成对端口的输入和输出。
                      7.8修改Term.c
      要修改终端程序,只需要改变一下如下3个串行端口函数:chrdy()、Xmit()和rch()。
可以安全地将对配置的控制留给BIOS函数。当直接访问uART时,chrdy()会被大幅度
    地简化(见列表7.12)。
        
153页
列表7.12
 /*Chrdy2.c
Listing 7.12 of DOS Programmer's Reference*/
#include<dos.h>
#define COM1 0x3f8
#define LSR 5
#define DTARDY 0x01
 int chrdy()
{
return (inportb(COM1+LSR)&DTARDY);
}
    用来读字符的rch()函数也大幅度地得到了简化(见列表7.13)。
列表7.13
/*Rch2.c
Listing 7.13 of DOS Programmer's Reference*/
#include<dos.h>
#define COM1 0x3f8 
#define RDR 0 
#define MASK 0x7f
int rch() 
{ 
return (inportb(COM1+RDR)&MASK);
}
  修改后的xmit()函数如列表7.14所示。
列表7.14
/*Xmit2.c
Listing 7.14 of DOS Programmer's Reference*/
#include<doS.h>
#define COM1 0x3f8
#define LSR 5
#define THR 0
#define THRRDY 0x20
int xmit(ch)
char ch;
{
register int cnt;
cnt=0;
while(!(inportb(COM1+LSR)&THRRDY)&&cnt<10000)
cnt++;
if(cnt>=10000)
return (-1);
 
154页
           outportb(com1 +THR, Ch);
                return (0);
          } 
      修改后的xmit()比原来的版本略复杂一点,因为在向THR写入字符之前必须保证
THR正在等着它。它还设置了一个最大重试次数计数器,以防止系统锁进无限循环之中
出不来。
      其他的term.c函数不需修改。作者对有关term.c 的BIOS版本以及为什么它很慢的
评论在这里还是有用的。甚至在直接访问UART时,还是没有足够的速度去处理1200
bps。
      在这个例子中,这些问题是由使用处理输入串行字符的方法而造成的。回忆前面,本
章的一些实例中使用了查询方法。换句话说,程序先检查键盘,然后是串行接口,永远这样
做下去。当程序的注意力集中在键盘或屏幕上时,如果到达串行端口的字符超过1个,那
么在程序有机会记录它们之前,就有可能会丢失一些输入的字符。
                      7.9回送检测
      在使用term.c()的修改版时,还可以加上一个允许回送检测的函数。换言之,UART
“认为”它正与一个远处的计算机对话;真正发生的是一个本地回送(在UART中),内容
就是正送往串行接口的东西。该过程的实现如列表7.15所示。
      列表7.15
          /* Loopback.c
              Listing 7.15 of DOS Programmer'S Reference*/ 
          #include<stdio.h>
          #include<dos.h>
          #define   MCR         0x3fc
          #define      LOOPBACK 0x10
          void loopback()
          { 
                int mcr_value;
                printf("Toggling Loopback\n");
                mcr_value = inportb(MCR);
                mcr_value = mcr_value^LOOPBACK;
                outportb(MCR,mcr_value);
                mcr_value = mcr_value&LOOPBACK;
                printf("Loopback");
                  if(mcr_value==0) 
                        printf("cleared");
                else
                        printf("set");
                printf(". . .  continuing\n");
          } 
      从term.c()引入loopback()(调用setup()函数后立刻进行),会将所有敲入的字符回
送回来,而不是沿串行通信连接进行传送。为什么使用loopback()呢?因为它有助于测试,
在测试与其他计算机联机的软件之前,它的测试能保证通信软件良好地运行。
 
155页
 也要注意该程序为COM1而直接访问了MCR。在计算机的回送方式中设置这个函
数以后,关闭回送的唯一方法是关闭计算机,然后再打开它,或再一次引入loopback()(添
加第二个loopback()到term.c()程序中去的最好位置,就是在最后结束的花括号之前)。
                7.10评价串行I/O设备
    基本的串行I/O设备对于高性能通信应用程序的工作是完全不够的。在高速度(大于
12000jps)、无差错文件运输程序或多宿主机、实时的终端操作中,高速度是必不可少的,而
且不应为获得兼容性而牺牲它(哪怕是轻微的,任何情况下都不能)。要获得高的吞吐量和
快速反应,在通信中直接访问硬件通常要比在其他应用程序中更合理。
    基本的term.c终端程序是丰富的,但不能处理高速度。在能增强某些功能之前,需要
了解中断和中断处理程序。第11章讨论了处理中断的基础知识。如果想看到一个更全面
的讨论和一个实例,即有关怎样编写中断驱动程序的知识,请参考Allen L.  wyatt(Que
公司)出版的《Advanced Assembly Language》一书。
    本节不是说非中断驱动程序就没有用了。如果能拼凑一个程序,在其中PC的注意力
全部集中在通信连接上,那么就能在没有中断的情况下安全地管理好。在此基础上,产生
的程序包也被广泛用作主导控制系统;它提供了来自以UNIX为基础的系统的pc控制。
所有与PC机的通信都在信息包中,并且如果不是文件传递者,就是需要执行的命令。该
程序是由程序员在几个小时中编写起来的。它只利用了最简单的技术,但都已应用了许多
年。它在高达19200bps的速度下仍能运行。简单地说,有时直接编程会是问题的最好解
答。但这个程序包不能在屏幕上显示所接收的数据,并且不能处理太大而不能装入RAM
的信息包。
                      7.11小    结
    本章集中讨论了IBM微机系列中的串行接口的使用和控制。该接口以原始的8250
UART(以及其后继件)为基础,它能为程序员提供大量的串行通信连接的方便控制。
    如果不考虑UART所带来的性能和便利问题,那么编写串行通信软件的任务就会繁
琐而复杂。尽管BIOS和DOS服务能在一定程度上简化该任务,但它们的价值也会减少,
因为它们只提供了对UART能力的有限访问。而且,它们会引入额外开销,从而严重降低
以时间为关键性因素的软件的性能。
 
原文地址:https://www.cnblogs.com/Chaobs/p/3838514.html