From b0582e63bccfe97ab8e93f98e344966fd334f3f6 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Thu, 5 Mar 2026 15:06:09 +0100 Subject: [PATCH] Signal scope --- .gitignore | 1 + cores/signal/sd_adc_q15/rtl/sd_adc_q15.v | 5 +- .../signal_scope/rtl/signal_scope_q15.v | 141 +++++++++++++ .../signal/signal_scope/signal_scope_q15.core | 32 +++ .../signal/signal_scope/tool/capture_plot.py | 193 ++++++++++++++++++ .../mimas_sd_adc_r2r/mimas_sd_adc_r2r.core | 1 + cores/system/mimas_sd_adc_r2r/rtl/toplevel.v | 17 +- 7 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 cores/signal/signal_scope/rtl/signal_scope_q15.v create mode 100644 cores/signal/signal_scope/signal_scope_q15.core create mode 100755 cores/signal/signal_scope/tool/capture_plot.py diff --git a/.gitignore b/.gitignore index 3c0160d..5fc3c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ out/ +*__pycache__* \ No newline at end of file diff --git a/cores/signal/sd_adc_q15/rtl/sd_adc_q15.v b/cores/signal/sd_adc_q15/rtl/sd_adc_q15.v index 7130a25..b884d93 100644 --- a/cores/signal/sd_adc_q15/rtl/sd_adc_q15.v +++ b/cores/signal/sd_adc_q15/rtl/sd_adc_q15.v @@ -38,7 +38,7 @@ module sd_adc_q15 #( ); lpf_iir_q15_k #( - .K(10) + .K(6) ) lpf ( .i_clk(i_clk_15), .i_rst_n(i_rst_n), .i_x_q15(raw_sample_q15), @@ -46,7 +46,8 @@ module sd_adc_q15 #( ); decimate_by_r_q15 #( - .R(375), // 15MHz/375 = 40KHz + .R(200), // 15MHz/200 = 75KHz + // .R(375), // 15MHz/375 = 40KHz .CNT_W(10) ) decimate ( .i_clk(i_clk_15), .i_rst_n(i_rst_n), diff --git a/cores/signal/signal_scope/rtl/signal_scope_q15.v b/cores/signal/signal_scope/rtl/signal_scope_q15.v new file mode 100644 index 0000000..b14c255 --- /dev/null +++ b/cores/signal/signal_scope/rtl/signal_scope_q15.v @@ -0,0 +1,141 @@ +`include "clog2.vh" + +module signal_scope_q15 #( + parameter depth = 2**12, + parameter chain = 1 +)( + input wire i_clk, + input wire i_rst, + input wire [15:0] i_signal_a, + input wire i_signal_valid_a, + input wire [15:0] i_signal_b, + input wire i_signal_valid_b, + input wire [15:0] i_signal_c, + input wire i_signal_valid_c, + input wire [15:0] i_signal_d, + input wire i_signal_valid_d +); + localparam aw = `CLOG2(depth); + localparam [aw-1:0] depth_last = depth-1; + + (* ram_style = "block" *) reg [16*4-1:0] mem[depth-1:0]; + reg [aw-1:0] counter; + reg count_enable; + reg rearm_prev; + + reg [15:0] signal_a; + reg [15:0] signal_b; + reg [15:0] signal_c; + reg [15:0] signal_d; + reg signal_a_pending; + reg signal_b_pending; + reg signal_c_pending; + reg signal_d_pending; + + wire [31:0] wb_adr; + wire [31:0] wb_dat; + wire [3:0] wb_sel; + wire wb_we; + wire wb_cyc; + wire wb_stb; + reg [31:0] wb_rdt; + reg wb_ack; + wire rearm_cmd; + wire [aw-1:0] wb_mem_idx = wb_adr[aw+2:3]; + + jtag_wb_bridge #( + .chain(chain), + .byte_aligned(0) + ) jtag_scope_bridge ( + .i_clk(i_clk), + .i_rst(i_rst), + .o_wb_adr(wb_adr), + .o_wb_dat(wb_dat), + .o_wb_sel(wb_sel), + .o_wb_we(wb_we), + .o_wb_cyc(wb_cyc), + .o_wb_stb(wb_stb), + .i_wb_rdt(wb_rdt), + .i_wb_ack(wb_ack), + .o_cmd_reset(rearm_cmd) + ); + + always @(posedge i_clk) begin + if(i_rst) begin + counter <= {aw{1'b0}}; + count_enable <= 1'b0; + rearm_prev <= 1'b0; + wb_ack <= 1'b0; + wb_rdt <= 32'b0; + signal_a <= 0; + signal_b <= 0; + signal_c <= 0; + signal_d <= 0; + signal_a_pending <= 1'b0; + signal_b_pending <= 1'b0; + signal_c_pending <= 1'b0; + signal_d_pending <= 1'b0; + end else begin + + // Sample signals + if(i_signal_valid_a) begin + signal_a <= i_signal_a; + signal_a_pending <= 1'b1; + end + if(i_signal_valid_b) begin + signal_b <= i_signal_b; + signal_b_pending <= 1'b1; + end + if(i_signal_valid_c) begin + signal_c <= i_signal_c; + signal_c_pending <= 1'b1; + end + if(i_signal_valid_d) begin + signal_d <= i_signal_d; + signal_d_pending <= 1'b1; + end + + // Rearm on rising edge of reset command from JTAG bridge. + rearm_prev <= rearm_cmd; + if(rearm_cmd && !rearm_prev) begin + counter <= {aw{1'b0}}; + count_enable <= 1'b1; + signal_a_pending <= 1'b0; + signal_b_pending <= 1'b0; + signal_c_pending <= 1'b0; + signal_d_pending <= 1'b0; + end + + // Write one full 4-channel frame at a time for maximum BRAM throughput. + if(count_enable && signal_a_pending && signal_b_pending && signal_c_pending && signal_d_pending) begin + if(counter <= depth_last) begin + mem[counter] <= {signal_a, signal_b, signal_c, signal_d}; + counter <= counter + 3'd1; + if(counter == depth_last) + count_enable <= 1'b0; + end else begin + count_enable <= 1'b0; + end + signal_a_pending <= 1'b0; + signal_b_pending <= 1'b0; + signal_c_pending <= 1'b0; + signal_d_pending <= 1'b0; + end + + // Simple WB slave response for JTAG reads (32-bit = 2x16-bit samples). + wb_ack <= wb_cyc & wb_stb & !wb_ack; + if(wb_cyc & wb_stb & !wb_ack) begin + if(wb_we) begin + wb_rdt <= 32'b0; + end else if(wb_mem_idx <= depth_last) begin + // A single frame is 64-bit: {a, b, c, d}. WB reads low/high 32-bit halves. + wb_rdt <= wb_adr[2] ? mem[wb_mem_idx][63:32] : mem[wb_mem_idx][31:0]; + end else begin + wb_rdt <= 32'b0; + end + end + + end + end + +endmodule diff --git a/cores/signal/signal_scope/signal_scope_q15.core b/cores/signal/signal_scope/signal_scope_q15.core new file mode 100644 index 0000000..a5cea67 --- /dev/null +++ b/cores/signal/signal_scope/signal_scope_q15.core @@ -0,0 +1,32 @@ +CAPI=2: + +name: joppeb:signal:signal_scope_q15:1.0 +description: Simple signal capture buffer for debug/scope use + +filesets: + rtl: + depend: + - joppeb:util:clog2 + - joppeb:wb:jtag_wb_bridge + files: + - rtl/signal_scope_q15.v + file_type: verilogSource + +targets: + default: + filesets: + - rtl + toplevel: signal_scope_q15 + parameters: + - depth + - chain + +parameters: + depth: + datatype: int + description: Number of samples stored in internal memory + paramtype: vlogparam + chain: + datatype: int + description: JTAG chain identifier + paramtype: vlogparam diff --git a/cores/signal/signal_scope/tool/capture_plot.py b/cores/signal/signal_scope/tool/capture_plot.py new file mode 100755 index 0000000..9bf2825 --- /dev/null +++ b/cores/signal/signal_scope/tool/capture_plot.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +import argparse +import sys +import time +from pathlib import Path + +import matplotlib.pyplot as plt + + +def _add_bridge_module_path() -> None: + here = Path(__file__).resolve() + bridge_tool = here.parents[3] / "wb" / "jtag_wb_bridge" / "tool" + sys.path.insert(0, str(bridge_tool)) + + +def _to_signed(value: int, width: int) -> int: + if width <= 0: + return value + sign_bit = 1 << (width - 1) + mask = (1 << width) - 1 + value &= mask + return value - (1 << width) if (value & sign_bit) else value + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Arm signal_scope once, dump samples over JTAG WB, and plot them." + ) + parser.add_argument("--port", type=int, default=0, help="Digilent device index") + parser.add_argument("--chain", type=int, default=1, help="JTAG USER chain") + parser.add_argument("--selector", type=str, default=None, help="Optional device selector string") + parser.add_argument( + "--depth", + type=int, + default=1024, + help="Number of scope frames to read (must match RTL depth)", + ) + parser.add_argument( + "--wait-s", + type=float, + default=0.05, + help="Seconds to wait after arm/dearm before reading", + ) + parser.add_argument( + "--unsigned", + action="store_true", + help="Plot samples as unsigned (default: signed two's complement)", + ) + parser.add_argument("--out", type=str, default=None, help="Optional PNG output path") + parser.add_argument( + "--dump-csv", + type=str, + default=None, + help="Optional CSV output path with columns: index,value", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Keep running: press Enter to recapture/replot in the same window", + ) + return parser.parse_args() + + +def capture_once(bridge, args: argparse.Namespace) -> list[tuple[int, int, int, int]]: + samples = [] + frame_count = args.depth + print("[signal_scope] Arming scope (set_reset=1 -> 0)...") + bridge.set_reset(True) + bridge.set_reset(False) + if args.wait_s > 0: + print(f"[signal_scope] Waiting {args.wait_s:.3f}s for capture to complete...") + time.sleep(args.wait_s) + + print(f"[signal_scope] Reading back {frame_count} frames...") + for idx in range(frame_count): + base = idx * 8 + low = bridge.read32(base) + high = bridge.read32(base + 4) + + ch_a = low & 0xFFFF + ch_b = (low >> 16) & 0xFFFF + ch_c = high & 0xFFFF + ch_d = (high >> 16) & 0xFFFF + if not args.unsigned: + ch_a = _to_signed(ch_a, 16) + ch_b = _to_signed(ch_b, 16) + ch_c = _to_signed(ch_c, 16) + ch_d = _to_signed(ch_d, 16) + samples.append((ch_a, ch_b, ch_c, ch_d)) + if idx and (idx % max(1, frame_count // 10) == 0): + pct = (100 * idx) // frame_count + print(f"[signal_scope] Read progress: {pct}% ({idx}/{frame_count})") + print(f"[signal_scope] Read complete: {len(samples)} frames") + return samples + + +def write_csv(samples: list[tuple[int, int, int, int]], csv_path: Path) -> None: + print(f"[signal_scope] Writing CSV to {csv_path}...") + with csv_path.open("w", encoding="utf-8") as f: + f.write("index,ch_a,ch_b,ch_c,ch_d\n") + for idx, values in enumerate(samples): + f.write(f"{idx},{values[0]},{values[1]},{values[2]},{values[3]}\n") + print(f"Wrote CSV: {csv_path}") + + +def plot_samples(ax, samples: list[tuple[int, int, int, int]], args: argparse.Namespace, capture_idx: int) -> None: + series = [[], [], [], []] + for ch_a, ch_b, ch_c, ch_d in samples: + series[0].append(ch_a) + series[1].append(ch_b) + series[2].append(ch_c) + series[3].append(ch_d) + + ax.cla() + ax.plot(series[0], linewidth=1, label="ch_a") + ax.plot(series[1], linewidth=1, label="ch_b") + ax.plot(series[2], linewidth=1, label="ch_c") + ax.plot(series[3], linewidth=1, label="ch_d") + ax.set_title(f"signal_scope_q15 capture #{capture_idx} (depth={args.depth}, chain={args.chain})") + ax.set_xlabel("Sample") + ax.set_ylabel("Value") + if not args.unsigned: + ax.set_ylim([-2**15, 2**15]) + ax.grid(True, alpha=0.3) + ax.legend(loc="upper right") + + +def main() -> int: + args = parse_args() + + if args.depth <= 0: + raise ValueError("--depth must be > 0") + + _add_bridge_module_path() + from libjtag_wb_bridge.jtag_bridge import JtagBridge # pylint: disable=import-error + + print( + f"[signal_scope] Starting capture: port={args.port}, chain={args.chain}, " + f"depth={args.depth}, selector={args.selector!r}" + ) + + with JtagBridge() as bridge: + print("[signal_scope] Opening JTAG bridge...") + if args.selector: + bridge.open_selector(args.selector, port=args.port, chain=args.chain) + else: + bridge.open(port=args.port, chain=args.chain) + print("[signal_scope] Bridge opened") + + print("[signal_scope] Clearing bridge flags and sending ping...") + bridge.clear_flags() + bridge.ping() + print("[signal_scope] Bridge ready") + + fig, ax = plt.subplots(figsize=(12, 4)) + capture_idx = 1 + + while True: + print(f"[signal_scope] Capture cycle #{capture_idx}") + samples = capture_once(bridge, args) + plot_samples(ax, samples, args, capture_idx) + fig.tight_layout() + fig.canvas.draw_idle() + fig.canvas.flush_events() + + if args.dump_csv: + write_csv(samples, Path(args.dump_csv)) + + if args.out: + out_path = Path(args.out) + print(f"[signal_scope] Saving plot to {out_path}...") + fig.savefig(out_path, dpi=150) + print(f"Wrote plot: {out_path}") + + if not args.interactive: + break + + plt.show(block=False) + answer = input("[signal_scope] Press Enter to recapture, or 'q' + Enter to quit: ") + if answer.strip().lower().startswith("q"): + break + capture_idx += 1 + + if not args.out: + print("[signal_scope] Showing plot window...") + plt.show() + + print("[signal_scope] Done") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cores/system/mimas_sd_adc_r2r/mimas_sd_adc_r2r.core b/cores/system/mimas_sd_adc_r2r/mimas_sd_adc_r2r.core index 722f1b8..23a2663 100644 --- a/cores/system/mimas_sd_adc_r2r/mimas_sd_adc_r2r.core +++ b/cores/system/mimas_sd_adc_r2r/mimas_sd_adc_r2r.core @@ -9,6 +9,7 @@ filesets: - joppeb:primitive:clkgen - joppeb:signal:sd_adc_q15 - joppeb:util:conv + - joppeb:signal:signal_scope_q15 files: - rtl/toplevel.v file_type: verilogSource diff --git a/cores/system/mimas_sd_adc_r2r/rtl/toplevel.v b/cores/system/mimas_sd_adc_r2r/rtl/toplevel.v index f729a5c..aa06912 100644 --- a/cores/system/mimas_sd_adc_r2r/rtl/toplevel.v +++ b/cores/system/mimas_sd_adc_r2r/rtl/toplevel.v @@ -60,7 +60,7 @@ module toplevel( // signal_q15 is unipolar and biased (0-3.3V -> 0..32767) reg signed [15:0] signal_unbiased_q15 = 16'sd0; reg signal_unbiased_valid = 1'b0; - localparam bias = 2**14; + localparam bias = 12050; localparam gain = 2; always @(posedge clk_15) begin if (sys_reset_r) begin @@ -88,4 +88,19 @@ module toplevel( assign LED[0] = signal_valid; assign LED[6:1] = dac_code; assign LED[7] = sys_reset_r; + + + signal_scope_q15 #( + .depth(2**10), + .chain(1) + ) scope1 ( + .i_clk(clk_15), + .i_rst(sys_reset_r), + .i_signal_a(signal_q15), + .i_signal_valid_a(signal_valid), + .i_signal_b(signal_unbiased_q15), + .i_signal_valid_b(signal_unbiased_valid), + .i_signal_valid_c(signal_valid), + .i_signal_valid_d(signal_valid) + ); endmodule