diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 022617c..239399c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,25 +22,31 @@ jobs: with: go-version-file: go.mod - - name: Run tests with coverage - run: go test -race -coverprofile=coverage.out ./... + - name: Run tests + run: go test -race ./... - - name: Get coverage percentage - id: coverage + hdl-lint: + name: HDL Lint (Verilator) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Install Verilator run: | - COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') - echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT - echo "Coverage: $COVERAGE%" + sudo apt-get update + sudo apt-get install -y verilator - - name: Update coverage badge - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: schneegans/dynamic-badges-action@v1.8.0 + - name: Setup Go + uses: actions/setup-go@v6 with: - auth: ${{ secrets.GIST_SECRET }} - gistID: ${{ vars.COVERAGE_GIST_ID }} - filename: coverage.json - label: coverage - message: ${{ steps.coverage.outputs.percentage }}% - valColorRange: ${{ steps.coverage.outputs.percentage }} - maxColorRange: 100 - minColorRange: 0 + go-version-file: go.mod + + - name: Build pcileechgen + run: make build + + - name: Run HDL lint + run: make hdl-lint diff --git a/.gitignore b/.gitignore index bb1e692..133c87f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ pcileech_datastore/ *.bit *.json !internal/board/boards.json +!testdata/donors/*.json # Go *.exe @@ -37,4 +38,6 @@ Thumbs.db *.log *.str *.xpr -.Xil/ \ No newline at end of file +.Xil/ +# iverilog sim build artifacts +sim/build/ diff --git a/.golangci.yml b/.golangci.yml index 09de4ea..82fdd71 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,7 +9,6 @@ linters: - bodyclose - durationcheck - errorlint - - exportloopref - nilerr - prealloc - unconvert diff --git a/KNOWN_ISSUES b/KNOWN_ISSUES new file mode 100644 index 0000000..92925bc --- /dev/null +++ b/KNOWN_ISSUES @@ -0,0 +1,69 @@ +problem: "device data collection failed" / BAR reads all 0xFF +cause: donor is in D3 sleep, IOMMU is off, or the donor is not bound to vfio-pci. +fix: enable IOMMU in GRUB (intel_iommu=on / amd_iommu=on iommu=pt), update-grub + + reboot, wake the device to D0, bind it to vfio-pci, turn Secure Boot off. + +problem: "VFIO_GROUP_SET_CONTAINER: invalid argument" +cause: the donor shares its IOMMU group with devices still on native drivers. +fix: move the whole group to vfio-pci, or isolate the donor (ACS). + +problem: PC crashes / hard reboot during build +cause: a memory conflict - usually the OS/boot drive got bound to VFIO, or a + shared group. Look for MCE / "probe failed -22" in journalctl. +fix: never bind the drive you boot from; use a separate donor; isolate the group. + +problem: "card in sleep mode" +cause: runtime PM or the host driver keeps the donor in D3. +fix: disable runtime PM, keep the donor in D0, bind it to vfio-pci. + +problem: Live / USB-boot Ubuntu +cause: live-USB + VFIO is unreliable. +fix: do a real install to disk. + +problem: Vivado "exit status 1" / "Invalid XML token" +cause: a corrupted Vivado install (device parts / XML), not the tool. +fix: reinstall Vivado cleanly; --skip-vivado proves the artifacts are fine. + +problem: Vivado license lost under sudo +cause: sudo strips PATH / XILINXD_LICENSE_FILE. +fix: pass them back: sudo env PATH="$PATH" XILINXD_LICENSE_FILE="$..." \ + ./bin/pcileechgen build ... (latest build also recovers them itself) + +problem: donor BAR > board BRAM -> [FAIL] +cause: the donor BAR is bigger than the board can emulate. +fix: pick a smaller-BAR donor or a bigger board + +problem: Windows Code 10 "cannot start" (most common report) +cause: the clone loads but doesn't behave like the real device BAR0 reads FF, + MSI-X/interrupt setup fails, or device-type emulation is incomplete. + Often hidden: stale Windows device history. +fix: try a network / audio / wifi donor first (NVMe and xHCI hit Code 10 until + done); run tools/cleanup_device_history.ps1 (Admin) then reboot; Core + Isolation off; correct BIOS. + +problem: stale device metadata / wrong driver after reflash +cause: Windows caches PCI device info in the registry. +fix: run the bundled cleanup_device_history utility, reboot, reconnect. + +problem: yellow triangle / device not recognized after flash +cause: a partial flash image, or the device needs repo enhancements. +fix: rewrite the firmware with a working flasher; get a steady green light first. + +problem: lone-dma-test shows BAD DTB / can't find ntoskrnl +cause: usually BIOS / Windows DMA settings, not the firmware. +fix: recheck BIOS + Windows settings; compare to a known-good firmware. + +Note (not a bug): NVMe full disk emulation is still in progress. +The admin queue, init, and IO submission/completion queues work, but there is no +backing store yet (IO reads return zeros, writes are discarded), so NVMe donors +commonly hit Code 10 / stornvme Event 11 until a sector cache is added. + +problem: NIC "hangs on BAR" during data collection (e.g. Aquantia 10GbE) +cause: BAR profiling write-probes live device registers to map RW/W1C bits; + firmware-mailbox NICs deadlock and the next MMIO read hangs the CPU. +fix: fixed - network-class donors are no longer write-probed. For any other + card that hangs on BAR, collect read-only with: build --probe-bars=false + +Note (not a bug): xHCI (USB) emulation is partial. +Clones enumerate, but host read/write + doorbell gaps remain, so USBXHCI often +hits Code 10 (e.g. VIA VL805). Targeted this cycle. \ No newline at end of file diff --git a/Makefile b/Makefile index 7efebbd..af766db 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test lint clean install +.PHONY: build test lint clean install fixtures hdl-lint sim BINARY_NAME=pcileechgen BUILD_DIR=bin @@ -31,6 +31,19 @@ clean: rm -rf $(BUILD_DIR) rm -f coverage.out coverage.html +# generate synthetic donor fixture jsons +fixtures: build + $(BUILD_DIR)/$(BINARY_NAME) fixtures --out testdata/donors + +sim: + @command -v iverilog >/dev/null 2>&1 || { echo "iverilog not installed (apt-get install iverilog)"; exit 1; } + ./sim/run.sh + +# lint generated sv with verilator +hdl-lint: build + @command -v verilator >/dev/null 2>&1 || { echo "verilator not installed; see Makefile"; exit 1; } + ./scripts/hdl-lint.sh + # Install binary to GOPATH/bin install: $(GO) install $(GOFLAGS) -ldflags "$(LDFLAGS)" ./cmd/pcileechgen/ diff --git a/README b/README index 2c0cae2..1b58e49 100644 --- a/README +++ b/README @@ -4,7 +4,7 @@ Custom firmware generator for PCILeech FPGA boards. Reads a real PCI/PCIe donor device through VFIO, clones its identity (config space, BARs, capabilities), and builds a ready-to-flash .bin bitstream via Vivado. -Help / Discord: https://discord.gg/kcWVCAhNSg +Discord Community: https://discord.gg/kcWVCAhNSg ****************************************************************************** * WARNING: FOR EDUCATIONAL AND RESEARCH USE ONLY * @@ -16,11 +16,25 @@ Help / Discord: https://discord.gg/kcWVCAhNSg ****************************************************************************** +SPECIAL THANKS +-------------- + FTWDMA Sponsorship + https://ftwdma.com + sgorm0 Sponsorship + https://github.com/sgorm0 + TrueTuring Sponsorship, NVMe and xHCI fixes + https://github.com/TrueTuring + CaptainDMA Sponsorship + https://captaindma.com + pcileech-fpga The FPGA framework this project builds upon + https://github.com/ufrisk/pcileech-fpga + + PREREQUISITES ------------- - Go 1.26+ - Linux with IOMMU/VFIO enabled - - Vivado 2023.2+ (for synthesis) + - Vivado 2023.2 (for synthesis) VFIO needs IOMMU enabled in BIOS and in the kernel parameters (intel_iommu=on or amd_iommu=on). @@ -60,6 +74,7 @@ COMMANDS build generate firmware artifacts (+ Vivado) validate verify artifacts match the donor context verify-manifest check SHA256 integrity of build output + mmio-trace capture live or import saved BAR MMIO access traces boards list supported FPGA boards version print version @@ -70,6 +85,20 @@ COMMANDS --force allow donor BAR > board BRAM (may truncate) --from-json offline build from saved device_context.json --output output directory (default: pcileech_datastore) + --mmio-trace shape TLP latency emulation from a saved donor MMIO trace + --ila insert a Vivado ILA debug core (probes BAR/TLP/interrupt) + --ila-depth ILA capture depth in samples (default 1024) + --probe-bars write-probe BAR registers (default on; auto-skipped for + network cards; set --probe-bars=false if a donor hangs) + + Global flags (all commands): + --log-level debug|info|warn|error (default info) + --log-file tee the full pre/mid/post-generation run to a file + --json-logs structured JSON logs + + Offline MMIO trace import: + pcileechgen mmio-trace --trace-file mmiotrace.txt --bar-base 0xf7800000 \ + --bar-index 2 --bar-size 4096 --class-code 0x010802 --json FEATURES @@ -137,6 +166,7 @@ OUTPUT device_context.json donor snapshot pcileech_cfgspace.coe 4KB scrubbed config space pcileech_cfgspace_writemask.coe per-register write masks + pcileech_cfgspace_w1cmask.coe per-bit write-1-to-clear mask pcileech_bar_zero4k.coe BAR0 content (sized to donor/BRAM) pcileech_bar_impl_device.sv register-level BAR implementation pcileech_tlps128_bar_controller.sv TLP BAR controller @@ -150,6 +180,8 @@ OUTPUT identify_init.hex NVMe Identify ROM (if NVMe) vivado_generate_project.tcl project creation vivado_build.tcl synthesis script + code10_report.txt Code-10 risk summary for this build + ila_debug.txt ILA JTAG capture workflow (if --ila) build_manifest.json checksums + metadata src/ patched board SV sources *.bin bitstream (after Vivado) @@ -161,6 +193,7 @@ DEVELOPMENT make test-coverage tests with coverage report make lint run linter make check vet + lint + test + make sim unit-simulate the behavioral RTL modules (needs iverilog) UTILITIES @@ -190,16 +223,6 @@ Windows device-history cleanup the device. -SPECIAL THANKS --------------- - TrueTuring sponsorship, NVMe and xHCI fixes - https://github.com/TrueTuring - pcileech-fpga the FPGA framework this project builds upon - https://github.com/ufrisk/pcileech-fpga - CaptainDMA for best FPGA DMA hardware - https://captaindma.com - - LICENSE ------- Creative Commons Zero v1.0 Universal (CC0-1.0) diff --git a/cmd/pcileechgen/build.go b/cmd/pcileechgen/build.go index 1e202ef..7a0e5c7 100644 --- a/cmd/pcileechgen/build.go +++ b/cmd/pcileechgen/build.go @@ -1,30 +1,56 @@ package main import ( + _ "embed" "fmt" "log/slog" "github.com/sercanarga/pcileechgen/internal/board" "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/donor/behavior" "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/fallback" + "github.com/sercanarga/pcileechgen/internal/firmware/output" "github.com/sercanarga/pcileechgen/internal/pci" "github.com/sercanarga/pcileechgen/internal/vivado" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) +//go:embed fallback_defaults.yaml +var fallbackDefaultsYAML []byte + +// loadFallbackConfig loads --fallback-config if given, otherwise falls back +// to the built-in embedded defaults (fallback recovery is on by default). +func loadFallbackConfig(path string) (*fallback.Config, error) { + if path != "" { + return fallback.LoadConfig(path) + } + var cfg fallback.Config + if err := yaml.Unmarshal(fallbackDefaultsYAML, &cfg); err != nil { + return nil, fmt.Errorf("failed to parse embedded fallback defaults: %w", err) + } + return &cfg, nil +} + // buildFlags groups all build command flags. type buildFlags struct { - bdf string - board string - vivadoPath string - output string - skipVivado bool - jobs int - timeout int - libDir string - fromJSON string - stockBar bool - force bool + bdf string + board string + vivadoPath string + output string + skipVivado bool + jobs int + timeout int + libDir string + fromJSON string + stockBar bool + force bool + mmioTrace string + ila bool + ilaDepth int + probeBars bool + fallbackConfig string } var buildOpts buildFlags @@ -78,7 +104,7 @@ func runBuild(cmd *cobra.Command, args []string) error { slog.Warn("donor largest BAR exceeds board BRAM (forced)", "donor_bar", donorDemand, "board_bram", boardBRAM) } - builder := vivado.NewBuilder(b, vivado.BuildOptions{ + bopts := vivado.BuildOptions{ VivadoPath: buildOpts.vivadoPath, OutputDir: buildOpts.output, LibDir: buildOpts.libDir, @@ -87,9 +113,55 @@ func runBuild(cmd *cobra.Command, args []string) error { SkipVivado: buildOpts.skipVivado, StockBar: buildOpts.stockBar, Force: buildOpts.force, - }) + } - return builder.Build(ctx) + if buildOpts.mmioTrace != "" { + h, err := donorTimingHistogram(buildOpts.mmioTrace, ctx) + if err != nil { + return fmt.Errorf("load donor timing trace: %w", err) + } + bopts.TimingHistogram = h + slog.Info("donor latency profile loaded", "trace", buildOpts.mmioTrace, "samples", h.SampleCount) + } + + if buildOpts.ila { + depth := buildOpts.ilaDepth + if depth <= 0 { + depth = firmware.DefaultILADepth + } + bopts.ILADepth = depth + slog.Info("ILA debug core enabled", "depth", depth, "probes", len(firmware.ILAProbes())) + } + + fbCfg, err := loadFallbackConfig(buildOpts.fallbackConfig) + if err != nil { + return fmt.Errorf("load fallback config: %w", err) + } + output.DefaultFallbackConfig = fbCfg + + return vivado.NewBuilder(b, bopts).Build(ctx) +} + +func donorTimingHistogram(traceFile string, ctx *donor.DeviceContext) (*behavior.TimingHistogram, error) { + idx := firmware.LargestBarIndex(ctx.BARContents) + var size int + var base uint64 + for _, bar := range ctx.BARs { + if bar.Index == idx { + size = int(bar.Size) + base = bar.Address + } + } + trace, err := loadMMIOTrace(mmioTraceOptions{ + bdf: ctx.Device.BDF.String(), + barIndex: idx, + barSize: size, + traceFile: traceFile, + }, base) + if err != nil { + return nil, err + } + return behavior.ExtractTimingHistogram(trace), nil } // loadDonorContext loads device context from JSON or live device. @@ -112,6 +184,7 @@ func loadDonorContext() (*donor.DeviceContext, error) { slog.Info("collecting donor device data") collector := donor.NewCollector() + collector.ProbeBARs = buildOpts.probeBars ctx, err := collector.Collect(bdf) if err != nil { return nil, fmt.Errorf("device data collection failed: %w", err) @@ -149,6 +222,7 @@ func init() { buildCmd.Flags().StringVar(&buildOpts.bdf, "bdf", "", "donor device BDF address (e.g. 0000:03:00.0)") buildCmd.Flags().StringVar(&buildOpts.board, "board", "", "target FPGA board name (required, e.g. PCIeSquirrel)") buildCmd.Flags().StringVar(&buildOpts.fromJSON, "from-json", "", "load donor device data from JSON file (offline build)") + buildCmd.Flags().StringVar(&buildOpts.mmioTrace, "mmio-trace", "", "donor MMIO trace file; drives TLP latency emulation off the donor's real timing") buildCmd.Flags().StringVar(&buildOpts.vivadoPath, "vivado-path", "", "path to Vivado installation") buildCmd.Flags().StringVar(&buildOpts.output, "output", "pcileech_datastore", "output directory") buildCmd.Flags().BoolVar(&buildOpts.skipVivado, "skip-vivado", false, "skip Vivado synthesis (only generate artifacts)") @@ -157,6 +231,10 @@ func init() { buildCmd.Flags().StringVar(&buildOpts.libDir, "lib-dir", "lib/pcileech-fpga", "path to pcileech-fpga library") buildCmd.Flags().BoolVar(&buildOpts.stockBar, "stock-bar", false, "use stock bar controller (diagnostic: skip custom SV modules)") buildCmd.Flags().BoolVar(&buildOpts.force, "force", false, "ignore donor BAR > board BRAM check") + buildCmd.Flags().BoolVar(&buildOpts.ila, "ila", false, "insert a Vivado ILA debug core probing BAR/TLP/interrupt signals") + buildCmd.Flags().IntVar(&buildOpts.ilaDepth, "ila-depth", firmware.DefaultILADepth, "ILA capture depth in samples (with --ila)") + buildCmd.Flags().BoolVar(&buildOpts.probeBars, "probe-bars", true, "write-probe BAR registers to map RW/W1C bits (always skipped for network cards; set false if a donor hangs on BAR)") + buildCmd.Flags().StringVar(&buildOpts.fallbackConfig, "fallback-config", "", "path to a custom fallback defaults YAML (fills zeroed BAR registers by device class); empty uses the built-in embedded defaults") _ = buildCmd.MarkFlagRequired("board") diff --git a/cmd/pcileechgen/build_test.go b/cmd/pcileechgen/build_test.go new file mode 100644 index 0000000..5159059 --- /dev/null +++ b/cmd/pcileechgen/build_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestDonorTimingHistogram_FromTrace(t *testing.T) { + var b strings.Builder + ts := 1000.0 + for i := 0; i < 64; i++ { + ts += 0.000002 * float64(1+(i%5)) + fmt.Fprintf(&b, "R 4 %.6f 2 0xf780010c 0x%x 0x0 0\n", ts, i) + } + tracePath := filepath.Join(t.TempDir(), "mmiotrace.txt") + if err := os.WriteFile(tracePath, []byte(b.String()), 0o644); err != nil { + t.Fatalf("write trace: %v", err) + } + + ctx := &donor.DeviceContext{ + Device: pci.PCIDevice{VendorID: 0x144D, DeviceID: 0xA808, ClassCode: 0x010802}, + BARs: []pci.BAR{{Index: 2, Size: 4096, Address: 0xf7800000}}, + BARContents: map[int][]byte{2: make([]byte, 4096)}, + } + + h, err := donorTimingHistogram(tracePath, ctx) + if err != nil { + t.Fatalf("donorTimingHistogram: %v", err) + } + if h == nil || h.SampleCount == 0 { + t.Fatalf("expected a populated histogram, got %+v", h) + } +} diff --git a/cmd/pcileechgen/check.go b/cmd/pcileechgen/check.go index f8794f8..7652073 100644 --- a/cmd/pcileechgen/check.go +++ b/cmd/pcileechgen/check.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "os" "strings" "time" @@ -33,7 +32,7 @@ Example: return fmt.Errorf("invalid BDF: %w", err) } - c := &checker{bdf: bdf, sysfs: donor.NewSysfsReader(), w: os.Stdout} + c := &checker{bdf: bdf, sysfs: donor.NewSysfsReader(), w: humanWriter()} return c.run() }, } @@ -150,8 +149,8 @@ func (c *checker) checkPowerState() { // attempt auto-wake fmt.Fprintf(c.w, color.Dim("Power state: %s - attempting D0 wake...\n"), ps) - if err := vfio.WakeToD0(c.bdf.String()); err != nil { - fmt.Fprintln(c.w, color.Failf("Power state: %s - failed to wake device: %v", ps, err)) + if wakeErr := vfio.WakeToD0(c.bdf.String()); wakeErr != nil { + fmt.Fprintln(c.w, color.Failf("Power state: %s - failed to wake device: %v", ps, wakeErr)) c.issues++ return } diff --git a/cmd/pcileechgen/fallback_defaults.yaml b/cmd/pcileechgen/fallback_defaults.yaml new file mode 100644 index 0000000..839fb69 --- /dev/null +++ b/cmd/pcileechgen/fallback_defaults.yaml @@ -0,0 +1,78 @@ +# fallback_defaults.yaml - built-in class-specific BAR register fallbacks. +# +# Consumed by internal/firmware/fallback.Apply, which only fills BAR +# registers that are currently zero (real donor data is never overwritten). +# Keys under device_classes are fallback.ClassKey(baseClass, subClass), e.g. +# "0108" for NVMe (base 01 mass storage, sub 08 NVMe). +# +# bar0_defaults values are copied verbatim from the .Reset field of the +# matching internal/firmware/devclass profile's BARDefaults (the same +# per-class register defaults the scrub strategies already trust), keyed by +# the register .Offset. Registers whose profile .Reset is 0x00000000 are +# omitted: Apply() never writes a zero default over a zeroed register anyway. +device_classes: + "0108": # NVMe (devclass/nvme.go: nvmeProfile) + description: "NVMe controller" + bar0_defaults: + "0x00000000": 0x0040FF17 # CAP_LO + "0x00000004": 0x00000020 # CAP_HI + "0x00000008": 0x00010400 # VS + "0x00000014": 0x00460001 # CC + "0x0000001C": 0x00000001 # CSTS + "0x00000024": 0x001F001F # AQA + + "0C03": # xHCI USB (devclass/xhci.go: xhciProfile) + description: "xHCI USB controller" + bar0_defaults: + "0x00000000": 0x01100020 # CAPLENGTH_HCIVER + "0x00000004": 0x02000120 # HCSPARAMS1 + "0x00000010": 0x00000001 # HCCPARAMS1 + "0x00000014": 0x00000100 # DBOFF + "0x00000018": 0x00000200 # RTSOFF + "0x00000020": 0x00000001 # USBCMD + "0x00000028": 0x00000001 # PAGESIZE + "0x00000420": 0x000002A0 # PORTSC1 + "0x00000430": 0x000002A0 # PORTSC2 + + "0200": # Ethernet (devclass/ethernet.go: ethernetProfile) + description: "Ethernet controller" + bar0_defaults: + "0x00000000": 0xBEADDE02 # MAC0_3 + "0x00000004": 0x000000EF # MAC4_5 + "0x00000034": 0x0C000000 # CHIPCMD_DW + "0x00000040": 0x2F000000 # TXCONFIG + "0x00000044": 0x00000E00 # RXCONFIG + "0x00000050": 0x00003FFF # RXMAXSIZE + "0x00000058": 0x00002060 # CPLUSCMD + "0x0000006C": 0x00003010 # PHYSTATUS + "0x000000DC": 0x80000000 # PHYAR + "0x000000E0": 0x80000000 # ERIAR + + "0300": # GPU (devclass/gpu.go: gpuProfile) + description: "GPU" + bar0_defaults: + "0x00000200": 0xFFFFFFFF # PMC_ENABLE + "0x00001804": 0x00100006 # PBUS_PCI_NV_1 + + "0106": # SATA AHCI (devclass/sata.go: sataProfile) + description: "SATA AHCI controller" + bar0_defaults: + "0x00000000": 0x40341F05 # CAP + "0x00000004": 0x80000000 # GHC + "0x0000000C": 0x00000001 # PI + "0x00000010": 0x00010301 # VS + "0x00000024": 0x00000004 # CAP2 + "0x00000124": 0x00000101 # PxSIG (port 0) + "0x00000128": 0x00000113 # PxSSTS (port 0) + + "0280": # Wi-Fi (devclass/wifi.go: wifiProfile) + description: "Wi-Fi controller" + bar0_defaults: + "0x00000024": 0x00000080 # GP_CTL + "0x00000054": 0x00000001 # UCODE_DRV_GP1 + + "0C80": # Thunderbolt (devclass/thunderbolt.go: thunderboltProfile) + description: "Thunderbolt controller" + bar0_defaults: + "0x00000008": 0x00000001 # LC_STS + "0x00000014": 0x00000001 # THUNDERBOLT_CAP diff --git a/cmd/pcileechgen/fixtures.go b/cmd/pcileechgen/fixtures.go new file mode 100644 index 0000000..6b006e8 --- /dev/null +++ b/cmd/pcileechgen/fixtures.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/sercanarga/pcileechgen/internal/donor/synthetic" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/spf13/cobra" +) + +var fixturesOpts struct { + out string +} + +var fixturesCmd = &cobra.Command{ + Use: "fixtures", + Short: "Generate synthetic donor fixtures for CI HDL verification", + Long: "Writes one representative device_context.json per device class.", + RunE: func(cmd *cobra.Command, args []string) error { + if err := os.MkdirAll(fixturesOpts.out, 0o755); err != nil { + return fmt.Errorf("create output dir: %w", err) + } + for _, class := range devclass.AllClasses() { + ctx := synthetic.Build(class) + if ctx == nil { + return fmt.Errorf("no synthetic builder for class %q", class) + } + data, err := json.MarshalIndent(ctx, "", " ") + if err != nil { + return fmt.Errorf("marshal %s: %w", class, err) + } + path := filepath.Join(fixturesOpts.out, class+".json") + if err := os.WriteFile(path, data, 0o644); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + fmt.Println("wrote", path) + } + return nil + }, +} + +func init() { + fixturesCmd.Flags().StringVar(&fixturesOpts.out, "out", "testdata/donors", + "output directory for fixture JSON files") + rootCmd.AddCommand(fixturesCmd) +} diff --git a/cmd/pcileechgen/main.go b/cmd/pcileechgen/main.go index e72948d..b9eb5ac 100644 --- a/cmd/pcileechgen/main.go +++ b/cmd/pcileechgen/main.go @@ -2,11 +2,29 @@ package main import ( "fmt" + "io" + "log/slog" "os" + "github.com/sercanarga/pcileechgen/internal/color" "github.com/spf13/cobra" ) +var ( + logLevel string + logFile string + logJSON bool + + logFileHandle *os.File +) + +func humanWriter() io.Writer { + if logFileHandle == nil { + return os.Stdout + } + return io.MultiWriter(os.Stdout, logFileHandle) +} + var rootCmd = &cobra.Command{ Use: "pcileechgen", Short: "PCILeech FPGA firmware generator", @@ -19,6 +37,44 @@ This tool requires: - Linux with IOMMU/VFIO support (for device reading) - A real donor PCI/PCIe card - Xilinx Vivado (optional, for bitstream synthesis)`, + PersistentPreRunE: setupLogging, +} + +func init() { + pf := rootCmd.PersistentFlags() + pf.StringVar(&logLevel, "log-level", "info", "log verbosity: debug|info|warn|error") + pf.StringVar(&logFile, "log-file", "", "also write a full log to this file (attach it when reporting Code 10)") + pf.BoolVar(&logJSON, "json-logs", false, "emit structured JSON logs instead of text") +} + +func setupLogging(_ *cobra.Command, _ []string) error { + levels := map[string]slog.Level{ + "debug": slog.LevelDebug, "info": slog.LevelInfo, + "warn": slog.LevelWarn, "error": slog.LevelError, + } + lvl, ok := levels[logLevel] + if !ok { + return fmt.Errorf("invalid --log-level %q (want debug|info|warn|error)", logLevel) + } + + var w io.Writer = os.Stderr + if logFile != "" { + f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("cannot open --log-file: %w", err) + } + logFileHandle = f + w = io.MultiWriter(os.Stderr, f) + color.Disable() + } + + opts := &slog.HandlerOptions{Level: lvl} + var h slog.Handler = slog.NewTextHandler(w, opts) + if logJSON { + h = slog.NewJSONHandler(w, opts) + } + slog.SetDefault(slog.New(h)) + return nil } func main() { diff --git a/cmd/pcileechgen/mmio_trace.go b/cmd/pcileechgen/mmio_trace.go new file mode 100644 index 0000000..0fa8767 --- /dev/null +++ b/cmd/pcileechgen/mmio_trace.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strconv" + "strings" + "time" + + "github.com/sercanarga/pcileechgen/internal/color" + "github.com/sercanarga/pcileechgen/internal/donor/behavior" + "github.com/sercanarga/pcileechgen/internal/donor/mmio" + "github.com/sercanarga/pcileechgen/internal/pci" + "github.com/spf13/cobra" +) + +type mmioTraceOptions struct { + bdf string + duration time.Duration + barIndex int + barSize int + barBase string + classCode string + jsonOutput bool + outputFile string + traceFile string +} + +var mmioTraceOpts mmioTraceOptions + +var mmioTraceCmd = &cobra.Command{ + Use: "mmio-trace", + Short: "Capture live donor MMIO accesses with mmiotrace", + Long: `Captures MMIO BAR accesses for a short duration using the kernel mmiotrace tracer. + +Example: + pcileechgen mmio-trace --bdf 0000:03:00.0 --duration 5s + pcileechgen mmio-trace --bdf 03:00.0 --bar-size 4096 --class-code 0x010802 + pcileechgen mmio-trace --trace-file mmiotrace.txt --bar-base 0xf7800000 --bar-index 2 --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if mmioTraceOpts.traceFile == "" { + if _, err := pci.ParseBDF(mmioTraceOpts.bdf); err != nil { + return fmt.Errorf("invalid BDF %q: %w", mmioTraceOpts.bdf, err) + } + } + if mmioTraceOpts.barIndex < 0 || mmioTraceOpts.barIndex > 5 { + return fmt.Errorf("--bar-index must be between 0 and 5") + } + if mmioTraceOpts.barSize <= 0 || mmioTraceOpts.barSize > 16*1024*1024 { + return fmt.Errorf("--bar-size must be between 1 and 16MB") + } + if mmioTraceOpts.duration <= 0 { + return fmt.Errorf("--duration must be greater than zero") + } + + classCode, err := parseTraceClassCode(mmioTraceOpts.classCode) + if err != nil { + return err + } + + barBase, err := parseTraceBARBase(mmioTraceOpts.barBase) + if err != nil { + return err + } + + trace, err := loadMMIOTrace(mmioTraceOpts, barBase) + if err != nil { + return err + } + + pattern := mmio.Analyze(trace) + profile := behavior.FromMMIOTrace(trace, classCode) + timing := behavior.ExtractTimingHistogram(trace) + + if mmioTraceOpts.jsonOutput { + report := map[string]any{ + "trace": trace, + "analysis": pattern, + "behavior_profile": profile, + "timing_histogram": timing, + } + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + if err := enc.Encode(report); err != nil { + return fmt.Errorf("render JSON report: %w", err) + } + } else { + printMMIOTraceReport(cmd.OutOrStdout(), trace, pattern, profile, timing) + } + + if mmioTraceOpts.outputFile != "" { + traceData, err := json.MarshalIndent(trace, "", " ") + if err != nil { + return fmt.Errorf("marshal trace: %w", err) + } + if err := os.WriteFile(mmioTraceOpts.outputFile, traceData, 0o644); err != nil { + return fmt.Errorf("write %q: %w", mmioTraceOpts.outputFile, err) + } + fmt.Fprintln(cmd.ErrOrStderr(), color.OK(fmt.Sprintf("saved trace to %s", mmioTraceOpts.outputFile))) + } + + return nil + }, +} + +func loadMMIOTrace(opts mmioTraceOptions, barBase uint64) (*mmio.TraceResult, error) { + if opts.traceFile != "" { + f, err := os.Open(opts.traceFile) + if err != nil { + return nil, fmt.Errorf("open trace file %q: %w", opts.traceFile, err) + } + defer f.Close() + + trace, err := mmio.ParseTextTrace(f, mmio.TextTraceOptions{ + BDF: opts.bdf, + BARIndex: opts.barIndex, + BARSize: opts.barSize, + BARBase: barBase, + }) + if err != nil { + return nil, fmt.Errorf("parse trace file %q: %w", opts.traceFile, err) + } + trace.StartTime = time.Now().Add(-trace.Duration) + return trace, nil + } + + start := time.Now() + trace, err := mmio.LiveTrace(opts.bdf, opts.duration) + if err != nil { + return nil, fmt.Errorf("trace capture failed: %w", err) + } + trace.BARIndex = opts.barIndex + trace.BARSize = opts.barSize + trace.Duration = opts.duration + trace.StartTime = start + return trace, nil +} + +func parseTraceClassCode(raw string) (uint32, error) { + if strings.TrimSpace(raw) == "" { + return 0, nil + } + + r := strings.TrimSpace(raw) + if !strings.HasPrefix(r, "0x") && !strings.HasPrefix(r, "0X") { + r = "0x" + r + } + + v, err := strconv.ParseUint(r, 0, 24) + if err != nil { + return 0, fmt.Errorf("invalid --class-code %q: %w", raw, err) + } + + return uint32(v), nil +} + +func parseTraceBARBase(raw string) (uint64, error) { + if strings.TrimSpace(raw) == "" { + return 0, nil + } + + r := strings.TrimSpace(raw) + if !strings.HasPrefix(r, "0x") && !strings.HasPrefix(r, "0X") { + r = "0x" + r + } + + v, err := strconv.ParseUint(r, 0, 64) + if err != nil { + return 0, fmt.Errorf("invalid --bar-base %q: %w", raw, err) + } + + return v, nil +} + +func printMMIOTraceReport(out io.Writer, trace *mmio.TraceResult, pattern *mmio.AccessPattern, profile *behavior.Profile, timing *behavior.TimingHistogram) { + if out == nil { + return + } + + fmt.Fprintln(out, color.Header("MMIO Trace Capture")) + fmt.Fprintf(out, "BDF: %s\n", trace.BDF) + fmt.Fprintf(out, "BAR index: %d | bar size: %d\n", trace.BARIndex, trace.BARSize) + fmt.Fprintf(out, "Duration: %s | records: %d\n\n", trace.Duration, len(trace.Records)) + + if len(trace.Records) == 0 { + fmt.Fprintln(out, color.Warn("No MMIO records were captured.\n")) + return + } + + fmt.Fprintln(out, mmio.FormatReport(pattern)) + + fmt.Fprintln(out, color.Header("Behavior Profile")) + fmt.Fprintf(out, "%s\n", behavior.FormatReport(profile)) + + fmt.Fprintln(out, color.Header("Timing Histogram")) + fmt.Fprintf(out, "Samples: %d\n", timing.SampleCount) + fmt.Fprintf(out, "Min cycles: %d\n", timing.MinCycles) + fmt.Fprintf(out, "Median cycles: %d\n", timing.MedianCycles) + fmt.Fprintf(out, "Max cycles: %d\n", timing.MaxCycles) +} + +func init() { + mmioTraceCmd.Flags().StringVar(&mmioTraceOpts.bdf, "bdf", "", "device BDF address (e.g. 0000:03:00.0)") + mmioTraceCmd.Flags().DurationVar(&mmioTraceOpts.duration, "duration", 5*time.Second, "trace length (e.g. 5s, 1m)") + mmioTraceCmd.Flags().IntVar(&mmioTraceOpts.barIndex, "bar-index", 0, "BAR index that was targeted for this trace") + mmioTraceCmd.Flags().IntVar(&mmioTraceOpts.barSize, "bar-size", 4096, "expected BAR size hint in bytes") + mmioTraceCmd.Flags().StringVar(&mmioTraceOpts.barBase, "bar-base", "", "absolute BAR base for offline trace address normalization") + mmioTraceCmd.Flags().StringVar(&mmioTraceOpts.classCode, "class-code", "", "PCI class code in hex (e.g. 0x010802)") + mmioTraceCmd.Flags().StringVar(&mmioTraceOpts.outputFile, "output", "", "save raw trace JSON to file") + mmioTraceCmd.Flags().StringVar(&mmioTraceOpts.traceFile, "trace-file", "", "analyze an existing mmiotrace text file instead of capturing live") + mmioTraceCmd.Flags().BoolVar(&mmioTraceOpts.jsonOutput, "json", false, "emit machine-readable report") + rootCmd.AddCommand(mmioTraceCmd) +} diff --git a/cmd/pcileechgen/mmio_trace_test.go b/cmd/pcileechgen/mmio_trace_test.go new file mode 100644 index 0000000..fefcfbc --- /dev/null +++ b/cmd/pcileechgen/mmio_trace_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseTraceBARBase(t *testing.T) { + got, err := parseTraceBARBase("f7800000") + if err != nil { + t.Fatalf("parseTraceBARBase returned error: %v", err) + } + if got != 0xf7800000 { + t.Fatalf("bar base = 0x%X, want 0xF7800000", got) + } +} + +func TestLoadMMIOTrace_FromTraceFile(t *testing.T) { + tracePath := filepath.Join(t.TempDir(), "mmiotrace.txt") + input := []byte("R 4 2456.105919 2 0xf780010c 0x4c02 0x0 0\n") + if err := os.WriteFile(tracePath, input, 0o644); err != nil { + t.Fatalf("write trace fixture: %v", err) + } + + trace, err := loadMMIOTrace(mmioTraceOptions{ + bdf: "0000:03:00.0", + barIndex: 2, + barSize: 4096, + traceFile: tracePath, + }, 0xf7800000) + + if err != nil { + t.Fatalf("loadMMIOTrace returned error: %v", err) + } + if trace.BDF != "0000:03:00.0" || trace.BARIndex != 2 || trace.BARSize != 4096 { + t.Fatalf("trace metadata = %+v", trace) + } + if len(trace.Records) != 1 || trace.Records[0].Offset != 0x10c || trace.Records[0].Value != 0x4c02 { + t.Fatalf("records = %+v", trace.Records) + } +} diff --git a/cmd/pcileechgen/validate.go b/cmd/pcileechgen/validate.go index eb7a812..b7b5d56 100644 --- a/cmd/pcileechgen/validate.go +++ b/cmd/pcileechgen/validate.go @@ -48,7 +48,8 @@ Example: if err != nil { return fmt.Errorf("failed to load device context: %w", err) } - fmt.Printf("Loaded donor context: %s\n\n", + out := humanWriter() + fmt.Fprintf(out, "Loaded donor context: %s\n\n", color.Bold(fmt.Sprintf("%04x:%04x (rev %02x)", ctx.Device.VendorID, ctx.Device.DeviceID, ctx.Device.RevisionID))) var b *board.Board @@ -57,10 +58,10 @@ Example: if err != nil { return err } - fmt.Printf("Board: %s (%s x%d)\n\n", b.Name, b.FPGAPart, b.PCIeLanes) + fmt.Fprintf(out, "Board: %s (%s x%d)\n\n", b.Name, b.FPGAPart, b.PCIeLanes) } else { - fmt.Println(color.Warn("No --board specified: link speed/width clamping not applied in validation")) - fmt.Println() + fmt.Fprintln(out, color.Warn("No --board specified: link speed/width clamping not applied in validation")) + fmt.Fprintln(out) } msixTableSize := 0 @@ -93,16 +94,16 @@ Example: // Print results for _, p := range v.result.Passed { - fmt.Println(color.OK(p)) + fmt.Fprintln(out, color.OK(p)) } for _, w := range v.result.Warnings { - fmt.Println(color.Warn(w)) + fmt.Fprintln(out, color.Warn(w)) } for _, f := range v.result.Failed { - fmt.Println(color.Fail(f)) + fmt.Fprintln(out, color.Fail(f)) } - fmt.Printf("\n%s\n", color.Header(v.result.Summary())) + fmt.Fprintf(out, "\n%s\n", color.Header(v.result.Summary())) if v.result.HasFailures() { return fmt.Errorf("%d validation(s) failed", len(v.result.Failed)) } diff --git a/cmd/pcileechgen/validate_test.go b/cmd/pcileechgen/validate_test.go new file mode 100644 index 0000000..47e0696 --- /dev/null +++ b/cmd/pcileechgen/validate_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "os" + "path/filepath" + "slices" + "testing" + + "github.com/sercanarga/pcileechgen/internal/firmware/codegen" + "github.com/sercanarga/pcileechgen/internal/firmware/output" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestValidateCOEFiles_ConfigSpaceMismatchFails(t *testing.T) { + dir := t.TempDir() + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF000) + + if err := os.WriteFile(filepath.Join(dir, "pcileech_cfgspace.coe"), []byte("bad cfgspace\n"), 0o644); err != nil { + t.Fatalf("write cfgspace fixture: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "pcileech_cfgspace_writemask.coe"), []byte(codegen.GenerateWritemaskCOE(cs)), 0o644); err != nil { + t.Fatalf("write writemask fixture: %v", err) + } + + v := &validator{ + outputDir: dir, + scrubbed: cs, + result: &output.ValidationResult{}, + } + + v.validateCOEFiles() + + if !slices.Contains(v.result.Failed, "pcileech_cfgspace.coe MISMATCH") { + t.Fatalf("failed validations = %v, want cfgspace mismatch failure", v.result.Failed) + } + if len(v.result.Warnings) != 0 { + t.Fatalf("warnings = %v, want none", v.result.Warnings) + } +} diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index 0498295..0000000 Binary files a/docs/logo.png and /dev/null differ diff --git a/go.mod b/go.mod index dfb9ce0..d302779 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect ) diff --git a/go.sum b/go.sum index 5f99fe6..51eeaa6 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/internal/board/board.go b/internal/board/board.go index dcfd305..79b470e 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -19,6 +19,8 @@ type Board struct { TopModule string `json:"top_module"` ProjectDir string `json:"project_dir"` SubDir string `json:"sub_dir"` + // SourceSubDir is mutually exclusive with SubDir; it only overrides source/IP paths. + SourceSubDir string `json:"source_sub_dir"` TCLFile string `json:"tcl_file"` BuildTCL string `json:"build_tcl"` } @@ -44,20 +46,22 @@ func (b *Board) BRAMSizeOrDefault() int { return DefaultBRAMSize } +// SourceBasePath returns the directory that contains source and IP folders. +func (b *Board) SourceBasePath(libDir string) string { + if b.SourceSubDir != "" { + return filepath.Join(libDir, b.ProjectDir, b.SourceSubDir) + } + return b.LibPath(libDir) +} + // SrcPath returns the path to source files for this board. func (b *Board) SrcPath(libDir string) string { - if b.SubDir != "" { - return filepath.Join(libDir, b.ProjectDir, b.SubDir, "src") - } - return filepath.Join(libDir, b.ProjectDir, "src") + return filepath.Join(b.SourceBasePath(libDir), "src") } // IPPath returns the path to IP cores for this board. func (b *Board) IPPath(libDir string) string { - if b.SubDir != "" { - return filepath.Join(libDir, b.ProjectDir, b.SubDir, "ip") - } - return filepath.Join(libDir, b.ProjectDir, "ip") + return filepath.Join(b.SourceBasePath(libDir), "ip") } // TCLPath returns the full path to the Vivado project generation TCL script. diff --git a/internal/board/board_embed_test.go b/internal/board/board_embed_test.go index 60e40a3..8662080 100644 --- a/internal/board/board_embed_test.go +++ b/internal/board/board_embed_test.go @@ -119,6 +119,31 @@ func TestBoard_Paths(t *testing.T) { } } +func TestBoard_SourceSubDirPaths(t *testing.T) { + b := &Board{ + ProjectDir: "ZDMA", + SourceSubDir: "100T", + TCLFile: "vivado_generate_project_100t.tcl", + BuildTCL: "vivado_build_100t.tcl", + } + + if got := b.SourceBasePath("/fpga"); got != "/fpga/ZDMA/100T" { + t.Errorf("SourceBasePath = %q", got) + } + if got := b.SrcPath("/fpga"); got != "/fpga/ZDMA/100T/src" { + t.Errorf("SrcPath = %q", got) + } + if got := b.IPPath("/fpga"); got != "/fpga/ZDMA/100T/ip" { + t.Errorf("IPPath = %q", got) + } + if got := b.TCLPath("/fpga"); got != "/fpga/ZDMA/vivado_generate_project_100t.tcl" { + t.Errorf("TCLPath = %q", got) + } + if got := b.BuildTCLPath("/fpga"); got != "/fpga/ZDMA/vivado_build_100t.tcl" { + t.Errorf("BuildTCLPath = %q", got) + } +} + func TestBoard_PathsNoSubDir(t *testing.T) { b := &Board{ ProjectDir: "PCIeSquirrel", diff --git a/internal/board/board_test.go b/internal/board/board_test.go index fd2a2d3..ac38a53 100644 --- a/internal/board/board_test.go +++ b/internal/board/board_test.go @@ -70,11 +70,17 @@ func TestBoardPaths(t *testing.T) { t.Errorf("CaptainDMA_100T TCLPath() = %q", cdma.TCLPath(libDir)) } - // Board with custom BuildTCL (ZDMA) + // Board with source tree nested below project TCL scripts (ZDMA) zdma, _ := Find("ZDMA") if zdma.BuildTCLPath(libDir) != libDir+"/ZDMA/vivado_build_100t.tcl" { t.Errorf("ZDMA BuildTCLPath() = %q", zdma.BuildTCLPath(libDir)) } + if zdma.SrcPath(libDir) != libDir+"/ZDMA/100T/src" { + t.Errorf("ZDMA SrcPath() = %q", zdma.SrcPath(libDir)) + } + if zdma.IPPath(libDir) != libDir+"/ZDMA/100T/ip" { + t.Errorf("ZDMA IPPath() = %q", zdma.IPPath(libDir)) + } } func TestBoardString(t *testing.T) { diff --git a/internal/board/boards.json b/internal/board/boards.json index 2eabef6..f9e8b11 100644 --- a/internal/board/boards.json +++ b/internal/board/boards.json @@ -97,7 +97,8 @@ "top_module": "pcileech_tbx4_100t_top", "project_dir": "ZDMA", "tcl_file": "vivado_generate_project_100t.tcl", - "build_tcl": "vivado_build_100t.tcl" + "build_tcl": "vivado_build_100t.tcl", + "source_sub_dir": "100T" }, { "name": "GBOX", @@ -166,4 +167,4 @@ "project_dir": "sp605_ft601", "tcl_file": "vivado_generate_project.tcl" } -] \ No newline at end of file +] diff --git a/internal/donor/bar_probe_guard.go b/internal/donor/bar_probe_guard.go new file mode 100644 index 0000000..a127719 --- /dev/null +++ b/internal/donor/bar_probe_guard.go @@ -0,0 +1,18 @@ +package donor + +func isNetworkClass(classCode uint32) bool { + return (classCode>>16)&0xFF == 0x02 +} + +func shouldProbeBAR(enabled bool, classCode uint32, content []byte) bool { + if !enabled { + return false + } + if len(content) > 0 && isAllFF(content) { + return false + } + if isNetworkClass(classCode) { + return false + } + return true +} diff --git a/internal/donor/bar_probe_guard_test.go b/internal/donor/bar_probe_guard_test.go new file mode 100644 index 0000000..d7715fe --- /dev/null +++ b/internal/donor/bar_probe_guard_test.go @@ -0,0 +1,31 @@ +package donor + +import "testing" + +func TestShouldProbeBAR(t *testing.T) { + good := []byte{0x00, 0x01, 0x02, 0x03} + ff := []byte{0xFF, 0xFF, 0xFF, 0xFF} + + cases := []struct { + name string + enabled bool + classCode uint32 + content []byte + want bool + }{ + {"disabled", false, 0x010802, good, false}, + {"network ethernet skipped", true, 0x020000, good, false}, + {"network other-subclass skipped (aquantia 10GbE)", true, 0x028000, good, false}, + {"all-FF skipped", true, 0x010802, ff, false}, + {"nvme probed", true, 0x010802, good, true}, + {"audio probed", true, 0x040300, good, true}, + {"no content still probed (non-network)", true, 0x010802, nil, true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := shouldProbeBAR(c.enabled, c.classCode, c.content); got != c.want { + t.Errorf("shouldProbeBAR(%v, %#x, len=%d) = %v, want %v", c.enabled, c.classCode, len(c.content), got, c.want) + } + }) + } +} diff --git a/internal/donor/bar_profiler.go b/internal/donor/bar_profiler.go index 83a0bc9..eab9ecf 100644 --- a/internal/donor/bar_profiler.go +++ b/internal/donor/bar_profiler.go @@ -12,7 +12,8 @@ type BARProbeResult struct { Offset uint32 `json:"offset"` Original uint32 `json:"original"` RWMask uint32 `json:"rw_mask"` // 1 = writable bit - MaybeRW1C bool `json:"maybe_rw1c"` // write-1-to-clear suspect + W1CMask uint32 `json:"w1c_mask"` // bits that self-cleared on write-of-1 + MaybeRW1C bool `json:"maybe_rw1c"` // true if W1CMask != 0 } // BARProfile is the full probe output for one BAR. @@ -58,7 +59,11 @@ func (p *BARProfiler) ProfileBAR(resourcePath string, barIndex, maxSize int) (*B if err != nil { return nil, fmt.Errorf("mmap R/W failed for BAR%d: %w", barIndex, err) } - defer syscall.Munmap(mapped) + defer func() { + if unmapErr := syscall.Munmap(mapped); unmapErr != nil { + _ = unmapErr + } + }() profile := &BARProfile{ BarIndex: barIndex, @@ -94,6 +99,16 @@ func probeRegisters(mem []byte, size int) []BARProbeResult { return probes } +// classifyRegisterBits derives the RW mask and the W1C-suspect mask from probe +// readbacks. Split out from probeOneRegister so it can be tested without a device. +func classifyRegisterBits(allOnes, allZeros, testVal, afterWrite uint32) (rwMask, w1cMask uint32) { + rwMask = allOnes ^ allZeros + if rwMask != 0 { + w1cMask = testVal & ^afterWrite & rwMask + } + return +} + // probeOneRegister does the write-readback dance on one DWORD. func probeOneRegister(mem []byte, offset uint32) BARProbeResult { off := int(offset) @@ -109,30 +124,25 @@ func probeOneRegister(mem []byte, offset uint32) BARProbeResult { binary.LittleEndian.PutUint32(mem[off:off+4], 0x00000000) allZeros := binary.LittleEndian.Uint32(mem[off : off+4]) - // put it back - binary.LittleEndian.PutUint32(mem[off:off+4], original) - - // RW mask: bits that flipped between all-ones and all-zeros + // W1C probe: write 1s to writable bits and read back, to see which self-clear. rwMask := allOnes ^ allZeros - - // RW1C check: write 1s to writable bits, see if they self-clear - maybeRW1C := false + var testVal, afterWrite uint32 if rwMask != 0 { - testVal := original | rwMask + testVal = original | rwMask binary.LittleEndian.PutUint32(mem[off:off+4], testVal) - afterWrite := binary.LittleEndian.Uint32(mem[off : off+4]) - cleared := testVal & ^afterWrite & rwMask - if cleared != 0 { - maybeRW1C = true - } - // Restore again - binary.LittleEndian.PutUint32(mem[off:off+4], original) + afterWrite = binary.LittleEndian.Uint32(mem[off : off+4]) } + // put it back + binary.LittleEndian.PutUint32(mem[off:off+4], original) + + _, w1cMask := classifyRegisterBits(allOnes, allZeros, testVal, afterWrite) + return BARProbeResult{ Offset: offset, Original: original, RWMask: rwMask, - MaybeRW1C: maybeRW1C, + W1CMask: w1cMask, + MaybeRW1C: w1cMask != 0, } } diff --git a/internal/donor/bar_profiler_classify_test.go b/internal/donor/bar_profiler_classify_test.go new file mode 100644 index 0000000..4b28b3c --- /dev/null +++ b/internal/donor/bar_profiler_classify_test.go @@ -0,0 +1,45 @@ +package donor + +import "testing" + +func TestClassifyRegisterBits_RWOnly(t *testing.T) { + rw, w1c := classifyRegisterBits(0xFF, 0x00, 0xFF, 0xFF) + if rw != 0xFF { + t.Errorf("rwMask: got 0x%X, want 0xFF", rw) + } + if w1c != 0 { + t.Errorf("w1cMask: got 0x%X, want 0", w1c) + } +} + +func TestClassifyRegisterBits_DetectsW1C(t *testing.T) { + rw, w1c := classifyRegisterBits(0x100, 0x00, 0x100, 0x00) + if rw != 0x100 { + t.Errorf("rwMask: got 0x%X, want 0x100", rw) + } + if w1c != 0x100 { + t.Errorf("w1cMask: got 0x%X, want 0x100", w1c) + } +} + +func TestClassifyRegisterBits_MixedRWandW1C(t *testing.T) { + rw, w1c := classifyRegisterBits(0x103, 0x00, 0x103, 0x003) + if rw != 0x103 { + t.Errorf("rwMask: got 0x%X, want 0x103", rw) + } + if w1c != 0x100 { + t.Errorf("w1cMask: got 0x%X, want 0x100", w1c) + } +} + +func TestProfileBARFromBuffer_NoW1COnPlainBuffer(t *testing.T) { + prof := ProfileBARFromBuffer(make([]byte, 16), 0) + for _, p := range prof.Probes { + if p.W1CMask != 0 { + t.Errorf("reg 0x%X: W1CMask 0x%X, want 0", p.Offset, p.W1CMask) + } + if p.MaybeRW1C { + t.Errorf("reg 0x%X: MaybeRW1C true, want false", p.Offset) + } + } +} diff --git a/internal/donor/collector.go b/internal/donor/collector.go index e004ee4..a45cb5f 100644 --- a/internal/donor/collector.go +++ b/internal/donor/collector.go @@ -22,18 +22,24 @@ const ( // Collector gathers donor PCI data from sysfs. type Collector struct { sysfs *SysfsReader + // ProbeBARs enables destructive write-readback BAR profiling. Network-class + // donors are never probed regardless, since writing live NIC registers + // (e.g. Aquantia firmware-mailbox 10GbE chips) hangs the device on BAR. + ProbeBARs bool } func NewCollector() *Collector { return &Collector{ - sysfs: NewSysfsReader(), + sysfs: NewSysfsReader(), + ProbeBARs: true, } } // NewCollectorWithSysfs lets tests inject a fake sysfs reader. func NewCollectorWithSysfs(sr *SysfsReader) *Collector { return &Collector{ - sysfs: sr, + sysfs: sr, + ProbeBARs: true, } } @@ -58,6 +64,11 @@ func (c *Collector) Collect(bdf pci.BDF) (*DeviceContext, error) { } ctx.ConfigSpace = cs + if cs.IsMultiFunction() { + ctx.IsMultiFunction = true + ctx.SiblingFunctions = c.collectSiblingFunctions(bdf) + } + bars, err := c.sysfs.ReadResourceFile(bdf) if err != nil { bars = pci.ParseBARsFromConfigSpace(cs) @@ -65,7 +76,7 @@ func (c *Collector) Collect(bdf pci.BDF) (*DeviceContext, error) { ctx.BARs = bars ctx.BARContents = c.collectBARMemory(bdf, bars) - ctx.BARProfiles = c.collectBARProfiles(bdf, bars, ctx.BARContents) + ctx.BARProfiles = c.collectBARProfiles(bdf, bars, ctx.BARContents, ctx.Device.ClassCode) ctx.Capabilities = pci.ParseCapabilities(cs) ctx.ExtCapabilities = pci.ParseExtCapabilities(cs) ctx.MSIXData = c.collectMSIXData(cs, ctx.BARContents) @@ -330,7 +341,7 @@ func (c *Collector) collectConfigSpace(bdf pci.BDF) (*pci.ConfigSpace, error) { if err != nil { slog.Info("sysfs config read failed, trying VFIO", "error", err) if bindErr := vfio.BindToVFIO(bdf.String()); bindErr != nil { - return nil, fmt.Errorf("config space read failed for %s (sysfs: %v, VFIO: %v)", bdf, err, bindErr) + return nil, fmt.Errorf("config space read failed for %s (sysfs: %w, VFIO: %w)", bdf, err, bindErr) } cs, err = c.sysfs.ReadConfigSpace(bdf) if err != nil { @@ -340,6 +351,33 @@ func (c *Collector) collectConfigSpace(bdf pci.BDF) (*pci.ConfigSpace, error) { return cs, nil } +// collectSiblingFunctions enumerates other PCI functions at the same +// bus:slot as bdf (function 0-7, excluding bdf itself). Real TB3/TB4 +// controllers commonly split bridge/NHI across sibling functions, and this +// captures just their identity - no BAR/capability collection is repeated +// for them. Siblings that don't exist or aren't accessible (permission +// denied, e.g. not bound to vfio-pci) are skipped rather than failing. +func (c *Collector) collectSiblingFunctions(bdf pci.BDF) []SiblingFunction { + var siblings []SiblingFunction + for fn := 0; fn < 8; fn++ { + if uint8(fn) == bdf.Function { + continue + } + sibBDF := pci.BDF{Domain: bdf.Domain, Bus: bdf.Bus, Device: bdf.Device, Function: uint8(fn)} + dev, err := c.sysfs.ReadDeviceInfo(sibBDF) + if err != nil { + continue + } + siblings = append(siblings, SiblingFunction{ + Function: uint8(fn), + VendorID: dev.VendorID, + DeviceID: dev.DeviceID, + ClassCode: dev.ClassCode, + }) + } + return siblings +} + func (c *Collector) collectBARMemory(bdf pci.BDF, bars []pci.BAR) map[int][]byte { eligible := eligibleBARs(bars) if len(eligible) == 0 { @@ -413,14 +451,19 @@ func (c *Collector) readBARs(bdf pci.BDF, eligible []pci.BAR, contents map[int][ } } -func (c *Collector) collectBARProfiles(bdf pci.BDF, bars []pci.BAR, barContents map[int][]byte) map[int]*BARProfile { +func (c *Collector) collectBARProfiles(bdf pci.BDF, bars []pci.BAR, barContents map[int][]byte, classCode uint32) map[int]*BARProfile { profiler := NewBARProfiler() profiles := make(map[int]*BARProfile) + if isNetworkClass(classCode) { + slog.Info("BAR profiling skipped: network-class donor (write-probing hangs NIC firmware, e.g. Aquantia)", "class", fmt.Sprintf("0x%06X", classCode)) + return profiles + } + for _, bar := range eligibleBARs(bars) { - // don't probe unresponsive BARs, writes can brick the device - if data, ok := barContents[bar.Index]; ok && isAllFF(data) { - slog.Info("BAR profiling skipped: content is all 0xFF", "bar", bar.Index) + // don't probe unresponsive BARs or when disabled, writes can brick the device + if !shouldProbeBAR(c.ProbeBARs, classCode, barContents[bar.Index]) { + slog.Info("BAR profiling skipped", "bar", bar.Index, "probe_enabled", c.ProbeBARs) continue } diff --git a/internal/donor/context.go b/internal/donor/context.go index 88c0e5c..8202307 100644 --- a/internal/donor/context.go +++ b/internal/donor/context.go @@ -22,48 +22,68 @@ type MSIXData struct { Entries []pci.MSIXEntry `json:"entries"` } +// SiblingFunction captures the identity of another PCI function found at the +// same bus:slot as the primary donor device (e.g. a TB3/TB4 controller's +// bridge and NHI functions). Metadata only - siblings never get BAR or +// capability collection, just enough to know they exist. +// +// ponytail: not firmware.DeviceIDs - internal/firmware already imports +// internal/donor (see helpers.go), so importing it back here would cycle. +type SiblingFunction struct { + Function uint8 `json:"function"` + VendorID uint16 `json:"vendor_id"` + DeviceID uint16 `json:"device_id"` + ClassCode uint32 `json:"class_code"` +} + // DeviceContext is the full snapshot of a donor device. type DeviceContext struct { CollectedAt time.Time `json:"collected_at"` ToolVersion string `json:"tool_version"` Hostname string `json:"hostname"` - Device pci.PCIDevice `json:"device"` - ConfigSpace *pci.ConfigSpace `json:"config_space"` - BARs []pci.BAR `json:"bars"` - BARContents map[int][]byte `json:"-"` // BAR memory contents, keyed by BAR index - BARProfiles map[int]*BARProfile `json:"-"` // probing results, keyed by BAR index - Capabilities []pci.Capability `json:"capabilities"` - ExtCapabilities []pci.ExtCapability `json:"ext_capabilities,omitempty"` - MSIXData *MSIXData `json:"msix_data,omitempty"` + Device pci.PCIDevice `json:"device"` + ConfigSpace *pci.ConfigSpace `json:"config_space"` + BARs []pci.BAR `json:"bars"` + BARContents map[int][]byte `json:"-"` // BAR memory contents, keyed by BAR index + BARProfiles map[int]*BARProfile `json:"-"` // probing results, keyed by BAR index + Capabilities []pci.Capability `json:"capabilities"` + ExtCapabilities []pci.ExtCapability `json:"ext_capabilities,omitempty"` + MSIXData *MSIXData `json:"msix_data,omitempty"` + IsMultiFunction bool `json:"is_multi_function,omitempty"` + SiblingFunctions []SiblingFunction `json:"sibling_functions,omitempty"` } // JSON wire format - config space as hex words, BARs as base64. type deviceContextJSON struct { - CollectedAt time.Time `json:"collected_at"` - ToolVersion string `json:"tool_version"` - Hostname string `json:"hostname"` - Device pci.PCIDevice `json:"device"` - ConfigSpaceHex []string `json:"config_space_hex"` - ConfigSpaceSize int `json:"config_space_size"` - BARs []pci.BAR `json:"bars"` - BARContents map[string]string `json:"bar_contents,omitempty"` // key: BAR index, value: base64 - BARProfiles map[string]*BARProfile `json:"bar_profiles,omitempty"` - Capabilities []pci.Capability `json:"capabilities"` - ExtCapabilities []pci.ExtCapability `json:"ext_capabilities,omitempty"` - MSIXData *MSIXData `json:"msix_data,omitempty"` + CollectedAt time.Time `json:"collected_at"` + ToolVersion string `json:"tool_version"` + Hostname string `json:"hostname"` + Device pci.PCIDevice `json:"device"` + ConfigSpaceHex []string `json:"config_space_hex"` + ConfigSpaceSize int `json:"config_space_size"` + BARs []pci.BAR `json:"bars"` + BARContents map[string]string `json:"bar_contents,omitempty"` // key: BAR index, value: base64 + BARProfiles map[string]*BARProfile `json:"bar_profiles,omitempty"` + Capabilities []pci.Capability `json:"capabilities"` + ExtCapabilities []pci.ExtCapability `json:"ext_capabilities,omitempty"` + MSIXData *MSIXData `json:"msix_data,omitempty"` + IsMultiFunction bool `json:"is_multi_function,omitempty"` + SiblingFunctions []SiblingFunction `json:"sibling_functions,omitempty"` } func (dc *DeviceContext) MarshalJSON() ([]byte, error) { j := deviceContextJSON{ - CollectedAt: dc.CollectedAt, - ToolVersion: dc.ToolVersion, - Hostname: dc.Hostname, - Device: dc.Device, - BARs: dc.BARs, - Capabilities: dc.Capabilities, - ExtCapabilities: dc.ExtCapabilities, - MSIXData: dc.MSIXData, + CollectedAt: dc.CollectedAt, + ToolVersion: dc.ToolVersion, + Hostname: dc.Hostname, + Device: dc.Device, + BARs: dc.BARs, + Capabilities: dc.Capabilities, + ExtCapabilities: dc.ExtCapabilities, + MSIXData: dc.MSIXData, + IsMultiFunction: dc.IsMultiFunction, + SiblingFunctions: dc.SiblingFunctions, } if dc.ConfigSpace != nil { @@ -111,6 +131,8 @@ func (dc *DeviceContext) UnmarshalJSON(data []byte) error { dc.Capabilities = j.Capabilities dc.ExtCapabilities = j.ExtCapabilities dc.MSIXData = j.MSIXData + dc.IsMultiFunction = j.IsMultiFunction + dc.SiblingFunctions = j.SiblingFunctions // Reconstruct config space from hex words if len(j.ConfigSpaceHex) > 0 { diff --git a/internal/donor/context_w1c_test.go b/internal/donor/context_w1c_test.go new file mode 100644 index 0000000..793ba84 --- /dev/null +++ b/internal/donor/context_w1c_test.go @@ -0,0 +1,46 @@ +package donor + +import ( + "strings" + "testing" + "time" + + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestDeviceContext_W1CMaskRoundTrip(t *testing.T) { + ctx := &DeviceContext{ + CollectedAt: time.Now(), + ConfigSpace: pci.NewConfigSpace(), + BARProfiles: map[int]*BARProfile{ + 0: {BarIndex: 0, Size: 4096, Probes: []BARProbeResult{ + {Offset: 0x10, Original: 0x000000FF, RWMask: 0x000000FF, W1CMask: 0x00000010, MaybeRW1C: true}, + {Offset: 0x14, Original: 0x00000000, RWMask: 0x00000000}, + }}, + }, + } + data, err := ctx.ToJSON() + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(data), `"w1c_mask": 16`) { + t.Errorf("W1CMask not serialized; output:\n%s", string(data)) + } + ctx2, err := FromJSON(data) + if err != nil { + t.Fatalf("unmarshal: %v", err) + } + prof := ctx2.BARProfiles[0] + if prof == nil { + t.Fatal("BARProfiles[0] missing") + } + var w1c uint32 + for _, p := range prof.Probes { + if p.Offset == 0x10 { + w1c = p.W1CMask + } + } + if w1c != 0x00000010 { + t.Errorf("W1CMask round-trip: got 0x%08X, want 0x00000010", w1c) + } +} diff --git a/internal/donor/mmio/import.go b/internal/donor/mmio/import.go new file mode 100644 index 0000000..5f6f004 --- /dev/null +++ b/internal/donor/mmio/import.go @@ -0,0 +1,120 @@ +package mmio + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" + "time" +) + +type TextTraceOptions struct { + BDF string + BARIndex int + BARSize int + BARBase uint64 +} + +func ParseTextTrace(r io.Reader, opts TextTraceOptions) (*TraceResult, error) { + if r == nil { + return nil, fmt.Errorf("trace reader is nil") + } + + trace := &TraceResult{ + BDF: opts.BDF, + BARIndex: opts.BARIndex, + BARSize: opts.BARSize, + } + + var first time.Duration + var last time.Duration + scanner := bufio.NewScanner(r) + for scanner.Scan() { + rec, ok := parseTextTraceLine(scanner.Text(), opts.BARBase) + if !ok { + continue + } + if len(trace.Records) == 0 { + first = rec.Timestamp + } + last = rec.Timestamp + trace.Records = append(trace.Records, rec) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("read trace: %w", err) + } + if len(trace.Records) == 0 { + return nil, fmt.Errorf("trace did not contain parseable MMIO records") + } + if last >= first { + trace.Duration = last - first + } + + return trace, nil +} + +func parseTextTraceLine(line string, barBase uint64) (AccessRecord, bool) { + fields := strings.Fields(strings.TrimSpace(line)) + if len(fields) < 5 { + return AccessRecord{}, false + } + + var rec AccessRecord + switch fields[0] { + case "R": + rec.Type = AccessRead + case "W": + rec.Type = AccessWrite + default: + return AccessRecord{}, false + } + + addrField, valueField, ok := traceAddressValueFields(fields) + if !ok { + return AccessRecord{}, false + } + + addr, err := parseHexUint64(addrField) + if err != nil { + return AccessRecord{}, false + } + value, err := parseHexUint32(valueField) + if err != nil { + return AccessRecord{}, false + } + + rec.Offset = traceOffset(addr, barBase) + rec.Value = value + if ts, err := strconv.ParseFloat(fields[2], 64); err == nil { + rec.Timestamp = time.Duration(ts * float64(time.Second)) + } + + return rec, true +} + +func traceAddressValueFields(fields []string) (string, string, bool) { + if strings.HasPrefix(fields[3], "0x") { + return fields[3], fields[4], true + } + if len(fields) >= 6 && strings.HasPrefix(fields[4], "0x") { + return fields[4], fields[5], true + } + return "", "", false +} + +func parseHexUint64(raw string) (uint64, error) { + return strconv.ParseUint(strings.TrimPrefix(strings.TrimPrefix(raw, "0x"), "0X"), 16, 64) +} + +func parseHexUint32(raw string) (uint32, error) { + value, err := strconv.ParseUint(strings.TrimPrefix(strings.TrimPrefix(raw, "0x"), "0X"), 16, 32) + return uint32(value), err +} + +func traceOffset(addr uint64, barBase uint64) uint32 { + if barBase != 0 && addr >= barBase { + return uint32(addr - barBase) + } + return uint32(addr & 0xFFF) +} diff --git a/internal/donor/mmio/import_test.go b/internal/donor/mmio/import_test.go new file mode 100644 index 0000000..15cc08d --- /dev/null +++ b/internal/donor/mmio/import_test.go @@ -0,0 +1,76 @@ +package mmio + +import ( + "strings" + "testing" + "time" +) + +func TestParseTextTrace_Ret2cShapeWithBARBase(t *testing.T) { + input := strings.NewReader(strings.Join([]string{ + "R 4 2456.105919 2 0xf780010c 0x4c02 0x0 0", + "W 4 2456.130642 2 0xf7800114 0x1 0x0 0", + }, "\n")) + + trace, err := ParseTextTrace(input, TextTraceOptions{ + BDF: "0000:03:00.0", + BARIndex: 2, + BARSize: 4096, + BARBase: 0xf7800000, + }) + + if err != nil { + t.Fatalf("ParseTextTrace returned error: %v", err) + } + if len(trace.Records) != 2 { + t.Fatalf("records = %d, want 2", len(trace.Records)) + } + if trace.Records[0].Type != AccessRead || trace.Records[0].Offset != 0x10c || trace.Records[0].Value != 0x4c02 { + t.Fatalf("first record = %+v", trace.Records[0]) + } + if trace.Records[1].Type != AccessWrite || trace.Records[1].Offset != 0x114 || trace.Records[1].Value != 0x1 { + t.Fatalf("second record = %+v", trace.Records[1]) + } + if trace.Duration <= 0 { + t.Fatal("duration should be derived from timestamps") + } +} + +func TestParseTextTrace_LiveTracePipeShape(t *testing.T) { + input := strings.NewReader("R 4 1234567.890 0xfee00100 0x00000001 extra\n") + + trace, err := ParseTextTrace(input, TextTraceOptions{BARSize: 4096}) + + if err != nil { + t.Fatalf("ParseTextTrace returned error: %v", err) + } + if len(trace.Records) != 1 { + t.Fatalf("records = %d, want 1", len(trace.Records)) + } + if trace.Records[0].Offset != 0x100 || trace.Records[0].Value != 1 { + t.Fatalf("record = %+v", trace.Records[0]) + } +} + +func TestParseTextTrace_RejectsEmptyTrace(t *testing.T) { + _, err := ParseTextTrace(strings.NewReader("not a trace line\n"), TextTraceOptions{}) + if err == nil { + t.Fatal("expected error for empty trace") + } +} + +func TestParseTextTrace_DurationUsesLastTimestamp(t *testing.T) { + input := strings.NewReader(strings.Join([]string{ + "R 4 1.000 0x1000 0x1", + "R 4 1.250 0x1004 0x2", + }, "\n")) + + trace, err := ParseTextTrace(input, TextTraceOptions{}) + + if err != nil { + t.Fatalf("ParseTextTrace returned error: %v", err) + } + if trace.Duration != 250*time.Millisecond { + t.Fatalf("duration = %s, want 250ms", trace.Duration) + } +} diff --git a/internal/donor/mmio/trace_live.go b/internal/donor/mmio/trace_live.go index 0fe97a8..4db6669 100644 --- a/internal/donor/mmio/trace_live.go +++ b/internal/donor/mmio/trace_live.go @@ -8,8 +8,6 @@ import ( "fmt" "log/slog" "os" - "strconv" - "strings" "time" ) @@ -33,22 +31,22 @@ func LiveTrace(bdf string, duration time.Duration) (*TraceResult, error) { return nil, fmt.Errorf("cannot read current tracer: %w", err) } defer func() { - if err := os.WriteFile(currentTracer, prevTracer, 0644); err != nil { - slog.Warn("failed to restore tracer", "error", err) + if writeErr := os.WriteFile(currentTracer, prevTracer, 0644); writeErr != nil { + slog.Warn("failed to restore tracer", "error", writeErr) } }() // switch to mmiotrace - if err := os.WriteFile(currentTracer, []byte("mmiotrace"), 0644); err != nil { - return nil, fmt.Errorf("cannot enable mmiotrace (CONFIG_MMIOTRACE=y needed): %w", err) + if writeErr := os.WriteFile(currentTracer, []byte("mmiotrace"), 0644); writeErr != nil { + return nil, fmt.Errorf("cannot enable mmiotrace (CONFIG_MMIOTRACE=y needed): %w", writeErr) } - if err := os.WriteFile(tracingOnPath, []byte("1"), 0644); err != nil { - slog.Warn("failed to enable tracing", "error", err) + if writeErr := os.WriteFile(tracingOnPath, []byte("1"), 0644); writeErr != nil { + slog.Warn("failed to enable tracing", "error", writeErr) } defer func() { - if err := os.WriteFile(tracingOnPath, []byte("0"), 0644); err != nil { - slog.Warn("failed to disable tracing", "error", err) + if writeErr := os.WriteFile(tracingOnPath, []byte("0"), 0644); writeErr != nil { + slog.Warn("failed to disable tracing", "error", writeErr) } }() @@ -98,44 +96,5 @@ func LiveTrace(bdf string, duration time.Duration) (*TraceResult, error) { // parseMMIOTraceLine parses one mmiotrace line (R/W ). func parseMMIOTraceLine(line string) (AccessRecord, bool) { - line = strings.TrimSpace(line) - // mmiotrace lines typically look like: - // R 4 1234567.890 0xfee00000 0x00000001 ... - // W 4 1234567.890 0xfee00000 0x00000001 ... - fields := strings.Fields(line) - if len(fields) < 5 { - return AccessRecord{}, false - } - - var rec AccessRecord - - switch fields[0] { - case "R": - rec.Type = AccessRead - case "W": - rec.Type = AccessWrite - default: - return AccessRecord{}, false - } - - // lower 12 bits = offset within 4K BAR page - addr, err := strconv.ParseUint(strings.TrimPrefix(fields[3], "0x"), 16, 64) - if err != nil { - return AccessRecord{}, false - } - rec.Offset = uint32(addr & 0xFFF) // BAR offset within 4K page - - // Parse value - val, err := strconv.ParseUint(strings.TrimPrefix(fields[4], "0x"), 16, 32) - if err != nil { - return AccessRecord{}, false - } - rec.Value = uint32(val) - - // Parse timestamp if available - if ts, err := strconv.ParseFloat(fields[2], 64); err == nil { - rec.Timestamp = time.Duration(ts * float64(time.Second)) - } - - return rec, true + return parseTextTraceLine(line, 0) } diff --git a/internal/donor/synthetic/builder.go b/internal/donor/synthetic/builder.go new file mode 100644 index 0000000..1363790 --- /dev/null +++ b/internal/donor/synthetic/builder.go @@ -0,0 +1,106 @@ +// Package synthetic builds donor device contexts for CI fixture generation. +package synthetic + +import ( + "time" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/sercanarga/pcileechgen/internal/pci" + "github.com/sercanarga/pcileechgen/internal/version" +) + +type classProfile struct { + vendorID uint16 + deviceID uint16 + classCode uint32 + headerType uint8 + bar0Size uint64 +} + +var profiles = map[string]classProfile{ + devclass.ClassNVMe: {0x144d, 0xa809, 0x010802, 0x00, 0x4000}, + devclass.ClassXHCI: {0x8086, 0x9d2f, 0x0c0330, 0x00, 0x10000}, + devclass.ClassEthernet: {0x8086, 0x15b7, 0x020000, 0x00, 0x20000}, + devclass.ClassAudio: {0x8086, 0x9d71, 0x040300, 0x00, 0x4000}, + devclass.ClassGPU: {0x10de, 0x1b06, 0x030000, 0x00, 0x4000}, + devclass.ClassSATA: {0x8086, 0x9d03, 0x010601, 0x00, 0x2000}, + devclass.ClassWiFi: {0x8086, 0x24fd, 0x028000, 0x00, 0x2000}, + devclass.ClassThunderbolt: {0x8086, 0x15d9, 0x080700, 0x00, 0x4000}, + devclass.ClassSDHCI: {0x10ec, 0x5229, 0x080501, 0x00, 0x1000}, + devclass.ClassGeneric: {0x1234, 0x5678, 0x000000, 0x00, 0x1000}, +} + +// Build returns a representative DeviceContext for class, or nil if unknown. +func Build(class string) *donor.DeviceContext { + p, ok := profiles[class] + if !ok { + return nil + } + cs := buildConfigSpace(p) + // Capabilities are populated by parsing the config space, mirroring how + // donor.FromJSON / collector populate real donor contexts (the field is a + // denormalized cache; the scrub pipeline re-parses the config space). + return &donor.DeviceContext{ + CollectedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + ToolVersion: version.Version, + Hostname: "ci-synthetic", + Device: pci.PCIDevice{ + BDF: pci.BDF{Bus: 0x03, Device: 0x00, Function: 0x0}, + VendorID: p.vendorID, + DeviceID: p.deviceID, + ClassCode: p.classCode, + HeaderType: p.headerType, + }, + ConfigSpace: cs, + BARs: buildBARs(p.bar0Size), + Capabilities: pci.ParseCapabilities(cs), + } +} + +func buildConfigSpace(p classProfile) *pci.ConfigSpace { + cs := &pci.ConfigSpace{Size: pci.ConfigSpaceLegacySize} + cs.WriteU16(0x00, p.vendorID) + cs.WriteU16(0x02, p.deviceID) + cs.WriteU8(0x08, 0x01) + cs.WriteU8(0x09, byte(p.classCode)) + cs.WriteU8(0x0A, byte(p.classCode>>8)) + cs.WriteU8(0x0B, byte(p.classCode>>16)) + cs.WriteU8(0x0E, p.headerType) + + cs.WriteU16(0x06, 0x0010) + + cs.WriteU8(0x34, 0x40) + + cs.WriteU8(0x40, pci.CapIDPowerManagement) + cs.WriteU8(0x41, 0x48) + cs.WriteU16(0x42, 0x0003) + cs.WriteU16(0x44, 0x0008) + cs.WriteU16(0x46, 0x0000) + + cs.WriteU8(0x48, pci.CapIDMSI) + cs.WriteU8(0x49, 0x54) + cs.WriteU16(0x4A, 0x0000) + cs.WriteU32(0x4C, 0x00000000) + cs.WriteU16(0x50, 0x0000) + cs.WriteU16(0x52, 0x0000) + + cs.WriteU8(0x54, pci.CapIDPCIExpress) + cs.WriteU8(0x55, 0x00) + cs.WriteU16(0x56, 0x0002) + + return cs +} + +func buildBARs(bar0Size uint64) []pci.BAR { + raw := uint32(^(bar0Size - 1)) & 0xFFFFFFF0 + return []pci.BAR{ + { + Index: 0, + RawValue: raw, + Address: 0, + Size: bar0Size, + Type: "mem32", + }, + } +} diff --git a/internal/donor/synthetic/builder_test.go b/internal/donor/synthetic/builder_test.go new file mode 100644 index 0000000..75b61de --- /dev/null +++ b/internal/donor/synthetic/builder_test.go @@ -0,0 +1,72 @@ +package synthetic + +import ( + "encoding/json" + "testing" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestBuild_RoundTrip(t *testing.T) { + for _, class := range devclass.AllClasses() { + t.Run(class, func(t *testing.T) { + ctx := Build(class) + if ctx == nil { + t.Fatalf("Build(%q) returned nil", class) + } + data, err := json.Marshal(ctx) + if err != nil { + t.Fatalf("marshal: %v", err) + } + got, err := donor.FromJSON(data) + if err != nil { + t.Fatalf("FromJSON: %v", err) + } + if got.Device.ClassCode != ctx.Device.ClassCode { + t.Errorf("class code lost in round-trip: got %#x want %#x", + got.Device.ClassCode, ctx.Device.ClassCode) + } + if len(got.BARs) == 0 { + t.Error("no BARs after round-trip; BAR0 needed for BAR model build") + } + if got.Device.VendorID != ctx.Device.VendorID { + t.Errorf("vendor ID lost in round-trip: got %#x want %#x", + got.Device.VendorID, ctx.Device.VendorID) + } + if len(got.BARs) > 0 && got.BARs[0].Size != ctx.BARs[0].Size { + t.Errorf("BAR0 size lost in round-trip: got %d want %d", + got.BARs[0].Size, ctx.BARs[0].Size) + } + }) + } +} + +func TestBuild_UnknownClassReturnsNil(t *testing.T) { + if Build("does-not-exist") != nil { + t.Error("Build with unknown class should return nil") + } +} + +func TestBuild_HasPCIeCapability(t *testing.T) { + for _, class := range devclass.AllClasses() { + t.Run(class, func(t *testing.T) { + ctx := Build(class) + if ctx == nil { + t.Fatalf("Build(%q) returned nil", class) + } + caps := pci.ParseCapabilities(ctx.ConfigSpace) + found := false + for _, c := range caps { + if c.ID == pci.CapIDPCIExpress { + found = true + break + } + } + if !found { + t.Errorf("%s: config space has no PCIe capability after parse", class) + } + }) + } +} diff --git a/internal/donor/sysfs.go b/internal/donor/sysfs.go index 40d5f87..70343ca 100644 --- a/internal/donor/sysfs.go +++ b/internal/donor/sysfs.go @@ -37,7 +37,7 @@ func (sr *SysfsReader) ScanDevices() ([]pci.PCIDevice, error) { return nil, fmt.Errorf("failed to read sysfs: %w", err) } - var devices []pci.PCIDevice + devices := make([]pci.PCIDevice, 0, len(entries)) for _, entry := range entries { // sysfs entries are symlinks, not plain directories name := entry.Name() diff --git a/internal/donor/vfio/diagnostics.go b/internal/donor/vfio/diagnostics.go index 77d29da..9f39096 100644 --- a/internal/donor/vfio/diagnostics.go +++ b/internal/donor/vfio/diagnostics.go @@ -32,7 +32,7 @@ func ListIOMMUGroupDevices(bdf string) ([]string, error) { return nil, fmt.Errorf("cannot list IOMMU group devices: %w", err) } - var devices []string + devices := make([]string, 0, len(entries)) for _, e := range entries { devices = append(devices, e.Name()) } diff --git a/internal/donor/vfio/reader.go b/internal/donor/vfio/reader.go index 4668022..3733ae6 100644 --- a/internal/donor/vfio/reader.go +++ b/internal/donor/vfio/reader.go @@ -77,9 +77,9 @@ func openSession(bdf string) (*vfioSession, error) { } // check API version (VFIO_API_VERSION is 0) - if err := vfioIoctl(s.containerFD, vfioGetAPIVersion, 0); err != nil { + if ioctlErr := vfioIoctl(s.containerFD, vfioGetAPIVersion, 0); ioctlErr != nil { s.close() - return nil, fmt.Errorf("VFIO API check failed: %w", err) + return nil, fmt.Errorf("VFIO API check failed: %w", ioctlErr) } // open group @@ -107,7 +107,7 @@ func openSession(bdf string) (*vfioSession, error) { vfioGroupGetDeviceFD, uintptr(unsafe.Pointer(&bdfBytes[0]))) if errno != 0 || int(fd) < 0 { s.close() - return nil, fmt.Errorf("cannot get VFIO device FD for %s: %v", bdf, errno) + return nil, fmt.Errorf("cannot get VFIO device FD for %s: %w", bdf, errno) } s.deviceFD = int(fd) @@ -118,7 +118,7 @@ func openSession(bdf string) (*vfioSession, error) { func vfioIoctl(fd int, request uintptr, arg uintptr) error { _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), request, arg) if errno != 0 { - return fmt.Errorf("ioctl 0x%x: %v", request, errno) + return fmt.Errorf("ioctl 0x%x: %w", request, errno) } return nil } @@ -177,7 +177,7 @@ func getRegionInfo(deviceFD, index int) (*vfioRegionInfo, error) { _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(deviceFD), vfioDeviceGetRegionInfo, uintptr(unsafe.Pointer(&info))) if errno != 0 { - return nil, fmt.Errorf("VFIO_DEVICE_GET_REGION_INFO index %d: %v", index, errno) + return nil, fmt.Errorf("VFIO_DEVICE_GET_REGION_INFO index %d: %w", index, errno) } return &info, nil } diff --git a/internal/donor/vfio/vfio.go b/internal/donor/vfio/vfio.go index b9dac94..81dea5e 100644 --- a/internal/donor/vfio/vfio.go +++ b/internal/donor/vfio/vfio.go @@ -131,14 +131,14 @@ func BindToVFIO(bdf string) error { if driverLink != "" { unbindPath := filepath.Join(devPath, "driver", "unbind") - if err := os.WriteFile(unbindPath, []byte(bdf), 0200); err != nil { - return fmt.Errorf("failed to unbind from current driver: %w", err) + if writeErr := os.WriteFile(unbindPath, []byte(bdf), 0200); writeErr != nil { + return fmt.Errorf("failed to unbind from current driver: %w", writeErr) } } overridePath := filepath.Join(devPath, "driver_override") - if err := os.WriteFile(overridePath, []byte("vfio-pci"), 0200); err != nil { - return fmt.Errorf("failed to set driver override: %w", err) + if writeErr := os.WriteFile(overridePath, []byte("vfio-pci"), 0200); writeErr != nil { + return fmt.Errorf("failed to set driver override: %w", writeErr) } newIDPath := "/sys/bus/pci/drivers/vfio-pci/new_id" @@ -146,8 +146,8 @@ func BindToVFIO(bdf string) error { _ = os.WriteFile(newIDPath, []byte(idStr), 0200) probePath := "/sys/bus/pci/drivers_probe" - if err := os.WriteFile(probePath, []byte(bdf), 0200); err != nil { - return fmt.Errorf("failed to probe device: %w", err) + if writeErr := os.WriteFile(probePath, []byte(bdf), 0200); writeErr != nil { + return fmt.Errorf("failed to probe device: %w", writeErr) } driverLink, err = os.Readlink(filepath.Join(devPath, "driver")) diff --git a/internal/donor/vfio/vfio_test.go b/internal/donor/vfio/vfio_test.go index f358026..2d1ca23 100644 --- a/internal/donor/vfio/vfio_test.go +++ b/internal/donor/vfio/vfio_test.go @@ -83,7 +83,9 @@ func TestIsBoundToVFIO_WithFakeSysfs(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // No driver symlink -> not bound if IsBoundToVFIO(bdf) { @@ -92,8 +94,12 @@ func TestIsBoundToVFIO_WithFakeSysfs(t *testing.T) { // Create a fake driver symlink pointing to vfio-pci fakeDriver := filepath.Join(tmpDir, "drivers", "vfio-pci") - os.MkdirAll(fakeDriver, 0755) - os.Symlink(fakeDriver, filepath.Join(devDir, "driver")) + if err := os.MkdirAll(fakeDriver, 0755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(fakeDriver, filepath.Join(devDir, "driver")); err != nil { + t.Fatal(err) + } if !IsBoundToVFIO(bdf) { t.Error("should be bound when driver symlink points to vfio-pci") @@ -107,7 +113,9 @@ func TestQuickStatus_WithFakeSysfs(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // No iommu group -> "no-iommu" status := QuickStatus(bdf) @@ -117,8 +125,12 @@ func TestQuickStatus_WithFakeSysfs(t *testing.T) { // Add vfio-pci driver -> "ready" fakeDriver := filepath.Join(tmpDir, "drivers", "vfio-pci") - os.MkdirAll(fakeDriver, 0755) - os.Symlink(fakeDriver, filepath.Join(devDir, "driver")) + if err := os.MkdirAll(fakeDriver, 0755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(fakeDriver, filepath.Join(devDir, "driver")); err != nil { + t.Fatal(err) + } status = QuickStatus(bdf) if status != "ready" { @@ -133,7 +145,9 @@ func TestCheckPowerState_WithFakeSysfs(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // No power_state file -> error _, err := CheckPowerState(bdf) @@ -142,7 +156,9 @@ func TestCheckPowerState_WithFakeSysfs(t *testing.T) { } // Write D0 - os.WriteFile(filepath.Join(devDir, "power_state"), []byte("D0\n"), 0644) + if writeErr := os.WriteFile(filepath.Join(devDir, "power_state"), []byte("D0\n"), 0644); writeErr != nil { + t.Fatal(writeErr) + } state, err := CheckPowerState(bdf) if err != nil { t.Fatalf("CheckPowerState failed: %v", err) @@ -159,7 +175,9 @@ func TestCheckBARAccessibility_WithFakeSysfs(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // No resource files -> empty results := CheckBARAccessibility(bdf) @@ -168,7 +186,9 @@ func TestCheckBARAccessibility_WithFakeSysfs(t *testing.T) { } // Create a non-empty resource0 file - os.WriteFile(filepath.Join(devDir, "resource0"), make([]byte, 4096), 0644) + if err := os.WriteFile(filepath.Join(devDir, "resource0"), make([]byte, 4096), 0644); err != nil { + t.Fatal(err) + } results = CheckBARAccessibility(bdf) if len(results) == 0 { t.Error("expected at least one BAR result") @@ -185,12 +205,16 @@ func TestEnableMemorySpace_WithFakeSysfs(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // Create a fake config file (4096 bytes, Command Register at 0x04 = 0x0000) config := make([]byte, 4096) configPath := filepath.Join(devDir, "config") - os.WriteFile(configPath, config, 0644) + if err := os.WriteFile(configPath, config, 0644); err != nil { + t.Fatal(err) + } // Enable memory space err := EnableMemorySpace(bdf) @@ -220,13 +244,17 @@ func TestEnableMemorySpace_AlreadyEnabled(t *testing.T) { bdf := "0000:03:00.0" devDir := filepath.Join(tmpDir, bdf) - os.MkdirAll(devDir, 0755) + if err := os.MkdirAll(devDir, 0755); err != nil { + t.Fatal(err) + } // Config with command register already set to 0x06 config := make([]byte, 4096) config[0x04] = 0x06 configPath := filepath.Join(devDir, "config") - os.WriteFile(configPath, config, 0644) + if err := os.WriteFile(configPath, config, 0644); err != nil { + t.Fatal(err) + } // Should be a no-op err := EnableMemorySpace(bdf) diff --git a/internal/firmware/ahci/identify.go b/internal/firmware/ahci/identify.go new file mode 100644 index 0000000..15d5880 --- /dev/null +++ b/internal/firmware/ahci/identify.go @@ -0,0 +1,86 @@ +// Package ahci builds the ATA IDENTIFY DEVICE data an AHCI/SATA clone returns +// to storahci. A real block (model, serial, LBA count, feature words) is what +// lets the disk enumerate instead of failing init. +package ahci + +import ( + "fmt" + "strings" +) + +// BuildIdentify returns the 256-word ATA IDENTIFY DEVICE block for a SATA disk +// of the given size in 512-byte sectors. Strings are byte-swapped per the ATA +// word convention. +func BuildIdentify(model, serial, fwRev string, sectors uint64) [256]uint16 { + var w [256]uint16 + + w[0] = 0x0040 // ATA device, non-removable + putATAString(&w, 10, 10, serial) + putATAString(&w, 23, 4, fwRev) + putATAString(&w, 27, 20, model) + w[47] = 0x8000 | 0x10 // max sectors per READ/WRITE MULTIPLE + w[49] = 0x0300 // LBA + DMA supported + w[50] = 0x4000 + w[53] = 0x0006 // words 70:64 and 88 valid + + // 28-bit total sectors (capped); 48-bit in words 100-103. + lba28 := sectors + if lba28 > 0x0FFFFFFF { + lba28 = 0x0FFFFFFF + } + w[60] = uint16(lba28) + w[61] = uint16(lba28 >> 16) + + w[64] = 0x0003 // PIO modes 3,4 + w[80] = 0x01F0 // ATA-4..8 + w[81] = 0x0000 + w[82] = 0x0000 + w[83] = 0x4400 // LBA48 + flush-ext supported + w[84] = 0x4000 + w[85] = 0x0000 + w[86] = 0x4400 // LBA48 + flush-ext enabled + w[87] = 0x4000 + w[88] = 0x203F // UDMA modes 0-5, mode 5 selected + + w[100] = uint16(sectors) + w[101] = uint16(sectors >> 16) + w[102] = uint16(sectors >> 32) + w[103] = uint16(sectors >> 48) + + w[106] = 0x4000 + w[217] = 0x0001 // non-rotating (SSD) + + // Integrity word: signature 0xA5 in low byte, checksum in high byte so the + // two's-complement sum of all 512 bytes is zero. + w[255] = 0x00A5 + var sum uint8 + for i := 0; i < 255; i++ { + sum += uint8(w[i]) + uint8(w[i]>>8) + } + sum += 0xA5 + w[255] |= uint16(uint8(-int8(sum))) << 8 + + return w +} + +// IdentifyHex renders the block as 128 little-endian 32-bit words, one hex +// dword per line, for $readmemh into the AHCI engine's identify ROM. +func IdentifyHex(w [256]uint16) string { + var b strings.Builder + for i := 0; i < 256; i += 2 { + dw := uint32(w[i]) | uint32(w[i+1])<<16 + fmt.Fprintf(&b, "%08x\n", dw) + } + return b.String() +} + +func putATAString(w *[256]uint16, start, words int, s string) { + buf := make([]byte, words*2) + for i := range buf { + buf[i] = ' ' + } + copy(buf, []byte(s)) + for i := 0; i < words; i++ { + w[start+i] = uint16(buf[2*i])<<8 | uint16(buf[2*i+1]) + } +} diff --git a/internal/firmware/ahci/identify_test.go b/internal/firmware/ahci/identify_test.go new file mode 100644 index 0000000..e23f556 --- /dev/null +++ b/internal/firmware/ahci/identify_test.go @@ -0,0 +1,64 @@ +package ahci + +import ( + "strings" + "testing" +) + +func ataString(w [256]uint16, start, words int) string { + var b []byte + for i := 0; i < words; i++ { + b = append(b, byte(w[start+i]>>8), byte(w[start+i])) + } + return strings.TrimRight(string(b), " ") +} + +func TestBuildIdentify(t *testing.T) { + const sectors = uint64(0x1_0000_0000) // 4G sectors -> 48-bit range + w := BuildIdentify("PCILeech SATA SSD", "SN0001", "1.0", sectors) + + if w[0] != 0x0040 { + t.Errorf("word0 = %04x, want 0040", w[0]) + } + if w[49]&0x0300 != 0x0300 { + t.Errorf("word49 LBA+DMA not set: %04x", w[49]) + } + if w[83]&0x0400 == 0 { + t.Errorf("word83 LBA48 not advertised: %04x", w[83]) + } + if got := ataString(w, 27, 20); got != "PCILeech SATA SSD" { + t.Errorf("model = %q", got) + } + if got := ataString(w, 10, 10); got != "SN0001" { + t.Errorf("serial = %q", got) + } + got := uint64(w[100]) | uint64(w[101])<<16 | uint64(w[102])<<32 | uint64(w[103])<<48 + if got != sectors { + t.Errorf("LBA48 sectors = %x, want %x", got, sectors) + } + + // integrity: signature 0xA5 + checksum so the byte sum is 0 mod 256 + if byte(w[255]) != 0xA5 { + t.Errorf("integrity signature = %02x, want a5", byte(w[255])) + } + var sum uint8 + for i := 0; i < 256; i++ { + sum += uint8(w[i]) + uint8(w[i]>>8) + } + if sum != 0 { + t.Errorf("checksum byte-sum = %d, want 0", sum) + } +} + +func TestIdentifyHex(t *testing.T) { + w := BuildIdentify("M", "S", "F", 2048) + hex := IdentifyHex(w) + lines := strings.Split(strings.TrimRight(hex, "\n"), "\n") + if len(lines) != 128 { + t.Fatalf("want 128 dword lines, got %d", len(lines)) + } + // dword0 = word1<<16 | word0 ; word0 = 0x0040 + if !strings.HasSuffix(lines[0], "0040") { + t.Errorf("dword0 = %q, want low word 0040", lines[0]) + } +} diff --git a/internal/firmware/barmodel/model.go b/internal/firmware/barmodel/model.go index 2ff38f0..391ac7a 100644 --- a/internal/firmware/barmodel/model.go +++ b/internal/firmware/barmodel/model.go @@ -16,8 +16,9 @@ type BARRegister struct { Width int // 1, 2, or 4 bytes Reset uint32 // reset/initial value (from donor snapshot or spec default) RWMask uint32 // writable bits (1 = host can write, 0 = read-only) + W1CMask uint32 // write-1-to-clear bits (1 = host writes 1 to clear, 0 = untouched) Name string // human-readable register name - IsRW1C bool // true if this register uses write-1-to-clear semantics + IsRW1C bool // true if any W1C bits are present IsFSMDriven bool // true if driven by a dedicated FSM always block (excluded from generic reset/write) } @@ -45,26 +46,55 @@ func BuildBARModel(barData []byte, classCode uint32, profile *donor.BARProfile) } // fall back to hardcoded spec tables + model := specBARModelForClass(classCode, barData) + if model != nil { + validateModel(model) + } + return model +} + +// specBARModelForClass returns the hardcoded spec model for a class, or nil. +func specBARModelForClass(classCode uint32, barData []byte) *BARModel { baseClass := (classCode >> 16) & 0xFF subClass := (classCode >> 8) & 0xFF progIF := classCode & 0xFF - - var model *BARModel switch { case baseClass == 0x01 && subClass == 0x08 && progIF == 0x02: - model = buildNVMeBARModel(barData) + return buildNVMeBARModel(barData) case baseClass == 0x0C && subClass == 0x03 && progIF == 0x30: - model = buildXHCIBARModel(barData) + return buildXHCIBARModel(barData) case baseClass == 0x02: - model = buildEthernetBARModel(barData) + return buildEthernetBARModel(barData) case baseClass == 0x04 && subClass == 0x03: - model = buildAudioBARModel(barData) + return buildAudioBARModel(barData) } + return nil +} - if model != nil { - validateModel(model) +// specRegAttr carries the W1CMask and IsFSMDriven flags the spec assigns to a +// known offset, so a probe-synthesized model stays consistent with the spec. +type specRegAttr struct { + W1CMask uint32 + IsFSMDriven bool +} + +// specRegisterAttrs returns the spec attributes for every offset the spec model +// marks as W1C or device-driven. +func specRegisterAttrs(classCode uint32) map[uint32]specRegAttr { + spec := specBARModelForClass(classCode, nil) + if spec == nil { + return nil } - return model + var out map[uint32]specRegAttr + for _, r := range spec.Registers { + if r.W1CMask != 0 || r.IsFSMDriven { + if out == nil { + out = make(map[uint32]specRegAttr) + } + out[r.Offset] = specRegAttr{W1CMask: r.W1CMask, IsFSMDriven: r.IsFSMDriven} + } + } + return out } // validateModel checks for misaligned or duplicate offsets. @@ -81,6 +111,22 @@ func validateModel(m *BARModel) { if prev, ok := seen[r.Offset]; ok { panic(fmt.Sprintf("barmodel: %s and %s share offset 0x%X", prev, r.Name, r.Offset)) } + // RW and W1C masks must be disjoint: a bit can't be both plain-writable + // and write-1-to-clear. + if r.W1CMask&r.RWMask != 0 { + panic(fmt.Sprintf("barmodel: register %s at offset 0x%X has overlapping W1C/RW masks (W1CMask=0x%08X & RWMask=0x%08X = 0x%08X) — they must be disjoint", + r.Name, r.Offset, r.W1CMask, r.RWMask, r.W1CMask&r.RWMask)) + } + if r.Width > 0 { + var widthMask uint32 = 0xFFFFFFFF + if bits := r.Width * 8; bits < 32 { + widthMask = (1 << bits) - 1 + } + if r.W1CMask&^widthMask != 0 || r.RWMask&^widthMask != 0 { + panic(fmt.Sprintf("barmodel: register %s at offset 0x%X has mask bits beyond Width %d (W1CMask=0x%08X, RWMask=0x%08X, valid=0x%08X)", + r.Name, r.Offset, r.Width, r.W1CMask, r.RWMask, widthMask)) + } + } seen[r.Offset] = r.Name } } @@ -156,19 +202,44 @@ func buildXHCIBARModel(barData []byte) *BARModel { // operational regs {Offset: 0x20, Width: 4, Name: "USBCMD", RWMask: 0x00002F0E, IsFSMDriven: true}, // USBSTS (mostly RW1C) - {Offset: 0x24, Width: 4, Name: "USBSTS", RWMask: 0x00000000, IsFSMDriven: true}, + {Offset: 0x24, Width: 4, Name: "USBSTS", RWMask: 0x00000000, W1CMask: 0x0000041C, IsFSMDriven: true}, // Page Size (read-only) {Offset: 0x28, Width: 4, Name: "PAGESIZE", RWMask: 0x00000000}, // Device Notification Control {Offset: 0x34, Width: 4, Name: "DNCTRL", RWMask: 0x0000FFFF}, - // Command Ring Control - 64-bit - {Offset: 0x38, Width: 4, Name: "CRCR_LO", RWMask: 0xFFFFFFF7}, + // Command Ring Control - 64-bit. bits[2:0] = RCS/CS/CA (RW), bit3 = CRR + // (RO - not modeled by the ring engine, see xhci_ring_engine.sv.tmpl), + // bits[5:4] reserved, bits[31:6] = 64-byte-aligned pointer (RW). + {Offset: 0x38, Width: 4, Name: "CRCR_LO", RWMask: 0xFFFFFFC7}, {Offset: 0x3C, Width: 4, Name: "CRCR_HI", RWMask: 0xFFFFFFFF}, // Device Context Base Address Array Pointer - 64-bit {Offset: 0x50, Width: 4, Name: "DCBAAP_LO", RWMask: 0xFFFFFFC0}, {Offset: 0x54, Width: 4, Name: "DCBAAP_HI", RWMask: 0xFFFFFFFF}, // Configure (CONFIG) {Offset: 0x58, Width: 4, Name: "CONFIG", RWMask: 0x000000FF}, + + // Primary interrupter (Runtime Register Space, RTSOFF fixed at 0x200 + // per xhci.go's ScrubBAR/PostInitRegisters -- IR0 registers live at + // RTSOFF+0x20). Consumed by xhci_ring_engine.sv.tmpl (Command + // Completion Event delivery) and bar_impl_device.sv.tmpl. + // IMAN: bit0=IP (RW1C, HW-set by the ring engine on event post), + // bit1=IE (plain RW). IsFSMDriven because IP is hardware-settable, + // not just software-clearable, so it needs the same hand-rolled + // write path as USBCMD/USBSTS above. + {Offset: 0x220, Width: 4, Name: "IMAN", RWMask: 0x00000002, W1CMask: 0x00000001, IsFSMDriven: true}, + // IMOD: interrupt moderation - accepted, not enforced (no rate limiting emulated). + {Offset: 0x224, Width: 4, Name: "IMOD", RWMask: 0xFFFFFFFF}, + // ERSTSZ: number of segments in the Event Ring Segment Table. + {Offset: 0x228, Width: 4, Name: "ERSTSZ", RWMask: 0x0000FFFF}, + // ERSTBA - 64-bit, 16-byte aligned pointer to the (single-segment) ERST. + {Offset: 0x230, Width: 4, Name: "ERSTBA_LO", RWMask: 0xFFFFFFF0}, + {Offset: 0x234, Width: 4, Name: "ERSTBA_HI", RWMask: 0xFFFFFFFF}, + // ERDP - 64-bit. bit0 = EHB (Event Handler Busy); left as plain RW + // (ponytail: the ring engine never throttles on EHB -- a handful of + // init-time commands never overrun a driver that hasn't drained + // events yet, so there's no real backpressure to model here). + {Offset: 0x238, Width: 4, Name: "ERDP_LO", RWMask: 0xFFFFFFF7}, + {Offset: 0x23C, Width: 4, Name: "ERDP_HI", RWMask: 0xFFFFFFFF}, } populateResetValues(regs, barData) @@ -302,7 +373,7 @@ func buildAudioBARModel(barData []byte) *BARModel { {Offset: 0x48, Width: 4, Name: "CORBWP_CORBRP", RWMask: 0x0000FFFF, IsFSMDriven: true}, // CORBCTL(8) + CORBSTS(8) + CORBSIZE(8) packed // CORBCTL: bit 1 (CORBRUN), bit 0 (CMEIE) writable; CORBSTS: bit 0 -> DWORD bit 8 (RPWP) RW1C; CORBSIZE: RO - {Offset: 0x4C, Width: 4, Name: "CORBCTL_STS_SIZE", RWMask: 0x00030003, IsRW1C: true}, + {Offset: 0x4C, Width: 4, Name: "CORBCTL_STS_SIZE", RWMask: 0x00030003, W1CMask: 0x00000100, IsRW1C: true}, // RIRB lower base address {Offset: 0x50, Width: 4, Name: "RIRBLBASE", RWMask: 0xFFFFFF80}, // RIRB upper base address @@ -312,9 +383,9 @@ func buildAudioBARModel(barData []byte) *BARModel { // RIRBCTL(8) + RIRBSTS(8) + RIRBSIZE(8) // RIRBCTL: bit 0 (RINTCTL), bit 1 (RIRBDMAEN), bit 2 (OIC) writable // RIRBSTS: bit 8 (RINTFL) RW1C, bit 9 (OIS) RW1C - {Offset: 0x5C, Width: 4, Name: "RIRBCTL_STS_SIZE", RWMask: 0x00000007, IsRW1C: true, IsFSMDriven: true}, + {Offset: 0x5C, Width: 4, Name: "RIRBCTL_STS_SIZE", RWMask: 0x00000007, W1CMask: 0x00000700, IsRW1C: true, IsFSMDriven: true}, // RIRBINTSTS - RIRB interrupt status (RW1C: bit 0 INTFL) - {Offset: 0x60, Width: 4, Name: "RIRBINTSTS", RWMask: 0x00000001, IsRW1C: true, IsFSMDriven: true}, + {Offset: 0x60, Width: 4, Name: "RIRBINTSTS", RWMask: 0x00000000, W1CMask: 0x00000001, IsRW1C: true, IsFSMDriven: true}, // IC (Immediate Command) - driver writes codec command {Offset: 0x64, Width: 4, Name: "IC", RWMask: 0xFFFFFFFF}, // IR (Immediate Response) - driver reads codec response (RO) @@ -386,19 +457,24 @@ func buildAudioBARModel(barData []byte) *BARModel { } } -// SynthesizeBARModel builds a model from probe data. -// Drops dead regs and treats RW1C as RO. +// SynthesizeBARModel builds a model from probe data, reconciling it with the spec. +// Spec-known W1C bits win over the probe and are emitted as real W1C; bits the +// probe flagged as W1C at unknown offsets are clamped to read-only (the probe is +// not trustworthy enough to emit live W1C hardware for them). func SynthesizeBARModel(profile *donor.BARProfile, classCode uint32) *BARModel { if profile == nil || len(profile.Probes) == 0 { return nil } nameHints := classRegisterNames(classCode) + attrs := specRegisterAttrs(classCode) - var regs []BARRegister + regs := make([]BARRegister, 0, len(profile.Probes)) for _, probe := range profile.Probes { - // skip dead regs - if probe.Original == 0 && probe.RWMask == 0 { + attr := attrs[probe.Offset] + // Drop dead regs, but keep ones the spec knows about — a status register + // legitimately reads 0 when idle. + if probe.Original == 0 && probe.RWMask == 0 && attr.W1CMask == 0 && !attr.IsFSMDriven { continue } @@ -408,16 +484,32 @@ func SynthesizeBARModel(profile *donor.BARProfile, classCode uint32) *BARModel { } rwMask := probe.RWMask - if probe.MaybeRW1C { - rwMask = 0 // RW1C -> force RO + var w1cMask uint32 + isRW1C := false + + // Spec W1C bits are authoritative. + if attr.W1CMask != 0 { + w1cMask = attr.W1CMask + isRW1C = true + rwMask &^= attr.W1CMask + } + + // Probe-suspected W1C bits are forced read-only, not emitted as W1C. + if probe.W1CMask != 0 { + rwMask &^= probe.W1CMask + } else if probe.MaybeRW1C { + rwMask = 0 } regs = append(regs, BARRegister{ - Offset: probe.Offset, - Width: 4, - Reset: probe.Original, - RWMask: rwMask, - Name: name, + Offset: probe.Offset, + Width: 4, + Reset: probe.Original, + RWMask: rwMask, + W1CMask: w1cMask, + Name: name, + IsRW1C: isRW1C, + IsFSMDriven: attr.IsFSMDriven, }) } diff --git a/internal/firmware/barmodel/model_test.go b/internal/firmware/barmodel/model_test.go index 115f947..632a2cf 100644 --- a/internal/firmware/barmodel/model_test.go +++ b/internal/firmware/barmodel/model_test.go @@ -674,3 +674,157 @@ func TestIsProbeDataReliable_AllRW(t *testing.T) { t.Error("all-RW probes should be considered unreliable") } } + +func TestAudioModel_W1CMasks(t *testing.T) { + model := buildAudioBARModel(nil) + got := map[uint32]uint32{} + for _, r := range model.Registers { + got[r.Offset] = r.W1CMask + } + if got[0x4C] != 0x00000100 { + t.Errorf("0x4C W1CMask: got 0x%08X, want 0x00000100", got[0x4C]) + } + if got[0x5C] != 0x00000700 { + t.Errorf("0x5C W1CMask: got 0x%08X, want 0x00000700", got[0x5C]) + } + if got[0x60] != 0x00000001 { + t.Errorf("0x60 W1CMask: got 0x%08X, want 0x00000001", got[0x60]) + } +} + +func TestAudioModel_RIRBINTSTS_RWMaskCleared(t *testing.T) { + model := buildAudioBARModel(nil) + for _, r := range model.Registers { + if r.Offset == 0x60 { + if r.RWMask != 0 { + t.Errorf("0x60 RWMask: got 0x%08X, want 0 (bit0 is W1C)", r.RWMask) + } + return + } + } + t.Error("0x60 not found") +} + +func TestAudioModel_W1CMasksDisjointFromRW(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("audio model has overlapping W1C/RW masks: %v", r) + } + }() + model := BuildBARModel(nil, 0x040300, nil) + if model == nil { + t.Fatal("audio model nil") + } + for _, r := range model.Registers { + if r.W1CMask&r.RWMask != 0 { + t.Errorf("register %s @ 0x%X: W1CMask 0x%08X overlaps RWMask 0x%08X", r.Name, r.Offset, r.W1CMask, r.RWMask) + } + } +} + +func TestXHCIModel_USBSTS_W1CMask(t *testing.T) { + model := BuildBARModel(nil, 0x0C0330, nil) + for _, r := range model.Registers { + if r.Name == "USBSTS" { + if r.W1CMask != 0x0000041C { + t.Errorf("USBSTS W1CMask: got 0x%08X, want 0x0000041C", r.W1CMask) + } + if r.RWMask != 0 { + t.Errorf("USBSTS RWMask: got 0x%08X, want 0", r.RWMask) + } + return + } + } + t.Error("USBSTS not found") +} + +func TestSynthesizeBARModel_SpecW1C_EmitsRealW1C(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x4C, Original: 0x00820000, RWMask: 0x00030003}, + }} + model := SynthesizeBARModel(profile, 0x040300) + if model == nil { + t.Fatal("model nil") + } + r := model.Registers[0] + if r.W1CMask != 0x00000100 { + t.Errorf("spec W1C at 0x4C: W1CMask got 0x%08X, want 0x00000100", r.W1CMask) + } + if !r.IsRW1C { + t.Error("spec W1C at 0x4C: IsRW1C should be true") + } + if r.RWMask&0x100 != 0 { + t.Error("spec W1C bit8 must be cleared from RWMask") + } +} + +func TestSynthesizeBARModel_ProbeSuspected_BitwiseRO(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x80, Original: 0x000000FF, RWMask: 0x000000FF, W1CMask: 0x00000010}, + }} + model := SynthesizeBARModel(profile, 0x040300) + if model == nil { + t.Fatal("model nil") + } + r := model.Registers[0] + if r.W1CMask != 0 { + t.Errorf("probe-suspected W1C must not be emitted: got 0x%08X", r.W1CMask) + } + if r.IsRW1C { + t.Error("probe-suspected must not be IsRW1C") + } + if r.RWMask != 0x000000EF { + t.Errorf("probe-suspected bit4 should be cleared from RWMask: got 0x%08X, want 0x000000EF", r.RWMask) + } +} + +func TestSynthesizeBARModel_SpecWinsOverProbe(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x4C, Original: 0x00820100, RWMask: 0x00030103, W1CMask: 0x00000100}, + }} + model := SynthesizeBARModel(profile, 0x040300) + if model == nil { + t.Fatal("model nil") + } + if model.Registers[0].W1CMask != 0x00000100 { + t.Errorf("spec>probe: W1CMask got 0x%08X, want 0x00000100", model.Registers[0].W1CMask) + } +} + +func TestSynthesizeBARModel_PropagatesIsFSMDriven(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x20, Original: 0x00000001, RWMask: 0x00002F0E}, + }} + model := SynthesizeBARModel(profile, 0x0C0330) + if model == nil { + t.Fatal("model nil") + } + for _, r := range model.Registers { + if r.Offset == 0x20 { + if !r.IsFSMDriven { + t.Error("synthesized USBCMD must keep IsFSMDriven") + } + return + } + } + t.Error("USBCMD not found") +} + +func TestSynthesizeBARModel_KeepsSpecW1COnQuiescentRegister(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x24, Original: 0x00000000, RWMask: 0x00000000}, + }} + model := SynthesizeBARModel(profile, 0x0C0330) + if model == nil { + t.Fatal("model nil") + } + for _, r := range model.Registers { + if r.Offset == 0x24 { + if r.W1CMask != 0x0000041C { + t.Errorf("quiescent USBSTS W1CMask: got 0x%08X, want 0x0000041C", r.W1CMask) + } + return + } + } + t.Error("quiescent USBSTS was dropped") +} diff --git a/internal/firmware/barprofile/profile.go b/internal/firmware/barprofile/profile.go new file mode 100644 index 0000000..c3766fc --- /dev/null +++ b/internal/firmware/barprofile/profile.go @@ -0,0 +1,219 @@ +package barprofile + +import ( + "encoding/binary" + "fmt" + "sort" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +const ( + HintProbeModel = "probe_model" + HintSpecModel = "spec_model" + HintShadowOnly = "shadow_only" + HintIOSpaceUnsupported = "io_space_unsupported" + + RegisterDead = "dead" + RegisterStatic = "static" + RegisterWritable = "writable" + RegisterRW1C = "rw1c" + RegisterAllOnes = "all_ones" + RegisterUnknown = "unknown" +) + +type Profile struct { + ClassCode uint32 `json:"class_code"` + DeviceClass string `json:"device_class,omitempty"` + BARs []BARSummary `json:"bars"` +} + +type BARSummary struct { + Index int `json:"index"` + Type string `json:"type"` + Size uint64 `json:"size"` + CapturedBytes int `json:"captured_bytes"` + Prefetchable bool `json:"prefetchable"` + Is64Bit bool `json:"is_64bit"` + EmulationHint string `json:"emulation_hint"` + Density string `json:"density"` + NonZeroBytes int `json:"non_zero_bytes"` + NonFFBytes int `json:"non_ff_bytes"` + DeadRegisters int `json:"dead_registers"` + StaticRegisters int `json:"static_registers"` + WritableRegisters int `json:"writable_registers"` + RW1CRegisters int `json:"rw1c_registers"` + AllOnesRegisters int `json:"all_ones_registers"` + Registers []RegisterSummary `json:"registers,omitempty"` +} + +type RegisterSummary struct { + Offset uint32 `json:"offset"` + Original uint32 `json:"original"` + RWMask uint32 `json:"rw_mask"` + Kind string `json:"kind"` +} + +func Build(ctx *donor.DeviceContext) *Profile { + profile := &Profile{} + if ctx == nil { + return profile + } + + profile.ClassCode = ctx.Device.ClassCode + if strategy := devclass.StrategyForClass(ctx.Device.ClassCode); strategy != nil { + profile.DeviceClass = strategy.DeviceClass() + } + + bars := append([]pci.BAR(nil), ctx.BARs...) + sort.Slice(bars, func(i, j int) bool { return bars[i].Index < bars[j].Index }) + for _, bar := range bars { + profile.BARs = append(profile.BARs, buildBARSummary(ctx, bar)) + } + return profile +} + +func buildBARSummary(ctx *donor.DeviceContext, bar pci.BAR) BARSummary { + data := ctx.BARContents[bar.Index] + probe := ctx.BARProfiles[bar.Index] + summary := BARSummary{ + Index: bar.Index, + Type: bar.Type, + Size: bar.Size, + CapturedBytes: len(data), + Prefetchable: bar.Prefetchable, + Is64Bit: bar.Is64Bit, + Density: densityFor(data), + NonZeroBytes: countNotEqual(data, 0x00), + NonFFBytes: countNotEqual(data, 0xFF), + } + summary.EmulationHint = emulationHint(bar, data, probe) + summary.Registers = registerSummaries(data, probe) + for _, reg := range summary.Registers { + addRegisterKind(&summary, reg.Kind) + } + return summary +} + +func emulationHint(bar pci.BAR, data []byte, probe *donor.BARProfile) string { + if bar.Type == pci.BARTypeIO { + return HintIOSpaceUnsupported + } + if probe != nil && len(probe.Probes) > 0 { + return HintProbeModel + } + if len(data) > 0 { + return HintSpecModel + } + return HintShadowOnly +} + +func registerSummaries(data []byte, probe *donor.BARProfile) []RegisterSummary { + if probe != nil && len(probe.Probes) > 0 { + return summariesFromProbe(probe) + } + return summariesFromData(data) +} + +func summariesFromProbe(profile *donor.BARProfile) []RegisterSummary { + regs := make([]RegisterSummary, 0, len(profile.Probes)) + for _, probe := range profile.Probes { + regs = append(regs, RegisterSummary{ + Offset: probe.Offset, + Original: probe.Original, + RWMask: probe.RWMask, + Kind: classifyRegister(probe.Original, probe.RWMask, probe.MaybeRW1C), + }) + } + sort.Slice(regs, func(i, j int) bool { return regs[i].Offset < regs[j].Offset }) + return regs +} + +func summariesFromData(data []byte) []RegisterSummary { + count := len(data) / 4 + regs := make([]RegisterSummary, 0, count) + for i := 0; i < count; i++ { + offset := uint32(i * 4) + value := binary.LittleEndian.Uint32(data[i*4 : i*4+4]) + regs = append(regs, RegisterSummary{ + Offset: offset, + Original: value, + Kind: classifyRegister(value, 0, false), + }) + } + return regs +} + +func classifyRegister(original, rwMask uint32, maybeRW1C bool) string { + switch { + case maybeRW1C: + return RegisterRW1C + case original == 0 && rwMask == 0: + return RegisterDead + case original == 0xFFFFFFFF && rwMask == 0: + return RegisterAllOnes + case rwMask != 0: + return RegisterWritable + case rwMask == 0: + return RegisterStatic + default: + return RegisterUnknown + } +} + +func addRegisterKind(summary *BARSummary, kind string) { + switch kind { + case RegisterDead: + summary.DeadRegisters++ + case RegisterStatic: + summary.StaticRegisters++ + case RegisterWritable: + summary.WritableRegisters++ + case RegisterRW1C: + summary.RW1CRegisters++ + case RegisterAllOnes: + summary.AllOnesRegisters++ + } +} + +func densityFor(data []byte) string { + if len(data) == 0 { + return "empty" + } + active := 0 + for _, b := range data { + if b != 0 && b != 0xFF { + active++ + } + } + ratio := float64(active) / float64(len(data)) + switch { + case ratio == 0: + return "blank" + case ratio < 0.05: + return "sparse" + case ratio > 0.75: + return "dense" + default: + return "mixed" + } +} + +func countNotEqual(data []byte, value byte) int { + count := 0 + for _, b := range data { + if b != value { + count++ + } + } + return count +} + +func (p *Profile) String() string { + if p == nil { + return "BAR behavior profile: " + } + return fmt.Sprintf("BAR behavior profile: class=0x%06X bars=%d", p.ClassCode, len(p.BARs)) +} diff --git a/internal/firmware/barprofile/profile_test.go b/internal/firmware/barprofile/profile_test.go new file mode 100644 index 0000000..1574ead --- /dev/null +++ b/internal/firmware/barprofile/profile_test.go @@ -0,0 +1,68 @@ +package barprofile + +import ( + "testing" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestBuildProfile_classifiesBarsAndRegisters(t *testing.T) { + ctx := &donor.DeviceContext{ + Device: pci.PCIDevice{ClassCode: 0x010802}, + BARs: []pci.BAR{ + {Index: 0, Size: 16, Type: pci.BARTypeMem64, Is64Bit: true}, + {Index: 2, Size: 8, Type: pci.BARTypeIO}, + }, + BARContents: map[int][]byte{ + 0: { + 0x00, 0x00, 0x00, 0x00, + 0x34, 0x12, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, + }, + 2: {0xAA, 0x00, 0x00, 0x00}, + }, + BARProfiles: map[int]*donor.BARProfile{ + 0: { + BarIndex: 0, + Size: 16, + Probes: []donor.BARProbeResult{ + {Offset: 0x00, Original: 0x00000000, RWMask: 0x00000000}, + {Offset: 0x04, Original: 0x00001234, RWMask: 0x00000000}, + {Offset: 0x08, Original: 0xFFFFFFFF, RWMask: 0x00000000}, + {Offset: 0x0C, Original: 0x00000080, RWMask: 0x00000080, MaybeRW1C: true}, + }, + }, + }, + } + + profile := Build(ctx) + + if profile.ClassCode != 0x010802 { + t.Fatalf("ClassCode = 0x%06X, want 0x010802", profile.ClassCode) + } + if profile.DeviceClass != "nvme" { + t.Fatalf("DeviceClass = %q, want nvme", profile.DeviceClass) + } + if len(profile.BARs) != 2 { + t.Fatalf("BAR count = %d, want 2", len(profile.BARs)) + } + bar0 := profile.BARs[0] + if bar0.EmulationHint != HintProbeModel { + t.Fatalf("BAR0 hint = %q, want %q", bar0.EmulationHint, HintProbeModel) + } + if bar0.StaticRegisters != 1 || bar0.RW1CRegisters != 1 || + bar0.AllOnesRegisters != 1 || bar0.DeadRegisters != 1 { + t.Fatalf("BAR0 summary mismatch: %+v", bar0) + } + if len(bar0.Registers) != 4 { + t.Fatalf("BAR0 registers = %d, want 4", len(bar0.Registers)) + } + if bar0.Registers[3].Kind != RegisterRW1C { + t.Fatalf("BAR0 register 0x0C kind = %q, want %q", bar0.Registers[3].Kind, RegisterRW1C) + } + if profile.BARs[1].EmulationHint != HintIOSpaceUnsupported { + t.Fatalf("BAR2 hint = %q, want %q", profile.BARs[1].EmulationHint, HintIOSpaceUnsupported) + } +} diff --git a/internal/firmware/codegen/codegen.go b/internal/firmware/codegen/codegen.go index 8f18098..2ea9beb 100644 --- a/internal/firmware/codegen/codegen.go +++ b/internal/firmware/codegen/codegen.go @@ -71,10 +71,10 @@ func GenerateWritemaskCOE(cs *pci.ConfigSpace) string { // scrubber already neutralized all writable extended cap fields. // header type 0 registers (DWORD index = byte offset / 4) - masks[0] = 0x00000000 // 0x00: VID:DID (read-only identity) - masks[1] = 0xFFFFFFFF // 0x04: Command:Status (OS needs full control) - masks[2] = 0x00000000 // 0x08: RevisionID:ClassCode (read-only identity) - masks[3] = 0xFF00FFFF // 0x0C: CLS+LT writable, HeaderType RO, BIST writable + masks[0] = 0x00000000 // 0x00: VID:DID (read-only identity) + masks[1] = 0xFFFFFFFF // 0x04: Command:Status (OS needs full control) + masks[2] = 0x00000000 // 0x08: RevisionID:ClassCode (read-only identity) + masks[3] = 0xFF00FFFF // 0x0C: CLS+LT writable, HeaderType RO, BIST writable // BAR registers 0x10-0x24 (DWORD 4-9): size-matching masks for i := 0; i < 6; i++ { @@ -96,7 +96,22 @@ func GenerateWritemaskCOE(cs *pci.ConfigSpace) string { masks[10] = 0x00000000 // 0x28: CardBus CIS (read-only) masks[11] = 0x00000000 // 0x2C: SubsysVID:SubsysDID (read-only identity) - masks[12] = 0x00000000 // 0x30: Expansion ROM (not implemented on FPGA) + + // 0x30: Expansion ROM Base Address. Layout is bit0=Enable, bits[10:1] + // reserved, bits[31:11]=address/size field. Mirror the BAR loop above: + // if the donor's own ROM BAR is enabled with a real address, preserve + // its writable bits (Enable + address bits, using the donor's own + // alignment as the size mask) so a BAR-sizing probe reads back the same + // size-encoding the donor reports. Most donors report 0 here (BIOS + // already claims/hides the option ROM at OS-probe time), in which case + // masks[12] stays 0 exactly as before - that already matches reality. + romVal := cs.ReadU32(0x30) + if romVal&0x01 != 0 && romVal&0xFFFFF800 != 0 { + masks[12] = (romVal & 0xFFFFF800) | 0x00000001 + } else { + masks[12] = 0x00000000 + } + masks[13] = 0x00000000 // 0x34: CapPtr + reserved (read-only) masks[14] = 0x00000000 // 0x38: reserved masks[15] = 0x000000FF // 0x3C: IntLine writable, IntPin/MinGnt/MaxLat RO @@ -168,4 +183,3 @@ func GenerateMSIXTableHex(entries []pci.MSIXEntry) string { return sb.String() } - diff --git a/internal/firmware/codegen/codegen_test.go b/internal/firmware/codegen/codegen_test.go index 57fa23b..ee7dde2 100644 --- a/internal/firmware/codegen/codegen_test.go +++ b/internal/firmware/codegen/codegen_test.go @@ -27,84 +27,6 @@ func TestGenerateConfigSpaceCOE(t *testing.T) { } } -func TestGenerateWritemaskCOE(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU16(0x00, 0x8086) - cs.WriteU32(0x10, 0xFFFFF000) // BAR0: 4KB 32-bit memory BAR (scrubbed; dynamic size from CappedBAR0Size) - - wm := GenerateWritemaskCOE(cs) - - if !strings.Contains(wm, "memory_initialization_radix=16") { - t.Error("writemask COE should contain radix") - } - - // parse data lines into DWORD array - lines := strings.Split(wm, "\n") - var dwords []string - for _, l := range lines { - l = strings.TrimSpace(l) - if l == "" || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "memory") { - continue - } - l = strings.TrimSuffix(l, ",") - l = strings.TrimSuffix(l, ";") - dwords = append(dwords, l) - } - - if len(dwords) != 1024 { - t.Fatalf("writemask should have 1024 DWORDs, got %d", len(dwords)) - } - - // DWORD 0 (VID:DID) must be read-only - if dwords[0] != "00000000" { - t.Errorf("VID:DID mask should be 00000000, got %s", dwords[0]) - } - - // DWORD 2 (Rev:ClassCode) must be read-only - if dwords[2] != "00000000" { - t.Errorf("Rev:ClassCode mask should be 00000000, got %s", dwords[2]) - } - - // DWORD 4 (BAR0) must match scrubbed BAR size mask (0xFFFFF000) - if dwords[4] != "fffff000" { - t.Errorf("BAR0 mask should be fffff000 (4KB size e.g.), got %s", dwords[4]) - } - - // DWORD 5 (BAR1) must be 0 (unused after scrubber clamp) - if dwords[5] != "00000000" { - t.Errorf("BAR1 mask should be 00000000 (unused), got %s", dwords[5]) - } - - // DWORD 11 (SubsysIDs) must be read-only - if dwords[11] != "00000000" { - t.Errorf("SubsysIDs mask should be 00000000, got %s", dwords[11]) - } - - // DWORD 15 (IntLine/IntPin) - only IntLine writable - if dwords[15] != "000000ff" { - t.Errorf("IntLine/IntPin mask should be 000000ff, got %s", dwords[15]) - } - - // capability region (0x40-0xFF) must be fully writable. - // the writemask only controls shadow BRAM; the Xilinx IP core - // processes config writes independently. locking here would create - // a dangerous BRAM/IP-core state mismatch. - for i := 0x40 / 4; i < 0x100/4; i++ { - if dwords[i] != "ffffffff" { - t.Errorf("cap region DWORD[%d] should be ffffffff, got %s", i, dwords[i]) - } - } - - // extended config space (0x100+) should be read-only - for i := 0x100 / 4; i < len(dwords); i++ { - if dwords[i] != "00000000" { - t.Errorf("ext cap DWORD[%d] should be 00000000, got %s", i, dwords[i]) - break - } - } -} - func TestGenerateBarContentCOE_Empty(t *testing.T) { coe := GenerateBarContentCOE(nil, 4096) if !strings.Contains(coe, "Zero-filled") { @@ -183,162 +105,12 @@ func TestGenerateMSIXTableHex_Empty(t *testing.T) { } } -// writemask for a donor with BAR0=0 that was scrubbed to 4KB memory BAR (default). -// this simulates the exact scenario where clampBARsToFPGA creates BAR0 (size from ctx/Capped). -func TestWritemask_ScrubbedBAR0FromZero(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU16(0x00, 0x1102) // Creative Labs - cs.WriteU16(0x02, 0x0012) - cs.WriteU32(0x10, 0xFFFFF000) // BAR0 = 4KB mem (from scrubber) - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - // BAR0 writemask should allow sizing writes - if dwords[4] != "fffff000" { - t.Errorf("BAR0 mask = %s, want fffff000 (4KB size mask e.g.)", dwords[4]) - } -} - -// simulate BAR sizing: host writes 0xFFFFFFFF, reads back, decodes size. -func TestWritemask_BARSizingSimulation(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x10, 0xFFFFF000) // 4KB (default) 32-bit memory BAR - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - // writemask must be 0xFFFFF000 for BAR0 - if dwords[4] != "fffff000" { - t.Fatalf("BAR0 mask = %s, want fffff000", dwords[4]) - } - - // simulate host BAR sizing: - // 1. host writes 0xFFFFFFFF to BAR0 - // 2. BRAM stores: 0xFFFFFFFF & writemask = 0xFFFFF000 - // 3. host reads back 0xFFFFF000 - // 4. host decodes: ~(0xFFFFF000 & ~0xF) + 1 = ~0xFFFFF000 + 1 = 0x1000 = 4KB - hostWrite := uint32(0xFFFFFFFF) - mask := uint32(0xFFFFF000) - bramResult := hostWrite & mask - if bramResult != 0xFFFFF000 { - t.Errorf("BAR sizing result = 0x%08X, want 0xFFFFF000", bramResult) - } - sizeBits := bramResult & ^uint32(0x0F) - barSize := ^sizeBits + 1 - if barSize != 4096 { - t.Errorf("decoded BAR size = %d, want 4096 (for this 4k mask test case)", barSize) - } -} - -// writemask with IO BAR donor that was overwritten to memory BAR by scrubber. -func TestWritemask_IOBAROverwrittenToMemory(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x10, 0xFFFFF000) // scrubber forced memory BAR - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - // must be memory BAR mask, NOT IO mask, NOT zero - if dwords[4] == "00000000" { - t.Error("BAR0 mask should not be zero after scrubber creates memory BAR") - } - if dwords[4] != "fffff000" { - t.Errorf("BAR0 mask = %s, want fffff000", dwords[4]) - } -} - -// identity registers must be read-only in writemask. -func TestWritemask_IdentityRegistersReadOnly(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU16(0x00, 0x1102) // VID - cs.WriteU16(0x02, 0x0012) // DID - cs.WriteU32(0x08, 0x04030000) // ClassCode + RevID - cs.WriteU16(0x2C, 0x1102) // SubsysVID - cs.WriteU16(0x2E, 0x0012) // SubsysDID - cs.WriteU32(0x10, 0xFFFFF000) // BAR0 - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - roRegs := map[int]string{ - 0: "VID:DID", - 2: "RevisionID:ClassCode", - 10: "CardBus CIS", - 11: "SubsysVID:SubsysDID", - 12: "Expansion ROM", - 13: "CapPtr", - 14: "Reserved", - } - for dw, name := range roRegs { - if dwords[dw] != "00000000" { - t.Errorf("%s (DWORD %d) mask = %s, want 00000000 (read-only)", name, dw, dwords[dw]) - } - } -} - -// command/status register must be fully writable. -func TestWritemask_CommandStatusWritable(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x10, 0xFFFFF000) - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - if dwords[1] != "ffffffff" { - t.Errorf("Command:Status mask = %s, want ffffffff", dwords[1]) - } -} - -// prefetchable memory BAR: type bits [3:0] must be read-only. -func TestWritemask_PrefetchableBAR(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x10, 0xFFFFF008) // 4KB (default) prefetchable 32-bit memory - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - // mask should be 0xFFFFF008 & 0xFFFFFFF0 = 0xFFFFF000 - // type bits [3:0] read-only, size bits writable - if dwords[4] != "fffff000" { - t.Errorf("prefetchable BAR mask = %s, want fffff000", dwords[4]) - } -} - -// multiple BARs: each should get its own mask. -func TestWritemask_MultipleBARs(t *testing.T) { - cs := pci.NewConfigSpace() - cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x10, 0xFFFFF000) // BAR0: 4KB memory - cs.WriteU32(0x14, 0x00000000) // BAR1: unused - cs.WriteU32(0x18, 0xFFFF0000) // BAR2: 64KB memory - - wm := GenerateWritemaskCOE(cs) - dwords := parseCOEDwords(t, wm) - - if dwords[4] != "fffff000" { - t.Errorf("BAR0 mask = %s, want fffff000", dwords[4]) - } - if dwords[5] != "00000000" { - t.Errorf("BAR1 mask = %s, want 00000000 (unused)", dwords[5]) - } - if dwords[6] != "ffff0000" { - t.Errorf("BAR2 mask = %s, want ffff0000", dwords[6]) - } -} - // config space COE must contain correct DWORDs for a scrubbed config space. func TestConfigSpaceCOE_ScrubbedDeviceIdentity(t *testing.T) { cs := pci.NewConfigSpace() cs.Size = pci.ConfigSpaceSize - cs.WriteU16(0x00, 0x1102) // VID - cs.WriteU16(0x02, 0x0012) // DID + cs.WriteU16(0x00, 0x1102) // VID + cs.WriteU16(0x02, 0x0012) // DID cs.WriteU32(0x08, 0x04030000) // ClassCode 04:03:00 coe := GenerateConfigSpaceCOE(cs) @@ -366,23 +138,42 @@ func TestConfigSpaceHex_DWORDFormat(t *testing.T) { } } -// parseCOEDwords extracts hex DWORD strings from a COE file. -func parseCOEDwords(t *testing.T, coe string) []string { +// nthCOEWord returns the raw hex word for masks[idx] from a formatCOE-style +// vector (one word per line, no per-line offset comment). +func nthCOEWord(t *testing.T, coe string, idx int) string { t.Helper() lines := strings.Split(coe, "\n") - var dwords []string - for _, l := range lines { - l = strings.TrimSpace(l) - if l == "" || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "memory") { - continue + start := -1 + for i, l := range lines { + if l == "memory_initialization_vector=" { + start = i + 1 + break } - l = strings.TrimSuffix(l, ",") - l = strings.TrimSuffix(l, ";") - dwords = append(dwords, l) } - if len(dwords) != 1024 { - t.Fatalf("expected 1024 DWORDs, got %d", len(dwords)) + if start == -1 || start+idx >= len(lines) { + t.Fatalf("could not find word %d in COE", idx) } - return dwords + return strings.TrimRight(lines[start+idx], ",;") } +// masks[12] (Expansion ROM, 0x30) must mirror the donor's own ROM BAR state +// instead of unconditionally hardcoding read-only. +func TestGenerateWritemaskCOE_ExpansionROM(t *testing.T) { + // most donors report no live ROM BAR - masks[12] stays read-only. + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + coe := GenerateWritemaskCOE(cs) + if got := nthCOEWord(t, coe, 12); got != "00000000" { + t.Errorf("expected masks[12]=00000000 for unused ROM BAR, got %s", got) + } + + // donor with an enabled ROM BAR: writemask should preserve Enable + the + // address/size bits, not hardcode to zero. + cs2 := pci.NewConfigSpace() + cs2.Size = pci.ConfigSpaceSize + cs2.WriteU32(0x30, 0xFFFF0001) // enabled, 64KB-aligned ROM base + coe2 := GenerateWritemaskCOE(cs2) + if got := nthCOEWord(t, coe2, 12); got != "ffff0001" { + t.Errorf("expected masks[12]=ffff0001 for enabled ROM BAR, got %s", got) + } +} diff --git a/internal/firmware/codegen/w1cmask.go b/internal/firmware/codegen/w1cmask.go new file mode 100644 index 0000000..e3cfdd3 --- /dev/null +++ b/internal/firmware/codegen/w1cmask.go @@ -0,0 +1,58 @@ +package codegen + +import "github.com/sercanarga/pcileechgen/internal/pci" + +const ( + statusW1C = 0xF900 + pmcsrW1C = 0x8000 + pcieDevStatusW1C = 0x000F + aerRootStatusW1C = 0x0000007F +) + +func setW1C(words []uint32, byteOff int, bits uint32, width int) { + idx := byteOff / 4 + if idx < 0 || idx >= len(words) { + return + } + if width == 32 { + words[idx] |= bits + return + } + if byteOff%4 == 2 { + words[idx] |= bits << 16 + } else { + words[idx] |= bits & 0xFFFF + } +} + +func W1CMaskWords(cs *pci.ConfigSpace) []uint32 { + words := make([]uint32, shadowCfgSpaceWords) + + setW1C(words, 0x06, statusW1C, 16) + + for _, c := range pci.ParseCapabilities(cs) { + switch c.ID { + case pci.CapIDPowerManagement: + setW1C(words, c.Offset+0x04, pmcsrW1C, 16) + case pci.CapIDPCIExpress: + setW1C(words, c.Offset+0x0A, pcieDevStatusW1C, 16) + } + } + + for _, e := range pci.ParseExtCapabilities(cs) { + if e.ID == pci.ExtCapIDAER { + setW1C(words, e.Offset+0x04, 0xFFFFFFFF, 32) + setW1C(words, e.Offset+0x10, 0xFFFFFFFF, 32) + setW1C(words, e.Offset+0x30, aerRootStatusW1C, 32) + } + } + + return words +} + +func GenerateW1CMaskCOE(cs *pci.ConfigSpace) string { + return formatCOE( + "; PCILeech config-space write-1-to-clear mask (1=bit clears on host write-1)\n", + W1CMaskWords(cs), + ) +} diff --git a/internal/firmware/codegen/w1cmask_test.go b/internal/firmware/codegen/w1cmask_test.go new file mode 100644 index 0000000..1fa8603 --- /dev/null +++ b/internal/firmware/codegen/w1cmask_test.go @@ -0,0 +1,63 @@ +package codegen + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestW1CMask_StatusRegisterAlwaysMarked(t *testing.T) { + cs := pci.NewConfigSpace() + words := W1CMaskWords(cs) + + if len(words) != shadowCfgSpaceWords { + t.Fatalf("expected %d words, got %d", shadowCfgSpaceWords, len(words)) + } + if words[1] != 0xF9000000 { + t.Errorf("status W1C mask: want 0xF9000000 at word 1, got 0x%08X", words[1]) + } + if words[0] != 0 { + t.Errorf("word 0 (id regs) must have no W1C bits, got 0x%08X", words[0]) + } +} + +func TestSetW1C_Placement(t *testing.T) { + cases := []struct { + name string + off int + bits uint32 + width int + idx int + want uint32 + }{ + {"16bit high half (status 0x06)", 0x06, 0xF900, 16, 1, 0xF9000000}, + {"16bit low half (pmcsr cap+0x04)", 0x44, 0x8000, 16, 0x11, 0x00008000}, + {"32bit aligned (AER status)", 0x104, 0xFFFFFFFF, 32, 0x41, 0xFFFFFFFF}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + words := make([]uint32, shadowCfgSpaceWords) + setW1C(words, c.off, c.bits, c.width) + if words[c.idx] != c.want { + t.Errorf("word[%#x] = 0x%08X, want 0x%08X", c.idx, words[c.idx], c.want) + } + }) + } +} + +func TestSetW1C_OutOfRangeNoPanic(t *testing.T) { + words := make([]uint32, shadowCfgSpaceWords) + setW1C(words, 0x4000, 0xFFFFFFFF, 32) +} + +func TestGenerateW1CMaskCOE_Format(t *testing.T) { + cs := pci.NewConfigSpace() + coe := GenerateW1CMaskCOE(cs) + if !strings.Contains(coe, "memory_initialization_radix=16;") { + t.Error("COE missing radix header") + } + if n := strings.Count(coe, "\n"); n < shadowCfgSpaceWords { + t.Errorf("COE has too few lines: %d", n) + } +} diff --git a/internal/firmware/codegen/writemask_test.go b/internal/firmware/codegen/writemask_test.go new file mode 100644 index 0000000..38905cc --- /dev/null +++ b/internal/firmware/codegen/writemask_test.go @@ -0,0 +1,229 @@ +package codegen + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestGenerateWritemaskCOE(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU16(0x00, 0x8086) + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + + if !strings.Contains(wm, "memory_initialization_radix=16") { + t.Error("writemask COE should contain radix") + } + + lines := strings.Split(wm, "\n") + dwords := make([]string, 0, len(lines)) + for _, l := range lines { + l = strings.TrimSpace(l) + if l == "" || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "memory") { + continue + } + l = strings.TrimSuffix(l, ",") + l = strings.TrimSuffix(l, ";") + dwords = append(dwords, l) + } + + if len(dwords) != 1024 { + t.Fatalf("writemask should have 1024 DWORDs, got %d", len(dwords)) + } + + if dwords[0] != "00000000" { + t.Errorf("VID:DID mask should be 00000000, got %s", dwords[0]) + } + + if dwords[2] != "00000000" { + t.Errorf("Rev:ClassCode mask should be 00000000, got %s", dwords[2]) + } + + if dwords[4] != "fffff000" { + t.Errorf("BAR0 mask should be fffff000 (4KB size e.g.), got %s", dwords[4]) + } + + if dwords[5] != "00000000" { + t.Errorf("BAR1 mask should be 00000000 (unused), got %s", dwords[5]) + } + + if dwords[11] != "00000000" { + t.Errorf("SubsysIDs mask should be 00000000, got %s", dwords[11]) + } + + if dwords[15] != "000000ff" { + t.Errorf("IntLine/IntPin mask should be 000000ff, got %s", dwords[15]) + } + + // capability region (0x40-0xFF) must be fully writable. + // the writemask only controls shadow BRAM; the Xilinx IP core + // processes config writes independently. locking here would create + // a dangerous BRAM/IP-core state mismatch. + for i := 0x40 / 4; i < 0x100/4; i++ { + if dwords[i] != "ffffffff" { + t.Errorf("cap region DWORD[%d] should be ffffffff, got %s", i, dwords[i]) + } + } + + for i := 0x100 / 4; i < len(dwords); i++ { + if dwords[i] != "00000000" { + t.Errorf("ext cap DWORD[%d] should be 00000000, got %s", i, dwords[i]) + break + } + } +} + +func TestWritemask_ScrubbedBAR0FromZero(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU16(0x00, 0x1102) + cs.WriteU16(0x02, 0x0012) + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[4] != "fffff000" { + t.Errorf("BAR0 mask = %s, want fffff000 (4KB size mask e.g.)", dwords[4]) + } +} + +func TestWritemask_BARSizingSimulation(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[4] != "fffff000" { + t.Fatalf("BAR0 mask = %s, want fffff000", dwords[4]) + } + + hostWrite := uint32(0xFFFFFFFF) + mask := uint32(0xFFFFF000) + bramResult := hostWrite & mask + if bramResult != 0xFFFFF000 { + t.Errorf("BAR sizing result = 0x%08X, want 0xFFFFF000", bramResult) + } + sizeBits := bramResult & ^uint32(0x0F) + barSize := ^sizeBits + 1 + if barSize != 4096 { + t.Errorf("decoded BAR size = %d, want 4096 (for this 4k mask test case)", barSize) + } +} + +func TestWritemask_IOBAROverwrittenToMemory(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[4] == "00000000" { + t.Error("BAR0 mask should not be zero after scrubber creates memory BAR") + } + if dwords[4] != "fffff000" { + t.Errorf("BAR0 mask = %s, want fffff000", dwords[4]) + } +} + +func TestWritemask_IdentityRegistersReadOnly(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU16(0x00, 0x1102) + cs.WriteU16(0x02, 0x0012) + cs.WriteU32(0x08, 0x04030000) + cs.WriteU16(0x2C, 0x1102) + cs.WriteU16(0x2E, 0x0012) + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + roRegs := map[int]string{ + 0: "VID:DID", + 2: "RevisionID:ClassCode", + 10: "CardBus CIS", + 11: "SubsysVID:SubsysDID", + 12: "Expansion ROM", + 13: "CapPtr", + 14: "Reserved", + } + for dw, name := range roRegs { + if dwords[dw] != "00000000" { + t.Errorf("%s (DWORD %d) mask = %s, want 00000000 (read-only)", name, dw, dwords[dw]) + } + } +} + +func TestWritemask_CommandStatusWritable(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[1] != "ffffffff" { + t.Errorf("Command:Status mask = %s, want ffffffff", dwords[1]) + } +} + +func TestWritemask_PrefetchableBAR(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF008) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[4] != "fffff000" { + t.Errorf("prefetchable BAR mask = %s, want fffff000", dwords[4]) + } +} + +func TestWritemask_MultipleBARs(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU32(0x10, 0xFFFFF000) + cs.WriteU32(0x14, 0x00000000) + cs.WriteU32(0x18, 0xFFFF0000) + + wm := GenerateWritemaskCOE(cs) + dwords := parseCOEDwords(t, wm) + + if dwords[4] != "fffff000" { + t.Errorf("BAR0 mask = %s, want fffff000", dwords[4]) + } + if dwords[5] != "00000000" { + t.Errorf("BAR1 mask = %s, want 00000000 (unused)", dwords[5]) + } + if dwords[6] != "ffff0000" { + t.Errorf("BAR2 mask = %s, want ffff0000", dwords[6]) + } +} + +func parseCOEDwords(t *testing.T, coe string) []string { + t.Helper() + lines := strings.Split(coe, "\n") + dwords := make([]string, 0, len(lines)) + for _, l := range lines { + l = strings.TrimSpace(l) + if l == "" || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "memory") { + continue + } + l = strings.TrimSuffix(l, ",") + l = strings.TrimSuffix(l, ";") + dwords = append(dwords, l) + } + if len(dwords) != 1024 { + t.Fatalf("expected 1024 DWORDs, got %d", len(dwords)) + } + return dwords +} diff --git a/internal/firmware/devclass/audio.go b/internal/firmware/devclass/audio.go index de81793..855221c 100644 --- a/internal/firmware/devclass/audio.go +++ b/internal/firmware/devclass/audio.go @@ -73,14 +73,14 @@ func audioProfile() *DeviceProfile { {Offset: 0x40, Width: 4, Name: "CORBLBASE", Reset: 0x00000000, RWMask: 0xFFFFFF80}, {Offset: 0x44, Width: 4, Name: "CORBUBASE", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, {Offset: 0x48, Width: 4, Name: "CORBWP_CORBRP", Reset: 0x00000000, RWMask: 0x0000FFFF, IsFSMDriven: true}, - {Offset: 0x4C, Width: 4, Name: "CORBCTL_STS_SIZE", Reset: 0x00820000, RWMask: 0x00030003, IsRW1C: true}, + {Offset: 0x4C, Width: 4, Name: "CORBCTL_STS_SIZE", Reset: 0x00820000, RWMask: 0x00030003, W1CMask: 0x00000100, IsRW1C: true}, // RIRB base addresses and control {Offset: 0x50, Width: 4, Name: "RIRBLBASE", Reset: 0x00000000, RWMask: 0xFFFFFF80}, {Offset: 0x54, Width: 4, Name: "RIRBUBASE", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, {Offset: 0x58, Width: 4, Name: "RIRBWP_RINTCNT", Reset: 0x00000000, RWMask: 0x0000FFFF, IsFSMDriven: true}, - {Offset: 0x5C, Width: 4, Name: "RIRBCTL_STS_SIZE", Reset: 0x00820000, RWMask: 0x00000307, IsRW1C: true, IsFSMDriven: true}, + {Offset: 0x5C, Width: 4, Name: "RIRBCTL_STS_SIZE", Reset: 0x00820000, RWMask: 0x00000007, W1CMask: 0x00000700, IsRW1C: true, IsFSMDriven: true}, // RIRBINTSTS - RIRB interrupt status (RW1C: bit 0 INTFL) - {Offset: 0x60, Width: 4, Name: "RIRBINTSTS", Reset: 0x00000000, RWMask: 0x00000001, IsRW1C: true, IsFSMDriven: true}, + {Offset: 0x60, Width: 4, Name: "RIRBINTSTS", Reset: 0x00000000, RWMask: 0x00000000, W1CMask: 0x00000001, IsRW1C: true, IsFSMDriven: true}, // IC (Immediate Command) - driver writes codec command {Offset: 0x64, Width: 4, Name: "IC", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, // IR (Immediate Response) - driver reads codec response (RO, returns 0) diff --git a/internal/firmware/devclass/ethernet.go b/internal/firmware/devclass/ethernet.go index 2c303bf..1912ecb 100644 --- a/internal/firmware/devclass/ethernet.go +++ b/internal/firmware/devclass/ethernet.go @@ -101,3 +101,48 @@ func ethernetProfile() *DeviceProfile { "at 2500Mbps full-duplex. TxConfig carries chip version ID.", } } + +// --- Generic (non-Realtek) fallback --- +// +// ponytail: e1000e/tg3/Aquantia/Marvell etc each have their own distinct, +// incompatible MMIO register layouts. Faking any of them correctly needs +// per-vendor modeling, out of scope for this pass. Honest passthrough +// (no writes) beats garbage RTL8168/8125 offsets into unrelated chips. + +type ethernetStrategyGenericImpl struct{ baseStrategy } + +func ethernetStrategyGeneric() DeviceStrategy { + return ðernetStrategyGenericImpl{baseStrategy{"Ethernet (Generic)", ClassEthernet, ethernetGenericProfile}} +} + +func (s *ethernetStrategyGenericImpl) ScrubBAR(data []byte) {} + +func (s *ethernetStrategyGenericImpl) PostInitRegisters(regs map[uint32]*uint32) {} + +func ethernetGenericProfile() *DeviceProfile { + return &DeviceProfile{ + ClassName: "Ethernet (Generic)", + PreferredBAR: 0, + MinBARSize: 4096, + Uses64BitBAR: true, + BARIsPrefetchable: false, + + PrefersMSIX: true, + MinMSIXVectors: 1, + + ExpectedCaps: []uint8{ + pci.CapIDPowerManagement, + pci.CapIDMSIX, + pci.CapIDPCIExpress, + }, + + SupportsPME: true, + MaxPowerState: 3, + + Notes: "Vendor-agnostic Ethernet passthrough. e1000e/tg3/Aquantia/Marvell and " + + "other non-Realtek NICs each use distinct, incompatible MMIO register " + + "layouts that would need per-vendor modeling to fake correctly, out of " + + "scope for this pass, so no BAR writes are made and no register defaults " + + "are assumed.", + } +} diff --git a/internal/firmware/devclass/gpu.go b/internal/firmware/devclass/gpu.go index 94bcaa7..1981bf1 100644 --- a/internal/firmware/devclass/gpu.go +++ b/internal/firmware/devclass/gpu.go @@ -60,7 +60,135 @@ func gpuProfile() *DeviceProfile { {Offset: 0x9410, Width: 4, Name: "PTIMER_TIME_1", Reset: 0x00000000, RWMask: 0x00000000}, }, - Notes: "NVIDIA/AMD GPU profile. Only 4K BRAM window visible - " + + Notes: "NVIDIA GPU profile (NV_PMC/NV_PBUS/NV_PTIMER). Only 4K BRAM window visible - " + "driver sees PMC_ENABLE and PTIMER but can't touch VRAM.", } } + +// --- AMD GPU --- + +type amdGPUStrategy struct{ baseStrategy } + +// ponytail: no verified AMD MMIO register map (RCC_DEV0_EPF0_STRAP0 family / +// ASIC revision regs) grounded from a real donor - safe no-op passthrough +// until a real AMD donor sample is available to reverse the layout. +func (s *amdGPUStrategy) ScrubBAR(data []byte) {} + +func (s *amdGPUStrategy) PostInitRegisters(regs map[uint32]*uint32) {} + +func amdGPUProfile() *DeviceProfile { + return &DeviceProfile{ + ClassName: "GPU (AMD)", + PreferredBAR: 0, + MinBARSize: 4096, + Uses64BitBAR: true, + BARIsPrefetchable: true, + + PrefersMSIX: true, + MinMSIXVectors: 1, + + ExpectedCaps: []uint8{ + pci.CapIDPowerManagement, + pci.CapIDMSIX, + pci.CapIDPCIExpress, + }, + ExpectedExtCaps: []uint16{ + pci.ExtCapIDAER, + pci.ExtCapIDLTR, + }, + + SupportsPME: true, + MaxPowerState: 3, + + // ponytail: no BARDefaults - AMDGPU register offsets not grounded, + // injecting fabricated values would be worse than leaving the BAR blank. + Notes: "AMD GPU profile. No verified amdgpu MMIO register map - " + + "safe passthrough (no BAR register scrub/defaults) until a real donor sample is available.", + } +} + +// --- Intel GPU --- + +type intelGPUStrategy struct{ baseStrategy } + +// ponytail: Intel iGPU register semantics (GTTMMADR, FORCEWAKE/FORCEWAKE_ACK +// protocol) are documented but nontrivial and not grounded from a real donor +// here - safe no-op passthrough until a real Intel donor sample is available. +func (s *intelGPUStrategy) ScrubBAR(data []byte) {} + +func (s *intelGPUStrategy) PostInitRegisters(regs map[uint32]*uint32) {} + +func intelGPUProfile() *DeviceProfile { + return &DeviceProfile{ + ClassName: "GPU (Intel)", + PreferredBAR: 0, + MinBARSize: 4096, + Uses64BitBAR: true, + BARIsPrefetchable: true, + + PrefersMSIX: true, + MinMSIXVectors: 1, + + ExpectedCaps: []uint8{ + pci.CapIDPowerManagement, + pci.CapIDMSIX, + pci.CapIDPCIExpress, + }, + ExpectedExtCaps: []uint16{ + pci.ExtCapIDAER, + pci.ExtCapIDLTR, + }, + + SupportsPME: true, + MaxPowerState: 3, + + // ponytail: no BARDefaults - GTTMMADR/forcewake register offsets not + // grounded, injecting fabricated values would be worse than leaving the BAR blank. + Notes: "Intel GPU profile. No verified i915 MMIO register map - " + + "safe passthrough (no BAR register scrub/defaults) until a real donor sample is available.", + } +} + +// --- Generic/unknown vendor GPU --- + +type genericGPUStrategy struct{ baseStrategy } + +// ponytail: unrecognized GPU vendor - never guess a register map, always +// safe no-op passthrough. +func (s *genericGPUStrategy) ScrubBAR(data []byte) {} + +func (s *genericGPUStrategy) PostInitRegisters(regs map[uint32]*uint32) {} + +func genericGPUProfile() *DeviceProfile { + return &DeviceProfile{ + ClassName: "GPU (Generic)", + PreferredBAR: 0, + MinBARSize: 4096, + Uses64BitBAR: true, + BARIsPrefetchable: true, + + PrefersMSIX: true, + MinMSIXVectors: 1, + + SupportsPME: true, + MaxPowerState: 3, + + Notes: "Unrecognized GPU vendor. No register map to ground against - " + + "safe passthrough (no BAR register scrub/defaults).", + } +} + +// gpuStrategyForAMD returns the AMD GPU strategy. +func gpuStrategyForAMD() DeviceStrategy { + return &amdGPUStrategy{baseStrategy{"GPU (AMD)", ClassGPU, amdGPUProfile}} +} + +// gpuStrategyForIntel returns the Intel GPU strategy. +func gpuStrategyForIntel() DeviceStrategy { + return &intelGPUStrategy{baseStrategy{"GPU (Intel)", ClassGPU, intelGPUProfile}} +} + +// gpuStrategyGeneric returns the neutral fallback strategy for unrecognized GPU vendors. +func gpuStrategyGeneric() DeviceStrategy { + return &genericGPUStrategy{baseStrategy{"GPU (Generic)", ClassGPU, genericGPUProfile}} +} diff --git a/internal/firmware/devclass/profile.go b/internal/firmware/devclass/profile.go index 3b477f3..d645587 100644 --- a/internal/firmware/devclass/profile.go +++ b/internal/firmware/devclass/profile.go @@ -30,6 +30,7 @@ type BARDefault struct { Name string Reset uint32 RWMask uint32 + W1CMask uint32 // write-1-to-clear bits IsRW1C bool IsFSMDriven bool // driven by dedicated FSM, excluded from generic reset/write } diff --git a/internal/firmware/devclass/sdhci.go b/internal/firmware/devclass/sdhci.go new file mode 100644 index 0000000..bdaf577 --- /dev/null +++ b/internal/firmware/devclass/sdhci.go @@ -0,0 +1,115 @@ +package devclass + +import ( + "github.com/sercanarga/pcileechgen/internal/pci" + "github.com/sercanarga/pcileechgen/internal/util" +) + +type sdhciStrategy struct{ baseStrategy } + +// presentStateNoCard is Present State (0x24) with no card in the slot: CINS/CSS/CDPL +// clear (bits 16-18), command/data lines idle (bits 0-2), DAT[3:0]/CMD signal levels +// pulled high (bus idle-high, bits 20-24), WP reads "not protected" (bit 19) since +// there's no card to protect. A host driver enumerates fine with no card present - +// card presence is a separate polled/interrupt event, not an enumeration blocker. +const presentStateNoCard = 0x01F80000 + +func (s *sdhciStrategy) ScrubBAR(data []byte) { + if len(data) < 0x30 { + return + } + util.WriteLE32(data, 0x24, presentStateNoCard) + + // Clock Control: force Internal Clock Stable (bit1) so a driver polling for clock + // lock never spins, and clear the Software Reset bits (24-26) so a donor dump + // captured mid-reset doesn't replay as "reset still in progress". + cc := util.ReadLE32(data, 0x2C) + cc |= 0x00000002 + cc &^= 0x07000000 + util.WriteLE32(data, 0x2C, cc) + + if len(data) >= 0x34 { + // Normal/Error Interrupt Status - no pending interrupts at boot. + util.WriteLE32(data, 0x30, 0x00000000) + } +} + +func (s *sdhciStrategy) PostInitRegisters(regs map[uint32]*uint32) { + if v, ok := regs[0x24]; ok { + *v = presentStateNoCard + } + if v, ok := regs[0x2C]; ok { + *v |= 0x00000002 + *v &^= 0x07000000 + } +} + +func sdhciProfile() *DeviceProfile { + return &DeviceProfile{ + ClassName: "SD Host Controller", + PreferredBAR: 0, // sdhci-pci (Linux) ioremaps BAR0 for the standard register set + MinBARSize: 4096, + Uses64BitBAR: false, + BARIsPrefetchable: false, + + PrefersMSIX: false, + MinMSIXVectors: 0, + + ExpectedCaps: []uint8{ + pci.CapIDPowerManagement, + pci.CapIDMSI, + pci.CapIDPCIExpress, + }, + ExpectedExtCaps: []uint16{ + pci.ExtCapIDAER, + }, + + SupportsPME: true, + MaxPowerState: 3, + + BARDefaults: []BARDefault{ + // SDMA System Address / Argument2 (for Auto CMD23) + {Offset: 0x00, Width: 4, Name: "SDMASYSADDR_ARG2", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, + // Block Size [15:0] (xfer size[11:0] + SDMA boundary[14:12]) + Block Count [31:16] + {Offset: 0x04, Width: 4, Name: "BLOCKSIZE_BLOCKCOUNT", Reset: 0x00000000, RWMask: 0xFFFF7FFF}, + // Argument1 - command argument + {Offset: 0x08, Width: 4, Name: "ARGUMENT1", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, + // Transfer Mode [15:0] (bits 0-5 used) + Command [31:16] (bits 0-13 used) + {Offset: 0x0C, Width: 4, Name: "XFERMODE_COMMAND", Reset: 0x00000000, RWMask: 0x3FFF003F}, + // Response 0-3 - card response, read-only (no card, no command ever completes) + {Offset: 0x10, Width: 4, Name: "RESPONSE0", Reset: 0x00000000, RWMask: 0x00000000}, + {Offset: 0x14, Width: 4, Name: "RESPONSE1", Reset: 0x00000000, RWMask: 0x00000000}, + {Offset: 0x18, Width: 4, Name: "RESPONSE2", Reset: 0x00000000, RWMask: 0x00000000}, + {Offset: 0x1C, Width: 4, Name: "RESPONSE3", Reset: 0x00000000, RWMask: 0x00000000}, + // Buffer Data Port - PIO data window + {Offset: 0x20, Width: 4, Name: "BUFFERDATAPORT", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, + // Present State - no card inserted, bus idle, lines pulled high + {Offset: 0x24, Width: 4, Name: "PRESENTSTATE", Reset: presentStateNoCard, RWMask: 0x00000000}, + // Host Control 1 [7:0] + Power Control [15:8] + Block Gap Control [23:16] + Wakeup Control [31:24] + {Offset: 0x28, Width: 4, Name: "HOSTCTRL1_PWRCTRL_BGAP_WAKECTRL", Reset: 0x00000000, RWMask: 0x070F0FFF}, + // Clock Control [15:0] (ICS bit1 is RO, forced via Scrub/PostInit) + Timeout Control [23:16] + Software Reset [31:24] + {Offset: 0x2C, Width: 4, Name: "CLOCKCTRL_TIMEOUTCTRL_SWRESET", Reset: 0x000E0000, RWMask: 0x070FFFCD}, + // Normal Interrupt Status [15:0] (bits 0-7 W1C, bit8 Card Interrupt is RO-live) + Error Interrupt Status [31:16] (bits 0-9 W1C) + {Offset: 0x30, Width: 4, Name: "NORMALINTSTS_ERRORINTSTS", Reset: 0x00000000, RWMask: 0x00000000, W1CMask: 0x03FF00FF}, + // Normal Interrupt Status Enable [15:0] (bits 0-8) + Error Interrupt Status Enable [31:16] (bits 0-9) + {Offset: 0x34, Width: 4, Name: "NORMALINTEN_ERRORINTEN", Reset: 0x00000000, RWMask: 0x03FF01FF}, + // Normal Interrupt Signal Enable [15:0] + Error Interrupt Signal Enable [31:16] - masked off, + // matches this codebase's "interrupts off for FPGA" convention (see sata.go GHC.IE) + {Offset: 0x38, Width: 4, Name: "NORMALSIGEN_ERRORSIGEN", Reset: 0x00000000, RWMask: 0x03FF01FF}, + // Capabilities - SDHCI 2.00 feature set: SDMA yes, no ADMA/high-speed/UHS, 3.3V only + // ponytail: base/timeout clock frequencies (50MHz/50KHz) are plausible round numbers picked + // for internal consistency, not lifted from a real silicon datasheet + {Offset: 0x40, Width: 4, Name: "CAPABILITIES", Reset: 0x01403232, RWMask: 0x00000000}, + // Slot Interrupt Status [15:0] (no pending) + Host Controller Version [31:16]. + // Version field = 0x01 -> SD Host Spec Version 2.00 (register encodes 0x00=1.00, 0x01=2.00, + // 0x02=3.00 per spec Table 2-16), consistent with the no-UHS/no-ADMA Capabilities above. + {Offset: 0xFC, Width: 4, Name: "SLOTINTSTS_HCVERSION", Reset: 0x00010000, RWMask: 0x00000000}, + }, + + Notes: "SDHCI (SD Host Controller Simplified Spec) 2.00 profile - vendor-agnostic per-spec register " + + "set, the same layout Linux's sdhci.c targets regardless of silicon vendor. No card inserted: " + + "Present State CINS/CSS/CDPL clear, host still enumerates normally (card detect is a separate " + + "polled/interrupt path). SDMA supported, no ADMA2/high-speed/UHS, 3.3V only. Interrupt Signal " + + "Enable masked off (interrupts off for FPGA).", + } +} diff --git a/internal/firmware/devclass/strategy.go b/internal/firmware/devclass/strategy.go index 27a393d..bd1910f 100644 --- a/internal/firmware/devclass/strategy.go +++ b/internal/firmware/devclass/strategy.go @@ -9,9 +9,18 @@ const ( ClassSATA = "sata" ClassWiFi = "wifi" ClassThunderbolt = "thunderbolt" + ClassSDHCI = "sdhci" ClassGeneric = "generic" ) +// AllClasses returns all supported device class strings. +func AllClasses() []string { + return []string{ + ClassNVMe, ClassXHCI, ClassEthernet, ClassAudio, ClassGPU, + ClassSATA, ClassWiFi, ClassThunderbolt, ClassSDHCI, ClassGeneric, + } +} + // DeviceStrategy centralizes device-class-specific behavior. type DeviceStrategy interface { ClassName() string @@ -42,39 +51,83 @@ func StrategyForClass(classCode uint32) DeviceStrategy { func StrategyForClassAndVendor(classCode uint32, vendorID uint16) DeviceStrategy { baseClass := (classCode >> 16) & 0xFF subClass := (classCode >> 8) & 0xFF + progIF := classCode & 0xFF switch { case baseClass == 0x01 && subClass == 0x08: return &nvmeStrategy{baseStrategy{"NVMe", ClassNVMe, nvmeProfile}} - case baseClass == 0x0C && subClass == 0x03: + case baseClass == 0x0C && subClass == 0x03 && progIF == 0x30: return &xhciStrategy{baseStrategy{"xHCI", ClassXHCI, xhciProfile}} case baseClass == 0x02 && subClass == 0x00: - return ðernetStrategy{baseStrategy{"Ethernet", ClassEthernet, ethernetProfile}} + return ethernetStrategyForVendor(vendorID) case baseClass == 0x04 && subClass == 0x03: return &audioStrategy{baseStrategy{"HD Audio", ClassAudio, audioProfile}} - case baseClass == 0x03 && subClass == 0x00: - return &gpuStrategy{baseStrategy{"GPU", ClassGPU, gpuProfile}} + case baseClass == 0x03 && subClass == 0x00, + baseClass == 0x03 && subClass == 0x02 && progIF == 0x00: + return gpuStrategyForVendor(vendorID) case baseClass == 0x01 && subClass == 0x06: return &sataStrategy{baseStrategy{"SATA AHCI", ClassSATA, sataProfile}} case baseClass == 0x02 && subClass == 0x80: return wifiStrategyForVendor(vendorID) case baseClass == 0x0C && subClass == 0x80: return &thunderboltStrategy{baseStrategy{"Thunderbolt", ClassThunderbolt, thunderboltProfile}} + case baseClass == 0x08 && subClass == 0x05: + return &sdhciStrategy{baseStrategy{"SD Host Controller", ClassSDHCI, sdhciProfile}} default: return &genericStrategy{baseStrategy{"Generic", ClassGeneric, genericProfile}} } } // wifiStrategyForVendor selects between mediatek and intel/broadcom wifi profiles. +// WWAN/cellular modem vendors under class 0x0280 don't share the iwlwifi-shaped +// register layout the wifi profiles assume, so they get routed to the safe +// generic passthrough instead of wrong register writes. func wifiStrategyForVendor(vendorID uint16) DeviceStrategy { + switch vendorID { + case 0x1199, 0x17CB, 0x2CB7, 0x2C7C: // Sierra Wireless, Qualcomm, Fibocom, Quectel + return &genericStrategy{baseStrategy{"Generic", ClassGeneric, genericProfile}} + } if vendorID == mediatekVID { return &mediatekWifiStrategy{baseStrategy{"Wi-Fi (MediaTek)", ClassWiFi, mediatekWifiProfile}} } return &wifiStrategy{baseStrategy{"Wi-Fi", ClassWiFi, wifiProfile}} } +// gpuStrategyForVendor selects a vendor-specific GPU strategy. NVIDIA is the +// only one with real register evidence today; AMD/Intel/generic bodies live +// in gpu.go. +func gpuStrategyForVendor(vendorID uint16) DeviceStrategy { + switch vendorID { + case 0, 0x10DE: // unknown (no vendor context) or NVIDIA + return &gpuStrategy{baseStrategy{"GPU", ClassGPU, gpuProfile}} + case 0x1002: // AMD + return gpuStrategyForAMD() + case 0x8086: // Intel + return gpuStrategyForIntel() + default: + return gpuStrategyGeneric() + } +} + +// ethernetStrategyForVendor selects a vendor-specific ethernet strategy. +// Realtek is the one with real register evidence today; the generic body +// lives in ethernet.go. +func ethernetStrategyForVendor(vendorID uint16) DeviceStrategy { + if vendorID == 0 || vendorID == 0x10EC { // unknown (no vendor context) or Realtek + return ðernetStrategy{baseStrategy{"Ethernet", ClassEthernet, ethernetProfile}} + } + return ethernetStrategyGeneric() +} + // --- Generic fallback --- +// genericStrategy is the deliberate default for PCI base classes with no +// PCI-SIG-standardized common register layout (e.g. 0x09 input controllers, +// 0x0A docking stations, 0x10 encryption accelerators, 0x11 signal +// processors). Donor silicon in these classes is vendor-proprietary and +// heterogeneous, unlike NVMe/xHCI/AHCI/SDHCI, so raw donor-snapshot +// passthrough is the correct, honest default rather than fabricating +// unverifiable fake registers. This is a scope boundary, not an oversight. type genericStrategy struct{ baseStrategy } func (s *genericStrategy) ScrubBAR(data []byte) {} diff --git a/internal/firmware/devclass/strategy_test.go b/internal/firmware/devclass/strategy_test.go index 6f21ecb..be1e097 100644 --- a/internal/firmware/devclass/strategy_test.go +++ b/internal/firmware/devclass/strategy_test.go @@ -662,6 +662,7 @@ func TestAllStrategies_Profile(t *testing.T) { p := s.Profile() if p == nil { t.Errorf("nil profile for %s", s.ClassName()) + continue } if p.ClassName == "" { t.Errorf("empty profile className for %s", s.ClassName()) diff --git a/internal/firmware/devclass/thunderbolt.go b/internal/firmware/devclass/thunderbolt.go index ed29f97..2defbe7 100644 --- a/internal/firmware/devclass/thunderbolt.go +++ b/internal/firmware/devclass/thunderbolt.go @@ -60,6 +60,9 @@ func thunderboltProfile() *DeviceProfile { }, Notes: "Intel Thunderbolt controller profile. " + - "LC_STS.READY=1. SECURITY_LEVEL=0 (no security) for full DMA access.", + "LC_STS.READY=1. SECURITY_LEVEL=0 (no security) for full DMA access. " + + "Models a single PCIe-tunneling function only - does not emulate the NHI " + + "(native host interface) DMA path or a full multi-function TB controller " + + "relationship (bridge + NHI as sibling functions).", } } diff --git a/internal/firmware/devclass/w1c_masks_test.go b/internal/firmware/devclass/w1c_masks_test.go new file mode 100644 index 0000000..db66649 --- /dev/null +++ b/internal/firmware/devclass/w1c_masks_test.go @@ -0,0 +1,45 @@ +package devclass + +import "testing" + +func TestAudioProfile_W1CMasks(t *testing.T) { + p := audioProfile() + got := map[uint32]uint32{} + for _, d := range p.BARDefaults { + got[d.Offset] = d.W1CMask + } + if got[0x4C] != 0x00000100 { + t.Errorf("0x4C W1CMask: got 0x%08X, want 0x00000100", got[0x4C]) + } + if got[0x5C] != 0x00000700 { + t.Errorf("0x5C W1CMask: got 0x%08X, want 0x00000700", got[0x5C]) + } + if got[0x60] != 0x00000001 { + t.Errorf("0x60 W1CMask: got 0x%08X, want 0x00000001", got[0x60]) + } +} + +func TestAudioProfile_MasksDisjoint(t *testing.T) { + p := audioProfile() + for _, d := range p.BARDefaults { + if d.W1CMask&d.RWMask != 0 { + t.Errorf("0x%X %s: W1CMask 0x%08X overlaps RWMask 0x%08X", d.Offset, d.Name, d.W1CMask, d.RWMask) + } + } +} + +func TestXHCIProfile_USBSTS_W1CMask(t *testing.T) { + p := xhciProfile() + for _, d := range p.BARDefaults { + if d.Name == "USBSTS" { + if d.W1CMask != 0x0000041C { + t.Errorf("USBSTS W1CMask: got 0x%08X, want 0x0000041C", d.W1CMask) + } + if d.RWMask != 0 { + t.Errorf("USBSTS RWMask: got 0x%08X, want 0", d.RWMask) + } + return + } + } + t.Error("USBSTS not found") +} diff --git a/internal/firmware/devclass/xhci.go b/internal/firmware/devclass/xhci.go index 376726b..c485543 100644 --- a/internal/firmware/devclass/xhci.go +++ b/internal/firmware/devclass/xhci.go @@ -22,14 +22,12 @@ func (s *xhciStrategy) ScrubBAR(data []byte) { } if len(data) > 0x1C { - dboff := util.ReadLE32(data, 0x14) - if dboff > 0x800 { - util.WriteLE32(data, 0x14, 0x00000100) - } - rtsoff := util.ReadLE32(data, 0x18) - if rtsoff > 0xC00 { - util.WriteLE32(data, 0x18, 0x00000200) - } + // DBOFF/RTSOFF are internal wiring constants, not donor-identity + // fields: bar_controller.sv.tmpl and barmodel's buildXHCIBARModel + // hardcode the doorbell/runtime layout at 0x100/0x200, so any donor + // value must be forced to match regardless of what the donor reports. + util.WriteLE32(data, 0x14, 0x00000100) + util.WriteLE32(data, 0x18, 0x00000200) } } @@ -84,20 +82,33 @@ func xhciProfile() *DeviceProfile { // Operational registers (at CAPLENGTH offset 0x20) // USBCMD - R/S=1 (running) {Offset: 0x20, Width: 4, Name: "USBCMD", Reset: 0x00000001, RWMask: 0x00001F0F}, - // USBSTS - HCH=0 (not halted) - {Offset: 0x24, Width: 4, Name: "USBSTS", Reset: 0x00000000, RWMask: 0x0000041C}, + // USBSTS - HCH=0 (not halted); status bits 2,3,4,10 are write-1-to-clear + {Offset: 0x24, Width: 4, Name: "USBSTS", Reset: 0x00000000, RWMask: 0x00000000, W1CMask: 0x0000041C}, // PAGESIZE - 4KB pages {Offset: 0x28, Width: 4, Name: "PAGESIZE", Reset: 0x00000001, RWMask: 0x00000000}, // DNCTRL - device notification control {Offset: 0x34, Width: 4, Name: "DNCTRL", Reset: 0x00000000, RWMask: 0x0000FFFF}, - // CRCR - command ring control - {Offset: 0x38, Width: 4, Name: "CRCR_LO", Reset: 0x00000000, RWMask: 0xFFFFFFF0}, + // CRCR - command ring control. bits[2:0]=RCS/CS/CA (RW), bit3=CRR + // (RO), bits[5:4] reserved, bits[31:6]=pointer (RW). Reconciled + // with barmodel/model.go's buildXHCIBARModel, which is what + // actually drives the generated SV (this profile's RWMask is + // only a donor-scrub/name-hint value, see barmodel.classRegisterNames). + {Offset: 0x38, Width: 4, Name: "CRCR_LO", Reset: 0x00000000, RWMask: 0xFFFFFFC7}, {Offset: 0x3C, Width: 4, Name: "CRCR_HI", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, // DCBAAP - device context base address {Offset: 0x50, Width: 4, Name: "DCBAAP_LO", Reset: 0x00000000, RWMask: 0xFFFFFFC0}, {Offset: 0x54, Width: 4, Name: "DCBAAP_HI", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, // CONFIG - max device slots enabled {Offset: 0x58, Width: 4, Name: "CONFIG", Reset: 0x00000000, RWMask: 0x000000FF}, + // Primary interrupter (Runtime Register Space, RTSOFF+0x20 -- see + // ScrubBAR/PostInitRegisters fixing RTSOFF at 0x200 above). + {Offset: 0x220, Width: 4, Name: "IMAN", Reset: 0x00000000, RWMask: 0x00000002, W1CMask: 0x00000001}, + {Offset: 0x224, Width: 4, Name: "IMOD", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, + {Offset: 0x228, Width: 4, Name: "ERSTSZ", Reset: 0x00000000, RWMask: 0x0000FFFF}, + {Offset: 0x230, Width: 4, Name: "ERSTBA_LO", Reset: 0x00000000, RWMask: 0xFFFFFFF0}, + {Offset: 0x234, Width: 4, Name: "ERSTBA_HI", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, + {Offset: 0x238, Width: 4, Name: "ERDP_LO", Reset: 0x00000000, RWMask: 0xFFFFFFF7}, + {Offset: 0x23C, Width: 4, Name: "ERDP_HI", Reset: 0x00000000, RWMask: 0xFFFFFFFF}, // PORTSC1 - port 1 status/control (powered, no device) {Offset: 0x420, Width: 4, Name: "PORTSC1", Reset: 0x000002A0, RWMask: 0x8EFFC3F2}, // PORTSC2 - port 2 status/control diff --git a/internal/firmware/fallback/fallback.go b/internal/firmware/fallback/fallback.go index b4d5a15..8952272 100644 --- a/internal/firmware/fallback/fallback.go +++ b/internal/firmware/fallback/fallback.go @@ -6,6 +6,8 @@ import ( "log/slog" "os" + "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" "gopkg.in/yaml.v3" ) @@ -72,13 +74,18 @@ func Apply(cfg *Config, classCode uint32, barContents map[int][]byte) []ApplyRes var results []ApplyResult slog.Info("applying fallback defaults", "description", dc.Description, "class", key) - // Find the target BAR: try each available BAR, preferring larger ones + // Prefer the device class's preferred BAR (e.g. BAR5/ABAR for SATA); fall + // back to the largest available BAR when the preferred one has no data. var targetBAR []byte var targetIdx int - for idx, data := range barContents { - if len(data) >= 4 && len(data) > len(targetBAR) { - targetBAR = data - targetIdx = idx + if pref := devclass.ProfileForClass(classCode).PreferredBAR; len(barContents[pref]) >= 4 { + targetBAR = barContents[pref] + targetIdx = pref + } else { + targetIdx = firmware.LargestBarIndex(barContents) + targetBAR = barContents[targetIdx] + if len(targetBAR) < 4 { + targetBAR = nil } } diff --git a/internal/firmware/fallback/fallback_test.go b/internal/firmware/fallback/fallback_test.go index ecaa6f0..cab42f0 100644 --- a/internal/firmware/fallback/fallback_test.go +++ b/internal/firmware/fallback/fallback_test.go @@ -110,6 +110,37 @@ func TestApply_SkipsNonZero(t *testing.T) { } } +func TestApply_PrefersPreferredBAROverLargest(t *testing.T) { + cfg := &Config{ + DeviceClasses: map[string]*DeviceClass{ + // SATA/AHCI: PreferredBAR is 5 (ABAR). + "0106": { + Description: "SATA AHCI", + BAR0Defaults: map[string]uint32{ + "0x00000000": 0x00000001, + }, + }, + }, + } + + barContents := map[int][]byte{ + 0: make([]byte, 8192), // larger than BAR5, but not the ABAR + 5: make([]byte, 4096), // smaller, but the class's preferred (ABAR) BAR + } + + // class 01:06:01 -> SATA AHCI + results := Apply(cfg, 0x010601, barContents) + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + if results[0].Field != "BAR5[0x0000]" { + t.Errorf("expected default stamped into BAR5 (preferred/ABAR), got field %s", results[0].Field) + } + if barContents[0][0] != 0 { + t.Errorf("BAR0 should be untouched, got %#v", barContents[0][:4]) + } +} + func TestApply_UnknownClass(t *testing.T) { cfg := &Config{ DeviceClasses: map[string]*DeviceClass{}, diff --git a/internal/firmware/ila.go b/internal/firmware/ila.go new file mode 100644 index 0000000..d44ac98 --- /dev/null +++ b/internal/firmware/ila.go @@ -0,0 +1,93 @@ +package firmware + +import ( + "fmt" + "strings" +) + +type ILAProbe struct { + Signal string + Width int +} + +const DefaultILADepth = 1024 + +func ILAProbes() []ILAProbe { + return []ILAProbe{ + {"wr_addr", 32}, + {"rd_req_addr", 32}, + {"bar0_raw_data", 32}, + {"bar0_raw_valid", 1}, + {"rd_req_valid", 1}, + {"intr_req", 1}, + {"in_is_wr_ready", 1}, + } +} + +func ILACreateIPTCL(depth int) string { + if depth <= 0 { + depth = DefaultILADepth + } + probes := ILAProbes() + var b strings.Builder + b.WriteString("create_ip -name ila -vendor xilinx.com -library ip -module_name ila_0\n") + b.WriteString("set_property -dict [list \\\n") + fmt.Fprintf(&b, " CONFIG.C_NUM_OF_PROBES {%d} \\\n", len(probes)) + fmt.Fprintf(&b, " CONFIG.C_DATA_DEPTH {%d} \\\n", depth) + b.WriteString(" CONFIG.C_INPUT_PIPE_STAGES {1} \\\n") + b.WriteString(" CONFIG.C_ADV_TRIGGER {true} \\\n") + b.WriteString(" CONFIG.C_EN_STRG_QUAL {1} \\\n") + b.WriteString(" CONFIG.C_TRIGIN_EN {false} \\\n") + b.WriteString(" CONFIG.C_TRIGOUT_EN {false} \\\n") + b.WriteString(" CONFIG.ALL_PROBE_SAME_MU {true} \\\n") + b.WriteString(" CONFIG.ALL_PROBE_SAME_MU_CNT {2} \\\n") + for i, p := range probes { + fmt.Fprintf(&b, " CONFIG.C_PROBE%d_WIDTH {%d} \\\n", i, p.Width) + } + b.WriteString("] [get_ips ila_0]\n") + b.WriteString("generate_target all [get_ips ila_0]\n") + return b.String() +} + +func ILADebugDoc() string { + probes := ILAProbes() + var b strings.Builder + b.WriteString("ILA debug core - capture workflow\n") + b.WriteString("=================================\n\n") + b.WriteString("The ILA is wired into pcileech_tlps128_bar_controller and works over JTAG\n") + b.WriteString("only (not the data/USB-C port). Connect the board's JTAG to a 2nd PC running\n") + b.WriteString("Vivado Hardware Manager; the target PC (with the card inserted) is separate.\n\n") + b.WriteString("Probes:\n") + for i, p := range probes { + fmt.Fprintf(&b, " probe%d %-15s (%d bit)\n", i, p.Signal, p.Width) + } + b.WriteString("\nArming a pre-trigger capture (example: NVMe CQ0 init):\n") + b.WriteString(" 1. open_hw_manager, connect to the board over JTAG, refresh hw_device.\n") + b.WriteString(" 2. In the ILA dashboard set a trigger on a probe - e.g. wr_addr == the CQ0\n") + b.WriteString(" doorbell offset, or intr_req == 1 for the first completion interrupt.\n") + b.WriteString(" 3. Set the trigger position early in the window (a pre-trigger position) so\n") + b.WriteString(" the buffer captures what happened BEFORE the trigger as well as after.\n") + b.WriteString(" 4. Enable capture control (storage qualification) to only store samples of\n") + b.WriteString(" interest - this stretches the small on-card buffer much further.\n") + b.WriteString(" 5. Arm the core, then do a warm reboot of the target PC. When the trigger\n") + b.WriteString(" arrives during init, the ILA captures the window; export it to a .ila/CSV.\n\n") + b.WriteString("Buffers are small, so expect several capture runs, each narrowed to one signal\n") + b.WriteString("or window, to reconstruct the full init sequence around a Code 10.\n") + return b.String() +} + +func ILAInstanceSV() string { + probes := ILAProbes() + var b strings.Builder + b.WriteString(" ila_0 u_ila_dbg (\n") + b.WriteString(" .clk(clk),\n") + for i, p := range probes { + comma := "," + if i == len(probes)-1 { + comma = "" + } + fmt.Fprintf(&b, " .probe%d(%s)%s\n", i, p.Signal, comma) + } + b.WriteString(" );\n") + return b.String() +} diff --git a/internal/firmware/ila_test.go b/internal/firmware/ila_test.go new file mode 100644 index 0000000..2664e6e --- /dev/null +++ b/internal/firmware/ila_test.go @@ -0,0 +1,71 @@ +package firmware + +import ( + "fmt" + "strings" + "testing" +) + +func TestILAProbes_Valid(t *testing.T) { + probes := ILAProbes() + if len(probes) == 0 { + t.Fatal("expected at least one probe") + } + for _, p := range probes { + if p.Signal == "" || p.Width <= 0 { + t.Errorf("invalid probe: %+v", p) + } + } +} + +func TestILACreateIPTCL(t *testing.T) { + tcl := ILACreateIPTCL(0) + n := len(ILAProbes()) + for _, want := range []string{ + "create_ip -name ila -vendor xilinx.com", + "-module_name ila_0", + fmt.Sprintf("CONFIG.C_NUM_OF_PROBES {%d}", n), + "CONFIG.C_DATA_DEPTH {1024}", + "CONFIG.C_PROBE0_WIDTH {32}", + "CONFIG.C_ADV_TRIGGER {true}", + "CONFIG.C_EN_STRG_QUAL {1}", + "CONFIG.ALL_PROBE_SAME_MU_CNT {2}", + "generate_target all [get_ips ila_0]", + } { + if !strings.Contains(tcl, want) { + t.Errorf("create_ip TCL missing %q:\n%s", want, tcl) + } + } + if !strings.Contains(ILACreateIPTCL(4096), "CONFIG.C_DATA_DEPTH {4096}") { + t.Error("custom depth not honored") + } +} + +func TestILAInstanceSV(t *testing.T) { + sv := ILAInstanceSV() + probes := ILAProbes() + if !strings.Contains(sv, "ila_0 u_ila_dbg") || !strings.Contains(sv, ".clk(clk)") { + t.Errorf("instance missing module/clk:\n%s", sv) + } + if !strings.Contains(sv, fmt.Sprintf(".probe0(%s)", probes[0].Signal)) { + t.Errorf("first probe not wired:\n%s", sv) + } + last := fmt.Sprintf(".probe%d(%s)\n );", len(probes)-1, probes[len(probes)-1].Signal) + if !strings.Contains(sv, last) { + t.Errorf("last probe / closing malformed:\n%s", sv) + } +} + +func TestILADebugDoc(t *testing.T) { + doc := ILADebugDoc() + for _, want := range []string{"JTAG", "trigger", "pre-trigger", "warm reboot", "probe0"} { + if !strings.Contains(doc, want) { + t.Errorf("ILA debug doc missing %q", want) + } + } + for _, p := range ILAProbes() { + if !strings.Contains(doc, p.Signal) { + t.Errorf("ILA debug doc should list probe signal %q", p.Signal) + } + } +} diff --git a/internal/firmware/nvme/identify.go b/internal/firmware/nvme/identify.go index 0416416..c7768a3 100644 --- a/internal/firmware/nvme/identify.go +++ b/internal/firmware/nvme/identify.go @@ -20,7 +20,7 @@ type IdentifyData struct { func BuildIdentifyData(ids firmware.DeviceIDs, barData []byte) *IdentifyData { d := &IdentifyData{} d.Controller = buildIdentifyController(ids, barData) - d.Namespace = buildIdentifyNamespace(barData) + d.Namespace = buildIdentifyNamespace(ids, barData) return d } @@ -37,7 +37,7 @@ func buildIdentifyController(ids firmware.DeviceIDs, barData []byte) [4096]byte copy(data[0x004:0x018], padASCII(sn, 20)) // MN (40B ASCII) - mn := modelNumberForVendor(ids.VendorID) + mn := modelNumberForDevice(ids.VendorID, ids.DeviceID) copy(data[0x018:0x040], padASCII(mn, 40)) // FR (8B ASCII) - vendor-appropriate firmware revision @@ -131,11 +131,10 @@ func buildIdentifyController(ids firmware.DeviceIDs, barData []byte) [4096]byte } // buildIdentifyNamespace - CNS=0 NSID=1, NVMe 1.4 spec Figure 245. -func buildIdentifyNamespace(barData []byte) [4096]byte { +func buildIdentifyNamespace(ids firmware.DeviceIDs, barData []byte) [4096]byte { var data [4096]byte - // 1TB / 512B sectors - var nsze uint64 = 1953525168 + nsze := nszeForDevice(ids.VendorID, ids.DeviceID) _ = barData // reserved for future donor extraction binary.LittleEndian.PutUint64(data[0x000:], nsze) // NSZE @@ -195,10 +194,23 @@ func vendorSNPrefix(vid uint16) string { } } -func modelNumberForVendor(vid uint16) string { +func modelNumberForDevice(vid, did uint16) string { switch vid { case 0x144D: - return "Samsung SSD 980 PRO 1TB" + switch did { + case 0xA808: + return "Samsung SSD 980 PRO 1TB" + case 0xA809: + return "Samsung SSD 980 PRO 2TB" + case 0xA80C: + return "Samsung SSD 980 PRO 512GB" + case 0xA80D: + return "Samsung SSD 990 PRO 1TB" + case 0xA80E: + return "Samsung SSD 990 PRO 2TB" + default: + return "Samsung SSD NVMe 1TB" + } case 0x1179: return "TOSHIBA KXG60ZNV1T02" case 0x1987: @@ -218,6 +230,20 @@ func modelNumberForVendor(vid uint16) string { } } +func nszeForDevice(vid, did uint16) uint64 { + switch vid { + case 0x144D: + switch did { + case 0xA809, 0xA80E: + return 3907029168 + default: + return 1953525168 + } + default: + return 1953525168 + } +} + func ouiForVendor(vid uint16) [3]byte { switch vid { case 0x144D: @@ -229,7 +255,7 @@ func ouiForVendor(vid uint16) [3]byte { case 0x1C5C: return [3]byte{0x00, 0xAD, 0x00} default: - return [3]byte{0x00, 0x00, 0x00} + return [3]byte{0x00, uint8(vid >> 8), uint8(vid)} } } diff --git a/internal/firmware/nvme/identify_test.go b/internal/firmware/nvme/identify_test.go index a267922..d9744de 100644 --- a/internal/firmware/nvme/identify_test.go +++ b/internal/firmware/nvme/identify_test.go @@ -2,6 +2,7 @@ package nvme import ( "encoding/binary" + "strings" "testing" "github.com/sercanarga/pcileechgen/internal/firmware" @@ -240,3 +241,46 @@ func TestIdentifyController_DifferentVendors(t *testing.T) { } } } + +func TestModelNumber_DID_Disambiguation(t *testing.T) { + ids := sampleIDs() + ids.DeviceID = 0xA80D + id := BuildIdentifyData(ids, nil) + mn := strings.TrimRight(string(id.Controller[0x018:0x040]), " ") + if strings.Contains(mn, "980 PRO") { + t.Errorf("Samsung DID 0xA80D (990 PRO) MN must not contain '980 PRO', got %q", mn) + } + if !strings.Contains(mn, "990") { + t.Errorf("Samsung DID 0xA80D (990 PRO) MN must contain '990', got %q", mn) + } +} + +func TestNamespace_NSZE_2TB(t *testing.T) { + ids := sampleIDs() + ids.DeviceID = 0xA809 + id := BuildIdentifyData(ids, nil) + nsze := binary.LittleEndian.Uint64(id.Namespace[0x000:]) + const nsze1TB = uint64(1953525168) + if nsze == nsze1TB { + t.Errorf("Samsung DID 0xA809 (2TB) NSZE must differ from 1TB default %d, got %d", nsze1TB, nsze) + } + ncap := binary.LittleEndian.Uint64(id.Namespace[0x008:]) + nuse := binary.LittleEndian.Uint64(id.Namespace[0x010:]) + if ncap != nsze { + t.Errorf("NCAP %d must equal NSZE %d", ncap, nsze) + } + if nuse != nsze { + t.Errorf("NUSE %d must equal NSZE %d", nuse, nsze) + } +} + +func TestOUI_UnknownVID_NotXerox(t *testing.T) { + ids := sampleIDs() + ids.VendorID = 0xBEEF + ids.SubsysVendorID = 0xBEEF + id := BuildIdentifyData(ids, nil) + oui := id.Controller[0x049:0x04C] + if oui[0] == 0 && oui[1] == 0 && oui[2] == 0 { + t.Errorf("Unknown VID OUI must not be 00:00:00 (Xerox's real OUI), got %02X:%02X:%02X", oui[0], oui[1], oui[2]) + } +} diff --git a/internal/firmware/output/bar_profile.go b/internal/firmware/output/bar_profile.go new file mode 100644 index 0000000..65da71e --- /dev/null +++ b/internal/firmware/output/bar_profile.go @@ -0,0 +1,21 @@ +package output + +import ( + "encoding/json" + "fmt" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware/barprofile" +) + +func (ow *OutputWriter) writeBARBehaviorProfile(ctx *donor.DeviceContext) error { + profile := barprofile.Build(ctx) + data, err := json.MarshalIndent(profile, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal BAR behavior profile: %w", err) + } + if err := ow.writeFile("bar_behavior_profile.json", string(data)+"\n"); err != nil { + return fmt.Errorf("failed to write BAR behavior profile: %w", err) + } + return nil +} diff --git a/internal/firmware/output/capabilities.go b/internal/firmware/output/capabilities.go new file mode 100644 index 0000000..28f304b --- /dev/null +++ b/internal/firmware/output/capabilities.go @@ -0,0 +1,75 @@ +package output + +import ( + "github.com/sercanarga/pcileechgen/internal/firmware/svgen" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func extractDonorCapabilities(cs *pci.ConfigSpace) svgen.DonorCapabilities { + if cs == nil { + return svgen.DonorCapabilities{} + } + + var caps svgen.DonorCapabilities + for _, cap := range pci.ParseCapabilities(cs) { + switch cap.ID { + case pci.CapIDPowerManagement: + caps.HasPMCap = true + caps.PMCapOffset = uint16(cap.Offset) + if legacyFieldFits(cap.Offset+2, 2) { + pmc := cs.ReadU16(cap.Offset + 2) + caps.PMESupportMask = uint8((pmc >> 11) & 0x1F) + } + if legacyFieldFits(cap.Offset+4, 2) { + caps.PMEDefault = cs.ReadU16(cap.Offset+4)&0x0100 != 0 + } + case pci.CapIDMSI: + caps.HasMSICap = true + caps.MSICapOffset = uint16(cap.Offset) + if legacyFieldFits(cap.Offset+2, 2) { + msgCtl := cs.ReadU16(cap.Offset + 2) + caps.MSIDisable64Bit = msgCtl&0x0080 == 0 + caps.MSIMultipleMsg = uint8((msgCtl >> 4) & 0x07) + } + case pci.CapIDMSIX: + caps.HasMSIXCap = true + caps.MSIXCapOffset = uint16(cap.Offset) + case pci.CapIDPCIExpress: + caps.HasPCIeCap = true + caps.PCIeCapOffset = uint16(cap.Offset) + if legacyFieldFits(cap.Offset+0x0C, 4) { + linkCap := cs.ReadU32(cap.Offset + 0x0C) + caps.PCIELinkSpeed = uint8(linkCap & 0x0F) + caps.PCIELinkWidth = uint8((linkCap >> 4) & 0x3F) + caps.PCIeASPMCap = uint8((linkCap >> 10) & 0x03) + } + if legacyFieldFits(cap.Offset+0x10, 2) { + linkCtl := cs.ReadU16(cap.Offset + 0x10) + caps.PCIeASPMEnable = uint8(linkCtl & 0x3) + } + } + } + + for _, cap := range pci.ParseExtCapabilities(cs) { + switch cap.ID { + case pci.ExtCapIDAER: + caps.HasAERCap = true + caps.AERCapOffset = uint16(cap.Offset) + case pci.ExtCapIDLTR: + caps.HasLTRCap = true + caps.LTRCapOffset = uint16(cap.Offset) + case pci.ExtCapIDL1PMSubstates: + caps.HasL1PMSubstates = true + caps.L1PMCapOffset = uint16(cap.Offset) + case pci.ExtCapIDDeviceSerialNumber: + caps.HasDSNCap = true + caps.DSNCapOffset = uint16(cap.Offset) + } + } + + return caps +} + +func legacyFieldFits(offset int, width int) bool { + return offset >= 0 && width > 0 && offset+width <= pci.ConfigSpaceLegacySize +} diff --git a/internal/firmware/output/capabilities_test.go b/internal/firmware/output/capabilities_test.go new file mode 100644 index 0000000..206d237 --- /dev/null +++ b/internal/firmware/output/capabilities_test.go @@ -0,0 +1,98 @@ +package output + +import ( + "testing" + + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func TestExtractDonorCapabilities_FromStandardAndExtendedCaps(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU16(0x06, 0x0010) + cs.WriteU8(0x34, 0x40) + + cs.WriteU8(0x40, pci.CapIDPowerManagement) + cs.WriteU8(0x41, 0x50) + cs.WriteU16(0x42, 0xA800) + cs.WriteU16(0x44, 0x0100) + + cs.WriteU8(0x50, pci.CapIDMSI) + cs.WriteU8(0x51, 0x60) + cs.WriteU16(0x52, 0x00F0) + + cs.WriteU8(0x60, pci.CapIDMSIX) + cs.WriteU8(0x61, 0x70) + + cs.WriteU8(0x70, pci.CapIDPCIExpress) + cs.WriteU8(0x71, 0x00) + cs.WriteU32(0x7C, 0x00000C43) + cs.WriteU16(0x80, 0x0002) + + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 0x140)) + cs.WriteU32(0x140, makeExtCapHeader(pci.ExtCapIDLTR, 0x180)) + cs.WriteU32(0x180, makeExtCapHeader(pci.ExtCapIDL1PMSubstates, 0x1C0)) + cs.WriteU32(0x1C0, makeExtCapHeader(pci.ExtCapIDDeviceSerialNumber, 0)) + + caps := extractDonorCapabilities(cs) + + if !caps.HasPMCap { + t.Fatal("PM capability should be detected") + } + if caps.PMCapOffset != 0x40 { + t.Errorf("PMCapOffset = 0x%03x, want 0x040", caps.PMCapOffset) + } + if caps.PMESupportMask != 0x15 { + t.Errorf("PMESupportMask = 0x%02x, want 0x15", caps.PMESupportMask) + } + if !caps.PMEDefault { + t.Error("PMEDefault should be true") + } + if !caps.HasMSICap { + t.Fatal("MSI capability should be detected") + } + if caps.MSICapOffset != 0x50 { + t.Errorf("MSICapOffset = 0x%03x, want 0x050", caps.MSICapOffset) + } + if caps.MSIDisable64Bit { + t.Error("MSIDisable64Bit should be false") + } + if caps.MSIMultipleMsg != 0x07 { + t.Errorf("MSIMultipleMsg = 0x%02x, want 0x07", caps.MSIMultipleMsg) + } + if !caps.HasMSIXCap { + t.Error("MSI-X capability should be detected") + } + if caps.MSIXCapOffset != 0x60 { + t.Errorf("MSIXCapOffset = 0x%03x, want 0x060", caps.MSIXCapOffset) + } + if !caps.HasPCIeCap { + t.Fatal("PCIe capability should be detected") + } + if caps.PCIeCapOffset != 0x70 { + t.Errorf("PCIeCapOffset = 0x%03x, want 0x070", caps.PCIeCapOffset) + } + if caps.PCIELinkSpeed != 0x03 { + t.Errorf("PCIELinkSpeed = 0x%02x, want 0x03", caps.PCIELinkSpeed) + } + if caps.PCIELinkWidth != 0x04 { + t.Errorf("PCIELinkWidth = 0x%02x, want 0x04", caps.PCIELinkWidth) + } + if caps.PCIeASPMCap != 0x03 { + t.Errorf("PCIeASPMCap = 0x%02x, want 0x03", caps.PCIeASPMCap) + } + if caps.PCIeASPMEnable != 0x02 { + t.Errorf("PCIeASPMEnable = 0x%02x, want 0x02", caps.PCIeASPMEnable) + } + if !caps.HasAERCap || !caps.HasLTRCap || !caps.HasL1PMSubstates || !caps.HasDSNCap { + t.Errorf("extended caps missing: %+v", caps) + } + if caps.AERCapOffset != 0x100 || caps.LTRCapOffset != 0x140 || + caps.L1PMCapOffset != 0x180 || caps.DSNCapOffset != 0x1C0 { + t.Errorf("extended cap offsets missing: %+v", caps) + } +} + +func makeExtCapHeader(id uint16, next int) uint32 { + return uint32(id) | (uint32(1) << 16) | (uint32(next&0xFFC) << 20) +} diff --git a/internal/firmware/output/code10.go b/internal/firmware/output/code10.go new file mode 100644 index 0000000..255972d --- /dev/null +++ b/internal/firmware/output/code10.go @@ -0,0 +1,138 @@ +package output + +import ( + "fmt" + "log/slog" + "strings" + + "github.com/sercanarga/pcileechgen/internal/board" + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func (ow *OutputWriter) code10Risks(ctx *donor.DeviceContext, b *board.Board, ids firmware.DeviceIDs) []string { + msixTableSize := 0 + if ctx.MSIXData != nil && ctx.MSIXData.TableSize > 0 { + msixTableSize = ctx.MSIXData.TableSize + } + demand := firmware.DonorBAR0Demand(ctx, b, msixTableSize) + capped := firmware.CappedBAR0Size(ctx, b, msixTableSize) + + devClass := "" + if s := devclass.StrategyForClassAndVendor(ctx.Device.ClassCode, ids.VendorID); s != nil { + devClass = s.DeviceClass() + } + + var risks []string + + if capped < demand { + risks = append(risks, fmt.Sprintf( + "BAR0 TRUNCATED %d -> %d bytes (board BRAM limit): driver reads above 0x%X return garbage. Use a board with larger bram_size or a smaller-BAR donor.", + demand, capped, capped)) + } + + switch devClass { + case devclass.ClassNVMe: + risks = append(risks, "NVMe donor: the admin + IO-queue FSM exists but there is NO backing store - IO reads return zeros and writes are discarded, so the disk won't mount (stornvme Event 11). A BRAM/host-DMA sector cache is required.") + case devclass.ClassXHCI: + risks = append(risks, "xHCI (USB) donor: no transfer engine is implemented (no command ring, event ring, doorbell array or DCBAAP) - the controller enumerates then the USB stack hangs (Code 10).") + case devclass.ClassSATA: + risks = append(risks, "SATA/AHCI donor: no command-list FSM - PxCI never clears, so storahci commands never complete (Code 10). This class is an identity clone only.") + case devclass.ClassWiFi: + risks = append(risks, "WiFi donor: no uCode-upload DMA handshake FSM, and the BAR model may fall back to a generic NIC layout - driver init typically fails (Code 10).") + case devclass.ClassGPU: + risks = append(risks, "GPU donor: only a 4K register window is emulated of a multi-MB VRAM/register space - structurally insufficient for any GPU driver (Code 10).") + case devclass.ClassThunderbolt: + risks = append(risks, "Thunderbolt donor: no link/mailbox/tunnelling FSM - static snapshot only, driver init will not complete (Code 10).") + case devclass.ClassEthernet: + risks = append(risks, "Ethernet donor: static identity clone with link-up state but no TX/RX descriptor-ring DMA - enumerates but does not pass traffic.") + } + + if !ow.StockBar && isInterruptDrivenClass(devClass) { + risks = append(risks, "interrupt-driven class with custom SV: VERIFY intr_req is wired to the PCIe core cfg_interrupt in your pcileech-fpga top module - this tool does not emit that connection, and if it is missing, interrupts never fire (Code 10).") + } + + if barData, ok := ctx.BARContents[firmware.LargestBarIndex(ctx.BARContents)]; ok { + switch { + case len(barData) == 0: + risks = append(risks, "selected BAR has no captured content (donor likely in D3/IOMMU off): BAR0 will read as zeros, init will fail. Re-run 'check' and confirm D0 + vfio-pci binding.") + case allBytes(barData, 0xFF): + risks = append(risks, "selected BAR content is all 0xFF (donor not readable at capture time): the clone's BAR0 will read FF and the driver will Code 10. Wake the donor to D0 and re-collect.") + } + } + + if msixTableSize == 0 && !hasMSICap(ctx.ConfigSpace) { + risks = append(risks, "no MSI-X table and no MSI capability detected on the donor: interrupt-driven drivers may never complete init (Code 10).") + } + + if ctx.MSIXData != nil && (ctx.MSIXData.TableBIR != 0 || ctx.MSIXData.PBABIR != 0) { + risks = append(risks, fmt.Sprintf( + "MSI-X relocated to BAR0: donor BIR was table=%d pba=%d but only BAR0 is emulated, so the clone reports BIR 0. Required for the table to work, but a fidelity/detection divergence from the donor.", + ctx.MSIXData.TableBIR, ctx.MSIXData.PBABIR)) + } + + if !ids.HasDSN { + risks = append(risks, "donor has no DSN: serial-number emulation disabled (usually harmless).") + } + if ids.HasPCIeCap && b.PCIeLanes > 0 && int(ids.LinkWidth) > b.PCIeLanes { + risks = append(risks, fmt.Sprintf("donor link width x%d clamped to board x%d: fine for most devices, but bandwidth-sensitive drivers may complain.", ids.LinkWidth, b.PCIeLanes)) + } + + return risks +} + +func isInterruptDrivenClass(devClass string) bool { + switch devClass { + case devclass.ClassNVMe, devclass.ClassXHCI, devclass.ClassAudio, devclass.ClassEthernet: + return true + } + return false +} + +func (ow *OutputWriter) writeCode10Report(ctx *donor.DeviceContext, b *board.Board, ids firmware.DeviceIDs) { + risks := ow.code10Risks(ctx, b, ids) + + header := fmt.Sprintf("Code-10 risk report for %04x:%04x (%s) on %s", + ids.VendorID, ids.DeviceID, ctx.Device.ClassDescription(), b.Name) + + if len(risks) == 0 { + slog.Info("code10-risk: none detected", "device", header) + _ = ow.writeFile("code10_report.txt", header+"\n\nNo obvious Code-10 risks detected at generation time.\n") + return + } + + for _, r := range risks { + slog.Warn("code10-risk", "detail", r) + } + + var sb strings.Builder + sb.WriteString(header + "\n\n") + for i, r := range risks { + fmt.Fprintf(&sb, "%d. %s\n", i+1, r) + } + sb.WriteString("\nIf the flash still Code-10s after addressing these, run tools/cleanup_device_history.ps1 (Admin) and reboot.\n") + _ = ow.writeFile("code10_report.txt", sb.String()) +} + +func allBytes(b []byte, v byte) bool { + for _, x := range b { + if x != v { + return false + } + } + return len(b) > 0 +} + +func hasMSICap(cs *pci.ConfigSpace) bool { + if cs == nil { + return false + } + for _, c := range pci.ParseCapabilities(cs) { + if c.ID == pci.CapIDMSI { + return true + } + } + return false +} diff --git a/internal/firmware/output/code10_test.go b/internal/firmware/output/code10_test.go new file mode 100644 index 0000000..24d98a1 --- /dev/null +++ b/internal/firmware/output/code10_test.go @@ -0,0 +1,102 @@ +package output + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/board" + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware" +) + +const ( + ccNVMe = 0x010802 + ccXHCI = 0x0C0330 + ccSATA = 0x010601 + ccWiFi = 0x028000 + ccGPU = 0x030000 + ccEth = 0x020000 +) + +func risksFor(t *testing.T, classCode uint32, stockBar bool) []string { + t.Helper() + ctx := makeDonorContext(0x1234, 0x5678, classCode) + b, err := board.Find("PCIeSquirrel") + if err != nil { + t.Fatalf("board.Find: %v", err) + } + ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) + ow := NewOutputWriter(t.TempDir(), "", 0, 0) + ow.StockBar = stockBar + return ow.code10Risks(ctx, b, ids) +} + +func joinLower(rs []string) string { return strings.ToLower(strings.Join(rs, "\n")) } + +func mustContain(t *testing.T, rs []string, sub string) { + t.Helper() + if !strings.Contains(joinLower(rs), strings.ToLower(sub)) { + t.Errorf("expected a risk mentioning %q, got:\n%s", sub, strings.Join(rs, "\n")) + } +} + +func mustNotContain(t *testing.T, rs []string, sub string) { + t.Helper() + if strings.Contains(joinLower(rs), strings.ToLower(sub)) { + t.Errorf("did NOT expect %q in risks, got:\n%s", sub, strings.Join(rs, "\n")) + } +} + +func TestCode10_NVMe_BackingStoreNotStaleText(t *testing.T) { + rs := risksFor(t, ccNVMe, false) + mustContain(t, rs, "backing store") + mustNotContain(t, rs, "io queues + bram cache are not implemented") +} + +func TestCode10_XHCI_NoTransferEngine(t *testing.T) { + rs := risksFor(t, ccXHCI, false) + mustContain(t, rs, "transfer engine") +} + +func TestCode10_SATA_NoFSM(t *testing.T) { + rs := risksFor(t, ccSATA, false) + mustContain(t, rs, "no command-list") +} + +func TestCode10_WiFi_NoFSM(t *testing.T) { + rs := risksFor(t, ccWiFi, false) + mustContain(t, rs, "wifi") +} + +func TestCode10_GPU_NoFSM(t *testing.T) { + rs := risksFor(t, ccGPU, false) + mustContain(t, rs, "gpu") +} + +func TestCode10_InterruptRoutingVerificationNote(t *testing.T) { + rs := risksFor(t, ccNVMe, false) + mustContain(t, rs, "intr_req") + + stock := risksFor(t, ccNVMe, true) + if strings.Contains(joinLower(stock), "intr_req") { + t.Errorf("stock-bar mode should not emit the intr_req routing note, got:\n%s", strings.Join(stock, "\n")) + } +} + +func TestCode10_Ethernet_NoPanic(t *testing.T) { + _ = risksFor(t, ccEth, false) +} + +func TestCode10_MSIX_BIRRelocationWarning(t *testing.T) { + ctx := makeDonorContext(0x1234, 0x5678, ccEth) + ctx.MSIXData = &donor.MSIXData{TableSize: 4, TableBIR: 2, PBABIR: 2} + b, _ := board.Find("PCIeSquirrel") + ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) + ow := NewOutputWriter(t.TempDir(), "", 0, 0) + rs := ow.code10Risks(ctx, b, ids) + mustContain(t, rs, "relocated to bar0") + + ctx.MSIXData = &donor.MSIXData{TableSize: 4, TableBIR: 0, PBABIR: 0} + rs0 := ow.code10Risks(ctx, b, ids) + mustNotContain(t, rs0, "relocated to bar0") +} diff --git a/internal/firmware/output/coverage_test.go b/internal/firmware/output/coverage_test.go index 95707ec..38b5e41 100644 --- a/internal/firmware/output/coverage_test.go +++ b/internal/firmware/output/coverage_test.go @@ -188,6 +188,35 @@ func TestWriteConditionalArtifacts_NVMe(t *testing.T) { } } +func TestWriteConditionalArtifacts_XHCI(t *testing.T) { + dir := t.TempDir() + ctx := makeDonorContext(0x8086, 0xA36D, 0x0C0330) + ow := NewOutputWriter(dir, "", 0, 0) + ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) + + cfg, err := ow.buildSVConfig(ctx, ctx.ConfigSpace, ids, 0x42, &board.Board{}); if err != nil { t.Fatal(err) } + if cfg.BARModel == nil { + t.Fatal("xHCI should have a BARModel") + } + + if err := ow.writeConditionalArtifacts(cfg, ctx); err != nil { + t.Fatalf("writeConditionalArtifacts failed: %v", err) + } + + // Regression check for the "no Go code ever renders xhci_ring_engine" + // showstopper: bar_controller.sv.tmpl instantiates pcileech_xhci_ring_engine + // and pcileech_xhci_dma_bridge, so the file defining them must exist. + data, err := os.ReadFile(filepath.Join(dir, "pcileech_xhci_ring_engine.sv")) + if err != nil { + t.Fatalf("pcileech_xhci_ring_engine.sv not created: %v", err) + } + for _, module := range []string{"module pcileech_xhci_ring_engine", "module pcileech_xhci_dma_bridge"} { + if !strings.Contains(string(data), module) { + t.Errorf("pcileech_xhci_ring_engine.sv missing %q", module) + } + } +} + func TestWriteConditionalArtifacts_MSIXDonor(t *testing.T) { dir := t.TempDir() ctx := makeDonorContext(0x8086, 0x15B7, 0x020000) diff --git a/internal/firmware/output/ila_wiring_test.go b/internal/firmware/output/ila_wiring_test.go new file mode 100644 index 0000000..398e08c --- /dev/null +++ b/internal/firmware/output/ila_wiring_test.go @@ -0,0 +1,37 @@ +package output + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/board" + "github.com/sercanarga/pcileechgen/internal/firmware" +) + +func TestBuildSVConfig_ILAInstance(t *testing.T) { + ctx := makeDonorContext(0x144D, 0xA808, ccNVMe) + b, err := board.Find("PCIeSquirrel") + if err != nil { + t.Fatalf("board.Find: %v", err) + } + ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) + ow := NewOutputWriter(t.TempDir(), "", 0, 0) + scrubbed, entropy, _ := ow.scrubAndVary(ctx, b, ids) + + cfg, err := ow.buildSVConfig(ctx, scrubbed, ids, entropy, b) + if err != nil { + t.Fatalf("buildSVConfig (no ILA): %v", err) + } + if cfg.ILAInstanceSV != "" { + t.Error("expected no ILA instance when ILADepth=0") + } + + ow.ILADepth = 1024 + cfg2, err := ow.buildSVConfig(ctx, scrubbed, ids, entropy, b) + if err != nil { + t.Fatalf("buildSVConfig (ILA): %v", err) + } + if !strings.Contains(cfg2.ILAInstanceSV, "ila_0 u_ila_dbg") { + t.Errorf("expected ILA instance, got %q", cfg2.ILAInstanceSV) + } +} diff --git a/internal/firmware/output/integration_test.go b/internal/firmware/output/integration_test.go index 83e0490..e9a7665 100644 --- a/internal/firmware/output/integration_test.go +++ b/internal/firmware/output/integration_test.go @@ -192,7 +192,9 @@ func TestIntegration_LatencyConfigWriteFields(t *testing.T) { func TestIntegration_OutputManifest(t *testing.T) { dir := t.TempDir() testFile := filepath.Join(dir, "pcileech_cfgspace.coe") - os.WriteFile(testFile, []byte("memory_initialization_radix=16;\nmemory_initialization_vector=00;"), 0644) + if err := os.WriteFile(testFile, []byte("memory_initialization_radix=16;\nmemory_initialization_vector=00;"), 0644); err != nil { + t.Fatal(err) + } manifest, err := GenerateManifest(dir, "test-version", "", 0x144D, 0xA808) if err != nil { @@ -206,7 +208,9 @@ func TestIntegration_OutputManifest(t *testing.T) { func TestIntegration_OutputValidator(t *testing.T) { dir := t.TempDir() coe := "memory_initialization_radix=16;\nmemory_initialization_vector=\n00000000\n00000000\n00000000\n00000000;" - os.WriteFile(filepath.Join(dir, "pcileech_cfgspace.coe"), []byte(coe), 0644) + if err := os.WriteFile(filepath.Join(dir, "pcileech_cfgspace.coe"), []byte(coe), 0644); err != nil { + t.Fatal(err) + } result := ValidateOutputDir(dir) // not all files exist so some will fail, but should not panic diff --git a/internal/firmware/output/latency_wiring_test.go b/internal/firmware/output/latency_wiring_test.go new file mode 100644 index 0000000..c0d1525 --- /dev/null +++ b/internal/firmware/output/latency_wiring_test.go @@ -0,0 +1,50 @@ +package output + +import ( + "testing" + + "github.com/sercanarga/pcileechgen/internal/board" + "github.com/sercanarga/pcileechgen/internal/donor/behavior" + "github.com/sercanarga/pcileechgen/internal/firmware" +) + +func TestBuildSVConfig_UsesDonorHistogram(t *testing.T) { + ctx := makeDonorContext(0x144D, 0xA808, ccNVMe) + b, err := board.Find("PCIeSquirrel") + if err != nil { + t.Fatalf("board.Find: %v", err) + } + ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) + ow := NewOutputWriter(t.TempDir(), "", 0, 0) + scrubbed, entropy, _ := ow.scrubAndVary(ctx, b, ids) + + cfg, err := ow.buildSVConfig(ctx, scrubbed, ids, entropy, b) + if err != nil { + t.Fatalf("buildSVConfig (no histogram): %v", err) + } + if cfg.LatencyConfig.HasHistogram { + t.Error("expected HasHistogram=false without a donor histogram") + } + + h := &behavior.TimingHistogram{ + SampleCount: 500, + MinCycles: 2, + MaxCycles: 9, + MedianCycles: 4, + } + for i := range h.Buckets { + h.Buckets[i] = uint8(i) + h.CDF[i] = uint8((i + 1) * 16) + } + ow.TimingHistogram = h + cfg2, err := ow.buildSVConfig(ctx, scrubbed, ids, entropy, b) + if err != nil { + t.Fatalf("buildSVConfig (with histogram): %v", err) + } + if !cfg2.LatencyConfig.HasHistogram { + t.Error("expected HasHistogram=true when a donor histogram is supplied") + } + if cfg2.LatencyConfig.CDF != h.CDF { + t.Errorf("expected CDF derived from donor histogram, got %v", cfg2.LatencyConfig.CDF) + } +} diff --git a/internal/firmware/output/manifest_test.go b/internal/firmware/output/manifest_test.go index 7cfdac7..75d45da 100644 --- a/internal/firmware/output/manifest_test.go +++ b/internal/firmware/output/manifest_test.go @@ -30,8 +30,12 @@ func TestGenerateManifest_Empty(t *testing.T) { func TestGenerateManifest_WithFiles(t *testing.T) { tmpDir := t.TempDir() // Create some output files - os.WriteFile(filepath.Join(tmpDir, "pcileech_cfgspace.coe"), []byte("test content"), 0644) - os.WriteFile(filepath.Join(tmpDir, "device_context.json"), []byte("{}"), 0644) + if err := os.WriteFile(filepath.Join(tmpDir, "pcileech_cfgspace.coe"), []byte("test content"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "device_context.json"), []byte("{}"), 0644); err != nil { + t.Fatal(err) + } m, err := GenerateManifest(tmpDir, "1.0.0", "TestBoard", 0x8086, 0x1533) if err != nil { @@ -53,8 +57,12 @@ func TestGenerateManifest_WithFiles(t *testing.T) { func TestGenerateManifest_WithSrcDir(t *testing.T) { tmpDir := t.TempDir() srcDir := filepath.Join(tmpDir, "src") - os.MkdirAll(srcDir, 0755) - os.WriteFile(filepath.Join(srcDir, "test.sv"), []byte("module test; endmodule"), 0644) + if err := os.MkdirAll(srcDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(srcDir, "test.sv"), []byte("module test; endmodule"), 0644); err != nil { + t.Fatal(err) + } m, err := GenerateManifest(tmpDir, "1.0.0", "", 0, 0) if err != nil { @@ -109,7 +117,9 @@ func TestWriteJSON(t *testing.T) { func TestFileHash(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "test.txt") - os.WriteFile(path, []byte("hello"), 0644) + if err := os.WriteFile(path, []byte("hello"), 0644); err != nil { + t.Fatal(err) + } hash, err := fileHash(path) if err != nil { @@ -121,7 +131,9 @@ func TestFileHash(t *testing.T) { // Same content -> same hash path2 := filepath.Join(tmpDir, "test2.txt") - os.WriteFile(path2, []byte("hello"), 0644) + if writeErr := os.WriteFile(path2, []byte("hello"), 0644); writeErr != nil { + t.Fatal(writeErr) + } hash2, _ := fileHash(path2) if hash != hash2 { t.Error("Same content should produce same hash") @@ -137,7 +149,9 @@ func TestFileHash(t *testing.T) { func TestVerifyManifest_AllPass(t *testing.T) { tmpDir := t.TempDir() - os.WriteFile(filepath.Join(tmpDir, "test.coe"), []byte("content"), 0644) + if err := os.WriteFile(filepath.Join(tmpDir, "test.coe"), []byte("content"), 0644); err != nil { + t.Fatal(err) + } hash, _ := fileHash(filepath.Join(tmpDir, "test.coe")) m := &BuildManifest{ @@ -146,7 +160,9 @@ func TestVerifyManifest_AllPass(t *testing.T) { }, } manifestPath := filepath.Join(tmpDir, "manifest.json") - m.WriteJSON(manifestPath) + if err := m.WriteJSON(manifestPath); err != nil { + t.Fatal(err) + } v, err := VerifyManifest(manifestPath, tmpDir) if err != nil { @@ -168,7 +184,9 @@ func TestVerifyManifest_MissingFile(t *testing.T) { }, } manifestPath := filepath.Join(tmpDir, "manifest.json") - m.WriteJSON(manifestPath) + if err := m.WriteJSON(manifestPath); err != nil { + t.Fatal(err) + } v, err := VerifyManifest(manifestPath, tmpDir) if err != nil { @@ -184,7 +202,9 @@ func TestVerifyManifest_MissingFile(t *testing.T) { func TestVerifyManifest_HashMismatch(t *testing.T) { tmpDir := t.TempDir() - os.WriteFile(filepath.Join(tmpDir, "test.coe"), []byte("content"), 0644) + if err := os.WriteFile(filepath.Join(tmpDir, "test.coe"), []byte("content"), 0644); err != nil { + t.Fatal(err) + } m := &BuildManifest{ Files: []ManifestEntry{ @@ -192,7 +212,9 @@ func TestVerifyManifest_HashMismatch(t *testing.T) { }, } manifestPath := filepath.Join(tmpDir, "manifest.json") - m.WriteJSON(manifestPath) + if err := m.WriteJSON(manifestPath); err != nil { + t.Fatal(err) + } v, err := VerifyManifest(manifestPath, tmpDir) if err != nil { @@ -213,7 +235,9 @@ func TestLoadManifest_Invalid(t *testing.T) { } tmpDir := t.TempDir() - os.WriteFile(filepath.Join(tmpDir, "bad.json"), []byte("not json"), 0644) + if writeErr := os.WriteFile(filepath.Join(tmpDir, "bad.json"), []byte("not json"), 0644); writeErr != nil { + t.Fatal(writeErr) + } _, err = LoadManifest(filepath.Join(tmpDir, "bad.json")) if err == nil { t.Error("LoadManifest should fail for invalid JSON") diff --git a/internal/firmware/output/validator.go b/internal/firmware/output/validator.go index e78d604..88ebabb 100644 --- a/internal/firmware/output/validator.go +++ b/internal/firmware/output/validator.go @@ -26,13 +26,16 @@ func (vr *ValidationResult) Summary() string { func (vr *ValidationResult) pass(msg string) { vr.Passed = append(vr.Passed, msg) } func (vr *ValidationResult) fail(msg string) { vr.Failed = append(vr.Failed, msg) } -func (vr *ValidationResult) warn(msg string) { vr.Warnings = append(vr.Warnings, msg) } // ValidateOutputDir checks that all expected files exist and aren't empty. func ValidateOutputDir(dir string) *ValidationResult { result := &ValidationResult{} for _, name := range ListOutputFiles() { + if !outputFileRequired(dir, name) { + continue + } + path := filepath.Join(dir, name) if strings.HasSuffix(name, "/") { @@ -87,9 +90,15 @@ func ValidateHexFile(hexContent string, expectedWords int) error { nonEmpty := 0 for i, line := range lines { line = strings.TrimSpace(line) - if line == "" { + if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, ";") { continue } + if beforeComment, _, ok := strings.Cut(line, "//"); ok { + line = strings.TrimSpace(beforeComment) + if line == "" { + continue + } + } nonEmpty++ if len(line) != 8 { return fmt.Errorf("line %d: expected 8 hex chars, got %d (%q)", i+1, len(line), line) @@ -108,6 +117,45 @@ func ValidateHexFile(hexContent string, expectedWords int) error { return nil } +func outputFileRequired(dir string, name string) bool { + switch name { + case "pcileech_msix_table.sv", "msix_table_init.hex": + return generatedLocalparamPresent(dir, "MSIX_NUM_VECTORS") + case "pcileech_nvme_admin_responder.sv", "pcileech_nvme_dma_bridge.sv": + return generatedFeatureEnabled(dir, "HAS_NVME_RESP") + default: + return true + } +} + +func generatedFeatureEnabled(dir string, feature string) bool { + data, err := os.ReadFile(filepath.Join(dir, "device_config.sv")) + if err != nil { + return true + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) >= 4 && fields[1] == feature && fields[2] == "=" { + return strings.TrimSuffix(fields[3], ";") == "1" + } + } + return false +} + +func generatedLocalparamPresent(dir string, name string) bool { + data, err := os.ReadFile(filepath.Join(dir, "device_config.sv")) + if err != nil { + return true + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[1] == name { + return true + } + } + return false +} + // ValidateCOEFile checks radix/vector directives and trailing semicolon. func ValidateCOEFile(content string) error { if !strings.Contains(content, "memory_initialization_radix") { diff --git a/internal/firmware/output/validator_optional_test.go b/internal/firmware/output/validator_optional_test.go new file mode 100644 index 0000000..debd94e --- /dev/null +++ b/internal/firmware/output/validator_optional_test.go @@ -0,0 +1,79 @@ +package output + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestValidateHexFile_GeneratedComments(t *testing.T) { + hex := "// generated header\nDEADBEEF // [000]\n; comment\n12345678\n" + if err := ValidateHexFile(hex, 2); err != nil { + t.Errorf("expected generated hex comments to be ignored, got: %v", err) + } +} + +func TestValidateOutputDir_SkipsDisabledOptionalArtifacts(t *testing.T) { + tmpDir := t.TempDir() + optional := map[string]bool{ + "pcileech_msix_table.sv": true, + "msix_table_init.hex": true, + "pcileech_nvme_admin_responder.sv": true, + "pcileech_nvme_dma_bridge.sv": true, + } + deviceConfig := "package device_config;\n" + + "localparam HAS_NVME_RESP = 0;\n" + + "localparam HAS_MSIX_INT = 0;\n" + + "endpackage\n" + + writeOutputFixture(t, tmpDir, optional, deviceConfig) + + result := ValidateOutputDir(tmpDir) + if result.HasFailures() { + t.Errorf("disabled optional artifacts should not fail validation: %v", result.Failed) + } +} + +func TestValidateOutputDir_RequiresMSIXArtifactsWhenTableConfigured(t *testing.T) { + tmpDir := t.TempDir() + optional := map[string]bool{ + "pcileech_msix_table.sv": true, + "msix_table_init.hex": true, + "pcileech_nvme_admin_responder.sv": true, + "pcileech_nvme_dma_bridge.sv": true, + } + deviceConfig := "package device_config;\n" + + "localparam MSIX_NUM_VECTORS = 4;\n" + + "localparam HAS_NVME_RESP = 0;\n" + + "endpackage\n" + + writeOutputFixture(t, tmpDir, optional, deviceConfig) + + result := ValidateOutputDir(tmpDir) + if len(result.Failed) != 2 { + t.Fatalf("expected missing MSI-X table artifacts to fail, got %v", result.Failed) + } +} + +func writeOutputFixture(t *testing.T, tmpDir string, skip map[string]bool, deviceConfig string) { + t.Helper() + for _, name := range ListOutputFiles() { + if skip[name] { + continue + } + if strings.HasSuffix(name, "/") { + if err := os.MkdirAll(filepath.Join(tmpDir, name), 0755); err != nil { + t.Fatalf("MkdirAll(%s): %v", name, err) + } + continue + } + content := "content" + if name == "device_config.sv" { + content = deviceConfig + } + if err := os.WriteFile(filepath.Join(tmpDir, name), []byte(content), 0644); err != nil { + t.Fatalf("WriteFile(%s): %v", name, err) + } + } +} diff --git a/internal/firmware/output/validator_test.go b/internal/firmware/output/validator_test.go index 0552765..225650d 100644 --- a/internal/firmware/output/validator_test.go +++ b/internal/firmware/output/validator_test.go @@ -109,9 +109,13 @@ func TestValidateOutputDir_WithFiles(t *testing.T) { tmpDir := t.TempDir() for _, name := range ListOutputFiles() { if strings.HasSuffix(name, "/") { - os.MkdirAll(filepath.Join(tmpDir, name), 0755) + if err := os.MkdirAll(filepath.Join(tmpDir, name), 0755); err != nil { + t.Fatal(err) + } } else { - os.WriteFile(filepath.Join(tmpDir, name), []byte("content"), 0644) + if err := os.WriteFile(filepath.Join(tmpDir, name), []byte("content"), 0644); err != nil { + t.Fatal(err) + } } } @@ -123,7 +127,9 @@ func TestValidateOutputDir_WithFiles(t *testing.T) { func TestValidateOutputDir_EmptyFile(t *testing.T) { tmpDir := t.TempDir() - os.WriteFile(filepath.Join(tmpDir, "pcileech_cfgspace.coe"), []byte(""), 0644) + if err := os.WriteFile(filepath.Join(tmpDir, "pcileech_cfgspace.coe"), []byte(""), 0644); err != nil { + t.Fatal(err) + } result := ValidateOutputDir(tmpDir) foundEmpty := false @@ -238,7 +244,11 @@ func TestValidationResult_Summary(t *testing.T) { } } -func TestValidateBARSizeExceed(t *testing.T){if e:=ValidateBARSize(8192,4096,0);len(e)==0{t.Error("ValidateBARSize(exceed) should report")}} +func TestValidateBARSizeExceed(t *testing.T) { + if e := ValidateBARSize(8192, 4096, 0); len(e) == 0 { + t.Error("ValidateBARSize(exceed) should report") + } +} // TestValidateBARSize_ExceedWithForce covers the exceed case (force logic lives in writer.buildSVConfig using len(issues) && !ow.Force) func TestValidateBARSize_ExceedWithForce(t *testing.T) { @@ -269,11 +279,11 @@ func TestLargeBAR_CmdPathRepro(t *testing.T) { barData := make([]byte, 16384) barData[0], barData[100] = 0xDE, 0xAD ctx := &donor.DeviceContext{ - Device: pci.PCIDevice{VendorID: 0x10DE, DeviceID: 0x1234, ClassCode: 0x030000, RevisionID: 0xA1}, - ConfigSpace: pci.NewConfigSpace(), - BARs: []pci.BAR{{Index: 0, Size: 16384, Type: pci.BARTypeMem32, RawValue: 0x00000000}}, - BARContents: map[int][]byte{0: barData}, - Capabilities: []pci.Capability{}, + Device: pci.PCIDevice{VendorID: 0x10DE, DeviceID: 0x1234, ClassCode: 0x030000, RevisionID: 0xA1}, + ConfigSpace: pci.NewConfigSpace(), + BARs: []pci.BAR{{Index: 0, Size: 16384, Type: pci.BARTypeMem32, RawValue: 0x00000000}}, + BARContents: map[int][]byte{0: barData}, + Capabilities: []pci.Capability{}, ExtCapabilities: []pci.ExtCapability{}, } @@ -284,9 +294,8 @@ func TestLargeBAR_CmdPathRepro(t *testing.T) { if donorDemand != 16384 || bar0Sz != 4096 { t.Fatalf("build trace: demand/capped 16k@4k want 16384/4096 got %d/%d", donorDemand, bar0Sz) } - if donorDemand > b4k.BRAMSizeOrDefault() { - // without force -> error (as in runBuild) - // with force -> warn + proceed (bar0Sz used for scrub/writer) + if donorDemand <= b4k.BRAMSizeOrDefault() { + t.Fatal("test setup should require force for the 4KB board") } // writer buildSVConfig pattern: Validate(donor) w/ Force @@ -305,17 +314,15 @@ func TestLargeBAR_CmdPathRepro(t *testing.T) { donorV := firmware.DonorBAR0Demand(ctx, b4k, msixSz) bar0V := firmware.CappedBAR0Size(ctx, b4k, msixSz) _ = bar0V // used for scrub in validator - if b := b4k; b != nil { - if iss := ValidateBARSize(donorV, b.BRAMSizeOrDefault(), 0); len(iss) > 0 { - // returns fmt err(iss[0]) -- always, no force param in validate - if iss[0] == "" { - t.Error("validate must surface the Bar0Size exceed msg") - } + if iss := ValidateBARSize(donorV, b4k.BRAMSizeOrDefault(), 0); len(iss) > 0 { + // returns fmt err(iss[0]) -- always, no force param in validate + if iss[0] == "" { + t.Error("validate must surface the Bar0Size exceed msg") } } // === tclgen (called from writer.writeTCLScripts) : StockBar forces zerowrite, reports correct Bar0ByteSize === - tclStock := tclgen.GenerateProjectTCL(ctx, b4k, "/tmp/lib", true /*stock*/) + tclStock := tclgen.GenerateProjectTCL(ctx, b4k, "/tmp/lib", true /*stock*/, 0) if !strings.Contains(tclStock, "&& 0") { t.Error("--stock-bar must render &&0 to use zerowrite4k path (no donor coe patch)") } @@ -324,14 +331,14 @@ func TestLargeBAR_CmdPathRepro(t *testing.T) { t.Error("tcl must report Bar0 config (correct donor Bar0ByteSize even under stock)") } - tclForceOversz := tclgen.GenerateProjectTCL(ctx, b4k, "/tmp/lib", false) + tclForceOversz := tclgen.GenerateProjectTCL(ctx, b4k, "/tmp/lib", false, 0) // Bar0ByteSize demand=16k >4k => &&0 , but correct size used for IP bar config if !strings.Contains(tclForceOversz, "CONFIG.Bar0_Size") { t.Error("oversized (force) must still set correct donor Bar0_Size in PCIe IP") } // fitting case on large board still >4k so zerowrite for bram_ip (custom handles) - tclFit := tclgen.GenerateProjectTCL(ctx, b16k, "/tmp/lib", false) + tclFit := tclgen.GenerateProjectTCL(ctx, b16k, "/tmp/lib", false, 0) if !strings.Contains(tclFit, "&& 0") { t.Log("note: large fitting may or not skip based on size; current >4k always skips patch for bram_ip") } @@ -340,9 +347,8 @@ func TestLargeBAR_CmdPathRepro(t *testing.T) { // boards cmd shows BRAM column explicitly // check uses largest from resource, errors/warns per checkForce, shows note "(donor BAR X > board BRAM Y)" largestBAR := uint64(16384) - if uint64(b4k.BRAMSizeOrDefault()) < largestBAR { - // !checkForce => issues++ , label Fail , note with mismatch - // checkForce => label Warn + if uint64(b4k.BRAMSizeOrDefault()) >= largestBAR { + t.Error("test setup should model a donor BAR larger than board BRAM") } // === reproduce: final artifacts use correct size (capped) === diff --git a/internal/firmware/output/writer.go b/internal/firmware/output/writer.go index 484074a..f749104 100644 --- a/internal/firmware/output/writer.go +++ b/internal/firmware/output/writer.go @@ -10,10 +10,13 @@ import ( "github.com/sercanarga/pcileechgen/internal/board" "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/donor/behavior" "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/ahci" "github.com/sercanarga/pcileechgen/internal/firmware/barmodel" "github.com/sercanarga/pcileechgen/internal/firmware/codegen" "github.com/sercanarga/pcileechgen/internal/firmware/devclass" + "github.com/sercanarga/pcileechgen/internal/firmware/fallback" "github.com/sercanarga/pcileechgen/internal/firmware/nvme" "github.com/sercanarga/pcileechgen/internal/firmware/overlay" "github.com/sercanarga/pcileechgen/internal/firmware/scrub" @@ -32,8 +35,21 @@ type OutputWriter struct { Timeout int StockBar bool Force bool + + TimingHistogram *behavior.TimingHistogram + ILADepth int + FallbackConfig *fallback.Config } +// DefaultFallbackConfig seeds new OutputWriters with the class-specific BAR +// register fallbacks (see internal/firmware/fallback). cmd/pcileechgen/build.go +// sets this once, from --fallback-config or the embedded defaults, before the +// vivado.Builder it invokes constructs an OutputWriter. +// ponytail: package-level var, not a threaded field on vivado.BuildOptions/Builder +// (out of scope here) - fine for the CLI's one-build-per-process usage; revisit +// if concurrent in-process builds are ever needed. +var DefaultFallbackConfig *fallback.Config + func NewOutputWriter(outputDir, libDir string, jobs, timeout int) *OutputWriter { if jobs <= 0 { jobs = 4 @@ -42,10 +58,11 @@ func NewOutputWriter(outputDir, libDir string, jobs, timeout int) *OutputWriter timeout = 3600 } return &OutputWriter{ - OutputDir: outputDir, - LibDir: libDir, - Jobs: jobs, - Timeout: timeout, + OutputDir: outputDir, + LibDir: libDir, + Jobs: jobs, + Timeout: timeout, + FallbackConfig: DefaultFallbackConfig, } } @@ -67,11 +84,15 @@ func (ow *OutputWriter) WriteAll(ctx *donor.DeviceContext, b *board.Board) error return err } + if err := ow.writeBARBehaviorProfile(ctx); err != nil { + return err + } + if err := ow.writeTCLScripts(ctx, b); err != nil { return err } - if err := ow.patchSVSources(ctx, b, ids); err != nil { + if err := ow.patchSVSources(b, ids); err != nil { return fmt.Errorf("SV patching failed: %w", err) } @@ -90,6 +111,8 @@ func (ow *OutputWriter) WriteAll(ctx *donor.DeviceContext, b *board.Board) error } } + ow.writeCode10Report(ctx, b, ids) + ow.writeManifest(ctx, ids) return nil } @@ -136,6 +159,10 @@ func (ow *OutputWriter) writeConfigSpaceArtifacts(ctx *donor.DeviceContext, scru codegen.GenerateWritemaskCOE(scrubbedCS)); err != nil { return fmt.Errorf("failed to write writemask COE: %w", err) } + if err := ow.writeFile("pcileech_cfgspace_w1cmask.coe", + codegen.GenerateW1CMaskCOE(scrubbedCS)); err != nil { + return fmt.Errorf("failed to write W1C mask COE: %w", err) + } msixTableSize := 0 if ctx.MSIXData != nil && ctx.MSIXData.TableSize > 0 { @@ -143,6 +170,10 @@ func (ow *OutputWriter) writeConfigSpaceArtifacts(ctx *donor.DeviceContext, scru } bar0Size := firmware.CappedBAR0Size(ctx, b, msixTableSize) + if results := fallback.Apply(ow.FallbackConfig, ctx.Device.ClassCode, ctx.BARContents); len(results) > 0 { + slog.Info("fallback defaults applied", "modifications", len(results)) + } + scrub.ScrubBarContent(ctx.BARContents, ctx.Device.ClassCode, ctx.Device.VendorID, bar0Size) if err := ow.writeFile("pcileech_bar_zero4k.coe", codegen.GenerateBarContentCOE(ctx.BARContents, firmware.CappedBAR0Size(ctx, b, msixTableSize))); err != nil { @@ -154,13 +185,18 @@ func (ow *OutputWriter) writeConfigSpaceArtifacts(ctx *donor.DeviceContext, scru // writeTCLScripts generates Vivado project and build TCL scripts. func (ow *OutputWriter) writeTCLScripts(ctx *donor.DeviceContext, b *board.Board) error { if err := ow.writeFile("vivado_generate_project.tcl", - tclgen.GenerateProjectTCL(ctx, b, ow.LibDir, ow.StockBar)); err != nil { + tclgen.GenerateProjectTCL(ctx, b, ow.LibDir, ow.StockBar, ow.ILADepth)); err != nil { return fmt.Errorf("failed to write project TCL: %w", err) } if err := ow.writeFile("vivado_build.tcl", tclgen.GenerateBuildTCL(b, ow.Jobs, ow.Timeout)); err != nil { return fmt.Errorf("failed to write build TCL: %w", err) } + if ow.ILADepth > 0 { + if err := ow.writeFile("ila_debug.txt", firmware.ILADebugDoc()); err != nil { + return fmt.Errorf("failed to write ILA debug doc: %w", err) + } + } return nil } @@ -200,7 +236,7 @@ var barControllerSubModules = []string{ // patchSVSources copies the board's SV tree (excluding files that will // be regenerated), and patches donor IDs into the remaining sources. -func (ow *OutputWriter) patchSVSources(ctx *donor.DeviceContext, b *board.Board, ids firmware.DeviceIDs) error { +func (ow *OutputWriter) patchSVSources(b *board.Board, ids firmware.DeviceIDs) error { srcDir := b.SrcPath(ow.LibDir) dstDir := filepath.Join(ow.OutputDir, "src") @@ -233,7 +269,9 @@ func (ow *OutputWriter) patchSVSources(ctx *donor.DeviceContext, b *board.Board, srcFile := filepath.Join(srcDir, "pcileech_tlps128_bar_controller.sv") dstFile := filepath.Join(dstDir, "pcileech_tlps128_bar_controller.sv") if data, err := os.ReadFile(srcFile); err == nil { - os.WriteFile(dstFile, data, 0644) + if writeErr := os.WriteFile(dstFile, data, 0644); writeErr != nil { + return fmt.Errorf("failed to copy stock BAR controller: %w", writeErr) + } } } @@ -328,7 +366,9 @@ func ListOutputFiles() []string { "device_context.json", "pcileech_cfgspace.coe", "pcileech_cfgspace_writemask.coe", + "pcileech_cfgspace_w1cmask.coe", "pcileech_bar_zero4k.coe", + "bar_behavior_profile.json", "vivado_generate_project.tcl", "vivado_build.tcl", "src/", @@ -343,6 +383,8 @@ func ListOutputFiles() []string { "config_space_init.hex", "msix_table_init.hex", "scrub_diff_report.txt", + "code10_report.txt", + "ila_debug.txt", "build_manifest.json", } } @@ -423,6 +465,20 @@ func extractMSIInfo(cs *pci.ConfigSpace) *svgen.MSIConfig { return nil } +// extraBARPresence reports, for BAR indices 3-6 (result index 0=BAR3 ... +// 3=BAR6), whether the donor's real hardware has a populated (nonzero-size) +// BAR there. Used by bar_controller.sv.tmpl to present a real aperture +// instead of pcileech_bar_impl_none for donors with genuine extra BARs. +func extraBARPresence(bars []pci.BAR) [4]bool { + var present [4]bool + for _, bar := range bars { + if idx := bar.Index - 3; idx >= 0 && idx < 4 && !bar.IsDisabled() { + present[idx] = true + } + } + return present +} + // buildSVConfig assembles the SVGeneratorConfig from donor context. func (ow *OutputWriter) buildSVConfig(ctx *donor.DeviceContext, scrubbedCS *pci.ConfigSpace, ids firmware.DeviceIDs, entropy uint32, b *board.Board) (*svgen.SVGeneratorConfig, error) { // Use the same BAR index for content data and probe profile to avoid @@ -444,7 +500,12 @@ func (ow *OutputWriter) buildSVConfig(ctx *donor.DeviceContext, scrubbedCS *pci. bm := barmodel.BuildBARModel(barData, ctx.Device.ClassCode, barProfile) slog.Info("BAR model built", "model_nil", bm == nil, - "reg_count", func() int { if bm != nil { return len(bm.Registers) }; return 0 }(), + "reg_count", func() int { + if bm != nil { + return len(bm.Registers) + } + return 0 + }(), ) strategy := devclass.StrategyForClassAndVendor(ctx.Device.ClassCode, ids.VendorID) @@ -468,15 +529,21 @@ func (ow *OutputWriter) buildSVConfig(ctx *donor.DeviceContext, scrubbedCS *pci. } cfg := &svgen.SVGeneratorConfig{ - DeviceIDs: ids, - BARModel: bm, - ClassCode: ctx.Device.ClassCode, - LatencyConfig: svgen.DefaultLatencyConfig(ctx.Device.ClassCode), - HasMSIX: bm != nil, - BuildEntropy: entropy, - PRNGSeeds: svgen.BuildPRNGSeeds(ids.VendorID, ids.DeviceID, entropy), - DeviceClass: devClass, - Bar0Size: bar0Size, + DeviceIDs: ids, + DonorCapabilities: extractDonorCapabilities(scrubbedCS), + BARModel: bm, + ClassCode: ctx.Device.ClassCode, + LatencyConfig: svgen.LatencyConfigFromHistogram(ow.TimingHistogram, ctx.Device.ClassCode), + HasMSIX: bm != nil, + BuildEntropy: entropy, + PRNGSeeds: svgen.BuildPRNGSeeds(ids.VendorID, ids.DeviceID, entropy), + DeviceClass: devClass, + Bar0Size: bar0Size, + ExtraBARPresent: extraBARPresence(ctx.BARs), + } + + if ow.ILADepth > 0 { + cfg.ILAInstanceSV = firmware.ILAInstanceSV() } if devClass == devclass.ClassNVMe { @@ -516,11 +583,15 @@ func (ow *OutputWriter) buildSVConfig(ctx *donor.DeviceContext, scrubbedCS *pci. // Validate *donor demand* (may exceed) against board BRAM; error unless --force. // (bar0Size is the Capped value actually used for artifacts/scrub/SV.) if issues := ValidateBARSize(donorBar, bram, 0); len(issues) > 0 { - if !ow.Force { return nil, fmt.Errorf("%s", issues[0]) } + if !ow.Force { + return nil, fmt.Errorf("%s", issues[0]) + } } if cfg.MSIXConfig != nil { if issues := ValidateBARSize(donorBar, bram, cfg.MSIXConfig.TableOffset); len(issues) > 0 { - if !ow.Force { return nil, fmt.Errorf("%s", issues[0]) } + if !ow.Force { + return nil, fmt.Errorf("%s", issues[0]) + } } } @@ -571,8 +642,8 @@ func (ow *OutputWriter) writeConditionalArtifacts(cfg *svgen.SVGeneratorConfig, if err != nil { return fmt.Errorf("generating pcileech_nvme_admin_responder.sv: %w", err) } - if err := ow.writeFile("pcileech_nvme_admin_responder.sv", nvmeSV); err != nil { - return err + if writeErr := ow.writeFile("pcileech_nvme_admin_responder.sv", nvmeSV); writeErr != nil { + return writeErr } bridgeSV, err := svgen.GenerateNVMeDMABridgeSV(cfg) @@ -588,13 +659,33 @@ func (ow *OutputWriter) writeConditionalArtifacts(cfg *svgen.SVGeneratorConfig, } } + if cfg.DeviceClass == devclass.ClassSATA { + model := "PCILeech SATA Disk" + serial := fmt.Sprintf("PL%012X", cfg.DeviceIDs.DSN&0xFFFFFFFFFFFF) + const sataSectors = uint64(0x1D1C0000) // ~250 GB at 512 B/sector + idw := ahci.BuildIdentify(model, serial, "1.0", sataSectors) + if err := ow.writeFile("ahci_identify_init.hex", ahci.IdentifyHex(idw)); err != nil { + return err + } + } + + if cfg.DeviceClass == devclass.ClassXHCI && cfg.BARModel != nil { + xhciSV, err := svgen.GenerateXHCIRingEngineSV(cfg) + if err != nil { + return fmt.Errorf("generating pcileech_xhci_ring_engine.sv: %w", err) + } + if err := ow.writeFile("pcileech_xhci_ring_engine.sv", xhciSV); err != nil { + return err + } + } + if cfg.DeviceClass == devclass.ClassAudio && cfg.BARModel != nil { hdaSV, err := svgen.GenerateHDARIRBDMASV(cfg) if err != nil { return fmt.Errorf("generating pcileech_hda_rirb_dma.sv: %w", err) } - if err := ow.writeFile("pcileech_hda_rirb_dma.sv", hdaSV); err != nil { - return err + if writeErr := ow.writeFile("pcileech_hda_rirb_dma.sv", hdaSV); writeErr != nil { + return writeErr } // MSI interrupt generator for HDA - critical for driver completion. @@ -622,6 +713,9 @@ func (ow *OutputWriter) logSVSummary(cfg *svgen.SVGeneratorConfig) { } case devclass.ClassXHCI: features = append(features, "xHCI FSM") + if cfg.BARModel != nil { + features = append(features, "xHCI Ring Engine", "xHCI DMA Bridge") + } case devclass.ClassAudio: features = append(features, "HD Audio FSM", "RIRB DMA Bridge") if cfg.MSIConfig != nil { @@ -636,6 +730,18 @@ func (ow *OutputWriter) logSVSummary(cfg *svgen.SVGeneratorConfig) { } else { features = append(features, "BRAM fallback") } + if cfg.DonorCapabilities.HasPMCap { + features = append(features, "donor PM cap") + } + if cfg.DonorCapabilities.HasMSICap { + features = append(features, "donor MSI cap") + } + if cfg.DonorCapabilities.HasMSIXCap { + features = append(features, "donor MSI-X cap") + } + if cfg.DonorCapabilities.HasPCIeCap { + features = append(features, "donor PCIe cap") + } features = append(features, "latency emulator", "interrupt controller") slog.Info("SV modules generated", "features", strings.Join(features, ", ")) } diff --git a/internal/firmware/output/writer_test.go b/internal/firmware/output/writer_test.go index 2f4600c..4ef1d15 100644 --- a/internal/firmware/output/writer_test.go +++ b/internal/firmware/output/writer_test.go @@ -1,10 +1,15 @@ package output import ( + "encoding/json" "os" "path/filepath" "strings" "testing" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware/barprofile" + "github.com/sercanarga/pcileechgen/internal/pci" ) func TestNewOutputWriter_Defaults(t *testing.T) { @@ -73,3 +78,53 @@ func TestListOutputFiles(t *testing.T) { } } } + +func TestWriteBARBehaviorProfile_writesProfileArtifact(t *testing.T) { + tmpDir := t.TempDir() + ow := NewOutputWriter(tmpDir, "lib/pcileech-fpga", 4, 3600) + ctx := &donor.DeviceContext{ + Device: pci.PCIDevice{ClassCode: 0x020000}, + BARs: []pci.BAR{ + {Index: 2, Size: 4, Type: pci.BARTypeMem32}, + }, + BARContents: map[int][]byte{ + 2: {0x11, 0x22, 0x33, 0x44}, + }, + } + + if err := ow.writeBARBehaviorProfile(ctx); err != nil { + t.Fatalf("writeBARBehaviorProfile failed: %v", err) + } + + data, err := os.ReadFile(filepath.Join(tmpDir, "bar_behavior_profile.json")) + if err != nil { + t.Fatalf("ReadFile failed: %v", err) + } + var profile barprofile.Profile + if err := json.Unmarshal(data, &profile); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + if profile.ClassCode != 0x020000 { + t.Fatalf("ClassCode = 0x%06X, want 0x020000", profile.ClassCode) + } + if len(profile.BARs) != 1 || profile.BARs[0].Index != 2 { + t.Fatalf("unexpected BAR profile: %+v", profile.BARs) + } +} + +func TestExtraBARPresence(t *testing.T) { + bars := []pci.BAR{ + {Index: 0, Size: 4096, Type: pci.BARTypeMem32}, + {Index: 3, Size: 65536, Type: pci.BARTypeMem32}, // real, populated + {Index: 4, Size: 0, Type: pci.BARTypeDisabled}, // genuinely absent + {Index: 5, Size: 256, Type: pci.BARTypeIO}, // real, populated + // BAR6 not present at all in the donor's list. + } + + present := extraBARPresence(bars) + + want := [4]bool{true, false, true, false} // BAR3, BAR4, BAR5, BAR6 + if present != want { + t.Errorf("extraBARPresence() = %v, want %v", present, want) + } +} diff --git a/internal/firmware/scrub/bar.go b/internal/firmware/scrub/bar.go index a736af3..ff09da2 100644 --- a/internal/firmware/scrub/bar.go +++ b/internal/firmware/scrub/bar.go @@ -1,6 +1,8 @@ package scrub import ( + "math/bits" + "github.com/sercanarga/pcileechgen/internal/firmware" "github.com/sercanarga/pcileechgen/internal/firmware/devclass" "github.com/sercanarga/pcileechgen/internal/util" @@ -29,6 +31,9 @@ func ScrubBarContentWithBRAM(barContents map[int][]byte, classCode uint32, vendo if strategy.DeviceClass() == devclass.ClassXHCI { scrubXHCIBar0(data, bramSize) } + if strategy.DeviceClass() == devclass.ClassSATA { + scrubSATABar0(data, bramSize) + } } // scrubXHCIBar0 clamps xHCI BAR0 registers to fit BRAM and fakes a running controller. @@ -48,8 +53,8 @@ func scrubXHCIBar0(data []byte, bramSize int) { xhciClampScratchpads(data) xhciClampXECP(data, bramSize) - maxSlots = xhciClampDBOFF(data, capLen, maxSlots, bramSize) - maxIntrs = xhciClampRTSOFF(data, capLen, maxIntrs, bramSize) + maxSlots = xhciClampDBOFF(data, maxSlots, bramSize) + maxIntrs = xhciClampRTSOFF(data, maxIntrs, bramSize) maxPorts = xhciClampPorts(capLen, maxPorts, bramSize) // write back clamped HCSPARAMS1 @@ -103,97 +108,45 @@ func xhciClampXECP(data []byte, bramSize int) { } } -// xhciClampDBOFF clamps doorbell offset to fit BRAM, shrinking MaxSlots if needed. -func xhciClampDBOFF(data []byte, capLen, maxSlots, bramSize int) int { - dboff := util.ReadLE32(data, 0x14) & ^uint32(0x03) - doorbellSize := (maxSlots + 1) * 4 - - if int(dboff)+doorbellSize > bramSize { - newDBOFF := bramSize - doorbellSize - if newDBOFF < 0 { - newDBOFF = capLen + 0x20 +// clampCountToFit shrinks claimed to the largest count whose region +// [base, base+claimed*itemSize) fits within bramSize, floored at 1. +func clampCountToFit(base, itemSize, claimed, bramSize int) int { + if base < bramSize { + maxFit := (bramSize - base) / itemSize + if claimed > maxFit { + claimed = maxFit } - newDBOFF = newDBOFF & ^0x1F - if newDBOFF < capLen+0x20 { - // can't fit, shrink MaxSlots - available := bramSize - (capLen + 0x20) - if available < 8 { - available = 8 - } - maxSlots = available/4 - 1 - if maxSlots < 1 { - maxSlots = 1 - } - doorbellSize = (maxSlots + 1) * 4 - newDBOFF = bramSize - doorbellSize - if newDBOFF < 0 { - newDBOFF = capLen + 0x20 - } - newDBOFF = newDBOFF & ^0x1F - } - util.WriteLE32(data, 0x14, uint32(newDBOFF)) } - return maxSlots + if claimed < 1 { + claimed = 1 + } + return claimed } -func xhciClampRTSOFF(data []byte, capLen, maxIntrs, bramSize int) int { - rtsoff := int(util.ReadLE32(data, 0x18) & ^uint32(0x1F)) - - if rtsoff > 0 && maxIntrs > 0 { - remaining := bramSize - rtsoff - 0x20 - if remaining < 0x20 { - remaining = 0x20 - } - maxFit := remaining / 0x20 - if maxFit < 1 { - maxFit = 1 - } - if maxIntrs > maxFit { - maxIntrs = maxFit - } +// xhciClampDBOFF shrinks MaxSlots so the doorbell array fits within BRAM. +// DBOFF itself is never relocated: bar_controller.sv.tmpl and barmodel's +// buildXHCIBARModel hardcode the doorbell array at a fixed offset (see +// devclass/xhci.go's ScrubBAR), so moving it here would desync the SV +// wiring from what the driver was told. +func xhciClampDBOFF(data []byte, maxSlots, bramSize int) int { + dboff := int(util.ReadLE32(data, 0x14) & ^uint32(0x03)) + maxSlots = clampCountToFit(dboff, 4, maxSlots+1, bramSize) - 1 + if maxSlots < 1 { + maxSlots = 1 } - if maxIntrs < 1 { - maxIntrs = 1 - } - - runtimeSize := 0x20 + maxIntrs*0x20 - if rtsoff+runtimeSize > bramSize { - newRTSOFF := capLen + 0x20 - newRTSOFF = (newRTSOFF + 0x1F) & ^0x1F - if newRTSOFF+runtimeSize > bramSize { - newRTSOFF = bramSize - runtimeSize - newRTSOFF = newRTSOFF & ^0x1F - } - rtsoff = newRTSOFF - util.WriteLE32(data, 0x18, uint32(rtsoff)) + return maxSlots +} - remaining := bramSize - rtsoff - 0x20 - if remaining < 0x20 { - remaining = 0x20 - } - maxFit := remaining / 0x20 - if maxFit < 1 { - maxFit = 1 - } - if maxIntrs > maxFit { - maxIntrs = maxFit - } - } - return maxIntrs +// xhciClampRTSOFF shrinks MaxIntrs so the runtime interrupter register sets +// (starting at RTSOFF+0x20) fit within BRAM. RTSOFF itself is never +// relocated, for the same reason DBOFF isn't -- see xhciClampDBOFF. +func xhciClampRTSOFF(data []byte, maxIntrs, bramSize int) int { + rtsoff := int(util.ReadLE32(data, 0x18) & ^uint32(0x1F)) + return clampCountToFit(rtsoff+0x20, 0x20, maxIntrs, bramSize) } func xhciClampPorts(capLen, maxPorts, bramSize int) int { - portBase := capLen + 0x400 - if portBase < bramSize { - maxPortsFit := (bramSize - portBase) / 0x10 - if maxPorts > maxPortsFit { - maxPorts = maxPortsFit - } - } - if maxPorts < 1 { - maxPorts = 1 - } - return maxPorts + return clampCountToFit(capLen+0x400, 0x10, maxPorts, bramSize) } // xhciSetOperationalState makes the controller look like it's running. @@ -218,3 +171,61 @@ func xhciSetOperationalState(data []byte, capLen, maxSlots int) { usbsts &= ^uint32(0x01 | 0x04) util.WriteLE32(data, capLen+4, usbsts) } + +// scrubSATABar0 clamps AHCI PI (Ports Implemented) to fit BRAM and fakes +// "no device present" for any port that fits but has no real donor data. +func scrubSATABar0(data []byte, bramSize int) { + if len(data) < 0x10 { + return + } + + pi := util.ReadLE32(data, 0x0C) + claimedPorts := bits.OnesCount32(pi) + if claimedPorts == 0 { + return + } + + maxPorts := sataClampPorts(claimedPorts, bramSize) + if maxPorts < claimedPorts { + // shrink to only the low N ports (0..maxPorts-1) that fit + pi &= (uint32(1) << uint(maxPorts)) - 1 + util.WriteLE32(data, 0x0C, pi) + } + + // Port 0 always has real profile/donor data. Any other implemented + // port that fits in BRAM but was never actually captured (its PxSSTS + // block is still zero) needs an explicit "no device" state, otherwise + // the host driver sees a phantom drive with garbage register content. + for port := 1; port < 32; port++ { + if pi&(1< len(data) { + break + } + if util.ReadLE32(data, base+0x28) != 0 { + continue // real donor data already shows a device here + } + sataMarkPortEmpty(data, base) + } +} + +// sataClampPorts clamps the AHCI port count to what fits within BRAM. Each +// port block is 0x80 bytes starting at 0x100. +// ponytail: assumes PI is the common contiguous 0..N-1 port bitmask; a +// sparse PI (spec-legal but rare) would need per-bit offset checks instead +// of a popcount-derived port count. +func sataClampPorts(claimedPorts, bramSize int) int { + return clampCountToFit(0x100, 0x80, claimedPorts, bramSize) +} + +// sataMarkPortEmpty writes the canonical "no device" register state for an +// AHCI port block that has no real donor data backing it. +func sataMarkPortEmpty(data []byte, base int) { + util.WriteLE32(data, base+0x28, 0x00000000) // PxSSTS - no device detected + util.WriteLE32(data, base+0x24, 0xFFFFFFFF) // PxSIG - no device signature + tfd := util.ReadLE32(data, base+0x20) + tfd &^= 0x81 // PxTFD - clear BSY/ERR so driver doesn't hang on phantom port + util.WriteLE32(data, base+0x20, tfd) +} diff --git a/internal/firmware/scrub/bar_test.go b/internal/firmware/scrub/bar_test.go index 8a5ce4d..b34c8e8 100644 --- a/internal/firmware/scrub/bar_test.go +++ b/internal/firmware/scrub/bar_test.go @@ -186,7 +186,7 @@ func TestXhciSetOperationalState(t *testing.T) { func TestXhciClampDBOFF_FitsInBRAM(t *testing.T) { data := make([]byte, 4096) util.WriteLE32(data, 0x14, 0x00000400) // DBOFF = 0x400 - slots := xhciClampDBOFF(data, 0x20, 32, 4096) + slots := xhciClampDBOFF(data, 32, 4096) if slots != 32 { t.Errorf("slots should stay 32 when it fits, got %d", slots) } @@ -194,21 +194,23 @@ func TestXhciClampDBOFF_FitsInBRAM(t *testing.T) { func TestXhciClampDBOFF_TooLarge(t *testing.T) { data := make([]byte, 4096) - util.WriteLE32(data, 0x14, 0x00000F00) // DBOFF = 0xF00 - slots := xhciClampDBOFF(data, 0x20, 64, 1024) + util.WriteLE32(data, 0x14, 0x00000100) // canonical DBOFF (see devclass/xhci.go ScrubBAR) + slots := xhciClampDBOFF(data, 64, 300) if slots < 1 { t.Errorf("slots should be >= 1, got %d", slots) } - dboff := util.ReadLE32(data, 0x14) - if int(dboff)+(slots+1)*4 > 1024 { - t.Errorf("DBOFF + doorbell area exceeds BRAM: dboff=0x%X slots=%d bram=1024", dboff, slots) + if slots >= 64 { + t.Errorf("slots should shrink when the doorbell area doesn't fit, got %d", slots) + } + if dboff := util.ReadLE32(data, 0x14); dboff != 0x100 { + t.Errorf("DBOFF must never be relocated, got 0x%X", dboff) } } func TestXhciClampDBOFF_VerySmallBRAM(t *testing.T) { data := make([]byte, 4096) util.WriteLE32(data, 0x14, 0x00000100) - slots := xhciClampDBOFF(data, 0x20, 128, 128) + slots := xhciClampDBOFF(data, 128, 128) if slots < 1 { t.Errorf("slots should be >= 1 even with tiny BRAM, got %d", slots) } @@ -217,7 +219,7 @@ func TestXhciClampDBOFF_VerySmallBRAM(t *testing.T) { func TestXhciClampRTSOFF_FitsInBRAM(t *testing.T) { data := make([]byte, 4096) util.WriteLE32(data, 0x18, 0x00000200) // RTSOFF = 0x200 - intrs := xhciClampRTSOFF(data, 0x20, 8, 4096) + intrs := xhciClampRTSOFF(data, 8, 4096) if intrs != 8 { t.Errorf("intrs should stay 8 when it fits, got %d", intrs) } @@ -225,17 +227,23 @@ func TestXhciClampRTSOFF_FitsInBRAM(t *testing.T) { func TestXhciClampRTSOFF_TooLarge(t *testing.T) { data := make([]byte, 4096) - util.WriteLE32(data, 0x18, 0x00000F00) // RTSOFF = 0xF00 - intrs := xhciClampRTSOFF(data, 0x20, 64, 1024) + util.WriteLE32(data, 0x18, 0x00000200) // canonical RTSOFF (see devclass/xhci.go ScrubBAR) + intrs := xhciClampRTSOFF(data, 64, 600) if intrs < 1 { t.Errorf("intrs should be >= 1, got %d", intrs) } + if intrs >= 64 { + t.Errorf("intrs should shrink when the runtime area doesn't fit, got %d", intrs) + } + if rtsoff := util.ReadLE32(data, 0x18); rtsoff != 0x200 { + t.Errorf("RTSOFF must never be relocated, got 0x%X", rtsoff) + } } func TestXhciClampRTSOFF_ZeroIntrs(t *testing.T) { data := make([]byte, 4096) util.WriteLE32(data, 0x18, 0x00000200) - intrs := xhciClampRTSOFF(data, 0x20, 0, 4096) + intrs := xhciClampRTSOFF(data, 0, 4096) if intrs < 1 { t.Errorf("zero intrs should clamp to >= 1, got %d", intrs) } diff --git a/internal/firmware/scrub/passes.go b/internal/firmware/scrub/passes.go index 1fc44b8..ab27049 100644 --- a/internal/firmware/scrub/passes.go +++ b/internal/firmware/scrub/passes.go @@ -69,10 +69,10 @@ func (p *scrubPMCapPass) Apply(cs *pci.ConfigSpace, b *board.Board, om *overlay. // PMCSR (cap+4): force D0, NoSoftReset, clear PME_Status + PME_Enable pmcsr := cs.ReadU16(cap.Offset + 4) - pmcsr &= 0xFFFC // bits [1:0] = 00 (D0) - pmcsr &= ^uint16(1 << 8) // bit 8 = PME_Enable off - pmcsr &= 0x7FFF // bit 15 = PME_Status clear - pmcsr |= 0x0008 // bit 3 = NoSoftReset + pmcsr &= 0xFFFC // bits [1:0] = 00 (D0) + pmcsr &= ^uint16(1 << 8) // bit 8 = PME_Enable off + pmcsr &= 0x7FFF // bit 15 = PME_Status clear + pmcsr |= 0x0008 // bit 3 = NoSoftReset om.WriteU16(cap.Offset+4, pmcsr, "PM: D0, NoSoftReset, PME disabled") } } @@ -190,8 +190,8 @@ func (p *scrubASPMPass) Apply(cs *pci.ConfigSpace, b *board.Board, om *overlay.M // bits 1:0 = ASPM enable, bit 8 = Clock PM enable if cap.Offset+0x10+2 <= pci.ConfigSpaceLegacySize { linkCtl := cs.ReadU16(cap.Offset + 0x10) - linkCtl &= 0xFFFC // bits 1:0 = ASPM enable - linkCtl &= ^uint16(1 << 8) // bit 8 = Enable Clock PM + linkCtl &= 0xFFFC // bits 1:0 = ASPM enable + linkCtl &= ^uint16(1 << 8) // bit 8 = Enable Clock PM om.WriteU16(cap.Offset+0x10, linkCtl, "disable ASPM L0s/L1 + Clock PM") } // clear LTR Mechanism Enable in Device Control 2 (cap+0x28) @@ -229,21 +229,4 @@ type normalizeAERMasksPass struct{} func (p *normalizeAERMasksPass) Name() string { return "normalize AER masks" } func (p *normalizeAERMasksPass) Apply(cs *pci.ConfigSpace, b *board.Board, om *overlay.Map, ctx *ScrubContext) { - if cs.Size < pci.ConfigSpaceSize { - return - } - for _, cap := range ctx.ExtCaps { - if cap.ID != pci.ExtCapIDAER { - continue - } - if cap.Offset+0x0C <= pci.ConfigSpaceSize { - om.WriteU32(cap.Offset+0x08, 0x00462030, "set AER uncorrectable mask (spec defaults)") - } - if cap.Offset+0x18 <= pci.ConfigSpaceSize { - om.WriteU32(cap.Offset+0x14, 0x00002000, "set AER correctable mask (spec defaults)") - } - if cap.Offset+0x10 <= pci.ConfigSpaceSize { - om.WriteU32(cap.Offset+0x0C, 0x0045E011, "set AER uncorrectable severity (CT non-fatal per spec)") - } - } } diff --git a/internal/firmware/scrub/passes_test.go b/internal/firmware/scrub/passes_test.go index 2417db8..0d57558 100644 --- a/internal/firmware/scrub/passes_test.go +++ b/internal/firmware/scrub/passes_test.go @@ -271,6 +271,72 @@ func TestValidateCapChainPass(t *testing.T) { p.Apply(cs, nil, om, ctxFor(cs)) } +func TestNormalizeAERMasksPass_PreservesDonorValues(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + aerHeader := uint32(pci.ExtCapIDAER) | (1 << 16) + cs.WriteU32(0x100, aerHeader) + const donorUncorrMask = uint32(0xDEAD1000) + const donorUncorrSev = uint32(0xCAFE2000) + const donorCorrMask = uint32(0x5A5A3000) + cs.WriteU32(0x108, donorUncorrMask) + cs.WriteU32(0x10C, donorUncorrSev) + cs.WriteU32(0x114, donorCorrMask) + + om := overlay.NewMap(cs) + p := &normalizeAERMasksPass{} + p.Apply(cs, nil, om, ctxFor(cs)) + + if got := cs.ReadU32(0x108); got != donorUncorrMask { + t.Errorf("AER Uncorrectable Mask overwritten: got 0x%08X, want donor 0x%08X", got, donorUncorrMask) + } + if got := cs.ReadU32(0x10C); got != donorUncorrSev { + t.Errorf("AER Uncorrectable Severity overwritten: got 0x%08X, want donor 0x%08X", got, donorUncorrSev) + } + if got := cs.ReadU32(0x114); got != donorCorrMask { + t.Errorf("AER Correctable Mask overwritten: got 0x%08X, want donor 0x%08X", got, donorCorrMask) + } +} + +func TestScrubAERPass_StatusZeroedMasksPreserved(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + aerHeader := uint32(pci.ExtCapIDAER) | (1 << 16) + cs.WriteU32(0x100, aerHeader) + cs.WriteU32(0x104, 0xFFFFFFFF) + cs.WriteU32(0x110, 0xFFFFFFFF) + cs.WriteU32(0x11C, 0xFFFFFFFF) + const donorUncorrMask = uint32(0xABCD0001) + const donorUncorrSev = uint32(0x12340002) + const donorCorrMask = uint32(0x56780003) + cs.WriteU32(0x108, donorUncorrMask) + cs.WriteU32(0x10C, donorUncorrSev) + cs.WriteU32(0x114, donorCorrMask) + + om := overlay.NewMap(cs) + p := &scrubAERPass{} + p.Apply(cs, nil, om, ctxFor(cs)) + + if cs.ReadU32(0x104) != 0 { + t.Error("AER Uncorrectable Status should be zeroed") + } + if cs.ReadU32(0x110) != 0 { + t.Error("AER Correctable Status should be zeroed") + } + if cs.ReadU32(0x11C) != 0 { + t.Error("AER Root Error Status should be zeroed") + } + if got := cs.ReadU32(0x108); got != donorUncorrMask { + t.Errorf("scrubAERPass must not touch Uncorrectable Mask: got 0x%08X, want 0x%08X", got, donorUncorrMask) + } + if got := cs.ReadU32(0x10C); got != donorUncorrSev { + t.Errorf("scrubAERPass must not touch Uncorrectable Severity: got 0x%08X, want 0x%08X", got, donorUncorrSev) + } + if got := cs.ReadU32(0x114); got != donorCorrMask { + t.Errorf("scrubAERPass must not touch Correctable Mask: got 0x%08X, want 0x%08X", got, donorCorrMask) + } +} + // --- Pass name tests --- func TestPassNames(t *testing.T) { diff --git a/internal/firmware/scrub/pcie_inject.go b/internal/firmware/scrub/pcie_inject.go index 5fe1bd1..8c66d46 100644 --- a/internal/firmware/scrub/pcie_inject.go +++ b/internal/firmware/scrub/pcie_inject.go @@ -61,7 +61,7 @@ func findFreeCapSpace(cs *pci.ConfigSpace, caps []pci.Capability, needed int) in func buildPCIeCapData(b *board.Board) [pcieCapSize]byte { var data [pcieCapSize]byte - maxSpeed := uint8(firmware.LinkSpeedGen2) + maxSpeed := firmware.LinkSpeedGen2 maxWidth := uint8(1) if b != nil { maxSpeed = b.MaxLinkSpeedOrDefault() @@ -97,7 +97,7 @@ func buildPCIeCapData(b *board.Board) [pcieCapSize]byte { linkCap := uint32(maxSpeed) | (uint32(maxWidth) << 4) | (6 << 12) | // L0s exit latency - (6 << 15) // L1 exit latency + (6 << 15) // L1 exit latency binary.LittleEndian.PutUint32(data[0x0C:], linkCap) // Link Control (cap+0x10) = 0x0000 (no ASPM) @@ -160,8 +160,7 @@ func injectPCIeCapIfMissing(cs *pci.ConfigSpace, b *board.Board, om *overlay.Map om.WriteU16(0x06, status|0x0010, "set Capabilities List bit in Status") } - // ensure CapPtr is valid when there are no existing caps - if len(ctx.Caps) == 0 && cs.CapabilityPointer() == 0 { + if len(ctx.Caps) == 0 { slog.Info("donor has no capability chain, creating minimal PM + MSI + PCIe chain") injectFullCapChain(cs, b, om, ctx) return diff --git a/internal/firmware/scrub/pruning.go b/internal/firmware/scrub/pruning.go index 01d18fc..984d65c 100644 --- a/internal/firmware/scrub/pruning.go +++ b/internal/firmware/scrub/pruning.go @@ -23,9 +23,9 @@ var unsafeStandardCaps = map[uint8]string{ pci.CapIDEnhancedAlloc: "Enhanced Allocation", pci.CapIDFlatteningPortal: "Flattening Portal", pci.CapIDPCIX: "PCI-X", - pci.CapIDBridgeSubsysVID: "Bridge Subsystem VID", - pci.CapIDSecureDevice: "Secure Device", - pci.CapIDAdvancedFeatures: "Advanced Features", + pci.CapIDBridgeSubsysVID: "Bridge Subsystem VID", + pci.CapIDSecureDevice: "Secure Device", + pci.CapIDAdvancedFeatures: "Advanced Features", } // PruneStandardCaps unlinks unsupported caps and returns what was removed. @@ -52,8 +52,14 @@ func PruneStandardCaps(cs *pci.ConfigSpace, om *overlay.Map) []string { om.WriteU8(prevNextOff, uint8(nextPtr), fmt.Sprintf("prune cap 0x%02X (%s): relink", capID, name)) - om.WriteU8(ptr, 0, "zero pruned cap header") - om.WriteU8(ptr+1, 0, "zero pruned cap next ptr") + size := capSizeAt(cs, capID, ptr) + if nextPtr > ptr { + size = nextPtr - ptr + } + if ptr+size > pci.ConfigSpaceLegacySize { + size = pci.ConfigSpaceLegacySize - ptr + } + om.ZeroRange(ptr, ptr+size, fmt.Sprintf("zero pruned cap body 0x%02X (%s)", capID, name)) removed = append(removed, fmt.Sprintf("%s (0x%02X) at 0x%02X", name, capID, ptr)) } else { diff --git a/internal/firmware/scrub/pruning_test.go b/internal/firmware/scrub/pruning_test.go index 259765e..8ce04d2 100644 --- a/internal/firmware/scrub/pruning_test.go +++ b/internal/firmware/scrub/pruning_test.go @@ -99,3 +99,48 @@ func TestValidateCapChain_OutOfBounds(t *testing.T) { t.Error("expected error for out-of-bounds pointer") } } + +func TestPruneStandardCaps_ZerosPayload(t *testing.T) { + cs := pci.NewConfigSpace() + cs.Data[0x06] = 0x10 + cs.WriteU8(0x34, 0x40) + + cs.WriteU8(0x40, pci.CapIDPowerManagement) + cs.WriteU8(0x41, 0x50) + + cs.WriteU8(0x50, pci.CapIDVPD) + cs.WriteU8(0x51, 0x60) + for i := 0x52; i < 0x60; i++ { + cs.Data[i] = 0xAA + } + + cs.WriteU8(0x60, pci.CapIDPCIExpress) + cs.WriteU8(0x61, 0x00) + cs.Data[0x62] = 0xBB + + cs.Data[0x04] = 0xCC + + om := overlay.NewMap(cs) + removed := PruneStandardCaps(cs, om) + + if len(removed) != 1 { + t.Fatalf("expected 1 removed cap, got %d", len(removed)) + } + + for i := 0x50; i < 0x60; i++ { + if cs.ReadU8(i) != 0 { + t.Errorf("pruned cap byte 0x%02X not zeroed: got 0x%02X", i, cs.ReadU8(i)) + } + } + + if cs.ReadU8(0x60) != pci.CapIDPCIExpress { + t.Errorf("PCIe cap ID at 0x60 corrupted: 0x%02X", cs.ReadU8(0x60)) + } + if cs.Data[0x62] != 0xBB { + t.Errorf("PCIe payload at 0x62 corrupted: 0x%02X", cs.Data[0x62]) + } + + if cs.Data[0x04] != 0xCC { + t.Errorf("standard header byte 0x04 corrupted: 0x%02X", cs.Data[0x04]) + } +} diff --git a/internal/firmware/scrub/quirks.go b/internal/firmware/scrub/quirks.go index 293ac09..efbaf5c 100644 --- a/internal/firmware/scrub/quirks.go +++ b/internal/firmware/scrub/quirks.go @@ -21,6 +21,21 @@ var vendorQuirks = []vendorQuirk{ Name: "Renesas xHCI FW status", Apply: fixRenesasFirmwareStatus, }, + // ponytail: KNOWN_ISSUES also names ASMedia (0x1B21, e.g. ASM1042A/ASM3142) + // and VIA (0x1106, e.g. VL805/VL806) as xHCI Code-10 offenders, but neither + // gets an entry here. Checked upstream drivers/usb/host/xhci-pci.c and + // pci-quirks.c: every real quirk for both vendors (XHCI_NO_64BIT_SUPPORT, + // XHCI_RESET_ON_RESUME, XHCI_ASMEDIA_MODIFY_FLOWCONTROL, XHCI_LPM_SUPPORT, + // XHCI_TRB_OVERFETCH, ...) is a driver-side runtime behavior flag with no + // PCI config-space counterpart - unlike Renesas's FW-status bits, there is no + // static "mark as done" register to poke. The one thing ASMedia does do in + // config space (usb_asmedia_modifyflowcontrol, regs 0xE0/0xF8/0xFC) is a live + // read-poll-write mailbox to real silicon that a static overlay write can't + // reproduce, and it fixes Ethernet-over-USB flow control, not enumeration - + // unrelated to Code 10. VIA VL805's only known FW handshake is an out-of-band + // SPI/SoC-mailbox load (Raspberry Pi's rpi_firmware_init_vl805), nothing in + // PCI config space either. No grounded register write found for either + // vendor - add one here if a real config-space fixup ever surfaces. // add new vendors here } diff --git a/internal/firmware/scrub/scrub_overlay_test.go b/internal/firmware/scrub/scrub_overlay_test.go index 783d0d1..e746ce9 100644 --- a/internal/firmware/scrub/scrub_overlay_test.go +++ b/internal/firmware/scrub/scrub_overlay_test.go @@ -58,8 +58,8 @@ func TestScrubConfigSpaceWithOverlay_BoardAwareLinkSpeed(t *testing.T) { // Capability pointer cs.WriteU8(0x34, 0x40) // PCIe Capability at offset 0x40 - cs.WriteU8(0x40, byte(pci.CapIDPCIExpress)) // PCIe cap ID - cs.WriteU8(0x41, 0x00) // next cap = 0 + cs.WriteU8(0x40, pci.CapIDPCIExpress) // PCIe cap ID + cs.WriteU8(0x41, 0x00) // next cap = 0 // Link Capabilities at cap+0x0C = 0x4C: Gen3, x4 cs.WriteU32(0x4C, 0x00000043) diff --git a/internal/firmware/scrub/scrub_test.go b/internal/firmware/scrub/scrub_test.go index 1fbb815..019d1ed 100644 --- a/internal/firmware/scrub/scrub_test.go +++ b/internal/firmware/scrub/scrub_test.go @@ -58,18 +58,18 @@ func TestScrubConfigSpace(t *testing.T) { } } -func makeExtCapHeader(id uint16, version uint8, nextOffset int) uint32 { - return uint32(id) | uint32(version)<<16 | uint32(nextOffset)<<20 +func makeExtCapHeader(id uint16, nextOffset int) uint32 { + return uint32(id) | 1<<16 | uint32(nextOffset)<<20 } func TestFilterExtCaps_RemoveMiddle(t *testing.T) { cs := pci.NewConfigSpace() cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 1, 0x150)) - cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDSRIOV, 1, 0x200)) - cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDDeviceSerialNumber, 1, 0x250)) - cs.WriteU32(0x250, makeExtCapHeader(pci.ExtCapIDLTR, 1, 0)) + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 0x150)) + cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDSRIOV, 0x200)) + cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDDeviceSerialNumber, 0x250)) + cs.WriteU32(0x250, makeExtCapHeader(pci.ExtCapIDLTR, 0)) removed := FilterExtCapabilities(cs, overlay.NewMap(cs)) @@ -90,8 +90,8 @@ func TestFilterExtCaps_AllRemoved(t *testing.T) { cs := pci.NewConfigSpace() cs.Size = pci.ConfigSpaceSize - cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDSRIOV, 1, 0x150)) - cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDResizableBAR, 1, 0)) + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDSRIOV, 0x150)) + cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDResizableBAR, 0)) removed := FilterExtCapabilities(cs, overlay.NewMap(cs)) if len(removed) != 2 { @@ -478,7 +478,8 @@ func TestRelocateMSIXToBRAM_TableOutside(t *testing.T) { // table offset should be relocated to 0x1000 tableReg := scrubbed.ReadU32(0x94) tableOff := tableReg &^ 0x07 - off := uint32(0x1000); if tableOff != off { + off := uint32(0x1000) + if tableOff != off { t.Errorf("MSI-X table offset should be relocated to 0x1000, got 0x%X", tableOff) } @@ -514,7 +515,8 @@ func TestRelocateMSIXToBRAM_TableInside(t *testing.T) { // table still relocated to 0x1000 (consistent placement) tableReg := scrubbed.ReadU32(0x94) tableOff := tableReg &^ 0x07 - off := uint32(0x1000); if tableOff != off { + off := uint32(0x1000) + if tableOff != off { t.Errorf("MSI-X table should be relocated to 0x1000, got 0x%X", tableOff) } } @@ -724,7 +726,8 @@ func TestRelocateMSIXToBRAM(t *testing.T) { // Table should be relocated to 0x1000 tableReg := cs.ReadU32(0x84) tableOff := tableReg & 0xFFFFFFF8 - off := uint32(0x1000); if tableOff != off { + off := uint32(0x1000) + if tableOff != off { t.Errorf("Table offset = 0x%x, want 0x1000", tableOff) } @@ -763,9 +766,9 @@ func TestSecondaryPCIeNotFiltered(t *testing.T) { cs.Size = pci.ConfigSpaceSize // AER -> SecondaryPCIe -> LTR - cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 1, 0x150)) - cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDSecondaryPCIe, 1, 0x200)) - cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDLTR, 1, 0)) + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 0x150)) + cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDSecondaryPCIe, 0x200)) + cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDLTR, 0)) removed := FilterExtCapabilities(cs, overlay.NewMap(cs)) if len(removed) != 0 { @@ -828,9 +831,9 @@ func TestL1PMSubstatesNotFiltered(t *testing.T) { cs.Size = pci.ConfigSpaceSize // AER -> L1PM -> LTR - cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 1, 0x150)) - cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDL1PMSubstates, 1, 0x200)) - cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDLTR, 1, 0)) + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 0x150)) + cs.WriteU32(0x150, makeExtCapHeader(pci.ExtCapIDL1PMSubstates, 0x200)) + cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDLTR, 0)) removed := FilterExtCapabilities(cs, overlay.NewMap(cs)) if len(removed) != 0 { @@ -908,8 +911,8 @@ func TestASPMFullyDisabledAfterScrub(t *testing.T) { cs.WriteU16(0x68, 0x0400) // L1PM Substates ext cap at 0x200 - cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 1, 0x200)) - cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDL1PMSubstates, 1, 0)) + cs.WriteU32(0x100, makeExtCapHeader(pci.ExtCapIDAER, 0x200)) + cs.WriteU32(0x200, makeExtCapHeader(pci.ExtCapIDL1PMSubstates, 0)) cs.WriteU32(0x204, 0x0000001F) // L1PM Capabilities: L1.1 + L1.2 support cs.WriteU32(0x208, 0x0000000A) // L1PM Control 1: L1.1 + L1.2 enabled cs.WriteU32(0x20C, 0x00000032) // L1PM Control 2 @@ -953,7 +956,11 @@ func TestASPMFullyDisabledAfterScrub(t *testing.T) { } } -func TestComputeBAR0SizeLarge(t *testing.T){if got:=ComputeBAR0Size(512,0);got!=32768{t.Errorf("ComputeBAR0Size(large)=%d",got)}} +func TestComputeBAR0SizeLarge(t *testing.T) { + if got := ComputeBAR0Size(512, 0); got != 32768 { + t.Errorf("ComputeBAR0Size(large)=%d", got) + } +} // TestPhisonNVMe64bMSIXScrub simulates the exact scenario for Code10 residual hunt: // NVMe (class 0x010802), vendor 0x1987 Phison, 64-bit BAR (e.g. 16KB size mask), MSIX present, @@ -973,8 +980,8 @@ func TestPhisonNVMe64bMSIXScrub(t *testing.T) { cs.WriteU8(0x0A, 0x08) // Subclass cs.WriteU8(0x0B, 0x01) // Base NVMe class 0108xx cs.WriteU8(0x0C, 0x10) - cs.WriteU8(0x0D, 0x00) // latency - cs.WriteU8(0x0E, 0x00) // header type 0 + cs.WriteU8(0x0D, 0x00) // latency + cs.WriteU8(0x0E, 0x00) // header type 0 cs.WriteU8(0x0F, 0x00) // 64b BAR0 16KB example: size mask FFFFC000 + type bits 0x4 (64b mem) cs.WriteU32(0x10, 0xFFFC0004) @@ -994,7 +1001,7 @@ func TestPhisonNVMe64bMSIXScrub(t *testing.T) { // MSI-X at 0x48 cs.WriteU8(0x48, pci.CapIDMSIX) cs.WriteU8(0x49, 0x60) - cs.WriteU16(0x4A, 0x0003) // 4 vectors, enabled later by relocate + cs.WriteU16(0x4A, 0x0003) // 4 vectors, enabled later by relocate cs.WriteU32(0x4C, 0x00000000) // table off (will relocate) cs.WriteU32(0x50, 0x00000008) // pba @@ -1002,7 +1009,9 @@ func TestPhisonNVMe64bMSIXScrub(t *testing.T) { cs.WriteU8(0x60, pci.CapIDPCIExpress) cs.WriteU8(0x61, 0x00) // fill minimal for cap parse - for i := 0x62; i < 0x60+60; i++ { cs.WriteU8(i, 0) } + for i := 0x62; i < 0x60+60; i++ { + cs.WriteU8(i, 0) + } cs.WriteU16(0x62, 0x0002) // ver2 endpoint // Phison vendor region at 0x40-0x4F (but will be cap-overlapped, knownWhitelist preserves 0x40-0x50 even if not cap) @@ -1059,7 +1068,7 @@ func TestPhisonNVMe64bMSIXScrub(t *testing.T) { // check Phison vendor region preserved (whitelist 0x40-0x50) // since 0x40-0x50 overlaps caps, check a non-cap vendor spot if any; here 0x80 not whitelisted so zeroed ok // but the pass zeroVendor is after caps; verify cmd/status/class intact - if scrubbed.ReadU32(0x08) >> 8 != classCode { + if scrubbed.ReadU32(0x08)>>8 != classCode { t.Error("class at 0x08 not matching") } } diff --git a/internal/firmware/svgen/generator.go b/internal/firmware/svgen/generator.go index a779731..ffeb1c1 100644 --- a/internal/firmware/svgen/generator.go +++ b/internal/firmware/svgen/generator.go @@ -22,6 +22,7 @@ type MSIConfig struct { // SVGeneratorConfig is the input data for all SV template renders. type SVGeneratorConfig struct { DeviceIDs firmware.DeviceIDs + DonorCapabilities DonorCapabilities // donor capability summary for donor-emulation visibility BARModel *barmodel.BARModel // nil = generic fallback (uses BRAM-based zerowrite4k) ClassCode uint32 LatencyConfig *LatencyConfig // TLP response timing (nil = no latency emulator) @@ -34,6 +35,43 @@ type SVGeneratorConfig struct { NVMeIdentify *nvme.IdentifyData // NVMe Identify Controller/Namespace data (nil = no responder) NVMeDoorbellStride uint32 // CAP.DSTRD - doorbell stride (0 = 4B, default) Bar0Size int + ILAInstanceSV string + // ExtraBARPresent flags donor BAR3-6 presence: index 0=BAR3 ... 3=BAR6. + // true = donor's real hardware has a populated (nonzero-size) BAR there, + // so bar_controller.sv.tmpl presents a real (loopaddr) aperture instead + // of pcileech_bar_impl_none. Zero value (all false) preserves the old + // always-none behavior. + ExtraBARPresent [4]bool +} + +// DonorCapabilities summarizes parsed capabilities from donor config space. +// Values are best-effort snapshots used by generated SV for optional emulation +// behavior and debugging visibility. +type DonorCapabilities struct { + HasPMCap bool + HasMSICap bool + HasMSIXCap bool + HasPCIeCap bool + PMCapOffset uint16 + MSICapOffset uint16 + MSIXCapOffset uint16 + PCIeCapOffset uint16 + PMESupportMask uint8 + PMEDefault bool + MSIDisable64Bit bool + MSIMultipleMsg uint8 + PCIELinkSpeed uint8 + PCIELinkWidth uint8 + PCIeASPMCap uint8 + PCIeASPMEnable uint8 + HasLTRCap bool + HasL1PMSubstates bool + HasAERCap bool + HasDSNCap bool + AERCapOffset uint16 + LTRCapOffset uint16 + L1PMCapOffset uint16 + DSNCapOffset uint16 } // NVMeSQ0DoorbellOffset returns the byte offset of the SQ0 tail doorbell. @@ -53,6 +91,18 @@ func (c *SVGeneratorConfig) NVMeCQ0DoorbellOffset() uint32 { return dbBase + 1*stride } +func (c *SVGeneratorConfig) NVMeSQ1DoorbellOffset() uint32 { + stride := uint32(4) << c.NVMeDoorbellStride + dbBase := uint32(board.DefaultBRAMSize) + return dbBase + 2*stride +} + +func (c *SVGeneratorConfig) NVMeCQ1DoorbellOffset() uint32 { + stride := uint32(4) << c.NVMeDoorbellStride + dbBase := uint32(board.DefaultBRAMSize) + return dbBase + 3*stride +} + func renderTemplate(name string, data any) (string, error) { tmplStr := mustReadTemplate(name + ".sv.tmpl") tmpl, err := template.New(name).Funcs(svFuncMap()).Parse(tmplStr) @@ -95,6 +145,12 @@ func GenerateNVMeDMABridgeSV(cfg *SVGeneratorConfig) (string, error) { return renderTemplate("nvme_dma_bridge", cfg) } +// GenerateXHCIRingEngineSV renders the xHCI Command/Event ring engine and its +// DMA bridge (both modules live in the same template file). +func GenerateXHCIRingEngineSV(cfg *SVGeneratorConfig) (string, error) { + return renderTemplate("xhci_ring_engine", cfg) +} + // GenerateHDARIRBDMASV renders the HDA RIRB DMA bridge module. func GenerateHDARIRBDMASV(cfg *SVGeneratorConfig) (string, error) { return renderTemplate("hda_rirb_dma", cfg) @@ -117,6 +173,7 @@ func svFuncMap() template.FuncMap { "hex04": func(v uint16) string { return fmt.Sprintf("%04X", v) }, "hex02": func(v uint8) string { return fmt.Sprintf("%02X", v) }, "sub": func(a, b int) int { return a - b }, + "add": func(a, b int) int { return a + b }, "mul": func(a, b int) int { return a * b }, "alignedOffset": func(off uint32) uint32 { return (off / 4) * 4 }, "classBase": func(cc uint32) uint8 { return uint8((cc >> 16) & 0xFF) }, @@ -130,6 +187,14 @@ func svFuncMap() template.FuncMap { uint8((mask >> 24) & 0xFF), } }, + "w1cMaskBytes": func(mask uint32) [4]uint8 { + return [4]uint8{ + uint8(mask & 0xFF), + uint8((mask >> 8) & 0xFF), + uint8((mask >> 16) & 0xFF), + uint8((mask >> 24) & 0xFF), + } + }, "cdfVal": func(cdf [16]uint8, i int) uint8 { if i < 0 || i > 15 { return 0 diff --git a/internal/firmware/svgen/generic_w1c_test.go b/internal/firmware/svgen/generic_w1c_test.go new file mode 100644 index 0000000..eb11105 --- /dev/null +++ b/internal/firmware/svgen/generic_w1c_test.go @@ -0,0 +1,106 @@ +package svgen_test + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/barmodel" + "github.com/sercanarga/pcileechgen/internal/firmware/svgen" +) + +// A register carrying W1CMask (but not IsRW1C/IsFSMDriven) is emitted by the +// generic write case with a per-byte blend: RW bits take wr_data, W1C bits clear +// on write-of-1, RO bits hold. +func TestGenericW1CEmitter_BlendedExpression(t *testing.T) { + cfg := &svgen.SVGeneratorConfig{ + DeviceIDs: firmware.DeviceIDs{VendorID: 0x1234, DeviceID: 0x5678, RevisionID: 0x01}, + DeviceClass: "", + PRNGSeeds: [4]uint32{1, 2, 3, 4}, + BARModel: &barmodel.BARModel{ + Size: 4096, + Registers: []barmodel.BARRegister{ + {Offset: 0x10, Width: 4, Name: "STATUS_CTL", Reset: 0, RWMask: 0x00000003, W1CMask: 0x00000100}, + {Offset: 0x14, Width: 4, Name: "CTL2", Reset: 0, RWMask: 0x000000FF}, + }, + }, + } + sv, err := svgen.GenerateBarImplDeviceSV(cfg) + if err != nil { + t.Fatalf("generate SV: %v", err) + } + if !strings.Contains(sv, "32'h00000010: begin") { + t.Error("W1C register 0x10 should be in the generic write case") + } + if !strings.Contains(sv, "reg_0x00000010[7:0] <= ((reg_0x00000010[7:0]") { + t.Error("W1C register byte0 should use the blended expression") + } + if !strings.Contains(sv, "~wr_data[15:8]") { + t.Error("W1C byte1 should contain a clear-on-write-1 term") + } + if !strings.Contains(sv, "& ~wr_data[15:8] & 8'h01") { + t.Error("W1C byte1 clear term should mask with 8'h01") + } + if !strings.Contains(sv, "(wr_data[7:0] & 8'h03)") { + t.Error("W1C register byte0 RW bits should blend wr_data & 8'h03") + } + if !strings.Contains(sv, "32'h00000014: begin") { + t.Error("plain RW register 0x14 should be in the generic write case") + } + if strings.Contains(sv, "reg_0x00000014[7:0] <= ((reg_0x00000014[7:0]") { + t.Error("plain RW register must not use the blended expression") + } +} + +// A synthesized device-driven register (xHCI USBCMD) stays out of the generic +// write case so it is not driven twice. +func TestSynthesizedXHCI_FSMRegExcludedFromGenericWriteCase(t *testing.T) { + profile := &donor.BARProfile{Size: 4096, Probes: []donor.BARProbeResult{ + {Offset: 0x20, Original: 0x00000001, RWMask: 0x00002F0E}, + }} + model := barmodel.SynthesizeBARModel(profile, 0x0C0330) + if model == nil { + t.Fatal("model nil") + } + cfg := &svgen.SVGeneratorConfig{ + DeviceIDs: firmware.DeviceIDs{VendorID: 0x8086, DeviceID: 0x1E31, RevisionID: 0x04}, + DeviceClass: "xhci", + PRNGSeeds: [4]uint32{1, 2, 3, 4}, + BARModel: model, + } + sv, err := svgen.GenerateBarImplDeviceSV(cfg) + if err != nil { + t.Fatalf("generate: %v", err) + } + if strings.Contains(sv, "32'h00000020: begin") { + t.Error("USBCMD must not appear in the generic write case") + } +} + +func TestAudioHandCodedW1CHandlers_RenderFromRealModel(t *testing.T) { + model := barmodel.BuildBARModel(make([]byte, 4096), 0x040300, nil) + if model == nil { + t.Fatal("audio model nil") + } + cfg := &svgen.SVGeneratorConfig{ + DeviceIDs: firmware.DeviceIDs{VendorID: 0x8086, DeviceID: 0x1A98, RevisionID: 0x00}, + DeviceClass: "audio", + PRNGSeeds: [4]uint32{1, 2, 3, 4}, + BARModel: model, + } + sv, err := svgen.GenerateBarImplDeviceSV(cfg) + if err != nil { + t.Fatalf("generate: %v", err) + } + for _, pattern := range []string{ + "reg_0x0000000C[23:16] <= reg_0x0000000C[23:16] & ~wr_data[23:16]", + "reg_0x0000000C[31:24] <= reg_0x0000000C[31:24] & ~wr_data[31:24]", + "intfl_clear_req", + "reg_0x00000060[0]", + } { + if !strings.Contains(sv, pattern) { + t.Errorf("expected audio W1C pattern %q", pattern) + } + } +} diff --git a/internal/firmware/svgen/ila_render_test.go b/internal/firmware/svgen/ila_render_test.go new file mode 100644 index 0000000..c7b305a --- /dev/null +++ b/internal/firmware/svgen/ila_render_test.go @@ -0,0 +1,36 @@ +package svgen_test + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/firmware" + "github.com/sercanarga/pcileechgen/internal/firmware/svgen" +) + +func TestGenerateBarControllerSV_ILAInstance(t *testing.T) { + base := &svgen.SVGeneratorConfig{ + DeviceIDs: firmware.DeviceIDs{VendorID: 0x1102, DeviceID: 0x0012}, + BARModel: hdaTestBARModel(), + DeviceClass: "audio", + ClassCode: 0x040300, + PRNGSeeds: [4]uint32{1, 2, 3, 4}, + } + + without, err := svgen.GenerateBarControllerSV(base) + if err != nil { + t.Fatalf("render without ILA: %v", err) + } + if strings.Contains(without, "u_ila_dbg") { + t.Error("controller should not contain ILA instance when ILAInstanceSV is empty") + } + + base.ILAInstanceSV = firmware.ILAInstanceSV() + with, err := svgen.GenerateBarControllerSV(base) + if err != nil { + t.Fatalf("render with ILA: %v", err) + } + if !strings.Contains(with, "ila_0 u_ila_dbg") || !strings.Contains(with, ".probe0(wr_addr)") { + t.Errorf("controller missing ILA instance:\n%s", with[len(with)-400:]) + } +} diff --git a/internal/firmware/svgen/nvme_controller_test.go b/internal/firmware/svgen/nvme_controller_test.go new file mode 100644 index 0000000..4836ce0 --- /dev/null +++ b/internal/firmware/svgen/nvme_controller_test.go @@ -0,0 +1,30 @@ +package svgen + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/firmware/nvme" +) + +func TestGenerateBarControllerSV_WiresNVMeQID1Doorbells(t *testing.T) { + cfg := testConfig() + cfg.NVMeIdentify = &nvme.IdentifyData{} + cfg.NVMeDoorbellStride = 0 + + result, err := GenerateBarControllerSV(cfg) + if err != nil { + t.Fatalf("GenerateBarControllerSV failed: %v", err) + } + + for _, want := range []string{ + "wire nvme_sq1_db_wr", + "wire nvme_cq1_db_wr", + ".sq1_doorbell_wr", + ".cq1_doorbell_wr", + } { + if !strings.Contains(result, want) { + t.Fatalf("NVMe bar controller should contain %q", want) + } + } +} diff --git a/internal/firmware/svgen/nvme_doorbell_test.go b/internal/firmware/svgen/nvme_doorbell_test.go new file mode 100644 index 0000000..190e030 --- /dev/null +++ b/internal/firmware/svgen/nvme_doorbell_test.go @@ -0,0 +1,22 @@ +package svgen + +import "testing" + +func TestNVMeDoorbellOffsets_QID1(t *testing.T) { + cfg := &SVGeneratorConfig{NVMeDoorbellStride: 0} + + if got := cfg.NVMeSQ1DoorbellOffset(); got != 0x1008 { + t.Errorf("SQ1 doorbell = 0x%X, want 0x1008", got) + } + if got := cfg.NVMeCQ1DoorbellOffset(); got != 0x100C { + t.Errorf("CQ1 doorbell = 0x%X, want 0x100C", got) + } + + cfg.NVMeDoorbellStride = 1 + if got := cfg.NVMeSQ1DoorbellOffset(); got != 0x1010 { + t.Errorf("SQ1 doorbell (stride=1) = 0x%X, want 0x1010", got) + } + if got := cfg.NVMeCQ1DoorbellOffset(); got != 0x1018 { + t.Errorf("CQ1 doorbell (stride=1) = 0x%X, want 0x1018", got) + } +} diff --git a/internal/firmware/svgen/nvme_responder_test.go b/internal/firmware/svgen/nvme_responder_test.go new file mode 100644 index 0000000..3ffdc48 --- /dev/null +++ b/internal/firmware/svgen/nvme_responder_test.go @@ -0,0 +1,49 @@ +package svgen + +import ( + "strings" + "testing" +) + +func TestGenerateNVMeResponderSV_UsesPRP2ForPageCrossingAdminData(t *testing.T) { + cfg := testConfig() + + result, err := GenerateNVMeResponderSV(cfg) + if err != nil { + t.Fatalf("GenerateNVMeResponderSV failed: %v", err) + } + + if !strings.Contains(result, "function [63:0] cmd_prp_addr") { + t.Fatal("NVMe responder should compute admin data addresses through a PRP helper") + } + if !strings.Contains(result, "{cmd_prp2[63:12], 12'h000}") { + t.Fatal("NVMe responder should use PRP2 for admin data crossing the first PRP page") + } + if strings.Contains(result, "dma_wr_addr <= cmd_prp1 +") { + t.Fatal("NVMe responder should not write all admin data relative to PRP1 only") + } +} + +func TestGenerateNVMeResponderSV_HandlesFormattingIOQueuePath(t *testing.T) { + cfg := testConfig() + + result, err := GenerateNVMeResponderSV(cfg) + if err != nil { + t.Fatalf("GenerateNVMeResponderSV failed: %v", err) + } + + for _, want := range []string{ + "input sq1_doorbell_wr", + "iosq_base", + "iocq_base", + "8'h80: begin", + "8'h00, 8'h08, 8'h09: begin", + "S_EXEC_READ_ZERO", + "S_EXEC_WRITE_STORE", + "nvme_store[io_store_addr]", + } { + if !strings.Contains(result, want) { + t.Fatalf("NVMe responder should contain %q", want) + } + } +} diff --git a/internal/firmware/svgen/svgen_test.go b/internal/firmware/svgen/svgen_test.go index 0e66ec9..bc558aa 100644 --- a/internal/firmware/svgen/svgen_test.go +++ b/internal/firmware/svgen/svgen_test.go @@ -1,6 +1,7 @@ package svgen import ( + "fmt" "strings" "testing" @@ -92,6 +93,49 @@ func TestGenerateBarControllerSV(t *testing.T) { } } +func TestGenerateBarControllerSV_ExtraBARs(t *testing.T) { + cfg := testConfig() + cfg.LatencyConfig = DefaultLatencyConfig(cfg.ClassCode) + cfg.ExtraBARPresent = [4]bool{true, false, true, false} // BAR3, BAR4, BAR5, BAR6 + result, err := GenerateBarControllerSV(cfg) + if err != nil { + t.Fatalf("GenerateBarControllerSV failed: %v", err) + } + if !strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar3(") { + t.Error("populated donor BAR3 should get a real loopaddr aperture") + } + if !strings.Contains(result, "pcileech_bar_impl_none i_bar4_none(") { + t.Error("absent donor BAR4 should stay unimplemented") + } + if !strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar5(") { + t.Error("populated donor BAR5 should get a real loopaddr aperture") + } + if !strings.Contains(result, "pcileech_bar_impl_none i_bar6_none(") { + t.Error("absent donor BAR6 should stay unimplemented") + } +} + +func TestGenerateBarControllerSV_NoExtraBARs(t *testing.T) { + cfg := testConfig() + cfg.LatencyConfig = DefaultLatencyConfig(cfg.ClassCode) + // zero-value ExtraBARs: no donor BAR3-6 populated, matches prior behavior. + result, err := GenerateBarControllerSV(cfg) + if err != nil { + t.Fatalf("GenerateBarControllerSV failed: %v", err) + } + if strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar3(") || + strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar4(") || + strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar5(") || + strings.Contains(result, "pcileech_bar_impl_loopaddr i_bar6(") { + t.Error("no donor BAR3-6 populated: should not instantiate any real apertures") + } + for i := 3; i <= 6; i++ { + if !strings.Contains(result, fmt.Sprintf("pcileech_bar_impl_none i_bar%d_none(", i)) { + t.Errorf("BAR%d should stay unimplemented when absent from donor", i) + } + } +} + func TestGenerateLatencyEmulatorSV(t *testing.T) { cfg := testConfig() cfg.LatencyConfig = DefaultLatencyConfig(cfg.ClassCode) diff --git a/internal/firmware/svgen/templates/bar_controller.sv.tmpl b/internal/firmware/svgen/templates/bar_controller.sv.tmpl index 0d23186..1ac9609 100644 --- a/internal/firmware/svgen/templates/bar_controller.sv.tmpl +++ b/internal/firmware/svgen/templates/bar_controller.sv.tmpl @@ -306,6 +306,16 @@ module pcileech_tlps128_bar_controller( wire [31:0] nvme_reg_csts; {{ end }} +{{ if and (eq .DeviceClass "xhci") .BARModel }} + // xHCI register wires -- from bar_impl_device to the ring engine + wire [31:0] xhci_reg_crcr_lo; + wire [31:0] xhci_reg_crcr_hi; + wire [31:0] xhci_reg_erstba_lo; + wire [31:0] xhci_reg_erstba_hi; + wire xhci_reg_iman_ie; + wire xhci_cmd_complete_pulse; +{{ end }} + {{ if .BARModel }} pcileech_bar_impl_device #( .BAR_SIZE ( {{ .BARModel.Size }} ) // var Bar0Size @@ -357,6 +367,13 @@ module pcileech_tlps128_bar_controller( .msi_trigger ( hda_msi_trigger ), .rirbintsts ( hda_rirbintsts ), .crst_falling ( hda_crst_falling ) +{{ end }}{{ if and (eq .DeviceClass "xhci") .BARModel }} , + .xhci_crcr_lo ( xhci_reg_crcr_lo ), + .xhci_crcr_hi ( xhci_reg_crcr_hi ), + .xhci_erstba_lo ( xhci_reg_erstba_lo ), + .xhci_erstba_hi ( xhci_reg_erstba_hi ), + .xhci_iman_ie ( xhci_reg_iman_ie ), + .xhci_cmd_complete ( xhci_cmd_complete_pulse ) {{ end }} ); @@ -567,6 +584,8 @@ module pcileech_tlps128_bar_controller( // catch doorbell writes -- offset derived from CAP.DSTRD + computed from (variable) Bar0Size wire nvme_sq0_db_wr = wr_valid && wr_bar[0] && (wr_addr_bar0 == 32'h{{ printf "%08X" .NVMeSQ0DoorbellOffset }}); wire nvme_cq0_db_wr = wr_valid && wr_bar[0] && (wr_addr_bar0 == 32'h{{ printf "%08X" .NVMeCQ0DoorbellOffset }}); + wire nvme_sq1_db_wr = wr_valid && wr_bar[0] && (wr_addr_bar0 == 32'h{{ printf "%08X" .NVMeSQ1DoorbellOffset }}); + wire nvme_cq1_db_wr = wr_valid && wr_bar[0] && (wr_addr_bar0 == 32'h{{ printf "%08X" .NVMeCQ1DoorbellOffset }}); // plumbing between the responder and the DMA bridge wire nvme_dma_rd_req; @@ -607,6 +626,10 @@ module pcileech_tlps128_bar_controller( .sq0_doorbell_val ( wr_data[15:0] ), .cq0_doorbell_wr ( nvme_cq0_db_wr ), .cq0_doorbell_val ( wr_data[15:0] ), + .sq1_doorbell_wr ( nvme_sq1_db_wr ), + .sq1_doorbell_val ( wr_data[15:0] ), + .cq1_doorbell_wr ( nvme_cq1_db_wr ), + .cq1_doorbell_val ( wr_data[15:0] ), .msix_enable ( msix_enable ), .msix_function_mask ( msix_function_mask ), .msix_vector0_addr ( msix_vector0_addr ), @@ -723,7 +746,119 @@ module pcileech_tlps128_bar_controller( assign tlps_rdeng.tready = tlps_out.tready && (dma_yield || !hda_tlp_tx_tvalid); {{ end }}{{ end }} -{{ if not (or (and (eq .DeviceClass "nvme") .NVMeIdentify) (and (eq .DeviceClass "audio") .BARModel)) }} +{{ if and (eq .DeviceClass "xhci") .BARModel }} + // ======================================================================== + // xHCI Command Ring + Event Ring engine + DMA bridge + // - ring engine walks the Command Ring and answers with Command + // Completion Events on the (single-segment) Event Ring + // - bridge turns those DMA ops into actual PCIe TLPs + // ======================================================================== + + // catch the Command doorbell (DB0, DBOFF fixed 0x100 per xhci.go) + wire xhci_cmd_db_wr = wr_valid && wr_bar[0] && (wr_addr_bar0 == 32'h00000100); + + wire xhci_dma_rd_req; + wire [63:0] xhci_dma_rd_addr; + wire [9:0] xhci_dma_rd_len; + wire xhci_dma_rd_valid; + wire [31:0] xhci_dma_rd_data; + wire xhci_dma_rd_done; + wire xhci_dma_wr_req; + wire [63:0] xhci_dma_wr_addr; + wire [31:0] xhci_dma_wr_data; + wire [3:0] xhci_dma_wr_be; + wire xhci_dma_wr_valid; + wire xhci_dma_wr_done; + wire xhci_msix_trigger; + + wire [127:0] xhci_tlp_tx_tdata; + wire [3:0] xhci_tlp_tx_tkeepdw; + wire xhci_tlp_tx_tvalid; + wire xhci_tlp_tx_tlast; + wire [8:0] xhci_tlp_tx_tuser; + wire xhci_tlp_tx_tready; + + pcileech_xhci_ring_engine i_xhci_ring_engine( + .rst ( rst ), + .clk ( clk ), + .crcr_lo ( xhci_reg_crcr_lo ), + .crcr_hi ( xhci_reg_crcr_hi ), + .erstba_lo ( xhci_reg_erstba_lo ), + .erstba_hi ( xhci_reg_erstba_hi ), + .iman_ie ( xhci_reg_iman_ie ), + .cmd_doorbell_wr ( xhci_cmd_db_wr ), + .cmd_complete_pulse ( xhci_cmd_complete_pulse ), + .msix_trigger ( xhci_msix_trigger ), + .dma_rd_req ( xhci_dma_rd_req ), + .dma_rd_addr ( xhci_dma_rd_addr ), + .dma_rd_len ( xhci_dma_rd_len ), + .dma_rd_valid ( xhci_dma_rd_valid ), + .dma_rd_data ( xhci_dma_rd_data ), + .dma_rd_done ( xhci_dma_rd_done ), + .dma_wr_req ( xhci_dma_wr_req ), + .dma_wr_addr ( xhci_dma_wr_addr ), + .dma_wr_data ( xhci_dma_wr_data ), + .dma_wr_be ( xhci_dma_wr_be ), + .dma_wr_valid ( xhci_dma_wr_valid ), + .dma_wr_done ( xhci_dma_wr_done ) + ); + + pcileech_xhci_dma_bridge i_xhci_dma_bridge( + .rst ( rst ), + .clk ( clk ), + .pcie_id ( pcie_id ), + .dma_wr_req ( xhci_dma_wr_req ), + .dma_wr_addr ( xhci_dma_wr_addr ), + .dma_wr_data ( xhci_dma_wr_data ), + .dma_wr_be ( xhci_dma_wr_be ), + .dma_wr_valid ( xhci_dma_wr_valid ), + .dma_wr_done ( xhci_dma_wr_done ), + .dma_rd_req ( xhci_dma_rd_req ), + .dma_rd_addr ( xhci_dma_rd_addr ), + .dma_rd_len ( xhci_dma_rd_len ), + .dma_rd_valid ( xhci_dma_rd_valid ), + .dma_rd_data ( xhci_dma_rd_data ), + .dma_rd_done ( xhci_dma_rd_done ), + .tlp_tx_tdata ( xhci_tlp_tx_tdata ), + .tlp_tx_tkeepdw ( xhci_tlp_tx_tkeepdw ), + .tlp_tx_tvalid ( xhci_tlp_tx_tvalid ), + .tlp_tx_tlast ( xhci_tlp_tx_tlast ), + .tlp_tx_tuser ( xhci_tlp_tx_tuser ), + .tlp_tx_tready ( xhci_tlp_tx_tready ), + .tlp_rx_tdata ( tlps_in.tdata ), + .tlp_rx_tkeepdw ( tlps_in.tkeepdw ), + .tlp_rx_tvalid ( tlps_in.tvalid ), + .tlp_rx_tuser ( tlps_in.tuser ) + ); + + // mux DMA bridge TLPs with rdengine BAR responses onto tlps_out + // + // fairness: after 8 consecutive DMA TLP words a 1-cycle gap is forced so + // BAR read completions cannot be starved by sustained DMA bursts + reg [2:0] xhci_dma_burst_cnt; + wire xhci_dma_yield = (xhci_dma_burst_cnt == 3'd7); + wire xhci_dma_tx = xhci_tlp_tx_tvalid && tlps_out.tready && !xhci_dma_yield; + assign xhci_tlp_tx_tready = tlps_out.tready && !xhci_dma_yield; + + always @(posedge clk) begin + if (xhci_dma_yield) + xhci_dma_burst_cnt <= 3'd0; + else if (xhci_dma_tx && xhci_dma_burst_cnt != 3'd7) + xhci_dma_burst_cnt <= xhci_dma_burst_cnt + 3'd1; + else if (!xhci_tlp_tx_tvalid) + xhci_dma_burst_cnt <= 3'd0; + end + + assign tlps_out.tdata = (xhci_tlp_tx_tvalid && !xhci_dma_yield) ? xhci_tlp_tx_tdata : tlps_rdeng.tdata; + assign tlps_out.tkeepdw = (xhci_tlp_tx_tvalid && !xhci_dma_yield) ? xhci_tlp_tx_tkeepdw : tlps_rdeng.tkeepdw; + assign tlps_out.tvalid = (xhci_tlp_tx_tvalid && !xhci_dma_yield) || tlps_rdeng.tvalid; + assign tlps_out.tlast = (xhci_tlp_tx_tvalid && !xhci_dma_yield) ? xhci_tlp_tx_tlast : tlps_rdeng.tlast; + assign tlps_out.tuser = (xhci_tlp_tx_tvalid && !xhci_dma_yield) ? xhci_tlp_tx_tuser : tlps_rdeng.tuser; + assign tlps_out.has_data = (xhci_tlp_tx_tvalid && !xhci_dma_yield) || tlps_rdeng.has_data; + assign tlps_rdeng.tready = tlps_out.tready && (xhci_dma_yield || !xhci_tlp_tx_tvalid); +{{ end }} + +{{ if not (or (and (eq .DeviceClass "nvme") .NVMeIdentify) (and (eq .DeviceClass "audio") .BARModel) (and (eq .DeviceClass "xhci") .BARModel)) }} // no DMA bridge: rdengine output goes straight to tlps_out assign tlps_out.tdata = tlps_rdeng.tdata; assign tlps_out.tkeepdw = tlps_rdeng.tkeepdw; @@ -769,27 +904,41 @@ module pcileech_tlps128_bar_controller( .intr_req ( bar2_intr_req ) ); - // BAR3-6: unimplemented - genvar gi; - generate - for (gi = 3; gi < 7; gi = gi + 1) begin : gen_bar_none - pcileech_bar_impl_none i_bar_none( - .rst ( rst ), - .clk ( clk ), - .wr_addr ( wr_addr ), - .wr_be ( wr_be ), - .wr_data ( wr_data ), - .wr_valid ( wr_valid && wr_bar[gi] ), - .rd_req_ctx ( rd_req_ctx ), - .rd_req_addr ( rd_req_addr ), - .rd_req_valid ( rd_req_valid && rd_req_bar[gi]), - .rd_rsp_ctx ( bar_rsp_ctx[gi] ), - .rd_rsp_data ( bar_rsp_data[gi] ), - .rd_rsp_valid ( bar_rsp_valid[gi] ) - ); - end - endgenerate - - assign intr_req = bar2_intr_req{{ if eq .DeviceClass "audio" }} | hda_intr_req{{ end }}{{ if eq .DeviceClass "nvme" }} | nvme_msix_trigger{{ end }}; + // BAR3-6: real aperture (loopaddr stub, same as BAR1) for any index the + // donor's own BAR layout actually has populated, otherwise unimplemented + // (genuinely no BAR there on the real hardware). +{{ range $i, $present := .ExtraBARPresent }}{{ $idx := add $i 3 }}{{ if $present }} + pcileech_bar_impl_loopaddr i_bar{{ $idx }}( + .rst ( rst ), + .clk ( clk ), + .wr_addr ( wr_addr ), + .wr_be ( wr_be ), + .wr_data ( wr_data ), + .wr_valid ( wr_valid && wr_bar[{{ $idx }}] ), + .rd_req_ctx ( rd_req_ctx ), + .rd_req_addr ( rd_req_addr ), + .rd_req_valid ( rd_req_valid && rd_req_bar[{{ $idx }}] ), + .rd_rsp_ctx ( bar_rsp_ctx[{{ $idx }}] ), + .rd_rsp_data ( bar_rsp_data[{{ $idx }}] ), + .rd_rsp_valid ( bar_rsp_valid[{{ $idx }}] ) + ); +{{ else }} + pcileech_bar_impl_none i_bar{{ $idx }}_none( + .rst ( rst ), + .clk ( clk ), + .wr_addr ( wr_addr ), + .wr_be ( wr_be ), + .wr_data ( wr_data ), + .wr_valid ( wr_valid && wr_bar[{{ $idx }}] ), + .rd_req_ctx ( rd_req_ctx ), + .rd_req_addr ( rd_req_addr ), + .rd_req_valid ( rd_req_valid && rd_req_bar[{{ $idx }}] ), + .rd_rsp_ctx ( bar_rsp_ctx[{{ $idx }}] ), + .rd_rsp_data ( bar_rsp_data[{{ $idx }}] ), + .rd_rsp_valid ( bar_rsp_valid[{{ $idx }}] ) + ); +{{ end }}{{ end }} + assign intr_req = bar2_intr_req{{ if eq .DeviceClass "audio" }} | hda_intr_req{{ end }}{{ if eq .DeviceClass "nvme" }} | nvme_msix_trigger{{ end }}{{ if eq .DeviceClass "xhci" }} | xhci_msix_trigger{{ end }}; +{{ .ILAInstanceSV }} endmodule diff --git a/internal/firmware/svgen/templates/bar_impl_device.sv.tmpl b/internal/firmware/svgen/templates/bar_impl_device.sv.tmpl index dfdaf0a..07c4760 100644 --- a/internal/firmware/svgen/templates/bar_impl_device.sv.tmpl +++ b/internal/firmware/svgen/templates/bar_impl_device.sv.tmpl @@ -50,6 +50,14 @@ module pcileech_bar_impl_device #( output wire [31:0] nvme_acq_lo, output wire [31:0] nvme_acq_hi, output wire [31:0] nvme_csts +{{ end }}{{ if eq .DeviceClass "xhci" }} , + // xHCI register outputs for the ring engine + HW-set IMAN.IP input + output wire [31:0] xhci_crcr_lo, + output wire [31:0] xhci_crcr_hi, + output wire [31:0] xhci_erstba_lo, + output wire [31:0] xhci_erstba_hi, + output wire xhci_iman_ie, + input xhci_cmd_complete {{ end }}); localparam [31:0] BAR_ADDR_MASK = BAR_SIZE - 1; @@ -131,6 +139,7 @@ module pcileech_bar_impl_device #( rs_prev <= 1'b0; reg_0x00000020 <= 32'h00000001; // USBCMD: R/S=1 (running) reg_0x00000024 <= 32'h00000000; // USBSTS: HCH=0 (not halted) + reg_0x00000220 <= 32'h00000000; // IMAN: IP=0, IE=0 end else begin rs_prev <= reg_0x00000020[0]; // USBCMD.R/S @@ -163,8 +172,29 @@ module pcileech_bar_impl_device #( if (!reg_0x00000020[0] && rs_prev && !hcrst_pending) begin reg_0x00000024[0] <= 1'b1; // USBSTS.HCH = 1 end + + // IMAN (0x220, RTSOFF+0x20 primary interrupter): bit0=IP + // (RW1C, HW-set by the ring engine on event post), bit1=IE (RW). + // IsFSMDriven because IP needs a HW-set path the generic W1C + // block (SW-clear only) can't provide. + if (wr_valid && (wr_addr[31:0] & 32'hFFFFFFFC) == 32'h00000220) begin + if (wr_be[0]) begin + reg_0x00000220[1] <= wr_data[1]; // IE + if (wr_data[0]) reg_0x00000220[0] <= 1'b0; // IP write-1-to-clear + end + end + if (xhci_cmd_complete) + reg_0x00000220[0] <= 1'b1; // IP set by the ring engine end end + + // xHCI register output wiring — CRCR/ERSTBA are plain RW (not + // IsFSMDriven), the ring engine masks alignment/control bits itself. + assign xhci_crcr_lo = reg_0x00000038; + assign xhci_crcr_hi = reg_0x0000003C; + assign xhci_erstba_lo = reg_0x00000230; + assign xhci_erstba_hi = reg_0x00000234; + assign xhci_iman_ie = reg_0x00000220[1]; {{ end }} {{ if eq .DeviceClass "audio" }} // ======================================================================== @@ -1044,7 +1074,14 @@ module pcileech_bar_impl_device #( {{ end }}{{ end }} end else if (wr_valid) begin case (wr_offset & 32'hFFFFFFFC) -{{ range .BARModel.Registers }}{{ if and (ne .RWMask 0) (not .IsRW1C) (not .IsFSMDriven) }} 32'h{{ hex08 (alignedOffset .Offset) }}: begin +{{ range .BARModel.Registers }}{{ if and (ne .W1CMask 0) (not .IsRW1C) (not .IsFSMDriven) }} 32'h{{ hex08 (alignedOffset .Offset) }}: begin + // W1C bits clear where wr_data is 1; RW bits take wr_data; RO bits hold. + if (wr_be[0]) reg_0x{{ hex08 .Offset }}[7:0] <= ((reg_0x{{ hex08 .Offset }}[7:0] & ~(8'h{{ hex02 (index (rwMaskBytes .RWMask) 0) }} | 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 0) }})) | (wr_data[7:0] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 0) }}) | (reg_0x{{ hex08 .Offset }}[7:0] & ~wr_data[7:0] & 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 0) }})); + if (wr_be[1]) reg_0x{{ hex08 .Offset }}[15:8] <= ((reg_0x{{ hex08 .Offset }}[15:8] & ~(8'h{{ hex02 (index (rwMaskBytes .RWMask) 1) }} | 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 1) }})) | (wr_data[15:8] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 1) }}) | (reg_0x{{ hex08 .Offset }}[15:8] & ~wr_data[15:8] & 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 1) }})); + if (wr_be[2]) reg_0x{{ hex08 .Offset }}[23:16] <= ((reg_0x{{ hex08 .Offset }}[23:16] & ~(8'h{{ hex02 (index (rwMaskBytes .RWMask) 2) }} | 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 2) }})) | (wr_data[23:16] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 2) }}) | (reg_0x{{ hex08 .Offset }}[23:16] & ~wr_data[23:16] & 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 2) }})); + if (wr_be[3]) reg_0x{{ hex08 .Offset }}[31:24] <= ((reg_0x{{ hex08 .Offset }}[31:24] & ~(8'h{{ hex02 (index (rwMaskBytes .RWMask) 3) }} | 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 3) }})) | (wr_data[31:24] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 3) }}) | (reg_0x{{ hex08 .Offset }}[31:24] & ~wr_data[31:24] & 8'h{{ hex02 (index (w1cMaskBytes .W1CMask) 3) }})); + end +{{ else if and (ne .RWMask 0) (not .IsRW1C) (not .IsFSMDriven) }} 32'h{{ hex08 (alignedOffset .Offset) }}: begin if (wr_be[0]) reg_0x{{ hex08 .Offset }}[7:0] <= (reg_0x{{ hex08 .Offset }}[7:0] & ~8'h{{ hex02 (index (rwMaskBytes .RWMask) 0) }}) | (wr_data[7:0] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 0) }}); if (wr_be[1]) reg_0x{{ hex08 .Offset }}[15:8] <= (reg_0x{{ hex08 .Offset }}[15:8] & ~8'h{{ hex02 (index (rwMaskBytes .RWMask) 1) }}) | (wr_data[15:8] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 1) }}); if (wr_be[2]) reg_0x{{ hex08 .Offset }}[23:16] <= (reg_0x{{ hex08 .Offset }}[23:16] & ~8'h{{ hex02 (index (rwMaskBytes .RWMask) 2) }}) | (wr_data[23:16] & 8'h{{ hex02 (index (rwMaskBytes .RWMask) 2) }}); diff --git a/internal/firmware/svgen/templates/device_config.sv.tmpl b/internal/firmware/svgen/templates/device_config.sv.tmpl index 582cdd3..0291b99 100644 --- a/internal/firmware/svgen/templates/device_config.sv.tmpl +++ b/internal/firmware/svgen/templates/device_config.sv.tmpl @@ -14,6 +14,9 @@ // Class Code: 0x{{ hex02 (classBase .ClassCode) }}{{ hex02 (classSub .ClassCode) }}{{ hex02 (classProgIF .ClassCode) }} {{ if .DeviceIDs.HasDSN }}// DSN: 0x{{ printf "%016X" .DeviceIDs.DSN }}{{ end }} {{ if .DeviceIDs.HasPCIeCap }}// PCIe Link: Speed={{ .DeviceIDs.LinkSpeed }} Width=x{{ .DeviceIDs.LinkWidth }}{{ end }} +{{ if .DonorCapabilities.HasPMCap }}// Donor PM: PME support=0x{{ printf "%02X" .DonorCapabilities.PMESupportMask }}, PME_Enable={{ if .DonorCapabilities.PMEDefault }}on{{ else }}off{{ end }}{{ end }} +{{ if .DonorCapabilities.HasMSICap }}// Donor MSI: multiple-msg=0x{{ printf "%02X" .DonorCapabilities.MSIMultipleMsg }}{{ if .DonorCapabilities.MSIDisable64Bit }}, 64-bit=off{{ end }}{{ end }} +{{ if .DonorCapabilities.HasPCIeCap }}// Donor PCIe: ASPM cap=0x{{ printf "%01X" .DonorCapabilities.PCIeASPMCap }}, ASPM enable=0x{{ printf "%01X" .DonorCapabilities.PCIeASPMEnable }}, link=x{{ .DonorCapabilities.PCIELinkWidth }} {{ .DonorCapabilities.PCIELinkSpeed }}GT/s{{ end }} // // Features: {{ if eq .DeviceClass "nvme" }}// - NVMe CC->CSTS state machine (dynamic handshake) @@ -61,6 +64,29 @@ package device_config; localparam HAS_XHCI_FSM = {{ if eq .DeviceClass "xhci" }}1{{ else }}0{{ end }}; localparam HAS_AUDIO_FSM = {{ if eq .DeviceClass "audio" }}1{{ else }}0{{ end }}; localparam HAS_MSIX_INT = {{ if .HasMSIX }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_PM_CAP = {{ if .DonorCapabilities.HasPMCap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_MSI_CAP = {{ if .DonorCapabilities.HasMSICap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_MSIX_CAP = {{ if .DonorCapabilities.HasMSIXCap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_PCIE_CAP = {{ if .DonorCapabilities.HasPCIeCap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_AER_CAP = {{ if .DonorCapabilities.HasAERCap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_LTR_CAP = {{ if .DonorCapabilities.HasLTRCap }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_L1PM_CAP = {{ if .DonorCapabilities.HasL1PMSubstates }}1{{ else }}0{{ end }}; + localparam HAS_DONOR_DSN_CAP = {{ if .DonorCapabilities.HasDSNCap }}1{{ else }}0{{ end }}; + localparam [11:0] DONOR_PM_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.PMCapOffset }}; + localparam [11:0] DONOR_MSI_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.MSICapOffset }}; + localparam [11:0] DONOR_MSIX_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.MSIXCapOffset }}; + localparam [11:0] DONOR_PCIE_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.PCIeCapOffset }}; + localparam [11:0] DONOR_AER_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.AERCapOffset }}; + localparam [11:0] DONOR_LTR_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.LTRCapOffset }}; + localparam [11:0] DONOR_L1PM_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.L1PMCapOffset }}; + localparam [11:0] DONOR_DSN_CAP_OFF = 12'h{{ printf "%03X" .DonorCapabilities.DSNCapOffset }}; + localparam [7:0] DONOR_PME_SUPPORT_MASK = 8'h{{ printf "%02X" .DonorCapabilities.PMESupportMask }}; + localparam DONOR_MSI_DISABLE_64 = {{ if .DonorCapabilities.MSIDisable64Bit }}1{{ else }}0{{ end }}; + localparam [2:0] DONOR_MSI_MULTIPLE_MSG = 3'b{{ printf "%03b" .DonorCapabilities.MSIMultipleMsg }}; + localparam [1:0] DONOR_PCIE_ASPM_CAP = 2'b{{ printf "%02b" .DonorCapabilities.PCIeASPMCap }}; + localparam [1:0] DONOR_PCIE_ASPM_ENABLE = 2'b{{ printf "%02b" .DonorCapabilities.PCIeASPMEnable }}; + localparam [7:0] DONOR_PCIELINK_SPEED = 8'd{{ .DonorCapabilities.PCIELinkSpeed }}; + localparam [7:0] DONOR_PCIELINK_WIDTH = 8'd{{ .DonorCapabilities.PCIELinkWidth }}; localparam HAS_LATENCY_EMU = {{ if .LatencyConfig }}1{{ else }}0{{ end }}; endpackage diff --git a/internal/firmware/svgen/templates/nvme_admin_responder.sv.tmpl b/internal/firmware/svgen/templates/nvme_admin_responder.sv.tmpl index 7667559..2510c15 100644 --- a/internal/firmware/svgen/templates/nvme_admin_responder.sv.tmpl +++ b/internal/firmware/svgen/templates/nvme_admin_responder.sv.tmpl @@ -13,7 +13,9 @@ // 0x09 Set Features – temperature, error recovery, queues, VWC, etc. // 0x0A Get Features – number of queues, interrupt coalescing // 0x0C AER – queued silently, never completed (that's legal) -// rest Create IO CQ/SQ, whatever – blanket success +// 0x80 Format NVM – accept format requests +// IO Q1 Read/Write – backed by a small BRAM sector cache (nvme_store) +// IO Q1 Flush/Write Zeroes/Dataset Mgmt – accepted, no data movement needed `timescale 1ns / 1ps @@ -35,6 +37,10 @@ module pcileech_nvme_admin_responder( input [15:0] sq0_doorbell_val, input cq0_doorbell_wr, // BAR0 + 0x1004 input [15:0] cq0_doorbell_val, + input sq1_doorbell_wr, // BAR0 + 0x1008 (DSTRD=0) + input [15:0] sq1_doorbell_val, + input cq1_doorbell_wr, // BAR0 + 0x100C (DSTRD=0) + input [15:0] cq1_doorbell_val, // MSI-X interrupt target — vector 0 config (from pcileech_msix_table). // Accepted to match the bar_controller wiring. The current interrupt @@ -84,6 +90,8 @@ module pcileech_nvme_admin_responder( localparam [3:0] S_EXEC_NSLIST = 4'h9; localparam [3:0] S_EXEC_LOGPAGE = 4'hA; localparam [3:0] S_SHUTDOWN = 4'hB; + localparam [3:0] S_EXEC_READ_ZERO = 4'hC; + localparam [3:0] S_EXEC_WRITE_STORE = 4'hD; reg [3:0] state; reg [3:0] return_state; // where to go after DMA wait completes @@ -93,6 +101,11 @@ module pcileech_nvme_admin_responder( reg [15:0] sq_head; reg [15:0] cq_head; reg cq_phase; // flips each time CQ wraps around + reg [15:0] iosq_tail; + reg [15:0] iosq_head; + reg [15:0] iocq_head; + reg iocq_phase; + reg active_io_cmd; wire [11:0] asqs = aqa[11:0]; wire [11:0] acqs = aqa[27:16]; @@ -109,22 +122,68 @@ module pcileech_nvme_admin_responder( wire [63:0] cmd_prp2 = {sqe[9], sqe[8]}; wire [31:0] cmd_cdw10 = sqe[10]; wire [31:0] cmd_cdw11 = sqe[11]; + wire [31:0] cmd_cdw12 = sqe[12]; wire [7:0] identify_cns = cmd_cdw10[7:0]; + function [63:0] cmd_prp_addr; + input [12:0] data_byte_offset; + reg [12:0] prp_byte_offset; + begin + prp_byte_offset = {1'b0, cmd_prp1[11:0]} + data_byte_offset; + if (prp_byte_offset[12]) + cmd_prp_addr = {cmd_prp2[63:12], 12'h000} + {52'h0, prp_byte_offset[11:0]}; + else + cmd_prp_addr = cmd_prp1 + {51'h0, data_byte_offset}; + end + endfunction + // CQE we're building – 4 DWORDs, written to host one at a time reg [31:0] cqe [0:3]; reg [1:0] cqe_dw_idx; // transfer progress reg [10:0] data_dw_cnt; + reg [10:0] io_zero_dw_limit; reg [12:0] id_rom_offset; + // ---- IO backing store: small BRAM sector cache for IO Read/Write -------- + // ponytail: NVME_STORE_NSECT x NVME_STORE_SECT_DW is a small fixed-size + // cache window (2KB), not a full-capacity disk backing store. LBAs beyond + // the window wrap (mod NVME_STORE_NSECT) instead of growing unbounded or + // corrupting an adjacent sector's data. Addressing scheme and NSECT=4 + // scale match sim/nvme_store.sv + sim/ahci_engine.sv. + localparam NVME_STORE_NSECT = 4; // sectors cached + localparam NVME_STORE_SECT_DW = 128; // dwords/sector (512B) - matches the + // block size io_zero_dw_limit already assumes + reg [31:0] nvme_store [0:(NVME_STORE_NSECT*NVME_STORE_SECT_DW)-1]; + integer nvme_store_init_idx; + initial begin + for (nvme_store_init_idx = 0; nvme_store_init_idx < NVME_STORE_NSECT*NVME_STORE_SECT_DW; nvme_store_init_idx = nvme_store_init_idx + 1) + nvme_store[nvme_store_init_idx] = 32'h00000000; + end + + // current dword's address within the store: sector = starting LBA + // (cmd_cdw10, mod NSECT via truncation) + which 512B block of this + // multi-block transfer we're on; dw = offset within that block. + wire [1:0] io_store_sector = cmd_cdw10[1:0] + data_dw_cnt[10:7]; + wire [6:0] io_store_dw = data_dw_cnt[6:0]; + wire [8:0] io_store_addr = {io_store_sector, io_store_dw}; + // edge detect for CC.EN so we can do a clean shutdown reg cc_en_prev; // masking low bits – NVMe spec says queue bases are page-aligned wire [63:0] asq_base = {asq_hi, asq_lo & 32'hFFFFF000}; wire [63:0] acq_base = {acq_hi, acq_lo & 32'hFFFFF000}; + reg [63:0] iosq_base; + reg [63:0] iocq_base; + reg [15:0] iosq_size; + reg [15:0] iocq_size; + reg iosq_enabled; + reg iocq_enabled; + wire [63:0] active_cq_base = active_io_cmd ? iocq_base : acq_base; + wire [15:0] active_cq_head = active_io_cmd ? iocq_head : cq_head; + wire active_cq_phase = active_io_cmd ? iocq_phase : cq_phase; // the spec says AER commands just sit around until something happens, // and nothing ever happens on our fake controller, so we count and ignore @@ -141,6 +200,11 @@ module pcileech_nvme_admin_responder( sq_head <= 16'h0; cq_head <= 16'h0; cq_phase <= 1'b1; + iosq_tail <= 16'h0; + iosq_head <= 16'h0; + iocq_head <= 16'h0; + iocq_phase <= 1'b1; + active_io_cmd <= 1'b0; dma_rd_req <= 1'b0; dma_wr_req <= 1'b0; dma_wr_valid <= 1'b0; @@ -148,11 +212,18 @@ module pcileech_nvme_admin_responder( sqe_dw_idx <= 4'h0; cqe_dw_idx <= 2'h0; data_dw_cnt <= 11'h0; + io_zero_dw_limit <= 11'h0; id_rom_offset <= 13'h0; id_rom_addr <= 13'h0; cc_en_prev <= 1'b0; aer_pending <= 4'h0; num_queues_granted <= 32'h00010001; // default: 1 SQ + 1 CQ + iosq_base <= 64'h0; + iocq_base <= 64'h0; + iosq_size <= 16'h0; + iocq_size <= 16'h0; + iosq_enabled <= 1'b0; + iocq_enabled <= 1'b0; end else begin // pulse signals – cleared every cycle, only set when needed dma_rd_req <= 1'b0; @@ -183,14 +254,26 @@ module pcileech_nvme_admin_responder( // host consumed some CQEs, update our copy of the head if (cq0_doorbell_wr) cq_head <= cq0_doorbell_val; + if (sq1_doorbell_wr) + iosq_tail <= sq1_doorbell_val; + if (cq1_doorbell_wr) + iocq_head <= cq1_doorbell_val; // anything new? go fetch it if (sq_tail != sq_head) begin + active_io_cmd <= 1'b0; dma_rd_req <= 1'b1; dma_rd_addr <= asq_base + {48'h0, sq_head} * 64; dma_rd_len <= 10'd16; sqe_dw_idx <= 4'h0; state <= S_FETCH_SQE; + end else if (iosq_enabled && iocq_enabled && iosq_tail != iosq_head) begin + active_io_cmd <= 1'b1; + dma_rd_req <= 1'b1; + dma_rd_addr <= iosq_base + {42'h0, iosq_head, 6'h0}; + dma_rd_len <= 10'd16; + sqe_dw_idx <= 4'h0; + state <= S_FETCH_SQE; end end @@ -206,14 +289,106 @@ module pcileech_nvme_admin_responder( // ---- figure out what the driver wants -------------------- S_DECODE_CMD: begin + if (active_io_cmd) begin + case (cmd_opcode) + 8'h02: begin + data_dw_cnt <= 11'h0; + if (cmd_cdw12[15:0] >= 16'd7) + io_zero_dw_limit <= 11'd1024; + else + io_zero_dw_limit <= ({8'h0, cmd_cdw12[2:0]} + 11'd1) << 7; + state <= S_EXEC_READ_ZERO; + end + 8'h01: begin + data_dw_cnt <= 11'h0; + if (cmd_cdw12[15:0] >= 16'd7) + io_zero_dw_limit <= 11'd1024; + else + io_zero_dw_limit <= ({8'h0, cmd_cdw12[2:0]} + 11'd1) << 7; + state <= S_EXEC_WRITE_STORE; + end + 8'h00, 8'h08, 8'h09: begin + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, iosq_head}; + cqe[3] <= {31'h0, iocq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + default: begin + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, iosq_head}; + cqe[3] <= {31'h0, iocq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + endcase + end else begin case (cmd_opcode) + // Delete IO Submission Queue + 8'h00: begin + iosq_enabled <= 1'b0; + iosq_tail <= 16'h0; + iosq_head <= 16'h0; + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, sq_head}; + cqe[3] <= {31'h0, cq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + + // Create IO Submission Queue + 8'h01: begin + iosq_base <= {cmd_prp1[63:12], 12'h000}; + iosq_size <= cmd_cdw10[31:16]; + iosq_enabled <= 1'b1; + iosq_tail <= 16'h0; + iosq_head <= 16'h0; + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, sq_head}; + cqe[3] <= {31'h0, cq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + // Get Log Page 8'h02: begin data_dw_cnt <= 11'h0; state <= S_EXEC_LOGPAGE; end + // Delete IO Completion Queue + 8'h04: begin + iocq_enabled <= 1'b0; + iocq_head <= 16'h0; + iocq_phase <= 1'b1; + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, sq_head}; + cqe[3] <= {31'h0, cq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + + // Create IO Completion Queue + 8'h05: begin + iocq_base <= {cmd_prp1[63:12], 12'h000}; + iocq_size <= cmd_cdw10[31:16]; + iocq_enabled <= 1'b1; + iocq_head <= 16'h0; + iocq_phase <= 1'b1; + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, sq_head}; + cqe[3] <= {31'h0, cq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + // Identify 8'h06: begin case (identify_cns) @@ -235,7 +410,7 @@ module pcileech_nvme_admin_responder( cqe[0] <= 32'h0; cqe[1] <= 32'h0; cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[3] <= {31'h0, cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end @@ -247,7 +422,7 @@ module pcileech_nvme_admin_responder( cqe[0] <= 32'h0; cqe[1] <= 32'h0; cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[3] <= {31'h0, cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end @@ -271,7 +446,7 @@ module pcileech_nvme_admin_responder( endcase cqe[1] <= 32'h0; cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[3] <= {31'h0, cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end @@ -285,7 +460,7 @@ module pcileech_nvme_admin_responder( endcase cqe[1] <= 32'h0; cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[3] <= {31'h0, cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end @@ -296,23 +471,35 @@ module pcileech_nvme_admin_responder( if (aer_pending < 4'hF) aer_pending <= aer_pending + 1'b1; // move the SQ head along but don't post any CQE - if (sq_head >= asqs) + if (sq_head >= {4'h0, asqs}) sq_head <= 16'h0; else sq_head <= sq_head + 1'b1; state <= S_IDLE; end + // Format NVM – accept the host format request. The + // minimal emulation reports an empty zeroed namespace. + 8'h80: begin + cqe[0] <= 32'h0; + cqe[1] <= 32'h0; + cqe[2] <= {cmd_cid, sq_head}; + cqe[3] <= {31'h0, cq_phase}; + cqe_dw_idx <= 2'h0; + state <= S_POST_CQE; + end + // everything else (Create IO CQ/SQ, etc.) – just say yes default: begin cqe[0] <= 32'h0; cqe[1] <= 32'h0; cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[3] <= {31'h0, cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end endcase + end end // ---- stream 4KB of identify data from ROM to host ------- @@ -327,7 +514,7 @@ module pcileech_nvme_admin_responder( end else begin dma_wr_req <= 1'b1; dma_wr_valid <= 1'b1; - dma_wr_addr <= cmd_prp1 + {53'h0, data_dw_cnt - 11'h1, 2'b00}; + dma_wr_addr <= cmd_prp_addr({data_dw_cnt - 11'h1, 2'b00}); dma_wr_data <= id_rom_data; dma_wr_be <= 4'hF; @@ -346,7 +533,7 @@ module pcileech_nvme_admin_responder( S_EXEC_NSLIST: begin dma_wr_req <= 1'b1; dma_wr_valid <= 1'b1; - dma_wr_addr <= cmd_prp1 + {53'h0, data_dw_cnt, 2'b00}; + dma_wr_addr <= cmd_prp_addr({data_dw_cnt, 2'b00}); dma_wr_data <= (data_dw_cnt == 11'h0) ? 32'h00000001 : 32'h0; dma_wr_be <= 4'hF; @@ -364,7 +551,7 @@ module pcileech_nvme_admin_responder( S_EXEC_LOGPAGE: begin dma_wr_req <= 1'b1; dma_wr_valid <= 1'b1; - dma_wr_addr <= cmd_prp1 + {53'h0, data_dw_cnt, 2'b00}; + dma_wr_addr <= cmd_prp_addr({data_dw_cnt, 2'b00}); dma_wr_be <= 4'hF; case (data_dw_cnt) @@ -383,12 +570,52 @@ module pcileech_nvme_admin_responder( state <= S_WAIT_DMA_WR; end + // ---- IO read: stream sectors back from the BRAM store ---- + S_EXEC_READ_ZERO: begin + dma_wr_req <= 1'b1; + dma_wr_valid <= 1'b1; + dma_wr_addr <= cmd_prp_addr({data_dw_cnt, 2'b00}); + dma_wr_data <= nvme_store[io_store_addr]; + dma_wr_be <= 4'hF; + + if (data_dw_cnt >= io_zero_dw_limit - 11'd1) begin + return_state <= S_WRITE_DATA; + end else begin + data_dw_cnt <= data_dw_cnt + 1'b1; + return_state <= S_EXEC_READ_ZERO; + end + state <= S_WAIT_DMA_WR; + end + + // ---- IO write: pull one DWORD from host into the store --- + S_EXEC_WRITE_STORE: begin + dma_rd_req <= 1'b1; + dma_rd_addr <= cmd_prp_addr({data_dw_cnt, 2'b00}); + dma_rd_len <= 10'd1; + state <= S_WAIT_DMA_RD; + end + + // ---- park here until the host DWORD comes back ----------- + S_WAIT_DMA_RD: begin + if (dma_rd_valid) + nvme_store[io_store_addr] <= dma_rd_data; + + if (dma_rd_done) begin + if (data_dw_cnt >= io_zero_dw_limit - 11'd1) begin + state <= S_WRITE_DATA; + end else begin + data_dw_cnt <= data_dw_cnt + 1'b1; + state <= S_EXEC_WRITE_STORE; + end + end + end + // ---- all data has been sent, pack a success CQE --------- S_WRITE_DATA: begin cqe[0] <= 32'h0; cqe[1] <= 32'h0; - cqe[2] <= {cmd_cid, sq_head}; - cqe[3] <= {15'h0, cq_phase}; + cqe[2] <= active_io_cmd ? {cmd_cid, iosq_head} : {cmd_cid, sq_head}; + cqe[3] <= {31'h0, active_cq_phase}; cqe_dw_idx <= 2'h0; state <= S_POST_CQE; end @@ -397,8 +624,8 @@ module pcileech_nvme_admin_responder( S_POST_CQE: begin dma_wr_req <= 1'b1; dma_wr_valid <= 1'b1; - dma_wr_addr <= acq_base + {48'h0, cq_head} * 16 - + {62'h0, cqe_dw_idx, 2'b00}; + dma_wr_addr <= active_cq_base + {44'h0, active_cq_head, 4'h0} + + {60'h0, cqe_dw_idx, 2'b00}; dma_wr_data <= cqe[cqe_dw_idx]; dma_wr_be <= 4'hF; @@ -422,18 +649,32 @@ module pcileech_nvme_admin_responder( S_SEND_MSIX: begin msix_trigger <= 1'b1; - // wrap SQ head - if (sq_head >= asqs) - sq_head <= 16'h0; - else - sq_head <= sq_head + 1'b1; + if (active_io_cmd) begin + if (iosq_head >= iosq_size) + iosq_head <= 16'h0; + else + iosq_head <= iosq_head + 1'b1; - // wrap CQ head and flip the phase bit - if (cq_head >= acqs) begin - cq_head <= 16'h0; - cq_phase <= ~cq_phase; + if (iocq_head >= iocq_size) begin + iocq_head <= 16'h0; + iocq_phase <= ~iocq_phase; + end else begin + iocq_head <= iocq_head + 1'b1; + end end else begin - cq_head <= cq_head + 1'b1; + // wrap SQ head + if (sq_head >= {4'h0, asqs}) + sq_head <= 16'h0; + else + sq_head <= sq_head + 1'b1; + + // wrap CQ head and flip the phase bit + if (cq_head >= {4'h0, acqs}) begin + cq_head <= 16'h0; + cq_phase <= ~cq_phase; + end else begin + cq_head <= cq_head + 1'b1; + end end state <= S_IDLE; @@ -445,6 +686,11 @@ module pcileech_nvme_admin_responder( sq_head <= 16'h0; cq_head <= 16'h0; cq_phase <= 1'b1; + iosq_tail <= 16'h0; + iosq_head <= 16'h0; + iocq_head <= 16'h0; + iocq_phase <= 1'b1; + active_io_cmd <= 1'b0; aer_pending <= 4'h0; state <= S_IDLE; end diff --git a/internal/firmware/svgen/templates/nvme_dma_bridge.sv.tmpl b/internal/firmware/svgen/templates/nvme_dma_bridge.sv.tmpl index 3b2115d..c6264e4 100644 --- a/internal/firmware/svgen/templates/nvme_dma_bridge.sv.tmpl +++ b/internal/firmware/svgen/templates/nvme_dma_bridge.sv.tmpl @@ -73,7 +73,7 @@ module pcileech_nvme_dma_bridge( // CplD receive buffer: hold up to 4 DWORDs from one 128-bit beat reg [31:0] rx_buf [0:3]; reg [2:0] rx_buf_cnt; // how many DWORDs in buffer (0-4) - reg [2:0] rx_buf_idx; // current emit index + reg [1:0] rx_buf_idx; // current emit index always @(posedge clk) begin if (rst) begin @@ -87,7 +87,7 @@ module pcileech_nvme_dma_bridge( rd_remaining <= 10'h0; rd_tag <= 8'h80; rx_buf_cnt <= 3'h0; - rx_buf_idx <= 3'h0; + rx_buf_idx <= 2'h0; end else begin dma_wr_done <= 1'b0; dma_rd_valid <= 1'b0; @@ -216,7 +216,7 @@ module pcileech_nvme_dma_bridge( // payload starts at [127:96] = 1 DWORD rx_buf[0] <= tlp_rx_tdata[127:96]; rx_buf_cnt <= 3'd1; - rx_buf_idx <= 3'd0; + rx_buf_idx <= 2'd0; bstate <= B_RD_EMIT; end end @@ -232,11 +232,11 @@ module pcileech_nvme_dma_bridge( if (rd_remaining <= 10'd1) begin dma_rd_done <= 1'b1; bstate <= B_IDLE; - end else if (rx_buf_cnt == 3'd0 || rx_buf_idx >= (rx_buf_cnt - 3'd1)) begin + end else if (rx_buf_cnt == 3'd0 || {1'b0, rx_buf_idx} >= (rx_buf_cnt - 3'd1)) begin // buffer emptied (or was never filled), need more data from bus bstate <= B_RD_DRAIN; end else begin - rx_buf_idx <= rx_buf_idx + 3'd1; + rx_buf_idx <= rx_buf_idx + 2'd1; end end @@ -254,7 +254,7 @@ module pcileech_nvme_dma_bridge( else rx_buf_cnt <= rd_remaining[2:0]; - rx_buf_idx <= 3'd0; + rx_buf_idx <= 2'd0; bstate <= B_RD_EMIT; end end diff --git a/internal/firmware/svgen/templates/xhci_ring_engine.sv.tmpl b/internal/firmware/svgen/templates/xhci_ring_engine.sv.tmpl new file mode 100644 index 0000000..f7f6c8e --- /dev/null +++ b/internal/firmware/svgen/templates/xhci_ring_engine.sv.tmpl @@ -0,0 +1,539 @@ +// +// xHCI Command Ring + Event Ring engine +// {{ hex04 .DeviceIDs.VendorID }}:{{ hex04 .DeviceIDs.DeviceID }} +// +// This sits behind the BAR controller and answers whatever xhci.sys throws +// at the Command Ring during controller init. PORTSC1/PORTSC2 report +// "powered, no device attached" (see xhci.go's xhciProfile) -- there is no +// downstream USB device to enumerate, so this emulates the HOST CONTROLLER +// surviving its own init handshake, not a device's GET_DESCRIPTOR dance. +// Same philosophy as pcileech_nvme_admin_responder: it doesn't pretend to +// be a full xHC, it just needs the driver to get past init. +// +// No-Op Command (type 23) -> Command Completion Event, success +// Enable Slot Command (type 9) -> success, Slot ID = FIXED_SLOT_ID +// anything else -> success anyway, echoing the TRB's +// own Slot ID field (bits[31:24] of +// DW3) so a command never just hangs +// Link TRB (type 6) -> followed (with Toggle Cycle), not +// treated as a command +// +// Proven in sim/xhci_ring_engine.sv + sim/xhci_ring_engine_tb.sv (raw +// register/memory-bus interface); this module is the same FSM adapted to +// the DMA-bridge (TLP) convention used by pcileech_nvme_admin_responder + +// pcileech_nvme_dma_bridge. +// + +`timescale 1ns / 1ps + +module pcileech_xhci_ring_engine #( + parameter [7:0] FIXED_SLOT_ID = 8'h01 + // ponytail: single fixed Slot ID -- zero devices attached means at most + // one plausible in-flight Enable Slot Command, not a real free-list + // across HCSPARAMS1.MaxSlots. +)( + input rst, + input clk, + + // CRCR / ERSTBA — driver-programmed, plain RW registers read straight + // from bar_impl_device (not IsFSMDriven; see barmodel's buildXHCIBARModel). + input [31:0] crcr_lo, + input [31:0] crcr_hi, + input [31:0] erstba_lo, + input [31:0] erstba_hi, + input iman_ie, // IMAN.IE — gates the interrupt pulse + + // Command doorbell pulse from the BAR write engine (DBOFF fixed 0x100, + // doorbell 0 = Command Ring, same detection style as NVMe's SQ/CQ doorbells). + input cmd_doorbell_wr, + + // pulse telling bar_impl_device to set IMAN.IP (bit0) — HW-set, SW W1C. + output reg cmd_complete_pulse, + // one-cycle pulse to fire the MSI-X interrupt (routed like NVMe's msix_trigger). + output reg msix_trigger, + + // DMA read — fetch TRBs / the ERST entry from host memory + output reg dma_rd_req, + output reg [63:0] dma_rd_addr, + output reg [9:0] dma_rd_len, + input dma_rd_valid, + input [31:0] dma_rd_data, + input dma_rd_done, + + // DMA write — post Command Completion Events back to the Event Ring + output reg dma_wr_req, + output reg [63:0] dma_wr_addr, + output reg [31:0] dma_wr_data, + output reg [3:0] dma_wr_be, + output reg dma_wr_valid, + input dma_wr_done +); + + localparam [5:0] TRB_LINK = 6'd6; + localparam [5:0] TRB_ENSLOT = 6'd9; + localparam [5:0] TRB_NOOP = 6'd23; + localparam [5:0] TRB_CMD_EVENT = 6'd33; + + localparam [3:0] S_IDLE = 4'h0; + localparam [3:0] S_FETCH_ISSUE = 4'h1; + localparam [3:0] S_FETCH_WAIT = 4'h2; + localparam [3:0] S_DECODE = 4'h3; + localparam [3:0] S_ERST_ISSUE = 4'h4; + localparam [3:0] S_ERST_WAIT = 4'h5; + localparam [3:0] S_EVT_WRITE = 4'h6; + localparam [3:0] S_WAIT_DMA_WR = 4'h7; + localparam [3:0] S_POST = 4'h8; + + reg [3:0] state; + reg [3:0] return_state; + reg [1:0] dw_idx; + + // command ring consumer state + reg cr_started; + reg [63:0] crdp; + reg ccs; + + // event ring producer state + reg erst_valid; + reg [63:0] erst_base; + reg [15:0] erst_size; + reg [15:0] evt_idx; + reg ecs; + reg [31:0] erstba_lo_prev, erstba_hi_prev; + + reg [31:0] trb0, trb1, trb2, trb3; + reg [63:0] cmd_trb_addr; + reg [7:0] slot_id_out; + + wire [63:0] evt_addr = erst_base + ({48'h0, evt_idx} << 4); + + always @(posedge clk) begin + if (rst) begin + state <= S_IDLE; + dw_idx <= 2'h0; + cr_started <= 1'b0; crdp <= 64'h0; ccs <= 1'b1; + erst_valid <= 1'b0; erst_base <= 64'h0; erst_size <= 16'h0; + evt_idx <= 16'h0; ecs <= 1'b1; + erstba_lo_prev <= 32'h0; erstba_hi_prev <= 32'h0; + dma_rd_req <= 1'b0; dma_wr_req <= 1'b0; dma_wr_valid <= 1'b0; + cmd_complete_pulse <= 1'b0; msix_trigger <= 1'b0; + end else begin + dma_rd_req <= 1'b0; dma_wr_req <= 1'b0; dma_wr_valid <= 1'b0; + cmd_complete_pulse <= 1'b0; msix_trigger <= 1'b0; + + // driver reprogrammed the Event Ring Segment Table -- refetch + // the (single) segment descriptor before posting the next event. + erstba_lo_prev <= erstba_lo; + erstba_hi_prev <= erstba_hi; + if ((erstba_lo != erstba_lo_prev) || (erstba_hi != erstba_hi_prev)) + erst_valid <= 1'b0; + + case (state) + // ---- wait for a Command doorbell ring ------------------ + S_IDLE: if (cmd_doorbell_wr) begin + if (!cr_started) begin + crdp <= {crcr_hi, crcr_lo[31:6], 6'b0}; + ccs <= crcr_lo[0]; + cr_started <= 1'b1; + end + state <= S_FETCH_ISSUE; + end + + // ---- pull 16 bytes (one TRB) from the command ring ----- + S_FETCH_ISSUE: begin + dma_rd_req <= 1'b1; + dma_rd_addr <= crdp; + dma_rd_len <= 10'd4; + dw_idx <= 2'h0; + state <= S_FETCH_WAIT; + end + S_FETCH_WAIT: begin + if (dma_rd_valid) begin + case (dw_idx) + 2'd0: trb0 <= dma_rd_data; + 2'd1: trb1 <= dma_rd_data; + 2'd2: trb2 <= dma_rd_data; + 2'd3: trb3 <= dma_rd_data; + endcase + dw_idx <= dw_idx + 2'd1; + end + if (dma_rd_done) + state <= S_DECODE; + end + + // ---- ring caught up, follow Link, or execute a command - + S_DECODE: begin + if (trb3[0] != ccs) begin + // cycle bit doesn't match -- nothing new queued + state <= S_IDLE; + end else if (trb3[15:10] == TRB_LINK) begin + crdp <= {trb1, trb0} & 64'hFFFFFFFFFFFFFFF0; + if (trb3[1]) ccs <= ~ccs; // Toggle Cycle + state <= S_FETCH_ISSUE; + end else begin + cmd_trb_addr <= crdp; + crdp <= crdp + 64'd16; + if (trb3[15:10] == TRB_NOOP) + slot_id_out <= 8'h00; + else if (trb3[15:10] == TRB_ENSLOT) + slot_id_out <= FIXED_SLOT_ID; + else + slot_id_out <= trb3[31:24]; // everything else: echo, say yes + state <= erst_valid ? S_EVT_WRITE : S_ERST_ISSUE; + dw_idx <= 2'h0; + end + end + + // ---- fetch the (single) Event Ring Segment Table entry - + S_ERST_ISSUE: begin + dma_rd_req <= 1'b1; + dma_rd_addr <= {erstba_hi, erstba_lo}; + dma_rd_len <= 10'd4; + dw_idx <= 2'h0; + state <= S_ERST_WAIT; + end + S_ERST_WAIT: begin + if (dma_rd_valid) begin + case (dw_idx) + 2'd0: erst_base[31:0] <= dma_rd_data; + 2'd1: erst_base[63:32] <= dma_rd_data; + 2'd2: erst_size <= dma_rd_data[15:0]; + default: ; + endcase + dw_idx <= dw_idx + 2'd1; + end + if (dma_rd_done) begin + erst_valid <= 1'b1; + dw_idx <= 2'h0; + state <= S_EVT_WRITE; + end + end + + // ---- write the 16-byte Command Completion Event -------- + S_EVT_WRITE: begin + dma_wr_req <= 1'b1; + dma_wr_valid <= 1'b1; + dma_wr_be <= 4'hF; + dma_wr_addr <= evt_addr + {60'h0, dw_idx, 2'b00}; + case (dw_idx) + 2'd0: dma_wr_data <= cmd_trb_addr[31:0]; + 2'd1: dma_wr_data <= cmd_trb_addr[63:32]; + 2'd2: dma_wr_data <= {8'h01, 24'h0}; // Completion Code = Success + 2'd3: dma_wr_data <= {slot_id_out, 8'h00, TRB_CMD_EVENT, 9'h0, ecs}; + endcase + if (dw_idx == 2'd3) begin + dw_idx <= 2'h0; + return_state <= S_POST; + end else begin + dw_idx <= dw_idx + 2'd1; + return_state <= S_EVT_WRITE; + end + state <= S_WAIT_DMA_WR; + end + + // ---- park here until the DMA bridge says "done" -------- + S_WAIT_DMA_WR: if (dma_wr_done) + state <= return_state; + + // ---- advance the event ring, raise the interrupt, loop - + S_POST: begin + cmd_complete_pulse <= 1'b1; + msix_trigger <= iman_ie; + if (evt_idx + 16'd1 >= erst_size) begin + evt_idx <= 16'h0; + ecs <= ~ecs; + end else begin + evt_idx <= evt_idx + 16'd1; + end + state <= S_FETCH_ISSUE; // drain loop: check for more queued TRBs + end + + default: state <= S_IDLE; + endcase + end + end +endmodule + +// +// xHCI DMA Bridge +// {{ hex04 .DeviceIDs.VendorID }}:{{ hex04 .DeviceIDs.DeviceID }} +// +// Identical TLP-wrapping logic to pcileech_nvme_dma_bridge (it is genuinely +// device-agnostic: plain address/data/len in, MWr/MRd TLPs out) -- copied +// rather than shared because the writer/generator plumbing that emits +// pcileech_nvme_dma_bridge.sv only does so for NVMe builds today. Renamed +// so both bridges can coexist if that plumbing is later unified. +// +// ponytail: byte-identical to pcileech_nvme_dma_bridge -- known duplicate, +// share it in a future pass, not now (deduping touches the well-tested NVMe path). +// + +module pcileech_xhci_dma_bridge( + input rst, + input clk, + + input [15:0] pcie_id, // our BDF, goes into requester ID + + // write side – from the ring engine + input dma_wr_req, + input [63:0] dma_wr_addr, + input [31:0] dma_wr_data, + input [3:0] dma_wr_be, + input dma_wr_valid, + output reg dma_wr_done, + + // read side – from the ring engine + input dma_rd_req, + input [63:0] dma_rd_addr, + input [9:0] dma_rd_len, + output reg dma_rd_valid, + output reg [31:0] dma_rd_data, + output reg dma_rd_done, + + // outgoing TLP stream – gets muxed with bar controller responses + output reg [127:0] tlp_tx_tdata, + output reg [3:0] tlp_tx_tkeepdw, + output reg tlp_tx_tvalid, + output reg tlp_tx_tlast, + output reg [8:0] tlp_tx_tuser, + input tlp_tx_tready, // backpressure from tlps_out mux + + // incoming TLP snoop – we peek at this to catch CplD + input [127:0] tlp_rx_tdata, + input [3:0] tlp_rx_tkeepdw, + input tlp_rx_tvalid, + input [8:0] tlp_rx_tuser +); + + localparam [3:0] B_IDLE = 4'h0; + localparam [3:0] B_WR_TLP = 4'h1; // present write beat 1, hold until tready + localparam [3:0] B_WR_DONE = 4'h2; + localparam [3:0] B_RD_TLP = 4'h3; // present read request, hold until tready + localparam [3:0] B_RD_WAIT = 4'h4; + localparam [3:0] B_RD_HDR = 4'h5; // first CplD beat: extract 1 payload DW + localparam [3:0] B_RD_DRAIN = 4'h6; // subsequent beats: extract up to 4 DW each + localparam [3:0] B_RD_EMIT = 4'h7; // emit buffered DWORDs one at a time + localparam [3:0] B_WR_TLP2 = 4'h8; // 64-bit MWr second beat + + reg [3:0] bstate; + + reg [9:0] rd_remaining; // how many DWORDs we're still expecting + reg [7:0] rd_tag; // tags start at 0x80 to stay off host tags + + // 64-bit address detection + wire wr_is_64bit = |dma_wr_addr[63:32]; + wire rd_is_64bit = |dma_rd_addr[63:32]; + + // CplD receive buffer: hold up to 4 DWORDs from one 128-bit beat + reg [31:0] rx_buf [0:3]; + reg [2:0] rx_buf_cnt; // how many DWORDs in buffer (0-4) + reg [1:0] rx_buf_idx; // current emit index + + always @(posedge clk) begin + if (rst) begin + bstate <= B_IDLE; + dma_wr_done <= 1'b0; + dma_rd_valid <= 1'b0; + dma_rd_done <= 1'b0; + dma_rd_data <= 32'h0; + tlp_tx_tvalid <= 1'b0; + tlp_tx_tlast <= 1'b0; + rd_remaining <= 10'h0; + rd_tag <= 8'h80; + rx_buf_cnt <= 3'h0; + rx_buf_idx <= 2'h0; + end else begin + dma_wr_done <= 1'b0; + dma_rd_valid <= 1'b0; + dma_rd_done <= 1'b0; + tlp_tx_tvalid <= 1'b0; + tlp_tx_tlast <= 1'b0; + + case (bstate) + + // ---- idle: writes take priority -------------------------------- + // (dma_wr_addr/data/be and dma_rd_addr/len are held stable by the + // ring engine until we signal done, so we only need to remember + // *which* transfer this is -- B_WR_TLP/B_RD_TLP re-derive the TLP + // from those still-live inputs every cycle they're held.) + B_IDLE: begin + if (dma_wr_valid) begin + bstate <= B_WR_TLP; + end else if (dma_rd_req) begin + bstate <= B_RD_TLP; + end + end + + // ---- present write beat 1: hold until tlps_out mux accepts ---- + B_WR_TLP: begin + if (wr_is_64bit) begin + // 4DW header MWr: first beat = header only + tlp_tx_tdata[31:0] <= { + 3'b011, // Fmt = 4DW w/ data + 5'b00000, // Type = MWr + 1'b0, 3'b000, + 4'b0000, + 1'b0, 1'b0, + 2'b00, 2'b00, + 10'h001 + }; + tlp_tx_tdata[63:32] <= {pcie_id, 8'h00, 4'hF, dma_wr_be}; + tlp_tx_tdata[95:64] <= dma_wr_addr[63:32]; + tlp_tx_tdata[127:96] <= {dma_wr_addr[31:2], 2'b00}; + + tlp_tx_tkeepdw <= 4'b1111; + tlp_tx_tlast <= 1'b0; + tlp_tx_tuser <= {8'h00, 1'b1}; + tlp_tx_tvalid <= 1'b1; + + if (tlp_tx_tready) + bstate <= B_WR_TLP2; + end else begin + // 3DW header MWr: header + payload in one beat + tlp_tx_tdata[31:0] <= { + 3'b010, // Fmt = 3DW w/ data + 5'b00000, // Type = MWr + 1'b0, 3'b000, + 4'b0000, + 1'b0, 1'b0, + 2'b00, 2'b00, + 10'h001 + }; + tlp_tx_tdata[63:32] <= {pcie_id, 8'h00, 4'hF, dma_wr_be}; + tlp_tx_tdata[95:64] <= {dma_wr_addr[31:2], 2'b00}; + tlp_tx_tdata[127:96] <= dma_wr_data; + + tlp_tx_tkeepdw <= 4'b1111; + tlp_tx_tlast <= 1'b1; + tlp_tx_tuser <= {8'h00, 1'b1}; + tlp_tx_tvalid <= 1'b1; + + if (tlp_tx_tready) + bstate <= B_WR_DONE; + end + end + + // ---- present read request: hold until tlps_out mux accepts ---- + B_RD_TLP: begin + if (rd_is_64bit) begin + // 4DW header MRd + tlp_tx_tdata[31:0] <= { + 3'b001, // Fmt = 4DW no data + 5'b00000, // Type = MRd + 1'b0, 3'b000, + 4'b0000, + 1'b0, 1'b0, + 2'b00, 2'b00, + dma_rd_len + }; + tlp_tx_tdata[63:32] <= {pcie_id, rd_tag, 4'hF, 4'hF}; + tlp_tx_tdata[95:64] <= dma_rd_addr[63:32]; + tlp_tx_tdata[127:96] <= {dma_rd_addr[31:2], 2'b00}; + + tlp_tx_tkeepdw <= 4'b1111; + end else begin + // 3DW header MRd + tlp_tx_tdata[31:0] <= { + 3'b000, // Fmt = 3DW no data + 5'b00000, // Type = MRd + 1'b0, 3'b000, + 4'b0000, + 1'b0, 1'b0, + 2'b00, 2'b00, + dma_rd_len + }; + tlp_tx_tdata[63:32] <= {pcie_id, rd_tag, 4'hF, 4'hF}; + tlp_tx_tdata[95:64] <= {dma_rd_addr[31:2], 2'b00}; + tlp_tx_tdata[127:96] <= 32'h0; + + tlp_tx_tkeepdw <= 4'b0111; + end + + tlp_tx_tlast <= 1'b1; + tlp_tx_tuser <= {8'h00, 1'b1}; + tlp_tx_tvalid <= 1'b1; + + if (tlp_tx_tready) begin + rd_remaining <= dma_rd_len; + rd_tag <= rd_tag + 1'b1; + bstate <= B_RD_WAIT; + end + end + + // ---- 64-bit MWr second beat: payload DWORD, hold until ready --- + B_WR_TLP2: begin + tlp_tx_tdata[31:0] <= dma_wr_data; + tlp_tx_tdata[127:32] <= 96'h0; + tlp_tx_tkeepdw <= 4'b0001; + tlp_tx_tlast <= 1'b1; + tlp_tx_tuser <= 9'h0; + tlp_tx_tvalid <= 1'b1; + if (tlp_tx_tready) + bstate <= B_WR_DONE; + end + + // ---- write done, signal ring engine ----------------------- + B_WR_DONE: begin + dma_wr_done <= 1'b1; + bstate <= B_IDLE; + end + + // ---- snoop RX bus for CplD matching our tag ------------------- + B_RD_WAIT: begin + if (tlp_rx_tvalid && tlp_rx_tuser[0]) begin + // CplD fmt+type = 0100_1010 = 0x4A + if (tlp_rx_tdata[31:24] == 8'h4A) begin + if (tlp_rx_tdata[79:72] == (rd_tag - 8'h01)) begin + // first CplD beat: 3DW header occupies [95:0], + // payload starts at [127:96] = 1 DWORD + rx_buf[0] <= tlp_rx_tdata[127:96]; + rx_buf_cnt <= 3'd1; + rx_buf_idx <= 2'd0; + bstate <= B_RD_EMIT; + end + end + end + end + + // ---- emit buffered DWORDs one per cycle to ring engine ---- + B_RD_EMIT: begin + dma_rd_data <= rx_buf[rx_buf_idx]; + dma_rd_valid <= 1'b1; + rd_remaining <= rd_remaining - 10'd1; + + if (rd_remaining <= 10'd1) begin + dma_rd_done <= 1'b1; + bstate <= B_IDLE; + end else if (rx_buf_cnt == 3'd0 || {1'b0, rx_buf_idx} >= (rx_buf_cnt - 3'd1)) begin + // buffer emptied (or was never filled), need more data from bus + bstate <= B_RD_DRAIN; + end else begin + rx_buf_idx <= rx_buf_idx + 2'd1; + end + end + + // ---- subsequent CplD beats: 4 payload DWORDs per beat --------- + B_RD_DRAIN: begin + if (tlp_rx_tvalid) begin + rx_buf[0] <= tlp_rx_tdata[31:0]; + rx_buf[1] <= tlp_rx_tdata[63:32]; + rx_buf[2] <= tlp_rx_tdata[95:64]; + rx_buf[3] <= tlp_rx_tdata[127:96]; + + // figure out how many valid DWORDs in this beat + if (rd_remaining >= 10'd4) + rx_buf_cnt <= 3'd4; + else + rx_buf_cnt <= rd_remaining[2:0]; + + rx_buf_idx <= 2'd0; + bstate <= B_RD_EMIT; + end + end + + default: bstate <= B_IDLE; + endcase + end + end + +endmodule diff --git a/internal/firmware/tclgen/ila_test.go b/internal/firmware/tclgen/ila_test.go new file mode 100644 index 0000000..b94d593 --- /dev/null +++ b/internal/firmware/tclgen/ila_test.go @@ -0,0 +1,40 @@ +package tclgen + +import ( + "strings" + "testing" + + "github.com/sercanarga/pcileechgen/internal/board" + "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/pci" +) + +func ilaTestCtx() (*donor.DeviceContext, *board.Board) { + cs := pci.NewConfigSpace() + cs.Size = pci.ConfigSpaceSize + cs.WriteU16(0x00, 0x8086) + cs.WriteU16(0x02, 0x1533) + return &donor.DeviceContext{ + Device: pci.PCIDevice{VendorID: 0x8086, DeviceID: 0x1533, ClassCode: 0x020000}, + ConfigSpace: cs, + BARs: []pci.BAR{}, + }, &board.Board{Name: "TestBoard", FPGAPart: "xc7a35tfgg484-2", PCIeLanes: 1, TopModule: "test_top"} +} + +func TestGenerateProjectTCL_ILAEnabled(t *testing.T) { + ctx, b := ilaTestCtx() + tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false, 2048) + for _, want := range []string{"create_ip -name ila", "-module_name ila_0", "CONFIG.C_DATA_DEPTH {2048}", "generate_target all [get_ips ila_0]"} { + if !strings.Contains(tcl, want) { + t.Errorf("ILA-enabled TCL missing %q", want) + } + } +} + +func TestGenerateProjectTCL_ILADisabled(t *testing.T) { + ctx, b := ilaTestCtx() + tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false, 0) + if strings.Contains(tcl, "create_ip -name ila") { + t.Error("ILA must not be emitted when depth is 0") + } +} diff --git a/internal/firmware/tclgen/msi_test.go b/internal/firmware/tclgen/msi_test.go index 3e72335..808e622 100644 --- a/internal/firmware/tclgen/msi_test.go +++ b/internal/firmware/tclgen/msi_test.go @@ -154,7 +154,7 @@ func TestGenerateProjectTCL_MSIXConfig(t *testing.T) { }, } - tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false) + tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false, 0) for _, want := range []string{ "MSIx_Table_Size", @@ -189,7 +189,7 @@ func TestGenerateProjectTCL_NoMSIX(t *testing.T) { BARs: []pci.BAR{}, } - tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false) + tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false, 0) // MSI-X should NOT be configured when donor has no MSI-X if strings.Contains(tcl, "MSIx_Table_Size") { diff --git a/internal/firmware/tclgen/tclgen.go b/internal/firmware/tclgen/tclgen.go index 3e93982..dc2ebb8 100644 --- a/internal/firmware/tclgen/tclgen.go +++ b/internal/firmware/tclgen/tclgen.go @@ -34,15 +34,16 @@ type projectTCLData struct { LinkWidth string TrgtLinkSpeed string - Bar0Enabled bool - Bar0Size string - Bar0Scale string - Bar064bit bool + Bar0Enabled bool + Bar0Size string + Bar0Scale string + Bar064bit bool Bar0ByteSize int - StockBar bool + StockBar bool DSNEnabled bool MSICapVectorsStr string + ILABlock string // MSI-X MSIXEnabled bool @@ -110,7 +111,7 @@ func buildBAR0Config(bar0Size int, ctx *donor.DeviceContext) bar0Config { // generated TCL (no donor content patch into bram_bar_zero4k), while still // reporting the correct (donor-demanded) Bar0ByteSize for PCIe IP BAR sizing // and other config. This matches --stock-bar CLI semantics. -func GenerateProjectTCL(ctx *donor.DeviceContext, b *board.Board, libDir string, stockBar bool) string { +func GenerateProjectTCL(ctx *donor.DeviceContext, b *board.Board, libDir string, stockBar bool, ilaDepth int) string { ids := firmware.ExtractDeviceIDs(ctx.ConfigSpace, ctx.ExtCapabilities) // Use the board's max link speed for the Xilinx IP core. @@ -165,6 +166,10 @@ func GenerateProjectTCL(ctx *donor.DeviceContext, b *board.Board, libDir string, MSICapVectorsStr: msiVectorsToTCL(msiVectors), } + if ilaDepth > 0 { + data.ILABlock = firmware.ILACreateIPTCL(ilaDepth) + } + if ctx.MSIXData != nil && ctx.MSIXData.TableSize > 0 { data.MSIXEnabled = true data.MSIXTableSize = ctx.MSIXData.TableSize - 1 @@ -172,7 +177,7 @@ func GenerateProjectTCL(ctx *donor.DeviceContext, b *board.Board, libDir string, is64 := bar0.Is64bit && bir == 0 data.MSIXTableBIR = barBIRToTCL(bir, is64) dstrd := uint32(0) - if bar0d := firmware.LargestBar(ctx.BARContents); bar0d != nil && len(bar0d) >= 8 { + if bar0d := firmware.LargestBar(ctx.BARContents); len(bar0d) >= 8 { dstrd = binary.LittleEndian.Uint32(bar0d[4:8]) & 0x0F } tableOff, pbaOffset, _ := firmware.MSIXPlacement(bar0Size, ctx.MSIXData.TableSize, ctx.Device.ClassCode, dstrd) diff --git a/internal/firmware/tclgen/tclgen_test.go b/internal/firmware/tclgen/tclgen_test.go index 95778a5..48bc866 100644 --- a/internal/firmware/tclgen/tclgen_test.go +++ b/internal/firmware/tclgen/tclgen_test.go @@ -37,7 +37,7 @@ func TestGenerateProjectTCL(t *testing.T) { BARs: []pci.BAR{}, } - tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false) + tcl := GenerateProjectTCL(ctx, b, "/tmp/lib", false, 0) if !strings.Contains(tcl, "8086") { t.Error("TCL should contain vendor ID") @@ -57,6 +57,9 @@ func TestGenerateProjectTCL(t *testing.T) { if strings.Contains(tcl, "MORE OPTIONS") { t.Error("TCL should not use MORE OPTIONS for seed") } + if !strings.Contains(tcl, "src/*.v") { + t.Error("TCL should glob src/*.v (ZDMA/GBOX ship pcileech_com as pcileech_com_e.v)") + } } func TestGenerateBuildTCL(t *testing.T) { @@ -106,7 +109,8 @@ func TestClampLinkWidth(t *testing.T) { } func TestBarSizeToTCL(t *testing.T) { - k4 := uint64(4096); scale, size := barSizeToTCL(k4) + k4 := uint64(4096) + scale, size := barSizeToTCL(k4) if scale != "Kilobytes" || size != "4" { t.Errorf("4KB: got %s/%s, want Kilobytes/4", scale, size) } diff --git a/internal/firmware/tclgen/templates/project.tcl.tmpl b/internal/firmware/tclgen/templates/project.tcl.tmpl index 6cdfa24..4aadc5b 100644 --- a/internal/firmware/tclgen/templates/project.tcl.tmpl +++ b/internal/firmware/tclgen/templates/project.tcl.tmpl @@ -26,7 +26,8 @@ if {[string equal [get_filesets -quiet sources_1] ""]} { set obj [get_filesets sources_1] set sv_files [glob -nocomplain "${origin_dir}/src/*.sv"] set svh_files [glob -nocomplain "${origin_dir}/src/*.svh"] -set all_src_files [concat $sv_files $svh_files] +set v_files [glob -nocomplain "${origin_dir}/src/*.v"] ;# ZDMA/GBOX: pcileech_com_e.v +set all_src_files [concat $sv_files $svh_files $v_files] if {[llength $all_src_files] > 0} { set imported_files [import_files -fileset sources_1 $all_src_files] } @@ -38,6 +39,9 @@ foreach f [get_files -of_objects [get_filesets sources_1] -filter {NAME =~ "*.sv foreach f [get_files -of_objects [get_filesets sources_1] -filter {NAME =~ "*.svh"}] { set_property -name "file_type" -value "Verilog Header" -objects $f } +foreach f [get_files -of_objects [get_filesets sources_1] -filter {NAME =~ "*.v"}] { + set_property -name "file_type" -value "SystemVerilog" -objects $f +} {{if not .StockBar}} # Remove any stock bar controller that might have been imported from board sources. @@ -94,6 +98,7 @@ set all_ips [get_ips -quiet *] if {[llength $all_ips] > 0} { upgrade_ip $all_ips } +{{ .ILABlock }} # Point BRAM IPs to the generated COE files (library defaults are all-zero) set bar_bram [get_ips -quiet bram_bar_zero4k] diff --git a/internal/pci/bar.go b/internal/pci/bar.go index b04e47e..a31a8fb 100644 --- a/internal/pci/bar.go +++ b/internal/pci/bar.go @@ -132,7 +132,7 @@ func ParseBARsFromSysfsResource(lines []string) []BAR { n, _ := fmt.Sscanf(lines[i], "0x%x 0x%x 0x%x", &start, &end, &flags) if n != 3 { // Try without 0x prefix - n, _ = fmt.Sscanf(lines[i], "%x %x %x", &start, &end, &flags) + _, _ = fmt.Sscanf(lines[i], "%x %x %x", &start, &end, &flags) } bar := BAR{Index: i} diff --git a/internal/util/file.go b/internal/util/file.go index b241aea..30b7bd8 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -36,8 +36,8 @@ func CopyDir(src, dst string) error { return err } - if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { - return err + if mkdirErr := os.MkdirAll(dst, srcInfo.Mode()); mkdirErr != nil { + return mkdirErr } entries, err := os.ReadDir(src) diff --git a/internal/util/file_test.go b/internal/util/file_test.go index 9d920d0..eeffdd5 100644 --- a/internal/util/file_test.go +++ b/internal/util/file_test.go @@ -41,9 +41,15 @@ func TestCopyDir(t *testing.T) { dstDir := filepath.Join(t.TempDir(), "dst") // Create nested structure - os.MkdirAll(filepath.Join(srcDir, "sub"), 0755) - os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0644) - os.WriteFile(filepath.Join(srcDir, "sub", "b.txt"), []byte("bbb"), 0644) + if err := os.MkdirAll(filepath.Join(srcDir, "sub"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(srcDir, "sub", "b.txt"), []byte("bbb"), 0644); err != nil { + t.Fatal(err) + } if err := CopyDir(srcDir, dstDir); err != nil { t.Fatalf("CopyDir error: %v", err) diff --git a/internal/vivado/builder.go b/internal/vivado/builder.go index 6294910..4299cab 100644 --- a/internal/vivado/builder.go +++ b/internal/vivado/builder.go @@ -8,6 +8,7 @@ import ( "github.com/sercanarga/pcileechgen/internal/board" "github.com/sercanarga/pcileechgen/internal/donor" + "github.com/sercanarga/pcileechgen/internal/donor/behavior" fwout "github.com/sercanarga/pcileechgen/internal/firmware/output" "github.com/sercanarga/pcileechgen/internal/util" ) @@ -22,6 +23,9 @@ type BuildOptions struct { SkipVivado bool StockBar bool Force bool + + TimingHistogram *behavior.TimingHistogram + ILADepth int } // WithDefaults returns a copy of opts with zero values replaced by sensible defaults. @@ -60,6 +64,8 @@ func (b *Builder) Build(ctx *donor.DeviceContext) error { ow := fwout.NewOutputWriter(b.opts.OutputDir, b.opts.LibDir, b.opts.Jobs, b.opts.Timeout) ow.StockBar = b.opts.StockBar ow.Force = b.opts.Force + ow.TimingHistogram = b.opts.TimingHistogram + ow.ILADepth = b.opts.ILADepth if err := ow.WriteAll(ctx, b.board); err != nil { return fmt.Errorf("artifact generation failed: %w", err) } @@ -115,6 +121,11 @@ func (b *Builder) Build(ctx *donor.DeviceContext) error { } } + // Hand the deliverables to the invoking user after a sudo build. + if err := chownOutputs(b.opts.OutputDir); err != nil { + slog.Warn("chown output files", "error", err) + } + slog.Info("build completed successfully") return nil } diff --git a/internal/vivado/envrestore.go b/internal/vivado/envrestore.go new file mode 100644 index 0000000..8e247c3 --- /dev/null +++ b/internal/vivado/envrestore.go @@ -0,0 +1,193 @@ +package vivado + +import ( + "context" + "errors" + "fmt" + "log/slog" + "maps" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "time" +) + +type probeFunc func(ctx context.Context) ([]byte, error) + +type envOverride map[string]string + +// errHomeUnresolved is returned when the invoking user's home can't be found +// under sudo. A wrong HOME would send Vivado to the wrong ~/.Xilinx. +var errHomeUnresolved = errors.New("could not determine invoking user home under sudo; use 'sudo -E' or export XILINXD_LICENSE_FILE in your shell profile") + +// licenseProbeVars are the env vars recovered from the invoking user's login shell. +var licenseProbeVars = map[string]struct{}{ + "XILINXD_LICENSE_FILE": {}, + "LM_LICENSE_FILE": {}, + "PATH": {}, +} + +// parseProbeOutput extracts the license vars from the probe shell's stdout. +// Unknown lines are ignored, the first match per var wins, and empty values are +// skipped. +func parseProbeOutput(out []byte) envOverride { + res := envOverride{} + for _, line := range strings.Split(string(out), "\n") { + key, val, ok := strings.Cut(line, "=") + if !ok { + continue + } + if _, want := licenseProbeVars[key]; !want { + continue + } + if _, dup := res[key]; dup || val == "" { + continue + } + res[key] = val + } + return res +} + +// setEnv sets key in a KEY=VALUE slice, replacing an existing entry in place or +// appending. The slice may be mutated. +func setEnv(env []string, key, val string) []string { + for i, e := range env { + if k, _, ok := strings.Cut(e, "="); ok && k == key { + env[i] = key + "=" + val + return env + } + } + return append(env, key+"="+val) +} + +func applyOverrides(env []string, ov envOverride) []string { + for k, v := range ov { + env = setEnv(env, k, v) + } + return env +} + +// parseSudoID parses a sudo-exported numeric id, rejecting empty, non-numeric, +// zero, and overflowing values. +func parseSudoID(s string) (uint32, bool) { + n, err := strconv.ParseUint(s, 10, 32) + if err != nil || n == 0 { + return 0, false + } + return uint32(n), true +} + +func isUnderSudo() bool { + return os.Geteuid() == 0 && os.Getenv("SUDO_USER") != "" && os.Getenv("SUDO_UID") != "" +} + +// resolveEnvOverrides restores HOME and, when a probe is supplied, merges in the +// license vars it returns. A nil or failing probe leaves HOME set alone. +func resolveEnvOverrides(home string, probe probeFunc) envOverride { + ov := envOverride{"HOME": home} + if probe == nil { + return ov + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + out, err := probe(ctx) + if err != nil { + slog.Warn("license env probe failed; using HOME only", "error", err) + return ov + } + maps.Copy(ov, parseProbeOutput(out)) + return ov +} + +// buildSudoOverrides enforces that home is known and wires the real probe when +// the caller (or a test) does not provide one. +func buildSudoOverrides(uid, gid uint32, home, shell string, probe probeFunc) (envOverride, error) { + if home == "" { + return nil, errHomeUnresolved + } + if probe == nil { + probe = func(ctx context.Context) ([]byte, error) { + return probeShellAsUser(ctx, shell, home, uid, gid) + } + } + return resolveEnvOverrides(home, probe), nil +} + +// envOverrides rebuilds the Vivado child environment under sudo: HOME from the +// invoking user's passwd entry, plus license vars probed from their login shell. +// It is a no-op when not under sudo. +func envOverrides() (envOverride, error) { + if !isUnderSudo() { + return nil, nil + } + uid, ok := parseSudoID(os.Getenv("SUDO_UID")) + if !ok { + return nil, nil + } + gid, _ := parseSudoID(os.Getenv("SUDO_GID")) + home, shell, err := resolveSudoUser(os.Getenv("SUDO_UID"), os.Getenv("SUDO_USER")) + if err != nil { + return nil, fmt.Errorf("resolve invoking user under sudo: %w", err) + } + return buildSudoOverrides(uid, gid, home, shell, nil) +} + +// resolveSudoUser looks up the invoking user by uid, falling back to the sudo +// username, and returns its home and login shell. +func resolveSudoUser(uidStr, sudoUser string) (string, string, error) { + u, err := user.LookupId(uidStr) + if err != nil && sudoUser != "" { + u, err = user.Lookup(sudoUser) + } + if err != nil { + return "", "", fmt.Errorf("user not found (uid=%s user=%s): %w", uidStr, sudoUser, err) + } + if sudoUser != "" && u.Username != sudoUser { + slog.Warn("SUDO_USER does not match passwd entry for SUDO_UID", "sudo_user", sudoUser, "passwd_user", u.Username) + } + return u.HomeDir, lookupShell(u.Username, u.Uid), nil +} + +// lookupShell reads the login shell from /etc/passwd, defaulting to bash. +func lookupShell(username, uid string) string { + data, err := os.ReadFile("/etc/passwd") + if err != nil { + return "/bin/bash" + } + for _, line := range strings.Split(string(data), "\n") { + f := strings.Split(line, ":") + if len(f) >= 7 && (f[0] == username || f[2] == uid) && f[6] != "" { + return f[6] + } + } + return "/bin/bash" +} + +// chownOutputs hands the copied *.bit/*.bin to the invoking user after a sudo +// build, so the firmware doesn't come out root-owned. No-op otherwise and never +// fatal. +func chownOutputs(outputDir string) error { + if !isUnderSudo() { + return nil + } + uid, ok := parseSudoID(os.Getenv("SUDO_UID")) + if !ok { + return nil + } + gid, _ := parseSudoID(os.Getenv("SUDO_GID")) + var firstErr error + for _, pattern := range []string{filepath.Join(outputDir, "*.bit"), filepath.Join(outputDir, "*.bin")} { + matches, _ := filepath.Glob(pattern) + for _, m := range matches { + if err := os.Lchown(m, int(uid), int(gid)); err != nil { + slog.Warn("chown output file", "file", m, "error", err) + if firstErr == nil { + firstErr = err + } + } + } + } + return firstErr +} diff --git a/internal/vivado/envrestore_linux.go b/internal/vivado/envrestore_linux.go new file mode 100644 index 0000000..eb96b8f --- /dev/null +++ b/internal/vivado/envrestore_linux.go @@ -0,0 +1,21 @@ +//go:build linux + +package vivado + +import ( + "context" + "os/exec" + "syscall" +) + +const probePrintfScript = `printf 'XILINXD_LICENSE_FILE=%s\nLM_LICENSE_FILE=%s\nPATH=%s\n' "$XILINXD_LICENSE_FILE" "$LM_LICENSE_FILE" "$PATH"` + +// probeShellAsUser runs the invoking user's login shell as uid:gid (a child-only +// credential drop; the parent stays root) with a minimal env so the profile +// files re-export the license vars, and returns the printed values. +func probeShellAsUser(ctx context.Context, shell, home string, uid, gid uint32) ([]byte, error) { + cmd := exec.CommandContext(ctx, shell, "-lic", probePrintfScript) + cmd.Env = []string{"HOME=" + home, "TERM=dumb", "PATH=/usr/bin:/bin"} + cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uid, Gid: gid}} + return cmd.Output() +} diff --git a/internal/vivado/envrestore_other.go b/internal/vivado/envrestore_other.go new file mode 100644 index 0000000..122fd8b --- /dev/null +++ b/internal/vivado/envrestore_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package vivado + +import ( + "context" + "errors" +) + +func probeShellAsUser(context.Context, string, string, uint32, uint32) ([]byte, error) { + return nil, errors.New("credential drop not supported on this platform") +} diff --git a/internal/vivado/envrestore_test.go b/internal/vivado/envrestore_test.go new file mode 100644 index 0000000..120f932 --- /dev/null +++ b/internal/vivado/envrestore_test.go @@ -0,0 +1,214 @@ +package vivado + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +func TestParseProbeOutput_AllKeys(t *testing.T) { + out := []byte("XILINXD_LICENSE_FILE=/opt/lic.lic\nLM_LICENSE_FILE=2100@host\nPATH=/usr/bin:/bin\n") + got := parseProbeOutput(out) + want := envOverride{ + "XILINXD_LICENSE_FILE": "/opt/lic.lic", + "LM_LICENSE_FILE": "2100@host", + "PATH": "/usr/bin:/bin", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("parseProbeOutput = %#v, want %#v", got, want) + } +} + +func TestParseProbeOutput_ValueWithEquals(t *testing.T) { + out := []byte("XILINXD_LICENSE_FILE=/a/b=port\n") + got := parseProbeOutput(out) + if got["XILINXD_LICENSE_FILE"] != "/a/b=port" { + t.Errorf("value with '=' = %q, want %q", got["XILINXD_LICENSE_FILE"], "/a/b=port") + } +} + +func TestParseProbeOutput_MissingKeyAbsent(t *testing.T) { + out := []byte("XILINXD_LICENSE_FILE=/opt/lic.lic\n") + got := parseProbeOutput(out) + if _, ok := got["LM_LICENSE_FILE"]; ok { + t.Error("LM_LICENSE_FILE should be absent when not in output") + } + if _, ok := got["PATH"]; ok { + t.Error("PATH should be absent when not in output") + } +} + +func TestParseProbeOutput_IgnoresNoiseAndEmpty(t *testing.T) { + out := []byte("some profile echo noise\nXILINXD_LICENSE_FILE=/x.lic\nRANDOM_THING=ignored\n\nLM_LICENSE_FILE=\n") + got := parseProbeOutput(out) + if got["XILINXD_LICENSE_FILE"] != "/x.lic" { + t.Errorf("XILINXD_LICENSE_FILE = %q, want /x.lic", got["XILINXD_LICENSE_FILE"]) + } + if _, ok := got["LM_LICENSE_FILE"]; ok { + t.Error("empty LM_LICENSE_FILE value should be dropped") + } +} + +func TestSetEnv_ReplaceInPlace(t *testing.T) { + env := []string{"HOME=/root", "PATH=/usr/bin"} + got := setEnv(env, "HOME", "/home/user") + want := []string{"HOME=/home/user", "PATH=/usr/bin"} + if !reflect.DeepEqual(got, want) { + t.Errorf("setEnv replace = %#v, want %#v", got, want) + } +} + +func TestSetEnv_AppendWhenAbsent(t *testing.T) { + env := []string{"PATH=/usr/bin"} + got := setEnv(env, "HOME", "/home/user") + want := []string{"PATH=/usr/bin", "HOME=/home/user"} + if !reflect.DeepEqual(got, want) { + t.Errorf("setEnv append = %#v, want %#v", got, want) + } +} + +func TestApplyOverrides_KeepsOthersAndDoesNotTouchXilinx(t *testing.T) { + env := []string{"XILINX_VIVADO=/tools/Xilinx/Vivado/2023.2", "PATH=/usr/bin"} + ov := envOverride{"HOME": "/home/user", "XILINXD_LICENSE_FILE": "/x.lic"} + got := applyOverrides(env, ov) + + if envGet(got, "XILINX_VIVADO") != "/tools/Xilinx/Vivado/2023.2" { + t.Errorf("XILINX_VIVADO must be untouched, got %q", envGet(got, "XILINX_VIVADO")) + } + if envGet(got, "HOME") != "/home/user" { + t.Errorf("HOME override not applied, got %q", envGet(got, "HOME")) + } + if envGet(got, "XILINXD_LICENSE_FILE") != "/x.lic" { + t.Errorf("license override not applied, got %q", envGet(got, "XILINXD_LICENSE_FILE")) + } +} + +func TestParseSudoID(t *testing.T) { + cases := []struct { + in string + want uint32 + ok bool + }{ + {"", 0, false}, + {"0", 0, false}, + {"abc", 0, false}, + {"-1", 0, false}, + {"4294967296", 0, false}, // 2^32 overflow + {"1000", 1000, true}, + } + for _, c := range cases { + got, ok := parseSudoID(c.in) + if ok != c.ok || got != c.want { + t.Errorf("parseSudoID(%q) = (%d,%v), want (%d,%v)", c.in, got, ok, c.want, c.ok) + } + } +} + +// envGet returns the value for key in a KEY=VALUE slice, or "". +func envGet(env []string, key string) string { + for _, e := range env { + if k, v, ok := strings.Cut(e, "="); ok && k == key { + return v + } + } + return "" +} + +// fakeProbe returns a probeFunc emitting the given stdout, or an error. +func fakeProbe(out string, err error) probeFunc { + return func(context.Context) ([]byte, error) { return []byte(out), err } +} + +func TestResolveEnvOverrides_ProbeMerged(t *testing.T) { + ov := resolveEnvOverrides("/home/u", fakeProbe("XILINXD_LICENSE_FILE=/a.lic\nLM_LICENSE_FILE=2100@h\nPATH=/u/bin\n", nil)) + if ov["HOME"] != "/home/u" { + t.Errorf("HOME = %q, want /home/u", ov["HOME"]) + } + if ov["XILINXD_LICENSE_FILE"] != "/a.lic" { + t.Errorf("XILINXD_LICENSE_FILE = %q, want /a.lic", ov["XILINXD_LICENSE_FILE"]) + } + if ov["LM_LICENSE_FILE"] != "2100@h" { + t.Errorf("LM_LICENSE_FILE = %q, want 2100@h", ov["LM_LICENSE_FILE"]) + } + if ov["PATH"] != "/u/bin" { + t.Errorf("PATH = %q, want /u/bin", ov["PATH"]) + } +} + +func TestResolveEnvOverrides_ProbeError_HomeOnly(t *testing.T) { + ov := resolveEnvOverrides("/home/u", fakeProbe("", errors.New("boom"))) + if len(ov) != 1 || ov["HOME"] != "/home/u" { + t.Fatalf("want HOME-only override on probe error, got %#v", ov) + } +} + +func TestResolveEnvOverrides_NilProbe_HomeOnly(t *testing.T) { + ov := resolveEnvOverrides("/home/u", nil) + if len(ov) != 1 || ov["HOME"] != "/home/u" { + t.Fatalf("want HOME-only override when probe is nil, got %#v", ov) + } +} + +func TestBuildSudoOverrides_HomeUnresolved_HardError(t *testing.T) { + _, err := buildSudoOverrides(1000, 1000, "", "/bin/bash", nil) + if !errors.Is(err, errHomeUnresolved) { + t.Fatalf("want errHomeUnresolved, got %v", err) + } +} + +func TestBuildSudoOverrides_Delegates(t *testing.T) { + ov, err := buildSudoOverrides(1000, 1000, "/home/u", "/bin/bash", fakeProbe("XILINXD_LICENSE_FILE=/a.lic\n", nil)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ov["XILINXD_LICENSE_FILE"] != "/a.lic" { + t.Errorf("probed license lost through buildSudoOverrides: %q", ov["XILINXD_LICENSE_FILE"]) + } + if ov["HOME"] != "/home/u" { + t.Errorf("HOME lost: %q", ov["HOME"]) + } +} + +func TestEnvOverrides_NotUnderSudo_Noop(t *testing.T) { + // Not root in tests, so this must be a no-op. + ov, err := envOverrides() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ov != nil { + t.Errorf("want nil override when not under sudo, got %#v", ov) + } +} + +func TestChownOutputs_NotUnderSudo_Noop(t *testing.T) { + // Not root -> chownOutputs must be a no-op and never touch files. + dir := t.TempDir() + bit := filepath.Join(dir, "out.bit") + if err := os.WriteFile(bit, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + if err := chownOutputs(dir); err != nil { + t.Fatalf("chownOutputs returned error when not under sudo: %v", err) + } + if _, err := os.Stat(bit); err != nil { + t.Errorf("output file should be untouched, got %v", err) + } +} + +func TestChownOutputs_NoMatches_NoError(t *testing.T) { + // Empty dir, no .bit/.bin -> no error, nothing to chown. + if err := chownOutputs(t.TempDir()); err != nil { + t.Fatalf("chownOutputs on empty dir returned error: %v", err) + } +} + +func TestChownOutputs_MissingDir_NoError(t *testing.T) { + // Best-effort: a missing output dir must not produce an error. + if err := chownOutputs(filepath.Join(t.TempDir(), "does-not-exist")); err != nil { + t.Fatalf("chownOutputs on missing dir returned error: %v", err) + } +} diff --git a/internal/vivado/reporter.go b/internal/vivado/reporter.go index 9b66c57..649ee93 100644 --- a/internal/vivado/reporter.go +++ b/internal/vivado/reporter.go @@ -114,7 +114,7 @@ func (e *LogEntry) IsBenign() bool { // ActionableEntries filters out INFO and known-benign entries. func (r *Report) ActionableEntries() []LogEntry { - var result []LogEntry + result := make([]LogEntry, 0, len(r.Entries)) for _, e := range r.Entries { if e.Severity == SeverityInfo { continue diff --git a/internal/vivado/vivado.go b/internal/vivado/vivado.go index 2aa1275..abf4f6a 100644 --- a/internal/vivado/vivado.go +++ b/internal/vivado/vivado.go @@ -2,8 +2,10 @@ package vivado import ( + "bytes" "context" "fmt" + "io" "log/slog" "os" "os/exec" @@ -95,19 +97,26 @@ func (v *Vivado) BinaryPath() string { return filepath.Join(v.Path, "bin", "vivado") } -// RunTCL executes a TCL script in Vivado batch mode with a timeout. +// RunTCL executes a TCL script in Vivado batch mode with a timeout. Under sudo +// the invoking user's license env is recovered via envOverrides; otherwise it's +// a no-op. func (v *Vivado) RunTCL(tclScript string, workDir string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() cmd := exec.CommandContext(ctx, v.BinaryPath(), "-mode", "batch", "-notrace", "-source", tclScript) cmd.Dir = workDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + var output bytes.Buffer + cmd.Stdout = io.MultiWriter(os.Stdout, &output) + cmd.Stderr = io.MultiWriter(os.Stderr, &output) - // Set up environment env := os.Environ() - env = append(env, fmt.Sprintf("XILINX_VIVADO=%s", v.Path)) + ov, err := envOverrides() + if err != nil { + return err + } + env = applyOverrides(env, ov) + env = setEnv(env, "XILINX_VIVADO", v.Path) cmd.Env = env slog.Info("running Vivado", "cmd", strings.Join(cmd.Args, " "), "dir", workDir, "timeout", timeout) @@ -119,5 +128,22 @@ func (v *Vivado) RunTCL(tclScript string, workDir string, timeout time.Duration) return fmt.Errorf("Vivado execution failed: %w", err) } + if line, ok := vivadoStartupFailure(output.String()); ok { + return fmt.Errorf("Vivado startup failed: %s", line) + } + return nil } + +func vivadoStartupFailure(output string) (string, bool) { + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.Contains(line, "application-specific initialization failed") { + return line, true + } + } + return "", false +} diff --git a/internal/vivado/vivado_test.go b/internal/vivado/vivado_test.go index 80e8232..e35dbb8 100644 --- a/internal/vivado/vivado_test.go +++ b/internal/vivado/vivado_test.go @@ -71,7 +71,9 @@ synth_design completed successfully route_design completed successfully write_bitstream completed successfully` - os.WriteFile(logPath, []byte(content), 0644) + if err := os.WriteFile(logPath, []byte(content), 0644); err != nil { + t.Fatal(err) + } report, err := ParseLogFile(logPath) if err != nil { @@ -110,3 +112,32 @@ func TestRunTCL_NonExistent(t *testing.T) { t.Error("RunTCL should fail with non-existent vivado") } } + +func TestRunTCL_ReturnsError_whenVivadoStartupFails(t *testing.T) { + installDir := t.TempDir() + binDir := filepath.Join(installDir, "bin") + if err := os.MkdirAll(binDir, 0755); err != nil { + t.Fatal(err) + } + vivadoPath := filepath.Join(binDir, "vivado") + script := `#!/bin/sh +echo 'application-specific initialization failed: couldn'\''t load file "librdi_commontasks.so": libtinfo.so.5: cannot open shared object file: No such file or directory' >&2 +exit 0 +` + if err := os.WriteFile(vivadoPath, []byte(script), 0755); err != nil { + t.Fatal(err) + } + + v := &Vivado{Path: installDir, Version: filepath.Base(installDir)} + err := v.RunTCL("vivado_generate_project.tcl", t.TempDir(), 5*time.Second) + + if err == nil { + t.Fatal("RunTCL should fail when Vivado reports a startup failure") + } + if !strings.Contains(err.Error(), "Vivado startup failed") { + t.Fatalf("RunTCL error = %q, want startup failure", err.Error()) + } + if !strings.Contains(err.Error(), "libtinfo.so.5") { + t.Fatalf("RunTCL error = %q, want loader detail", err.Error()) + } +} diff --git a/scripts/hdl-lint-negative-test.sh b/scripts/hdl-lint-negative-test.sh new file mode 100755 index 0000000..9ed1408 --- /dev/null +++ b/scripts/hdl-lint-negative-test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# verify verilator catches real errors (smoke test) +set -euo pipefail +command -v verilator >/dev/null 2>&1 || { echo "verilator missing"; exit 1; } + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +# undefined module +cat > "$TMP/uses_undefined.sv" <<'EOF' +module uses_undefined(input wire clk); + wire x; + nonexistent_module u_nonexistent(.clk(clk), .o(x)); +endmodule +EOF +if verilator --lint-only -Wno-fatal "$TMP/uses_undefined.sv" testdata/stubs/blackbox.sv \ + >"$TMP/a.log" 2>&1; then + echo "NEGATIVE TEST (a) FAILED: lint passed on undefined module"; exit 1 +fi +echo "OK (a): undefined module correctly fails lint" + +# syntax error +cat > "$TMP/syntax_err.sv" <<'EOF' +module syntax_err(input wire clk +endmodule +EOF +if verilator --lint-only -Wno-fatal "$TMP/syntax_err.sv" \ + >"$TMP/b.log" 2>&1; then + echo "NEGATIVE TEST (b) FAILED: lint passed on syntax error"; exit 1 +fi +echo "OK (b): syntax error correctly fails lint" + +# CQE field order +cat > "$TMP/cqe_bad.sv" <<'EOF' +module cqe_bad(input wire clk); + logic [31:0] cqe_dw2; + logic [15:0] sq_head; + logic [15:0] cmd_cid; + // Intentionally wrong: spec wants {sq_head, cmd_cid}. + assign cqe_dw2 = {cmd_cid, sq_head}; +endmodule +EOF +if verilator --lint-only --Wall -Wno-fatal "$TMP/cqe_bad.sv" \ + >"$TMP/c.log" 2>&1; then + # This is a WARNING-level issue (WIDTH), not an error. + # Verilator 5+ may pass with WIDTH only. Check for WIDTH in the log. + if grep -q 'WIDTH\|UNUSED\|UNDEF' "$TMP/c.log"; then + echo "OK (c): CQE-packing structural issue caught (WIDTH/UNUSED in log)" + else + echo "WARN (c): CQE-packing test produced no warnings — structural field-order" + echo " issues may need elaboration-level detection or sim (phase 2)." + fi +else + echo "OK (c): CQE-packing test correctly fails lint" +fi + +echo "" +echo "All negative checks passed." diff --git a/scripts/hdl-lint.sh b/scripts/hdl-lint.sh new file mode 100755 index 0000000..4fbcc0d --- /dev/null +++ b/scripts/hdl-lint.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# lint generated SV across all donor fixtures and boards +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +require() { command -v "$1" >/dev/null 2>&1 || { echo "missing dep: $1" >&2; exit 1; }; } +require verilator +require ./bin/pcileechgen + +FIXTURES=(testdata/donors/*.json) +# Board names come from the CLI so the matrix stays in sync with board.go. +# (NR>2 skips the two-line table header; bash 3.2 compat via while-read.) +BOARDS=() +while IFS= read -r line; do BOARDS+=("$line"); done < <( + ./bin/pcileechgen boards | awk 'NR>2 && $1!="----" && $1!="" && $1!="Total:"{print $1}' +) + +if [ "${#BOARDS[@]}" -eq 0 ]; then + echo "ERROR: no boards parsed from 'pcileechgen boards'" >&2 + exit 1 +fi + +# whitelist of svgen output files (primitives like pcileech_fifo.sv are blackboxed) +SVGEN_PATTERN='pcileech_bar_impl_device.sv|pcileech_tlps128_bar_controller.sv|pcileech_bar_impl_msi.sv|tlp_latency_emulator.sv|device_config.sv|pcileech_msix_table.sv|pcileech_nvme_admin_responder.sv|pcileech_nvme_dma_bridge.sv|pcileech_hda_rirb_dma.sv|pcileech_hda_msi.sv' + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +total=0; pass=0; skip=0; fail=0 + +for fixture in "${FIXTURES[@]}"; do + class="$(basename "$fixture" .json)" + for board in "${BOARDS[@]}"; do + total=$((total+1)) + cell="${class}×${board}" + out="$TMP/$class/$board" + mkdir -p "$out" + + build_log="$out/build.log" + if ! ./bin/pcileechgen build --from-json "$fixture" --board "$board" \ + --skip-vivado --output "$out" --force >"$build_log" 2>&1; then + # Donor BAR > board BRAM (or other benign incompatibility): skip, do not fail CI. + echo "SKIP $cell (build incompatible — see $build_log)" + skip=$((skip+1)) + continue + fi + + sv_files=() + while IFS= read -r f; do sv_files+=("$f"); done < <( + find "$out" -name '*.sv' -print \ + | grep -E "$SVGEN_PATTERN" \ + | sort + ) + + if [ "${#sv_files[@]}" -eq 0 ]; then + echo "SKIP $cell (no svgen SV emitted)" + skip=$((skip+1)) + continue + fi + + lint_log="$out/verilator.log" + if verilator --lint-only -Wno-fatal --top-module pcileech_bar_impl_device \ + +incdir+"$out/src" \ + "${sv_files[@]}" testdata/stubs/blackbox.sv \ + >"$lint_log" 2>&1; then + echo "PASS $cell" + pass=$((pass+1)) + else + echo "FAIL $cell (see $lint_log)" + cat "$lint_log" >&2 + fail=$((fail+1)) + fi + done +done + +echo "HDL lint: total=$total pass=$pass skip=$skip fail=$fail" +[ "$fail" -eq 0 ] diff --git a/sim/README.md b/sim/README.md new file mode 100644 index 0000000..980e521 --- /dev/null +++ b/sim/README.md @@ -0,0 +1,34 @@ +# sim/ — behavioral RTL unit simulations + +Self-contained Icarus Verilog testbenches for the behavioral modules that +address the emulation-gap audit (config-space W1C, MSI-X PBA, NVMe backing +store). These prove the *logic* of each module in simulation; they are not the +full PCILeech datapath (that needs the `pcileech-fpga` submodule + Vivado). + +## Run + + make sim # all modules + ./sim/run.sh # all modules + ./sim/run.sh cfg_w1c_shadow # one module + +Requires `iverilog` (`apt-get install iverilog`). Each `_tb.sv` is +compiled with its DUT `.sv`; a module passes only when its testbench +prints `ALL TESTS PASSED`. The harness exits non-zero on any failure and the +red path is verified (a wrong DUT makes the testbench report `FAIL`). + +## Modules + +| DUT | Audit gap it closes | +|-----|---------------------| +| `cfg_w1c_shadow.sv` | config-space registers are write-1-to-clear (shadow BRAM stored writes verbatim → detectable). Driven by `pcileech_cfgspace_w1cmask.coe`. | +| `msix_pba.sv` | MSI-X PBA was a static zero array; pending bits now set on masked requests and deliver on unmask. | +| `nvme_store.sv` | NVMe IO reads returned zeros and writes were discarded (Event 11); this is the BRAM sector cache the responder writes through. | +| `ahci_engine.sv` | SATA/AHCI had no command engine — PxCI never cleared, so storahci times out (Code 10 "I/O adapter hardware error"). Implements the slot-0 command FSM: IDENTIFY + READ/WRITE DMA over a sector store, D2H FIS, PxIS/intr, PxCI clear. | +| `xhci_ring_engine.sv` | xHCI had no Command/Event Ring engine, so usbxhci.sys hangs waiting for command completions. Implements the Command Ring walk (CRCR-latched dequeue pointer, Link TRB following, cycle-bit consumer state) answering No-Op Command (type 23) and Enable Slot Command (type 9) — plus a generic success fallback for anything else — with Command Completion Events (type 33) posted through the single-segment Event Ring (ERST fetch, producer cycle state), IMAN.IP/interrupt. | + +## Integrating into firmware + +These are reference modules. Wiring them into the generated SV (e.g. routing +`cfg_w1c_shadow` into the config-space shadow write path, instantiating +`nvme_store` in `pcileech_nvme_admin_responder.sv`) is the next step and must be +validated with Vivado synthesis + on-hardware testing. diff --git a/sim/ahci_engine.sv b/sim/ahci_engine.sv new file mode 100644 index 0000000..0104f07 --- /dev/null +++ b/sim/ahci_engine.sv @@ -0,0 +1,107 @@ +`default_nettype none +// AHCI port command engine (slot 0). On PxCI bit0 with PxCMD.ST+FRE set it +// walks the command list -> command table (H2D FIS) -> PRDT data base, then: +// IDENTIFY (0xEC) -> DMA identify block to DBA +// READ DMA EXT (0x25) -> DMA one sector from backing store to DBA +// WRITE DMA EXT (0x35) -> DMA one sector from DBA into backing store +// then writes a D2H Register FIS to PxFB+0x40, sets PxTFD ready, PxIS.DHRS, +// raises intr, and clears PxCI -- the handshake storahci waits on at init. +// (identify payload here is a placeholder pattern; the real donor-derived +// block is loaded from the codegen-emitted ahci_identify_init.hex.) +module ahci_engine #( + parameter SECT_DW = 128, // 512 bytes / sector + parameter NSECT = 4 +)( + input wire clk, + input wire rst_n, + input wire reg_wr, + input wire [11:0] reg_addr, + input wire [31:0] reg_wdata, + input wire [11:0] reg_raddr, + output reg [31:0] reg_rdata, + output reg mem_rd, + output reg [31:0] mem_addr, + input wire [31:0] mem_rdata, + input wire mem_rvalid, + output reg mem_wr, + output reg [31:0] mem_waddr, + output reg [31:0] mem_wdata, + output reg intr +); + localparam [11:0] A_PxCLB=12'h100, A_PxFB=12'h108, A_PxIS=12'h110, + A_PxIE=12'h114, A_PxCMD=12'h118, A_PxTFD=12'h120, + A_PxSIG=12'h124, A_PxSSTS=12'h128, A_PxCI=12'h138; + + reg [31:0] pxclb, pxfb, pxis, pxie, pxcmd, pxtfd, pxci; + wire [31:0] pxsig = 32'h00000101; + wire [31:0] pxssts = 32'h00000113; + + reg [31:0] store [0:NSECT*SECT_DW-1]; + + localparam S_IDLE=0,S_HDR=1,S_HDRW=2,S_F0=3,S_F0W=4,S_F1=5,S_F1W=6, + S_PRDT=7,S_PRDTW=8,S_WDATA=9,S_RD=10,S_RDW=11,S_FIS=12,S_DONE=13; + reg [3:0] st; + reg [31:0] ctba, dba, cmd, lba; + reg [8:0] cnt; + wire [13:0] sidx = lba[1:0]*SECT_DW + cnt; + + always @(*) begin + case (reg_raddr) + A_PxCLB: reg_rdata=pxclb; A_PxFB: reg_rdata=pxfb; + A_PxIS: reg_rdata=pxis; A_PxIE: reg_rdata=pxie; + A_PxCMD: reg_rdata=pxcmd; A_PxTFD:reg_rdata=pxtfd; + A_PxSIG: reg_rdata=pxsig; A_PxSSTS:reg_rdata=pxssts; + A_PxCI: reg_rdata=pxci; default:reg_rdata=32'h0; + endcase + end + + always @(posedge clk) begin + if (!rst_n) begin + pxclb<=0;pxfb<=0;pxis<=0;pxie<=0;pxcmd<=0;pxtfd<=32'h50;pxci<=0; + st<=S_IDLE;mem_rd<=0;mem_wr<=0;intr<=0;cnt<=0; + end else begin + mem_rd<=0; mem_wr<=0; intr<=0; + if (reg_wr) case (reg_addr) + A_PxCLB: pxclb<=reg_wdata; A_PxFB: pxfb<=reg_wdata; + A_PxIE: pxie<=reg_wdata; A_PxCMD:pxcmd<=reg_wdata; + A_PxIS: pxis<=pxis & ~reg_wdata; + A_PxCI: pxci<=pxci | reg_wdata; + default: ; + endcase + + case (st) + S_IDLE: if (pxci[0]&&pxcmd[0]&&pxcmd[4]) begin + pxtfd<=32'h80; mem_rd<=1; mem_addr<=pxclb+32'h8; st<=S_HDR; + end + S_HDR: st<=S_HDRW; + S_HDRW: if (mem_rvalid) begin ctba<=mem_rdata; mem_rd<=1; mem_addr<=mem_rdata; st<=S_F0; end + S_F0: st<=S_F0W; + S_F0W: if (mem_rvalid) begin cmd<=mem_rdata; mem_rd<=1; mem_addr<=ctba+32'h4; st<=S_F1; end + S_F1: st<=S_F1W; + S_F1W: if (mem_rvalid) begin lba<=mem_rdata; mem_rd<=1; mem_addr<=ctba+32'h80; st<=S_PRDT; end + S_PRDT: st<=S_PRDTW; + S_PRDTW:if (mem_rvalid) begin + dba<=mem_rdata; cnt<=0; + st<=(cmd[23:16]==8'h35)?S_RD:S_WDATA; // 0x35 write reads host first + end + // IDENTIFY / READ: produce data to DBA + S_WDATA: if (cnt>2] = 32'h00000200; // cmd header dw2 = CTBA + hm['h200>>2] = {8'h00, cmd, 16'h0027}; // H2D FIS dword0 + hm['h204>>2] = lba; // FIS dword1 (LBA) + hm['h280>>2] = dba; // PRDT[0] DBA + intr_seen=0; + wr(12'h138, 32'h1); // PxCI bit0 + reg_raddr<=12'h138; @(posedge clk); + i=0; while (reg_rdata!==0 && i<500) begin @(posedge clk); i=i+1; end + end + endtask + + initial begin + for (i=0;i<1024;i=i+1) hm[i]=0; + @(posedge clk); rst_n<=1; + wr(12'h100,32'h0); // PxCLB + wr(12'h108,32'h600); // PxFB + wr(12'h114,32'h1); // PxIE + wr(12'h118,32'h11); // PxCMD ST+FRE + + // 1) IDENTIFY -> data at DBA 0x400 + issue(8'hEC, 0, 32'h400); + reg_raddr<=12'h138; @(posedge clk); chk("identify: PxCI clear", reg_rdata, 0); + chk("identify data[0]", hm['h400>>2], 32'hA5000000); + chk("identify data[127]", hm[('h400>>2)+127], 32'hA500007F); + chk("identify intr", {31'h0,intr_seen}, 32'h1); + + // 2) WRITE sector 1 from host buffer 0x800 + for (i=0;i<128;i=i+1) hm[('h800>>2)+i] = 32'hBEEF0000 | i; + issue(8'h35, 1, 32'h800); + reg_raddr<=12'h138; @(posedge clk); chk("write: PxCI clear", reg_rdata, 0); + + // 3) READ sector 1 back to 0xA00 -> must equal what we wrote + for (i=0;i<128;i=i+1) hm[('hA00>>2)+i] = 32'h0; + issue(8'h25, 1, 32'hA00); + reg_raddr<=12'h138; @(posedge clk); chk("read: PxCI clear", reg_rdata, 0); + chk("read-back[0]", hm['hA00>>2], 32'hBEEF0000); + chk("read-back[63]", hm[('hA00>>2)+63], 32'hBEEF003F); + chk("read-back[127]", hm[('hA00>>2)+127], 32'hBEEF007F); + chk("D2H FIS", hm['h640>>2], 32'h00500034); + + if (errors==0) $display("ALL TESTS PASSED"); + else $display("%0d FAILURES", errors); + $finish; + end +endmodule +`default_nettype wire diff --git a/sim/cfg_w1c_shadow.sv b/sim/cfg_w1c_shadow.sv new file mode 100644 index 0000000..6cf285c --- /dev/null +++ b/sim/cfg_w1c_shadow.sv @@ -0,0 +1,14 @@ +`default_nettype none +module cfg_w1c_shadow ( + input wire [31:0] cur_val, + input wire [31:0] wr_data, + input wire [31:0] wr_mask, + input wire [31:0] w1c_mask, + output wire [31:0] new_val +); + wire [31:0] ro_keep = cur_val & ~wr_mask & ~w1c_mask; + wire [31:0] rw_take = wr_data & wr_mask & ~w1c_mask; + wire [31:0] w1c_keep = cur_val & w1c_mask & ~wr_data; + assign new_val = ro_keep | rw_take | w1c_keep; +endmodule +`default_nettype wire diff --git a/sim/cfg_w1c_shadow_tb.sv b/sim/cfg_w1c_shadow_tb.sv new file mode 100644 index 0000000..9b6125f --- /dev/null +++ b/sim/cfg_w1c_shadow_tb.sv @@ -0,0 +1,45 @@ +`default_nettype none +`timescale 1ns/1ps +module cfg_w1c_shadow_tb; + reg [31:0] cur, data, wmask, w1c; + wire [31:0] nv; + integer errors = 0; + + cfg_w1c_shadow dut (.cur_val(cur), .wr_data(data), .wr_mask(wmask), .w1c_mask(w1c), .new_val(nv)); + + task check(input [255:0] name, input [31:0] got, input [31:0] exp); + begin + if (got !== exp) begin + $display("FAIL %0s: got %08x exp %08x", name, got, exp); + errors = errors + 1; + end else begin + $display("ok %0s = %08x", name, got); + end + end + endtask + + initial begin + cur = 32'hF900_0000; data = 32'h0800_0000; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("w1c clears written bit", nv, 32'hF100_0000); + + cur = 32'hF900_0000; data = 32'h0000_0000; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("w1c keeps unwritten bits", nv, 32'hF900_0000); + + cur = 32'hF900_0000; data = 32'hF900_0000; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("w1c full clear", nv, 32'h0000_0000); + + cur = 32'h0000_0000; data = 32'h0000_0002; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("rw bit takes data", nv, 32'h0000_0002); + + cur = 32'h0000_0000; data = 32'h0000_0001; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("ro bit ignored", nv, 32'h0000_0000); + + cur = 32'hF900_0000; data = 32'h0800_0002; wmask = 32'hFFFF_0006; w1c = 32'hF900_0000; + #1; check("mixed w1c+rw", nv, 32'hF100_0002); + + if (errors == 0) $display("ALL TESTS PASSED"); + else $display("%0d FAILURES", errors); + $finish; + end +endmodule +`default_nettype wire diff --git a/sim/msix_pba.sv b/sim/msix_pba.sv new file mode 100644 index 0000000..14806d8 --- /dev/null +++ b/sim/msix_pba.sv @@ -0,0 +1,45 @@ +`default_nettype none +module msix_pba #(parameter NVEC = 4) ( + input wire clk, + input wire rst_n, + input wire [NVEC-1:0] vector_mask, + input wire req_valid, + input wire [$clog2(NVEC)-1:0] req_vec, + output reg [NVEC-1:0] pba, + output reg deliver_valid, + output reg [$clog2(NVEC)-1:0] deliver_vec +); + localparam VW = $clog2(NVEC); + + reg found; + integer i; + + always @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + pba <= {NVEC{1'b0}}; + deliver_valid <= 1'b0; + deliver_vec <= {VW{1'b0}}; + end else begin + deliver_valid <= 1'b0; + deliver_vec <= {VW{1'b0}}; + + if (req_valid && vector_mask[req_vec]) begin + pba[req_vec] <= 1'b1; + end else if (req_valid) begin + deliver_valid <= 1'b1; + deliver_vec <= req_vec; + end else begin + found = 1'b0; + for (i = 0; i < NVEC; i = i + 1) begin + if (!found && !vector_mask[i] && pba[i]) begin + deliver_valid <= 1'b1; + deliver_vec <= i[VW-1:0]; + pba[i] <= 1'b0; + found = 1'b1; + end + end + end + end + end +endmodule +`default_nettype wire diff --git a/sim/msix_pba_tb.sv b/sim/msix_pba_tb.sv new file mode 100644 index 0000000..9517e4d --- /dev/null +++ b/sim/msix_pba_tb.sv @@ -0,0 +1,81 @@ +`default_nettype none +`timescale 1ns/1ps +module msix_pba_tb; + localparam NVEC = 4; + localparam VW = $clog2(NVEC); + + reg clk = 0; + reg rst_n = 0; + reg [NVEC-1:0] vector_mask = 0; + reg req_valid = 0; + reg [VW-1:0] req_vec = 0; + wire [NVEC-1:0] pba; + wire deliver_valid; + wire [VW-1:0] deliver_vec; + + integer errors = 0; + + msix_pba #(.NVEC(NVEC)) dut ( + .clk(clk), .rst_n(rst_n), + .vector_mask(vector_mask), + .req_valid(req_valid), .req_vec(req_vec), + .pba(pba), .deliver_valid(deliver_valid), .deliver_vec(deliver_vec) + ); + + always #5 clk = ~clk; + + task check(input [255:0] name, input [31:0] got, input [31:0] exp); + begin + if (got !== exp) begin + $display("FAIL %0s: got %08x exp %08x", name, got, exp); + errors = errors + 1; + end else begin + $display("ok %0s = %08x", name, got); + end + end + endtask + + initial begin + rst_n = 0; + repeat (2) @(posedge clk); + rst_n = 1; + @(posedge clk); #1; + check("reset clears pba", {28'b0, pba}, 32'h0); + + vector_mask = 4'b0010; + req_valid = 1; req_vec = 2'd1; + @(posedge clk); #1; + req_valid = 0; + check("T1 pba[1] set on masked req", {31'b0, pba[1]}, 32'h1); + check("T1 deliver_valid=0 (no deliver)",{31'b0, deliver_valid}, 32'h0); + + vector_mask = 4'b0000; + req_valid = 1; req_vec = 2'd2; + @(posedge clk); #1; + req_valid = 0; + check("T2 deliver_valid=1", {31'b0, deliver_valid}, 32'h1); + check("T2 deliver_vec=2", {30'b0, deliver_vec}, 32'h2); + check("T2 pba[2] stays 0", {31'b0, pba[2]}, 32'h0); + + check("T3 pre: pba[1] still pending", {31'b0, pba[1]}, 32'h1); + @(posedge clk); #1; + check("T3 deliver_valid=1 on unmask", {31'b0, deliver_valid}, 32'h1); + check("T3 deliver_vec=1", {30'b0, deliver_vec}, 32'h1); + check("T3 pba[1] cleared", {31'b0, pba[1]}, 32'h0); + @(posedge clk); #1; + check("T3 deliver_valid falls next cycle",{31'b0,deliver_valid},32'h0); + + vector_mask = 4'b1111; + req_valid = 1; req_vec = 2'd0; @(posedge clk); #1; req_valid = 0; + req_valid = 1; req_vec = 2'd3; @(posedge clk); #1; req_valid = 0; + check("T4 pre: pba has pending bits", {31'b0, |pba}, 32'h1); + rst_n = 0; @(posedge clk); #1; + rst_n = 1; @(posedge clk); #1; + check("T4 pba cleared by reset", {28'b0, pba}, 32'h0); + + if (errors == 0) $display("ALL TESTS PASSED"); + else $display("%0d FAILURES", errors); + $finish; + end +endmodule +`default_nettype wire diff --git a/sim/nvme_store.sv b/sim/nvme_store.sv new file mode 100644 index 0000000..5afecac --- /dev/null +++ b/sim/nvme_store.sv @@ -0,0 +1,38 @@ +`default_nettype none +module nvme_store #( + parameter NSECT = 4, + parameter SECT_DW = 8 +) ( + input wire clk, + input wire [$clog2(NSECT)-1:0] wr_sector, + input wire [$clog2(SECT_DW)-1:0] wr_dw, + input wire [31:0] wr_data, + input wire wr_en, + input wire [$clog2(NSECT)-1:0] rd_sector, + input wire [$clog2(SECT_DW)-1:0] rd_dw, + input wire rd_en, + output reg [31:0] rd_data, + output reg rd_valid +); + localparam DEPTH = NSECT * SECT_DW; + localparam AW = $clog2(NSECT) + $clog2(SECT_DW); + + reg [31:0] mem [0:DEPTH-1]; + integer i; + initial begin + for (i = 0; i < DEPTH; i = i + 1) + mem[i] = 32'h0000_0000; + end + + wire [AW-1:0] wr_addr = {wr_sector, wr_dw}; + wire [AW-1:0] rd_addr = {rd_sector, rd_dw}; + + always @(posedge clk) begin + if (wr_en) + mem[wr_addr] <= wr_data; + rd_valid <= rd_en; + if (rd_en) + rd_data <= mem[rd_addr]; + end +endmodule +`default_nettype wire diff --git a/sim/nvme_store_tb.sv b/sim/nvme_store_tb.sv new file mode 100644 index 0000000..476d1f4 --- /dev/null +++ b/sim/nvme_store_tb.sv @@ -0,0 +1,89 @@ +`default_nettype none +`timescale 1ns/1ps +module nvme_store_tb; + localparam NSECT = 4; + localparam SECT_DW = 8; + + reg clk = 0; + always #5 clk = ~clk; + + reg wr_en = 0; + reg [1:0] wr_sector = 0; + reg [2:0] wr_dw = 0; + reg [31:0] wr_data = 0; + reg rd_en = 0; + reg [1:0] rd_sector = 0; + reg [2:0] rd_dw = 0; + wire [31:0] rd_data; + wire rd_valid; + + nvme_store #(.NSECT(NSECT), .SECT_DW(SECT_DW)) dut ( + .clk(clk), + .wr_en(wr_en), .wr_sector(wr_sector), .wr_dw(wr_dw), .wr_data(wr_data), + .rd_en(rd_en), .rd_sector(rd_sector), .rd_dw(rd_dw), + .rd_data(rd_data), .rd_valid(rd_valid) + ); + + integer errors = 0; + + task check; + input [255:0] name; + input [31:0] got; + input [31:0] exp; + begin + if (got !== exp) begin + $display("FAIL %0s: got %08x exp %08x", name, got, exp); + errors = errors + 1; + end else begin + $display("ok %0s = %08x", name, got); + end + end + endtask + + task do_write; + input [1:0] sect; + input [2:0] dw; + input [31:0] data; + begin + wr_en = 1; wr_sector = sect; wr_dw = dw; wr_data = data; + @(posedge clk); #1; + wr_en = 0; + end + endtask + + task do_read; + input [1:0] sect; + input [2:0] dw; + begin + rd_en = 1; rd_sector = sect; rd_dw = dw; + @(posedge clk); #1; + rd_en = 0; + end + endtask + + initial begin + @(posedge clk); #1; + + do_write(0, 0, 32'hDEAD_BEEF); + do_read(0, 0); + check("write-readback", rd_data, 32'hDEAD_BEEF); + check("rd_valid-high", {31'd0, rd_valid}, 32'h0000_0001); + + do_read(2, 5); + check("unwritten-zero", rd_data, 32'h0000_0000); + + do_write(0, 0, 32'hCAFE_F00D); + do_read(1, 0); + check("no-alias-sector", rd_data, 32'h0000_0000); + + do_write(0, 0, 32'h1234_5678); + do_write(0, 0, 32'hABCD_EF01); + do_read(0, 0); + check("overwrite-new", rd_data, 32'hABCD_EF01); + + if (errors == 0) $display("ALL TESTS PASSED"); + else $display("%0d FAILURES", errors); + $finish; + end +endmodule +`default_nettype wire diff --git a/sim/run.sh b/sim/run.sh new file mode 100755 index 0000000..f4f506f --- /dev/null +++ b/sim/run.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -uo pipefail +cd "$(dirname "$0")" +mkdir -p build + +command -v iverilog >/dev/null 2>&1 || { echo "iverilog not installed (apt-get install iverilog)"; exit 127; } + +if [ "$#" -gt 0 ]; then + tbs=() + for n in "$@"; do tbs+=("${n%_tb.sv}_tb.sv"); done +else + tbs=(*_tb.sv) +fi + +fail=0 +for tb in "${tbs[@]}"; do + base="${tb%_tb.sv}" + dut="${base}.sv" + if [ ! -f "$tb" ]; then echo "[MISS] testbench $tb not found"; fail=1; continue; fi + if [ ! -f "$dut" ]; then echo "[MISS] DUT $dut not found"; fail=1; continue; fi + if ! iverilog -g2012 -Wall -o "build/${base}.vvp" "$dut" "$tb" 2>"build/${base}.log"; then + echo "[FAIL] $base (compile)"; cat "build/${base}.log"; fail=1; continue + fi + out="$(vvp "build/${base}.vvp" 2>&1)" + if echo "$out" | grep -q "ALL TESTS PASSED"; then + echo "[PASS] $base" + else + echo "[FAIL] $base"; echo "$out"; fail=1 + fi +done + +if [ "$fail" -eq 0 ]; then echo "== sim: all modules passed =="; else echo "== sim: FAILURES =="; fi +exit $fail diff --git a/sim/xhci_ring_engine.sv b/sim/xhci_ring_engine.sv new file mode 100644 index 0000000..7fd9111 --- /dev/null +++ b/sim/xhci_ring_engine.sv @@ -0,0 +1,241 @@ +`default_nettype none +// xHCI Command Ring + Event Ring engine. +// +// Emulates the HOST CONTROLLER side of the xHCI init handshake with +// xhci.sys -- there is no downstream USB device attached (PORTSC CCS=0 on +// both ports, see xhci.go's xhciProfile), so this does not pretend to +// enumerate a real device. It only has to: +// - latch the Command Ring pointer + cycle state from CRCR on the first +// doorbell ring (CRCR is only consumed once, while the ring is stopped) +// - walk the ring on each Command doorbell (DB0) write, following Link +// TRBs (type 6) and consuming every TRB whose cycle bit matches the +// ring's current consumer cycle state, until it catches up (cycle +// mismatch = ring drained) +// - answer No-Op Command (type 23) and Enable Slot Command (type 9) with +// a Command Completion Event (type 33), and fall back to a generic +// Success completion (echoing the TRB's own Slot ID field) for anything +// else so a command never just hangs the driver +// - post events through the one-segment Event Ring described by the ERST +// entry at ERSTBA (fetched lazily, invalidated whenever ERSTBA is +// rewritten), tracking its own enqueue index + producer cycle state +// - set IMAN.IP and pulse an interrupt (gated on IMAN.IE) per completion +// +// ponytail: single fixed Slot ID (FIXED_SLOT_ID) for Enable Slot Command -- +// with zero devices attached there is at most one plausible slot in flight, +// not a real free-list across HCSPARAMS1.MaxSlots. ERDP.EHB and CRCR.CRR +// are not modeled: neither is polled during a no-device init handshake, +// only real-hardware backpressure/abort bookkeeping the driver doesn't need +// to see here. +module xhci_ring_engine #( + parameter [7:0] FIXED_SLOT_ID = 8'h01 +)( + input wire clk, + input wire rst_n, + + input wire reg_wr, + input wire [11:0] reg_addr, + input wire [31:0] reg_wdata, + input wire [11:0] reg_raddr, + output reg [31:0] reg_rdata, + + output reg mem_rd, + output reg [31:0] mem_addr, + input wire [31:0] mem_rdata, + input wire mem_rvalid, + output reg mem_wr, + output reg [31:0] mem_waddr, + output reg [31:0] mem_wdata, + + output reg intr +); + localparam [11:0] A_DB0 = 12'h100; // Command doorbell (DBOFF fixed 0x100) + localparam [11:0] A_CRCR_LO = 12'h038; + localparam [11:0] A_CRCR_HI = 12'h03C; + localparam [11:0] A_ERSTBA_LO = 12'h230; // RTSOFF(0x200) + IR0 ERSTBA (0x30) + localparam [11:0] A_ERSTBA_HI = 12'h234; + localparam [11:0] A_IMAN = 12'h220; // RTSOFF(0x200) + IR0 IMAN (0x00... +0x20 IR0 base) + + localparam [5:0] TRB_LINK = 6'd6; + localparam [5:0] TRB_ENSLOT = 6'd9; + localparam [5:0] TRB_NOOP = 6'd23; + localparam [5:0] TRB_CMD_EVENT = 6'd33; + + localparam [3:0] S_IDLE = 4'h0; + localparam [3:0] S_FETCH = 4'h1; + localparam [3:0] S_FETCHW = 4'h2; + localparam [3:0] S_DECODE = 4'h3; + localparam [3:0] S_ERST = 4'h4; + localparam [3:0] S_ERSTW = 4'h5; + localparam [3:0] S_EVT = 4'h6; + localparam [3:0] S_POST = 4'h7; + + // driver-programmed registers + reg [31:0] crcr_lo, crcr_hi; + reg [31:0] erstba_lo, erstba_hi; + reg [1:0] iman; // bit0=IP (RW1C, HW-set), bit1=IE (RW) + + // command ring consumer state + reg cr_started; + reg [31:0] crdp; // command ring dequeue pointer (32-bit: sim host address space) + reg ccs; // consumer cycle state + + // event ring producer state + reg erst_valid; + reg [31:0] erst_base; + reg [15:0] erst_size; + reg [15:0] evt_idx; + reg ecs; // producer cycle state (starts at 1 per spec) + + reg [3:0] state; + reg [1:0] cnt; + reg [31:0] trb0, trb1, trb2, trb3; + reg [31:0] cmd_trb_addr; + reg [7:0] slot_id_out; + + wire [31:0] evt_addr = erst_base + {16'h0, evt_idx, 4'b0}; + + always @(*) begin + case (reg_raddr) + A_CRCR_LO: reg_rdata = crcr_lo; + A_CRCR_HI: reg_rdata = crcr_hi; + A_ERSTBA_LO: reg_rdata = erstba_lo; + A_ERSTBA_HI: reg_rdata = erstba_hi; + A_IMAN: reg_rdata = {30'h0, iman}; + default: reg_rdata = 32'h0; + endcase + end + + always @(posedge clk) begin + if (!rst_n) begin + crcr_lo <= 32'h0; crcr_hi <= 32'h0; + erstba_lo <= 32'h0; erstba_hi <= 32'h0; + iman <= 2'b00; + cr_started <= 1'b0; crdp <= 32'h0; ccs <= 1'b1; + erst_valid <= 1'b0; erst_base <= 32'h0; erst_size <= 16'h0; + evt_idx <= 16'h0; ecs <= 1'b1; + state <= S_IDLE; cnt <= 2'h0; + mem_rd <= 1'b0; mem_wr <= 1'b0; intr <= 1'b0; + end else begin + mem_rd <= 1'b0; mem_wr <= 1'b0; intr <= 1'b0; + + if (reg_wr) case (reg_addr) + A_CRCR_LO: if (!cr_started) crcr_lo <= reg_wdata & 32'hFFFFFFC7; + A_CRCR_HI: if (!cr_started) crcr_hi <= reg_wdata; + A_ERSTBA_LO: begin erstba_lo <= reg_wdata & 32'hFFFFFFF0; erst_valid <= 1'b0; end + A_ERSTBA_HI: begin erstba_hi <= reg_wdata; erst_valid <= 1'b0; end + A_IMAN: begin + iman[1] <= reg_wdata[1]; // IE, plain RW + if (reg_wdata[0]) iman[0] <= 1'b0; // IP, write-1-to-clear + end + default: ; + endcase + + case (state) + // ---- wait for a Command doorbell ring ------------------ + S_IDLE: if (reg_wr && reg_addr == A_DB0) begin + if (!cr_started) begin + crdp <= {crcr_lo[31:6], 6'b0}; + ccs <= crcr_lo[0]; + cr_started <= 1'b1; + end + cnt <= 2'h0; + state <= S_FETCH; + end + + // ---- pull 16 bytes (one TRB) from the command ring ----- + S_FETCH: begin + mem_rd <= 1'b1; + mem_addr <= crdp + {28'h0, cnt, 2'b00}; + state <= S_FETCHW; + end + S_FETCHW: if (mem_rvalid) begin + case (cnt) + 2'd0: trb0 <= mem_rdata; + 2'd1: trb1 <= mem_rdata; + 2'd2: trb2 <= mem_rdata; + 2'd3: trb3 <= mem_rdata; + endcase + if (cnt == 2'd3) begin cnt <= 2'h0; state <= S_DECODE; end + else begin cnt <= cnt + 2'd1; state <= S_FETCH; end + end + + // ---- ring caught up, follow Link, or execute a command - + S_DECODE: begin + if (trb3[0] != ccs) begin + // cycle bit doesn't match -- nothing new queued + state <= S_IDLE; + end else if (trb3[15:10] == TRB_LINK) begin + crdp <= trb0 & 32'hFFFFFFF0; + if (trb3[1]) ccs <= ~ccs; // Toggle Cycle + cnt <= 2'h0; + state <= S_FETCH; + end else begin + cmd_trb_addr <= crdp; + crdp <= crdp + 32'd16; + if (trb3[15:10] == TRB_NOOP) + slot_id_out <= 8'h00; + else if (trb3[15:10] == TRB_ENSLOT) + slot_id_out <= FIXED_SLOT_ID; + else + slot_id_out <= trb3[31:24]; // everything else: echo, say yes + cnt <= 2'h0; + state <= erst_valid ? S_EVT : S_ERST; + end + end + + // ---- fetch the (single) Event Ring Segment Table entry - + S_ERST: begin + mem_rd <= 1'b1; + mem_addr <= erstba_lo + {28'h0, cnt, 2'b00}; + state <= S_ERSTW; + end + S_ERSTW: if (mem_rvalid) begin + case (cnt) + 2'd0: erst_base <= mem_rdata; + 2'd2: erst_size <= mem_rdata[15:0]; + default: ; + endcase + if (cnt == 2'd3) begin + erst_valid <= 1'b1; + cnt <= 2'h0; + state <= S_EVT; + end else begin + cnt <= cnt + 2'd1; + state <= S_ERST; + end + end + + // ---- write the 16-byte Command Completion Event -------- + S_EVT: begin + mem_wr <= 1'b1; + mem_waddr <= evt_addr + {28'h0, cnt, 2'b00}; + case (cnt) + 2'd0: mem_wdata <= cmd_trb_addr; + 2'd1: mem_wdata <= 32'h0; + 2'd2: mem_wdata <= {8'h01, 24'h0}; // Completion Code = Success + 2'd3: mem_wdata <= {slot_id_out, 8'h00, TRB_CMD_EVENT, 9'h0, ecs}; + endcase + if (cnt == 2'd3) begin cnt <= 2'h0; state <= S_POST; end + else cnt <= cnt + 2'd1; + end + + // ---- advance the event ring, raise the interrupt, loop - + S_POST: begin + iman[0] <= 1'b1; // IP + intr <= iman[1]; // gated on IE + if (evt_idx + 16'd1 >= erst_size) begin + evt_idx <= 16'h0; + ecs <= ~ecs; + end else begin + evt_idx <= evt_idx + 16'd1; + end + cnt <= 2'h0; + state <= S_FETCH; // drain loop: check for more queued TRBs + end + + default: state <= S_IDLE; + endcase + end + end +endmodule +`default_nettype wire diff --git a/sim/xhci_ring_engine_tb.sv b/sim/xhci_ring_engine_tb.sv new file mode 100644 index 0000000..969a24e --- /dev/null +++ b/sim/xhci_ring_engine_tb.sv @@ -0,0 +1,130 @@ +`default_nettype none +`timescale 1ns/1ps +module xhci_ring_engine_tb; + reg clk=0, rst_n=0; + reg reg_wr=0; + reg [11:0] reg_addr=0, reg_raddr=0; + reg [31:0] reg_wdata=0; + wire [31:0] reg_rdata; + wire mem_rd, mem_wr; + wire [31:0] mem_addr, mem_waddr, mem_wdata; + reg [31:0] mem_rdata; reg mem_rvalid; + wire intr; + integer errors=0, i; + + xhci_ring_engine dut(.clk(clk),.rst_n(rst_n), + .reg_wr(reg_wr),.reg_addr(reg_addr),.reg_wdata(reg_wdata), + .reg_raddr(reg_raddr),.reg_rdata(reg_rdata), + .mem_rd(mem_rd),.mem_addr(mem_addr),.mem_rdata(mem_rdata),.mem_rvalid(mem_rvalid), + .mem_wr(mem_wr),.mem_waddr(mem_waddr),.mem_wdata(mem_wdata),.intr(intr)); + + always #5 clk=~clk; + + reg [31:0] hm [0:16383]; + always @(posedge clk) begin + mem_rvalid <= mem_rd; + if (mem_rd) mem_rdata <= hm[mem_addr[15:2]]; + if (mem_wr) hm[mem_waddr[15:2]] <= mem_wdata; + end + + reg intr_seen=0; + always @(posedge clk) if (intr) intr_seen<=1; + + task wr(input [11:0] a, input [31:0] d); + begin @(posedge clk); reg_wr<=1; reg_addr<=a; reg_wdata<=d; + @(posedge clk); reg_wr<=0; end + endtask + task chk(input [255:0] n, input [31:0] g, input [31:0] e); + begin if (g!==e) begin $display("FAIL %0s: got %08x exp %08x",n,g,e); errors=errors+1; end + else $display("ok %0s = %08x",n,g); end + endtask + // ring the command doorbell, wait for the completion interrupt, then + // let the FSM's trailing "check for more queued TRBs" drain pass settle + // back to idle before handing control back (it keeps walking the ring + // for a few cycles after posting the event). + task ring_doorbell; + begin + intr_seen = 0; + wr(12'h100, 32'h0); + i=0; while (!intr_seen && i<500) begin @(posedge clk); i=i+1; end + repeat (20) @(posedge clk); + end + endtask + + initial begin + for (i=0;i<16384;i=i+1) hm[i]=0; + @(posedge clk); rst_n<=1; + + // Command Ring base 0x1000, RCS=1 + wr(12'h038, 32'h00001001); + wr(12'h03C, 32'h00000000); + + // Event Ring Segment Table at 0x2000: {base=0x3000, hi=0, size=16} + hm['h2000>>2] = 32'h00003000; + hm[('h2000>>2)+1] = 32'h00000000; + hm[('h2000>>2)+2] = 32'h00000010; // ERSTSZ = 16 TRBs + wr(12'h230, 32'h00002000); + wr(12'h234, 32'h00000000); + + // enable interrupts + wr(12'h220, 32'h00000002); // IMAN.IE=1 + + // ---- 1) No-Op Command at 0x1000 (type 23, cycle=1) ---- + hm['h1000>>2] = 32'h0; + hm[('h1000>>2)+1] = 32'h0; + hm[('h1000>>2)+2] = 32'h0; + hm[('h1000>>2)+3] = 32'h00005C01; // type=23<<10=0x5C00, cycle=1 + + ring_doorbell(); + chk("noop: intr fired", {31'h0, intr_seen}, 32'h1); + chk("noop: evt[0] dw0 (trb ptr)", hm['h3000>>2], 32'h00001000); + chk("noop: evt[0] dw1", hm[('h3000>>2)+1], 32'h00000000); + chk("noop: evt[0] dw2 (compl)", hm[('h3000>>2)+2], 32'h01000000); + chk("noop: evt[0] dw3 (type/slot/cycle)", hm[('h3000>>2)+3], 32'h00008401); + + reg_raddr<=12'h220; @(posedge clk); #1; + chk("noop: IMAN.IP set", reg_rdata, 32'h00000003); + + // driver acks the interrupt (W1C bit0), IE stays set + wr(12'h220, 32'h00000003); + reg_raddr<=12'h220; @(posedge clk); #1; + chk("noop: IMAN.IP cleared", reg_rdata, 32'h00000002); + + // ---- 2) Enable Slot Command at 0x1010 (type 9, cycle=1) ---- + hm['h1010>>2] = 32'h0; + hm[('h1010>>2)+1] = 32'h0; + hm[('h1010>>2)+2] = 32'h0; + hm[('h1010>>2)+3] = 32'h00002401; // type=9<<10=0x2400, cycle=1 + + ring_doorbell(); + chk("enslot: evt[1] dw0 (trb ptr)", hm[('h3010>>2)], 32'h00001010); + chk("enslot: evt[1] dw3 (slot=1)", hm[('h3010>>2)+3], 32'h01008401); + + // ---- 3) Link TRB (TC=1) back to 0x1000, then a new No-Op with + // cycle=0 (ccs will have toggled) ----------------------- + hm['h1020>>2] = 32'h00001000; // link target + hm[('h1020>>2)+1] = 32'h0; + hm[('h1020>>2)+2] = 32'h0; + hm[('h1020>>2)+3] = 32'h00001803; // type=6<<10=0x1800, TC=1, cycle=1 + + hm['h1000>>2] = 32'h0; + hm[('h1000>>2)+1] = 32'h0; + hm[('h1000>>2)+2] = 32'h0; + hm[('h1000>>2)+3] = 32'h00005C00; // type=23<<10=0x5C00, cycle=0 (post-toggle) + + ring_doorbell(); + chk("link+noop: evt[2] dw0 (trb ptr)", hm[('h3020>>2)], 32'h00001000); + chk("link+noop: evt[2] dw3", hm[('h3020>>2)+3], 32'h00008401); + + // ---- register readback sanity ---- + reg_raddr<=12'h038; @(posedge clk); #1; + chk("CRCR_LO readback", reg_rdata, 32'h00001001); + reg_raddr<=12'h230; @(posedge clk); #1; + chk("ERSTBA_LO readback", reg_rdata, 32'h00002000); + + if (errors==0) $display("ALL TESTS PASSED"); + else $display("%0d FAILURES", errors); + $finish; + end +endmodule +`default_nettype wire diff --git a/testdata/.verilator.vlt b/testdata/.verilator.vlt new file mode 100644 index 0000000..e4d55c9 --- /dev/null +++ b/testdata/.verilator.vlt @@ -0,0 +1,8 @@ +`verilator_config` + +/* verilator lint_off DECLFILENAME */ +/* verilator lint_off UNUSED */ +/* verilator lint_off CASEINCOMPLETE */ +/* verilator lint_off PINMISSING */ +/* verilator lint_off BLKSEQ */ +/* verilator lint_off WIDTH */ diff --git a/testdata/donors/audio.json b/testdata/donors/audio.json new file mode 100644 index 0000000..53310bd --- /dev/null +++ b/testdata/donors/audio.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 40305, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 262912, + "header_type": 0 + }, + "config_space_hex": [ + "9d718086", + "00100000", + "04030001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294950912, + "address": 0, + "size": 16384, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/ethernet.json b/testdata/donors/ethernet.json new file mode 100644 index 0000000..104716a --- /dev/null +++ b/testdata/donors/ethernet.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 5559, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 131072, + "header_type": 0 + }, + "config_space_hex": [ + "15b78086", + "00100000", + "02000001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294836224, + "address": 0, + "size": 131072, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/generic.json b/testdata/donors/generic.json new file mode 100644 index 0000000..ce98f0f --- /dev/null +++ b/testdata/donors/generic.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 4660, + "device_id": 22136, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 0, + "header_type": 0 + }, + "config_space_hex": [ + "56781234", + "00100000", + "00000001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294963200, + "address": 0, + "size": 4096, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/gpu.json b/testdata/donors/gpu.json new file mode 100644 index 0000000..6eaaa44 --- /dev/null +++ b/testdata/donors/gpu.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 4318, + "device_id": 6918, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 196608, + "header_type": 0 + }, + "config_space_hex": [ + "1b0610de", + "00100000", + "03000001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294950912, + "address": 0, + "size": 16384, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/nvme.json b/testdata/donors/nvme.json new file mode 100644 index 0000000..29bdc51 --- /dev/null +++ b/testdata/donors/nvme.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 5197, + "device_id": 43017, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 67586, + "header_type": 0 + }, + "config_space_hex": [ + "a809144d", + "00100000", + "01080201", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294950912, + "address": 0, + "size": 16384, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/sata.json b/testdata/donors/sata.json new file mode 100644 index 0000000..0d64c7a --- /dev/null +++ b/testdata/donors/sata.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 40195, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 67073, + "header_type": 0 + }, + "config_space_hex": [ + "9d038086", + "00100000", + "01060101", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294959104, + "address": 0, + "size": 8192, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/thunderbolt.json b/testdata/donors/thunderbolt.json new file mode 100644 index 0000000..e78e67c --- /dev/null +++ b/testdata/donors/thunderbolt.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 5593, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 526080, + "header_type": 0 + }, + "config_space_hex": [ + "15d98086", + "00100000", + "08070001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294950912, + "address": 0, + "size": 16384, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/wifi.json b/testdata/donors/wifi.json new file mode 100644 index 0000000..7e35292 --- /dev/null +++ b/testdata/donors/wifi.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 9469, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 163840, + "header_type": 0 + }, + "config_space_hex": [ + "24fd8086", + "00100000", + "02800001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294959104, + "address": 0, + "size": 8192, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/donors/xhci.json b/testdata/donors/xhci.json new file mode 100644 index 0000000..c838f62 --- /dev/null +++ b/testdata/donors/xhci.json @@ -0,0 +1,115 @@ +{ + "collected_at": "2026-01-01T00:00:00Z", + "tool_version": "b4ab213-7-ga15c776", + "hostname": "ci-synthetic", + "device": { + "bdf": { + "Domain": 0, + "Bus": 3, + "Device": 0, + "Function": 0 + }, + "vendor_id": 32902, + "device_id": 40239, + "subsys_vendor_id": 0, + "subsys_device_id": 0, + "revision_id": 0, + "class_code": 787248, + "header_type": 0 + }, + "config_space_hex": [ + "9d2f8086", + "00100000", + "0c033001", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000040", + "00000000", + "00000000", + "00034801", + "00000008", + "00005405", + "00000000", + "00000000", + "00020010", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000", + "00000000" + ], + "config_space_size": 256, + "bars": [ + { + "index": 0, + "raw_value": 4294901760, + "address": 0, + "size": 65536, + "type": "mem32", + "prefetchable": false, + "is_64bit": false + } + ], + "capabilities": [ + { + "id": 1, + "offset": 64, + "data": "AUgDAAgAAAA=" + }, + { + "id": 5, + "offset": 72, + "data": "BVQAAAAAAAAAAAAA" + }, + { + "id": 16, + "offset": 84, + "data": "EAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + } + ] +} \ No newline at end of file diff --git a/testdata/stubs/blackbox.sv b/testdata/stubs/blackbox.sv new file mode 100644 index 0000000..ac28244 --- /dev/null +++ b/testdata/stubs/blackbox.sv @@ -0,0 +1,15 @@ +// stub xilinx modules so verilator can resolve svgen port refs +`timescale 1ns/1ps +`default_nettype none + +module pcileech_pcie_cfg_a7 ( + input wire clk, + input wire rst +); +endmodule + +module pcileech_fifo ( + input wire clk, + input wire rst +); +endmodule