基于FPGA的ALU构建:手把手教程(Verilog实现)

从零开始在FPGA上构建一个ALU:不只是“做加法”,而是理解计算机的起点(Verilog实战)

你有没有想过,当你写下 a + b 这行代码时,背后到底发生了什么?
它不是魔法,也不是抽象概念——它是 硬件在真实电路中流动的电信号 。而这一切的核心,就是我们今天要亲手实现的模块: 算术逻辑单元(ALU)

这不只是一次“照着抄代码”的练习,而是一场深入数字系统底层的探索。我们将用 Verilog 在 FPGA 上从头搭建一个功能完整的 ALU,理解每一条线、每一个标志位的意义,并最终让它在开发板上跑起来。

准备好了吗?让我们从最基础的问题开始:

CPU 是怎么“算数”的?

ALU 到底是什么?别被术语吓住

简单说, ALU 就是 CPU 的“计算器”+“逻辑大脑” 。它接收两个数据(比如 A 和 B),再根据指令决定:“现在你是要做加法?还是取反?或者判断哪个数更小?” 最后输出结果和一些“状态信息”,比如是否为零、有没有进位。

听起来像软件里的 if-else ?没错!但这里的“判断”是由纯硬件电路完成的——没有操作系统,没有编译器,只有门电路和触发器。

它长什么样?我们可以画出来

 +----------------------+ A ----->| | | ALU |-----> Result B ----->| | | Flags: Z, C, O | Op ---->| | +----------------------+ 

输入:
- A , B :操作数(例如 8 位二进制)
- op :操作码(3 位可选 8 种操作)

输出:
- result :运算结果
- zero :结果是否为零?
- carry :是否有进位/借位?
- overflow :有符号运算是否溢出?

整个模块是 组合逻辑 ——意味着只要输入变了,输出就会立刻响应(忽略传播延迟)。没有时钟驱动,也没有记忆功能。这种即时性让它非常适合放在处理器的数据通路中高速运转。


我们要做什么?目标清晰才不会迷路

本项目的目标非常明确:

✅ 实现一个 8 位 ALU ,支持以下 7 种常见操作:

操作码 操作 功能说明
000 ADD A + B
001 SUB A - B
010 AND A & B
011 OR A | B
100 XOR A ^ B
101 NOT ~A (忽略 B)
110 SLT A < B(有符号比较,返回 1 或 0)

✅ 自动生成三个关键状态标志:
- zero :结果全为 0
- carry :加减法中的进位/借位
- overflow :有符号整数溢出检测

✅ 使用标准 Verilog 编写,可在 Xilinx、Intel 等主流 FPGA 平台上综合部署

✅ 可扩展性强:未来能轻松集成到自定义 RISC 风格 CPU 中


核心设计思路:多个功能单元 + 一个多路选择器

ALU 的本质是一个“多合一”的功能切换器。

想象一下厨房里的灶台:你可以炒菜、煮汤、蒸饭……但同一时间只能用一个功能。ALU 也是这样:内部所有运算单元都在同时工作,但最终只让一个结果“通过门口”的多路选择器(MUX)输出。

不过为了节省资源,我们并不真的并行计算所有结果。而是使用 case 语句,在行为级描述中按需生成对应逻辑——综合工具会自动优化成等效的组合电路。


Verilog 实战:一行一行写出来

下面是我们的核心模块定义。注意这是典型的 组合逻辑建模方式

module alu_8bit( input [7:0] A, input [7:0] B, input [2:0] op, output reg [7:0] result, output reg zero, output reg carry, output reg overflow ); 

我们使用 reg 类型作为输出变量,是因为在 always @(*) 块中赋值需要存储类型,但这不代表它会生成寄存器——只要逻辑是组合性的,就不会有时序元件。

接下来是主逻辑块:

