The AVR32 architecture contains an instruction that can be used to interact with a coprocessor. In the case of the AT32UC microcontroller, the coprocessor is a Floating Point Unit (FPU) that can be used to perform floating-point calculations. During my initial work on the AVR32 QEMU implementation, I left this part out.

There were no FPU instructions in the OPS-SAT firmware, and therefore there was no need to add this functionality. However, in December 2023, a research team that uses the AVR332 QEMU implementation to build an actual satellite asked me for help (by the way, how cool is this? My software is used to help build real satellites). Their firmware was using the FPU. Hence, it could not be tested with the emulator. Of course, I was ready to help, and so I spent a part of my new year’s holiday implementing the COP instruction.

The COP instruction

The AVR32 instruction set defines the COP instruction like this: CP#(CRd) ← CP#(CRx) Op CP#(CRy). CR represents a register in the coprocessor, Op is an operation and # is the number of the coprocessor. For example, a coprocessor could provide an add operation with the number 5. In this case, cop 1, 0, 2, 3, 5 would tell the coprocessor 1 to add the content of its registers 2 and 3 and then move the result into register 0.

The COP instruction has three fields of four bits each that are used to address the CRd, CRx, and CRy registers. There are also three fields with a total length of seven bits that are used to name the coprocessor operation. The remaining 10 bits of the instruction are used for the opcode.

Handling COP calls

I implemented the parsing of the CPO instruction like I did for every other instruction. The actual functionality was moved into a helper file (helper_fpu-c), as there are multiple operations in the coprocessor. The helper is provided with the register numbers and the operation. Because the operation is split into three fields, they are combined first.

static bool trans_COP(DisasContext *ctx, arg_COP *a){
    uint32_t op = (a->oph << 5) | (a->opm << 1) | (a->opl);
    gen_helper_cop(cpu_env, tcg_constant_i32(a->crd), tcg_constant_i32(a->crx), tcg_constant_i32(a->cry), tcg_constant_i32(op));
    //...
}

The helper also gets a reference to the cpu_env. This is necessary because the AT32UC coprocessor does not have its own registers. Instead, it is working directly on the CPU registers.

Please note that the values of a->crx are converted into actual TCGv variables that contain the corresponding number.

Identifying coprocessor operations

In general, the supported coprocessor operations depend on the emulated processor. The AT32UC provides 16 different coprocessor operations. All of them are used to work with floating-point numbers (after all, it is an FPU).

The available coprocessor operations are specified in the AT32UC manual. The description is similar to the instruction description from the AVR32 architecture document. My first goal was to find out how the FPU uses the operation fields to identify what operation needs to be executed.

After looking at the bit pattern of the floating-point operations, I noticed that some operations are using the second field of the three operation fields as a register number for a register RA. When bit 6 of the operation field is set to 0, the bits one to four are used to identify a register and not to identify an operation (bit 26 in the image).

Opcode of FMAC.s

Besides that, there seems to be nothing special. Each operation can be identified by matching the opcode pattern. This can be done similar to the process for the regular instruction translation.

void helper_cop(CPUAVR32AState *env, uint32_t rd, uint32_t rx,  uint32_t ry, uint32_t op)
{
// opm is used as RA register
    if(!(op >> 6)){
        uint32_t ra_num = (op & 0b0011110) >> 1;
        op = (op & 0b1100001);
        switch (op) {
            case 0:
                fmacs(env, rd, rx, ry, ra_num);
                break;
            case 1:
                fnmacs(env, rd, rx, ry, ra_num);
                break
            case 0x20:
        //...

I decided to first check if the RA register is used. In that case, only three bits from the operation field remain for the opcode. The above switch statement is used to identify the corresponding operations.

If bit 26 is set to one, all six bits of the operation field are used to identify the operation. In that case, the whole op argument can be matched:

        if(op == 0b1000000){
            fadds(env, rd, rx, ry);
        }
        else if(op == 0b1000010){
            fsubs(env, rd, rx, ry);
        }
        else if(op == 0b1000100){
            fmuls(env, rd, rx, ry);
        }
        //...

Conclusion

Coprocessor operation for AVR32 devices can be identified by performing pattern matching. This article explains one way to achieve this.

In the next article, I will explain how floating-point arithmetics can be done in QEMU.