Up to this point we haven’t really mentioned floating point values or instructions
at all, except how to declare them in the .data
section and the syscalls for
reading and printing them. There are two reasons we’ve left them alone till now.
First, they use a whole separate set of registers and instructions. Second, and
partly because of the first reason, most MIPS college courses do not ever require
you to know or use floating point values. Since this book is targeted at college
students, if you know you won’t need to know this feel free to skip this chapter.
While the greensheet contains a nice table for the normal registers it is completely lacking for the floating point registers. There are 32 32-bit floating point registers. You can use them all for floats but they are paired even-odd for doubles. In other words, you can only use even numbers for doubles, because storing a double at $f0 actually uses $f0 and $f1 because it takes 64 bits/8 bytes.
As far as the calling conventions for floating point registers, it is actually hard to find anything definitive and clear even for the basics. You could make up your own but the float/double syscalls, and the tiny code snippet in Patterson and Hennessy were at least consistent with this old website[1] so we’ll go with that. I have seen at least one course page where the prof wanted all float registers preserved which seems excessive and ridiculous but prof’s are gonna prof.
Name | Use | Preserved Across a Call |
---|---|---|
$f0-$f2 |
Function Results |
No |
$f4-$f10 |
Temporaries |
No |
$f12-f14 |
Arguments |
No |
$f16-f18 |
Temporaries |
No |
$f20-f30 |
Saved Temporaries |
Yes |
This table is based on doubles so it may look like it’s skipping odd registers but
they’re included where the even they’re paired with is. So, for example you actually
have 4 registers for float arguments $f12
through $f15
but only 2 for doubles
$f12
and $f14
. Similarly you have 12 saved registers for floats but 6 for doubles.
You might find things like
this, which seems
to say that SPIM doesn’t support using the odd registers at all but both example
programs for this chapter use $f1
and work with both SPIM and MARS. Given that,
and the fact that it references instructions [l.d] and [l.s] which don’t work
(li.s
and li.d
do, see below), it’s probably really out of date.
Most of the next table is actually on the Greensheet but not all of it and I thought it worth reproducing here.
Name | Opcode | Format | Operation |
---|---|---|---|
Load Word to Coprocessor 1 |
lwc1 (or l.s) |
|
F[ft] = M[R[rs]+n] |
Store Word from Coprocessor 1 |
swc1 (or s.s) |
|
M[R[rs]+n] = F[ft] |
Load Double to Coprocessor 1 |
ldc1 (or l.d) |
|
F[ft] = M[R[rs]+n] F[ft+1] = M[R[rs]+n+4] |
Store Double from Coprocessor 1 |
sdc1 (or s.d) |
|
M[R[rs]+n] = F[ft] M[R[rs]+n+4] = F[ft+1] |
Move From Coprocessor 1 |
mfc1 |
|
R[rd] = F[fs] |
Move To Coprocessor 1 |
mtc1 |
|
F[fs] = R[rd] |
Convert Word To Single Precision |
cvt.s.w |
|
F[fd] = (float)F[fs] |
Convert Single Precision To Word |
cvt.w.s |
|
F[fd] = (int)F[fs] |
Convert Word To Double Precision |
cvt.d.w |
|
F[fd] = (double)F[fs] |
Convert Double Precision To Word |
cvt.w.d |
|
F[fd] = (int)F[fs] |
Branch on FP True |
bc1t |
|
if (FPcond) goto label; |
Branch on FP False |
bc1f |
|
if (!FPcond) goto label; |
FP Compare |
c.y.x |
|
FPcond = (F[fs] op F[ft]) ? 1 : 0 |
Absolute Value |
abs.x |
|
F[fs] = (F[ft] > 0) ? F[ft] : -F[ft] |
Add |
add.x |
|
F[fd] = F[fs] + F[ft] |
Subtract |
sub.x |
|
F[fd] = F[fs] - F[ft] |
Multiply |
mul.x |
|
F[fd] = F[fs] * F[ft] |
Divide |
div.x |
|
F[fd] = F[fs] / F[ft] |
Negation |
neg.x |
|
F[fs] = -F[ft] |
Move |
mov.x |
|
F[fd] = F[fs] |
With all of the opcodes that end in .x, the x is either s for single precision or d for double precision.
The y in the Compare instructions are one of eq, lt, le. Naturally op would be the matching ==, <, ⇐. Unfortunately, you don’t get not equal, greater than, or greater equal, even as pseudoinstructions, but it’s easy enough to flip the order of operands or branch on the opposite result.
We’re going to briefly go over some of the more different aspects of dealing with floating point numbers, but since most of it is the same but with a new set of registers and calling convention, we won’t be rehashing most concepts.
The first thing to know when dealing with floats is how to get float (or double) literals into registers where you can actually operate on them.
There are two ways. The first, and simpler way, is to declare them as globals
and then use the lwc1
or ldw1
instructions:
.data
a: .float 3.14159
b: .double 1.61
.text
main:
la $t0, a
lwc1 $f0, 0($t0) # get a into $f0
la $t0, b
ldc1 $f2, 0($t0) # get b into $f2-3
# other code here
The second way is to use the regular registers and convert the values. Of course this means unless you want an integer value, you’d have to actually do it twice and divide, and even that would limit you to rational numbers. It looks like this.
mtc1 $0, $f0 # move 0 to $f0 (0 integer == 0.0 float)
# get 4 to 4.0 in $f2
li $t0, 4
mtc1 $t0, $f2
cvt.s.w $f2, $f2 # convert 4 to 4.0
As you can see, other than 0 which is a special case, it requires at least 3 instructions, more than the 2 (or 1 if you load directly from the address) of the first method.
Note
|
There is a 3rd way that is even easier, but it’s only supported in SPIM. The
pseudoinstructions li.s and li.d work exactly like li except to load float
and double literals into float/double registers.
|
Branching based on floating point values is slightly different than normal. Instead of being able to test and jump in a single convenient instruction, you have to test first and then jump in a second instruction if the test was true or not. This is the same way x86 does it. The test sets a special control/flag register (or a certain bit or bits in the register) and then all jumps are based on its state.
Using it looks like this:
c.lt.s $f0, $f2 # fpcond = f0 < f2
bc1t was_less # if (f0 < f2) goto was_less
# do something for f0 >= f2
j blah
was_less:
# do something for f0 < f2
blah:
Finally, lets do a simple example of writing a function that takes a float and returns a float. I’m not going to bother doing one for doubles because it’d be effectively the same, or doing one that requires the stack, because the only differences from normal are a new set of registers and knowing which ones to save or not from the table above.
So, how about a function to convert a fahrenheit temperature to celsius:
.data
# 5/9 = 0.5 with 5 repeating
fahrenheit2celsius: .float 0.5555555
.text
# float convert_F2C(float degrees_f)
convert_F2C:
la $t0, fahrenheit2celsius
lwc1 $f0, 0($t0) # get conversion factor
# C = (F - 32) * 5/9
li $t0, 32
mtc1 $t0, $f1 # move int 32 to f1
cvt.s.w $f1, $f1 # convert to 32.0
sub.s $f12, $f12, $f1 # f12 = degrees - 32
mul.s $f0, $f0, $f12 # f0 = 0.555555 * f12
jr $ra
You can see we follow the convention with the argument coming in $f12
and the
result being returned in $f0
. In this function we use both methods for getting
a value into float registers; one we load from memory and the other, being
an integer, we move and convert.
As I said before, it is rare for courses to even bother covering floating point instructions or assign any homework or projects that use them, but hopefully this brief overview, combined with the knowledge of previous chapters is sufficient.
There are also 2 example programs conversions.s and calc_pi.s for you to study.