always @(*) begin case(op) 3'b000: begin // ADD: A + B {carry, result} = A + B; overflow = (A[7] == B[7]) && (A[7] != result[7]); end 3'b001: begin // SUB: A - B {carry, result} = A - B; overflow = (A[7] != B[7]) && (A[7] != result[7]); end 3'b010: begin // AND result = A & B; carry = 1'b0; overflow = 1'b0; end 3'b011: begin // OR result = A | B; carry = 1'b0; overflow = 1'b0; end 3'b100: begin // XOR result = A ^ B; carry = 1'b0; overflow = 1'b0; end 3'b101: begin // NOT A result = ~A; carry = 1'b0; overflow = 1'b0; end 3'b110: begin // SLT: Signed Less Than result = ($signed(A) < $signed(B)) ? 8'h01 : 8'h00; carry = 1'b0; overflow = 1'b0; end default: begin result = 8'bx; carry = 1'b0; overflow = 1'b0; end endcase zero = (result == 8'd0); end 

关键点解析:每一行都不能含糊

📌 加法与进位处理: {carry, result} = A + B;

这里用了拼接操作符 {} 来捕获 9 位结果。因为两个 8 位数相加最多产生 9 位(包括进位),所以高位自动成为 carry 标志。

例如: 255 + 1 = 256 → 二进制 1_0000_0000 ,那么 carry=1 , result=8'h00

📌 溢出判断(Overflow):只对有符号运算有意义

补码系统中,溢出发生在“不该变号却变了号”的时候。

  • ADD 溢出条件 :两个正数相加得负数,或两个负数相加得正数
    即: A[7]==B[7] result[7]!=A[7]
  • SUB 溢出条件 :正数减负数得负数,或负数减正数得正数
    即: A[7]!=B[7] result[7]!=A[7]

这个表达式虽然简洁,但非常精准地抓住了本质。

📌 SLT 如何正确比较负数?

直接用 < 在 Verilog 中默认是无符号比较!所以我们必须加上 $signed() 强制解释为有符号数。

$signed(8'b1111_1111) // 表示 -1 $signed(8'b0000_0001) // 表示 +1 

这样才能保证 -1 < 1 成立。

📌 Zero 标志为什么放在外面?

因为它依赖于 result ,而 result case 中已被赋值。统一在外面判断,避免重复写。

而且这样更安全:无论哪种操作,都能确保 zero 被更新。

📌 default 分支不能少!

哪怕你觉得“不可能走到这里”,也一定要加 default 。否则综合工具可能会推断出锁存器(latch),导致不可预测的行为。


设计技巧与避坑指南:这些经验书上不教

✅ 技巧 1:永远使用 always @(*) 处理组合逻辑

不要写成 always @(A, B, op) ,那样容易遗漏敏感信号。 @(*) 是自动推导敏感列表的最佳实践。

✅ 技巧 2:阻塞赋值 = ,不是非阻塞 <=

组合逻辑中用 = ;时序逻辑才用 <= 。混用会导致仿真与实际不符。

✅ 技巧 3:显式初始化所有输出路径

即使写了 default ,也要确保每个分支都给 carry overflow 赋值,防止意外生成 latch。

✅ 技巧 4:考虑后续升级为流水线结构

如果你打算把它放进 CPU 流水线里,建议将当前模块改为同步设计(加入时钟),并在顶层控制其使能。但现在先专注功能验证。

❌ 常见错误:忘记 signed 修饰导致 SLT 出错

新手最容易犯的错误就是写成:

result = (A < B) ? 1 : 0; // 错!这是无符号比较! 

结果 0xFF (-1) 会被当作 255 ,比 0x01 大,于是 -1 > 1 —— 显然错了。

记住口诀: 涉及符号,必加 $signed


怎么验证它真的能工作?Testbench 不可少

光看代码没用,得跑起来才知道对不对。下面是一个简单的 testbench 示例:

