Skip to content

Commit d4d3e97

Browse files
authored
Fixes sendBits()'s handling of signed numbers in svsim (#4599)
1 parent a597006 commit d4d3e97

File tree

4 files changed

+224
-15
lines changed

4 files changed

+224
-15
lines changed

svsim/src/main/resources/simulation-driver.cpp

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -215,41 +215,51 @@ static void sendReady() { writeMessage(MESSAGE_READY, "ready"); }
215215
static void sendAck() { writeMessage(MESSAGE_ACK, "ack"); }
216216

217217
// This method may modify the bytes in the buffer
218+
// Scala's BigInt.toString(16) follows the format of
219+
// `s"${if(value<0) "-" else ""}${value.abs.toString(16)}"`
218220
static void sendBits(uint8_t *mutableBytes, int bitCount, bool isSigned) {
219221
if (bitCount <= 0) {
220222
failWithError("Cannot send 0-bit value.");
221223
}
222-
if (isSigned && bitCount <= 1) {
223-
failWithError("Cannot send 1-bit signed value.");
224-
}
225224
writeMessageStart(MESSAGE_BITS);
226225
writeMessageBody("%08X ", bitCount) int byteCount = (bitCount + 7) / 8;
227226
if (isSigned) {
228227
uint8_t signBitMask;
228+
// A very dirty way to sign-extend the most significant byte
229+
// when the signed number is negative
230+
uint8_t lastByteNegativePad;
229231
switch (bitCount % 8) {
230232
case 1:
231233
signBitMask = 0b00000001;
234+
lastByteNegativePad = 0b11111110;
232235
break;
233236
case 2:
234237
signBitMask = 0b00000010;
238+
lastByteNegativePad = 0b11111100;
235239
break;
236240
case 3:
237241
signBitMask = 0b00000100;
242+
lastByteNegativePad = 0b11111000;
238243
break;
239244
case 4:
240245
signBitMask = 0b00001000;
246+
lastByteNegativePad = 0b11110000;
241247
break;
242248
case 5:
243249
signBitMask = 0b00010000;
250+
lastByteNegativePad = 0b11100000;
244251
break;
245252
case 6:
246253
signBitMask = 0b00100000;
254+
lastByteNegativePad = 0b11000000;
247255
break;
248256
case 7:
249257
signBitMask = 0b01000000;
258+
lastByteNegativePad = 0b10000000;
250259
break;
251260
case 0:
252261
signBitMask = 0b10000000;
262+
lastByteNegativePad = 0b00000000;
253263
break;
254264
}
255265
if (mutableBytes[byteCount - 1] & signBitMask) {
@@ -258,13 +268,17 @@ static void sendBits(uint8_t *mutableBytes, int bitCount, bool isSigned) {
258268
int carry = 1;
259269
for (int i = 0; i < byteCount; i++) {
260270
int byte = mutableBytes[i];
271+
// We need to sign-extend the last byte
272+
if (i == byteCount - 1) {
273+
byte = mutableBytes[i] | lastByteNegativePad;
274+
}
261275
byte = (~byte & 0xFF) + carry;
262276
carry = byte >> 8;
263277
mutableBytes[i] = (uint8_t)byte;
264278
}
279+
// Since we sign-extended the number,
280+
// two's compliment just works, no need to strip bits (#4593)
265281
}
266-
// Strip irrelevant bits
267-
mutableBytes[byteCount - 1] &= signBitMask - 1;
268282
}
269283
for (int i = byteCount - 1; i >= 0; i--) {
270284
writeMessageBody("%02X", mutableBytes[i]);
@@ -410,6 +424,9 @@ int scanHexByteReverse(const char **reverseScanCursor,
410424
}
411425

412426
/**
427+
* Scala's BigInt.toString(16) follows the format of
428+
* `s"${if(value<0) "-" else ""}${value.abs.toString(16)}"`
429+
*
413430
* Returned value must be manually freed.
414431
*/
415432
static uint8_t *scanHexBits(const char **scanCursor, const char *scanEnd,
@@ -423,21 +440,18 @@ static uint8_t *scanHexBits(const char **scanCursor, const char *scanEnd,
423440
}
424441

425442
bool isNegative;
426-
int valueBitCount;
427443
if (**scanCursor == '-') {
428444
(*scanCursor)++;
429445
if (reverseScanCursor < *scanCursor) {
430446
failWithError("Unexpected end of negative value when %s.", description);
431447
}
432448
isNegative = true;
433-
if (bitCount <= 1) {
434-
failWithError("Cannot scan 1-bit-wide negative value when %s.",
449+
if (bitCount < 1) {
450+
failWithError("Cannot scan 0-bit-wide negative value when %s.",
435451
description);
436452
}
437-
valueBitCount = bitCount - 1;
438453
} else {
439454
isNegative = false;
440-
valueBitCount = bitCount;
441455
}
442456

443457
int byteCount = (bitCount + 7) / 8;
@@ -447,7 +461,7 @@ static uint8_t *scanHexBits(const char **scanCursor, const char *scanEnd,
447461
const char *firstCharacterOfValue = *scanCursor;
448462
int carry = 1; // Only used when `isNegative` is true
449463
int scannedByteCount = 0;
450-
int valueByteCount = (valueBitCount + 7) / 8;
464+
int valueByteCount = (bitCount + 7) / 8;
451465
while (scannedByteCount < valueByteCount) {
452466
int scannedByte = scanHexByteReverse(&reverseScanCursor,
453467
firstCharacterOfValue, description);
@@ -473,11 +487,10 @@ static uint8_t *scanHexBits(const char **scanCursor, const char *scanEnd,
473487
// A mask of the "inapplicable" bits in the high order byte, used to determine
474488
// if we received too many bits for the value we are trying to scan. This
475489
// value could be calculated with bitwise operations, but I find a table to be
476-
// cleaner and easier to understand. We use `valueBitCount` instead of
477-
// `bitCount` because the sign bit should be `1` for negative numbers along
478-
// with all of the other leading bits.
490+
// cleaner and easier to understand. There's no sign bit in Scala's `BigInt.toString(16)`,
491+
// instead a minus sign will be present.
479492
uint8_t highOrderByteMask;
480-
switch (valueBitCount % 8) {
493+
switch (bitCount % 8) {
481494
case 1:
482495
highOrderByteMask = 0b11111110;
483496
break;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
module SIntWire #(parameter WIDTH = 32)
4+
(
5+
input [WIDTH-1:0] in,
6+
output [WIDTH-1:0] out
7+
);
8+
assign out = in;
9+
endmodule
10+
11+
// We are testing low-level APIs, no support for parameterized modules
12+
module SIntTest
13+
(
14+
input [7:0] in_8,
15+
output [7:0] out_8,
16+
input [30:0] in_31,
17+
output [30:0] out_31,
18+
input [31:0] in_32,
19+
output [31:0] out_32,
20+
input [32:0] in_33,
21+
output [32:0] out_33,
22+
output [7:0] out_const_8,
23+
output [30:0] out_const_31,
24+
output [31:0] out_const_32,
25+
output [32:0] out_const_33
26+
);
27+
28+
SIntWire #(8) wire_8(.in(in_8), .out(out_8));
29+
SIntWire #(31) wire_31(.in(in_31), .out(out_31));
30+
SIntWire #(32) wire_32(.in(in_32), .out(out_32));
31+
SIntWire #(33) wire_33(.in(in_33), .out(out_33));
32+
assign out_const_8 = 8'h80;
33+
assign out_const_31 = 31'b1000000000000000000000000000000;
34+
assign out_const_32 = 32'b10000000000000000000000000000000;
35+
assign out_const_33 = 33'b100000000000000000000000000000000;
36+
37+
endmodule

svsim/src/test/scala/BackendSpec.scala

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,95 @@ trait BackendSpec extends AnyFunSpec with Matchers {
183183
val traceReader = new BufferedReader(new FileReader(s"${simulation.workingDirectoryPath}/trace.vcd"))
184184
traceReader.lines().count() must be > 1L
185185
}
186+
187+
// Check sendBits()
188+
it("communicates data to the Scala side correctly (#4593)") {
189+
import Resources._
190+
workspace.reset()
191+
workspace.elaborateSIntTest()
192+
workspace.generateAdditionalSources()
193+
simulation = workspace.compile(
194+
backend
195+
)(
196+
workingDirectoryTag = name,
197+
commonSettings = CommonCompilationSettings(),
198+
backendSpecificSettings = compilationSettings,
199+
customSimulationWorkingDirectory = None,
200+
verbose = false
201+
)
202+
simulation.run(
203+
verbose = false,
204+
executionScriptLimit = None
205+
) { controller =>
206+
val bitWidths: Seq[Int] = List(8, 31, 32, 33)
207+
val outConstPorts = bitWidths.map(b => controller.port(s"out_const_${b}"))
208+
209+
controller.setTraceEnabled(true)
210+
211+
for (idxBitWidth <- 0 until bitWidths.length) {
212+
val bitWidth = bitWidths(idxBitWidth)
213+
val outConst = outConstPorts(idxBitWidth)
214+
val outConstVal = BigInt(-1) << (bitWidth - 1)
215+
var isOutConstChecked: Boolean = false
216+
outConst.check(isSigned = true) { value =>
217+
isOutConstChecked = true
218+
assert(value.asBigInt === outConstVal)
219+
}
220+
assert(isOutConstChecked === false)
221+
controller.completeInFlightCommands()
222+
assert(isOutConstChecked === true)
223+
}
224+
}
225+
}
226+
227+
// Check both scanHexBits() and sendBits()
228+
it("communicates data from and to the Scala side correctly") {
229+
simulation.run(
230+
verbose = false,
231+
executionScriptLimit = None
232+
) { controller =>
233+
val bitWidths: Seq[Int] = List(8, 31, 32, 33)
234+
val inPorts = bitWidths.map(b => controller.port(s"in_${b}"))
235+
val outPorts = bitWidths.map(b => controller.port(s"out_${b}"))
236+
237+
controller.setTraceEnabled(true)
238+
239+
// Some values near bounds
240+
def boundValues(bitWidth: Int): Seq[BigInt] = {
241+
val minVal = BigInt(-1) << (bitWidth - 1)
242+
val maxVal = (BigInt(1) << (bitWidth - 1)) - 1
243+
val deltaRange = maxVal.min(BigInt(257))
244+
val valueNearZero = for { v <- -deltaRange to deltaRange } yield v
245+
val valueNearMax = for { delta <- BigInt(0) to deltaRange } yield maxVal - delta
246+
val valueNearMin = for { delta <- BigInt(0) to deltaRange } yield minVal + delta
247+
valueNearMin ++ valueNearZero ++ valueNearMax
248+
}
249+
250+
for (idxBitWidth <- 0 until bitWidths.length) {
251+
val bitWidth = bitWidths(idxBitWidth)
252+
val in = inPorts(idxBitWidth)
253+
val out = outPorts(idxBitWidth)
254+
255+
val inValues = boundValues(bitWidth)
256+
val outValues = inValues
257+
for ((inVal, outVal) <- inValues.zip(outValues)) {
258+
in.set(inVal)
259+
in.check(isSigned = true) { value =>
260+
assert(value.asBigInt === inVal)
261+
}
262+
var isOutChecked: Boolean = false
263+
out.check(isSigned = true) { value =>
264+
isOutChecked = true
265+
assert(value.asBigInt === inVal)
266+
}
267+
assert(isOutChecked === false)
268+
269+
controller.completeInFlightCommands()
270+
assert(isOutChecked === true)
271+
}
272+
}
273+
}
274+
}
186275
}
187276
}
188277
}

svsim/src/test/scala/Resources.scala

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,75 @@ object Resources {
4646
)
4747
)
4848
}
49+
def elaborateSIntTest(): Unit = {
50+
workspace.addPrimarySourceFromResource(getClass, "/SIntTest.sv")
51+
workspace.elaborate(
52+
ModuleInfo(
53+
name = "SIntTest",
54+
ports = Seq(
55+
new ModuleInfo.Port(
56+
name = "in_8",
57+
isSettable = true,
58+
isGettable = true
59+
),
60+
new ModuleInfo.Port(
61+
name = "in_31",
62+
isSettable = true,
63+
isGettable = true
64+
),
65+
new ModuleInfo.Port(
66+
name = "in_32",
67+
isSettable = true,
68+
isGettable = true
69+
),
70+
new ModuleInfo.Port(
71+
name = "in_33",
72+
isSettable = true,
73+
isGettable = true
74+
),
75+
new ModuleInfo.Port(
76+
name = "out_8",
77+
isSettable = false,
78+
isGettable = true
79+
),
80+
new ModuleInfo.Port(
81+
name = "out_31",
82+
isSettable = false,
83+
isGettable = true
84+
),
85+
new ModuleInfo.Port(
86+
name = "out_32",
87+
isSettable = false,
88+
isGettable = true
89+
),
90+
new ModuleInfo.Port(
91+
name = "out_33",
92+
isSettable = false,
93+
isGettable = true
94+
),
95+
new ModuleInfo.Port(
96+
name = "out_const_8",
97+
isSettable = false,
98+
isGettable = true
99+
),
100+
new ModuleInfo.Port(
101+
name = "out_const_31",
102+
isSettable = false,
103+
isGettable = true
104+
),
105+
new ModuleInfo.Port(
106+
name = "out_const_32",
107+
isSettable = false,
108+
isGettable = true
109+
),
110+
new ModuleInfo.Port(
111+
name = "out_const_33",
112+
isSettable = false,
113+
isGettable = true
114+
)
115+
)
116+
)
117+
)
118+
}
49119
}
50120
}

0 commit comments

Comments
 (0)