串口编程/8250 UART编程
串口编程: 简介和 OSI 网络模型 -- RS-232 线路和连接 -- 典型的 RS232 硬件配置 -- 8250 UART -- DOS -- MAX232 驱动器/接收器系列 -- Windows 中的 TAPI 通信 -- Linux 和 Unix -- Java -- Hayes 兼容调制解调器和 AT 命令 -- 通用串行总线 (USB) -- 形成数据包 -- 错误校正方法 -- 双向通信 -- 数据包恢复方法 -- 串行数据网络 -- 实际应用开发 -- 串行连接上的 IP
最后,我们从线缆、电压和硬核电子工程应用中抽离出来,尽管我们仍然需要了解相当多的计算机芯片架构知识。虽然本节的主要内容将集中在 8250 UART 上,但实际上我们将接触到三种计算机芯片
- 8250 UART
- 8259 PIC(可编程中断控制器)
- 8086 CPU(中央处理器)
请记住,这些是芯片系列,而不仅仅是芯片本身的编号。计算机设计多年来已发展相当多,通常所有这三种芯片都被放置在同一块硅片上,因为它们之间联系紧密,并且为了降低设备的总体成本。所以当我提到 8086 时,我同时也指的是后续芯片,包括 80286、80386、奔腾以及英特尔以外的制造商生产的兼容芯片。除了 8086 之外,不同芯片之间在串行数据通信方面有一些细微的差异和需要关注的地方,但在许多情况下,理论上你可以为最初的 IBM PC 编写串行通信软件,并且它应该可以完美地在你的现代计算机上运行,而你的现代计算机正在运行最新的 Linux 或 Windows XP 版本。
现代操作系统通过低级驱动程序处理这里涵盖的大部分细节,因此这更像是一个快速了解这种工作原理,而不是你可能自己实现的东西,除非你正在编写自己的操作系统。对于那些设计小型嵌入式计算机设备的人来说,在如此深的层次上了解 8250 变得非常重要。
就像 8086 一样,8250 已经发展了相当多,例如发展成了 16550 UART。在下面,我将介绍如何检测 PC 上许多不同的 UART 芯片,以及一些影响每个芯片的怪癖或变化。这些差异并没有 CPU 架构的变化那么显著,更新 UART 芯片的主要原因是使其与现有的速度快得多的 CPU 兼容。8250 本身无法跟上奔腾芯片的速度。
请记住,这正试图为软件方面的串行编程打下基础。虽然这对于硬件设计也有用,但这里描述的内容缺少很多,无法实现一个完整的系统。
我们应该回到英特尔 8086 之前,回到最初的英特尔 CPU,即 4004 以及它的后续产品 8008。8008 的所有计算机指令或操作码在今天的英特尔芯片中仍然有效,因此即使是 30 年前的端口 I/O 教程在今天仍然有效。较新的 CPU 增强了处理更多数据更有效率的指令,但原始指令仍然存在。
当 8008 发布时,英特尔试图设计一种方法,让 CPU 与外部设备进行通信。他们选择了一种称为 I/O 端口架构的方法,这意味着芯片有一组特殊的引脚专用于与外部设备进行通信。在 8008 中,这意味着总共有十六 (16) 个引脚专用于与芯片进行通信。确切的细节根据芯片设计和其他因素有所不同,这些因素超出了当前讨论的范围,但总体理论相当简单。
八个引脚代表一个 I/O 代码,该代码指示一个特定的设备。这就是我们所知的 I/O 端口。由于这只是一个二进制代码,因此它代表着将 256 个不同的设备连接到 CPU 的可能性。它比这更复杂一些,但你仍然可以把它想象成软件中的一个小镇邮局,那里有一个拥有 256 个邮箱供顾客使用的邮箱组。
下一组引脚代表正在交换的实际数据。你可以把它想象成放进或取出的明信片。
外部设备所需要做的就是查找它的 I/O 代码,然后当它匹配它被“分配”的查找对象时,它就控制了相应的端口。一个引脚信号指示数据是发送到 CPU 还是从 CPU 发送。对于那些熟悉设置早期 PC 的人来说,这也是发生 I/O 冲突的地方:当两个或多个设备试图同时访问相同的 I/O 端口时。在那些早期系统中,这曾是一个令人头疼的问题,特别是在添加新设备时。
顺便说一下,这与传统 RAM 的工作原理非常相似,一些 CPU 设计直接在 RAM 中模拟整个过程,保留一块内存用于 I/O 控制。这有一些问题,包括它占用了一部分本来可以用于软件的潜在内存。最终,在 IBM PC 和后来的 PC 系统中,内存映射 I/O (MMIO) 和端口映射 I/O (PMIO) 被广泛使用,因此情况变得非常复杂。但是,对于串行通信,我们将坚持使用端口 I/O 方法,因为 8250 芯片就是这么工作的。
当你真正开始在软件中使用它时,将数据发送到或接收自端口 9 的汇编语言指令看起来像这样
out 9, al ; sending data from register al out to port 9 in al, 9 ; getting data from port 9 and putting it in register al
在更高层次的语言中进行编程时,它变得更简单一些。一个典型的 C 语言端口 I/O 库通常像这样编写
char test; test = 255; outp(9,test); inp(9,&test);
对于许多版本的 Pascal 来说,它将 I/O 端口视为一个可以访问的大型数组,该数组简称为 Port
procedure PortIO(var Test: Byte); begin Port[9] := Test; Test := Port[9]; end;
警告!!这确实是警告。在不知道它连接到什么的情况下随机访问计算机中的 I/O 端口,可能会严重破坏你的计算机。最起码,它会使操作系统崩溃,导致计算机无法工作。写入某些 I/O 端口会导致永久更改计算机的内部配置,使得需要去维修店才能修复你通过软件造成的损坏。更糟的是,在某些情况下,它会对计算机造成实际损坏。这意味着计算机内部的一些芯片将不再工作,需要更换这些组件才能让计算机重新工作。损坏的芯片表明计算机的工程设计很糟糕,但不幸的是,这种情况确实会发生,你应该意识到这一点。
不要害怕使用 I/O 端口,只要确保你知道你正在写入什么,以及如果你打算使用特定 I/O 端口,你知道哪些设备“映射”到它。我们稍后将更详细地介绍如何识别串行通信的 I/O 端口。最后,我们开始编写一些代码,后面还有更多内容。
x86 端口 I/O 扩展
[edit | edit source]8088 CPU 和 8086 CPU 之间存在一些差异。对软件开发影响最大的是,8086 能够访问 65536 个不同的 I/O 端口,而不是只有 256 个。但是,计算机配置可能使用不到 16 根 I/O 地址总线线,例如 IBM PC 只使用 10 根线,这使得只有 1024 个不同的端口。端口号的高位被忽略,这使得多个端口号成为同一端口的别名。
此外,除了简单地发送或接收单个字符外,8086 还可以一次发送和接收 16 位数据。16 位字字节使用连续的端口号以小端序方式读写。386 芯片甚至可以让你同时发送和接收 32 位数据。对超过 65536 个不同 I/O 端口的需求从来都不是一个严重的问题,如果设备需要更大的内存空间,可以直接内存访问 (DMA) 方法可用。在这种情况下,设备直接读写计算机的 RAM,而不是通过 CPU。我们这里不讨论这个主题。
此外,虽然 8086 CPU 能够寻址 65536 个不同的 I/O 端口,但在实际应用中并没有这样做。英特尔芯片设计师为了节省成本,只为地址线留了 10 位,这对必须处理遗留系统的软件设计师来说意味着很多。这也意味着 I/O 端口地址 $1E8 和 $19E8(以及其他,这只是一个例子)在这些早期 PC 上会解析到同一个 I/O 端口。奔腾 CPU 没有这个限制,但为早期一些硬件编写的软件有时会写入“别名”的 I/O 端口地址,因为那些高位被忽略了。还有一些其他的遗留问题,但幸运的是,对于 8250 芯片和串行通信来说,这并不是一个问题,除非你碰巧有一个“利用”这种别名情况的串行驱动程序。这个问题通常只会在你使用超过典型 PC 上的 2 个或 4 个串行 COM 端口时才会出现。
x86 处理器中断
[edit | edit source]8086 CPU 和兼容芯片有一个被称为中断线的线。这实际上是一根连接到计算机其他部分的线,它可以被打开,让 CPU 知道是时候停止它正在做的事情,并关注一些 I/O 情况。
在 8086 中,有两种中断:硬件中断和软件中断。两种中断之间有一些有趣的差异,但从软件的角度来看,它们本质上是一样的。8086 CPU 允许 256 个中断,但可供设备执行硬件中断的数量受到相当大的限制。
IRQ 解释
[edit | edit source]硬件中断从 IRQ 0 到 IRQ 15 编号。IRQ 代表中断请求。总共有十五个不同的硬件中断。在你认为我不知道如何计数或做数学运算之前,我们需要在这里进行一些历史课,我们将在继续讨论 8259 芯片时完成。当原始的 IBM-PC 被制造出来时,它只有八个 IRQ,标记为 IRQ 0 到 IRQ 7。当时人们认为这对于几乎所有可能放在 PC 上的东西都足够了,但很快人们就发现这对于所有正在添加的东西来说远远不够。当 IBM-PC/AT 被制造出来时(第一个使用 80286 CPU 的 PC,以及今天 PC 上常见的许多增强功能),人们决定不再使用单个 8259 芯片,而是使用两个相同的芯片,并将它们“链接”在一起,以将中断的数量从 8 个扩展到 15 个。为了完成这项任务,必须牺牲一个 IRQ,那就是 IRQ 2。
这里的要点是,如果一个设备想要通知 CPU 它有一些数据准备好了,它会发送一个信号,表明它想要停止计算机上当前运行的任何软件,而是运行一个被称为中断处理程序的特殊“小程序”。一旦中断处理程序完成,计算机就可以回到之前正在做的事情。如果中断处理程序足够快,你甚至不会注意到处理程序已被使用。
事实上,如果你正在 PC 上阅读这篇文章,在你阅读这句话的时间内,你的计算机已经使用了几个中断处理程序。每次你使用键盘或鼠标,或者通过互联网接收一些数据时,你的计算机的某个地方都使用了一个中断处理程序来检索这些信息。
中断处理程序
[edit | edit source]我们将在稍后详细介绍中断处理程序,但现在我想解释一下它们到底是什么。中断处理程序是一种向 CPU 显示当中断触发时应该运行哪一段软件的方法。
8086 CPU 的 RAM 中有一部分被指定为“指向”中断软件位于 RAM 中的其他位置。采用这种方法的优势在于,CPU 只需简单地查找就可以找到软件的具体位置,然后将软件执行转移到 RAM 中的该位置。这也允许你作为程序员更改 CPU 在 RAM 中“指向”的位置,而不是指向操作系统中的某个位置,你可以自定义中断处理程序并在那里添加其他内容。
最佳方法在很大程度上取决于你的操作系统。对于像 MS-DOS 这样的简单操作系统,它实际上鼓励你直接编写这些中断处理程序,特别是在你处理外部外设时。其他操作系统,如 Linux 或 MS-Windows,使用的是一种“驱动程序”方法,它钩接到这些中断处理程序或服务程序,然后应用程序软件与驱动程序交互,而不是直接与设备交互。程序实际如何做到这一点,很大程度上取决于你使用的特定操作系统。如果你要编写自己的操作系统,你必须直接编写这些中断处理程序,并建立访问这些处理程序以发送和检索数据的协议。
软件中断
[edit | edit source]在我们继续之前,我想简单地提一下软件中断。软件中断使用 8086 汇编指令“int”来调用,例如
int $21
从软件应用程序的角度来看,这实际上只是另一种调用子程序的方式,但它有一个变化。在中断处理程序中运行的“软件”不必来自同一个应用程序,甚至不必来自同一个编译器。事实上,这些子程序通常直接用汇编语言编写。在上面的例子中,这个中断实际上调用了一个“DOS”子程序,它允许你执行一些与 DOS 直接相关的 I/O 访问。根据寄存器的值(在本例中通常是 8086 中的 AX 寄存器),它可以确定你想要从 DOS 获取什么信息,例如当前时间、日期、磁盘大小,以及几乎所有你通常与 DOS 相关联的东西。编译器通常会隐藏这些细节,因为设置这些中断例程可能有点棘手。
现在真正要搞乱事情了。“硬件中断”也可以从“软件中断”中调用,实际上,这是一种确保你正确编写软件的合理方法。这里的区别在于,软件中断只有在通过这个汇编操作码显式调用后才会被调用,或者它们的部分软件代码才会在 CPU 中运行。
8259 PIC(可编程中断控制器)
[edit | edit source]8259 芯片是整个硬件中断过程的“核心”。外部设备直接连接到这个芯片,或者在 PC-AT 兼容机(你对现代 PC 最熟悉的东西)的情况下,它将有两个连接在一起的设备。实际上有十六根线进入这对芯片,每根线都标记为 IRQ-0 到 IRQ-15。
这些芯片的目的是帮助“优先级排序”中断信号,并以某种有序的方式组织它们。无法预测某个设备何时会“请求”中断,因此,多个设备经常会争夺 CPU 的注意力。
一般来说,编号较低的 IRQ 优先级较高。换句话说,如果 IRQ-1 和 IRQ-4 同时请求注意,IRQ-1 优先级较高,并且在 CPU 看来会首先被触发。IRQ-4 必须等到 IRQ-1 完成其“中断服务例程”或 ISR 之后才能执行。
但是,如果相反的情况发生,IRQ-4 正在执行其 ISR(请记住,这是软件,就像你可能通常编写的任何计算机应用程序一样),IRQ-1 将“中断”IRQ-4 的 ISR,并推送其自己的 ISR 以代替它运行,并在完成时返回到 IRQ-4 的 ISR。当然也有例外情况,但现在让我们保持简单。
让我们回到原始的 IBM-PC 一分钟。当它被制造出来时,主板上只有一个 8259 芯片。当 IBM-AT 问世时,IBM 的工程师决定添加第二个 8259 芯片以添加一些额外的 IRQ 信号。由于 CPU(此时是 80286)上只有一个引脚可以接收中断通知,因此人们决定从原始的 8259 芯片中获取 IRQ-2,并用它链接到下一个芯片。对于任何依赖 IRQ-2 的设备来说,IRQ-2 被重新路由到 IRQ-9。采用这种方案的好处是,即使有七个其他设备现在“共享”这个中断,打算使用 IRQ-2 的软件仍然会收到“通知”。这些是 IRQ-8 到 IRQ-15。
然而,就优先级而言,这意味着 IRQ-8 到 IRQ-15 的优先级高于 IRQ-3。这主要是在您试图弄清楚哪个设备可以优先于另一个,以及当设备试图引起您的注意时通知它的重要性时会引起关注。如果您正在处理运行特定计算机配置的软件,此优先级级别非常重要。
这里应该注意的是,COM1(串行通信通道一)通常使用 IRQ-4,而 COM2 使用 IRQ-3,这使得 COM2 在接收数据方面的优先级高于 COM1。通常,软件并不关心这一点,但在某些罕见的情况下,您确实需要知道这个事实。
8259 有几个与 I/O 端口地址相关的“寄存器”。当我们谈到 8250 芯片时,我们将更多地了解这个概念。对于典型的 PC 计算机系统,以下是与 8259 相关的典型主端口地址。
寄存器名称 | I/O 端口 |
---|---|
主中断控制器 | $0020 |
从中断控制器 | $00A0 |
此主端口地址是我们在软件中直接与 8259 芯片通信时使用的。可以通过这些 I/O 端口地址向该芯片发送许多命令,但就我们的目的而言,我们实际上并不需要处理它们。其中大部分用于通过计算机的基本输入输出系统 (BIOS) 对计算机设备进行初始设置和配置,除非您从头开始重写 BIOS,否则您实际上不必担心这一点。此外,每台计算机在您处理此级别设备时的行为都略有不同,因此这更适合计算机制造商关注,而不是应用程序程序员应该处理的事情,这正是 BIOS 软件编写的原因。
请记住,这是大多数 PC 兼容型计算机系统的“典型”端口 I/O 地址,可能会因制造商试图实现的目标而异。通常,您不必担心此级别的兼容性问题,但当我们谈到串行端口的端口 I/O 地址时,这将成为一个更大的问题。
我将在这里花点时间来解释“寄存器”一词的含义。当您在这一级别处理设备时,设计设备的电气工程师会提到更改设备配置的寄存器。这可以在几个抽象级别上发生,因此我想消除一些混淆。
寄存器只是一个供设备直接操作的小块 RAM。在像 8086 或奔腾这样的 CPU 中,这些是用于直接执行数学运算(如将两个数字加在一起)的内存区域。这些通常被称为 AX、SP 等。典型的 CPU 上只有很少的寄存器,因为对这些寄存器的访问是直接编码到基本机器级指令中的。
当我们谈论设备寄存器时,请记住这些不是 CPU 寄存器,而是设备本身的内存区域。这些通常被设计为连接到端口 I/O 内存,因此,当您写入或读取端口 I/O 地址时,您是在直接访问设备寄存器。有时会存在更深一层的抽象,您将拥有一个指示要更改哪个寄存器的端口 I/O 地址,以及另一个包含要发送到该寄存器的数据的端口 I/O 地址。您处理设备的方式取决于其复杂程度以及您要执行的操作。
从本质上讲,它们是寄存器,但请记住,这些设备中的每一个通常都可以被视为一个完整的计算机,而您所做的一切只是建立它将如何与主 CPU 通信。不要在这里纠结并将其与 CPU 寄存器混淆。
使用中断控制器时,您必须定期交互的一个方面是通知 8259 PIC 控制器中断服务例程已完成。当您的软件执行中断处理程序时,CPU 没有自动方法向 8259 芯片发出您已完成的信号,因此需要设置 PIC 中的特定“寄存器”以允许下一个中断处理程序访问计算机系统。完成此操作的典型软件如下
Port[$20] := $20;
这将发送名为“中断结束”的命令,通常简称为缩写“EOI”。可以向此寄存器发送其他命令,但就我们的目的而言,这是我们唯一需要关注的命令。
现在,这将清除“主”PIC,但如果您使用的是在“从”PIC 上触发的设备,您还需要通知该芯片中断服务已完成。这意味着您需要像这样向该芯片发送“EOI”
Port[$A0] := $20; Port[$20] := $20;
您可以做其他事情来使您的计算机系统正常运行,但现在让我们保持简单。
在我们离开 8259 PIC 主题之前,我想介绍一下设备屏蔽的概念。连接到 PIC 的每个设备都可以从它们如何通过 PIC 芯片中断 CPU 的角度来看“打开”或“关闭”。通常作为应用程序开发人员,我们真正关心的是设备是否已打开,尽管如果您试图隔离性能问题,您可能会关闭某些其他设备。请记住,如果您关闭设备,中断将无法工作,直到它重新打开。这可能包括键盘或您操作计算机可能需要的其他关键设备。
设置此屏蔽的寄存器称为“操作控制字 1”或“OCW1”。它位于 PIC 基地址 + 1,或对于“主”PIC 位于端口 I/O 地址 $21。这是您需要进行位操作的地方,我在这里不会详细介绍。下表显示了为了启用或禁用每个硬件中断设备而需要更改的相关位。
位 | IRQ 启用 | 设备功能 |
---|---|---|
7 | IRQ7 | 并行端口 (LPT1) |
6 | IRQ6 | 软盘控制器 |
5 | IRQ5 | 保留/声卡 |
4 | IRQ4 | 串行端口 (COM1) |
3 | IRQ3 | 串行端口 (COM2) |
2 | IRQ2 | 从 PIC |
1 | IRQ1 | 键盘 |
0 | IRQ0 | 系统定时器 |
位 | IRQ 启用 | 设备功能 |
---|---|---|
7 | IRQ15 | 保留 |
6 | IRQ14 | 硬盘驱动器 |
5 | IRQ13 | 数学协处理器 |
4 | IRQ12 | PS/2 鼠标 |
3 | IRQ11 | PCI 设备 |
2 | IRQ10 | PCI 设备 |
1 | IRQ9 | 重定向的 IRQ2 设备 |
0 | IRQ8 | 实时时钟 |
假设我们要打开 IRQ3(串行端口 COM2 的典型端口),我们将使用以下软件
Port[$21] := Port[$21] and $F7; {Clearing bit 3 for enabling IRQ3}
要关闭它,我们将使用以下软件
Port[$21] := Port[$21] or $08; {Setting bit 3 for disabling IRQ3}
如果您在使任何东西正常工作时遇到问题,您只需在软件中发送此命令
Port[$21] := 0;
这只会启用所有内容。这可能不是一件好事,但您将不得不根据使用情况进行实验。尽量不要使用这种捷径,因为这不仅是懒惰程序员的标志,而且还可能产生副作用,导致您的计算机的行为与预期不同。如果您在这一级别处理计算机,目标是尽可能少地更改,以避免对您使用的任何其他软件造成损害。
现在我们已经了解了 8259 芯片,让我们继续讨论 UART 本身。虽然 PIC 的端口 I/O 地址相当标准,但计算机制造商通常会移动串行端口本身的内容。此外,如果您有作为附加卡的一部分的串行端口设备(如计算机扩展插槽中的 ISA 或 PCI 卡),这些设备通常会与内置到计算机主板中的设备具有不同的设置。可能需要一些时间来找到这些设置,并且在您试图编写软件时了解这些值非常重要。通常,这些值可以在计算机的 BIOS 设置屏幕中找到,或者如果您可以在计算机启动时暂停消息,它们可以在计算机启动过程的一部分中找到。
对于“典型”PC 系统,以下是每个串行 COM 端口的端口 I/O 地址和 IRQ。
COM 端口 | IRQ | 基端口 I/O 地址 |
---|---|---|
COM1 | IRQ4 | $3F8 |
COM2 | IRQ3 | $2F8 |
COM3 | IRQ4 | $3E8 |
COM4 | IRQ3 | $2E8 |
如果您注意到这里有一些有趣的事情,您会发现 COM3 和 COM1 共享同一个中断。这不是错误,而是在编写中断服务例程时需要牢记的事情。通过 8259 PIC 芯片提供的 15 个中断仍然不足以允许现代计算机上找到的所有设备都有其独立的硬件中断,因此在这种情况下,您需要学习如何与其他设备共享中断。当我们进入访问串行数据端口的实际软件时,我将更详细地介绍这一点,但现在请记住不要严格为一个设备编写软件。
基端口 I/O 地址对于我们将要讨论的下一个主题很重要,即直接访问 UART 寄存器。
UART 芯片共有 12 个不同的寄存器,映射到 8 个不同的端口 I/O 位置。是的,你没有看错,12 个寄存器在 8 个位置。显然这意味着有多个寄存器使用相同的端口 I/O 位置,并且会影响 UART 的配置方式。实际上,两个寄存器实际上是同一个,但处于不同的上下文中,因为你用来传输要从串行数据端口发送出去的字符的端口 I/O 地址与你可以从中读取发送到计算机的字符的地址相同。另一个 I/O 端口地址在写入数据时与读取数据时具有不同的上下文... 并且在写入数据后与读取数据时的数字不同。稍后会详细介绍。
该芯片最初设计时出现的一个问题是,设计人员需要能够使用 16 位发送有关串行数据波特率的信息。这实际上占用两个不同的“寄存器”,并由称为“除数锁存器访问位”或“DLAB”的东西切换。当 DLAB 设置为“1”时,可以设置波特率寄存器,而当它设置为“0”时,寄存器具有不同的上下文。
这一切听起来很混乱吗?确实可能,但让我们一次一步地来。以下是典型的 UART 芯片中可以找到的每个寄存器的表格。
基本地址 | DLAB | I/O 访问 | 缩写 | 寄存器名称 |
---|---|---|---|---|
+0 | 0 | 写入 | THR | 发送器保持缓冲区 |
+0 | 0 | 读取 | RBR | 接收器缓冲区 |
+0 | 1 | 读/写 | DLL | 除数锁存器低字节 |
+1 | 0 | 读/写 | IER | 中断使能寄存器 |
+1 | 1 | 读/写 | DLH | 除数锁存器高字节 |
+2 | x | 读取 | IIR | 中断识别寄存器 |
+2 | x | 写入 | FCR | FIFO 控制寄存器 |
+3 | x | 读/写 | LCR | 线路控制寄存器 |
+4 | x | 读/写 | MCR | 调制解调器控制寄存器 |
+5 | x | 读取 | LSR | 线路状态寄存器 |
+6 | x | 读取 | MSR | 调制解调器状态寄存器 |
+7 | x | 读/写 | SR | 暂存寄存器 |
DLAB 列中的“x”表示 DLAB 的状态对该偏移量范围内要访问哪个寄存器没有影响。还要注意,有些寄存器是只读的。如果你尝试向它们写入数据,你可能会遇到调制解调器出现问题(最坏情况),或者数据会被简单地忽略(通常是结果)。如前所述,某些寄存器共享一个端口 I/O 地址,其中一个寄存器将在你向其写入数据时使用,而另一个寄存器将在从同一地址检索数据时使用。
每个串行通信端口都有自己的一组这些寄存器。例如,如果你想访问 COM1 的线路状态寄存器 (LSR),并且假设基本 I/O 端口地址为 $3F8,那么用于获取此寄存器中信息的 I/O 端口地址将在 $3F8 + $05 或 $3FD 处找到。一些示例代码如下
const COM1_Base = $3F8; COM2_Base = $2F8; LSR_Offset = $05; function LSR_Value: Byte; begin Result := Port[COM1_Base+LSR_Offset]; end;
每个寄存器中都包含大量信息,以下是每个寄存器的含义及其包含的信息的解释。
发送器保持缓冲区/接收器缓冲区
[edit | edit source]偏移量:+0。发送和接收缓冲区是相关的,而且通常甚至使用相同的内存。这也是 8250 芯片的后续版本有重大影响的领域之一,因为后续型号在数据以串行数据形式传输之前,在芯片内部集成了某些数据缓冲。基本 8250 芯片一次只能接收一个字节,而后续芯片(如 16550 芯片)则会保存多达 16 个字节,以便在发送或接收之前(有时两者都... 取决于制造商)进行传输或接收,这样你就不必等待字符发送。这在多任务环境中很有用,在这些环境中,计算机执行许多任务,并且可能需要几毫秒才能恢复处理串行数据流。
这些寄存器实际上是串行数据通信的“核心”,以及数据如何从你的软件传输到另一台计算机以及如何从其他设备获取数据。对这些寄存器的读写操作很简单,只需访问相应 UART 的端口 I/O 地址即可。
如果接收缓冲区已占用或 FIFO 已满,则丢弃传入数据,并将接收线路状态中断写入 IIR 寄存器。线路状态寄存器中还会设置溢出错误位。
除数锁存器字节
[edit | edit source]偏移量:+0 和 +1。除数锁存器字节用于控制调制解调器的波特率。顾名思义,它用作除数来确定芯片将要使用的波特率。
实际上,这比这更简单。这实际上是一个倒计时时钟,每次 UART 传输一个位时都会使用该时钟。每次发送一个位时,一个倒计时寄存器都会重置为该值,然后倒计时到零。该时钟通常以 115.2 kHz 运行。换句话说,每秒 11.5 万次,一个计数器会下降以确定何时发送下一个位。在设计过程中的某一时刻,预计可能会使用其他频率使 UART 正常工作,但在为该芯片编写的现有软件数量众多的情况下,该频率几乎成为所有 PC 平台上使用的 UART 芯片的标准频率。它们可能在某些部分使用更快的时钟(例如 1.843 MHz 时钟),但该频率的一部分将用于向下扩展到 115.2 kHz 时钟。
关于 UART 时钟速度的更多信息(高级内容):对于许多 UART 芯片来说,驱动 UART 的时钟频率为 1.8432 MHz。然后将该频率通过一个分频器电路,将频率降低 16 倍,得到上面提到的 115.2 KHz 频率。如果你正在使用该芯片制作一些定制设备,国家半导体规格书允许使用 3.072 MHz 时钟和 18.432 MHz 时钟。这些更高的频率将允许你在更高的波特率下进行通信,但需要主板上的定制电路,并且通常还需要新的驱动程序来处理这些新的频率。有趣的是,你仍然可以在这些更高的时钟频率下以 50 波特的速度运行,但在原始 IBM-PC/XT 制造时,这不像现在这样重要,因为现在需要更高的数据吞吐量。
如果你使用以下数学公式,你可以确定需要将哪些数字放入除数锁存器字节中
这将为你提供以下表格,该表格可用于确定串行通信的常见波特率
波特率 | 除数(十进制) | 除数锁存器高字节 | 除数锁存器低字节 |
---|---|---|---|
50 | 2304 | $09 | $00 |
110 | 1047 | $04 | $17 |
220 | 524 | $02 | $0C |
300 | 384 | $01 | $80 |
600 | 192 | $00 | $C0 |
1200 | 96 | $00 | $60 |
2400 | 48 | $00 | $30 |
4800 | 24 | $00 | $18 |
9600 | 12 | $00 | $0C |
19200 | 6 | $00 | $06 |
38400 | 3 | $00 | $03 |
57600 | 2 | $00 | $02 |
115200 | 1 | $00 | $01 |
需要注意的是,600 及以上的波特率都将除数锁存器高字节设置为零。一个马虎的程序员可能会尝试跳过设置高字节,假设没有人会处理这么低的波特率,但这并非总是可以假设的。良好的编程习惯建议你应该仍然尝试将它设置为零,即使你只是在使用更高的波特率。
还要注意,除了上面列出的标准波特率之外,还有其他可能的波特率。虽然这在典型的应用程序中不鼓励,但它会是一件很有趣的事情。此外,你还可以尝试以这种方式与旧设备进行通信,在这些设备中,标准 API 库可能不允许特定应该兼容的波特率。这应该说明了为什么在这一级别上了解这些芯片仍然非常有用。
在使用这些寄存器时,还要记住,这些是唯一需要将除数锁存器访问位设置为“1”的寄存器。这将在下面进一步介绍,但我想要提到,对于应用程序软件来说,将 DLAB 设置为“1”只为了更改波特率的即时操作,然后在执行任何其他 I/O 访问调制解调器之前,将 DLAB 立即设置为“0”会很有用。这只是一个良好的工作习惯,并且使你需要为访问 UART 编写的其他软件更加干净、更容易。
一句警告:不要将两个除数锁存器字节的值都设置为“0”。虽然这(可能)不会损坏 UART 芯片,但 UART 传输串行数据的方式将是不可预测的,并且会从一台计算机到另一台计算机,甚至从你启动计算机的一次到下次都不一样。这是一个错误条件,如果你正在编写与该级别的波特率设置一起使用的软件,那么你应该捕获除数锁存器的潜在“0”值。
以下是一些设置和检索 COM1 的波特率的示例软件。
const COM1_Base = $3F8; COM2_Base = $2F8; LCR_Offset = $03; Latch_Low = $00; Latch_High = $01; procedure SetBaudRate(NewRate: Word); var DivisorLatch: Word; begin DivisorLatch := 115200 div NewRate; Port[COM1_Base + LCR_Offset] := Port[COM1_Base + LCR_Offset] or $80; {Set DLAB} Port[COM1_Base + Latch_High] := DivisorLatch shr 8; Port[COM1_Base + Latch_Low] := DivisorLatch and $FF; Port[COM1_Base + LCR_Offset] := Port[COM1_Base + LCR_Offset] and $7F; {Clear DLAB} end; function GetBaudRate: Integer; var DivisorLatch: Word; begin Port[COM1_Base + LCR_Offset] := Port[COM1_Base + LCR_Offset] or $80; {Set DLAB} DivisorLatch := (Port[COM1_Base + Latch_High] shl 8) + Port[COM1_Base + Latch_Low]; Port[COM1_Base + LCR_Offset] := Port[COM1_Base + LCR_Offset] and $7F; {Clear DLAB} Result := 115200 div DivisorLatch; end;
中断使能寄存器
[edit | edit source]偏移量:+1。这个寄存器允许你控制 UART 何时以及如何使用与串行 COM 端口关联的硬件中断触发中断事件。如果使用得当,这可以实现对系统资源的有效利用,并允许你实质上实时地对通过串行数据线发送的信息做出反应。关于这一点的更多内容将在后面介绍,但重点是你可以使用 UART 来告诉你何时需要提取一些数据。该寄存器既可以读也可以写。
以下表格显示了该寄存器中的每个位以及它将使能的事件,以允许你检查该芯片的状态。
位 | 说明 |
---|---|
7 | 保留 |
6 | 保留 |
5 | 使能低功耗模式 (16750) |
4 | 使能休眠模式 (16750) |
3 | 使能调制解调器状态中断 |
2 | 使能接收线路状态中断 |
1 | 使能发送器保持寄存器为空中断 |
0 | 使能接收数据可用中断 |
接收数据中断是一种让你知道是否有数据在等待你从 UART 中提取的方法。这可能是你将使用的最常用的一个位,并且具有更多用途。
发射器保持寄存器为空中断是为了通知你,输出缓冲区(在芯片的更高级模型中,例如 16550)已完成发送你放入缓冲区的所有内容。这是一种简化数据传输例程的方法,使其占用更少的 CPU 时间。
接收器线路状态中断指示 LSR 寄存器中的某些内容可能已更改。这通常是错误条件,如果你要为 UART 编写一个有效的错误处理程序,该处理程序将为你的应用程序的最终用户提供纯文本描述,那么你应该考虑这一点。这当然需要更高级的编程知识。
调制解调器状态中断是为了在与你的计算机连接的外部调制解调器发生变化时通知你。这可能包括电话“铃声”(你可以在你的软件中模拟它)、你已成功连接到另一个调制解调器(载波检测已开启)或有人“挂断”电话(载波检测已关闭)。它还可以帮助你了解外部调制解调器或数据设备是否可以继续接收数据(发送许可)。本质上,它处理 RS-232 标准中的其他线,而不只是严格的发送和接收线。
另外两种模式是专门针对 16750 芯片的,有助于将芯片置于“低功耗”状态,以用于笔记本电脑或具有电池等非常有限电源的嵌入式控制器。在早期芯片上,你应该将这些位视为“保留”,并且只将“0”放入其中。
偏移量:+2。此寄存器用于帮助识别你所使用的 UART 芯片的独特特性。此寄存器有两种用途。
- 识别 UART 触发中断的原因。
- 识别 UART 芯片本身。
其中,识别中断服务例程被调用的原因可能最为重要。
下表解释了此寄存器的部分详细信息,以及其上每一位表示的内容。
位 | 说明 | ||||
---|---|---|---|---|---|
7 和 6 | 位 7 | 位 6 | |||
0 | 0 | 芯片上没有 FIFO | |||
0 | 1 | 保留条件 | |||
1 | 0 | FIFO 已启用,但未运行 | |||
1 | 1 | FIFO 已启用 | |||
5 | 64 字节 FIFO 已启用(仅限 16750) | ||||
4 | 保留 | ||||
3、2 和 1 | 位 3 | 位 2 | 位 1 | 复位方法 | |
0 | 0 | 0 | 调制解调器状态中断 | 读取调制解调器状态寄存器 (MSR) | |
0 | 0 | 1 | 发射器保持寄存器为空中断 | 读取中断识别寄存器 (IIR) 或 写入发射器保持缓冲区 (THR) | |
0 | 1 | 0 | 接收数据可用中断 | 读取接收缓冲区寄存器 (RBR) | |
0 | 1 | 1 | 接收器线路状态中断 | 读取线路状态寄存器 (LSR) | |
1 | 0 | 0 | 保留 | 不适用 | |
1 | 0 | 1 | 保留 | 不适用 | |
1 | 1 | 0 | 超时中断待处理(16550 及更高版本) | 读取接收缓冲区寄存器 (RBR) | |
1 | 1 | 1 | 保留 | 不适用 | |
0 | 中断待处理标志 |
当你在为 8250 芯片(及其更高版本)编写中断处理程序时,你需要查看此寄存器,以确定中断的具体触发原因。
如前所述,多个串行通信设备可以共享同一个硬件中断。“位 0”的使用将让你知道(或确认)这确实是导致中断的设备。你需要做的是检查所有串行设备(位于单独的端口 I/O 地址空间),并获取此寄存器的内容。请记住,至少有可能多个设备同时触发中断,因此,当你执行此串行设备扫描时,请确保检查所有设备,即使第一个设备实际上需要处理。某些计算机系统可能不需要这样做,但这是一个良好的编程实践。还有可能,由于你之前处理了 UART,你已经处理了给定中断的所有 UART。当此位为“0”时,它表示 UART 正在触发中断。当它为“1”时,表示中断已被处理,或者此特定 UART 不是触发设备。我知道这对于计算机中使用的典型位标志来说似乎有点倒退,但这被称为数字逻辑被断言为低电平,在电子电路设计中非常常见。但是,这种逻辑模式进入软件领域则更加不寻常。
位 1、2 和 3 有助于识别 UART 中用于调用硬件中断的具体中断事件类型。这些与之前使用 IER 寄存器启用的中断相同。但是,在这种情况下,每次处理寄存器并处理中断时,它都是唯一的。如果由于同时发生多件事而导致 UART 发生多个“触发”,这将通过多个硬件中断来调用。早期的芯片组不使用位 3,但这是这些 UART 系统上的一个保留位,始终设置为逻辑状态“0”,因此在尝试破译已使用哪种中断时,编程逻辑不必不同。
为了解释 FIFO 超时中断,这是一种检查数据包结束或传入数据流是否已停止的方法。通常,此中断被触发的条件必须存在:一些数据需要在传入 FIFO 中,并且尚未被计算机读取。通过串行数据链路发送到 UART 的数据传输必须已结束,并且没有新的字符被接收。CPU 处理传入数据之前,必须从 FIFO 中检索任何数据,然后超时才会发生。超时通常在传输或接收至少 4 个字符所需的时间之后发生。如果你正在谈论以 1200 波特率、8 个数据位、2 个停止位、奇校验发送的数据,则这将花费大约 40 毫秒,就你的 4 GHz Pentium CPU 可以完成的事情而言,几乎是一个永恒。
上面列出的“复位方法”描述了如何通知 UART 给定中断已处理。当你访问复位方法下提到的寄存器时,这将清除该 UART 的中断条件。如果同一 UART 的多个中断已被触发,要么它不会清除 CPU 上的中断信号(在你完成时触发新的硬件中断),要么如果你检查回此寄存器 (IIR) 并查询中断待处理标志以查看是否有更多中断需要处理,则可以继续并尝试使用适当的应用程序代码解决你可能需要处理的任何新的中断问题。
位 5、6 和 7 正在报告用于传输和接收字符的 FIFO 缓冲区的当前状态。在最初发布时,16550 芯片设计中存在一个错误,该错误在 FIFO 中存在严重缺陷,导致 FIFO 报告其正在工作,但实际上并非如此。由于一些软件已经编写好与 FIFO 一起工作,因此保留了此位(此寄存器的位 7),但添加了位 6 以确认 FIFO 实际上正常工作,以防一些新的软件想要忽略早期版本上的硬件 FIFO。 16550 芯片。这种模式在该芯片的未来版本中也一直保留。在 16750 芯片上,已实施了额外的 64 字节 FIFO,位 5 用于指定此扩展缓冲区的存在。这些 FIFO 缓冲区可以使用下面列出的寄存器打开和关闭。
偏移量:+2。这是一个相对“新的”寄存器,它不是原始 8250 UART 实现的一部分。此寄存器的目的是控制芯片上的先进先出 (FIFO) 缓冲区的工作方式,并帮助你在应用程序中微调其性能。这甚至使你能够“打开”或“关闭”FIFO。
请记住,这是一个“只写”寄存器。尝试读取其内容只会为你提供中断识别寄存器 (IIR),而 IIR 具有完全不同的上下文。
位 | 说明 | |||
---|---|---|---|---|
7 & 6 | 位 7 | 位 6 | 中断触发级别(16 字节) | 触发级别(64 字节) |
0 | 0 | 1 字节 | 1 字节 | |
0 | 1 | 4 字节 | 16 字节 | |
1 | 0 | 8 字节 | 32 字节 | |
1 | 1 | 14 字节 | 56 字节 | |
5 | 启用 64 字节 FIFO(16750) | |||
4 | 保留 | |||
3 | DMA 模式选择 | |||
2 | 清除发射器 FIFO | |||
1 | 清除接收器 FIFO | |||
0 | 启用 FIFO |
向位 0 写入“0”将禁用 FIFO,本质上将 UART 转换为 8250 兼容模式。实际上,这也使此寄存器中的其余设置变得无用。如果你在此处写入“0”,它还将阻止 FIFO 发送或接收数据,因此通过串行数据端口发送的任何数据都可能在此设置更改后被加扰。建议仅在尝试重置串行通信协议并清除应用程序软件中可能存在的任何工作缓冲区时才禁用 FIFO。一些文档建议将此位设置为“0”也会清除 FIFO 缓冲区,但我建议使用位 1 和 2 进行显式缓冲区清除。
位 1 和 2 用于清除内部 FIFO 缓冲区。这在你首次启动应用程序时非常有用,你可能希望清除之前可能由使用 UART 的先前软件“遗留”的任何数据,或者如果你想要重置通信连接。这些位是“自动”重置的,因此,如果你将其中任何一个设置为逻辑“1”状态,你就不必以后再将它们设置为“0”。发送一个逻辑“0”只是告诉 UART 不要重置 FIFO 缓冲区,即使要更改 FIFO 控制的其他方面。
位 3 与 DMA(直接内存访问)的工作方式有关,主要是在你尝试从 FIFO 中检索数据时。这主要对试图直接访问串行数据并将这些数据存储在内部缓冲区的芯片设计师有用。UART 芯片本身有两个数字逻辑引脚,分别标记为 RXRDY 和 TXRDY。如果你试图使用 UART 芯片设计计算机电路,这可能有用甚至很重要,但对于 PC 系统上应用程序开发人员而言,它几乎没有用处,你可以放心地忽略它。
位 5 允许 16750 UART 芯片将缓冲区从 16 字节扩展到 64 字节。这不仅会影响缓冲区的大小,还会控制触发阈值的大小,如下一节所述。在早期的芯片类型上,这是一个保留位,应保持在逻辑“0”状态。在 16750 上,它使该 UART 的表现更像只有 16 字节 FIFO 的 16550。
位 6 和 7 描述触发阈值。这是在触发中断之前存储在 FIFO 中的字符数量,中断将通知您应从 FIFO 中删除数据。如果您预计会有大量数据通过串行数据链路发送,您可能需要增加缓冲区的大小。触发器最大值小于 FIFO 缓冲区大小的原因是,某些软件可能需要一些时间才能访问 UART 并检索数据。请记住,当 FIFO 充满时,您将开始丢失 FIFO 中的数据,因此,在达到此阈值后,务必确保您已检索到数据。如果您在尝试检索 UART 数据时遇到软件计时问题,您可能需要降低阈值。在阈值设置为 1 字节的极端情况下,它基本上会像基本的 8250 一样工作,但增加了可靠性,即在您没有机会立即获取所有字符的情况下,某些字符可能会被捕获在缓冲区中。
偏移量:+3。此寄存器有两个主要用途
- 设置除数锁存器访问位 (DLAB),允许您设置除数锁存器字节的值。
- 设置用于接收和发送串行数据的位模式。换句话说,您将使用的串行数据协议(8-1-无、5-2-偶校验等)。
位 | 说明 | |||
---|---|---|---|---|
7 | 除数锁存器访问位 | |||
6 | 设置断开使能 | |||
3, 4 & 5 | 位 5 | 位 4 | 位 3 | 奇偶校验选择 |
0 | 0 | 0 | 无奇偶校验 | |
0 | 0 | 1 | 奇校验 | |
0 | 1 | 1 | 偶校验 | |
1 | 0 | 1 | 标记 | |
1 | 1 | 1 | 空格 | |
2 | 0 | 一位停止位 | ||
1 | 1.5 位停止位或 2 位停止位 | |||
0 & 1 | 位 1 | 位 0 | 字长 | |
0 | 0 | 5 位 | ||
0 | 1 | 6 位 | ||
1 | 0 | 7 位 | ||
1 | 1 | 8 位 |
前两位(位 0 和位 1)控制通过串行协议传输的每个数据“字”发送多少数据位。对于大多数串行数据传输,这将是 8 位,但您会发现一些早期协议和旧设备需要更少的数据位。例如,一些军事加密设备每个串行“字”只使用 5 位数据,就像一些电传设备一样。早期的 ASCII 电传打字机终端只使用 7 位数据,而且事实上,这种传统在 SMTP 格式中得以保留,SMTP 格式只对电子邮件消息使用 7 位 ASCII。显然,这需要在您能够使用 RS-232 协议成功完成消息传输之前确定。
位 2 控制 UART 发送到接收设备的停止位数量。可以选择一位或两位停止位,逻辑“0”表示一位停止位,“1”表示两位停止位。在 5 位数据的情况下,UART 反而会发送“1.5 位停止位”。请记住,在此上下文中,“位”实际上是一个时间间隔:在 50 波特(每秒位数)下,每个位需要 20 毫秒。因此,“1.5 位停止位”将有至少 30 毫秒的字符间间隔。这与“5 位数据”设置相关联,因为只有使用 5 位 Baudot 而不是 7 位或 8 位 ASCII 的设备使用“1.5 位停止位”。
另一件需要牢记的事情是,RS-232 标准只规定每个串行数据字结束时至少会保持一位数据位周期为逻辑“1”(换句话说,从起始位、数据位、奇偶校验位和停止位组成的完整字符)。如果您在两台计算机之间遇到计时问题,但通常能够一次发送一个字符,您可能需要添加第二位停止位,而不是降低波特率。这会在每个字符的传输速度中增加一位的惩罚,而不是通过降低波特率(通常)将传输速度降低一半。
位 3、4 和 5 控制每个串行字如何响应奇偶校验信息。当位 3 为逻辑“0”时,这会导致不发送任何奇偶校验位与串行数据字一起发送。相反,它会立即移动到停止位,并承认在此级别的奇偶校验检查实际上毫无用处。通过包含奇偶校验位,您仍然可能会获得一些数据传输可靠性的提高,但还有其他更可靠和实用的方法,将在本书的其他章节中讨论。如果您想包含奇偶校验检查,以下解释了除“无”奇偶校验以外的每种奇偶校验方法
- 奇校验
- 串行字数据部分中的每一位都作为逻辑“1”位的数量的简单计数加起来。如果这是一个奇数位,奇偶校验位将作为逻辑“0”发送。如果计数为偶数,奇偶校验位将作为逻辑“1”发送,以使“1”位的数量为奇数。
- 偶校验
- 与奇校验一样,位加在一起。但是,在这种情况下,如果位的数量最终为奇数,它将作为逻辑“1”发送,以使“1”位的数量为偶数,这与奇校验正好相反。
- 标记奇偶校验
- 在这种情况下,奇偶校验位始终为逻辑“1”。虽然这可能看起来有点不寻常,但这主要是为了测试和诊断目的。如果您想确保串行连接接收端的软件能够正确响应奇偶校验错误,您可以发送标记或空格奇偶校验,并发送不符合接收 UART 或设备对奇偶校验的期望的字符。此外,对于仅限标记奇偶校验,您可以将此位用作额外的“停止位”。请记住,RS-232 标准期望逻辑“1”来结束串行数据字,因此接收计算机将无法区分“标记”奇偶校验位和停止位。本质上,您可以通过使用此设置以及通过适当使用此寄存器的停止位部分来实现 3 位或 2.5 位停止位。这是一种“调整”计算机设置的方法,典型应用程序不允许您这样做,或者至少可以更深入地了解串行数据设置。
- 空格奇偶校验
- 与标记奇偶校验一样,这使得奇偶校验位“粘性”,因此它不会改变。在这种情况下,它每次传输字符时都会输入逻辑“0”作为奇偶校验位。这样做没有太多实际用途,除了粗略地将每个串行字输入 9 位数据或用于上述诊断目的。
当位 6 设置为 1 时,会导致 TX 线变为逻辑“0”并保持该状态,接收 UART 将其解释为“0”位的长流 - “断开状态”。要结束“断开”,将位 6 恢复为 0。
偏移量:+4。此寄存器允许您在软件控制下进行“硬件”流控制。或者,更实际地说,它允许直接操作 UART 上的四根不同的线,您可以将这些线设置为任何独立的逻辑状态序列,并能够提供对调制解调器的控制。还应注意,大多数 UART 需要辅助输出 2 设置为逻辑“1”才能启用中断。
位 | 说明 |
---|---|
7 | 保留 |
6 | 保留 |
5 | 自动流控制使能 (16750) |
4 | 回环模式 |
3 | 辅助输出 2 |
2 | 辅助输出 1 |
1 | 请求发送 |
0 | 数据终端就绪 |
在典型的 PC 平台上,这些输出中只有请求发送 (RTS) 和数据终端就绪 (DTR) 实际上连接到 DB-9 连接器上的 PC 输出。如果您幸运地拥有 DB-25 串行连接器(在 PC 平台上更常用于并行通信),或者如果您在扩展卡上拥有自定义 UART,辅助输出可能会连接到 RS-232 连接。如果您将此芯片用作自定义电路中的一个组件,这将为您提供一些“免费”的额外输出信号,您可以在芯片设计中使用这些信号来发出任何您可能希望由 TTL 输出触发的信号,并且将在软件控制下。有更简单的方法可以做到这一点,但在这种情况下,它可能会节省您在布局上的额外芯片。
“回环”模式主要是一种测试 UART 以验证主 CPU 和 UART 之间的电路是否正常工作的方法。最终用户很少,如果有的话,需要测试这一点,但这对于使用 UART 的某些软件的一些初始测试可能有用。当将其设置为逻辑状态“1”时,任何放入发送寄存器的字符都会立即在 UART 的接收寄存器中找到。其他逻辑信号,如上面列出的 RTS 和 DTS,将出现在调制解调器状态寄存器中,就像您在串行通信端口末端放了一个回环 RS-232 设备一样。简而言之,这允许您仅使用软件进行回环测试。除了这些诊断目的以及使用 UART 的某些早期软件开发测试之外,这永远不会使用。
在 16750 上,有一种特殊的模式可以使用调制解调器控制寄存器来调用。基本上,这允许 UART 直接控制 RTS 和 DTS 的状态,以进行硬件字符流控制,具体取决于 FIFO 的当前状态。这种行为还受 FIFO 控制寄存器 (FCR) 的位 5 的状态影响。虽然这很有用,并且可以改变您编写 UART 控制软件的方式的一些逻辑,但 16750 作为芯片相对较新,并且在许多计算机系统中并不常见。如果您知道您的计算机有一个 16750 UART,那就享受利用此增强功能带来的乐趣吧。
偏移量:+5。此寄存器主要用于根据已接收的数据向您提供有关 UART 中可能存在的错误条件的信息。请记住,这是一个“只读”寄存器,写入此寄存器的任何数据都可能会被忽略,或者更糟糕的是,会导致 UART 的不同行为。此信息有几个用途,下面将提供一些有关它如何用于诊断串行数据连接问题的信息
位 | 说明 |
---|---|
7 | 接收 FIFO 中的错误 |
6 | 数据保持寄存器为空 |
5 | 发送器保持寄存器为空 |
4 | 断开中断 |
3 | 帧错误 |
2 | 奇偶校验错误 |
1 | 溢出错误 |
0 | 数据就绪 |
位 7 指的是 FIFO 中字符的错误。如果当前位于 FIFO 中的任何字符都存在此处列出的其他错误消息之一(例如帧错误、奇偶校验错误等),这提醒您需要清除 FIFO,因为 FIFO 中的字符数据不可靠并且存在一个或多个错误。在没有 FIFO 的 UART 芯片上,这是一个保留的位字段。
位 5 和 6 指的是字符发送器电路的状态,可以帮助你识别 UART 是否准备好接收另一个字符。如果所有字符(包括 FIFO,如果已启用)都已发送,并且“移位寄存器”也已完成发送,则位 6 设置为逻辑“1”。此移位寄存器是 UART 内部的内存块,从发送保持缓冲区 (THB) 或 FIFO 获取数据,并负责将数据实际转换为串行格式,一次发送一个数据位,并将移位寄存器的内容向下移一位以获取下一个位的数值。位 5 只是告诉你 UART 能够接收更多字符,包括发送到 FIFO 中的字符。
当串行数据输入线在至少与整个串行数据“字”(包括起始位、数据位、奇偶校验位和停止位)一样长的时期内接收“0”位时,中断 (位 4) 将变为逻辑“1”状态,对于给定的波特率,此时间对应于除数锁存字节。 (串行线的正常状态是在空闲时发送“1”位,或发送始终为一个“0”位的起始位,然后发送可变数据和奇偶校验位,然后发送“1”的停止位,如果线路进入空闲状态,则继续发送更多的“1”位。) 而不是正常状态的“0”位的长序列通常意味着发送串行数据到你的计算机的设备由于某种原因停止了。在串行通信中,这通常是一种正常情况,但这样你就可以监控另一个设备是如何工作的。一些串行终端有一个键,使它们可以生成这种“中断条件”作为带外信令方法。
当最后一个位不是停止位时,就会发生帧错误 (位 3)。或者更准确地说,停止位是逻辑“0”。这有几个原因,包括你可能在两台计算机之间的时序不匹配。这通常是由于波特率不匹配造成的,但也可能涉及其他原因,包括设备之间的物理布线问题或电缆太长。你甚至可能将数据位的数量弄错了,因此当遇到此类错误时,请仔细检查串行数据协议,以确保 UART 的所有设置(数据位长度、奇偶校验和停止位数量)都符合预期。
奇偶校验错误 (位 2) 也可能表明波特率不匹配,就像帧错误一样(特别是如果两种错误同时发生)。当预期(奇数、偶数、标记或空格)的奇偶校验算法未找到时,会设置此位。如果你在 UART 设置中使用“无奇偶校验”,则此位应该始终为逻辑“0”。当没有发生帧错误时,这是一种识别布线存在一些问题的方法,尽管你可能还需要处理其他问题。
溢出错误 (位 1) 是程序设计不当或操作系统没有给你对 UART 的适当访问权限的标志。当有字符等待读取,并且传入的移位寄存器试图将下一个字符的内容移入接收缓冲区 (RBR) 时,就会发生这种错误情况。在具有 FIFO 的 UART 上,这也表示 FIFO 已满。
可以做一些事情来帮助解决此错误,包括查看访问 UART 的软件的效率,特别是监控和读取传入数据的部分。在多任务操作系统上,你可能需要确保读取传入数据的软件部分位于单独的线程中,并且线程优先级较高或时间关键,因为这对使用串行通信数据的软件来说是一个非常重要的操作。应用程序的良好软件实践还包括添加通过软件完成的应用程序特定“缓冲区”,这为你的应用程序提供了更多机会来根据需要处理传入数据,并远离从 UART 获取数据的时限敏感子程序。此缓冲区可以小到 1KB,也可以大到 1MB,并且很大程度上取决于你正在处理的数据类型。还有其他更奇特的缓冲技术适用于应用程序开发领域,将在后面的模块中介绍。
如果你使用的是更简单的操作系统,如 MS-DOS 或实时操作系统,则需要区分轮询驱动访问 UART 和中断驱动软件。编写中断驱动程序效率更高,本书将专门有一部分介绍如何编写用于 UART 访问的软件。
最后,如果你无法解决防止溢出错误出现的问题,你可能需要考虑降低串行传输的波特率。这并不总是一个选择,实际上应该是你在软件中尝试解决此问题时最后的选择。为了快速测试基本算法是否有效,你可以从较低的波特率开始,逐渐提高速度,但这只应在软件的初始开发阶段进行,而不是在发布给客户或作为公开分发的软件进行。
数据就绪位 (位 0) 实际上是最简单的部分。这是一种简单地通知你 UART 有数据可供你的软件提取的方式。当此位为逻辑“1”时,就该读取接收缓冲区 (RBR) 了。在具有活动的 FIFO 的 UART 上,此位将保持逻辑“1”状态,直到你读取完 FIFO 的所有内容。
调制解调器状态寄存器
[edit | edit source]偏移量:+6。此寄存器是另一个只读寄存器,用于通知你的软件调制解调器的当前状态。以这种方式访问的调制解调器可以是外部调制解调器,也可以是使用 UART 作为与计算机接口的内部调制解调器。
位 | 说明 |
---|---|
7 | 载波检测 |
6 | 振铃指示器 |
5 | 数据设备就绪 |
4 | 发送许可 |
3 | 载波检测增量 |
2 | 振铃指示器后沿 |
1 | 数据设备就绪增量 |
0 | 发送许可增量 |
位 7 和 6 与调制解调器活动直接相关。载波检测将在调制解调器“连接”到另一个调制解调器时保持逻辑“1”状态。当此位变为逻辑“0”状态时,你可以假设电话连接已断开。振铃指示器位直接与 RS-232 线路(也称为“RI”或振铃指示器)相连。通常,当检测到电话线路上的“振铃电压”时,此位会变为逻辑“1”状态,就像传统电话会响铃以通知你有人试图打电话给你一样。
当我们进入 AT 调制解调器命令部分时,将有其他方法可以显示,这些方法可以通知你有关调制解调器状态的信息,以及其他信息,这些信息将作为正常串行数据流中的字符发送,而不是特殊线。实际上,这些额外的位几乎毫无价值,但从一开始就是规范的一部分,并且对于 UART 设计人员来说相对容易实现。但是,这可能是一种有效地发送一些额外信息或允许使用 UART 的软件设计师从其他设备获得其他目的的逻辑位信号的方式。
“数据设备就绪”和“发送许可”位(位 4 和 5)直接位于 RS-232 电缆上,并且与“请求发送”和“数据终端就绪”相匹配的线,这些线与“调制解调器控制寄存器 (MCR)”一起发送。使用两个寄存器中的这四个位,你可以执行“硬件流控制”,你可以向另一个设备发出信号,指示它何时发送更多数据,或者指示它在尝试处理信息时暂停并停止发送数据。当我们进入数据流控制时,将在另一个模块中详细介绍此主题。
关于“增量”位(位 0、1、2 和 3)的说明。在这种情况下,“增量”意味着变化,就像某个位的状态发生变化一样。这来自其他科学领域,如火箭科学,其中 delta-vee 表示速度的变化。就本寄存器而言,如果与该位相关的位(如载波检测增量与载波检测)自上次访问此寄存器以来已改变其逻辑状态,则这些位中的每一个在下一次访问此调制解调器状态寄存器时将为逻辑“1”。振铃指示器后沿与其他位几乎相同,只是当“振铃指示器”位从逻辑“1”状态变为逻辑“0”状态时,它才会处于逻辑“1”状态。这些信息并没有太多实际用途,但是有一些软件试图利用这些位并根据这些位对从 UART 接收的数据进行一些操作。如果你忽略了这 4 位,你仍然可以制作非常健壮的串行通信软件。
暂存寄存器
[edit | edit source]偏移量:+7。Scratch 寄存器是一个有趣的谜。为了尝试将所有其他 I/O 端口地址中的一大堆寄存器挤在一起,设计人员有一个额外的“寄存器”,他们不知道该如何处理它。请记住,在处理计算机体系结构时,处理 2 的幂更容易,因此他们“被迫”必须寻址 8 个 I/O 端口。允许另一个设备使用此额外的 I/O 端口将使主板设计过于复杂。
在 8250 UART 的某些变体中,写入此 Scratch 寄存器的任何数据都可以在你读取此寄存器的 I/O 端口时供软件使用。实际上,这为你提供了额外的 1 字节“内存”,你可以以任何你认为有用的方式在你的应用程序中使用它。除了病毒作者(也许我不应该提供任何想法),这个寄存器实际上没有很好的用途。有限的用途是你可以使用此寄存器来识别 UART 的特定变体,因为原始的 8250 不会存储通过此寄存器发送给它的数据。由于这种芯片在 PC 设计中几乎不再使用(这些公司使用更先进的芯片,如 16550),你不会在大多数现代 PC 类平台中找到这种“错误”。下面将详细介绍如何通过软件识别你的计算机中使用的 UART 芯片,以及每个串行端口。
UART 的软件识别
[edit | edit source]就像可以通过软件例程识别计算机系统中的许多组件一样,也可以检测到计算机上使用的 UART 的版本或变体。这是因为 UART 芯片的每个不同版本都具有一些独特的特性,如果进行排除法,就可以识别出您正在使用的版本。如果您尝试提高串行 I/O 例程的性能,了解是否有可用于传输和发送信息的缓冲区,以及更好地了解 PC 上的设备,这些信息可能很有用。
例如,您可以通过判断 Scratch 寄存器是否工作来确定 UART 的版本。在最初的 8250 和 8250A 芯片中,这些芯片模型的设计存在缺陷,导致 Scratch 寄存器无法正常工作。如果您向该寄存器写入一些数据,然后发现数据发生改变,则说明您计算机中的 UART 是这两种芯片模型之一。
另一个需要查看的地方是 FIFO 控制寄存器。如果将该寄存器的位“0”设置为逻辑 **1**,则您试图启用 UART 上的 FIFO,而这些 FIFO 仅存在于该芯片的较新版本中。读取位“6”和“7”将帮助您确定您使用的是 16550 还是 16550A 芯片。位“5”将帮助您确定芯片是否为 16750。
下面是一个完整的伪代码算法,帮助您确定正在使用的芯片类型
Set the value "0xE7" to the FCR to test the status of the FIFO flags. Read the value of the IIR to test for what flags actually got set. If Bit 7 is set Then If Bit 6 is set Then If Bit 5 is set Then UART is 16750 Else UART is 16550A End If Else UART is 16550 End If Else you know the chip doesn't use FIFO, so we need to check the scratch register Set some arbitrary value like 0x2A to the Scratch Register. You don't want to use 0xFF or 0x00 as those might be returned by the Scratch Register instead for a false postive result. Read the value of the Scratch Register If the arbitrary value comes back identical UART is 16450 Else UART is 8250 End If End If
用 Pascal 语言编写时,上面的算法最终看起来像这样
const COM1_Addr = $3F8; FCR = 2; IIR = 2; SCR = 7; function IdentifyUART: String; var Test: Byte; begin Port[COM1_Addr + FCR] := $E7; Test := Port[COM1_Addr + IIR]; if (Test and $80) > 0 then if (Test and $40) > 0 then if (Test and $20) > 0 then IdentifyUART := '16750' else IdentifyUART := '16550A' else IdentifyUART := '16550' else begin Port[COM1_Addr + SCR] := $2A; if Port[COM1_Addr + SCR] = $2A then IdentifyUART := '16450' else IdentifyUART := '8250'; end; end;
我们仍然没有区分 8250、8250A 或 8250B;但在大多数当前的计算机上,这样做几乎毫无意义,因为这些芯片早已过时,不太可能再找到。
可以使用类似的过程来确定计算机的 CPU,但超出了本书的范围。
- 中断编程的历史
- 8259 芯片信息,并解释了其他寄存器 (失效链接?)
- 串行/RS232 端口的接口
虽然 8250 是台式机中最流行的 UART,但其他流行的 UART 包括
- Atmel AVR 内部的 UART:... 嵌入式系统/Atmel_AVR#Serial_Communication
- Microchip PIC 内部的 UART:"Microchip AN774:使用 PICmicro® USART 的异步通信"
- 苹果 Macintosh 内部的 UART:...
- "位敲击" UART:... [1]
串口编程: 简介和 OSI 网络模型 -- RS-232 线路和连接 -- 典型的 RS232 硬件配置 -- 8250 UART -- DOS -- MAX232 驱动器/接收器系列 -- Windows 中的 TAPI 通信 -- Linux 和 Unix -- Java -- Hayes 兼容调制解调器和 AT 命令 -- 通用串行总线 (USB) -- 形成数据包 -- 错误校正方法 -- 双向通信 -- 数据包恢复方法 -- 串行数据网络 -- 实际应用开发 -- 串行连接上的 IP