Building constants in RISC-V
============================
[Published 2019-11-24, Updated 2020-01-01]
No instruction in RISC-V is currently longer than 32 bits. Therefore
you need to use multiple instructions if you want to put a big number
in a register (unless you read it from e.g. memory).
The instructions you will need
------------------------------
Some common instructions for putting values in registers are:
addi rd, rs1, imm // rd = rs1 + imm (imm is 12 bits)
lui rd, imm // rd = imm << 12 (imm is 20 bits)
slli rd, rs1, imm // rd = rs1 << imm
That seems easy. Just fill the upper 20 bits using lui and the lower
12 bits using addi and then you can shift the whole thing up a bit
using slli if you want to fill in more bits.
No, it's not always that easy.
Sign extension
--------------
The lui instruction doesn't just write the specified value shifted 12
bits on 64-bit RISC-V (but on 32-bit it does). It also copies the
most significant bit and repeats it in the more significant bits.
This is called sign extension.
The addi instruction does sign extension too (on the immediate value
but not on the result). In this case it's very useful because that
allows you to add negative numbers.
In the case of the lui instruction it doesn't make much sense. This
instruction is mostly used to set the upper 20 bits of a number, as
far as I know. Sign extension in this case doesn't help at all. It
causes problems.
So this means that you have to do some tricks if the most significant
bits in the immediate values are 1.
The tricks
----------
In the case of addi when you want to add a number that has the bit
with value 1<<11 set (imm[11]), the number will be seen as a negative
number since this is the most significant bit (the sign bit). Bits
imm[10:0] will still be added as if nothing happened; the difference
is that the value represented by imm[11] (1<<11) will be subtracted
instead of added. You have to compensate for this by adding an extra
1<<12 if imm[11] is 1, both to remove the subtraction of 1<<11, and to
add the 1<<11 that comes from the value of bit imm[11].
In the case of lui there is no simple little trick you can do because
the sign extension happens on the whole result at the end. This
destroys all higher bits (rd[32] and above) that you may have put
there earlier. One way to deal with this is to never put a 1 in the
most significant bit (imm[19]) and just fill in the lower bits and
repeatedly shift the bits up. This creates more instructions than
what would be needed if lui did not sign extend. Another way that
makes fewer instructions but can be even slower to execute is to
simply load the value from memory instead of building it with
instructions (this is what gcc seems to do).