mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
+84
-7
@@ -3,6 +3,9 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import glob
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
def load_yaml(args):
|
def load_yaml(args):
|
||||||
@@ -39,12 +42,74 @@ def fetch_file(url, filename):
|
|||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
logging.warning("couldn't download {}".format(url))
|
logging.warning("couldn't download {}".format(url))
|
||||||
exit(1)
|
return False
|
||||||
|
|
||||||
with open(filename, 'wb') as fh:
|
with open(filename, 'wb') as fh:
|
||||||
logging.info("written to {}".format(filename))
|
logging.info("written to {}".format(filename))
|
||||||
fh.write(r.content)
|
fh.write(r.content)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_wokwi_file(wokwi_id:int, file_requested:str,
|
||||||
|
destination_name:str = None):
|
||||||
|
'''
|
||||||
|
fetch_wokwi_file -- perform a request on the wokwi api to
|
||||||
|
retrieve a file from a given project.
|
||||||
|
|
||||||
|
@param wokwi_id: project id (integer)
|
||||||
|
@param file_requested: the specific file requested for the
|
||||||
|
project, e.g. verilog or diagram.json
|
||||||
|
@param destination_name: file to write out to with result
|
||||||
|
'''
|
||||||
|
if destination_name is None or not len(destination_name):
|
||||||
|
destination_name = file_requested
|
||||||
|
|
||||||
|
url = f'https://wokwi.com/api/projects/{wokwi_id}/{file_requested}'
|
||||||
|
return fetch_file(url, destination_name)
|
||||||
|
|
||||||
|
|
||||||
|
def install_wokwi_testing(wokwi_id:int, destination_dir:str='src',
|
||||||
|
resource_dir:str=None):
|
||||||
|
|
||||||
|
wokwi_id_str = str(wokwi_id)
|
||||||
|
#wokwi_user_module = f'user_module_{wokwi_'
|
||||||
|
|
||||||
|
if resource_dir is None or not len(resource_dir):
|
||||||
|
resource_dir = os.path.join(os.path.dirname(__file__), 'testing')
|
||||||
|
|
||||||
|
|
||||||
|
# directories in testing/lib to copy over
|
||||||
|
pyLibsDir = os.path.join(resource_dir, 'lib')
|
||||||
|
|
||||||
|
# src template dir
|
||||||
|
srcTplDir = os.path.join(resource_dir, 'src-tpl')
|
||||||
|
|
||||||
|
for libfullpath in glob.glob(os.path.join(pyLibsDir, '*')):
|
||||||
|
logging.info(f'Copying {libfullpath} to {destination_dir}')
|
||||||
|
shutil.copytree(
|
||||||
|
libfullpath,
|
||||||
|
os.path.join(destination_dir, os.path.basename(libfullpath)),
|
||||||
|
dirs_exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for srcTemplate in glob.glob(os.path.join(srcTplDir, '*')):
|
||||||
|
with open(srcTemplate, 'r') as f:
|
||||||
|
contents = f.read()
|
||||||
|
customizedContents = re.sub('WOKWI_ID', wokwi_id_str, contents)
|
||||||
|
outputFname = os.path.basename(srcTemplate)
|
||||||
|
with open(os.path.join(destination_dir, outputFname), 'w') as outf:
|
||||||
|
logging.info(f'writing src tpl to {outputFname}')
|
||||||
|
outf.write(customizedContents)
|
||||||
|
outf.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_source(yaml):
|
def get_project_source(yaml):
|
||||||
# wokwi_id must be an int or 0
|
# wokwi_id must be an int or 0
|
||||||
@@ -56,14 +121,26 @@ def get_project_source(yaml):
|
|||||||
|
|
||||||
# it's a wokwi project
|
# it's a wokwi project
|
||||||
if wokwi_id != 0:
|
if wokwi_id != 0:
|
||||||
url = "https://wokwi.com/api/projects/{}/verilog".format(wokwi_id)
|
src_file = f'user_module_{wokwi_id}.v'
|
||||||
src_file = "user_module_{}.v".format(wokwi_id)
|
if not fetch_wokwi_file(wokwi_id, 'verilog',
|
||||||
fetch_file(url, os.path.join("src", src_file))
|
os.path.join("src", src_file) ):
|
||||||
|
logging.error(f'Could not fetch verilog file for wokwi project {wokwi_id}')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
# also fetch the wokwi diagram
|
# also fetch the wokwi diagram
|
||||||
url = "https://wokwi.com/api/projects/{}/diagram.json".format(wokwi_id)
|
if not fetch_wokwi_file(wokwi_id, 'diagram.json',
|
||||||
diagram_file = "wokwi_diagram.json"
|
os.path.join("src", 'wokwi_diagram.json')):
|
||||||
fetch_file(url, os.path.join("src", diagram_file))
|
logging.error(f'Could not fetch diagram.json file for wokwi project {wokwi_id}')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# attempt to download the *optional* truthtable for the project
|
||||||
|
if fetch_wokwi_file(wokwi_id, 'truthtable.md',
|
||||||
|
os.path.join('src', 'truthtable.md')):
|
||||||
|
logging.info(f'Wokwi project {wokwi_id} has a truthtable included, will test!')
|
||||||
|
install_wokwi_testing(wokwi_id)
|
||||||
|
|
||||||
|
|
||||||
return [src_file, 'cells.v']
|
return [src_file, 'cells.v']
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
### Wokwi/TinyTapeout testing system ###
|
||||||
|
|
||||||
|
|
||||||
|
A system has been devised to allow for simplified verification and testing of wokwi projects.
|
||||||
|
|
||||||
|
In this initial iteration, the process basically involves creating a 'truth table' type description of desired inputs and expected outputs, which allows for testing of both combinatorial and sequential/clocked circuits.
|
||||||
|
|
||||||
|
The system is designed to allow for simple extensions to this base functionality: the testing framework itself will be described below to assist in expanding it to encompass additional tests and validation of wokwi or other TT projects.
|
||||||
|
|
||||||
|
|
||||||
|
## Truth-table system ##
|
||||||
|
|
||||||
|
In its simplest incarnation, the truth-table defines a set of inputs provided to the circuit and describes the expected outputs.
|
||||||
|
|
||||||
|
The table itself is written as markdown, consisting of
|
||||||
|
|
||||||
|
* a header
|
||||||
|
* a separator
|
||||||
|
* the table rows
|
||||||
|
using markdown formatting. Eg
|
||||||
|
|
||||||
|
| inputs | outputs |
|
||||||
|
|-----------|-----------|
|
||||||
|
| 1111 0000 | xxxx 1100 |
|
||||||
|
|
||||||
|
etc
|
||||||
|
|
||||||
|
|
||||||
|
Inputs are in the left-side column, expected outputs on the right, specified
|
||||||
|
as MSB (i.e. bit 7 to 0, from left to right).
|
||||||
|
|
||||||
|
Any bits specified as 'x' or '-' are treated as "don't change" for
|
||||||
|
inputs and "don't care" for outputs.
|
||||||
|
|
||||||
|
|
||||||
|
The header is ignored, as are whitespaces, so you may format as appropriate.
|
||||||
|
Each row must have a least 8 non-whitespace bit specifications per column. E.g.
|
||||||
|
for a 2 bit adder, a table might consist of
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Cin B A | Cout S |
|
||||||
|
|---------------------|-------------|
|
||||||
|
| ----- 0 0 0 |-- 0 ---- 0 |
|
||||||
|
| ----- 0 0 1 |-- 0 ---- 1 |
|
||||||
|
| ----- 0 1 0 |-- 0 ---- 1 |
|
||||||
|
| ----- 0 1 1 |-- 1 ---- 0 |
|
||||||
|
| ----- 1 0 0 |-- 0 ---- 1 |
|
||||||
|
| ----- 1 0 1 |-- 1 ---- 0 |
|
||||||
|
| ----- 1 1 0 |-- 1 ---- 0 |
|
||||||
|
| ----- 1 1 1 |-- 1 ---- 1 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
A ‘t’ or ‘T’ bit will be toggled from whatever it was before.
|
||||||
|
|
||||||
|
An empty output column will apply the input, but not perform any test (i.e. behave the same as 8 ‘x’/’-’).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Clocked designs ####
|
||||||
|
|
||||||
|
In addition to maintaining state for toggling the clock, one or more ‘c’ bits in an input column will cause the system to:
|
||||||
|
|
||||||
|
1) Set any bits specified, including toggling an “t” bits, without changing the ‘c’ bits (setup);
|
||||||
|
2) It will then toggle all ‘c’ bits, inverting them, but not changing anything else from previous step; and finally
|
||||||
|
3) It will toggle the ‘c’ bits again, returning them to their original state.
|
||||||
|
|
||||||
|
This means setup and hold times are respected and signals may be clocked in with a single row in the table, rather than having 3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Any entry with a bit set to 'c' will:
|
||||||
|
* setup any specified bits (0 or 1) and toggle any bits as required
|
||||||
|
* then toggle (i.e. invert) the clock bit(s), without changing anything else
|
||||||
|
* then re-toggle the same clock bit(s), returning them to their original state
|
||||||
|
|
||||||
|
|
||||||
|
|IN: CBA RC | output | comment |
|
||||||
|
|--------------|--------------|-----------|
|
||||||
|
| 000 000 00 | -- ----- - | init |
|
||||||
|
| --- --- 1c | -- ----- - | reset |
|
||||||
|
| --- --- 0c | -- ----- - | |
|
||||||
|
| --- 111 -c | -- 11100 - | |
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
| --- 000 tc | -- ----- - | reset |
|
||||||
|
| --- --- tc | -- 11100 - | locked |
|
||||||
|
| --- 111 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 100 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 101 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 100 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
| --- 000 -c | -- 11111 - | still open|
|
||||||
|
| --- --- 1c | -- 11100 - | reset |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
=============================
|
||||||
|
Note that using ‘c’ is optional. A row like
|
||||||
|
|
||||||
|
|
||||||
|
|IN: CBA RC | output | comment |
|
||||||
|
|--------------|--------------|-----------|
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
|
||||||
|
|
||||||
|
Could be done manually, with
|
||||||
|
|
||||||
|
|
||||||
|
|IN: CBA RC | output | comment |
|
||||||
|
|--------------|--------------|-----------|
|
||||||
|
| --- 110 -- | -- ----- - | set combo |
|
||||||
|
| --- --- -1 | -- ----- - | clock |
|
||||||
|
| --- --- -0 | -- 11111 - | success |
|
||||||
|
|
||||||
|
|
||||||
|
### Test framework function ###
|
||||||
|
|
||||||
|
The tt-tools system, in addition to downloading verilog and JSON for wokwi projects, now attempts to download a file name truthtable.md.
|
||||||
|
|
||||||
|
If this file is found, it assumes that te truthtable test must be run. To do this, the system now:
|
||||||
|
|
||||||
|
1) copies any directories found under testing/lib to the src/ directory, assuming these are python support packages; and
|
||||||
|
2) for each file in testing/src-tpl it will read in the file, replace any instance of WOKWI_ID with the appropriate id, and then write out a corresponding file into the actual src/ directory.
|
||||||
|
|
||||||
|
From this point, all that should be required is to run a make from within the src directory to launch cocotb with the test necessary to put the table through its paces.
|
||||||
|
|
||||||
|
|
||||||
|
2023-04-06 Pat Deegan
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
'''
|
||||||
|
TruthTable testing
|
||||||
|
Copyright (C) 2023 Pat Deegan, https://psychogenic.com
|
||||||
|
|
||||||
|
A simple system to take truth table data from yosys, like
|
||||||
|
|
||||||
|
io_in | io_out
|
||||||
|
---------- | -----------
|
||||||
|
8'11111000 | 8'xxxxxxx0
|
||||||
|
8'11111001 | 8'xxxxxxx1
|
||||||
|
8'11111010 | 8'xxxxxxx0
|
||||||
|
...
|
||||||
|
|
||||||
|
or manually crafted combi- or clocked sequences, encapsulated in
|
||||||
|
markdown tables such as
|
||||||
|
|
||||||
|
|IN: CBA RC | output | comment |
|
||||||
|
|--------------|--------------|-----------|
|
||||||
|
| --- --- 1c | -- ----- - | reset |
|
||||||
|
| --- --- 0c | -- ----- - | |
|
||||||
|
| --- 111 -c | -- 11100 - | |
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
| --- 000 tc | -- ----- - | reset |
|
||||||
|
| --- --- tc | -- 11100 - | locked |
|
||||||
|
...
|
||||||
|
|
||||||
|
and run it through cocotb harness.
|
||||||
|
|
||||||
|
SAMPLE USAGE:
|
||||||
|
|
||||||
|
@cocotb.test()
|
||||||
|
async def truthTableCompare(dut):
|
||||||
|
i_bus = dut.io_in
|
||||||
|
o_bus = dut.io_out
|
||||||
|
tt = truthtable.loadSimpleTruthTable('test/truthtable.txt')
|
||||||
|
await tt.testAll(i_bus, o_bus, dut._log)
|
||||||
|
|
||||||
|
|
||||||
|
SPDX-FileCopyrightText: © 2023 Pat Deegan, https://psychogenic.com
|
||||||
|
SPDX-License-Identifier: Apache2.0
|
||||||
|
'''
|
||||||
|
|
||||||
|
from string import Template as StringTemplate
|
||||||
|
from cocotb.triggers import Timer
|
||||||
|
from cocotb.binary import BinaryValue
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class SaneBinaryValue(BinaryValue):
|
||||||
|
def __init__(self, vstr:str):
|
||||||
|
super().__init__(vstr, n_bits=len(vstr), bigEndian=False)
|
||||||
|
numbits = len(vstr)
|
||||||
|
self.hardBit = [True]*numbits
|
||||||
|
for i in range(numbits):
|
||||||
|
c = vstr[i]
|
||||||
|
if c != '1' and c != '0':
|
||||||
|
self.hardBit[(numbits - 1) - i] = False
|
||||||
|
|
||||||
|
class TruthMapping:
|
||||||
|
def __init__(self, resultingValue:str):
|
||||||
|
self.result = SaneBinaryValue(resultingValue)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Expecting {self.result.binstr}'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TruthTable:
|
||||||
|
'''
|
||||||
|
Holds the sequence/expected mapping of inputs -> outputs
|
||||||
|
and provides an async testAll() to perform the testing.
|
||||||
|
'''
|
||||||
|
def __init__(self, interStepDelay:int=10, interStepTimeUnits:str='ns'):
|
||||||
|
self.mappings = []
|
||||||
|
self.stepDelayTime = interStepDelay
|
||||||
|
self.stepDelayUnits = interStepTimeUnits
|
||||||
|
|
||||||
|
|
||||||
|
def addMapping(self, relationship:TruthMapping):
|
||||||
|
self.mappings.append(relationship)
|
||||||
|
|
||||||
|
def getMapping(self, idx:int) -> TruthMapping:
|
||||||
|
if idx >= len(self.mappings):
|
||||||
|
raise IndexError('out of bounds on mapping')
|
||||||
|
|
||||||
|
return self.mappings[idx]
|
||||||
|
|
||||||
|
def numMappings(self):
|
||||||
|
return len(self.mappings)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.numMappings()
|
||||||
|
|
||||||
|
def __getitem__(self, idx:int):
|
||||||
|
return self.getMapping(idx)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
numsteps = len(self)
|
||||||
|
retStrs = [f"Truthtable with {numsteps} mappings"]
|
||||||
|
for i in range(numsteps):
|
||||||
|
step = self[i]
|
||||||
|
retStrs.append(f'{i + 1}\t{str(step)}')
|
||||||
|
|
||||||
|
return '\n'.join(retStrs)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
print(str(self))
|
||||||
|
|
||||||
|
|
||||||
|
async def testAll(self, i_bus, o_bus, logger=None):
|
||||||
|
for i in range(len(self)):
|
||||||
|
|
||||||
|
i_bus.value = self[i].state
|
||||||
|
await Timer(self.stepDelayTime, units=self.stepDelayUnits) # wait a tad
|
||||||
|
|
||||||
|
logger.debug(f'Set i_bus to {i_bus.value} vs {self[i].state}')
|
||||||
|
for idx in range(8):
|
||||||
|
logger.debug(f'{i}-{idx}: i_bus[{idx}] == {i_bus[idx]} == {self[i].state[idx]}')
|
||||||
|
|
||||||
|
expectedResult = self[i].result
|
||||||
|
if logger is not None:
|
||||||
|
logger.info(f'State {i}, setting input to {self[i].state}, expecting {expectedResult} (got {o_bus.value})')
|
||||||
|
|
||||||
|
# this is so stupid that it's probably wrong... there _must_ be
|
||||||
|
# a good way to do this, otherwise _what_ is the point of having
|
||||||
|
# unknown and don't care... but anyway,
|
||||||
|
# will die if o_bus[n] is some defined value
|
||||||
|
# but we expect a don't know (x) or even a don't care (-), ugh
|
||||||
|
# manual style
|
||||||
|
for bit in range(len(o_bus)):
|
||||||
|
if expectedResult.hardBit[bit]:
|
||||||
|
# ok, safe to compare, duh
|
||||||
|
# logger.info(f'Doing bit {bit}: {o_bus[bit]} == {expectedResult[bit]}')
|
||||||
|
assert o_bus[bit] == expectedResult[bit]
|
||||||
|
|
||||||
|
|
||||||
|
class OneToOneTruthMapping(TruthMapping):
|
||||||
|
def __init__(self, state:str, output:str):
|
||||||
|
super().__init__(output)
|
||||||
|
self.state = SaneBinaryValue(state)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Set {self.state.binstr}, expect {self.result.binstr}'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableEntryParser:
|
||||||
|
'''
|
||||||
|
A little class that validates and cleans up
|
||||||
|
input and output specifications.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, logger):
|
||||||
|
self.log = logger
|
||||||
|
self.optionalPrefix = r"(\d'|0b)?"
|
||||||
|
self.validInputValues = r'(\s*[01tTcCxX-]\s*){8}' # 0/1 hard-coded, t/T toggle, c/C clocking, x or - unchanged
|
||||||
|
self.validOutputValues = r'(\s*[01xX-]\s*){8}' # 0/1 hard-coded,x or - don't care
|
||||||
|
|
||||||
|
|
||||||
|
valueMatchReTpl = StringTemplate(r'\s*$prefix($valmatch)')
|
||||||
|
self.inputValueValidatorRe = re.compile(
|
||||||
|
valueMatchReTpl.substitute(
|
||||||
|
prefix=self.optionalPrefix, valmatch=self.validInputValues))
|
||||||
|
self.outputValueValidatorRe = re.compile(
|
||||||
|
valueMatchReTpl.substitute(
|
||||||
|
prefix=self.optionalPrefix, valmatch=self.validOutputValues))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def validateAndClean(self, val:str, usingRegex):
|
||||||
|
|
||||||
|
if val is None or not len(val):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
mtch = usingRegex.match(val)
|
||||||
|
|
||||||
|
|
||||||
|
if mtch is None:
|
||||||
|
vcln = val.strip()
|
||||||
|
if not len(vcln):
|
||||||
|
# just whitespace
|
||||||
|
return None
|
||||||
|
|
||||||
|
if vcln.find('#') >= 0 or vcln.find('//') >=0:
|
||||||
|
# just a comment
|
||||||
|
return None
|
||||||
|
|
||||||
|
if vcln.find('1') >=0 or vcln.find('0') >=0 or vcln.find('x') >= 0:
|
||||||
|
self.log.error(f"Bad value passed ? '{val}'")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
return re.sub(r'\s+', '', mtch.group(0)).lower()
|
||||||
|
|
||||||
|
def inputFrom(self, val:str):
|
||||||
|
return self.validateAndClean(val, self.inputValueValidatorRe)
|
||||||
|
|
||||||
|
def outputFrom(self, val:str):
|
||||||
|
if val is None or not len(val):
|
||||||
|
return None
|
||||||
|
return self.validateAndClean(val, self.outputValueValidatorRe)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableParser:
|
||||||
|
'''
|
||||||
|
Base class for tabular configuration values for input sequence and
|
||||||
|
expected outputs.
|
||||||
|
|
||||||
|
This class handles maintaining the truth table and the state between
|
||||||
|
steps in a sequence.
|
||||||
|
|
||||||
|
Basically does everything except handle the particularities of a
|
||||||
|
give table format (e.g. markdown).
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, logger=None, numIOBits:int=8):
|
||||||
|
if logger is None:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
self.log = logger
|
||||||
|
self.numIOBits = numIOBits
|
||||||
|
self.valueParser = TestTableEntryParser(self.log)
|
||||||
|
|
||||||
|
|
||||||
|
self._truthTable = None
|
||||||
|
self._textTable = []
|
||||||
|
|
||||||
|
self._currentInput = SaneBinaryValue('0'*numIOBits)
|
||||||
|
self._ignoreOutput = SaneBinaryValue('-'*numIOBits)
|
||||||
|
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._currentInput = SaneBinaryValue('00000000')
|
||||||
|
self._truthTable = None
|
||||||
|
self._textTable = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def truthTable(self) -> TruthTable:
|
||||||
|
if self._truthTable is None:
|
||||||
|
self._truthTable = TruthTable()
|
||||||
|
|
||||||
|
return self._truthTable
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _inputStateFromBits(self, inputBits):
|
||||||
|
|
||||||
|
newBits = ['0'] * self.numIOBits
|
||||||
|
|
||||||
|
for i in range(self.numIOBits):
|
||||||
|
|
||||||
|
if inputBits[i] == '0' or inputBits[i] == '1':
|
||||||
|
newBits[i] = inputBits[i]
|
||||||
|
self._currentInput[i] = int(inputBits[i])
|
||||||
|
elif inputBits[i] == 't':
|
||||||
|
# toggled
|
||||||
|
self._currentInput[i] = ~self._currentInput[i]
|
||||||
|
newBits[i] = str(self._currentInput[i])
|
||||||
|
else:
|
||||||
|
# unchanged
|
||||||
|
newBits[i] = str(self._currentInput[i])
|
||||||
|
|
||||||
|
|
||||||
|
return ''.join(newBits)
|
||||||
|
|
||||||
|
|
||||||
|
def addStep(self, inputValue:str, outputValue:str=''):
|
||||||
|
inputBits = self.valueParser.inputFrom(inputValue)
|
||||||
|
|
||||||
|
if inputBits is None or not len(inputBits):
|
||||||
|
self.log.info(f'Unable to parse input bits: {inputValue}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if inputBits.find('c') >= 0:
|
||||||
|
# clocking...
|
||||||
|
setupInputs = inputBits.replace('c', 'x')
|
||||||
|
self.log.debug(f'Have clocking in inputs {inputBits}, adding setup step {setupInputs}')
|
||||||
|
self.addStep(setupInputs) # setup
|
||||||
|
|
||||||
|
# clock edge 1
|
||||||
|
|
||||||
|
clockEdges = inputBits.replace('t', 'x') # don't toggle twice!
|
||||||
|
|
||||||
|
clockInAndHold = clockEdges.replace('c', 't')
|
||||||
|
|
||||||
|
self.log.debug(f'Now clock {clockEdges} x2')
|
||||||
|
|
||||||
|
self.addStep(clockInAndHold) # clock in
|
||||||
|
|
||||||
|
# clock edge 2
|
||||||
|
self.addStep(clockInAndHold) # clock out
|
||||||
|
|
||||||
|
|
||||||
|
inputBinVal = SaneBinaryValue(self._inputStateFromBits(inputBits))
|
||||||
|
|
||||||
|
outputBinVal = self._ignoreOutput
|
||||||
|
|
||||||
|
outputBits = ''
|
||||||
|
if outputValue is not None and len(outputValue):
|
||||||
|
self.log.info(f'outval is "{outputValue}"')
|
||||||
|
outputBits = self.valueParser.outputFrom(outputValue)
|
||||||
|
if outputBits is None or not len(outputBits):
|
||||||
|
self.log.info(f'Unable to parse output bits: {outputBits}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
outputBinVal = SaneBinaryValue(outputBits)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self.log.info(f'Mapped {inputBits} -> {outputBits} ({inputBinVal} -> {outputBinVal})')
|
||||||
|
|
||||||
|
self.truthTable.addMapping(OneToOneTruthMapping(inputBinVal.binstr, outputBinVal.binstr))
|
||||||
|
self._textTable.append((inputBits, outputBits))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generateFrom(self, contents:str):
|
||||||
|
self.reset() # reset the table
|
||||||
|
self.log.error("Override generateFrom() in subclass")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
retStrs = [f'Truth table parser with {len(self._textTable)} steps added.']
|
||||||
|
for i in range(len(self._textTable)):
|
||||||
|
step = self._textTable[i]
|
||||||
|
if step[1] is not None and len(step[1]):
|
||||||
|
retStrs.append(f'{i+1}\t{step[0]} -> {step[1]}')
|
||||||
|
else:
|
||||||
|
retStrs.append(f'{i+1}\t{step[0]}')
|
||||||
|
|
||||||
|
return '\n'.join(retStrs)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
print(str(self))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownTestTableParser(TestTableParser):
|
||||||
|
'''
|
||||||
|
A specialization of the TestTableParser that handles the
|
||||||
|
particulars of extracting test inputs/outputs from a markdown
|
||||||
|
table.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, columnSeparatorRegex:str=r'\|', logger=None):
|
||||||
|
'''
|
||||||
|
We want to support markdown tables.
|
||||||
|
These may have some header row, followed by a separator (which may
|
||||||
|
or may not include aligment info, i.e. ':'), finally followed
|
||||||
|
by our content of interest. E.g.
|
||||||
|
|
||||||
|
| some name | another | <- column headers
|
||||||
|
|-----------|---------:| <- header sep
|
||||||
|
| CONTENT | CONTENT | <- actual content of interest
|
||||||
|
|
||||||
|
CONTENT is 8 bits, MSB
|
||||||
|
[7 ... 0]
|
||||||
|
A B C D E F G H
|
||||||
|
|
||||||
|
Where each bit is either:
|
||||||
|
0
|
||||||
|
1
|
||||||
|
X (or x) don't change/don't care
|
||||||
|
T (or t) toggle
|
||||||
|
C (or c) clock
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
super().__init__(logger)
|
||||||
|
|
||||||
|
self.columnSeperatorRe = columnSeparatorRegex
|
||||||
|
eol = r'(\r\n|\r|\n)'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# the |-------|----- ... line
|
||||||
|
tableHeaderSepTpl = StringTemplate(r'\s*($sep\s*:?-{1,}:?\s*){2,}$sep$eol')
|
||||||
|
self.headerSepRe = re.compile(tableHeaderSepTpl.substitute(sep=self.columnSeperatorRe, eol=eol), re.M)
|
||||||
|
|
||||||
|
# each content line thereafter
|
||||||
|
# | CONTENT | CONTENT |
|
||||||
|
contentRowTpl = StringTemplate(r'^\s*(($sep\s*[^$sep\r\n]+){1,})$sep$eol')
|
||||||
|
self.contentRowRe = re.compile(contentRowTpl.substitute(sep=self.columnSeperatorRe, eol=eol), re.M)
|
||||||
|
|
||||||
|
|
||||||
|
self.rowItemSplitterRe = re.compile(self.columnSeperatorRe)
|
||||||
|
|
||||||
|
def parseMarkdownTable(self, contents:str):
|
||||||
|
|
||||||
|
headerSepSearch = self.headerSepRe.search(contents)
|
||||||
|
if headerSepSearch is not None:
|
||||||
|
# did have a header
|
||||||
|
# dump it, and everything before it
|
||||||
|
headerSpan = headerSepSearch.span()
|
||||||
|
headerEndPos = headerSpan[1]
|
||||||
|
self.log.debug(f"Dumping header to {headerEndPos}\n{contents[:headerEndPos]}")
|
||||||
|
contents = contents[headerEndPos:]
|
||||||
|
|
||||||
|
# extract valid rows in first pass
|
||||||
|
self.log.debug(f"Content row RE is {self.contentRowRe}")
|
||||||
|
contentRowMatches = self.contentRowRe.findall(contents)
|
||||||
|
if contentRowMatches is None or not len(contentRowMatches):
|
||||||
|
self.log.error('No valid rows found in markdown table')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
numSteps = 0
|
||||||
|
for aRow in contentRowMatches:
|
||||||
|
self.log.debug(f'Matched row {aRow}')
|
||||||
|
entries = self.rowItemSplitterRe.split(aRow[0])
|
||||||
|
|
||||||
|
# note, we have entries like
|
||||||
|
# SEP AAA SEP BBB SEP
|
||||||
|
# so splitting on SEP gives us an empty entry at front of list
|
||||||
|
# hence the requirement for len == 2 for at least 1 entry, len == 3
|
||||||
|
# for input and output.
|
||||||
|
|
||||||
|
if len(entries) < 2:
|
||||||
|
self.log.info(f'Row has insufficient entries ? ("{aRow[0]}")')
|
||||||
|
continue
|
||||||
|
|
||||||
|
inVal = entries[1]
|
||||||
|
outVal = None
|
||||||
|
|
||||||
|
if len(entries) > 2 and len(entries[2]):
|
||||||
|
outVal = entries[2].strip()
|
||||||
|
|
||||||
|
if self.addStep(inVal, outVal):
|
||||||
|
numSteps += 1
|
||||||
|
|
||||||
|
|
||||||
|
return numSteps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def generateFrom(self, contents:str):
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
tableParsed = self.parseMarkdownTable(contents)
|
||||||
|
|
||||||
|
if tableParsed is None or not tableParsed:
|
||||||
|
self.log.error("Could not extract truthtable from contents")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.truthTable
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parseSimpleTable(contents:str):
|
||||||
|
tt = TruthTable()
|
||||||
|
m = re.compile(r'''^\s*\d'(\d+)\s*\|\s*\d'([zZxX\d]+)''', re.M)
|
||||||
|
for match in m.findall(contents):
|
||||||
|
if match[1] != 'x':
|
||||||
|
# we skip anything where all results are don't care
|
||||||
|
# result = match[1].replace('x', '-') # now using the BinaryValue override
|
||||||
|
result = match[1]
|
||||||
|
tt.addMapping(OneToOneTruthMapping(match[0],result))
|
||||||
|
|
||||||
|
return tt
|
||||||
|
|
||||||
|
def parseMarkdownTable(contents:str, logger=None):
|
||||||
|
m = MarkdownTestTableParser(logger=logger)
|
||||||
|
table = m.generateFrom(contents)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def loadSimpleTruthTable(filepath:str):
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
contents = f.read()
|
||||||
|
return parseSimpleTable(contents)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def loadMarkdownTruthTable(filepath:str, logger=None):
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
contents = f.read()
|
||||||
|
return parseMarkdownTable(contents, logger)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
TruthTableExample = '''
|
||||||
|
\\io_in | \\io_out
|
||||||
|
---------- | -----------
|
||||||
|
8'11111000 | 8'xxxxxxx0
|
||||||
|
8'11111001 | 8'xxxxxxx1
|
||||||
|
8'11111010 | 8'xxxxxxx0
|
||||||
|
8'11111011 | 8'xxxxxxx1
|
||||||
|
8'11111100 | 8'xxxxxxx0
|
||||||
|
8'11111101 | 8'xxxxxxx1
|
||||||
|
8'11111110 | 8'xxxxxxx1
|
||||||
|
8'11111111 | 8'xxxxxxx0
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
# with comments and spacing
|
||||||
|
TruthTableMarkdownExample = '''
|
||||||
|
|IN: CBA RC | output | comment |
|
||||||
|
|--------------|--------------|-----------|
|
||||||
|
| 000 000 00 | -- ----- - | init |
|
||||||
|
| --- --- 1c | -- ----- - | reset |
|
||||||
|
| --- --- 0c | -- ----- - | |
|
||||||
|
| --- 111 -c | -- 11100 - | |
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
| --- 000 tc | -- ----- - | reset |
|
||||||
|
| --- --- tc | -- 11100 - | locked |
|
||||||
|
| --- 111 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 100 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 101 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 100 -c | -- 11100 - | bad combo |
|
||||||
|
| --- 110 -c | -- 11111 - | success |
|
||||||
|
| --- 000 -c | -- 11111 - | still open|
|
||||||
|
| --- --- 1c | -- 11100 - | reset |
|
||||||
|
'''
|
||||||
|
if __name__ == "__main__":
|
||||||
|
m = MarkdownTestTableParser()
|
||||||
|
table = m.generateFrom(TruthTableMarkdownExample)
|
||||||
|
|
||||||
|
m.dump()
|
||||||
|
table.dump()
|
||||||
|
#tt = parseSimpleTable(TruthTableExample)
|
||||||
|
#print(f'with state {tt[0].state} you get {tt[0].result}')
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Makefile
|
||||||
|
# See https://docs.cocotb.org/en/stable/quickstart.html for more info
|
||||||
|
|
||||||
|
# defaults
|
||||||
|
SIM ?= icarus
|
||||||
|
TOPLEVEL_LANG ?= verilog
|
||||||
|
|
||||||
|
# normal simulation
|
||||||
|
ifneq ($(GATES),yes)
|
||||||
|
|
||||||
|
# this is the only part you should need to modify:
|
||||||
|
VERILOG_SOURCES += $(PWD)/cells.v $(PWD)/user_module_WOKWI_ID.v $(PWD)/test_wokwi.v
|
||||||
|
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
# gate level simulation requires some extra setup, you shouldn't need to touch this
|
||||||
|
COMPILE_ARGS += -DGL_TEST
|
||||||
|
COMPILE_ARGS += -DFUNCTIONAL
|
||||||
|
COMPILE_ARGS += -DUSE_POWER_PINS
|
||||||
|
COMPILE_ARGS += -DSIM
|
||||||
|
COMPILE_ARGS += -DUNIT_DELAY=#1
|
||||||
|
VERILOG_SOURCES += $(PDK_ROOT)/sky130A/libs.ref/sky130_fd_sc_hd/verilog/primitives.v
|
||||||
|
VERILOG_SOURCES += $(PDK_ROOT)/sky130A/libs.ref/sky130_fd_sc_hd/verilog/sky130_fd_sc_hd.v
|
||||||
|
|
||||||
|
# this gets copied in by the GDS action workflow
|
||||||
|
VERILOG_SOURCES += $(PWD)/test_wokwi.v $(PWD)/gate_level_netlist.v
|
||||||
|
endif
|
||||||
|
|
||||||
|
# TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file
|
||||||
|
TOPLEVEL = test_wokwi
|
||||||
|
|
||||||
|
# MODULE is the basename of the Python test file
|
||||||
|
MODULE = test
|
||||||
|
|
||||||
|
# include cocotb's make rules to take care of the simulator setup
|
||||||
|
include $(shell cocotb-config --makefiles)/Makefile.sim
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'''
|
||||||
|
|
||||||
|
Wokwi TinyTapeout truth-table based auto test sequence.
|
||||||
|
|
||||||
|
SPDX-FileCopyrightText: © 2023 Pat Deegan, https://psychogenic.com
|
||||||
|
SPDX-License-Identifier: Apache2.0
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import cocotb
|
||||||
|
import testutils.truthtable as truthtable
|
||||||
|
|
||||||
|
@cocotb.test()
|
||||||
|
async def truthTableCompare(parentDUT):
|
||||||
|
'''
|
||||||
|
truthTableCompare -- loads markdown truth table and, if load succeeded, actually performs the tests.
|
||||||
|
'''
|
||||||
|
usermodule = parentDUT.dut
|
||||||
|
i_bus = parentDUT.io_in
|
||||||
|
o_bus = parentDUT.io_out
|
||||||
|
tt = truthtable.loadMarkdownTruthTable('truthtable.md', usermodule._log)
|
||||||
|
if tt is None:
|
||||||
|
usermodule._log.info('No truth table loaded, no table compare test to run')
|
||||||
|
return
|
||||||
|
|
||||||
|
usermodule._log.debug(str(tt))
|
||||||
|
await tt.testAll(i_bus, o_bus, usermodule._log)
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: Apache2.0
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
`default_nettype none
|
||||||
|
|
||||||
|
module test_wokwi ();
|
||||||
|
wire [7:0] io_out;
|
||||||
|
wire [7:0] io_in;
|
||||||
|
|
||||||
|
user_module_WOKWI_ID dut (
|
||||||
|
`ifdef GL_TEST
|
||||||
|
.vccd1( 1'b1),
|
||||||
|
.vssd1( 1'b0),
|
||||||
|
`endif
|
||||||
|
.io_in (io_in),
|
||||||
|
.io_out(io_out)
|
||||||
|
);
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
$dumpfile("wokwi_tb_WOKWI_ID.vcd");
|
||||||
|
$dumpvars(0, test_wokwi);
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
Reference in New Issue
Block a user