Fixing Decades-old Bugs in the HP-35
[Part 2 of the HP Calc series ]
Making the JavaScript-based HP-35 microcode emulator has been a fun little project. Last time we disassembled the original bits from the ROM. I say “disassemble” but really our microcode instructions were an array of JavaScript functions. This time, I’m continuing the fun with what I’ll call an “assembler”; taking text-based assembly listing as input and producing the same array of thunks. This will be useful because several HP calculator ROMs are available in this form in Eric Smith’s Nonpareil project. Someday I want to emulate the whole line of classic HP calculators but for now I just want to run different versions of the HP-35 ROMs.
As much fun as the buggy v2 ROM is, I want to now fix the bugs. I don’t need to twiddle the microcode myself (though Jacques Laporte elucidates the root cause of the bugs beautifully on his site). Dave Cochran fixed them several decades ago! All I need to do is upgrade my ROM.
Microcode Assembler
I couldn’t find an object code listing for the v4 rom, but Peter Monta has a nice assembly listing. To make an assembler, I just parse with a series of regular expressions. F#’s active patterns came in super handy for this! In fact, the assembler turned out to be fewer lines of code than the disassembler.
let asm code js ver =
let (|Pat|_|) (p : string) s =
let m = Regex.Match(s, (p.Replace("X", "([^\s]+)")
.Replace("Y[Z]", "([^\[]+)\[([^\]]+)\]")
.Replace("[Z]", "\[([^\]]+)\]")
.Replace("Z", "([\d]+)")))
if m.Success then Some(List.tail [for g in m.Groups -> g.Value]) else None
let labelsAndCode =
fullPath code |> readFileLines |> List.ofSeq
|> List.choose (function
| Pat @"^\s*[\.;]" _ | Pat @"^\s*$" _ -> None // skip non-code (blank, comment, .rom, ...)
| Pat @"([^\s:]*):*\s+([^;\t]+)" [label; code] -> Some(label, code)
| _ -> failwith "Parse error.")
let labelLinesMap =
labelsAndCode
|> List.mapi (fun num (lbl, _) -> if lbl.Length > 0 then Some(lbl, num) else None)
|> List.choose id
|> Map.ofList
let label (lbl : string) =
if lbl.StartsWith("@") then System.Convert.ToInt32(lbl.Substring(1), 8) // octal address
else Map.find lbl labelLinesMap
let (|Fld|_|) = function
| "p" -> Some(-1, -1)
| "m" -> Some(3, 12)
| "x" -> Some(0, 2)
| "w" -> Some(0, 13)
| "wp" -> Some(0, -1)
| "ms" -> Some(3, 13)
| "xs" -> Some(2, 2)
| "s" -> Some(13, 13)
| _ -> failwith "Unsupported field name."
let sp = sprintf
let parse = function
| Pat "return" _ -> "retn"
| Pat "no operation" _ -> "nop"
| Pat "down rotate" _ -> "downrot"
| Pat "clear registers" _ -> "clearregs"
| Pat "clear status" _ -> "clears"
| Pat "display off" _ -> "dispoff"
| Pat "display toggle" _ -> "disptoggle"
| Pat "stack -> a" _ -> "stacka"
| Pat "c -> stack" _ -> "cstack"
| Pat "p \+ 1 -> p" _ -> "incp"
| Pat "p - 1 -> p" _ -> "decp"
| Pat "keys -> rom address" _ -> "keyrom"
| Pat "jsb X" [l] -> sp "jsb(%d)" (label l)
| Pat "go to X" [l] -> sp "goto(%d)" (label l)
| Pat "X -> sZ" [n; b] -> sp "sets(%s,%s)" b n // 5 –> s3
| Pat "select rom Z" [n] -> sp "setrom(%s)" n
| Pat "0 -> Y[Z]" [r; Fld (s, e)] -> sp "zeroreg(%s,%d,%d)" r s e // 0 -> a[w]
| Pat "0 - c - 1 -> c[Z]" [Fld (s, e)] -> sp "negsubc(%d,%d)" s e // 0 - c - 1 -> c[s]
| Pat "0 - c -> c[Z]" [Fld (s, e)] -> sp "negc(%d,%d)" s e // 0 - c -> c[x]
| Pat @"[^\s]+ \+ 1 -> Y[Z]" [r; Fld (s, e)] -> sp "increg(%s,%d,%d)" r s e // a + 1 -> a[p]
| Pat @"[^\s]+ - 1 -> Y[Z]" [r; Fld (s, e)] -> sp "decreg(%s,%d,%d)" r s e // a - 1 -> a[x]
| Pat "X \+ X -> Y[Z]" [r1; r2; r3; Fld (s, e)] -> sp "add(%s,%s,%s,%d,%d)" r3 r1 r2 s e
| Pat "X - X -> Y[Z]" [r1; r2; r3; Fld (s, e)] -> sp "sub(%s,%s,%s,%d,%d)" r3 r1 r2 s e
| Pat "X -> Y[Z]" [r1; r2; Fld (s, e)] -> sp "setreg(%s,%s,%d,%d)" r2 r1 s e // a -> b[w]
| Pat "X exchange Y[Z]" [r1; r2; Fld (s, e)] -> sp "exchreg(%s,%s,%d,%d)" r1 r2 s e
| Pat "X exchange X" [r1; r2] -> sp "exchreg(%s,%s,%d,%d)" r1 r2 0 13 // c exchange m
| Pat "X -> p" [n] -> sp "setp(%s)" n // 3 –> p
| Pat "X -> X" [r2; r1] -> sp "setreg(%s,%s,%d,%d)" r1 r2 0 13 // c exchange m
| Pat "shift left Y[Z]" [r; Fld (s, e)] -> sp "shiftl(%s,%d,%d)" r s e // shiftl a[w]
| Pat "shift right Y[Z]" [r; Fld (s, e)] -> sp "shiftr(%s,%d,%d)" r s e // shiftr a[w]
| Pat "if Y[Z] = 0" [r; Fld (s, e)] -> sp "ifregzero(%s,%d,%d)" r s e // if c[xs] = 0
| Pat "if X >= Y[Z]" [r1; r2; Fld (s, e)] -> sp "regsgte(%s,%s,%d,%d)" r1 r2 s e
| Pat "if Y[Z] >= 1" [r; Fld (s, e)] -> sp "regsgte1(%s,%d,%d)" r s e // if a[p] >= 1
| Pat "if sX = 0" [n] -> sp "tests(%s)" n // if s3 = 0
| Pat "if p # X" [n] -> sp "testp(%s)" n // if p # 11,
| Pat "load constant X" [n] -> sp "loadconst(%s)" n
| x -> sp "!!! UNKNOWN !!! %s" x
writeOutput js ver (fun output –>
labelsAndCode
|> List.map snd // code only
|> List.map parse
|> Seq.iter (fun a -> output.WriteLine(sprintf " %s," a)))
Testing the assembler was very simple. For the v2 ROM, Peter happens to have both an object code file which I can run through the disassembler and an assembly listing which I can run through the assembler. Then diff the output; a perfect match! By the way, this is a nice general approach to testing; write two significantly different algorithms to solve the same problem and use them to verify each other. They do this on the Excel team for example; the complicated super-efficient recalc engine is verified by a second simple brute force one.
Voila!
So now then assuming the assembler was working, I ran the v4 listing through. Load up the new ROM and voila! The calculator seems to work (unit tests pass) without bugs (e.g. try exp(ln(2.02)). Comparing the behavior of the two ROMs, I can indeed verify also that Mike Sebastian’s calculator forensics result is a match. I’m pretty confident it’s all working…
Here it is running the v2 (buggy) ROM: https://www.lkjsdf.com/archive/HP/35/?v=2
Here it is running the v4 (fixed) ROM: https://www.lkjsdf.com/archive/HP/35/?v=4
Have fun playing with it!
Comments
- Anonymous
October 01, 2010
Added a photo-realistic skin to it - feels like the real thing now: www.lkjsdf.com/.../Skin - Anonymous
January 08, 2012
Dropped source here: github.com/.../HP35