Pintos Loader.S 详解(二):配置串口
Pintos 引导加载程序配置串行端口,用于在没有显示器的环境下输出调试信息。
概述
这部分代码配置计算机的串行端口(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):地线
为什么在引导时使用串口?
- VGA 可能不可用:在模拟器或服务器环境中,可能没有显示器
- 远程调试:可以通过串口连接到另一台电脑查看输出
- 日志记录:串口输出可以被重定向到文件
- 简单可靠:串口协议非常简单,不需要复杂的驱动
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 地址 |
|---|---|---|
| 0 | COM1 | 0x3F8 |
| 1 | COM2 | 0x2F8 |
| 2 | COM3 | 0x3E8 |
| 3 | COM4 | 0x2E8 |
为什么用 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 | 波特率 |
|---|---|
| 000 | 110 |
| 001 | 150 |
| 010 | 300 |
| 011 | 600 |
| 100 | 1200 |
| 101 | 2400 |
| 110 | 4800 |
| 111 | 9600 |
第 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 指令的作用:
- 将标志寄存器 FLAGS 压栈
- 将 CS(代码段)压栈
- 将 IP(指令指针)压栈
- 跳转到中断向量表中对应的处理程序
INT 14h, AH=00h(初始化串口)的参数:
| 寄存器 | 作用 |
|---|---|
| AH | 0x00 = 初始化功能 |
| AL | 参数(波特率、数据位等) |
| DX | 串口号(0-3) |
返回值:
| 寄存器 | 内容 |
|---|---|
| AH | 串口状态 |
| AL | Modem 状态 |
“Destroys AX” 注释的含义:
BIOS 中断会修改 AX 寄存器的值。调用后,我们不能假设 AX 还保持原来的值。如果需要 AX 的原值,必须在调用前保存。
第 5-6 行:打印启动信息
1
2
call puts
.string "Pintos"
这是什么?
调用 puts 函数打印字符串 “Pintos”。
特殊的调用约定:
这里使用了一种巧妙的技术——字符串直接跟在 call 指令后面。puts 函数会:
- 从返回地址处读取字符串
- 打印字符串
- 返回到字符串之后继续执行
我们将在后面的文档中详细解释 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 停止位
- 波特率 2400 → 位 7-5 = 101
- 奇校验 → 位 4-3 = 01
- 2 停止位 → 位 2 = 1
- 7 数据位 → 位 1-0 = 10
结果:10101110 = 0xAE
1
mov $0xae, %al # 2400 bps, Odd-7-2
练习思考
如果要使用 COM2(串口 1)而不是 COM1,应该如何修改代码?
计算参数值:19200 bps 是否被 BIOS INT 14h 支持?(提示:查看波特率选项表)
为什么在嵌入式系统开发中串口调试仍然很流行?
练习答案
点击查看答案 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 | 波特率 |
|---|---|
| 000 | 110 |
| 001 | 150 |
| 010 | 300 |
| 011 | 600 |
| 100 | 1200 |
| 101 | 2400 |
| 110 | 4800 |
| 111 | 9600 |
19200 bps 不在列表中。要使用更高波特率,需要:
- 直接编程串口控制器(写 I/O 端口 0x3F8 等)
- 设置除数锁存器来配置自定义波特率
- 这会增加代码复杂度,不适合 512 字节限制
点击查看答案 3
串口调试在嵌入式系统开发中仍然流行的原因:
- 简单可靠:
- 协议简单,几乎不需要驱动
- 硬件实现成熟稳定
- 不依赖复杂的软件栈
- 早期可用:
- 在系统启动的最早阶段就可以工作
- 不需要操作系统支持
- 可以调试引导过程
- 低资源消耗:
- 不需要显卡、显示器
- 只需要 2-3 根线(TX、RX、GND)
- 适合资源受限的嵌入式设备
- 远程访问:
- 可以通过串口服务器远程调试
- 不需要物理接触设备
- 适合机房、工厂等环境
- 历史兼容性:
- 大量现有工具支持
- 开发人员熟悉
- 文档丰富
下一部分
串口配置完成后,接下来我们要扫描硬盘,寻找 Pintos 内核分区。请参阅下一篇文章。