RISC-V SBI Wrappers: Preserve RV32 Argument Pairs and Separate Error from Value
A supervisor's timer wrapper works on RV64 but schedules an unexpected deadline on RV32. The source passes a 64-bit value, yet the SBI call receives only its low word or sees the halves reversed. A correct C prototype alone does not establish the register arrangement at an inline-assembly boundary.
Review the SBI binary encoding and the specific extension together. The return path has a similar trap: the error and value registers are separate outputs, not interchangeable success indicators.
Bind extension, function and arguments explicitly
The ratified SBI v3.0 binary encoding uses a7 for the extension ID and a6 for the function ID in extensions defined from v0.2 onward. Arguments occupy a0 through a5, and the call crosses the privilege interface with ECALL.
The callee preserves registers other than a0 and a1. Those two registers form the result: error in a0, value in a1. A wrapper's compiler constraints must describe the consumed inputs and produced outputs; a stale C variable is not an architectural return value.
Do not mix this convention with the legacy extension interface merely because both use ECALL. Identify the actual EID/FID and the associated argument definition before inspecting the assembly.
Split wide arguments according to XLEN
The binary encoding places a 2×XLEN argument in consecutive registers, with its low half in the lower-numbered register and high half in the next one. For RV32, a 64-bit first argument therefore uses a0 for bits 31:0 and a1 for bits 63:32.
The TIME extension takes an absolute 64-bit timer value. An illustrative value 0x0000000200000030 must reach RV32 as a0=0x00000030 and a1=0x00000002. On RV64 the whole value fits in a0.
A test using only small deadlines can miss a dropped high word. Include a nonzero high half in an offline register-marshalling fixture, and compare the reconstructed 64-bit value without programming a real timer.

Verify the consuming implementation as well as the wrapper
In OpenSBI v1.9's TIME handler, the RV32 branch reconstructs the deadline as the high a1 word shifted by 32 and combined with a0. The RV64 branch consumes a0 directly.
That exact boundary is useful when reviewing a downstream wrapper: inspect the emitted registers immediately before ECALL and the firmware's expected reconstruction. A memory dump of the C argument's bytes does not establish that the right values reached those registers.
Also preserve the absolute-time meaning. Passing a duration rather than the intended absolute deadline is a separate bug that a correctly split argument will transmit faithfully.
Check error before interpreting value
For modern SBI calls, nonzero sbiret.error means the value in a1 is unspecified unless that function explicitly defines otherwise. Do not use a nonzero value as a generic success indication, and do not retain a previous successful value after a failed call.
For functions returning status or identifiers, branch on error first and decode value only under the documented success contract. TIME's set-timer function specifies success in its error result but does not define a returned deadline in the value field. Reading a1 back as confirmation of the programmed deadline is therefore invalid.
The same physical registers have changed roles across the call: on RV32, a1 carried the input's high half and then became an output register. The wrapper must capture both return registers instead of assuming an input survives there.
A proposed audit uses distinct bit patterns in each input half and controlled success/failure result pairs. It verifies the wrapper's complete register contract before any target timing test. No ECALL or timer programming was performed here.