Serial Ports
串口(串行端口-Serial ports)是IBM-PC兼容计算机上常见的传统通信端口。 PC上连接外围设备的串口在很大程度上已经被弃用,而推荐使用USB和其它现代外围接口, 然而,在某些行业中,它仍然普遍用于与工业硬件 (例如CNC机器) 或商业设备 (例如POS终端) 进行接口连接。 历史上,拨号调制解调器通过计算机的串行端口连接是很常见的,而底层UART硬件本身的设计也反映串口的常用程度。
串口通常由UART硬件控制。 这是负责对串行接口发送的数据进行编码和解码的硬件芯片。 现代串口通常实现RS-232标准,并且可以使用各种不同的连接器接口。 DE-9接口是现代系统中最常用的串口连接器之一。
操作系统开发人员对串口特别感兴趣,因为它们的驱动程序比USB更容易实现,并且在许多x86系统中仍然很常见。 操作系统开发人员通常使用系统的串口进行调试,因为它们不需要复杂的硬件设置,并且在操作系统初始化的早期阶段传输信息时非常有用。 许多仿真器(如QEMU和Bochs)允许将串行输出重定向到主机的标准输入输出(Stdio)或文件上。
线路、引脚、连接器等
Wikipedia的Serial ports页面上有很多信息,这里对其进行了总结。 串行接口非常简单。 它实际上有两种: 25针和9针。其中25针端口并没有更好用,只是具有更多的引脚 (大多数未使用) 并且更大。 9针更小,使用频率更高,但在过去,25针的使用频率更高。 9针的被称为DE-9,而25针的被称为DB-25。 他们使用母插头插入你的计算机 (除非你的计算机很奇怪用的是母端口,在这种情况下,你的电缆将需要公插头)。 此wikipedia页面提供了有关所用插头的更多信息。
两者都有相同的基本引脚类型。 DB-25将大部分引脚作为接地引脚或简单地不连接,而DE-9仅具有一个接地引脚。 有一个发送pin(用于发送信息)和一个接收pin(用于获取信息)。 大多数串口都以双工模式运行--也就是说,它们可以同时发送和接收。 还有一些其他引脚,用于硬件握手。 在过去,没有双工模式的情况下,如果一台计算机想要发送某个信息,它必须使用一个硬件握手引脚告诉另一台设备或计算机它将要发送。 然后,另一台设备将使用另一个握手引脚告诉对方,它可以开始发送。 今天已有双工模式,但握手引脚仍在使用。
如果要连接两台计算机,电缆中需要两样东西:
- 电缆需要有两个母插头,这样才能插入两台计算机。
- 电缆需要具有其发送-接收线,并且需要握手线来做切换。 这可以在电缆本身中完成,也可以作为扩展,成为零调制解调器(Null Modem)
对于串行终端设备,不需要以这种方式设置电缆。 设备的接收端切换了电线连接,并且具有母端口,这意味着你可以将公插头插入其中。
为什么要使用串口
在自制内核开发的早期阶段中,你可能想知道为什么要费心编写串行驱动程序。 有几个原因可能会让你理解:
- GDB调试
- 你可以使用串口连接到主机,并使用gdb调试器调试你的操作系统。 这涉及在你的操作系统内为GDB编写存根。
- 无头控制台(Headless console)
- 你可以在没有显示器、键盘或鼠标的情况下操作计算机,而使用TTY或VT100等协议将串行端口用作控制台。
- 外部日志记录
- 当系统本身有时有可能崩溃时,最好在测试系统出现三重故障(triple-faults)之前将调试输出安全地发送到另一台计算机。
- 网络和文件传输
- 当其他更传统的方法不可用时,串行端口对于在系统之间传输信息很有用。
对串行通信端口进行编程
如果要使用串口进行通信,首先必须对其进行初始化。 你需要告诉它另一台计算机或设备之间的连接速度会有多快 (这被称为波特率-baud rate) --你的速度必须与其他设备或计算机设置的速度相同,否则会出现问题。 使用较慢的速度可能更安全,除非你出于某种原因需要较快的速度,例如,如果你想通过串行连接玩多人游戏。 你还需要设置奇偶校验类型和字符(character,译者注:本文说的字符是一次传输的数据,不是指文本中的字符)中的位数。 同样,这些也必须设置为与对方计算机或设备相同的值,否则通信将无法工作。
一旦你设置了这些内容,你还需要设置中断处理程序。 你可以通过轮询端口以查看是否有任何新的数据到达,或者是否该发送另一个字符, 但这会减慢速度,并且在大多数实时应用程序或多线程环境中无法很好地工作。 在运行游戏的情况下,这根本不是一个好主意。
你可以将IRQ #4用于COM端口1或3,将IRQ #3用于COM端口2或4 (当你收到中断时,你可以知道哪个端口发送了中断)。 IRQ处理程序会检查你是否正在接收某些内容,如果是,他们会接收字符并以某种方式进行处理,例如将其放入缓冲区。 它们还要检查对方是否准备好从你那里接收东西,如果你有要发送的东西,它就会被发送。
端口地址
COM端口的地址(译者注:这里指Intel IO地址)可能会有所不同,具体取决于它们与机器的连接方式以及BIOS的配置方式。 一些BIOS配置工具允许你对这些进行查看和设置,因此,如果你对测试计算机有疑问,这里可能是一个很好的入门。
在大多数情况下,前两个COM端口将位于指定的地址,而其他COM端口的地址则不太可靠。
COM端口 | IO端口 |
---|---|
COM1 | 0x3F8 |
COM2 | 0x2F8 |
COM3 | 0x3E8 |
COM4 | 0x2E8 |
COM5 | 0x5F8 |
COM6 | 0x4F8 |
COM7 | 0x5E8 |
COM8 | 0x4E8 |
你可以在BIOS Data Area中找到COM端口的IO端口地址; 但是,请注意,这在现代/UEFI系统上不起作用,UEFI可以告诉你只存在于芯片组中的串口(并且可能并没有可以插入的连接器),UEFI不会告诉你固件不知道(也不能知道)的任何其他串口(例如,扩展卡上的),并且会使你的操作系统容易受到“BIOS quirks/bugs”的影响。 由于串口具有相对标准的IO端口,因此使用手动探测技术更有效。 具体来说,实现串口控制器时,请先查看暂存器寄存器是否可以存储值,然后尝试环回(Loopback)测试(你应该使用该测试来确定串行端口是否存在故障)。
一旦你有了COM端口的基地址,你就可以添加一个偏移量来访问其中一个数据寄存器。 其中一个寄存器保存所谓的DLAB,即Divisor Latch Access Bit(除数锁存访问位)。 该位设1时,偏移量0和1被映射到除数寄存器的低字节和高字节,用于设置端口的波特率。 此位清0时,偏移量0和1映射到其正常寄存器。 DLAB位仅影响端口偏移0和1,其他偏移忽略此设置。
IO端口偏移 | DLAB的设置 | 映射到此端口的寄存器 |
---|---|---|
0 | 0 | 数据寄存器(Data register)。 读取该寄存器将从接收缓冲区读取。 写入此寄存器将写入传输缓冲区。 |
+1 | 0 | 中断启用寄存器(Interrupt Enable Register)。 |
+0 | 1 | 当DLAB设置为1时,这是用于设置波特率除数值的最低有效字节。 |
+1 | 1 | 当DLAB设置为1时,这是设置波特率除数值的最高有效字节。 |
+2 | - | 中断标识和FIFO控制寄存器(Interrupt Identification and FIFO control Register) |
+3 | - | 线路控制寄存器(Line Control Register)。 这个寄存器的最高有效位是DLAB。 |
+4 | - | 调制解调器控制寄存器(Modem Control Register)。 |
+5 | - | 线路状态寄存器(Line Status Register)。 |
+6 | - | 调制解调器状态寄存器Modem Status Register()。 |
+7 | - | 暂存寄存器(Scratch Register)。 |
线路协议
通过导线传输的串行数据可以设置许多不同的参数。 通常,发送设备和接收设备需要向各自串行控制器写入相同的协议参数值,以便通信成功。
如今,你可以认为8N1(8位,无奇偶校验,一个停止位)几乎是默认值。
波特率
串行控制器(UART)有一个内部时钟,以每秒115200次的速度运行,还有一个时钟除数,用于控制波特率。 这与可编程中断定时器(PIT-Programmable Interrupt Timer)使用的系统,类型完全相同。
为了设置端口的速度,需要计算给定波特率所需的除数,并将其编程到除数寄存器中。 例如,除数为1将给出115200波特,除数为2将给出57600波特,3将给出38400波特,等等。
不要试图使用除数0来获得无限波特率,这是行不通的。 这种情况下大多数串行控制器将生成未指定且不可预测的波特率 (无论如何,无限的波特肯定将意味着无限的传输错误。)
要将除数设置到控制器,请执行以下操作:
- 设置线路控制寄存器(Line Control Register)的最高有效位为1。 这是DLAB位,开启允许访问除数寄存器。
- 将除数值的最低有效字节发送到[PORT + 0]。
- 将除数值的最高有效字节发送到[PORT + 1]。
- 清除线路控制寄存器的最高有效位为0。
数据位
数据字符(译者注:再次说明这里指一次传输的位数大小,不是文本字符)中的位数是可变的。 当然,拥有更少的比特会更快,但它们存储的信息更少。 如果只发送ASCII文本,可能只需要7位。
通过写入线路控制寄存器[PORT + 3]的两个最低有效位来设置此值。
Bit 1 | Bit 0 | 字符长度 (位) |
---|---|---|
0 | 0 | 5 |
0 | 1 | 6 |
1 | 0 | 7 |
1 | 1 | 8 |
停止位
串行控制器可以配置为在每个数据字符之后发送若干位。 这些可靠性支持位可以被控制器用来验证发送和接收设备是否同相。
如果字符长度具体为5位,则停止位只能设置为1或1.5。 对于其他字符长度,停止位只能设置为1或2。
要设置停止位的数量,请设置线路控制寄存器(Line Control Register) [PORT + 3] 的位2。
Bit 2 | 停止位 |
---|---|
0 | 1 |
1 | 1.5/2(取决于字符长度) |
奇偶校验
控制器可以在传输的每个字符的末尾添加或接受奇偶校验位。 利用该奇偶校验位,如果单个数据位因干扰而反转,则会引发奇偶校验错误(parity error)。 奇偶校验类型可以是NONE, EVEN, ODD, MARK或SPACE。
如果奇偶校验设置为“NONE”,则不会添加奇偶校验位,也不会添加奇偶校验位。 如果发送方发送的是接收方没有预料到的,则很可能会导致错误。
如果奇偶校验是MARK或SPACE,则期望奇偶校验位始终分别设置为1或0。
如果奇偶校验设置为EVEN或ODD,控制器通过将所有数据位和奇偶校验位的值相加来计算奇偶校验的值。(译者注:这个过程并不需要软件关心,控制器只要负责双方设置一致即可) 如果端口设置为具有EVEN奇偶校验,则结果必须为偶数。 如果设置为具有ODD奇偶校验,则结果必须为奇数。
要设置端口奇偶校验,请设置线路控制寄存器[PORT + 3]的位3、4和5。
Bit 5 | Bit 4 | Bit 3 | 奇偶校验 |
---|---|---|---|
- | - | 0 | NONE |
0 | 0 | 1 | ODD |
0 | 1 | 1 | EVEN |
1 | 0 | 1 | MARK |
1 | 1 | 1 | SPACE |
中断启用寄存器(Interrupt enable register)
要在中断模式下与串行端口通信,必须正确设置中断启用寄存器(见上表)。 要确定应启用哪些中断,必须将具有以下位(0=禁用,1=启用)的值写入中断启用寄存器:
Bit | 中断 |
---|---|
0 | 可用数据 |
1 | 发送器为空 |
2 | Break/error |
3 | 状态更改 |
4-7 | 未使用 |
调制解调器控制寄存器(Modem Control Register)
调制解调器控制寄存器是硬件握手寄存器的一半。 虽然大多数串行设备不再使用硬件握手,但这些线路仍然包含在所有16550芯片兼容的UART中。 这些可以用作通用输出端口,也可以实际执行握手。 通过写入调制解调器控制寄存器,它会将这些线路设置为激活状态。
Bit | 名称 | 含义 |
---|---|---|
0 | 数据终端就绪(DTR - Data Terminal Ready) | 控制数据终端就绪引脚 |
1 | 请求发送 (RTS-Request to Send) | 控制请求发送Pin |
2 | OUT 1 | 控制硬件引脚(OUT1),PC中未使用 |
3 | Out 2 | 控制硬件引脚(OUT2),用于在PC实现中启用IRQ |
4 | Loop | 为UART的诊断测试提供本地环回功能 |
5 | 0 | 未使用 |
6 | 0 | 未使用 |
7 | 0 | 未使用 |
大多数PC串行端口使用OUT2来控制断开(通过三态信号门-tristates)IRQ线路的电路。 这使得多个串行端口可以共享一条IRQ线路,只要一次只启用一个端口即可。 环回模式是一种诊断功能。 当位4设置为逻辑1时,会发生以下情况:发送串行输出(SOUT)设置为标记(逻辑1)状态; 接收器串行输入 (SIN) 断开; 发送移位寄存器(Transmitter Shift Register)的输出被“环回”到接收移位寄存器(Receiver Shift Register)的输入中; 四个调制解调器控制输入(DSR、CTS、RI和DCD)断开; 四个调制解调器控制输出 (DTR,RTS,OUT 1和OUT 2) 内部连接到四个调制解调器控制输入, 调制解调器控制输出引脚被迫处于非活动状态(高)。 在环回模式中,立即接收传输的数据。 此功能允许处理器验证UART的发送和接收数据路径。 在环回模式下,接收和发送中断完全可操作。 它们来自外部。 调制解调器控制中断也是可操作的,但是中断的源现在是调制解调器控制寄存器的较低的四位,而不是四个调制解调器控制输入。 中断仍由中断启用寄存器控制。
线路状态寄存器(Line status register)
线路状态寄存器对于检查错误和启用轮询非常有用。(译者注:寄存器中的set-设置和clear-清楚,不明确说明就是指位为1和0,并不是中文中常见动词含义)
Bit | 名称 | 含义 |
---|---|---|
0 | 数据就绪 (DR - Data ready ) | 设置是否有可以读取的数据 |
1 | 溢出错误(OE - Overrun error) | 设置是否有数据丢失 |
2 | 奇偶校验错误(PE-Parity error) | 如果奇偶校验检测到传输中存在错误,则设置1 |
3 | 帧错误 (FE-Framing error) | 设置是否缺少停止位 |
4 | 中断指示器(BI-Break indicator) | 设置数据输入是否中断 |
5 | 传送器保持寄存器为空(THRE - Transmitter holding register empty) | 如果传输缓冲区为空(即可以发送数据),则设置1 |
6 | 传送器为空 (TEMT - Transmitter empty) | 如果发送器没有执行任何操作则,设置1 |
7 | Impending 错误 | 设置输入缓冲区中的word是否出错 |
调制解调器状态寄存器(Modem Status Register)
该寄存器提供来自外围设备的控制线的当前状态。 除了该当前状态信息之外,调制解调器状态寄存器的四位还提供更改信息。 每当来自调制解调器的控制输入改变状态时,这些位被设置为逻辑1。每当CPU读取调制解调器状态寄存器时,它们被重置为逻辑0
Bit | 名称 | 含义 |
---|---|---|
0 | 增量清除以发送(DCTS-Delta Clear to Send) | 表示CTS输入自上次读取以来状态已更改 |
1 | |表示DSR输入自上次读取以来已改变状态 | |
2 | Ring指示器的跳变沿 (TERI - Trailing Edge of Ring Indicator) | 表示输入到芯片的RI已从低状态变为高状态 |
3 | 增量数据载波检测(DDCD - Delta Data Carrier Detect) | 表示DCD输入自上次读取以来状态已更改 |
4 | 清除发送(CTS - Clear to Send) | 反向CTS信号 |
5 | 数据集就绪 (DSR - Data Set Ready) | 反向DSR信号 |
6 | Ring指示器(RI) | RI信号倒置 |
7 | 数据载波检测(DCD - Data Carrier Detect) | 反向DCD信号 |
如果设置了MCR的位4 (LOOP 位),则高4位将镜像调制解调器控制寄存器中设置的4条状态输出线。
终端
- 正文: 终端
一旦你可以放心地发送和接收字节,你可能希望将串口连接到终端 (现代或者更可能是终端仿真器)。 当按下一个键时,它们会发送特定的字节序列,并可以解释代码以在屏幕上移动光标和改变颜色。
示例代码
初始化
#define PORT 0x3f8 // COM1
static int init_serial() {
outb(PORT + 1, 0x00); // 禁用所有中断
outb(PORT + 3, 0x80); // 使能DLAB(设置波特率除数)
outb(PORT + 0, 0x03); // 将除数设置为3 (lo byte) 38400波特
outb(PORT + 1, 0x00); // (hi byte)
outb(PORT + 3, 0x03); // 8位,无奇偶校验,一个停止位
outb(PORT + 2, 0xC7); // 开启FIFO,清零,14字节阈值
outb(PORT + 4, 0x0B); // IRQs启用,RTS/DSR设置
outb(PORT + 4, 0x1E); // 设置为环回模式,测试串行芯片
outb(PORT + 0, 0xAE); // 测试串口芯片(发送字节0xAE,检查串口是否返回相同字节)
// 检查串口是否有故障 (即: 与发送的字节不同)
if(inb(PORT + 0) != 0xAE) {
return 1;
}
// 如果串行没有故障,则将其设置为正常操作模式
// (启用IRQ并启用OUT#1和OUT#2位时不环回)
outb(PORT + 4, 0x0F);
return 0;
}
请注意,上面的初始化代码使用不同的值写入[PORT + 1]两次。 一次与[Port + 0]一起写入除数寄存器,一次写入中断寄存器,如上一节已经对过程进行了说明。 对行控制(Line Control)寄存器 [PORT + 3] 的第二次写入会再次清除DLAB,并设置其他各种位。
接收数据
int serial_received() {
return inb(PORT + 5) & 1;
}
char read_serial() {
while (serial_received() == 0);
return inb(PORT);
}
发送数据
int is_transmit_empty() {
return inb(PORT + 5) & 0x20;
}
void write_serial(char a) {
while (is_transmit_empty() == 0);
outb(PORT,a);
}
词汇表
- 波特率(Baud Rate)
- 是串行线路在其两种状态之间切换的速度。 这不等同于bps(bit pre second),因为存在起始位和停止位。 在8/N/1线路上,10波特=1字节。 由于具有多个波形,调制解调器比普通串行线路更复杂,但对于操作系统开发而言,这是无关紧要的。
- 波特率除数(baud rate divisor)
- 串行端口可以运行的最快速度,为115200位每秒。
- 停止位(stop bits)
- 在每个字符之间发送的NULL位,以同步发射器和接收器。
- UART
- 全称是,通用异步接收器/收发器(Universal Asynchronous Receiver/Transceiver): 将字节在串行线上每比特发送或接受的芯片。