From 9322766cef43a53dcd057a55b26bc7b3d9e37717 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sun, 22 Feb 2026 21:27:40 +0100 Subject: [PATCH] vibed jtagram with script as drop in replacement of serving_ram --- rtl/arch/spartan-6/jtag_tap_spartan6.v | 49 +++++++ rtl/serv/serving_ram_dp.v | 54 +++++++ rtl/serv/serving_ram_jtag.v | 65 ++++++++ rtl/serv/serving_ram_jtag_bridge.v | 107 ++++++++++++++ scripts/jtag_write_ram.py | 196 +++++++++++++++++++++++++ 5 files changed, 471 insertions(+) create mode 100644 rtl/arch/spartan-6/jtag_tap_spartan6.v create mode 100644 rtl/serv/serving_ram_dp.v create mode 100644 rtl/serv/serving_ram_jtag.v create mode 100644 rtl/serv/serving_ram_jtag_bridge.v create mode 100755 scripts/jtag_write_ram.py diff --git a/rtl/arch/spartan-6/jtag_tap_spartan6.v b/rtl/arch/spartan-6/jtag_tap_spartan6.v new file mode 100644 index 0000000..9078163 --- /dev/null +++ b/rtl/arch/spartan-6/jtag_tap_spartan6.v @@ -0,0 +1,49 @@ +`default_nettype none + +// Spartan-6 JTAG TAP wrapper with an architecture-neutral interface. +// Re-implement this module for other FPGA families with the same port list. +module jtag_tap_spartan6 + #(parameter USER_CHAIN = 1) + ( + output wire o_drck, + output wire o_capture, + output wire o_shift, + output wire o_update, + output wire o_reset, + output wire o_sel, + output wire o_tdi, + input wire i_tdo + ); + + wire drck1; + wire drck2; + wire sel1; + wire sel2; + wire tdo1; + wire tdo2; + + localparam USE_CHAIN2 = (USER_CHAIN == 2); + + assign o_drck = USE_CHAIN2 ? drck2 : drck1; + assign o_sel = USE_CHAIN2 ? sel2 : sel1; + assign tdo1 = USE_CHAIN2 ? 1'b0 : i_tdo; + assign tdo2 = USE_CHAIN2 ? i_tdo : 1'b0; + + BSCAN_SPARTAN6 + #(.JTAG_CHAIN(USER_CHAIN)) + bscan_spartan6 + ( + .CAPTURE(o_capture), + .DRCK1(drck1), + .DRCK2(drck2), + .RESET(o_reset), + .SEL1(sel1), + .SEL2(sel2), + .SHIFT(o_shift), + .TDI(o_tdi), + .UPDATE(o_update), + .TDO1(tdo1), + .TDO2(tdo2) + ); + +endmodule diff --git a/rtl/serv/serving_ram_dp.v b/rtl/serv/serving_ram_dp.v new file mode 100644 index 0000000..0f0e174 --- /dev/null +++ b/rtl/serv/serving_ram_dp.v @@ -0,0 +1,54 @@ +`default_nettype none +`include "../util/clog2.vh" + +module serving_ram_dp + #(// Memory parameters + parameter depth = 256, + parameter aw = `CLOG2(depth), + parameter memfile = "", + parameter sim = 1'b0) + ( + // CPU port (compatible with serving_ram) + input wire i_clk, + input wire [aw-1:0] i_waddr, + input wire [7:0] i_wdata, + input wire i_wen, + input wire [aw-1:0] i_raddr, + output reg [7:0] o_rdata, + + // Debug/programming port + input wire i_dbg_clk, + input wire [aw-1:0] i_dbg_addr, + input wire [7:0] i_dbg_wdata, + input wire i_dbg_wen, + output wire [7:0] o_dbg_rdata + ); + + reg [7:0] mem [0:depth-1] /* verilator public */; + + always @(posedge i_clk) begin + if (i_wen) + mem[i_waddr] <= i_wdata; + o_rdata <= mem[i_raddr]; + end + + always @(posedge i_dbg_clk) begin + if (i_dbg_wen) + mem[i_dbg_addr] <= i_dbg_wdata; + end + + // Asynchronous debug read simplifies JTAG readback logic. + assign o_dbg_rdata = mem[i_dbg_addr]; + + integer i; + initial begin + if (sim == 1'b1) begin + for (i = 0; i < depth; i = i + 1) + mem[i] = 8'h00; + end + if (|memfile) begin + $display("Preloading %m from %s", memfile); + $readmemh(memfile, mem); + end + end +endmodule diff --git a/rtl/serv/serving_ram_jtag.v b/rtl/serv/serving_ram_jtag.v new file mode 100644 index 0000000..ab15be5 --- /dev/null +++ b/rtl/serv/serving_ram_jtag.v @@ -0,0 +1,65 @@ +`default_nettype none +`include "../util/clog2.vh" + +// Drop-in serving RAM variant with USER JTAG programming access. +module serving_ram_jtag + #( + parameter depth = 256, + parameter aw = `CLOG2(depth), + parameter memfile = "", + parameter sim = 1'b0, + parameter USER_CHAIN = 1 + ) + ( + input wire i_clk, + input wire [aw-1:0] i_waddr, + input wire [7:0] i_wdata, + input wire i_wen, + input wire [aw-1:0] i_raddr, + output wire [7:0] o_rdata + ); + + wire dbg_clk; + wire [aw-1:0] dbg_addr; + wire [7:0] dbg_wdata; + wire dbg_wen; + wire [7:0] dbg_rdata; + + serving_ram_dp + #( + .depth(depth), + .aw(aw), + .memfile(memfile), + .sim(sim) + ) + i_serving_ram_dp + ( + .i_clk(i_clk), + .i_waddr(i_waddr), + .i_wdata(i_wdata), + .i_wen(i_wen), + .i_raddr(i_raddr), + .o_rdata(o_rdata), + .i_dbg_clk(dbg_clk), + .i_dbg_addr(dbg_addr), + .i_dbg_wdata(dbg_wdata), + .i_dbg_wen(dbg_wen), + .o_dbg_rdata(dbg_rdata) + ); + + serving_ram_jtag_bridge + #( + .depth(depth), + .aw(aw), + .USER_CHAIN(USER_CHAIN) + ) + i_serving_ram_jtag_bridge + ( + .o_ram_clk(dbg_clk), + .o_ram_addr(dbg_addr), + .o_ram_wdata(dbg_wdata), + .o_ram_wen(dbg_wen), + .i_ram_rdata(dbg_rdata) + ); + +endmodule diff --git a/rtl/serv/serving_ram_jtag_bridge.v b/rtl/serv/serving_ram_jtag_bridge.v new file mode 100644 index 0000000..dd30689 --- /dev/null +++ b/rtl/serv/serving_ram_jtag_bridge.v @@ -0,0 +1,107 @@ +`default_nettype none +`include "../util/clog2.vh" + +// Simple USER JTAG data-register protocol (LSB-first): +// bit[0] : write_enable (1=write, 0=read/select) +// bit[32:1] : 32-bit address +// bit[40:33] : write data +// +// On UPDATE: +// - write command: writes byte to RAM +// - read command : updates current read address for next CAPTURE/SHIFT readback +// - RAM uses the lower aw address bits from the 32-bit protocol address +// +// On CAPTURE, readback register loads: +// bit[0] : valid (always 1) +// bit[8:1] : read data at current read address +// remaining bits : zero +module serving_ram_jtag_bridge + #( + parameter depth = 256, + parameter aw = `CLOG2(depth), + parameter USER_CHAIN = 1 + ) + ( + output wire o_ram_clk, + output wire [aw-1:0] o_ram_addr, + output wire [7:0] o_ram_wdata, + output wire o_ram_wen, + input wire [7:0] i_ram_rdata + ); + + localparam integer JTAG_AW = 32; + localparam integer FRAME_W = 1 + JTAG_AW + 8; + localparam integer PAD_W = FRAME_W - 9; + + wire tap_drck; + wire tap_shift; + wire tap_update; + wire tap_reset; + wire tap_sel; + wire tap_tdi; + wire tap_tdo; + + reg [FRAME_W-1:0] shift_in; + reg [FRAME_W-1:0] shift_out; + reg [aw-1:0] read_addr; + reg shift_active_d; + + wire cmd_write; + wire [JTAG_AW-1:0] cmd_addr; + wire [aw-1:0] cmd_addr_ram; + wire [7:0] cmd_wdata; + + assign cmd_write = shift_in[0]; + assign cmd_addr = shift_in[JTAG_AW:1]; + assign cmd_wdata = shift_in[JTAG_AW+8:JTAG_AW+1]; + assign cmd_addr_ram = cmd_addr[aw-1:0]; + + // Update command shift register and shift response out on DRCK. + // Readback data is loaded on the first shift pulse of a DR scan. + always @(posedge tap_drck or posedge tap_reset) begin + if (tap_reset) begin + shift_in <= {FRAME_W{1'b0}}; + shift_out <= {FRAME_W{1'b0}}; + shift_active_d <= 1'b0; + end else if (tap_sel && tap_shift) begin + if (!shift_active_d) + shift_out <= {{PAD_W{1'b0}}, i_ram_rdata, 1'b1}; + else + shift_out <= {1'b0, shift_out[FRAME_W-1:1]}; + shift_in <= {tap_tdi, shift_in[FRAME_W-1:1]}; + shift_active_d <= 1'b1; + end else begin + shift_active_d <= 1'b0; + end + end + + // Read command selects the address for the next capture. + always @(posedge tap_update or posedge tap_reset) begin + if (tap_reset) + read_addr <= {aw{1'b0}}; + else if (tap_sel && !cmd_write) + read_addr <= cmd_addr_ram; + end + + assign o_ram_clk = tap_update; + assign o_ram_wen = tap_update & tap_sel & cmd_write; + assign o_ram_wdata = cmd_wdata; + assign o_ram_addr = tap_update ? cmd_addr_ram : read_addr; + + assign tap_tdo = shift_out[0]; + + jtag_tap_spartan6 + #(.USER_CHAIN(USER_CHAIN)) + i_jtag_tap + ( + .o_drck(tap_drck), + .o_capture(), + .o_shift(tap_shift), + .o_update(tap_update), + .o_reset(tap_reset), + .o_sel(tap_sel), + .o_tdi(tap_tdi), + .i_tdo(tap_tdo) + ); + +endmodule diff --git a/scripts/jtag_write_ram.py b/scripts/jtag_write_ram.py new file mode 100755 index 0000000..176e4d7 --- /dev/null +++ b/scripts/jtag_write_ram.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Write a file into serving_ram_jtag over Spartan-6 USER JTAG via OpenOCD. + +This script targets the protocol implemented by rtl/serv/serving_ram_jtag_bridge.v: + frame bit[0] = write_enable + frame bit[32:1] = 32-bit address + frame bit[40:33] = data byte + +Notes: +- Frame is shifted LSB-first (OpenOCD drscan integer value format matches this). +- USER1/USER2 opcode selection is Spartan-6 specific (IR opcodes 0x02/0x03, IR length 6). +""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import subprocess +import sys +import tempfile +from typing import Dict, List, Tuple + +JTAG_ADDR_W = 32 +JTAG_FRAME_W = 1 + JTAG_ADDR_W + 8 + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Write file to serving RAM over JTAG") + p.add_argument("input", help="Input file (.bin or readmemh-style .hex/.mem)") + p.add_argument( + "--ram-addr-width", + "--addr-width", + dest="ram_addr_width", + type=int, + default=8, + help="RAM address width (aw) in HDL, default: 8", + ) + p.add_argument("--base-addr", type=lambda x: int(x, 0), default=0, help="Base address for .bin input") + p.add_argument("--tap", default="xc6s.tap", help="OpenOCD tap name (default: xc6s.tap)") + p.add_argument( + "--user-chain", + type=int, + choices=[1, 2], + default=1, + help="BSCAN user chain used in HDL (default: 1)", + ) + p.add_argument("--openocd-cfg", action="append", default=[], help="OpenOCD -f config file (repeatable)") + p.add_argument("--openocd-cmd", action="append", default=[], help="Extra OpenOCD -c command before programming") + p.add_argument("--limit", type=int, default=None, help="Write only first N bytes") + p.add_argument("--dry-run", action="store_true", help="Generate and print TCL only") + return p.parse_args() + + +def _strip_line_comments(line: str) -> str: + return line.split("//", 1)[0] + + +def parse_readmemh_text(path: pathlib.Path) -> Dict[int, int]: + """Parse a simple readmemh-style file with optional @address directives.""" + text = path.read_text(encoding="utf-8") + words: Dict[int, int] = {} + addr = 0 + + for raw_line in text.splitlines(): + line = _strip_line_comments(raw_line).strip() + if not line: + continue + for tok in line.split(): + tok = tok.strip() + if not tok: + continue + if tok.startswith("@"): + addr = int(tok[1:], 16) + continue + if not re.fullmatch(r"[0-9a-fA-F]+", tok): + raise ValueError(f"Unsupported token '{tok}' in {path}") + val = int(tok, 16) + if val < 0 or val > 0xFF: + raise ValueError(f"Byte value out of range at address 0x{addr:x}: {tok}") + words[addr] = val + addr += 1 + + return words + + +def load_image(path: pathlib.Path, base_addr: int) -> List[Tuple[int, int]]: + suffix = path.suffix.lower() + if suffix == ".bin": + blob = path.read_bytes() + return [(base_addr + i, b) for i, b in enumerate(blob)] + if suffix in {".hex", ".mem", ".vmem"}: + words = parse_readmemh_text(path) + return sorted(words.items()) + raise ValueError("Unsupported input format. Use .bin, .hex, .mem, or .vmem") + + +def build_write_frame(addr: int, data: int) -> int: + return (data << (JTAG_ADDR_W + 1)) | ((addr & ((1 << JTAG_ADDR_W) - 1)) << 1) | 0x1 + + +def build_openocd_tcl(entries: List[Tuple[int, int]], tap: str, user_chain: int, pre_cmds: List[str]) -> str: + ir_opcode = 0x02 if user_chain == 1 else 0x03 + + lines: List[str] = [] + lines.append("init") + for cmd in pre_cmds: + lines.append(cmd) + lines.append(f"irscan {tap} 0x{ir_opcode:x} -endstate IDLE") + + for addr, data in entries: + frame = build_write_frame(addr, data) + lines.append(f"drscan {tap} {JTAG_FRAME_W} 0x{frame:x} -endstate IDLE") + + lines.append("shutdown") + lines.append("") + return "\n".join(lines) + + +def run_openocd(cfg_files: List[str], script_path: pathlib.Path) -> int: + cmd = ["openocd"] + for cfg in cfg_files: + cmd += ["-f", cfg] + cmd += ["-f", str(script_path)] + + proc = subprocess.run(cmd) + return proc.returncode + + +def main() -> int: + args = parse_args() + in_path = pathlib.Path(args.input) + + if not in_path.exists(): + print(f"error: input file not found: {in_path}", file=sys.stderr) + return 2 + + entries = load_image(in_path, args.base_addr) + if args.limit is not None: + entries = entries[: args.limit] + + if not entries: + print("error: no bytes found to write", file=sys.stderr) + return 2 + + if args.ram_addr_width < 1 or args.ram_addr_width > JTAG_ADDR_W: + print( + f"error: --ram-addr-width must be in [1, {JTAG_ADDR_W}] for this protocol", + file=sys.stderr, + ) + return 2 + + max_jtag_addr = (1 << JTAG_ADDR_W) - 1 + max_addr = (1 << args.ram_addr_width) - 1 + for addr, _ in entries: + if addr < 0 or addr > max_jtag_addr: + print( + f"error: address 0x{addr:x} exceeds 32-bit protocol range (max 0x{max_jtag_addr:x})", + file=sys.stderr, + ) + return 2 + if addr > max_addr: + print( + f"error: address 0x{addr:x} exceeds RAM addr width {args.ram_addr_width} (max 0x{max_addr:x})", + file=sys.stderr, + ) + return 2 + + tcl = build_openocd_tcl(entries, args.tap, args.user_chain, args.openocd_cmd) + + if args.dry_run: + print(tcl, end="") + print(f"# bytes: {len(entries)}", file=sys.stderr) + return 0 + + if not args.openocd_cfg: + print("error: provide at least one --openocd-cfg unless using --dry-run", file=sys.stderr) + return 2 + + with tempfile.NamedTemporaryFile("w", suffix=".tcl", delete=False) as tf: + tf.write(tcl) + tcl_path = pathlib.Path(tf.name) + + print(f"Programming {len(entries)} bytes via JTAG...") + rc = run_openocd(args.openocd_cfg, tcl_path) + if rc != 0: + print(f"error: openocd failed with exit code {rc}", file=sys.stderr) + print(f"TCL kept at: {tcl_path}", file=sys.stderr) + return rc + + print("Done.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())