/
TutorialSuite.scala
442 lines (386 loc) · 17.6 KB
/
TutorialSuite.scala
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
//See LICENSE for license details.
package firesim.midasexamples
import java.io.File
import scala.util.matching.Regex
import scala.io.Source
import org.scalatest.Suites
import org.scalatest.matchers.should._
abstract class TutorialSuite(
val targetName: String, // See GeneratorUtils
targetConfigs: String = "NoConfig",
platformConfigs: String = "HostDebugFeatures_DefaultF1Config",
tracelen: Int = 8,
simulationArgs: Seq[String] = Seq()
) extends firesim.TestSuiteCommon with Matchers {
val backendSimulator = "verilator"
val targetTuple = s"$targetName-$targetConfigs-$platformConfigs"
val commonMakeArgs = Seq(s"TARGET_PROJECT=midasexamples",
s"DESIGN=$targetName",
s"TARGET_CONFIG=${targetConfigs}",
s"PLATFORM_CONFIG=${platformConfigs}")
def run(backend: String,
debug: Boolean = false,
logFile: Option[File] = None,
waveform: Option[File] = None,
args: Seq[String] = Nil) = {
val makeArgs = Seq(
s"run-$backend%s".format(if (debug) "-debug" else ""),
"LOGFILE=%s".format(logFile map toStr getOrElse ""),
"WAVEFORM=%s".format(waveform map toStr getOrElse ""),
"ARGS=%s".format(args mkString " "))
if (isCmdAvailable(backend)) {
make(makeArgs:_*)
} else 0
}
/**
* Runs MIDAS-level simulation on the design.
*
* @param b Backend simulator: "verilator" or "vcs"
* @param debug When true, captures waves from the simulation
* @param args A seq of PlusArgs to pass to the simulator.
* @param shouldPass When false, asserts the test returns a non-zero code
*/
def runTest(b: String, debug: Boolean = false, args: Seq[String] = simulationArgs, shouldPass: Boolean = true) {
val prefix = if (shouldPass) "pass in " else "fail in "
val testEnvStr = s"${b} MIDAS-level simulation"
val wavesStr = if (debug) " with waves enabled" else ""
val argStr = " with args: " + args.mkString(" ")
val haveThisBehavior = prefix + testEnvStr + wavesStr + argStr
if (isCmdAvailable(b)) {
it should haveThisBehavior in {
assert((run(b, debug, args = args) == 0) == shouldPass)
}
} else {
ignore should haveThisBehavior in { }
}
}
// Checks that a bridge generated log in ${genDir}/${synthLog} matches output
// generated directly by the RTL simulator (usually with printfs)
def diffSynthesizedLog(synthLog: String,
stdoutPrefix: String = "SYNTHESIZED_PRINT ",
synthPrefix: String = "SYNTHESIZED_PRINT ",
synthLinesToDrop: Int = 0) {
behavior of s"${synthLog}"
it should "match the prints generated by the verilated design" in {
val verilatedLogFile = new File(outDir, s"/${targetName}.${backendSimulator}.out")
val synthLogFile = new File(genDir, s"/${synthLog}")
val verilatedOutput = extractLines(verilatedLogFile, stdoutPrefix).sorted
val synthPrintOutput = extractLines(synthLogFile, synthPrefix, synthLinesToDrop).sorted
diffLines(verilatedOutput, synthPrintOutput)
}
}
// Checks that a bridge generated log in ${genDir}/${synthLog} is empty
def assertSynthesizedLogEmpty(synthLog: String) {
s"${synthLog}" should "be empty" in {
val synthLogFile = new File(genDir, s"/${synthLog}")
val lines = extractLines(synthLogFile, prefix = "")
assert(lines.isEmpty)
}
}
def expectedFMR(expectedValue: Double, error: Double = 0.0) {
it should s"run with an FMR between ${expectedValue - error} and ${expectedValue + error}" in {
val verilatedLogFile = new File(outDir, s"/${targetName}.${backendSimulator}.out")
val lines = Source.fromFile(verilatedLogFile).getLines.toList.reverse
val fmrRegex = raw"^FMR: (\d*\.\d*)".r
val fmr = lines.collectFirst {
case fmrRegex(value) => value.toDouble
}
assert(fmr.nonEmpty, "FMR value not found.")
assert(fmr.get >= expectedValue - error)
assert(fmr.get <= expectedValue + error)
}
}
// Check that we are extracting from the desired ROI by checking that the
// bridge-inserted cycle prefix matches the target-side cycle prefix
def checkPrintCycles(filename: String, startCycle: Int, endCycle: Int, linesPerCycle: Int) {
it should "have synthesized printfs in the desired cycles" in {
val synthLogFile = new File(genDir, s"/${filename}")
val synthPrintOutput = extractLines(synthLogFile, prefix = "")
val length = synthPrintOutput.size
assert(length == linesPerCycle * (endCycle - startCycle + 1))
for ((line, idx) <- synthPrintOutput.zipWithIndex) {
val currentCycle = idx / linesPerCycle + startCycle
val printRegex = raw"^CYCLE:\s*(\d*) SYNTHESIZED_PRINT CYCLE:\s*(\d*).*".r
line match {
case printRegex(cycleA, cycleB) =>
assert(cycleA.toInt == currentCycle)
assert(cycleB.toInt == currentCycle)
}
}
}
}
/**
* Compares an AutoCounter output CSV against a reference generated using in-circuit printfs.
*/
def checkAutoCounterCSV(filename: String, stdoutPrefix: String) {
it should s"produce a csv file (${filename}) that matches in-circuit printf output" in {
val scrubWhitespace = raw"\s*(.*)\s*".r
def splitAtCommas(s: String) = {
s.split(",")
.map(scrubWhitespace.findFirstMatchIn(_).get.group(1))
}
def quotedSplitAtCommas(s: String) = {
s.split("\",\"")
.map(scrubWhitespace.findFirstMatchIn(_).get.group(1))
}
val refLogFile = new File(outDir, s"/${targetName}.${backendSimulator}.out")
val acFile = new File(genDir, s"/${filename}")
val refVersion ::refClockInfo :: refLabelLine :: refDescLine :: refOutput =
extractLines(refLogFile, stdoutPrefix, headerLines = 0).toList
val acVersion ::acClockInfo :: acLabelLine :: acDescLine :: acOutput =
extractLines(acFile, prefix = "" , headerLines = 0).toList
assert(acVersion == refVersion)
val refLabels = splitAtCommas(refLabelLine)
val acLabels = splitAtCommas(acLabelLine)
acLabels should contain theSameElementsAs refLabels
val swizzle: Seq[Int] = refLabels.map { acLabels.indexOf(_) }
def checkLine(acLine: String, refLine: String, tokenizer: String => Seq[String] = splitAtCommas) {
val Seq(acFields, refFields) = Seq(acLine, refLine).map(tokenizer)
val assertMessagePrefix = s"Row commencing with ${refFields.head}:"
assert(acFields.size == refFields.size, s"${assertMessagePrefix} lengths do not match")
for ((field, columnIdx) <- refFields.zipWithIndex) {
assert(field == acFields(swizzle(columnIdx)),
s"${assertMessagePrefix} value for label ${refLabels(columnIdx)} does not match."
)
}
}
for ((acLine, refLine) <- acOutput.zip(refOutput)) {
checkLine(acLine, refLine)
}
}
}
mkdirs()
behavior of s"$targetName"
elaborateAndCompile()
compileMlSimulator(backendSimulator)
runTest(backendSimulator)
}
//class PointerChaserF1Test extends TutorialSuite(
// "PointerChaser", "PointerChaserConfig", simulationArgs = Seq("`cat runtime.conf`"))
class GCDF1Test extends TutorialSuite("GCD")
// Hijack Parity to test all of the Midas-level backends
class ParityF1Test extends TutorialSuite("Parity") {
runTest("verilator", true)
runTest("vcs", true)
}
class ShiftRegisterF1Test extends TutorialSuite("ShiftRegister")
class ResetShiftRegisterF1Test extends TutorialSuite("ResetShiftRegister")
class EnableShiftRegisterF1Test extends TutorialSuite("EnableShiftRegister")
class StackF1Test extends TutorialSuite("Stack")
class RiscF1Test extends TutorialSuite("Risc")
class RiscSRAMF1Test extends TutorialSuite("RiscSRAM")
class AccumulatorF1Test extends TutorialSuite("Accumulator")
class VerilogAccumulatorF1Test extends TutorialSuite("VerilogAccumulator")
class AssertModuleF1Test extends TutorialSuite("AssertModule")
class AutoCounterModuleF1Test extends TutorialSuite("AutoCounterModule",
simulationArgs = Seq("+autocounter-readrate=1000", "+autocounter-filename-base=autocounter")) {
checkAutoCounterCSV("autocounter0.csv", "AUTOCOUNTER_PRINT ")
}
class AutoCounter32bRolloverTest extends TutorialSuite("AutoCounter32bRollover",
simulationArgs = Seq("+autocounter-readrate=1000", "+autocounter-filename-base=autocounter")) {
checkAutoCounterCSV("autocounter0.csv", "AUTOCOUNTER_PRINT ")
}
class AutoCounterCoverModuleF1Test extends TutorialSuite("AutoCounterCoverModule",
simulationArgs = Seq("+autocounter-readrate=1000", "+autocounter-filename-base=autocounter")) {
checkAutoCounterCSV("autocounter0.csv", "AUTOCOUNTER_PRINT ")
}
class AutoCounterPrintfF1Test extends TutorialSuite("AutoCounterPrintfModule",
simulationArgs = Seq("+print-file=synthprinttest.out"),
platformConfigs = "AutoCounterPrintf_HostDebugFeatures_DefaultF1Config") {
diffSynthesizedLog("synthprinttest.out0", stdoutPrefix = "AUTOCOUNTER_PRINT CYCLE", synthPrefix = "CYCLE")
}
class AutoCounterGlobalResetConditionF1Test extends TutorialSuite("AutoCounterGlobalResetCondition",
simulationArgs = Seq("+autocounter-readrate=1000", "+autocounter-filename-base=autocounter")) {
def assertCountsAreZero(filename: String, clockDivision: Int) {
s"Counts reported in ${filename}" should "always be zero" in {
val log = new File(genDir, s"/${filename}")
val versionLine :: lines = extractLines(log, "", headerLines = 0).toList
val sampleLines = lines.drop(AutoCounterVerificationConstants.headerLines - 1)
assert(versionLine.split(",")(1).toInt == AutoCounterVerificationConstants.expectedCSVVersion)
val perfCounterRegex = raw"(\d*),(\d*),(\d*)".r
sampleLines.zipWithIndex foreach {
case (perfCounterRegex(baseCycle,localCycle,value), idx) =>
assert(baseCycle.toInt == 1000 * (idx + 1))
assert(localCycle.toInt == (1000 / clockDivision) * (idx + 1))
assert(value.toInt == 0)
}
}
}
assertCountsAreZero("autocounter0.csv", clockDivision = 1)
assertCountsAreZero("autocounter1.csv", clockDivision = 2)
}
class PrintfModuleF1Test extends TutorialSuite("PrintfModule",
simulationArgs = Seq("+print-no-cycle-prefix", "+print-file=synthprinttest.out")) {
diffSynthesizedLog("synthprinttest.out0")
}
class NarrowPrintfModuleF1Test extends TutorialSuite("NarrowPrintfModule",
simulationArgs = Seq("+print-no-cycle-prefix", "+print-file=synthprinttest.out")) {
diffSynthesizedLog("synthprinttest.out0")
}
class PrintfGlobalResetConditionTest extends TutorialSuite("PrintfGlobalResetCondition",
simulationArgs = Seq("+print-no-cycle-prefix", "+print-file=synthprinttest.out")) {
// The log should be empty.
assertSynthesizedLogEmpty("synthprinttest.out0")
assertSynthesizedLogEmpty("synthprinttest.out1")
}
class PrintfCycleBoundsTestBase(startCycle: Int, endCycle: Int) extends TutorialSuite(
"PrintfModule",
simulationArgs = Seq(
"+print-file=synthprinttest.out",
s"+print-start=${startCycle}",
s"+print-end=${endCycle}"
)) {
checkPrintCycles("synthprinttest.out0", startCycle, endCycle, linesPerCycle = 5)
}
class PrintfCycleBoundsF1Test extends PrintfCycleBoundsTestBase(startCycle = 172, endCycle = 9377)
class TriggerPredicatedPrintfF1Test extends TutorialSuite("TriggerPredicatedPrintf",
simulationArgs = Seq("+print-file=synthprinttest.out")) with TriggerPredicatedPrintfConsts {
val startCycle = assertTriggerCycle + 2
val endCycle = deassertTriggerCycle + 2
checkPrintCycles("synthprinttest.out0", startCycle, endCycle, linesPerCycle = 2)
}
class WireInterconnectF1Test extends TutorialSuite("WireInterconnect")
class TrivialMulticlockF1Test extends TutorialSuite("TrivialMulticlock") {
runTest("verilator", true)
runTest("vcs", true)
}
class TriggerWiringModuleF1Test extends TutorialSuite("TriggerWiringModule")
class MulticlockAssertF1Test extends TutorialSuite("MulticlockAssertModule")
class AssertTortureTest extends TutorialSuite("AssertTorture") with AssertTortureConstants {
def checkClockDomainAssertionOrder(clockIdx: Int): Unit = {
it should s"capture asserts in the same order as the reference printfs in clock domain $clockIdx" in {
val verilatedLogFile = new File(outDir, s"/${targetName}.verilator.out")
// Diff parts of the simulation's stdout against itself, as the synthesized
// assertion messages are dumped to the same file as printfs in the RTL
val expected = extractLines(verilatedLogFile, prefix = s"${printfPrefix}${clockPrefix(clockIdx)}")
val actual = extractLines(verilatedLogFile, prefix = s"Assertion failed: ${clockPrefix(clockIdx)}")
diffLines(expected, actual)
}
}
// TODO: Create a target-parameters instance we can inspect here
Seq.tabulate(4)(i => checkClockDomainAssertionOrder(i))
}
class AssertGlobalResetConditionF1Test extends TutorialSuite("AssertGlobalResetCondition")
class MulticlockPrintF1Test extends TutorialSuite("MulticlockPrintfModule",
simulationArgs = Seq("+print-file=synthprinttest.out",
"+print-no-cycle-prefix")) {
diffSynthesizedLog("synthprinttest.out0")
diffSynthesizedLog("synthprinttest.out1",
stdoutPrefix = "SYNTHESIZED_PRINT_HALFRATE ",
synthPrefix = "SYNTHESIZED_PRINT_HALFRATE ",
// Corresponds to a single cycle of extra output.
synthLinesToDrop = 5)
}
class MulticlockAutoCounterF1Test extends TutorialSuite("MulticlockAutoCounterModule",
simulationArgs = Seq("+autocounter-readrate=1000", "+autocounter-filename-base=autocounter")) {
checkAutoCounterCSV("autocounter0.csv", "AUTOCOUNTER_PRINT ")
checkAutoCounterCSV("autocounter1.csv", "AUTOCOUNTER_PRINT_SLOWCLOCK ")
}
// Basic test for deduplicated extracted models
class TwoAddersF1Test extends TutorialSuite("TwoAdders")
class RegfileF1Test extends TutorialSuite("Regfile")
class MultiRegfileF1Test extends TutorialSuite("MultiRegfile")
class MultiRegfileFMRF1Test extends TutorialSuite("MultiRegfileFMR") {
// A threaded model that relies on another model to implement an internal
// combinational path (like an extracted memory model) will only simulate
// one target thread-cycle every two host cycles.
expectedFMR(2.0 * MultiRegfile.nCopiesToTime)
}
class MultiSRAMF1Test extends TutorialSuite("MultiSRAM")
class MultiSRAMFMRF1Test extends TutorialSuite("MultiSRAMFMR") {
expectedFMR(MultiRegfile.nCopiesToTime) // No comb paths -> 1:1
}
class NestedModelsF1Test extends TutorialSuite("NestedModels")
class MultiRegF1Test extends TutorialSuite("MultiReg")
class PassthroughModelTest extends TutorialSuite("PassthroughModel") {
expectedFMR(2.0)
}
class PassthroughModelNestedTest extends TutorialSuite("PassthroughModelNested") {
expectedFMR(2.0)
}
class PassthroughModelBridgeSourceTest extends TutorialSuite("PassthroughModelBridgeSource") {
expectedFMR(1.0)
}
class ResetPulseBridgeActiveHighTest extends TutorialSuite(
"ResetPulseBridgeTest",
// Disable assertion synthesis to rely on native chisel assertions to catch bad behavior
platformConfigs = "NoSynthAsserts_HostDebugFeatures_DefaultF1Config",
simulationArgs = Seq(s"+reset-pulse-length0=${ResetPulseBridgeTestConsts.maxPulseLength}")) {
runTest(backendSimulator,
args = Seq(s"+reset-pulse-length0=${ResetPulseBridgeTestConsts.maxPulseLength + 1}"),
shouldPass = false)
}
class ResetPulseBridgeActiveLowTest extends TutorialSuite(
"ResetPulseBridgeTest",
targetConfigs = "ResetPulseBridgeActiveLowConfig",
platformConfigs = "NoSynthAsserts_HostDebugFeatures_DefaultF1Config",
simulationArgs = Seq(s"+reset-pulse-length0=${ResetPulseBridgeTestConsts.maxPulseLength}")) {
runTest(backendSimulator,
args = Seq(s"+reset-pulse-length0=${ResetPulseBridgeTestConsts.maxPulseLength + 1}"),
shouldPass = false)
}
class TerminationF1Test extends TutorialSuite("TerminationModule") {
1 to 10 foreach {x => runTest(backendSimulator, args = Seq("+termination-bridge-tick-rate=10", s"+seed=${x}"), shouldPass = true)}
}
// Suite Collections
class ChiselExampleDesigns extends Suites(
new GCDF1Test,
new ParityF1Test,
new ResetShiftRegisterF1Test,
new EnableShiftRegisterF1Test,
new StackF1Test,
new RiscF1Test,
new RiscSRAMF1Test,
new AccumulatorF1Test,
new VerilogAccumulatorF1Test,
new TerminationF1Test
)
class PrintfSynthesisCITests extends Suites(
new PrintfModuleF1Test,
new NarrowPrintfModuleF1Test,
new MulticlockPrintF1Test,
new PrintfCycleBoundsF1Test,
new TriggerPredicatedPrintfF1Test,
new PrintfGlobalResetConditionTest,
)
class AssertionSynthesisCITests extends Suites(
new AssertModuleF1Test,
new MulticlockAssertF1Test,
new AssertTortureTest,
new AssertGlobalResetConditionF1Test,
)
class AutoCounterCITests extends Suites(
new AutoCounterModuleF1Test,
new AutoCounterCoverModuleF1Test,
new AutoCounterPrintfF1Test,
new MulticlockAutoCounterF1Test,
new AutoCounterGlobalResetConditionF1Test,
new AutoCounter32bRolloverTest,
)
class GoldenGateMiscCITests extends Suites(
new TwoAddersF1Test,
new TriggerWiringModuleF1Test,
new WireInterconnectF1Test,
new TrivialMulticlockF1Test,
new RegfileF1Test,
new MultiRegfileF1Test,
new MultiSRAMF1Test,
new NestedModelsF1Test,
new MultiRegF1Test
)
// These groups are vestigial from CircleCI container limits
class CIGroupA extends Suites(
new ChiselExampleDesigns,
new PrintfSynthesisCITests,
new firesim.fasedtests.CIGroupA,
new AutoCounterCITests,
new ResetPulseBridgeActiveHighTest,
new ResetPulseBridgeActiveLowTest,
)
class CIGroupB extends Suites(
new AssertionSynthesisCITests,
new GoldenGateMiscCITests,
new firesim.fasedtests.CIGroupB,
new firesim.AllMidasUnitTests,
new firesim.FailingUnitTests
)