More from Ken Shirriff's blog
In the 1970s, floating-point arithmetic was a mess. Computer manufacturers had a dozen incompatible arithmetic standards. Moreover, floating-point systems were designed around hardware simplicity rather than mathematical rigor, leading to problems with numerical stability. This changed when Intel introduced the 8087 floating-point coprocessor chip in 1980, designed to be as accurate as possible, even in the corner cases. The 8087 became popular because it could be installed in the IBM PC, making floating-point operations up to 100 times faster in applications ranging from spreadsheets to CAD. But more importantly, the 8087 became the floating-point standard used by most computers today. The 8087 implemented its instructions in complex low-level code called microcode. I'm part of a group, the Opcode Collective, that is reverse-engineering this microcode, and I've recently made some progress. In this post, I examine the microcode for one of the 8087's instructions—FSCALE—and describe how this microcode works. The FSCALE (Floating-point Scale) instruction provides a quick way to scale a number by a power of two, much faster than a multiplication. I figured that FSCALE was a simple, almost trivial instruction that would be straightforward to understand and explain. Spoiler: it is not simple. FSCALE uses over 140 micro-instructions and three levels of subroutine calls to handle many special cases. But the FSCALE microcode illustrates many interesting parts of the 8087, such as the shifter, the adder, and the exponent converter, and also reveals a hidden feature of the 8087, so hopefully you will find it interesting. To explore the microcode, I opened up an 8087 chip and created a high-resolution image with a microscope. The large microcode ROM is in the center, holding the 1648 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 significand (also known as the fractional part). Die of the Intel 8087 floating-point unit chip, with main functional blocks labeled. The die is 5mm×6mm. Click for a larger image. Zooming in on the bottom part of the chip shows the datapath circuitry; I've highlighted the relevant parts below.1 The exponent ROM holds various constants. The exponent converter is a specialized circuit that examines exponents, detects special values, and converts between exponent formats.2 The shifter is a large component; it allows a 64-bit3 value to be shifted left or right by arbitrary amounts. (I wrote about the 8087's shifter circuitry here.) The adder is the heart of the 8087's calculations; it is used in a loop for multiplication, division, and square roots. The B register holds one input to the adder, while multiple sources can provide the other input. The sum register holds the adder's output. The eight stack registers and the temporary registers hold floating-point numbers. A close-up of the 8087's datapath, showing functional blocks that are used by FSCALE. Details of the 8087 In this section, I'll explain some features of the 8087 that are important for the FSCALE microcode. To use the 8087, a programmer stores values in its eight internal registers, organized as a stack. Each register holds an 80-bit floating-point number. To optimize performance, each value in the register stack has an associated "tag" value, which is mostly invisible to the programmer.4 A tag labels a value 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 (NaN), 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 also has temporary registers that it uses internally: tmpA, tmpB, and tmpC. Like the stack registers, tmpA and tmpB are 80-bit registers, along with two tag bits. However, tmpC only holds a 64-bit significand. 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 called a "temporary real"; for the rest of this article, I'll only be considering temporary real values. A number has three parts: the sign bit, the 15-bit exponent, and the 64-bit significand (the fractional part), In most cases, a floating-point number is represented by sign × significand × 2exponent. The significand is a 64-bit binary number of the form 1.bbb...: a leading 1, followed by the binary point (the binary equivalent of the decimal point) and the rest of the bits.5 What makes floating-point numbers useful is that their scope covers the incredibly small to the astronomically large, thanks to the exponent, which ranges from -16382 to 16383. One important detail is that the exponent is stored with a "bias" of 16383 added to it. Thus, the stored exponent is always positive, even if the real exponent is negative.6 The 80-bit temporary real format. The triangle indicates the binary point, analogous to the decimal point. From the Intel Numerics Supplement. The 8087 supports several types of numbers that are represented as special cases with special exponents, as shown below. Zero and infinity have both positive and negative values. "Not a Number" (NaN) represents values that don't make sense, such as 0/0 or sqrt(-1); NaN has a large number of representations, not a single value. The 8087 also supports denormalized and unnormalized values, which are extremely small values where the significand doesn't have a leading 1. The encoding of special values. Based on Table S-31 in the Intel Numerics Supplement, but highly simplified. The "x" bits are arbitrary, as long as they don't conflict with another type. The 8087 has a complicated exception system with six types of exceptions to indicate if something went wrong with an arithmetic operation. The most serious is the "invalid operation", indicating that the operation does not make sense, such as 0/0 or ∞-∞. It also includes accesses to an empty register (stack overflow or underflow) or operations on a NaN value. The 8087 also has an overflow exception if a value is too large to store, an underflow exception if a value is too small, and a divide-by-zero exception (excluding 0/0). A denormalized operand exception indicates that the result is too small to store as a normal value, but can be stored as a denormalized value. Finally, a precision exception indicates that a value cannot be represented exactly and must be rounded. (Precision exceptions are very common; even 1/10 will yield one.) The 8087 provides fine-grain control over each exception type, specified by bits in the control register. If an exception is unmasked, the 8087 sends an interrupt to the 8086 processor, which handles the problem in software, for instance by terminating the program or logging an error. Alternatively, the exception can be masked and the 8087 will continue execution as best it can. For instance, an invalid result will be replaced by NaN, while an overflow or divide-by-zero will be replaced by infinity. A precision exception will result in rounding. The point of masked exceptions is that calculations continue, yielding an answer that is as accurate as possible; in most cases, this is what the programmer wants. These features make the 8087 flexible and provide accuracy, but they also make the microcode much more complicated, since the combinations of special cases need to be handled appropriately. The 8087's microcode Executing an 8087 instruction can require hundreds of internal steps to compute the result. These steps are implemented in microcode with micro-instructions that specify 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 the 1648 micro-instructions that implement the 8087's instruction set. 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 micro-instruction's type, which controls the meaning of the remaining bits. The first type is a transfer operation, which transfers data from one internal register to another. The two fields specify the source and destination. The three remaining 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 controls the adder (which can also subtract). The miscellaneous instructions include stack pointer operations, tag modification, exceptions, and subroutine return. 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/returns based on numerous conditions, while the last bit inverts the condition. A local jump is a relative jump to a nearby micro-instruction. Structure of an 8087 micro-instruction. The FSCALE microcode When the 8087 starts executing an instruction, the instruction decoder circuitry determines the starting address of the microcode corresponding to the instruction. This 11-bit address is loaded into the microcode engine, which starts executing the microcode.7 The microcode for FSCALE (shown below) starts at decimal address 748.8 The idea behind FSCALE is straightforward: if you want to scale a floating-point number by 2N (for an integer N), you add N to the number's exponent. This allows you to multiply or divide by a power of two much faster than using the full floating-point multiplication operation. However, the microcode for FSCALE is unexpectedly complicated and uses several microcode subroutines. In brief, the microcode first checks for arguments that are zero and then handles other special arguments. It converts the scale argument to an integer and adds it to the exponent. Finally, it handles any overflow or underflow. In more detail, the microcode routine starts by moving the first argument from the top of the stack (st(0)) to the tmpA temporary register. If the argument is zero, the routine immediately returns. (Thus, scaling 0 by anything—even NaN—will give a result of 0.) Next, the second value on the stack (the second argument) is moved to the tmpB temporary register. Likewise, the code returns if this value is 0, so scaling anything by 0 leaves the value unchanged.9 Next, a constant value is selected; selecting a constant and using it are two separate micro-instructions. (The 8087 has separate ROMs for 16-bit exponent constants and 67-bit significand constants; this one is an exponent constant.) In the normal case, execution jumps to address #0763, skipping the call to subroutine SPECIAL_TMPS. FSCALE: #0748 st(0) -> tmpA Input argument from top of stack #0749 jmp #0776 if tmpA:tag ZERO Bail if 0 #0750 stackPtr++ #0751 st(0) -> tmpB Scale argument from stack(1) #0752 stackPtr-- #0753 jmp #0776 if tmpB:tag ZERO Bail if 0 #0754 expconst 0x403e Const 403e: exp shift to convert to int #0755 jmp #0763 if not tmp empty/special/div #0756 call SPECIAL_TMPS Special handling #0757 jmp #0762 if flag #0758 jmp #0761 if not tmpB:tag SPECIAL #0759 except:invalid Invalid exception, use NaN #0760 NaN -> tmpA #0761 jmp #0776 if intr #0762 jmp #0775 if expConv[0] Return tmpA if expConv set, otherwise continue #0763 tmpB:exp -> Breg Normal path #0764 tmpB:sign,exp -> expConv ExpConv will test tmpB's sign #0765 expConst -> tmpC Const 403e #0766 adder: tmpC - Breg cin=1 403e-exp is amount to shift to convert tmpB to int #0767 sumreg:frac -> shiftcount Store in shifter control #0768 shift tmpB:frac R count byte bit Perform the shift #0769 shift R -> Breg Breg holds scale argument as an int #0770 jmp #0777 if neg Negative Breg needs separate handling #0771 adder: tmpA:exp + Breg cin=0 Add the scale to the exponent #0772 sumreg:frac -> expConv Put result in expConv to check #0773 sumreg:frac -> tmpA:exp Update exponent with sum #0774 call NONNORMAL_RESULT if not exp normal Handle overflow/underflow #0775 tmpA -> st(0) Save result back to stack #0776 RNI Done: Run Next Instruction #0777 adder: tmpA:exp - Breg cin=1 Subtract Breg #0778 jmp #0772 Continue processing Continuing at #0763, the second argument is converted from a float to an integer, which takes a few steps. For example, suppose the argument is 9, which in floating point is 1.001×23. The significand bits 1000 are "left justified", but for an integer, these bits need to be "right justified" by shifting them to the right. In general, if the exponent is n, the significand is shifted right by 63-n bits. But recall that the exponent is biased by 16383. Thus, the significand must be shifted right by 63-(exp-16383) bits, that is 0x403e-exp bits. (This explains the constant 0x403e earlier in the microcode.) Converting a float to an int by shifting. In the microcode, the subtraction takes several steps. At #0763, the exponent of the second argument is moved to the B register, one of the inputs to the adder (completely different from tmpB).10 Next, the sign and exponent are moved to the exponent converter, a circuit that, among other things, tests for overflow. Next, the constant 0x403e (selected back at #0754) is moved to the tmpC register. At #0766, the adder is activated, subtracting the exponent from the constant.11 The adder puts the result into the sum register, and this value is copied to the shift count register, which controls the shifter. This value indicates how many bits the second argument must be shifted to convert it to an integer. At #0768, the shifter is activated to shift by the desired amount, using both the bit shift part and the byte shift part. As with the adder, activating the shifter and reading the result are separate micro-instructions; the result is put into the B register. The core part of the FSCALE instruction is finally performed at #0771, adding the second argument to the first argument's exponent. The adder is activated to add the B register value (the scale) to the exponent, and the updated value is stored in tmpA's exponent. (Except if the scale factor is negative, it is subtracted via the #0777 path.)12 The value is also sent to the exponent converter circuit, which checks the exponent for overflow or underflow; if so, subroutine NONNORMAL_RESULT is called. But in the normal case, the updated value is copied from tmpA to the top-of-stack register st(0). Finally, RNI (Run Next Instruction) indicates that the microcode routine is done and the instruction is completed. Thus, even in the straightforward case, FSCALE takes about 22 micro-instructions. Handling empty or special arguments What happens if an argument accesses an empty stack location (i.e. stack underflow) or is a special value (infinity, denorm, NaN)? These cases are handled by a micro-subroutine that I'll call SPECIAL_TMPS15 because it processes special values in tmpA and/or tmpB. This subroutine is a general-purpose routine, used by basic arithmetic operations, FSCALE, FTST (test), and FPREM (partial remainder). The control flow through SPECIAL_TMPS is rather convoluted since the code must prioritize issues if, say, one argument is empty and the other is a denorm. I'll just give a brief summary; see the footnote13 for details. First, the subroutine converts any denorms to unnorms. Then it checks for access to empty stack locations, raising an exception or interrupt if so. Then it checks the two arguments again. If either is NaN, an exception or interrupt is triggered. Otherwise, it returns a status indicating the type of arguments. Unexpectedly, if both arguments are NaN, the code compares the two NaN values and returns the larger. This behavior may seem very weird, but it's a documented feature.14 You might think that NaN is a single value, but it's actually an enormous family of values. The idea was that the programmer could use different NaN values to signal where a problem occurs. For instance, you could put a different NaN in each location of an uninitialized array, so you could tell which position was accessed. For some reason, the designers of the 8087 decided that if you perform an operation with two different NaNs, the result is the larger one. Thus, the microcode needs code that detects if both operands are NaN and computes the larger, using a subtraction for the comparison (#1518). SPECIAL_TMPS (J5): #1484 call SPECIAL_VAL if tmpA:tag SPECIAL Handle special values in tmpA/tmpB #1485 xchg tmp #1486 call SPECIAL_VAL if tmpA:tag SPECIAL Handle tmpB special #1487 xchg tmp #1488 1 -> flag Flag=1 by default #1489 jmp #1500 if not tmp empty/special/div 0 -> expConv if tmps okay #1490 1 -> expConv #1491 jmp #1497 if not tmpA/B empty #1492 except:invalid Invalid if either empty #1493 jmp #1525 if compare instruction No NaN for comparison #1494 jmp #1511 if intr Return if interrupt not masked #1495 NaN -> tmpA NaN if interrupt masked #1496 return #1497 jmp #1502 if tmpA:tag SPECIAL Special cases #1498 jmp #1505 if tmpB:tag SPECIAL #1499 0 -> flag Div normal path: #1500 zero -> expConv Return flag 0, expConv 0 #1501 return #1502 call SPECIAL_VAL TmpA special #1503 jmp #1512 if not flag Jump if NaN, fallthrough if infinity #1504 jmp #1509 if not tmpB:tag SPECIAL #1505 xchg tmp TmpB special #1506 call SPECIAL_VAL #1507 xchg tmp #1508 jmp #1521 if not flag Jump if NaN, return if infinity #1509 0 -> flag Clear flag, return #1510 return #1511 RNI End instruction with interrupt #1512 jmp #1522 if not tmpB:tag SPECIAL TmpA NaN, now check tmpB #1513 xchg tmp #1514 call SPECIAL_VAL Check tmpB #1515 xchg tmp #1516 jmp #1522 if flag Jump if tmpB is not NaN #1517 except:invalid Invalid exception #1518 tmpB:frac -> Breg Both args are NaN, find larger #1519 adder: tmpA:frac - Breg cin=1 #1520 jmp #1522 if adder sign See if tmpA #1521 tmpB -> tmpA Take larger #1522 except:invalid Invalid exception #1523 jmp #1525 if compare instruction No interrupt for comparison instruction #1524 jmp #1511 if intr End instruction with interrupt #1525 1 -> flag Return with flag set #1526 return End of J5 This subroutine makes heavy use of a helper subroutine, SPECIAL_VAL,16 that processes one argument. The helper converts a denormalized argument to an unnormalized argument, raising an exception or interrupt as appropriate. It also flags an input of infinity. The hardware for the micro-instruction that exchanges tmpA and tmpB at #1485 is interesting. Instead of physically moving the values between the two registers, the micro-instruction toggles a flip-flop that exchanges the meaning of tmpA and tmpB. That is, if the flip-flop is set, a reference to tmpA goes to tmpB and vice versa. (This is a standard trick in microprocessors; the Intel 8080's XCHG instruction exchanges the DE and HL registers in a similar way. The Z80 uses the same trick for the EX and EXX instructions to exchange the regular register set with the secondary register set.) The Intel 8087 chip is packaged in a 40-pin DIP (dual in-line package), as are the 8080 and Z80. This photo is here as a break from all the microcode. Handling a non-normal result If you take a very large number and scale it larger, you can end up with overflow. If you take a very small number and scale it smaller, you can end up with a denormalized number or underflow. This will trigger an overflow, denorm, or underflow excaption, and an interrupt if unmasked. Moreover, the 8087 supports four rounding modes: round to nearest valid value, round down (toward -∞), round up (toward +∞), or round (chop) toward zero. Depending on the rounding mode, an overflow can result in either ∞ or the largest possible floating-point number. Similarly, an underflow can result in either zero or the smallest possible floating-point number. And depending on the infinity mode (affine or projective), infinity can be either signed or unsigned. Thus, the FSCALE microcode needs to handle many special cases for the result. The subroutine to handle a non-normal result in tmpA is below. One interesting micro-instruction is update overflow/underflow exceptions, which triggers an exception if appropriate. For most exceptions, a micro-instruction triggers the exception (for example, except:precision at #0346). But for the overflow and underflow exceptions, the microcode delegates the task to hardware. Specifically, the 8087's "exponent converter" circuit examines the exponent to see if an overflow or underflow exists, based on the selected floating-point precision. The micro-instruction sets the overflow and underflow flags based on these values. Thus, a complex task is performed by a single microcode instruction, thanks to the hardware support of the exponent converter. NONNORMAL_RESULT (J16): #0318 return if tmpA:tag ZERO Handle non-normal result #0319 update overflow/underflow exceptions Trigger exceptions if exp conv says to #0320 expconst 0x6000 The interrupt bias constant 0x6000 #0321 jmp #0329 if not intr #0322 expConst -> Breg Interrupt path #0323 jmp #0326 if neg #0324 adder: tmpA:exp + Breg cin=0 Add bias for underflow #0325 jmp #0327 #0326 adder: tmpA:exp - Breg cin=1 Subtract for bias overflow #0327 sumreg:frac -> tmpA:exp New exponent to tmpA #0328 return Interrupt, so done #0329 jmp #0344 if neg Masked exception #0330 tmpA:exp -> Breg Underflow #0331 adder: 1 - Breg cin=1 Amount to shift denormal #0332 call CREATE_DENORM Create a denormal #0333 adder: zero + Breg cin=0, roundmode Add zero to round #0334 call ADJUST_PRECISION Adjust to specified precision #0335 jmp #0340 if Sum register is zero If zero, return +/- zero as appropriate #0336 zero -> tmpA:exp Denorm: exponent is 0 #0337 sumreg:frac -> tmpA:frac Save denorm fraction #0338 special -> tmpA tag Tag denom as special #0339 return #0340 tmpA sign -> sign latch Return +/- zero #0341 zero -> tmpA #0342 sign latch -> tmpA sign #0343 return #0344 NaN/Inf -> tmpA:exp Overflow: maybe return infinity #0345 tmpA:frac -> tmpB:frac Save tmpA frac in tmpB #0346 except:precision Set precision exception #0347 Inf -> tmpA:frac Put infinity in frac #0348 special -> tmpA tag Mark infinity as special #0349 return if not round chop If rounding up, return infinity #0350 1 -> Breg Return max float: adjust down #0351 adder: tmpA:exp - Breg cin=1 #0352 sumreg:frac -> tmpA:exp Exp=7fff-1=7ffe #0353 adder: zero - Breg cin=1 #0354 sumreg:frac -> tmpA:frac Frac 0-1 = ff...ff #0355 norm -> tmpA tag Normal value #0356 return if tmpB:frac[63] Return max float unless unnorm #0357 tmpB:frac -> tmpA:frac Return original tmpA frac #0358 return The 8087 has interesting behavior if an overflow or underflow is unmasked and an interrupt occurs. The idea is to let the interrupt handler know what the exponent should have been. However, the proper value can't be used since it is too big or too small to fit in the exponent field (which is why the exception occurred). The solution is to add or subtract the constant 0x6000, resulting in an exponent that fits. The interrupt handler can subtract or add this constant to get the correct exponent. Lines #0322 to 0328 perform this addition or subtraction. For a masked underflow, a denorm value is created by the subroutine CREATE_DENORM. The value is rounded to the specified precision by ADJUST_PRECISION. Finally, if the value is too small for a denorm, the value +0 or -0 is returned as appropriate. For a masked overflow, the 8087 either returns Infinity or the largest-possible float, depending on the specified rounding mode. Infinity is represented by an exponent of all 1s, and a significand of 1000...; these values are loaded directly onto the bus by transistors. The maximum float, however, is computed: 1 is subtracted from the infinity exponent, and 1 is subtracted from a zero significand. Helper subroutine: creating a denormal One controversial feature of the 8087 is denormals, numbers that are smaller than "regular" floats. Recall that floating-point numbers have a significand with the first bit set to 1. But what happens if you hit the smallest possible exponent and want an even smaller number? The 8087 lets you break the rule that the significand starts with 1, producing smaller numbers known as denormalized numbers or denorms. Denorms significantly extend the range, providing numbers up to a factor of 263 smaller. However, denorms don't have as much precision since the upper bits are "wasted". Moreover, calculations with denorms can be substantially slower because special handling is required. Example of a normal number, reduced by a factor of 8, resulting in a denormal. The diagram above shows a normal number with the minimum possible exponent (-16382, which is 1 after biasing). Dividing the number by 8 (or scaling by -3) creates a denorm since the exponent can't be reduced any further. Instead, the significand is shifted 3 bits to the right. The exponent is replaced with the special value 0, indicating that the number is a denorm. In the 8087, denorms are created by a microcode subroutine that I'll call CREATE_DENORM; it is used by many arithmetic operations, not just FSCALE. This subroutine takes a normal number and a shift amount. By shifting the normal number (as in the example above), it creates a denormalized number. The microcode (below) uses the exponent converter to check if the shift is 64 or more. If so, there will be nothing left after the shift, so zero is returned. Otherwise, the value is shifted to the right and the denorm is stored in the B register. CREATE_DENORM (J20): #0522 sumreg:frac -> expConv Create denorm #0523 sumreg:frac -> shiftcount Number of bits to shift #0524 jmp #0528 if exponent[6:14] == 0 Jump if #0525 zero -> Breg No bits left, use zero #0526 shift tmpA:frac L 0 bytes, 0 bits Run through shifter? #0527 jmp #0532 #0528 shift tmpA:frac R count byte bit Shift right by the specified amount #0529 shift R -> Breg Result to Breg #0530 shift tmpA:frac L ~count byte bit Now shift back for sticky test #0531 NOP Wait for shifter #0532 rounding(h) -> Breg[grs] Store the three rounding bits in the Breg #0533 return But why is the value then shifted to the left (#0530)? The purpose of this is to get the rounding bits. One of the principles of the 8087 is to get rounding correct, which is a lot harder than it seems. In order to decide how to round up a number, you need to keep track of an impossibly large number of bits. For instance, if you calculate 1 + 0 and round up, you get 1. But if you calculate, say, 1 + 2-10000 and round up, you get a float a bit higher than 1. The problem is how do you distinguish the two sums before rounding, without storing thousands of bits? The trick is that the 8087 keeps three bits for use in rounding: the "guard" bit, the "round" bit, and the "sticky" bit. If you consider a "tail" of bits to the right of the significand, the guard bit is the most significant bit of the tail, followed by the round bit. The sticky bit is special: it is the OR of all the remaining bits in the tail, indicating if any of them are 1. Thus, 1 + 2-10000 has the sticky bit set, while 1 + 0 does not, so the two values can be rounded up differently. To generate the sticky bit, the 8087 uses a very large 64-bit NOR gate that tests the tail bits in parallel. A diagram showing how the guard, round, and sticky bits are computed from a right shift. The numbers in this example are different from the previous example. When a number is shifted to the right (e.g., when creating a denormal), bits are lost off the right. To generate the rounding bits, the value is shifted to the left, keeping all the tail bits that will eventually be discarded, and discarding the bits that will be in the final significand. The top two bits go into the guard and round bits, while the remaining bits are ORed together to generate the sticky bit from the rest.17 The diagram above is an example of this process. Suppose the value is being shifted to the right by 4 bits. The tail bits abcd (or at least d) will get lost in the shift. The rounding bits are computed by shifting the original significand to the right by 59 bits (the complement of 4). Bit 62 (a) becomes the new guard bit, bit 61 (b) becomes the new round bit, and the OR of the remaining 64 bits becomes the new sticky bit. (Note that the old guard, round, and sticky bits get ORed in too, so they aren't lost.) Merging the significand from the first shift with the rounding bits from the second shift produces the desired result. Helper subroutine: adjusting precision Although the 8087 supports three lengths of floats, it performs all calculations with 80-bit "temporary reals". At the end of an instruction, it converts the result to the desired length. (As a consequence, most instructions aren't any faster if you use a shorter float.) A microcode subroutine, which I call ADJUST_PRECISION, converts the result to the precision that is specified in the 8087's control word, using the specified rounding mode. This subroutine is used by most of the arithmetic instructions. The 8087 supports three types of real numbers. From the Intel Numerics Supplement. The first code path handles temporary reals (which have 64 bits of precision). The control word specifies one of four rounding modes. However, there are only two actions that can be taken for a particular significand: either round down (chop) or round up (chop and increment by 1). This decision is made by complicated logic circuits that examine the rounding bits, the rounding mode, and the sign to determine whether to round up or down. This simplifies the microcode but makes the hardware more complicated. The microcode performs a conditional return, returning if the significand doesn't need to be rounded up. Otherwise, the microcode increments the significand by adding 0 with a carry-in. It then checks for overflow, in which case it replaces the value with Infinity and sets a special flag.18 ADJUST_PRECISION (J11): #0299 jmp #0306 if not precision64 #0300 return if not round up, update CC1 Update condition code, maybe return #0301 adder: sumreg:frac + 0 cin=1 Add 1 to round up #0302 return if not sumreg[64] #0303 Inf -> sumreg:frac,sign Return infinity if overflow #0304 2count++ Set special flag #0305 return #0306 23/52 -> shiftcount Short or long real: get appropriate shift #0307 shift sumreg:frac,rnd L count byte bit sticky Shift to generate rounding bits #0308 NOP Wait for shifter to complete #0309 rounding(H) -> sumreg[grs] Store rounding bits #0310 shift sumreg:frac R ~count byte bit Shift right to drop excess bits #0311 shift R -> sumreg:frac #0312 jmp #0314 if not round up, update CC1 Update condition code #0313 adder: sumreg:frac + 0 cin=1 Round up if appropriate #0314 shift sumreg:frac L ~count byte bit Shift left to realign #0315 shift L -> sumreg:frac,sign #0316 return if not sumreg[64] Return if not overflow #0317 jmp #0303 Return infinity The code is more complicated when returning a smaller precision (short real or long real), since the significand must be shortened. First, the code at #0306 loads the shifter with either 23 or 52, depending on the precision specified in the control word, and then shifts the value left. This produces the rounding bits as in the previous section. Next, the value is shifted to the right, shortening it to the desired length. As before, the significand is incremented or not, depending on whether it should be rounded up or not. Finally, the value is shifted back to the left, so the most significant bit of the significand is on the left. As before, if rounding up caused an overflow, infinity is returned. One bizarre feature is that a jump with the "round up" conditional also has a side effect of updating the 8087's programmer-visible condition code register (CC1), indicating if the result was rounded up or down. That is, the 8087 has extra circuitry to detect this specific condition and load the value into the condition code latch. Strangely, the 8087 documentation doesn't describe this condition code action; Intel didn't document it until the 387SX floating-point chip in 1987.19 Conclusions Floating-point has a long history before the 8087. For instance, the IBM System/360 mainframes (1964) supported 32-bit and 64-bit floating-point numbers. In 1977, AMD introduced the Am9511 floating-point chip, supporting 16- and 32-bit floating-point numbers, along with transcendental functions. What made the 8087 revolutionary is that it was carefully designed to be as mathematically accurate as possible, largely thanks to numerical expert William Kahan. (The 8087 led to the IEEE 754 Standard, now used by almost every computer and ending the anarchy of incompatible floating-point standards.) The 8087 ended up extraordinarily complicated with three different sizes of floating-point numbers, four sizes of integers, four rounding modes, infinity modes, a collection of exceptions that could be masked or unmasked, denormalized and unnormalized numbers, signed and unsigned infinities, signed zeros, and a whole family of Not-a-Numbers. These features combine, yielding many corner cases. The 8087 deals with this complexity both through specialized circuits and through tangled microcode. How complicated is the 8087? For users who didn't have an 8087 chip, Intel sold an 8087 Support Library that exactly emulated the 8087's instructions (but much slower). The emulator took 16K bytes of 8086 code, which was a lot when a full BASIC interpreter could fit in 8K. Another way of looking at this is that the hardware of the 8087 drastically reduced the amount of software required: the 8087 itself used 3.3K of microcode, compared to the 16K for the emulator in 8086 code. I plan to continue reverse-engineering the 8087 microcode; for updates, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. I've been working on this with the members of the "Opcode Collective", especially Smartest Blob and Gloriouscow, who converted the ROM images to microcode data and extensively analyzed the contents. See the 8087 repository on GitHub for more. Notes and references The 8087 patents provide some details on the hardware, but unfortunately not the microcode. The patent diagram below shows the architecture of the 8087; I've highlighted the relevant parts. The fraction bus and exponent bus are shown in red. The adder and associated registers are in yellow. (For subtraction, the B register selector selects the complement.) The shifter is in green. The exponent constant ROM and the exponent converter are in orange. The temporary registers and stack registers are in blue. The architecture of the 8087. Based on the patent. Click this image (or any other) to magnify. ↩ The exponent converter is surprisingly complicated because the 8087 has three different formats for floating-point numbers with three different sizes of exponent fields (8 bits, 11 bits, and 15 bits). Moreover, the different sizes of exponents are stored with different biases. Thus, converting between different sizes of exponents is not trivial. The exponent converter also recognizes overflow and underflow for the different exponent sizes, as well as special values such as infinity and NaN. I plan to describe the exponent converter in more detail later. ↩ The significand in the 8087 is nominally 64 bits wide. However, the 8087 uses three extra low-order bits for rounding, called Guard, Round, and Sticky. These bits ensure that a value is always rounded in the right direction. Some parts of the datapath have additional bits for sign or overflow: the shifter is 68 bits wide, and the adder is 69 bits wide. For the most part, I'll ignore these extra bits and refer to the datapath as 64 bits wide. ↩ Tags are normally invisible to the programmer, but can be accessed through special operations. Specifically, a programmer can dump the 8087's state to memory; the tags are stored in a 16-bit "tag word". ↩ The external representations of floating-point numbers have an implied leading one, with only the bits after the binary point explicitly stored. This provides one additional bit of resolution "for free". The internal 80-bit representation, however, has an explicit leading one to simplify calculations. ↩ One reason that the exponents are biased is that to find the larger of two floating-point numbers, you can compare them lexicographically as signed integers, rather than needing to examine the exponents separately. ↩ Most of the 8087's instructions are implemented in microcode, but a few are hard-wired. For more details on instruction decoding, see Instruction decoding in the Intel 8087 floating-point chip. ↩ I use decimal addresses for the microcode because the Opcode Collective started using decimal addresses, and it would be confusing to change now. ↩ The microcode shows that scaling 0 by anything, or scaling anything by 0, leaves the value unchanged. My view is that the designers took a shortcut here, rather than returning the "right" value. Since the 8087 defines 0×∞ as NaN, it seems to me that 0×2∞ should also be NaN, so FSCALE(0, ∞) should be NaN, not 0. The designers probably made the valid decision that nobody really cared about corner cases on the obscure FSCALE instruction. For other instructions, the behavior with denormals, unnormals, and zeros is documented (tables S-24 to S-26 in the Numerics Supplement documentation), but FSCALE is omitted. ↩ The 8087 has separate buses for the exponent and the significand, and the adder is only connected to the significand bus, so how does the exponent get to the adder? The trick is that there is a 16-bit gateway between the exponent bus and the significand bus, so the exponent can be copied over. ↩ I described the 8087's adder here. In brief, subtraction is performed by inverting the B register's value when it is fed into the adder. The carry-in to the adder is set to 1, so this in effect performs a two's-complement subtraction. ↩ Why does the microcode have separate paths to add a positive scale and subtract a negative scale? The reason is that values are stored as a sign bit and an unsigned value, not two's complement like standard integers. As a result, the adder can't perform signed addition directly. Instead, the adder circuitry must be explicitly directed to complement the B register value and perform a subtraction. ↩ This flowchart shows the SPECIAL_TMPS subroutine. The structure of this routine is complicated because paths split off and rejoin. One tricky path is the code to determine if there are 0, 1, or 2 NaN values, and take the maximum NaN if there are two. Another complication is the exception exits, which raise an interrupt if the interrupt is not masked, but not for a comparison instruction. The two return values are returned through flag and expConv. A flowchart for the SPECIAL_TMPS subroutine. Click for a larger version. The actions of SPECIAL_TMPS are summarized below. It returns status through the flag flip-flop and the exponent converter register (expConv). Its actions are: table.status {border-collapse: collapse;} table.status tr:first-child {border-bottom: 1px solid #ccc;} table.status th,td {padding: 0 10px; text-align: center;} table.status th:first-child,td:first-child {border-right: 1px solid #ccc;} InputResultflagexpConv emptyNaN, exception11 NaN(larger) NaN, exception11 infinityinfinity01 denormunnorm10 div abnormalno change00 (The last row signals an abnormal value during division computation; I'm still investigating this.) ↩ Prof. William Kahan, who guided the development of the 8087, was disappointed that some floating-point features were unused because of a vicious circle: the features didn't receive good compiler support, so programmers didn't use the features, so compiler developers claimed a lack of demand for the features and didn't implement support. Using multiple values of NaN to record how and/or where an NaN came into existence was an example of a feature that lacked software support. See Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic for a detailed discussion of NaN and other issues. ↩ The 8087 makes heavy use of micro-subroutines, with a 6-level stack for microcode subroutine calls. Microcode jumps and subroutine calls get the address from a jump table. The index from the jump table comes from 6 bits of the micro-instruction. We unimaginatively named the entries in the microcode jump table as J0, J1, and so forth based on the index, but I'm adding more meaningful names as I figure them out. As for the names for micro-instructions, we don't have any information on what names were used by Intel (unlike the 8086). I invented names, influenced by the names in Gloriouscow's disassembly. ↩ A subroutine that I call SPECIAL_VAL handles denormalized values, infinity, and NaN. (This subroutine is primarily used by SPECIAL_TMPS, but is also used by FRNDINT (round to integer) and FSQRT.) First, the subroutine looks at the exponent of tmpA; if the exponent is zero, the value is denormalized. (The value could also be zero, but that was handled earlier.) If so, the denorm exception is set. Comparison instructions such as FCOM handle denorms differently, but I'll ignore that for now. The code at #1576 tests if the denorm triggered an interrupt; if so, the instruction ends with the interrupt. If the interrupt was masked, the code converts the denorm to an unnorm by changing the tag to norm and changing the exponent to 1 (which corresponds to the very negative, smallest valid value because of the exponent bias). The result of the subroutine is returned through a special flag flip-flop. SPECIAL_VAL (J12): #1572 tmpA:exp -> sumreg:frac Handle special value #1573 jmp #1581 if not Sum register is zero Test exp for denorm #1574 except:denorm #1575 jmp #1577 if compare instruction No exception for comparison #1576 jmp #1571 if DE (denormalized) interrupt RNI if exception #1577 norm -> tmpA tag Handle denorm: tag empty? or valid? #1578 1 -> tmpA:exp Change to unnorm #1579 0 -> flag Clear flag #1580 return #1581 shift tmpA:frac L 0 bytes, 1 bits Shift to check if infinity vs NaN #1582 shift L -> sumreg:frac #1583 jmp #1579 if not Sum register is zero Clear flag for NaN #1584 1 -> flag Set flag for infinity #1585 return At #1581, the code checks if the value is infinity or NaN. Interestingly, this test isn't done directly, but by manipulating the value with the shifter. Recall that infinity has a significand of 10...00, while NaN has at least one additional 1 bit. The code shifts the significand one bit to the left; a zero result indicates infinity, while a nonzero result indicates NaN. As before, the result is returned in the flag flip-flop. ↩ The logic to compute the rounding bits is more complicated than described. There are two micro-instructions with slightly different behavior depending on the expConv value, but I won't get into that here. ↩ The ADJUST_PRECISION subroutine appears to return infinity if the significand overflows after rounding up, but I'm not entirely happy with this. For instance, 1.111... should round up to 2, not infinity; the significand overflows, but that's not an overflow of the float. Presumably, this gets fixed somewhere else. ↩ I don't know why Intel failed to document the feature that a condition code indicates whether a value was rounded up or down. The 8087 documentation is very thorough with corner cases; usually, when I find a strange circuit, I can find a line in the documentation that explains why it is there. Maybe the condition code feature was buggy, so it was easier to not document it? Maybe this feature was a hidden trap to catch competitors that copied the chip? (Intel had a secret instruction in the 8086 for this purpose, but NEC's version of the 8086 didn't have it, much to the disappointment of Intel's lawyers.) Maybe Intel wasn't sure if they wanted to support the feature in later versions? (This is why some of the 8085 processor's instructions weren't documented.) For now, it's a mystery. ↩
Spacelab was a reusable laboratory that could be carried in the Space Shuttle's cargo bay, providing lab space for astronauts and experiments.1 Because Spacelab was a European project, it used a French-built minicomputer, the Mitra 125 MS,2 rather than the Shuttle's main computers, IBM-built AP-101 systems. For storage, the Spacelab computer contained 128 kilobytes of RAM. Rather than silicon memory, the computer used magnetic core memory, with each bit stored in a tiny ferrite ring. In this article, I take a close look at this computer's core memory system. The core stack from the Spacelab computer. I removed the top board to show the core planes. The illustration below shows how Spacelab fit inside the Shuttle's cargo bay. The pressurized laboratory is the cylindrical module in the front of the cargo bay, connected to the Shuttle by a tunnel. Experiments were mounted on pallets behind the laboratory. The laboratory held three identical Mitra computers.3 One computer managed Spacelab itself, while the second computer managed the experiments. The third computer provided a backup in case of failures. Spacelab was a pressurized cylinder in the Shuttle's cargo bay, connected to the Shuttle by a tunnel. It provided a laboratory for researchers to perform experiments. This illustration of Spacelab is from NASA, C-1976-4380. The photo below shows the core memory stack, removed from the computer. The core memory stack takes up roughly a third of the computer. The entire side panel of the computer detaches, and the core memory unit slides out. Since the computer is cooled by conduction, firmly attaching the core memory stack to the side panel kept it cool. The core memory stack consists of seven boards: a driver board, four core plane boards, a second driver board, and an interface board. Each board has two 160-pin connectors that plug into a large daughter board on each side, providing extensive connectivity between the boards. The daughter board on the right has another 160-pin connector that links the memory stack to the rest of the computer. (These connectors are the long blue connectors in the photo.) The core memory stack in front of the Mitra computer. The circuit boards have been removed from the far side of the computer. How core memory works One of the hardest problems for early computers was storage. Computers of the late 1940s and early 1950s stored data through techniques such as sound waves in mercury, spots on a CRT screen, or spinning magnetic drums, but these all had limitations. What computers needed was dense, inexpensive storage that was fast, reliable, and could be accessed randomly. During World War II, Germany developed special magnetic alloys that could "flip" from one magnetic state to another. After the war, American researchers realized that these materials could be used for storing binary data: "It was completely obvious that you could make a memory with this material," in the words of Jan Rajchman. Different aspects of core memory were patented by various inventors (including independent inventor Frederick Viehe, An Wang at Harvard, Jan Rajchman at RCA, and Jay Forrester at MIT), leading to expensive patent battles. (IBM ended up paying $400,000 to Wang—who used the money to build the computer company Wang Laboratories—and $13,000,000 to MIT.) I view Jay Forester as the most important inventor, developing the design of practical core memory, researching magnetic materials, and building the first core memory in 1953 for the groundbreaking Whirlwind computer. Core memory is based around a tiny toroidal magnetic core, one per bit.4 A core can be magnetized clockwise or counterclockwise to store a bit. The core can be magnetized by threading a wire through the core: running a current through the wire produces a magnetic field that magnetizes the core, while running a current in the opposite direction produces the opposite magnetization. A key problem with core memory was how to wire the cores without an absurd number of wires: if each core had a separate wire, just 16 KB of storage would require over 100,000 wires. The solution was called "coincident current addressing". The cores are arranged in a grid, with horizontal and vertical wires, as shown below. By running a current through one horizontal wire and one vertical wire, the single core at the intersection was selected. But wouldn't that magnetize all the cores along the horizontal and vertical wires? The key was that the cores were constructed from special magnetic materials with a property called hysteresis: a small current leaves the core completely unchanged, while a larger current flips the core's magnetic state. The currents through the horizontal and vertical wires were carefully selected so each wire had half the current necessary to flip the core; where the wires intersected, the two currents provided sufficient magnetic field to flip the core. Energizing an X drive wire and a Y drive wire selects one core, highlighted in yellow. Diagram adapted from iDigital Computer Components and Circuits, R. K. Richards, p355 The next step was reading the core. A sense wire was threaded through all the cores in the two-dimensional plane. To read a core, the X and Y select wires were driven to flip the desired core to the 0 state. If the core was already in the 0 state, nothing happened. But if the core was originally in the 1 state, the magnetic field changed as the core changed state. This induced a small current in the sense line, indicating that the core held a 1. Note that reading the value of a bit destroys that value. Thus, a core needs to be rewritten after reading, to restore the original data. To access a word of memory at a time, core planes were combined into a three-dimensional stack (below). Since each plane held one bit of the word, a 16-bit word would have a stack of 16 planes. All the planes shared the signals to drive the X and Y lines, so a one-word column through the stack was accessed in parallel. Each plane had a separate sense line to read out the bit. The core stack from the Saturn V LVDC (Launch Vehicle Digital Computer) consists of 14 core planes. This stack is at the US Space & Rocket Center. Photo from NCAR EOL. I retouched the photo to reduce distortion from the plastic case. But how do you write different values to the different bits? The trick was to put an "inhibit" line through all the cores in a plane, running the inhibit line in the opposite direction to the X lines. Putting a current through the inhibit line would cancel out the current through the X line, preventing the core in that plane from being modified. To summarize, a read-write cycle consisted of first energizing a pair of X and Y lines to select a word and write a 0 to the column of cores in that word. The sense lines provided a readout of the bit values. Next, the X and Y lines were energized in the opposite direction to write a 1 to the cores. At the same time, the inhibit lines were energized for each plane with a 0 bit. Thus, the cores either flipped back to 1 or stayed at 0, as required. Many core memories, such as the one below, used a shared wire for sense and inhibit, so there were three wires through each core. Closeup of an IBM 360 Model 50 core plane. The cores in this computer were called 19-32 because their inner diameter was 19 mils and their outer diameter was 32 mils (0.8 mm). The final ingredient to make core memory practical was the diode matrix. The X and Y lines require driver circuits that can produce fast, bidirectional high-current (e.g. 600 mA) pulses. A core memory plane can have hundreds of these lines. Providing a separate driver for each wire would be very expensive, especially in the vacuum tube era. The solution was to put separate drivers at each end of the wire, with each driver supporting multiple wires. For a trivial example, suppose you have 9 vertical lines. Put three drivers (A, B, and C) on the top, each connected to three wires, and three drivers on the bottom (1, 2, and 3), each connected to three wires. By energizing a driver at the top and a driver at the bottom (e.g. B and 1), the corresponding wire will be energized. Now, N drivers on each side control N2 wires, supporting N4 cores in total. Illustration of how "top" and "bottom" drivers work together to select a single line (red) through the core matrix. However, current can take alternate paths, such as the pink path. Unfortunately, it's not quite that easy. Current can take "sneak paths" through the cores, such as the path in pink above. The solution is to add diodes to ensure that current can't take the wrong path. Since a wire needs to be driven with currents in both directions (to flip cores both ways), two diodes are required on each wire, as shown below, one in each direction. Each matrix input (A, B, etc.) is replaced with two inputs, one to drive each direction. (The horizontal wires also require diodes, not shown.) Adding diodes ensures that current only takes the desired path. Since each wire requires two diodes, core memories used many diodes. Fortunately, diodes were small and inexpensive, so a large quantity of diodes was manageable. The photo below shows the diode stack for the computer used in the Saturn V rocket, the Launch Vehicle Digital Computer. Closeup of the diode matrix in the Saturn V LVDC. Diodes are mounted vertically using cordwood construction between two printed circuit boards. Originally, core memories were tediously constructed by hand. For the Whirlwind computer, it took a full 40 hours to wire a 64×64 core plane. Companies such as IBM soon developed automated techniques to manufacture core memory, and the price dropped by a factor of two every two years, similar to Moore's Law.5 Core memories became fast, inexpensive, and reliable, and were the most popular form of main-memory storage until semiconductor memory took over in the 1970s. The Spacelab computer's core memory The Spacelab computer's memory was manufactured in 1980, a late date for core memory, so it is advanced and high density. The photo below shows one of the four core plane boards from the computer. Each board holds 16K of 18-bit words (32 KB), so the computer has 128 KB of RAM in total. The computer is a 16-bit computer, but each word also has a parity bit and a "storage protect" bit, bringing the total to 18 bits. (The storage protect bit provided write protection on a word-by-word basis, preventing programs from being accidentally overwritten. Because core memory is nonvolatile, a program could be loaded into memory once and would be immediately available every time the computer was powered on.) One of the core memory boards from the Spacelab computer. The core memory board is arranged with 1024 vertical (Y) wires and 288 horizontal (X) wires, supporting 294,912 lithium ferrite cores. These very thin wires are soldered to tiny pads on the printed-circuit board. The board supports 18 bits, which is visible as 18 alternating stripes of green and copper because alternating sense lines have different colors. The board has 36 sense lines: the left and right halves of the board have independent sense lines to reduce noise, so the board has 36 sense lines for 18 bits. The sense wires pass through four holes in the board (green arrows) and are soldered on the back of the board. The photo below shows a close-up of the cores. Each core is approximately 32 mils (0.8mm) in diameter, the same as the IBM System/360 cores shown earlier. However, the cores are stacked much closer, with only a small gap between cores. The X and Y select lines are copper-colored, while the sense lines are green. (The wires are all enameled to prevent short circuits.) The sense wires loop around at the left, forming a single circuit through each bit section. Half the Y lines form loops at the bottom; the other half form loops at the top. Thus, each Y line passes through the plane twice in a U-shaped path, which will turn out to be important. A close-up of the cores. I think that some rows tilt left and some tilt right to ensure that the sense lines keep the same polarity when they switch direction. Photo courtesy of CuriousMarc. The other side of each circuit board holds the sense amplifiers and the diode matrix for the core plane. The diode chips are the square black packages, each containing 16 diodes for 8 core lines.6 In the red-outlined regions, one end of each vertical U-loop is connected to a diode chip; the lines of diagonal holes are the vias that pass each signal through the board. The other end of each vertical U-loop is connected to one of the blue board connectors on the side; these vias are in the blue-outlined regions. The horizontal lines use the diode chips and vias in the green regions. One end of each line is connected to a diode chip, while the other end is connected to a board connector through traces on the other side. Note that some vertical lines connect to the diode chips at the top of the board, while others connect at the bottom. Similarly, some horizontal lines connect at the left while others connect at the right. The back side of the core plane board holds the diode matrices and sense amplifiers. The central region (yellow) holds 18 sense amplifier chips, the black DIP integrated circuits, each containing two amplifiers.7 The white packages are resistor packages, holding multiple resistors to bias and terminate the sense amplifier lines. The wires from the sense amplifiers are connected as twisted pairs that are soldered to the board right next to the corresponding sense amplifier chips. Using twisted pairs for the whole distance prevents the wires from picking up electrical noise, which could overwhelm the tiny signals in the sense wires. The sense wires pass from one side of the board to the other through four holes in the board (yellow arrows), and then are glued down as they traverse a significant distance on the board. (It must have been difficult to manufacture the board without breaking the tiny, fragile wires.) Each sense wire loop forms a twisted pair that is fed to the other side through a hole in the circuit board. Above the hole, you can see a gray blob where sense wires were spliced for some reason. Also note how alternating vertical wires are soldered to the circuit board, with circular vias connected to the other side. The other vertical wires form loops. are soldered to the circuit board Detecting signals on the sense lines is tricky because the pulses are very small, a few millivolts. Because the sense lines run next to the X drive lines, they can easily pick up noise from the high-current pulses on the X lines. To minimize this noise, the sense lines cross each other between two plane sections, forming a "bow tie", as shown below. The result is that an X line runs next to the positive sense line for half the length and the negative sense line for the other half. Thus, the induced noise cancels out. A close-up of the sense lines. The 16 sense lines in the middle are green, while the sense lines above and below (as well as the X lines) are copper. Note that the sense lines cross, while the X lines continue horizontally. The large circles are vias through the board. The core memory in the Spacelab computer used a different architecture from a typical core memory, improving performance by eliminating the inhibit line. This architecture was called a 2½D memory.8 If you're familiar with core memory, the lack of inhibit lines may seem puzzling: how do you write 1 to some bits and 0 to other bits? The trick is to have separate X driver circuitry for each bit.9 When writing data, the X lines are only energized for bits that receive a 1; the other lines are left unenergized, so the bits remain at 0. The disadvantage is that instead of one set of X driver circuits, you now need one set for each bit, a factor of 18 more for an 18-bit word. However, with the development of core drivers on integrated circuits, the cost of the additional driver circuitry became less significant. The memory system used an technique called phase reversal to cut the number of vertical drivers in half. Recall that pairs of vertical wires are joined by a U-connection. By driving the wire in a particular direction, the left side or the right side of the pair can be selected. For example, the drawing below shows how the two wires select the left core, but not the right core. In the left core, both currents go through the core in the same direction, inducing a magnetic field in the toroid.10 But in the right core, the two currents cancel out, so there is no magnetic field created. But if the current in the vertical loop is reversed, the right core will be selected, rather than the left core. The point is that instead of using two drivers for the vertical wires, one driver is used, reversing the current to select the left or right core. Connecting pairs of vertical wires into a U-shaped loop lets each driver control twice as many cores. The diagram below shows the complex wiring for X drive wires. Each band of 16 wires corresponds to one bit in the 18-bit word, and has a separate sense wire. The top band of 16 X lines is connected to four contacts on the board connector; each contact is connected to four X lines through the curving PCB traces. The bottom band of 16 X lines is wired to diode modules on the other side of the board, connected through the round vias. (Each wire has the opposite connections—diode module or board connector—on the other end.)11 One group of four X wires is energized through the connector, while four wires are energized through the diode matrix, selecting one of the 16 X wires in the group. The PCB wiring for the X lines. Other boards in the memory stack The memory stack has seven boards in total, arranged as a driver board, the four core planes, a second driver board, and an interface board. I haven't examined these boards in detail, but I'll give some preliminary information. The photo below shows one of the two driver boards. It provides the high-current pulses for the X and Y select lines. The board is crammed with specialized core memory driver chips12, along with a few logic chips to control the drivers. It has separate drivers for the two ends of the select lines, allowing the matrix selection described earlier. One of the two memory driver boards. Click this image (or any other) for a larger version. Since there are two driver boards and four core memory boards, at first I thought that each driver board controlled two core memory boards. The configuration turns out to be more complicated, with one more layer of matrix selections to cut the number of drivers in half. To simplify slightly, consider the X lines on a core board to have left ends and right ends, both of which must be energized to activate a line. For the left ends, the first driver board powers core boards 1 and 2, while the second driver board powers core boards 3 and 4. The right ends are shuffled: the first driver board powers core boards 1 and 3, while the second driver board powers core boards 2 and 4. Now, if the first driver board powers the left and right ends, core board 1 is the only one with both ends active. If the first driver board powers the left ends while the second board powers the right ends, core board 2 is activated. Similarly, core board 3 or 4 can be activated. The point is that since each set of drivers is connected to two core boards, two sets of drivers are required instead of four. The final board is the interface to the rest of the computer. It has many transistor arrays in DIP packages, along with many resistors. It seems that the board uses discrete transistors to drive the bus, rather than using interface chips, which is unexpected. The board has some wire-wrapped jumpers in the lower center region, presumably for configuration. The interface board has some unused space in the lower left. Conclusions Core memory had a long life, surviving even as computers migrated from vacuum tubes to transistors and then integrated circuits, but eventually semiconductor memory made it obsolete.13 Core memories lasted even longer in aerospace applications since it had two key advantages over semiconductor memory: it retained data even without power, and it was resistant to radiation. The Spacelab computer, manufactured in 1980, was near the end of core memory's reign, so it is more advanced than a typical core memory system, with higher density, extensive use of integrated circuits, and the 2½D architecture. But eventually the high density, low cost, and low power consumption of semiconductor memory won out. In 1991, the Space Shuttle flew with upgraded main computers, the IBM AP-101S that used semiconductor memory instead of magnetic core. Spacelab's Mitra computers were also replaced, using the AP-101SL, which was based on the AP-101S but modified to support the instruction set and peripherals of the original Spacelab computer.14 Although core memory is now firmly in the past, it still lives on in the expression "core dump". I plan to investigate the Spacelab computer some more. For updates, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. Credits: Thanks to Steve Jurvetson for providing the Spacelab computer. Thanks to CuriousMarc for photography and help disassembling the computer. AI statement: Despite the presence of the em dash, no AI was used in the writing of this article (details). Notes and references It seems that 16 Shuttle flights used the Spacelab pressurized module, while 6 or 9 flights just used the unpressurized Spacelab pallets. (Why do sources never agree?) Originally, Spacelab was expected to be used for 30 flights every year (Status Of The Spacelab Program, 1974). ↩ The Spacelab 125 MS computer was built by a French company called CIMSA, using the Mitra architecture created by CII. I explained the complex history of these companies in my previous Spacelab computer article, so I won't go into it here. On the ground, the Spacelab project used Mitra 125 S computers that were functionally identical to the Mitra 125 MS (details) computers that were used in space. A core memory board from a Mitra 125 S ground computer was described on EEVblog (video, video). The computers had identical architectures, but the 125 MS was militarized and designed for "severe environmental conditions" (details). The EEVblog memory board was manufactured by Ampex and has a different design from the board that I examined. The Mitra 125 S memory board, built by Ampex. Screenshot from EEVblog #668. ↩ Spacelab was modular, so it could be be flown in different configurations. The habitable module could be flown in two different sizes, with experiment pallets mounted outside the module. Spacelab could also be flown without the habitable module, with experiments controlled from inside the Shuttle. In this case, the computers and other equipment were mounted in a smaller pressurized cylider called the "igloo". ↩ I'm describing "standard" core memory, but there were many esoteric designs for core memory. One approach used two cores per bit. Another approach used cores with multiple holes, such as cubical BIAX cores, transfluxors with a large hole and a small hole, or IBM's three-hole design. Many of these approaches could read a core without erasing it (non-destructive readout), but almost all cores used standard toroids. ↩ Later, companies discovered that it was cheaper to have core memories hand-manufactured in Asia and moved away from automated production. (See Memories that Shaped an Industry, p. 251. If you're interested in the history of core memory, this is the book to read.) ↩ The diode array chip is marked FSA2977 and contains 16 diodes, 8 common-cathode and 8-common anode. Pins 2 through 9 are connected to eight core wires. Pin 1 is driven high, or pin 10 is driven low, depending on the desired current direction. I couldn't find a datasheet for this part, but it appears to be similar to the Motorola MAD1103 Core-Driver Diode Array or the Silicon General SG5772F. ↩ A schematic matching the diode array, from the Motorola MC1103P datasheet. The sense amplifiers are National Semiconductor DS5534 chips. Each IC contains two differential amplifiers, converting the tiny sense signals into logic signals. The strobe signals indicate when the amp should read a bit; the strobes come from the IC on the left side of the board, a 54150 dual 4-input NAND gate, 50Ω line driver. Diagram of the sense amplifier, from the National Interface Integrated Circuits Databook. ↩ The 2½D memory architecture is described in detail in 2 1/2 D High Speed Memory Systems—Past, Present, and Future. Due to complicated factors and tradeoffs, the 2½D approach was attractive for systems of 16 Kword storage and above. In particular, eliminating the inhibit line boosted performance. IBM's Large Capacity Storage system used a 2½D architecture with just two wires per core to provide a megabyte of storage at a comparatively low cost, sharing the X line with the sense line. However, this approach turned out to be slow, so using three wires per core (as in the Spacelab computer) was more common. ↩ Note that the 2½D architecture requires separate per-bit drivers along one axis, not both. Since cores require two currents to flip, inactivating one axis is enough to prevent the corresponding cores from flipping. ↩ The direction of the magnetic field is given by the "right-hand rule": if you point the thumb of your right hand in the direction of the current, the magnetic field curves around the wire in the direction of your fingers. It may not be obvious how the currents add or cancel when the wires are in different directions. You can imagine moving the two wires until they are parallel, and then see if the currents are in the same direction or opposite. (This follows from Ampère's law, which states that the magnetic field around a curve (e.g. the core) is proportional to the net current through the corresponding surface.) ↩ For reference, this footnote describes the details of the core plane wiring, probably in more detail than anyone wants. For the Y lines, there are 1024 vertical lines, forming 512 U-shaped loops. Half of these are connected at the top, and half at the bottom. One end of each loop is wired directly to a connector on the side, while the other end connects to a diode matrix. The connectors provide 32 lines that can act as a source or a sink. Each of the 32 lines is connected to 16 vertical wires, for 512 vertical wires in total. Each quadrant of the board has 8 of the 32 lines, connected to a group of 8 vertical wires, a second group of 8 vertical wires, and so forth for 16 groups. For the diode connections, the connectors provide 16 source lines and 16 sink lines. Each diode chip has one source line and one sink line, feeding 8 vertical wires. Each source and sink line is connected to four diode chips, one in each quadrant in a mirrored pattern. Thus, the 16 source lines and 16 sink lines are connected to 64 diode chips, feeding 512 vertical loops. (Since each quadrant of the board has unique direct connections and the diode connections within a quadrant are unique, a unique core is selected. Specifically, 32 direct connections times 16 diode connections gives 512 combinations to select a vertical loop. The polarity selects which half of the loop is active, uniquely selecting one of 1024 vertical wires.) For the horizontal wiring, the 288 wires are grouped into 18 bands (one for each bit), with 16 wires per band. Each band has four direct signals from the connector. Each one is connected to four horizontal lines, 16 in total. (The visible PCB traces (shown earlier) connect the 16 wires to four connector pins (A,B,C,D) in the pattern AABBCCDDDDCCBBAA.) For the horizontal diode connections, the connector provides 4 source wires and 4 sink wires, which feed the 16 horizontal wires in a pattern 1234123412341234. By energizing the appropriate direct and diode wires on either side, one of the 16 lines is selected. One complication is that each diode chip has 8 outputs, but each source/sink goes to 4 wires. The solution is that each bit group uses half of four diode chips (4 outputs from each). Thus, the four source and sink wires are shared across two bit groups. This is not a problem for selection because the direct connections control whether the bit is active or not. The horizontal diodes are arranged asymmetrically. The left side has 8 diode chips at the top and 8 at the bottom, supporting 8 groups of 16 wires. The right side has 20 diode chips (4 additional in the middle), supporting 10 groups of 16 wires. Thus, all 18 bit groups are supported, with some asymmetry in the board layout. The left and right sides of the core plane have separate sense lines, so there are 36 sense lines in total. These go to the 18 dual sense amplifiers. Each sense amplifier has two outputs connected, a wired-OR to combine the left-hand data with the right-hand data, providing 18 bits of output to the connector. A diagram showing the topology of a core board. Click this image (or any other) for a larger version.) The diagram above summarizes the structure, showing one of the 18 bits. It omits the details of which connections are at the top, bottom, left, or right. ↩ Each driver board has 53 core driver chips of type SN55325. The SN55325 core driver chip, from the databook. Each chip has two 600 mA "sources" and two 600 mA "sinks" connected to two outputs. By energizing a source on one end of a line and a sink on the other end, the line can be driven in the desired direction. The driver board also has 39 driver chips of type SN55327. These chips are similar, except they can be used as either four sources or two sinks. These chips are used for the diode matrix inputs, where an input is either a source or a sink. ↩ I wrote about the Spacelab computer's CPU earlier. I've written about other core memory systems including the IBM 1401 core memory, IBM 360 core memory, Saturn V LVDC, and Apollo Guidance Computer. ↩ The Space Shuttle's replacement AP-101S computer used semiconductor memory, so it needed to deal with volatility and radiation. The new computer used battery backup to preserve memory contents when powered off, a feature that core memory had provided automatically. To avoid data corruption from radiation, the new computer had six extra storage bits for each word to implement an error-correcting code. The computer constantly scanned for bit errors and corrected them. Radiation wasn't just a theoretical risk: a single Shuttle flight could encounter over 100 bit flips due to radiation (details). For more information on the AP-101S computer, see my previous article, The rise and fall of IBM's 4 Pi aerospace computers. I wrote about Spacelab's original computer and the upgraded AP-101SL computer in Reverse engineering circuitry in a Spacelab computer from 1980. ↩
In 1948, IBM introduced the 604 Electronic Calculating Punch. This machine was a programmable calculator, about the size of a double refrigerator. It was not quite a computer, but was programmed by plugging wires into a plugboard. This machine read numbers from a punch card, performed up to 60 calculations on these numbers, and then recorded the results by punching holes in the card.1 It processed 100 cards per minute—over one card per second—and IBM advertised it as the equivalent of 150 engineers. The machine rented for $550 a month, making it very popular, with over 5600 units produced.2 The IBM 604 Electronic Calculating Punch. Photo from Ed Thelen's IBM 604 page. The IBM 604 came out just after the transistor was invented, too early to use transistors. At the time, calculators and computers were moving from slow electromechanical components to fast vacuum tubes. One of the innovations of the 604 was to combine a vacuum tube and its associated circuitry into a pluggable module. Along the left side of the photo above, you can see rows of these modules with the handles sticking out, making it easy to replace a faulty module. More modules are behind the silver metal covers. In total, the IBM 604 used about 1300 vacuum tubes. The photo below shows a pluggable tube module, with a vacuum tube underneath the insulated handle. The nine pins at the bottom of the module plugged into a socket in the 604, with the sockets connected by backplane wiring. The vacuum tube was also socketed, so a bad tube could be quickly replaced. At the left, the resistors and capacitors are mounted on insulating wafers. Modules provided a dense way to implement circuits, packing components into three dimensions. The TR-3 trigger module from the IBM 604 Electronic Calculating Punch. Each pluggable tube module implemented a specific function, such as an inverter, amplifier, or power driver. The module above is a "trigger" module, type TR-3. A trigger is a circuit with two states—on and off—and can be switched from one state to the other, providing one bit of temporary storage. (In modern terminology, this is called a flip-flop.) Triggers were important building blocks in the 604, generating timing signals and storing pulses. Arithmetic in the IBM 604 was implemented with decimal counters, built from TR3 triggers. In this article, I describe the circuitry of the TR-3 trigger module. (I recently wrote about a thyratron module in the 604; this is a different module.) After reverse-engineering the module, I powered it up. The video above shows the module in operation. By pressing buttons, I switch the trigger from one state to the other. Glowing orange neon bulbs show the state of the trigger module. The fundamental feature of the trigger is that it stays in a state until I push the other button. This might appear trivial, but the ability to store information is vitally important for computation. How a vacuum tube works The trigger module uses a common type of vacuum tube called a triode, which amplifies a weak signal to control a stronger signal. The diagram below shows the construction of a triode vacuum tube. The heater is a filament, similar to an incandescent light bulb, that heats the cathode to roughly 750 ºC. At this high temperature, the cathode emits electrons. If a large positive voltage (say, 150 volts) is put on the plate, the negatively charged electrons are attracted to the plate. The stream of electrons from the cathode to the plate causes a current to flow through the tube. Since air would block the electrons, the fragile glass envelope holds a vacuum, giving the vacuum tube its name. The current is controlled by the grid: if a small negative voltage is placed on the grid, it repels the negative electrons, preventing them from reaching the plate and blocking the current through the tube.3 Thus, a small signal on the grid controls the large current through the tube. The components of a triode vacuum tube. From IBM 604 Customer Engineering manual. The advantage of vacuum tubes was that they could switch on and off millions of times per second, phenomenally faster than electromechanical devices such as relays. The clock speed of the IBM 604 was 50 kilohertz, much below what a tube could handle, but three orders of magnitude faster than the 50 hertz pulses in an electromechanical accounting machine like the contemporaneous IBM 407. The tube that I used in the module is called a 2033.6 This tube is a dual triode, combining two triodes into one physical glass tube. Dual triodes were very popular because they doubled the density of the circuitry. In the photo below, the two vertical black structures are the plates of the two triodes; the other structures are not visible as they are inside the plates. The 2033 dual-triode vacuum tube. This tube is a "miniature" vacuum tube, about 5 cm long including the seven pins at the bottom of the glass envelope.4 Since a single triode has five connections, you might wonder how a dual triode manages with seven pins instead of 10. The trick is that both triodes share the cathode and heater connections, which limits the tube to applications that don't require separate cathodes. One disadvantage of vacuum tubes is that the heater uses considerable power. This tube's heater requires 6.3 volts at 300 milliamps—almost 2 watts per tube. Using 6.3 volts may seem a bit random, but many vacuum tubes used this voltage for historical reasons: this was the typical voltage provided by a 6-volt automobile battery.5 In the photo below, you can see the orange glow from the two heaters, mostly hidden by the plates but visible at the top and bottom. The tube powered up, showing the glowing filaments. Inverters and the trigger circuit The trigger circuit is based on two inverters, so I'll start by explaining the tube inverter circuit.7 The idea of an inverter is to amplify and invert the input signal: a "low" input results in a "high" output and vice versa. First, consider a low input: if a negative voltage is applied to the grid, the flow of electrons is blocked, turning off the tube. In this case, the resistor pulls the output high with 150 volts. However, if a positive voltage is applied to the grid, the tube turns on and conducts current. This current pulls the output down, due to the voltage drop across the resistor, producing a low output of 50 volts. Thus, a low input causes a high (150 V) output, while a high input causes a low (50 V) output, providing the desired inverter action. Note that the input signal has a swing of over 50 volts, very large compared to a transistor circuit. Moreover, the output voltages are much higher than the input voltages, which is somewhat inconvenient when connecting circuits. An inverter circuit. Adapted from IBM 604 CE Manual. A trigger is constructed from two inverters connected in a loop. The output of the first inverter is fed into the second inverter, and the output of the second inverter is looped back to the first inverter. If the first inverter has a high output, the second inverter has a low output, which is fed back to the first, maintaining the high output from the first inverter. The situation is similar but opposite if the first inverter has a low output. Thus, this circuit has two stable states, with one inverter on and the other off.8 Once the circuit is placed into a state, it will remain in that state until forced into the other state. Two inverters in a loop can store a 0 or a 1. I reverse-engineered the TR-3 module, creating the schematic below.9 It's a bit tricky to see the loop of inverters because the two inverters share one tube and are wired in a cross-coupled arrangement. In brief, one inverter uses the left half of the tube and the other uses the right half. The plate output from one side is wired to the grid input on the other side, through 200K and 1K resistors. The two module outputs (pins 7 and 8) are taken from the plates, but output 8 has a resistor between it and the plate. As a result, the two outputs provide different voltage levels, making the module more flexible to use.10 The two inputs force the trigger into one state or the other. The inputs are connected through 40 pF capacitors, providing AC coupling so the inputs can use different voltage levels from the outputs. Reverse-engineered schematic of the TR-3 trigger module. Note that the pin numbers for the module are different from the pin numbers for the tube. One tricky part is the connection between one inverter's output and the other inverter's input. The problem is that the output voltage is 50 to 150 volts, but the input grid voltage must be close to zero (a bit positive or a bit negative). The solution is to use a large negative voltage (-100 volts) and a resistor divider as a level shifter. With a large positive voltage from the plate and a large negative bias voltage, the resulting grid voltage ends up being moderately positive or moderately negative. As a result, the circuit requires both a high positive voltage (for the plate) and a high negative voltage (for the bias), complicating the power supply requirements. The inputs are fed into the grid through capacitors, allowing a pulse to pass through the capacitor to the grid. You might expect that a positive pulse would turn on the triode, but the module was used in the opposite way, with a negative pulse to turn off the triode. (This direction is more sensitive, because a tube has more gain when it is on.) Thus, a negative pulse on the left input will turn off the left side. The plate output of the left side goes high, pulling the gate of the right side high, turning the right side on. The plate output from the right side goes low, pulling the gate of the left side low, keeping the left tube off. Similarly, a negative pulse on the right input turns off the right side, causing the left side to turn on. Multiple pulses have no effect; that side remains off. Positive pulses also have no effect; the circuit is designed so a positive pulse is not sufficient to turn a triode on.11 I found the trigger circuit to be somewhat temperamental: the trigger needs to be stable enough to stay in one state or the other, while also unstable enough that an input pulse will reliably flip it to the other state. The circuit depends on carefully balancing the grid voltages and the input voltages. I experimented with different supply voltages and found that in some cases the trigger would oscillate, while in other cases, the trigger would get stuck in one state. Interestingly, the later IBM 650 computer abandoned this type of trigger circuit, instead using diode logic (AND and OR gates) to set and reset a loop of two inverters. With this type of trigger, the state is determined by reliable Boolean logic, rather than analog interactions of changing voltages. Conclusion The development of the trigger is an under-appreciated step in the history of digital computers. Because the trigger holds information—state—it can be used to create a state machine. This allows a computer to perform operations step by step, rather than a jumble of actions all happening at the same time. The trigger circuit dates back to 1918, when two British physicists, William Eccles and Frank Jordan, invented a circuit that used two cross-coupled triodes to create a circuit with two stable states. They viewed this circuit as a type of relay, triggered by a small signal and retaining its state until it was reset. They patented the circuit (Improvements in ionic relays) and wrote about it: A Trigger Relay Utilising Three-Electrode Thermionic Vacuum Tubes. (The Eccles-Jordan trigger circuit below is conceptually similar to the TR-3 trigger module, using cross-coupled triodes. One difference is that the input is coupled with a transformer.) A diagram of the Eccles-Jordan trigger relay, from their 1919 paper. The Eccles-Jordan trigger eventually led to digital counters. In 1939, the journal Electronics published an article Trigger Circuits, describing how trigger circuits could be combined to construct high-speed counters. One problem was that triggers can be easily combined to count in binary, but in the 1940s, calculating and accounting machines generally used decimal numbers, not binary. In the groundbreaking ENIAC computer (1945), bulky counters were constructed by putting ten triggers in a ring to count each decimal digit. IBM engineers invented a more efficient decimal counter that used four triggers instead of ten, coming up with binary-coded decimal (BCD) and obtaining a 1946 patent: Electronic Counting Circuit. IBM used this counting circuit in the 603 Electronic Multiplier (1946), followed by the 604 Electronic Calculating Punch (1948). Modern computers use triggers—albeit under the modern name "flip-flops"—by the millions, but now they are microscopic transistor circuits instead of vacuum-tube modules. For updates, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. Thanks to Robert Garner for providing the module and to CuriousMarc for hardware support. AI statement: Despite the presence of the em dash, no AI was used in the writing of this article (details). A 1951 advertisement for the IBM 604, describing how the system was like having 150 extra engineers. Slide rules were the common calculating tool at the time. Notice that diversity amongst engineers was limited to hairstyle. From Fortune, December 1951 via Wikimedia, scanned by Michael Holley. Notes and references The punch cards were read and punched by a separate unit, the IBM 521 Card Reader/Punch, which was connected to the IBM 604 through a thick cable. The 521 had a card magazine on the upper left to hold cards to be read. After cards were processed, they were collected in the hopper in the middle of the 521. Note the plugboard control panels in both the 604 and the 521. The IBM 521 Card Reader/Punch to the right of the IBM 604 Electronic Calculating Punch. Photo from Customer Engineering Manual of Instruction. The punch cards were standard IBM 80-column cards, introduced back in 1928. The position of a hole in a column indicated the digit value for that column. For a particular task, the 80 columns would be divided into fields to hold various numbers. The 604 only supported numbers, not other alphanumeric symbols. A negative number was indicated by punching an additional hole over the units digit, using the second row from the top. (This was called an "X-punch", unrelated to the letter X.) Punch card code, from IBM 29 Card Punch Reference Manual. This code is somewhat later, with a variety of special characters. ↩ An interesting video showing the manufacturing and operation of the IBM 604 is here. For information on the IBM 604, see the Operating Manual. The Customer Engineering Manual of Instruction explains the 604 in detail, showing a TR-3 tube module on page 20. See IBM's Early Computers for information on the development of the 604. ↩ You can think of a triode as analogous to an NPN transistor, with the grid as the base, the plate as the collector, and the cathode as the emitter. ↩ The IBM 604 also used dual-triode vacuum tubes with nine pins, rather than seven, such as the type 5965. (Seven and nine pins were standard sizes for tubes.) The nine-pin tubes had separate connections for each cathode; this allowed the tubes to be used in circuits such as "cathode followers". The two filaments were in series, sharing a common "middle" pin, which is why the tube used nine pins instead of 10. ↩ For a discussion of filament voltages, see Valves, 1939. This article discusses how car radios motivated the use of 6.3 volt filaments in the United States, where 6-volt car batteries were common. ↩ The 2033 tube is very similar to the popular 6J6 tube, but optimized for computer circuits. The IBM 1684 tube is also very similar. The tube module that I examined was missing its tube, so I can't guarantee that the 2033 is the correct tube. ↩ The standardized tube modules weren't as standardized as one might expect. For instance, the 604 used 27 different types of inverter modules in total. For a detailed discussion of the tube inverter, see IBM 604 CE Manual, pages 26-41. ↩ A trigger circuit is symmetrical, so how do you define whether a trigger circuit is on or off? IBM's convention was that if the left triode was conducting, the trigger was on, while if the right triode was conducting, the trigger was off. See IBM 604 CE Manual, page 54. ↩ The 604 manual includes a schematic of the TR-3 module (and other modules). Inconveniently, I didn't find this schematic until I had reverse-engineered the module; I made minor adjustments to my schematic based on this. This schematic is a bit tricky to interpret. All the resistances are in thousands of ohms (e.g. 1 is 1 KΩ), and capacitances are in "micromicrofarads" (i.e. pF). The circled numbers indicate pins of the module, while numbers in square boxes indicate voltages according to an obscure standard: 2 is +150V, and 5 is -100 V. "2-110" and "300638" are IBM part numbers. "6J" indicates that the tube is in the 6J family, where 6 indicates the heater voltage and J indicates a triode. The 604 documentation used cryptic boxes as symbols for the modules, with the arrows indicating the inputs and outputs; note that output 7 is at a lower position than output 8, indicating a lower voltage level. Schematic from the CE Manual of Instruction, page 260. ↩ The two outputs from the trigger module are at different voltage levels. The idea is that a circuit could use either output, depending on which voltage level was more convenient. The asymmetrical outputs caused me great trouble, however, since I wanted to attach neon bulb indicators to show the state of both outputs. I had to carefully adjust the voltages so that the bulbs had enough voltage in the "on" state to turn on, but also a sufficiently low voltage in the "off" state to turn off. (When a neon bulb turns on, the neon gas ionizes, so it requires a significantly lower voltage to turn the bulb off.) The IBM 604 used neon bulbs to show the state of various circuits, both in the front panel and internally. However, unlike me, IBM used a single bulb for each trigger, either on or off, so the inconsistent voltage levels didn't cause problems. ↩ The 604 used 12 different types of trigger modules, from TR-1 through TR-42. The different trigger circuits were similar, but had different component values to tune the characteristics, as well as different resistors for the output levels. A few types used resistive inputs instead of capacitively coupled inputs. The trigger modules were used in a variety of different ways. Briefly removing the negative bias from one side would turn that side on; this was used for reset circuits. Second, a plate could be pulled low, turning off the tube on the other side. Third, the input could be connected directly to the input, rather than going through a capacitor, with a negative voltage turning the triode off and a positive voltage turning the triode on. Other triggers used a capacitor between the plate and grid on each side to filter out noise and contact bounce. Some triggers used capacitor inputs (as in the module I described), but fed the same negative input pulse to both sides. The pulse is ignored by the triode that is on, but flips the triode that is off. The result is that the trigger switches state on each pulse—analogous to a toggle flip-flop—and divides the input pulses by two. ↩
1948 was an interesting time for computing. For decades, businesses had used punch card equipment that added and sorted electromechanically. Now these electromechanical relays and counting wheels were being used to build room-filling general-purpose computers such as Harvard Mark I (1944) and IBM's SSEC (1948). But slow electromechanical mechanisms were already becoming obsolete. World War II had fostered the development of electronics and vacuum tubes for radio, radar, and navigation. Electronic technology was being used in massive electronic computers, such as Colossus (1943) and ENIAC (1946). The first stored-program computer, the Manchester Baby, was built in 1948. The IBM 604 Electronic Calculating Punch behind a Type 521 Card Reader/Punch. Photo from IBM. Note the panels in the side of the 604 and in the front of the 521 to hold plugboards. In the midst of these technological advances, IBM introduced the Electronic Calculating Punch, type 604.1 This system may seem like a step backward: it wasn't a computer, but a programmable calculator that performed a fixed set of operations.2 However, it was much smaller3 than a computer—about the size of a double refrigerator—and much cheaper: renting for $550 a month, it was affordable by businesses and universities. Since it used vacuum tubes, it was much more powerful than electromechanical equipment; it could do 60 operations in under a second, including multiplication and division. As a result, the IBM 604 became very popular, with over 5600 units produced. Moreover, IBM's experience with electronics in the 604 led to the success of its vacuum-tube computers in the 1950s. One of the innovations of the 604 was the pluggable module, which combined a tube and its associated circuitry as shown below. The insulated handle was used to remove and install modules in the calculator. The nine pins at the bottom of the module plugged into a socket in the 604, with the sockets connected with backplane wiring. The tube was also socketed, so a bad tube could be quickly replaced. At the right, the resistors and capacitors are mounted on insulating wafers in the module.4 A thyratron tube module from the IBM 604 Electronic Calculating Punch. The 604 used several different types of modules. This module has a thyratron tube, a special type of tube that acts as a high-current switch. I put this module in a circuit and powered it up. The video below shows the module controlling a light bulb. The first button sends a small signal to the module (center), turning it on and illuminating the bulb. As I'll explain below, a thyratron tube stays on until its power is cut off, which I did with the second button. Pluggable modules may seem trivial, but they were an important innovation. Previously, vacuum tube equipment was typically built from a metal chassis with tubes mounted on the top and the other components, such as resistors and capacitors, mounted underneath. IBM developed a different approach: pluggable modules, where each module held a vacuum tube along with its associated components. These patented modules were dense, since they packed components in three dimensions. Moreover, by using a small set of standardized modules, the modules could be mass-produced and the computers assembled on a production line. Maintenance and repair were simplified; modules could be swapped to find the bad module, which was replaced with a spare. These modules were so important that IBM featured them in ads for the 604. IBM used tube modules in later vacuum tube computers, using larger eight-tube modules in the high-end 700-series computers. An ad for the IBM 604, highlighting the pluggable modules. From Time magazine, March 31, 1952, page 65. Click this image (or any other) for a larger version. Vacuum tubes and the thyratron The IBM 604 used about 1250 vacuum tubes. While vacuum tubes come in many different types, a typical type is the triode. A triode is analogous to a transistor: a small input signal is amplified to control a much larger current. In a transistor, the control signal is applied to the gate, controlling the current between the source and drain. In a triode tube, the control signal is applied to the grid, controlling the current between the cathode and the plate. The components of a triode vacuum tube. From IBM 604 Customer Engineering manual. The diagram above shows the construction of a vacuum tube. The heater is a filament, very similar to an incandescent light bulb, that heats up the cathode to roughly 750 ºC. At this high temperature, the cathode emits electrons. When a large positive voltage (say, 100 volts) is put on the plate, the negatively-charged electrons are attracted. The stream of electrons from the cathode to the plate causes a current to flow through the tube. The current is controlled by the grid: if a small negative voltage is placed on the grid, it repels the negative electrons, preventing them from reaching the plate and blocking the current through the tube. A thyratron tube is similar to a vacuum tube, except it has a tiny bit of xenon gas inside, allowing it to handle higher current.7 Like a triode, the thyratron is controlled by the grid. However, when current starts to flow through the thyratron, the xenon ionizes and the xenon plasma carries current. Unlike a vacuum tube, the grid cannot stop the flow of current. Once the gas is ionized, a thyratron tube stays on until you remove its power5 and the gas deionizes in microseconds.6 You can see this behavior in the video. When I pushed the first button, a small control signal ionized the gas, turning the tube on. The large current through the ionized gas illuminated the light bulb. The light stayed on until I briefly cut the power with the second button; the gas deionized, turning off the tube. The thyratron tube, type 2D21. The photo above shows the thyratron tube, type 2D21, a miniature 7-pin tube.8 The plate is visible inside the tube, with the other components hidden by the plate. The dark stain at the top of the tube is the "getter", a reactive substance such as barium that absorbs impurities inside the tube. In the 604, thyratron tubes drove relay coils and powered the electromagnets that punched holes in cards. Other IBM systems also used these thyratron tubes. For instance, the IBM 83 Card Sorter used thyratron tubes as short-term storage to keep track of which holes had been detected in a card. Conclusion The IBM 604 occupies an interesting position between electromechanical accounting machines and electronic computers. Although it has the speed of an electronic computer, it was still a calculator, lacking computer features such as loops, memory, and stored programs. Despite these limitations, the 604 was highly successful and led to other important IBM products. IBM extended the 604 in 1949 so it could be programmed by punch cards in combination with plugboards; this was called the Card-Programmed Electronic Calculator. This system was still not quite a computer, but was very useful for scientific calculation at places such as Los Alamos National Labs (link). In 1953, IBM announced the successor to the 604, the IBM 650. Unlike the 604, the 650 was a programmable, general-purpose computer; it became the most popular computer of the 1950s. Eric Schlaepfer (TubeTime) has a box of IBM 650 modules, which we hope to power up soon. For updates, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. Thanks to CuriousMarc for extensive milling work to build the socket and colorful breakout box to hold the module. AI statement: Despite the presence of the em dash, no AI was used in the writing of this article (details). Notes and references For information on the IBM 604, see the Operating Manual. The Customer Engineering Manual of Instruction explains the circuitry. See IBM's Early Computers for information on the development of the 604. For a detailed description of an application, see this petroleum engineering article, using the 604 to predict the profitability of an oil property. ↩ The IBM 604 operated by reading numbers from a punch card, performing up to 60 operations, and punching the result onto the punch card. This was repeated for each card, processing 100 cards per minute. The IBM 604 was not a stored-program computer, so it didn't have code. Instead, the IBM 604 was programmed by plugging wires into plugboards. The plugboard below was inserted into the 604, while a second plugboard, twice as large, went in the card punch unit to control which columns of the 80-column punch card were read and punched. An IBM 604 plugboard. Photo from National Museum of American History, CI.328576. (Click for a larger image.) Looking at the plugboard above, the column on the left with the heading "PROGRAM" had a row for each programming step. A wire from that row was connected to the function to be performed on that step. The system supported conditionals: the operation that was performed on a step could be changed or skipped with the calculator selectors ("CALC. SEL.") on the right. (A selector was a relay that could send a signal along one of two paths (Normal or Transfer) based on a Control input.) For more information on the plugboards, see the Operator's Manual. ↩ The IBM 604 weighed 1310 pounds, while the attached 521 Card Reader/Punch weighed 670 pounds. The system used 5.5 KW of power. (Vacuum tubes are power-hungry; the module that I used required 3.75 watts for the heater alone.) ↩ I reverse-engineered the MD7A thyratron module to create the schematic below. Black pin numbers are module pins (1-9), while red pin numbers are tube pins (1-7). Schematic of the IBM MD7A module, reverse-engineered. For my experiment, I powered the module with about 100 volts on the plate (pin 5). I used pin 3 of the module for the input, using about 8 volts to trigger the thyratron. Pin 4 is the output, pulled high when the thyratron fires. I connected the light bulb between pin 4 and ground (pin 6). I ignored pins 7, 8, and 9. ↩ One disadvantage of a thyratron is that you need to remove its power to turn it off. In the 604, a mechanical cam in the card reader/punch activated a microswitch to turn off the power (details. Since the card reader/punch used cams on a rotating shaft for its timings, one more cam wasn't an inconvenience. ↩ The behavior of a thyratron is very similar to the silicon-controlled rectifier (SCR). This semiconductor device is also called a thyristor, short for thyratron transistor. ↩ The xenon pressure in the thyratron tube is very small, just .05 Torr, less than 1/10,000 of atmospheric pressure (source). Vacuum tubes, in comparison, have a vacuum that is orders of magnitude higher, around 10-6 Torr. Some high-power thyratron tubes use mercury vapor, such as the ones inside a 1940s power supply that we examined. These tubes give off a blue glow when active. The xenon tube, in comparison, didn't emit any light that I could see, apart from the orange glow from the filament. ↩ The pinout for the 2D21 thyratron tube is shown below, and the datasheet is here. Thyratrons use the same symbols as vacuum tubes, except the large black dot indicates the presence of gas in the tube. Symbol for the 2D21 thyratron tube. From IBM 604 Customer Engineering manual. As the symbol shows, the 2D21 tube has two grids, so it is technically a tetrode (four active elements). The second grid improves performance by screening the control grid from the cathode and the plate, reducing capacitance. (See Thyratrons for modern industry.) For my experiment, I ignored the screen grid. (The 604 also used some pentagrid tubes with a whopping five grids: two control grids, two screen grids, and a suppressor grid.) ↩
Spacelab was a reusable laboratory that could be carried in the cargo bay of the Space Shuttle, providing lab space for astronauts and experiments. Spacelab was controlled by a French-built minicomputer, called the Mitra 125 MS. Unlike modern computers, this computer didn't contain a microprocessor chip. Instead, its 16-bit processor was constructed from several boards of chips. In this article, I reverse-engineer one of the processor boards, shown below, part of the computer's Arithmetic/Logic Unit (ALU). The Mitra 125 MS computer, built by CIMSA, with one of the ALU/register cards shown. Spacelab consisted of a pressurized cylindrical laboratory that held experiments, computers, and work areas for researchers. A tunnel connected the laboratory to the Shuttle, allowing researchers to move between the Shuttle and Spacelab. Spacelab also supported up to five unpressurized "pallets" that were exposed to space, holding experiments such as telescopes and sensors. The illustration below shows the tunnel, the Spacelab laboratory, and a pallet installed in the Shuttle's cargo bay.1 Illustration of the Spacelab-3 mission. From NASA. Because Spacelab was a European project, it used a European computer, the Mitra 125 MS. The Mitra line started in 1971 when a French company called CII introduced the Mitra 15 minicomputer, a 16-bit computer that used magnetic core memory. Mitra is a French acronym2 that translates as "Mini-machine for Real-Time and Automatic Computing." As the name suggests, Mitra was both small and designed for real-time computing, making it suitable for controlling experiments. The Mitra 15 was a popular computer, with almost 8000 units sold. In 1975, CII produced a successor called the Mitra 125. The Mitra 125 improved on the Mitra 15 by adding memory management, I/O processors, higher performance, and additional instructions. Spacelab used the Mitra 125 MS minicomputer,3 a militarized variant of the Mitra 125 that was produced by a company called CIMSA. A Spacelab mission had three of these computers: the Subsystem Computer controlled and managed Spacelab itself, while the Experiment Computer handled the experiments. A Backup Computer could take over if either computer failed.1 These computers were part of Spacelab's Command and Data Management Subsystem, which controlled experiments and collected data.4 The three computers were normally mounted in the Spacelab laboratory underneath the Work Bench Rack (details). The computers were controlled through a keyboard and a color CRT display, called the Data Display System (DDS). The computer installation and a DDS are visible in the photo below. This photo shows astronauts inside Spacelab (but not in space). The Spacelab computers were mounted under the Work Bench (right arrow). The Data Display System (left arrow) provided the interface to the computers. Photo is STS-51B Crew Portrait, 1984. For some Spacelab missions, the laboratory was omitted entirely, providing more room for experiment pallets. In this case, the computers were mounted in a small pressurized cylinder called the igloo. The researchers remained in the Shuttle, controlling experiments through two Data Display Systems that were mounted in the Shuttle's rear flight deck (photo). The 74181 ALU chip The Spacelab computer didn't use a microprocessor chip. Instead, like most minicomputers at the time, it was built from simple integrated circuits that were combined to implement the computer's circuitry. Unlike modern CMOS integrated circuits, these chips contained bipolar transistors, which were fast, but large and power-hungry, a technology known as TTL (transistor-transistor logic). Electronics hobbyists of a certain age will recall the popular 7400 series of TTL chips. The Spacelab computer was built from the military grade of these chips, the 5400 series. The most complex chip in the computer was probably the '181 Arithmetic/Logic Unit (ALU) chip, containing about 170 transistors. The arithmetic/logic unit is the heart of a computer, performing arithmetic operations as well as Boolean logic operations. In 1970, Texas Instruments put a complete 4-bit arithmetic/logic unit on a single chip, called the 74181. Since the chip was fast, compact, and inexpensive, it was widely used, providing the ALU in computers from the popular PDP-11 and Xerox Alto to the powerful VAX-11/780 "superminicomputer". The 74181 provides a full set of binary logical operations, including AND, OR, XOR, and complement. For arithmetic, it includes addition, subtraction, incrementing, and decrementing.5 Inconveniently, the 74181 doesn't support shifting right. Moreover, multiplication and division were much too complicated to be included in the 74181. Instead, a processor implemented multiplication and division through repeated addition or subtraction, combined with shifting. Likewise, floating-point operations were way beyond the capability of the 74181, but a processor could use the 74181 when performing the steps of a floating-point operation. Although the 74181 only handled four bits, multiple 74181 chips could be combined to handle larger words, such as 16 bits or 32 bits. To handle carries, the chips could be chained together, with the carry-out from one chip fed into the carry-in of the next chip. This approach was simple but slow, since the carry had to "ripple" through all the chips before the answer could be obtained. The carry process could be sped up by using a carry-lookahead chip called the 74182, which speeds up addition by computing the carries from four 74181 chips (i.e., 16 bits) in parallel. The Mitra's ALU/register boards The Spacelab computer used eight '181 ALU chips to implement a 32-bit adder.6 (Specifically, these chips are the 54S181, a variant of the 74181: "54" indicates that the chips handle the military temperature range, and "S" indicates that the chip is built from high-speed Schottky logic.) However, the ALU boards required numerous additional chips. Depending on the instruction, eight different inputs could be selected for the ALU. Chips called multiplexers selected the desired value, requiring 32 multiplexer chips. Three 32-bit registers provided storage for ALU inputs and outputs, requiring 24 chips. Two 54S182 carry-lookahead chips provided fast carry computation. Finally, some simple logic chips (inverters and NAND gates) tied things together. Due to the number of chips required, the ALU/register circuitry was spread across three boards, as shown below. (I reverse-engineered the board on the right.7) The '181 chips are immediately visible as they are much larger than the other chips; they have 24 pins, compared to 14 or 16 pins for the other chips. The first board has two '181 chips, while the last two boards each have three '181 chips. The last two boards are similar, but not identical. The three ALU/register boards from the Spacelab computer. Click this image (or any other) for a larger version. Finding a 32-bit ALU was a surprise to me, since the computer is a 16-bit computer. The expanded ALU was probably implemented to improve performance. Multiplying two 16-bit numbers yields a 32-bit result, so a 32-bit ALU makes multiplication faster. Moreover, the computer supports 32-bit floating-point numbers, so the 32-bit ALU presumably makes floating-point operations faster. The diagram below shows the architecture of the computer's 32-bit ALU system. In the middle is the ALU itself, operating on two 32-bit operands: A and B. At the left, multiplexers ("mux") select one of four values for A and one of four values for B. At the right, the output of the ALU can be stored in three 32-bit registers, or sent to the rest of the computer via the bus. The first two registers are shift registers, allowing the value to be shifted left or right, while the third register simply holds the value in flip-flops. The first two registers are connected by buses to the rest of the computer, while the value of the third register can only be accessed by using it for another arithmetic operation.8 I suspect that the shift registers are used for multiplication and division to shift the arguments at each step. Block diagram of the ALU/register board. The inputs to the multiplexers provide flexibility. For instance, you can add register 1 to a number from the bus, or add register 2 shifted to the right to register 3. (Note that this shifting is implemented by wiring the inputs to the multiplexer shifted left or right, completely separate from the shift register's shifting.) The "all 1's" input presumably acts as -1 in two's-complement, providing a decrement. The B input can be taken from the bus, allowing the value to come from memory or from a general-purpose register. The mix input is a jumble of signal lines, register bits, a shift register input, and a pull-up with no apparent pattern. I describe a few more mysteries in the footnote;9 presumably, the mysteries would be resolved if I reverse-engineered the whole computer. The functions of the multiplexers, ALU chips, and registers depend on what instruction is being executed. Specifically, the computer's microcode engine generates control signals for the computer, including the ALU/register boards. Some of these control signals select which multiplexer inputs are used. Other control signals select the ALU's function. Finally, control signals select which register receives the ALU's output. The board that I reverse engineered implements 12 of the 32 bits of the ALU and registers. The diagram below shows the role of each chip on the board. The three 4-bit ALU chips are indicated 2, 1, and 0. Each ALU chip has two multiplexer chips to select the four A input bits and two multiplexer chips to select the four B input bits.10 Thus, there are 12 multiplexer chips on the board. The three 12-bit registers A, B, and C are each implemented with three 4-bit chips. Three hex inverter chips and a 4-input NAND chip complete the board.11 The ALU/register board with the chips labeled. These printed-circuit boards (PCBs) have some interesting features. In most electronics, circuit boards have holes only where they are needed, but the Spacelab boards have holes in a fixed grid pattern. (IBM used similar boards in its System/360 computers in the 1960s.12) A hole can hold an IC pin or other component. Or a hole can be used as a via, connecting PCB traces on different layers. Another interesting feature of the boards is the vertical metal bars underneath the integrated circuits. These bars carry heat away from the integrated circuits. The PCB traces are more visible on the back of the board (below). The traces are thin enough that two traces can pass between a pair of holes. Note the yellow "bodge" wires, correcting errors on the circuit board. I assume that these errors were fixed for the computers used in flight. Back of an ALU/register board. This is a different board from the one I reverse engineered, since I wanted to show the yellow wires. Each board has a 96-pin connector at the bottom, which plugs into the computer's motherboard. Note the three cylindrical pins sticking out of the connector. These pins are keyed to ensure that a board can only be plugged into the correct slot. That is, each pin has a metal tab oriented in one of six directions. On the motherboard, the connectors have corresponding notches. If the tabs and the notches don't match up, the board can't be plugged in. A close-up of the connector, showing the keying. Also note that the zig-zag pin numbering on the left changes to an irregular number on the right. Unexpectedly, pin 52 is between pins 49 and 51, for example, The boards in the Spacelab computer are dense, tightly packing integrated circuits to minimize the size of the computer. However, the boards are considerably less dense than American aerospace computers. In particular, the Spacelab computer used the same integrated circuit packages that were used in consumer electronics: through-hole DIPs (dual in-line packages with two rows of pins). In contrast, IBM's line of 4 Pi aerospace computers used "flat-pack" integrated circuits that were considerably smaller and thinner (details). As a result, IBM's double-sided circuit boards could hold 156 integrated circuits compared to 30 on a single-sided Mitra board of roughly the same size. A brief history of the French computer industry leading up to this computer Bull is one of France's earliest computing companies, created in 1931. Bull initially sold punch-card equipment, competing with IBM. By the 1960s, Bull was a major computer company with products such as the transistorized Gamma 60 computer, a large-scale mainframe that was said to be the first system specifically designed for parallel and multiprogramming. Unfortunately, Bull had difficulty competing with IBM, its stock collapsed, and Bull was acquired by General Electric in 1964, forming Bull-GE. The collapse and controversial takeover were a blow to the French computer industry, and the incident was dubbed the Affaire Bull. To make things worse, GE soon canceled two of Bull's computers, focusing instead on GE's computer line. The Affaire Bull was not only an affront to French pride, but an indication that France was largely dependent on the US for computer technology. A second incident revealed the critical military consequences of France's weakness. In the early 1960s, France was attempting to improve its nuclear strength by develop a hydrogen bomb. The mathematics of fusion is computationally intense, so France attempted to buy powerful American computers: the CDC 6600 supercomputer and the IBM 360/92.13 However, the US government blocked the export of these computers to France in an attempt to limit nuclear proliferation. These problems led French president Charles de Gaulle to decide that France needed a strong computer industry of its own. In 1966, he developed a plan for computing (Plan Calcul)14, where the French government would reorganize the computer industry, picking companies to lead in each sector from minicomputers to semiconductors. In the minicomputer sector, the government created a company called CII by combining three French computer companies: SEA, CAE, and SETI. CII was primarily owned by a large French company called Thomson-CSF (now Thales).15 CII played a key role in the Spacelab computer, since CII developed the Mitra line of computers. In the mid-1970s, CII and the American company Honeywell merged, with the computer division spun off to form a new company called SEMS, with majority shareholder Thomson. Another Thomson subsidiary, CIMSA, focused on military electronics and produced the militarized versions of the Mitra line. In particular, CIMSA produced the computer for Spacelab.16 France's Plan Calcul is generally viewed as a failure. Despite expensive subsidies, the French computer industry remained weak and unable to escape American dominance. When Giscard d'Estaing was elected president of France in 1974, he ended Plan Calcul. There are various interpretations, such as the failure of government planning versus the free market, but my view is that in the 1960s and 1970s, IBM crushed most challengers in the computer industry, both American and foreign, so Plan Calcul didn't have a chance. As for Bull, the company went through a dizzying sequence of American takeovers and nationaizations by France.17 Just two months ago (March 2026), the company was reacquired by the French government. Replacement by the IBM AP-101SL computer Since Spacelab was a European project, using a European computer was a point of pride. Unfortunately, the French computers were eventually replaced by IBM computers due to performance needs and undoubtedly political factors. During the Space Shuttle program, the computers on the Shuttle and in Spacelab became obsolete as computer technology rapidly advanced. Although the computers were originally considered powerful, their performance and memory capacity became problems over time. The Space Shuttle's IBM AP-101 computers were upgraded to IBM AP-101S computers, first flying in 1991. The AP-101S was half the size, three times faster, and had more than twice the memory, using semiconductor memory instead of magnetic core memory. The Spacelab computer system needed a similar upgrade, and in 1991, the CIMSA computers on Spacelab were replaced with IBM AP-101SL computers. The AP-101SL was based on the Shuttle's upgraded AP-101S computer, but modified to support the Mitra's hardware architecture, instruction set, and I/O capabilities. The packaging of IBM's computer was slightly changed to match the dimensions of the CIMSA computer and to use an external heat exchanger rather than an internal heat exchanger. The IBM AP-101SL Spacelab computer. The circuit boards are much larger than the original Spacelab computer boards or the original AP-101B boards. Note the flat-pack ICs on the boards. Photo courtesy of Kyle Owen. Changing the Shuttle's 32-bit AP-101S computer to run the 16-bit Mitra instruction set was easier than you might expect, since the AP-101S already supported multiple instruction sets: a 32-bit instruction set derived from the IBM System/360 and a 16-bit instruction set called 1750A that was an Air Force Standard. Because the AP-101S implemented its instructions in microcode—low-level software that specified the steps of a machine instruction—the instruction set could be modified by updating the microcode. I compared the circuit boards in an AP-101S with the boards in an AP-101SL to quantify the changes. The semiconductor memory boards and power supplies were essentially identical. The CPU boards had minor changes. Unsurprisingly, the I/O boards were completely different, and the complex I/O Processor (IOP) in the Shuttle's AP-101S was omitted. For more on the IBM AP-101 line, see my History of IBM's 4 Pi computers. Conclusions The Spacelab computer provides an interesting look at how computers were built before microprocessors took over. The components of a computer, such as the ALU, registers, and control circuitry, were constructed from simple chips. Since each chip didn't do much, the computer required 36 boards full of chips. Even so, the computer was compact enough to go into space. By modern standards, these computers aren't much—each computer had a memory capacity of just 128 KB of magnetic core memory—but they played a critical part in the space program. I'm not going to reverse-engineer the full computer, but I may write some more about it. For updates, follow me on Bluesky (@righto.com), Mastodon (@[email protected]), or RSS. Credits: Thanks to Steve Jurvetson for providing the Spacelab computer for examination. AI statement: Despite the presence of the em dash, no AI was used in the writing of this article (details). Notes and references For details on Spacelab, see Spacelab News Reference. ↩↩ To avoid cluttering the main article, I'll summarize the French acronyms and companies in this footnote. CAE: Compagnie européenne d'automatisme électronique (European Electronic Automation Company). A French computer company founded in 1960, selling versions of American computers such as TRW's RW-300. Part of the 1966 merger that formed CII. CII: Compagnie internationale d'informatique (International Computer Company): the company that created the Mitra line of minicomputers. CII also sold computers designed by the American company SDS (Scientific Systems), which was bought by Xerox in 1969 and became XDS (Xerox Data Systems). XDS was shut down in 1975, costing Xerox hundreds of millions of dollars. CIMSA: Compagnie d'informatique militaire, spatiale et aéronautique (Military, Space, and Aeronautical Computing Company): the company that manufactured the Spacelab computer. CSF: Compagnie Générale de Télégraphie Sans Fil (General Wireless Telegraphy Company). A radio company dating back to 1918. It merged with Thomson in 1968 to form Thomson-CSF. MATRA: Mécanique Aviation Traction (Mechanics-Aviation-Traction). An electronics company that was the contractor for Spacelab's data systems. Mitra: Mini-machine pour l'Informatique Temps Réel et Automatique ("Mini-machine for Real-Time and Automatic Computing"). A line of minicomputers. SEA: Société d'électronique et d'automatisme (Electronics and Automation Company): a French computer manufacturer, started in 1947 and merged into CII in 1966. SEMS: Société Européenne de Mini-informatique et de Systèmes (European Society for Minicomputers and Systems). A subsidiary of Thomson, created by the French government in 1976 during the merger of CII and Honeywell. SEMS took over the manufacturing of Mitra computers from CII. SETI: Société européenne de traitement de l'information (European Information Processing Society). SETI was a French computer company formed in 1961. The American computer company Packard Bell owned a quarter of SETI, and SETI sold the desk-sized Packard Bell 250 computer. ↩ On the ground, the Spacelab project used Mitra 125 S computers that were functionally identical to the Mitra 125 MS (details). ↩ Spacelab's Command and Data Management Subsystem (CDMS) is surprisingly complicated because of the data communication paths between Spacelab, the Shuttle, and the ground. Moreover, multiple units store, encode, and decode data. In the CDMS block diagram below, I've highlighted the three computers; they are just a small part of the CDMS. See Section 3.5 of Spacelab News Reference or The Command and Data Management System of Spacelab for details on CDMS. A block diagram of Spacelab's Command and Data Management Subsystem. From The Command and Data Management System of Spacelab. Click for a larger version. ↩ I reverse-engineered the 74181 ALU chip in this article and explained the motivation for its quirky set of operations in this article. ↩ Another board in the Spacelab computer has four 74S181 chips implementing a 16-bit ALU. My guess is that this board is part of the I/O processor. The board has the cryptic label "HMSG". ↩ My reverse-engineering process was straightforward but tedious. I used a multimeter to beep out the connections between the integrated circuits as well as the connections to the connector. (Unlike many systems that I look at, these boards didn't have conformal coating, which made beeping out the connections practical.) I created a schematic in KiCad from this data; this schematic was "physical", with the layout of the chips and pins matching their physical location on the board. Next, I converted the integrated circuit symbols from physical rectangles to logical symbols. Finally, I moved the symbols around on the schematic to make a reasonable schematic. (I had to go back and beep out more connections as I discovered errors or missing connections.) Theoretically, I could reverse-engineer the entire computer, but reverse-engineering one of the 36 boards is enough for me. ↩ My full reverse-engineered schematic of the ALU/register board is below. Click for a larger version. Schematic of the ALU/register board. ↩ A few mysteries remain in the ALU/register board. The three registers probably act as an accumulator, a temporary register, and an extra register for multiplication/division, but it's not clear which register is which. I don't understand why the inputs are organized as they are; for instance, you can't add register 1 to register 2 shifted. The mix input seems very random; maybe these signals are part of a self test? On the board, I expected to see 12 bits out of a uniform 32-bit ALU. However, the top two 4-bit "nibbles" have different control lines and different zero-detection from the third. Perhaps this is because the Mitra floating-point numbers have 24 bits of mantissa and 8 bits of exponent. It would make sense for the ALU/register board to handle these parts separately. Another mystery is that the board has a circuit to test two hardwired bits and two external bits to see if they are all 0 or all 1, for some reason. ↩ The multiplexer chips are dual 4-to-1 multiplexers. Thus, two multiplexer chips are required to support four bits. ↩ The chips in the Spacelab computer use a variety of part number systems. A few chips have standard industry part numbers such as "SNJ5483" (equivalent to a 7483 adder). Most of the chips are labeled with military part numbers such as JM38510/07801 BJB, using the MIL-M-38510 standard. These part numbers can be cross-referenced using the MIL-HDBK-983 handbook. Other chips, like the ones below, have Fairchild part numbers that are a mystery to me. The first line is presumably the part number, "929 567" and "929 705", but I can't find these numbers anywhere. If you know what these numbers mean, please let me know! (07263 is the CAGE code for Fairchild, and the last line is the date code.) Two Fairchild ICs with mysterious part numbers. The ALU/register board that I examined uses the following JM38510 part numbers, which I have converted to standard parts: /01403 = 54153 dual 4-1 multiplexer /07003 = 54S04 hex inverter /07006 = 54S20 4-input NAND /07601 = 54S194 4-bit shift register /07801 = 54S1814-bit ALU /30107 = 54LS175 quad flip-flop ↩ The photo below compares an IBM board (top) with a Spacelab board (bottom), both from the early 1980s. It's interesting how similar the boards are. Both use a 0.1" grid of holes, unlike most printed-circuit boards, which only use holes where needed. Both boards are multi-layer with integrated circuits on one side. The IBM board is denser; the chips are spaced 0.1" apart rather than 0.3" apart. An IBM computer board (top) and a board from the Spacelab computer (bottom). I don't know which IBM system used this board, but it was a commercial system, not an aerospace system. This board is a bit unusual for IBM, since most of the chips are standard DIPs rather than the square metal cans that IBM typically used. ↩ The US blocked computer exports to France with NSAM 294, a 1964 National Security Action Memorandum. The US later allowed sales of the CDC 6600 and IBM 360/91 computers to France on the condition that France not use the computers for atomic weapon development, a condition that France apparently violated. See A.E.C. Bids Industry Avoid Sales Aiding French Tests (1964) and Paris Promises Not to Use Equipment for Atomic Weapons (1966). The CDC 6600 supercomputer executed up to 10 million instructions per second (MIPS) while the IBM 360/91 executed about 17 MIPS. (In comparison, a 1995 Pentium Pro or a 2012 cell phone is faster than these computers.) In 1971, Henry Kissinger was still blocking computer exports to France, as shown in this transcript. (One confusing issue in these articles is that IBM announced the 360/92 computer in 1964, but renamed it as the 360/91 before it shipped in 1967.) ↩ Some contemporary articles on Plan Calcul are France Entering Computer Battle: Starts All-French Company to Compete (New York Times, 1967) and France: First the Bomb, Then the "Plan Calcul" (Science, 1967). See History of Computing in France: A Brief Sketch for an overview of the French computer industry. ↩ Thomson has a complicated history. In 1883, two Americans, Thomson and Houston, started the Thomson-Houston Electric Company. A decade later, this company became General Electric, with a French subsidiary: Thomson Houston International. After various mergers, the French subsidiary became Thomson-CSF, a major defense and electronics firm. In a sense, Thomson-Houston both created and destroyed GE. The Thomson-Houston Electrical Company became GE, but the French subsidiary of Thomson-Houston ended up being a key part of GE's collapse almost a century later. Specifically, the French rail transport company Alsthom (later Alstom) was formed from the French heavy engineering subsidiary of Thomson-Hudson in 1928; the "thom" in "Alsthom" comes from "Thomson". In 2014, General Electric acquired Alstom for $10.1 billion. The acquisition was a disaster, and in 2018, GE wrote off $23 billion. This loss, along with other financial problems, led to GE's announcement in 2021 that it would break up into three companies. ↩ One more company should be mentioned: MATRA. MATRA was the contractor for Spacelab's data systems, so the Spacelab computer was produced under a contract from MATRA. People often confuse Mitra (the name of the computer line) with MATRA. ↩ Due to financial difficulties, Bull was acquired by General Electric in 1964, then was acquired by Honeywell, nationalized by France, partnered with NEC, acquired Zenith, privatized by France, and acquired by Atos. Less than two months ago, France acquired Bull, continuing the series of reorganizations. ↩
More in technology
Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry. We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’
You want less of them. That’s the reason. You may find that it’s too hard to stop people from doing the thing, literally blood, sweat, and tears trying to prosecute people, but that’s a different thing.
Solitaire Alone Together I made a new game. It's called Solitaire Alone Together. It's Windows 98 solitaire, but you can play with everyone else on the internet. Read the full post on my blog! Here's a raw link, if you need it: https://eieio.games/blog/solitaire-alone-together
This post is a living diary of all the times I messed up something with my website in a funny way. I value those who have the confidence to own their mistakes and share the learning with others, and so this is me doing just that! That Time I Accidentally Made a Tarpit That Time I Accidentally Made Really Large Headers That Time I Accidentally Made a Tarpit Back to Top A "tarpit" is an unofficial term used in computing to describe an intentionally slow response to a request. In these modern times many people are using tarpits as a way to combat the relentless theft of data by AI companies, although there's little to no evidence of that actually being in any way effective. I don't use tarpits, at least not intentionally, but there was that one time when I accidentally created a tarpit and trapped all visitors in it. As I've shared previously, I refuse connections from IP addresses that are blocked or belong to a blocked subnet, and I enforce this firewall during the TCP handshake. The logic here is straightforward: there's no reason to waste resources doing a TLS handshake, accepting an HTTP request, and then rejecting the connection if I already know I'm going to reject it at the earliest step. At the time, the code worked like this: the HTTP server would repeatedly call the Accept() function below expecting a new connection. I've added some comments to help explain the logic. func (l *firewallListener) Accept() (net.Conn, error) { // Accept the connection from the TCP listener. This blocks until there is a connection to accept or the listner was closed. conn, err := l.l.AcceptTCP() if err != nil { return conn, err } // Separate the IP address out from the remote address (which includes the port) ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } // Check if it's blocked, if so close the connection and return a refuseError if IsBlocked(ip, true) { conn.Close() return nil, &refuseError{} } // Otherwise return the connection on to the HTTP server return conn, nil } If the incoming connection was from a blocked IP then I'd return a refuseError. I need to use a specific error interface because the HTTP server will halt if it encounters a non-temporary error from the call to Accept(), so I need to return an error that satisfies the definition of a temporary error. I defined refuseError like this: type refuseError struct{} func (e *refuseError) Error() string { return "." } func (e *refuseError) Timeout() bool { return true } func (e *refuseError) Temporary() bool { return true } func (e *refuseError) Is(err error) bool { return err == context.DeadlineExceeded } This did accomplish the goal of rejecting connections before the TLS handshake for blocked addresses, but it had one really unintended and difficult to track down side-effect. Accepting connections is done serially, after which servers typically then process that request on a dedicated thread (or in Go's case a goroutine). This means that any delays during the accept loop will block all incoming connection. What I had missed while reviewing the code for Go's HTTP server is that when it receives a temporary error from Accept() is that while it doesn't abort, it does sleep for up to a maximum of 1 second. This sleep blocks the entire server for all incoming connections. You can see a trimmed copy of the code that does this below, with some marks I've added which I will explain. // src/net/http/server.go // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. for { // (1) rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } // (2) if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } s.logf("http: Accept error: %v; retrying in %v", err, tempDelay) // (3) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := s.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return // (4) go c.serve(connCtx) } At mark 1 the server calls the Accept() function, this is the exact function that I defined above where I might return a temporary error. At mark 2 it checks if an error was returned, and if so if that error is temporary. If there was a temporary error, at mark 3 it sleeps for an increasing amount of time up-to 1 second, otherwise, at mark 4 it processes the connection on a dedicated goroutine, which allows the server to accept the next connection. I'm not entirely sure why the Go developers added this sleep delay and the change when it was introduced doesn't provide any meaningful insight. Regardless, it caused significant latency connecting to my website when a flood of rejected requests was coming in. It just goes to show how important it is to write meaningful commit messages, because you never know when somebody might come back years later wondering "why was this done?". I sure home I don't come to eat those words later. Coincidentally, you can actually see this happening if you look carefully at one of the metric graphs I shared in my first post about my server's security model: Securing My Web Infrastructure. This is the graph I shared in that blog post and while I didn't know it at the time, the fact that these request spikes all cap-out at around 60 requests per minute was not a coincidence. These requests were not being made with a limit in mind, attackers rarely ever care about things like that, instead it the accidental tarpit I had created. The downside to this was that while the malicious requests were being rate-limited, all requests were being rate-limited, up to a point of taking so long they timed out. The Fix Fixing the issue was relatively straightforward enough. Instead of returning a temporary error to the HTTP server during the accept loop, just don't return anything at all and wait for the next valid connection. func (l *firewallListener) Accept() (net.Conn, error) { for { conn, err := l.l.AcceptTCP() if err != nil { return conn, err } ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } if IsBlocked(ip, true) { conn.SetLinger(0) conn.Close() continue } return conn, nil } } Now, when the HTTP server calls Accept(), the only time it returns is with a connection from an IP that isn't blocked, or if there genuinely is an error. No more sleep delays, no more excessive timeouts. That Time I Accidentally Made Really Large Headers Back to Top For about 10 years now all major browsers have support for a security feature known as a Content Security Policy or CSP. A CSP is an HTTP header provided by the server that instructs the browser on where it can load assets from, this could be scripts, images, stylesheets, fonts, etc. The objective of using a CSP is to prevent against injected HTML that tries to load assets, such as a malicious Javascript file, from a remote source. With so much user-provided content being available online, it's very possible for this to happen without an attacker compromising the entire web server. CSP protects against that by saying "scripts can only be loaded from these domains". That's a really simplified way of looking at it, anyways. My web server supports injecting the CSP header automatically, but before I go on I need to explain a little bit about the structure of my web server. When an incoming HTTP request is accepted (having passed all firewall checks and assertions), we look at the destination host for the request. This can either be the value of the Host header or as specified during the TLS handshake. We then look at a map of hosts to apps. Apps are just an interface that accept a few methods: type App interface { Cleanup() ReloadConfig() ServeHTTP(rw http.ResponseWriter, r *http.Request) Setup(dataDir string) error Shutdown() } One of the apps is the Proxy app, which is a reverse proxy - it accepts the incoming HTTP request and then proxies it on to another host. This is a very common design, especially with increasingly complex TLS setups. Because each app is unique to a host, and different hosts have different requirements for CSP rules, the proxy app includes a CSP preset that we use to build the header value, or skip it entirely. When the proxy app was going to copy an HTTP request to the downstream host, it would build the CSP header, however there was a slight bug... func (a *App) ServeHTTP(rw http.ResponseWriter, inRequest *ht2.Request) { // --snip -- if a.CSP != nil { a.CSP.ConnectSrc += " " + inRequest.Origin } CopyHttpRequest(inRequest, outRequest, rw, CopyHttpRequestOptions{ Origin: inRequest.Origin, Csp: a.CSP, Cors: a.CORS, AddHeaders: !a.SkipHeaders, UseHTTP3: a.UseHTTP3, InsecureTLS: a.InsecureTLS, }) } I'm really unsure as to what I was doing with the line to append to the ConnectSrc, but the impact is that I'm appending to a variable that lives on the App, rather than a variable that is per-request. This meant that every time there was a request to the app, any request at all, the origin would be appended to the header value. This went on for quite a long time unnoticed and unresolved, largely because I am constantly tweaking and tinkering with my web server, after all, it's how I made having a website fun again. Each time I restarted the server process, the header value would be reset, but only for it to continue to grow and grow. Eventually, after a period of being busy with other matters, the server process stayed running for long enough that the header value grew too large and HTTP clients began to reject it. There is no defined maximum for an HTTP header value, however most HTTP clients use 100KiB, which is perfectly reasonable, and this header value would continue to grow well beyond that. Diagnosing this issue turned out to be difficult as tools like Curl would fail with errors relating to entities being too large, but stopped short of saying what specifically. I eventually used openssl s_client to send an HTTP request by hand and observed my terminal window being filled with a domain name repeated thousands of times. Looking at the commit history, it was really unclear why I added the culprit lines of code. The commit message just says "Improved CSP support". It just goes to show how important it is to write - hey look it's those words I'm now having to eat! The Fix The fix was to just delete those three lines of code. Yup, it really was that simple, and fixing this bug actually made a larger positive impact than I had expected, as it was immediately clear when I fixed the bug by looking at outbound network bytes: So much traffic was being wasted on excessive header sizes. You might look at these mistakes I've made and think "wow, Ian, these are some obvious mistakes, I never would have made them!" to which I say "good for you!" with the utmost sarcasm and disdain. I enjoy making and refining software, and making anything means making mistakes along the way. Each time I make mistakes such as the ones above, I improve my skills of investigation, diagnosing, and repair. Skills that, judging by my peers in the industry, seemingly everyone is quickly willing to throw away because a robot does it "better" than you. Header Image: "Car accident on the Ffestiniog to Bala road. Nobody was hurt" by Geoff Charles, CC BY-SA 4.0, via Wikimedia Commons.
Back in 2021, I wired up data from Buienalarm and Buienradar through Node-RED and a big pile of Jinja2 templates to get a rain forecast graph on my Apple Watch: eight Unicode block characters showing the next two hours in 15-minute chunks, glanceable without unlocking the phone. It worked, and I used it every day […] The post Buienwatch, a custom Home Assistant integration for Buienradar and Buienalarm graphs on your Apple Watch appeared first on Style over Substance.