Verilog 编码规范与 CI 自动化

模块例化格式规范

所有模块例化必须遵循严格的每行一个端口格式,端口连接对齐排列。这是最重要的格式规则,因为它直接影响代码审查质量并减少连线错误。

正确格式


// 每行一个端口,连接点对齐在开括号列。
// 点号-名称在左侧,信号名在右侧,行尾带逗号。
// 闭括号独占一行。
axi_write_master #(
    .DATA_WIDTH    (64),
    .ADDR_WIDTH    (32),
    .ID_WIDTH      (4)
) u_axi_master (
    .clk           (clk),
    .rst_n         (rst_n),
    .awvalid       (m_axi_awvalid),
    .awready       (m_axi_awready),
    .awaddr        (m_axi_awaddr),
    .awid          (m_axi_awid),
    .awlen         (m_axi_awlen),
    .awsize        (m_axi_awsize),
    .awburst       (m_axi_awburst),
    .wvalid        (m_axi_wvalid),
    .wready        (m_axi_wready),
    .wdata         (m_axi_wdata),
    .wstrb         (m_axi_wstrb),
    .wlast         (m_axi_wlast),
    .bvalid        (m_axi_bvalid),
    .bready        (m_axi_bready),
    .bid           (m_axi_bid),
    .bresp         (m_axi_bresp)
);

错误格式(禁止使用)


// 不允许这样做 —— 多个端口挤在一行
axi_write_master u_axi_master (.clk(clk), .rst_n(rst_n), .awvalid(m_axi_awvalid),
    .awready(m_axi_awready));

// 不允许这样做 —— 点号未对齐
axi_write_master u_axi_master (
.clk(clk),
.rst_n(rst_n),
.awvalid(m_axi_awvalid)
);

对齐规则

Verible Linter/格式化工具配置

Verible 是 Google 开源 SystemVerilog Linter 和格式化工具。推荐用于在 CI 中强制执行编码规范。

安装


# 下载预编译二进制文件(推荐)
VERIBLE_VER="v0.0-3790-gf524f6b2"
wget https://github.com/chipsalliance/verible/releases/download/${VERIBLE_VER}/verible-${VERIBLE_VER}-linux-x86_64.tar.gz
tar -xzf verible-${VERIBLE_VER}-linux-x86_64.tar.gz -C /home/user/tools/
export PATH="/home/user/tools/verible-${VERIBLE_VER}/bin:$PATH"

# 或从源码构建
git clone https://github.com/chipsalliance/verible.git /home/user/tools/verible_src
cd /home/user/tools/verible_src
bazel build -c opt //...

Linter 配置

创建 lint 规则文件以抑制误报并强制执行项目特定的规则。


# /home/user/work/FPGA_Prj/.verible_lint.rules
# 默认启用所有规则,选择性禁用

waiver-files: ".verible_lint.waiver"

# 规则配置
module-filename: enable
no-trailing-spaces: enable
no-tabs: enable
line-length: "cols:120"
explicit-parameter-storage-type: disable # 并非所有工具都支持
forbid-defparam: enable
generate-label-prefix: enable
always-comb: enable
case-statements-default: enable

创建豁免文件,记录已知且不会修复的违规项:


# /home/user/work/FPGA_Prj/.verible_lint.waiver
# 格式: waiver <<<工具名称>>> <<<规则名称>>> <<<文件名>>>
waive --rule=module-filename --line=1 --location="rtl/fpga_top.v" \
      --reason="顶层模块名称与文件名不同,此为设计安排"

格式化器配置


# /home/user/work/FPGA_Prj/.verible_format.rules
indentation_spaces: 4
column_limit: 120
wrap_end_of_line_comments: true
assignment_statement_alignment: align
try_wrap_long_lines: true
port_declarations_alignment: align
module_net_variable_alignment: align
named_port_alignment: align
named_parameter_alignment: align
formal_parameters_alignment: align
expand_coverpoints: true

运行 Verible


