跳转到内容

串行编程/8250 UART 编程

来自维基教科书,开放世界中的开放书籍

最后,我们正在远离电线、电压和硬核电气工程应用,尽管我们仍然需要了解相当多的计算机芯片架构知识。虽然本节的主要重点将集中在 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 I/O 端口

[编辑 | 编辑源代码]

我们应该回到英特尔 8086 之前,回到最初的英特尔 CPU,即 4004,及其后续产品 8008。8008 的所有计算机指令或操作码在今天的英特尔芯片中仍然起作用,因此即使是 30 年前的端口 I/O 教程在今天也是有效的。更新的 CPU 改进了处理更多数据更有效率的指令,但原始指令仍然存在。

当 8008 发布时,英特尔试图设计一种方法,让 CPU 与外部设备通信。他们选择了一种叫做 I/O 端口架构的方法,这意味着芯片有一组特殊的引脚专门用于与外部设备通信。在 8008 中,这意味着总共有 16 个引脚专门用于与芯片通信。确切的细节因芯片设计和其他因素而异,这些因素过于详细,不适合目前的讨论,但总体理论相当简单。

8 个引脚代表一个 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 芯片的工作方式。

软件 I/O 访问

[编辑 | 编辑源代码]

当你真正开始在软件中使用它时,向端口 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 端口“映射”到什么设备。我们将在稍后深入了解如何识别用于串行通信的 I/O 端口的更多细节。最后,我们开始编写一些软件,还有更多内容。

x86 端口 I/O 扩展

[edit | edit source]

8088 CPU 和 8086 之间存在一些差异。对软件开发影响最大的一个区别是,8086 可以访问 65536 个不同的 I/O 端口,而不是只有 256 个端口 I/O 地址。但是,计算机配置可能使用少于 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 代表中断请求。总共有 15 个不同的硬件中断。在你认为我不知道如何计数或做数学运算之前,我们在这里需要上一节历史课,我们将在继续讨论 8259 芯片时完成。当原始的 IBM-PC 构建时,它只有 8 个 IRQ,分别标记为 IRQ 0 到 IRQ 7。当时人们认为这足以满足几乎所有将要安装在 PC 上的东西,但很快人们意识到这远远不够满足当时正在添加的所有东西。当 IBM-PC/AT 制造出来时(第一个搭载 80286 CPU 的计算机,以及许多如今在 PC 上常见的增强功能),人们决定使用两个相同的芯片而不是一个 8259 芯片,并将它们“链接”在一起,以便将中断数量从 8 个扩展到 15 个。为了完成这项任务,必须牺牲一个 IRQ,那就是 IRQ 2。

重点是,如果一个设备想要通知 CPU 它有一些数据已准备好供 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 寄存器

[edit | edit source]

8259 有几个与 I/O 端口地址相关的“寄存器”。当我们接触到 8250 芯片时,我们将进一步了解这个概念。对于典型的 PC 计算机系统,以下是与 8259 相关的典型主要端口地址


中断控制器端口 I/O 地址
寄存器名称 I/O 端口
主中断控制器 $0020
从中断控制器 $00A0

这个主要端口地址是我们将在软件中用来直接与 8259 芯片通信的。可以通过这些 I/O 端口地址向该芯片发送许多命令,但就我们的目的而言,我们实际上不需要处理它们。大多数这些命令用于由计算机的基本输入输出系统 (BIOS) 完成计算机设备的初始设置和配置,除非您从头开始重写 BIOS,否则您真的不必担心这一点。此外,当您在此级别处理设备时,每台计算机的行为都略有不同,因此这是计算机制造商更应该担心的事情,而不是应用程序程序员应该处理的事情,这正是 BIOS 软件被编写的根本原因。

请记住,这是大多数 PC 兼容型计算机系统的“典型”端口 I/O 地址,并且可能会因制造商试图实现的目标而异。通常您不必担心此级别的兼容性问题,但当我们接触到串行端口的端口 I/O 地址时,这将成为一个更大的问题。

设备寄存器

[edit | edit source]

我将在这里花一点时间来解释“寄存器”一词的含义。当您在此级别处理设备时,设计设备的电气工程师会提到改变设备配置的寄存器。这可以在几个抽象级别发生,因此我想消除一些混淆。

寄存器只是一个小的 RAM 部分,设备可以直接操作。在像 8086 或奔腾这样的 CPU 中,这些是用于直接执行数学运算(例如将两个数字加在一起)的内存区域。这些通常被称为 AX、SP 等。典型的 CPU 上只有很少的寄存器,因为对这些寄存器的访问直接编码到基本的机器级指令中。

