The LLVM project provides a powerful infrastructure for compiler development, enabling the creation of efficient and optimized code for various target architectures. To harness the full potential of LLVM, developers often need to provide accurate and detailed information about the target hardware. Two essential components in this process are the TableGen files for Register Info and Instruction Info.

TableGen is necessary for writing the abstract target description. This tool translates a target description file (.td) into C++ code that is used in code generation. Its main goal is to reduce large, tedious descriptions into smaller and flexible definitions that are easier to manage and structure. The idea is that TableGen is a maintainable and human-readable description which is translated into C++ by the tablegen tool and compiled, along with the hand-coded C++ files from your backend, when building LLVM.

In this blog post, we will explore the process of writing a TableGen file for PRU’s Register Info

Understanding Register Info:

Register Info files in LLVM provide crucial information about the target hardware’s register set. This includes the number of registers, their names, allocation order, register classes, and more. By accurately defining the register information, LLVM can generate optimized machine code specific to the target architecture.

Register fields in the PRU AM335x architecture:

  • REG, REG1, REG2, ... (8 to 32 bits): The PRU AM335x architecture supports multiple general-purpose registers, denoted as REG, REG1, REG2, and so on. These registers can hold values ranging from 8 to 32 bits. They are commonly used for general-purpose calculations and data manipulation. Examples of such registers include r0, r1, r2, etc.

  • Rn, Rn1, Rn2, ... (32-bit register fields): The PRU AM335x architecture provides a set of 32-bit registers, denoted as Rn, Rn1, Rn2, and so forth. These registers span from r0 to r31, offering a wider range of storage for larger data types or calculations.

  • Rn.bx (byte field): The PRU AM335x architecture includes byte fields specified as Rn.bx. These byte fields allow for the selection of a specific byte within a register. The ‘x’ in Rn.bx can take values from 0 to 3, representing the four available bytes in the register. Examples include r0.b0 (byte 0 of register r0) or r0.b3 (byte 3 of register r0).

  • Rn.wx (two-byte field - word): To handle two-byte (word) fields, the PRU AM335x architecture introduces Rn.wx notation. The ‘x’ in Rn.wx can take values from 0 to 2, representing the three possible word spans. A word spans two bytes, and each value of ‘x’ denotes the starting byte of the word. Examples include r0.w0 (word spanning bytes 0 and 1), r0.w1 (word spanning bytes 1 and 2), and so on.

PRU Register Info tablegen file

At the top of the file we find the following class:

  class PRUReg<bits<16> num, string name, list<Register> subregs = [], list<string> altNames = []> : RegisterWithSubRegs<name, subregs> {
  field bits<16> Num = num;
 
  let HWEncoding = num;
  let Namespace = "PRU";
  let SubRegs = subregs;
  let AltNames = altNames;
}  

That code declares a class PRUReg that inherits from the internalized Register class which is defined in include/llvm/Target/Target.td. The code is also telling us that we must provide up to four arguments when inheriting from this class, the last 2 are optional.

Moving on, we find the following register definitions in the file. Each line of code is a TableGen record that defines a single register.

Since we know, the registers can be byte field, two-byte field(word), 32-bit register, we need to define all of these.

   //// 8 bit subregs
 def r0_b0 : PRUReg<0, "r0.b0">;
 def r0_b1 : PRUReg<0, "r0.b1">;
 def r0_b2 : PRUReg<0, "r0.b2">;
 def r0_b3 : PRUReg<0, "r0.b3">;
 def r1_b0 : PRUReg<1, "r1.b0">;
 def r1_b1 : PRUReg<1, "r1.b1">;
 def r1_b2 : PRUReg<1, "r1.b2">;
 def r1_b3 : PRUReg<1, "r1.b3">;
 .....

 //// 16 bit subregs
 def r0_w0 : PRUReg<0, "r0.w0", [r0_b0, r0_b1]>, DwarfRegNum<[]>;
 def r0_w2 : PRUReg<0, "r0.w2", [r0_b2, r0_b3]>, DwarfRegNum<[]>;
 def r0_w1 : PRUReg<0, "r0.w1", [r0_b1, r0_b2]>, DwarfRegNum<[]>;
 def r1_w0 : PRUReg<1, "r1.w0", [r1_b0, r1_b1]>, DwarfRegNum<[]>;
 def r1_w2 : PRUReg<1, "r1.w2", [r1_b2, r1_b3]>, DwarfRegNum<[]>;
 def r1_w1 : PRUReg<1, "r1.w1", [r1_b1, r1_b2]>, DwarfRegNum<[]>;
  .....

 //// 32 bit main regs
 def r0 : PRUReg<0, "r0", [r0_w0, r0_w2]>, DwarfRegNum<[0]>;
 def r1 : PRUReg<1, "r1", [r1_w0, r1_w2]>, DwarfRegNum<[1]>;
 def r2 : PRUReg<2, "r2", [r2_w0, r2_w2]>, DwarfRegNum<[2]>;
 def r3 : PRUReg<3, "r3", [r2_w0, r3_w2]>, DwarfRegNum<[3]>;
 def r4 : PRUReg<4, "r4", [r4_w0, r4_w2]>, DwarfRegNum<[4]>;
 def r5 : PRUReg<5, "r5", [r5_w0, r5_w2]>, DwarfRegNum<[5]>;

The records inherit from two classes: PRUReg (that we already discussed) and DwarfRegNum. DwarfRegNum is yet another internalized class defined used to provide debug information for GCC and GDB.

The last few lines in the file define the Register Classes.

 def breg : RegisterClass<"PRU", [i8], 8, (sequence "b%u", 0, 3)>;

 def reg8 : RegisterClass<"PRU", [i8], 8, (add
    (sequence "r%u_b0", 0, 31),
    (sequence "r%u_b1", 0, 31),
    (sequence "r%u_b2", 0, 31),
    (sequence "r%u_b3", 0, 31)
 )>;

 def reg16 : RegisterClass<"PRU", [i16], 8, (add
    (sequence "r%u_w0", 0, 31),
    (sequence "r%u_w1", 0, 31),
    (sequence "r%u_w2", 0, 31)
 )>;

 def reg32 : RegisterClass<"PRU", [i32], 8, (sequence "r%u", 0, 31)>;