← ブログ一覧へ
FPGACI/CDGitHub ActionsDockerYosysnextpnrcocotb自动化开源工具DevOps

开源 FPGA 10|FPGA 的 CI/CD:GitHub Actions 自动综合+仿真+时序报告

この記事は中国語で書かれ、Google 翻訳で自動翻訳されています。
中国語の原文を見る →
   ____ ___     ____ ____  
  / ___|_ _|   / ___|  _ \ 
 | |    | |   | |   | | | |
 | |___ | |   | |___| |_| |
  \____|___|   \____|____/ 

   让每一次 git push 都不破坏时序

系列第 10 篇(收官) · 工具版本:GitHub Actions / Docker / Yosys 0.40 / nextpnr-ice40 0.7 / cocotb 1.9 上一篇:MicroLED 驱动控制器:从需求到 FPGA 实现


0. 这一篇要解决什么问题

前九篇把工具学会了,现在要问一个工程问题:怎么保证每次修改都不破坏已有的功能和时序?

单人项目靠自律。多人项目靠流程。FPGA 设计的一个典型痛点是:某同学改了一个模块,综合时间 10 分钟,等综合结束发现时序违例,才发现是 3 天前引入的——这时 git bisect 就很麻烦了。

CI/CD(持续集成/持续交付) 解决这个问题:每次 PR(或 commit),自动跑综合、仿真、时序分析,不达标则阻止合并。

本篇目标:

  1. 理解 FPGA CI/CD 的价值和挑战
  2. 构建完整 GitHub Actions 流水线(lint → synth → sim → timing)
  3. 自动解析 Yosys/nextpnr 报告,生成 PR comment
  4. 时序不达标自动 fail PR
  5. 分析成本和实际使用建议

本篇不覆盖:

  • GitLab CI/CD(原理相同,YAML 格式略有差异)
  • 硬件在环测试(HIL,需要物理开发板,在本地 runner 上跑)
  • Vivado/Quartus 的 CI 集成(商业工具 License 问题复杂)

1. 为什么 FPGA 需要 CI/CD

没有 CI/CD 的典型痛苦:

Day 1: Alice 修改 FIR 滤波器,手动综合 OK,推送代码
Day 2: Bob 修改 SPI 控制器,综合发现时序违例(Slack = -2.1ns)
       Bob: "是我改的问题还是 Alice 改的?"
       两人各自 checkout,综合验证,花了 3 小时
       
Day 3: 发现是 Alice 修改后关键路径变长了
       但 Alice 的本地综合没有触发(因为她目标频率设的是 25MHz,
       而项目实际要求 50MHz……)

有 CI/CD:
       Alice 推送 → CI 自动跑 50MHz 综合 → Slack 报告
       "-0.3ns,FAIL" → PR 被阻止 → Alice 当天修复

FPGA CI/CD 的价值:

  • 防止时序退化:每次修改都验证时序,不累积负债
  • 文档化资源消耗:每次 PR 自动记录 LUT/FF/BRAM 变化
  • 强制跑仿真:不让”改了但没测”的代码进入主干
  • 团队协作:明确的自动化标准,减少口头约定

2. Docker 镜像准备

使用 hdl/containers 项目的预构建镜像,或者自建:

# Dockerfile.fpga-ci
# 包含 Yosys + nextpnr-ice40 + nextpnr-ecp5 + icestorm + iverilog + cocotb
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