当我们谈论设备寄存器时,请记住这些不是 CPU 寄存器,而是设备本身的内存区域。这些通常被设计成连接到端口 I/O 内存,因此当您写入或读取端口 I/O 地址时,您实际上是在直接访问设备寄存器。有时会有进一步的抽象级别,您将有一个端口 I/O 地址指示您要更改哪个寄存器,另一个端口 I/O 地址将包含您要发送到该寄存器的數據。您如何处理设备取决于它有多复杂以及您将做什么。

从某种意义上说,它们是寄存器,但请记住,这些设备中的每一个都可以被认为是一个完整的计算机,而您所做的只是建立它将如何与主 CPU 通信。不要在这里停滞不前,不要把这些与 CPU 寄存器混淆。

ISR 清理

[edit | edit source]

在使用中断控制器时,您必须定期进行交互的一个领域是告知 8259 PIC 控制器中断服务例程已完成。当您的软件执行中断处理程序时,CPU 没有自动的方法向 8259 芯片发出已完成的信号,因此 PIC 中的特定“寄存器”需要被设置为让下一个中断处理程序能够访问计算机系统。完成此操作的典型软件如下所示

  Port[$20] := $20;

这将发送一个名为“中断结束”的命令,通常简写为“EOI”。还有其他命令可以发送到此寄存器,但就我们的目的而言,这是我们唯一需要关心的命令。

现在这将清除“主”PIC,但是如果您使用的是在“从”PIC 上触发的设备,您还需要告知该芯片中断服务已完成。这意味着您还需要像这样向该芯片发送“EOI”

  Port[$A0] := $20;
  Port[$20] := $20;

您可以做其他事情来使您的计算机系统顺利运行,但现在让我们保持简单。

PIC 设备屏蔽

[edit | edit source]

在我们离开 8259 PIC 的主题之前,我想介绍一下设备屏蔽的概念。连接到 PIC 的每一个设备都可以从它们如何通过 PIC 芯片中断 CPU 的角度来看被“打开”或“关闭”。通常作为应用程序开发人员,我们真正关心的只是设备是否被打开,尽管如果您试图隔离性能问题,您可能会关闭一些其他设备。请记住,如果您关闭了一个设备,则中断将无法工作,直到它被重新打开。这可能包括您可能需要操作计算机的键盘或其他关键设备。

设置此掩码的寄存器称为“操作控制字 1”或“OCW1”。它位于 PIC 基地址 + 1,或者对于“主”PIC 位于端口 I/O 地址 $21。这是您需要查看位操作的地方,我在这里不会详细介绍。以下表格显示了为了启用或禁用每个硬件中断设备而需要更改的相关位


主 OCW1 ($21)
IRQ 已启用 设备功能
7 IRQ7 并行端口 (LPT1)
6 IRQ6 软盘控制器
5 IRQ5 保留/声卡
4 IRQ4 串行端口 (COM1)
3 IRQ3 串行端口 (COM2)
2 IRQ2 从 PIC
1 IRQ1 键盘
0 IRQ0 系统计时器


从 OCW1 ($A1)
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;

这只会启用所有内容。这可能不是一件好事,但您将不得不根据您正在使用的东西进行实验。尽量不要使用这种捷径,因为这不仅是懒惰程序员的标志,而且还会产生副作用,您的计算机可能表现出与您预期不同的行为。如果您在此级别处理计算机,目标是尽可能少地更改,以免对您使用的任何其他软件造成损害。

串行 COM 端口内存和 I/O 分配

[edit | edit source]

现在我们已经了解了 8259 芯片,让我们继续讨论 UART 本身。虽然 PIC 的端口 I/O 地址相当标准,但电脑制造商通常会更改串行端口本身的位置。此外,如果您有作为附加卡的一部分的串行端口设备(例如计算机扩展插槽中的 ISA 或 PCI 卡),这些设备通常会具有与计算机主板上内置的设备不同的设置。找到这些设置可能需要一些时间,而且在尝试编写软件时了解这些值非常重要。通常,这些值可以在计算机的 BIOS 设置屏幕中找到,或者如果您可以在计算机开机时暂停信息,则可以在计算机启动过程中找到它们。

对于一个“典型”的 PC 系统,以下是每个串行 COM 端口的端口 I/O 地址和 IRQ。


