mirror of https://github.com/YosysHQ/yosys.git
Add UDP support
This commit is contained in:
parent
cc9692caab
commit
0feda723a8
|
|
@ -23,6 +23,8 @@ yosys_frontend(verilog
|
|||
verilog_frontend.h
|
||||
verilog_lexer.h
|
||||
verilog_location.h
|
||||
verilog_udp.cc
|
||||
verilog_udp.h
|
||||
${FLEX_verilog_lexer_OUTPUTS}
|
||||
${BISON_verilog_parser_OUTPUTS}
|
||||
INCLUDE_DIRS
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ static parser::symbol_type process_str(char *str, int len, bool triple, parser::
|
|||
%x SYNOPSYS_FLAGS
|
||||
%x IMPORT_DPI
|
||||
%x BASED_CONST
|
||||
%x UDPTABLE
|
||||
|
||||
UNSIGNED_NUMBER [0-9][0-9_]*
|
||||
FIXED_POINT_NUMBER_DEC [0-9][0-9_]*\.[0-9][0-9_]*([eE][-+]?[0-9_]+)?
|
||||
|
|
@ -355,6 +356,9 @@ TIME_SCALE_SUFFIX [munpf]?s
|
|||
|
||||
"module" { return parser::make_TOK_MODULE(out_loc); }
|
||||
"endmodule" { return parser::make_TOK_ENDMODULE(out_loc); }
|
||||
"primitive" { return parser::make_TOK_PRIMITIVE_DEF(out_loc); }
|
||||
"endprimitive" { return parser::make_TOK_ENDPRIMITIVE(out_loc); }
|
||||
"table" { BEGIN(UDPTABLE); return parser::make_TOK_TABLE(out_loc); }
|
||||
"function" { return parser::make_TOK_FUNCTION(out_loc); }
|
||||
"endfunction" { return parser::make_TOK_ENDFUNCTION(out_loc); }
|
||||
"task" { return parser::make_TOK_TASK(out_loc); }
|
||||
|
|
@ -699,15 +703,29 @@ import[ \t\r\n]+\"(DPI|DPI-C)\"[ \t\r\n]+function[ \t\r\n]+ {
|
|||
{FIXED_POINT_NUMBER_DEC}{TIME_SCALE_SUFFIX} { return parser::make_TOK_TIME_SCALE(out_loc); }
|
||||
{FIXED_POINT_NUMBER_NO_DEC}{TIME_SCALE_SUFFIX} { return parser::make_TOK_TIME_SCALE(out_loc); }
|
||||
|
||||
<INITIAL,BASED_CONST>"/*" { comment_caller=YY_START; BEGIN(COMMENT); }
|
||||
<INITIAL,BASED_CONST,UDPTABLE>"/*" { comment_caller=YY_START; BEGIN(COMMENT); }
|
||||
<COMMENT>. /* ignore comment body */
|
||||
<COMMENT>\n /* ignore comment body */
|
||||
<COMMENT>"*/" { BEGIN(comment_caller); }
|
||||
|
||||
|
||||
<INITIAL,BASED_CONST>[ \t\r\n] /* ignore whitespaces */
|
||||
<INITIAL,BASED_CONST,UDPTABLE>[ \t\r\n] /* ignore whitespaces */
|
||||
<INITIAL,BASED_CONST>\\[\r\n] /* ignore continuation sequence */
|
||||
<INITIAL,BASED_CONST>"//"[^\r\n]* /* ignore one-line comments */
|
||||
<INITIAL,BASED_CONST,UDPTABLE>"//"[^\r\n]* /* ignore one-line comments */
|
||||
|
||||
/* UDP state table: each field is a single symbol (or a parenthesised
|
||||
transition). ':' and ';' delimit the input/state/output fields and
|
||||
terminate rows; "endtable" leaves the table sub-language. */
|
||||
<UDPTABLE>"endtable" { BEGIN(0); return parser::make_TOK_ENDTABLE(out_loc); }
|
||||
<UDPTABLE>\([01xXbB?][01xXbB?]\) {
|
||||
string_t val = std::make_unique<std::string>(YYText());
|
||||
return parser::make_TOK_UDP_VALUE(std::move(val), out_loc);
|
||||
}
|
||||
<UDPTABLE>[01xXbB?rRfFpPnN*\-] {
|
||||
string_t val = std::make_unique<std::string>(YYText());
|
||||
return parser::make_TOK_UDP_VALUE(std::move(val), out_loc);
|
||||
}
|
||||
<UDPTABLE>[:;] { return char_tok(*YYText(), out_loc); }
|
||||
|
||||
<INITIAL>. { return char_tok(*YYText(), out_loc); }
|
||||
<*>. { BEGIN(0); return char_tok(*YYText(), out_loc); }
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@
|
|||
bool current_modport_input, current_modport_output;
|
||||
bool default_nettype_wire = true;
|
||||
std::istream* lexin;
|
||||
// state of the user-defined primitive currently being parsed
|
||||
std::unique_ptr<UdpParseData> udp;
|
||||
|
||||
AstNode* saveChild(std::unique_ptr<AstNode> child);
|
||||
AstNode* pushChild(std::unique_ptr<AstNode> child);
|
||||
|
|
@ -429,6 +431,7 @@
|
|||
#include <string>
|
||||
#include <memory>
|
||||
#include "frontends/verilog/verilog_frontend.h"
|
||||
#include "frontends/verilog/verilog_udp.h"
|
||||
|
||||
struct specify_target {
|
||||
char polarity_op;
|
||||
|
|
@ -487,6 +490,8 @@
|
|||
%token <string_t> TOK_SVA_LABEL TOK_SPECIFY_OPER TOK_MSG_TASKS
|
||||
%token <string_t> TOK_BASE TOK_BASED_CONSTVAL TOK_UNBASED_UNSIZED_CONSTVAL
|
||||
%token <string_t> TOK_USER_TYPE TOK_PKG_USER_TYPE
|
||||
%token <string_t> TOK_UDP_VALUE
|
||||
%token TOK_PRIMITIVE_DEF TOK_ENDPRIMITIVE TOK_TABLE TOK_ENDTABLE
|
||||
%token TOK_ASSERT TOK_ASSUME TOK_RESTRICT TOK_COVER TOK_FINAL
|
||||
%token ATTR_BEGIN ATTR_END DEFATTR_BEGIN DEFATTR_END
|
||||
%token TOK_MODULE TOK_ENDMODULE TOK_PARAMETER TOK_LOCALPARAM TOK_DEFPARAM
|
||||
|
|
@ -550,7 +555,8 @@
|
|||
%type <string_t> opt_label opt_sva_label tok_prim_wrapper hierarchical_id hierarchical_type_id integral_number
|
||||
%type <string_t> type_name
|
||||
%type <ast_t> opt_enum_init enum_type struct_type enum_struct_type func_return_type typedef_base_type
|
||||
%type <boolean_t> opt_property always_comb_or_latch always_or_always_ff
|
||||
%type <boolean_t> opt_property always_comb_or_latch always_or_always_ff udp_output_reg_opt
|
||||
%type <string_t> udp_entry_tail
|
||||
%type <boolean_t> opt_signedness_default_signed opt_signedness_default_unsigned
|
||||
%type <integer_t> integer_atom_type integer_vector_type
|
||||
%type <al_t> attr if_attr case_attr
|
||||
|
|
@ -602,6 +608,7 @@ input: {
|
|||
|
||||
design:
|
||||
module design |
|
||||
udp_primitive design |
|
||||
defattr design |
|
||||
task_func_decl design |
|
||||
param_decl design |
|
||||
|
|
@ -714,6 +721,124 @@ module:
|
|||
extra->exitTypeScope();
|
||||
};
|
||||
|
||||
// User-defined primitives (IEEE 1364-2005 clause 8). The parser collects the
|
||||
// definition into extra->udp and make_udp_module() lowers it into a normal
|
||||
// behavioural module.
|
||||
udp_primitive:
|
||||
attr TOK_PRIMITIVE_DEF TOK_ID {
|
||||
auto u = std::make_unique<UdpParseData>();
|
||||
u->loc = @2;
|
||||
u->name = *$3;
|
||||
u->attributes = std::move($1);
|
||||
extra->udp = std::move(u);
|
||||
} TOK_LPAREN udp_port_list TOK_RPAREN TOK_SEMICOL
|
||||
udp_port_decls_opt
|
||||
udp_initial_opt
|
||||
udp_body
|
||||
TOK_ENDPRIMITIVE opt_label {
|
||||
auto mod = make_udp_module(*extra->udp);
|
||||
SET_AST_NODE_LOC(mod.get(), @2, @$);
|
||||
checkLabelsMatch(@13, "Primitive name", $3.get(), $13.get());
|
||||
extra->saveChild(std::move(mod));
|
||||
extra->udp.reset();
|
||||
};
|
||||
|
||||
udp_port_list:
|
||||
udp_port_item |
|
||||
udp_port_list TOK_COMMA udp_port_item;
|
||||
|
||||
udp_port_item:
|
||||
TOK_ID {
|
||||
extra->udp->port_order.push_back(*$1);
|
||||
} |
|
||||
TOK_OUTPUT udp_output_reg_opt TOK_ID {
|
||||
extra->udp->port_order.push_back(*$3);
|
||||
extra->udp->output_name = *$3;
|
||||
extra->udp->output_declared = true;
|
||||
if ($2)
|
||||
extra->udp->is_sequential = true;
|
||||
} udp_init_assign_opt |
|
||||
TOK_INPUT TOK_ID {
|
||||
extra->udp->port_order.push_back(*$2);
|
||||
extra->udp->input_names.insert(*$2);
|
||||
};
|
||||
|
||||
udp_port_decls_opt:
|
||||
udp_port_decls | %empty;
|
||||
|
||||
udp_port_decls:
|
||||
udp_port_decl | udp_port_decls udp_port_decl;
|
||||
|
||||
udp_port_decl:
|
||||
TOK_OUTPUT udp_output_reg_opt TOK_ID udp_init_assign_opt TOK_SEMICOL {
|
||||
extra->udp->output_name = *$3;
|
||||
extra->udp->output_declared = true;
|
||||
if ($2)
|
||||
extra->udp->is_sequential = true;
|
||||
} |
|
||||
TOK_INPUT udp_input_id_list TOK_SEMICOL |
|
||||
TOK_REG TOK_ID TOK_SEMICOL {
|
||||
extra->udp->is_sequential = true;
|
||||
};
|
||||
|
||||
udp_input_id_list:
|
||||
TOK_ID {
|
||||
extra->udp->input_names.insert(*$1);
|
||||
} |
|
||||
udp_input_id_list TOK_COMMA TOK_ID {
|
||||
extra->udp->input_names.insert(*$3);
|
||||
};
|
||||
|
||||
udp_output_reg_opt:
|
||||
TOK_REG { $$ = true; } |
|
||||
%empty { $$ = false; };
|
||||
|
||||
udp_init_assign_opt:
|
||||
TOK_EQ expr {
|
||||
extra->udp->initial_val = std::move($2);
|
||||
} |
|
||||
%empty;
|
||||
|
||||
udp_initial_opt:
|
||||
TOK_INITIAL TOK_ID TOK_EQ expr TOK_SEMICOL {
|
||||
extra->udp->initial_val = std::move($4);
|
||||
} |
|
||||
%empty;
|
||||
|
||||
udp_body:
|
||||
TOK_TABLE udp_entries TOK_ENDTABLE;
|
||||
|
||||
udp_entries:
|
||||
udp_entry | udp_entries udp_entry;
|
||||
|
||||
udp_entry:
|
||||
udp_input_list TOK_COL TOK_UDP_VALUE udp_entry_tail TOK_SEMICOL {
|
||||
UdpTableRow row;
|
||||
row.loc = @1;
|
||||
row.inputs = std::move(extra->udp->scratch_inputs);
|
||||
extra->udp->scratch_inputs.clear();
|
||||
if ($4) {
|
||||
row.current = *$3;
|
||||
row.output = *$4;
|
||||
extra->udp->table_is_sequential = true;
|
||||
} else {
|
||||
row.output = *$3;
|
||||
}
|
||||
extra->udp->rows.push_back(std::move(row));
|
||||
};
|
||||
|
||||
udp_entry_tail:
|
||||
TOK_COL TOK_UDP_VALUE { $$ = std::move($2); } |
|
||||
%empty { $$ = nullptr; };
|
||||
|
||||
udp_input_list:
|
||||
TOK_UDP_VALUE {
|
||||
extra->udp->scratch_inputs.push_back(*$1);
|
||||
} |
|
||||
udp_input_list TOK_UDP_VALUE {
|
||||
extra->udp->scratch_inputs.push_back(*$2);
|
||||
};
|
||||
|
||||
module_para_opt:
|
||||
TOK_HASH TOK_LPAREN module_para_list TOK_RPAREN | %empty;
|
||||
|
||||
|
|
@ -1656,6 +1781,7 @@ module_path_primary:
|
|||
| TOK_ID
|
||||
// Deviate from specification: Normally string would not be allowed, however they are necessary for the ecp5 tests
|
||||
| TOK_STRING
|
||||
| TOK_LPAREN module_path_expression TOK_RPAREN
|
||||
// | module_path_concatenation
|
||||
// | module_path_multiple_concatenation
|
||||
// | function_subroutine_call
|
||||
|
|
@ -1723,10 +1849,15 @@ system_timing_arg :
|
|||
TOK_NEGEDGE ignspec_id |
|
||||
ignspec_expr ;
|
||||
|
||||
// Arguments may be omitted (e.g. the optional notifier/condition slots of
|
||||
// $setuphold), which appears as empty fields between commas.
|
||||
opt_system_timing_arg :
|
||||
system_timing_arg | %empty ;
|
||||
|
||||
system_timing_args :
|
||||
system_timing_arg |
|
||||
opt_system_timing_arg |
|
||||
system_timing_args TOK_IGNORED_SPECIFY_AND system_timing_arg |
|
||||
system_timing_args TOK_COMMA system_timing_arg ;
|
||||
system_timing_args TOK_COMMA opt_system_timing_arg ;
|
||||
|
||||
// for the time being this is OK, but we may write our own expr here.
|
||||
// as I'm not sure it is legal to use a full expr here (probably not)
|
||||
|
|
@ -2314,6 +2445,17 @@ single_cell_no_array:
|
|||
log_assert(extra->cell_hack);
|
||||
SET_AST_NODE_LOC(extra->cell_hack, @1, @$);
|
||||
extra->cell_hack = nullptr;
|
||||
} |
|
||||
/* no instance name: gate/UDP-style instantiation (IEEE 1364-2005 7.1, 8.6) */ {
|
||||
extra->astbuf2 = extra->astbuf1->clone();
|
||||
if (extra->astbuf2->type != AST_PRIMITIVE)
|
||||
extra->astbuf2->str = stringf("$cell$%d", autoidx++);
|
||||
extra->cell_hack = extra->astbuf2.get();
|
||||
extra->ast_stack.back()->children.push_back(std::move(extra->astbuf2));
|
||||
} TOK_LPAREN cell_port_list TOK_RPAREN {
|
||||
log_assert(extra->cell_hack);
|
||||
SET_AST_NODE_LOC(extra->cell_hack, @1, @$);
|
||||
extra->cell_hack = nullptr;
|
||||
}
|
||||
|
||||
single_cell_arraylist:
|
||||
|
|
@ -2339,18 +2481,9 @@ prim_list:
|
|||
prim_list TOK_COMMA single_prim;
|
||||
|
||||
single_prim:
|
||||
single_cell |
|
||||
/* no name */ {
|
||||
extra->astbuf2 = extra->astbuf1->clone();
|
||||
log_assert(!extra->cell_hack);
|
||||
extra->cell_hack = extra->astbuf2.get();
|
||||
// TODO optimize again
|
||||
extra->ast_stack.back()->children.push_back(std::move(extra->astbuf2));
|
||||
} TOK_LPAREN cell_port_list TOK_RPAREN {
|
||||
log_assert(extra->cell_hack);
|
||||
SET_AST_NODE_LOC(extra->cell_hack, @1, @$);
|
||||
extra->cell_hack = nullptr;
|
||||
}
|
||||
// single_cell now also covers the no-instance-name form (for both gate
|
||||
// primitives and UDP/module instantiations).
|
||||
single_cell
|
||||
|
||||
cell_parameter_list_opt:
|
||||
TOK_HASH TOK_LPAREN cell_parameter_list TOK_RPAREN | %empty;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,561 @@
|
|||
/*
|
||||
* yosys -- Yosys Open SYnthesis Suite
|
||||
*
|
||||
* Copyright (C) 2026 Remy Goldschmidt <taktoa@gmail.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Lowering of Verilog user-defined primitives (UDPs, IEEE 1364-2005 clause 8)
|
||||
* into ordinary Yosys netlists.
|
||||
*
|
||||
* A UDP is an event-driven, four-valued state machine: on every change of an
|
||||
* input it re-evaluates its state table using the new input values, the
|
||||
* *previous* input values (to recognise transitions), and the current output
|
||||
* state. We reproduce exactly that behaviour with a small netlist:
|
||||
*
|
||||
* assign out = G(in, prev_in, state); // combinational table evaluation
|
||||
* prev_in <= in; // $ff: previous input vector
|
||||
* state <= out; // $ff: previous output / state
|
||||
*
|
||||
* The two registers are `$ff` cells (formal/simulation flip-flops with an
|
||||
* implicit global clock that advances once per simulation event). Because
|
||||
* `out` is purely combinational it reflects the result of the current event
|
||||
* immediately, while the registers lag by one event and therefore hold the
|
||||
* "previous" values that the next evaluation needs. Under `yosys sim` (which
|
||||
* advances the global clock once per input change) this matches Icarus
|
||||
* Verilog's native UDP simulation bit-for-bit, including all x behaviour.
|
||||
*
|
||||
* The table is matched with case-equality (`$eqx`/`$nex`) so that the four
|
||||
* values 0/1/x are distinguished exactly; z inputs are treated as x by the
|
||||
* surrounding logic just as the standard requires.
|
||||
*/
|
||||
|
||||
#include "frontends/verilog/verilog_udp.h"
|
||||
#include "frontends/verilog/verilog_frontend.h"
|
||||
#include "kernel/log.h"
|
||||
|
||||
YOSYS_NAMESPACE_BEGIN
|
||||
using namespace AST;
|
||||
|
||||
namespace VERILOG_FRONTEND {
|
||||
|
||||
namespace {
|
||||
|
||||
using ast_t = std::unique_ptr<AstNode>;
|
||||
|
||||
struct UdpLower {
|
||||
UdpParseData &udp;
|
||||
AstSrcLocType loc;
|
||||
|
||||
int n_in = 0;
|
||||
std::vector<std::string> in_names; // input port names, header order
|
||||
std::vector<std::string> sig_names; // z->x mapped input signals (used for matching)
|
||||
std::string out_name;
|
||||
|
||||
AstNode *mod = nullptr; // module being built (for adding wires/cells)
|
||||
AstNode *out_wire = nullptr;
|
||||
int autoidx = 0;
|
||||
|
||||
UdpLower(UdpParseData &udp) : udp(udp), loc(udp.loc) {}
|
||||
|
||||
[[noreturn]] void error(const std::string &msg)
|
||||
{
|
||||
std::string fn = loc.begin.filename ? *loc.begin.filename : std::string("<unknown>");
|
||||
const char *nm = udp.name.empty() ? "" : udp.name.c_str() + (udp.name[0] == '\\' ? 1 : 0);
|
||||
log_file_error(fn, loc.begin.line, "UDP `%s': %s\n", nm, msg.c_str());
|
||||
}
|
||||
|
||||
// --- tiny AST builders ---------------------------------------------------
|
||||
ast_t N(AstNodeType t) { return std::make_unique<AstNode>(loc, t); }
|
||||
ast_t N(AstNodeType t, ast_t a) { return std::make_unique<AstNode>(loc, t, std::move(a)); }
|
||||
ast_t N(AstNodeType t, ast_t a, ast_t b) { return std::make_unique<AstNode>(loc, t, std::move(a), std::move(b)); }
|
||||
ast_t N(AstNodeType t, ast_t a, ast_t b, ast_t c) { return std::make_unique<AstNode>(loc, t, std::move(a), std::move(b), std::move(c)); }
|
||||
|
||||
ast_t mk_id(const std::string &name) { auto n = N(AST_IDENTIFIER); n->str = name; return n; }
|
||||
ast_t mk_const(RTLIL::State s) { return AstNode::mkconst_bits(loc, std::vector<RTLIL::State>{s}, false); }
|
||||
|
||||
ast_t mk_and(ast_t a, ast_t b)
|
||||
{
|
||||
if (!a) return b;
|
||||
if (!b) return a;
|
||||
return N(AST_BIT_AND, std::move(a), std::move(b));
|
||||
}
|
||||
ast_t mk_or(ast_t a, ast_t b)
|
||||
{
|
||||
if (!a) return b;
|
||||
if (!b) return a;
|
||||
return N(AST_BIT_OR, std::move(a), std::move(b));
|
||||
}
|
||||
// case-equality: (sig === value), always a definite 0/1
|
||||
ast_t mk_eqx(ast_t sig, RTLIL::State value) { return N(AST_EQX, std::move(sig), mk_const(value)); }
|
||||
// case-inequality of two one-bit signals: (a !== b)
|
||||
ast_t mk_nex(ast_t a, ast_t b) { return N(AST_NEX, std::move(a), std::move(b)); }
|
||||
|
||||
// --- symbol helpers ------------------------------------------------------
|
||||
// One-bit expression that is true iff `sig` matches the level symbol.
|
||||
// Returns null for the unconstrained symbols '?' (and 'b' is expanded).
|
||||
ast_t level_match(char sym, ast_t sig)
|
||||
{
|
||||
switch (tolower(sym)) {
|
||||
case '0': return mk_eqx(std::move(sig), RTLIL::State::S0);
|
||||
case '1': return mk_eqx(std::move(sig), RTLIL::State::S1);
|
||||
case 'x': return mk_eqx(std::move(sig), RTLIL::State::Sx);
|
||||
case '?': return nullptr; // matches anything
|
||||
case 'b': {
|
||||
// 0 or 1 (not x)
|
||||
auto a = mk_eqx(sig->clone(), RTLIL::State::S0);
|
||||
auto b = mk_eqx(std::move(sig), RTLIL::State::S1);
|
||||
return mk_or(std::move(a), std::move(b));
|
||||
}
|
||||
default:
|
||||
error(stringf("unexpected level symbol `%c' in table", sym));
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_edge_symbol(const std::string &s)
|
||||
{
|
||||
if (s.size() >= 1 && s.front() == '(')
|
||||
return true;
|
||||
if (s.size() == 1) {
|
||||
char c = tolower(s[0]);
|
||||
return c == 'r' || c == 'f' || c == 'p' || c == 'n' || c == '*';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// One-bit expression that is true iff the transition (piv -> in) matches the
|
||||
// edge symbol. Both operands are one-bit signals.
|
||||
ast_t edge_match(const std::string &sym, const std::string &piv, const std::string &in)
|
||||
{
|
||||
std::vector<std::pair<char, char>> pairs;
|
||||
if (!sym.empty() && sym.front() == '(') {
|
||||
if (sym.size() != 4 || sym.back() != ')')
|
||||
error(stringf("malformed edge specifier `%s' in table", sym.c_str()));
|
||||
pairs.emplace_back(sym[1], sym[2]);
|
||||
} else {
|
||||
switch (tolower(sym[0])) {
|
||||
case '*': pairs.emplace_back('?', '?'); break;
|
||||
case 'r': pairs.emplace_back('0', '1'); break;
|
||||
case 'f': pairs.emplace_back('1', '0'); break;
|
||||
case 'p': pairs = {{'0','1'},{'0','x'},{'x','1'}}; break;
|
||||
case 'n': pairs = {{'1','0'},{'1','x'},{'x','0'}}; break;
|
||||
default: error(stringf("unexpected edge symbol `%s' in table", sym.c_str()));
|
||||
}
|
||||
}
|
||||
ast_t acc;
|
||||
for (auto &p : pairs) {
|
||||
ast_t m = mk_and(level_match(p.first, mk_id(piv)), level_match(p.second, mk_id(in)));
|
||||
// an edge is always a change of value
|
||||
m = mk_and(std::move(m), mk_nex(mk_id(piv), mk_id(in)));
|
||||
acc = mk_or(std::move(acc), std::move(m));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// --- row matching --------------------------------------------------------
|
||||
// `piv_names` is empty for a combinational table (no edges, no previous
|
||||
// inputs). `state_name` is empty for a combinational table.
|
||||
ast_t row_match(const UdpTableRow &row, const std::vector<std::string> &piv_names,
|
||||
const std::string &state_name)
|
||||
{
|
||||
ast_t acc;
|
||||
int edge_count = 0;
|
||||
for (int i = 0; i < n_in; i++) {
|
||||
const std::string &sym = row.inputs[i];
|
||||
if (is_edge_symbol(sym)) {
|
||||
if (piv_names.empty())
|
||||
error("input transitions are only allowed in sequential UDPs");
|
||||
if (++edge_count > 1)
|
||||
error("a UDP table row may contain at most one input transition");
|
||||
acc = mk_and(std::move(acc), edge_match(sym, piv_names[i], sig_names[i]));
|
||||
} else {
|
||||
acc = mk_and(std::move(acc), level_match(sym[0], mk_id(sig_names[i])));
|
||||
}
|
||||
}
|
||||
if (!row.current.empty()) {
|
||||
if (is_edge_symbol(row.current))
|
||||
error("the current-state field of a UDP row must be a level");
|
||||
acc = mk_and(std::move(acc), level_match(row.current[0], mk_id(state_name)));
|
||||
}
|
||||
return acc; // null means "matches unconditionally"
|
||||
}
|
||||
|
||||
bool row_has_edge(const UdpTableRow &row)
|
||||
{
|
||||
for (auto &sym : row.inputs)
|
||||
if (is_edge_symbol(sym))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
char row_output(const UdpTableRow &row)
|
||||
{
|
||||
char c = row.output.empty() ? '?' : tolower(row.output[0]);
|
||||
if (c != '0' && c != '1' && c != 'x' && c != '-')
|
||||
error(stringf("unexpected output symbol `%s' in table", row.output.c_str()));
|
||||
return c;
|
||||
}
|
||||
|
||||
// --- determinism check ---------------------------------------------------
|
||||
// The set of values {0,1,x} (bits 0,1,2) a level symbol can match.
|
||||
static unsigned level_set(char c)
|
||||
{
|
||||
switch (tolower(c)) {
|
||||
case '0': return 1;
|
||||
case '1': return 2;
|
||||
case 'x': return 4;
|
||||
case 'b': return 3;
|
||||
case '?': return 7;
|
||||
default: return 7; // empty current-state field: unconstrained
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<std::pair<char, char>> edge_pairs(const std::string &s)
|
||||
{
|
||||
if (!s.empty() && s.front() == '(')
|
||||
return {{s[1], s[2]}};
|
||||
switch (tolower(s.empty() ? '?' : s[0])) {
|
||||
case '*': return {{'?', '?'}};
|
||||
case 'r': return {{'0', '1'}};
|
||||
case 'f': return {{'1', '0'}};
|
||||
case 'p': return {{'0','1'},{'0','x'},{'x','1'}};
|
||||
case 'n': return {{'1','0'},{'1','x'},{'x','0'}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The set of (from,to) transitions an edge symbol can match, encoded as a
|
||||
// bitmask over from*3+to (with from != to).
|
||||
static unsigned edge_set(const std::string &s)
|
||||
{
|
||||
unsigned m = 0;
|
||||
for (auto &p : edge_pairs(s)) {
|
||||
unsigned fs = level_set(p.first), ts = level_set(p.second);
|
||||
for (int f = 0; f < 3; f++)
|
||||
for (int t = 0; t < 3; t++)
|
||||
if ((fs >> f & 1) && (ts >> t & 1) && f != t)
|
||||
m |= 1u << (f * 3 + t);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
int edge_pos(const UdpTableRow &row)
|
||||
{
|
||||
int pos = -1;
|
||||
for (int i = 0; i < n_in; i++)
|
||||
if (is_edge_symbol(row.inputs[i])) {
|
||||
if (pos >= 0) {
|
||||
loc = row.loc;
|
||||
error("a UDP table row may contain at most one input transition");
|
||||
}
|
||||
pos = i;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Reject tables whose behaviour is not uniquely determined: two rows that
|
||||
// can match the same evaluation (same input values / transition and current
|
||||
// state) but specify different definite outputs. `x` outputs are
|
||||
// don't-cares and never conflict; level-sensitive rows are allowed to
|
||||
// override edge-sensitive rows (IEEE 1364-2005 8.7), so only same-kind rows
|
||||
// are compared.
|
||||
void check_determinism()
|
||||
{
|
||||
for (size_t a = 0; a < udp.rows.size(); a++)
|
||||
for (size_t b = a + 1; b < udp.rows.size(); b++) {
|
||||
const UdpTableRow &A = udp.rows[a], &B = udp.rows[b];
|
||||
char oa = row_output(A), ob = row_output(B);
|
||||
if (oa == ob || oa == 'x' || ob == 'x')
|
||||
continue;
|
||||
int ea = edge_pos(A), eb = edge_pos(B);
|
||||
if (ea != eb)
|
||||
continue; // level-vs-edge (defined), or edges on different inputs
|
||||
bool overlap = true;
|
||||
for (int i = 0; i < n_in && overlap; i++) {
|
||||
if (i == ea)
|
||||
overlap = (edge_set(A.inputs[i]) & edge_set(B.inputs[i])) != 0;
|
||||
else
|
||||
overlap = (level_set(A.inputs[i][0]) & level_set(B.inputs[i][0])) != 0;
|
||||
}
|
||||
if (overlap && !A.current.empty() && !B.current.empty())
|
||||
overlap = (level_set(A.current[0]) & level_set(B.current[0])) != 0;
|
||||
if (overlap) {
|
||||
loc = A.loc;
|
||||
error(stringf("non-deterministic state table: the rows at lines %d and %d "
|
||||
"match the same input combination but specify different "
|
||||
"outputs (`%c' and `%c')",
|
||||
A.loc.begin.line, B.loc.begin.line, oa, ob));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OR of the match expressions of all rows whose output symbol is `want`.
|
||||
// Returns null for an empty bucket. Matches Icarus Verilog, which groups
|
||||
// table rows by output value rather than by table order.
|
||||
ast_t bucket_match(const std::vector<const UdpTableRow*> &rows, char want,
|
||||
const std::vector<std::string> &piv_names, const std::string &state_name)
|
||||
{
|
||||
ast_t acc;
|
||||
for (auto *row : rows) {
|
||||
if (row_output(*row) != want)
|
||||
continue;
|
||||
ast_t m = row_match(*row, piv_names, state_name);
|
||||
if (!m)
|
||||
m = AstNode::mkconst_int(loc, 1, false, 1); // unconditional match
|
||||
acc = mk_or(std::move(acc), std::move(m));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// `cond ? then_expr : else_expr`, dropping the choice when the bucket is
|
||||
// empty (cond is null).
|
||||
ast_t sel(ast_t cond, ast_t then_expr, ast_t else_expr)
|
||||
{
|
||||
if (!cond)
|
||||
return else_expr;
|
||||
return N(AST_TERNARY, std::move(cond), std::move(then_expr), std::move(else_expr));
|
||||
}
|
||||
|
||||
// --- netlist construction helpers ---------------------------------------
|
||||
AstNode *add_wire(const std::string &name, bool is_input, bool is_output, int port_id)
|
||||
{
|
||||
auto w = N(AST_WIRE);
|
||||
w->str = name;
|
||||
w->is_input = is_input;
|
||||
w->is_output = is_output;
|
||||
if (port_id)
|
||||
w->port_id = port_id;
|
||||
AstNode *ptr = w.get();
|
||||
mod->children.push_back(std::move(w));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
// A $ff (implicit global clock flip-flop): q <= d on every simulation event.
|
||||
void add_ff(ast_t d_expr, const std::string &q_name, bool have_init, RTLIL::State init)
|
||||
{
|
||||
AstNode *q = add_wire(q_name, false, false, 0);
|
||||
if (have_init)
|
||||
q->set_attribute(ID::init, AstNode::mkconst_bits(loc, std::vector<RTLIL::State>{init}, false));
|
||||
|
||||
auto cell = N(AST_CELL);
|
||||
cell->str = stringf("$udp$ff$%d", autoidx++);
|
||||
auto celltype = N(AST_CELLTYPE);
|
||||
celltype->str = "$ff";
|
||||
cell->children.push_back(std::move(celltype));
|
||||
|
||||
auto para = N(AST_PARASET);
|
||||
para->str = "\\WIDTH";
|
||||
para->children.push_back(AstNode::mkconst_int(loc, 1, false, 32));
|
||||
cell->children.push_back(std::move(para));
|
||||
|
||||
auto arg_d = N(AST_ARGUMENT);
|
||||
arg_d->str = "\\D";
|
||||
arg_d->children.push_back(std::move(d_expr));
|
||||
cell->children.push_back(std::move(arg_d));
|
||||
|
||||
auto arg_q = N(AST_ARGUMENT);
|
||||
arg_q->str = "\\Q";
|
||||
arg_q->children.push_back(mk_id(q_name));
|
||||
cell->children.push_back(std::move(arg_q));
|
||||
|
||||
mod->children.push_back(std::move(cell));
|
||||
}
|
||||
|
||||
// --- entry point ---------------------------------------------------------
|
||||
std::unique_ptr<AstNode> run()
|
||||
{
|
||||
// Resolve and validate ports.
|
||||
if (udp.port_order.size() < 2)
|
||||
error("a UDP must have an output and at least one input");
|
||||
if (!udp.output_declared)
|
||||
error("missing output port declaration");
|
||||
out_name = udp.output_name;
|
||||
if (udp.port_order[0] != out_name)
|
||||
error("the output port must be the first port in the port list");
|
||||
for (size_t i = 1; i < udp.port_order.size(); i++) {
|
||||
if (!udp.input_names.count(udp.port_order[i]))
|
||||
error(stringf("port `%s' is missing an input declaration",
|
||||
udp.port_order[i].c_str() + 1));
|
||||
in_names.push_back(udp.port_order[i]);
|
||||
}
|
||||
n_in = GetSize(in_names);
|
||||
|
||||
for (auto &row : udp.rows) {
|
||||
if (GetSize(row.inputs) != n_in) {
|
||||
loc = row.loc;
|
||||
error(stringf("table row has %d input fields but the UDP has %d inputs",
|
||||
GetSize(row.inputs), n_in));
|
||||
}
|
||||
}
|
||||
if (udp.is_sequential && !udp.table_is_sequential)
|
||||
error("sequential UDP has a combinational state table");
|
||||
if (!udp.is_sequential && udp.table_is_sequential)
|
||||
error("combinational UDP has a sequential state table");
|
||||
if (udp.rows.empty())
|
||||
error("empty state table");
|
||||
|
||||
check_determinism();
|
||||
loc = udp.loc;
|
||||
|
||||
// Build the module skeleton.
|
||||
auto module = N(AST_MODULE);
|
||||
module->str = udp.name;
|
||||
if (udp.attributes)
|
||||
for (auto &it : *udp.attributes)
|
||||
module->attributes[it.first] = std::move(it.second);
|
||||
module->set_attribute(ID(udp), AstNode::mkconst_int(loc, 1, false));
|
||||
mod = module.get();
|
||||
|
||||
int port_id = 1;
|
||||
out_wire = add_wire(out_name, false, true, port_id++);
|
||||
for (auto &in : in_names)
|
||||
add_wire(in, true, false, port_id++);
|
||||
|
||||
// "The z values passed to UDP inputs shall be treated the same as x
|
||||
// values." Map each input through (in === 1'bz) ? 1'bx : in and use the
|
||||
// mapped signal everywhere the table is matched. (The constants also
|
||||
// force these cells to be evaluated in the first simulation step.)
|
||||
for (int i = 0; i < n_in; i++) {
|
||||
std::string xn = stringf("$udp$zx%d", i);
|
||||
add_wire(xn, false, false, 0);
|
||||
ast_t map = N(AST_TERNARY,
|
||||
mk_eqx(mk_id(in_names[i]), RTLIL::State::Sz),
|
||||
mk_const(RTLIL::State::Sx), mk_id(in_names[i]));
|
||||
mod->children.push_back(N(AST_ASSIGN, mk_id(xn), std::move(map)));
|
||||
sig_names.push_back(xn);
|
||||
}
|
||||
|
||||
if (!udp.is_sequential)
|
||||
build_combinational();
|
||||
else
|
||||
build_sequential();
|
||||
|
||||
mod = nullptr;
|
||||
return module;
|
||||
}
|
||||
|
||||
void build_combinational()
|
||||
{
|
||||
// out = 0 if any 0-row matches, else 1 if any 1-row matches, else x.
|
||||
// (x-output rows do not force x; they simply are not 0- or 1-rows.)
|
||||
std::vector<const UdpTableRow*> rows;
|
||||
for (auto &row : udp.rows) {
|
||||
if (row_has_edge(row))
|
||||
error("combinational UDPs may not contain input transitions");
|
||||
if (row_output(row) == '-')
|
||||
error("`-' (no change) is only allowed in sequential UDPs");
|
||||
rows.push_back(&row);
|
||||
}
|
||||
ast_t expr = sel(bucket_match(rows, '0', {}, ""), mk_const(RTLIL::State::S0),
|
||||
sel(bucket_match(rows, '1', {}, ""), mk_const(RTLIL::State::S1),
|
||||
mk_const(RTLIL::State::Sx)));
|
||||
mod->children.push_back(N(AST_ASSIGN, mk_id(out_name), std::move(expr)));
|
||||
}
|
||||
|
||||
void build_sequential()
|
||||
{
|
||||
// State registers: one $ff per input for the previous input value, plus
|
||||
// one $ff for the previous output (the current state).
|
||||
std::vector<std::string> piv_names;
|
||||
for (int i = 0; i < n_in; i++)
|
||||
piv_names.push_back(stringf("$udp$piv%d", i));
|
||||
std::string state_name = "$udp$state";
|
||||
|
||||
// out = G(in, prev_in, state):
|
||||
// level rows (priority) override edge rows (priority); if nothing
|
||||
// matches, hold the state when no input changed, else x.
|
||||
std::vector<const UdpTableRow*> level_rows, edge_rows;
|
||||
for (auto &row : udp.rows) {
|
||||
if (row_has_edge(row))
|
||||
edge_rows.push_back(&row);
|
||||
else
|
||||
level_rows.push_back(&row);
|
||||
}
|
||||
|
||||
// A UDP only re-evaluates its table when one of its inputs *changes*;
|
||||
// otherwise the output holds. So:
|
||||
// out = event ? table(in, prev_in, state) : state
|
||||
// where event = (in !== prev_in). The constant bit padded onto each
|
||||
// side forces this comparison (and hence the whole output) to be
|
||||
// evaluated in the very first simulation step, when all inputs are still
|
||||
// x and would otherwise never be re-evaluated; there the event is false,
|
||||
// so the output correctly holds the initial state.
|
||||
auto in_concat = N(AST_CONCAT);
|
||||
auto piv_concat = N(AST_CONCAT);
|
||||
for (int i = 0; i < n_in; i++) {
|
||||
in_concat->children.push_back(mk_id(sig_names[i]));
|
||||
piv_concat->children.push_back(mk_id(piv_names[i]));
|
||||
}
|
||||
in_concat->children.push_back(mk_const(RTLIL::State::S0));
|
||||
piv_concat->children.push_back(mk_const(RTLIL::State::S0));
|
||||
ast_t event = N(AST_NEX, std::move(in_concat), std::move(piv_concat));
|
||||
|
||||
// table(): rows are tried by output value in a fixed priority that
|
||||
// matches Icarus Verilog's evaluator (vvp/udp.cc):
|
||||
// level 0 > level 1 > level x > level hold > edge 0 > edge 1 > edge hold > x
|
||||
// Level entries dominate edge entries; an edge entry on an input that did
|
||||
// not transition cannot match (handled inside the per-row match).
|
||||
auto S = [&](RTLIL::State s) { return mk_const(s); };
|
||||
auto St = [&]() { return mk_id(state_name); };
|
||||
ast_t table =
|
||||
sel(bucket_match(level_rows, '0', piv_names, state_name), S(RTLIL::State::S0),
|
||||
sel(bucket_match(level_rows, '1', piv_names, state_name), S(RTLIL::State::S1),
|
||||
sel(bucket_match(level_rows, 'x', piv_names, state_name), S(RTLIL::State::Sx),
|
||||
sel(bucket_match(level_rows, '-', piv_names, state_name), St(),
|
||||
sel(bucket_match(edge_rows, '0', piv_names, state_name), S(RTLIL::State::S0),
|
||||
sel(bucket_match(edge_rows, '1', piv_names, state_name), S(RTLIL::State::S1),
|
||||
sel(bucket_match(edge_rows, '-', piv_names, state_name), St(),
|
||||
S(RTLIL::State::Sx))))))));
|
||||
ast_t out_expr = N(AST_TERNARY, std::move(event), std::move(table), mk_id(state_name));
|
||||
mod->children.push_back(N(AST_ASSIGN, mk_id(out_name), std::move(out_expr)));
|
||||
|
||||
// The output is combinational; also give it the initial value so the
|
||||
// very first sampled output is correct even before it is evaluated.
|
||||
RTLIL::State init = udp_init_state();
|
||||
out_wire->set_attribute(ID::init, AstNode::mkconst_bits(loc, std::vector<RTLIL::State>{init}, false));
|
||||
|
||||
// previous-input registers (store the z->x mapped values)
|
||||
for (int i = 0; i < n_in; i++)
|
||||
add_ff(mk_id(sig_names[i]), piv_names[i], /*have_init=*/true, RTLIL::State::Sx);
|
||||
|
||||
// state register (previous output)
|
||||
add_ff(mk_id(out_name), state_name, /*have_init=*/true, init);
|
||||
}
|
||||
|
||||
// Reduce the UDP's initial-value expression to a single 4-state bit.
|
||||
// `initial q = <init_val>;` is restricted by the standard to 1'b0, 1'b1,
|
||||
// 1'bx, 1, or 0, all of which the front-end parses to an AST_CONSTANT.
|
||||
RTLIL::State udp_init_state()
|
||||
{
|
||||
if (!udp.initial_val)
|
||||
return RTLIL::State::Sx;
|
||||
AstNode *e = udp.initial_val.get();
|
||||
if (e->type != AST_CONSTANT || e->bits.empty())
|
||||
error("initial value must be a constant 1'b0, 1'b1 or 1'bx");
|
||||
return e->bits.front();
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
std::unique_ptr<AstNode> make_udp_module(UdpParseData &udp)
|
||||
{
|
||||
UdpLower lower(udp);
|
||||
return lower.run();
|
||||
}
|
||||
|
||||
} // namespace VERILOG_FRONTEND
|
||||
|
||||
YOSYS_NAMESPACE_END
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* yosys -- Yosys Open SYnthesis Suite
|
||||
*
|
||||
* Copyright (C) 2026 Remy Goldschmidt <taktoa@gmail.com>
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Support for Verilog user-defined primitives (UDPs), IEEE 1364-2005
|
||||
* clause 8. The Verilog parser collects a UDP definition into a
|
||||
* UdpParseData structure; make_udp_module() then lowers it into an
|
||||
* ordinary behavioural AST module that the rest of Yosys synthesises
|
||||
* using the existing FF/latch inference passes.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef VERILOG_UDP_H
|
||||
#define VERILOG_UDP_H
|
||||
|
||||
#include "kernel/yosys.h"
|
||||
#include "frontends/ast/ast.h"
|
||||
|
||||
YOSYS_NAMESPACE_BEGIN
|
||||
|
||||
namespace VERILOG_FRONTEND
|
||||
{
|
||||
// A single row of the UDP state table. The input symbols are stored in
|
||||
// port-list order (i.e. matching the order of the input ports in the UDP
|
||||
// header, not the order of the input declarations).
|
||||
struct UdpTableRow {
|
||||
std::vector<std::string> inputs; // one raw symbol per input port
|
||||
std::string current; // current-state symbol ("" if combinational)
|
||||
std::string output; // output / next-state symbol
|
||||
AST::AstSrcLocType loc;
|
||||
};
|
||||
|
||||
// Everything the parser collects while reading a `primitive ... endprimitive`
|
||||
// block. Consumed by make_udp_module().
|
||||
struct UdpParseData {
|
||||
AST::AstSrcLocType loc;
|
||||
std::string name; // UDP name (escaped, e.g. "\\foo")
|
||||
|
||||
std::vector<std::string> port_order; // port names in header order; [0] is the output
|
||||
std::string output_name; // name of the (single) output port
|
||||
pool<std::string> input_names; // names declared as inputs
|
||||
bool output_declared = false;
|
||||
bool is_sequential = false; // true once a reg declaration is seen
|
||||
|
||||
std::unique_ptr<AST::AstNode> initial_val; // optional initial value expression (or null)
|
||||
|
||||
std::unique_ptr<dict<RTLIL::IdString, std::unique_ptr<AST::AstNode>>> attributes;
|
||||
|
||||
std::vector<UdpTableRow> rows;
|
||||
bool table_is_sequential = false; // table rows carry a current-state field
|
||||
|
||||
// scratch used while accumulating the input fields of the row currently
|
||||
// being parsed
|
||||
std::vector<std::string> scratch_inputs;
|
||||
};
|
||||
|
||||
// Lower a parsed UDP into a behavioural AST_MODULE. Reports user errors via
|
||||
// log_file_error(). Never returns null.
|
||||
std::unique_ptr<AST::AstNode> make_udp_module(UdpParseData &udp);
|
||||
}
|
||||
|
||||
YOSYS_NAMESPACE_END
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python3
|
||||
# Differential check of Yosys's UDP lowering against Icarus Verilog.
|
||||
#
|
||||
# For the `primitive` in the given .v file, drive random single-input changes
|
||||
# (4-state: 0/1/x/z), simulate with iverilog (which evaluates the UDP natively)
|
||||
# and with `yosys sim` (which simulates the lowered netlist), and assert that the
|
||||
# two output traces are equal under multi-valued logic.
|
||||
#
|
||||
# Usage: udp_cosim.py <udp.v> [num_stimuli] [steps]
|
||||
# Requires: iverilog, vvp, python3, and $YOSYS pointing at the yosys binary.
|
||||
|
||||
import os, sys, re, subprocess, random, tempfile
|
||||
|
||||
YOSYS = os.environ.get("YOSYS", "yosys")
|
||||
|
||||
def sh(cmd):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
|
||||
def parse_primitive(path):
|
||||
txt = open(path).read()
|
||||
m = re.search(r'primitive\s+(\w+)\s*\(([^)]*)\)', txt)
|
||||
ports = [re.sub(r'\b(output|input|reg)\b', '', p).strip()
|
||||
for p in m.group(2).split(',')]
|
||||
return m.group(1), ports[0], ports[1:]
|
||||
|
||||
def gen_tb(name, out, ins, stim):
|
||||
decl = "\n".join(" reg %s;" % s for s in ins)
|
||||
inst = ", ".join([out] + ins)
|
||||
body = "\n".join(" #10 %s = 1'b%s;" % (ins[i], v) for (i, v) in stim)
|
||||
return ("module tb;\n%s\n wire %s;\n %s dut(%s);\n"
|
||||
" initial begin\n $dumpfile(\"wave\");\n $dumpvars(0, tb);\n"
|
||||
"%s\n #10 $finish;\n end\nendmodule\n"
|
||||
% (decl, out, name, inst, body))
|
||||
|
||||
def trace(vcd, signame):
|
||||
sid = None
|
||||
for line in vcd.splitlines():
|
||||
m = re.match(r'\$var\s+\w+\s+1\s+(\S+)\s+(\S+)', line)
|
||||
if m and m.group(2) == signame:
|
||||
sid = m.group(1); break
|
||||
if sid is None:
|
||||
return None
|
||||
t, out = 0, []
|
||||
for line in vcd.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('#'):
|
||||
t = int(line[1:])
|
||||
elif len(line) >= 2 and line[0] in '01xz' and line[1:] == sid:
|
||||
out.append((t, line[0]))
|
||||
else:
|
||||
mm = re.match(r'b(\S+)\s+(\S+)', line)
|
||||
if mm and mm.group(2) == sid:
|
||||
out.append((t, mm.group(1)[-1]))
|
||||
return out
|
||||
|
||||
def value_at(tr, t):
|
||||
v = 'x'
|
||||
for (tt, vv) in tr:
|
||||
if tt <= t: v = vv
|
||||
else: break
|
||||
return v
|
||||
|
||||
def run_one(udp_file, name, out, ins, stim):
|
||||
with tempfile.TemporaryDirectory() as wd:
|
||||
open(os.path.join(wd, "tb.v"), "w").write(gen_tb(name, out, ins, stim))
|
||||
if sh("iverilog -o %s/sim.vvp %s %s/tb.v" % (wd, udp_file, wd)).returncode != 0:
|
||||
return None # iverilog rejected this table; skip
|
||||
sh("cd %s && vvp sim.vvp -fst >/dev/null 2>&1" % wd)
|
||||
sh("cd %s && vvp sim.vvp >/dev/null 2>&1" % wd)
|
||||
ref = trace(open("%s/wave.vcd" % wd).read(), out)
|
||||
r = sh("%s -q -p 'read_verilog %s; sim -r %s/wave.fst -scope tb -vcd %s/y.vcd'"
|
||||
% (YOSYS, udp_file, wd, wd))
|
||||
if r.returncode != 0:
|
||||
raise SystemExit("yosys sim failed:\n" + r.stdout + r.stderr)
|
||||
mod = trace(open("%s/y.vcd" % wd).read(), out)
|
||||
end = 10 * (len(stim) + 1)
|
||||
ts = sorted(set([t for t, _ in ref] + [t for t, _ in mod] + list(range(0, end + 1, 10))))
|
||||
return [(t, value_at(ref, t), value_at(mod, t)) for t in ts
|
||||
if value_at(ref, t) != value_at(mod, t)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
udp_file = sys.argv[1]
|
||||
nstim = int(sys.argv[2]) if len(sys.argv) > 2 else 20
|
||||
steps = int(sys.argv[3]) if len(sys.argv) > 3 else 60
|
||||
name, out, ins = parse_primitive(udp_file)
|
||||
tested = 0
|
||||
for seed in range(nstim):
|
||||
rnd = random.Random(seed * 1009 + 1)
|
||||
vals4 = ['0', '1', 'x', 'z']
|
||||
stim = [(rnd.randrange(len(ins)),
|
||||
rnd.choice(vals4) if rnd.random() < 0.3 else rnd.choice('01'))
|
||||
for _ in range(steps)]
|
||||
mm = run_one(udp_file, name, out, ins, stim)
|
||||
if mm is None:
|
||||
continue
|
||||
tested += 1
|
||||
if mm:
|
||||
print("MISMATCH in %s (seed %d):" % (name, seed))
|
||||
for (t, a, b) in mm[:12]:
|
||||
print(" t=%d iverilog=%s yosys=%s" % (t, a, b))
|
||||
print("stimulus:", stim)
|
||||
raise SystemExit(1)
|
||||
if tested == 0:
|
||||
raise SystemExit("iverilog rejected %s for all stimuli" % name)
|
||||
print("OK %s (%d stimuli matched)" % (name, tested))
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
read_verilog <<EOT
|
||||
primitive my_and (o, a, b);
|
||||
output o; input a, b;
|
||||
table
|
||||
1 1 : 1 ;
|
||||
0 ? : 0 ;
|
||||
? 0 : 0 ;
|
||||
endtable
|
||||
endprimitive
|
||||
module top (output o, input a, b);
|
||||
my_and (o, a, b); // gate-style: no instance name (IEEE 1364-2005 8.6)
|
||||
endmodule
|
||||
EOT
|
||||
hierarchy -top top
|
||||
select -assert-count 1 top/t:my_and
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# A UDP whose rows overlap with conflicting outputs must be rejected.
|
||||
logger -expect error "non-deterministic state table" 1
|
||||
read_verilog <<EOT
|
||||
primitive nd (o, a, b);
|
||||
output o; reg o; input a, b;
|
||||
table
|
||||
// a b : q : q+
|
||||
1 ? : ? : 1 ;
|
||||
? 1 : ? : 0 ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOT
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env bash
|
||||
# Check that Yosys's lowering of Verilog user-defined primitives (UDPs)
|
||||
# simulates identically to Icarus Verilog's native UDP support, under
|
||||
# multi-valued (0/1/x/z) logic. Uses the UDP examples from IEEE 1364-2005
|
||||
# clause 8.
|
||||
set -e
|
||||
|
||||
if ! command -v iverilog >/dev/null 2>&1; then
|
||||
echo "skipping udp_sim.sh: iverilog not found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# 8.2 combinational multiplexer
|
||||
cat > "$WORK/mux.v" <<'EOF'
|
||||
primitive multiplexer (mux, control, dataA, dataB);
|
||||
output mux; input control, dataA, dataB;
|
||||
table
|
||||
// c a b : mux
|
||||
0 1 ? : 1 ;
|
||||
0 0 ? : 0 ;
|
||||
1 ? 1 : 1 ;
|
||||
1 ? 0 : 0 ;
|
||||
x 0 0 : 0 ;
|
||||
x 1 1 : 1 ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
# 8.3 level-sensitive latch
|
||||
cat > "$WORK/latch.v" <<'EOF'
|
||||
primitive latch (q, clock, data);
|
||||
output q; reg q; input clock, data;
|
||||
table
|
||||
// clk data : q : q+
|
||||
0 1 : ? : 1 ;
|
||||
0 0 : ? : 0 ;
|
||||
1 ? : ? : - ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
# 8.4 rising-edge D flip-flop
|
||||
cat > "$WORK/dff.v" <<'EOF'
|
||||
primitive d_edge_ff (q, clock, data);
|
||||
output q; reg q; input clock, data;
|
||||
table
|
||||
// clk data : q : q+
|
||||
(01) 0 : ? : 0 ;
|
||||
(01) 1 : ? : 1 ;
|
||||
(0?) 1 : 1 : 1 ;
|
||||
(0?) 0 : 0 : 0 ;
|
||||
(?0) ? : ? : - ;
|
||||
? (??): ? : - ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
# 8.5 D flip-flop with an initial value
|
||||
cat > "$WORK/dff1.v" <<'EOF'
|
||||
primitive dff1 (q, clk, d);
|
||||
input clk, d; output q; reg q;
|
||||
initial q = 1'b1;
|
||||
table
|
||||
r 0 : ? : 0 ;
|
||||
r 1 : ? : 1 ;
|
||||
f ? : ? : - ;
|
||||
? * : ? : - ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
# 8.5 set/reset flip-flop with an initial value
|
||||
cat > "$WORK/srff.v" <<'EOF'
|
||||
primitive srff (q, s, r);
|
||||
output q; reg q; input s, r;
|
||||
initial q = 1'b1;
|
||||
table
|
||||
1 0 : ? : 1 ;
|
||||
f 0 : 1 : - ;
|
||||
0 r : ? : 0 ;
|
||||
0 f : 0 : - ;
|
||||
1 1 : ? : 0 ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
# 8.7 edge-triggered JK flip-flop with level-sensitive preset/clear
|
||||
cat > "$WORK/jk.v" <<'EOF'
|
||||
primitive jk_edge_ff (q, clock, j, k, preset, clear);
|
||||
output q; reg q; input clock, j, k, preset, clear;
|
||||
table
|
||||
? ?? 01 : ? : 1 ;
|
||||
? ?? *1 : 1 : 1 ;
|
||||
? ?? 10 : ? : 0 ;
|
||||
? ?? 1* : 0 : 0 ;
|
||||
r 00 00 : 0 : 1 ;
|
||||
r 00 11 : ? : - ;
|
||||
r 01 11 : ? : 0 ;
|
||||
r 10 11 : ? : 1 ;
|
||||
r 11 11 : 0 : 1 ;
|
||||
r 11 11 : 1 : 0 ;
|
||||
f ?? ?? : ? : - ;
|
||||
b *? ?? : ? : - ;
|
||||
b ?* ?? : ? : - ;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
cat > "$WORK/dff_complex.v" <<'EOF'
|
||||
primitive dff_complex (q, v, clk, d, xcr);
|
||||
output q; reg q; input v, clk, d, xcr;
|
||||
table
|
||||
* ? ? ? : ? : x;
|
||||
? (x1) 0 0 : ? : 0;
|
||||
? (x1) 1 0 : ? : 1;
|
||||
? (x1) 0 1 : 0 : 0;
|
||||
? (x1) 1 1 : 1 : 1;
|
||||
? (x1) ? x : ? : -;
|
||||
? (bx) 0 ? : 0 : -;
|
||||
? (bx) 1 ? : 1 : -;
|
||||
? (x0) b ? : ? : -;
|
||||
? (x0) ? x : ? : -;
|
||||
? (01) 0 ? : ? : 0;
|
||||
? (01) 1 ? : ? : 1;
|
||||
? (10) ? ? : ? : -;
|
||||
? b * ? : ? : -;
|
||||
? ? ? * : ? : -;
|
||||
endtable
|
||||
endprimitive
|
||||
EOF
|
||||
|
||||
for u in mux latch dff dff1 srff jk dff_complex; do
|
||||
python3 "$(dirname "$0")/udp_cosim.py" "$WORK/$u.v" 20 60
|
||||
done
|
||||
Loading…
Reference in New Issue