# 安装基础依赖
RUN apt-get update && apt-get install -y \
    build-essential \
    python3 python3-pip \
    iverilog \
    gnat \
    wget curl git \
    libreadline-dev \
    libffi-dev \
    libeigen3-dev \
    tcl-dev \
    && rm -rf /var/lib/apt/lists/*

# 从 OSS CAD Suite 安装预编译工具(最快)
RUN wget -qO oss-cad.tgz \
    https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2024-03-01/oss-cad-suite-linux-x64-20240301.tgz \
    && tar xf oss-cad.tgz -C /opt \
    && rm oss-cad.tgz

ENV PATH="/opt/oss-cad-suite/bin:${PATH}"

# 安装 cocotb
RUN pip3 install cocotb cocotb-bus pytest

# 验证安装
RUN yosys --version && nextpnr-ice40 --version

CMD ["/bin/bash"]
# 构建并推送到 GitHub Container Registry(ghcr.io)
docker build -f Dockerfile.fpga-ci -t ghcr.io/your-org/fpga-ci:latest .
docker push ghcr.io/your-org/fpga-ci:latest

# 也可以直接用 hdl/containers 的现成镜像
# ghcr.io/hdl/pkg/nextpnr:ice40  (包含 ice40 + ECP5 支持)

3. 完整 GitHub Actions Workflow

# .github/workflows/fpga-ci.yml
# FPGA CI/CD:lint → synth → sim → timing

name: FPGA CI

on:
  push:
    branches: [ main, develop ]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'constraints/**'
      - '.github/workflows/fpga-ci.yml'
  pull_request:
    branches: [ main ]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'constraints/**'

# 允许 Actions 写 PR comments
permissions:
  contents: read
  pull-requests: write
  issues: write

jobs:
  # ─────────────────────────────────────────
  # Job 1: Lint(语法检查,最快,先跑)
  # ─────────────────────────────────────────
  lint:
    name: "Lint (Verilog 语法检查)"
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/hdl/pkg/nextpnr:ice40
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Verilog 语法检查(iverilog)
        run: |
          for f in src/*.v; do
            echo "Checking $f..."
            iverilog -Wall -t null -g2005 $f
          done
        
      - name: Verilog 语法检查(verilator lint)
        run: |
          verilator --lint-only -Wall \
            --top-module top \
            src/top.v src/pwm_gen16.v \
            src/spi_slave_ctrl.v src/i2c_master.v \
            src/fault_detect.v

  # ─────────────────────────────────────────
  # Job 2: Synthesis(综合,生成资源报告)
  # ─────────────────────────────────────────
  synthesis:
    name: "Synthesis (Yosys + iCE40HX8K)"
    needs: lint
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/hdl/pkg/nextpnr:ice40
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Yosys 综合
        id: synth
        run: |
          mkdir -p build
          yosys -p "
            read_verilog -sv src/top.v src/pwm_gen16.v \
              src/spi_slave_ctrl.v src/i2c_master.v src/fault_detect.v;
            synth_ice40 -top top -json build/design.json;
            stat -json > build/synth_stat.json
          " 2>&1 | tee build/synth.log
          
          # 提取资源数字
          LUT=$(grep "SB_LUT4" build/synth.log | grep -oP '\d+' | head -1)
          FF=$(grep "SB_DFF\|SB_DFFE" build/synth.log | grep -oP '\d+' | paste -sd '+' | bc)
          BRAM=$(grep "SB_RAM40_4K" build/synth.log | grep -oP '\d+' | head -1)
          
          echo "lut=$LUT" >> $GITHUB_OUTPUT
          echo "ff=$FF"   >> $GITHUB_OUTPUT
          echo "bram=${BRAM:-0}" >> $GITHUB_OUTPUT
          
          echo "### 综合结果" >> $GITHUB_STEP_SUMMARY
          echo "| 资源 | 使用量 | 总量 | 利用率 |" >> $GITHUB_STEP_SUMMARY
          echo "|------|--------|------|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| LUT4 | $LUT | 7680 | $(echo "scale=1; $LUT * 100 / 7680" | bc)% |" >> $GITHUB_STEP_SUMMARY
          echo "| FF   | $FF  | 7680 | $(echo "scale=1; $FF * 100 / 7680" | bc)% |" >> $GITHUB_STEP_SUMMARY
          echo "| BRAM | ${BRAM:-0} | 32 | - |" >> $GITHUB_STEP_SUMMARY

      - name: 上传综合产物
        uses: actions/upload-artifact@v4
        with:
          name: synthesis-outputs
          path: build/

      - name: 输出综合摘要
        run: |
          echo "LUT: ${{ steps.synth.outputs.lut }}"
          echo "FF:  ${{ steps.synth.outputs.ff }}"
          echo "BRAM: ${{ steps.synth.outputs.bram }}"
    
    outputs:
      lut:  ${{ steps.synth.outputs.lut }}
      ff:   ${{ steps.synth.outputs.ff }}
      bram: ${{ steps.synth.outputs.bram }}

  # ─────────────────────────────────────────
  # Job 3: Simulation(cocotb 仿真)
  # ─────────────────────────────────────────
  simulation:
    name: "Simulation (cocotb + iverilog)"
    needs: lint
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/hdl/pkg/nextpnr:ice40
    
    steps:
      - uses: actions/checkout@v4
      
      - name: 安装 cocotb
        run: pip3 install cocotb pytest
      
      - name: 运行 cocotb 仿真测试
        run: |
          cd tests/
          timeout 300 make SIM=icarus TOPLEVEL=pwm_gen16 MODULE=test_pwm \
            COCOTB_RESULTS_FILE=results.xml 2>&1 | tee sim.log
          
          # 检查是否超时
          if [ $? -eq 124 ]; then
            echo "❌ 仿真超时(300 秒)"
            exit 1
          fi
        env:
          PYTHONPATH: "."
      
      - name: 上传 JUnit 测试报告
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: simulation-results
          path: tests/results.xml
      
      - name: 解析测试结果
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: always()
        with:
          files: tests/results.xml
          comment_title: "🧪 仿真测试结果"

  # ─────────────────────────────────────────
  # Job 4: Timing(时序分析,最重要)
  # ─────────────────────────────────────────
  timing:
    name: "Timing (nextpnr @ 50 MHz)"
    needs: synthesis
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/hdl/pkg/nextpnr:ice40
    
    steps:
      - uses: actions/checkout@v4
      
      - name: 下载综合产物
        uses: actions/download-artifact@v4
        with:
          name: synthesis-outputs
          path: build/
      
      - name: nextpnr 布局布线 + 时序分析
        id: timing
        run: |
          mkdir -p build
          nextpnr-ice40 \
            --hx8k \
            --package ct256 \
            --json build/design.json \
            --pcf constraints/hx8k.pcf \
            --asc build/design.asc \
            --freq 50 \
            --placer heap \
            --seed 42 \
            2>&1 | tee build/timing.log
          
          # 提取 Fmax 和 Slack
          FMAX=$(grep "Max frequency" build/timing.log | \
            grep -oP '\d+\.\d+(?= MHz)' | head -1)
          SLACK=$(grep "Slack:" build/timing.log | \
            grep -oP '[+-]?\d+\.\d+(?= ns)' | head -1)
          PASS_FAIL=$(grep "Max frequency" build/timing.log | \
            grep -oP 'PASS|FAIL' | head -1)
          
          echo "fmax=$FMAX"       >> $GITHUB_OUTPUT
          echo "slack=$SLACK"     >> $GITHUB_OUTPUT
          echo "result=$PASS_FAIL" >> $GITHUB_OUTPUT
          
          echo "### 时序分析结果" >> $GITHUB_STEP_SUMMARY
          echo "| 指标 | 值 |" >> $GITHUB_STEP_SUMMARY
          echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
          echo "| 目标频率 | 50 MHz |" >> $GITHUB_STEP_SUMMARY
          echo "| 实测 Fmax | ${FMAX} MHz |" >> $GITHUB_STEP_SUMMARY
          echo "| Slack | ${SLACK} ns |" >> $GITHUB_STEP_SUMMARY
          echo "| 结果 | ${PASS_FAIL} |" >> $GITHUB_STEP_SUMMARY
          
          # 时序不达标则 fail
          if [ "$PASS_FAIL" = "FAIL" ]; then
            echo "❌ 时序违例:Fmax = ${FMAX} MHz(目标 50 MHz),Slack = ${SLACK} ns"
            exit 1
          fi
          echo "✅ 时序通过:Fmax = ${FMAX} MHz,Slack = +${SLACK} ns"
      
      - name: 上传 bitstream
        uses: actions/upload-artifact@v4
        if: success()
        with:
          name: bitstream
          path: build/design.asc
    
    outputs:
      fmax:   ${{ steps.timing.outputs.fmax }}
      slack:  ${{ steps.timing.outputs.slack }}
      result: ${{ steps.timing.outputs.result }}

  # ─────────────────────────────────────────
  # Job 5: PR Comment(汇总报告)
  # ─────────────────────────────────────────
  report:
    name: "PR 报告"
    needs: [synthesis, simulation, timing]
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request' && always()
    
    steps:
      - name: 生成 PR Comment
        uses: actions/github-script@v7
        with:
          script: |
            const synth_lut  = '${{ needs.synthesis.outputs.lut }}'  || 'N/A';
            const synth_ff   = '${{ needs.synthesis.outputs.ff }}'   || 'N/A';
            const synth_bram = '${{ needs.synthesis.outputs.bram }}' || 'N/A';
            const fmax       = '${{ needs.timing.outputs.fmax }}'    || 'N/A';
            const slack      = '${{ needs.timing.outputs.slack }}'   || 'N/A';
            const timing_ok  = '${{ needs.timing.outputs.result }}' === 'PASS';
            const sim_ok     = '${{ needs.simulation.result }}' === 'success';
            
            const badge_timing = timing_ok ?
              '![timing](https://img.shields.io/badge/timing-PASS-brightgreen)' :
              '![timing](https://img.shields.io/badge/timing-FAIL-red)';
            const badge_sim = sim_ok ?
              '![sim](https://img.shields.io/badge/simulation-PASS-brightgreen)' :
              '![sim](https://img.shields.io/badge/simulation-FAIL-red)';
            
            const body = `## 🔧 FPGA CI 报告
            
            ${badge_timing} ${badge_sim}
            
            ### 综合资源占用(iCE40HX8K)
            
            | 资源 | 本次 | 总量 | 利用率 |
            |------|------|------|--------|
            | LUT4 | ${synth_lut} | 7680 | ~${Math.round(synth_lut/7680*100)}% |
            | FF   | ${synth_ff}  | 7680 | ~${Math.round(synth_ff/7680*100)}%  |
            | BRAM | ${synth_bram} | 32 | - |
            
            ### 时序分析(目标 50 MHz)
            
            | 指标 | 值 |
            |------|-----|
            | 最大频率 Fmax | **${fmax} MHz** |
            | Slack | ${slack} ns |
            | 结论 | ${timing_ok ? '✅ PASS' : '❌ FAIL(需要修复时序)'} |
            
            > 使用 \`--seed 42\` 固定随机种子,保证 CI 结果稳定。
            `;
            
            // 查找已有的 CI comment,有则更新,无则新建
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const existing = comments.find(c =>
              c.body.includes('🔧 FPGA CI 报告') &&
              c.user.type === 'Bot'
            );
            
            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body: body
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body: body
              });
            }

4. Yosys 报告解析脚本

#!/bin/bash
# parse_synth_report.sh — 从 Yosys 输出解析资源占用

LOG="$1"  # Yosys 综合日志路径

# 提取各种单元数量
parse_cell() {
    local cell_name="$1"
    grep "$cell_name" "$LOG" | grep -oP '\s+\d+' | tr -d ' ' | tail -1
}

LUT4=$(parse_cell "SB_LUT4")
DFF=$(parse_cell "SB_DFF$")
DFFE=$(parse_cell "SB_DFFE")
BRAM=$(parse_cell "SB_RAM40_4K")
DSP=$(parse_cell "SB_MAC16")
PLL=$(parse_cell "SB_PLL40")

TOTAL_FF=$((${DFF:-0} + ${DFFE:-0}))

echo "==========================="
echo "  Yosys 综合资源报告"
echo "==========================="
printf "  %-12s %6s / %-6s  (%.1f%%)\n" "LUT4"  "${LUT4:-0}"   "7680" \
    "$(echo "scale=1; ${LUT4:-0} * 100 / 7680" | bc)"
printf "  %-12s %6s / %-6s  (%.1f%%)\n" "FF"    "$TOTAL_FF"     "7680" \
    "$(echo "scale=1; $TOTAL_FF * 100 / 7680" | bc)"
printf "  %-12s %6s / %-6s\n"           "BRAM"  "${BRAM:-0}"    "32"
printf "  %-12s %6s / %-6s\n"           "DSP"   "${DSP:-0}"     "8"
printf "  %-12s %6s / %-6s\n"           "PLL"   "${PLL:-0}"     "2"
echo "==========================="
# parse_timing_report.sh — 从 nextpnr 输出解析时序

LOG="$1"
TARGET_MHZ="${2:-50}"

FMAX=$(grep "Max frequency" "$LOG" | grep -oP '\d+\.\d+(?= MHz)' | head -1)
SLACK=$(grep "Slack:" "$LOG" | grep -oP '[+-]?\d+\.\d+(?= ns)' | head -1)
CRIT_PATH=$(grep "Info: curr total" -A 20 "$LOG" | tail -15)

echo "==========================="
echo "  nextpnr 时序分析报告"
echo "==========================="
echo "  目标频率: ${TARGET_MHZ} MHz"
echo "  实测 Fmax: ${FMAX} MHz"
echo "  Slack:    ${SLACK} ns"
if grep -q "PASS at ${TARGET_MHZ}" "$LOG"; then
    echo "  结论:     ✅ PASS"
    exit 0
else
    echo "  结论:     ❌ FAIL(时序违例)"
    echo ""
    echo "  关键路径(最后 15 行):"
    echo "$CRIT_PATH"
    exit 1
fi

5. Badge 生成

在 README.md 中嵌入自动更新的 badge:

<!-- README.md -->
# My FPGA Project

[![FPGA CI](https://github.com/your-org/your-repo/actions/workflows/fpga-ci.yml/badge.svg)](https://github.com/your-org/your-repo/actions/workflows/fpga-ci.yml)

<!-- 动态 badge(需要 shields.io 从 gist 读取) -->
![synthesis](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/your-user/GIST_ID/raw/synth-badge.json)
![timing](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/your-user/GIST_ID/raw/timing-badge.json)
![tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/your-user/GIST_ID/raw/test-badge.json)

在 CI 中更新 badge 数据(写入 GitHub Gist):

# 在 report job 中添加:
- name: 更新 Badge(写入 Gist)
  uses: schneegans/dynamic-badges-action@v1.7.0
  with:
    auth: ${{ secrets.GIST_TOKEN }}
    gistID: YOUR_GIST_ID
    filename: timing-badge.json
    label: timing
    message: "${{ needs.timing.outputs.fmax }} MHz"
    color: ${{ needs.timing.outputs.result == 'PASS' && 'brightgreen' || 'red' }}

6. 成本分析

GitHub Actions 免费额度(2024):
  公开仓库: 无限制(免费)
  私有仓库: 2000 分钟/月(免费 tier)

典型 FPGA CI 任务耗时:

  Job         | 典型耗时 | 说明
  ────────────┼──────────┼──────────────────────
  lint        | ~1 min   | iverilog + verilator
  synthesis   | ~3 min   | Yosys synth_ice40(~500 LUT)
  simulation  | ~5 min   | cocotb + iverilog(10 个测试)
  timing      | ~5 min   | nextpnr(iCE40HX8K)
  report      | ~1 min   | GitHub Script
  ────────────┼──────────┼──────────────────────
  每次 PR 合计 | ~15 min  |

私有仓库每月 2000 分钟 = 133 次 PR/month
对于个人项目或小团队,完全够用。

如果需要更多分钟:
  - 用自托管 Runner(自己的服务器,时间不计入 GitHub 额度)
  - 升级 GitHub Teams:每用户 3000 分钟/月

7. 三大避坑

🚧 避坑 #1:Docker image 体积

OSS CAD Suite 解压后约 3.5 GB。如果每次 CI 都从头下载,单次 CI 光下载就要 5 分钟,成本翻倍。解决方案: (1) 把 OSS CAD Suite 打包进自定义 Docker 镜像,推送到 GitHub Container Registry(ghcr.io),用 image: 字段直接引用(GitHub Actions 会缓存 layer); (2) 或者用 actions/cache 缓存 /opt/oss-cad-suite 目录(按版本号做 cache key)。 推荐方案 (1):一次构建,到处复用,CI 环境稳定可复现。

🚧 避坑 #2:nextpnr seed 不固定导致 CI flaky

nextpnr 的模拟退火放置算法是随机的。不固定 --seed,同样的代码不同次运行可能得到不同的时序结果:今天 PASS,明天 FAIL(纯因为运气不同,代码没变)。这会导致 “flaky CI”——开发者不相信 CI 结果,开始习惯性地重跑。解决:永远在 CI 中加 --seed 42(或任意固定整数),保证结果确定性。本地开发时可以不加 seed(让工具自己探索最优);CI 用固定 seed 保证稳定。

🚧 避坑 #3:仿真超时设置

cocotb 测试如果有无限循环(比如等待一个永远不会来的信号),会挂住 CI,耗尽 runner 时间配额,直到 GitHub 强制终止(默认 6 小时)。两个措施: (1) 在 Makefile 的 make 命令前加 timeout 300(5 分钟超时),超时则返回 exit code 124; (2) 在 cocotb 测试里加 @cocotb.test(timeout_time=1000, timeout_unit='ns') 装饰器,给每个测试设置最大仿真时间。 不要假设仿真测试”一定会结束”。


8. 自托管 Runner(本地跑更快)

# 修改 runs-on 使用本地 Runner
jobs:
  synthesis:
    runs-on: self-hosted  # 用你自己的 Mac mini 或 Linux 服务器
    # 其余不变
# 在本地机器上安装 GitHub Actions Runner
# (适合有 Mac mini 或 NAS 的人,Kevin 你有 Mac mini ✓)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-osx-arm64.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.317.0/actions-runner-osx-arm64-2.317.0.tar.gz
tar xzf actions-runner-osx-arm64.tar.gz

# 注册到你的 GitHub 仓库(需要 PAT Token)
./config.sh --url https://github.com/your-org/your-repo \
            --token YOUR_TOKEN

# 启动 Runner(后台运行)
./run.sh &

自托管 Runner 的好处:

  • 不计入 GitHub 的分钟额度(完全免费)
  • Mac mini 的综合速度比 GitHub 的 Ubuntu runner 快 2-3 倍
  • 可以访问本地开发板做硬件在环测试

9. 验证步骤

# 1. Fork 或创建测试仓库
git init my-fpga-ci-test
cd my-fpga-ci-test

# 2. 复制 src/ 和 constraints/(从第 09 篇的 MicroLED 项目)
mkdir src constraints tests .github/workflows

# 3. 放置 workflow 文件
cp fpga-ci.yml .github/workflows/

# 4. 初次推送,触发 CI
git add .
git commit -m "feat: add FPGA CI/CD pipeline"
git push origin main

# 5. 在 GitHub Actions 页面观察各 job 执行
# https://github.com/your-org/your-repo/actions

# 6. 创建一个破坏时序的 PR,验证 CI 能 fail
# 在 pwm_gen16.v 里加一个不必要的大组合逻辑链
# → PR 应该被 timing job FAIL 阻止

# 7. 验证 PR comment 自动生成
# PR 页面应看到 "🔧 FPGA CI 报告" comment,包含资源和时序表格

10. 系列回顾:10 篇解决了什么问题

《开源 FPGA 实战》系列走完了。下面是一张完整的回顾表:

#主题核心问题关键工具/技术交付物
01iCEstick 开箱点灯开源工具链第一步怎么走Yosys + nextpnr-ice40 + iceprog点灯 + UART Hello
02时序收敛实战Slack 为负怎么修nextpnr 约束、关键路径分析从 FAIL 到 PASS 的完整流程
03形式验证怎么证明 RTL 永不出错SymbiYosys + SMT 求解器FIFO 不溢出的形式证明
04LiteX SoC 深潜怎么用 Python 搭一个 CPU SoCLiteX + VexRiscv + DDR3跑 Linux 的软核 SoC
05cocotb 仿真怎么用 Python 写测试台cocotb + iverilog/verilator替代 Verilog testbench 的 Python 测试框架
06ECP5 + ULX3S中大规模 FPGA 的外设集成TMDS encoder + SD SPI + ESP32HDMI + SD 卡 + ESP32 同时运行
07高云 Tang Nano 9K国产 FPGA 开源工具链apicula + nextpnr-gowin + OSER10国产 FPGA 开源流程全通
08Bambu HLSC→RTL 不花 License 费Bambu HLS + Vitis HLS 对比FIR 滤波器 C→Verilog→iCE40
09MicroLED 控制器FPGA 原型验证真实工程问题SPI + 16×PWM + I2C + 故障检测~500 LUT MicroLED 驱动原型
10CI/CD 流水线怎么防止修改破坏时序GitHub Actions + DockerPR 自动综合+仿真+时序报告

11. 后续学习路线

走完这 10 篇,你已经能用完整的开源工具链做真实的 FPGA 项目了。下面是几条值得继续探索的方向:

A. 更大的 FPGA:Xilinx UltraScale+

iCE40 和 ECP5 是入门器件。真实的高性能应用(HBM AI 推理、100G 网络、PCIe Gen5)需要 Xilinx/AMD 的 UltraScale+。

开源工具对 Xilinx 的支持还很有限,但:

  • nextpnr-xilinx(实验阶段,7 系列)
  • F4PGA / SymbiFlow(Xilinx 7 系列开源流,Google 赞助,可用于 Arty A7)
  • Vivado 的免费版(WebPack)可以跑 Artix-7,功能有限但够用

B. RISC-V SoC 开发

第 04 篇用 LiteX + VexRiscv 搭了基础 SoC。进阶方向:

  • CVA6(Ariane):更完整的 RV64GC 核,有 MMU,能跑 Linux
  • BOOM(Berkeley Out-of-Order Machine):乱序执行,性能接近商业核
  • Rocket Chip:可配置 RISC-V 生成器,学术届用的最广
  • OpenTitan:Google 开源的安全 SoC(带 Root-of-Trust)

C. ASIC 流片

有了 FPGA 原型(第 09 篇),下一步可以考虑真实流片:

  • Efabless MPW:Sky130 工艺,开源项目免费,用 OpenROAD + Magic
  • IHP SG13G2:130nm BiCMOS,IHP(德国研究所)开放 PDK
  • TSMC/SMIC MPW:通过中介(efabless、imec)拼片,约 $5K-20K
  • 工具链:OpenROAD(综合+布局布线)→ Magic(DRC/LVS)→ Klayout(GDS)

D. 形式化验证进阶

第 03 篇介绍了 SymbiYosys 基础。进阶方向:

  • JasperGold / Questa Formal:商业形式验证工具(学习用途有学术授权)
  • CBMC / ESBMC:C 语言形式验证(配合 HLS 用)
  • TLA+:系统级协议验证(适合 SoC 总线协议)

参考资料

资源链接 / 说明
hdl/containers(预构建 Docker 镜像)hdl/containers,包含 yosys/nextpnr/cocotb
OSS CAD SuiteYosysHQ/oss-cad-suite-build
GitHub Actions 文档docs.github.com/actions
nextpnr —seed 说明nextpnr README,“Seed and Reproducibility” 章节
cocotb timeout 装饰器cocotb.readthedocs.io/en/stable/triggers.html
F4PGA(Xilinx 7 系列开源 P&R)f4pga.org,Arty A7 支持
CVA6 RISC-V 核openhwgroup/cva6
Efabless OpenMPWefabless.com/open_shuttle_program
OpenROAD ASIC 工具链openroad.readthedocs.io
EnricoMi/publish-unit-test-result-actionGitHub Actions 测试报告发布 Action

系列完结。感谢跟读到这里的每一位。硬件开源时代已经来临——工具是免费的,器件是买得到的,社区是开放的。去做些有趣的东西吧。🦞