常见的 UART IRQ 和 I/O 端口地址
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 寄存器

[edit | edit source]

UART 芯片总共有 12 个不同的寄存器,映射到 8 个不同的端口 I/O 位置。是的,您读得没错,12 个寄存器在 8 个位置。显然这意味着不止一个寄存器使用相同的端口 I/O 位置,并且会影响 UART 的配置方式。实际上,两个寄存器实际上是同一个,但处于不同的上下文,因为您传输要从串行数据端口发送的字符的端口 I/O 地址与您可以在其中读取发送到计算机的字符的地址相同。另一个 I/O 端口地址在写入数据时与读取数据时具有不同的上下文...... 并且在写入数据后与读取数据时的数字不同。稍后我们将详细介绍。

该芯片最初设计时出现的一个问题是,设计者需要能够以 16 位发送有关串行数据波特率的信息。这实际上占用两个不同的“寄存器”,并通过所谓的“分频器锁存器访问位”或“DLAB”进行切换。当 DLAB 设置为“1”时,可以设置波特率寄存器,而当它为“0”时,寄存器具有不同的上下文。

这一切听起来很令人困惑吗?它可能是,但让我们一次一步地了解它。以下是可以在典型的 UART 芯片中找到的每个寄存器的表格


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 的频率运行。换句话说,每秒 115 千次,一个计数器向下计数以确定何时发送下一个位。在设计过程中的某个时候,人们预计可能会使用其他频率来使 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 访问调制解调器之前将其恢复为“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 来让您确切地知道何时需要提取一些数据。此寄存器具有读写访问权限。

以下是显示此寄存器中的每个位以及它将启用的事件的表格,这些事件允许您检查此芯片的状态


中断使能寄存器 (IER)
备注
7 保留
6 保留
5 启用低功耗模式 (16750)
4 启用睡眠模式 (16750)
3 启用调制解调器状态中断
2 启用接收机线路状态中断
1 启用发射机保持寄存器为空中断
0 启用接收数据可用中断

接收数据中断是一种让您知道是否有数据在等待您从 UART 中拉取的方法。这可能是您比其他位使用得更多、用途更广泛的位。

发射机保持寄存器为空中断是让您知道输出缓冲区(在芯片的更高级模型中,如 16550)已完成发送您推送到缓冲区的所有内容。这是一种简化数据传输例程的方法,使其占用更少的 CPU 时间。

接收机线路状态中断表明 LSR 寄存器中的某些内容可能已更改。这通常是一个错误情况,如果您要为 UART 编写一个高效的错误处理程序,该处理程序将向应用程序的最终用户提供纯文本描述,那么您应该考虑这一点。这当然需要更高级的编程知识。

调制解调器状态中断是在与您的计算机连接的外部调制解调器发生更改时通知您的。这可能包括电话“铃声”响(您可以在软件中模拟此操作)、您已成功连接到另一个调制解调器(载波检测已打开)、或有人已“挂断”电话(载波检测已关闭)。它还可以帮助您了解外部调制解调器或数据设备是否可以继续接收数据(发送允许)。本质上,它处理 RS-232 标准中的其他线,而不是严格的发送和接收线。

其他两种模式严格适用于 16750 芯片,并帮助将芯片置于“低功耗”状态,以便在笔记本电脑或具有非常有限电源(如电池)的嵌入式控制器等设备上使用。在较早的芯片上,您应该将这些位视为“保留”,并且只向其中写入“0”。

中断识别寄存器

[edit | edit source]

偏移量:+2。此寄存器用于帮助识别您使用的 UART 芯片的唯一特性。此寄存器有两个用途

  • 识别 UART 触发中断的原因。
  • 识别 UART 芯片本身。

其中,识别中断服务例程被调用的原因可能最为重要。

下表解释了此寄存器的某些详细信息,以及它上的每个位所代表的内容


中断识别寄存器 (IIR)
备注
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 缓冲区可以使用下面列出的寄存器打开和关闭。

FIFO 控制寄存器

[edit | edit source]

偏移量:+2。这是一个相对“新的”寄存器,它不是原始 8250 UART 实现的一部分。此寄存器的目的是控制芯片上先进先出 (FIFO) 缓冲区的工作方式,并帮助您微调应用程序中的性能。这甚至可以让您“打开”或“关闭”FIFO。

请记住,这是一个“只写”寄存器。试图读取内容只会得到中断识别寄存器 (IIR),它有完全不同的上下文。


