From 22f615459796284d34116f3a39199db9bdbd217b Mon Sep 17 00:00:00 2001 From: Desmond Kirkpatrick Date: Wed, 18 Sep 2024 17:34:43 -0700 Subject: [PATCH] Serialization (#92) --- doc/components/serialization.md | 11 + lib/rohd_hcl.dart | 1 + lib/src/serialization/deserializer.dart | 70 ++++ lib/src/serialization/serialization.dart | 5 + lib/src/serialization/serializer.dart | 91 +++++ test/serialization_test.dart | 439 +++++++++++++++++++++++ 6 files changed, 617 insertions(+) create mode 100644 doc/components/serialization.md create mode 100644 lib/src/serialization/deserializer.dart create mode 100644 lib/src/serialization/serialization.dart create mode 100644 lib/src/serialization/serializer.dart create mode 100644 test/serialization_test.dart diff --git a/doc/components/serialization.md b/doc/components/serialization.md new file mode 100644 index 000000000..91668a489 --- /dev/null +++ b/doc/components/serialization.md @@ -0,0 +1,11 @@ +# Serialization / Deserialization + +ROHD-HCL implements a `Serializer` and `Deserializer` set of components that enable converting wide structures to a serialized narrow stream of data and vice-versa. + +## Serializer + +The `Serializer` is a module that accepts a wider `LogicArray` `deserialized` for input data and optionally a `Logic` `readyIn` to allow for pausing serialization. While `readyIn` is high, the `Serializer` sequentially outputs chunks of data on the Logic `serialized` output until the entire `LogicArray` `deserialized` has been transferred to the output. At that point the `Serializer` raises Logic `done`. This process will continue until the Logic `readyIn` is lowered, allowing for back-to-back transfers of wide data over the Logic `serialized` stream. The number of serialization steps in the current transfer is available in Logic `count`. Lowering `readyIn` will pause the transfer and raising it will continue from where it paused. + +## Deserializer + +The `Deserializer` is a module that accepts a `Logic` `serialized` stream over a pre-defined `length` number of clocks. It outputs a `LogicArray` `deserialized` of the same length of Logic words that match the width of Logic `serialized`. Deserialization runs while Logic `enable` is high, and Logic `validOut` is emitted when deserialization is complete. This process will continue when Logic `enable` is high, allowing for back-to-back deserialization transfers of a narrow stream of data into wider (`length`) `LogicArray`s during `length` number of serialization steps (clocks while `enable` is high) for each transfer. The number of serialization steps in the current transfer is available in Logic `count`. Lowering `enable` will pause the transfer and raising it will continue from where it paused. diff --git a/lib/rohd_hcl.dart b/lib/rohd_hcl.dart index de909ffa7..efb817ba9 100644 --- a/lib/rohd_hcl.dart +++ b/lib/rohd_hcl.dart @@ -16,6 +16,7 @@ export 'src/interfaces/interfaces.dart'; export 'src/memory/memories.dart'; export 'src/models/models.dart'; export 'src/rotate.dart'; +export 'src/serialization/serialization.dart'; export 'src/shift_register.dart'; export 'src/sort.dart'; export 'src/summation/summation.dart'; diff --git a/lib/src/serialization/deserializer.dart b/lib/src/serialization/deserializer.dart new file mode 100644 index 000000000..7faa64208 --- /dev/null +++ b/lib/src/serialization/deserializer.dart @@ -0,0 +1,70 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// deserializer.dart +// A deserialization block, deserializing narrow input data onto a wide channel. +// +// 2024 August 27 +// Author: desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// [Deserializer] aggregates data from a serialized stream. +class Deserializer extends Module { + /// Aggregated data output. + LogicArray get deserialized => output('deserialized') as LogicArray; + + /// Length of aggregate to deserialize. + final int length; + + /// [done] emitted when the last element is committed to [deserialized]. + /// The timing is that you can latch [deserialized] when [done] is high. + Logic get done => output('done'); + + /// Return the current count of elements that have been serialized out. + Logic get count => output('count'); + + /// Build a Deserializer that takes serialized input [serialized] + /// and aggregates it into one wide output [deserialized] of length [length]. + /// + /// Updates one element per clock while [enable] (if connected) is high, + /// emitting [done] when completing the filling of wide output `LogicArray` + /// [deserialized]. + Deserializer(Logic serialized, this.length, + {required Logic clk, + required Logic reset, + Logic? enable, + super.name = 'deserializer'}) { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + if (enable != null) { + enable = addInput('enable', enable); + } + serialized = (serialized is LogicArray) + ? addInputArray('serialized', serialized, + dimensions: serialized.dimensions, + elementWidth: serialized.elementWidth) + : addInput('serialized', serialized, width: serialized.width); + final cnt = Counter.simple( + clk: clk, reset: reset, enable: enable, maxValue: length - 1); + addOutput('count', width: cnt.width) <= cnt.count; + addOutput('done') <= cnt.overflowed; + addOutputArray('deserialized', + dimensions: (serialized is LogicArray) + ? ([length, ...serialized.dimensions]) + : [length], + elementWidth: (serialized is LogicArray) + ? serialized.elementWidth + : serialized.width) + .elements + .forEachIndexed((i, d) => + d <= + flop( + clk, + reset: reset, + en: (enable ?? Const(1)) & count.eq(i), + serialized)); + } +} diff --git a/lib/src/serialization/serialization.dart b/lib/src/serialization/serialization.dart new file mode 100644 index 000000000..9e556c724 --- /dev/null +++ b/lib/src/serialization/serialization.dart @@ -0,0 +1,5 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +export 'deserializer.dart'; +export 'serializer.dart'; diff --git a/lib/src/serialization/serializer.dart b/lib/src/serialization/serializer.dart new file mode 100644 index 000000000..97f591746 --- /dev/null +++ b/lib/src/serialization/serializer.dart @@ -0,0 +1,91 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// serializer.dart +// A serialization block, serializing wide input data onto a narrower channel. +// +// 2024 August 27 +// Author: desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// Serializes wide aggregated data onto a narrower serialization stream. +class Serializer extends Module { + /// Serialized output, one data item per clock. + Logic get serialized => output('serialized'); + + /// Return [done] = true when we have processed `deserialized`completely. + /// [done] is asserted with the final element being serialized so that + /// at the next clock edge, you have [done] with the last element latched at + /// the same time. + Logic get done => output('done'); + + /// The number of current serialization steps completed in the + /// transfer is [count]. + Logic get count => output('count'); + + /// Build a Serializer that takes the array [deserialized] and sequences it + /// onto the [serialized] output. + /// + /// Delivers one element per clock while [enable] + /// is high (if connected). If [flopInput] is true, the + /// [Serializer] is configured to latch the input data and hold it until + /// [done] is asserted after the full `LogicArray` [deserialized] is + /// transferred. This will delay the serialized output by one cycle. + Serializer(LogicArray deserialized, + {required Logic clk, + required Logic reset, + Logic? enable, + bool flopInput = false, + super.name = 'serializer'}) { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + if (enable != null) { + enable = addInput('enable', enable); + } + deserialized = addInputArray('deserialized', deserialized, + dimensions: deserialized.dimensions, + elementWidth: deserialized.elementWidth); + + addOutput('count', width: log2Ceil(deserialized.dimensions[0])); + addOutput('done'); + + final reducedDimensions = List.from(deserialized.dimensions) + ..removeAt(0); + if (deserialized.dimensions.length > 1) { + addOutputArray('serialized', + dimensions: reducedDimensions, + elementWidth: deserialized.elementWidth); + } else { + addOutput('serialized', width: deserialized.elementWidth); + } + + final cnt = Counter.simple( + clk: clk, + reset: reset, + enable: enable, + maxValue: deserialized.elements.length - 1); + + final latchInput = (enable ?? Const(1)) & ~cnt.count.or(); + count <= + (flopInput + ? flop(clk, reset: reset, en: enable, cnt.count) + : cnt.count); + + final dataOutput = + LogicArray(deserialized.dimensions, deserialized.elementWidth); + for (var i = 0; i < deserialized.elements.length; i++) { + dataOutput.elements[i] <= + (flopInput + ? flop( + clk, reset: reset, en: latchInput, deserialized.elements[i]) + : deserialized.elements[i]); + } + serialized <= dataOutput.elements.selectIndex(count); + done <= + (flopInput + ? flop(clk, reset: reset, en: enable, cnt.equalsMax) + : cnt.equalsMax); + } +} diff --git a/test/serialization_test.dart b/test/serialization_test.dart new file mode 100644 index 000000000..16ac89dd5 --- /dev/null +++ b/test/serialization_test.dart @@ -0,0 +1,439 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// serialization_test.dart +// Tests for serializer and deserializer components +// +// 2024 August 27 +// Author: desmond Kirkpatrick +// +// ignore_for_file: invalid_use_of_protected_member + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // This test pauses the serializer during processing at each count level + // to ensure that it completes correctly (monitoring the count and done) + test('serializer', () async { + const len = 10; + const width = 8; + final dataIn = LogicArray([len], width); + final clk = SimpleClockGenerator(10).clk; + final start = Logic(); + final reset = Logic(); + final mod = Serializer(dataIn, clk: clk, reset: reset, enable: start); + + await mod.build(); + unawaited(Simulator.run()); + + start.inject(0); + reset.inject(0); + var clkCount = 0; + for (var i = 0; i < len; i++) { + dataIn.elements[i].inject(i); + } + await clk.nextPosedge; + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + start.inject(1); + var predictedClk = 0; + while (mod.done.value.toInt() != 1) { + await clk.nextPosedge; + predictedClk = (clkCount + 1) % len; + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + clkCount++; + } + clkCount = 0; + predictedClk = 0; + while ((clkCount == 0) | (mod.done.value.toInt() != 1)) { + await clk.nextPosedge; + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + predictedClk = (clkCount + 1) % len; + clkCount++; + } + await clk.nextPosedge; + + var counting = true; + start.inject(0); + for (var disablePos = 0; disablePos < len; disablePos++) { + clkCount = 0; + predictedClk = 0; + var activeClkCount = 0; + while (mod.done.value.toInt() == 0) { + if (clkCount == disablePos) { + counting = false; + start.inject(0); + } else { + start.inject(1); + } + await clk.nextPosedge; + predictedClk = (counting ? activeClkCount + 1 : activeClkCount) % len; + activeClkCount = counting ? activeClkCount + 1 : activeClkCount; + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + clkCount = clkCount + 1; + start.inject(1); + counting = true; + } + await clk.nextPosedge; + } + await Simulator.endSimulation(); + }); + + test('serializer for larger structures', () async { + const len = 10; + const width = 8; + final dataIn = LogicArray([len, 2], width); + final clk = SimpleClockGenerator(10).clk; + final start = Logic(); + final reset = Logic(); + final mod = Serializer(dataIn, clk: clk, reset: reset, enable: start); + + await mod.build(); + unawaited(Simulator.run()); + + start.inject(0); + reset.inject(0); + var clkCount = 0; + for (var i = 0; i < len; i++) { + for (var j = 0; j < 2; j++) { + dataIn.elements[i].elements[j].inject(i * 2 + j); + } + } + await clk.nextPosedge; + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + start.inject(1); + final val = mod.serialized as LogicArray; + var predictedClk = 0; + while (mod.done.value.toInt() != 1) { + expect(val.elements[0].value.toInt(), equals(predictedClk * 2)); + expect(val.elements[1].value.toInt(), equals(predictedClk * 2 + 1)); + await clk.nextPosedge; + predictedClk = (clkCount + 1) % len; + expect(mod.count.value.toInt(), equals(predictedClk)); + clkCount++; + } + expect(val.elements[0].value.toInt(), equals(predictedClk * 2)); + expect(val.elements[1].value.toInt(), equals(predictedClk * 2 + 1)); + await Simulator.endSimulation(); + }); + + test('serializer to deserializer for larger structures', () async { + const len = 10; + const width = 8; + final dataIn = LogicArray([len, 2], width); + final clk = SimpleClockGenerator(10).clk; + final start = Logic(); + final reset = Logic(); + final mod = Serializer(dataIn, clk: clk, reset: reset, enable: start); + + final mod2 = Deserializer(mod.serialized, len, + clk: clk, reset: reset, enable: start); + + await mod.build(); + await mod2.build(); + unawaited(Simulator.run()); + + WaveDumper(mod2); + + start.inject(0); + reset.inject(0); + for (var i = 0; i < len; i++) { + for (var j = 0; j < 2; j++) { + dataIn.elements[i].elements[j].inject(i * 2 + j); + } + } + await clk.nextPosedge; + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + start.inject(1); + while (mod2.done.value.toInt() != 1) { + await clk.nextPosedge; + } + await clk.nextPosedge; + + final dataOut = mod2.deserialized; + for (var i = 0; i < len; i++) { + for (var j = 0; j < 2; j++) { + expect(dataOut.elements[i].elements[j].value, + equals(dataIn.elements[i].elements[j].value)); + } + } + await Simulator.endSimulation(); + }); + + // This test does a careful check of the data transfer sequence to make sure + // data transfer is in expected order by sequencing in first a set of 1s and + // then a set of zeros and checking all transfers. + test('deserializer rollover', () async { + const len = 6; + const width = 4; + final dataIn = Logic(width: width); + final clk = SimpleClockGenerator(10).clk; + final enable = Logic(); + final reset = Logic(); + final mod = + Deserializer(dataIn, len, clk: clk, reset: reset, enable: enable); + + await mod.build(); + unawaited(Simulator.run()); + + enable.inject(0); + reset.inject(0); + await clk.nextPosedge; + reset.inject(1); + + var clkCount = 0; + await clk.nextPosedge; + reset.inject(0); + dataIn.inject(255); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + enable.inject(1); + await clk.nextPosedge; + clkCount++; + var value = BigInt.from(15); + final mask = ~(value << (width * len)); + expect(mod.count.value.toInt(), equals(clkCount)); + expect(mod.deserialized.value.toBigInt(), equals(value)); + var nxtValue = BigInt.zero; + for (var i = 0; i < len * 2 - 2; i++) { + if (i < len - 1) { + dataIn.inject(15); + nxtValue = (value << width) | value; + if (i == len - 2) { + clkCount = -1; + } + } else { + dataIn.inject(0); + nxtValue = (nxtValue << width) & mask; + } + await clk.nextPosedge; + clkCount++; + expect(mod.count.value.toInt(), equals(clkCount)); + expect(mod.deserialized.value.toBigInt(), equals(nxtValue)); + value = nxtValue; + } + + await Simulator.endSimulation(); + }); + + // This test uses enable to pause the deserialization once at each count level + // to ensure it completes and fires done at the right time. + test('deserializer enable', () async { + const len = 6; + const width = 4; + final dataIn = Logic(width: width); + final clk = SimpleClockGenerator(10).clk; + final enable = Logic(); + final reset = Logic(); + final mod = + Deserializer(dataIn, len, clk: clk, reset: reset, enable: enable); + await mod.build(); + unawaited(Simulator.run()); + + enable.inject(0); + reset.inject(0); + await clk.nextPosedge; + reset.inject(1); + + await clk.nextPosedge; + reset.inject(0); + dataIn.inject(15); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + var value = BigInt.from(15); + final mask = ~(value << (width * len)); + enable.inject(1); + var clkCount = 0; + var nxtValue = value; + while ((clkCount == 0) | (mod.done.value.toInt() == 0)) { + await clk.nextPosedge; + final predictedClk = (clkCount + 1) % len; + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.deserialized.value.toBigInt(), equals(nxtValue)); + nxtValue = (value << width) | value; + value = nxtValue; + clkCount = clkCount + 1; + } + clkCount = 0; + dataIn.inject(0); + while ((clkCount == 0) | (mod.done.value.toInt() == 0)) { + await clk.nextPosedge; + final predictedClk = (clkCount + 1) % len; + nxtValue = (nxtValue << width) & mask; + + expect(mod.count.value.toInt(), equals(predictedClk)); + clkCount = clkCount + 1; + value = nxtValue; + } + enable.inject(0); + for (var disablePos = 0; disablePos < len; disablePos++) { + clkCount = 0; + var activeClkCount = 0; + dataIn.inject(0); + while ((clkCount == 0) | (mod.done.value.toInt() == 0)) { + if (clkCount == disablePos) { + enable.inject(0); + } + expect(mod.count.value.toInt(), equals(activeClkCount)); + await clk.nextPosedge; + if (clkCount != disablePos) { + activeClkCount = (activeClkCount + 1) % len; + } + clkCount = clkCount + 1; + enable.inject(1); + } + } + await clk.nextPosedge; + await clk.nextPosedge; + await Simulator.endSimulation(); + }); + + // This test ensures that the clock cycles and counts of the serializer + // line up when doing back-to-back transfers. + test('serializer timing', () async { + const len = 10; + const width = 8; + final dataIn = LogicArray([len], width); + final clk = SimpleClockGenerator(10).clk; + final start = Logic(); + final reset = Logic(); + unawaited(Simulator.run()); + for (final testFlopped in [false, true]) { + final mod = Serializer(dataIn, + clk: clk, reset: reset, enable: start, flopInput: testFlopped); + + await mod.build(); + + start.put(0); + reset.put(0); + var clkCount = testFlopped ? 0 : 0; + for (var i = 0; i < len; i++) { + dataIn.elements[i].put(i); + } + await clk.nextPosedge; + + reset.put(1); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + start.put(1); + var predictedClk = 0; + if (testFlopped) { + await clk.nextPosedge; + } + while (mod.done.value.toInt() != 1) { + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + await clk.nextPosedge; + predictedClk = (clkCount + 1) % len; + clkCount++; + } + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + clkCount = 0; + predictedClk = 0; + await clk.nextPosedge; + + while ((clkCount == 0) | (mod.done.value.toInt() != 1)) { + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + predictedClk = (clkCount + 1) % len; + await clk.nextPosedge; + clkCount++; + } + expect(mod.count.value.toInt(), equals(predictedClk)); + expect(mod.serialized.value.toInt(), equals(predictedClk)); + } + + await clk.nextPosedge; + await clk.nextPosedge; + await Simulator.endSimulation(); + }); + + test('deserializer to serializer to deserializer', () async { + const len = 6; + const width = 4; + final dataIn = Logic(width: width); + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final deserializer = Deserializer(dataIn, len, clk: clk, reset: reset); + + await deserializer.build(); + final firstDeserializerDone = deserializer.done | + flop(clk, Const(1), reset: reset, en: deserializer.done); + final serializer = Serializer(deserializer.deserialized, + clk: clk, enable: firstDeserializerDone, reset: reset); + + await serializer.build(); + final serializerStart = firstDeserializerDone; + final deserializer2 = Deserializer(serializer.serialized, len, + clk: clk, reset: reset, enable: serializerStart); + + await deserializer2.build(); + unawaited(Simulator.run()); + + reset.put(0); + await clk.nextPosedge; + reset.put(1); + await clk.nextPosedge; + await clk.nextPosedge; + + var clkCount = 0; + await clk.nextPosedge; + reset.put(0); + while (deserializer.done.value != LogicValue.one) { + expect(deserializer.count.value.toInt(), equals(clkCount)); + dataIn.put(clkCount++); + await clk.nextPosedge; + } + dataIn.put(clkCount); + for (var i = 0; i < len; i++) { + expect(deserializer.deserialized.elements[i].value.toInt(), equals(i)); + } + while (deserializer2.done.value != LogicValue.one) { + if (serializerStart.value == LogicValue.one) { + expect(serializer.count.value.toInt(), + equals(serializer.serialized.value.toInt())); + } + await clk.nextPosedge; + clkCount++; + } + for (var i = 0; i < len; i++) { + expect(deserializer2.deserialized.elements[i].value.toInt(), equals(i)); + } + await clk.nextPosedge; + + await clk.nextPosedge; + await Simulator.endSimulation(); + }); +}