文章

Pintos Loader.S 详解(二):配置串口

Pintos 引导加载程序配置串行端口,用于在没有显示器的环境下输出调试信息。

Pintos Loader.S 详解(二):配置串口

概述

这部分代码配置计算机的串行端口(Serial Port),使得我们可以通过串口输出调试信息。这在没有显示器的环境下(如模拟器、服务器)非常有用。


原始代码

1
2
3
4
5
6
7
8
9
# Configure serial port so we can report progress without connected VGA.
# See [IntrList] for details.
	sub %dx, %dx			# Serial port 0.
	mov $0xe3, %al			# 9600 bps, N-8-1.
					# AH is already 0 (Initialize Port).
	int $0x14			# Destroys AX.

	call puts
	.string "Pintos"

前置知识

什么是串口(Serial Port)?

串口是一种古老但非常可靠的通信接口。它按照串行方式传输数据——一次传输一个比特(bit)。

1
2
3
4
5
6
7
    计算机 A                    计算机 B
   ┌────────┐                 ┌────────┐
   │        │  TX ──────► RX  │        │
   │  串口  │                 │  串口  │
   │        │  RX ◄────── TX  │        │
   └────────┘                 └────────┘
           GND ◄───────► GND
  • TX (Transmit):发送线
  • RX (Receive):接收线
  • GND (Ground):地线

为什么在引导时使用串口?

  1. VGA 可能不可用:在模拟器或服务器环境中,可能没有显示器
  2. 远程调试:可以通过串口连接到另一台电脑查看输出
  3. 日志记录:串口输出可以被重定向到文件
  4. 简单可靠:串口协议非常简单,不需要复杂的驱动

BIOS 串口服务(INT 14h)

BIOS 提供了串口操作的中断服务,通过 int $0x14 调用:

AH 值功能
0x00初始化串口
0x01发送字符
0x02接收字符
0x03获取串口状态

逐行详解

第 1 行:选择串口

1
sub %dx, %dx			# Serial port 0.

这是什么?

将 DX 寄存器清零,选择串口 0(COM1)。

串口编号:

DX 值串口名称I/O 地址
0COM10x3F8
1COM20x2F8
2COM30x3E8
3COM40x2E8

为什么用 sub %dx, %dx

和前面一样,这比 mov $0, %dx 节省 1 字节。


第 2 行:设置串口参数

1
mov $0xe3, %al			# 9600 bps, N-8-1.

这是什么?

将 0xE3 放入 AL 寄存器,这是串口初始化的参数。

0xE3 的含义(二进制:11100011):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  位 7-5: 波特率
  ┌───┬───┬───┐
  │ 1 │ 1 │ 1 │ = 111 = 9600 bps
  └───┴───┴───┘
  
  位 4-3: 奇偶校验
  ┌───┬───┐
  │ 0 │ 0 │ = 00 = None(无校验)
  └───┴───┘
  
  位 2: 停止位
  ┌───┐
  │ 0 │ = 1 个停止位
  └───┘
  
  位 1-0: 数据位
  ┌───┬───┐
  │ 1 │ 1 │ = 11 = 8 位数据
  └───┴───┘

参数详解:

参数含义
波特率9600 bps每秒传输 9600 比特
奇偶校验None不进行校验
数据位8 位每个字符 8 比特
停止位1 位每帧结束用 1 个停止位

这种配置通常简写为 “9600 N-8-1”“9600 8N1”

串口通信帧格式:

1
2
3
4
5
6
    ┌─────┬───────────────┬──────┬──────┐
    │起始位│   8 位数据    │校验位│停止位│
    │  0  │ D0 D1 ... D7  │ (无) │  1  │
    └─────┴───────────────┴──────┴──────┘
    
    时间 →

波特率选项表:

位 7-5波特率
000110
001150
010300
011600
1001200
1012400
1104800
1119600

第 3 行:注释说明

1
				# AH is already 0 (Initialize Port).

这是什么?

这是一条注释,解释为什么没有显式设置 AH。

为什么 AH 已经是 0?

回顾前面的代码:

1
sub %ax, %ax    # 这行将整个 AX(包括 AH 和 AL)清零

AX 寄存器由两部分组成:

1
2
3
4
5
    AX (16位)
┌───────┬───────┐
│  AH   │  AL   │
│ 高8位 │ 低8位 │
└───────┴───────┘

我们用 sub %ax, %ax 清零了整个 AX,所以 AH = 0。 然后 mov $0xe3, %al 只修改了 AL,AH 仍然是 0。

AH = 0 的意义:

对于 INT 14h,AH = 0 表示”初始化串口”功能。


第 4 行:调用 BIOS 中断

1
int $0x14			# Destroys AX.

这是什么?

调用 BIOS 中断 0x14(串口服务)。

int 指令的作用:

  1. 将标志寄存器 FLAGS 压栈
  2. 将 CS(代码段)压栈
  3. 将 IP(指令指针)压栈
  4. 跳转到中断向量表中对应的处理程序

INT 14h, AH=00h(初始化串口)的参数:

寄存器作用
AH0x00 = 初始化功能
AL参数(波特率、数据位等)
DX串口号(0-3)

返回值:

寄存器内容
AH串口状态
ALModem 状态

“Destroys AX” 注释的含义:

BIOS 中断会修改 AX 寄存器的值。调用后,我们不能假设 AX 还保持原来的值。如果需要 AX 的原值,必须在调用前保存。


第 5-6 行:打印启动信息

