RISC-V Assembly Language - Convert A String to A Float

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:

  1. 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.

  2. 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).

  3. 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.

  4. 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.