现代计算机世界的串行设备是非常吸引人的。感谢串行设备,借助这样的设备,再利 用如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=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 #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 #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,®s,®s); 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 #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 #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 #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能力的有限访问。而且,它们会引入额外开销,从而严重降低 以时间为关键性因素的软件的性能。