-
The 592x384 display
02/28/2026 at 18:35 • 0 commentsEarly disassembly of the self-test, together with the MAME 16500 emulation code, revealed that the screen is 592x384 and the memory is at 0x600000. But writing to the memory is tricky. There is a screen memory control register at 0x201000. Writing certain "magic" values (discovered by disassembly) to the screen memory control register (e.g., 0xE00) allow one to draw a black pixel and other values (e.g., 0xF00) allow one to draw a white pixel. Specifically, to write a pixel--regardless of color--you write a one to the right bit in screen memory. Zeroes are completely ignored.
The pixels are stored in the low nibble of each word of logical memory, with the high bit of the nibble being the left-most byte. The rest of the memory map is straightfoward. Each scanline is 592/4=148 words long, and they are stored in sequence from top to bottom, with no padding.
This was enough information for me to make my Wiztris port.
But the mysteries were not finished. I tried very hard, with lots of test code, to figure out how the screen memory control register worked. I also found some strange things. Sometimes I was able to draw a pixel with two different screen modes. The pixel looked the same, but behaved differently when I wrote to it in a different mode. This was very puzzling--it was as if the system remembered how I wrote the pixel. But at the same time the service manual clearly stated that there was 64K of 4bit memory for the display, which was only slightly more than 4 bits per pixel times 592 times 384, so there was no way for the system to remember the mode in which I wrote the pixel, or so I thought.
This was very frustrating. Finally, I did some reproducible experiments and wrote up a detailed stackexchange query about the mystery. But before posting, I decided to ask the query of Gemini Pro. I never some Gemini work this long--it felt like 5-10 minutes--but then it came up with something I hadn't thought of. It concluded that the display had two bitplanes, which were XOR'ed together to produce the output. One of the bitplanes was for data and the other for an attribute, used for inverted video cursors. This would, of course, require twice as much screen memory as the service manual seemed to say. Gemini's explanation was that the service manual was only stating the capacity of each video RAM chip, and there was more than one.
This explanation made sense, and I was able to identify screen display modes that fit with the hypotheses. Indeed, it was possible to write separately to a data plane and an attribute plane. Both writes go to the same 68000 logical address, but the screen memory control register selects which plane they go to.
I did some more experimentation, but I was still getting results that didn't fit with the two-plane hypothesis. Finally, I guessed that maybe there are more than two bitplanes (the 16500 units, which have color displays, have four). With some experimentation, I was able to confirm the hypothesis. There are four bitplanes, which one can label:
- 0: data
- 1: overlay-data
- 2: overlay
- 3: attribute
There is a complicated function that combines these to the final display. One nice feature of this function is that if you keep attribute at 0, and set overlay to 1, then the screen shows overlay-data instead of data. This allows for pop-up windows without having to save what's in the background: one just draws the pop-up in overlay mode. I haven't checked empirically, but this has got to be how the system software does its display.
The memory control register works as follows. It is a binary word of the form:
- 0000zyxw0000dcba.
The four low bits of the low byte (dcba) control which planes are active for a write, with a clear bit indicating an active plane. Thus, 0000zyxw00001111 results in no-functionality, while 0000zyxw00000000 writes to all four planes at once. The four low bits of the upper byte (zyxw) control what value is written when the 68000 writes a 1 to a logical memory bit. These are reversed: if the value bit in the control register is 1, a 0 is written, and if it's a 0, a 1 is written.
I almost had everything. But not quite! One day I turned up the adjustment pot in the back of the scope for the screen brightness. And then to my surprise I saw that while I had previously thought there were two screen colors--black and white--there is actually a third, namely gray. (If I had read all of the service manual, I would have seen on page 6C-13 that the display connector had both a half-bright and a full-bright pin.) So I had to experiment again and revise what I thought was the function combining the four bitplanes into a single screen pixel. Here is the final function, since then double-checked by finding in the ROM where it's loaded as a screen palette:
0000: 0 0001: 1 0010: 0.5 0011: 0.5 0100: 0 0101: 0 0110: 1 0111: 1 1000: 1 1001: 0 1010: 0 1011: 0 1100: 1 1101: 1 1110: 0 1111: 0The planes, left-to-right, are: attribute, overlay-data, overlay, data. The outputs are 0 (black), 0.5 (gray) and 1 (white). (In the actual palette registers, these are written as 0, 1 and 2. Sadly, there is only one shade of gray.)
-
Extracting ROM
02/11/2026 at 14:10 • 0 commentsI "needed" to figure out how disk i/o worked. While I could keep on using Ghidra on the system test and OS files, I felt that to make real progress on reverse-engineering, I'd need to pull the 64K ROM. I already pulled short bits and pieces with my memory-to-screen dumper, transcribing it manually or with AI, and disassembling, but it was a slow and annoying process.
![]()
It was time for something quite different. I probably could have physically taken the unit apart and pulled the data off the ROM chip. But it's scary to do that. The oscilloscope board is above the computer board, and above both is the scary power supply with a big warning. I had to dig through there once when I shorted something in setting up my Gotek and had to reinsulate the ribbon cable (it burned through the insulation on one wire but the wire was intact).
The alternative I settled on was QR codes. Now that I could run C code on the device, I could put a chunk of ROM into a QR code, take a photo of the screen, and decode on my PC.
The first challenge was finding a QR code library for C that I could run on the device itself. I tried some and they were slow or crashy on large QR codes. I was abotu to give up, until I came across the Nayuki library. It worked great on the device.
The second challenge was decoding a big sequence of QR code screen photographs programmatically. It turns out that most of the standard QR code python libraries weren't able to handle the combination between the logic analyzer's little CRT screen and the large QR codes I was using. I don't know if it was the scan lines, the specific contrast issues, the screen curvature, or the Moire. I ended up going with the QReader library which uses machine learning to preprocess the QR code. As a result it takes several settings. But it works. For most of the images.
I ended up sending 512 bytes of ROM at a time, base64-encoded (I couldn't get binary-encoded QR codes to decode properly using QReader). QR code generation on the logic analyzer was slow, and I added a bit of a pause. As a result, each QR code was shown for about 10 seconds at a time. I had a Sony A7RII camera pointed at the screen, with an external timer taking a picture every two seconds, in case not every picture worked. The setup took about a thousand pictures. I downsized them with Imagemagick, and ran them through QReader to reconstruct a ROM binary. All code is here.
![]()
At some point the cat walked between the camera and the screen, but didn't stay long enough to mess up all the pictures of that particular QR code. I had one hiccup: I accidentally deleted the photos mid-way through the run, and had to re-take the rest.
But then I had a nice 64K ROM file, which I was able to run through Ghidra.
-
Developing in C
01/25/2026 at 23:02 • 0 commentsThe hardest part of developing in C for the 68000 is finding a compiler. The amount of Googling was insane. I came across some projects to compile gcc as a 68k cross compiler, but they were Linuxy. I tried under Cygwin and failed. And then I found a MingW build on sourceforge. I combined this with Tom Storey's bare metal toolkit, modified crt0.S and platform.ld to load all code into RAM at 0x00984500, and it was off to the races. Again, there was some python scripting to add the HP's executable file header which I was slowly reverse-engineering (latest version here). I hadn't done programming on old hardware with a modern compiler before, and---wow!--gcc is really, really good with full optimization.
I wrote various simple functions to interact with the hp's code: this is now my libhp165x library. It is satisfying to be able to write code in a modern environment and produce binaries that are less than 35K in size.
Finally, I started porting my 2002 Wiztris falling blocks game (here is the current code). It was very satisfying to get it to work. I had to remove the high score code from the initial version, however, because I didn't know how to do disk i/o.
-
Beginnings of reverse engineering
01/25/2026 at 22:50 • 0 commentsWith the chunking of the files discovered and the executable file origin known, I was able to make a start on disassembling pieces of the PVTEST_ self-test code with Ghidra. Messages shown by the test code to the user were useful, as looking at references in the code to them I was able to located PVTEST_ code that writes text to the screen. Eventually, I found that the screen buffer is at 0x600000. It turns out, I could have also found this from the (probably non-working) MAME code for related units that I later found.
Finding an easy to use assembler took a bit, and I finally settled on EASy68K. It produces Motorola S-Records, and I wrote a python script that copied the binary data from the S-Record file to the beginning of the PVTEST_ code. I could then put the modified code into a .LIF file, and then convert that to an .HFE file for the Gotek Then I could just launch my code by going to the self-test in the oscilloscope's I/O menu, and switching disks in my Gotek drive. It was awkward, and there were a lot of reboots.
Writing to the screen turned out to be more confusing than I expected. Writing zero bits did nothing. Writing one bits did different things at different times. It turned out from the Ghidra disassembly (I could also have learned this from the MAME code) that there were different writing modes controlled by a write-only hardware register at 0x0200100. For instance, if it's in mode 0xFE00, then writing 1's produces white pixels, while in mode 0xFF00, writing 1's produces black pixels. Discovering the intricacies of the screen i/o would still be for the future, but at least I was able to do basic output--put text on screen, and draw pixels.
Next was input. I eventually found a ROM get key routine that the code called (0xEB38). With input and output I was in theory able to do a lot.
I wrote various small bits of assembly test code, including a lot of code to dump memory locations to the screen. (The latest version is here. You can specify which address to go to, and press RUN to toggle whether the display is static or dynamic.) I was able to learn where in RAM the ROM code stored the current key and various information about the spinner state. (See my ever-evolving "messy notes".)
-
Beginnings of reverse engineering
01/25/2026 at 22:50 • 0 commentsWith the chunking of the files discovered and the executable file origin known, I was able to make a start on disassembling pieces of the PVTEST_ self-test code with Ghidra. Messages shown by the test code to the user were useful, as looking at references in the code to them I was able to located PVTEST_ code that writes text to the screen. Eventually, I found that the screen buffer is at 0x600000. It turns out, I could have also found this from the (probably non-working) MAME code for related units that I later found.
Finding an easy to use assembler took a bit, and I finally settled on EASy68K. It produces Motorola S-Records, and I wrote a python script that copied the binary data from the S-Record file to the beginning of the PVTEST_ code. I could then put the modified code into a .LIF file, and then convert that to an .HFE file for the Gotek Then I could just launch my code by going to the self-test in the oscilloscope's I/O menu, and switching disks in my Gotek drive. It was awkward, and there were a lot of reboots.
Writing to the screen turned out to be more confusing than I expected. Writing zero bits did nothing. Writing one bits did different things at different times. It turned out from the Ghidra disassembly (I could also have learned this from the MAME code) that there were different writing modes controlled by a write-only hardware register at 0x0200100. For instance, if it's in mode 0xFE00, then writing 1's produces white pixels, while in mode 0xFF00, writing 1's produces black pixels. Discovering the intricacies of the screen i/o would still be for the future, but at least I was able to do basic output--put text on screen, and draw pixels.
Next was input. I eventually found a ROM get key routine that the code called (0xEB38). With input and output I was in theory able to do a lot.
I wrote various small bits of assembly test code, including a lot of code to dump memory locations to the screen. (The latest version is here. You can specify which address to go to, and press RUN to toggle whether the display is static or dynamic.) I was able to learn where in RAM the ROM code stored the current key and various information about the spinner state. (See my ever-evolving "messy notes".)
I also realized that I didn't need to do the awkward and long-drawn out process of booting up the unit and launching my code from the self-test menu option. I could simply make a Gotek virtual disk where my code was called SYSTEM_, and it would happily boot the code. Nor did my code need to be overlaid on the existing PVTEST_ code. It could be stand-alone and could be small so it would load fast (though it needed some zeroes that I erroneously thought was some kind of padding at the end).
-
HP 165x LIF files and chunking
01/23/2026 at 03:33 • 0 commentsIt was time to download and fire up Ghidra, and run it on PVTEST_ to get to know what the code is like. I ran it in 68000 mode.
There were some oddities with the disassembly. And a crucial piece of information was missing: I didn't know at what memory location the code was loaded, the origin of the assembly file (I had a pretty good guess of where the file header ended and where the code began--there was a JMP instruction that looked like a start). Looking at the disassembly, I had an idea. The code accessed various data addresses via instructions like
movea.l #0x9aa84c,A0and
pea (0x9a5939).lI noticed that often these addresses were pretty much in order. Which makes sense, in that in earlier pieces of code you tend to access earlier data. There was a good chance that these referred to data in the program itself. I had a clever (I think) idea that I would look at sequences of human-readable null-delimited ASCII strings, of which there were many, and look for code in the disassembly that accessed memory locations whose spacing equalled the size of these strings. With the help of dumping what looked like the right instructions from the Ghidra disassembly I eventually found some instructions and corresponding strings that seemed to match. This would let me know what memory address the strings were loaded at, and hence I could calculate the address the whole code got loaded at.
Except it didn't quite work.
Moreover, there were some weird things. Occasionally text strings were interrupted by a short piece of junk. And occasionally disassembly would break down weirdly.
I had also been looking at the way the oscilloscope saved data. I noticed that it saved some of the data in 256 byte chunks (which happens to be the LIF block size), each starting with 0x00 0xFE. I guessed that these two bytes indicated the length of the data to come (254 bytes). And then I guessed that maybe the executable files were similarly divided into 256 byte chunks, and what I thought was junk was a chunk header. It sure looked that way.
The chunking setup also allowed the system to have files whose length wasn't a multiple of block size, even though the LIF directory only listed file sizes in block size multiples. For the last header might be a number smaller than 0x00 0xFE.
I wrote a quick and dirty unchunker and ran it the code. Suddenly everything started fitting into place. Looking at the spacing of memory accesses and lengths of strings, I was able to figure out that the code is loaded at 0x00984500. Yay!
Eventually, I incorporated the unchunking and chunking into my lifutils.py code. Indeed, once I managed to extract the ROM code (a future story), I learned that the ROM disk code automatically chunks files being written and unchunks them on read, so we should really think of the chunking headers as a special feature of the LIF file system as found on the HP 165x units.
-
Data transfer
01/23/2026 at 03:17 • 0 commentsThe HP 165x analyzers use the LIF file system, like many HP computers. The HXC-firmware Gotek uses HFE files. (I tried RAW files on the flashdrive, but they didn't work.) But it's possible to convert between HFE files to RAW format with the HXC floppy emulator software, and RAW format is just a series of sectors, which should make a perfectly fine LIF image.
hxcfe -finput:FILE.HFE -conv:RAW_LOADER -foutput:FILE.LIFYou can then extract files from the FILE.LIF volume using the well-established lifutils. And in theory you can change the FILE.LIF volume, and turn it back to an HFE file on your Gotek's flashdrive with hxcfe again.
Except there are some hiccups. There is no layout file for HP 165x analyzers included with hxcfe. My first attempt was by converting the HFE file to XML, and then it was pretty easy to write a python converter from LIF to XML, which hxcfe can convert to HFE.
Eventually, I made an HP layout file. The latest version is here (it's improved from the original because it includes special track 79 stuff which has nonstandard sectors that the units use to check if they want to boot from the floppy). So you can now convert from LIF to HFE:
hxcfe -uselayout:hp165x79.xml -finput:FILE.LIF -conv:HXC_HFE -foutput:FILE.HFEBut there are still difficulties. While I was able to extract a SYSTEM_ file from the main floppy and a PVTEST_ (PV = Performance Verification, apparently) file from the self-test flloppy, lifutils refused to write them back for multiple reasons. First, the LIF volumes were lacking some metadata. Second, the file IDs for HP 165x executable files, namely C001, are not supported by lifutils. I could (and did) modify lifutils to get around one or both of these issues, but it was time to write a LIF tool more specialized to the HP 165x setting.
So I wrote a little lifutils.py python utility (latest here) thanks to this great writeup. I had to do be careful about the special track 79 stuff. Now I could write data back and forth. In particular, I could change a text stringin SYSTEM_ with a binary editor and see the changes show up on the oscilloscope screen. Hacking is on!
It later turned out that I was missing one crucial ingredient about how the file system worked. But that's for the next installment.
-
The Gotek
01/17/2026 at 14:26 • 0 commentsI've always been worried about aging magnetic media. Late last year, my faithful oscilloscope didn't boot up. Fortunately, I had multiple copies of the system discs (the OS lets you make copies of a disc), so I was able to get it back up, and I made a new copy. But I was worried. I finally got a Gotek to replace the aging Sony MP-F52W-30 drive, made an adapter to interface the HP's Sony-compatible cable with the Gotek (there are some instructions here and more details here). Getting the disc change connection working was the hardest. It turns out that while my SFRKC30.AT3 Gotek looks from a distance like an SFRKC30.AT2 Gotek, it exposes the PA14 pin in the same place as the older SFRC922, and so on the AT3 one should follow the older unit's instructions.
I was now able to use HFE files (which are a complicated file format that handles all sorts of non-standard discs) on a flash drive to boot the oscilloscope. Yay! To this day (January 17, 2027), I still haven't been able to get direct image files to work, though.
![]()
-
The beginning
01/17/2026 at 14:13 • 0 commentsAbout a decade ago, I saw an HP 1653B Oscilloscope and Logic Analyzer on Craigslist for $50, including probes, pods, diskettes and three volumes of manuals. I was going to buy it, but when the seller heard about my hobby interests, he just gave it to me for free. Thank you!
The copyright date on the ROM code is 1987. It's a great unit. Reliable and nice. I've used it as an oscilloscope (but not logic analyzer) for many projects. There are full manuals here.
The system has a 3.5" floppy drive which it boots from. I've always been afraid of the floppies failing.
Some years back, I made a Greaseweazle out of a Blue Pill and an old PC floppy drive. This let me transfer the system disc to my PC:
gw read --format ibm.scan --tracks=c=0-79:h=0-1 outputfile.hfe::bitrate=500The HFE file can then be converted to a raw file with SAMDisk:
SAMDisk copy outputfile.hfe outputfile.raw
The file system is LIF and I was able to use lifutils to extra the SYSTEM_ operating system file from the raw files. I tried to disassemble it with Ghidra in order to run my code on it, but failed (more on that later).
![]()
Alexander R Pruss


