开源 FPGA 01|iCEstick 开箱第一个项目:全开源工具链点灯到 UART
_ ___ ___ ___ _ _ ___ _ __
(_)/ __| __/ __| |_(_)/ __|| |/ /
| | (__| _|\__ \ _| | (__ | ' <
|_|\___|___|___/\__|_|\___||_|\_\
开源 FPGA 实战系列 · 第 01 篇
iCEstick + 全开源工具链 = 自由
系列第 1 篇 · 目标器件:Lattice iCE40HX1K(iCEstick) 工具版本:Yosys 0.40 · nextpnr-ice40 0.7 · IceStorm 20240114 系列入口:本篇无上一篇,这里是起点。 前置阅读(可选):开源 FPGA 工具链全景
0. 这一篇要解决什么问题
你买了一块 iCEstick,二十几块钱,插上 USB 就能用。现在问题是:
- 工具链怎么装?不要 Vivado 的 35 GB 安装包,不要 License 申请流程,不要账号注册。
- 第一个项目怎么跑?LED 闪烁是最小验证,但流程必须完整——综合、布局布线、生成 bitstream、烧写。
- UART 怎么做?点灯之后,下一步是和 PC 通信。115200 波特率,echo 回显,picocom 验证。
- 资源占了多少?blinky 用了几个 LUT?UART 呢?心里要有数。
本篇不覆盖的内容:
- SystemVerilog / Chisel / HLS
- 时序约束深入(见第 02 篇)
- SoC / CPU 集成(见第 04 篇)
- 仿真(见第 05 篇)
1. 为什么选 iCEstick
市面上廉价开发板很多,为什么偏偏推荐 iCEstick 作为起点?
硬件规格
| 参数 | 数值 | 备注 |
|---|---|---|
| FPGA | iCE40HX1K | HX = High performance eXtended |
| LUT(查找表) | 1,280 | 4-input LUT |
| BRAM | 64 Kbit(4 块 × 16 Kbit) | 单端口 256×16 或双端口 256×8 |
| PLL | 1 | 支持 16-275 MHz 输出 |
| GPIO | 206 个(实际引出约 40 个) | 通过 .pcf 约束 |
| USB 接口 | FT2232H(双通道) | Channel A = JTAG/SPI 烧写;Channel B = UART |
| SPI Flash | 8 Mbit(Winbond W25Q80) | 存储 bitstream,上电自动加载 |
| 时钟晶振 | 12 MHz | 接 FPGA 引脚 21 |
| 板载 LED | 5 个(LEDO~LED4) | 绿色,接引脚 99/98/97/96/95 |
| 尺寸 | USB 棒形,约 60×20mm | 直插电脑 USB |
| 价格 | $22(官方) / ¥60-80(淘宝) | 开源工具支持最成熟的器件之一 |
为什么是 iCE40 而不是其他?
Project IceStorm 在 2015 年完成了对 iCE40 的完整逆向工程,包括:
- 每个引脚的比特流编码
- PLL/BRAM 的配置格式
- 时序模型(延迟数据)
这意味着 Yosys + nextpnr-ice40 + icestorm 这条工具链是经过数年生产验证的,不是玩具。相比之下:
- Xilinx:nextpnr-xilinx 仍处于实验阶段
- Intel/Altera:无完整开源 P&R 支持
- ECP5:成熟,但比 iCE40 晚两年,复杂度更高(适合第 06 篇)
对于第一个项目,iCEstick 的”小”是优点:设计跑完不到 5 秒,失败了立刻重试。
2. 工具链安装
方法 A:OSS CAD Suite(强烈推荐)
YosysHQ 维护的一键安装包,包含 Yosys、nextpnr、IceStorm、iverilog、cocotb 等所有工具:
# 下载 Linux x86-64(2024-01-14 版本)
wget https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2024-01-14/oss-cad-suite-linux-x64-20240114.tgz
tar xzf oss-cad-suite-linux-x64-20240114.tgz
# 激活环境(每次新终端)
source oss-cad-suite/environment
# 验证版本
yosys --version # Yosys 0.40 (git sha1 a1bb0255d, ...)
nextpnr-ice40 --version # nextpnr-ice40 -- Next Generation Place and Route (Version nextpnr-0.7)
iceprog --version # iceprog: unrecognized option '--version'(正常,无版本flag)
macOS:
# Apple Silicon / Intel
wget https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2024-01-14/oss-cad-suite-darwin-arm64-20240114.tgz
tar xzf oss-cad-suite-darwin-arm64-20240114.tgz
source oss-cad-suite/environment
方法 B:包管理器(Ubuntu 22.04+)
sudo apt update
sudo apt install -y \
yosys \
nextpnr-ice40 \
fpga-icestorm \
iverilog \
picocom
# 检查版本
yosys --version # Yosys 0.36(Ubuntu 22.04 仓库版本,略旧)
nextpnr-ice40 --version # 0.6
🚧 避坑 1:iceprog 需要 udev rules
在 Linux 上,非 root 用户默认没有 USB 设备写权限,
iceprog会报Can't find iCE FTDI USB device。解决方法:
# 创建 udev 规则 sudo tee /etc/udev/rules.d/53-lattice-ftdi.rules << 'EOF' ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", MODE="0660", GROUP="plugdev", TAG+="uaccess" EOF # 将当前用户加入 plugdev 组 sudo usermod -aG plugdev $USER # 重新加载规则(或重启) sudo udevadm control --reload-rules sudo udevadm trigger # 重新登录后生效(或 newgrp plugdev)macOS 无需此操作,libftdi 直接使用 IOKit。
安装体积对比
| 工具 | 安装体积 | 备注 |
|---|---|---|
| OSS CAD Suite | ~500 MB | 包含所有开源 EDA 工具 |
| Yosys + nextpnr + IceStorm(apt) | ~50 MB | 最小安装 |
| Xilinx Vivado ML(免费版) | ~35 GB | 仅支持 Xilinx 器件 |
| Intel Quartus Lite | ~5 GB | 仅支持 Intel 器件 |
3. 第一个项目:Blinky
3.1 Verilog 源码
100 MHz 输入(iCEstick 是 12 MHz,但原理相同)→ 时钟分频 → 1 Hz LED 闪烁:
// blinky.v
// iCEstick: 12 MHz 外部时钟,LED 1 Hz 闪烁
// 分频计数器:12_000_000 / 2 = 6_000_000 个时钟周期 = 0.5s(半周期)
module blinky (
input wire clk, // 12 MHz 外部晶振,iCEstick 引脚 21
output reg led // 板载 LED,iCEstick 引脚 99 (LED0)
);
// 分频计数器:需要能数到 6_000_000
// 2^23 = 8_388_608 > 6_000_000,所以用 23 bit
reg [22:0] counter;
always @(posedge clk) begin
if (counter == 23'd5_999_999) begin
counter <= 23'd0;
led <= ~led; // 翻转 LED
end else begin
counter <= counter + 23'd1;
end
end
// 初始化(用于仿真;FPGA 上 counter 和 led 默认从 0 开始)
initial begin
counter = 23'd0;
led = 1'b0;
end
endmodule
3.2 引脚约束文件 .pcf
# blinky.pcf
# iCEstick HX1K 引脚约束
# 参考:https://github.com/YosysHQ/icestorm/blob/master/examples/icestick/icestick.pcf
# 12 MHz 外部晶振
set_io clk 21
# 板载 LED(绿色,低电平点亮?不,iCEstick 是高电平点亮)
# LED0=99, LED1=98, LED2=97, LED3=96, LED4=95
set_io led 99
🚧 避坑 2:.pcf pin 方向与极性
iCEstick 的 LED 是高电平点亮(LED 阳极通过限流电阻接 FPGA 输出,阴极接 GND)。 如果你在其他板子上移植,一定要查原理图确认极性,否则 LED 永远亮或永远灭。
另外,
.pcf的set_io语法中:
set_io -nowarn clk 21可以抑制未约束警告- 不支持像 Vivado
.xdc那样的get_ports语法- 引脚编号是 iCE40 封装引脚号,不是 iCEstick 边缘连接器编号
3.3 完整工具链命令流
# 项目目录结构
mkdir blinky && cd blinky
# 创建 blinky.v 和 blinky.pcf(内容见上)
# ── Step 1: 综合(Yosys)──
# synth_ice40: 针对 iCE40 的综合 pass
# -top: 顶层模块名
# -json: 输出网表格式(供 nextpnr 使用)
yosys -p "read_verilog blinky.v; synth_ice40 -top blinky -json blinky.json"
# 综合输出(关键部分):
# === blinky ===
# Number of cells: 2
# SB_DFF 1 ← LED 寄存器
# SB_LUT4 1 ← 计数器逻辑(大部分被折叠)
# 等等... 实际 stat 见后文
# ── Step 2: 布局布线(NextPnR)──
# --hx1k: iCE40HX1K 器件
# --package tq144: iCEstick 使用 TQFP-144 封装
# --pcf: 引脚约束文件
# --asc: 输出 ASCII 配置文件(IceStorm 格式)
# --freq 12: 时钟频率约束 12 MHz(松弛,blinky 不需要严格时序)
nextpnr-ice40 \
--hx1k \
--package tq144 \
--json blinky.json \
--pcf blinky.pcf \
--asc blinky.asc \
--freq 12
# ── Step 3: 打包 bitstream(icepack)──
# 将 ASCII .asc 格式转换为二进制 .bin
icepack blinky.asc blinky.bin
# ── Step 4: 烧写(iceprog)──
# 写入 SPI Flash(上电自动加载)
iceprog blinky.bin
# 也可以只写 SRAM(断电丢失,适合快速迭代)
iceprog -S blinky.bin
3.4 综合资源报告
Yosys stat 命令输出(blinky):
=== blinky ===
Number of wires: 4
Number of wire bits: 27
Number of public wires: 2
Number of public wire bits: 2
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 20
SB_CARRY 5
SB_DFF 1
SB_DFFE 16 ← 23-bit 计数器的 FF
SB_LUT4 18
Estimated number of LCs: 20
blinky 实际使用:约 20 LUT(1280 总量的 1.6%)
4. UART Echo 示例
4.1 UART TX 模块
// uart_tx.v
// 参数化 UART 发送器
// CLK_FREQ: 时钟频率(Hz)
// BAUD_RATE: 波特率
module uart_tx #(
parameter CLK_FREQ = 12_000_000,
parameter BAUD_RATE = 115200
) (
input wire clk,
input wire rst_n,
input wire [7:0] tx_data, // 要发送的字节
input wire tx_valid, // 数据有效(脉冲)
output reg tx_ready, // 可以接收新数据
output reg tx_pin // 串口 TX 引脚
);
// 波特率分频:12MHz / 115200 ≈ 104 个时钟周期/bit
localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; // = 104
reg [15:0] baud_cnt;
reg [3:0] bit_cnt; // 0=空闲/起始位,1-8=数据位,9=停止位
reg [8:0] shift_reg; // {stop_bit, data[7:0]}
reg sending;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_pin <= 1'b1; // UART 空闲时为高电平
tx_ready <= 1'b1;
sending <= 1'b0;
baud_cnt <= 16'd0;
bit_cnt <= 4'd0;
shift_reg <= 9'h1FF;
end else begin
if (!sending) begin
tx_pin <= 1'b1;
tx_ready <= 1'b1;
if (tx_valid) begin
// 开始发送:起始位(低电平)
shift_reg <= {1'b1, tx_data}; // 停止位 + 数据
tx_pin <= 1'b0; // 起始位
tx_ready <= 1'b0;
sending <= 1'b1;
baud_cnt <= 16'd1;
bit_cnt <= 4'd0;
end
end else begin
if (baud_cnt == BAUD_DIV - 1) begin
baud_cnt <= 16'd0;
if (bit_cnt == 4'd8) begin
// 停止位已发完
tx_pin <= 1'b1;
sending <= 1'b0;
tx_ready <= 1'b1;
end else begin
// 发送数据位(LSB first)
tx_pin <= shift_reg[0];
shift_reg <= {1'b1, shift_reg[8:1]};
bit_cnt <= bit_cnt + 4'd1;
end
end else begin
baud_cnt <= baud_cnt + 16'd1;
end
end
end
end
endmodule
4.2 UART RX 模块
// uart_rx.v
// UART 接收器,3 倍过采样(用于噪声抑制)
module uart_rx #(
parameter CLK_FREQ = 12_000_000,
parameter BAUD_RATE = 115200
) (
input wire clk,
input wire rst_n,
input wire rx_pin, // 串口 RX 引脚
output reg [7:0] rx_data, // 接收到的字节
output reg rx_valid // 数据有效(单周期脉冲)
);
localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; // = 104
localparam HALF_BAUD_DIV = BAUD_DIV / 2; // = 52(对齐到位中央)
// 输入同步化(防止亚稳态)
reg rx_sync0, rx_sync1, rx_prev;
wire rx_fall = rx_prev & ~rx_sync1; // 下降沿 = 起始位
always @(posedge clk) begin
rx_sync0 <= rx_pin;
rx_sync1 <= rx_sync0;
rx_prev <= rx_sync1;
end
reg [15:0] baud_cnt;
reg [3:0] bit_cnt;
reg [7:0] shift_reg;
reg receiving;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
receiving <= 1'b0;
rx_valid <= 1'b0;
baud_cnt <= 16'd0;
bit_cnt <= 4'd0;
shift_reg <= 8'd0;
end else begin
rx_valid <= 1'b0;
if (!receiving) begin
if (rx_fall) begin
// 检测到起始位下降沿
receiving <= 1'b1;
baud_cnt <= 16'd1;
bit_cnt <= 4'd0;
end
end else begin
if (baud_cnt == (bit_cnt == 0 ? HALF_BAUD_DIV : BAUD_DIV) - 1) begin
baud_cnt <= 16'd0;
if (bit_cnt == 4'd0) begin
// 验证起始位(应为低电平)
if (rx_sync1 != 1'b0) begin
receiving <= 1'b0; // 误触发,放弃
end else begin
bit_cnt <= 4'd1;
end
end else if (bit_cnt <= 4'd8) begin
// 采样数据位
shift_reg <= {rx_sync1, shift_reg[7:1]};
bit_cnt <= bit_cnt + 4'd1;
end else begin
// 停止位
receiving <= 1'b0;
if (rx_sync1 == 1'b1) begin
rx_data <= shift_reg;
rx_valid <= 1'b1;
end
end
end else begin
baud_cnt <= baud_cnt + 16'd1;
end
end
end
end
endmodule
4.3 顶层 Echo 模块
// uart_echo.v
// 接收一个字节,立刻原样发回
module uart_echo (
input wire clk, // 12 MHz
input wire rst_n, // 复位(低有效),接按键或上电复位
input wire rx_pin, // UART RX(从 PC 来)
output wire tx_pin, // UART TX(发给 PC)
output wire led // 活动指示灯(收到字节时闪一下)
);
wire [7:0] rx_data;
wire rx_valid;
wire tx_ready;
reg tx_valid_r;
reg [7:0] tx_data_r;
reg [3:0] led_cnt;
uart_rx #(
.CLK_FREQ (12_000_000),
.BAUD_RATE (115200)
) rx_inst (
.clk (clk),
.rst_n (rst_n),
.rx_pin (rx_pin),
.rx_data (rx_data),
.rx_valid (rx_valid)
);
uart_tx #(
.CLK_FREQ (12_000_000),
.BAUD_RATE (115200)
) tx_inst (
.clk (clk),
.rst_n (rst_n),
.tx_data (tx_data_r),
.tx_valid (tx_valid_r),
.tx_ready (tx_ready),
.tx_pin (tx_pin)
);
// Echo 逻辑:收到字节后立刻发回
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_valid_r <= 1'b0;
tx_data_r <= 8'd0;
led_cnt <= 4'd0;
end else begin
tx_valid_r <= 1'b0;
if (rx_valid && tx_ready) begin
tx_data_r <= rx_data;
tx_valid_r <= 1'b1;
led_cnt <= 4'hF; // 点亮 LED
end
if (led_cnt != 4'd0)
led_cnt <= led_cnt - 4'd1;
end
end
assign led = (led_cnt != 4'd0);
endmodule
4.4 UART 引脚约束
# uart_echo.pcf
# iCEstick FTDI Channel B = UART
# Channel B TX = FPGA 引脚 8(发给 PC)
# Channel B RX = FPGA 引脚 9(从 PC 收)
set_io clk 21 # 12 MHz 晶振
set_io rx_pin 9 # FTDI Channel B RXD
set_io tx_pin 8 # FTDI Channel B TXD
set_io rst_n 71 # 可选:接一个按键,或直接接 VCC(默认不复位)
set_io led 99 # LED0
4.5 综合和烧写 UART Echo
# Makefile(推荐)
cat > Makefile << 'EOF'
DESIGN = uart_echo
DEVICE = hx1k
PACKAGE = tq144
FREQ = 12
SRCS = uart_rx.v uart_tx.v uart_echo.v
all: $(DESIGN).bin
$(DESIGN).json: $(SRCS)
yosys -p "read_verilog $(SRCS); synth_ice40 -top $(DESIGN) -json $@"
$(DESIGN).asc: $(DESIGN).json $(DESIGN).pcf
nextpnr-ice40 --$(DEVICE) --package $(PACKAGE) \
--json $(DESIGN).json --pcf $(DESIGN).pcf \
--asc $@ --freq $(FREQ)
$(DESIGN).bin: $(DESIGN).asc
icepack $< $@
prog: $(DESIGN).bin
iceprog $<
clean:
rm -f *.json *.asc *.bin *.log
EOF
make
make prog
UART Echo 综合资源报告:
=== uart_echo ===
Number of cells: 83
SB_CARRY 10
SB_DFF 2
SB_DFFE 56 ← 各种寄存器
SB_LUT4 68
SB_PLL40_CORE 0 ← 没用 PLL
Estimated number of LCs: 78 ≈ 80 LUT(1280 总量的 6%)
4.6 验证:picocom 测试
# 找到 iCEstick 的串口设备
ls /dev/ttyUSB* # Linux: 通常是 /dev/ttyUSB1(Channel B)
ls /dev/tty.usbserial-* # macOS
# 用 picocom 连接(115200 8N1)
picocom -b 115200 --echo /dev/ttyUSB1
# 输入任意字符,应该立刻看到相同字符回显
# Ctrl+A Ctrl+X 退出
# 或用 minicom
minicom -b 115200 -D /dev/ttyUSB1
🚧 避坑 3:nextpnr seed 影响布线结果
NextPnR 的模拟退火放置器是随机算法,默认 seed=1。同一个设计,不同 seed 可能导致:
- 布线成功 vs 布线失败(对于资源紧张的设计)
- 最高频率差异 ±5-10%
解决方法:
# 手动指定 seed(0-65535 任意数) nextpnr-ice40 --seed 42 ... # 或者写一个 seed 扫描脚本(找最优结果) for seed in $(seq 1 10); do nextpnr-ice40 --hx1k --package tq144 \ --json design.json --pcf design.pcf \ --asc design_seed${seed}.asc \ --freq 80 --seed $seed 2>&1 | grep "Max frequency" done对于 blinky 这种简单设计,seed 影响可以忽略。但当设计接近 80% 资源占用时,seed 差异可能意味着成败之别。
5. 与 Vivado 工作流对比
| 对比项 | 全开源(Yosys + NextPnR + IceStorm) | Xilinx Vivado(免费版) |
|---|---|---|
| 安装体积 | ~50 MB(仅命令行工具) | ~35 GB(含 IP 和设备支持) |
| 安装时间 | ~2 分钟 | 2-4 小时 |
| License | 无需(MIT/ISC 许可证) | 需要注册账号,免费版有器件限制 |
| 支持器件 | iCE40、ECP5、Gowin 等 | 仅 Xilinx 器件 |
| blinky 综合时间 | ~0.5 秒 | ~30-60 秒(GUI 启动 + 综合) |
| UART echo 综合时间 | ~1 秒 | ~45 秒 |
| GUI | 无(纯命令行,适合 CI/CD) | 有(复杂但功能完整) |
| Verilog 支持 | Verilog 2005,部分 SV | 完整 SystemVerilog 2012 |
| IP 核 | LiteX 生态 | Vivado IP Catalog(数百种) |
| 时序收敛质量 | 优秀(iCE40 资源充裕时) | 优秀 |
| 最大器件 | iCE40: 8K LUT;ECP5: 85K LUT | Ultrascale+: 2M LUT+ |
| 脚本自动化 | 原生 Makefile/Python | Tcl 脚本 |
| 调试工具 | iceprog + picocom | ILA + VIO + Vivado Analyzer |
结论: 对于 iCE40/ECP5/Gowin 这类器件,开源工具链在功能上完全够用,速度和便利性甚至超过 Vivado(零等待启动、原生 CI)。Vivado 的优势在高端 Xilinx 器件和复杂 IP 集成。
6. 本篇 checklist / 验证步骤
完成本篇后,你应该能独立完成以下步骤:
- 安装 OSS CAD Suite,验证
yosys --version和nextpnr-ice40 --version - 配置 udev rules(Linux),
iceprog无需 sudo 即可运行 - 编译并烧写 blinky,iCEstick 上 LED0 以约 1 Hz 闪烁
- 查看
yosys stat输出,确认 blinky 使用 ~20 LUT - 编译并烧写 uart_echo
- 用 picocom 验证 UART echo(输入字符立刻回显)
- 查看 nextpnr 时序报告,确认
Max frequency > 12 MHz(PASS) - 尝试修改 seed:
nextpnr-ice40 --seed 42 ...,观察输出变化
7. 下一篇预告
开源 FPGA 02:时序收敛实战 将深入时序分析:
- Setup/Hold Slack 公式的直觉理解
- 如何制造一个时序违例(8-bit 乘法器)
- 用
nextpnr --report timing.json提取关键路径 - Python 脚本解析 timing.json,找出 worst path
- 修复手段:流水线、逻辑重排
- 修复前后:Fmax 从 62 MHz 提升到 98 MHz
如果你在本篇的 uart_echo 里加入更复杂的逻辑(比如一个小 FIFO),就会开始遇到真实的时序问题——那正是第 02 篇要解决的。
参考资料
| 资源 | 链接 / 文档号 | 说明 |
|---|---|---|
| Project IceStorm | YosysHQ/icestorm | iCE40 逆向工程,引脚映射文档 |
| nextpnr-ice40 文档 | YosysHQ/nextpnr/docs | .pcf 语法、器件参数 |
| iCEstick 用户手册 | Lattice FPGA-TN-02030 | 原理图、引脚定义、FTDI 配置 |
| Yosys 手册 | yosyshq.readthedocs.io | synth_ice40 pass 详解 |
| OSS CAD Suite | YosysHQ/oss-cad-suite-build | 一键安装包,含所有工具 |
| iCEStick 示例集合 | YosysHQ/icestorm/examples | 官方 blinky、uart 示例 |
| UART 协议标准 | TIA-232-F(RS-232) | 8N1 格式、时序定义 |
| openFPGALoader | trabucayre/openFPGALoader | iceprog 的替代方案,支持更多硬件 |
🦞 Kaiyo 的硬件工程日志
第一次用 Yosys + iceprog 看着 LED 闪起来,那种感觉和用 Vivado 完全不一样——你能感受到整个流程的每一步都是透明的,综合在干什么、布线花了多少秒、资源用了多少,全部一目了然。这就是开源工具的魅力:没有黑箱,只有代码。