-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchisel-verify-jnl.tex
1466 lines (1254 loc) · 107 KB
/
chisel-verify-jnl.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[conference]{IEEEtran}
\usepackage{cite}
\usepackage{pslatex} % -- times instead of computer modern, especially for the plain article class
\usepackage[colorlinks=false,bookmarks=false]{hyperref}
\usepackage{booktabs}
\usepackage{graphicx}
\usepackage{xcolor}
\usepackage{multirow}
\usepackage{comment}
\usepackage{listings}
%\usepackage{flushend} % even out the last page, but use only at the end when there is a bibliography
\usepackage{xspace} % For using \SV with trailing spaces
\usepackage{cleveref} % Needed for correctly referencing listings
\usepackage{subfig}
\newcommand{\code}[1]{{\small{\texttt{#1}}}}
\newcommand{\SV}{SystemVerilog\xspace}
% fatter TT font
\renewcommand*\ttdefault{txtt}
% another TT, suggested by Alex
% \usepackage{inconsolata}
% \usepackage[T1]{fontenc} % needed as well?
%\newcommand{\todo}[1]{{\emph{TODO: #1}}}
\newcommand{\todo}[1]{{\color{olive} TODO: #1}}
\newcommand{\martin}[1]{{\color{blue} Martin: #1}}
\newcommand{\simon}[1]{{\color{green} Simon: #1}}
\newcommand{\abcdef}[1]{{\color{red} Author2: #1}}
\newcommand{\rewrite}[1]{{\color{red} rewrite: #1}}
\newcommand{\ducky}[1]{{\color{orange} Richard: #1}}
\newcommand{\kasper}[1]{{\color{purple} Kasper: #1}}
\newcommand{\hjd}[1]{{\color{pink} Hans: #1}}
% uncomment following for final submission
%\renewcommand{\todo}[1]{}
%\renewcommand{\martin}[1]{}
%\renewcommand{\simon}[1]{}
%\renewcommand{\kasper}[1]{}
%\renewcommand{\ducky}[1]{}
%%% ZF
\usepackage{listings}
\lstset{
columns=fullflexible,
% basicstyle=\ttfamily\footnotesize,
basicstyle=\ttfamily\small,
%columns=fullflexible, keepspaces=true,
numbers=left,
numberblanklines=false,
captionpos=b,
% breaklines=true,
escapeinside={@}{@},
numbersep=5pt,
language=C,
tabsize=2,
breakatwhitespace=true,
breaklines=true,
deletekeywords={for},
% keywordstyle=\ttfamily
numbersep=5pt,
xleftmargin=.10in,
%xrightmargin=.25in
}
\newcommand{\longlist}[3]{{\lstinputlisting[float, caption={#2}, label={#3}, frame=tb, captionpos=b]{#1}}}
\title{Verification of Chisel Hardware Designs with ChiselVerify}
%\author{
%\IEEEauthorblockN{No Author Given}
%\IEEEauthorblockA{No Institute Given}
%}
\author{\IEEEauthorblockN{Andrew Dobis\IEEEauthorrefmark{1}\IEEEauthorrefmark{3}, Kevin Laeufer\IEEEauthorrefmark{2}, Hans Jakob Damsgaard\IEEEauthorrefmark{1}\IEEEauthorrefmark{4}\thanks{Hans Jakob Damsgaard carried out his work at the Technical University of Denmark. He is currently with Tampere University as part of the EU Horizon 2020 APROPOS project, MSCA grant agreement No. $956090$.}, Tjark Petersen\IEEEauthorrefmark{1},\\
Kasper Juul Hesse Rasmussen\IEEEauthorrefmark{1},
Enrico Tolotto\IEEEauthorrefmark{1}, Simon Thye Andersen\IEEEauthorrefmark{1},\\
Richard Lin\IEEEauthorrefmark{2}, Martin Schoeberl\IEEEauthorrefmark{1}}\\[-6pt]
\IEEEauthorblockA{\IEEEauthorrefmark{1}\textit{Department of Applied Mathematics and Computer Science},
\textit{Technical University of Denmark},
Lyngby, Denmark \\
\IEEEauthorrefmark{2}\textit{Department of Electrical Engineering and Computer Sciences},
\textit{UC Berkeley},
Berkeley, CA \\
\IEEEauthorrefmark{3}\textit{Department of Computer Science},
\textit{ETH Zürich},
Zürich, Switzerland \\
\IEEEauthorrefmark{4}\textit{Electrical Engineering Unit}, \textit{Tampere University}, Tampere, Finland \\[6pt]
andrew.dobis@inf.ethz.ch, laeufer@berkeley.edu, hans.damsgaard@tuni.fi, s186083@student.dtu.dk, \\
s183735@student.dtu.dk, s190057@student.dtu.dk, simon.thye@gmail.com, richard.lin@berkeley.edu, masca@dtu.dk}
}
\begin{document}
\IEEEoverridecommandlockouts
\IEEEpubid{\makebox[\columnwidth]{978-1-6654-0712-0/21/\$31.00~\copyright2021 IEEE \hfill} \hspace{\columnsep}\makebox[\columnwidth]{ }}
\maketitle
\IEEEpubidadjcol
\thispagestyle{empty}
\pagestyle{empty}
\begin{abstract}
With the current ever-increasing demand for performance, hardware developers
find themselves turning ever-more towards the construction of application-specific accelerators
to achieve higher performance and lower energy consumption.
In order to meet the ever-shortening time constraints, both hardware development and
verification tools need to be improved.
%We can no longer depend on Moore's Law to increase computing performance.
%Performance increase with general-purpose processors has come to a halt.
%The only way to achieve higher performance or lower energy consumption
%is by building application-specific accelerators.
%To support the development of such accelerators, we need better development
%and verification tools.
Chisel, as a hardware construction language, tackles this problem by speeding up the development
of digital designs. However, the Chisel infrastructure lacks tools for
verification. This paper improves the efficiency of verification in Chisel by proposing
methods to support both formal and dynamic verification of digital designs in Scala.
It builds on top of ChiselTest, the official testing framework for Chisel.
Our work supports functional coverage, constrained random verification, bus functional models,
and transaction-level modeling in a verification library named ChiselVerify, while the formal
methods are directly integrated into Chisel3.
\end{abstract}
\begin{IEEEkeywords}
digital design, verification, Chisel, Scala
\end{IEEEkeywords}
%\section*{Notes}
%
%We will be invited to extend this paper for a journal submission. Here we start to collect ideas:
%
%\begin{itemize}
%\item Rewrite most of the text to avoid copyright issues
%\item As usual, more related work \url{https://link.springer.com/chapter/10.1007/978-3-030-53288-8_19} \url{https://people.eecs.berkeley.edu/~ksen/papers/smtsampler.pdf} see also \url{https://github.com/TsaiAnson/verif/blob/master/core/src/SMTSampler.scala}
%\item Comparison with UVM (performance and complexity)
%\item More evaluation, e.g., use S4NOC as a non-trivial DUT
%\item Use a different CSP solver
%\item Kevin's formal verification work (Install Z3)
%\item see also notes.tex
%\item functional fuzzer
%\item Use ChiselVerify/ChiselTest to test VHDL/Verilog designs
%\item \url{https://docs.cocotb.org/en/stable/quickstart.html}
%\item Fast test writing by using Chisel/Scala to test VHDL tests (Simon would like it)
%\item Transpiling VHDL to Verilog
%\item Tiling tests \url{https://github.com/TsaiAnson/verif} L2 cache \url{https://github.com/TsaiAnson/verif/blob/master/tilelink/test/TLL2CacheTest.scala}
%\item \url{https://github.com/ucb-bar/riscv-mini} has bug in unaligned ld/st
%\item Fuzzing paper: \url{https://compsec.snu.ac.kr/papers/hur-difuzzrtl.pdf}
%\item \martin{shall bring in an arbitration circuit as a test case.} (Verification plan: a packet comes through eventually (nothing lost), no packet is ``created'' out of the air, tests under light and heavy load, measure latency and bandwidth, should be in general fair arbitration)
%\end{itemize}
%
%Some ideas from Andrew:
%
%I think we could maybe add a part on the Queryable functional coverage and compare it to the pre-defined version. Have an in depth look at verification with PSL and SVA in the Related work section. We could also potentially extend the constraint random stuff to add @Tjark Petersen's work.
%
%We can also discuss on Slack.
\section{Introduction}
\label{sec:introduction}
General-purpose processors performance increase is down to single digit percentage per year.
This leads to a new golden age for computer architects designing domain-specific hardware accelerators
for future performance improvements~\cite{domain-hw-acc:2020}.
The design of these accelerators is often complex, and their development is time-consuming and error-prone.
To mitigate this issue, we shall learn from software development trends such as agile software
development~\cite{agile:manifesto}, and adapt to agile hardware development~\cite{henn-patt:turing:2019}.
One move in this direction is Chisel~\cite{chisel:dac2012, chisel:book}, a Scala-embedded hardware construction language,
that was introduced in order to move digital circuit description to a more software-like high-level language.
Hardware design is still dominated by the traditional hardware description languages, such as VHDL and SystemVerilog.
SystemVerilog adds object oriented features to the language. However, those features are only available for test benches,
not for describing hardware.
Chisel goes one (or several) steps forward in hardware description, as it enables object-oriented and functional programming
to describe hardware. This allows one to not only concisely describe hardware, but also to describe hardware generators.
This elevates current practice of Perl, TCL, or Python generating VHDL or SystemVerilog code to use the powerful programming
language Scala.
However, Chisel does not yet provide efficient verification tools or libraries. The ChiselTest package~\cite{chiseltest}
only provides primitives to write classical test benches.
Therefore, we build upon ChiselTest and add verification features. Those features are inspired by the
Universal Verification Method (UVM), but implemented by leveraging
Scala's conciseness and support for both object-oriented and functional programming.
ChiselVerify, supports both coverage-oriented and constrained
random verification flows with more features than those available in UVM.
Additionally, formal methods for verifying chisel designs using bounded model checking are proposed
enabling the formal verification of Chisel designs based solely on a specification.
These two proposals combined make Chisel's verification capabilities on par with industry standards such as UVM.
For the evaluation, we use three designs: (1) the execution stage of the Leros processor~\cite{leros:arcs2019}, (2) an arbitration circuit, and (3) an industrial use case, a min-heap sorting circuit.
We show that ChiselVerify can check many features of the min-heap with few lines of
verification code.
The contributions of this paper are:
\begin{itemize}
\item Tools for defining and gathering functional coverage information about a Chisel design.
\item A Domain Specific Language for constraint programming inside a ChiselTest test bench.
\item A Bus Functional Model for the AXI4 standardized interface\footnote{All three available as part of ChiselVerify \url{https://github.com/chiselverify/chiselverify}}.
\item Formal methods\footnote{Available as part of ChiselTest \url{https://github.com/ucb-bar/chiseltest}} for verifying Chisel designs.
\end{itemize}
This paper is an extension of~\cite{ChiselVerify:2021}.
These extensions include the following:
\begin{itemize}
\item A proposal of formal verification methods for verifying Chisel designs.
\item An additional functional coverage approach through implicit verification plans.
\item An in-depth evaluation and comparison between our methods and those available in UVM and cocotb~\cite{cocotb:2018}.
\end{itemize}
The paper is organized into 7 sections.
First, Section~\ref{sec:related} describes related work, and section~\ref{sec:background} covers
background on hardware verification.
Section~\ref{sec:verify} describes our solution for enabling verification in Chisel, called ChiselVerify.
Section~\ref{sec:formal} presents an orthogonal approach to classic test driven verification by using formal
methods for verification.
Section~\ref{sec:eval} evaluates ChiselVerify with three use cases, and Section~\ref{sec:conclude} concludes the paper.
\section{Related Work}
\label{sec:related}
%\todo{\url{https://capra.cs.cornell.edu/latte21/}}
Most new designs are being described in SystemVerilog; an extension of traditional Verilog
which introduces many non-synthesizable elements. These extensions include various object-oriented
programming constructs and are intended to allow for writing more advanced test benches. However,
contrary to Chisel, the object-oriented design approach cannot be used for hardware
description. SystemVerilog also offers constructs for gathering statement and functional
coverage information~\cite{spear2008systemverilog}, but our solution differs from these in
several ways. In addition to SystemVerilog's range- or transition-based bins,
ChiselVerify's \texttt{cover} constructs support temporal relations as well as generalized
conditional bins based on user-defined predicates.
The temporal relation definition capabilities found in both ChiselVerify and its formal counterpart can also be seen in the Property Specification Language (PSL) and the similar SystemVerilog Assertions~\cite{Boule2008}, which are two current solutions allowing for the use of temporal logic in relation to both coverage and assertions.
Our approaches, however, differ in many ways, but are still all based around concepts taken from Linear-Temporal Logic (LTL) such as the \texttt{past} operator~\cite{Dax2010:temporal-logics}.
For example, PSL bases itself on a wide variety of Sequential-Extended Regular Expressions (SEREs), which define temporal relations between different boolean expressions.
SEREs, however, are quite complex and require the use of many operators to describe potentially simple temporal relations.
In contrast, our solution aims to provide a simplified set of temporal constructs to express LTL.
These encompass a similar range of relations when used in conjunction with different types of bins in our coverage tools.
Cocotb~\cite{cocotb:2018} is a Python-based verification framework for VHDL and Verilog designs.
It is enabled by extensions to Python for coroutine support and DUT interfacing using a very simple \texttt{dut.port.value} interface.
Cocotb was also extended with a library called cocotb-coverage, which allows for functional coverage and constrained random verification to be added to the Python-based test benches.
This solution follows the same goals as ours, since it aims to improve verification efficiency by allowing for verification to take place in a high-level environment.
However, our solution aims to do so in a way that is more closely integrated into the design flow, by allowing for a high-level design to be verified in an equally high-level environment.
Cocotb, in contrast, makes verification happen in a completely separate environment as the design process.
In a later section, we will compare ChiselVerify's functionalities to those available in cocotb.
Adding to the verification features introduced in SystemVerilog, designers also have
access to the UVM, which was designed to be a
standardized way of writing SystemVerilog test benches by focusing on both horizontal and
vertical re-use~\cite{uvm2015}. Unfortunately, its generality leads to an inherent
verbosity, i.e., even simple tests require at least around 800 lines of code. As a result,
UVM imposes significant initial time-investment demands but is re-usable once it gets up and
running. Moreover, newcomers may experience that UVM is less accessible than simpler approaches
such as ChiselVerify as its structure differs from most traditional test benches.
Other works have focused on proposing different verification tools for other HCLs, such as bluecheck~\cite{bluecheck:2015} for the BlueSpec HDL,
or fault~\cite{fault:cav2020} which introduces verification components for the Magma HCL.
Similarly to ChiselVerify, these solutions propose verification functionalities for their specific HCLs.
Bluecheck focuses on enabling the creation of generic testbenches, similarly to UVM, that are synthesizable.
Fault focuses on providing formal and constraint random verification functionalities, similarly to ChiselVerify, but embedded in Python.
Yosys~\cite{Yosys} is an open-source tool for circuit synthesis.
Claire Wolf developed the Yosys Open SYnthesis Suite (Yosys) as part of her
Bachelor thesis at the Vienna University of Technology~\cite{yosys:bachelor}.
The focus of Yosys is on high-level digital synthesis.
Yosys uses the open-source logic synthesis tool ABC~\cite{abc:cav:2010} for gate-level optimizations.
Yosys is a framework that can be extended.
We can use Yosys for interactive design investigation, circuit analysis, or symbolic model checking.
Beyond the general frameworks available in SystemVerilog and UVM, other projects have proposed
using software testing techniques to do hardware verification. For example, RFuzz~\cite{rfuzz2018}
is a generalized method that enables efficient ``coverage-guided fuzz mutational testing''.
It relies on FPGA-accelerated simulation and novel techniques for deterministic memory
resetting to use fuzzing (i.e.\ randomized testing with dynamic seeding based on achieved
coverage results) on digital circuits. Specifically, RFuzz automates collection of and adjustments
based on branch coverage. In comparison to RFuzz, ChiselVerify offers a different type
of solution focusing on implementing verification functionalities in a language while
RFuzz offers an efficient way of using these to ameliorate testing; particularly by using coverage
tools to guide its randomized verification. For the sake of completeness, we also mention another
work of ours presenting the use of ChiselVerify's coverage tools for functional-coverage driven mutation-based
fuzzing~\cite{verify:fuzzing:2021}, as well as a similar work, which proposes a coverage-driven mutation-based fuzzer for SpinalHDL~\cite{ruep2022spinalfuzz}.
% Kevin: I commented out this paragraph, because it makes no sense. Bounded Model Checking **is** used for software verification (e.g., CBMC) and bit-vector theory is also used for
% verifying C or C++ programs (e.g., CBMC, Klee)
% A different approach, also inspired by software verification, is to model the expected behavior of a design in an abstract manner using Satisfiability Modulo Theory (SMT) logic~\cite{smt}.
% This model can be seen as a series of constraints which can then be given to an SMT solver, such as Z3~\cite{de2008z3}, which returns whether or not a set of inputs exists that satisfies the given constraints.
% This approach is often used in software verification, but can be quite cumbersome to write, since we have to hand-write a model of our program by expressing constraints between its different variables.
% The formal methods proposed in this paper differ from that however, since they create an abstraction above bare SMT logic to make it easier to define formal specifications for a design.
% Additionally, modeling hardware with SMT logic requires the use of Bit-Vector theories in order to correctly represent the design, which are much more computationally complex to reason about than more mathematical theories~\cite{DecisionBitVector:1998}.
% Thus, a main difference between the proposed formal methods and traditional software verification is that our solution requires the use of bounded model checking in order to combat the complexity of bit vector theories.
The SecChisel framework~\cite{deng2019secchisel} adds security labels and
static type checking of information flow to the Chisel languages.
The type checks are performed using the Z3 SMT solver~\cite{de2008z3}.
This is different from bounded model checking which verifys user defined
assertions over signals and is able to find sound multi-cycle counter examples
to failing properties. However, bounded model checking is incomplete,
meaning we can miss bugs that only occure many cycles into the design execution.
Static information flow tracking only looks at the combinatorial logic
only to ensure that now information is leaked.
This allows it guarantee that no information will be leaked, but can lead to
false positives where the type check fails, but no bug exists.
To the best of our knowledge, ChiselVerify, along with the formal methods proposed in this work, forms the only framework that provides easy-to-use formal and dynamic verification functionalities which are well-integrated into the Chisel and ChiselTest ecosystem.
The CHA library~\cite{yu2022cha} implements temporal assertions
similar to SystemVerilog Assertions for Chisel designs.
It builds upon the formal verification framework which we discuss
in Section~\ref{sec:formal}.
\section{Background}
\label{sec:background}
Before going into details of ChiselVerify, we provide a brief overview of hardware verification,
and Chisel and its related existing verification techniques.
\subsection{Verification of Digital Designs}
Verification of digital designs refers to testing done before tape-out through simulation
or (FPGA-based) emulation~\cite{spear2008systemverilog}. SystemVerilog is one
of the main languages used for verification. It allows for engineers to define constraint-driven
randomized test benches and metrics to gather functional coverage resulting for a suite of
tests. We are interested in three verification features: functional coverage,
constrained random verification, and bus functional modeling---all three of which are available in
SystemVerilog, but rather complex to use as they are embedded in a low-level language.
\subsubsection{functional coverage}
One of the most frequently used verification tools is test coverage which enables measuring
progress and effectiveness of testing processes. In contrast to the common, quantitative statement
coverage metric, which measures how many lines of code have been tested, functional coverage is
qualitative and aims at answering which functionalities have been
tested~\cite{spear2008systemverilog}. This enables measuring how correctly a design implements its
specification, and it is measured relative to a verification plan which includes the following:
\begin{itemize}
\item \texttt{Bin}s that declare ranges of values that should be tested for (i.e., expected values of a given port), and
\item \texttt{cover} constructs specifying ports that need to be sampled in the coverage report, defined using a set of \texttt{bin}s.
\end{itemize}
\begin{figure*}
\centering
\includegraphics[width=0.8\linewidth]{Chisel_FIRRTL_VERILOG.pdf}
\caption{Overview of the Chisel compilation pipeline.}
\label{fig:chisel-pipe}
\end{figure*}
\subsubsection{Constrained Random Verification}
Using constrained random verification features, a verification engineer can create random variables constrained to
specified sets of values. In doing so, even a relatively small test suite can, statistically,
cover many functionalities of a design. Moreover, constraining inputs ensures that no
unnecessary tests are run for input combinations that would not appear during regular
operation~\cite{MehtaCRV2018}.
A set of constrained random variables define a Constraint Satisfaction Problem (CSP). In CSPs,
problem entities are represented as a finite, homogeneous collection of constraints. CSP solvers
seek solutions to such problems and, thus, serve as the basis for constrained random verification.
Constrained random data types are native and declared with the \texttt{rand} keyword in SystemVerilog.
Implementations of SystemVerilog simulators have a built-in CSP solver allowing for randomization through its \texttt{randomize} method.
\subsubsection{Bus Functional Models}
Abstraction is often the key to solve complex problems. Bus functional models represent such
a technique. They implement models of (standardized) interfaces, like the Advanced eXtensible Interface version 4 (AXI4) from ARM~\cite{axi4standard}, that enable
interacting with either master or servant components at a transaction level that abstracts away
bit-fiddling of individual wires. Digital hardware vendors often provide IP generators whose output
blocks are equipped with such interfaces. Well-developed bus functional models enable simpler, safer, and less
verbose interactions with such components.
\subsection{The Chisel Hardware Construction Language}\label{subsec:chisel}
Chisel is a ``hardware construction language'' embedded in the general purpose programming language Scala~\cite{bachrach2012chisel, chisel:book}.
It allows designers to effectively write Scala programs that generate hardware descriptions at the Register Transfer Level (RTL).
Compared to traditional hardware description languages, like
VHDL and Verilog, Chisel is much more high-level and allows for object-oriented and functional
programming in the context of digital design.
One popular open-source application is the powerful RocketChip system on chip generator~\cite{rocketchip}.
The user-facing API of Chisel is a Scala library with some syntactic sugar that allows the user to generate RTL designs.
These designs then have to be converted into a format that is understood by simulators as well as FPGA and ASIC synthesis tools.
The lowering is done by the FIRRTL compiler which converts a high-level Intermediate Representation (IR) into a normalized structural representation~\cite{firrtl}.
The low-level representation is then exported into a subset of Verilog that was chosen as a common subset supported by the majority of backend tools.
Besides serving as a convenient way to lower Chisel circuits into Verilog, the FIRRTL IR and accompanying compiler infrastructure
also makes it easy to add circuit analysis and instrumentation passes, known as \textit{transforms}. Moreover, it is possible to simulate circuits described in FIRRTL using the Treadle execution engine, before converting them into (System)Verilog which can, of course, be simulated using commercial or open-source tools like Verilator~\cite{verilator}. Figure~\ref{fig:chisel-pipe} shows an overview of the Chisel compilation pipeline.
Being embedded in Scala, Chisel is executed on the Java virtual machine (JVM) and can
use existing Scala and Java libraries for design and verification.
The JVM also allows for the use of the Java native interface for calling C functions,
thus enabling co-simulation of Scala testers, Chisel designs, and a C-based golden models. This is
valuable for companies wishing to keep their existing C models while using Scala/Chisel for simulation.
\subsection{Testing Chisel Designs}
A Chisel design can be tested with ChiselTest~\cite{chiseltest}, a non-synthesizable
testing framework for Chisel that emphasizes simplicity while providing ways to scale up
complexity. Like Chisel, ChiselTest is a Scala library that provides an interface into several
simulators through \texttt{peek} (read value from circuit), \texttt{poke} (write value to
circuit), and \texttt{step} (advance time) operations. Tests written with ChiselTest are just
imperative Scala programs that run one line after another.
However, ChiselTest is missing fundamental functionalities that improve verification efficiency.
For example, it does not provide either of the three aforementioned features: functional
coverage, constrained random variables, or bus functional models, despite these features being
crucial for efficient verification.
\section{Verification with Chisel}
\label{sec:verify}
As an extension of ChiselTest, we propose ChiselVerify, which introduces verification functionalities to the Chisel ecosystem.
ChiselVerify bases itself on ChiselTest by using its design interfacing features in order to enable various verification functionalities directly in Chisel, such as functional coverage, constrained random verification, and bus functional modeling.
In the following subsections, we present how we achieved our solution.
We start by presenting its functional coverage capabilities, using both implicit and explicit specifications.
We then propose constrained random verification tools for automating testing of general designs.
% , as well as a constrained program generation tool for verifying processors designed in Chisel. MS: not there (yet)
Finally, we look at how bus functional models can increase testing ease, and demonstrate our method by creating a model of the standardized AXI4 interface.
\subsection{Coverage in Chisel}
% Proposed rewrite
ChiselVerify allows for functional coverage constructs to be defined over a Chisel design directly in Scala.
This is possible directly inside of a ChiselTest test bench, and enables the gathering of information about completeness of a test with relation to a given specification defined using a verification plan.
In order to enable functional coverage in Chisel, we explore and define two methods for describing a specification, using an \emph{implicit} or an \emph{explicit} verification plan.
These two methods are defined as follows:
\begin{itemize}
\item \textbf{Implicit verification plan:} Omit the verification plan, and obtain coverage data through queries on the ports we want to check \emph{after} the test bench.
\item \textbf{Explicit verification plan:} Declare the ports that will be sampled \emph{before} the test bench.
\end{itemize}
Both methods have their advantages and disadvantages, which we will discuss in detail in a later section.
In both cases, in order to define our methods, we needed to be able to do the following:
\begin{itemize}
\item Create a verification plan, either implicitly using a \texttt{QueryableCoverage} object, which creates a verification plan containing all of our device's ports, or explicitly using \texttt{cover} constructs,
\item sample our tested device's ports, using ChiselTest's interfacing capabilities,
\item keep track of the different sampled values that hit for each bin, using a coverage database, and
\item compile all of the results into a comprehensible and programmatically useful coverage report.
\end{itemize}
We will describe how the two different solutions are used to gather functional coverage information.
\paragraph{Implicit Verification Plans} As presented above, our solution enables the gathering of functional coverage information without explicitly having to define a specification.
This is enabled through the use of a \texttt{QueryableCoverage} object, which is simply defined on a given DUT.
The object must be defined within a ChiselTest test bench, allowing for the existence of a usable DUT.
Once that is done, it must be sampled throughout the test suite using its \texttt{sample} method.
Doing so will store the current value of every port in the DUT, thus enable for a future coverage queries.
Obtaining coverage information using this method is done at the end of a test suite using coverage queries of the form of \texttt{get(port, expectedHits, range)}, where the arguments have the following meaning:
\begin{itemize}
\item \texttt{port} represents the DUT port for which we want coverage information.
\item \texttt{expectedHits} is optional and represents a specification of number of hits we would expect for this port.
\item \texttt{range} is also optional and represents the range in which we want to sample the port.
\end{itemize}
A query of this sort yields a \texttt{CoverageResult} case class that can then be used either programmatically or to generate a readable report using its \texttt{print} method.
One can also simply print out a full report of all ports in the DUT using the \texttt{QueryableCoverage} object's \texttt{printAll} method.
\begin{lstlisting}[captionpos=b,caption={Example use of a \texttt{QueryableCoverage} object in order to gain information about the DUT's testing process. Note that \texttt{outA} and \texttt{outB} simply output the values of \texttt{a} and \texttt{b}.},label={lst:implcov},language=scala]
val coverage = new QueryableCoverage(dut)
for (fun <- 0 until 50) {
dut.io.a.poke(toUInt(fun))
dut.io.b.poke(toUInt(fun % 4))
coverage.sample()
}
coverage.get(dut.io.outA, 50).print()
coverage.get(dut.io.outB).print()
coverage.get(dut.io.outA, range = 0 to 4).print()
coverage.printAll()
\end{lstlisting}
Listing~\ref{lst:implcov} shows a basic use of the implicit coverage tool.
The above example outputs the following coverage Report:
\begin{verbatim}
Port io_outA has 50 hits = 100.0% coverage.
Port io_outB has 4 hits.
Port io_outA for Range 0 to 4 has 5 hits
= 100.0% coverage.
========= COVERAGE REPORT =========
Port io_outB has 4 hits.
Port io_outA has 50 hits.
Port io_b has 4 hits.
Port io_a has 50 hits.
===================================
\end{verbatim}
This report shows the coverage results from our simple test in the form of the number of hits associated to each port.
Given a specification for the amount of expected hits, the tool also shows a coverage percentage over the port.
We can also see that given a range, the tool is also able to output a coverage percentage using the range length as the expected number of hits.
The \texttt{QueryableCoverage} object can be a useful tool for gathering simpler coverage information on a DUT.
However, in order to define complex specifications for our functional coverage, explicit verification plans must be used.
\paragraph{Explicit Verification Plans} As detailed in our previous work on the topic~\cite{dobisCoverage:ETS22}, our solution allows for the definition of a target specification using a set of \texttt{cover} constructs defined inside of a \texttt{CoverageReporter}.
This reporter functions as a front-end element that allows for the definition of complex specifications using only high-level constructs.
The main interface is the \texttt{register} method, which stores \texttt{cover} construct to \texttt{bin} mappings inside of a \texttt{CoverageDB} database object, and groups them as a single \texttt{covergroup}.
After the definition of the verification plan, the registered elements of the specification must be sampled explicitly by the user using the \texttt{sample} method, which checks the value of the associated port at that simulation cycle against the bins in order to determine if the specification is met for that value or not.
Once the sampling has been done, at the end of a test suite, a functional coverage report is generated by the reporter using the \texttt{report} method, which interprets the results stored in the database and compiles them to into a Scala \texttt{case class}.
This report object can then be used to check for coverage thresholds, i.e., whether or not the coverage has surpassed a specific amount, or to guide the mutation of inputs in a mutation-based fuzzer, as was explored in previous work~\cite{verify:fuzzing:2021}.
\begin{lstlisting}[captionpos=b,caption={Small verification plan defined using 3 \texttt{cover} constructs, including one cross coverage construct},label={lst:basicfuncov},language=scala]
val cr = new CoverageReporter
cr.register(
cover("accu", dut.io.accu)(
bin("lo10", 0 to 9),
bin("First100", 0 to 99)),
cover("test", dut.io.test)(
bin("testLo10", 0 to 9)),
cover("accuAndTest", dut.io.accu, dut.io.test)(
cross("both1", 1 to 1, 1 to 1))
\end{lstlisting}
Listing~\ref{lst:basicfuncov} shows a basic verification plan defined using our functional coverage tool.
This example shows the use of \texttt{cover} constructs with both a single and multiple ports.
The use of multiple ports defines a \textit{cross coverage} relation between the two ports, meaning that sampled values are considered a \emph{hit} if they meet all specifications simultaneously~\cite{spear2008systemverilog}.
Concretely, in our example, a hit would be considered if, within the same cycle, both \texttt{dut.io.accu} and \texttt{dut.io.test} are sampled with the value \texttt{1}.
After defining our specification in the form of a verification plan, we need to define the ideal location, within our test bench, to sample the registered \texttt{cover} constructs using the reporter.
The reporter's \texttt{sample} method can be used to sample either all groups simultaneously or simply one group at a time, by specifying the \texttt{id} of the group we want to sample in the method call.
All \texttt{cover} constructs within a group will always be sampled together.
Sampling different groups at different locations throughout the test suite can allow for more targeted coverage information to be gathered.
Finally, once we are done sampling throughout the test suite, we can print out a readable coverage report by calling \texttt{cr.printReport()} which, for our example, results in the following:
\begin{verbatim}
=============== COVERAGE REPORT ===============
================= GROUP ID: 1 =================
COVER_POINT PORT NAME: accu
BIN lo10 COVERING 0 to 9 HAS 8 HIT(S) = 80%
BIN First100 COVERING 0 to 99 HAS 9 HIT(S) = 9%
===============================================
COVER_POINT PORT NAME: test
BIN testLo10 COVERING 0 to 9 HAS 8 HIT(S) = 80%
===============================================
CROSS_POINT accuAndTest FOR POINTS accu AND test
BIN both1 COVERING 1 to 1 CROSS 1 to 1 HAS
1 HIT(S) = 100%
===============================================
\end{verbatim}
This report shows the three \texttt{cover} constructs that we registered and the associated hits that each of their \texttt{bins} has obtained.
These hits represent the number of times that registered port was sampled with a unique value found within the given range.
Each \texttt{bin} is then given a coverage percentage, based on the ratio between the number of hits obtained and the total number of possible values in the range.
In addition to the basic \texttt{cover} constructs presented until now, our tool allows for the definition of more complex relations, including delayed cross coverage, and purely conditional coverage.
Inspired by concepts from Linear Temporal Logic~\cite{Dax2010:temporal-logics}, delayed cross coverage allows the user to define a relation between two ports sampled at different clock cycles.
The idea is similar to how a \texttt{cross} works, but this time rather than sampling both points in the same clock cycle, we compare one port, at the starting clock cycle, to another port sampled a given number of clock cycles later.
The temporal separation between the two ports is defined using a \texttt{delay}, for which we have defined four different types:
\begin{itemize}
\item \texttt{Exactly}, a hit is obtained if a value sampled from the second port meets its specification, as defined in the bin, exactly the given number of clock cycles after the first point was sampled.
\item \texttt{Eventually}, a hit is obtained if the second port meets its specification at any point within the given number of clock cycles after the first point was sampled.
\item \texttt{Always}, a hit is obtained if the second port meets its specification at every clock cycle for a given number of clock cycles after the first point was sampled.
\item \texttt{Never}, a hit is obtained if the second port never has a sampled value that meets its specification during any clock cycle for a given number of clock cycles after the first point was sampled.
\end{itemize}
The use of timing-related bins is only possible if the coverage reporter is used to step the clock instead of ChiselTest's regular interface, since it allows for the \texttt{CoverageDB}'s internal clock to remain correct.
Conditional coverage enables the definition of bins containing fully custom hit-consideration rules using a user-defined predicate.
Using this type of construct, one can check for arbitrary relations between an arbitrary number of ports.
For example, it is possible to create a bin that checks for every clock cycle where all fields in a vector are equal.
This is done by using a function of type \texttt{Seq[BigInt] => Boolean} in the \texttt{bin} declaration.
The report then shows the number of distinct sampled values for which the predicate held throughout the testing process.
Given the unbounded nature of these bins, an ``expected number of hits'' argument is required for each condition in order to yield a final percentage alongside the number of hits.
Using these different types of \texttt{cover} constructs, one can express the specification of virtually any design.
\paragraph{Explicit vs. Implicit Verification Plans}
Comparing the two solutions, Explicit verification plans enable more precise and complex definitions of specifications than implicit verification plans.
This is due to the computational load inherently present in Implicit verification plans.
Since Implicit verification plans require sampling over the entire range defined by the bit-width of every port, it is limited to use with simpler designs.
However, implicit verification plans do not require the re-simulation of a design in order to obtain different coverage information, while explicit verification plans do.
All in all, these coverage tools allow for the gathering of functional coverage directly in Chisel, thus filling in one of the holes inside of Chisel's ecosystem.
Another tool our solution brings to Chisel, and enables one of the uses of functional coverage presented earlier in this section~\cite{verify:fuzzing:2021}, is constrained random verification, which is detailed in the following paragraph.
\subsection{Constrained Random Verification}
%Proposed rewrite:
A coverage-driven verification suite is not complete without access to randomization tools.
ChiselVerify thus provides a set of tools, inspired by those available in SystemVerilog, which allow for the declaration and randomization of random objects.
This is done by providing a ``constraint programming" domain-specific language that runs on an existing CSP solver named JaCoP~\cite{jacop2013}.
\subsubsection{ChiselVerify's Constraint Programming DSL}
Our solution allows for the declaration of a constrained random object by defining a \texttt{class} that extends the \texttt{RandObj} trait.
The newly defined \texttt{RandObj} class contains all of the constraints and random variables that will later be used in our constrained random tests.
This information will be stored inside of a \texttt{Model}, that is given to the \texttt{RandObj} on initialization.
A \texttt{Model} is simply an object which functions as a database for our generated random number. It can be initialized with a seed to allow for predictable pseudo-random number generation.
In total, there are two main elements that are used inside of the object to drive the randomization process: random variables and constraints.
\paragraph{Random Variables} The first element represents a random value generator and is associated to constraints that will determine the set of values that the random variable can take.
Our DSL allows for the declaration of two different types of random variables:
\begin{itemize}
\item \emph{Regular}, can take any value that satisfies the associated constraints.
\item \emph{Cyclic}, can not take the same values twice until the entire set of valid values has been explored.
\end{itemize}
Similar to the interface designed for the functional coverage tools, both types are declared using a single unified function call \texttt{rand} to which we give a lower and an upper bound on the values the variable can take.
As an example, \texttt{rand(0, 5, Cyclic)} will declare a random variable that will yield six distinct values in a row before starting to repeat itself.
\paragraph{Constraints} Our solution allows for the definition of both single constraints and \texttt{ConstraintGroup}s.
Constraints are defined by applying constraint operators on random variables.
Additionally, the \texttt{IfCond} and \texttt{ElseC} constructs allow for the definition of conditional constraints.
Every defined constraint may be enabled or disabled at any point in the test suite.
This can also be done to multiple constraints simultaneously by enabling or disabling a \texttt{ConstraintGroup}.
\paragraph{Using a RandObj} After declaring and filling random objects with random variables and constraints, a \texttt{RandObj} must be instantiated and then randomized using the \texttt{randomize} method within a test bench.
The \texttt{randomize} method's return value is predicated on the solvability of the constraints by the CSP solver.
Once randomized the random variables within a \texttt{RandObj} yield a valid random value when prompted with their respective \texttt{value()} methods.
\begin{lstlisting}[language=scala, caption={Example usage of a random object. \texttt{rand(min, max, type=Normal)} declares a random variable. Any operation on a random variable generates a constraint.}, label={lst:randobjscala}]
class Packet extends RandObj(new Model(3)) {
val idx = rand(0, 10)
val size = rand(1, 100)
val len = rand(1, 100)
val payload: Array[Rand] = Array.tabulate(11)(
rand(1, 100)
)
// Example Constraint with operations
val single: Constraint = (payload(0) == (len - size))
// Example conditional constraint
val conditional = IfCon(len == 1) {
payload.size == 3
} ElseC {
payload.size == 10
}
val idxConst = idx < payload.size
}
\end{lstlisting}
Listing~\ref{lst:randobjscala} shows an example use of the different ways one can define a random variable with constraints.
As seen in the example, collections of random variables, such as arrays, can be defined and constraints can be placed on the collections themselves.
This approach is seen in the \texttt{payload} random variable, where a constraint is placed on the size of an array of random variables.
The \texttt{conditional} random variable shows how conditional constraints can be declared.
In our example, the constraint placed on \texttt{payload} depends on the value of the \texttt{len} random variable.
These constrained random objects are a powerful tool that can be combined with the aforementioned coverage functionalities to create coverage-driven randomized tests.
With the use of our solutions, such a setup greatly improves the automation capabilities of Chisel test benches.
However, these capabilities may be further improved by abstracting away groups of wires and operating on an operation or transaction level instead.
\subsection{Verification with Bus Functional Models}
Component re-use, portability and flexibility are also interesting in the context of digital
designs. A good way to achieve these characteristics is by equipping one's designs with standardized
interfaces. Verification engineers can test such components at a transaction level by combining constrained random verification
and coverage measures with bus functional models. As an example, we provide a bus functional model for AXI4, an open standard by
ARM~\cite{axi4standard}.
\subsubsection{Introduction to AXI4}
The AXI4 protocol by ARM is a generic, flexible interconnect standard. It comprises five independent handshake-based channels; three for write operations (\textit{Write Address}, \textit{Write Data}, and \textit{Write Response}) and two for read operations (\textit{Read Address} and \textit{Read Data}). Interconnect operations, known as transactions, consist of sequences of transfers across either set of channels. All channels share a common clock and reset.
As an example, consider a write transaction of 16 data elements. First, the manager provides the transaction attributes (e.g., target address and data size) as a transfer on the \textit{Write Address} channel.
Next, it transfers the data elements one at a time over the \textit{Write Data} channel.
Finally, the subordinate indicates transaction status on the \textit{Write Response} channel. Note that channel independence means that data may be transferred before the transaction attributes and that the read channels may operate at the same time~\cite{axi4standard}.
\subsubsection{Implementation}
To enable easy verification of AXI4-interfaced components using ChiselVerify, we provide definitions of channel wire bundles, abstract manager and subordinate classes, and a transaction-based bus functional model: the \texttt{FunctionalManager} class. Through parameterizing the manager with a \texttt{Subordinate} DUT provides a simple interface to control the DUT. Its two most important methods are \texttt{createWriteTrx} and \texttt{createReadTrx} to create and enqueue write and read transactions, respectively.
Our bus functional model implementation utilizes ChiselTest's multithreading features to allow for non-blocking calls to the aforementioned methods and for emulating the channel independence more closely. When a write transaction is enqueued and no other write transactions are in flight, the bus functional model spawns three new threads, one for each channel. The threads handle the bit-fiddling required to operate the channels.
\subsubsection{A Test Example}
Returning to the example of transferring 16 data elements, consider the test for a module called \texttt{Memory} listed below. First, a write transaction with 16 data elements takes just one call to \texttt{createWriteTrx} most of whose arguments have default values. It is equally simple to create a subsequent read transaction. Depending on the DUT implementation and due to channel independence, not waiting for a write to complete before initiating a read may return incorrect results.
\begin{lstlisting}[language=scala, caption={Using the AXI4 bus functional model with ChiselTest}, label={lst:axitest}]
class MemorySpec extends AnyFlatSpec with ChiselScalatestTester {
behavior of "My memory module"
it should "write and read" in {
test(new Memory()) { dut =>
val bfm = new FunctionalManager(dut)
bfm.createWriteTrx(0, Seq.fill(16){0x7FFFFFFF},
len = 15, size = 2)
bfm.createReadTrx(0, len = 15, size = 2)
}
}
}
\end{lstlisting}
\subsection{Future Work}
In this paper our development stays mainly in the Chisel world and comparing it against UVM.
However, an interesting approach would be to mix and mach different approaches and tools.
The easiest combination is to use Chisel for the circuit description and UVM for verifiaction.
Chisel generates standard Verilog code that can be integrated with an UVM testing flow.
SystemVerilog components can also be integrated in Chisel as black boxes and we can
verify them with ChiselVerify on Verilator. However, the SystemVerilog code shall only
include Verilog code constructs that Verilator supports.
The most challenging approach would be to combine UVM and ChiselVerify. For example,
having an UVM test driver for the DTU, but using ChiselVerify with a golden reference
model written in Scala. We consider this interesting development question as future work.
\section{Formal Verification}
\label{sec:formal}
% introduce dynamic verification
% While working on the RTL level description of a new circuit, designers need to quickly test their design
% in order to iteratively improve it. Extensive testing is also a common requirement before sending a design to be
% fabricated as bugs discovered after ASIC fabrication can be costly or even impossible to fix.
% The most common approach to testing RTL is to write a test bench program that interacts with a simulation of the design.
% Errors are found through manual waveform inspection or assertions in the design or the test bench.
% explain bounded model checking
An alternative to exercising the circuit description with a set of concrete inputs is to symbolically explore the circuit execution
for any inputs for a limited number of cycles.
This technique is called bounded model checking~\cite{bmc} and works by unrolling the circuit for $k$ cycles and then asking a
Boolean satisfiability~\cite{sat} or SMT~\cite{smt} solver whether there exists a set of inputs and starting states for the memories and registers in the
design, for which an assertion is violated.
If the solver returns a satisfying assignment to this query, we obtain a counter example that can be expressed as a test bench that
initialized the state to concrete values from the solver and then drives the inputs for $k$ cycles with the inputs obtained from the solver.
If the solver returns that there is no such assignment, we get a guarantee that our circuit will not hit any assertion violation for the first $k$
cycles of its execution.
% the current state of the open-source ecosystem for formal verification
There has been a long tradition of open-source formal verification systems from the academic community~\cite{mcmillan1993smv, cimatti2000nusmv, mishchenko2007abc}.
% only citing select solvers for space gains
%~\cite{mann2021pono, mcmillan1993smv, cimatti2000nusmv, owre1996pvs, goel2020avr, vizel2014interpolating, mishchenko2007abc}.
However, because of the traditional academic incentive structure, these research systems were hard to use or did not support
enough features of the RTL design language to be widely used by a community of open source RTL designers.
This changed with the introduction of the yosys~\cite{Yosys} tool which has become the de facto standard for processing
Verilog for synthesis or formal verification.
Yosys allows academics to focus on developing model checkers for the simple btor2~\cite{btor2} or aiger~\cite{aiger} formats
without having to worry about supporting the much more complicated Verilog standard.
The open-source SymbiYosys~\cite{symbiyosys} tool wraps yosys as well as various formal verification engines in order to allow users to
verify their designs. All a user has to provide are the Verilog sources of their design including assertions and assumptions
as well as a small configuration script. SymbiYosys translates any failing traces it discovers into Verilog test benches and VCD waveform dumps
for the user to inspect~\footnote{With the open-source GHDL plugin, yosys now also supports formally verifying VHDL circuits.}.
\begin{figure}
\centering
\includegraphics{woset_formal_ide_flow.pdf}
\caption{Formal checks can be launched from a Scala IDE.}
\label{fig:ide-flow}
\end{figure}
% problems with the SymbiYosys approach
In this paper we describe our approach to providing Chisel users with an easy way to formally verify their designs.
We adapt many good ideas from yosys and build several new convenience features on top of them, taking advantage
of the existing compiler infrastructure for Chisel.
We first present two introductory examples and then provide details on how the formal backend was engineered to ensure that
all failures could be replayed in simulation to help debugging in Section~\ref{sec:undef}.
We then discuss reset assumptions (Section~\ref{sec:reset}) and a simple approach to temporal assertions using an improved version of the
past statement which avoids common pitfalls compared to the equivalent functionality in Verilog (Section~\ref{sec:past}).
\subsection{Example: Verifying a GCD Circuit}
We focus on a simple greatest common denominator (GCD) circuit which is a standard Chisel example.\footnote{The GCD code is part of the Chisel template: \url{https://github.com/freechipsproject/chisel-template}}
Dynamic tests written with ChiselTest can be executed through a Scala IDE or from a shell with the \code{sbt test} command.
With our proposed extension, formal checks can be run in a similar fashion.
The user just needs to substitute the \code{test(new DecoupledGcd(16))}
command with \code{verify(new DecoupledGcd(16), BoundedCheck(10))}.
%, as well as provide the type of verification job as
%\code{BoundedCheck(10)} and extend the testing class with the \code{Formal} trait.
If the user now clicks the test icon again or runs the \code{sbt test} command, a formal bounded check will
be executed for ten cycles after reset instead of a simulation test.
The only additional program required is a copy of the open-source SMT solver Z3~\cite{de2008z3}.
Initially the check will always pass, no matter which changes we make to our circuit.
Since the GCD circuit contains no assertions, there is nothing to tell the solver if the circuits misbehaves.
In order to actually verify something, an assertion needs to be added directly to the circuit by using the
Chisel \code{assert} statement.
The decoupled GCD circuit used as an example has an \code{input} and an \code{output} channel
as well as a 1-bit \code{busy} register.
We expect that while the circuit is busy, no new input is accepted:
\begin{verbatim}
when(busy) {
verification.assert(!input.fire())
}
\end{verbatim}
This assertion will pass because the circuit does indeed fulfill the property after reset.
We now introduce a small bug by connecting \code{input.ready} to \code{true.B} and rerun the test.
An assertion violation will be reported one cycle after reset.
The user is also presented with an error message indicating the Scala line number of the failing assertion.
To debug the problem, they can find a VCD waveform dump in the standard test directory created by the
ChiselTest library.
Since we replay the test on a concrete simulator, the error message and VCD will be exactly the same
as if the user was running a simulation test.
% Any improvements to our simulation interface or error reporting will thus immediately benefit formal verification
% users as well.
A more advanced property we expect to hold is that if the input and output channels are idle, the busy
signal will remain the same in the next cycle:
\begin{verbatim}
when(past(!input.fire() && !output.fire())) {
verification.assert(stable(busy))
}
\end{verbatim}
Here we make use of our \code{past} function for temporal properties which is described in detail in Section~\ref{sec:past}.
When working in a standard Scala IDE like the open-source IntelliJ IDEA with the Scala plugin, the user can launch the formal check with the press of a button.
The success or failure will be communicated the same way as any other unit test. A VCD waveform dump is automatically generated to help debug failing checks.
This is illustrated in Figure~\ref{fig:ide-flow}.
\subsection{Example: Verifying the Read Behavior of a RTL Memory}
\begin{figure}
\centering
\begin{lstlisting}[language=scala]
class Quiz15 extends Module {
/* [...] I/O definitions */
val mem = SyncReadMem(256, UInt(32.W), WriteFirst)
when(iWrite) { mem.write(iWAddr, iData) }
oData := mem.read(iRAddr, iRead)
when(past(iWrite && iRead &&
iWAddr === iRAddr)) {
verification.assert(oData === past(iData))
}
}
class ZipCpuQuizzes extends AnyFlatSpec
with ChiselScalatestTester with Formal {
"Quiz15" should "pass with WriteFirst" in {
verify(new Quiz15, Seq(BoundedCheck(5)))
}
}
\end{lstlisting}
\includegraphics{woset_2021_quiz15.pdf}
\caption[]{RTL code with inline assertion, formal test and counter example waveform for a verification example.}
\label{fig:quiz15}
\end{figure}
The example shown in Figure~\ref{fig:quiz15} verifies that when a Chisel memory with synchronous read port and
\code{WriteFirst} behavior has a read and a write access to the same address,
the new value will be returned.
The check fails if \code{WriteFirst} is substituted with \code{ReadFirst} or \code{Undefined}.
% The implementation of \code{undefined} values is detailed in Section~\ref{sec:undef}.
It is based on \href{https://zipcpu.com/answer/2021/07/03/fv-answer15.html}{a Verilog example from a popular blog}~\footnote{\url{https://zipcpu.com/answer/2021/07/03/fv-answer15.html}}.
In the Chisel version, the assertion is automatically delayed until at least one cycle after reset, when there are valid \code{past} values available.
A bounded model check is executed by the \code{verify} command, which is called from a standard Scala unit test.
When the check fails, the failing inputs and starting states are replayed on a simulator, resulting in a waveform file that is identical
to the output we would get from a dynamic verification run.
However, since we used bounded model checking to find the failing trace, it will be as short as possible.
In our example, two cycles after reset are needed to fail the property.
The first cycle contains the read and write requests and the second cycle observes the arbitrary result on the read port if
we set the memory behavior to \code{Undefined} for read/write conflicts.
The included screenshot was obtained with the open-source GTKWave waveform viewer.
\subsection{A Formal Backend for FIRRTL}\label{sec:undef}
In order to implement the verify command, introduced in the previous sections, we need to convert the Chisel circuit into a format that is understood
by open-source model checkers or SMT solvers.
We can do this by using the FIRRTL compiler to convert the circuit to Verilog and then using yosys to convert to the
model checking formats.
While we initially used this approach, we eventually decided that it would be better to add a formal backend to the FIRRTL
compiler directly.
This way we can avoid the complicated Verilog semantics, model circuit behavior in greater detail and easily replay counter
example traces on our FIRRTL simulator called treadle.
Users want their Chisel designs to be implemented with as little hardware as possible.
In order to allow for efficient implementations, the FIRRTL specification was crafted to allow
some operations to result in arbitrary results.
For example, a wire connected to \code{DontCare} or to the result of a division by zero carries an arbitrary value.
Reading from a memory while the read port is disabled, reading from the same address that another port is writing to or
writing from two memory ports to the same address all generate an arbitrary value result.
Not all of these behaviors are represented in the generated Verilog.
The compiler is free to substitute arbitrary values with (more) concrete values, like always returning a memory read result even
when the read port is disabled or by assigning a priority to write operations so that at least one of them will complete.
Thus if we first generate Verilog and then use yosys, we are only verifying one concrete translation of the design,
but there may be other legal translations that would violate the property.
This is relevant, e.g., in the context of memories when we use an external SRAM compiler that might try to
rely on the fact that write-write collisions can have arbitrary results in order to generate better hardware.
This is the reason why we decided to carefully model arbitrary values as part of the FIRRTL compiler's new formal backend.
Once the formal engine finds starting states and inputs that lead to an assertion violation, we need to help the user debug their design.
Since we do not have the large resources of a major EDA vendor, we would like to reuse as much of the existing simulator infrastructure as we can.
If we can replay the failing trace on our existing simulator, the VCD waveform dump and the error reporting will be of the same quality as
when writing a concrete test bench.
In order to be able to replay failures caused by arbitrary values, we carefully engineered two FIRRTL passes that analyze the circuit and
add wires to detect when a result is arbitrary as well as a mux to substitute the result with a connection to a \code{DefRandom} node in that case.
The new \code{DefRandom} construct provides a named arbitrary value that can change every clock cycle,
very much like a \code{anyseq} annotated wire in Verilog.
The formal backend implements \code{DefRandom} nodes as inputs that can be freely chosen by the formal engine.
To make \code{DefRandom} work with our simulator, we replace the nodes with registers of the same type that are never updated by the hardware.
Instead we use the software interface to our simulator to update these registers with the values chosen by the formal engine in each cycle.
Figure~\ref{fig:verify-flow} shows our compilation flow in more detail.
The \code{verify} command is implemented as part of the ChiselTest library and uses several compiler passes that make up the FIRRTL formal backend.
We hook into the FIRRTL compiler to model undefined behavior with \code{DefRandom} statements and to delay temporal assertions as part of our safe past construct.
We then add reset assumptions, flatten the system, convert to a formal transition system and then serialize the system to SMTLib or btor2.
We provide bindings to launch various formal engines from ChiselTest.
If a counter example is found, we convert the \code{DefRandom} nodes in the circuit to registers before loading the circuit into the treadle simulator
to replay the failure and obtain a simulation quality VCD and error message.
The btor2 format does not support hierarchical circuits and we thus always flatten the system by inlining everything into a single module.
In order to ensure that we produce a good waveform dump, the counter example will be replayed on the non-inlined circuit.
We make use of the built-in annotation support of the FIRRTL compiler to automatically track name changes of all registers and memories in
the design as they are inlined. This way we can map initial states found by the formal engine back to their hierarchical names.
Once the circuit has been flattened, the conversion to a transition system is fairly straight forward.
We implemented a SMTLib and btor2 encoding that is very similar to the one pioneered by yosys.
We used the FIRRTL specification to accurately translate FIRRTL expressions to the bit-vector expression language defined by the SMTLib format~\cite{smtlib}.
Our backend supports memory and register initialization using the same user annotations as the Verilog backend.
Multi-clock support through a clock stuttering pass is work in progress, for now only circuits with a single clock domain are officially supported.
\begin{figure}
\centering
\includegraphics{woset_verify_flow.pdf}
\caption{Our Formal Verification Flow}
\label{fig:verify-flow}
\end{figure}
\subsection{Reset Assumptions}\label{sec:reset}
In Chisel, users rarely need to worry about resets.
Registers with reset values are automatically connected to the default reset % port of the module
and module instances just inherit their reset domain from their parent.
In Verilog, users have to manually ensure that assertions are only triggered after the circuit was properly reset.
We decided to provide sensible defaults instead.
Assertion statements are automatically disabled, just like it has been the case for print and stop statements
since the early days of Chisel.
As part of our formal verification support, we provide a FIRRTL pass
that automatically adds a constraint for the reset of the top level module to be active during the first cycle of execution.
Thus, by default, users do not have to worry about reset. Their assumptions will only fire
after their circuit has been properly reset and hence we ensure that there are no false positives.
We do provide options for power-users to write assertions that are active during reset and to
disable reset assumptions or increase the number of reset cycles.
\subsection{Simple Temporal Assertions}\label{sec:past}
\begin{figure}
\centering
\includegraphics{woset_safe_past.pdf}
\caption{Hardware generated by our tool to implement the temporal assertion in Figure~\ref{fig:quiz15}.}
\label{fig:past}
\end{figure}
While a simple \code{assert} statement allows us to specify a property over signals during a single
cycle, it is not enough to express properties that require us to reason about multiple cycles.
The traditional answer to this problem are temporal assertion languages like SystemVerilog Assertions~\cite{SystemVerilog}.
However, these are complex to implement efficiently and as of now there has not been a successful open-source implementation.
The community around SymbiYosys has instead advocated for the use of plain assertions with the Verilog \code{past}
function. This function returns the previous value of an expression and thus allows us to write properties that span multiple cycles.
While conceptually simple, the \code{past} construct as defined by the Verlog standard has one major problem:
In the first cycle of the circuit execution, there is no past value and the \code{past} function always returns X.
Thus the user has to take care to keep track of how many cycles have passed since the verification started and only enable assertions
once all past values are valid.
This particular pitfall is often the topic of a \href{http://zipcpu.com/quiz/2019/11/16/quiz07.html}{popular formal verification quiz}.
%In addition to that, \scalainline{past} values can also be invalid because they happened while
%the circuit was going through reset.
We made use of some of the unique capabilities offered by Chisel in order to implement what we consider to be a safer version of the
\code{past} function.
In the frontend, our \code{past} is a Scala function which creates an appropriate amount of delay registers in the current
clock and reset domain. That alone provides functionality similar to the Verilog version of \code{past}.
We go further by annotating the delay register and asking for a FIRRTL pass to be run when lowering the design.
This pass looks at a graph of all \code{past} delay registers and assertions in a module.
An edge indicates that the input to the assertion or register is connected to the output of a delay register through combinatorial logic.
We traverse the resulting tree (by design there can be no cycles) starting at each assertion
to find the longest path of \code{past} delay registers in order to determine the number of cycles the assertion needs to be delayed.
Finally we generate a cycle counter register and use its value to guard the individual assertions.
Since our \code{past} function only relies on synthesizable hardware it can also be used in software and FPGA based
simulation testing~\cite{karandikar2018firesim}.
Figure~\ref{fig:past} shows how the temporal assertion from Figure~\ref{fig:quiz15} results in a circuit with two registers created by the \code{past} function:
One to delay the condition from the \code{when} statement and the other to delay the input data before it is compared to the current output data.
By default an assertion is only enabled when reset is inactive and the surrounding \code{when} condition is true.
Our compiler pass analyzes the connectivity graph with the result that both the enable condition as well as the predicate are delayed by a single past register.
Thus the assertion enable signal is automatically extended to include the condition that at least 1 cycle must have past since the last reset.
The new enable condition is derived from a synthesizable, saturating \code{cycle} counter which is created by the compiler pass.
\section{Evaluation}
\label{sec:eval}
In order to evaluate the different verification tools provided by ChiselVerify, we will be using three different example circuits, namely an accumulator ALU from the Leros processor~\cite{leros:arcs2019}, an arbiter circuit, and a priority queue provided by Microchip~\cite{microchip}.
These are tested using ChiselTest test benches augmented with our verification tools.
The test benches are then compared to similar tests written using SystemVerilog with UVM, and \texttt{cocotb}, using the generated Verilog descriptions as a DUT.
We have selected UVM since it is an industry standard that can be used to verify Verilog designs, and \texttt{cocotb} since it enables similar verification functionalities in a high-level environment like what we propose with ChiselVerify.
The results are then compared in terms of the verbosity of the verification code.
As a final evaluation, we look at the overhead induced by the use of ChiselVerify-specific functionalities, such as functional coverage and constrained random verification, by comparing the runtime of a bare ChiselTest test bench, to the same one enhanced with our verification tools.
\subsection{Evaluation Circuits}
We evaluate ChiselVerify on three distinct circuits, which we describe in the following paragraphs.
\subsubsection{Leros Accumulator ALU}
The first circuit we will verify with our solution is an accumulator ALU from the Leros processor.
It supports operations such as \texttt{add}, \texttt{load}, \texttt{shiftRight} and logic operations.
In order to have a complete test suite, we need to try all available operations and use all potential input value corner cases.
We will thus model our test bench to do so.