FIFO 控制寄存器 (FCR)
备注
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 启用 FIFOs

将“0”写入位 0 将禁用 FIFOs,本质上将 UART 转换为 8250 兼容模式。实际上,这也使该寄存器中其余设置变得无用。如果您在此处写入“0”,它还会阻止 FIFOs 发送或接收数据,因此通过串行数据端口发送的任何数据在此设置更改后都可能被加扰。仅当您尝试重置串行通信协议并清除应用程序软件中可能存在的任何工作缓冲区时,才建议禁用 FIFOs。一些文档建议将此位设置为“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 的行为更像 16550,只有一个 16 字节 FIFO。

位 6 和 7 描述触发阈值。这是存储在 FIFO 中的字符数,在触发中断之前,该中断将让您知道应从 FIFO 中删除数据。如果您预计将通过串行数据链路发送大量数据,您可能需要增加缓冲区的大小。FIFO 缓冲区大小的触发值最大值较小的原因是,一些软件可能需要一段时间才能访问 UART 并检索数据。请记住,当 FIFO 充满时,您将开始丢失来自 FIFO 的数据,因此务必确保在达到此阈值后已检索到数据。如果您在尝试检索 UART 数据时遇到软件计时问题,您可能需要降低阈值。在阈值设置为 1 字节的最极端情况下,它将基本像基本的 8250 一样工作,但增加了在您没有机会立即获取所有字符的情况下,一些字符可能会被捕获到缓冲区中的可靠性。

线路控制寄存器

[edit | edit source]

偏移量:+3。此寄存器有两个主要用途

  • 设置除数锁存器访问位 (DLAB),允许您设置除数锁存器字节的值。
  • 设置用于接收和发送串行数据的位模式。换句话说,您将使用的串行数据协议 (8-1-无、5-2-偶等)。


线路控制寄存器 (LCR)
备注
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 位数据,一些 TELEX 设备也是如此。早期的 ASCII 电传打字机终端只使用 7 位数据,实际上,这种传统一直保留在 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。

调制解调器控制寄存器

[edit | edit source]

偏移量:+4。此寄存器允许您在软件控制下进行“硬件”流控制。或者更实用地说,它允许直接操作 UART 上的四条不同电线,您可以将这些电线设置为任何独立的逻辑状态序列,并能够提供对调制解调器的控制。还应注意,大多数 UART 需要将辅助输出 2 设置为逻辑“1”才能启用中断。


调制解调器控制寄存器 (MCR)
备注
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 根据 FIFO 的当前状态直接控制 RTS 和 DTS 的状态,以进行硬件字符流控制。FIFO 控制寄存器 (FCR) 的第 5 位的状态也会影响此行为。虽然这很有用,并且可以改变您编写 UART 控制软件的逻辑方式,但 16750 相对来说是一种新的芯片,在许多计算机系统中并不常见。如果您知道您的计算机有 16750 UART,那么享受使用这种增强功能的乐趣。

线路状态寄存器

[编辑 | 编辑源代码]

偏移量:+5。该寄存器主要用于根据接收到的数据提供有关 UART 中可能存在的错误条件的信息。请记住,这是一个“只读”寄存器,写入该寄存器的任何数据都可能被忽略,或者更糟糕的是,会导致 UART 的不同行为。该信息有几种用途,下面将提供有关如何利用它来诊断串行数据连接问题的一些信息。


线路状态寄存器 (LSR)
备注
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 的字符。

中断断开(第 4 位)在串行数据输入线在至少与整个串行数据“字”一样长的时间内接收“0”位时变为逻辑状态“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 的所有内容为止。

调制解调器状态寄存器

[编辑 | 编辑源代码]

偏移量:+6。该寄存器是另一个只读寄存器,用于向您的软件通告调制解调器的当前状态。以这种方式访问的调制解调器可以是外部调制解调器,也可以是使用 UART 作为与计算机接口的内部调制解调器。


调制解调器状态寄存器 (MSR)
备注
7 载波检测
6 振铃指示
5 数据设备准备就绪
4 清除发送
3 增量数据载波检测
2 后沿振铃指示
1 增量数据设备准备就绪
0 增量清除发送

第 7 位和第 6 位与调制解调器活动直接相关。当调制解调器“连接”到另一个调制解调器时,载波检测将保持逻辑状态“1”。当它变为逻辑状态“0”时,您可以假设电话连接已断开。振铃指示位直接连接到 RS-232 电线,该电线也标记为“RI”或振铃指示。通常,当检测到电话线上的“振铃电压”时,此位将变为逻辑状态“1”,就像传统电话会响起以通知您有人在尝试呼叫您一样。

