转码记录 Vol.03 | 25 Summer:三门硬核课 + 全栈 AI 项目上线,这学期造了一个 CPU
上学期(25 Winter)的自学集中在理解层面。这学期开始验证理解和动手之间的差距。三门校内课:EECS 2021 要求用 Verilog 亲手实现一个能跑 RISC-V 指令的 CPU;EECS 2031 用 C 语言处理系统编程任务;EECS 2030 深入到面向对象的实现者视角。课外还做了 Spring Boot + React 的全栈 AI 项目,从零走到 Docker 部署上线。
上学期(25 Winter)的自学集中在理解层面——CS 61C 让我能在脑子里从 C 代码一路跟到 RISC-V 汇编再到 CPU 的流水线,Nand2Tetris 打通了高级语言到机器码的完整翻译路径。
理解是一件事,动手实现是另一件事。这学期开始验证两者之间的差距。
三门校内课:EECS 2021 要求用 Verilog 亲手实现一个能跑 RISC-V 指令的 CPU;EECS 2031 要求用 C 语言处理实际的系统编程任务;EECS 2030 在 Java OOP 的基础上继续深入到实现者视角。课外还做了一个 Spring Boot + React 的全栈 AI 项目,从零走到上线。
校内课程
EECS 2021 – Computer Organization
这门课分两个阶段,难度跨度显著,成绩 A。
Part 1:RISC-V 汇编,4 个 Lab
上学期在 CS 61C 里学过 RISC-V,这门课的 Lab 是在课程框架下把它真正写清楚。
汇编的心智负担在于没有任何抽象:变量不存在,只有寄存器(t0-t6 临时寄存器,s0-s11 保存寄存器 ,a0-a7 函数参数,ra 返回地址);函数不存在,只有跳转指令和寄存器约定;作用域不存在,只有手动维护的栈帧。
每次函数调用的完整流程:
- 调用方(caller)把参数放进
a0-a7,把ra压栈保存 jal ra, function_label跳转,同时把返回地址写进ra- 被调用方(callee)分配栈帧(
addi sp, sp, -N),保存 callee-saved 寄存器 - 执行函数体
- 恢复 callee-saved 寄存器,释放栈帧,
jalr zero, ra, 0返回
写多了之后,看 C 代码能直接在脑子里对应汇编结构:一个 if-else 是 beq / bne + 跳转标签,一个 for 循环是计数寄存器 + 条件分支,一个函数调用是上面那套流程的精缩版。
Part 2:从零用 Verilog 实现 CPU,4 个 Lab
💻 这是这学期工作量最大、收获最高的部分。
Verilog 是硬件描述语言(HDL),语法上接近 C,但执行语义完全不同:你不是在写"先做这步再做那步",而是在描述电路的结构和信号的连接关系。所有模块并行工作,时钟信号控制状态更新,信号不是变量,是随时间变化的电平。
实现路径:
模块 1:ALU(算术逻辑单元)
→ 支持加、减、与、或、移位、比较
模块 2:寄存器堆(Register File)
→ 32 个 32-bit 寄存器,支持同步读写
模块 3:数据通路(Datapath)
→ 连接 ALU、寄存器堆、指令存储器、数据存储器
模块 4:控制单元(Control Unit)
→ 根据指令的 opcode / funct 字段生成控制信号
最终:单周期 CPU(Single-cycle CPU)
→ 能执行 RISC-V RV32I 指令集的子集
真正难的不是单个模块,而是整合时的调试。
软件调试可以用 print 或断点;Verilog 仿真调试(用 iverilog + gtkwave)靠的是波形图——每个信号在每个时钟周期的电平高低。一个功能不对,可能是:
- 组合逻辑计算错了(ALU 输出不正确)
- 时序逻辑的时钟沿方向错了(用了 negedge 而不是 posedge)
- 信号位宽不匹配导致截断(32-bit 信号赋给 8-bit 端口)
- 控制信号没有覆盖某条指令的 case
这门课还覆盖了流水线、缓存、虚拟内存的架构设计。CS 61C 里这些是概念,这门课里是"这些机制的每一个部件具体是什么信号,在哪个时钟周期更新"。层次更精确了一个档次。
EECS 2030 – Advanced Object Oriented Programming
正式课名是高级面向对象编程,成绩 A+。不是入门 OOP,而是专注于实现者视角。
EECS 1022 教的是 OOP 的基本概念(类、对象、方法)。EECS 2030 的重点是:给你一个 API 规范(接口文档 + 前置/后置条件),你来实现它,并通过单元测试验证。这个视角切换是这门课最核心的东西。
继承和多态的深度:抽象类(abstract class)和接口(interface)的区别不只是语法,而是设计意图——抽象类用于"这些子类有共同的实现",接口用于"这些类有共同的行为契约但实现完全独立"。Java 的单继承 + 多接口实现体现的是对这个区别的设计选择。
递归的机制:这门课里递归不只是"函数调用自己",而是要理解调用栈——每次递归调用在栈上压一个帧,帧里存局部变量和返回地址,递归深度过大会 stack overflow,尾递归为什么在某些语言里可以被优化掉。结合上学期汇编课里手动管理栈帧的经验,这学期对递归的理解要清晰得多——汇编层面的栈帧和 Java 层面的调用帧是同一个东西,只是抽象层次不同。
链表和二叉树的实现:课程要求自己实现 LinkedList、Stack、Queue、BinaryTree,包括 Iterator 的设计。要考虑边界情况(空链表的删除、树平衡的维护),要思考每个操作的不变量(invariant)是什么——这正是 MATH 1090 里前置/后置条件框架的实际应用。
排序算法:QuickSort 和 MergeSort 的实现和复杂度分析。QuickSort 平均 O(n log n) 来自每次分区的期望效果,最坏 O(n²) 出现在 pivot 极端不平衡时。MergeSort 稳定、最坏也是 O(n log n),代价是需要额外 O(n) 空间。选哪个取决于具体场景——稳定性要求、内存限制、数据的初始有序程度。
API 文档和测试用例的要求让这门课有了工程规范的感觉——不只是"代码能跑",而是"代码有文档、有测试、行为可验证"。
EECS 2031 – Software Tools
分两个阶段:Linux / Shell + C 语言。
Linux 和 Shell 脚本
常用 CLI 工具(grep、find、awk、sed、xargs)、管道(|)、重定向(>、>>、<)、Shell 脚本基础(变量、循环、条件)。
学完这部分,日常开发的工作流效率直接提升。用 find . -name "*.java" | xargs grep "TODO" 这类命令批量处理文件,不再需要手动点。Makefile 的原理(依赖关系图 + 命令)和版本控制工作流也是这门课的内容。
C 语言和指针
C 是这学期真正要新学的东西,难点集中在指针上。
指针是地址:int *p = &x 意味着 p 存的是变量 x 的内存地址,*p 才是这个地址处的值。混淆 p 和 *p 是新手最常犯的错误,特别是在函数参数里传指针修改外部变量时。
数组名是指针:int arr[5] 中,arr 等价于 &arr[0]。arr[i] 和 *(arr + i) 完全等价,指针算术直接对应内存地址偏移。字符串是 char 数组加上末尾的 \0,C 标准库函数(strlen、strcpy)靠找到 \0 来确定字符串长度,没有 \0 就是缓冲区溢出。
手动内存管理:malloc 在堆上分配内存;用完必须 free,否则内存泄漏。free 之后不能再用这个指针(悬空指针),访问悬空指针是未定义行为(undefined behavior)——程序可能崩溃,也可能"正常运行"但产生错误结果,后者更难 debug。
结合 EECS 2021 和 CS 61C 里学到的内存布局(栈在高地址向下增长,堆在低地址向上增长,静态段存全局变量),C 的内存行为变得非常具体,不再是抽象的"指针危险"警告。
自学实践:Spring Boot + React 全栈 AI 项目
这学期在校内课之外,用 Spring Boot(后端)+ React(前端)做了一个集成 AI 功能的全栈项目,走完了从本地开发到 Docker 部署的完整链路。
后端(Spring Boot):Spring 的核心是 IoC(控制反转)——对象的生命周期由框架管理,不是你 new 出来的,而是通过依赖注入(DI)拿到。Controller 层处理 HTTP 请求,Service 层处理业务逻辑,Repository 层负责数据访问,三层分离让代码结构清晰、可测试。集成 AI 功能用的是 LLM API,在后端做请求封装、Prompt 管理和响应解析。
前端(React):组件化思维和 Android 的 View 层有结构上的相似性,但 React 的单向数据流(状态从父组件流向子组件,子组件通过 callback 通知父组件)和 Android 的事件驱动模型有明显差异。用 useState 和 useEffect 管理状态和副作用,fetch / axios 做前后端通信。
Docker 部署:Docker 解决的是环境一致性问题——"在我机器上能跑"不等于"在服务器上能跑"。用 Dockerfile 描述构建步骤打成镜像,docker-compose 把前端、后端、数据库三个服务协调起来,一条命令启动完整的应用栈。
这是第一次真正走通"写代码 → 打包 → 部署 → 能访问"这个完整流程。很多地方是能跑就行,工程质量还有大量提升空间。但这个起点很重要——下学期 OS、数据库、网络课学的很多内容,在这个项目里都能找到对应的实际场景。
这学期结束时的状态
这学期是从硬件层到系统层的过渡节点。
往下看:造了 CPU,写了 C,清楚了高级语言到机器码的全链路。往上看:第一次把应用跑起来部署出去,开始接触真实工程问题(错误处理、环境一致性、并发边界)。
下学期(25 Fall)把焦点转移到系统软件层:操作系统(EECS 3221 + MIT 6.S081)、数据库(EECS 3421 + CS 186)、网络(CS 168),三个方向同步推进。这学期建立的硬件基础——流水线、内存层级、系统调用的 CPU 层面实现——会直接支撑 OS 课的理解。
学期之外的思考
用 Verilog 造 CPU 之前,我以为自己"理解"了 CPU 是怎么工作的——流水线、取指、译码、执行,CS 61C 里都讲过。实际开始写 Verilog 之后,发现"理解"和"会做"之间有一个很大的空洞。
理解是静态的:知道流水线有五个阶段,知道数据冒险是什么。会做是动态的:在脑子里同时追踪五个流水线阶段里各自有什么数据,哪个信号在这个时钟周期更新,哪个要等下一拍。这种动态追踪能力不是读书读出来的,必须自己出过错、追过波形、修过 bug 才能建立。
这是这学期最实质的认知升级:书面理解和操作性理解是两件事,且后者的建立成本远比前者高。
全栈 AI 项目第一次上线,感受比预期平静。做出来的时刻有满足感,但很快消退——因为真实用户的第一个 bug 随之而来。一个本地测试从未触发的边界情况,在真实环境里被触发了。那时候的第一反应不是慌乱,而是开始 debug:日志在哪里,错误在哪一层,复现路径是什么。
这个反应比项目本身更有价值。它说明某种工程本能开始形成了:面对"东西坏了"时,先找问题,不先评估情绪。
还有一个隐约但真实的身份变化。以前做数据分析时,遇到新需求的第一反应是"找个工具来用"——有没有现成的库,有没有现成的模板。这学期做 CPU、写 C 语言指针、搭全栈项目,开始有一种不同的本能:先搞清楚这个问题是什么,再想怎么解决。
"找工具"和"先理解问题"是两种不同的工程思维。转码最初的动机就是想建立后者。这学期它开始有了一点点形状。
下一篇写 25 Fall——OS、数据库、网络三个系统方向同时打通,加上一个上线的 Agent 应用。
