Confusion Regarding Freedom E SDK inline asm

In encoding.h we see the following

#define write_csr(reg, val) ({ \
  if (__builtin_constant_p(val) && (unsigned long)(val) < 32) \
    asm volatile ("csrw " #reg ", %0" :: "i"(val)); \
  else \
    asm volatile ("csrw " #reg ", %0" :: "r"(val)); })

#define swap_csr(reg, val) ({ unsigned long __tmp; \
  if (__builtin_constant_p(val) && (unsigned long)(val) < 32) \
    asm volatile ("csrrw %0, " #reg ", %1" : "=r"(__tmp) : "i"(val)); \
  else \
    asm volatile ("csrrw %0, " #reg ", %1" : "=r"(__tmp) : "r"(val)); \
  __tmp; })

#define set_csr(reg, bit) ({ unsigned long __tmp; \
  if (__builtin_constant_p(bit) && (unsigned long)(bit) < 32) \
    asm volatile ("csrrs %0, " #reg ", %1" : "=r"(__tmp) : "i"(bit)); \
  else \
    asm volatile ("csrrs %0, " #reg ", %1" : "=r"(__tmp) : "r"(bit)); \
  __tmp; })

#define clear_csr(reg, bit) ({ unsigned long __tmp; \
  if (__builtin_constant_p(bit) && (unsigned long)(bit) < 32) \
    asm volatile ("csrrc %0, " #reg ", %1" : "=r"(__tmp) : "i"(bit)); \
  else \
    asm volatile ("csrrc %0, " #reg ", %1" : "=r"(__tmp) : "r"(bit)); \
  __tmp; })

I have been studying this code in my RISC-V series, and we are confused about the check for whether the second value is < 32.

I was reading clear_csr, and understand that the macro is using the GCC statement expression extension, and that the inline assembly syntax is specifying that the bitmask is an input parameter, the first path hinting that the value is an immediate, and the second hinting a register. However, I do not understand what the < 32 check is doing, or how this changes the encoding of the instruction.

Looking at the RISC-V specs, I see that CSRRC instruction is encoded as the I-type encoding. The first 12 bits specify the CSR, followed by 5 bits for rs1 which specifies the register that the bitmask goes in, and then 3-bits to indicate CSRRC, 5 bits for rd which specifies the destination register which is __tmp in our code, and finally 7-bits to encode the SYSTEM opcode.

Originally I thought that it was packing the immediate into the 5-bits of rs1, which would explain why it would need to check for < 32, but reading the docs it seems that rs1 is exclusively for referring to one of the 32 general purpose registers. There would be no way for it to differentiate between an immediate packed into the rs1 space and a value in rs1 indicating a register… So I am left wondering, why the check for < 32? What am I missing?

Relevant episodes of my series to this question would be episodes 11 and 13 (12 was a tangent about two’s complement)

1 Like

What you’re missing is that the assembler will treat some mnemonics as generic over their arguments. In this cases this means csrrc can refer to either the CSRRC, or the CSRRCI instruction, depending on the 3rd argument.
CSRRCI uses a variant of the I-type encoding that uses the rs1 field as an unsigned 5-bit immediate (zimm[4:0] in the ISA spec), pretty much as you originally thought.

2 Likes

In the cases where an immediate is used, I wonder why the instruction mnemonic didn’t explicitly specify the immediate variant. For instance, something like this:

#define write_csr(reg, val) ({ \
  if (__builtin_constant_p(val) && (unsigned long)(val) < 32) \
    asm volatile ("csrwi " #reg ", %0" :: "i"(val)); \
  else \
    asm volatile ("csrw " #reg ", %0" :: "r"(val)); })

Arguably, this would have been more clear. Also, this would demand that the assembler use the immediate variant, while I suppose the other form would give some more leeway. I don’t recall seeing anything in the specs saying that the assembler must optimize to the immediate versions of the instructions when possible, although I imagine any assembler will do so in practice.