当我们进入 AT 调制解调器命令部分时,将显示其他方法来通知您有关此信息以及有关调制解调器状态的其他信息,并且这些信息将作为正常串行数据流中的字符发送,而不是特殊电线。实际上,这些额外的位没什么用,但从一开始就是规范的一部分,并且调制解调器设计人员比较容易实现。但是,它可能是一种有效地发送一些附加信息或允许使用 UART 的软件设计人员从其他设备获取一些逻辑位信号以用于其他目的的方式。

“数据设备准备就绪”和“清除发送”位(第 4 位和第 5 位)直接位于 RS-232 电缆上,并且与“请求发送”和“数据终端准备就绪”匹配的电线通过“调制解调器控制寄存器 (MCR) 传输。使用两个寄存器中的这四个位,您可以执行“硬件流控制”,您可以在其中向另一个设备发出信号,指示它何时发送更多数据,或在您尝试处理信息时暂停发送数据。在后面的模块中,当我们进入数据流控制时,将对该主题进行更多介绍。

关于“delta”位(位 0、1、2 和 3)的说明。在本例中,“delta”表示变化,如某个位的状态变化。这源于其他科学领域,例如火箭科学,其中 delta-vee 表示速度变化。就本寄存器而言,如果与之关联的位(例如 Delta 数据载波检测与载波检测)从您上次访问该调制解调器状态寄存器时更改了逻辑状态,则这些位中的每一个在您下次访问该寄存器时将为逻辑“1”。尾随边沿振铃指示器与其他位大体相同,只是当“振铃指示器”位从逻辑“1”变为逻辑“0”状态时,它才处于逻辑“1”状态。这些知识实际上并没有什么实际用处,但有一些软件试图利用这些位,并根据这些位对从 UART 接收到的数据进行一些操作。如果您忽略这 4 位,您仍然可以制作非常健壮的串行通信软件。

暂存寄存器

[编辑 | 编辑源代码]

偏移量:+7。Scratch Register 是一个有趣的谜题。设计人员为了将大量寄存器挤进所有其他 I/O 端口地址,而留了一个额外的“寄存器”,他们不知道该怎么用。请记住,在处理计算机体系结构时,处理 2 的幂会更容易,因此他们“被迫”要寻址 8 个 I/O 端口。允许另一个设备使用这个额外的 I/O 端口会使主板设计过于复杂。

在 8250 UART 的某些变体中,写入此 scratch register 的任何数据都可以在您读取此寄存器的 I/O 端口时供软件使用。实际上,这为您提供了一个额外的字节“内存”,您可以在应用程序中以任何您认为有用的方式使用它。除了病毒作者(也许我不应该给出任何想法),这个寄存器实际上没有很好的用途。有限的用途是,您可以使用这个寄存器来识别 UART 的特定变体,因为原始的 8250 不会存储通过这个寄存器发送给它的数据。由于该芯片在 PC 设计中几乎不再使用(这些公司正在使用更先进的芯片,如 16550),因此您不会在大多数现代 PC 类型平台中发现这个“bug”。下面将详细介绍如何通过软件识别计算机中使用的 UART 芯片,以及每个串行端口。

UART 的软件识别

[编辑 | 编辑源代码]

就像可以通过软件例程识别计算机系统中的许多组件一样,也可以检测到计算机上找到的 UART 的版本或变体。这是因为 UART 芯片的每个不同版本都有一些独特的特性,如果您进行一个排除过程,您就可以识别出您正在处理的版本。如果您试图提高串行 I/O 例程的性能,了解是否有可用于传输和发送信息的缓冲区,以及更好地了解 PC 上的设备,这些信息可能很有用。

您可以确定 UART 版本的一个例子是 Scratch Register 是否工作。在第一个 8250 和 8250A 芯片中,这些芯片模型的设计存在一个缺陷,导致 Scratch Register 不工作。如果您向这个寄存器写入一些数据,并且它返回的是更改后的数据,那么您就知道计算机中的 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,但这超出了本书的范围。

外部链接

[编辑 | 编辑源代码]

虽然 8250 是台式计算机上最流行的 UART,但其他流行的 UART 包括

其他串行编程文章

[编辑 | 编辑源代码]
华夏公益教科书