# 对项目中所有 Verilog 文件进行 Lint 检查
verible-verilog-lint \
    --rules_config=.verible_lint.rules \
    --waiver_files=.verible_lint.waiver \
    rtl/*.v rtl/*.sv ip_common/rtl/*.v

# 就地格式化文件
verible-verilog-format \
--rules_config=.verible_format.rules \
--inplace \
rtl/*.v rtl/*.sv

# 仅检查格式而不修改(CI 模式)
verible-verilog-format \
--rules_config=.verible_format.rules \
--verify \
rtl/*.v rtl/*.sv

verible.filelist 文件管理

verible.filelist 是项目中包含哪些文件的唯一权威来源。所有工具(Verilator、Icarus、ModelSim 脚本、Vivado Tcl)都应从此单一文件派生其文件列表。

文件格式


# /home/user/work/FPGA_Prj/Project/WebServer/verible.filelist
# 每行一个文件。# 表示注释。路径相对于项目根目录。

# RTL 源文件
rtl/fpga_top.v
rtl/wb_interconnect.v
rtl/uart_core.v
rtl/gpio_controller.v
rtl/spi_master.v

# IP 公共库(跨项目共享)
../../ip_common/rtl/async_fifo.v
../../ip_common/rtl/sync_ff.v
../../ip_common/rtl/crc32.v
../../ip_common/rtl/gray_counter.v

# AXI 基础设施
../../ip_axi/rtl/axi_write_master.v
../../ip_axi/rtl/axi_read_master.v
../../ip_axi/rtl/axi_crossbar.v

# Testbench(综合排除)
sim/tb_fpga_top.v
sim/tb_uart_core.v

生成工具特定文件列表

使用脚本将 verible.filelist 转换为各工具特定格式:


#!/bin/bash
# /home/user/work/FPGA_Prj/scripts/filelist_gen.sh
# 将 verible.filelist 转换为工具特定格式

PROJECT_DIR="$1"
FLIST="${PROJECT_DIR}/verible.filelist"

if [ ! -f "$FLIST" ]; then
echo "错误: ${PROJECT_DIR} 中未找到 verible.filelist"
exit 1
fi

# 过滤掉注释和 Testbench 文件
SOURCES=$(grep -v '^#' "$FLIST" | grep -v '^$' | grep -v 'sim/')

# --- Vivado Tcl 格式 ---
echo "# 由 verible.filelist 自动生成" > "${PROJECT_DIR}/build/vivado_files.tcl"
while IFS= read -r f; do
echo "add_files -norecurse $f" >> "${PROJECT_DIR}/build/vivado_files.tcl"
done <<< "$SOURCES"

# --- Verilator / Icarus 格式 ---
while IFS= read -r f; do
echo "-I$(dirname $f)" >> "${PROJECT_DIR}/build/incdirs.txt"
done <<< "$SOURCES"

# --- ModelSim .f 文件格式 ---
echo "# 由 verible.filelist 自动生成" > "${PROJECT_DIR}/build/modelsim.f"
echo "$SOURCES" | sed 's/^/vlog +acc +cover /' >> "${PROJECT_DIR}/build/modelsim.f"

echo "已生成: build/vivado_files.tcl, build/incdirs.txt, build/modelsim.f"

Verible 集成

Verible 原生支持读取文件列表:


verible-verilog-lint --rules_config=.verible_lint.rules \
    -f verible.filelist

Git 标签快照规范

每次成功构建(综合通过、仿真通过或比特流生成)都应使用带注释的标签进行标记。

标签格式


snapshot-YYYYMMDDHHmmss

示例:snapshot-20260724153000(2026 年 7 月 24 日 15:30:00)

默认仓库

该规范默认适用于以下 5 个 FPGA 仓库:

创建标签


#!/bin/bash
# /home/user/work/FPGA_Prj/scripts/tag_release.sh
# 为所有 5 个 FPGA 仓库打快照标签

TAG="snapshot-$(date +%Y%m%d%H%M%S)"
MESSAGE="Build snapshot: $(date '+%Y-%m-%d %H:%M:%S')"

REPOS=(
"/home/user/work/FPGA_Prj"
"/home/user/work/ip_common"
"/home/user/work/ip_axi"
"/home/user/work/ip_riscv"
"/home/user/work/fpga_ila"
)

for repo in "${REPOS[@]}"; do
echo "=== 正在为 $repo 打标签 ==="
cd "$repo" || exit 1
if [ -n "$(git status --porcelain)" ]; then
echo " 警告: 存在未提交的更改;先自动提交..."
git add -A
git commit -m "打标签 ${TAG} 前的自动提交"
fi
git tag -a "$TAG" -m "$MESSAGE"
echo " 已打标签: $TAG"
done

echo "=== 所有仓库已打标签 ==="
echo "推送命令: git push origin $TAG (在每个仓库中执行)"

使用标签


# 跨所有仓库检出特定快照
TAG="snapshot-20260724153000"
for repo in "${REPOS[@]}"; do
    cd "$repo" && git checkout "$TAG"
done

# 比较两个快照
cd /home/user/work/FPGA_Prj
git diff snapshot-20260724000000..snapshot-20260724153000 -- rtl/

run_all.sh 自检脚本模式

每个 FPGA 项目都应有一个 run_all.sh,作为 CI 的单一入口点。它运行 Lint、仿真,并可选择运行综合。

模板


#!/bin/bash
# /home/user/work/FPGA_Prj/Project/WebServer/run_all.sh
# 单命令自检: lint + 仿真 + (可选) 构建
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # 无色

PASSED=0
FAILED=0

check_step() {
local desc="$1"
echo -e "${YELLOW}[运行]${NC} $desc"
}

pass_step() {
echo -e "${GREEN}[通过]${NC} $1"
PASSED=$((PASSED + 1))
}

fail_step() {
echo -e "${RED}[失败]${NC} $1"
FAILED=$((FAILED + 1))
}

# ── 步骤 1: Verilator Lint ──
check_step "Verilator lint 检查"
if verilator --lint-only -Wall -Wno-DECLFILENAME \
-I rtl/ -I ../../ip_common/rtl/ -I ../../ip_axi/rtl/ \
-f verible.filelist \
--top-module fpga_top 2>&1 | tee build/lint.log; then
pass_step "Verilator lint"
else
fail_step "Verilator lint"
fi

# ── 步骤 2: Verible 格式检查 ──
check_step "Verible 格式检查"
if command -v verible-verilog-format &>/dev/null; then
if verible-verilog-format --verify -f verible.filelist 2>&1 | tee build/format.log; then
pass_step "Verible 格式"
else
fail_step "Verible 格式 (运行 verible-verilog-format --inplace 修复)"
fi
else
echo -e "${YELLOW}[跳过]${NC} Verible 未安装"
fi

# ── 步骤 3: Icarus Verilog 编译检查 ──
check_step "Icarus 编译检查"
# 仅编译,不仿真
FLIST=()
while IFS= read -r f; do
[[ "$f" =~ ^# ]] && continue
[[ -z "$f" ]] && continue
[[ "$f" =~ ^sim/ ]] && continue
FLIST+=("$f")
done < verible.filelist

if iverilog -g2012 -I rtl/ -I ../../ip_common/rtl/ -I ../../ip_axi/rtl/ \
-o /dev/null "${FLIST[@]}" 2>&1 | tee build/iverilog.log; then
pass_step "Icarus 编译"
else
fail_step "Icarus 编译"
fi

# ── 步骤 4: 仿真(如果 Testbench 存在) ──
if [ -f sim/tb_fpga_top.v ]; then
check_step "仿真 (Icarus)"
iverilog -g2012 -I rtl/ -I ../../ip_common/rtl/ -I ../../ip_axi/rtl/ \
-o build/sim.vvp "${FLIST[@]}" sim/tb_fpga_top.v
if vvp build/sim.vvp 2>&1 | tee build/sim.log; then
pass_step "仿真"
else
fail_step "仿真"
fi
fi

# ── 汇总 ──
echo ""
echo "========================================"
echo -e "结果: ${GREEN}${PASSED} 通过${NC}, ${RED}${FAILED} 失败${NC}"
echo "========================================"

if [ "$FAILED" -gt 0 ]; then
exit 1
fi
exit 0

关键设计原则

批量 CI:跨 5 个 FPGA 仓库

主运行脚本


#!/bin/bash
# /home/user/work/FPGA_Prj/scripts/ci_all.sh
# 在所有 5 个 FPGA 仓库中运行 CI 检查

REPOS=(
"/home/user/work/FPGA_Prj"
"/home/user/work/ip_common"
"/home/user/work/ip_axi"
"/home/user/work/ip_riscv"
"/home/user/work/fpga_ila"
)

TOTAL_PASS=0
TOTAL_FAIL=0
declare -A RESULTS

for repo in "${REPOS[@]}"; do
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ CI: $(basename $repo)"
echo "╚══════════════════════════════════════════╝"

cd "$repo"

if [ ! -f run_all.sh ]; then
echo -e "\033[1;33m[跳过]\033[0m $repo 中没有 run_all.sh"
RESULTS["$repo"]="SKIP"
continue
fi

if bash run_all.sh; then
RESULTS["$repo"]="PASS"
TOTAL_PASS=$((TOTAL_PASS + 1))
else
RESULTS["$repo"]="FAIL"
TOTAL_FAIL=$((TOTAL_FAIL + 1))
fi
done

echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ CI 汇总 ║"
echo "╠══════════════════════════════════════════╣"
for repo in "${REPOS[@]}"; do
printf "║ %-30s %6s ║\n" "$(basename $repo)" "${RESULTS[$repo]}"
done
echo "╠══════════════════════════════════════════╣"
printf "║ %-30s %6s ║\n" "总通过" "$TOTAL_PASS"
printf "║ %-30s %6s ║\n" "总失败" "$TOTAL_FAIL"
echo "╚══════════════════════════════════════════╝"

if [ "$TOTAL_FAIL" -gt 0 ]; then
exit 1
fi

GitHub Actions 集成


# /home/user/work/FPGA_Prj/.github/workflows/fpga-ci.yml
name: FPGA CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
verilator-lint:
runs-on: ubuntu-24.04
strategy:
matrix:
repo: [FPGA_Prj, ip_common, ip_axi, ip_riscv, fpga_ila]
steps:
- uses: actions/checkout@v4
with:
repository: BuckHuang/${{ matrix.repo }}
path: ${{ matrix.repo }}

- name: 安装 Verilator
run: sudo apt install -y verilator iverilog

- name: 安装 Verible
run: |
wget -q https://github.com/chipsalliance/verible/releases/download/v0.0-3790/verible-v0.0-3790-linux-x86_64.tar.gz
tar -xzf verible-*.tar.gz
echo "$PWD/verible-v0.0-3790/bin" >> $GITHUB_PATH

- name: 运行自检
run: |
cd ${{ matrix.repo }}
bash run_all.sh

- name: 上传日志
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-${{ matrix.repo }}
path: ${{ matrix.repo }}/build/*.log

tag-on-success:
needs: verilator-lint
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: 创建快照标签
run: |
TAG="snapshot-$(date +%Y%m%d%H%M%S)"
git tag -a "$TAG" -m "CI snapshot: $(date -Iseconds)"
git push origin "$TAG"

提交前钩子(Pre-Commit Hook)用于格式化


#!/bin/bash
# /home/user/work/FPGA_Prj/.git/hooks/pre-commit
# 提交前自动格式化 Verilog 文件

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(v|sv|vh)$')

if [ -z "$STAGED_FILES" ]; then
exit 0
fi

if ! command -v verible-verilog-format &>/dev/null; then
echo "警告: 未找到 verible-verilog-format,跳过自动格式化"
exit 0
fi

echo "正在自动格式化已暂存的 Verilog 文件..."
for f in $STAGED_FILES; do
verible-verilog-format --inplace "$f"
git add "$f"
done
echo "格式化完成。"

规范汇总

规范 规则
模块例化 每行一个端口,点号对齐,); 独占一行
例化命名 前缀 u_(例如 u_axi_fifo
格式化工具 Verible verible-verilog-format
Lint 工具 Verible verible-verilog-lint + Verilator --lint-only
文件列表 每个项目仓库一个 verible.filelist;工具均从中派生
自检 每个项目根目录有 run_all.sh
Git 标签 snapshot-YYYYMMDDHHmmss 带注释标签
CI GitHub Actions,按 5 个仓库矩阵执行
Pre-commit 自动格式化已暂存的 Verilog 文件