1
2
call puts
.string "Pintos"

这是什么?

调用 puts 函数打印字符串 “Pintos”。

特殊的调用约定:

这里使用了一种巧妙的技术——字符串直接跟在 call 指令后面。puts 函数会:

  1. 从返回地址处读取字符串
  2. 打印字符串
  3. 返回到字符串之后继续执行

我们将在后面的文档中详细解释 puts 函数。

执行效果:

在 VGA 显示器和串口上都输出:

1
Pintos

这让用户知道引导加载程序已经开始运行。


串口通信原理图解

数据发送过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
         CPU                    串口控制器                  串口线
    ┌──────────┐             ┌──────────────┐           ┌────────┐
    │ 发送字符 │  写入数据   │              │  串行输出 │        │
    │  'P'     │ ─────────► │ 发送缓冲区   │ ────────► │ TX ──► │
    │          │            │              │           │        │
    └──────────┘             └──────────────┘           └────────┘
    
    字符 'P' = 0x50 = 01010000
    
    传输波形:
    ─┐ ┌─┐   ┌─┐   ┌─────┐
     └─┘ └───┘ └───┘     └─
      0 0 0 0 1 0 1 0 停止
      │ └──┬──┘ └─┬─┘
      │    │     │
    起始  数据   数据

为什么是 9600 波特率?

  • 9600 bps 意味着每秒传输 9600 个比特
  • 每个字符需要 10 比特(1 起始 + 8 数据 + 1 停止)
  • 所以每秒最多传输 960 个字符
  • 对于引导阶段的调试输出来说足够了

在模拟器中的应用

QEMU

Pintos 通常在 QEMU 模拟器中运行。QEMU 可以将串口输出重定向:

1
2
3
4
5
# 串口输出到终端
qemu-system-i386 -serial stdio ...

# 串口输出到文件
qemu-system-i386 -serial file:serial.log ...

Bochs

在 Bochs 模拟器中,可以配置:

1
com1: enabled=1, mode=file, dev=serial.log

常见问题

Q1: 如果没有串口会怎样?

如果物理硬件上没有串口,BIOS 可能会忽略这些操作,或者返回错误状态。但这不会导致引导失败——我们还有 VGA 输出作为后备。

Q2: 为什么选择 9600 波特率?

9600 是一个常见的、广泛支持的波特率。更高的波特率(如 115200)可能在某些旧 BIOS 上不支持。

Q3: 什么是 “N-8-1”?

  • N:No parity(无奇偶校验)
  • 8:8 个数据位
  • 1:1 个停止位

这是最常见的串口配置。

Q4: 为什么同时输出到 VGA 和串口?

冗余输出确保在各种环境下都能看到调试信息:

  • 有显示器时看 VGA
  • 无显示器时看串口
  • 调试时两者都看

实践示例

手动计算初始化参数

假设你想配置:2400 bps, 奇校验, 7 数据位, 2 停止位

  1. 波特率 2400 → 位 7-5 = 101
  2. 奇校验 → 位 4-3 = 01
  3. 2 停止位 → 位 2 = 1
  4. 7 数据位 → 位 1-0 = 10

结果:10101110 = 0xAE

1
mov $0xae, %al    # 2400 bps, Odd-7-2

练习思考

  1. 如果要使用 COM2(串口 1)而不是 COM1,应该如何修改代码?

  2. 计算参数值:19200 bps 是否被 BIOS INT 14h 支持?(提示:查看波特率选项表)

  3. 为什么在嵌入式系统开发中串口调试仍然很流行?


练习答案

点击查看答案 1

只需将 DX 设置为 1 而不是 0:

mov $1, %dx          # Serial port 1 (COM2)
mov $0xe3, %al       # 9600 bps, N-8-1
int $0x14

或者使用节省空间的方式:

sub %dx, %dx
inc %dx              # DX = 1, Serial port 1

串口编号对应关系:

  • DX = 0 → COM1 (0x3F8)
  • DX = 1 → COM2 (0x2F8)
  • DX = 2 → COM3 (0x3E8)
  • DX = 3 → COM4 (0x2E8)
点击查看答案 2

不支持。 BIOS INT 14h 的波特率选项表只到 9600 bps:

位 7-5波特率
000110
001150
010300
011600
1001200
1012400
1104800
1119600

19200 bps 不在列表中。要使用更高波特率,需要:

  • 直接编程串口控制器(写 I/O 端口 0x3F8 等)
  • 设置除数锁存器来配置自定义波特率
  • 这会增加代码复杂度,不适合 512 字节限制
点击查看答案 3

串口调试在嵌入式系统开发中仍然流行的原因:

  1. 简单可靠
    • 协议简单,几乎不需要驱动
    • 硬件实现成熟稳定
    • 不依赖复杂的软件栈
  2. 早期可用
    • 在系统启动的最早阶段就可以工作
    • 不需要操作系统支持
    • 可以调试引导过程
  3. 低资源消耗
    • 不需要显卡、显示器
    • 只需要 2-3 根线(TX、RX、GND)
    • 适合资源受限的嵌入式设备
  4. 远程访问
    • 可以通过串口服务器远程调试
    • 不需要物理接触设备
    • 适合机房、工厂等环境
  5. 历史兼容性
    • 大量现有工具支持
    • 开发人员熟悉
    • 文档丰富

下一部分

串口配置完成后,接下来我们要扫描硬盘,寻找 Pintos 内核分区。请参阅下一篇文章。

本文由作者按照 CC BY 4.0 进行授权