[myhdl-list] Visual Timing Specification for unit testing - complete example
Brought to you by:
jandecaluwe
From: George P. <ge...@ga...> - 2006-10-03 02:47:11
|
All, I've come up with a solution that made writing unit tests massively more fun and less tedious and error-prone for me. I've found a way to make an easily human-readable ascii timing diagram. However, this timing diagram is special because the very same diagram can be parsed and used to drive a MyHDL unit test. I've pasted a complete working example using an up_counter hardware module. This example also demonstrates the use of Python dicts for signal grouping. In fact, the test code relies on this functionality. I have some improvements planned (such as support for negedges). Feedback welcome. Enjoy, George # - Begin code --------------------------------------------------------------- # myhdl_vts.py # George Pantazopoulos http://www.gammaburst.net from myhdl import * # ---------------------------------------------------------------------------- def visual_timing_spec_parse(vts, print_cts=False): """ Visual Timing Specification parser for MyHDL Parses a Visual Timing Specification into a condensed format suitable for driving or checking signals A Visual Timing Specification (VTS) is an ascii-based format for conveying signal timing information. The same timing spec is both human- and machine-readable. A VTS object can be used for driving signals as well as checking signal outputs. Example VTS objects: signals_to_drive = dict( edges = "|0....|1....|2....|3....|4....|5....|6....", rst = "------____________________________________", we = "______------------________________________", re = "____________------------__________________", dwr = "0x00 0b110 0xBB 0xCC 0xDD ... 0xEE ") correct_outputs = dict( edges = "|0....|1....|2....|3....|4....|5....|6....", drd = "X 0 X 0b110 0xBB X X ", empty = "X ------____________------------------", full = "X ____________________________________") - A VTS is a Python dictionary. - Each dictionary key is the name of a signal in the design. - Each key's value is an ASCII string describing that signals behavior - In addition to the signal keys, a special key named 'edges' is required. Its string value contains edge markers and padding. The edge markers are denoted by '|' for positive edges. Other characters are currently ignored. How a VTS is parsed: -------------------- - Only characters "under" the edge markers are looked at. - '-' = signal is high at this clock edge - '_' = signal is low at this clock edge - '.' = no change (used when driving signals) - 'X' = "don't care" (used when checking signals) - integer data values are supported. Their first character needs to be under the edge marker. Hex values must be preceded by "0x" Binary values must be preceded by "0b" Padding may be added to the 'edges' string to accomodate data values of any length. One whitespace character must follow the data item and come before the next edge marker. Timing: ------- When driving signals with the VTS, assume that the signal will be valid at the clock edge. When checking signals with the VTS, the signal value checked for must be valid by the time the corresponding edge arrives. Condensed Timing Spec --------------------- The result of parsing a Visual Timing Spec is a Condensed Timing Spec. Its format is similar to the VTS, except that it's not restricted to ASCII and contains only the data for each clock edge, with no padding. The CTS is to be passed as input to the actual driver and monitor functions. TODO: ----- TODO: Make it possible to optionally specify negedges too. Eg: edges = "|0....v.....|1....v.....|2....v...." Where 'v' denotes a negedge TODO: Make it possible to concatenate multiple VTS's into a list, so long timing specs remain easily readable. """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __revision__ = "" __date__ = "2 Oct 2006" __version__ = "0.1.0" __requires__ = ('myhdl', '>= 0.5.1') # Create an empty Condensed Timing Specification cts = dict() for sig in vts: cts[sig] = [] # There should be dictionary item called "edges" # We'll use this to figure out when to sample edgenum = 0 # For each character in the 'edges' specifier string for i in range(len(vts['edges'])): # Grab the character value c = vts['edges'][i] if c == '|': # We sample here. # For each signal specifier string in vts for sig in vts: if sig == 'edges': data = edgenum edgenum += 1 elif sig != 'edges': # Get the character at the edge sampling index d = vts[sig][i] if d == '_': data = False elif d == '-': data = True # This is a "don't-care". # Intended for use when checking signals, # not driving them. elif d == 'X': data = ' ' # "dont update this signal" when used for driving elif (d == '.'): data = ' ' # A space under an edge marker is illegal, because it # could mean a formatting error was made by the user. # # This type of error can be hard to spot. # If we don't trap this, it could lead to wasted time # and misleading test results. elif d == ' ': diag = "signal: " + sig + ", offset=" + str(i) raise Exception, \ "Spaces are not allowed under edge markers " + diag else: # Treat this char that's under the edge marker # as the start of a data value string # # We need to parse the data string until we reach # whitespace or the next edge marker. # # grab the next chars until we hit whitespace or the # next edge marker. # # Start at the char 'under' the current edge marker. # TODO: Stop if we hit the next edge marker n = 0 dstr = "" while d != ' ': d = vts[sig][i+n] dstr += d n += 1 # convert to an int. # The (required) trailing whitespace is ok. # Hex if dstr[:2] == "0x": data = int(dstr[2:],16) # Binary elif dstr[:2] == "0b": data = int(dstr[2:], 2) # Decimal else: data = int(dstr) # Add the data item to the appropriate signal's list. cts[sig].append(data) if print_cts: print "cts: " import pprint print pprint.pprint(cts) return cts # ---------------------------------------------------------------------------- def drive_signals_from_cts(sigs, cts): """ drive_signals_from_cts() - unit test tool for MyHDL Given a Condensed Timing Spec object, drive the signals according to that specification. See the Visual Timing Spec parser doc. The signal dictionary must contain a 'clk' signal """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __revision__ = "" __date__ = "2 Oct 2006" __version__ = "0.1.0" __requires__ = ('myhdl', '>= 0.5.1') if 'clk' not in sigs: raise Exception, \ "signal dictionary 'sigs' must contain a 'clk' signal" clk = sigs['clk'] for i in range(len(cts['edges'])): # Setup for positive edge i # Drive each signal with the correct value for # the upcoming positive edge i for sig in cts: value = cts[sig][i] if sig != 'edges': if value != ' ': sigs[sig].next = value # positive edge i yield clk.posedge # negative edge i yield clk.negedge # ---------------------------------------------------------------------------- def check_signals_against_cts(sigs, cts): """ check_signals_against_cts() - unit test tool for MyHDL Given a Condensed Timing Spec object, ensure the signals match that specification. See the Visual Timing Spec parser doc. The signal dictionary must contain a 'clk' signal """ __author__ = "George Pantazopoulos http://www.gammaburst.net" __revision__ = "" __date__ = "2 Oct 2006" __version__ = "0.1.0" __requires__ = ('myhdl', '>= 0.5.1') if 'clk' not in sigs: raise Exception, \ "signal dictionary 'sigs' must contain a 'clk' signal" clk = sigs['clk'] for i in range(len(cts['edges'])): # Setup for edge i # Edge i yield clk.posedge # Check the outputs that should have been valid by # the time this edge comes around for sig in cts: value = cts[sig][i] if sig != 'edges': # Skip this for 'don't-care' values if value != ' ': # If the signal is not the value it should have been if sigs[sig] != value: # Raise an exception and show some helpful info. info = "Edge #" + str(i) + \ " Signal \'" + sig + "\'" + \ " = " + str(sigs[sig]) + \ ". Correct value = " + str(value) raise Exception, info yield clk.negedge raise StopSimulation() # ---------------------------------------------------------------------------- def up_counter(sigs): # Unbundle needed signals. Name them from the perspective of this module. clk_i = sigs['clk'] rst_i = sigs['rst'] count_o = sigs['count'] enable_i = sigs['count_en'] @always(clk_i.posedge) def count_proc(): if rst_i: count_o.next = 0 else: if enable_i: count_o.next = (count_o + 1) % 2**len(count_o) return instances() # ---------------------------------------------------------------------------- def up_counter_bench(): # Embedded function definition for clock generator def clkgen(clk): while True: yield delay(10) clk.next = not clk # Shared clk signal clk = Signal(bool(0)) # Create the signal group for the counter counter_sigs=dict(clk = clk, rst = Signal(bool(0)), count = Signal(intbv(0)[4:]), count_en = Signal(bool(0))) # Instantiate a clock generator and connect to the shared clock clkgen_inst = clkgen(clk) dut = up_counter(counter_sigs) # Visual Timing Specification # --------------------------- signals_to_drive = dict( edges = "|0....|1....|2....|3....|4....|5....|6....", rst = "-------------_____________________________", count_en = "__________________------------------------") correct_outputs = dict( edges = "|0....|1....|2....|3....|4....|5....|6....", count = "X 0 0 0 1 2 3 ") # Signal driver driver = drive_signals_from_cts( sigs = counter_sigs, cts = visual_timing_spec_parse(signals_to_drive) ) # Signal monitor monitor = check_signals_against_cts( sigs = counter_sigs, cts = visual_timing_spec_parse(correct_outputs) ) return instances() # ---------------------------------------------------------------------------- def test_up_counter(): tb = up_counter_bench() sim = Simulation(tb) sim.run() test_up_counter() # - End code ----------------------------------------------------------------- |