From 4c0ab905078e2e35573ea6a3d439fcfca387aa68 Mon Sep 17 00:00:00 2001 From: Matthew Otto <106996253+Matthew-Otto@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:50:59 -0500 Subject: [PATCH] Add JTAG simulation driver --- src/debug/tap.sv | 1 + testbench/jtag/clk_divider.sv | 19 ++++ testbench/jtag/jtag_driver.sv | 174 ++++++++++++++++++++++++++++++ testbench/jtag/svf_convert.py | 192 ++++++++++++++++++++++++++++++++++ testbench/jtag/test.svf | 8 ++ testbench/testbench.sv | 23 +++- 6 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 testbench/jtag/clk_divider.sv create mode 100644 testbench/jtag/jtag_driver.sv create mode 100644 testbench/jtag/svf_convert.py create mode 100644 testbench/jtag/test.svf diff --git a/src/debug/tap.sv b/src/debug/tap.sv index 9514b1056..9e09b873e 100644 --- a/src/debug/tap.sv +++ b/src/debug/tap.sv @@ -79,6 +79,7 @@ module tap ( PauseIR : State <= tms ? Exit2IR : PauseIR; Exit2IR : State <= tms ? UpdateIR : ShiftIR; UpdateIR : State <= tms ? SelectDR : RunTestIdle; + default : State <= TLReset; endcase end diff --git a/testbench/jtag/clk_divider.sv b/testbench/jtag/clk_divider.sv new file mode 100644 index 000000000..2cc639ba3 --- /dev/null +++ b/testbench/jtag/clk_divider.sv @@ -0,0 +1,19 @@ +module clk_divider #(parameter DIV) ( + input logic clk_in, reset, + output logic clk_out +); + integer count; + + always_ff @(posedge clk_in) begin + if (reset) begin + count <= 0; + clk_out <= 0; + end else if (count == DIV) begin + clk_out <= ~clk_out; + count <= 0; + end else begin + count <= count + 1; + end + end + +endmodule \ No newline at end of file diff --git a/testbench/jtag/jtag_driver.sv b/testbench/jtag/jtag_driver.sv new file mode 100644 index 000000000..74613d7e7 --- /dev/null +++ b/testbench/jtag/jtag_driver.sv @@ -0,0 +1,174 @@ +module jtag_driver( + input logic clk, reset, + + output logic tdi, tms, tck, + input logic tdo +); + localparam logic [2:0] INSTR_RUN = 3'b000; + localparam logic [2:0] INSTR_SIR = 3'b001; + localparam logic [2:0] INSTR_SDR = 3'b010; + + localparam CMD_BITS = 3; + localparam LENGTH_BITS = 10; + localparam DATA_BITS = 48; + localparam WIDTH = CMD_BITS + LENGTH_BITS + DATA_BITS*3; + localparam DEPTH = 100; + + enum logic [4:0] {RESET, NOP, LOAD, DECODE, COMPLETE, ERROR, DR1, DR2, DR3, SHIFTDR, + IR1, IR2, IR3, IR4, SHIFTIR, RTI1, RTI2} State; + + bit [WIDTH-1:0] MEM [DEPTH-1:0]; + + logic [$clog2(DEPTH)-1:0] IP; + logic [CMD_BITS-1:0] Instruction; + logic [LENGTH_BITS-1:0] Length, Idx; + logic [DATA_BITS-1:0] ScanIn, ScanOut, Mask, ScanData; + + + always_ff @(posedge clk) begin + if (reset) begin + IP <= 0; + Idx <= 0; + State <= RESET; + end else + case (State) + RESET : begin + // TAP controller has no reset signal + // we must clock a 111110 into TMS to guarantee Run-Test/Idle state + if (Idx == 5) + State <= LOAD; + else + Idx <= Idx + 1; + end + + LOAD : begin + {Instruction, Length, ScanIn, ScanOut, Mask} <= MEM[IP]; + if (|MEM[IP]) + State <= DECODE; + else + State <= COMPLETE; + IP <= IP + 1; + end + + DECODE : begin + ScanData <= '0; + Idx <= 0; + case (Instruction) + INSTR_RUN : State <= NOP; + INSTR_SDR : State <= DR1; + INSTR_SIR : State <= IR1; + default : State <= ERROR; + endcase + end + + NOP : begin + if (Idx == Length-1) + State <= LOAD; + else + Idx <= Idx + 1; + end + + DR1 : State <= DR2; + DR2 : State <= DR3; + DR3 : State <= SHIFTDR; + + SHIFTDR : begin + ScanData[Idx] <= tdo; + if (Idx == Length-1) + State <= RTI1; + else + Idx <= Idx + 1; + end + + IR1 : State <= IR2; + IR2 : State <= IR3; + IR3 : State <= IR4; + IR4 : State <= SHIFTIR; + + SHIFTIR : begin + ScanData[Idx] <= tdo; + if (Idx == Length-1) + State <= RTI1; + else + Idx <= Idx + 1; + end + + RTI1 : begin + if (|(ScanOut & Mask) & ((ScanData & Mask) != (ScanOut & Mask))) + State <= ERROR; + else + State <= RTI2; + end + RTI2 : State <= LOAD; + + COMPLETE : State <= COMPLETE; + ERROR : State <= ERROR; + endcase + end + + // Drive TMS/TDI on falling edge of clock to match spec and also avoid weird simulation bugs + always_ff @(negedge clk) begin + // default values + tdi <= 1'bx; + + case (State) + RESET : tms <= (Idx != 5); + + LOAD, + DECODE, + NOP, + COMPLETE : tms <= 0; + + DR1 : tms <= 1; + DR2 : tms <= 0; + DR3 : tms <= 0; + + SHIFTDR : begin + tms <= (Idx == Length-1); + tdi <= ScanIn[Idx]; + end + + IR1 : tms <= 1; + IR2 : tms <= 1; + IR3 : tms <= 0; + IR4 : tms <= 0; + + SHIFTIR : begin + tms <= (Idx == Length-1); + tdi <= ScanIn[Idx]; + end + + RTI1 : tms <= 1; + RTI2 : tms <= 0; + endcase + end + + // tck driver + always_comb begin + case (State) + LOAD, + DECODE, + COMPLETE, + ERROR : begin + tck = 0; + end + + RESET, + NOP, + DR1, + DR2, + DR3, + SHIFTDR, + IR1, + IR2, + IR3, + IR4, + SHIFTIR, + RTI1, + RTI2 : begin + tck = clk; + end + endcase + end + +endmodule \ No newline at end of file diff --git a/testbench/jtag/svf_convert.py b/testbench/jtag/svf_convert.py new file mode 100644 index 000000000..3f61543bf --- /dev/null +++ b/testbench/jtag/svf_convert.py @@ -0,0 +1,192 @@ +from enum import Enum +from math import log2 + +# Assembled SVF command format +CMD_BITS = 3 +LENGTH_BITS = 10 +DATA_BITS = 48 + +# Derived constants +MAXLEN = 2**LENGTH_BITS + +def main(): + filepath = "testbench/jtag/test.svf" + dest_filepath = "testbench/jtag/test.mem" + with open(filepath, "r") as file: + data = file.read() + data = data.lower() + + tokens = svf_tokenizer(data) + tokens = remove_comments(tokens) + cmds = parse_tokens(tokens) + with open(dest_filepath, "w") as file: + for cmd in cmds: + asm = assemble_svf(cmd) + file.write(asm + '\n') + + +def svf_tokenizer(data): + """Reads raw SVF ascii and converts to a list of tokens""" + keywords = ["sir", "sdr", "runtest", "tdi", "tdo", "mask", "smask", "(", ")", ";"] + comment_keywords = ["//", "!"] + keywords += comment_keywords + + tokens = [] + eof = len(data) + token = "" + for idx, char in enumerate(data): + if char != " " and char != "\n": + token += char + if (idx+1 < eof): + if data[idx+1] == " ": + if token: + tokens.append(token) + token = "" + elif data[idx+1] == "\n": + if token: + tokens.append(token) + token = "" + tokens.append("\n") + elif token in keywords or data[idx+1] in keywords: + if token: + tokens.append(token) + token = "" + return tokens + + +def remove_comments(tokens): + """Removes comments and newlines from list of tokens""" + pruned = [] + comment = False + for t in tokens: + if t == "\n": + comment = False + continue + if comment: + continue + if t in ["//", "!"]: + comment = True + continue + pruned.append(t) + return pruned + +def parse_tokens(tokens): + """groups tokens belonging to the same SVF command and checks if valid""" + cmds = [] + + cmd = Command() + start_idx = 0 + i = -1 + while i+1 < len(tokens): + i += 1 + t = tokens[i] + if t == ";": + if cmd.complete(): + cmds.append(cmd) + cmd = Command() + start_idx = i+1 + continue + else: + raise Exception(f"Error: incomplete SVF command terminated : '{' '.join(tokens[start_idx:i+1])}'") + + if cmd.op is None: + try: + cmd.op = SVF[t] + except KeyError: + raise Exception(f"Error: expected an SVF command, got '{t}' : '{' '.join(tokens[start_idx:i+1])}'") + continue + + if cmd.length is None: + try: + cmd.length = int(t) + except Exception: + raise Exception(f"Error: expected a length value, got '{t}' : '{' '.join(tokens[start_idx:i+1])}'") + if cmd.length == 0: + raise Exception(f"Error: length parameter must not be 0 : '{' '.join(tokens[start_idx:i+1])}'") + if cmd.length >= MAXLEN: + raise Exception(f"Error: not enough bits to represent command length : {cmd.length} > {MAXLEN-1} : '{' '.join(tokens[start_idx:i+2])}'") + if cmd.op != SVF.runtest and cmd.length > DATA_BITS: + raise Exception(f"Error: length exceeds number of bits in data field : {cmd.length} > {DATA_BITS} : '{' '.join(tokens[start_idx:i+2])}'") + continue + + match cmd.op: + case SVF.runtest: + continue + case SVF.sdr | SVF.sir: + if t not in ("tdi", "tdo", "mask"): + raise Exception(f"Error: unknown keyword '{t}' : '{' '.join(tokens[start_idx:i+1])}'") + if not (tokens[i+1] == "(" and tokens[i+3] == ")"): + raise Exception(f"Error: could not interpret value following token '{t}' (missing parentheses) : '{' '.join(tokens[start_idx:i+2])}'") + try: + val = int(tokens[i+2], 16) + i += 3 + if t == "tdi": + cmd.tdi = val + elif t == "tdo": + cmd.tdo = val + elif t == "mask": + cmd.mask = val + except Exception: + raise Exception(f"Error: could not interpret {t} value: '{tokens[i+2]}' : '{' '.join(tokens[start_idx:i+1])}'") + if val > 0 and int(log2(val)) > cmd.length: + raise Exception(f"Error: shift data cannot be a value larger than the maximum implied by the length parameter : log2({val}) > {cmd.length}" + + f" : '{' '.join(tokens[start_idx:i+2])}'") + continue + case _: + raise Exception(f"Error: did not match on valid SVF command : '{' '.join(tokens[start_idx:i+1])}'") + if t != ";": + raise Exception(f"Error: file ended with incomplete command") + return cmds + + + +def assemble_svf(cmd): + """Converts svf command object to proprietary format for Wally simulation""" + cmdcode = (cmd.op.value << (LENGTH_BITS + DATA_BITS*3)) + match cmd.op: + case SVF.runtest: + cmdcode += (cmd.length << DATA_BITS*3) + case SVF.sdr | SVF.sir: + if cmd.length: + cmdcode += (cmd.length << DATA_BITS*3) + if cmd.tdi: + cmdcode += (cmd.tdi << DATA_BITS*2) + if cmd.tdo: + cmdcode += (cmd.tdo << DATA_BITS) + if cmd.mask: + cmdcode += (cmd.mask) + elif not cmd.mask: # If mask isnt specified, set all bits to 1 + cmdcode += 2**DATA_BITS-1 + hexcmd = hex(cmdcode)[2:] + # TODO: pad left with 0 if needed (if CMD_BITS increases) + return hexcmd + + +class Command: + def __init__(self): + self.op = None + self.length = None + self.tdi = None + self.tdo = None + self.mask = None + + def complete(self): + """Returns true if object contains enough information to form a complete command""" + if self.op == SVF.runtest: + return self.length + elif self.op in (SVF.sdr, SVF.sir): + z = self.length and (self.tdi is not None or self.tdo is not None) + return z + else: + return False + + +# Supported SVF commands +class SVF(Enum): + runtest = 0 + sir = 1 + sdr = 2 + + +if __name__ == "__main__": + main() diff --git a/testbench/jtag/test.svf b/testbench/jtag/test.svf new file mode 100644 index 000000000..62150cd16 --- /dev/null +++ b/testbench/jtag/test.svf @@ -0,0 +1,8 @@ +// SVF commands to pull to idcode out of jtag module +SIR 5 TDI(1); // IDCODE +SDR 32 TDO(DEADBEEF); +SDR 32 TDO(DEADBEAF); // JTAG_Driver should throw error if TDO value doesn't match what is scanned out +RUNTEST 55; +sir 8 !and multiline commands +tdo(99); + diff --git a/testbench/testbench.sv b/testbench/testbench.sv index 69fd30ae6..e1c39a45e 100644 --- a/testbench/testbench.sv +++ b/testbench/testbench.sv @@ -67,6 +67,8 @@ module testbench; logic reset_ext, reset; logic ResetMem; + // JTAG driver signals + logic jtag_clk; // divided clock that runs jtag_driver logic tck; logic tdi; logic tms; @@ -271,7 +273,7 @@ module testbench; logic ResetCntRst; logic CopyRAM; - string signame, elffilename, memfilename, bootmemfilename, uartoutfilename, pathname; + string signame, elffilename, memfilename, bootmemfilename, uartoutfilename, pathname, jtagmemfilename; integer begin_signature_addr, end_signature_addr, signature_size; integer uartoutfile; @@ -579,10 +581,21 @@ module testbench; assign SDCIntr = 1'b0; end - // Change these if testing debug - assign tck = 0; - assign tdi = 0; - assign tms = 0; + // TODO: drive jtag clock at 1/10th core clock (variable) + if (P.DEBUG_SUPPORTED) begin + always @(posedge clk) begin + jtagmemfilename = "../tests/test.svf.memfile"; + if (LoadMem) begin + $readmemh(jtagmemfilename, jtag.MEM); + $display("Read memfile %s", jtagmemfilename); + end + end + + clk_divider #(2) cdiv (.clk_in(clk), .clk_out(jtag_clk), .reset(reset_ext)); + jtag_driver jtag(.clk(jtag_clk), .reset(reset_ext), .tdi, .tms, .tck, .tdo); + end else begin + assign {tdi,tms,tck} = '0; + end wallypipelinedsoc #(P) dut( .clk, .reset_ext, .reset, .tck, .tdi, .tms, .tdo,