Disassembling HP-97 program cards
In my first post about the HP-97 I started restoring the hardware of the device. In my second post I started reading the cards. In this post, I make sense of those cards, and ultimately write a "disassembler" for them. I have disassembler in quotes here, because it's not a true disassembly. The contents of the program cards aren't exactly machine code, and the instructions aren't exactly assembly. It is, however, the closest word to what I'm doing that you might already be familiar with.
Checking off Checking Checksums
I left the last post unable to correctly calculate the checksums on the cards. I shared that post with Teenix, and he added another section to his notes document. Embarrassingly, the problem was staring me straight in the face. If we recall the table I shared:
\(n_0\) | \(n_1\) | \(n_2\) | \(n_3\) | \(n_4\) | \(n_5\) | \(n_6\) | Notes |
---|---|---|---|---|---|---|---|
2 | 2 | 2 | 0 | 0 | 0 | 4 | $1F high |
b | 0 | 0 | 9 | f | 8 | 5 | $1F low |
0 | 3 | 8 | 3 | 9 | b | 8 | $1E high |
5 | 1 | 4 | e | 0 | 0 | 0 | $1E low |
5 | 3 | e | 0 | 8 | f | 8 | $1D high |
0 | 0 | 0 | 0 | 0 | 0 | 0 | $1D low |
. | . | . | . | . | . | . | skipped |
0 | 0 | 0 | 0 | 0 | 0 | 0 | $10 low |
17 | 9 | 1c | 1a | 20 | 22 | 19 | computed checksum |
7 | 9 | c | a | 0 | 2 | 9 | mod 16 |
7 | a | c | b | 1 | 4 | b | card checksum |
0 | 1 | 0 | 1 | 1 | 2 | 2 | abs error |
Notice that the error on nibble 1 (\(n_1\)) is exactly equal to \(\frac{n_0}{n_{16}}\)? The portion that would be truncated is actually carried!! The error of \(n_6\) is equal to what would carry from \(n_5\). Nibble 5 is missing \(n_4\). So on, and so forth.
With that sorted, I was able to correctly verify the checksum on all 46 captured cards.
Making sense of the stored instructions
The next struggle was figuring out exactly how the instructions are stored on the magnetic cards. In this case, I spent quite a bit of time going down the rabbit hole of 6-bit instruction words. This was not for no reason, as the users manual, service manual, and the HP Journal article all discuss the 6-bit words. I simply couldn't get the math to work out; no matter how you try, you can't pack 6-bit words into the 56-bit registers in a clean way! They simply don't divide! Ultimately the epiphany came when I made the modification to the calculator that I threatened in the previous article.
I tack-soldered small wires onto the CRC chip for each of the signals that go to the card reader. Even though the motor still didn't work, I could use these signals to learn a lot more about what's happening. By entering a program into the calculator, then trying to write it to a card, I could see the impact of adding single instructions. It became clear that each instruction added exactly one byte to the card.
Then, I was able to derive more value from the thread in the classic notes document relating to how the 56-bit registers in the calculator's memory are written to the card. The program memory sits in the middle of a 64-word memory bank, flanked by the primary and secondary register files. Each of these are organized into 16-word pages. The register files contain registers 0-9, A-E, and an indirect register I, for a total of 16. And, the program memory goes from memory address 0x10 to 0x2F. Interestingly, instruction zero (first) is at 0x2F, and the program grows "upward".
With this information, it was clear that instructions are simply 1-byte wide, and that 7 of them are packed within a single 56-bit register, and a register is stored as two 28-bit records on the card. Any unused instructions are filled with 0x00, which is a Run/Stop
or R/S
instruction. You can think of those as No-ops.
Once I figured that out, I started looking for some reference for all the instruction OpCodes, and what instruction they mapped to. I initially started entering simple programs into the calculator, printing them out, saving them to a card (virtually) and writing the opcode numbers in the margin of the tape.
Eventually, while playing around with the Stat-Pac that I scanned, and the version I downloaded from Teenix's website (they were similar, but enigmatically different) I realized that I could just rely on the emulator's depiction of the program; the numbers matched. Essentially, I loaded a few cards into the emulator and compared the register contents to the program steps that the emulator displayed. Clearly there was some notion of this mapping in the emulator, but I don't believe it's open source.
Anyway, I spent a couple hours loading programs and adding instructions that I hadn't figured out yet. Eventually, I was left with quite a few opcodes that didn't have instructions. I certainly didn't expect that 100% of the available opcodes would be valid, but I was sure I was missing some. In part, because my disassembler crashes when it hits an unknown opcode, and there were a few in my stack of cards. Then, I noticed in Appendix E that HP had helpfully listed every possible instruction, what it prints out, and what keys must be pressed to achieve it. I was able to enter the remainder of those into the emulator to get the code for them.
Long story short, I now have a complete notion (that I think is accurate) of all the HP-97 opcodes, what they mean, how they'll print, and what keycodes are necessary to create them in my open source HP-97 program card disassembler.
Ultimately, there are something like 7 opcodes that seem not to be associated with a published instruction. I'm dying of curiosity for what'll happen if I write them to a card! Likely, I'll just get an error, but it'll be fun to see if anything else happens. This must be what fuzzers and crackers feel like all. the. time.
Reading and writing HP-97 emulator card files (hpp format)
It should be clear that I found Teenix's emulator to be highly valuable. And, it was clear that I'd want to be able to directly load my scanned cards into it. That necessitated writing a serde (seralizer/deserializer) for the format. While the classics notes document now has a description of the file format, the emulator is extremely fussy, and it won't accept cards that aren't exactly the same as the ones he produces. Everything down to the precise line terminations for each line must match exactly.
For example, the first two lines: NeWe
(magic number) and file size have only carriage returns (no, not linefeeds, like unix endings. just carriage returns). The rest of the lines must be carriage return/linefeed combos (windows line endings). And, the end of the file must have a carriage return and linefeed. Once I learned those tricks, his document is accurate.
Having that out of the way allows me to take a real life Stat-Pac card (or any, of course), load it into my HP-97, capture the output, and run it in the emulator. That's extremely cool!!
Disassembler output examples
Just for fun, here's the command-line output of the program, loading that same area of a circle program:
Processing 01_AreaOfCircle:
000 *LBLA 21 11
001 X² 53
002 Pi 16-24
003 × -35
004 RTN 24
005 R/S 51
I even tried to get the printing of the lines just right. There was only one frustration around that, and that was the inverse trigonometric operators. The HP-97's printer has a special character set that includes a superscript -1
, which is a single-width character. I simply couldn't figure out a way to achieve that with unicode. So, I had to cheat, and let them take up the column that's normally reserved to have an *
to let the labels stand out more. For example, from the diagnostics card:
...
032 ISZi 16 26 45
...
041 *LBL2 21 02
042 2 02
043 5 05
044 STOI 35 46
045 SIN 41
046 SIN⁻¹ 16 41
047 GSB3 23 03
048 COS 42
049 COS⁻¹ 16 42
050 GSB3 23 03
051 TAN 43
052 TAN⁻¹ 16 43
053 GSB3 23 03
054 →P 34
055 →R 44
056 GSB3 23 03
057 SIN 41
058 →HMS 16 35
059 HMS→ 16 36
060 SIN⁻¹ 16 41
...
Next steps...
From now, the next steps are more nebulous. I'm still hoping my lead on a new sense amplifier and Rom-0 chips comes through, so I can repair my device. If that doesn't happen, though, Teenix is still working on a replacement CPU board. I'll certainly buy one of those regardless of whether I "need" it. Also, I'm strongly considering designing a replacement for the card reader board that's based around a mixed-signal microcontroller. I think there are probably devices that had good analog comparators and motor drivers. I think 2 comparators and a motor driver would be all that's needed besides some basic programmability. This, of course, would be a lot more time consuming to design. At a minimum, it's fun to think about making a copy of the PCB that's mechanically compatible with the existing PCB that would let me have a play with loading and reading cards outside of the calculator.