Exercises — RISC-V extensions (M, A, F, D, V, C)
These graded problems test the RISC-V extensions (M, A, F, D, V, C) material. Each level goes deeper: L1 you just recognise the pieces, by L5 you build something. Every problem has a full worked solution folded up — try it first, then unfold.
Before we start, one reminder of notation used everywhere below:
Prerequisites if any line feels unfamiliar: 5.1.07-RISC-V-base-ISA, 5.2.03-IEEE-754-floating-point, 3.3.06-Instruction-encoding, 6.4.02-Cache-coherence-protocols.
Level 1 — Recognition
Recall Solution L1.1
Read the shape of each mnemonic:
MULHU— a multiply → M extension.sc.w— store-conditional, an atomic → A extension.FADD.S— the.Ssuffix means single-precision → F extension.FLD— load a double (.Dfamily, theDinFLD) → D extension.amoadd.w— atomic memory operation → A extension.DIVU— unsigned divide → M extension.
The tell: MUL/DIV/REM ⇒ M. LR/SC/AMO ⇒ A. .S ⇒ F. .D ⇒ D.
Recall Solution L1.2
- Sign = 1 bit, exponent = 8 bits, mantissa = 23 bits. Total ✓.
- Bias .
Level 2 — Application
Recall Solution L2.1
0x00010000 .
Product (a 64-bit value).
MULtakes the lower 32 bits[31:0]0x00000000.MULHUtakes the upper 32 bits[63:32]0x00000001.
This is why two instructions exist: the 32-bit result register cannot hold a 64-bit product, so the answer is split.
Recall Solution L2.2
Write the hex in binary, 4 bits per digit:
0100 0000 0100 1001 0000 1111 1101 1011.
- Sign = bit 31 =
0→ positive, . - Exponent = next 8 bits
1000 0000. Real exponent . - Mantissa = last 23 bits
100 1001 0000 1111 1101 1011. Its fractional value is - Value .
That is — 0x40490FDB is the standard single-precision encoding of .
Level 3 — Analysis
Recall Solution L3.1
Same bits, two interpretations.
Signed (MULH): 0xFFFFFFFF = , 0x00000002 = .
Product . In 64-bit two's complement .
Upper 32 bits [63:32] = 0xFFFFFFFF (all sign bits).
Unsigned (MULHU): 0xFFFFFFFF = , 0x00000002 = .
Product .
Upper 32 bits = 0x00000001.
Why different: the bits multiplied are identical, but signed math sign-extends 0xFFFFFFFF into a small negative number, while unsigned math treats it as a huge positive number. The low 32 bits happen to match (0xFFFFFFFE), but the high halves diverge completely.
Recall Solution L3.2
lr.w t0, (a0)loads the counter and places a reservation on that address for our core.- The other core writes to the same cache line. The coherence protocol (see 6.4.02-Cache-coherence-protocols) sends an invalidation → our reservation is cleared.
sc.w t2, t1, (a0)checks the reservation, finds it invalid, so it does not store and setst2 = 1(failure).bnez t2, retryseest2 ≠ 0and jumps back toretry, re-reading the now-updated value.
The guarantee: a store only lands if no one touched the address in between, so no increment is ever lost. Success ⇒ t2 = 0; failure ⇒ t2 = 1.
Level 4 — Synthesis
Recall Solution L4.1
retry:
lr.w t0, (a0) # t0 = current value, reserve address
bge t0, a1, done # if current >= a1, nothing to do
sc.w t2, a1, (a0) # try to store a1
bnez t2, retry # store failed? someone raced us -> retry
done:Reasoning step by step:
lr.wreads and reserves.bge t0, a1, done— if the existing value is already the larger one, we skip the store entirely (a store would only waste a reservation).- Otherwise
sc.wattempts the write; failure ⇒ retry with the fresh value.
Single instruction equivalent: amomax.w t0, a1, (a0) — atomically reads mem[a0], writes max(old, a1), returns the old value in t0. It is one hardware-locked operation instead of a 4-instruction loop, so it is faster and cannot livelock.
Recall Solution L4.2
- Unpack (see 5.2.03-IEEE-754-floating-point):
- → exp , mantissa
- → exp , mantissa
- Align to the larger exponent (): shift 's mantissa right by places: .
- Add mantissas: → value .
- Normalize: already has one bit before the point — done.
- Pack: sign , exp , mantissa (the
.11after the implicit 1). Bits:0 10000000 11000000000000000000000=0x40600000.
Check: decodes to ✓.
Level 5 — Mastery
Recall Solution L5.1
Single precision has a 23-bit mantissa plus 1 implicit bit = 24 significant bits.
- . The next representable single-precision number above it is , not , because at this magnitude the spacing between representable values (the ULP) is .
- Adding lands exactly halfway; round-to-nearest-even rounds down to .
- Result:
sumstays16777216.0— the+1vanished.
Double precision: 52+1 = 53 significant bits. At magnitude the ULP is , far below 1, so is represented exactly. This is exactly why summing millions of values uses a double accumulator.
Recall Solution L5.2
a = 0x80000000 signed . b = 3 unsigned.
True product .
In 64-bit two's complement: .
MUL= low 32 bits[31:0]=0x80000000.MULHSU= high 32 bits[63:32]=0xFFFFFFFE(signed×unsigned high half).
Reconstruct: . Read 0xFFFFFFFE as signed :
Exactly the true product. MULHSU is the right variant here because the first operand is signed and the second unsigned.
Recall Solution L5.3
- Integer scaling ×100 = multiply ⇒ need M.
- One shared counter touched by two concurrent tasks ⇒ atomic increment ⇒ need A.
- "No fractional math" ⇒ omit F and D (FPU is silicon/power you don't spend).
- Base integer ISA is always present: I.
ISA string: RV32IMA. You save the FPU area/energy — the whole point of RISC-V modularity: pay only for what the application uses. (C compressed could be added for code size, but the problem didn't require it.)
Recall Self-test summary
M splits a wide product into MUL (low) + MULH (high) — which variant? ::: The one matching operand signedness: MULH signed×signed, MULHU unsigned×unsigned, MULHSU signed×unsigned. Why does sc.w sometimes return 1? ::: Its reservation was invalidated by another core's write — this is normal contention, so you retry. When adding two floats, do you add the exponents? ::: No — you align to the common (larger) exponent by shifting the smaller mantissa; adding exponents is for multiply. Why did 2^24 + 1 stay 2^24 in single precision? ::: The ULP at that magnitude is 2, so +1 rounds to nearest-even back down; double has enough bits to represent it exactly.