Appearance
寄存器、计数器与移位寄存器
概念
上一篇我们做到了"记住一位"。但 CPU 需要的是记住一个字:
- PC:记住 32 位地址
- IR:记住 32 位指令
- 通用寄存器:记住 32 × 64 位(32 个寄存器,每个 64 位)
- 状态寄存器:记住 1 个标志组(ZF/SF/CF/OF)
办法简单到只有一句话:
这就是 寄存器(register)。而在此基础上加上反馈与计数逻辑,就得到能自己循环数数的计数器(counter);把触发器串起来,就得到能一位位搬运数据的移位寄存器(shift register)。这三样东西,构成了 CPU 里除 ALU 之外的全部"有状态"部件。
本层(L1 电路)在这里回答了上一层(L2 组成原理)的核心问题:L2 里说"PC 加 1 后写回 PC"、"寄存器堆两个读端口一个写端口",这些记不住就无从谈起的东西,底层全是"触发器 + 译码器 + MUX"。
原理
一、并行寄存器:n 个触发器 + 一根时钟
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
D3 ───→│ D │ │ D │ │ D │ │ D │ ← 共 4 个边沿 D 触发器
│ FF │ │ FF │ │ FF │ │ FF │
D2 ───→└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
... │ │ │ │
┌──┴───────┴───────┴───────┴──┐
D0 ───→│ 共用时钟 CP │ ← 所有触发器"同时"更新
└────────────────────────────┘
│ │ │ │
Q3 Q2 Q1 Q0要点:
| 要点 | 说明 |
|---|---|
| 共用时钟 | 所有位在同一边沿更新,这就是"并行寄存器"的含义(同步更新) |
| 位宽可任意 | 32 位就是 32 个触发器并排 |
| 输入来源 | 一般由 MUX 选(数据来自 ALU 结果 / 内存 / 立即数) |
带使能的寄存器(load enable):实际寄存器不会每个时钟都乱写,需要一个 LOAD 或 EN 信号:
┌──────────┐
D_i ────────┬───→│ D Q │───→ Q_i
│ │ FF │
Q_i (回读) ─┴──→│ ↑ │
↑ └──┬───────┘
LOAD ──┬─────┘ │
│ ┌───┐ │
└─→│AND│←────┘ CP
└───┘
⇒ 每位的 D 端是一个二选一 MUX:LOAD=1 取新值,LOAD=0 保持旧值关键实现细节:"保持"不是让时钟停下来,而是在
三态输出:寄存器堆的输出要能"挂到公共总线上",靠三态门:
| 控制 | 输出 |
|---|---|
| 输出正常电平(驱动总线) | |
| 高阻 |
为什么需要三态:多条数据线要接到同一根物理总线上,任何时刻只能有一个在驱动;其余全部置高阻。这是总线(bus)存在的物理前提。
二、寄存器堆: 个寄存器怎么"按地址取"
CPU 里有一小撮寄存器(通常 32 个),要能同时读两个、写一个。这东西叫寄存器堆(register file):
┌─────────────────────────────┐
读地址1 ───→ │ 5-32 译码 → 32 选 1 MUX │───→ 读出数据 1
(5 位) │ │
│ ┌──────────────┐ │
读地址2 ───→ │ │ 32 × 64 位 │ │───→ 读出数据 2
(5 位) │ │ 触发器阵列 │ │
│ └──────┬───────┘ │
写地址 ───→ │ 5-32 译码 ─┘(写使能) │
(5 位) │ │
写数据 ───→ │───→ 各寄存器 D 端 │
(64 位) │ │
写使能 ───→ │ (时钟沿写入) │
└─────────────────────────────┘三个结构要点(全部是前两篇的旧零件):
| 部件 | 用到的旧知识 |
|---|---|
| 读: | circuit/11-combinational.md 的数据选择器 |
| 写: | circuit/11-combinational.md 的译码器 |
| 存储: | circuit/13-flipflop.md 的 D 触发器 |
代价推算(32 个 × 64 位):
| 项 | 数量 |
|---|---|
| 存储位 | |
| 读端口 MUX | |
| 写译码 | 1 个 5-32 译码器 |
"双端口读"是寄存器堆的性能关键:一条加法指令要同时取两个源操作数,如果只有一个读端口就得串行读两次,慢一倍。这就是"寄存器堆 = 多端口小内存"这个说法的由来。
三、移位寄存器:把触发器串起来
把前一级的输出接到后一级的输入,同一个时钟驱动:
串行输入 SI ──→┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ FF3 │──→│ FF2 │──→│ FF1 │──→│ FF0 │──→ 串行输出 SO
时钟 CP ─────→└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
Q3 Q2 Q1 Q0
└─────────┴─────────┴─────────┘
↓
并行输出四种组合(输入端与输出端各可"串"可"并"):
| 类型 | 输入 | 输出 | 用途 |
|---|---|---|---|
| 串入串出(SISO) | 1 位 | 1 位 | 延迟线(延迟 |
| 串入并出(SIPO) | 1 位 | 串并转换(接收串行数据) | |
| 并入串出(PISO) | 1 位 | 并串转换(发送串行数据) | |
| 并入并出(PIPO) | 就是普通寄存器 |
移位寄存器的三个经典用途:
| 用途 | 原理 |
|---|---|
| 乘 / 除 | 左移一位 = |
| 串并转换 | 串行通信的核心:UART 就是"起始位 + 数据位 + 停止位"的移位寄存器 |
| 延迟 / 同步 | 输入信号延后 |
双向移位寄存器 74LS194(4 位,经典器件):
| 功能 | ||
|---|---|---|
| 0 | 0 | 保持 |
| 0 | 1 | 右移( |
| 1 | 0 | 左移( |
| 1 | 1 | 并行置数 |
外加
四、计数器:会自己数数的寄存器
计数器 = 寄存器 + 反馈逻辑,让状态按固定顺序循环。
异步(行波)计数器
把触发器串成链,前一级的
CP ──→┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ FF0 │→─│ FF1 │→─│ FF2 │→─│ FF3 │
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
Q0 Q1 Q2 Q3
(÷2) (÷4) (÷8) (÷16)优点:结构极简,只有一根时钟线在驱动。 致命缺陷:进位像水波一样一级级传,每一位的翻转都要等前一位翻转完成。
位数越多,最高频率越低。
同步计数器
所有触发器共用时钟,进位用组合逻辑提前算好喂给各触发器的
| 触发器 | 翻转条件 |
|---|---|
| 第 0 位 | 每个时钟都翻( |
| 第 1 位 | 当 |
| 第 2 位 | 当 |
| …… | …… |
| 第 | 当 |
关键对比:同步计数器的 circuit/12-alu.md)。
模 N 计数器:两种设计手法
| 手法 | 做法 | 特点 |
|---|---|---|
| 复位法(异步清零) | 计数到 | 简单,但 |
| 置数法(同步置数) | 计数到 | 无毛刺,是推荐做法 |
考试常考的陷阱:用异步清零端做模
,实际会数到 再回 0,但 这个状态会在一瞬间出现(宽度约等于门延迟),如果这个状态被当作别的电路的输入,就会出错。
环形与约翰逊计数器
| 类型 | 反馈接法 | 状态数( | 特点 |
|---|---|---|---|
| 环形计数器 | 末位 | 独热码(只有一个 1)——检测译码只需一根线 | |
| 约翰逊(扭环)计数器 | 末位取反 | 状态利用率翻倍,仍是"相邻只变一位"的格雷特性 |
格雷码计数器:让状态按格雷码变化(每次只有一位翻转),从根本上消除多路同时切换造成的毛刺——这是 circuit/11-combinational.md 里"用格雷码编码地址"那条经验的落地。
分频
| 位 | 分频比 | 占空比 |
|---|---|---|
| 50% | ||
| 50% | ||
| 50%(二进制计数器的优点) |
注意:异步计数器每位都是 50% 占空比;但如果用"模
复位法"就不是 50% 了(因为不是满量程),需要额外逻辑整形。
五、常用 MSI 器件速查
| 器件 | 功能 | 关键引脚 |
|---|---|---|
| 74LS194 | 4 位双向移位寄存器 | |
| 74LS163 | 4 位同步二进制计数器 | |
| 74LS161 | 4 位二进制计数器 | |
| 74LS160 | 4 位十进制(模 10)计数器 | 同 161,但只数 0~9 |
| 74LS193 | 4 位可逆(加/减)计数器 | |
| 74LS373 | 8 位 D 锁存器(三态) | |
| 74LS374 | 8 位 D 触发器(三态) | 边沿触发版 |
161 与 163 的唯一区别:161 的清零是异步的,163 的清零是同步的。设计模
时选谁,直接决定电路是否无毛刺。这是最经典的对比考点。
示例
例 1:4 位并行寄存器的写使能实现(逐项验证)
设计带
LOAD的 4 位寄存器,验证"保持、 写入"。
第一步:每位的
第二步:逐项验证(取
| 时钟沿后 | 结果 | ||
|---|---|---|---|
| 0 | 0 | 保持( | |
| 1 | 1 | 写入 ✓ |
第三步:四位结果对照。
| 沿后 | |
|---|---|
| 0 | 0110(原值,保持) |
| 1 | 1011(新值,写入) |
实现提醒:不要用"与门去切时钟"(门控时钟)。门控时钟会在
变化的瞬间产生窄脉冲,让触发器误动作。正确做法就是上面这个"在 D 端做二选一"。
例 2:异步 vs 同步计数器的最高频率(完整计算)
某工艺下:触发器
, ,一位进位组合逻辑 。求 4 位异步、4 位同步、16 位同步计数器的 。
方案 A:4 位异步(行波)计数器
最高位
方案 B:4 位同步计数器
关键是时钟到时钟的路径:
方案 C:16 位同步计数器(进位链串行)
进位逻辑要穿过 15 级:
对比表:
| 方案 | 关键路径 | 相对 A | |
|---|---|---|---|
| A:4 位异步 | 25.0 MHz | 1.00× | |
| B:4 位同步 | 55.6 MHz | 2.22× | |
| C:16 位同步(串行进位) | 16.7 MHz | 0.67× |
三条结论:
- 同步 > 异步(同位数下 2.22 倍)。
- 同步计数器的位数不能无限加——串行进位链会把优势吃回去(方案 C 甚至比 4 位异步还慢)。
- 出路是先行进位:把进位链改成对数级,16 位同步也能跑到接近方案 B 的频率。这正是
circuit/12-alu.md里 CLA 的同一个思路。
例 3:用 74LS163 设计模 10 计数器(两种手法对比)
用 4 位同步计数器 74LS163 实现模 10 计数器(
)。
方案一:复位法(同步清零)
- 计数序列:
。到 9 时触发清零。 - 检测条件:
且 ,即 。 - 逻辑:
(一个 NAND,两个输入)。
验证(74LS163 是同步清零,所以 9 这个状态完整出现一拍):
| 时钟 | 当前 | 下一个沿后 | ||
|---|---|---|---|---|
| 0 | 0000 | 0 | 1 | 0001 |
| … | … | … | … | … |
| 9 | 1001 | 1 | 0 | 0000 ← 同步清零 |
| 10 | 0000 | 0 | 1 | 0001 |
模数核算:状态 0000~1001 共 10 个 ✓。
方案二:置数法(同步置数)
- 让计数器数
,到 9 时下一个沿装入 0: - 检测
, 。
效果与复位法相同,但置数法更通用——可以做"从任意初值数
关键对比(考试重点):
| 同步清零(74LS163) | 异步清零(74LS161) | |
|---|---|---|
| 9 这个状态 | 完整出现一拍 | 完整出现一拍(因为检测的是 9) |
| 若改成检测 10 | 1010 只闪现一个门延迟 | — |
| 毛刺风险 | 无 | 有(若用"数到 N 再清"的写法) |
| 推荐度 | 推荐 | 慎用 |
为什么异步清零"检测
而非 "?因为异步清零立刻生效,所以要在数到 的那一瞬把它打回去;而同步清零要等下一个沿,所以检测 才对。这是 161/163 最经典的错题来源。
例 4:4 位约翰逊计数器(状态序列与模数)
,反馈 ,求完整状态环与模数。
逐拍推演(设初态 0000;约定整体左移,反馈位从
| 拍 | 反馈 | ||||
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 1 | 0 | 0 | 0 | 1 | 1 |
| 2 | 0 | 0 | 1 | 1 | 1 |
| 3 | 0 | 1 | 1 | 1 | 1 |
| 4 | 1 | 1 | 1 | 1 | 0 |
| 5 | 1 | 1 | 1 | 0 | 0 |
| 6 | 1 | 1 | 0 | 0 | 0 |
| 7 | 1 | 0 | 0 | 0 | 0 |
| 8 | 0 | 0 | 0 | 0 | 回到第 0 拍 ✓ |
模数核算:状态数
读这个序列:它是"1 从低位一排排灌进来,再从低位一排排排出去"——相邻两拍只变一位吗? 检查第 3→4 拍:0111 → 1111(只变 1111 → 1110(只变
用了几位触发器、能译出几个状态:
| 类型 | 触发器数 | 状态数 | 译码用的门 |
|---|---|---|---|
| 二进制计数器 | 需 | ||
| 环形计数器 | 1 根线(独热) | ||
| 约翰逊计数器 | 2 输入门即可(相邻一位) |
为什么控制器里爱用独热/约翰逊:状态译码几乎不花钱。代价是状态利用率低(
远小于 )。现代 FPGA 的状态机仍大量使用独热编码,理由完全相同。
例 5:移位寄存器做串并转换与乘 2(完整计算)
① 把串行输入
10110011依次移入 8 位 SIPO 寄存器,求 8 拍后的并行输出。 ② 把00001011左移一位、右移一位,各得多少?
① 串并转换(先入的位会跑到最高位还是最低位?)
关键:约定"先入先到高位"还是"先入先到低位"。本站统一用**"新位从
| 拍 | 输入位 | 移位后寄存器 |
|---|---|---|
| 0 | — | 00000000 |
| 1 | 1 | 00000001 |
| 2 | 0 | 00000010 |
| 3 | 1 | 00000101 |
| 4 | 1 | 00001011 |
| 5 | 0 | 00010110 |
| 6 | 0 | 00101100 |
| 7 | 1 | 01011001 |
| 8 | 1 | 10110011 |
8 拍后并行输出 = 10110011 ✓ —— 第一位移入的"1"最终跑到了最高位。
工程提醒:这就是 UART/SPI 里"MSB first 还是 LSB first"这个约定俗成的来源。收发双方必须约定一致,否则数据会整个镜像。
② 移位 = 乘除 2
完整对照表(8 位,避免溢出干扰):
| 操作 | 二进制 | 十进制 | 关系 |
|---|---|---|---|
| 原值 | 00001011 | 11 | — |
| 左移 1 位(低位补 0) | 00010110 | 22 | |
| 右移 1 位(逻辑右移,高位补 0) | 00000101 | 5 | |
| 右移 1 位(算术右移,高位补符号位 0) | 00000101 | 5 | 同上(正数无差别) |
负数时的关键区别(这才是考点):
| 操作 | 二进制(8 位补码) | 十进制 |
|---|---|---|
11111010 | −6 | |
| 逻辑右移 1 位 | 01111101 | +125 ⚠️ |
| 算术右移 1 位 | 11111101 | −3 ✓ |
结论:右移必须区分"逻辑(补 0)"与"算术(补符号位)"。C 语言里
>>对有符号负数的行为由实现定义(多数编译器做算术右移),这就是lang课程里要知道的坑。
例 6:寄存器堆的规模核算
某 CPU:32 个通用寄存器,每个 64 位,2 个读端口 + 1 个写端口。核算触发器数量、译码器规模、MUX 规模。
第一步:存储单元。
第二步:地址译码。
| 用途 | 器件 |
|---|---|
| 写地址译码 | 1 个 5-32 译码器(输出接各寄存器的写使能) |
| 读端口 1 选择 | 1 个 32 选 1 MUX(64 位宽) |
| 读端口 2 选择 | 1 个 32 选 1 MUX(64 位宽) |
第三步:MUX 的等效门数。 一个 64 位宽的 32 选 1 MUX = 64 个"32 选 1 单比特 MUX"。用 2 选 1 MUX 搭:
两个读端口:
第四步:总量对照。
| 项 | 数量 |
|---|---|
| D 触发器 | 2048 |
| 2 选 1 MUX | 3968 |
| 5-32 译码器 | 1 |
读出一句话:寄存器堆的"选择电路"比"存储电路"还大。这是"多端口寄存器堆"昂贵的原因,也是编译器拼命想把变量放在寄存器里(少访存)却受限于寄存器数量的硬件根源——
lang课程会从这个约束出发讲"寄存器分配"。
例 7:用程序复算
#include <stdio.h>
int main(void) {
/* --- 例2:异步 vs 同步计数器 fmax --- */
printf("--- 例2 最高频率 ---\n");
double tpd = 10.0, tsu = 5.0, tc = 3.0; /* ns */
double tA = 4 * tpd;
double tB = tpd + tc + tsu;
double tC = tpd + 15 * tc + tsu;
printf(" A 4位异步 t=%5.1f ns f=%6.2f MHz\n", tA, 1000.0 / tA);
printf(" B 4位同步 t=%5.1f ns f=%6.2f MHz 提速 %.2fx\n",
tB, 1000.0 / tB, tA / tB);
printf(" C 16位同步 t=%5.1f ns f=%6.2f MHz\n", tC, 1000.0 / tC);
/* --- 例3:模 10 计数器(同步清零)--- */
printf("\n--- 例3 模10计数器 ---\n");
int Q = 0;
for (int i = 0; i < 20; i++) {
printf("%d ", Q);
Q = (Q == 0x9) ? 0 : (Q + 1); /* 现态为 1001 时,下一拍同步回 0 */
if (i % 10 == 9) printf("\n");
}
printf("经过 20 拍后回到 0:%s\n", Q == 0 ? "是" : "否");
/* --- 例4:4 位约翰逊计数器 --- */
printf("\n--- 例4 4位约翰逊计数器 ---\n");
int j = 0, seen_states = 0;
for (int i = 0; i < 12; i++) {
printf("%d%d%d%d ", (j >> 3) & 1, (j >> 2) & 1, (j >> 1) & 1, j & 1);
int fb = (~(j >> 3)) & 1; /* 反馈 ~Q3 */
j = ((j << 1) | fb) & 0xF;
seen_states++;
if (j == 0) break;
}
printf("\n回到初态用了 %d 拍 -> 模数 = %d (期望 2n = 8)\n", seen_states, seen_states);
/* --- 例5:串并转换 + 移位 --- */
printf("\n--- 例5 串并转换 ---\n");
int serial[8] = {1, 0, 1, 1, 0, 0, 1, 1}; /* 待发序列 10110011,先发最高位 */
int reg = 0;
for (int i = 0; i < 8; i++) {
reg = ((reg << 1) | serial[i]) & 0xFF; /* 新位从低位进入 */
printf(" 第%d拍 输入%d -> ", i + 1, serial[i]);
for (int b = 7; b >= 0; b--) putchar('0' + ((reg >> b) & 1));
printf("\n");
}
printf(" 8 拍后 = %d (0x%02X),输入序列 = 10110011\n", reg, reg);
printf("\n--- 例5 乘除 2 与逻辑/算术右移 ---\n");
int x = 11; /* 00001011 */
printf(" 11 左移1位 = %d (=11*2)\n", (x << 1) & 0xFF);
printf(" 11 右移1位 = %d (=11/2)\n", x >> 1);
int neg = 0xFA; /* -6 */
printf(" -6 逻辑右移1位 = %d (高位补0, 变成正数)\n", (neg & 0xFF) >> 1);
printf(" -6 算术右移1位 = %d (高位补1, 保持负数)\n", ((neg & 0xFF) >> 1) | 0x80);
/* --- 例6:寄存器堆规模 --- */
printf("\n--- 例6 寄存器堆规模 ---\n");
int nreg = 32, w = 64;
int ff = nreg * w;
int mux21 = 2 * w * (nreg - 1);
printf(" D 触发器 = %d ; 2选1 MUX = %d ; 地址位 = %d\n",
ff, mux21, 5);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
--- 例2 最高频率 ---
A 4位异步 t= 40.0 ns f= 25.00 MHz
B 4位同步 t= 18.0 ns f= 55.56 MHz 提速 2.22x
C 16位同步 t= 60.0 ns f= 16.67 MHz
--- 例3 模10计数器 ---
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
经过 20 拍后回到 0:是
--- 例4 4位约翰逊计数器 ---
0000 0001 0011 0111 1111 1110 1100 1000
回到初态用了 8 拍 -> 模数 = 8 (期望 2n = 8)
--- 例5 串并转换 ---
第1拍 输入1 -> 00000001
第2拍 输入0 -> 00000010
...
第8拍 输入1 -> 10110011
8 拍后 = 179 (0xB3),输入序列 = 10110011
--- 例5 乘除 2 与逻辑/算术右移 ---
11 左移1位 = 22 (=11*2)
11 右移1位 = 5 (=11/2)
-6 逻辑右移1位 = 125 (高位补0, 变成正数)
-6 算术右移1位 = 253 (高位补1, 保持负数)
--- 例6 寄存器堆规模 ---
D 触发器 = 2048 ; 2选1 MUX = 3968 ; 地址位 = 5注意例 3 的写法:同步清零要检测
(即 9),并且让 9 这个状态完整出现一拍——所以循环体是"先打印现态,再由现态推下一拍"。若误改成检测 1010(即 10),计数器就变成模 11(数 0~10);这与"异步清零要检测"正好相反,是 161/163 最经典的失分点。
# ============ 例2:异步 vs 同步计数器 fmax ============
print("=== 例2 最高频率(tpd=10ns, tsu=5ns, 进位组合=3ns) ===")
tpd, tsu, tc = 10.0, 5.0, 3.0
tA = 4 * tpd
tB = tpd + tc + tsu
tC = tpd + 15 * tc + tsu
for nm, t in (("A 4位异步 ", tA), ("B 4位同步 ", tB), ("C 16位同步", tC)):
print(f" {nm} t={t:5.1f} ns f={1000/t:6.2f} MHz")
print(f" 同步/异步提速 = {tA/tB:.2f}x")
# ============ 例3:模 10 计数器(同步清零:检测 9) ============
print("\n=== 例3 模10计数器(74LS163 同步清零,检测 1001) ===")
Q, seq = 0, []
for _ in range(20):
seq.append(Q)
Q = 0 if Q == 0b1001 else (Q + 1) & 0xF # 现态为 9 -> 下一拍回 0
print(" 序列:", seq)
print(" 周期 =", seq.index(0, 1), " 不同状态数 =", len(set(seq)), " -> 模 10")
# 若误检测 10(1010) -> 变成模 11
Q, seq2 = 0, []
for _ in range(20):
seq2.append(Q)
Q = 0 if Q == 0b1010 else (Q + 1) & 0xF
print(" 误检测 1010 的序列:", seq2)
print(" -> 模", max(seq2) + 1, "(数 0~10,共 11 个状态)")
# ============ 例4:约翰逊计数器 ============
print("\n=== 例4 4位约翰逊计数器(左移,反馈 ~Q3 入 D0) ===")
j, states = 0, []
for _ in range(10):
states.append(format(j, '04b'))
fb = 1 - ((j >> 3) & 1)
j = ((j << 1) | fb) & 0xF
if j == 0:
break
print(" 状态环:", " -> ".join(states))
print(f" 模数 = {len(states)} (期望 2n = 8)")
print("\n=== 例4 4位环形计数器(左移,反馈 Q3 入 D0) ===")
ring, rs = 0b1000, []
for _ in range(6):
rs.append(format(ring, '04b'))
fb = (ring >> 3) & 1
ring = ((ring << 1) | fb) & 0xF
if ring == 0b1000:
break
print(" 状态环:", " -> ".join(rs), f"-> 模 {len(rs)} (期望 n = 4)")
print(" 每个状态都是独热(只有一个 1):",
all(s.count('1') == 1 for s in rs))
# ============ 例5:串并转换 + 移位 ============
print("\n=== 例5 串并转换(新位从 D0 进,整体左移) ===")
serial = [1, 0, 1, 1, 0, 0, 1, 1] # 待发序列 10110011(MSB first)
reg = 0
for i, b in enumerate(serial, 1):
reg = ((reg << 1) | b) & 0xFF
print(f" 第{i}拍 输入{b} -> {reg:08b}")
print(f" 8 拍后 = {reg} (0x{reg:02X}),待发序列 = 10110011 -> "
f"{'PASS' if format(reg, '08b') == '10110011' else 'FAIL'}")
print("\n=== 例5 移位 = 乘除 2 ===")
x = 0b00001011
print(f" 11 左移1位 = {x << 1} (=11*2)")
print(f" 11 右移1位 = {x >> 1} (=11//2)")
neg = 0b11111010 # -6 的 8 位补码
print(f" -6 二进制 {neg:08b}")
print(f" -6 逻辑右移 = {(neg & 0xFF) >> 1} (高位补0,变成大正数)")
print(f" -6 算术右移 = {((neg & 0xFF) >> 1) | 0x80} (高位补1,仍是 -3)")
print(f" 左移溢出示例: 179 左移一位 = {(179 << 1) & 0xFF},真值 358(溢出位被丢弃)")
# ============ 例6:寄存器堆规模 ============
print("\n=== 例6 寄存器堆规模(32 × 64 位,2 读 1 写) ===")
nreg, w = 32, 64
print(f" D 触发器 = {nreg * w}")
print(f" 单个 32 选 1(单比特)MUX 需 2 选 1 MUX = {nreg - 1} 个")
print(f" 两个读端口共需 2 选 1 MUX = {2 * w * (nreg - 1)} 个")
print(f" 地址位 = {(nreg - 1).bit_length()}")
# ============ 分频表 ============
print("\n=== 二进制计数器分频表(f_CP = 64 MHz) ===")
fcp = 64e6
for k in range(6):
print(f" Q{k}: 分频 {2**(k+1):>3} -> {fcp / 2**(k+1) / 1e6:6.3f} MHz")
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出要点:4 位异步 25.0 MHz / 4 位同步 55.6 MHz(2.22×)/ 16 位同步 16.7 MHz;模 10 计数器序列恰为 0~9 十个状态,误检测 1010 会变成模 11;约翰逊计数器 8 拍回到初态(10110011;逻辑右移把
考点
考点
1. 必背数字与公式
2. 同步 vs 异步计数器(必考对比)
| 项 | 异步(行波) | 同步 |
|---|---|---|
| 时钟 | 前级 | 所有触发器共用 CP |
| 延迟 | ||
| 译码毛刺 | 多(各位不同时翻转) | 少 |
| 结构 | 极简 | 需额外的进位组合逻辑 |
| 适用 | 低速分频 | 绝大多数场合 |
3. 复位法 vs 置数法(设计模
| 复位法 | 置数法 | |
|---|---|---|
| 手段 | 清零端( | 置数端( |
| 异步器件(74LS161) | 检测 | 检测 |
| 同步器件(74LS163) | 检测 | 检测 |
| 毛刺 | 异步清零有( | 无 |
| 灵活性 | 只能"数到 | 可从任意初值数 |
一句话记忆:"异步清零要数到
,同步清零数到 "——因为异步立刻生效,同步要等下一个沿。
4. 高频陷阱
- 用"与门切时钟"做使能(门控时钟)→ 产生毛刺,触发器误动作。正确做法是在
端做二选一 MUX。 - 三态门的高阻
不是 0 也不是 1,而是"断开"。绝不允许多个三态门同时驱动同一条总线(总线争用)。 - 异步计数器位数越多频率越低,别答成"与位数无关"——这个结论只对同步计数器成立。
- "左移 = 乘 2"只在无溢出时成立。8 位
10110011(179) 左移一位 =01100110(102) ≠ 358,溢出位被丢弃。 - 右移必须分逻辑/算术:逻辑右移补 0,算术右移补符号位。负数做逻辑右移会变成大正数(
)。 - 约翰逊计数器的反馈是
,不是末 位 (后者是环形计数器)。末 位 - 环形计数器有"死循环"风险:非独热初态(如
0000、1010)可能永远出不来,工程上要加自启动逻辑。(约翰逊计数器的全 0 初态是自启动的。) - 74LS161 与 74LS163 的清零方式不同(异步 / 同步),做题先看器件型号;74LS160 是十进制(模 10)版。
- 寄存器堆"读是组合的、写是时序的":读地址一变输出立刻变(不占时钟沿),写必须等时钟沿。这个区别直接决定 L2 里"能否在同一周期完成读-改-写"。
5. 与后续章节的接口
- 寄存器 + 组合逻辑 + 状态译码 = 有限状态机,这是下一篇
circuit/15-fsm.md的全部内容。 - 异步计数器/移位寄存器的延迟累积会在
circuit/16-timing.md里用"关键路径"的语言重新讲一遍。 - 寄存器堆的端口规模是 L2 组成原理"指令周期"与 L4 操作系统"上下文切换"(要保存/恢复寄存器)的成本来源。
- 移位寄存器是 L2 里"乘法器(移位 + 加)"和 net 里"串行通信 UART"的公共积木。
小结
- 寄存器 =
个 D 触发器并联 + 共用时钟;"保持"要在 端做二选一(LOAD门控数据),不要门控时钟。 - 三态输出是总线存在的物理前提:同一时刻只能有一个驱动源。
- 寄存器堆 = 触发器阵列 + 译码器(写)+ MUX(读);它的选择电路比存储电路还大,这就是寄存器数量受限的硬件根源。
- 移位寄存器四种组合(SISO/SIPO/PISO/PIPO)覆盖延迟线、串并转换、并串转换三大用途;左移乘 2、右移除 2,但右移必须区分逻辑与算术。
- 异步计数器的延迟随位数线性累积(
);同步计数器用共用时钟 + 提前算好的进位,频率几乎与位数无关——但进位链太长会吃掉全部收益,出路仍是先行进位。 - 模
计数器两条路:复位法(简单但有毛刺)与置数法(推荐);异步清零检测 、同步清零检测 。 - 环形计数器模
、约翰逊计数器模 ——状态利用率低,但译码几乎不花钱,所以控制器与 FPGA 状态机至今偏爱它们。
回到主线:现在我们已经拥有了"记住一个字"的全部部件:寄存器、计数器、移位寄存器。
但真正的难题还剩最后一个:当电路有了"状态",它就不再是"输入定输出"那么简单了。交通灯要按"红→绿→黄→红"循环,CPU 要按"取指→译码→执行→写回"循环,序列检测器要认得出"1101"这个模式——怎样系统地设计一个"有状态"的电路? 这就是下一篇的主题。
下一篇:有限状态机:时序电路的设计方法
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。