module tb_alu; reg [7:0] A, B; reg [2:0] op; wire [7:0] result; wire zero, carry, overflow; // 实例化被测模块 alu_8bit uut ( .A(A), .B(B), .op(op), .result(result), .zero(zero), .carry(carry), .overflow(overflow) ); initial begin $dumpfile("alu.vcd"); $dumpvars(0, tb_alu); // 测试 ADD A = 8'd5; B = 8'd3; op = 3'b000; #10 assert(result === 8'd8 && carry === 0) else $error("ADD failed"); // 测试 SUB with borrow A = 8'd3; B = 8'd5; op = 3'b001; #10 assert(result === 8'd254 && carry === 1) else $error("SUB borrow failed"); // 测试溢出:127 + 1 = -128? A = 8'sd127; B = 8'sd1; op = 3'b000; #10 assert(overflow === 1) else $error("Overflow not detected"); // 测试 SLT: -1 < 1 ? A = 8'sd-1; B = 8'sd1; op = 3'b110; #10 assert(result === 8'h01) else $error("SLT signed compare failed"); $display("All tests passed!"); $finish; end endmodule 

运行这个 Testbench,可以用 ModelSim 或 Vivado Simulator 快速验证功能正确性。


下载到 FPGA:让灯“说出”运算结果

如果你想在开发板上演示,可以这样做:

  • 输入 A B 由拨码开关提供
  • 操作码 op 用 3 个按键选择
  • 输出 result 接 8 个 LED
  • zero carry overflow 各用一个 LED 指示

然后烧录进板子,动手切换开关,亲眼看到 1 + 1 = 2 的灯光亮起——那种成就感,远超任何理论讲解。

进阶玩法:接上 UART 模块,通过串口发送命令,远程执行运算并回传结果,打造一个微型“计算终端”。


更进一步:这不是终点,而是起点

你现在拥有的不仅仅是一个 ALU,而是一个 可复用的核心构件

下一步你可以尝试:

🔧 扩展功能
- 添加左移/右移操作(SHL/SHR)
- 支持乘法(可用查找表或迭代实现)
- 增加更多标志位,如符号标志 SF

🧠 构建简易 CPU
- 加入寄存器文件(Register File)
- 实现简单的指令解码器
- 搭建单周期数据通路
- 写汇编程序运行在你的“自制CPU”上

🚀 性能优化
- 将 Ripple Carry Adder 替换为 Carry Lookahead Adder,减少关键路径延迟
- 使用 LUT 分布式实现部分逻辑,提升速度


写在最后:为什么我们要自己造轮子?

今天的开发者动辄使用 ARM Cortex-M、RISC-V 内核,甚至调用 HLS 工具把 C 代码转成硬件。但我们越来越远离“机器如何真正工作”的本质。

而当你亲手写出第一个 A + B 的加法器,看到进位信号一级级传递,理解为何 -128 + (-1) 会溢出,你会突然明白:

计算机不是黑盒,它是一步步构建出来的逻辑世界。

这个 ALU 项目看似简单,但它承载的是数字系统设计的根基。它教会你的不只是 Verilog 语法,更是思维方式:如何拆解问题、如何组织模块、如何验证假设。

无论你是电子专业学生、嵌入式工程师,还是自学硬件的爱好者,我都强烈建议你动手实现一次。

不要停留在“看懂了”,要去“做出能跑的”。

当你按下下载按钮,LED 亮起那一刻,你就已经跨过了从理论到实践的最后一道门槛。


如果你在实现过程中遇到问题,欢迎留言交流。也可以分享你的扩展版本,比如加入了乘法器的 ALU,或是用它搭建的迷你 CPU 架构。我们一起把这块“数字积木”搭得更高。

Read more

从 LLaMA-Factory 微调到高通 NPU 部署: Qwen-0.6B 全链路移植指南

