Appearance
微机原理与接口技术
概念
前面几门课是分头讲的:silicon 讲器件怎么开关,circuit 讲门怎么组合,arch 讲计算机整体怎么组织。微机原理要回答的是另一件事:把 CPU、存储器、接口和总线装到同一块板子上之后,它们之间到底连了几根线、一个地址怎么落到某个具体的寄存器上。
一句话说清它是什么:微机原理就是计算机组成原理的"接线版"——部件还是那些部件,只是换了个视角:不问"流水线有几级",而问"这条指令的地址从哪根线出来、接到哪个芯片的第几个引脚"。
它只有一个核心模型,四样东西:
| 部件 | 干什么 | 与谁相连 |
|---|---|---|
| CPU | 取指、译码、执行 | 三总线 |
| 存储器 | 存指令和数据 | 三总线 |
| I/O 接口 | 把外设信号翻译成总线信号 | 三总线 + 外设 |
| 总线 | 大家共享的信息通道 | 上面三者 |
为什么嵌入式要从这一篇开头:单片机就是"把上表四样东西塞进一个芯片里",芯片的引脚就是总线的延伸。看不懂"端口地址",后面 GPIO、UART 的寄存器代码一行都写不出来——那些宏定义本质上全是这里的地址换算。
这一层回答了上一层什么问题:circuit 教会我们"门能搭出寄存器和加法器",但没回答"这些部件怎么连成一整台能跑程序的机器"。微机原理补的就是这一环:部件之间的连接方式。
原理
一、三总线:计算机内部的"公共汽车"
总线是一组共享的、分时使用的导线。微机内部的部件都挂在这组线上,同一时刻只有一个主方在发。按传什么分成三类:
| 总线 | 方向 | 传什么 | 决定什么 |
|---|---|---|---|
| 数据总线 DB | 双向 | 数据、指令码 | 一次能搬多少位(字长) |
| 地址总线 AB | 单向(CPU 发出) | 地址编号 | 能访问多少个单元(容量) |
| 控制总线 CB | 双向 | 读/写、中断、时钟等 | 什么时候读、什么时候写 |
两条必须记牢的因果关系:
- 地址总线宽度 → 可寻址空间:
n根地址线 →2^n个单元。 - 数据总线宽度 → 一次搬运量:16 位数据总线一次搬 2 字节,32 位一次搬 4 字节。
总线是整机性能的咽喉:所有部件共用一条路,同一时刻只能有一对在通话,这就是后面在 arch 里算过的"总线带宽不匹配"问题。放大器件的速度很快,但路只有一条。
二、CPU 内部:把上一层的门变成一条通路
CPU 里能看见的东西就三块:
+---------------------------+
| 控制器 CU |
| PC -> IR -> 译码 -> 控制 |
+------------+--------------+
| 控制信号
+------------v--------------+
| 运算器 ALU + 寄存器组 |
| AC / 通用寄存器 / PSW |
+------------+--------------+
| 数据
+------------v--------------+
| 内部总线 -> 外部三总线 |
+---------------------------+一次"取指-译码-执行"的循环,就是上面这张图上信号走一圈:
- 取指:PC 把地址送上地址总线,存储器把该单元的字节送回数据总线,装进 IR;PC 自动加 1。
- 译码:控制器拆开 IR,知道这是加法、要访问哪个地址。
- 执行:控制器发出读/写信号,数据在 ALU 与寄存器之间流动,结果写回。
寄存器和存储器在这里第一次被分清:寄存器在 CPU 里面,用几根地址线编码就能选中(快、少);存储器在外面,要用完整地址总线去访问(慢、多)。
三、存储器的组织:地址空间、字长与端序
存储器是一长串编号的格子,每个格子存一个字节。三件事决定它怎么用:
- 地址空间:
2^n个单元,每个单元 1 字节(按字节编址)或一个字。 - 字长:CPU 一次处理的位数。8086 是 16 位,所以它的"一个字"= 2 字节。
- 端序:多字节数据在内存里的字节顺序。
端序这件事看起来只是约定,但它是嵌入式的第一道坎:
数据 0x12345678 | 低地址 → 高地址 | 谁在用 |
|---|---|---|
| 小端 little-endian | 78 56 34 12 | x86、ARM 默认 |
| 大端 big-endian | 12 34 56 78 | 网络字节序、部分 RISC |
小端的直觉记法:低地址放"低位",也就是最低有效字节在最前面。收发包、读传感器寄存器、写 Flash,只要两端端序不一致,就会读出"字节反了"的数——这是嵌入式最常见的 bug 之一。
四、编址方式:统一编址 vs 独立编址
接口里有若干个端口(寄存器)。CPU 怎么访问它们,两种方案:
| 方式 | 端口占不占内存地址 | 用什么指令访问 | 优点 | 缺点 |
|---|---|---|---|---|
| 统一编址(存储器映射) | 占,从地址空间里划一块 | 普通访存指令 | 指令多、寻址方式丰富 | 吃掉内存地址空间 |
| 独立编址(端口映射) | 不占,单独一套地址空间 | 专用 I/O 指令 | 不占内存、程序可读性好 | 指令少、寻址方式受限 |
ARM、RISC-V 走的是统一编址(所以它们的"寄存器"就是用指针去读写某个地址);x86 走的是独立编址,用 IN / OUT 指令,8086 的端口号是 16 位,于是有 65536 个端口。
五、段式寻址:20 位地址怎么装进 16 位寄存器
8086 的地址总线是 20 位,可寄存器只有 16 位。塞不下怎么办?用两个 16 位数拼一个 20 位数:
text
物理地址 = 段寄存器 x 16 + 偏移 = (段寄存器 << 4) + 偏移也就是把段值左移 4 位再相加。这带来三个后果,全是考点:
- 一个段最大 64 KB:偏移是 16 位,
0x0000到0xFFFF。 - 同一个物理地址有多种写法:段可以以 16 字节为步长滑动,所以同一处内存能被许多组
段:偏移指到。 - 段与段可以重叠:段并不是互不相交的分区,只是"窗口"。
示例
例 1:段:偏移 换算与"一个地址多种写法"
取段寄存器 0x1000、偏移 0x0234:
物理地址 = 0x1000 x 16 + 0x0234
= 0x10000 + 0x0234
= 0x10234再看另两组:0x1000:0020 与 0x1002:0000:
0x1000 x 16 + 0x0020 = 0x10000 + 0x20 = 0x10020
0x1002 x 16 + 0x0000 = 0x10020 + 0x00 = 0x10020同一个物理地址 0x10020,两种完全不同的段:偏移写法。因为段寄存器每加 1,基址就前进 16 字节,只要偏移相应减 16,落点不变。推而广之,段内偏移最大 0xFFFF,所以一个物理地址最多有 4096 种写法。
还有一个著名的边界:0xFFFF:FFFF 算出来是
0xFFFF x 16 + 0xFFFF = 0xFFFF0 + 0xFFFF = 0x10FFEF超出 1 MB(0x100000)了,这就是当年 PC 上那根被戏称"门"的 A20 地址线的来源——它是段式寻址与 20 位总线的自然产物。
例 2:存储器扩展:位扩展 × 字扩展
手上有 16K x 4 位的芯片,要拼成 64K x 8 位的存储器,需要几片?两个维度各算一次:
位扩展(凑数据宽度):8 位 / 4 位 = 2 片并排,同一地址各出一半数据位
字扩展(凑容量) :64K / 16K = 4 组,各占一段地址,由片选信号选中
总片数 :2 x 4 = 8 片引出线:
片内地址线:log2(16384) = 14 根(16K = 2^14,片内自己寻址)
片选线 :4 根(选 4 组中的哪一组)
总地址线 :log2(65536) = 16 根 = 14 根片内 + 2 根片选译码
数据线 :8 根位扩展管宽度、字扩展管深度,两者是正交的:缺宽度就并排加芯片,缺容量就串起来加片选。
例 3:C 实现——一台 8 位累加器微机的取指-译码-执行
把上面三件事装进一个能跑的模型:存储器是一个 256 字节的数组,指令是"4 位操作码 + 4 位地址",CPU 只有 PC、IR、AC 三个寄存器。
/* micro.c —— 用 C 搭一台 8 位累加器式微机:取指-译码-执行 */
#include <stdio.h>
#include <stdint.h>
#define MEM_SIZE 256
static uint8_t mem[MEM_SIZE]; /* 存储器:这就是"地址空间" */
static uint8_t acc; /* 累加器 AC */
static uint8_t pc; /* 程序计数器 PC */
static int halted;
/* 指令格式:高 4 位操作码 + 低 4 位地址 */
enum { OP_LDA = 0x1, OP_ADD = 0x2, OP_STA = 0x3, OP_JMP = 0x4, OP_HLT = 0xF };
static void load(void)
{
/* 程序从 0x00 起,数据从 0x08 起,两者不打架 */
mem[0] = 0x18; /* LDA 8 : AC <- mem[8] */
mem[1] = 0x29; /* ADD 9 : AC <- AC + mem[9] */
mem[2] = 0x3A; /* STA A : mem[A] <- AC */
mem[3] = 0x1A; /* LDA A : AC <- mem[A] */
mem[4] = 0xF0; /* HLT */
mem[8] = 0x05; /* 操作数 5 */
mem[9] = 0x03; /* 操作数 3 */
}
static void step(void)
{
uint8_t ir = mem[pc++]; /* 取指:PC 自增 */
uint8_t op = ir >> 4; /* 译码:拆出操作码 */
uint8_t ad = ir & 0x0F; /* 拆出地址 */
switch (op) {
case OP_LDA: acc = mem[ad]; break;
case OP_ADD: acc = (uint8_t)(acc + mem[ad]); break;
case OP_STA: mem[ad] = acc; break;
case OP_JMP: pc = ad; break;
case OP_HLT: halted = 1; break;
default: printf("illegal opcode 0x%02X\n", ir); halted = 1;
}
printf("PC=%02X IR=%02X ACC=%02X MEM[%X]=%02X\n", pc, ir, acc, ad, mem[ad]);
}
int main(void)
{
load();
while (!halted) {
step();
}
printf("result: 5 + 3 = %d\n", acc);
printf("model: 8-bit address / 8-bit data -> %d B space\n", MEM_SIZE);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
PC=01 IR=18 ACC=05 MEM[8]=05
PC=02 IR=29 ACC=08 MEM[9]=03
PC=03 IR=3A ACC=08 MEM[A]=08
PC=04 IR=1A ACC=08 MEM[A]=08
PC=05 IR=F0 ACC=08 MEM[0]=18
result: 5 + 3 = 8
model: 8-bit address / 8-bit data -> 256 B space五行输出就是五条指令:前四行把 5 和 3 加起来存进 mem[0xA],第五行的 HLT 让循环结束。取指那一行 uint8_t ir = mem[pc++]; 里已经含了"PC 自增",这就是取指周期的全部内容。
例 4:Python——地址空间、端序与扩展片数验算
# 微机原理:地址空间、段式寻址、端序与总线带宽
def phys(seg, off):
"""8086 实地址 = 段寄存器 x 16 + 偏移"""
return (seg << 4) + off
def ways(p):
"""同一个物理地址能由多少种 段:偏移 表示"""
lo = -(-(p - 0xFFFF) // 16) if p >= 0xFFFF else 0
return p // 16 - max(lo, 0) + 1
print("=== 一、8086 的地址空间与总线带宽 ===")
print(f"地址总线 20 位 -> 2^20 = {2 ** 20} B = {2 ** 20 // 1024 // 1024} MB")
print(f"数据总线 16 位 @ 5 MHz 单周期 -> 2 B x 5 MHz = {2 * 5} MB/s")
print(f"独立编址 I/O:16 位端口号 -> {2 ** 16} 个端口(8 位端口号则 256 个)")
print("\n=== 二、段:偏移 -> 物理地址 ===")
for seg, off in [(0x1000, 0x0234), (0x1000, 0x0020), (0x1002, 0x0000), (0xFFFF, 0xFFFF)]:
print(f"{seg:04X}:{off:04X} -> 0x{phys(seg, off):05X}")
print(f"0x1000:0020 与 0x1002:0000 同一单元:{phys(0x1000, 0x20) == phys(0x1002, 0x0)}")
print(f"段内偏移最大 0xFFFF -> 一个段最大 {0xFFFF + 1} B = 64 KB")
print(f"段寄存器每加 1 前进 16 B -> 同一物理地址最多有 {ways(0x50000)} 种 段:偏移 写法")
print("\n=== 三、端序:0x12345678 在内存里的字节顺序 ===")
v = 0x12345678
le = " ".join(f"{b:02X}" for b in v.to_bytes(4, "little"))
be = " ".join(f"{b:02X}" for b in v.to_bytes(4, "big"))
print(f"小端(x86/ARM 默认)低地址起:{le}")
print(f"大端(网络序/部分 RISC)低地址起:{be}")
print("\n=== 四、存储器扩展:用 16Kx4 位芯片拼 64Kx8 位 ===")
bit_expand = 8 // 4
word_expand = (64 * 1024) // (16 * 1024)
print(f"位扩展 {bit_expand} 片 x 字扩展 {word_expand} 片 = {bit_expand * word_expand} 片")
print(f"片内地址线 log2(16384) = {(16 * 1024).bit_length() - 1} 根,片选线 {word_expand} 根")
print(f"总地址线 {(64 * 1024).bit_length() - 1} 根 + 数据线 8 根")
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
=== 一、8086 的地址空间与总线带宽 ===
地址总线 20 位 -> 2^20 = 1048576 B = 1 MB
数据总线 16 位 @ 5 MHz 单周期 -> 2 B x 5 MHz = 10 MB/s
独立编址 I/O:16 位端口号 -> 65536 个端口(8 位端口号则 256 个)
=== 二、段:偏移 -> 物理地址 ===
1000:0234 -> 0x10234
1000:0020 -> 0x10020
1002:0000 -> 0x10020
FFFF:FFFF -> 0x10FFEF
0x1000:0020 与 0x1002:0000 同一单元:True
段内偏移最大 0xFFFF -> 一个段最大 65536 B = 64 KB
段寄存器每加 1 前进 16 B -> 同一物理地址最多有 4096 种 段:偏移 写法
=== 三、端序:0x12345678 在内存里的字节顺序 ===
小端(x86/ARM 默认)低地址起:78 56 34 12
大端(网络序/部分 RISC)低地址起:12 34 56 78
=== 四、存储器扩展:用 16Kx4 位芯片拼 64Kx8 位 ===
位扩展 2 片 x 字扩展 4 片 = 8 片
片内地址线 log2(16384) = 14 根,片选线 4 根
总地址线 16 根 + 数据线 8 根考点
- 地址总线位数与容量:
n根地址线 →2^n字节。20 位 → 1 MB,16 位 → 64 KB,32 位 → 4 GB。这个换算必须能秒答。 - 数据总线位数与字长:8086 是 16 位数据总线,但内存按字节编址,所以它"一个字 = 2 字节";一次总线周期搬 2 字节。
- 端序:小端 = 低地址放低字节(
0x12345678→78 56 34 12)。网络字节序是大端,写通信代码时htons()/htonl()就是干这个的。 - 统一编址 vs 独立编址:ARM 是统一编址(用指针读写外设),x86 是独立编址(用
IN/OUT)。别把两套混着答。 - 段式寻址三个易错点:① 公式是
段 x 16 + 偏移,不是段 + 偏移;② 一个段最大 64 KB,不是 1 MB;③ 同一物理地址有多种段:偏移写法,所以比较两个逻辑地址是否指向同一处,必须先换算成物理地址。 - 存储器扩展:位扩展看数据宽度(并排),字扩展看容量(串联 + 片选),两者相乘。片内地址线 =
log2(单片容量),剩下的高位接片选译码。 - 常见陷阱:把"地址线根数"与"地址空间大小"混为一谈;把 8086 的
0xFFFF:FFFF直接当成 1 MB 边界(它其实越界到0x10FFEF)。
小结
- 微机的四个部件(CPU、存储器、接口、总线)挂在三总线上,总线宽度决定了容量与吞吐。
- 地址总线定容量、数据总线定搬运量、控制总线定时机——三条因果关系贯穿全课。
- 端序决定多字节数据怎么摆,小端低地址放低字节;跨设备通信必须统一端序。
- 编址方式两分:ARM 式统一编址(指针访问外设)、x86 式独立编址(专用 I/O 指令)。
- 段式寻址用一个乘 16 的左移,把 16 位寄存器扩展成 20 位地址,代价是段可重叠、地址有多种写法。
回到主线:这是分支课 embed 的第一篇,它把主线的三层接口缝合起来——器件给了开关(silicon)、门给了算与存(circuit)、组成原理给了结构(arch),微机原理给的是"把这些部件接起来的那几根线"。它直接依赖 总线的分类与定时 与 I/O 系统,而它自己要交出去的东西是"端口地址"——下一章的中断控制器、再往后的每一个外设寄存器,都要用这一篇的地址换算去定位。
这一章只答了"部件怎么连",没答"CPU 怎么被外部事件打断"。下一章补上这台机器最要紧的一根线——中断请求线。
下一篇:中断系统与定时器
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。