Microcode inside the Intel 8087 floating-point chip: register exchange
Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,629 words · 5 segments analyzed
In 1980, Intel introduced the 8087 floating-point chip, a co-processor that made floating-point operations up to 100 times faster. This chip was highly influential, and today most processors use the floating-point standard introduced by the 8087. The 8087 uses complicated algorithms to accurately compute functions such as square roots, tangents, and exponentials. These algorithms are implemented inside the chip in low-level code called microcode. I'm part of a group, the Opcode Collective, that is reverse-engineering this microcode. In this post, I take a close look at the microcode for one of the 8087's instructions—FXCH—and explain how the microcode works. The FXCH (Floating-point Exchange) instruction exchanges two floating-point registers. You might expect this instruction to be trivial, but there's more going on than you might expect; the microcode uses 14 micro-instructions to implement the exchange instruction. The Intel 8087 chip is packaged in a 40-pin DIP (dual in-line package). To explore the microcode, I opened up an 8087 chip and created a high-resolution image with a microscope. The large microcode ROM occupies a central position, holding the micro-instructions that control the chip. The microcode engine on the left steps through the microcode, handling jumps and subroutine calls. The bottom half of the chip is the "datapath", the circuitry that performs floating-point calculations; it is split into a 16-bit datapath for the number's exponent and a 64-bit datapath for the number's fractional part (also known as the significand). Die of the Intel 8087 floating-point unit chip, with main functional blocks labeled. The die is 5mm×6mm. Click for a larger image. This post focuses on the temporary registers and stack registers that are highlighted in red. The chip has two temporary registers and eight stack registers, each holding a number's exponent and fraction. Each register also has two tag bits that label the type of value in the register. The stack control circuitry at the right manages the stack, keeping track of the top-of-stack position as values are pushed onto the stack or popped off the stack.
Executing an 8087 instruction such as arctan requires hundreds of internal steps to compute the result. These steps are implemented in microcode with micro-instructions specifying each step of the algorithm. (Keep in mind the two levels of instructions: the assembly language instructions used by a programmer and the undocumented low-level micro-instructions inside the chip.) The microcode ROM holds 1648 micro-instructions, implementing the 8087's instruction set. Each micro-instruction is 16 bits long and performs a simple operation such as moving data inside the chip, adding two values, or shifting data. I'm working with the Opcode Collective to reverse-engineer the micro-instructions and fully understand the microcode (link). The 8087's micro-instructions are complicated, with many corner cases and ad hoc functions, but I'll provide a simplified overview. Each micro-instruction consists of 16 bits, as shown below. The first three bits specify the type of the micro-instruction, which controls the meaning of the remaining bits. The first type indicates a transfer operation, transferring data from one internal register to another. The two fields specify the source and destination for the data. The three unspecified bits are used for various special cases. Next is a shift operation, which uses the barrel shifter to shift a value left or right. The third type of micro-instruction uses the adder/subtractor. It can also be used in a loop for multiplication or division. Fourth are various arithmetic control micro-instructions that configure the adder, set rounding modes, and so forth. The far jump and far call micro-instructions perform a jump or subroutine call to a target micro-address in a fixed list. The condition field allows conditional jumps/calls based on numerous conditions, while the last bit inverts the condition. A local jump allows a conditional jump to a nearby micro-instruction. Finally, the miscellaneous micro-instructions range from returning from a subroutine or raising an exception to ending the microcode execution. Structure of an 8087 micro-instruction. How values are stored inside the 8087 chip The 8087 supports a variety of data types: floating-point numbers of various sizes, integers, and binary-coded decimal. But internally, everything is stored as an 80-bit floating-point number.
A number has three parts: a 64-bit significand (the fractional part), a 15-bit exponent, and a sign bit. The chip has two separate data paths: one for the significand, and one for the exponent and sign. The chip has eight registers to store numbers during calculations, the top registers in the diagram below. However, the registers are organized in an unusual way: as a stack, with numbers pushed to the stack and popped from the stack. Instead of accessing, say, register #3, you might access the third register from the top of the stack, denoted ST(3); as values are pushed or popped, ST(3) changes. The stack-based architecture was intended to improve the instruction set, simplify compiler design, and make function calls more efficient, although it didn't work as well as hoped.
Many 8087 instructions act on the top of the stack. For instance, the square root instruction replaces the value on the top of the stack with its square root. But what if you want to take the square root of a value in the middle of the stack? The solution is the FXCH instruction, the focus of this article. This instruction exchanges the value on the top of the stack with a specified stack position, providing access to values inside the stack. One more feature of the 8087 is important to this discussion: each value in the register stack has an associated "tag" value, labeling it as valid, special, zero, or empty. A "normal" floating-point value is tagged as valid. If the floating-point value is infinity, Not a Number, or a denormalized value, then it is tagged as special. A zero value is tagged as zero. Finally, if a register is empty (e.g., its value has been popped off the stack), the register is tagged as empty. The 8087 uses tags to optimize performance and detect errors.1 For instance, if a programmer pops too many values from the stack and tries to read a stack register that is tagged empty, the 8087 raises an "invalid operation" exception. The eight stack registers are visible to the programmer, but the 8087 also has temporary registers that it uses internally.
Two of these temporary registers are important for this article: tmpA and tmpB. Like the stack registers, each temporary register is an 80-bit register, along with two tag bits. The FXCH microcode In this section, I'll explain how the microcode for the FXCH exchange instruction works. This instruction exchanges the top-of-stack register with the register at a specified position in the stack. If either register is empty, the instruction will raise an "invalid operation" exception and replace the missing value(s) with the special value "Not a Number" (NaN). The microcode for the instruction is below, consisting of 14 micro-instructions.2 The first micro-instruction is a transfer, where the source is the top of stack value ST(0) and the destination is the temporary A register. The source specification causes the 64 significand to be placed on the fraction bus, the 16-bit exponent and sign to be placed on the exponent bus, and the two tag bits to be sent to the tag circuitry. The destination tmpA causes the bus values to be stored into the temporary register. Thus, the bits in the micro-instruction cause the desired transfer to take place. The third micro-instruction is similar, but uses a register inside the stack, ST(i), with the index specified in the machine instruction.
FXCH entry point: #0200 ST(0) -> tmpA read top of stack #0201 nop Wait a cycle #0202 ST(i) -> tmpB Read specified stack register #0203 if !(tmpA or tmpB empty) jmp #0210 Jump if both registers exist #0204 set invalid exception Raise an invalid exception #0205 if (unmasked) jmp #0213 If interrupt, end #0206 if !(tmpA empty) jmp #0208 Check if tmpA is empty #0207 NaN -> tmpA If so, move NaN to tmpA #0208 if !(
tmpB empty) jmp #0210 Check if tmpB is empty #0209 NaN -> tmpB If so, move NaN to tmpB The happy path and error path continue here: #0210 tmpB -> ST(0) Save tmpB to the top of stack #0211 nop Wait a cycle #0212 tmpA -> ST(i) Save tmpA to the specified stack register #0213 RNI End of routine: Run Next Instruction #0214 nop Unused #0215 nop Unused #0216 nop Unused
Next, the relative jump at micro-address #0203 illustrates a different type of micro-instruction: the conditional jump. The micro-instruction specifies a condition, in this case, testing if either temporary register is empty. (That is, the hardware tests the tag bits associated with the temporary registers to see if either is the "empty" tag.) The micro-instruction has a bit set to invert the condition. Finally, the micro-instruction has an offset of +6, yielding the jump target #0210. The advantage of a relative offset over specifying a full micro-address is that the offset only requires six bits. (For more information on how conditions are evaluated, see my article Conditions in the Intel 8087 floating-point chip's microcode.) If either register is empty, the next micro-instruction raises an "invalid" exception. As I'll explain in the next section, you can program the 8087 to either generate an interrupt on an exception or continue processing. The next instruction is a conditional jump that tests if the exception was "unmasked", indicating that an interrupt was generated. In this case, the microcode ends while the main 8086 processor handles the interrupt. Assuming the interrupt was masked, the microcode now replaces empty values with the special Not a Number value, first checking tmpA and then tmpB. The source NaN causes circuitry to pull the exponent bus to all 1's and the fraction bus to all 0's, except for the top two bits.