前言 在大模型端侧化部署的趋势下,如何将微调后的 LLM 跑在手机 NPU 上是很多开发者的痛点。本文将手把手教你如何将使用 LLaMA-Factory 微调后的 Qwen-0.6B 模型,一步步移植到高通(Qualcomm)骁龙平台的 NPU 上,实现低功耗、高速度的本地化推理。 一、 导出微调模型 首先,在 LLaMA-Factory 界面中选择好微调后的检查点(Checkpoint),填写导出路径,点击 “开始导出” 。 导出成功后,你会在目录下看到如下文件: * model.safetensors(模型权重) * config.json(模型配置) * tokenizer.json 等(分词器相关) 要将微调后的 Qwen-0.6B 模型移植到高通 NPU,第一步就是格式转换。safetensors 是目前

新手用AI写文章,AI味太重了?收藏这几个提示词瞬间去除AI写作痕迹!

现在很多新入局自媒体的人用AI辅助写作,但是稍有不慎就会被平台限流、封号。究其原因在于AI写的文字太AI风了,所以平台不会给流量! 要去除文章AI痕迹的核心思路是:第一步使用好提示词,好的提示词本身就降低了AI味道;第二步人工优化,在进一步降低AI味的同时还要修正错误和漏洞。 今天我把自己的经验结合起来,分享一下降低AI味的提示词。 一、赋予角色 给定一个具体的角色,比如说你在做育儿领域的爆款文章的时候,就可以给AI赋予一个资深育儿专家的身份。 举例:你是育儿专家,擅长写育儿类自媒体爆款文章。你主要的工作就是写出更有人情味、自然流畅、没有机器写作痕迹的文章,长短句并用,不用列表和总结,少用连接词,内容要打破AI生硬的感觉,在语言风格、情感表达、逻辑结构上全方位地接近人类真实的写作习惯。 二、人物画像 人物画像是对角色的补充,可以指定人物的年龄、性别、爱好等,做IP号的时候,就给AI发一张画像。 例子:语言风格转换专家,对于人类写作的特色有着非常深刻的认识。把AI生成的“冷冰冰”的文字转为通俗易懂、口语化的表达方式。依靠多年的积累,你能够很快地发现AI文本中重复啰嗦的

ESP-Drone: 乐鑫 ESP32/ESP32-S2/ESP32-S3 开发的小型无人机解决方案

ESP-Drone: 乐鑫 ESP32/ESP32-S2/ESP32-S3 开发的小型无人机解决方案

目录 概述 1 主要特性 2 ESP-Drone无人机的硬件类型 3 硬件组装示意图 4 项目源代码 概述 ESP-Drone 是基于乐鑫 ESP32/ESP32-S2/ESP32-S3 开发的小型无人机解决方案,可使用手机 APP 或游戏手柄通过 Wi-Fi 网络进行连接和控制。该方案硬件结构简单,代码架构清晰,支持功能扩展,可用于 STEAM 教育等领域。 1 主要特性 ESP-Drone 具备以下特性: 支持自稳定模式 (Stabilize mode):自动控制机身水平,保持平稳飞行。支持定高模式 (Height-hold mode):自动控制油门输出,保持固定高度。支持定点模式 (Position-hold mode):自动控制机身角度,保持固定空间位置。支持 PC 上位机调试:

安路Anlogic FPGA下载器的驱动安装与测试教程

安路Anlogic FPGA下载器的驱动安装与测试教程

参考链接:安路下载器JTAG驱动安装 - 米联客(milianke) - 博客园 安路支持几款下载器: AL-LINK在线下载器是基于上海安路信息科技股份科技有限公司全系列 CPLD/FPGA 器件,结合公司自研的 TD 软件,可实现在线 JTAG 程序下载、ChipWatcher 在线调试、FLASH 读写、Device Chain 模式烧录。下载器配合 USB-B 数据线、2.54mm 间距 10 针扁平线使用,实物如图所示 1.下载并安装软件 工具与资料下载-国产FPGA创新者 - 安路科技 (需要注册登录) 2.安装驱动 当完成TD软件安装后,可以在安装路径下找到对应驱动。 2.1 右击anlocyusb.inf选择安装: 2.2