Appearance
在 FPGA 上跑通一个自己的电路
关于
runnable:本站线上不提供代码运行,本机也没有 Verilog 综合/仿真工具(无 iverilog、无 Vivado/Quartus)。所以本篇的 Verilog 是逐行人工审查的,行为正确性用随文的 Python 模型实跑验算。这一篇的重点也不在代码,而在**"设计 → 综合 → 布线 → 上板"这条链路上每一站到底发生了什么**。
概念
前一篇我们学会了用 Verilog 描述电路。但描述完,代码还躺在文本文件里——它怎么变成一颗真的在跳动的芯片?
FPGA(Field-Programmable Gate Array,现场可编程门阵列)的核心思想只有一句话:
不用去工厂流片,在自己桌上就能把硬件"重新接线"。
它和 ASIC 的区别,是这一篇的出发点:
| FPGA | ASIC(专用集成电路) | |
|---|---|---|
| 逻辑实现 | 查表(LUT)现场配置 | 晶体管真的长成那个逻辑 |
| 可改吗 | 随时重配(秒级,"现场可编程") | 流片后不能改(改一次重做一版掩模) |
| 首次成本 | 几百到几千元(买片子) | 几百万到几亿元(掩模 + EDA) |
| 单品成本 | 高(同功能面积大 10~40 倍) | 极低(量产后) |
| 速度 | 慢(多级 LUT + 绕线延迟) | 快 |
| 功耗 | 高(可编程开销量级 10×) | 低 |
| 适合 | 小批量、原型验证、协议多变 | 大批量、极致性能/功耗 |
一条判据(业界共识):产量小于约 10 万片,选 FPGA 更划算。 这就是为什么 CPU/GPU 是 ASIC,而芯片流片前的原型验证平台一定是 FPGA——连 Intel、AMD 自己都这么干。
本篇在主线上的位置:L1 前面 11 篇把电路设计完了,这一篇回答"设计完了怎么变成硬件"。它是 L1 的实践出口,同时也是L2 的前奏——L2 会用同一套 HDL 去描述一台 CPU 的数据通路与控制器。
原理
一、用查找表(LUT)实现任意逻辑
问题:一片 FPGA 出厂时,里面的连线什么逻辑都不是。它是怎么"变成"与门、加法器、甚至 CPU 的?
答案藏在 LUT 里。
LUT(Look-Up Table,查找表):一个
关键洞察:
以 4 输入 LUT(LUT4,主流架构的基本单元) 为例:
┌─────────────────────────────┐
in0 ──►│ │
in1 ──►│ 16 × 1 位 SRAM │──┐
in2 ──►│ (存 16 行真值表的答案) │ │
in3 ──►│ │ ├──► 16 选 1 ──► out
└─────────────────────────────┘ │ MUX
│
{in3,in2,in1,in0} 当作地址 ────────┘于是神奇的结论出现了:
| 想实现 | 只需把 16 位内容写成 |
|---|---|
y = a & b | 真值表(a,b 任意组合,其余输入无所谓,取最小项) |
y = a ^ b ^ c ^ d | 异或真值表(8 个 1) |
| 任意 4 变量函数 | 对应的 16 位真值表 |
所以 4 输入 LUT 能实现 4 变量的任意组合逻辑——不需要"造一个与门",而是把真值表烧进去。这就是"现场可编程"的字面意思。
那 5 变量的函数怎么办? 拆成两级:一个 LUT4 算低位,输出和第五个变量再进一个 LUT。代价是多一级延迟——这直接解释了后面"为什么 FPGA 比 ASIC 慢"。
LUT 的延迟是"常数"的:不管实现与门还是异或,穿过一个 LUT 的时间几乎一样(都是"查一次表")。这个性质在时序分析里非常友好,也是 16 篇里"关键路径 = 级数 × 单级延迟"的根据。
二、FPGA 的四种资源
一片 FPGA 内部不是均匀的,而是几种功能块按行/列摆放,中间夹着可编程互连:
| 资源 | 名称 | 装什么 |
|---|---|---|
| CLB / LE | 逻辑块(Configurable Logic Block / Logic Element) | 几个 LUT + 几个触发器 + 进位链。组合逻辑和时序都在这 |
| IOB | 输入输出块 | 引脚驱动、上下拉、电平标准(LVCMOS/LVDS)、DDR |
| BRAM | 块 RAM | 几 KB~几十 KB 的双口 SRAM,用作 FIFO、缓存、数据缓冲 |
| DSP | 乘法器片 | 硬件 |
外加一整层"可编程互连":横竖的导线 + 大量的开关盒/连接盒,由配置位决定"哪根线接哪根线"。布线的资源往往比逻辑资源更紧张——这是 FPGA 设计的常识。
一张典型 CLB 的内部:
┌─────────────────── CLB ───────────────────┐
│ 真值表/配置位 │
in ─►│ ┌────┐ ┌────┐ │
│ │LUT4│ │LUT4│ … (通常 4~6 个 LUT) │
│ └─┬──┘ └─┬──┘ │
│ │ │ │
│ ▼ ▼ 存放结果 │
│ ┌────┐ ┌────┐ │
│ │ FF │ │ FF │ ← 触发器(可选"过不过 FF") │
│ └─┬──┘ └─┬──┘ │
│ └────┬───┘ 进位链 ─────────────┐ │
└─────────┼───────────────────────────────┼───┘
▼ ▼
输出到互连 级联给邻居注意最后那个"进位链(carry chain)":这是 FPGA 里专门为加法器/计数器铺的一条"快车道"——进位不绕普通互连,而是直接连到相邻逻辑块。12 篇讲的进位延迟,在 FPGA 里靠这条专用通路压下去。
三、从代码到比特流:五站链路
写好 Verilog 到板子上跑起来,中间要走五站:
①设计入口 ②综合 ③映射
source.v → 门级网表(.v) → 逻辑块+触发器网表
│
▼
⑤下载配置 ④布局布线(P&R)
.bit 文件 ← 位置 + 走线 + 时序报告| 站 | 做什么 | 关键产物 |
|---|---|---|
| ① 设计入口 | HDL 代码 + 约束文件(引脚、时钟频率) | .v + .xdc/.sdc/.qsf |
| ② 综合(Synthesis) | "HDL → 门":把 always/assign 翻译成与或非 + 触发器 | 门级网表 .v |
| ③ 映射(Mapping) | "门 → LUT":把逻辑覆盖成 LUT + 触发器 | 块级网表 |
| ④ 布局布线(Place & Route) | "块 → 具体位置 + 具体走线",并做时序分析 | 位置文件 + 时序报告 |
| ⑤ 生成比特流并下载 | 把配置位打包,通过 JTAG 烧进 FPGA | .bit / .bin |
整条链路上最慢、最不可控的是第 ④ 步。原因很直白:同样的逻辑,摆在哪里、线绕多远,延迟差好几倍。EDA 工具要在"找到合法解"和"找到够快的解"之间反复迭代。
这解释了 16 篇那个
slack从哪来:综合出来的电路在布线后才知道真实延迟,布局布线报告里的时序余量(slack)才是"能不能跑在目标频率"的最终裁决。
四、时序收敛(timing closure)
Fmax(最高工作频率)由关键路径决定——12/16 篇的道理,在 FPGA 上表现为:
注意这里多出来的一项是
**"时序收敛"**是指:反复改代码/加约束/改流水线,直到报告里所有路径 slack ≥ 0。
三条最常用的收敛手段(都能从 16 篇的公式推出来):
| 手段 | 打的是哪一项 | 代价 |
|---|---|---|
| 插流水线寄存器(pipelining) | 把长组合逻辑切成几段,每段 | 吞吐↑、单次延迟↑(16 篇结论) |
| 复制逻辑(replication) | 减少扇出,缩短走线 | 面积↑、功耗↑ |
| 改约束/改架构(多周期路径、降频) | 直接放宽 | 变慢,或改变功能语义 |
| 加寄存器打拍(retiming) | 移动触发器位置,平衡前后段延迟 | 工具自动做,可能改不动 |
"降频救不了 hold" 这条在 FPGA 上同样成立(16 篇)。保持违例必须靠加延迟单元(buffer)或改触发器,这是硬件里少数"改不掉的物理事实"。
再补一句现实的:FPGA 上最容易出错的一类时序问题是"跨时钟域"。板上往往有多个时钟(50 MHz 晶振、100 MHz 差分、摄像头像素时钟……),它们之间传数据必须过同步器(两级触发器打拍),否则亚稳态迟早发生——这正是 16 篇最"工程"的那部分。
五、工具链与最小上板流程
| 环节 | 免费工具 | 说明 |
|---|---|---|
| 编辑 | VS Code | 纯文本 |
| 仿真 | Icarus Verilog(iverilog)+ GTKWave | 命令行、免费、看得见波形 |
| 综合/布线 | 厂商工具:AMD(Xilinx) Vivado / Intel Quartus / 高云 Gowin EDA | 各家只支持自家芯片 |
| 下载 | 同上(含 JTAG 下载器) | 生成 .bit 直接烧 |
最小上板流程(以"LED 心跳灯"为例):
- 写代码:一个计数器,把 50 MHz 分频到 1 Hz,驱动 LED;
- 写约束:告诉工具"哪个信号接哪个引脚、输入时钟是多少 MHz";
- 综合 + 布线:看时序报告(
slack是否 ≥ 0)、看资源占用(多少 LUT/FF); - 下载:JTAG 烧
.bit; - 上板验证:LED 是不是每秒闪一次。
第 3 步的时序报告是这一篇真正的产出。代码跑通只证明功能对,报告跑绿才证明硬件能跑。
示例
例 1:一个 4 输入 LUT 到底能干什么?(Python 验算)
任务:验证"4 输入 LUT 可以实现任意 4 变量函数"——穷举全部
python
# 4 输入 LUT 的能力边界:穷举所有 4 变量函数
def eval_truth_table(tt, inputs):
"""tt: 16 位整数,第 i 位 = 输入组合 i 的输出"""
idx = (inputs[3] << 3) | (inputs[2] << 2) | (inputs[1] << 1) | inputs[0]
return (tt >> idx) & 1
# 任取几个函数,反推其 LUT 内容
def build_tt(func):
tt = 0
for i in range(16):
in0, in1, in2, in3 = i & 1, (i >> 1) & 1, (i >> 2) & 1, (i >> 3) & 1
if func(in0, in1, in2, in3):
tt |= (1 << i)
return tt
funcs = {
"与 a&b": lambda a, b, c, d: a & b,
"四变量异或": lambda a, b, c, d: a ^ b ^ c ^ d,
"多数表决 maj3": lambda a, b, c, d: 1 if (a + b + c) >= 2 else 0,
"任意怪函数 f": lambda a, b, c, d: 1 if (a, b, c, d) in
{(0, 1, 1, 0), (1, 0, 0, 1), (1, 1, 1, 0), (0, 0, 1, 1)} else 0,
}
for name, f in funcs.items():
tt = build_tt(f)
# 回读验证:用 LUT 方式重算 16 行,必须与原函数完全一致
ok = all(eval_truth_table(tt, [(i & 1), (i >> 1) & 1, (i >> 2) & 1, (i >> 3) & 1])
== (1 if f((i & 1), (i >> 1) & 1, (i >> 2) & 1, (i >> 3) & 1) else 0)
for i in range(16))
print(f"{name:16s} LUT内容 = 0x{tt:04X} ({tt:016b}) 回读一致 = {ok}")
# 能力边界:所有 2^16 种 LUT 内容与所有 4 变量函数一一对应
print("\n4 输入 LUT 的不同内容数 =", 2 ** 16, "= 4 变量函数总数", 2 ** (2 ** 4))预期输出:
与 a&b LUT内容 = 0x8888 (1000100010001000) 回读一致 = True
四变量异或 LUT内容 = 0x6996 (0110100110010110) 回读一致 = True
多数表决 maj3 LUT内容 = 0xE8E0 (1110100011100000) 回读一致 = True
任意怪函数 f LUT内容 = 0x4806 (0100100000000110) 回读一致 = True
4 输入 LUT 的不同内容数 = 65536 = 4 变量函数总数 65536结论:
- LUT 的"程序"就是真值表本身,16 位一字不差地对应一个 4 变量函数;
- 内容数 = 函数数 = 65536,这就是"任意函数都能实现"的严格含义;
- 四变量异或的 LUT 内容是
0x6996——一个很漂亮的对称模式,记住它有助于理解 LUT 的"查表"本质。
注意
a&b的 LUT 内容里有一半的位是冗余的:因为函数只依赖 2 个变量,另外 4 种组合(变化)的值是"无关项",工具会填 0 或 1 都行。这也是为什么有些 4 输入函数会浪费 LUT 资源。
例 2:LED 心跳灯 —— 分频 + 计数器(Verilog)
目标:板载 50 MHz 时钟 → 产生 1 Hz 的 LED 闪烁。
思路(直接复用 14 篇的分频结论):
verilog
module heartbeat (
input wire clk, // 50 MHz 板载时钟
input wire rst_n, // 低有效复位
output reg led // LED 输出
);
// 50_000_000 / 2 / 1 = 25_000_000 - 1
// 需要 25 位寄存器(2^25 = 33_554_432 > 25_000_000 > 2^24)
localparam integer HALF = 25_000_000 - 1;
reg [24:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 25'd0;
led <= 1'b0;
end else if (cnt == HALF) begin
cnt <= 25'd0;
led <= ~led; // 到点翻转一次:翻转间隔 0.5 s → 闪烁周期 1 s
end else begin
cnt <= cnt + 1'b1;
end
end
endmodule逐行审查要点:
| 行 | 为什么这么写 |
|---|---|
localparam integer HALF = 25_000_000 - 1 | 计数到 HALF 需要 25 位(reg [24:0] |
cnt == HALF 而不是 cnt == 25_000_000 | 从 0 数到 HALF 正好 HALF+1 = 2.5×10⁷ 个时钟,差一就偏 |
led <= ~led | 每 2.5×10⁷ 个时钟翻转 → 闪烁周期 = 2 × 0.5 s = 1 s,即 1 Hz |
always @(posedge clk ...) 内全用 <= | 非阻塞赋值(20 篇铁律):时序逻辑必须用 <= |
negedge rst_n 异步复位 | 对应触发器的 CLR 端(13 篇) |
这里藏着最经典的"上板不亮/怎么这么慢"陷阱。很多人这样算:"HALF = 50_000_000 - 1"——这样每 1 秒才翻转一次,一个完整明暗周期变成 2 秒,看到的闪烁频率只有 0.5 Hz,肉眼会觉得"怎么这么慢",而不是"写错了"。
正确做法是先想清楚"要的是闪烁频率还是翻转频率":
| 想要的 | 换算 |
|---|---|
| 闪烁频率 1 Hz(每秒亮→暗或暗→亮一次) | 半周期 0.5 s ⇒ 翻转间隔 0.5 s ⇒ HALF + 1 = 25×10⁶ ⇒ HALF = 25_000_000 - 1 |
| 每 1 秒翻转一次 | HALF + 1 = 50×10⁶ ⇒ 闪烁周期 2 s(0.5 Hz) |
两者只差一个因子 2,但肉眼看到的是一倍的差别——这就是新手第一个坑。记住只一句话:led <= ~led 是"翻转",翻转两次才算一个"闪烁周期"。
用 Python 模拟这段逻辑跑一遍,确认翻转时刻和周期:
python
# 行为级模拟:低频下验证"翻转时刻"与"周期"的对应关系
def simulate(clk_hz, half_period_cnt, ticks):
"""ticks: 模拟多少个时钟;返回 (翻转次数, 每个翻转发生的时钟序号)"""
cnt = 0
led = 0
toggles = []
for t in range(ticks):
if cnt == half_period_cnt:
cnt = 0
led ^= 1
toggles.append(t)
else:
cnt += 1
return toggles
# 用一个"缩小版"参数验证逻辑:HALF = 4(即每 5 个时钟翻转一次)
to = simulate(50, 4, 50)
print("HALF=4 时的翻转时钟序号:", to[:6])
print("相邻翻转间隔:", [to[i + 1] - to[i] for i in range(len(to) - 1)][:5])
print("→ 间隔恒为 HALF+1 =", 5, ";完整闪烁周期 =", 2 * 5, "个时钟")
# 真实参数换算
CLK, HALF = 50_000_000, 25_000_000 - 1
print(f"\n真实参数: clk={CLK/1e6:.0f} MHz, HALF={HALF}")
print(f" 翻转间隔 = {HALF + 1} 个时钟 = {(HALF + 1) / CLK:.3f} s")
print(f" 闪烁周期 = {2 * (HALF + 1)} 个时钟 = {2 * (HALF + 1) / CLK:.3f} s")
print(f" 闪烁频率 = {CLK / (2 * (HALF + 1)):.3f} Hz")
print(f"\n若想让闪烁频率 = 1 Hz,HALF 应为 {CLK // 2 - 1}(半周期 0.5 s)")
print(f" 验证: {CLK / (2 * (CLK // 2)):.3f} Hz")预期输出:
HALF=4 时的翻转时钟序号: [4, 9, 14, 19, 24, 29]
相邻翻转间隔: [5, 5, 5, 5, 5]
→ 间隔恒为 HALF+1 = 5 ;完整闪烁周期 = 10 个时钟
真实参数: clk=50 MHz, HALF=24999999
翻转间隔 = 25000000 个时钟 = 0.500 s
闪烁周期 = 50000000 个时钟 = 1.000 s
闪烁频率 = 1.000 Hz
若想让闪烁频率 = 1 Hz,HALF 应为 24999999(半周期 0.5 s)
验证: 1.000 Hz结论:
- 翻转间隔恒为
HALF + 1个时钟,与HALF的取值无关(仿真里是 5,真实是 2.5×10⁷); - 本例
HALF = 2.5\times10^7 - 1恰好得到 1 Hz 的闪烁频率——注意"闪烁频率"= ,分母那个 2 就是"翻转"与"周期"的差别; - 位数校验:
,25 位够用;若HALF取 ,就要 26 位了。
例 3:资源占用与时序——把账算清
任务:上板前的两份必看报告,资源报告和时序报告,用数字把它们读明白。
场景:一个 32 位计数器 + 一个 4 输入 LUT 实现的比较器,跑在目标 100 MHz(
| 报告项 | 数值 | 读法 |
|---|---|---|
| LUT 占用 | 32(计数器)+ 8(比较器)= 40 个 | 一片小 FPGA 有几千个 LUT,绰绰有余 |
| FF 占用 | 32 个 | 与 reg [31:0] 一一对应(一个 bit 一个触发器,14 篇) |
| 关键路径 | 计数器最高位 | 进位链 + 一级比较逻辑 |
| 该路径延迟 | 走线占了一半以上 | |
| 约束周期 | 10.0 ns | 100 MHz |
| slack | $10.0 - 4.2 = $ +5.8 ns | ≥ 0,时序收敛 |
| 若走线变成 8.0 ns | slack = 0,刚好卡边,工具可能要重布 |
python
# 时序账:FPGA 路径延迟里"走线"占了多大比例
def path_delay(tco, lut_stages, t_lut, t_route, tsu):
return tco + lut_stages * t_lut + t_route + tsu
tco, t_lut, tsu = 0.5, 0.4, 0.3
print("=== 综合布线后的关键路径账(单位 ns)===")
for name, stages, route in [("布线顺利", 3, 2.2),
("布线紧张", 3, 5.0),
("布线很糟", 3, 8.0)]:
d = path_delay(tco, stages, t_lut, route, tsu)
slack_100 = 10.0 - d # 100 MHz → T = 10 ns
fmax = 1e3 / d # ns → MHz
route_ratio = route / d * 100
print(f"{name}: 延迟={d:.1f} ns 走线占比={route_ratio:.0f}% "
f"slack@100MHz={slack_100:+.1f} ns Fmax={fmax:.1f} MHz")
print("\n=== 插流水线寄存器后(把组合逻辑切成 2 段)===")
# 切成两段后每段 LUT 级数减半(3 → 2,取整),走线也随之分成两段
for name, stages, route in [("布线顺利", 2, 1.1), ("布线紧张", 2, 2.5)]:
d = path_delay(tco, stages, t_lut, route, tsu)
print(f"{name}: 单段延迟={d:.1f} ns → Fmax={1e3/d:.1f} MHz "
f"(吞吐↑,但整条链路多了 1 个周期延迟)")预期输出:
=== 综合布线后的关键路径账(单位 ns)===
布线顺利: 延迟=4.2 ns 走线占比=52% slack@100MHz=+5.8 ns Fmax=238.1 MHz
布线紧张: 延迟=7.0 ns 走线占比=71% slack@100MHz=+3.0 ns Fmax=142.9 MHz
布线很糟: 延迟=10.0 ns 走线占比=80% slack@100MHz=+0.0 ns Fmax=100.0 MHz
=== 插流水线寄存器后(把组合逻辑切成 2 段)===
布线顺利: 单段延迟=2.4 ns → Fmax=416.7 MHz (吞吐↑,但整条链路多了 1 个周期延迟)
布线紧张: 单段延迟=3.8 ns → Fmax=263.2 MHz (吞吐↑,但整条链路多了 1 个周期延迟)结论(这一篇最该带走的三句话):
- 走线延迟占关键路径的 50%~80%——这是 FPGA 与 ASIC 速度差的主因,也解释了 16 篇里"为什么插流水线寄存器有效";
- 同样的代码,布线结果不同,Fmax 能差 2 倍以上——这正是"时序收敛"要靠反复迭代的原因;
- 插流水线后单段延迟从 4.2 ns 降到 2.4 ns,Fmax 从 238 → 417 MHz(1.75×)——代价是整条链路的延迟多了一个时钟周期,与 16 篇"吞吐↑、延迟↑"完全一致。
考点
这一段不是"考试要考",而是"上板别踩"。 FPGA 属实践内容,不与 408 直接对应,但它把前面 11 篇的结论都用了一遍。
考点
1. FPGA 是什么(一句话)
- "一堆可编程逻辑块 + 一张可编程连线网"。可编程的手段是查找表(LUT),不是"造一个与门"。
- 与 ASIC 的分界:小批量/原型/多协议 → FPGA;大批量/极致性能功耗 → ASIC。经验阈值约 10 万片。
2. LUT 必须理解到位
输入 LUT = 位存储器 + 选 1 MUX,内容就是真值表。- 4 输入 LUT 可实现任意 4 变量组合逻辑,内容数 = 函数数 =
。 - LUT 延迟与实现什么函数无关(都查一次表)→ 这让时序估算变得简单。
输入 LUT 用起来"看起来浪费":只用 2 个变量时,另外 行是无关项。
3. 四种资源各管什么
- CLB/LE:LUT + 触发器 + 进位链(12 篇的加法器延迟靠它压)。
- IOB:引脚、电平标准、DDR。
- BRAM:块状双口 SRAM,当 FIFO/缓冲。
- DSP:硬件乘加,一个时钟一次 MAC。
- 别忘了"可编程互连"本身也是资源,而且常常是瓶颈。
4. 从代码到板子的五站
综合 → 映射 → 布局布线 → 比特流 → 下载。
- 综合是 "HDL → 门";映射是 "门 → LUT";布局布线是 "块 → 位置+走线"。
- 只有布局布线后有真实时序报告——代码能仿真通过 ≠ 能在目标频率跑。
5. 时序收敛:怎么判断、怎么救
- 判据只有一条:所有路径
slack ≥ 0。 slack < 0的四条救法:插流水线寄存器、复制逻辑降扇出、retiming、放宽约束/降频。- "降频救不了 hold"(16 篇)——保持违例必须加延迟单元或换触发器。
- 跨时钟域必过两级同步器,否则亚稳态(16 篇 MTBF 公式)迟早兑现。
6. 与新手的三个最像"灵异事件"的坑
- "LED 闪得比想象中慢/快一倍" → 混淆了 翻转频率 与 闪烁周期(差 2 倍)。
- "综合报告说资源够,但布线失败/时序不收敛" → 布线资源与走线延迟才是硬约束。
- "仿真全对,上板偶发错" → 多半是跨时钟域没同步,或约束文件里时钟周期填错(工具按错的
算 slack,永远"绿")。
小结
- FPGA = 可编程逻辑块 + 可编程互连,靠 LUT 存真值表实现任意逻辑;ASIC 是把晶体管真的长成那个逻辑。小批量选 FPGA,大批量选 ASIC。
- 4 输入 LUT = 16 位内容 + 16 选 1 MUX,能实现任意 4 变量函数(65536 种内容 ↔ 65536 个函数);LUT 延迟与函数无关。
- 四种资源:CLB(LUT+FF+进位链)、IOB、BRAM、DSP;别忘了互连本身也是资源。
- 五站链路:设计 → 综合(HDL→门) → 映射(门→LUT) → 布局布线(位置+走线+时序报告) → 比特流下载。报告在第四站才出来。
- 时序收敛的唯一判据是
slack ≥ 0;走线延迟占关键路径 50%~80%,是 FPGA 慢的主因,也是"插流水线为什么有效"的答案。 - 上板三坑:翻转/周期差 2 倍、布线资源与时序而非 LUT 数才是瓶颈、跨时钟域必须两级同步。
回到主线:L1(电 · 电路与数字逻辑)到此彻底收口。
回头看一下这一层从第一句到最后一句的逻辑:
硅器件(L0)
│ 给它合适的电压,它就能当开关
▼
门 ──→ 组合逻辑(算)──→ 加法器 ──→ ALU
│
│ 加一条反馈回路
▼
触发器 ──→ 寄存器 ──→ 计数器/移位 ──→ 状态机(有行为)
│
│ 算清"能不能跑、跑多快"
▼
时序分析(slack / Fmax / 亚稳态)
│
│ 用文本描述、落到芯片上
▼
Verilog ──→ FPGA(这一篇)L1 回答了什么问题:"L0 的硅器件,怎么变成能算、能记、能按规则行动、并且真能跑起来的电路。"
还缺什么:把这些零件按"指令"组织起来。
现在的电路能算、能记、能跑,但它自己不知道要干什么。谁告诉它"读内存、算加法、写回、跳到下一条"?这些动作怎么排成一个循环?Cache、中断、I/O 挂在哪里?
这就是 L2【机器 · 计算机组成原理】。 而在写 L2 之前,还有一个更基础的问题要先回答清楚:
一台机器"能干什么",是由它的"指令"定义的。那么指令长什么样?放在哪?一条指令里为什么要分成好几个域?
这是 L3【语言 · 汇编与 C】的第一篇要回答的——也是整条链条从"硬件"转向"人"的第一个转弯。
下一篇:指令集与寄存器组织
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。