From 447efc4a861302de1ce3dbd83a67581c78e9d9da Mon Sep 17 00:00:00 2001 From: hanzhangliu Date: Fri, 21 Aug 2026 14:56:56 +0800 Subject: [PATCH] release: prepare mpc-soc user kit 0.0.2 --- .github/workflows/ci.yml | 116 +++--------- .github/workflows/pages.yml | 10 +- .github/workflows/user-kit.yml | 179 ++++++++++++++++++ CHANGELOG.md | 32 +++- README.en.md | 19 +- README.md | 18 +- VERSION | 2 +- dev/site-docs.json | 5 +- dev/site/.vitepress/config.mts | 13 +- .../theme/components/DocsDashboard.vue | 3 + dev/site/.vitepress/theme/style.css | 31 ++- dev/user-kit.json | 61 ++++++ dev/user-kit/.gitignore | 11 ++ dev/user-kit/README.en.md | 36 ++++ dev/user-kit/README.md | 32 ++++ docs/cn/architecture.md | 12 +- docs/cn/boot-flow.md | 11 +- docs/cn/ci.md | 18 +- docs/cn/examples/hello.md | 8 +- docs/cn/maintainer-release.md | 24 ++- docs/cn/memory-map.md | 7 +- docs/cn/simulation.md | 28 +-- docs/cn/user-guide.md | 72 ++++--- docs/cn/user-kit.md | 87 +++++++++ docs/en/architecture.md | 15 +- docs/en/boot-flow.md | 17 +- docs/en/ci.md | 22 ++- docs/en/examples/hello.md | 11 +- docs/en/maintainer-release.md | 27 ++- docs/en/memory-map.md | 12 +- docs/en/simulation.md | 30 +-- docs/en/user-guide.md | 76 +++++--- docs/en/user-kit.md | 95 ++++++++++ dv/verilator/Makefile | 51 ++--- dv/verilator/README.md | 20 +- dv/verilator/csrc/sim_main.cpp | 42 +++- mk/common.mk | 57 +----- mk/dev.mk | 18 +- mk/user.mk | 58 +++--- scripts/check_fixed_config.py | 99 ++++++++++ scripts/ci/install_verilator.sh | 39 ++++ scripts/ci/prepare_minimal_ecos_sdk.sh | 86 +++++++++ scripts/export_user_kit.py | 177 +++++++++++++++++ scripts/verilator_regress.py | 71 +++++-- sw/Makefile | 51 ++++- sw/bootrom/README.md | 4 + tests/test_doctor.py | 40 ++++ tests/test_export_user_kit.py | 126 ++++++++++++ tests/test_sim_timeout.py | 113 +++++++++++ tests/test_user_kit_workflow.py | 38 ++++ tests/test_verilator_regress.py | 62 ++++++ 51 files changed, 1907 insertions(+), 385 deletions(-) create mode 100644 .github/workflows/user-kit.yml create mode 100644 dev/user-kit.json create mode 100644 dev/user-kit/.gitignore create mode 100644 dev/user-kit/README.en.md create mode 100644 dev/user-kit/README.md create mode 100644 docs/cn/user-kit.md create mode 100644 docs/en/user-kit.md create mode 100644 scripts/check_fixed_config.py create mode 100755 scripts/ci/install_verilator.sh create mode 100644 scripts/ci/prepare_minimal_ecos_sdk.sh create mode 100644 scripts/export_user_kit.py create mode 100644 tests/test_doctor.py create mode 100644 tests/test_export_user_kit.py create mode 100644 tests/test_sim_timeout.py create mode 100644 tests/test_user_kit_workflow.py create mode 100644 tests/test_verilator_regress.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 738cf04..bf75b34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - name: Install dependencies run: | @@ -34,110 +34,54 @@ jobs: gcc-riscv64-unknown-elf \ make \ python3 \ - python3-yaml \ - verilator + python3-yaml + + - name: Install Verilator 5.050 + run: bash scripts/ci/install_verilator.sh - name: Prepare minimal ECOS SDK headers - run: | - mkdir -p "$ECOS_SDK/include" \ - "$ECOS_SDK/components/libc/include" \ - "$ECOS_SDK/components/libgcc/include" \ - "$ECOS_SDK/lib" - - cat > "$ECOS_SDK/include/hal_sys_uart.h" <<'EOF' - #ifndef HAL_SYS_UART_H - #define HAL_SYS_UART_H - #include - void hal_sys_uart_init(void); - void hal_sys_putchar(char c); - void hal_sys_putstr(char *str); - uint8_t hal_sys_getchar(void); - #endif - EOF - - cat > "$ECOS_SDK/include/hal_gpio.h" <<'EOF' - #ifndef HAL_GPIO_H - #define HAL_GPIO_H - #include - #define GPIO_LEVEL_LOW 0u - #define GPIO_LEVEL_HIGH 1u - void hal_gpio_set_dir(uint32_t value); - uint32_t hal_gpio_get_dir(void); - uint32_t hal_gpio_get_input(void); - uint32_t hal_gpio_get_output(void); - void hal_gpio_set_output(uint32_t value); - void hal_gpio_set_level(uint32_t pin, uint32_t level); - uint32_t hal_gpio_get_level(uint32_t pin); - #endif - EOF - - cat > "$ECOS_SDK/include/hal_timer.h" <<'EOF' - #ifndef HAL_TIMER_H - #define HAL_TIMER_H - #include - void hal_timer_stop(void); - void hal_timer_set_prescale(uint32_t value); - void hal_timer_set_cmp(uint32_t value); - void hal_timer_set_ctrl(uint32_t value); - uint32_t hal_timer_get_stat(void); - void hal_timer_clear(void); - void delay_ms(uint32_t value); - #endif - EOF - - cat > "$ECOS_SDK/include/hal_uart.h" <<'EOF' - #ifndef HAL_UART_H - #define HAL_UART_H - #include - void hal_uart_init(uint32_t baud); - void hal_uart_putchar(char c); - void hal_uart_putstr(const char *str); - int hal_uart_getchar(void); - #endif - EOF - - cat > "$ECOS_SDK/components/libc/include/string.h" <<'EOF' - #ifndef STRING_H - #define STRING_H - #include - void *memcpy(void *dest, const void *src, size_t n); - void *memset(void *s, int c, size_t n); - size_t strlen(const char *s); - int strcmp(const char *s1, const char *s2); - #endif - EOF - - cat > "$ECOS_SDK/components/libc/include/stdio.h" <<'EOF' - #ifndef STDIO_H - #define STDIO_H - int printf(const char *format, ...); - int puts(const char *s); - #endif - EOF - - cat > "$ECOS_SDK/components/libgcc/include/libgcc.h" <<'EOF' - #ifndef LIBGCC_H - #define LIBGCC_H - #endif - EOF + run: bash scripts/ci/prepare_minimal_ecos_sdk.sh "$ECOS_SDK" - name: Check Python scripts run: python3 -m py_compile scripts/*.py + - name: Run unit tests + run: python3 -m unittest discover -s tests -v + - name: Check bilingual documentation run: make -f Makefile.dev docs-check - name: Check generated SoC package is current run: | - make gen-soc-pkg + make -f Makefile.dev gen-soc-pkg git diff --exit-code hw/include/soc_pkg.sv + - name: Check fixed 50 MHz configuration + run: make -f Makefile.dev config-check + - name: Build software from source run: make -C sw BOARD=mpc-soc APP=hello + - name: Check software configuration rebuilds + run: | + make -C sw BOARD=mpc-soc APP=hello LINK_TARGET=mem + grep -q '^#define CONFIG_LINK_TARGET_MEM 1$' build/sw/mpc-soc/hello/generated/autoconf.h + test "$(riscv64-unknown-elf-readelf -h build/sw/mpc-soc/hello/hello.elf | awk '/Entry point address:/ {print $4}')" = "0xc0000000" + make -C sw BOARD=mpc-soc APP=hello LINK_TARGET=xip + grep -q '^#define CONFIG_LINK_TARGET_XIP 1$' build/sw/mpc-soc/hello/generated/autoconf.h + test "$(riscv64-unknown-elf-readelf -h build/sw/mpc-soc/hello/hello.elf | awk '/Entry point address:/ {print $4}')" = "0x30000000" + - name: Build Verilator simulator run: make verilate TRACE=0 + - name: Run source-built hello smoke + run: | + make -C dv/verilator sim \ + BOOTROM_IMAGE="$PWD/build/sw/mpc-soc/hello/hello.bin" \ + UART_STOP_TEXT='done!' \ + ALLOW_TIMEOUT=0 \ + TRACE=0 + - name: Run bootrom regression run: make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8e6631b..f170fef 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -47,23 +47,23 @@ jobs: timeout-minutes: 10 steps: - name: Check out source - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 with: fetch-depth: 0 - name: Set up Node - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 with: node-version: 24 cache: npm cache-dependency-path: dev/site/package-lock.json - name: Set up Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b - name: Install dependencies run: npm --prefix dev/site ci - name: Validate and build documentation run: make -f Makefile.dev docs-site-check - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b with: path: build/docs-site/.vitepress/dist @@ -79,4 +79,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e diff --git a/.github/workflows/user-kit.yml b/.github/workflows/user-kit.yml new file mode 100644 index 0000000..02757af --- /dev/null +++ b/.github/workflows/user-kit.yml @@ -0,0 +1,179 @@ +name: User Kit + +'on': + pull_request: + branches: [main] + workflow_dispatch: {} + workflow_run: + workflows: [CI] + types: [completed] + branches: [main] + +permissions: + contents: read + +concurrency: + group: user-kit-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + export_and_test: + if: >- + github.event_name != 'workflow_run' || + ( + github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.head_repository.full_name == github.repository + ) + runs-on: ubuntu-24.04 + timeout-minutes: 45 + outputs: + soc_version: ${{ steps.release.outputs.version }} + env: + SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + steps: + - name: Check out tested source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + ref: ${{ env.SOURCE_SHA }} + + - name: Read release version + id: release + shell: bash + run: | + set -euo pipefail + version="$(tr -d '[:space:]' < VERSION)" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "Invalid VERSION: $version" >&2 + exit 2 + } + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + make \ + python3 \ + python3-yaml + + - name: Install Verilator 5.050 + run: bash scripts/ci/install_verilator.sh + + - name: Export user kit + run: make -f Makefile.dev export-user-kit + + - name: Verify user and maintainer boundaries + run: | + test ! -e build/user-kit/.github + test ! -e build/user-kit/dev + test ! -e build/user-kit/Makefile.dev + test ! -e build/user-kit/mk/dev.mk + test ! -e build/user-kit/tests + test ! -e build/user-kit/sw/Makefile + test ! -e build/user-kit/sw/ecos + test ! -e build/user-kit/sw/ecos.mk + test -f build/user-kit/SOC_KIT_VERSION + test -f build/user-kit/sw/bootrom/hello/test.yml + test -f build/user-kit/sw/bootrom/hello/retrosoc_fw.bin + test "$(find build/user-kit/sw/bootrom -mindepth 1 -maxdepth 1 -type d -printf '%f\n')" = "hello" + + - name: Test fixed-image user workflow + run: | + make -C build/user-kit doctor + make -C build/user-kit sim TRACE=0 + make -C build/user-kit check CASE=hello TRACE=0 + + - name: Clean tested user kit + run: | + rm -rf build/user-kit/build + test ! -e build/user-kit/build + test ! -e build/user-kit/Makefile.dev + test ! -e build/user-kit/dev + + - name: Upload versioned user kit + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: mpc-soc-user-kit-${{ steps.release.outputs.version }} + path: build/user-kit/ + if-no-files-found: error + include-hidden-files: true + retention-days: 14 + + publish: + if: >- + github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.head_repository.full_name == github.repository + needs: export_and_test + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + env: + SOC_VERSION: ${{ needs.export_and_test.outputs.soc_version }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + GH_TOKEN: ${{ github.token }} + steps: + - name: Check out tested source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Download tested user kit + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: mpc-soc-user-kit-${{ needs.export_and_test.outputs.soc_version }} + path: build/user-kit + + - name: Publish new User Kit version + shell: bash + run: | + set -euo pipefail + tag="user-kit-v${SOC_VERSION}" + if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + echo "${tag} already exists; keeping the published version unchanged." + exit 0 + fi + + export_dir="$PWD/build/user-kit" + staging_dir="$(mktemp -d)" + cp -a "$export_dir/." "$staging_dir/" + + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git switch --orphan release-user-kit-work + git rm -rf --ignore-unmatch . + cp -a "$staging_dir/." . + git add -A + git commit -m "user-kit: release ${SOC_VERSION} from ${SOURCE_SHA}" + git tag -a "$tag" -m "mpc-soc User Kit ${SOC_VERSION}" + git push --atomic origin \ + +HEAD:refs/heads/release/user-kit \ + "refs/tags/${tag}" + + - name: Publish immutable release archive + shell: bash + run: | + set -euo pipefail + tag="user-kit-v${SOC_VERSION}" + if gh release view "$tag" >/dev/null 2>&1; then + echo "GitHub Release ${tag} already exists." + exit 0 + fi + + git fetch --force origin "+refs/tags/${tag}:refs/tags/${tag}" + archive="$RUNNER_TEMP/mpc-soc-user-kit-${SOC_VERSION}.tar.gz" + git archive --format=tar.gz \ + --prefix="mpc-soc-user-kit-${SOC_VERSION}/" \ + --output="$archive" "$tag" + gh release create "$tag" "$archive" \ + --verify-tag \ + --title "mpc-soc User Kit ${SOC_VERSION}" \ + --notes "Fixed-image core integration User Kit for mpc-soc ${SOC_VERSION}." diff --git a/CHANGELOG.md b/CHANGELOG.md index b94a6dc..c9368e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,35 @@ 本文件记录 `mpc-soc` SoC 交付版本。版本说明以中文为第一语言;英文 README 仅作为辅助入口。 -## Unreleased +## 0.0.2 - 2026-08-20 -- 文档迁到 `docs/cn/` 与 `docs/en/`,根 README 改为中文优先的用户入口。 -- 用户命令收敛到 `make doctor/check/sim/trace/wave`,维护者回归和文档站改走 `Makefile.dev`。 -- 文档站改为 VitePress 工作台,并继续展示 SoC 规格、IP 状态和地址空间。 +### 版本定位 + +`0.0.2` 首次提供由 CI 生成的独立 User Kit。开发者继续在 `main` 维护完整 SoC、 +软件和回归环境;core 接入用户从 `release/user-kit` 获取精简、已验证的发行环境。 + +### User Kit + +- 用户修改面收敛为 core RTL、AXI wrapper、filelist 和必要的 core 槽位连接。 +- User Kit 只保留固定 `hello` `.bin` 镜像,不提供 ECOS SDK、软件或驱动编译入口。 +- `make check`、`make sim` 和 `make trace` 均使用发行包中的固定镜像。 +- `make lint` 作为独立 RTL/filelist 检查目标,仿真和 lint 目标保持分离。 +- 文档按用户发行边界裁剪,不导出维护者命令、软件流程和其他回归示例。 + +### 配置与验证 + +- 当前版本继续使用固化地址空间和 50 MHz 时钟;配置文件不作为用户重定向 + `asic_top.v` 的入口。 +- 超时仅在测试显式允许时才可通过,已经配置的 UART、GPIO 等通过条件不会被 + `ALLOW_TIMEOUT` 覆盖。 +- 完整 CI 继续运行默认 `CORE_SEL=0` 和 22 个固定 `.bin` 回归用例。 + +### 发行机制 + +- Pull Request 独立导出并验证 User Kit;正式发布只接受仓库自身 `main` push 对应的 + 同一 SHA 完整 CI 成功结果。 +- `release/user-kit` 指向最新版本,每个版本同时创建不可变的 + `user-kit-v` 标签和 GitHub Release 压缩包。 ## 0.0.1 - 2026-07-16 diff --git a/README.en.md b/README.en.md index 4c197ef..7c4d0d4 100644 --- a/README.en.md +++ b/README.en.md @@ -4,7 +4,7 @@ [![CI](https://github.com/openecos-projects/mpc-soc/actions/workflows/ci.yml/badge.svg)](https://github.com/openecos-projects/mpc-soc/actions/workflows/ci.yml) -Current version: `0.0.1`. See the Chinese-first release notes in +Current version: `0.0.2`. See the Chinese-first release notes in [CHANGELOG.md](CHANGELOG.md). `mpc-soc` is a simulatable RISC-V SoC board for multi-project chips. The @@ -14,10 +14,23 @@ Documentation site: [mpc-soc docs](https://openecos-projects.github.io/mpc-soc/) ## User workflow +Regular users should not start from the development `main` branch. Begin with +the CI-generated release branch: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +git switch -c user/ +``` + +See [Getting and using the User Kit](docs/en/user-kit.md). + ```sh make doctor make check -make sim APP=hello +make lint +make sim make trace make wave ``` @@ -30,7 +43,7 @@ select it with `CORE_SEL`. Follow the ## Repository boundaries - `config/`, `hw/`, `dv/`, and `sw/`: SoC configuration, hardware, simulation, - and software. + and maintainer software. - `docs/cn/` and `docs/en/`: path-matched bilingual documentation sources. - `mk/` and `Makefile`: stable user build interface. - `Makefile.dev`: regression, documentation, and generated-file maintenance. diff --git a/README.md b/README.md index 9141cba..0dc8073 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![CI](https://github.com/openecos-projects/mpc-soc/actions/workflows/ci.yml/badge.svg)](https://github.com/openecos-projects/mpc-soc/actions/workflows/ci.yml) -当前版本:`0.0.1`。版本说明见 [CHANGELOG.md](CHANGELOG.md)。 +当前版本:`0.0.2`。版本说明见 [CHANGELOG.md](CHANGELOG.md)。 `mpc-soc` 给多项目芯片提供可仿真、可跑软件的 RISC-V SoC 底板。当前工具基线是 Verilator 5.050。 @@ -13,10 +13,22 @@ Verilator 5.050。 ## 用户流程 +普通用户不要直接使用开发分支 `main`。从 CI 生成的发行分支开始: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +git switch -c user/ +``` + +详见 [User Kit 获取与使用](docs/cn/user-kit.md)。 + ```sh make doctor make check -make sim APP=hello +make lint +make sim make trace make wave ``` @@ -27,7 +39,7 @@ make wave ## 仓库边界 -- `config/`、`hw/`、`dv/`、`sw/`:SoC 配置、硬件、仿真和软件。 +- `config/`、`hw/`、`dv/`、`sw/`:SoC 配置、硬件、仿真和维护者软件。 - `docs/cn/`、`docs/en/`:路径一一对应的双语文档源。 - `mk/`、`Makefile`:稳定的用户构建入口。 - `Makefile.dev`:回归、文档站和生成物检查等维护入口。 diff --git a/VERSION b/VERSION index 8acdd82..4e379d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 +0.0.2 diff --git a/dev/site-docs.json b/dev/site-docs.json index 47304cb..1767f05 100644 --- a/dev/site-docs.json +++ b/dev/site-docs.json @@ -1,14 +1,13 @@ { "pages": [ "index.md", + "user-kit.md", "user-guide.md", "architecture.md", "simulation.md", - "software.md", "memory-map.md", "ip-readiness.md", "boot-flow.md", - "examples/hello.md", - "examples/psram.md" + "examples/hello.md" ] } diff --git a/dev/site/.vitepress/config.mts b/dev/site/.vitepress/config.mts index cd92f9c..63e083d 100644 --- a/dev/site/.vitepress/config.mts +++ b/dev/site/.vitepress/config.mts @@ -13,11 +13,10 @@ const cnSidebar = [ { text: '用户接入', items: [ + { text: '获取 User Kit', link: '/user-kit' }, { text: '接入指南', link: '/user-guide' }, { text: '仿真与波形', link: '/simulation' }, - { text: '软件流程', link: '/software' }, - { text: 'Hello 冒烟', link: '/examples/hello' }, - { text: 'PSRAM 访问', link: '/examples/psram' } + { text: 'Hello 冒烟', link: '/examples/hello' } ] } ] @@ -35,11 +34,10 @@ const enSidebar = [ { text: 'User workflow', items: [ + { text: 'Get the User Kit', link: '/en/user-kit' }, { text: 'Integration guide', link: '/en/user-guide' }, { text: 'Simulation and waveforms', link: '/en/simulation' }, - { text: 'Software flow', link: '/en/software' }, - { text: 'Hello smoke', link: '/en/examples/hello' }, - { text: 'PSRAM access', link: '/en/examples/psram' } + { text: 'Hello smoke', link: '/en/examples/hello' } ] } ] @@ -56,7 +54,8 @@ export default defineConfig({ }, head: [ ['meta', { name: 'theme-color', content: '#0a8f7a' }], - ['meta', { name: 'color-scheme', content: 'light dark' }] + ['meta', { name: 'color-scheme', content: 'light dark' }], + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/mpc-soc/mark.svg' }] ], markdown: { lineNumbers: true, diff --git a/dev/site/.vitepress/theme/components/DocsDashboard.vue b/dev/site/.vitepress/theme/components/DocsDashboard.vue index 9ba613e..8e8660c 100644 --- a/dev/site/.vitepress/theme/components/DocsDashboard.vue +++ b/dev/site/.vitepress/theme/components/DocsDashboard.vue @@ -8,6 +8,7 @@ import { CheckCircle2, Cpu, FlaskConical, + PackageOpen, TerminalSquare } from '@lucide/vue' import { SOC_DATA } from '../soc-data' @@ -66,12 +67,14 @@ const copy = isEn const docs = isEn ? [ + { icon: PackageOpen, label: 'Get the User Kit', detail: 'Clone the CI-tested user release', to: `${base}/user-kit` }, { icon: BookOpen, label: 'User guide', detail: 'Attach a core and run SoC simulation', to: `${base}/user-guide` }, { icon: Cpu, label: 'Architecture', detail: 'asicTop, SimTop, and the AXI fabric', to: `${base}/architecture` }, { icon: Box, label: 'Memory map', detail: 'Flash, PSRAM, and peripheral windows', to: `${base}/memory-map` }, { icon: FlaskConical, label: 'IP readiness', detail: 'Smoke status and verification limits', to: `${base}/ip-readiness` } ] : [ + { icon: PackageOpen, label: '获取 User Kit', detail: '拉取经过 CI 验证的用户发行版', to: `${base}/user-kit` }, { icon: BookOpen, label: '用户接入', detail: '把 core 接到现有槽位并跑仿真', to: `${base}/user-guide` }, { icon: Cpu, label: '架构说明', detail: 'asicTop、SimTop 与 AXI 互连', to: `${base}/architecture` }, { icon: Box, label: '地址空间', detail: 'Flash、PSRAM 和外设窗口', to: `${base}/memory-map` }, diff --git a/dev/site/.vitepress/theme/style.css b/dev/site/.vitepress/theme/style.css index 3500cc2..ab30022 100644 --- a/dev/site/.vitepress/theme/style.css +++ b/dev/site/.vitepress/theme/style.css @@ -50,7 +50,7 @@ } .docs-dashboard { - max-width: 1440px; + max-width: 1320px; margin: 0 auto; padding: 48px 32px 80px; } @@ -327,6 +327,11 @@ border-right: 1px solid var(--vp-c-divider); } +.doc-link:last-child:nth-child(odd) { + grid-column: 1 / -1; + border-right: 0; +} + .doc-link > svg:first-child { color: var(--vp-c-brand-1); } @@ -474,6 +479,15 @@ .vp-doc :not(pre) > code { border: 1px solid var(--vp-c-divider); + overflow-wrap: anywhere; + word-break: break-word; +} + +.vp-doc p, +.vp-doc li, +.vp-doc td, +.vp-doc th { + overflow-wrap: anywhere; } /* A documentation page is narrower than the viewport because it has two rails. */ @@ -539,6 +553,7 @@ .dashboard-grid { grid-template-columns: 1fr; } .doc-links { grid-template-columns: 1fr; } + .doc-link:last-child:nth-child(odd) { grid-column: auto; } .doc-link:nth-child(odd) { border-right: 0; } .selection-timeline { @@ -562,10 +577,16 @@ .soc-overview { display: grid; + grid-template-columns: minmax(0, 1fr); gap: 28px; + min-width: 0; margin-top: 34px; } +.soc-overview > div { + min-width: 0; +} + .overview-heading { display: flex; align-items: end; @@ -582,6 +603,9 @@ .ip-table-shell { overflow: auto; + width: 100%; + max-width: 100%; + min-width: 0; border: 1px solid var(--vp-c-divider); border-radius: 6px; background: var(--vp-c-bg-elv); @@ -732,6 +756,11 @@ } @media (max-width: 760px) { + .overview-heading { + align-items: stretch; + flex-direction: column; + } + .memory-grid { grid-template-columns: 1fr; } diff --git a/dev/user-kit.json b/dev/user-kit.json new file mode 100644 index 0000000..cf63cbf --- /dev/null +++ b/dev/user-kit.json @@ -0,0 +1,61 @@ +{ + "files": [ + "LICENSE", + "VERSION", + "Makefile", + "mk/common.mk", + "mk/user.mk", + "dv/verilator/Makefile", + "dv/verilator/README.md", + "dv/verilator/csrc/sim_main.cpp", + "scripts/verilator_regress.py" + ], + "trees": [ + "config", + "hw/common/rtl", + "hw/filelist", + "hw/include", + "hw/ip/archinfo/rtl", + "hw/ip/clint/rtl", + "hw/ip/core", + "hw/ip/crc/rtl", + "hw/ip/flash/model", + "hw/ip/gpio/rtl", + "hw/ip/i2c/rtl", + "hw/ip/plic/rtl", + "hw/ip/psram/model/esp_psram64h", + "hw/ip/pwm/rtl", + "hw/ip/rcu/rtl", + "hw/ip/rng/rtl", + "hw/ip/rtc/rtl", + "hw/ip/spi/legacy_apb/rtl", + "hw/ip/spi/rtl", + "hw/ip/timer/rtl", + "hw/ip/uart/rtl", + "hw/ip/uart16550/rtl", + "hw/ip/wdg/rtl", + "hw/soc/top", + "sw/bootrom/hello" + ], + "public_docs": { + "manifest": "dev/site-docs.json", + "exclude": [ + "index.md" + ] + }, + "overrides": [ + { + "source": "dev/user-kit/README.md", + "destination": "README.md" + }, + { + "source": "dev/user-kit/README.en.md", + "destination": "README.en.md" + }, + { + "source": "dev/user-kit/.gitignore", + "destination": ".gitignore" + } + ], + "metadata": "SOC_KIT_VERSION" +} diff --git a/dev/user-kit/.gitignore b/dev/user-kit/.gitignore new file mode 100644 index 0000000..9798f83 --- /dev/null +++ b/dev/user-kit/.gitignore @@ -0,0 +1,11 @@ +/build/ + +*.fst +*.log +*.map +*.o +*.pyc + +__pycache__/ +obj_dir/ +third_party/ecos-sdk/ diff --git a/dev/user-kit/README.en.md b/dev/user-kit/README.en.md new file mode 100644 index 0000000..08a146d --- /dev/null +++ b/dev/user-kit/README.en.md @@ -0,0 +1,36 @@ +# mpc-soc User Kit + +[中文说明](README.md) + +This is the trimmed `mpc-soc` environment for core integration users. It +contains the fixed SoC, core wrapper integration points, the Verilator +simulation flow, and a fixed `hello` `.bin` image. Software SDK and driver builds, +maintainer CI, the documentation site, and the full archived regression are +intentionally excluded. + +## Getting started + +```sh +make doctor +make check +make lint +make sim +``` + +`make check` and `make sim` validate the SoC with the bundled fixed `hello` +image. `make lint` checks the integrated RTL and file list. Users can write +their own core RTL, wrapper, and required slot connections, but this release +does not provide a software or driver build environment. + +See the [user integration guide](docs/en/user-guide.md), the fixed +[memory map](docs/en/memory-map.md), and +[getting and updating the User Kit](docs/en/user-kit.md). + +`SOC_KIT_VERSION` records the release format, SoC version, and corresponding +maintainer source commit. `release/user-kit` points to the latest release; +each version also has an immutable `user-kit-v` tag and GitHub Release +archive. Create your own development branch first: + +```sh +git switch -c user/ +``` diff --git a/dev/user-kit/README.md b/dev/user-kit/README.md new file mode 100644 index 0000000..cd7c0e8 --- /dev/null +++ b/dev/user-kit/README.md @@ -0,0 +1,32 @@ +# mpc-soc User Kit + +[English](README.en.md) + +这是 `mpc-soc` 面向 core 接入用户的精简发行环境。它包含固定 SoC、core wrapper +接入点、Verilator 仿真链路和固定 `hello` `.bin` 镜像,不包含软件 SDK、驱动构建、维护者 +CI、文档站或全量归档回归。 + +## 开始使用 + +```sh +make doctor +make check +make lint +make sim +``` + +`make check` 和 `make sim` 使用随发行包提供的固定 `hello` 镜像验证 SoC; +`make lint` 检查接入后的 RTL 和 filelist。用户可以编写自己的 core RTL、wrapper +及必要的槽位连接,但当前版本不提供软件或驱动编译环境。 + +core 接入流程见[用户接入指南](docs/cn/user-guide.md),固化地址空间见 +[内存映射](docs/cn/memory-map.md),User Kit 获取和升级方式见 +[User Kit 获取与使用](docs/cn/user-kit.md)。 + +`SOC_KIT_VERSION` 记录发行格式、SoC 版本和对应的维护者源码提交。上游 +`release/user-kit` 指向最新发行版;每个版本同时提供不可变的 `user-kit-v` +标签和 GitHub Release 压缩包。请先创建自己的开发分支: + +```sh +git switch -c user/ +``` diff --git a/docs/cn/architecture.md b/docs/cn/architecture.md index 37a9afb..99beea3 100644 --- a/docs/cn/architecture.md +++ b/docs/cn/architecture.md @@ -16,15 +16,19 @@ ## 配置来源 -- `config/soc.yml` 描述模板 SoC 顶层和选用的 IP 路径。 -- `config/memory.yml` 是生成 RTL package 和 C 头文件的内存映射来源。 -- `config/boards/sim.yml` 描述仿真板级默认配置。 +- `config/soc.yml` 记录当前 SoC 顶层、IP 路径和固定的 50 MHz 时钟元数据。 +- `config/memory.yml` 记录当前版本的固定地址映射,并生成 RTL package。 +- `config/boards/sim.yml` 记录仿真板级的 50 MHz 默认配置。 + +这些配置在当前版本中是随固化 `asic_top.v` 发布的描述信息,不是用户可重定向 SoC +地址或时钟的配置入口。维护者 CI 会检查各处 50 MHz 固定值没有漂移。 ## 集成流程 1. SoC 集成顶层保持在 `hw/soc/top/` 下;新增或重命名模块时同步更新 `hw/filelist/verilator.f`。 2. 顶层总线、时钟、复位和中断连线放在 `hw/soc/` 下。 3. 同步更新 `hw/filelist/soc.f` 和 `hw/filelist/verilator.f`。 -4. 内存映射变化时,从 `config/memory.yml` 重新生成 `hw/include/soc_pkg.sv`,并同步 `sw/ecos/board.h`。 +4. 当前版本保持固化的地址空间和 50 MHz 时钟;不要通过修改配置文件尝试重定向 + `asic_top.v`。可配置地址空间留到后续版本实现完整链路后支持。 CPU 替换或 bring-up 工作请遵循 [用户接入指南](user-guide.md) 中的 core 槽位和 AXI 接口约定。 diff --git a/docs/cn/boot-flow.md b/docs/cn/boot-flow.md index d88611f..8333835 100644 --- a/docs/cn/boot-flow.md +++ b/docs/cn/boot-flow.md @@ -2,16 +2,15 @@ [English](../en/boot-flow.md) -Verilator 流程从 `build/sw///` 读取软件原始二进制镜像,并通过 -`+bootrom=` 传给仿真 harness。 +User Kit 的 Verilator 流程读取固定的 +`sw/bootrom/hello/retrosoc_fw.bin`,并通过 `+bootrom=` 传给仿真 harness。 ## 模板流程 1. 复位后进入 SoC 启动地址,取指路径走 SPI flash。 2. 仿真 harness 通过 `+bootrom=` 传入原始 flash 镜像路径。 3. RTL 发起 SPI flash 读访问时,flash DPI 模型从该镜像读取字节。 -4. `sw/ecos/start.S` 中的启动代码初始化栈、清零 `.bss`,然后调用 `main`。 -5. 平台驱动使用 `sw/ecos/` 下的 board-package 头文件。 +4. 固定镜像中的启动代码初始化运行环境并执行 `hello`。 +5. 用户接入的 core 通过现有总线和外设路径运行同一个镜像。 -Verilator 仿真应使用 `.bin` 镜像。传入 ELF 文件时,仿真会把 ELF 容器字节当作 -flash 内容加载。 +当前版本不提供软件 SDK 或驱动构建入口,也不支持替换发行包中的固定镜像。 diff --git a/docs/cn/ci.md b/docs/cn/ci.md index e14a6bc..4a60674 100644 --- a/docs/cn/ci.md +++ b/docs/cn/ci.md @@ -6,21 +6,35 @@ 根工程使用 `.github/workflows/ci.yml` 执行 Ubuntu 24.04 单平台 CI。文档站使用 独立的 `.github/workflows/pages.yml`,以 Node.js 构建 VitePress 静态文件。 +`.github/workflows/user-kit.yml` 导出并测试用户环境。 ## 自动门禁 提交到 `main` 或向 `main` 提交 pull request 时运行: - Python 脚本语法检查 -- `make gen-soc-pkg` 后确认 `hw/include/soc_pkg.sv` 无差异 +- `make -f Makefile.dev gen-soc-pkg` 后确认 `hw/include/soc_pkg.sv` 无差异 +- 检查当前版本的配置、RTL package 和 BSP 默认时钟均为固定 50 MHz - `make -f Makefile.dev docs-check` -- 软件构建:`make sw APP=hello` +- 维护者软件构建:`make -C sw BOARD=mpc-soc APP=hello` +- XIP 与 MEM 链接目标的软件配置重建检查 - Verilator 仿真器构建:`make verilate TRACE=0` +- 使用 `done!` 通过条件运行本次源码构建的 `hello` 镜像 - bootrom 回归:`make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0` - 确认未产生应提交的未跟踪文件 +`make lint` 是独立的用户/维护者检查目标,当前不属于 CI 门禁;CI 仍只构建并运行 +默认 `CORE_SEL=0` 示例 core。 + ## 文档站部署 文档、主题或站点构建脚本变化时,`Documentation Pages` 工作流先运行 `make -f Makefile.dev docs-site-check`。pull request 只构建并验证站点;合并到 `main` 后部署到 `https://openecos-projects.github.io/mpc-soc/`。 + +## User Kit 发布 + +User Kit workflow 在 pull request 中导出并运行固定 `hello` 镜像 smoke。合并到 +`main` 后,只有同一 SHA 的完整 `CI` 成功,才会触发正式导出与发布。新版本更新 +orphan `release/user-kit` 分支,并创建不可变的 `user-kit-v` 标签和 +GitHub Release 压缩包;已存在的版本不会被覆盖。开发者文件不会进入发行包。 diff --git a/docs/cn/examples/hello.md b/docs/cn/examples/hello.md index 476aa32..240b890 100644 --- a/docs/cn/examples/hello.md +++ b/docs/cn/examples/hello.md @@ -14,11 +14,7 @@ make trace make wave ``` -`check` 等价于: - -```sh -make -f Makefile.dev bootrom-sim CASE=hello OUTPUT=list TRACE=0 -``` +`check` 和 `trace` 都使用发行包中的固定 `hello` 镜像;后者额外生成 FST 波形。 ## 期望结果 @@ -26,5 +22,5 @@ make -f Makefile.dev bootrom-sim CASE=hello OUTPUT=list TRACE=0 - 日志中出现 UART 停止文本 `done!` - `trace` 在 `build/wave/SimTop.fst` 写出非空波形 -如果失败,先确认 `make doctor` 通过,并且没有把 ELF 当成 flash 镜像传入。 +如果失败,先确认 `make doctor` 通过,并且固定 `hello` 镜像仍然存在。 下一步再看 [用户接入指南](../user-guide.md) 接入自己的 core。 diff --git a/docs/cn/maintainer-release.md b/docs/cn/maintainer-release.md index 5d981a4..54c18db 100644 --- a/docs/cn/maintainer-release.md +++ b/docs/cn/maintainer-release.md @@ -11,6 +11,8 @@ - `docs/cn/`、`docs/en/`:路径一一对应的双语文档源 - `dev/site/`:VitePress 主题和站点资源,不保存第二份 Markdown - `dev/site-docs.json`:公共站点页面白名单 +- `dev/user-kit.json`:User Kit 文件和目录白名单 +- `dev/user-kit/`:User Kit 顶层 README 与忽略规则 - `config/`、`hw/`、`dv/`、`sw/`:SoC 本体,保持现有硬件、验证和软件分层 ## 合并前验证 @@ -18,8 +20,10 @@ ```sh make -f Makefile.dev docs-check make -f Makefile.dev docs-site-check -make gen-soc-pkg +make -f Makefile.dev export-user-kit +make -f Makefile.dev gen-soc-pkg git diff --exit-code hw/include/soc_pkg.sv +make -f Makefile.dev config-check make check make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 ``` @@ -31,8 +35,26 @@ make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 ```text CI -> 源码、软件、Verilator 和 bootrom 回归 Documentation Pages -> 仅构建 dev/site-docs.json 允许的用户页面 +User Kit -> 在完整 CI 成功后导出、验证并发布版本 ``` +User Kit workflow 只接受仓库自身 `main` push 对应的完整 CI 成功事件,不接受 PR +触发的 `workflow_run`。它在 `build/user-kit` 中执行 `doctor` 和固定 `hello` 镜像 +仿真,清理该目录的生成物后上传同一个已测试目录。只有该 artifact 通过后,publish +job 才会继续。新版本会原子更新 `release/user-kit` 并创建 +`user-kit-v` 标签,随后将压缩包发布到对应 GitHub Release;已存在的版本 +保持不变。 + +## 仓库设置 + +在 GitHub `Settings > Actions > General` 中允许 workflow 写仓库内容。 +`release/user-kit` 必须允许 User Kit workflow 强制更新,不需要手工创建该分支。 +同时需要允许 workflow 创建标签和 GitHub Release。该分支是按版本更新的 orphan +分支,不能直接用于向 `main` 创建普通 PR。 + +修改 `dev/user-kit.json` 后必须重新导出,确认包内没有 `.github/`、`dev/`、 +`Makefile.dev`、`mk/dev.mk`、软件 SDK/构建文件、内部测试或 `hello` 之外的归档镜像。 + ## 历史迁移映射 这些路径只对维护历史有用,不属于用户文档: diff --git a/docs/cn/memory-map.md b/docs/cn/memory-map.md index 4e5800a..4093f1a 100644 --- a/docs/cn/memory-map.md +++ b/docs/cn/memory-map.md @@ -2,8 +2,8 @@ [English](../en/memory-map.md) -当前内存映射同时体现在 `config/memory.yml`、`hw/include/soc_pkg.sv` 和 -`sw/ecos/board.h` 中。首页的地址卡片从同一份配置生成。 +当前内存映射固化在 `asic_top.v` 中,并同步记录在 `config/memory.yml`、 +`hw/include/soc_pkg.sv` 和 `sw/ecos/board.h`。首页的地址卡片从配置记录生成。 | 区域 | 基地址 | 大小 | 说明 | | --- | ---: | ---: | --- | @@ -28,4 +28,5 @@ | rng | `0x1030_0000` | `0x0000_1000` | 伪随机数生成器 | | crc | `0x1030_1000` | `0x0000_1000` | CRC 计算引擎 | -在生成器覆盖全部目标之前,需要保持硬件 package、软件头文件和文档同步。 +当前版本只支持表中的固定地址。用户接入 core 时应直接使用这些地址,不应通过修改 +配置文件、软件头文件或 `asic_top.v` 自定义地址空间;可配置地址空间将在后续版本提供。 diff --git a/docs/cn/simulation.md b/docs/cn/simulation.md index 3dd0a86..f71ff17 100644 --- a/docs/cn/simulation.md +++ b/docs/cn/simulation.md @@ -11,32 +11,40 @@ I2C 和 QSPI 引脚连接仿真板级模型。 ```sh make doctor make check -make sim APP=hello +make lint +make sim make trace make wave ``` - `check` 运行归档的 `hello` 冒烟 -- `sim` 构建所选应用并启动仿真 +- `lint` 使用独立于仿真构建的规则检查 RTL 和 filelist +- `sim` 使用固定 `hello` 镜像启动仿真 - `trace` 以 `TRACE=1` 运行 `hello` - `wave` 打开 `build/wave/SimTop.fst` -指定其他应用或槽位: +达到 `MAX_CYCLES` 默认判定为失败;固定镜像必须在上限前输出 `done!`。 +`ALLOW_TIMEOUT=1` 不会覆盖这个已经配置的 UART 通过条件。 + +`make lint` 会保留仿真兼容模式中屏蔽的状态机、组合环和复位网络警告。警告会输出 +供接入 core 时检查,Verilator 语法或语义错误仍会使目标失败。 + +指定待测 core 槽位: ```sh -make sim APP=gpio CORE_SEL=0 TRACE=0 -make check CASE=uart_poll +make sim CORE_SEL=0 TRACE=0 +make check CORE_SEL=0 ``` 直接运行 pad 级顶层: ```sh -make sim TOP=asicTop APP=hello +make sim TOP=asicTop ``` ## 流程约定 -- `build/sw///.bin`:仿真消费的原始 flash 镜像 +- `sw/bootrom/hello/retrosoc_fw.bin`:User Kit 固定使用的原始 flash 镜像 - `hw/filelist/verilator.f`:传给 Verilator 的 RTL file list - `dv/verilator/csrc/sim_main.cpp`:C++ harness - `build/verilator/obj_dir_SimTop/VSimTop`:默认仿真器 @@ -48,8 +56,4 @@ make sim TOP=asicTop APP=hello harness 会把原始二进制镜像加载到 SPI flash 模型。这里应使用 `.bin` 镜像;传入 ELF 文件时,仿真会把 ELF 字节当作 flash 内容加载。 -完整 bootrom 回归属于维护者入口: - -```sh -make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 -``` +完整 bootrom 回归由维护者在开发仓库中运行,不属于 User Kit 命令面。 diff --git a/docs/cn/user-guide.md b/docs/cn/user-guide.md index aa17f00..29db4c0 100644 --- a/docs/cn/user-guide.md +++ b/docs/cn/user-guide.md @@ -3,38 +3,55 @@ [English](../en/user-guide.md) 这份指南面向要把自己的 RISC-V core 接到 `mpc-soc`,并跑通 SoC 级仿真的用户。 -用户只需要准备 core RTL 和 wrapper,按约定接入现有槽位,不必改外设、软件 BSP -或维护者回归脚本。 +用户只需要准备 core RTL 和 wrapper,按约定接入现有槽位,不必改外设、固定软件 +镜像或维护者回归脚本。 完整示例: - [Hello 冒烟](examples/hello.md):默认 core 跑通控制台输出 -- [PSRAM 访问](examples/psram.md):验证外部存储器窗口 -## 1. 检查工具 +## 1. 获取 User Kit -需要 Python 3、GNU Make、C++ 编译器和 Verilator。推荐 Verilator 5.050。 -软件构建还需要 `riscv64-unknown-elf-gcc`。 +普通用户不要从开发分支 `main` 开始。先拉取由 CI 独立构建和仿真验证的用户发行 +分支,再创建自己的开发分支: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +git switch -c user/ +``` + +上游 `release/user-kit` 会被 CI 强制更新,不要直接在该分支长期开发。需要推送到 +自己的仓库、升级发行包或查看交付边界时,参考 +[User Kit 获取与使用](user-kit.md)。 + +## 2. 检查工具 + +需要 Python 3.9+、PyYAML、GNU Make、C++ 编译器和 Verilator 5.050。 +当前 User Kit 要求使用该版本。固定镜像仿真不需要 RISC-V 工具链或软件 SDK。 ```sh make doctor ``` -## 2. 先跑通默认 SoC +## 3. 先跑通默认 SoC 不要先改 RTL。先确认当前仓库在默认 `CORE_SEL=0` 下可以仿真: ```sh make check +make lint make trace make wave ``` - `check` 运行归档的 `hello` bootrom 冒烟 -- `trace` 重新构建 `hello` 并打开波形 +- `lint` 检查当前 RTL/filelist,并报告仿真兼容模式屏蔽的结构警告 +- `trace` 使用固定 `hello` 镜像重新运行仿真并生成波形 - `wave` 用 GTKWave 打开最近一次 `TRACE=1` 生成的 FST -## 3. 选择 core 槽位 +## 4. 选择 core 槽位 当前 SoC 通过 `CORE_SEL` 选择启用的 core: @@ -46,7 +63,7 @@ make wave 若要使用槽位 2 或更高编号,需要在 `asic_top.v` 中移除对应 tie-off, 例化自己的 wrapper,并使用 `CORE_SEL=` 运行。 -## 4. 按约定写 wrapper +## 5. 按约定写 wrapper SoC 期望每个 core wrapper 暴露以下接口: @@ -77,7 +94,7 @@ hw/ip/core/npc_wrapper_template.sv 复制该文件、重命名模块,并用你的 core 例化替换模板中的空闲 master assignment。 -## 5. 接入 RTL +## 6. 接入 RTL 1. 把 core RTL 和 wrapper 放到 `hw/ip/core/` 或其他仓库内目录。 2. 把新文件加入 `hw/filelist/verilator.f`。现有条目是: @@ -92,36 +109,33 @@ hw/ip/core/npc_wrapper_template.sv `_cmp_io_interrupt_out_`。 4. 在 wrapper 内部或例化位置 tie off 未使用的可选 slave 端口。 -## 6. 验证自己的 core +## 7. 验证自己的 core ```sh -make check CASE=hello CORE_SEL= -make sim APP=hello CORE_SEL= TRACE=0 MAX_CYCLES=1000 +make check CORE_SEL= +make lint +make sim CORE_SEL= TRACE=0 ``` -冒烟通过后再跑完整回归。回归属于维护者入口: +冒烟通过后,把 core 变更交给维护者在开发仓库中运行完整回归。 +`release/user-kit` 不包含维护者入口。 -```sh -make -f Makefile.dev regress CORE_SEL= OUTPUT=list TRACE=0 -make -f Makefile.dev clean-build -``` - -## 7. 编写或复用软件 +## 8. 用户可编写内容 -默认软件流从 `sw/ecos/templates//` 构建,输出到 -`build/sw/mpc-soc//`。仿真消费原始 `.bin`,不要传入 ELF。 +当前版本允许用户编写和交付: -```sh -make sw APP=hello -make sim APP=hello -``` +- core RTL +- AXI wrapper +- `hw/filelist/verilator.f` 中的必要条目 +- 目标 core 槽位的必要连接 -更完整的 BSP 和 SDK 说明见 [软件流程](software.md)。 +SoC 地址空间、外设 RTL、固定 `hello` 镜像、软件和驱动不属于支持的用户修改面。 +若用户自行修改这些内容,需要自行承担集成和验证责任。 ## 常见错误 - 改了 RTL 但没更新 `hw/filelist/verilator.f` -- 把 ELF 当成 flash 镜像传给仿真器 +- 替换或删除发行包中的固定 `hello` 镜像 - 未启用槽位仍保持 tie-off,却使用了对应 `CORE_SEL` - 提交了 `build/` 下的生成产物 diff --git a/docs/cn/user-kit.md b/docs/cn/user-kit.md new file mode 100644 index 0000000..9b5276f --- /dev/null +++ b/docs/cn/user-kit.md @@ -0,0 +1,87 @@ +# User Kit 获取与使用 + +[English](../en/user-kit.md) + +User Kit 是 `mpc-soc` 面向 core 接入用户的精简发行版本。开发者在 `main` 维护 +完整 SoC、全量回归、CI 和文档站;普通用户只需要获取经过 CI 独立构建和仿真验证 +的 `release/user-kit` 分支。 + +发行包包含固定 SoC RTL、core wrapper 接入点、Verilator harness、公开文档和一个 +固定的 `hello` 归档镜像。它不包含软件 SDK、驱动构建、`Makefile.dev`、 +`mk/dev.mk`、维护者 CI、站点源码、内部测试或其他归档回归镜像。 + +## 获取 User Kit + +维护者完成首次发布后,直接拉取发行分支,不要 clone `main`: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +``` + +GitHub Actions 同时提供版本化 artifact。正式版本还会创建不可变的 +`user-kit-v` 标签和 GitHub Release 压缩包。正常开发可使用 +`release/user-kit` 获取最新版本,需要固定版本时使用版本标签或 Release。 + +## 建立用户开发分支 + +`release/user-kit` 在新版本发布时由 CI 更新,不要直接在该分支长期开发: + +```sh +git switch -c user/ +``` + +推送到自己的仓库时,把项目仓库保留为只读上游: + +```sh +git remote rename origin upstream +git remote add origin https://github.com//.git +git push -u origin user/ +``` + +## 检查发行环境 + +需要 Python 3.9+、PyYAML、GNU Make、C++ 编译器和 Verilator 5.050。 +当前 User Kit 要求使用该版本,不需要 RISC-V 工具链或 ECOS SDK。 + +```sh +make doctor +make check +make lint +make sim +``` + +`make check` 和 `make sim` 都使用发行包内唯一保留的固定 `hello` 镜像。 +`make lint` 用于检查用户新增的 core RTL 和 filelist。当前版本不支持在 User Kit +中编译或替换软件和驱动。 + +core 接入步骤见[用户接入指南](user-guide.md),地址必须使用 +[内存映射](memory-map.md)中已经固化的范围。 + +## 版本与升级 + +`SOC_KIT_VERSION` 记录: + +- `KIT_FORMAT_VERSION`:User Kit 目录和接口格式版本 +- `SOC_VERSION`:`mpc-soc` 版本 +- `SOURCE_COMMIT`:生成发行包的 `main` 源码提交 + +发行分支是没有共享开发历史的 orphan 分支,目前没有自动升级命令。升级时重新 +获取干净 User Kit,再迁移自己的 core RTL、wrapper 和必要的 filelist/槽位改动: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git mpc-soc-new +cp -a my-mpc-soc/hw/ip/core/ mpc-soc-new/hw/ip/core/ +``` + +迁移后先运行 `make check`,再使用固定镜像验证自己的 core。不要把上游发行分支 +强制合并到旧的用户仓库。 + +## 交付边界 + +用户应交付 core RTL、wrapper、filelist 增量及必要的槽位连接说明,不要提交 +`build/` 生成物,也不要修改固化地址映射。User Kit 与 `main` 没有共享提交历史, +不能直接从用户分支向 `main` 创建普通 PR;维护者应在开发仓库中审查并集成用户 +提交内容。 diff --git a/docs/en/architecture.md b/docs/en/architecture.md index 3452fdd..6ed8308 100644 --- a/docs/en/architecture.md +++ b/docs/en/architecture.md @@ -20,9 +20,13 @@ fabric. The default simulation top is `SimTop`, which wraps pad-level ## Configuration sources -- `config/soc.yml` describes the template SoC top and selected IP paths. -- `config/memory.yml` is the source of the generated RTL package and C headers. -- `config/boards/sim.yml` describes the default simulation board. +- `config/soc.yml` records the current SoC top, IP paths, and fixed 50 MHz clock metadata. +- `config/memory.yml` records the fixed address map for this release and generates the RTL package. +- `config/boards/sim.yml` records the 50 MHz simulation-board default. + +In this release these files describe the configuration shipped with the fixed +`asic_top.v`; they are not user controls for retargeting the SoC address map or +clock. Maintainer CI checks that the fixed 50 MHz values have not drifted. ## Integration flow @@ -30,8 +34,9 @@ fabric. The default simulation top is `SimTop`, which wraps pad-level modules are added or renamed. 2. Keep top-level bus, clock, reset, and interrupt wiring under `hw/soc/`. 3. Keep `hw/filelist/soc.f` and `hw/filelist/verilator.f` in sync. -4. When the memory map changes, regenerate `hw/include/soc_pkg.sv` from - `config/memory.yml` and update `sw/ecos/board.h`. +4. Keep the fixed address map and 50 MHz clock in this release. Do not edit the + configuration files to retarget `asic_top.v`; configurable addressing is + deferred until a later release has the complete integration chain. Follow the [user integration guide](user-guide.md) for core slots and the AXI port contract. diff --git a/docs/en/boot-flow.md b/docs/en/boot-flow.md index 761c2ac..8047d12 100644 --- a/docs/en/boot-flow.md +++ b/docs/en/boot-flow.md @@ -2,9 +2,9 @@ [中文说明](../cn/boot-flow.md) -The Verilator flow reads a raw software image from -`build/sw///` and passes it to the simulation harness as -`+bootrom=`. +The User Kit Verilator flow reads the fixed +`sw/bootrom/hello/retrosoc_fw.bin` image and passes it to the simulation +harness as `+bootrom=`. ## Template flow @@ -12,9 +12,10 @@ The Verilator flow reads a raw software image from 2. The harness receives the raw flash image path through `+bootrom=`. 3. When RTL issues an SPI flash read, the flash DPI model returns bytes from that image. -4. Startup code in `sw/ecos/start.S` sets up the stack, clears `.bss`, and - calls `main`. -5. Platform drivers use the board-package headers under `sw/ecos/`. +4. Startup code in the fixed image initializes the runtime and executes + `hello`. +5. A user-integrated core runs the same image through the existing bus and + peripheral paths. -Use a `.bin` image for Verilator. If you pass an ELF file, the simulator -loads the ELF container bytes as flash contents. +This release does not provide software SDK or driver build entry points, and +does not support replacing the fixed image shipped in the User Kit. diff --git a/docs/en/ci.md b/docs/en/ci.md index 384dfca..4a05693 100644 --- a/docs/en/ci.md +++ b/docs/en/ci.md @@ -7,23 +7,39 @@ site. The root project uses `.github/workflows/ci.yml` for a single-platform Ubuntu 24.04 CI. Documentation uses `.github/workflows/pages.yml` and builds a -VitePress site with Node.js. +VitePress site with Node.js. `.github/workflows/user-kit.yml` exports and tests +the user environment. ## Automatic gates Pushes and pull requests to `main` run: - Python syntax checks -- `make gen-soc-pkg` and a check that `hw/include/soc_pkg.sv` is current +- `make -f Makefile.dev gen-soc-pkg` and a check that `hw/include/soc_pkg.sv` is current +- a check that configuration, RTL package, and BSP defaults remain fixed at 50 MHz - `make -f Makefile.dev docs-check` -- software build: `make sw APP=hello` +- maintainer software build: `make -C sw BOARD=mpc-soc APP=hello` +- software configuration rebuild check for XIP and MEM link targets - Verilator build: `make verilate TRACE=0` +- source-built `hello` simulation with the `done!` pass condition - bootrom regression: `make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0` - a check that no commit-worthy untracked files were created +`make lint` is a separate user/maintainer target and is not currently a CI gate. +CI continues to build and run only the default `CORE_SEL=0` example core. + ## Documentation deployment When docs, the theme, or site scripts change, `Documentation Pages` runs `make -f Makefile.dev docs-site-check`. Pull requests only build and validate the site. Merges to `main` deploy to `https://openecos-projects.github.io/mpc-soc/`. + +## User Kit publishing + +The User Kit workflow exports the package and runs the fixed `hello` image in +pull requests. After a merge to `main`, publication starts only after the full +`CI` workflow succeeds for the same SHA. A new version updates the orphan +`release/user-kit` branch and creates an immutable `user-kit-v` tag +and GitHub Release archive. Existing versions are never overwritten, and +developer-only files never enter the release. diff --git a/docs/en/examples/hello.md b/docs/en/examples/hello.md index 4dcfa1f..81bbe00 100644 --- a/docs/en/examples/hello.md +++ b/docs/en/examples/hello.md @@ -15,11 +15,8 @@ make trace make wave ``` -`check` is equivalent to: - -```sh -make -f Makefile.dev bootrom-sim CASE=hello OUTPUT=list TRACE=0 -``` +`check` and `trace` both use the fixed `hello` image in the release; the latter +also generates an FST waveform. ## Expected result @@ -27,6 +24,6 @@ make -f Makefile.dev bootrom-sim CASE=hello OUTPUT=list TRACE=0 - The UART stop text `done!` appears in the log - `trace` writes a non-empty waveform to `build/wave/SimTop.fst` -If this fails, confirm `make doctor` passes and that you did not pass an ELF -as the flash image. Then follow the [user integration guide](../user-guide.md) +If this fails, confirm `make doctor` passes and the fixed `hello` image is +still present. Then follow the [user integration guide](../user-guide.md) to attach your own core. diff --git a/docs/en/maintainer-release.md b/docs/en/maintainer-release.md index cc88092..f4aedec 100644 --- a/docs/en/maintainer-release.md +++ b/docs/en/maintainer-release.md @@ -13,6 +13,8 @@ site. - `docs/cn/` and `docs/en/`: path-matched bilingual sources - `dev/site/`: VitePress theme and site assets, not a second Markdown tree - `dev/site-docs.json`: public site page allowlist +- `dev/user-kit.json`: User Kit file and directory allowlist +- `dev/user-kit/`: User Kit top-level READMEs and ignore rules - `config/`, `hw/`, `dv/`, and `sw/`: the SoC itself. Keep the current hardware, verification, and software layout @@ -21,8 +23,10 @@ site. ```sh make -f Makefile.dev docs-check make -f Makefile.dev docs-site-check -make gen-soc-pkg +make -f Makefile.dev export-user-kit +make -f Makefile.dev gen-soc-pkg git diff --exit-code hw/include/soc_pkg.sv +make -f Makefile.dev config-check make check make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 ``` @@ -34,8 +38,29 @@ After a merge and push to `main`: ```text CI -> source, software, Verilator, and bootrom regression Documentation Pages -> only the user pages allowed by dev/site-docs.json +User Kit -> export, test, and publish after full CI succeeds ``` +The User Kit workflow only accepts a successful full CI run from a push to the +repository's own `main` branch; pull-request `workflow_run` events are not +eligible. It runs `doctor` and fixed-image `hello` simulation inside +`build/user-kit`, removes generated output, and uploads that same tested +directory. A new version atomically updates `release/user-kit`, creates a +`user-kit-v` tag, and uploads an archive to the matching GitHub +Release. Existing versions remain unchanged. + +## Repository settings + +Allow workflows to write repository contents under GitHub +`Settings > Actions > General`. The User Kit workflow must be allowed to force +update `release/user-kit` and create tags and GitHub Releases; the branch does +not need to be created manually. It is an orphan release branch and cannot open +a normal pull request directly to `main`. + +After changing `dev/user-kit.json`, re-export and verify that the kit contains +no `.github/`, `dev/`, `Makefile.dev`, `mk/dev.mk`, software SDK/build files, +internal tests, or archived images other than `hello`. + ## Historical migration map These paths are maintainer history only and do not belong in user docs: diff --git a/docs/en/memory-map.md b/docs/en/memory-map.md index 2dcf576..c43fb41 100644 --- a/docs/en/memory-map.md +++ b/docs/en/memory-map.md @@ -2,9 +2,9 @@ [中文说明](../cn/memory-map.md) -The current map is recorded in `config/memory.yml`, `hw/include/soc_pkg.sv`, -and `sw/ecos/board.h`. The homepage address cards are generated from the same -configuration. +The current map is fixed in `asic_top.v` and mirrored in `config/memory.yml`, +`hw/include/soc_pkg.sv`, and `sw/ecos/board.h`. The homepage address cards are +generated from the configuration record. | Region | Base | Size | Description | | --- | ---: | ---: | --- | @@ -29,5 +29,7 @@ configuration. | rng | `0x1030_0000` | `0x0000_1000` | Pseudo-random number generator | | crc | `0x1030_1000` | `0x0000_1000` | CRC calculation engine | -Keep the hardware package, software headers, and documentation in sync until -the generators cover every target. +This release supports only the fixed addresses in the table. Core integrations +must use them and must not customize the address space by editing configuration, +software headers, or `asic_top.v`. Configurable addressing is deferred to a +later release. diff --git a/docs/en/simulation.md b/docs/en/simulation.md index 3d3c079..72bd8fd 100644 --- a/docs/en/simulation.md +++ b/docs/en/simulation.md @@ -11,32 +11,41 @@ connects board models for SPI flash, PSRAM, GPIO, UART, I2C, and QSPI. ```sh make doctor make check -make sim APP=hello +make lint +make sim make trace make wave ``` - `check` runs the archived `hello` smoke -- `sim` builds the selected application and starts simulation +- `lint` checks RTL and the file list with rules separate from the simulation build +- `sim` starts simulation with the fixed `hello` image - `trace` runs `hello` with `TRACE=1` - `wave` opens `build/wave/SimTop.fst` -Select another application or slot: +Reaching `MAX_CYCLES` is a failure unless the fixed image has printed `done!`. +`ALLOW_TIMEOUT=1` never overrides this configured UART pass condition. + +`make lint` keeps state-machine, combinational-loop, and reset-network warnings +that the simulation compatibility mode suppresses. Warnings are reported for +core integration review; Verilator syntax or semantic errors still fail the target. + +Select the core slot under test: ```sh -make sim APP=gpio CORE_SEL=0 TRACE=0 -make check CASE=uart_poll +make sim CORE_SEL=0 TRACE=0 +make check CORE_SEL=0 ``` Run the pad-level top directly: ```sh -make sim TOP=asicTop APP=hello +make sim TOP=asicTop ``` ## Flow conventions -- `build/sw///.bin`: raw flash image consumed by simulation +- `sw/bootrom/hello/retrosoc_fw.bin`: fixed raw flash image used by the User Kit - `hw/filelist/verilator.f`: RTL file list passed to Verilator - `dv/verilator/csrc/sim_main.cpp`: C++ harness - `build/verilator/obj_dir_SimTop/VSimTop`: default simulator @@ -48,8 +57,5 @@ make sim TOP=asicTop APP=hello The harness loads a raw binary into the SPI flash model. Use a `.bin` image. If you pass an ELF, the simulator treats the ELF bytes as flash contents. -Full bootrom regression belongs to the maintainer entry: - -```sh -make -f Makefile.dev regress OUTPUT=list STOP_ON_FAIL=1 TRACE=0 -``` +Maintainers run the full bootrom regression in the development repository; it +is not part of the User Kit command surface. diff --git a/docs/en/user-guide.md b/docs/en/user-guide.md index 867f09c..80a3fe8 100644 --- a/docs/en/user-guide.md +++ b/docs/en/user-guide.md @@ -4,39 +4,58 @@ This guide is for users who want to attach their own RISC-V core to `mpc-soc` and run SoC-level simulation. Provide the core RTL and a wrapper, then connect -it to an existing slot. You do not need to change peripherals, the software -BSP, or maintainer regression scripts. +it to an existing slot. You do not need to change peripherals, the fixed +software image, or maintainer regression scripts. Worked examples: - [Hello smoke](examples/hello.md): run the default core to a console pass -- [PSRAM access](examples/psram.md): check the external memory window -## 1. Check the tools +## 1. Get the User Kit -You need Python 3, GNU Make, a C++ compiler, and Verilator. Verilator 5.050 is -the recommended baseline. Software builds also need `riscv64-unknown-elf-gcc`. +Regular users should not start from the development `main` branch. Clone the +user release that CI has independently built and simulated, then create your +own development branch: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +git switch -c user/ +``` + +CI force-updates the upstream `release/user-kit` branch, so do not develop on +it directly. See [Getting and using the User Kit](user-kit.md) when pushing to +your own repository, updating the release, or checking the delivery boundary. + +## 2. Check the tools + +You need Python 3.9+, PyYAML, GNU Make, a C++ compiler, and Verilator 5.050. +The current User Kit requires this exact version. Fixed-image simulation does +not require a RISC-V toolchain or software SDK. ```sh make doctor ``` -## 2. Run the default SoC first +## 3. Run the default SoC first Do not start by editing RTL. Confirm the repository simulates with the default `CORE_SEL=0`: ```sh make check +make lint make trace make wave ``` - `check` runs the archived `hello` bootrom smoke -- `trace` rebuilds `hello` and writes a waveform +- `lint` checks the current RTL/file list and reports structural warnings hidden by simulation compatibility mode +- `trace` reruns the fixed `hello` image and writes a waveform - `wave` opens the latest `TRACE=1` FST in GTKWave -## 3. Choose a core slot +## 4. Choose a core slot The SoC selects the enabled core with `CORE_SEL`: @@ -48,7 +67,7 @@ The lowest-risk path is to replace the wrapper on slot 0 or slot 1. To use slot 2 or above, remove the matching tie-off in `asic_top.v`, instantiate your wrapper, and run with `CORE_SEL=`. -## 4. Follow the wrapper contract +## 5. Follow the wrapper contract Each core wrapper must expose: @@ -80,7 +99,7 @@ hw/ip/core/npc_wrapper_template.sv Copy it, rename the module, and replace the idle master assignments with your core instance. -## 5. Connect the RTL +## 6. Connect the RTL 1. Place the core RTL and wrapper under `hw/ip/core/` or another in-repo path. 2. Add the new files to `hw/filelist/verilator.f`. The current core entries are: @@ -95,36 +114,35 @@ Required sources must appear before `hw/soc/top/asic_top.v`. `_cpu__io_master_*` plus `_cmp_io_interrupt_out_`. 4. Tie off unused optional slave ports in the wrapper or at the instance. -## 6. Validate your core +## 7. Validate your core ```sh -make check CASE=hello CORE_SEL= -make sim APP=hello CORE_SEL= TRACE=0 MAX_CYCLES=1000 +make check CORE_SEL= +make lint +make sim CORE_SEL= TRACE=0 ``` -After the smoke passes, run the full regression from the maintainer entry: +After the smoke passes, deliver the core changes for the maintainer to run the +full regression in the development repository. `release/user-kit` does not +contain maintainer entry points. -```sh -make -f Makefile.dev regress CORE_SEL= OUTPUT=list TRACE=0 -make -f Makefile.dev clean-build -``` - -## 7. Build or reuse software +## 8. User-editable content -The default software flow builds `sw/ecos/templates//` into -`build/sw/mpc-soc//`. Simulation consumes a raw `.bin` image, not an ELF. +This release supports users writing and delivering: -```sh -make sw APP=hello -make sim APP=hello -``` +- core RTL +- the AXI wrapper +- required entries in `hw/filelist/verilator.f` +- required connections for the selected core slot -See the [software flow](software.md) for the BSP and SDK details. +The SoC address map, peripheral RTL, fixed `hello` image, software, and drivers +are outside the supported user modification surface. Users who change them are +responsible for their own integration and validation. ## Common mistakes - Changing RTL without updating `hw/filelist/verilator.f` -- Passing an ELF to the simulator as a flash image +- Replacing or deleting the bundled fixed `hello` image - Using a `CORE_SEL` whose slot is still tied off - Committing generated files under `build/` diff --git a/docs/en/user-kit.md b/docs/en/user-kit.md new file mode 100644 index 0000000..a5a8576 --- /dev/null +++ b/docs/en/user-kit.md @@ -0,0 +1,95 @@ +# Getting and using the User Kit + +[中文说明](../cn/user-kit.md) + +The User Kit is the trimmed `mpc-soc` release for core integration users. +Developers maintain the complete SoC, full regression, CI, and documentation +site on `main`. Regular users only fetch the CI-tested `release/user-kit` +branch. + +The release contains the fixed SoC RTL, core wrapper integration points, +Verilator harness, public documentation, and one fixed archived `hello` image. +It excludes the software SDK, driver builds, `Makefile.dev`, `mk/dev.mk`, +maintainer CI, site sources, internal tests, and all other regression images. + +## Get the User Kit + +After the first maintainer release, clone the release branch instead of +`main`: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git my-mpc-soc +cd my-mpc-soc +``` + +GitHub Actions also provides a versioned artifact. Each formal version has an +immutable `user-kit-v` tag and GitHub Release archive. Use +`release/user-kit` for the latest version, or a tag/Release to pin a version. + +## Create a user development branch + +CI updates `release/user-kit` when a new version is published. Do not develop +on that branch directly: + +```sh +git switch -c user/ +``` + +Keep the project repository as a read-only upstream when pushing to your own +repository: + +```sh +git remote rename origin upstream +git remote add origin https://github.com//.git +git push -u origin user/ +``` + +## Check the released environment + +The flow requires Python 3.9+, PyYAML, GNU Make, a C++ compiler, and Verilator +5.050. The User Kit requires this exact version. A RISC-V toolchain and ECOS +SDK are not needed. + +```sh +make doctor +make check +make lint +make sim +``` + +`make check` and `make sim` both use the only bundled fixed image, `hello`. +`make lint` checks user-added core RTL and file-list changes. This release does +not support compiling or replacing software and drivers inside the User Kit. + +Follow the [user integration guide](user-guide.md). Addresses must use the +fixed ranges in the [memory map](memory-map.md). + +## Version and upgrades + +`SOC_KIT_VERSION` records: + +- `KIT_FORMAT_VERSION`: User Kit directory and interface format +- `SOC_VERSION`: the `mpc-soc` version +- `SOURCE_COMMIT`: the `main` source commit used for the release + +The release is currently an orphan branch without shared development history, +so there is no automatic upgrade command. Fetch a clean User Kit and migrate +your core RTL, wrapper, and required filelist or slot changes: + +```sh +git clone --branch release/user-kit --single-branch \ + https://github.com/openecos-projects/mpc-soc.git mpc-soc-new +cp -a my-mpc-soc/hw/ip/core/ mpc-soc-new/hw/ip/core/ +``` + +Run `make check` after migrating, then validate your core with the fixed image. Do +not force-merge the upstream release branch into an existing user repository. + +## Delivery boundary + +Users should deliver core RTL, the wrapper, filelist changes, and any required +slot connection notes. Do not submit `build/` outputs or modify the fixed +address map. The User Kit and `main` do not share history, so a normal pull +request from a user branch to `main` is not possible. Maintainers review and +integrate the delivered changes in the development repository. diff --git a/dv/verilator/Makefile b/dv/verilator/Makefile index a2cfdae..6987a11 100644 --- a/dv/verilator/Makefile +++ b/dv/verilator/Makefile @@ -6,6 +6,7 @@ BOARD ?= mpc-soc APP ?= hello TRACE ?= 1 MAX_CYCLES ?= 200000 +ALLOW_TIMEOUT ?= 0 CORE_SEL ?= 0 FAST_PSRAM ?= 1 UART_INPUT ?= @@ -20,6 +21,7 @@ GPIO_DRIVE ?= GPIO_EXPECT ?= GPIO_EXPECT_MASK ?= EXTRA_VERILATOR_FLAGS ?= +EXTRA_LINT_FLAGS ?= BUILD_DIR ?= $(SOC_ROOT)/build/verilator OBJ_DIR := $(BUILD_DIR)/obj_dir_$(TOP) @@ -27,58 +29,62 @@ WAVE_DIR := $(SOC_ROOT)/build/wave LOG_DIR := $(SOC_ROOT)/build/log RTL_FILELIST ?= $(SOC_ROOT)/hw/filelist/verilator.f SOC_PKG ?= $(SOC_ROOT)/hw/include/soc_pkg.sv -BOOTROM_DIR ?= $(SOC_ROOT)/build/sw/$(BOARD)/$(APP) -DEFAULT_BOOTROM_IMAGE = $(shell images=$$(find "$(BOOTROM_DIR)" -maxdepth 1 -type f -name '*.bin' 2>/dev/null | sort); preferred=$$(printf '%s\n' "$$images" | grep -v '/app\.bin$$' | head -n 1); if [ -n "$$preferred" ]; then printf '%s' "$$preferred"; else printf '%s\n' "$$images" | head -n 1; fi) -BOOTROM_IMAGE ?= $(DEFAULT_BOOTROM_IMAGE) +BOOTROM_IMAGE ?= $(SOC_ROOT)/sw/bootrom/hello/retrosoc_fw.bin SW_IMAGE ?= $(BOOTROM_IMAGE) SIM_BIN := $(OBJ_DIR)/V$(TOP) BUILD_CONFIG := $(OBJ_DIR)/.build-config WAVE_FILE ?= $(WAVE_DIR)/$(TOP).fst WAVE_VIEW ?= $(CURDIR)/$(TOP).gtkw SIM_LOG ?= $(LOG_DIR)/$(TOP)-$(notdir $(basename $(SW_IMAGE))).log -RUN_ARGS ?= +bootrom=$(SW_IMAGE) +wave=$(WAVE_FILE) +max-cycles=$(MAX_CYCLES) +core-sel=$(CORE_SEL) $(if $(UART_INPUT),'+uart-in=$(UART_INPUT)' +uart-start-cycle=$(UART_START_CYCLE),) $(if $(UART_STOP_TEXT),'+uart-stop-text=$(UART_STOP_TEXT)',) $(if $(UART_FAIL_TEXT),'+uart-fail-text=$(UART_FAIL_TEXT)',) $(if $(UART1_EXPECT),'+uart1-expect=$(UART1_EXPECT)' +uart1-bit-cycles=$(UART1_BIT_CYCLES) $(if $(UART1_ARM_TEXT),'+uart1-arm-text=$(UART1_ARM_TEXT)',),) $(if $(GPIO_IN),+gpio-in=$(GPIO_IN),) $(if $(GPIO_DRIVE),+gpio-drive=$(GPIO_DRIVE),) $(if $(GPIO_EXPECT),+gpio-expect=$(GPIO_EXPECT),) $(if $(GPIO_EXPECT_MASK),+gpio-expect-mask=$(GPIO_EXPECT_MASK),) +RUN_ARGS ?= +bootrom=$(SW_IMAGE) +wave=$(WAVE_FILE) +max-cycles=$(MAX_CYCLES) +allow-timeout=$(ALLOW_TIMEOUT) +core-sel=$(CORE_SEL) $(if $(UART_INPUT),'+uart-in=$(UART_INPUT)' +uart-start-cycle=$(UART_START_CYCLE),) $(if $(UART_STOP_TEXT),'+uart-stop-text=$(UART_STOP_TEXT)',) $(if $(UART_FAIL_TEXT),'+uart-fail-text=$(UART_FAIL_TEXT)',) $(if $(UART1_EXPECT),'+uart1-expect=$(UART1_EXPECT)' +uart1-bit-cycles=$(UART1_BIT_CYCLES) $(if $(UART1_ARM_TEXT),'+uart1-arm-text=$(UART1_ARM_TEXT)',),) $(if $(GPIO_IN),+gpio-in=$(GPIO_IN),) $(if $(GPIO_DRIVE),+gpio-drive=$(GPIO_DRIVE),) $(if $(GPIO_EXPECT),+gpio-expect=$(GPIO_EXPECT),) $(if $(GPIO_EXPECT_MASK),+gpio-expect-mask=$(GPIO_EXPECT_MASK),) CLR_RESET := \033[0m CLR_INFO := \033[1;34m CLR_STEP := \033[1;32m CLR_ERR := \033[1;31m VERILATOR_PROCASSINIT_FLAG = $(shell tmp=$$(mktemp /tmp/verilator-probe.XXXXXX.v); printf 'module m; endmodule\n' > $$tmp; if $(VERILATOR) -Wno-PROCASSINIT --lint-only $$tmp >/dev/null 2>&1; then printf '%s' '-Wno-PROCASSINIT'; fi; rm -f $$tmp) -VERILATOR_FLAGS ?= -Wall -Wno-UNUSEDPARAM -Wno-UNUSEDSIGNAL -Wno-DECLFILENAME -Wno-PINCONNECTEMPTY -Wno-TIMESCALEMOD -Wno-DEFPARAM -Wno-BLKSEQ $(VERILATOR_PROCASSINIT_FLAG) -Wno-CASEINCOMPLETE -Wno-UNOPTFLAT -Wno-SYNCASYNCNET -Wno-GENUNNAMED -DFAST_FLASH --timescale-override 1ns/1ns --timing --cc --exe --build +VERILATOR_COMMON_WARNING_FLAGS := -Wall -Wno-UNUSEDPARAM -Wno-UNUSEDSIGNAL -Wno-DECLFILENAME -Wno-PINCONNECTEMPTY -Wno-TIMESCALEMOD -Wno-DEFPARAM -Wno-BLKSEQ $(VERILATOR_PROCASSINIT_FLAG) -Wno-GENUNNAMED +VERILATOR_SIM_WARNING_FLAGS := -Wno-CASEINCOMPLETE -Wno-UNOPTFLAT -Wno-SYNCASYNCNET +VERILATOR_COMMON_FLAGS := -DFAST_FLASH --timescale-override 1ns/1ns --timing +VERILATOR_SIM_FLAGS ?= $(VERILATOR_COMMON_WARNING_FLAGS) $(VERILATOR_SIM_WARNING_FLAGS) $(VERILATOR_COMMON_FLAGS) --cc --exe --build +VERILATOR_LINT_FLAGS ?= $(VERILATOR_COMMON_WARNING_FLAGS) -Wno-fatal $(VERILATOR_COMMON_FLAGS) --lint-only ifeq ($(FAST_PSRAM),1) -VERILATOR_FLAGS += -DFAST_PSRAM +VERILATOR_SIM_FLAGS += -DFAST_PSRAM +VERILATOR_LINT_FLAGS += -DFAST_PSRAM endif -VERILATOR_FLAGS += $(EXTRA_VERILATOR_FLAGS) -VERILATOR_FLAGS += --Mdir $(OBJ_DIR) --top-module $(TOP) -VERILATOR_FLAGS += -CFLAGS "-std=c++17 -O2 -DSIM_TOP_HEADER=\\\"V$(TOP).h\\\" -DSIM_TOP_CLASS=V$(TOP)" +VERILATOR_SIM_FLAGS += $(EXTRA_VERILATOR_FLAGS) +VERILATOR_SIM_FLAGS += --Mdir $(OBJ_DIR) --top-module $(TOP) +VERILATOR_SIM_FLAGS += -CFLAGS "-std=c++17 -O2 -DSIM_TOP_HEADER=\\\"V$(TOP).h\\\" -DSIM_TOP_CLASS=V$(TOP)" +VERILATOR_LINT_FLAGS += $(EXTRA_LINT_FLAGS) --top-module $(TOP) RTL_SOURCES := $(shell find $(SOC_ROOT)/hw -type f \( -name '*.v' -o -name '*.sv' -o -name '*.vh' -o -name '*.svh' -o -name '*.f' \)) HARNESS_SOURCES := $(shell find $(CURDIR)/csrc -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \)) -SOC_PKG_INPUTS := $(SOC_ROOT)/config/memory.yml $(SOC_ROOT)/scripts/gen_soc_pkg.py ifeq ($(TOP),asicTop) -VERILATOR_FLAGS += -CFLAGS "-DSIM_TOP_ASICTOP=1" +VERILATOR_SIM_FLAGS += -CFLAGS "-DSIM_TOP_ASICTOP=1" endif ifeq ($(TOP),SimTop) -VERILATOR_FLAGS += -CFLAGS "-DSIM_TOP_SIMTOP=1" +VERILATOR_SIM_FLAGS += -CFLAGS "-DSIM_TOP_SIMTOP=1" endif ifeq ($(TRACE),1) -VERILATOR_FLAGS += --trace-fst -CFLAGS "-DVM_TRACE=1" +VERILATOR_SIM_FLAGS += --trace-fst -CFLAGS "-DVM_TRACE=1" endif -.PHONY: help check-tools check-image gen-soc-pkg verilate sim wave clean FORCE +.PHONY: help check-tools check-image check-soc-pkg lint verilate sim wave clean FORCE help: @printf "$(CLR_INFO)Verilator simulation entry$(CLR_RESET)\n" + @printf " make -C dv/verilator lint [TOP=SimTop]\n" @printf " make -C dv/verilator verilate [TOP=SimTop]\n" @printf " make -C dv/verilator sim [BOOTROM_IMAGE=] [MAX_CYCLES=200000] [CORE_SEL=0] [FAST_PSRAM=1]\n" @printf " make -C dv/verilator wave\n\n" @printf "$(CLR_INFO)Inputs:$(CLR_RESET)\n" @printf " RTL_FILELIST=$(RTL_FILELIST)\n" @printf " SOC_PKG=$(SOC_PKG)\n" - @printf " BOOTROM_DIR=$(BOOTROM_DIR)\n" @printf " SW_IMAGE=$(SW_IMAGE)\n" @printf " CORE_SEL=$(CORE_SEL)\n" + @printf " ALLOW_TIMEOUT=$(ALLOW_TIMEOUT)\n" @printf " FAST_PSRAM=$(FAST_PSRAM)\n" @printf " UART_INPUT=$(UART_INPUT)\n" @printf " UART_STOP_TEXT=$(UART_STOP_TEXT)\n" @@ -100,10 +106,8 @@ check-tools: check-image: @test -f $(SW_IMAGE) || (printf "$(CLR_ERR)ERROR: bootrom image not found: $(SW_IMAGE)$(CLR_RESET)\n"; printf "$(CLR_ERR)Place a raw flash image at $(BOOTROM_IMAGE) or pass BOOTROM_IMAGE=$(CLR_RESET)\n"; exit 2) -gen-soc-pkg: $(SOC_PKG) - -$(SOC_PKG): $(SOC_PKG_INPUTS) - @python3 $(SOC_ROOT)/scripts/gen_soc_pkg.py --output $(SOC_PKG) +check-soc-pkg: + @test -f $(SOC_PKG) || (printf "$(CLR_ERR)ERROR: fixed SoC package not found: $(SOC_PKG)$(CLR_RESET)\n"; exit 2) FORCE: @@ -115,15 +119,18 @@ $(BUILD_CONFIG): FORCE | $(BUILD_DIR) printf '%s\n' 'TRACE=$(TRACE)'; \ printf '%s\n' 'FAST_PSRAM=$(FAST_PSRAM)'; \ printf '%s\n' 'RTL_FILELIST=$(RTL_FILELIST)'; \ - printf '%s\n' 'VERILATOR_FLAGS=$(VERILATOR_FLAGS)'; \ + printf '%s\n' 'VERILATOR_SIM_FLAGS=$(VERILATOR_SIM_FLAGS)'; \ } > $@.tmp @cmp -s $@.tmp $@ || mv $@.tmp $@ @rm -f $@.tmp $(SIM_BIN): $(BUILD_CONFIG) $(RTL_SOURCES) $(HARNESS_SOURCES) $(SOC_PKG) | $(BUILD_DIR) $(WAVE_DIR) $(LOG_DIR) - @$(VERILATOR) $(VERILATOR_FLAGS) -f $(RTL_FILELIST) $(CURDIR)/csrc/sim_main.cpp + @$(VERILATOR) $(VERILATOR_SIM_FLAGS) -f $(RTL_FILELIST) $(CURDIR)/csrc/sim_main.cpp + +lint: check-tools check-soc-pkg + @$(VERILATOR) $(VERILATOR_LINT_FLAGS) -f $(RTL_FILELIST) -verilate: check-tools gen-soc-pkg $(SIM_BIN) +verilate: check-tools check-soc-pkg $(SIM_BIN) sim: check-image verilate @printf "$(CLR_STEP)Running $(SIM_BIN)$(CLR_RESET)\n" diff --git a/dv/verilator/README.md b/dv/verilator/README.md index 12afc41..4060f06 100644 --- a/dv/verilator/README.md +++ b/dv/verilator/README.md @@ -1,29 +1,35 @@ # 仿真流程(Verilator) -本目录是 SoC 级 Verilator 入口。流程从 `build/sw///` 读取原始 flash 镜像。 +本目录是 SoC 级 Verilator 入口。User Kit 固定使用随包提供的 +`sw/bootrom/hello/retrosoc_fw.bin` 原始 flash 镜像。 ## 快速开始 ```sh -# 如有需要,先构建软件: -# make sw APP=hello make sim MAX_CYCLES=1000 make wave ``` -根目录用户入口是 `make doctor`、`make check`、`make sim`、`make trace` 和 `make wave`。 +根目录用户入口是 `make doctor`、`make check`、`make lint`、`make sim`、`make trace` 和 `make wave`。 直接入口: ```sh -make -C dv/verilator sim BOOTROM_IMAGE=../../build/sw/mpc-soc/hello/hello.bin MAX_CYCLES=1000 +make -C dv/verilator sim \ + BOOTROM_IMAGE=../../sw/bootrom/hello/retrosoc_fw.bin \ + UART_STOP_TEXT='done!' MAX_CYCLES=500000 +make -C dv/verilator lint ``` ## 流程 -1. 软件构建到 `build/sw///`。 -2. `make sim` 默认 Verilate `hw/filelist/verilator.f`,并运行 `build/verilator/obj_dir_SimTop/VSimTop`。 +1. `make sim` 使用发行包中的固定 `hello` 镜像。 +2. 仿真默认 Verilate `hw/filelist/verilator.f`,并运行 `build/verilator/obj_dir_SimTop/VSimTop`。 3. harness 接收 `+bootrom=`,把原始二进制加载到 SPI flash DPI 模型,驱动 `clock/reset`,并在 `TRACE=1` 时写出 FST。 4. 日志输出到 `build/log/`;波形输出到 `build/wave/`。 +`lint` 使用独立的 Verilator lint 参数,只检查 RTL/filelist,不构建或运行 C++ +harness。仿真为了兼容固化顶层而屏蔽的 `CASEINCOMPLETE`、`UNOPTFLAT` 和 +`SYNCASYNCNET` 警告会在 lint 输出中保留。 + 默认 Verilator 顶层是 `SimTop`。它包装 pad 级 `asicTop` 并连接仿真板级模型。只有在 harness 需要直接驱动 pad 级 SoC 顶层时才使用 `TOP=asicTop`。仿真专用板级行为应保留在 `dv/verilator/csrc/` 中,共享 RTL 和 filelist 条目放在 `hw/` 下。 diff --git a/dv/verilator/csrc/sim_main.cpp b/dv/verilator/csrc/sim_main.cpp index d690b56..a493eea 100644 --- a/dv/verilator/csrc/sim_main.cpp +++ b/dv/verilator/csrc/sim_main.cpp @@ -35,6 +35,13 @@ std::uint32_t gpio_expect_mask = 0xf; std::size_t gpio_expect_index = 0; bool gpio_expect_matched = false; +enum class ExitReason { + StopConditionMatched, + FinishRequested, + FailureObserved, + CycleLimitReached, +}; + std::string plusarg_value(const char* prefix, int argc, char** argv) { const std::string key(prefix); for (int index = 1; index < argc; ++index) { @@ -400,6 +407,7 @@ int main(int argc, char** argv) { uart_fail_text = plusarg_value("+uart-fail-text=", argc, argv); const std::uint64_t max_cycles = plusarg_u64("+max-cycles=", DEFAULT_MAX_CYCLES, argc, argv); const auto core_sel = static_cast(plusarg_u64("+core-sel=", 0, argc, argv)); + const bool allow_timeout = plusarg_u64("+allow-timeout=", 0, argc, argv) != 0; const std::uint64_t uart_start_cycle = plusarg_u64("+uart-start-cycle=", 10000, argc, argv); const std::uint64_t uart1_bit_cycles = plusarg_u64("+uart1-bit-cycles=", 1, argc, argv); const auto gpio_in = static_cast(plusarg_u64("+gpio-in=", 0, argc, argv)); @@ -418,6 +426,9 @@ int main(int argc, char** argv) { } std::cout << "Max cycles: " << max_cycles << '\n'; std::cout << "Core select: " << (core_sel & 0xfU) << '\n'; + if (allow_timeout) { + std::cout << "Timeout policy: allow cycle-limit completion without pass conditions\n"; + } if (!uart_input.empty()) { std::cout << "UART input: " << uart_input << " (start cycle " << uart_start_cycle << ")\n"; } @@ -457,10 +468,12 @@ int main(int argc, char** argv) { #endif top->reset = 1; - for (std::uint64_t cycle = 0; cycle < max_cycles && !context->gotFinish() && - !all_stop_conditions_matched(uart1_wave) && !uart_fail_matched && - !uart1_wave.failed(); - ++cycle) { + std::uint64_t cycles_executed = 0; + for (; cycles_executed < max_cycles && !context->gotFinish() && + !all_stop_conditions_matched(uart1_wave) && !uart_fail_matched && + !uart1_wave.failed(); + ++cycles_executed) { + const auto cycle = cycles_executed; drive_static_inputs(top.get(), uart_rx.level(cycle), core_sel); drive_gpio_inputs(top.get(), gpio_in, gpio_drive); top->clock = 0; @@ -495,6 +508,15 @@ int main(int argc, char** argv) { } #endif + ExitReason exit_reason = ExitReason::CycleLimitReached; + if (uart_fail_matched || uart1_wave.failed()) { + exit_reason = ExitReason::FailureObserved; + } else if (all_stop_conditions_matched(uart1_wave)) { + exit_reason = ExitReason::StopConditionMatched; + } else if (context->gotFinish()) { + exit_reason = ExitReason::FinishRequested; + } + if (!gpio_expect_values.empty() && !gpio_expect_matched) { std::cerr << "SIM FAIL: GPIO output sequence stopped at index " << gpio_expect_index << " before " << context->time() << " ticks" << '\n'; @@ -522,6 +544,14 @@ int main(int argc, char** argv) { return 1; } + const bool has_pass_condition = + !uart_stop_text.empty() || !gpio_expect_values.empty() || uart1_wave.enabled(); + if (exit_reason == ExitReason::CycleLimitReached && (has_pass_condition || !allow_timeout)) { + std::cerr << "SIM FAIL: cycle limit reached before an explicit pass condition at " + << context->time() << " ticks" << '\n'; + return 1; + } + if (uart1_wave.enabled()) { std::cout << "SIM PASS: UART1 TX 8N1 waveform and stop conditions observed at " << context->time() << " ticks" << '\n'; @@ -529,8 +559,10 @@ int main(int argc, char** argv) { std::cout << "SIM PASS: GPIO output sequence observed at " << context->time() << " ticks" << '\n'; } else if (!uart_stop_text.empty()) { std::cout << "SIM PASS: UART stop text observed at " << context->time() << " ticks" << '\n'; + } else if (exit_reason == ExitReason::CycleLimitReached) { + std::cout << "SIM PASS: allowed cycle limit reached at " << context->time() << " ticks" << '\n'; } else { - std::cout << "SIM PASS: reached " << context->time() << " ticks" << '\n'; + std::cout << "SIM PASS: simulation finished at " << context->time() << " ticks" << '\n'; } return 0; } diff --git a/mk/common.mk b/mk/common.mk index b177a82..b8a38fc 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -2,10 +2,9 @@ SHELL := /bin/bash SOC_ROOT := $(CURDIR) SOC_MAKEFILE := $(firstword $(MAKEFILE_LIST)) -BOARD ?= mpc-soc -APP ?= hello TOP ?= SimTop MAX_CYCLES ?= 500000 +ALLOW_TIMEOUT ?= 0 TRACE ?= 0 CORE_SEL ?= 0 FAST_PSRAM ?= 1 @@ -17,62 +16,10 @@ PYTHON ?= python3 VERILATOR ?= verilator GTKWAVE ?= gtkwave CXX ?= c++ -CROSS_COMPILE ?= riscv64-unknown-elf- -VERILATOR_RECOMMENDED_VERSION := 5.050 +VERILATOR_REQUIRED_VERSION := 5.050 DOCS_SITE_ROOT := $(SOC_ROOT)/dev/site DOCS_SITE_SOURCE := $(SOC_ROOT)/build/docs-site -TEST_APP_DIR := $(SOC_ROOT)/sw/tests/$(APP) -UART_INPUT ?= -UART_STOP_TEXT ?= -UART_FAIL_TEXT ?= -UART1_EXPECT ?= -UART1_BIT_CYCLES ?= 8 -UART1_ARM_TEXT ?= -GPIO_IN ?= -GPIO_DRIVE ?= -GPIO_EXPECT ?= -GPIO_EXPECT_MASK ?= - -ifeq ($(APP),asm_hello) -ifeq ($(strip $(UART_INPUT)),) -UART_INPUT := 0123456789 -endif -ifeq ($(strip $(UART_STOP_TEXT)),) -UART_STOP_TEXT := done! -endif -endif - -ifeq ($(APP),psram_sweep) -ifeq ($(strip $(UART_STOP_TEXT)),) -UART_STOP_TEXT := psram sweep ok -endif -ifeq ($(strip $(UART_FAIL_TEXT)),) -UART_FAIL_TEXT := psram fail -endif -endif - -ifeq ($(APP),psram_basic) -ifeq ($(strip $(UART_STOP_TEXT)),) -UART_STOP_TEXT := psram ok -endif -ifeq ($(strip $(UART_FAIL_TEXT)),) -UART_FAIL_TEXT := psram fail -endif -endif - -ifeq ($(wildcard $(TEST_APP_DIR)/Makefile),) -BOOTROM_DIR ?= $(SOC_ROOT)/build/sw/$(BOARD)/$(APP) -SW_TARGET := sw -else -BOOTROM_DIR ?= $(TEST_APP_DIR)/build -SW_TARGET := test-sw -endif - -DEFAULT_BOOTROM_IMAGE = $(shell images=$$(find "$(BOOTROM_DIR)" -maxdepth 1 -type f -name '*.bin' 2>/dev/null | sort); preferred=$$(printf '%s\n' "$$images" | grep -v '/app\.bin$$' | head -n 1); if [ -n "$$preferred" ]; then printf '%s' "$$preferred"; else printf '%s\n' "$$images" | head -n 1; fi) -BOOTROM_IMAGE ?= $(DEFAULT_BOOTROM_IMAGE) -BOOTROM_IMAGE_ABS = $(if $(filter /%,$(BOOTROM_IMAGE)),$(BOOTROM_IMAGE),$(SOC_ROOT)/$(BOOTROM_IMAGE)) -BOOTROM_IMAGE_USER_SET := $(filter command line environment override,$(origin BOOTROM_IMAGE)) WAVE_FILE ?= $(SOC_ROOT)/build/wave/$(TOP).fst CLR_RESET := \033[0m CLR_INFO := \033[1;34m diff --git a/mk/dev.mk b/mk/dev.mk index 202aebb..c4e857e 100644 --- a/mk/dev.mk +++ b/mk/dev.mk @@ -1,12 +1,16 @@ -.PHONY: dev-help bootrom-sim regress clean-build docs-check docs-site-install \ - docs-site-prepare docs-site-dev docs-site-build docs-site-preview docs-site-check +.PHONY: dev-help bootrom-sim regress config-check gen-soc-pkg clean-build docs-check docs-site-install \ + docs-site-prepare docs-site-dev docs-site-build docs-site-preview docs-site-check \ + export-user-kit dev-help: @printf '%s\n' 'mpc-soc maintainer commands' @printf '%s\n' ' make -f Makefile.dev docs-check Validate bilingual docs' @printf '%s\n' ' make -f Makefile.dev docs-site-check Build the public documentation site' + @printf '%s\n' ' make -f Makefile.dev config-check Check fixed SoC configuration' + @printf '%s\n' ' make -f Makefile.dev gen-soc-pkg Regenerate the fixed SoC package' @printf '%s\n' ' make -f Makefile.dev bootrom-sim CASE=... Run one archived bootrom case' @printf '%s\n' ' make -f Makefile.dev regress Run the full bootrom regression' + @printf '%s\n' ' make -f Makefile.dev export-user-kit Export the tested user environment' @printf '%s\n' ' make -f Makefile.dev clean-build Remove the entire build/ tree' bootrom-sim: @@ -16,6 +20,12 @@ bootrom-sim: regress: @$(PYTHON) $(SOC_ROOT)/scripts/verilator_regress.py --output "$(OUTPUT)" --core-sel "$(CORE_SEL)" $(if $(CASES),--cases "$(CASES)",) $(if $(filter 1,$(STOP_ON_FAIL)),--stop-on-fail,) $(if $(filter-out 500000,$(MAX_CYCLES)),--max-cycles "$(MAX_CYCLES)",) $(if $(filter-out 0,$(TRACE)),--trace "$(TRACE)",) +config-check: + @$(PYTHON) $(SOC_ROOT)/scripts/check_fixed_config.py + +gen-soc-pkg: + @$(PYTHON) $(SOC_ROOT)/scripts/gen_soc_pkg.py + docs-check: @$(PYTHON) $(SOC_ROOT)/scripts/check_docs.py @@ -37,6 +47,10 @@ docs-site-preview: docs-site-check: docs-check docs-site-build +export-user-kit: + @$(PYTHON) $(SOC_ROOT)/scripts/export_user_kit.py \ + --root $(SOC_ROOT) --output $(SOC_ROOT)/build/user-kit + clean-build: @test -n "$(SOC_ROOT)" -a "$(SOC_ROOT)" != "/" || (printf "$(CLR_ERR)ERROR: invalid SOC_ROOT=$(SOC_ROOT)$(CLR_RESET)\n"; exit 2) @printf "$(CLR_INFO)Removing $(SOC_ROOT)/build$(CLR_RESET)\n" diff --git a/mk/user.mk b/mk/user.mk index f536582..93a66a5 100644 --- a/mk/user.mk +++ b/mk/user.mk @@ -1,50 +1,46 @@ -.PHONY: help doctor check sim sw test-sw verilate gen-soc-pkg trace wave clean +.PHONY: help doctor check sim lint verilate trace wave clean + +USER_BOOTROM_IMAGE := $(SOC_ROOT)/sw/bootrom/hello/retrosoc_fw.bin help: @printf '%s\n' 'mpc-soc user commands' @printf '%s\n' ' make doctor Check required tools' @printf '%s\n' ' make check [CASE=hello] Run one archived bootrom smoke' - @printf '%s\n' ' make sim [APP=hello] Build software and simulate' - @printf '%s\n' ' make trace [APP=hello] Simulate with FST tracing' + @printf '%s\n' ' make lint Run Verilator RTL lint' + @printf '%s\n' ' make sim Simulate with the bundled hello image' + @printf '%s\n' ' make trace Simulate the hello image with FST tracing' @printf '%s\n' ' make wave Open the generated FST in GTKWave' - @printf '%s\n' ' make sw [APP=hello] Build a software image' @printf '%s\n' ' make clean Remove generated build output' doctor: @command -v $(PYTHON) >/dev/null || (printf '%s\n' 'ERROR: Python 3 not found'; exit 127) + @$(PYTHON) -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' || (printf '%s\n' 'ERROR: Python 3.9 or newer is required'; exit 127) + @$(PYTHON) -c 'import yaml' >/dev/null 2>&1 || (printf '%s\n' \ + 'ERROR: PyYAML not found; install python3-yaml or pip install pyyaml'; exit 127) @command -v $(CXX) >/dev/null || (printf '%s\n' 'ERROR: C++ compiler not found'; exit 127) @command -v $(VERILATOR) >/dev/null || (printf '%s\n' 'ERROR: Verilator not found'; exit 127) - @command -v $(CROSS_COMPILE)gcc >/dev/null || (printf '%s\n' \ - 'ERROR: RISC-V toolchain not found: $(CROSS_COMPILE)gcc'; exit 127) @actual=$$($(VERILATOR) --version | awk '{print $$2}'); \ printf 'Python: %s\n' "$$($(PYTHON) --version 2>&1)"; \ printf 'C++: %s\n' "$$($(CXX) --version | head -n 1)"; \ - printf 'Verilator: %s (recommended: %s)\n' "$$actual" '$(VERILATOR_RECOMMENDED_VERSION)'; \ - printf 'Toolchain: %s\n' "$$($(CROSS_COMPILE)gcc --version | head -n 1)"; \ - if [ "$$actual" != '$(VERILATOR_RECOMMENDED_VERSION)' ]; then \ - printf '%s\n' 'WARNING: Verilator $(VERILATOR_RECOMMENDED_VERSION) is the series baseline'; \ + printf 'Verilator: %s (required: %s)\n' "$$actual" '$(VERILATOR_REQUIRED_VERSION)'; \ + if [ "$$actual" != '$(VERILATOR_REQUIRED_VERSION)' ]; then \ + printf '%s\n' 'ERROR: Verilator $(VERILATOR_REQUIRED_VERSION) is required' >&2; \ + exit 2; \ fi -gen-soc-pkg: - @$(PYTHON) $(SOC_ROOT)/scripts/gen_soc_pkg.py - -sw: - @$(MAKE) -C sw BOARD="$(BOARD)" APP="$(APP)" - -test-sw: - @$(MAKE) -C "sw/tests/$(APP)" APP=main - -verilate: gen-soc-pkg +verilate: @$(MAKE) -C dv/verilator verilate TOP="$(TOP)" TRACE="$(TRACE)" VERILATOR="$(VERILATOR)" -sim: gen-soc-pkg $(SW_TARGET) - @bootrom_image="$(BOOTROM_IMAGE_ABS)"; \ - if [ -z "$(BOOTROM_IMAGE_USER_SET)" ]; then \ - images=$$(find "$(BOOTROM_DIR)" -maxdepth 1 -type f -name '*.bin' 2>/dev/null | sort); \ - preferred=$$(printf '%s\n' "$$images" | grep -v '/app\.bin$$' | head -n 1); \ - if [ -n "$$preferred" ]; then bootrom_image="$$preferred"; else bootrom_image=$$(printf '%s\n' "$$images" | head -n 1); fi; \ - fi; \ - $(MAKE) -C dv/verilator sim TOP="$(TOP)" BOARD="$(BOARD)" APP="$(APP)" BOOTROM_IMAGE="$$bootrom_image" MAX_CYCLES="$(MAX_CYCLES)" TRACE="$(TRACE)" CORE_SEL="$(CORE_SEL)" FAST_PSRAM="$(FAST_PSRAM)" UART_INPUT="$(UART_INPUT)" UART_STOP_TEXT="$(UART_STOP_TEXT)" UART_FAIL_TEXT="$(UART_FAIL_TEXT)" UART1_EXPECT="$(UART1_EXPECT)" UART1_BIT_CYCLES="$(UART1_BIT_CYCLES)" UART1_ARM_TEXT="$(UART1_ARM_TEXT)" GPIO_IN="$(GPIO_IN)" GPIO_DRIVE="$(GPIO_DRIVE)" GPIO_EXPECT="$(GPIO_EXPECT)" GPIO_EXPECT_MASK="$(GPIO_EXPECT_MASK)" VERILATOR="$(VERILATOR)" +lint: + @$(MAKE) -C dv/verilator lint TOP="$(TOP)" FAST_PSRAM="$(FAST_PSRAM)" VERILATOR="$(VERILATOR)" + +sim: + @test -f "$(USER_BOOTROM_IMAGE)" || (printf '%s\n' \ + 'ERROR: bundled boot image not found: $(USER_BOOTROM_IMAGE)'; exit 2) + @$(MAKE) -C dv/verilator sim TOP="$(TOP)" BOARD="mpc-soc" APP="hello" \ + BOOTROM_IMAGE="$(USER_BOOTROM_IMAGE)" MAX_CYCLES="$(MAX_CYCLES)" \ + ALLOW_TIMEOUT="$(ALLOW_TIMEOUT)" TRACE="$(TRACE)" CORE_SEL="$(CORE_SEL)" \ + FAST_PSRAM="$(FAST_PSRAM)" UART_STOP_TEXT="done!" VERILATOR="$(VERILATOR)" check: @$(PYTHON) $(SOC_ROOT)/scripts/verilator_regress.py --cases "$(CASE)" \ @@ -55,18 +51,16 @@ check: @printf '%s\n' 'SOC SMOKE CHECK PASS' trace: - @$(MAKE) -f $(SOC_MAKEFILE) sim APP="$(APP)" TRACE=1 CORE_SEL="$(CORE_SEL)" MAX_CYCLES="$(MAX_CYCLES)" + @$(MAKE) -f $(SOC_MAKEFILE) sim TRACE=1 CORE_SEL="$(CORE_SEL)" MAX_CYCLES="$(MAX_CYCLES)" wave: @command -v $(GTKWAVE) >/dev/null 2>&1 || (printf '%s\n' \ 'ERROR: GTKWave not found; install it or set GTKWAVE='; exit 127) @test -f "$(WAVE_FILE)" || (printf '%s\n' \ 'ERROR: waveform not found: $(WAVE_FILE)' \ - 'Run make trace APP=$(APP) first.'; exit 2) + 'Run make trace first.'; exit 2) @$(GTKWAVE) "$(WAVE_FILE)" >/dev/null 2>&1 & @printf '%s\n' 'Opened waveform: $(WAVE_FILE)' clean: @$(MAKE) -C dv/verilator clean - @$(MAKE) -C sw clean BOARD="$(BOARD)" APP="$(APP)" - @if [ -d "sw/tests/$(APP)" ]; then $(MAKE) -C "sw/tests/$(APP)" APP=main clean; fi diff --git a/scripts/check_fixed_config.py b/scripts/check_fixed_config.py new file mode 100644 index 0000000..85311da --- /dev/null +++ b/scripts/check_fixed_config.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import sys +from pathlib import Path + +try: + import yaml +except ImportError as exc: + raise SystemExit("ERROR: PyYAML is required; install python3-yaml or pyyaml") from exc + + +FIXED_CLOCK_HZ = 50_000_000 +FIXED_CLOCK_MHZ = 50 +ROOT = Path(__file__).resolve().parents[1] + + +def yaml_clock(path: Path) -> int: + with path.open("r", encoding="utf-8") as file: + data = yaml.safe_load(file) + if not isinstance(data, dict) or not isinstance(data.get("clock_hz"), int): + raise ValueError(f"{path}: clock_hz must be an integer") + return data["clock_hz"] + + +def matched_int(path: Path, pattern: str, label: str) -> int: + match = re.search(pattern, path.read_text(encoding="utf-8"), re.MULTILINE) + if match is None: + raise ValueError(f"{path}: cannot find {label}") + return int(match.group(1)) + + +def main() -> int: + try: + checks = [ + ("config/soc.yml clock_hz", yaml_clock(ROOT / "config" / "soc.yml"), FIXED_CLOCK_HZ), + ( + "config/boards/sim.yml clock_hz", + yaml_clock(ROOT / "config" / "boards" / "sim.yml"), + FIXED_CLOCK_HZ, + ), + ( + "hw/include/soc_pkg.sv SOC_CLOCK_HZ", + matched_int( + ROOT / "hw" / "include" / "soc_pkg.sv", + r"^\s*localparam int unsigned SOC_CLOCK_HZ = (\d+);", + "SOC_CLOCK_HZ", + ), + FIXED_CLOCK_HZ, + ), + ( + "sw/ecos/board.h MPC_SOC_CLOCK_HZ", + matched_int( + ROOT / "sw" / "ecos" / "board.h", + r"^#define\s+MPC_SOC_CLOCK_HZ\s+(\d+)u", + "MPC_SOC_CLOCK_HZ", + ), + FIXED_CLOCK_HZ, + ), + ( + "sw/Makefile CPU_FREQ_MHZ default", + matched_int( + ROOT / "sw" / "Makefile", + r"^CPU_FREQ_MHZ\s*\?=\s*(\d+)", + "CPU_FREQ_MHZ", + ), + FIXED_CLOCK_MHZ, + ), + ( + "sw/Makefile TIMER_FREQ_MHZ default", + matched_int( + ROOT / "sw" / "Makefile", + r"^TIMER_FREQ_MHZ\s*\?=\s*(\d+)", + "TIMER_FREQ_MHZ", + ), + FIXED_CLOCK_MHZ, + ), + ] + except (OSError, ValueError, yaml.YAMLError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + failures = [] + for label, actual, expected in checks: + if actual != expected: + failures.append(f"{label}: expected {expected}, got {actual}") + + if failures: + for failure in failures: + print(f"ERROR: {failure}", file=sys.stderr) + return 1 + + print("fixed SoC clock configuration: 50 MHz") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/install_verilator.sh b/scripts/ci/install_verilator.sh new file mode 100755 index 0000000..893f46b --- /dev/null +++ b/scripts/ci/install_verilator.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +verilator_version=5.050 +verilator_commit=848d926ebd4addacacd294dc84e35d9d4ae8078c +expected_version="Verilator ${verilator_version}" + +if command -v verilator >/dev/null 2>&1 && + verilator --version | grep -Fq "$expected_version"; then + verilator --version + exit 0 +fi + +tool_root=${RUNNER_TEMP:-/tmp/mpc-soc-ci-tools} +source_dir="$tool_root/verilator-src-$verilator_version" +install_dir="$tool_root/verilator-$verilator_version" + +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + autoconf bison flex g++ git help2man libfl-dev liblz4-dev make perl \ + python3 zlib1g-dev + +rm -rf "$source_dir" "$install_dir" +git init "$source_dir" +git -C "$source_dir" remote add origin https://github.com/verilator/verilator.git +git -C "$source_dir" fetch --depth 1 origin "$verilator_commit" +git -C "$source_dir" checkout --detach FETCH_HEAD +test "$(git -C "$source_dir" rev-parse HEAD)" = "$verilator_commit" + +( + cd "$source_dir" + autoconf + ./configure --prefix="$install_dir" + make -j"$(nproc)" + make install +) + +"$install_dir/bin/verilator" --version | grep -F "$expected_version" +printf '%s/bin\n' "$install_dir" >> "$GITHUB_PATH" diff --git a/scripts/ci/prepare_minimal_ecos_sdk.sh b/scripts/ci/prepare_minimal_ecos_sdk.sh new file mode 100644 index 0000000..834f57b --- /dev/null +++ b/scripts/ci/prepare_minimal_ecos_sdk.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +sdk_dir="${1:?usage: prepare_minimal_ecos_sdk.sh }" +mkdir -p \ + "$sdk_dir/include" \ + "$sdk_dir/components/libc/include" \ + "$sdk_dir/components/libgcc/include" \ + "$sdk_dir/lib" + +cat > "$sdk_dir/include/hal_sys_uart.h" <<'EOF' +#ifndef HAL_SYS_UART_H +#define HAL_SYS_UART_H +#include +void hal_sys_uart_init(void); +void hal_sys_putchar(char c); +void hal_sys_putstr(char *str); +uint8_t hal_sys_getchar(void); +#endif +EOF + +cat > "$sdk_dir/include/hal_gpio.h" <<'EOF' +#ifndef HAL_GPIO_H +#define HAL_GPIO_H +#include +#define GPIO_LEVEL_LOW 0u +#define GPIO_LEVEL_HIGH 1u +void hal_gpio_set_dir(uint32_t value); +uint32_t hal_gpio_get_dir(void); +uint32_t hal_gpio_get_input(void); +uint32_t hal_gpio_get_output(void); +void hal_gpio_set_output(uint32_t value); +void hal_gpio_set_level(uint32_t pin, uint32_t level); +uint32_t hal_gpio_get_level(uint32_t pin); +#endif +EOF + +cat > "$sdk_dir/include/hal_timer.h" <<'EOF' +#ifndef HAL_TIMER_H +#define HAL_TIMER_H +#include +void hal_timer_stop(void); +void hal_timer_set_prescale(uint32_t value); +void hal_timer_set_cmp(uint32_t value); +void hal_timer_set_ctrl(uint32_t value); +uint32_t hal_timer_get_stat(void); +void hal_timer_clear(void); +void delay_ms(uint32_t value); +#endif +EOF + +cat > "$sdk_dir/include/hal_uart.h" <<'EOF' +#ifndef HAL_UART_H +#define HAL_UART_H +#include +void hal_uart_init(uint32_t baud); +void hal_uart_putchar(char c); +void hal_uart_putstr(const char *str); +int hal_uart_getchar(void); +#endif +EOF + +cat > "$sdk_dir/components/libc/include/string.h" <<'EOF' +#ifndef STRING_H +#define STRING_H +#include +void *memcpy(void *dest, const void *src, size_t n); +void *memset(void *s, int c, size_t n); +size_t strlen(const char *s); +int strcmp(const char *s1, const char *s2); +#endif +EOF + +cat > "$sdk_dir/components/libc/include/stdio.h" <<'EOF' +#ifndef STDIO_H +#define STDIO_H +int printf(const char *format, ...); +int puts(const char *s); +#endif +EOF + +cat > "$sdk_dir/components/libgcc/include/libgcc.h" <<'EOF' +#ifndef LIBGCC_H +#define LIBGCC_H +#endif +EOF diff --git a/scripts/export_user_kit.py b/scripts/export_user_kit.py new file mode 100644 index 0000000..918c5f3 --- /dev/null +++ b/scripts/export_user_kit.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Export the supported user-facing mpc-soc development environment.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +from pathlib import Path + + +def resolve_child(root: Path, value: object, context: str) -> Path: + if not isinstance(value, str) or not value: + raise ValueError(f"{context}: expected a non-empty relative path") + relative = Path(value) + if relative.is_absolute() or ".." in relative.parts: + raise ValueError(f"{context}: path escapes its root: {value}") + candidate = (root / relative).resolve() + try: + candidate.relative_to(root.resolve()) + except ValueError as exc: + raise ValueError(f"{context}: path escapes its root: {value}") from exc + return candidate + + +def string_list(value: object, context: str) -> list[str]: + if not isinstance(value, list) or not all( + isinstance(item, str) and item for item in value + ): + raise ValueError(f"{context}: expected an array of non-empty strings") + return value + + +def tracked_paths(root: Path, source_name: str) -> set[Path]: + result = subprocess.run( + ["git", "ls-files", "-z", "--", source_name], + cwd=root, + check=True, + capture_output=True, + ) + return { + Path(os.fsdecode(path)) + for path in result.stdout.split(b"\0") + if path + } + + +def reject_symlinks(source: Path) -> None: + if source.is_symlink(): + raise ValueError(f"user-kit source must not be a symlink: {source}") + if source.is_dir(): + for path in source.rglob("*"): + if path.is_symlink(): + raise ValueError(f"user-kit source contains a symlink: {path}") + + +def copy_entry( + root: Path, output: Path, source_name: str, target_name: str | None = None +) -> None: + source = resolve_child(root, source_name, "source") + target = resolve_child(output, target_name or source_name, "destination") + if not source.exists(): + raise FileNotFoundError(f"user-kit source does not exist: {source_name}") + reject_symlinks(root / source_name) + target.parent.mkdir(parents=True, exist_ok=True) + if source.is_dir(): + allowed = tracked_paths(root, source_name) + if not allowed: + raise ValueError(f"user-kit source directory has no tracked files: {source_name}") + + def ignore_untracked(directory: str, names: list[str]) -> list[str]: + directory_path = Path(directory).resolve() + ignored: list[str] = [] + for name in names: + path = directory_path / name + relative = path.relative_to(root) + if path.is_dir(): + if not any(item == relative or relative in item.parents for item in allowed): + ignored.append(name) + elif relative not in allowed: + ignored.append(name) + return ignored + + shutil.copytree(source, target, ignore=ignore_untracked) + else: + shutil.copy2(source, target) + + +def copy_public_docs(root: Path, output: Path, config: object) -> None: + if not isinstance(config, dict) or set(config) != {"manifest", "exclude"}: + raise ValueError("public_docs: expected only manifest and exclude") + manifest = resolve_child(root, config["manifest"], "public_docs.manifest") + docs_config = json.loads(manifest.read_text(encoding="utf-8")) + if not isinstance(docs_config, dict) or set(docs_config) != {"pages"}: + raise ValueError(f"{manifest}: expected an object containing only pages") + pages = string_list(docs_config["pages"], f"{manifest}.pages") + excluded = set(string_list(config["exclude"], "public_docs.exclude")) + unknown_exclusions = excluded - set(pages) + if unknown_exclusions: + names = ", ".join(sorted(unknown_exclusions)) + raise ValueError(f"public_docs.exclude: pages are not published: {names}") + for page in pages: + page_path = Path(page) + if page_path.is_absolute() or ".." in page_path.parts or page_path.suffix != ".md": + raise ValueError(f"{manifest}: invalid page entry: {page!r}") + if page in excluded: + continue + for locale in ("cn", "en"): + copy_entry(root, output, str(Path("docs") / locale / page_path)) + + +def write_metadata(root: Path, output: Path, destination: object) -> None: + metadata = resolve_child(output, destination, "metadata") + version = (root / "VERSION").read_text(encoding="utf-8").strip() + source_commit = os.environ.get("SOURCE_SHA") or os.environ.get("GITHUB_SHA") or "unknown" + metadata.parent.mkdir(parents=True, exist_ok=True) + metadata.write_text( + "KIT_FORMAT_VERSION=1\n" + f"SOC_VERSION={version}\n" + f"SOURCE_COMMIT={source_commit}\n", + encoding="utf-8", + ) + + +def export_user_kit(root: Path, output: Path, manifest: Path) -> None: + root = root.resolve() + output = output.resolve() + build_root = (root / "build").resolve() + manifest = manifest.resolve() + if output == build_root or build_root not in output.parents: + raise ValueError("output must be a child directory of build/") + + config = json.loads(manifest.read_text(encoding="utf-8")) + expected = {"files", "trees", "public_docs", "overrides", "metadata"} + if not isinstance(config, dict) or set(config) != expected: + raise ValueError(f"{manifest}: expected only {', '.join(sorted(expected))}") + + files = string_list(config["files"], "files") + trees = string_list(config["trees"], "trees") + overrides = config["overrides"] + if not isinstance(overrides, list): + raise ValueError("overrides: expected an array") + + shutil.rmtree(output, ignore_errors=True) + output.mkdir(parents=True) + for source in files: + copy_entry(root, output, source) + for source in trees: + copy_entry(root, output, source) + copy_public_docs(root, output, config["public_docs"]) + for index, override in enumerate(overrides): + if not isinstance(override, dict) or set(override) != {"source", "destination"}: + raise ValueError(f"overrides[{index}]: expected only source and destination") + copy_entry(root, output, override["source"], override["destination"]) + write_metadata(root, output, config["metadata"]) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--manifest", type=Path) + args = parser.parse_args() + root = args.root.resolve() + manifest = (args.manifest or root / "dev" / "user-kit.json").resolve() + try: + export_user_kit(root, args.output, manifest) + except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError) as exc: + parser.error(str(exc)) + print(f"Exported user kit to {args.output.resolve()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verilator_regress.py b/scripts/verilator_regress.py index a75a808..e6fd4ef 100644 --- a/scripts/verilator_regress.py +++ b/scripts/verilator_regress.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import re import subprocess import sys import time @@ -12,6 +13,31 @@ yaml = None +SAFE_CASE_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*\Z") + + +def validate_case_name(value: str, source: str = "case name") -> str: + if not SAFE_CASE_NAME.fullmatch(value): + raise ValueError(f"invalid {source}: {value!r}; use only letters, digits, '.', '_' or '-'") + return value + + +def resolve_case_image(case_dir: Path, image_name: str) -> Path: + image_path = Path(image_name) + if image_path.is_absolute() or len(image_path.parts) != 1 or image_path.name in ("", ".", ".."): + raise ValueError(f"image must be a file directly under {case_dir}: {image_name!r}") + if image_path.suffix != ".bin": + raise ValueError(f"boot image must use the .bin suffix: {image_name!r}") + + case_root = case_dir.resolve() + resolved = (case_root / image_path).resolve() + if resolved.parent != case_root: + raise ValueError(f"boot image resolves outside its case directory: {image_name!r}") + if not resolved.is_file(): + raise FileNotFoundError(f"boot image not found: {resolved}") + return resolved + + def load_case_config(case_dir: Path) -> dict: config_path = case_dir / "test.yml" if not config_path.exists(): @@ -29,6 +55,8 @@ def load_case_config(case_dir: Path) -> dict: with config_path.open("r", encoding="utf-8") as file: config = yaml.safe_load(file) or {} + if not isinstance(config, dict): + raise ValueError(f"{config_path} must contain a YAML mapping") config.setdefault("name", case_dir.name) return config @@ -38,15 +66,29 @@ def split_cases(value: str) -> list[str]: def discover_cases(bootrom_dir: Path, selected: str) -> list[Path]: + bootrom_dir = bootrom_dir.resolve() if selected: - return [bootrom_dir / case for case in split_cases(selected)] + cases = [] + for case in split_cases(selected): + validate_case_name(case) + case_dir = (bootrom_dir / case).resolve() + if case_dir.parent != bootrom_dir: + raise ValueError(f"case resolves outside bootrom directory: {case!r}") + if not case_dir.is_dir(): + raise FileNotFoundError(f"bootrom case not found: {case}") + cases.append(case_dir) + return cases cases = [] for path in sorted(bootrom_dir.iterdir()): if not path.is_dir(): continue - if (path / "test.yml").exists() or list(path.glob("*.bin")): - cases.append(path) + validate_case_name(path.name, "case directory name") + case_dir = path.resolve() + if case_dir.parent != bootrom_dir: + raise ValueError(f"case directory resolves outside bootrom directory: {path}") + if (case_dir / "test.yml").exists() or list(case_dir.glob("*.bin")): + cases.append(case_dir) return cases @@ -77,15 +119,14 @@ def make_run_log_dir(root: Path) -> Path: def run_case(args: argparse.Namespace, case_dir: Path) -> tuple[bool, Path, float]: started_at = time.monotonic() config = load_case_config(case_dir) - case_name = str(config.get("name") or case_dir.name) - image = case_dir / str(config.get("image", "main-asm.bin")) - if not image.exists(): - raise FileNotFoundError(f"{case_name}: boot image not found: {image}") + case_name = validate_case_name(str(config.get("name") or case_dir.name), "test.yml name") + image = resolve_case_image(case_dir, str(config.get("image", "main-asm.bin"))) uart = config.get("uart") or {} max_cycles = args.max_cycles if args.max_cycles is not None else config.get("max_cycles", 500000) trace = args.trace if args.trace is not None else int(bool(config.get("trace", False))) core_sel = args.core_sel if args.core_sel is not None else config.get("core_sel", 0) + allow_timeout = args.allow_timeout if args.allow_timeout is not None else bool(config.get("allow_timeout", False)) uart_input = args.uart_input if args.uart_input is not None else uart.get("input", "") uart_start_cycle = args.uart_start_cycle if args.uart_start_cycle is not None else uart.get("start_cycle", 10000) uart_stop_text = args.uart_stop_text if args.uart_stop_text is not None else uart.get("stop_text", "") @@ -103,8 +144,8 @@ def run_case(args: argparse.Namespace, case_dir: Path) -> tuple[bool, Path, floa gpio_expect = ",".join(str(item) for item in gpio_expect) log_dir = args.log_dir - log_path = log_dir / f"{case_name}.log" - make_log_path = log_dir / f"{case_name}.make.log" + log_path = log_dir / f"{case_dir.name}.log" + make_log_path = log_dir / f"{case_dir.name}.make.log" command = [ "make", @@ -112,10 +153,11 @@ def run_case(args: argparse.Namespace, case_dir: Path) -> tuple[bool, Path, floa str(args.root / "dv" / "verilator"), "sim", make_arg("APP", case_name), - make_arg("BOOTROM_IMAGE", str(image.resolve())), + make_arg("BOOTROM_IMAGE", str(image)), make_arg("MAX_CYCLES", max_cycles), make_arg("TRACE", trace), make_arg("CORE_SEL", core_sel), + make_arg("ALLOW_TIMEOUT", int(allow_timeout)), make_arg("SIM_LOG", str(log_path.resolve())), ] @@ -167,6 +209,7 @@ def main() -> int: parser.add_argument("--max-cycles", type=int, default=None) parser.add_argument("--trace", type=int, choices=(0, 1), default=None) parser.add_argument("--core-sel", type=int, default=None) + parser.add_argument("--allow-timeout", action=argparse.BooleanOptionalAction, default=None) parser.add_argument("--uart-input", default=None) parser.add_argument("--uart-start-cycle", type=int, default=None) parser.add_argument("--uart-stop-text", default=None) @@ -182,7 +225,6 @@ def main() -> int: args = parser.parse_args() args.root = args.root.resolve() - args.log_dir = make_run_log_dir(args.root) bootrom_dir = args.bootrom_dir or (args.root / "sw" / "bootrom") bootrom_dir = bootrom_dir.resolve() @@ -190,10 +232,15 @@ def main() -> int: print(f"ERROR: bootrom directory not found: {bootrom_dir}", file=sys.stderr) return 2 - cases = discover_cases(bootrom_dir, args.cases) + try: + cases = discover_cases(bootrom_dir, args.cases) + except (FileNotFoundError, ValueError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 if not cases: print(f"ERROR: no bootrom cases found under {bootrom_dir}", file=sys.stderr) return 2 + args.log_dir = make_run_log_dir(args.root) run_started_at = time.monotonic() if args.output == "list": diff --git a/sw/Makefile b/sw/Makefile index 9ea0adc..b4f5a8f 100644 --- a/sw/Makefile +++ b/sw/Makefile @@ -18,6 +18,7 @@ BOARD_PACKAGE_DIR := $(PORT_DIR) APP_DIR := $(PORT_DIR)/templates/$(APP) BUILD_DIR := $(SOC_ROOT)/build/sw/$(BOARD)/$(APP) TARGET := $(BUILD_DIR)/$(APP).elf +BUILD_CONFIG := $(BUILD_DIR)/.build-config AUTOCONF_H := $(BUILD_DIR)/generated/autoconf.h LINKER_INPUT ?= $(if $(wildcard $(APP_DIR)/sections.lds),$(APP_DIR)/sections.lds,$(BOARD_PACKAGE_DIR)/sections.lds) LINKER_SCRIPT := $(BUILD_DIR)/sections.lds @@ -47,7 +48,7 @@ BOARD_INCLUDES += $(foreach dir,$(wildcard $(SOC_ROOT)/sw/$(BOARD_PACKAGE_DIR)/d BOARD_INCLUDES += -I$(BUILD_DIR) APP_INCLUDES := -I$(SOC_ROOT)/sw/$(APP_DIR) -.PHONY: all check-board check-app info list clean +.PHONY: all check-board check-app info list clean FORCE all: check-board check-app $(TARGET) @$(SIZE) $(TARGET) @@ -77,7 +78,49 @@ list: @printf "$(CLR_INFO)Apps for BOARD=$(BOARD):$(CLR_RESET)\n" @find $(PORT_DIR)/templates -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf ' %f\n' | sort -$(AUTOCONF_H): Makefile ecos.mk +FORCE: + +$(BUILD_CONFIG): FORCE + @mkdir -p $(dir $@) + @{ \ + printf '%s\n' 'BOARD=$(BOARD)'; \ + printf '%s\n' 'APP=$(APP)'; \ + printf '%s\n' 'LINK_TARGET=$(LINK_TARGET)'; \ + printf '%s\n' 'LINK_DEFS=$(LINK_DEFS)'; \ + printf '%s\n' 'DRIVERS=$(DRIVERS)'; \ + printf '%s\n' 'FIRMWARE_NAME=$(FIRMWARE_NAME)'; \ + printf '%s\n' 'FIRMWARE_VERSION=$(FIRMWARE_VERSION)'; \ + printf '%s\n' 'CPU_FREQ_MHZ=$(CPU_FREQ_MHZ)'; \ + printf '%s\n' 'TIMER_FREQ_MHZ=$(TIMER_FREQ_MHZ)'; \ + printf '%s\n' 'UART_BAUD_RATE=$(UART_BAUD_RATE)'; \ + printf '%s\n' 'BOARD_FLASH_SIZE_KB=$(BOARD_FLASH_SIZE_KB)'; \ + printf '%s\n' 'BOARD_RAM_SIZE_KB=$(BOARD_RAM_SIZE_KB)'; \ + printf '%s\n' 'BOARD_RAM_TYPE_NAME=$(BOARD_RAM_TYPE_NAME)'; \ + printf '%s\n' 'ECOS_SDK=$(ECOS_SDK)'; \ + printf '%s\n' 'CROSS_COMPILE=$(CROSS_COMPILE)'; \ + printf '%s\n' 'CC=$(CC)'; \ + printf '%s\n' 'OBJCOPY=$(OBJCOPY)'; \ + printf '%s\n' 'OBJDUMP=$(OBJDUMP)'; \ + printf '%s\n' 'SIZE=$(SIZE)'; \ + printf '%s\n' 'ARCH_FLAGS=$(ARCH_FLAGS)'; \ + printf '%s\n' 'OPT_FLAGS=$(OPT_FLAGS)'; \ + printf '%s\n' 'WARN_FLAGS=$(WARN_FLAGS)'; \ + printf '%s\n' 'COMMON_CFLAGS=$(COMMON_CFLAGS)'; \ + printf '%s\n' 'COMMON_ASFLAGS=$(COMMON_ASFLAGS)'; \ + printf '%s\n' 'COMMON_LDFLAGS=$(COMMON_LDFLAGS)'; \ + printf '%s\n' 'ECOS_INCLUDES=$(ECOS_INCLUDES)'; \ + printf '%s\n' 'ECOS_LIB_DIRS=$(ECOS_LIB_DIRS)'; \ + printf '%s\n' 'ECOS_LIBS=$(ECOS_LIBS)'; \ + printf '%s\n' 'BOARD_INCLUDES=$(BOARD_INCLUDES)'; \ + printf '%s\n' 'APP_INCLUDES=$(APP_INCLUDES)'; \ + printf '%s\n' 'LINKER_INPUT=$(LINKER_INPUT)'; \ + printf '%s\n' 'C_SRCS=$(C_SRCS)'; \ + printf '%s\n' 'S_SRCS=$(S_SRCS)'; \ + } > $@.tmp + @cmp -s $@.tmp $@ || mv $@.tmp $@ + @rm -f $@.tmp + +$(AUTOCONF_H): $(BUILD_CONFIG) Makefile ecos.mk @mkdir -p $(dir $@) @printf "/*\n" > $@ @printf " * Automatically generated file; DO NOT EDIT.\n" >> $@ @@ -100,11 +143,11 @@ $(AUTOCONF_H): Makefile ecos.mk printf "#define CONFIG_DRIVER_%s 1\n" "$$upper" >> $@; \ done -$(LINKER_SCRIPT): $(LINKER_INPUT) $(AUTOCONF_H) +$(LINKER_SCRIPT): $(LINKER_INPUT) $(AUTOCONF_H) $(BUILD_CONFIG) @mkdir -p $(dir $@) @$(CC) -E -P -x c $(LINK_DEFS) $(COMMON_CFLAGS) $(BOARD_INCLUDES) $(APP_INCLUDES) $(ECOS_INCLUDES) -o $@ $< -$(TARGET): $(OBJS) $(LINKER_SCRIPT) +$(TARGET): $(OBJS) $(LINKER_SCRIPT) $(BUILD_CONFIG) @mkdir -p $(dir $@) @$(CC) $(COMMON_LDFLAGS) -T$(LINKER_SCRIPT) -Wl,-Map,$(BUILD_DIR)/$(APP).map -o $@ $(OBJS) $(ECOS_LIB_DIRS) $(ECOS_LIBS) @$(OBJCOPY) -O binary $@ $(BUILD_DIR)/$(APP).bin diff --git a/sw/bootrom/README.md b/sw/bootrom/README.md index 14bce9e..3285b57 100644 --- a/sw/bootrom/README.md +++ b/sw/bootrom/README.md @@ -11,6 +11,10 @@ sw/bootrom// [可选构建产物: .elf/.txt/.hex/sections.info] ``` +``、`test.yml` 中的 `name` 只允许字母、数字、点、下划线和连字符。 +`image` 必须是用例目录下的直接 `.bin` 文件名;不接受绝对路径、子目录或 `..`。 +这些约束确保固定归档回归不会读取用例目录之外的镜像或写出目录之外的日志。 + `test.yml` 示例: ```yaml diff --git a/tests/test_doctor.py b/tests/test_doctor.py new file mode 100644 index 0000000..a032170 --- /dev/null +++ b/tests/test_doctor.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class DoctorVersionTest(unittest.TestCase): + def run_doctor(self, version: str) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as temporary: + fake_verilator = Path(temporary) / "verilator" + fake_verilator.write_text( + f"#!/bin/sh\nprintf '%s\\n' 'Verilator {version} test-build'\n", + encoding="utf-8", + ) + fake_verilator.chmod(0o755) + return subprocess.run( + ["make", "doctor", f"VERILATOR={fake_verilator}"], + cwd=ROOT, + capture_output=True, + text=True, + ) + + def test_required_verilator_version_passes(self) -> None: + result = self.run_doctor("5.050") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("Verilator: 5.050 (required: 5.050)", result.stdout) + + def test_older_verilator_version_fails(self) -> None: + result = self.run_doctor("4.038") + self.assertNotEqual(result.returncode, 0) + self.assertIn("ERROR: Verilator 5.050 is required", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_export_user_kit.py b/tests/test_export_user_kit.py new file mode 100644 index 0000000..1161afa --- /dev/null +++ b/tests/test_export_user_kit.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import os +import shutil +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "export_user_kit.py" +OUTPUT = ROOT / "build" / "test-user-kit" + + +class ExportUserKitTest(unittest.TestCase): + UNTRACKED_TREE_FILE = ROOT / "hw" / "common" / "rtl" / ".user-kit-review-untracked" + SYMLINK_TREE_FILE = ROOT / "hw" / "common" / "rtl" / ".user-kit-review-link" + + def tearDown(self) -> None: + shutil.rmtree(OUTPUT, ignore_errors=True) + self.UNTRACKED_TREE_FILE.unlink(missing_ok=True) + self.SYMLINK_TREE_FILE.unlink(missing_ok=True) + + def test_export_contains_user_surface_only(self) -> None: + environment = os.environ.copy() + environment["SOURCE_SHA"] = "unit-test-source" + subprocess.run( + [ + "python3", + str(SCRIPT), + "--root", + str(ROOT), + "--output", + str(OUTPUT), + ], + check=True, + capture_output=True, + text=True, + env=environment, + ) + + self.assertTrue((OUTPUT / "Makefile").is_file()) + self.assertTrue((OUTPUT / "hw" / "soc" / "top" / "asic_top.v").is_file()) + self.assertTrue((OUTPUT / "sw" / "bootrom" / "hello" / "test.yml").is_file()) + self.assertTrue((OUTPUT / "sw" / "bootrom" / "hello" / "retrosoc_fw.bin").is_file()) + self.assertFalse((OUTPUT / "sw" / "Makefile").exists()) + self.assertFalse((OUTPUT / "sw" / "ecos").exists()) + self.assertFalse((OUTPUT / "sw" / "ecos.mk").exists()) + self.assertFalse((OUTPUT / "scripts" / "gen_soc_pkg.py").exists()) + self.assertFalse((OUTPUT / "Makefile.dev").exists()) + self.assertFalse((OUTPUT / ".github").exists()) + self.assertFalse((OUTPUT / "dev").exists()) + self.assertFalse((OUTPUT / "tests").exists()) + self.assertFalse((OUTPUT / "dv" / "verilator" / "tests").exists()) + self.assertFalse((OUTPUT / "docs" / "cn" / "index.md").exists()) + self.assertFalse((OUTPUT / "docs" / "en" / "index.md").exists()) + self.assertEqual( + sorted(path.name for path in (OUTPUT / "sw" / "bootrom").iterdir()), + ["hello"], + ) + metadata = (OUTPUT / "SOC_KIT_VERSION").read_text(encoding="utf-8") + self.assertIn("KIT_FORMAT_VERSION=1\n", metadata) + self.assertIn("SOURCE_COMMIT=unit-test-source\n", metadata) + + def test_export_rejects_output_outside_build(self) -> None: + result = subprocess.run( + [ + "python3", + str(SCRIPT), + "--root", + str(ROOT), + "--output", + str(ROOT / "user-kit-outside-build"), + ], + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("output must be a child directory of build/", result.stderr) + + def test_export_omits_untracked_tree_files(self) -> None: + self.UNTRACKED_TREE_FILE.write_text("must not ship\n", encoding="utf-8") + self.run_export() + self.assertFalse((OUTPUT / "hw" / "common" / "rtl" / self.UNTRACKED_TREE_FILE.name).exists()) + + def test_export_rejects_symlinks(self) -> None: + try: + self.SYMLINK_TREE_FILE.symlink_to(ROOT / "LICENSE") + except (OSError, NotImplementedError) as exc: + self.skipTest(f"symlinks unavailable: {exc}") + result = subprocess.run( + [ + "python3", + str(SCRIPT), + "--root", + str(ROOT), + "--output", + str(OUTPUT), + ], + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("contains a symlink", result.stderr) + + def run_export(self) -> None: + environment = os.environ.copy() + environment["SOURCE_SHA"] = "unit-test-source" + subprocess.run( + [ + "python3", + str(SCRIPT), + "--root", + str(ROOT), + "--output", + str(OUTPUT), + ], + check=True, + capture_output=True, + text=True, + env=environment, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_sim_timeout.py b/tests/test_sim_timeout.py new file mode 100644 index 0000000..eeae6d1 --- /dev/null +++ b/tests/test_sim_timeout.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SIM_MAIN = ROOT / "dv" / "verilator" / "csrc" / "sim_main.cpp" + + +class SimulatorTimeoutTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + compiler = shutil.which("c++") + if compiler is None: + raise unittest.SkipTest("a C++ compiler is required") + + cls.temp_dir = tempfile.TemporaryDirectory() + temp = Path(cls.temp_dir.name) + (temp / "verilated.h").write_text( + """#pragma once +#include + +class VerilatedContext { + public: + void commandArgs(int, char**) {} + bool gotFinish() const { return false; } + void timeInc(std::uint64_t amount) { time_ += amount; } + std::uint64_t time() const { return time_; } + void traceEverOn(bool) {} + + private: + std::uint64_t time_ = 0; +}; + +class Verilated { + public: + static void gotFinish(bool) {} +}; +""", + encoding="utf-8", + ) + (temp / "VTest.h").write_text( + """#pragma once +#include "verilated.h" + +class VTest { + public: + explicit VTest(VerilatedContext*) {} + void eval() {} + void final() {} + + int clock = 0; + int reset = 0; +}; +""", + encoding="utf-8", + ) + cls.simulator = temp / "sim-timeout-test" + subprocess.run( + [ + compiler, + "-std=c++17", + f"-I{temp}", + '-DSIM_TOP_HEADER="VTest.h"', + "-DSIM_TOP_CLASS=VTest", + "-DVM_TRACE=0", + str(SIM_MAIN), + "-o", + str(cls.simulator), + ], + check=True, + capture_output=True, + text=True, + ) + + @classmethod + def tearDownClass(cls) -> None: + cls.temp_dir.cleanup() + + def run_sim(self, *arguments: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(self.simulator), "+max-cycles=2", *arguments], + capture_output=True, + text=True, + ) + + def test_cycle_limit_without_pass_condition_fails(self) -> None: + result = self.run_sim() + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("SIM FAIL: cycle limit reached", result.stderr) + + def test_stop_condition_timeout_fails(self) -> None: + result = self.run_sim("+uart-stop-text=never") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("SIM FAIL: UART stop text not observed", result.stderr) + + def test_allow_timeout_does_not_override_stop_condition(self) -> None: + result = self.run_sim("+uart-stop-text=never", "+allow-timeout=1") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("SIM FAIL: UART stop text not observed", result.stderr) + + def test_cycle_limit_can_be_allowed_for_exploration(self) -> None: + result = self.run_sim("+allow-timeout=1") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("SIM PASS: allowed cycle limit reached", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_user_kit_workflow.py b/tests/test_user_kit_workflow.py new file mode 100644 index 0000000..b4fc07b --- /dev/null +++ b/tests/test_user_kit_workflow.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github" / "workflows" / "user-kit.yml" + + +class UserKitWorkflowTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.workflow = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) + + def test_workflow_run_jobs_only_accept_main_pushes_from_this_repository(self) -> None: + jobs = self.workflow["jobs"] + for job_name in ("export_and_test", "publish"): + condition = jobs[job_name]["if"] + with self.subTest(job=job_name): + self.assertIn("workflow_run.event == 'push'", condition) + self.assertIn("workflow_run.head_branch == 'main'", condition) + self.assertIn( + "workflow_run.head_repository.full_name == github.repository", + condition, + ) + + def test_tested_directory_is_cleaned_without_reexporting(self) -> None: + steps = self.workflow["jobs"]["export_and_test"]["steps"] + clean_step = next(step for step in steps if step.get("name") == "Clean tested user kit") + self.assertIn("rm -rf build/user-kit/build", clean_step["run"]) + self.assertNotIn("export-user-kit", clean_step["run"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_verilator_regress.py b/tests/test_verilator_regress.py new file mode 100644 index 0000000..c8213db --- /dev/null +++ b/tests/test_verilator_regress.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from scripts.verilator_regress import discover_cases, resolve_case_image, validate_case_name + + +class VerilatorRegressionBoundaryTest(unittest.TestCase): + def test_selected_case_must_be_a_direct_child(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + bootrom_dir = Path(temporary) + (bootrom_dir / "hello").mkdir() + + with self.assertRaisesRegex(ValueError, "invalid case name"): + discover_cases(bootrom_dir, "../hello") + with self.assertRaisesRegex(ValueError, "invalid case name"): + discover_cases(bootrom_dir, "/tmp/hello") + + def test_selected_case_must_exist(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaisesRegex(FileNotFoundError, "bootrom case not found"): + discover_cases(Path(temporary), "missing") + + def test_case_name_is_safe_for_make_and_logs(self) -> None: + self.assertEqual(validate_case_name("gpio-toggle_1.0"), "gpio-toggle_1.0") + for value in ("", ".", "..", "case/name", "case name", "name=value"): + with self.subTest(value=value), self.assertRaises(ValueError): + validate_case_name(value) + + def test_image_must_be_a_bin_inside_the_case(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + case_dir = Path(temporary) / "hello" + case_dir.mkdir() + image = case_dir / "hello.bin" + image.write_bytes(b"test") + + self.assertEqual(resolve_case_image(case_dir, "hello.bin"), image.resolve()) + for value in ("../outside.bin", "/tmp/outside.bin", "hello.elf"): + with self.subTest(value=value), self.assertRaises(ValueError): + resolve_case_image(case_dir, value) + + def test_image_symlink_cannot_escape_case(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + case_dir = root / "hello" + case_dir.mkdir() + outside = root / "outside.bin" + outside.write_bytes(b"test") + link = case_dir / "hello.bin" + try: + link.symlink_to(outside) + except (OSError, NotImplementedError) as exc: + self.skipTest(f"symlinks unavailable: {exc}") + + with self.assertRaisesRegex(ValueError, "outside its case directory"): + resolve_case_image(case_dir, link.name) + + +if __name__ == "__main__": + unittest.main()