diff --git a/doc/components/floating_point.md b/doc/components/floating_point.md index c1631b366..eaa4bb026 100644 --- a/doc/components/floating_point.md +++ b/doc/components/floating_point.md @@ -51,7 +51,7 @@ Other special widths of floating-point values supported are: - [FloatingPointBF16Value](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointBF16Value-class.html) - [FloatingPointTF32Value](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointTF32Value-class.html) -Finally, we have a [random value constructor](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointValue/FloatingPointValue.random.html) generator for testing purposes, generating valid [FloatingPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointValue-class.html) types, optionally constrained to normal range (mantissa in $[1, 2)$). +Finally, we have a [random value constructor](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointValuePopulator/random.html) generator for testing purposes, generating valid [FloatingPointValue](https://intel.github.io/rohd-hcl/rohd_hcl/FloatingPointValue-class.html) types, optionally constrained to normal range (mantissa in $[1, 2)$). ### Populators diff --git a/doc/components/memory.md b/doc/components/memory.md index abe88456c..cf8c33438 100644 --- a/doc/components/memory.md +++ b/doc/components/memory.md @@ -21,3 +21,5 @@ The read path is combinational, so data is provided immediately according to the ## Memory Models The `MemoryModel` has the same interface as a `Memory`, but is non-synthesizable and uses a software-based `SparseMemoryStorage` as a backing for data storage. This is a useful tool for testing systems that have relatively large memories. + +The `MemoryStorage` class also provides utilities for reading (`loadMemString`) and writing (`dumpMemString`) verilog-compliant memory files (e.g. for `readmemh`). diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index ba7b52546..88fabeb3c 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -14,4 +14,7 @@ class RohdHclException implements Exception { /// Creates an [Exception] for the ROHD Hardware Component Library. RohdHclException(this.message); + + @override + String toString() => 'RohdHclException($message)'; } diff --git a/lib/src/models/memory_model.dart b/lib/src/models/memory_model.dart index 6491bca0f..54c919e0d 100644 --- a/lib/src/models/memory_model.dart +++ b/lib/src/models/memory_model.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2024 Intel Corporation +// Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // memory_model.dart @@ -130,7 +130,9 @@ class MemoryModel extends Memory { /// Updates read data for [rdPort] after [readLatency] time. Future _updateRead(DataPortInterface rdPort, LogicValue data) async { - await clk.waitCycles(readLatency - 1); + if (readLatency > 1) { + await clk.waitCycles(readLatency - 1); + } rdPort.data.inject(data); } } diff --git a/lib/src/models/sparse_memory_storage.dart b/lib/src/models/sparse_memory_storage.dart index 21bf3d8db..d0ad70415 100644 --- a/lib/src/models/sparse_memory_storage.dart +++ b/lib/src/models/sparse_memory_storage.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sparse_memory_storage.dart @@ -6,8 +6,10 @@ // // 2023 June 12 +import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/src/exceptions.dart'; +import 'package:rohd_hcl/src/utils.dart'; /// A storage for memory models. abstract class MemoryStorage { @@ -69,7 +71,16 @@ abstract class MemoryStorage { onInvalidRead = onInvalidRead ?? _defaultOnInvalidRead, alignAddress = alignAddress ?? _defaultAlignAddress; - /// Reads a verilog-compliant hex file and preloads memory with it. + /// Reads a verilog-compliant mem file and preloads memory with it. + /// + /// The loaded address will increment each [dataWidth] bits of data between + /// address `@` annotations. Data is parsed according to the provided + /// [radix], which must be a positive power of 2 up to 16. The address for + /// data will increment by 1 for each [bitsPerAddress] bits of data, which + /// must be a power of 2 less than [dataWidth]. + /// + /// Line comments (`//`) are supported and any whitespace is supported as a + /// separator between data. Block comments (`/* */`) are not supported. /// /// Example input format: /// ``` @@ -81,79 +92,167 @@ abstract class MemoryStorage { /// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /// ``` - void loadMemHex(String hexMemContents) { - /// The number of bytes per cacheline. - const lineBytes = 4; + void loadMemString(String memContents, + {int radix = 16, int bitsPerAddress = 8}) { + if (radix <= 0 || (radix & (radix - 1) != 0) || radix > 16) { + throw RohdHclException('Radix must be a positive power of 2 (max 16).'); + } + + if (dataWidth % bitsPerAddress != 0) { + throw RohdHclException('dataWidth must be a multiple of bitsPerAddress.'); + } + + final bitsPerChar = log2Ceil(radix); + final charsPerLine = dataWidth ~/ bitsPerChar; + final addrIncrPerLine = dataWidth ~/ bitsPerAddress; var address = 0; - var bytes = []; + final chunks = []; + var chunksLength = 0; + + void addChunk([String? chunk]) { + if (chunk != null) { + // ignore: parameter_assignments + chunk = chunk.trim(); + + chunks.add(chunk); + chunksLength += chunk.length; + } + + while (chunksLength >= charsPerLine) { + final pendingData = chunks.reversed.join(); + final cutPoint = chunksLength - charsPerLine; + final thisData = pendingData.substring(cutPoint); + chunks.clear(); + + final remaining = pendingData.substring(0, cutPoint); + + chunksLength = 0; + + if (remaining.isNotEmpty) { + chunks.add(remaining); + chunksLength = remaining.length; + } - void addByte(String byte) { - bytes.add(byte); - if (bytes.length == lineBytes) { final lvData = LogicValue.ofBigInt( - BigInt.parse(bytes.reversed.join(), radix: 16), lineBytes * 8); - final addr = LogicValue.ofInt(address - address % lineBytes, addrWidth); + BigInt.parse(thisData, radix: radix), dataWidth); + final addr = + LogicValue.ofInt(address - (address % addrIncrPerLine), addrWidth); setData(addr, lvData); - bytes = []; + + address += addrIncrPerLine; } } - List reconstruct(LogicValue data) { - final out = []; - for (var counter = 0; counter < lineBytes; counter++) { - out.add(data - .getRange(counter * 8, (counter + 1) * 8) - .toInt() - .toRadixString(16) - .padLeft(2, '0')); + void padToAlignment() { + addChunk(); + while (chunksLength != 0) { + addChunk('0'); } - - return out; } - for (var line in hexMemContents.split('\n')) { + for (var line in memContents.split('\n')) { + // if there's a `//` comment on this line, ditch everything after it + final commentIdx = line.indexOf('//'); + if (commentIdx != -1) { + line = line.substring(0, commentIdx); + } + line = line.trim(); + + if (line.isEmpty) { + continue; + } + if (line.startsWith('@')) { // pad out remaining bytes as 0 - // add that many to address - if (bytes.isNotEmpty) { - final thres = lineBytes - bytes.length; - for (var i = 0; i < thres; i++) { - addByte('00'); - address++; - } + padToAlignment(); + + // if it doesn't match the format, throw + if (!RegExp('@([0-9a-fA-F]+)').hasMatch(line)) { + throw RohdHclException('Invalid address format: $line'); } // check to see if this block already exists in memory final lineAddr = int.parse(line.substring(1), radix: 16); final lineAddrLv = - LogicValue.ofInt(lineAddr - lineAddr % lineBytes, addrWidth); + LogicValue.ofInt(lineAddr - lineAddr % addrIncrPerLine, addrWidth); if (getData(lineAddrLv) != null) { // must reconstruct the bytes array ending at the provided address - final endOff = lineAddr % lineBytes; + final endOff = lineAddr % addrIncrPerLine; final origData = getData(lineAddrLv); - final newData = reconstruct(origData!).sublist(0, endOff); - bytes = newData; + chunks + ..clear() + ..add(origData! + .getRange(0, endOff * bitsPerAddress) + .toInt() + .toRadixString(radix)); } - address = lineAddr; + address = lineAddrLv.toInt(); } else { - for (final byte in line.split(RegExp(r'\s'))) { - addByte(byte); - address++; - } + line.split(RegExp(r'\s')).forEach(addChunk); } } // pad out remaining bytes as 0 - // add that many to address - final thres = lineBytes - bytes.length; - if (bytes.isNotEmpty) { - for (var i = 0; i < thres; i++) { - addByte('00'); - address++; + padToAlignment(); + } + + /// Dumps the contents of memory to a verilog-compliant hex file. + /// + /// The address will increment each [bitsPerAddress] bits of data between + /// address `@` annotations. Data is output according to the provided [radix], + /// which must be a positive power of 2 up to 16. + String dumpMemString({int radix = 16, int bitsPerAddress = 8}) { + if (radix <= 0 || (radix & (radix - 1) != 0) || radix > 16) { + throw RohdHclException('Radix must be a positive power of 2 (max 16).'); + } + + if (isEmpty) { + return ''; + } + + final bitsPerChar = log2Ceil(radix); + + final addrs = addresses.sorted(); + + final memString = StringBuffer(); + + LogicValue? currentAddr; + + for (final addr in addrs) { + if (currentAddr != addr) { + memString.writeln('@${addr.toInt().toRadixString(16)}'); + currentAddr = addr; } + + final data = getData(addr)!; + memString.writeln(data + .toInt() + .toRadixString(radix) + .padLeft(data.width ~/ bitsPerChar, '0')); + currentAddr = currentAddr! + + LogicValue.ofInt(dataWidth ~/ bitsPerAddress, addrWidth); } + + return memString.toString(); + } + + /// Reads a verilog-compliant hex file and preloads memory with it. + /// + /// Example input format: + /// ``` + /// @80000000 + /// B3 02 00 00 33 05 00 00 B3 05 00 00 13 05 F5 1F + /// 6F 00 40 00 93 02 10 00 17 03 00 00 13 03 83 02 + /// 23 20 53 00 6F 00 00 00 + /// @80000040 + /// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + /// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + /// ``` + @Deprecated('Use `loadMemString` instead.') + void loadMemHex(String hexMemContents) { + loadMemString(hexMemContents); } /// Resets all memory to initial state. @@ -195,6 +294,9 @@ abstract class MemoryStorage { /// Returns true if there is no data stored in this memory. bool get isEmpty; + + /// A list of [addresses] which have data stored in this memory. + List get addresses; } /// A sparse storage for memory models. @@ -217,15 +319,32 @@ class SparseMemoryStorage extends MemoryStorage { throw RohdHclException('Can only write to valid addresses.'); } + if (addr.width != addrWidth) { + throw RohdHclException('Address width must be $addrWidth.'); + } + + if (data.width != dataWidth) { + throw RohdHclException('Data width must be $dataWidth.'); + } + _memory[addr] = data; } @override - LogicValue? getData(LogicValue addr) => _memory[addr]; + LogicValue? getData(LogicValue addr) { + if (addr.width != addrWidth) { + throw RohdHclException('Address width must be $addrWidth.'); + } + + return _memory[addr]; + } @override void reset() => _memory.clear(); @override bool get isEmpty => _memory.isEmpty; + + @override + List get addresses => _memory.keys.toList(); } diff --git a/test/sparse_memory_storage_test.dart b/test/sparse_memory_storage_test.dart index b4d2147c3..3cde4da38 100644 --- a/test/sparse_memory_storage_test.dart +++ b/test/sparse_memory_storage_test.dart @@ -14,12 +14,128 @@ import 'package:rohd_hcl/src/models/sparse_memory_storage.dart'; import 'package:test/test.dart'; void main() { - test('sparse memory storage can load a simple file', () { - final hex = File('test/example1.hex').readAsStringSync(); - final storage = SparseMemoryStorage(addrWidth: 32, dataWidth: 32) - ..loadMemHex(hex); + group('sparse memory storage', () { + test('can load a simple file (legacy)', () { + final hex = File('test/example1.hex').readAsStringSync(); + final storage = SparseMemoryStorage(addrWidth: 32, dataWidth: 32) + // ignore: deprecated_member_use_from_same_package + ..loadMemHex(hex); - expect(storage.getData(LogicValue.ofInt(0x8000000c, 32))!.toInt(), - equals(0x1ff50513)); + expect(storage.getData(LogicValue.ofInt(0x8000000c, 32))!.toInt(), + equals(0x1ff50513)); + }); + + test('can load, dump, and reload a simple file', () { + final hex = File('test/example1.hex').readAsStringSync(); + final storage = SparseMemoryStorage(addrWidth: 32, dataWidth: 32) + ..loadMemString(hex); + + expect(storage.getData(LogicValue.ofInt(0x8000000c, 32))!.toInt(), + equals(0x1ff50513)); + + final dumped = storage.dumpMemString(); + + final storage2 = SparseMemoryStorage(addrWidth: 32, dataWidth: 32) + ..loadMemString(dumped); + + expect(storage2.getData(LogicValue.ofInt(0x8000000c, 32))!.toInt(), + equals(0x1ff50513)); + }); + + test('store 32-bit 1-per-addr data', () { + final storage = SparseMemoryStorage(addrWidth: 8, dataWidth: 32); + for (var i = 0; i < 10; i++) { + storage.setData( + LogicValue.ofInt(i, 8), LogicValue.ofInt(i, 4).replicate(8)); + } + + final memStr = storage.dumpMemString(bitsPerAddress: 32); + + expect(memStr, ''' +@0 +00000000 +11111111 +22222222 +33333333 +44444444 +55555555 +66666666 +77777777 +88888888 +99999999 +'''); + }); + + test('binary radix read and write', () { + const data = ''' +@2 +00000000 +11111111 +01010101 +1010 1111 +'''; + + final storage = SparseMemoryStorage(addrWidth: 8, dataWidth: 8) + ..loadMemString(data, radix: 2); + + final memStr = storage.dumpMemString(radix: 2); + + const expected = ''' +@2 +00000000 +11111111 +01010101 +11111010 +'''; + + expect(memStr, expected); + }); + + test('comments and whitespace and out of order work properly', () { + const data = ''' + +@100 +00000000 +//11111111 +2222 //2222 + +33333333 // some comment +4444//4444 +@80 +5555 4321 + +// @100 +65432100 +77777 777 + + +8 8888887 + 9876 +54 3 +2 + + +'''; + + final storage = SparseMemoryStorage(addrWidth: 12, dataWidth: 32) + ..loadMemString(data, bitsPerAddress: 32); + + final memStr = storage.dumpMemString(bitsPerAddress: 32); + + const expected = ''' +@80 +43215555 +65432100 +77777777 +88888878 +23549876 +@100 +00000000 +33332222 +44443333 +'''; + + expect(memStr, expected); + }); }); }