Introduction
Converting a string to a floating-point number (equivalent to C's
atof or strtod) in RISC-V assembly requires parsing the string
into its constituent parts (integer part, fractional part, and
optional exponent), calculating the numeric value using integer
math, and then converting the final integer values into a float
using the RISC-V floating-point extension (F or D).
Below is a complete, modular, and optimized RISC-V assembly
function (string_to_float) that parses a standard decimal
float string (e.g., "-123.456") and returns a single-precision
32-bit float.
Code
Note: Convert this code to a function with a prologue, body,
and epilogue.
.global string_to_float
# ----------------------------------------------------------------------
# Function: string_to_float
# Arguments:
# a0 = const char *str (Pointer to null-terminated ASCII string)
# Returns:
# fa0 = float (The converted single-precision floating-point value)
# ----------------------------------------------------------------------
string_to_float:
# --- Register Usage ---
# a0: string pointer tracking current char
# t0: current character byte
# t1: integer accumulator (whole or fractional part)
# t2: boolean flag for negative sign (0 = positive, 1 = negative)
# t3: divisor tracking power of 10 for the fractional part
# t4: constant 10 for multiplication/division
# fa0: final floating-point result
# fa1: temporary float for calculation
li t1, 0 # Initialize integer accumulator = 0
li t2, 0 # Default sign is positive (0)
li t4, 10 # Constant 10 for parsing digits
# 1. Parse Sign
lbu t0, 0(a0) # Load first character
li t3, 45 # ASCII '-' is 45
bne t0, t3, check_plus
li t2, 1 # Mark as negative
addi a0, a0, 1 # Move to next character
j parse_integer
check_plus:
li t3, 43 # ASCII '+' is 43
bne t0, t3, parse_integer
addi a0, a0, 1 # Move past '+'
parse_integer:
# 2. Parse Integer (Whole) Part
lbu t0, 0(a0) # Get next character
beqz t0, done_parsing # If null-terminator, finish
li t3, 46 # ASCII '.' is 46
beq t0, t3, setup_fraction
# Verify if character is a valid digit '0'-'9'
li t3, 48 # ASCII '0'
blt t0, t3, done_parsing
li t3, 57 # ASCII '9'
bgt t0, t3, done_parsing
# Accumulate integer: t1 = (t1 * 10) + (t0 - '0')
mul t1, t1, t4
addi t0, t0, -48
add t1, t1, t0
addi a0, a0, 1 # Advance string pointer
j parse_integer
setup_fraction:
addi a0, a0, 1 # Skip the decimal point '.'
# Convert the parsed whole integer part to float immediately
fcvt.s.w fa0, t1 # fa0 = (float) integer_part
li t1, 0 # Reset accumulator for fractional digits
li t3, 1 # t3 will track the denominator (10^n)
parse_fraction:
# 3. Parse Fractional Part
lbu t0, 0(a0)
beqz t0, compute_fraction
# Verify if digit
li t5, 48
blt t0, t5, compute_fraction
li t5, 57
bgt t0, t5, compute_fraction
# Accumulate fractional digits as an integer: t1 = (t1 * 10) + digit
mul t1, t1, t4
addi t0, t0, -48
add t1, t1, t0
# Scale up denominator: t3 = t3 * 10
mul t3, t3, t4
addi a0, a0, 1
j parse_fraction
compute_fraction:
# Convert fractional accumulator and scale factor to float
fcvt.s.w fa1, t1 # fa1 = (float) fractional_digits_integer
fcvt.s.w ft0, t3 # ft0 = (float) scale_denominator
# Calculate fractional value: fa1 = fa1 / ft0
fdiv.s fa1, fa1, ft0 # Perform IEEE 754 float division
# Combine parts: fa0 = integer_part + fractional_part
fadd.s fa0, fa0, fa1
j apply_sign
done_parsing:
# Fallback for strings without a decimal point (pure integers)
fcvt.s.w fa0, t1
apply_sign:
# 4. Handle Negative Sign if flag is set
beqz t2, exit_func
fneg.s fa0, fa0 # Invert float sign bit if t2 == 1
exit_func:
ret # Return with value in fa0
Algorithm & Architectural Breakdown
The logic mirrors a hardware-efficient implementation of standard
algorithmic steps:
- Sign Logic Handling: The routine scans the leading byte for - or +.
A binary flag register (t2) stores whether the value requires negation
at completion using the fneg.s (Floating-Point Negate Single) instruction.
- Double-Pass Integer Accumulation: The whole number component is
computed inside integer registers using scalar scaling
(t1 = t1 * 10 + digit) to prevent premature precision loss.
When the decimal pointer (.) is hit, the current integer value
is cast straight into the fa0 floating-point register via
fcvt.s.w (Floating-point Convert to Single from Word).
- Fraction Scale Division: Fractional numbers are accumulated as a
pure integer while tracking their depth factor in t3 (e.g., 456 with
depth 1000). Both integers are pushed to the F-extension register
subsystem via fcvt.s.w and divided using fdiv.s to correctly position
the radix.
Calling Conventions: In compliance with the official standard
RISC-V Calling Conventions, the string memory pointer argument is
fetched from a0, and the returning value resolves strictly within fa0.