lui
lui
rd, imm
rd = imm << 12
R
31–25funct7
24–20rs2
19–15rs1
14–12funct3
11–7rd
6–0opcode
I
31–20imm[11:0]
19–15rs1
14–12funct3
11–7rd
6–0opcode
S
31–25imm[11:5]
24–20rs2
19–15rs1
14–12funct3
11–7imm[4:0]
6–0opcode
B
31–25imm[12,10:5]
24–20rs2
19–15rs1
14–12funct3
11–7imm[4:1,11]
6–0opcode
U
31–12imm[31:12]
11–7rd
6–0opcode
J
31–12imm[20,10:1,11,19:12]
11–7rd
6–0opcode

lui helps build large constants, and to see why it is needed, start with a limitation. Every RISC-V instruction is itself just 32 bits wide, and those bits must encode the operation and its registers as well as any constant. That leaves only about 12 bits for a constant inside a normal instruction — enough for small numbers, but nowhere near enough for a full 32-bit value. So large constants cannot be loaded in one instruction; they have to be assembled in pieces.

lui (load upper immediate) handles the top piece. It takes a 20-bit constant and places it in the *upper* 20 bits of the destination register, setting the low 12 bits to zero. The form is lui rd, value.

The usual recipe for a full constant is two instructions: lui lays down the high 20 bits, then an addi adds the low 12. That pairing is exactly what the li pseudo-instruction does for you automatically, which is why you will rarely write lui by hand for ordinary numbers — li is friendlier.

Where lui does appear directly is loading round addresses whose low bits are already zero, such as the fixed addresses of hardware devices, where a single lui supplies the whole value. Its close sibling is auipc, which builds an address relative to the current location instead of an absolute number.