Disassembling the Microtronic 2090

Copyright © 2024, Jason T. Jacques

Last updated 2024-09-21

When a suggested YouTube video about the Science Fair Microcomputer Trainer caught my eye, I could never have anticipated the rabbit hole that I was about to stumble into.

This document offers a long read into my involvement in Michael Wessel's endeavours to acquire and understand the ROM code of the Busch Microtronic 2090. It is not a tutorial, but rather my recollection and reflections. At each juncture I provide copious references and delve into the background and history of the development or discovery, taking the opportunity to disclose some hopefully interesting titbit or aside. Despite its length, it is almost certainly incomplete. Nevertheless, to aid the more focused reader, this text is split into several sections. As far as possible each section stands alone.

Vintage microcomputer trainers

In February 2024 Michael Wessel uploaded the latest video in his series offering a brief overview and profile of some early vintage educational computer systems. In his video, it appeared that Michael had relied on one of the unreferenced sources that misreport the launch date of the Microcomputer Trainer as 1976. While the TMS1100 may have been initially released in 1976, the Science Fair kit had, in fact, been listed as new for '85 on page 155 of the 1985 Radio Shack catalogue.

1985 Radio Shack catalogue, page 155
Announcing the Science Fair Microcomputer Trainer in the 1985 Radio Shack Catalogue

Not only that, the Science Fair is based on the older Gakken FX R-165, released in 1981. Building on Gakken's established Denshi Block approach, the FX R-165 was a hybrid electronics kit and microcomputer trainer. The FX series itself following on from the popular EX series of electronic project kits. Nostalgia for these systems eventually compelled Gakken to re-release both the EX 150 electronic kit in 2002 (and a tiny Electronic Block Mini in 2011), as well as their own recreation of the FX R-165 computer block, the GMC-4, in 2009.

Gakken FX R-165
The Japanese Gakken FX Microcomputer R-165

Back in the early '80s, the Tandy Corporation, owner of Radio Shack, had noted the novelty of this low-cost educational computer trainer. It seems that Tandy made arrangements with Gakken to bring the core of the FX computer to their own house brand Science Fair. Oddly, despite Science Fair being most famously known for spring-terminal electronic kits, Tandy decided to remove the electronic experimenter aspect of the product and instead focus solely on the microcomputer aspect. The result was the Radio Shack exclusive Science Fair Microcomputer Trainer, a cheaper, cut-down, cardboard facsimile of the Japanese product. The Science Fair version lacked the electronic experimenter focus, normally typical of the brand, and marked the mysterious E3 instruction, responsible for handling external input, as NOT USED.

My own research in to the Science Fair, and later the Gakken R-165, had only developed over the preceding few years. I've long had an interest in extremely basic microprocessors with a view to their application in computer science education. In my teens I constructed Robert Östling's The Brainfuck Computer which implements the unfortunately named Brainfuck programming language. While Brainfuck (or BF as I wish it was more universally called) is an esoteric language, it is fully Turing complete and provides a novel playground for thinking about fundamental computation and minimal CPU instruction sets. Just unfortunate about that name.

Brainfuck Computer Schematic
Robert Östling's Brainfuck Computer Schematic

I had stumbled across the Science Fair several years earlier while investigating other early educational microcomputer trainers, such as the Heathkit ET-3400 (Motorola 6800), the Micro-Professor MPF-I (Zilog Z80), the Science of Cambridge MK14 (National Semiconductor SC/MP), the Intel SDK-85 (Intel 8085), and the MOS KIM-1 (MOS Technology 6502).

My enquiries about the KIM-1 had lead me to a video by Artifact Electronics in which he demonstrates a small collection of early consumer computing devices. While each of the devices shown has its own charm, as an owner of several Science Fair Electronics kits I was captivated by the, then unknown to me, Science Fair Microcomputer Trainer.

Artifact Electronic's Microcomputer Trainer video
Artifact Electronic's Science Fair Microcomputer Trainer

Many early microcomputer trainers expose the full power of their CPU to the user, offering a basic machine code monitor ROM, and just enough peripheral hardware, to allow the user to enter and execute simple programs. Not so for the Science Fair. The Science Fair is aimed at beginners and children, with the box proclaiming For Ages 12 to Adult. Instead of expecting the user to learn the potentially perplexing TMS1000 series instruction set, it instead presents a simplified collection of just 16 opcodes - augmented by a further 16 built-in subroutines - which are interpreted by compact virtual machine embedded in the monitor ROM.

As Michael's YouTube video was then fairly recent, I decided to fire off some additional information about the Science Fair, and its history, to the errant YouTuber. A decision that would end up eating many more hours of my life than I care to admit.

The Science Fair is a fairly well explored machine. Back in March 2015, Sean Riddle had decapped the TMS1100 which powers the trainer. By removing the plastic enclosure of an IC, and revealing the die inside, the contents of mask-programmed ROMs can be read, literally, right from the silicon chip. Despite the destructiveness of the process, decapping is sometimes required to recover the ROM from TMS1000 series microcontrollers. With the right knowledge, electronically reading the ROM is relatively straightforward from early TMS1000 chips. However, later revisions include protection circuits to obfuscate some, or possibly all, of the ROM code. Unfortunately, Sean's attempt to decap the MP1312 MAS^8414 labelled TMS1100 had damaged the die making a purely optical dump impossible.

Damaged Science Fair Microcomputer Trainer die
The damaged TMS1100 die from a Science Fair Microcomputer Trainer by Sean Riddle

Fortunately, some time later decle, with his own interest in the Science Fair, and Sean began to communicate about the possibility of securing a copy of the ROM code in a less destructive fashion. Together, and based on the work of Kevin "Kevtris" Horton, they managed to electronically extract a partial ROM. Combining the bits visible on the damaged IC with an obfuscated electronic dump of the code, the two managed to reconstruct a complete copy of the ROM.

Just a few weeks later, in April 2016, decle had proven the validity of the code and developed an emulator for the Science Fair, called Nybl. Nybl is itself a curiosity, and an extremely clever feat of engineering. Nybl emulates the Science Fair on the even more vintage Mattel Intellivision released back in 1979. Despite the limited hardware of the host system, decle notes that his emulator runs at about one third of the speed of a real Science Fair Microcomputer Trainer and takes an average of just 5 CP1610 instructions (the CPU at the heart of the Intellivision) to process a single TMS1100 command.

decle's Nybl
Animation showing decle's Nybl emulator, for the Intellivision, in operation

As it happened, I had not been the only one to contact Michael about his Science Fair video. decle had also reached out regarding his own experience with the Microcomputer Trainer, including information about Nybl and how he had helped to acquire the Science Fair ROM.

While Michael had been documenting a variety of early educational machines, his real passion is the Busch Microtronic 2090. The Microtronic is an early educational computer system created by the Busch Modell company for the West German market and released in 1981. Like the Science Fair, the Microtronic hides the complexities of the TMS1600 microcontroller and, instead, presents the user with a simplified instruction set which is interpreted by the monitor ROM's built in virtual machine. Since 2016 Michael has worked on a number of impressive emulators and recreations of the Microtronic. Most recently, in 2024, his Microtronic extension project the PicoRAM 2090, was confirmed as a Grand Prize winner of the 2023/10 Retro Challenge.

Busch Microtronic 2090
Front cover of the Busch Microtronic 2090 brochure

In developing his recreations and add-ons, Michael had sought and archived a huge library of information about the Microtronic in his Github repository. Not only marketing materials, reviews, and the manuals - of which there are several - but also the schematics and other important historical documents. One of the most interesting is the diplomarbeit of one of the key individuals in the development of the Microtronic, and later the owner of Busch, Jörg Vallen. Vallen himself had provided some information, and permissions, to aid Michael's various projects. Despite this encouraging support from Vallen one artefact was missing from Michael's collection: the monitor ROM. The ROM contains the reference implementation of the Microtronic instruction set, crucial to making a truly accurate recreation. Worse still, it seemed that Busch themselves no longer had a copy of the ROM source code.

Yet, there was hope. At least two other companies must have had copies of the ROM. The first was a small firm called MRT. As detailed in Vallen's diplomarbeit, Busch did not develop the ROM code for the Microtronic internally. Instead this task was outsourced to another firm, MRT: Mess- und Regeltechnik. Sadly, it seems, MRT has long since been dissolved and no contact could be made. However, still one final firm must have received copies of the elusive ROM: Texas Instruments.

TMS1000 die layout
The layout of a TMS1000 family die, showing the ROM area (1), by Texas Instruments

The TMS1000 series chips store their code in a mask ROM. Once the ROM code for the chip is ready, Texas Instruments would create a photomask that was used during the manufacturing of the wafer to permanently embed the desired program in the fabric of the silicon itself. Unfortunately, despite Michael's efforts, contact with TI was also unfruitful and the ROM was considered lost. What was known, however, was that TI had a process for verifying these chips. A process that allowed the code in the TMS1000 to be accessed and executed. This test mode is further described in a TI patent, and was the very mechanism that Sean, decle, and Kevtris had been exploiting to dump the ROM from other TMS1000 series chips, such as in the Science Fair, almost a decade earlier.

Interestingly, Busch includes a photograph of what appears to be a TMS1600 die in their marketing materials for the Microtronic. Whether or not this is the actual die inside the Microtronic CPU, or a stock photograph from another TMS1000 series device, is unknown. Noting the white body and 64 gold contact pads, it may very well be an example of a rare ROM-less chip. Either way, TI or Busch elected to prevent would-be reverse engineers getting a look at any code that might be present. The blacked-out area in the upper right obscures the location of the mask-programmed ROM.

Photograph of a TMS1600 die
Possible photograph of a TMS1600 die

The Microtronic uses the more powerful TMS1600 chip. Based on the TMS1100, the TMS1600 features more I/O, a deeper hardware call stack, and more ROM space. While decle, Sean, and Kevtris had managed to dump the ROM from TMS1000 series chips, the exact technique had not been made public. Further, while related to the TMS1100, the specific protocol required for dumping the TMS1600 was unknown to all outside of TI. A fact attested to by the lack of TMS1600 ROMs in the wild. Yet, the idea that it might be possible to extract the ROM code from a TMS1600 was tantalising. It would represent an opportunity to finally realise an authentic recreation of the Busch Microtronic.

TMS1600 in the Busch Microtronic 2090
The Microtronic's TMS1600 CPU in situ, to the left of the computer's RAM (white socket)

It was then, in late February 2024, as Michael pursued the goal of dumping the ROM, that I was fortunate enough to be included in an email exchange between Michael and decle.

Dumping the ROM

In 2016, decle, with the assistance of Sean Riddle, had successfully dumped the ROM of the TMS1100 at the heart of the Science Fair Microcomputer Trainer. Based on the work of Sean Riddle, and by extension Kevin "Kevtris" Horton, decle had used a Parallax Propeller chip to successfully dump 7 of the 8 bits in each byte of the 2,048 byte ROM. decle ported this work to the ubiquitous Arduino platform, and verified that he was able to recreate the dumping process.

The main TMS1000 series are PMOS chips, and nominally run using negative voltages. While some TMS1000 series CPUs run at as much as as -15 V, the TMS1100 in the Science Fair and the TMS1600 in the Microtronic use a more modest -9 V. Negative voltages might seem unusual, but these are only negative relative to the chip ground. Simply swapping the terminals of a normal power supply or battery gives an effective negative voltage. As the TMS1000 series uses positive logic, where a high signal is less negative, this also aligns the signal direction with the other logic.

With the CPU voltages sorted, we need to interface with the Arduino. While the TMS1000 chips operate at relative 9 V, the Arduino works at just 5 V. To get the Arduino to talk to a TMS1000 we need to boost the signals. decle achieved this using a simple inverting amplifier, made up of a couple of resistors and an NPN transistor. One amplifier is needed to boost each output. This inversion is handled in the software on the Arduino. Equally, to bring the 9 V output signal to a level appropriate for the Arduino a simple resistor divider is needed to protect the Arduino inputs from excessive voltage. decle provided a simple schematic to illustrate these circuits and where to make the connections on both the Arduino and the TMS1100.

TMS1100 dump circuit
Inverting amplifiers and voltage divider for Arduino

Armed with decle's schematic and the the new Arduino code, both Michael and I were able to successfully recreate the dump procedure on the Science Fair.

The ROM in TMS1000 series chips is logically separated into several levels: chapters, pages, and words. The TMS1000 has just one chapter which holds the entire ROM of 1,024 bytes. Still, due to the limited 6-bit program counter in the TMS1000 series the ROM must be subdivided into 16 pages of 64 8-bit instruction words. More advanced chips, such as the TMS1100 and TMS1600, require multiple chapters, each of 16 pages. Two chapters for the 2,048 byte TMS1100 and four chapters for the 4,096 byte TMS1600.

The precise mechanism to dump the ROM is, in theory, not too complex. The chip is held in reset, the desired program counter is clocked in serially over K1. The page and chapter are transferred in parallel over K2, K4, and, K8. The K2 pin serves double duty, indicating to the chip that the address should be loaded into the program counter, and the value can then be serially clocked out via the O7 pin. The main issue is getting all the timings and sequencing correct. While this gave us 7 of the 8 bits in each byte, I considered that it might be possible to infer the correct value automatically.

All jump instructions, both CALL and BR, have the high-bit set. The test procedure allows both the program counter to be read and a given particular ROM addresses to be executed. Releasing the chip from reset allowed the chip to begin executing the current instruction. If the instruction was a jump, the new program counter would be out of order. Notwithstanding some edge cases, simply executing each byte and checking the program counter would allow most jumps to be validated. Hacking on decle's tool, I was able to apply this approach and conclusively identify the value of 2,007 out of the 2,048 bytes; more than 99% of the bits in the Science Fair ROM. Additionally, it correctly inferred the rest of the bits, matching the known dump.

In my experiments, I noticed that by tweaking the execution timings, some chips - such as the TM1100 in the slightly earlier Gakken Microcomputer R-165 - could be compelled to reveal the entirety of their contents. Even more esoteric and inventive techniques can likely be applied to further interrogate the internals of the chip using carefully crafted test algorithms and techniques, such as those hinted at by the patent and TI's documentation. Combining these techniques it should be possible to extract the user-defined I/O mapping, discussed later, and potentially even reverse engineer customised instructions. Bridges that can be crossed as needed.

The next challenge would be to translate these successes to the TMS1600 at the heart of the Microtronic.

Pinning down the TMS1600

Buoyed by our successes with the TMS1100, Michael attempted to translate what we had learnt to the Microtronic. Directly applying the technique did not, unfortunately, result in a ROM dump. The first step was to try and get the Microtronic to clock along with the Arduino. This process allows the Arduino to control the clock and send addresses to the chip. decle demonstrated the basic technique in his handy video. In addition to correctly wiring the clock, and holding the chip in reset, this requires that the appropriate R pin is held high. Without knowing what the internals of the chip look like the only solution was guesswork.

Unfortunately, unlike the TMS1100 in the Science Fair, the TMS1600 at the heart of the Microtronic is not easily taken out of circuit. While it might be possible to test the chip in circuit, Michael carefully isolated each test pin. After several painstaking attempts, the R8 pin was identified as the likely candidate. Unfortunately, this did not result in a dump.

Michaels next task was to identify the protocol. The exact signals, and sequence, are crucial to getting the TMS1000 series to spill its secrets. First, the ROM on the TMS1600 is twice the size of the TMS1100. To access this extra space required a further bit in the chapter. decle surmised that this bit would be on the K8 pin, which appeared to result in interesting output; though still not the ROM. The final part of the protocol was the timing. The TMS1000 series is tested by loading the desired address in both serially and in parallel, simultaneously. This requires careful sequencing. After testing each of the possible timing sequences, Michael achieved seemingly sensible results. The chip appeared to be responding, but we still were not quite there.

Several further changes to external circuit, the power supply, and the PCB were tested. Eventually, after several days, and much consternation, Michael succeeded. Achieving the ROM dump required removing a support chip, tweaking resistor values, isolating the clock signals, and carefully controlling the voltage and power supply. Michael carefully documented this important breakthrough in his contemporary YouTube video and later Hackaday log. For a more thorough and accurate description, I encourage you to consult his reflections. Interestingly, the dump did not have any protection bit. In the end, after dealing with the minor changes for the extended ROM space and updating the timings, decle's original dumping program seemed to have cracked it. The ROM was finally revealed.

A note on the Gakken FX Computer R-165

The Science Fair Microcomputer Trainer has a well tested ROM. It's been supported in MAME since 0.172, shortly after Sean Riddle and decle successfully recreated the ROM from a damaged decap and a partial dump. Interestingly the artwork in MAME, and the name displayed, is that of the Gakken FX-Micom R-165. This is on the erroneous understanding that the ROM is identical to the Science Fair Microcomputer Trainer. In fact, my own dump of the R-165 has revealed some small, but impactful, differences in the ROM. The revision make two changes. The first change, to key detection, is likely to reduce switch bounce with the morse-style key switches used on the Science Fair. The second change improves the randomisation of, in my opinion, the best of the built-in games: Rat Bashing. The randomisation in the R-165's Whack-a-Mole alike game is ineffectual, and subsequent runs have predictable sequences. This is improved for the Science Fair version of the ROM. For posterity, the differences between the two ROMs is reproduced here in a format suitable for xxd.

00000308: 0e
00000311: 84
000004c2: 65
000004c5: 60
000004cb: 08
000004ce: a7
000004d6: f3
000004d8: a1
000004dd: 76
000004de: 24
000004e1: 25
000004e7: 71
000004eb: 49
000004ec: 0e
000004ef: 2a
000004f0: 96
000004f3: 74
000004f5: 9e
000004f7: 49
000004f9: ab
000004fa: 0f
000004fc: b3
000004fd: 4e
000004fe: 28
00000544: 5f
00000548: 99
00000549: 9b
0000054a: 41
0000054c: be
00000550: 1c
00000551: 0c
00000553: 0b
00000554: 46
00000555: 15
00000559: 08
00000560: b4
00000562: 04
00000564: 0e
00000565: 0b
00000566: 1c
00000568: 21
0000056a: c7
00000572: 0c
00000744: 00
00000749: 00
0000074c: 00
00000753: 00
00000759: 00
00000765: 00
00000766: 00
00000772: 00

Assuming the above is saved in a file named sfmt-to-r165.hex it can be merged into to a copy of the mp1312 ROM file, as used by MAME, to recreate the true Gakken FX Computer R-165 ROM.

$ xxd -r sfmt-to-r165.hex mp1312

Disassembling the code

The nature of the TMS1000 series, with their permanently factory programmed mask ROM combined with their obsolete status, somewhat limits the impetus for maintaining available tooling for these chips. Back in the 1970s, TI offered a variety of tools for development: ROM-less development chips (the TMS1097, TMS1098, and TMS1099); System Evaluator units to aid in debugging (the SE-1 and SE-2, as well as the later SE-1000P, SE-1100P, and SE-1400); a local development system, called AMPL1000, for TMS9900-based mini computers; as well as offering a fully-featured development environment, including simulator, on "nationwide time-sharing systems and at TI computer facilities". Unfortunately, the development chips are rare, the system evaluator hardware obsolete, and the software surely lost to time.

Fortunately, as an early device of its type, the TMS1000 series is a relatively simple processor. The fixed-size 8-bit instruction word means there are only 256 possible bit patterns to decode. Further, as the TM1000 series instruction set is limited to immediate operands, directly encoded into the instruction, the entire standard instruction set can be concisely described on one side of a sheet of regular printer paper.

One caveat to this apparently simple decoding is the program counter, or PC. With most processors the PC is sequential. Not so for the TMS1000 series. Here the PC is a type of shift register and "counts to the next ROM address in a pseudo random sequence". While one source claims that this was to make piracy of the ROM more difficult, this is unlikely. The complete sequence is shown as part of the sample program in the contemporary TMS1000 Series Programmer's Reference Manual, available direct from TI for just $4.95. The more likely explanation lies in the fact that linear feedback shift register counters are known to be more efficient, and occupy significantly less die space, than traditional binary counters when the outputs are required to be synchronised.

Correcting for the unusual PC sequence is not crucial to a naive approach to disassembling the code, but failure to convert the actual location to a logical, sequential, order makes the resulting source indecipherable. Equally important is being able to account for the actual location in jump and call operations, necessitating either the disassembler also translating the target addresses (potentially ill-advised, as we may return to later) or indicating the true in-ROM address of each instruction. Another important note on the PC, which we will undoubtably return to, is its starting location. While the PC itself is set to zero, the page registers, PA and PB, are reset to 0xF on power-up. This means that the program in ROM does not start in the first page, but rather at the beginning of the last page in the first chapter.

Chips in the TMS1000 series have one final trap: the instruction PLA. The instruction PLA, or OPCPLA, is a programmable logic array, specified by the customer and configured during manufacturing of the chip. The OPCPLA allows the basic instruction set of the chip to be redefined. Microprogramming, the process of redefining the OPCPLA, is comprehensively described in Section IX of the Programmers Manual. Fortunately, it seems, changes to the instruction PLA are relatively rare. Firstly, TI's recommended process is to always attempt to use the standard instruction set before resorting to microprogramming. Secondly, over half of the instructions cannot be substantively changed. And, lastly, the SE-1 and SE-2 system evaluators use the standard instruction sets, making custom instruction PLAs more challenging and costly to test.

Given the comprehensive documentation, writing your own disassembler for the TMS1000 series is eminently achievable. Kevtris is known to have used a disassembler coded in BASIC, and similarly decle has written his own Python script. For the less motivated, like myself, perfectly good open-source options are available. My pick is naken_asm by Michael Kohn.

naken_asm was originally designed to provide a simple assembler for another Texas Instruments microcontroller, the TI MSP430, but now supports a wide variety of CPUs. Importantly, it includes support for assembling, and disassembling, code for the TMS1000 series. While the TMS1000 and TMS1100 have different instruction sets, naken_asm supports both. However, the TMS1600 that powers the core of the Microtronic does not presently appear on the list of supported CPUs. Fortunately, the TMS1600 (and TMS1400) standard instruction set is identical to the TMS1100 instruction set - with one notable variation.

To support the extended number of chapters in the more powerful TMS1400 and TMS1600 chips, the COMC instruction was replaced with the TPC instruction. COMC represents "complement chapter buffer", which allows switching between chapters on two-chapter chips. For four chapter chips, such as the TMS1600, the TPC or "transfer page buffer to chapter buffer" instruction allows the programmer to copy the lower two bits of page buffer into the chapter buffer, providing a mechanism to specify any of the four possible chapters.

As an open-source program, you could, of course, hack together an extension for naken_asm to support the TMS1600, or even simply replace the errant code in the TMS1100 decoder. However, the lazy option is simply to translate the generated source using something like sed, e.g.

sed 's/comc/tpc /'

The even lazier option is to do nothing. Which is exactly what I do.

Using naken_asm

naken_asm supports a variety of input formats, but a simple binary format can easily be generated from the plain text dumps we saved as follows.

$ grep "^[0-9A-F]\{2\}" microtronic.txt | xxd -r -p - microtronic.bin

First, we filter out the cruft using grep, insisting that all lines start with at least two hex digits. Then we restore the plain hex codes to binary format using xxd. With any luck we end up with a 4,096 byte file, which can be verified like so.

$ wc -c microtronic.bin
    4096 microtronic.bin

To actually disassemble the code, naken_asm comes with a complementary utility: naken_util. Documentation is pretty sparse, but the upshot is that you need to use the -disasm flag and specify the CPU type, which for us is the close enough -tms1100. Finally, this is followed by the filename to disassemble. naken_util sends the resulting code to stdout, so you may like to redirect that to a file with the basic output redirection operator: >. If you choose to correct COMC to TPC, by piping through sed, right before this redirect would be the time to do so.

$ naken_util -disasm -tms1100 microtronic.bin > microtronic.lst

Peeking inside microtronic.lst we find the following. The lost listing of the Busch Microtronic 2090 ROM.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
000 0/0/00: 4f     tcy 15
001 0/0/01: 0c     rstr
002 0/0/03: 04     dyn
003 0/0/07: 81     br 0x01 (linear_address=0x01)
004 0/0/0f: 0f     retn
005 0/0/1f: 28     ldx 0
006 0/0/3f: 4f     tcy 15
007 0/0/3e: 7f     cla
008 0/0/3d: 27     tam
009 0/0/3b: 04     dyn
00a 0/0/37: bd     br 0x3d (linear_address=0x08)
00b 0/0/2f: 0f     retn
00c 0/0/1e: 2c     ldx 1
00d 0/0/3c: ff     call 0x3f (linear_address=0x06)
00e 0/0/39: 2a     ldx 2
00f 0/0/33: ff     call 0x3f (linear_address=0x06)
...

Throughout this document I use naken_asm disassembly format and address notation.

Understanding the hardware

Having secured what looked to be a reasonable listing of the ROM, our first task was to attempt to validate it. Without access to an actual Microtronic to interrogate for clues, my first port of call was the schematic diagram that Michael had made available through his Github repository. There is a lot of useful information here, and we'll refer to it more than once. But, one thing in particular stood out to me. A curious link between R11 and K/L. As we shall see, this provides an important mechanism to control access to the external SRAM, and crucially offers an easily identified entry point for beginning to understanding the ROM.

Busch Microtronic 2090 Computer System Schematic
The schematic for the Busch Microtronic 2090

Much of the general information about the TMS1000 series can be found in the excellent and comprehensive TMS 1000 Series Programmer's Reference Manual. Additional information, including some specific information about the newer and extended TMS1600, can additionally be found in the TMS 1000 Family Microcomputer Data Book.

TMS1000 series chips have three main I/O channels that you must be aware of. Firstly, there is the O channel. O for output. The O channel is driven by a built-in 5-to-8-bit decoder called the Output PLA (Programmable Logic Array), or OPLA. The OPLA controls eight output pins on the device (O0 to O7) and is typically used to allow the chip to map individual values, such as the number 3, to a bit pattern that can represent the relevant digit on a seven-segment display. As the TMS1000 series are four bit processors, the OPLA retrieves the fifth bit from the so-called status latch (SL). This allows the ROM developer to easily define two different outputs for the same nominally 4-bit O register value, one with, and one without, the SL bit set. On most TMS1000 series chips, the output PLA is limited to 20 terms, that is to say that 20 of the 32 possible five-bit values can be uniquely mapped. However, by cleverly combining patters, it is possible to achieve larger numbers of output patterns than a naive mapping approach might imply. On the superior TMS1600, however, this limit is lifted. This allows each of the 32 possible 5-bit values to be mapped to an individual output pattern.

A second output channel is available in the form of the R channel. The R channel is simpler than its O counterpart. Here there is no PLA. Instead, the R channel comprises a series of output pins that can each be individually addressed using the Y register. These are usually used in concert with the O outputs, allowing the device to strobe across a series of seven segment displays, activating each one in turn. Indeed, the Microtronic does just this. Rendering individual numbers via the OPLA to the O pins, and selecting which position on the Texas Instruments TIL 393-6 display to render it via the R0 to R5 output pins. Still, the R channel has another common application, from which it may well get its name: indicating the row in a matrix keyboard layout.

In fact, on the Microtronic the R pins are connected to the columns of the keyboard. The keyboard panel on several Texas Instruments calculators, the original application for the series, is of the same design as that of the Microtronic but in the portrait orientation.

Most TMS1000 series chips have only four inputs in the form of the K port. These inputs are labelled, for their column value in binary, K1, K2, K4, and K8. Typically, the K port is wired to provide a matrix keyboard, with key switches at the intersection of the individual K inputs and the selected R outputs. Again, this approach is used with the Microtronic. The layout puts a key-switch at each of the 24 intersections between the six R outputs, R0-R5, and the four K inputs, K1-K8. By rapidly enabling and disabling each of the relevant R outputs, strobing them one-by-one, while reading from the K input port, any of the 24 keys can be identified.

The K port on the TMS1600 has one additional trick: a frequency divider on K8. In some applications the data input rate might be too high for the TMS1600 to reasonably respond, but by dividing the input count in hardware the CPU can spend more time doing actual computation. The divider can be configured to directly pass the input state or divide by 2, 10, or 20 at the time of manufacture. However, for devices like the Microtronic such division is unnecessary. In fact, as the K8 line is connected to the keyboard, any divider would make the keyboard unusable.

The garden variety TMS1000 might only have one input port, but the TMS1600 actually has two. It retains the 4-bit K port but adds a new 4-bit input: the L port. The L port offer a secondary set of four input pins, L1, L2, L4, and L8. The L port has a special feature, which again likely led, in part, to its name: it can be latched. Rather than provide only an instantaneous input, the L port can be switched into sense mode by the SE MODE pin. When SE MODE is high, any momentary input will be held until the latch is reset. A reset is caused when SE MODE is brought low. This latching mode allows the TMS1600 to avoid missing external data, as long as it can process the waiting state before the next input. In the schematic for the Microtronic, however, the SE MODE pin is not connected at all. In fact it is not even identified on the main IC. Fortuitously, the TMS1600 has an internal pull-down resistor on the SE MODE pin, ensuring that, by default, the L port simply pass through the momentary state.

While the TMS1600 present two input ports, on two sets of pins, they are, in fact, internally connected to the same 4-bit input bus by means of a multiplexer (mux). To select which input should be read the TMS1600 offers the K/L selector pin. When high, the L port is used. When low, the K port is selected. Like the SE MODE pin, the TMS1600 has an internal pull-down resistor on the K/L pin which defaults the chip to reading the K port. However, as we noted, the Microtronic connects the K/L input pin directly to the R11 output pin, putting this mux under software control.

Reading the SRAM

The Microtronic uses an external 2114 SRAM to store user code. By using a combination of the O (O0-O3) and R (R0-R5) channels of the TMS1600, the Microtronic can address any position in the SRAM. The S pin (sometimes known as the CS pin) on a 2114 must be held low to enable, or chip select, the device. On the Microtronic schematic S is permanently tied to GND. As the Microtronic uses the O and R ports to strobe the keyboard and drive the display, the SRAM is constantly attempting to present the contents of spurious addresses on its output. However, as the SRAM outputs are connected to the L inputs, this spurious data on the L input bus is no bother to the TMS1600. The ROM simply holds R11, and by extension the K/L selector, low which selects the K inputs and ignores the L inputs entirely.

To read the SRAM, the Microtronic ROM must drive K/L high using the R11 output. The mnemonic instruction to set a given R output high is, maybe unsurprisingly, SETR. The R pin is specified by the Y register. As such, we would expect to see the Y register set to 11 by means of the TCY instruction. While it would be possible to interleave these instructions with others, the sequence TCY 11, SETR is a logical starting point. The following multi-line regex searches for this sequence.

tcy 11.*\n.*setr

Amazingly, this indicates only one candidate location: 0/9/03. As hypothesised, the ROM sets R11, and thus K/L high. While this enables the L inputs, to actually read the SRAM, the ROM must execute the TKA instruction. TKA loads the data from the input bus into the A register. As we can see, a candidate TKA command follows just five instructions later. So, what do those five instructions do?

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
242 0/9/03: 4d     tcy 11
243 0/9/07: 0d     setr
244 0/9/0f: 1d     ldp 11
245 0/9/1f: dc     call 0x1c (linear_address=0x20)
246 0/9/3f: 17     ldp 14
247 0/9/3e: d9     call 0x19 (linear_address=0x36)
248 0/9/3d: 08     tka
249 0/9/3b: e3     call 0x23 (linear_address=0x23)
24a 0/9/37: 44     tcy 2
24b 0/9/2f: 27     tam

After setting R11 high at 0/9/07, the ROM calls a subroutine at 0/b/1c. How does this work, and why that address? The chapter buffer register, CB, has not be changed via the TPC (or COMC) instruction, thus we stay in chapter 0; however, we have loaded 11, or 0xB in to the page buffer register, PB, by means of the LDP instruction. Finally, we have a CALL instruction, which provides the last segment of the target address: 0x1C.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
2e0 0/b/1c: 2c     ldx 1
2e1 0/b/38: 4a     tcy 5
2e2 0/b/31: 13     ldp 12
2e3 0/b/23: e1     call 0x21 (linear_address=0x1a)
2e4 0/b/06: 40     tcy 0
2e5 0/b/0d: 7f     cla
2e6 0/b/1b: 02     ynea
2e7 0/b/36: 42     tcy 4
2e8 0/b/2d: 21     tma
2e9 0/b/1a: 0a     tdo
2ea 0/b/34: 0f     retn

This subroutine sets the X register to 1 (using LDX) and the Y register to 5. One of the main uses of the X and Y registers is to provide the index into the internal RAM in the TMS1000 series chips. The ROM then calls another subroutine, at 0/c/21. Unlike the TMS1100, the TMS1600 has a three level stack. Down another level we go.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
31a 0/c/21: 21     tma
31b 0/c/02: 4f     tcy 15
31c 0/c/05: 27     tam
31d 0/c/0b: 46     tcy 6
31e 0/c/17: 0c     rstr
31f 0/c/2e: 04     dyn
320 0/c/1c: 97     br 0x17 (linear_address=0x1e)
321 0/c/38: 4f     tcy 15
322 0/c/31: 38     tbit1 0
323 0/c/23: a9     br 0x29 (linear_address=0x2b)
324 0/c/06: 3a     tbit1 1
325 0/c/0d: 91     br 0x11 (linear_address=0x2f)
326 0/c/1b: 39     tbit1 2
327 0/c/36: 93     br 0x13 (linear_address=0x33)
328 0/c/2d: 3b     tbit1 3
329 0/c/1a: b2     br 0x32 (linear_address=0x37)
32a 0/c/34: 0f     retn
32b 0/c/29: 40     tcy 0
32c 0/c/12: 0d     setr
32d 0/c/24: 4f     tcy 15
32e 0/c/08: 86     br 0x06 (linear_address=0x24)
32f 0/c/11: 48     tcy 1
330 0/c/22: 0d     setr
331 0/c/04: 4f     tcy 15
332 0/c/09: 9b     br 0x1b (linear_address=0x26)
333 0/c/13: 44     tcy 2
334 0/c/26: 0d     setr
335 0/c/0c: 4f     tcy 15
336 0/c/19: ad     br 0x2d (linear_address=0x28)
337 0/c/32: 4c     tcy 3
338 0/c/25: 0d     setr
339 0/c/0a: b4     br 0x34 (linear_address=0x2a)
33a 0/c/15: 0f     retn

This is a thorny bit of code. First, it loads the internal RAM at M(X,Y) into the A register, or accumulator, (TMA). At this point recall that X = 1 and Y = 5; thus A = M(1,5). Next it sets Y = 15 and deposits the A register in its new home using TAM; thus M(1,15) = A, where A = M(1,5), whatever that happens to be.

At 0/c/0b the ROM then sets the Y register to 6, then turns off the R6 output using the RSTR instruction. The code proceeds to decrement the Y register with the DYN instruction. If Y is greater than 0, before the decrement, then the status flag, S, is set to 1 otherwise it is set to 0. The result of this is that the following branch instruction, BR 0x17, causes the RSTR and DYN instructions to turn off all R outputs from R6 down to R0, inclusive.

Consulting the schematic, we can see R6 drives the control lines on a 4016 Quad Bilateral Switch. This IC allows Microtronic to switch between reading external inputs and the keyboard on the K port. It's not clear why the developers felt they needed to turn these inputs off. After all, we are currently reading from the L inputs; K/L is high. Potentially a sequencing optimisation that saves a few bytes elsewhere, but maybe an example of an off-by-one error. The remainder of the loop turns off all of the R outputs that provide addressing to the external SRAM R5 to R0. These are connected to A9 to A4 on the SRAM, respectively. The ROM is preparing to set an address!

When the loop has completed, and the branch at 0/c/1c is not taken, we move forward to 0/c/38. Here, the ROM restores Y = 15, preparing to access the internal RAM. The following series of instructions from 0/c/31 use the TBIT1 instruction to test whether the specified bit at the current memory location, M(1,15) is set to 1. If it is, the S bit is set and the following BR is executed. Each of these branch targets sets the Y register to the matching bit position, uses the SETR instruction to turn on the matching output, restores Y = 15 (required to access the correct memory location), and branches back to continue testing the next bit in the sequence. Having set all the the R outputs R0 to R3, the code finally calls the RETN instruction to to return from this subroutine.

There is a stray RETN at 0/c/15. At first this looks like a clever optimisation. Saving a jump by allowing the subroutine to return from two separate locations. However, on closer inspection we never reach that instruction. Rather than immediately return, instead the code at 0/c/0a branches back to 0/c/34 to actually execute the RETN instruction. A quick search and review of all instances of BR 0x15 in the ROM indicates that this code is never used. Even if it were it would, in almost all possible scenarios, only add more of the same inefficiency.

To summarise, the code so far has copied M(1,5) to M(1,15). It then used the bit pattern in this location to set R0 to R3. In turn, these are connected to address lines A4 to A7 on the external SRAM. Essentially, the ROM has used the value in M(1,5) to set the high bits of the SRAM address.

Our call to 0/c/21 complete, we return whence we came: 0/b/06.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
2e4 0/b/06: 40     tcy 0
2e5 0/b/0d: 7f     cla
2e6 0/b/1b: 02     ynea
2e7 0/b/36: 42     tcy 4
2e8 0/b/2d: 21     tma
2e9 0/b/1a: 0a     tdo
2ea 0/b/34: 0f     retn

Now, the ROM proceeds to configure the status latch, SL. The value of SL is used by the OPLA in determining the value presented on the O port. First the ROM sets Y = 0, it then it sets A = 0 by using the clear accumulator instruction (CLA), and finally it checks whether Y is not equal to A with YNEA - which of course it is. This has the effect of setting SL = 0.

Next the ROM sets Y = 4, moving our internal RAM pointer to M(1,4). It reads the memory location into A, using TMA. Finally, it outputs that value on the O port using TDO. As discussed in Understanding the hardware, the TDO command combines the value of the SL register with the value written to the O register and maps this to the O outputs using the output PLA, or OPLA. Short of decapping the chip, unequivocally confirming the the mapping is a challenge. Not impossible, but certainly a challenge. However, as we know we are in the middle of setting the address lines on the external SRAM, and O0 to O3 are connected to A0 to A3, it stands to reason that we will be setting a bit pattern representing the binary value stored in M(1,4).

Fortunately, even without a Microtronic to hand, we can verify the significant part of the mapping. Michael Wessel's prize winning PicoRAM 2090 provides a plug-in replacement for the 2114 SRAM on a Busch Microtronic. By checking how the PicoRAM maps the signals it receives to the Pico's internal buffer, we can check for any unexpected mappings. When loading in example program listings from the PicoRAM firmware, the PicoRAM reads bytes into the buffer sequentially. When responding to an address on the address lines, the PicoRAM does not remap the address inputs, but instead fetches those exact bytes from the buffer.

As no remapping of the address is occurring in the PicoRAM, we can conclude that a real Microtronic presents the expected binary bit patterns to the address line. 0b0000 on O3 to O0 (MSB to LSB) for 0, 0b0001 for 1, 0b0010 for 2, and so on. We still cannot confirm the values of O4 to O7 for each of the possible input values, but as these additional O pins do not connect to the SRAM we can surmise they are probably low or that the display is, at least, otherwise disabled. After all, the Microtronic display does not show any visible aberrations during SRAM access.

At this point in proceedings, the ROM has set A0 to A3 using the value in M(1,4) and A4 to A7 using the value in M(1,5). We can conclude that these internal RAM locations are used to store the external SRAM address.

Address 0/b/34 contains a RETN, so, again, we return to the caller: 0/9/3f.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
246 0/9/3f: 17     ldp 14
247 0/9/3e: d9     call 0x19 (linear_address=0x36)
248 0/9/3d: 08     tka
249 0/9/3b: e3     call 0x23 (linear_address=0x23)
24a 0/9/37: 44     tcy 2
24b 0/9/2f: 27     tam

The ROM proceeds to prepare for and call 0/e/19. This subroutine is relatively simple, but rather mysterious. For brevity, it is not included here. However, it simply keeps the CPU busy by clearing the A register and looping over an IAC instruction, incrementing the accumulator and using a pair of BR instructions, dependent on the S flag set by the overflow of A, to loop 16 times. It does this twice in 0/e/xx before moving to 0/2/0c, where it loops around once again, and from where it finally returns from this subroutine.

This subroutine idles the CPU for over 150 instructions, each of which take 6 clock cycles. It could be surmised that these wait states were added to allow the external SRAM time to respond to the address change and to present the appropriate data to the L inputs. However, at a nominal 500 kHz, each TMS1600 instruction takes 12,000 nanoseconds. These 150-odd instructions take 1,800,000 nanoseconds, or 1.8 milliseconds. While that might not seem long, the slowest 2114 SRAM offered by TI, in 1979/1980, offered a ta(A) - access time from address - of 450ns and would ready data on the I/O pins in less than a quarter of a single Microtronic clock cycle. Odd indeed.

Back at 0/9/3d transfers the data from the input bus using TKA. TKA nominally stands for transfer K-inputs to accumulator, however, as the ROM is currently holding K/L high the instruction reads from the L inputs. Finally, the A register holds the first 4 bits of data from the SRAM!

Before storing the data in the internal memory, using the TAM instruction, the ROM calls yet another subroutine: 0/9/23.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
263 0/9/23: 4f     tcy 15
264 0/9/06: 27     tam
265 0/9/0d: 38     tbit1 0
266 0/9/1b: 93     br 0x13 (linear_address=0x33)
267 0/9/36: 30     sbit 0
268 0/9/2d: 3a     tbit1 1
269 0/9/1a: 8c     br 0x0c (linear_address=0x35)
26a 0/9/34: 32     sbit 1
26b 0/9/29: 39     tbit1 2
26c 0/9/12: b2     br 0x32 (linear_address=0x37)
26d 0/9/24: 31     sbit 2
26e 0/9/08: 3b     tbit1 3
26f 0/9/11: 8a     br 0x0a (linear_address=0x39)
270 0/9/22: 33     sbit 3
271 0/9/04: 21     tma
272 0/9/09: 0f     retn
273 0/9/13: 34     rbit 0
274 0/9/26: ad     br 0x2d (linear_address=0x28)
275 0/9/0c: 36     rbit 1
276 0/9/19: a9     br 0x29 (linear_address=0x2b)
277 0/9/32: 35     rbit 2
278 0/9/25: 88     br 0x08 (linear_address=0x2e)
279 0/9/0a: 37     rbit 3
27a 0/9/15: 84     br 0x04 (linear_address=0x31)

Another substantial subroutine. So, what does it do? First it loads Y = 15, updating our memory pointer to M(1,15), and then stores the contents A register in that location: M(1,15) = A. The ROM then proceeds to do a series of tests on each bit, similar to what we saw before as it used the bit pattern from part of the address to set the R outputs. Just like before, when TBIT1 sets the S status flag to 1, the code branches to the respective segment of the code; if not the branch that follows is skipped.

For example, if the bit in position 0 was 1 then the code branches to 0/9/13. This instruction resets, or clears, the bit using RBIT. If the bit in position 0 had already been 0, the S flag is set to 0, the branch is skipped, and instead the code sets the bit with SBIT. If flips the bit! The code proceeds to step through all four bit positions, flipping each in turn. Finally, it copies the result from the internal memory, back into the A register and returns.

To summarise, this subroutine temporarily stores the accumulator, A, in M(X,15); inverts each individual bit; copies the result into A; and, finally, returns. A 4-bit complement operation. But why?

While some external memories offer inverted output, such as the 74S289, the 2114 SRAM is not one of them. Instead we have to blame the 4502 Strobed Hex Inverter/Buffer. The Microtronic uses R7 to R10 to indicate the data that should be written to the RAM. However, these pins are also used to provide the user-controlled outputs. As the data pins on the 2114 SRAM are bidirectional, these pins must also be connected to the L inputs. Without some mechanism to block these signals the Microtronic would have to disable the user-outputs every time it wanted to access the RAM. Such constant switching, as would be needed while the user-program was running, would constantly interrupt the user-specified output signals. The 4502 provides a mechanism to prevent the user-specified outputs interfering with the L inputs, without turning those output signals off.

What is special about the 4502 is that it offers three-state outputs (also called tri-state or 3-state outputs). That is to say, when DISABLE, on pin 4, is pulled high the chip presents a high-impedance state on its outputs preventing the presence of the device causing interference with the other signals on the so-called L bus. As indicated in the schematic, by default, this pin is held high by a pull-up resistor. When the Microtronic wishes to write to the SRAM it simply pulls R13 high, which pulls the W pin on the 2114 SRAM low via a transistor, indicating it should write the incoming data to the indicated address on the A0 to A9 inputs. The same signal, via the transistor, pulls the DISABLE pin low on the 4502, enabling the chip. This allows the signals from R7 to R10 to pass from the input pins, through the device, and on to the L bus. Crucially, when the 4502 passes the data through, it inverts the signals. This results in the complement of the intended data being stored in the SRAM.

Briefly, it's worth noting that there is a small error, here, in the the schematic. The R13 transistor is backwards. Not on an actual Microtronic, as Michael Wessel has verified, but in the diagram. On a real device, all of the emitters in that group of transistors are connected together and it is the collector that pulls both pin 4 of the 4502 and the W line of the 2144 SRAM low.

While the 4052 does the job, inverting the data makes a lot of work for the TMS1600 to recover the intended value from the inverted bits. As an alternative, non-inverting buffers, such as the 4053, could have been used. Still, assuming that the hardware had been set, and no other chip could be used, this approach is painfully inefficient. If the ROM is recovering 0xF it takes 16 instructions to set all of the bits. Worse, if the ROM has has to reset every bit, to recover 0x0, it would take 20 instructions.

Instead of implementing the complement bit-by-bit, the TMS1600 offers the CPAIZ instruction. CPAIZ is complement accumulator and increment (two's complement accumulator). While this instruction is not exactly what is needed, after all it adds one to the value, this is easily resolved. The handy DAN operation subtracts one from the accumulator, turning the two's complement back into a regular complement. Curiously, as seen in the documentation, the DAN instruction is equivalent to A + 14 + 1 → A. As it happens, the TMS1600 instruction set does not really support subtraction; instead it handles this as an addition and overflows the maximum value.

Ignoring the copying of the A register in to M(1,15) - as it's only used as a temporary location to allow the bit manipulation to occur - this entire subroutine could be replaced by two instructions. Or, if the authors really need the complement to remain a separate subroutine, three. e.g.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
    0/9/23: 3d     cpaiz
    0/9/06: 77     dan
    0/9/0d: 0f     retn

Nevertheless, once the data recovery phase is complete, the ROM returns from the subroutine call and the code resumes at 0/9/37.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
24a 0/9/37: 44     tcy 2
24b 0/9/2f: 27     tam

Here we see the final two actions needed to complete this SRAM read. These ROM code sets Y = 2, and thus our memory pointer to M(1,2). Finally it copies the freshly inverted A register, representing the word read in from the SRAM, into this space in the TMS1600's internal memory.

The ROM goes on to read two more words before returning back to the caller. First it enables R5, before doing another SRAM read and placing the result in M(1,1). It then proceeds to switch R5 off again, switch on R4, and read another word from the SRAM before storing it in M(1,0). Finally, it turns off R11 disabling the L inputs, and indicating the end of the RAM access, before executing the RETN instruction.

Why does the ROM do two further reads? Again, we refer to the schematic. Here we see that R4 and R5 are connected to A8 and A9 on the SRAM respectively. These signals are combined with the base address to provide access to each 4-bit chunk of the Microtronic's user-facing 12-bit instruction set. The schematic notes that when A8 and A9 are 00 (both low) this is mapped to the befehl: the command. When just A9 is high, this is mapped to the MSB: the most significant bits of the operand. Finally, when just A8 is high, this is mapped to the LSB: the least significant bits of the operand.

Interestingly, the Microtronic never uses the final combination where both A8 and A9 are high. A quarter of the SRAM on the Microtronic is simply never used.

The Microtronic memory map

Our exploration of the memory access routine has revealed some useful information about the way the Microtronic handles memory, both external and internal.

We know that the pointer to the base address of the current instruction in SRAM is at M(1,5) and M(1,4). The ROM then uses this to address the SRAM and read thee 4-bit words, representing the Microtronic's 12-bit instruction word, into M(1,2), M(1,1) and M(1,0).

Location Content
M(1,5) pointer to SRAM address (high bits)
M(1,4) pointer to SRAM address (low bits)
M(1,2) instruction buffer (command)
M(1,1) instruction buffer (most significant bits)
M(1,0) instruction buffer (least significant bits)

Finding nim

The Microtronic comes with a few built in programs, but one of the more interesting is the nim game. Nim is detailed on page 7 of the Microtronic manual. The purpose of the game is remove match sticks from a pile, but not to be the person who takes the last one. Mathematically the game is interesting in itself, and the Microtronic exploits the mathematical theory to, very frequently, win the game. However, for the purposes of exploring of the ROM, that's not what is makes the program notable. While the game naturally exists in the ROM, when recalled it is copied into the SRAM and executed as would be any other user-defined Microtronic program.

The full code for NIM, 69 12-bit words, is as follows:

F08 FE0 F41 FF2 FF1 FF4 045 046
516 FF4 854 D19 904 E19 B3F F03
0D1 0E2 911 E15 C1A 902 D1A 1F0
FE0 F00 F02 064 10C 714 B3F 11A
10B C24 46A FBB 8AD E27 C29 8BE
E2F 51C E2C C22 914 E2F C1C F03
0D1 0E2 F41 902 D09 911 E38 C09
1E2 1E3 1F5 FE5 105 FE5 C3A 01D
02E F04 64D FCE F07

To recall the program from ROM, the user presses HALT, PGM, 7. At which point the Microtronic begins copying the code into the SRAM.

To write to the SRAM the Microtronic must drive R13 high. Just as before we can search for this with a simple regular expression.

tcy 13.*\n.*setr

Again, we find just one candidate block of code.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
314 0/c/35: 4b     tcy 13
315 0/c/2b: 0d     setr
316 0/c/16: 4b     tcy 13
317 0/c/2c: 4b     tcy 13
318 0/c/18: 0c     rstr
319 0/c/30: 0f     retn

Here, we see the R13 line being briefly driven high, before being set low again. This writes the current data on the data pins to the address specified by the address pins. After this, the subroutine returns. There is an obvious unnecessary duplication of the TCY command at 0/c/2c. Equally, unless this stub of code is being reused elsewhere, it might have been possible to remove the TCY at 0/c/16 as well; after all, Y is already set correctly. Like with the strange delays in the read subroutine, it is possible that these seemingly nonsensical operations are simply for delay; but again, even a single clock pulse should provide ample time for the write to complete. I am wont to wonder if these two instructions, at one point, read LDP 14, CALL 0x19. Like in the read routine, adding a substantial and unnecessary pause to proceedings.

Unlike with the read subroutine, where the interesting code followed our sentinel snippet, when writing to the SRAM all the configuration must be done first. As such, we must work our way back through the ROM code. The three lines immediately preceding the write request look like so.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
311 0/c/0e: 21     tma
312 0/c/1d: 17     ldp 14
313 0/c/3a: c3     call 0x03 (linear_address=0x02)

Working backwards, the call at 0/c/3a is very unlikely to be branched into from elsewhere. Code that wishes to CALL 0x03 can do so directly, so this instruction is not interesting it its own right. Likewise for the LDP at 0/c/1d. While it could, potentially, save a byte to trampoline from here to 0/e/03, it doesn't make much sense to do so. Just to be sure, though, we can quickly check the code with a cryptic regex.

(ldp 12(.*\n){1,2}|0/c/).*(call|br) 0x(3a|1d)

For the uninitiated, we are asking the matcher to find either an LDP instruction which is setting the page buffer to C, or for the code to already be in chapter 0 and page C. Where the page buffer is set, we allow for any extra cruft on that line, including the new line, and up to one further intermediate line (as described below). Finally, the matcher is then told to ignore any remaining junk on the line and look for a CALL or BR to either 0x3A or 0x1D. No results.

It's worth noting that this is not a completely exhaustive search. We could allow for more intermediate lines to account for more complicated sequences, e.g. interleaving configuration and test to prepare for a conditional jump. But as we allow more potentially unrelated content to match, even if we increase the complexity of the pattern to try and minimise issues, we risk getting larger and larger numbers of false positives. For our purposes, this pattern is good enough.

Taking a peek at what that subroutine at 0/e/03 does, we find that it takes the contents of the accumulator, A, and sets outputs R7, R8, R9, and R10 using a similar technique we saw earlier to set the relevant R outputs used for the SRAM address. As we can see on the schematic, R7 to R10 are connected to the user outputs and, maybe more crucially, to the 4502 which can selectively gate them into the SRAM when it is time to write the data.

The next line, upwards, 0/c/0e looks promising. This line reads the current memory address into the A register. Knowing that the next line will call a subroutine that will present that value on the outputs, 0/c/0e is certainly a candidate target address. Another (almost identical) regex to the rescue.

(ldp 12(.*\n){1,2}|0/c/).*(call|br) 0x0e

This time we find five candidate callers. Two of these callers are just a few lines above, likely reusing this block of code to output the other two of the three 4-bit words that make up the Microtronic's 12-bit instructions. Curiously, however, there are three more callers coming from in from 3/f/xx; the first at 3/f/06.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
fe0 3/f/1c: 44     tcy 2
fe1 3/f/38: 10     ldp 0
fe2 3/f/31: 0b     comc
fe3 3/f/23: 13     ldp 12
fe4 3/f/06: ce     call 0x0e (linear_address=0x11)

Going backwards through the code we see that the page buffer is being set to C, using LDP, and the chapter buffer to 0 by means of the TPC instruction (mentally translated from the errant COMC). The instruction before this, at 3/f/1c, changes Y = 2. This is interesting. As we've identified, the subroutine starting at 0/c/0e loads the memory location specified by X and Y into the A register and processes it for presentation to the SRAM. It is, in essence, a data output subroutine. So, what data is being stored in memory location Y?

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
fd5 3/f/2b: 40     tcy 0
fd6 3/f/16: 61     tcmiy 8
fd7 3/f/2c: 60     tcmiy 0
fd8 3/f/18: 6f     tcmiy 15
fd9 3/f/30: 60     tcmiy 0
fda 3/f/21: 60     tcmiy 0
fdb 3/f/02: 60     tcmiy 0
fdc 3/f/05: 10     ldp 0
fdd 3/f/0b: 0b     comc
fde 3/f/17: 1d     ldp 11
fdf 3/f/2e: dc     call 0x1c (linear_address=0x20)

Scrolling up we look to see what is setting the memory at Y = 2. The call to 0/b/1c, set up by 3/f/05 onwards, looks familiar. That subroutine is used to set the SRAM address. We considered it in detail when investigating SRAM reads. Crucial, but it doesn't set the contents of the internal memory at M(x,2).

Stepping back further, we see a series of TCMIY instructions. These instructions store a value at the current internal memory location then increment the Y pointer. At 3/f/2b we are shown the base address for these insertions into the internal RAM, 0.

A summary of the result of these writes, to the internal RAM, is as follows.

X Y Value
? 0 8
? 1 0
? 2 F
? 3 0
? 4 0
? 5 0

Recall, the first value of interest is at Y = 2. When we consider the other candidate calls to our so-called data output subroutine at 0/c/0e we see that they set Y = 1 before the next call and Y = 0 before the final call. Thus they output, in order, F08 to the SRAM. Sound familiar? F08 is the first 12-bit word in our nim game.

We still don't know what X is. Though we might have an inkling, it would be nice to check. This requires working backwards. Immediately preceding the code that writes to internal RAM is an unconditional branch. So, what branches or calls into 3/f/2b?

(ldp 15(.*\n){1,2}|3/f/).*(call|br) 0x2b

A single candidate. One that sets X = 1. The question marks in the previous table would be 1.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
e3d 3/8/28: 2c     ldx 1
e3e 3/8/10: 1f     ldp 15
e3f 3/8/20: ab     br 0x2b (linear_address=0x15)

This aligns nicely with our existing understanding of the Microtronic memory map. M(1,2), M(1,1), M(1,0) hold the current instruction and M(1,5), M(1,4) tell us where it can (or should) be found SRAM.

Having, very probably, found the first word of the nim game can we find the rest? To do so we must look forward. After our final data output subroutine call, the CPU resumes at 3/f/0c.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
ff5 3/f/0c: 40     tcy 0
ff6 3/f/19: 0f     retn
ff7 3/f/32: 15     ldp 10
ff8 3/f/25: b7     br 0x37 (linear_address=0x0a)

First the code sets Y = 0 and then we find a RETN. The question is, return to where? This can be a problem of dipping into the code as we have done. We have no idea of what the call stack might look like at this point. However, this is where a little luck and intuition might just work out for us. As it happens, when the hardware call stack is empty an errant return is essentially ignored. In essence, if our call stack is empty then this instructions is a no-op. One interesting application of this feature is that it can be used to allow the same segment of code to be used in two different ways. One way when the call stack is empty and another when there is somewhere to actually return to.

Interestingly what follows this RETN is an unconditional branch to 3/a/37. This could be a form of trampoline, allowing another jump in this same page to redirect to the new location. It could, also, be used from another page, but there would be no real benefit to doing so. Nevertheless, both scenarios are easily checked.

(ldp 15(.*\n){1,2}|3/f/).*(call|br) 0x32

No results. So, maybe we do have a tricksy bit of code reuse on our hands. Let's assume so. In which case, let's look at the target location of this branch to 3/a/37

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
e8a 3/a/37: 60     tcmiy 0
e8b 3/a/2f: 67     tcmiy 14
e8c 3/a/1e: 6f     tcmiy 15
e8d 3/a/3c: c0     call 0x00 (linear_address=0x00)
e8e 3/a/39: 68     tcmiy 1
e8f 3/a/33: 62     tcmiy 4
e90 3/a/27: 6f     tcmiy 15
e91 3/a/0e: c0     call 0x00 (linear_address=0x00)
e92 3/a/1d: 64     tcmiy 2
e93 3/a/3a: 6f     tcmiy 15
e94 3/a/35: 6f     tcmiy 15
e95 3/a/2b: c0     call 0x00 (linear_address=0x00)
...

Having previously set Y = 0 the code now begins populating M(1,0), M(1,1), and M(1,2) with data. This time FE0, the second byte of nim! Right where we expect it to be stashed in the internal RAM, ready for output. At 3/a/39 we find the code to populate this cache with the third nim byte, F41; at 3/a/1d the fourth, FF2; and so on.

But what about the numerous CALL 0x00 instructions? Without an explicit chapter and page, these call the code at 3/a/00. For brevity, this block of code is not reproduced here. However, to summarise, the code updates the SRAM pointer, in M(1,4) and M(1,5), before branching back to the code we saw earlier, at 3/f/05, that sets the SRAM address and writes out the three 4-bit values. This time, as the CALL to 0x00 has populated the stack, our seemingly superfluous RETN at 3/f/19 takes effect. This then drops the CPU back at the correct place in the sequence ready to transfer the next word of instructions from the ROM, into RAM, and eventually into the SRAM. I leave the detailed review of this sequence as an exercise for the reader.

By finding nim, it seems we have a basic understanding of how the Microtronic reads and writes the SRAM. Maybe more importantly, we have confirmed that the logic is sound and thus we probably have a sensible dump of the ROM.

While all this analysis is fun, it's probably time to see what else can be done with the Microtronic ROM.

The breadboard Microtronic

Beware of bugs in the above code; I have only proved it correct, not tried it. -- Donald Knuth

Not having actually executed the code so far, we can only be so confident about the validity of our ROM. The acid test, as it were, would be to run the ROM on real Microtronic hardware and see what it does.

Unfortunately, as the TMS1000 series have a mask programmed ROM, we can't practically execute the code on a real TMS1600. ROM-less evaluation chips were made available; however, they are exceptionally rare. While the TMS1099 (a ROM-less TMS1000) has a WikiChip page, and the TMS1098 (a ROM-less TMS1100) even has a YouTube demonstration, we would need a TMS1097 to support the 4,096 byte ROM as used on the Microtronic. Outside of official Texas Instruments documentation, specifically the datasheet and price list, references to the TMS1097 in the wild seem practically non-existent.

We could, of course, simulate the TMS1600 entirely in software, but the Microtronic is more than just its software. The TMS1600 is the conductor of a cacophony of hardware. It controls, or reacts to, a variety of other physical devices: the SRAM, display, keyboard, and more.

Much of the hardware that makes up the Microtronic is still in production to this day. In addition to the basic parts-bin passives, resistors and capacitors in various values, all of the following can still be purchased new and in quantity.

More troubling, obsolete or out of production, parts include the following.

While considered obsolete, 2114 SRAMs are readily available; even in quantity. There are companies, such as Rochester Electronics, that specialise in this kind of medium-volume vintage parts. Alternatively, newer, larger, SRAMs could also easily be pressed into service by simply tying unused inputs up or down, as needed, and using only part of their capacity; after all, this is what Busch did with original SRAM in the Microtronic anyway.

The TIL393 display is now sadly out of production. Until recently, a few suppliers seemed to have new old stock available but these displays are increasingly hard to find. Still, it is relatively easy to recreate such a display with modern 7-segment LEDs. Individual units can be used or, to minimise parts, two 3-digit displays could easily be paralleled. Michael Wessel has recently demonstrated such a 7-segment based display attached to a real Microtronic.

While reputable suppliers are available - Mouser was linked here only as DigiKey had no in stock BC237 transistors at the time of writing - I, in fact, purchased all the active components via AliExpress. Total cost of approximately £8, delivered; though I'm ignoring all the other tat I added to the orders that I probably didn't need to buy. For the obsolete 2114 SRAM and a knock-off, but vintage, TIL393 display, I turned to eBay. About another £6. Your milage may vary. I also spent a few pounds on various extra resistors, capacitors, etc. I am also ignoring the cost of wires, breadboards, and LEDs that I already had. All told, you could probably argue assembling my breadboard Microtronic cost me somewhere between £20 and £50 in parts.

The most crucial part, however, is the TMS1600 compatible microcontroller. While it might not be possible to source a TMS1097, today there are an array of modern microcontrollers available that can easily be programmed to carry out a wide variety of tasks. One such device is the humble ATmega328P, the processor at the core of the popular Arduino Uno R3. I could have, and maybe should have, chosen another platform. However, these chips are cheap, easy to program, readily available, and I happened to have a bunch to hand.

Breadboard Microtronic 2090 - Self Test
The breadboard Microtronic 2090 passing the built-in self test

Should you, too, like to build your own Microtronic, a few comments on the schematic.

  1. As previously noted, the transistor connected to R13 is backwards. All of the emitters in that group of transistors should connected together. The collector should connect to both pin 4 of the 4502 and the W line of the 2144 SRAM.
  2. The 4060 in the lower right is incorrectly labelled as a 4016. This use of a 4060 can clearly be seen on the front cover of the manual and in other photographs of the PCB.
  3. Pin 12 on the 4060 must be pulled low (to GND). If left floating, as implied by the schematic, the counter will randomly reset.
  4. It is pin 3 on the 4060, Q14, that is connected to pin 3 on the 4013. This label is missing on the schematic.
  5. Power and GND are not specified for any of the ICs (except the TMS1600). All of the 5 V chips should have their source pins connected to VSS, +5 V, and and GND set to GND. Note the helpful annotations converting between the two voltage potentials indicated on the pins 9, 10, and 11 of the INTERFACE socket.
  6. The pin numbers for R0 to R5 are incorrectly labeled. R0 is pin 40 on the TMS1600, and R1 to R5 are on pins 1 to 5 respectively as shown in the data book. Connections are made with respect to their designators, not their pins; e.g. A9 on the SRAM (pin 15) should be connected to R5 on the TMS1600 (pin 5).
  7. R0 to R5 multiplex the TIL393 from right to left (i.e. R0 is the right-most digit).

  8. To reduce the number of components, it may be possible to replace the block of transistors, above the TIL393-6 display, with the common ULN2003. In fact a prototype Microtronic clearly uses a ULN2004 (a minor variant) for this function, which likely explains why these transistors are shown as a block rather than individual components.

As everything other than the TMS1600 runs at 5 V, including the ATmega328P, I simply run the whole system off of the same 5 V rails. To create a true drop-in substitute CPU you would need to add an extra on-board power supply to adjust the input voltage and include additional logic shifting to faithfully recreate the inputs and outputs of the Microtronic's TMS1600. Much of the Microtronic's circuit complexity is dealing with the mismatched logic between the TMS1600 and the other ICs. For a prototype breadboard recreation, powering everything from a -9 V supply only to convert it back again seemed unnecessary and misguided.

Hardware done, but we'll still need an emulator capable of executing the ROM.

Emulating a TMS1000

To properly test the Microtronic ROM, we need an emulator for the TMS1000 series. Surprisingly, there are a few different ones available. One of the more unusual is a TMS1000 emulator in Minecraft. This zany creation was made back in 2011, by WarlockD, and demonstrated in their YouTube video.

A more practical emulator is mvem, an emulator from early 2014 by Paul Robson for the Microvision. The Microvision was the first hand-held games console to use interchangeable cartridges. The CPU was part of the cartridge and could be either a Texas Instruments TMS1100 or Intel 8021 depending on the game. mvem can run on Windows and does appear to have been ported to the Arduino. Sean Riddle then ported the mvem code to a PIC18F46K22 clocked at 64 MHz, eventually managing to achieve timing accurate emulation. While MHz aren't everything, the ATmega328P tops out at less than a third of that, which doesn't bode well for performance.

MAME, an emulator for classic arcade systems, emulates innumerable games and devices. It has supported parts of the TMS1000 series since at least 2009, with significant updates around 2016. However, MAME is targeted at much more powerful host devices and the TMS1000 support is part of the larger MAME infrastructure which might not lend itself to easy isolation. Like MAME, hotkeysoft has developed a collection of emulators, including one for the TMS1000 series. The hotkeysoft emulators are designed to be portable, and were ported to the ATmega328P in late 2019. However, the hotkeysoft emulators are reasonably tightly coupled to their target ROMs and the ports, at least, don't provide precision timing.

These emulators all have their charm, and there is nothing fundamentally wrong with their approaches. They all suit their need and niche. My planned use case was, however, slightly different. To provide something I could drop into the existing Microtronic schematic, I'd need it not just to run the ROM but also to act externally like a TMS1000. As such, the emulator would need to emulate a naked TMS1000 series chip, rather than tie it to a specific application. This focus on being a drop-in-replacement has the benefit that it could, in theory, emulate a variety of TMS1000 series chips, in a variety of real-world applications, just by swapping out the ROM. Many of the emulators identified do indeed emulate a variety of TMS1000 series chips, and it is certainly a potentially useful feature. In fact, the first ROM I wanted to run was not actually that of the Microtronic. Rather, I intended to run the well-tested ROM from the Science Fair Microcomputer Trainer.

The TMS1100

The Science Fair Microcomputer Trainer is a pretty basic device. Far simpler than the Microtronic. A chunk of the schematic for the Science Fair is literally printed on its cardboard frame. As the final wiring must be completed by the end user, the remainder is in the manual. With the exception of the oscillator circuit and the one-transistor amplifier for the speaker, there is little more to it than a few morse-style key switches and a handful of LEDs with current limiting resistors.

The Science Fair uses an 400 kHz ceramic resonator to clock the CPU. While ceramic resonators represented a cost saving over more accurate crystals, the speed of a TMS1000 series chip can actually be configured using a simple RC circuit. So why does the cost-reduced Science Fair use a resonator at all? Sound. The Science Fair generates audio tones using nothing more than carefully timed loops to pulse the speaker. For simple beeps, bleeps, and bloops that might not matter. Musical tones require pretty accurate timing or the tune will sound a little off key.

Additionally, while the TMS1600 and TMS1100 are very closely related, the TMS1100 is the simpler of the two. Offering less I/O, a smaller ROM, and a simpler call stack. The instruction set is also comprehensively described in the programmers manual.

While, as we now know, emulators exist for the TMS1100, my application presupposed two main features: to emulate the more-or-less naked chip, and to provide instruction-accurate timing. For an ATmega328P, together, these two requirements present quite the challenge.

The first is challenge is the I/O. The TMS1100 has 4 inputs and 19 outputs, requiring a total of 23 pins. Fortunately the ATmega328P has precisely 23 I/O pins. Unfortunately one of those is actually the reset pin. While we don't really need the reset pin, once configured as an I/O pin the device cannot be reprogrammed without the use of a high-voltage programmer; fixable but a faff that would slow development and limit the appeal.

To avoid having to use the reset pin as an input, I use a nasty hack which abuses the AREF pin as the input for K8. When checking for K8 the key reading routine actually switches either ADC0 or ADC1 to input mode and assumes that pin is low. If AREF is high, due to a key being pressed, the ADC will read low; thus K8 is 1. If AREF is low, helped by a pull-down resistor, then the ADC will be at the same potential as AREF and the ADC will read high; indicating that K8 is 0. As indicated, this does require that, during the test, one of those ADCs is not itself being used as an output. To ensure this will be true, these pins are used as R2 and R3 respectively (labelled RE and RF on the Science Fair itself). The Science Fair ROM uses these lines to strobe across the keyboard matrix, one column at a time. Therefore we know that at no time will both of these lines be in use and held high simultaneously. Using the ADC does introduce a small delay in the routine, but by carefully structuring the code we can minimises the additional overhead of this technique.

The next challenge is timing. Again, while we can eke out just enough I/O from the chip, we can only do so if we give up the use of an external oscillator. Due to the popularity of the Arduino Uno, the ATmega328P is typically run at 16 MHz using an external crystal. Unfortunately, this requires sacrificing two I/O pins. The ATmega328P can run using its internal oscillator, but it is limited to just 8 MHz. A far cry from the 20 MHz top speed we might have otherwise taken advantage of.

Despite these challenges, I successfully developed a fairly compact, yet configurable, emulator in C. To achieve the required speed the core of the emulator is a threaded interpreter, which shaves precious cycles off of the execution time. Equally, many pre-computable values, such as the program counter, are converted into lookup tables using macros at compile time. This trades a small amount of space, of which we have plenty, for significant speed improvements. Similarly, macros are exploited elsewhere to provide what looks like loose binding to the hardware that is then tightly integrated by the preprocessor. This provides reasonably clean separation between the hardware and the emulator core, improving portability and giving good flexibility for changing the I/O mapping.

The resulting emulator correctly emulates a TMS1100 and will run any run any ROM which use the standard instruction set. To allow this flexibility, rather than couple the output mapping to a specific ROM, the emulator makes use of the Berkley Espresso PLA file format for the OPLA configuration. The Berkley PLA format is also used by MAME. This allows the emulator to use both the easily found ROM and matching OPLA files. Importantly, the emulator achieves the 400 kHz target frequency of the Science Fair Microcomputer Trainer and offers sufficiently accurate instruction timing to output the expected musical tones without audible distortion. This timing can be handled internally by an interrupt timer, or, if I/O is available, can be driven by an actual external oscillator circuit; guaranteeing compatibility with a specific device.

The TMS1600

The TMS1600 instruction set is essentially identical to that of the TMS1100. There are just two significant changes. The first change is required to handle the increased ROM space. As the TMS1600 has 4, rather than 2, chapters the TMS1100's COMC instruction to toggle between them is replaced with the TPC instruction on the TMS1600. The TPC command copies the two least significant bits from the current page buffer, PB, into the chapter buffer, CB. The second change is due to the enhanced three-level hardware stack on the TMS1600. This is managed by updates to the way the CALL and RETN instructions operate, now cascading the return addresses through the extra registers and latches.

Updates to effect these changes are rather trivial, and the emulator code can easily switch between TMS1100 and TMS1600 mode. There is one small issue, however. The ATmega328P has 23 I/O pins, compared to the TMS1600's combined 32. Of course, the pin limitation was actually not an insurmountable problem when I first started development; now it was much more pressing. Fortunately, it's nothing a handful of 74HC595 shift registers can't fix.

The use of shift registers to handle output does have a significant draw back, however. Serially shifting the bits out takes far longer than simply setting an I/O register. Fortunately, the use of the shift registers does allow us to free up extra pins on the ATmega328P. As such we can now avoid the nasty hack that uses the AREF pin as an input and, more importantly, free up the pins needed to use a proper external crystal to drive the microcontroller clock.

In addition to changes to the output, the headers and macros that make up the hardware abstraction layer for the emulator were also expanded to support input from the additional four L pins as well as the related SE MODE and K/L input. While the Microtronic does not use the SE MODE pin, it seemed prudent to keep the emulator as flexible as possible.

The final hurdle for the Microtronic was the OPLA. For the Science Fair the OPLA was readily available. Sean Riddle had recovered it by decapping the chip, and uploaded photos of the resulting die. As we hadn't physically cracked open the Microtronic's TMS1600, another approach was needed.

Inferring the OPLA

There are a few different possible approaches to discovering the output PLA. The first is the destructive decapping method, which we have so far managed to avoid. Microtronic's are fairly rare, and getting rarer. A second less destructive option would be to simply record what the Microtronic does.

As described in the relevant patent, it is also possible to execute the code on the chip and observe the outputs. With detailed knowledge of the ROM, it might be possible to identify the outputs for all possible input values to the decoder. By carefully executing code that configures all possible states, and reading the subsequent output pins, the mapping might be recorded.

It's worth noting that for variants supporting less than 32 terms, such as a TMS1100 which supports just 20, the resultant OPLA may appear to be larger than the permitted. Any apparent OPLA with more than the maximum number of terms would need to be minimised. This can be done using tools such as Berkley's Espresso. Neither the detected or minimised representation would likely match what was physically on the die, but they would be logically equivalent. However, without having a Microtronic to hand, neither decapping or executing the code on a real Microtronic CPU would be possible.

The final approach is to infer the patterns, not from device itself, but from the ROM and the documented behaviour. From the schematic we can tell that the Microtronic uses the O outputs to drive two things. First, it drives the individual digits on the display. Second, as we have already identified in the code, it sets the four lowest bits of the RAM address. First, let's consider the display.

To drive the display, the Microtronic combines both the O outputs, which generates the digit to display, and the R outputs which select which of the six positions it should be displayed in. Most significantly, though, the Microtronic must set R12 high to enable the display circuitry. This should be easy to spot in the ROM.

tcy 12.*\n.*setr

This presents two candidates. In both circumstances, though, we see an interesting pattern preceding the SETR pattern.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
34b 0/d/2f: 4a     tcy 5
34c 0/d/1e: 7f     cla
34d 0/d/3c: 02     ynea
34e 0/d/39: 43     tcy 12
34f 0/d/33: 0d     setr
350 0/d/27: 47     tcy 14
351 0/d/0e: 22     tmy
352 0/d/1d: 21     tma
353 0/d/3a: 0a     tdo

First the system sets Y to some arbitrary value, it the clears the A register, and does a comparison between the two with YNEA. This has the effect of setting the SL register to 1. Equally, it is probably reasonable to assume that the Microtronic displays the respective digit on the seven-segment display in response to each value. This combination is almost what is shown for the Typical Coding Example for the OPLA in the Programmer's Reference Manual. However, on the Microtronic schematic we can see that O0 is connected to segment A, O1 to segment B, and so on. Essentially, the bit pattern must be shifted down by one position in the Microtronic to account for how the display is connected.

Seven-segment display showing segment labels.
Segment names for the display outputs

There are various ways of representing numbers on a seven-segment display. In particular, the digits 1, 6, 7, and 9, have popular variants. Fortunately, the test program, PGM-0 on a Microtronic show each digit in turn. We can check we have selected the correct representations by checking what a real Microtronic does and comparing the display to the character set shown in the TMS1000 Series Programmer's Reference Manual. Fortunately, a video showing the test program exist on YouTube. While the video could be clearer, there appears to be decimal point after the b. Careful inspection of the Microtronic instruction manual shows the same discrepancy where it illustrates the expected output of the test program on page 5.

Interestingly, the prototype Microtronic, shown in an earlier brochure, uses an alternative representation of the (probable) b as 6.. This odd representation seems, at least to me, curiously reminiscent of the additional top stroke seen on a Fraktur or blackletter 𝔟. Without the decimal point, this representation is indistinguishable from a the numeral 6 on a seven-segment display. This initial representation may have necessitated an additional marker, in the form of the decimal point, which then remained in subsequent updates to the OPLA design.

Prototype Microtronic 2090
Display style used on a prototype Microtronic 2090

MAME uses OPLA files with the input map in LSB-first order. The display section of the OPLA is as follows, where 1-4 represent the bit positions of the value in A when TDO is executed, S is the SL register, and A-G and . are the segments as shown in the segment diagram. Note that the header has been separated as it is should not be included in a Berkley-format PLA file.

1248S ABCDEFG.
00001 11111100
10001 01100000
01001 11011010
11001 11110010
00101 01100110
10101 10110110
01101 10111110
11101 11100000
00011 11111110
10011 11100110
01011 11101110
11011 00111111
00111 10011100
10111 01111010
01111 10011110
11111 10001110

With half of the patterns accounted for, and the display done, let's now return to the SRAM addressing. As we previously explored, the SRAM access uses R0 to R3 to set the lower 4 bits of the address. Additionally, it sets SL to 0. Noting that the bit order is least-significant first, it seems sensible that writing 0 to the O register would result in 0000 on the outputs, 1 as 1000, 2 as 0100, and so on. As long as our pattern choice is unique, and consistent for both reads and writes, the exact mapping will not matter. However, it would be nice to use the same mapping as a real Microtronic.

As it happens, Michael Wessel's PicoRAM 2090 interfaces directly with the Microtronic via the SRAM socket. It also offers some built in programs that can be paged into the virtualised memory space. By verifying the mapping in the PicoRAM, and ensuring we are compatible, we can be sure that our mapping matches a real Microtronic. The PicoRAM function enter_program() copies the built-in programs sequentially into the virtual RAM. The main RAM access loop, likewise, outputs the requested data using the logical sequential mapping. This confirms that the Microtronic developers didn't scramble the addresses. We can map the second half of our OPLA as follows. For clarity, in this table the header for the right column indicates the O output pin.

1248S 01234567
00000 00000000
10000 10000000
01000 01000000
11000 11000000
00100 00100000
10100 10100000
01100 01100000
11100 11100000
00010 00010000
10010 10010000
01010 01010000
11010 11010000
00110 00110000
10110 10110000
01110 01110000
11110 11110000

Our sixteen values can fully exploit only half of our output pins. The Microtronic only uses O0 to O3, and it is now clear why. It is important to note that without monitoring an actual Microtronic during SRAM access we can't be sure what state pins O4 to O7 are in for each of these values. However, the only other components tied to these outputs are the display, which is turned off during SRAM access. As such their state will not affect operation and they can reasonably be set to 0.

With all the software in place, I flashed an ATmega328P with the emulator - completed with ROM and OPLA - and connected up the shift registers that extended the I/O. Finally, the rats nest of wires was ready to use with my breadboard Microtronic.

Debugging the keyboard

Having assembled our very own Microtronic, and built a substitute CPU, we can finally test the ROM with real hardware. Amazingly, after a brown-out induced false start - I had a loose wire connecting the power - my breadboard Microtronic sprung to life and greeted me with 00 000. This is the expected Microtronic initial display. A promising start, and a good indication that the mapping for the display part of our OPLA was correct.

Never having used a Microtronic, I needed some help to get started. Unlike Texas Instruments, who appear to have removed the history section from their website including all mention of the TMS1000, the Busch company proudly highlights their role in early computing history. A page detailing the Microtronic, including links to download the user manuals, is available on the Busch website. Page 5 provides instructions for the built-in test program: press HALT-PGM-0.

The test program exercises the display, synchronously showing each of the hex values 0 to F on each digit on the display before requiring the user to press each of the hexadecimal input keys. Next it verifies the I/O ports, which requires the user to have wired them as described in the document, and finally it goes on to test the memory. A complete run of the test program can be seen on YouTube. Unfortunately, no amount of mashing that key sequence appeared to do anything whatsoever.

Despite the grave warnings about pressing keys randomly and producing unsinn (nonsense), on page 4 of the manual, I proceeded to do exactly that. It appeared that the keys 0, 1, 2; 4, 5, 6; 8, 9, A; C, D, and E worked correctly but none of the other keys produced any response.

Busch Microtronic 2090 Keyboard Layout
The Microtronic keyboard layout

Consulting the keyboard layout, we can see a distinct pattern. Keys in the left three columns - attached to R0, R1, and R2 - worked fine. Keys to the right of the keyboard did not. Initially I checked for loose wires, but nothing seemed to be amiss. Cross-referencing with the schematic shows that for all of these keys to work all of the K inputs must be registering signals. The afflicted keys were all attached to R3, R4, and R5. This would point to an issue in the output driver of my emulator. Maybe the code driving the '595 shift register was faulty? This would be a good candidate, but was unlikely: the same lines are also used to multiplex the display, and the left-most two digits driven by R4 and R5 were illuminated. While the third-from-the-left was blank, as it should be, even if there was an issue we clearly had signal on the HALT and PGM column I had been trying to use. To be certain, I cracked out an oscilloscope. The signals were present, and appeared to be strobing across the various columns as would be expected. That meant the issue was likely in software.

Keyboard input would require the use of the KA instruction. A quick search over the ROM code shows there are 13 occurrences. Immediately before the first occurrence R6 is set high before doing the read.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
02b 0/0/29: 46     tcy 6
02c 0/0/12: 0d     setr
02d 0/0/24: 08     tka

In the schematic, R6 is attached to the CONTROL pins of the 4016. When R6 is high, the 4016 passes the signal from the user inputs to the K pins; thus this block must be to do with external data, not the keyboard. The next occurrence looked more promising.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
064 0/1/06: 0e     knez
065 0/1/0d: b6     br 0x36 (linear_address=0x27)
066 0/1/1b: 83     br 0x03 (linear_address=0x02)
067 0/1/36: 08     tka
068 0/1/2d: 4f     tcy 15
069 0/1/1a: 27     tam

What caught my eye here was not the TKA, but rather the KNEZ at 0/1/06. KNEZ checks to see if all the K inputs are low, and if so sets the status, S. While it could be used for other forms of data input, it is mostly useful for monitoring a keyboard for a key down condition.

If a key is detected at 0/1/06, then the branch at 0/1/0d executes and we end up reading the key. If not, then the code branches off to 0/1/03. The code that goes on to handle the keypress contains a lot of branches and loops, so it wasn't entirely clear what was going on. At 0/1/23, immediately before the KNEZ, there is a RETN.

(ldp 1(.*\n){1,2}|0/1/).*(call) 0x(1f)

Was this another example of clever code reuse, as we saw with nim? There are a couple CALL instructions to 0/1/1f, which would support this theory. But, having dipped in without a full context and with things not working, it was hard to be sure. Maybe there was a subtle bug being exposed by these branches and calls?

While my emulator seemed to work perfectly for the TMS1100, the TMS1600 is a subtly different beast. The Programmer's Reference Manual fully explores the TMS1100 in minute detail, including precisely detailing the effect of each instruction. Documentation for the TMS1600 is far rarer, and my amended implementation was, in part, based on the information in the much more concise Data Book. The one major difference, and one that had occurred to me as potentially problematic, was the three-level stack.

The TMS1100 documentation is clear that "call instructions in a subroutine are invalid" and the precise logic and side-effects are described in detail. For the TMS1600, however, while the revised call stack is similarly documented, I hadn't observed any specific information on what would happen if response to an attempted call stack overflow. It seemed likely it would simply overwrite the oldest call, but maybe there was some latent logic which would prevent this? Equally, while the notes for the TMS1600 boast that these changes mean "long branches can be executed at any time", the subtle change to the BR logic that is implied (i.e. that we can safely ignore the status of the call latches) is not as explicitly stated. I suspected I had made an error in implementing these extensions.

To try and disentangle these jumps I did what any seasoned programmer would do. I turned on the debugger, set a breakpoint, and stepped through the code. Unfortunately, my emulator did not have a debugger - yet. As the code was already running in an emulator, even if it was on a chip, adding a debugger wasn't too arduous. The ATmega328P already has a serial UART available, and once this was configured correctly it is a simple matter of listening for the next address at which to pause, and sending the current instruction and any pertinent registers down the wire. I then simply set my breakpoint for 0/1/2d, the instruction after the probable TKA to ensure the input would be stashed in memory, and pressed a key.

Nothing was immediately and obviously wrong - and the call stack didn't look like it has overflowed, yet - but single-stepping through the code meant things were now going at a glacial pace. After hammering on the enter key a few dozen times, it immediately became obvious that going through the code, line-by-line, in the debugger wasn't going to solve this as quickly as I'd hoped. Instead, I settled on a revised plan. Record the data from the debugger, both for a recognised and unrecognised key press, and do a diff to compare them. Rather than me meticulously considering every instruction and possible code path, this way the computer would simply figure out the point of divergence for me. The question was, how much data did I need, and when to stop?

Despite half the keyboard not working, the Microtronic ROM was not going haywire. The code paths obviously converged before anything went seriously awry. Notably, the display continued to operate. As the Microtronic uses a multiplexed display, this meant that the update routine was continuing to run. One potential answer is to keep checking until the display gets refreshed, but unfortunately that doesn't happen immediately. I could try and figure out the complete routine, but instead I took the lazy approach: set the target address to an unlikely value and turn off the power once the display updates.

I reset the machine, set the first breakpoint to 0/1/2d, and pressed the 1 key. The emulation stopped. A good sign. This code was indeed the keyboard handler. I then set a breakpoint for the last address in the ROM, f/f/ff, and enabled the trace. The emulator resumed executing, all the while reporting copious amounts of debug information over the serial interface. When the display updated to show the recognised key press, I pulled the plug.

Notably, the display took three full refresh cycles to update. Important to know, as we don't expect the display to actually update when using the right half of the keyboard. With the working 1 key recorded, it was time to reset the machine and repeat the process; this time trying the incorrectly handled 3 key. Knowing that the display would not update, I dutifully waited for three display refreshes before removing power. I saved the traces, and checked the diff. So, what changed?

The following snippets are were produced by the git diff tool, comparing the saved the output from the debugger. Lines marked - are from the 1 keypress. Lines marked + are from the 3 keypress. Additional unchanged content has been added for context.

-    a: 1, x: 0 y: 1, m(0,1): 0
+    a: 1, x: 0 y: 3, m(0,3): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 0
 0/1/2d: 4f tcy f
     a: 1, x: 0 y: f, m(0,f): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 0
 0/1/1a: 27 tam
     a: 1, x: 0 y: f, m(0,f): 1
     pb: 1, cb: 0, s: 1, sl: 0, cl: 0

Unsurprisingly, immediately after the keypress, Y is set to 1 for the first attempt and 3 for the second attempt. This is not, however, the key. Rather, it is the R line that must have been active to allow the key to be detected. The result from previous TKA instruction, immediately preceding our breakpoint, is copied from A into M(0,F). This represents the K input that was recorded. As both the 1 and 3 keys are connected to K1, we see 1 has been stored for both.

     a: 1, x: 0 y: f, m(0,f): 1
     pb: b, cb: 0, s: 1, sl: 0, cl: 2
 0/b/24: 2d     ldx 5
-    a: 1, x: 5 y: f, m(5,f): 6
+    a: 1, x: 5 y: f, m(5,f): 2
     pb: b, cb: 0, s: 1, sl: 0, cl: 2
 0/b/08: 4b     tcy d
-    a: 1, x: 5 y: d, m(5,d): 2
+    a: 1, x: 5 y: d, m(5,d): c
     pb: b, cb: 0, s: 1, sl: 0, cl: 2
 0/b/11: 3e     imac
-    a: 3, x: 5 y: d, m(5,d): 2
+    a: d, x: 5 y: d, m(5,d): c
     pb: b, cb: 0, s: 0, sl: 0, cl: 2
 0/b/22: 93     br 0x13
-    a: 3, x: 5 y: d, m(5,d): 2
+    a: d, x: 5 y: d, m(5,d): c
     pb: b, cb: 0, s: 1, sl: 0, cl: 2
 0/b/04: 27     tam
-    a: 3, x: 5 y: d, m(5,d): 3
+    a: d, x: 5 y: d, m(5,d): d
     pb: b, cb: 0, s: 1, sl: 0, cl: 2
 0/b/09: 0f     retn
     ...

The next change indicates a difference in memory location M(5,F). The ROM accesses, then increments this location. M(5,F) appears to be some kind of counter subroutine, but doesn't appear to be important to the keyboard decoding. It is likely we simply caught the ROM at a different phase of the main loop; one that is monitored by this memory location.

     a: 0, x: 0 y: 0, m(0,0): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 1
 0/1/1c: 47     tcy e
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 1, cb: 0, s: 1, sl: 0, cl: 1
 0/1/38: 22     tmy
-    a: 0, x: 0 y: 1, m(0,1): 0
+    a: 0, x: 0 y: 3, m(0,3): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 1
 0/1/31: 0d     setr
-    a: 0, x: 0 y: 1, m(0,1): 0
+    a: 0, x: 0 y: 3, m(0,3): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 1
 0/1/23: 0f     retn
-    a: 0, x: 0 y: 1, m(0,1): 0
+    a: 0, x: 0 y: 3, m(0,3): 0
     pb: 1, cb: 0, s: 1, sl: 0, cl: 0
 0/1/08: 0e     knez
     ...

At 0/1/1c we see that the ROM then loads the value representing the R pin from M(0,E). The ROM must have stashed it here before the first breakpoint. We know this is the R value of interest as it now reactivates the line to see if the key has been released with the KNEZ at 0/1/08. In our experiment, it has. The key was released while the ROM was paused.

Once again, we see the same counter update code, starting at 0/b/24, incrementing the counter at M(5,F). Different in each run, but likely insignificant. In addition to updating the counter, this block of code briefly sets R6 high and reads from the user inputs. It is probable that this code is related to the time keeping functionality of the Microtronic. The Microtronic can store and display the current time, but doing so depends on the user linking the 1 Hz clock output to user input 4 and setting the clock, as described on page 8 and 9 of the manual. This frequent and repetitive reading of the inputs is likely to be required to access the user's clock signal.

     a: 0, x: 0 y: 0, m(0,0): 0
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/03: 47     tcy e
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/07: 28     ldx 0
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/0f: 7f     cla
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/1f: 00     mnea
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/3f: bd     br 0x3d
-    a: 0, x: 0 y: e, m(0,e): 1
+    a: 0, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/3d: 70     iac
-    a: 1, x: 0 y: e, m(0,e): 1
+    a: 1, x: 0 y: e, m(0,e): 3
     pb: 2, cb: 0, s: 0, sl: 0, cl: 0
 0/2/3b: 00     mnea
-    a: 1, x: 0 y: e, m(0,e): 1
-    pb: 2, cb: 0, s: 0, sl: 0, cl: 0
+    a: 1, x: 0 y: e, m(0,e): 3
+    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
 0/2/37: bc     br 0x3c
     ...

Our next difference, starting at 0/2/03, is much more significant. It is not the instructions that differ here, but the contents of memory at M(0,E), our value representing the active R output when the key was pressed. The code begins testing the value in memory against the A register using MNEA. If M(0,E) is not equal to A, then the code branches, where it increments A with IAC and tests again. Otherwise, it carries on to the next instruction. For our two different keypresses, this is where the substantive divergence takes place. To make things clearer, consider the more succinct disassembly.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
082 0/2/03: 47     tcy 14
083 0/2/07: 28     ldx 0
084 0/2/0f: 7f     cla
085 0/2/1f: 00     mnea
086 0/2/3f: bd     br 0x3d (linear_address=0x08)
087 0/2/3e: b8     br 0x38 (linear_address=0x21)
088 0/2/3d: 70     iac
089 0/2/3b: 00     mnea
08a 0/2/37: bc     br 0x3c (linear_address=0x0d)
08b 0/2/2f: 1c     ldp 3
08c 0/2/1e: 83     br 0x03 (linear_address=0x02)
08d 0/2/3c: 70     iac
08e 0/2/39: 00     mnea
08f 0/2/33: 9d     br 0x1d (linear_address=0x12)
090 0/2/27: 1c     ldp 3
091 0/2/0e: 96     br 0x16 (linear_address=0x16)
092 0/2/1d: 70     iac
093 0/2/3a: 20     tay
094 0/2/35: ac     br 0x2c (linear_address=0x17)
095 0/2/2b: 1c     ldp 3
096 0/2/16: ad     br 0x2d (linear_address=0x28)
097 0/2/2c: 70     iac
098 0/2/18: 00     mnea

Having updated the memory pointer to M(0,E), the ROM clears A, thus A = 0, and checks if M(0,E) != A; if so the code branches to 0/2/3d. Here, we increment A, A = 1, and test M(0,E) != A again. For our first keypress M(0,E) == A so, we skip the next instruction and instead move forward to 0/2/2f. These instructions then sets the page to 3 and branch to 0/3/03. The 1 key works, so instead let us resume with the debug trace for the 3 key.

    a: 1, x: 0 y: e, m(0,e): 3
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
0/2/3c: 70  iac
    a: 2, x: 0 y: e, m(0,e): 3
    pb: 2, cb: 0, s: 0, sl: 0, cl: 0
0/2/39: 00  mnea
    a: 2, x: 0 y: e, m(0,e): 3
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
0/2/33: 9d  br 0x1d
    a: 2, x: 0 y: e, m(0,e): 3
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
0/2/1d: 70  iac
    a: 3, x: 0 y: e, m(0,e): 3
    pb: 2, cb: 0, s: 0, sl: 0, cl: 0
0/2/3a: 20  tay
    a: 3, x: 0 y: 3, m(0,3): 0
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
0/2/35: ac  br 0x2c
    a: 3, x: 0 y: 3, m(0,3): 0
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0
0/2/2c: 70  iac
    a: 4, x: 0 y: 3, m(0,3): 0
    pb: 2, cb: 0, s: 0, sl: 0, cl: 0
0/2/18: 00  mnea
    a: 4, x: 0 y: 3, m(0,3): 0
    pb: 2, cb: 0, s: 1, sl: 0, cl: 0

At 0/2/3c the code sets A = 2 and tests M(0,E) != A which causes a branch to 0/2/1d where the code sets A = 3 and, instead of testing, the ROM copies the accumulator into Y at 0/2/3a using the TAY instruction. At this point in the code A will always be 3, which moves our memory pointer to M(0,3); a location which (at least on this run) contains 0. The code now does an unconditional branch to 0/2/2c where it resumes incrementing A and testing against this strangely updated memory location.

In a block of code that seems to be incrementing the accumulator, testing with it, and jumping in response, the TAY seems a little out of place. In fact, the TAY at 0/2/3a has corrupted the pointer to the memory that stored the R value. The instructions at 0/2/2b and 0/2/16, which would likely jump off to compute the key for the R3 line are never executed, and the tests for any keys attached to R4 and R5 almost certainly fail as they too are looking at the wrong location. It seems the issue is not the emulator, after all, but rather the ROM dump.

Of course, a real Microtronic has a fully functional keyboard. So, our issue must stem from an incorrect dump. As it happens, when Michael was extracting the ROM from his Microtronic he captured two subtly different versions. The first tool used took a simple approach of saving what was reported. The second tool attempted to execute suspect instructions and uses heuristics to infer the true value. While the two dumps differed by 10 bytes, the inference approach can make mistakes and our inspection had shown that the ROM appeared to be logical and complete. For the Microtronic, the ROM appeared unprotected and we had deemed the simpler approach a success. However, armed with the knowledge that the first ROM was faulty, comparing the dumps revealed that byte 0/2/3a was one of the few that differed between the two version. The inferential dumping tool had deemed that this byte should be 0x00, MNEA.

Changing out the byte and re-flashing the ATmega328P results in a correctly working keyboard. The correction from 0x00 to 0x20 represents just a single flipped bit, but a whole heap of potential trouble. Unfortunately, the need for a correction suggests that the other nine bytes that differ between the dumps may also be significant. In fact, more worryingly, it potentially opens the door to issues anywhere in the ROM.

If you compare the above listing to the now publicly available ROM file and disassembly you will note that 02:3a has been corrected to MNEA which is opcode 00. This correction was confirmed after repeatedly redumping the ROM to verify the suspect byte.

Correction by inspection

The ROM now apparently working, some further verification was in order. As a rudimentary test I played a few rounds of the nim game, identified earlier in the ROM, as described on page 7 of the Microtronic instruction manual. The game seemed to work correctly, and, by following the instructions, I could both beat it and be roundly defeated.

However, having identified one errant byte in the ROM it seems prudent to verify the others. The diff between the two dumps threw up 10 mismatched bytes.

Address ROM A ROM B
0/2/3a 20 00
0/c/23 a9 29
1/0/2b c4 84
1/5/32 10 00
1/6/0a 04 00
1/6/21 8d 0d
1/d/28 80 00
2/9/08 b7 37
2/f/02 89 09
3/f/15 04 00

0/c/23

Having serendipitously caught the first error already, the first byte to consider is 0/c/23.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
321 0/c/38: 4f     tcy 15
322 0/c/31: 38     tbit1 0
323 0/c/23: a9     br 0x29 (linear_address=0x2b)
324 0/c/06: 3a     tbit1 1
325 0/c/0d: 91     br 0x11 (linear_address=0x2f)
326 0/c/1b: 39     tbit1 2
327 0/c/36: 93     br 0x13 (linear_address=0x33)
328 0/c/2d: 3b     tbit1 3
329 0/c/1a: b2     br 0x32 (linear_address=0x37)

This code looks good. It is clearly a series of ifs that is checking each bit in a specific memory location and doing some action in response. The alternative 29 ldx 4 would seem out of place. In fact, we have already looked at this code when we were investigating the operation of the SRAM. The fact that nim loads and runs strongly suggests the existing SRAM code is correct.

1/0/2b

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
402 1/0/03: 2c     ldx 1
403 1/0/07: 44     tcy 2
...
410 1/0/27: 21     tma
411 1/0/0e: 72     a5aac
412 1/0/1d: 93     br 0x13 (linear_address=0x33)
413 1/0/3a: 21     tma
414 1/0/35: 7a     a6aac
415 1/0/2b: c4     call 0x04 (linear_address=0x31)
416 1/0/16: 21     tma
417 1/0/2c: 76     a7aac
418 1/0/18: 91     br 0x11 (linear_address=0x2f)
419 1/0/30: 21     tma
41a 1/0/21: 7e     a8aac
41b 1/0/02: a4     br 0x24 (linear_address=0x2d)

Next we consider 1/0/2b. This instruction is part of a block that repeatedly loads a specific memory location, adds a constant to it, and jumps to another location if an overflow occurs. If we look back through the code to identify which memory location, and cross-reference with our understanding of the Microtronic memory map, we recognise this nibble, at M(1,2), is the address of the current Microtronic instruction. This block is likely part of the Microtronic's instruction decoder.

While the CALL is not impossible, it does look a little unlikely considering that all the other jumps in this block are handled with BR. To be certain, it would be prudent to test the instruction decoder by exercising the instruction in question.

Verifying the Instruction Decoder

To work out which instruction, we need to exercise, we must first work out under what circumstances would the ROM execute the CALL. CALL, like BR is conditional on the status flag, S. In this instance S is set by AxAAC when the addition causes an overflow. What number + 6 causes an overflow in 4-bit memory? 10, or A.

Cross-referencing with the Microtronic instruction set, we see that instructions in the form A s d, where s is the source register and d is the destination, are defined as "s oder d = d", a "logical or operation". As it happens, the OR instruction is not used in the nim game we explored earlier. We can't use nim to verify the instruction is working correctly.

Time to write a simple Microtronic program. The following code is commented, using a modern notation, but the full description of each operation is best described in the original instruction reference card. For those of us who don't speak German, a couple of translated summaries are available.

Address Command Comment
00 1A0 r0 = 0xA (n.b. 0b1010)
01 151 r1 = 0x5 (n.b. 0b0101)
02 A10 r0 = r0 | r1
03 F10 output 1 digit, from r0
04 C04 goto 04 (infinite loop)

Succinctly, the program loads register 0 with A (1010); loads register 1 with 5 (0101); carries out the suspect instruction by ORing the two registers and storing the result in register 0; the program then outputs the 0 register on the display; and, to end, it goes into an infinite loop to allow us read the result.

Why not just loop back to the beginning, with C00?

Once the display parameters have been set, the Microtronic immediately displays any changes to the registers. You can think of the Fns display instruction as mapping the n registers as the video memory, starting at register s. If we loop back to the beginning, register 0 is reset to A, which is then immediately displayed on the LEDs, before it is then updated by the OR instruction. The result is the display rapidly switching between both values.

The result of the OR should, of course, be F (1111). Any other value would indicate that the the OR decoder is not working. The CALL at 1/0/2b would look to be a likely culprit. If the instruction is incorrect, we should also be prepared for the Microtronic to go haywire. An unexpected instruction might have any number of unintended side-effects.

On running the program with a CALL at 1/0/2b we get the result 5, which is incorrect. Interestingly, the machine continues to operate suggesting that, again, the code paths converge before any significant upset occurs. Nevertheless, the instruction needs to be fixed. Flipping the erroneous bit in the ROM, which sets the instruction byte to the alternative 84, has the desired effect. Running the Microtronic program now displays F, as expected.

1/5/32

Resuming our investigation of the mismatched bytes, next we have 1/5/32. This byte is both preceded and followed by a sequence of 0x00 bytes. It is probable, if not certain, that these are padding bytes. A stray LDP here makes little sense. As such, we can infer that 1/5/32 is very likely a bit-flipped 0x00.

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
573 1/5/13: 1e     ldp 7
574 1/5/26: a7     br 0x27 (linear_address=0x10)
575 1/5/0c: 00     mnea
576 1/5/19: 00     mnea
577 1/5/32: 10     ldp 0
578 1/5/25: 00     mnea
579 1/5/0a: 00     mnea
57a 1/5/15: 00     mnea
57b 1/5/2a: 00     mnea
57c 1/5/14: 00     mnea
57d 1/5/28: 80     br 0x00 (linear_address=0x00)
57e 1/5/10: 00     mnea
57f 1/5/20: 00     mnea

We can verify whether this is padding (though not what the true value would be) by checking to see if any code jump to this 1/5/32, or any of the preceding bytes up until we reach an unconditional branch (at 1/5/26) as follows.

(ldp 5(.*\n){1,2}|1/5/).*(call|br) 0x(32|19|0c)

No results. Padding. Zero it is.

1/5/28

Having identified 1/5/32 as a misread padding byte, 1/5/28 also caught my eye. While this byte was consistent between both dumps, it too looked like a bit flip had occurred during the read. Again, we can check with a regex.

(ldp 5(.*\n){1,2}|1/5/).*(call|br) 0x(28|14|2a|15|0a|25)

A single candidate in page 5, but one which loads the page buffer to switch to page 8 in the preceding instruction. As we are in page 5, these bytes never execute. While a padding byte, that is never actually executed, will not affect operation, the perfectionist in me required this to be 'fixed'. The 0x80 was replaced with the much more likely 0x00.

1/6/0a

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
5b7 1/6/32: 00     mnea
5b8 1/6/25: 00     mnea
5b9 1/6/0a: 04     dyn
5ba 1/6/15: 00     mnea
5bb 1/6/2a: 00     mnea

Similar to other stray bytes amongst a series of zeros, the apparent DYN at 1/6/0a makes little sense.

(ldp 6(.*\n){1,2}|1/6/).*(call|br) 0x(0a|25|32|19|0c|26|13)

Again, checking backwards to the next unconditional jump (at 1/6/09) we find two candidates in chapter 2. The first switches to chapter 3, and the second stays in chapter 2. Thus, neither of these candidates lead here and these bytes are never executed. Another single bit flip, restored by using the alternative and much more likely 0x00.

This regex also suggests that the branch at 1/6/26 is unused and therefore may also be erroneous, though unlike our previous example the pattern is not entirely implausible. We'll leave this for now but may want to return to it later.

1/6/21

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
597 1/6/2c: 2c     ldx 1
598 1/6/18: 48     tcy 1
599 1/6/30: 21     tma
59a 1/6/21: 8d     br 0x0d (linear_address=0x25)
59b 1/6/02: 2c     ldx 1
59c 1/6/05: 40     tcy 0
59d 1/6/0b: 1e     ldp 7
59e 1/6/17: c3     call 0x03 (linear_address=0x02)
59f 1/6/2e: 0f     retn

At 1/6/21 we currently have an unconditional in-page branch, the alternative is 0x0d or SETR. SETR uses the current Y value, and the preceding configuration clearly relates to setting the memory pointer. Again, refering to our understanding of the Microtronic memory map this would relate to loading the second nibble in a Microtronic instruction word. If we swap this out, the ROM will turn on R1 and then would unnecessarily re-set X to 1 (which it already is). This would seem like odd behaviour in what appears to be part of the instruction decoding stage. As such, the existing branch instruction, 0x8D, looks to be correct.

1/d/28

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
77b 1/d/2a: 00     mnea
77c 1/d/14: 00     mnea
77d 1/d/28: 80     br 0x00 (linear_address=0x00)
77e 1/d/10: 00     mnea
77f 1/d/20: 00     mnea

At 1/d/28 we find another stray branch in a padding byte. Likely another bit flip, and an instruction that should likely have read as a zero. There are a significant number of zero bytes between 1/d/28 and the preceding unconditional branch (not shown) but we can take our belt-and-braces approach to check if any of these are used.

(ldp 13(.*\n){1,2}|1/d/).*(call|br) 0x(28|14|2a|15|0a|25|32|19|0c)

Only a single candidate, in chapter 2 with no change of chapter. None of the listed padding bytes in 1/d/xx are ever executed, and thus the code at 1/d/28 is never reached. Zero it is.

2/9/08

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
a6b 2/9/29: 15     ldp 10
a6c 2/9/12: 80     br 0x00 (linear_address=0x00)
a6d 2/9/24: 15     ldp 10
a6e 2/9/08: b7     br 0x37 (linear_address=0x0a)
a6f 2/9/11: 15     ldp 10
a70 2/9/22: b9     br 0x39 (linear_address=0x0e)

At 2/9/08 we have a branch in what looks to be a jump table of some sort. The alternative instruction rbit 3 (byte value: 37) makes little sense here. If we replace the byte, the ROM will end up both clearing bit 3 at the current memory location but also setting the page buffer to 10 once each side of it. It is much more likely that the existing branch is correct.

2/f/02

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
bce 2/f/39: 2f     ldx 7
bcf 2/f/33: 40     tcy 0
bd0 2/f/27: 3e     imac
bd1 2/f/0e: 27     tam
bd2 2/f/1d: 7a     a6aac
bd3 2/f/3a: ab     br 0x2b (linear_address=0x15)
bd4 2/f/35: 89     br 0x09 (linear_address=0x32)
bd5 2/f/2b: 7f     cla
bd6 2/f/16: 25     tamiyc
bd7 2/f/2c: 3e     imac
bd8 2/f/18: 27     tam
bd9 2/f/30: 7a     a6aac
bda 2/f/21: ab     br 0x2b (linear_address=0x15)
bdb 2/f/02: 89     br 0x09 (linear_address=0x32)
bdc 2/f/05: 26     tamza
bdd 2/f/0b: b7     br 0x37 (linear_address=0x0a)

At 2/f/02 we have a branch. The alternative is 09 COMX. This block of code is part of a BCD carry handler. It clears the current accumulator, sets the current memory location to 0, and increments the next memory location. If that location is now greater 9 it loops round and repeats the carry process. In this instance, it is conceivable that the ROM could switch to an alternate area of internal RAM (COMX would change the memory pointer) and store the final result (plus 6) elsewhere for use in further calculations. However, the preceding code at 2/f/39, which starts the initial increment operation, branches to 2/f/09 if no carry is required. Therefore, it makes sense that once the carry has completed the carry handler would also branch to the same segment. The branch seems like the most likely instruction.

3/f/15

Linr Addr   Opcode Instruction
---- ------ ------ -------------------------------
ff7 3/f/32: 15     ldp 10
ff8 3/f/25: b7     br 0x37 (linear_address=0x0a)
ff9 3/f/0a: 09     comx
ffa 3/f/15: 04     dyn
ffb 3/f/2a: 00     mnea
ffc 3/f/14: 00     mnea

Finally, at 3/f/15 we have a DYN instruction. This area is right at the end of the ROM and is followed by only padding bytes. Additionally, it is preceded by an unconditional jump, and no instruction branches into either 3/f/0a or 3/f/15. The only candidate is in the wrong chapter, and does not attempt to change the chapter buffer. As such this is probably another padding byte.

(ldp 15(.*\n){1,2}|3/f/).*(call|br) 0x(0a|15)

COMX

Knowing that it is also not used, why do we not consider the COMX instruction at 3/f/0a suspicious? To ensure proper operation, Texas Instruments tested the manufactured dies using an automated program. As noted in the Programmers Manual, the test algorithm requires the COMX instruction. Having decided that the earlier instruction at 2/f/02 was not COMX it means this important instruction does not appear anywhere else in the ROM. TI recommend that the required, but otherwise unused, instructions are simply inserted in an unused area of the ROM. Therefore, while the instruction is unused in the Microtronic ROM it is likely an accurate byte.

Corrected bytes

Having completed our inspection, our final list of corrections is as follows. Note that no correction needs to be applied to the publicly available ROM file and disassembly.

Address ROM A ROM B Final Required
0/2/3a 20 00 00 Y
0/c/23 a9 29 a9 Y
1/0/2b c4 84 84 Y
1/5/32 10 00 00 N
1/6/0a 04 00 00 N
1/6/21 8d 0d 8d Y
1/d/28 80 00 00 N
2/9/08 b7 37 b7 Y
2/f/02 89 09 89 Y
3/f/15 04 00 00 N
1/5/28 80 80 00 N

Where a byte is required to be set as noted in the Final column, this is indicated in the Req. column. For padding bytes, the exact value is of no real consequence. Padding bytes are never executed. Of the two original ROM dumps, the original version of the dump, labelled A here, only requires two changes. The byte at 0/2/3a in the keyboard decoder, and at 1/0/2b for the Microtronic's OR instruction.

However, by chance, we have identified two other possible erroneous bit-flips (at 1/5/28 and 1/6/26). These potential issues were not highlighted by a differences between our initial dumps, and suggest that there may be other suspect bits throughout the ROM. Inspection of these 10 bytes was arduous enough, checking the other 4,086 by hand would be an undertaking indeed. A problem for another day.

A need for speed

As a novice Microtronic user, and with non-existent German, I turned to the only English language source I knew of: Michael Wessel's YouTube channel. In particular, I started with a simple program he uses to demonstrate the speed of the Microtronic. The shorter of the two programs he highlights is as follows:

Address Command Comment
00 F02 turn off the display
01 510 r0 += 1
02 FE0 output r0
03 C01 goto 01

This program presents a simple binary counter on the outputs. User output 0 will alternate between high and low every six instructions. This allows us to measure the maximum instruction throughput, at least for this combination of increment, output, and goto instructions. While the program worked, Michael demonstrated a 19 Hz toggle rate yet my breadboard Microtronic was toggling the output LED at just 14 Hz.

First I verified that the emulator was not falling behind. While some minor variation might be acceptable, the emulator must not get into a state where the queue of instructions to process consistently build up. Despite the ATmega328P being run at 16 MHz, the need to shift data out via shift register would have a significant impact on the target emulation speed. An easy substitution was the clock crystal. Thus far I had run the ATmega328P at the typical 16 Mhz, but a 20 MHz base clock would speed communication with the shift registers and provide more native clock cycles with which to process each TMS1600 instruction.

Despite the available serial debug capability, the serial line would simply not be fast enough to reliably communicate the state of the virtual CPU. Another solution to monitoring the chip was needed. Instead, I added a line of code to the synchronisation loop in the emulator that outputs a high signal on a spare I/O pin if more than one emulated instruction was due for execution.

While the monitoring pin indicated the occasional instruction queue, this was well within tolerance and it appeared the emulator cleared the backlog immediately after the next instruction. In truth, this does indicate that the emulator is not truly real-time on a micro-level. If, for example, a ROM only had long sequences of only costly output instructions (e.g. SETR, RSTR), then the emulator might fall behind. However, in practice, these sequences are likely to be rare. With the Microtronic ROM the emulator is able to maintain the requested instruction throughput when averaged over just a handful of typical TMS1600-native instructions.

It might seem that output instructions are the core of the Microtronic speed test program we are using, but these, in themselves, are not troubling. The Microtronic ROM itself is an emulator for another instruction set: Microtronic instructions. It's emulators all the way down. For each Microtronic output instruction, hundreds of TMS1600 instructions need to be processed by the virtual CPU: fetching and decoding the Microtronic instruction, reading data from registers, and controlling the relevant I/O lines to actually output the data. It was not the theoretical compounding of I/O instructions that was to blame, rather the Microtronic in Michael's video seemed to go a fair bit faster than 500 kHz.

Measuring the internal oscillator

The TMS1600 at the heart of the Microtronic relies on an internally generated clock signal. TMS1000 series chips can use an external clock, and the Microtronic itself does have a pseudo real-time clock circuit on the PCB, but these are not used to clock the CPU on the Microtronic. Instead, Busch relied on the built in oscillator. This is configured by shorting the OSC1 and OSC2 pins, and tying these to Vss and Vdd via a capacitor and resistor, respectively. The data book for the TMS1000 series offers some expected clock rates for given parameters, but the particular values used by the Microtronic, 56pF and 22K, are not explicitly listed.

One approach to measure the speed of the internal oscillator is a similar technique to the one used by Michael for the Microtronic itself. Get the TMS1600 to toggle one of the pins and multiply the recorded period by the total number of clock cycles used by those instructions. One pin that is regularly toggled on the Microtronic is R11. As we discovered in Reading the SRAM, the Microtronic must raise R11 to read the SRAM. Combined with our handy debugger, we can count how many instructions the virtual TMS1600 carries out between raising R11 and setting it low again. Unfortunately, due to the bit-flipping inversion of the data the precise number of TMS1600 native instructions executed varies, but it takes approximately 570 instructions for each three nibble, 12-bit, read.

Additionally, it is important to recall that on TMS1000 series chips, each instruction takes 6 clock cycles. Knowing all this we can do the following relatively simple calculation, where R11_s is the period R11 is held high and OSC_hz is our resulting internal oscillator frequency.

OSC_hz = 1 / ( R11_s / (570 × 6) )

Michael kindly did a reading on his test-bed Microtronic, which showed that during each read R11 is held high for 6.060 ms, or 0.00606 seconds. This gives us a final estimated internal oscillator frequency of ~ 565 kHz. Over 10% faster than the nominal speed noted on the schematic, in the brochure, and on the Busch website.

Setting the speed of the emulator is handled at compile time, so I updated the target frequency to 565, recompiled the code, and reprogrammed the ATmega328P. Running the same experiment on my breadboard system revealed a precise match for the SRAM access, as indicated by R11, but I was still only achieving 16 Hz in the binary counter test. To actually achieve a 19 Hz binary count we would expect the internal oscillator on the TMS1600 to run at around 670 kHz!

19 / 16 × 565 kHz = 671 kHz

While this is significantly faster, it is not entirely preposterous. Again, referring to the data book, we can see that for a TMS1600, while a 56pF capacitor is not shown, a 47pF capacitor and 22K resistor would result in a speed well north of 600 kHz. In fact this speed is literally off the chart!

Michael's test-bed Microtronic had been in the wars. The eventually successful attempts to dump the ROM had resulted in a number of cut traces and bodge wires scattered across the PCB. The careful balance of the RC circuit, which sets the oscillator frequency, may well have been affected by these various modifications and injuries.

Michael's test bed Microtronic
Michael Wessel's test bed Microtronic

A second test with a real, intact, Microtronic revealed that, like the computer in the original video, it too achieved a 19 Hz binary counter. More importantly it indicated a R11 high period of 5.060 ms, or 0.00506 seconds. Plugging this into the formula gives the internal oscillator speed.

1 / ( 0.00506 / (570 × 6) ) = 676 kHz

The TMS1600 in this Microtronic is running at just a hair over 675 kHz! While this is almost precisely what our calculations predicted, it represents a significant overclock. The Microtronic is running 35% faster than Busch claim, and over 20% higher than Texas Instrument's maximum rated oscillator frequency for the TMS1600.

Redumping the ROM

While the ROM appeared to be fully functional, the fact that some bytes did not differ between the two dumps, yet were clearly incorrect, opened the door to errors anywhere else in the ROM. The only solution was to try and refine the dumping process. Armed with our known corrections Michael took up the challenge.

Unfortunately, due to the considerable time that had passed, Michael had dismantled the breadboard dumping solution. This did, however, give him the opportunity to make a refined dumping circuit. A little protoboard later, and Michael had created a far more impressive tool with which to interrogate the TMS1600.

Michael's TMS1600 dump tool
Michael's TMS1600 dump tool

By tweaking the voltage, Michael was able to correct for some of the bytes. However, there seemed to be some additional issues with reading certain bits. Eventually, Michael discovered some questionable signal levels. These appeared to indicate some bleed of the signal into the next read. By using lower voltages and repeatedly reading the questionable bytes, Michael was able to acquire a consistent dump that fixed the known bad bytes. For a more complete description, I encourage you to read about the redumping procedure in Michael's own words.

Problematic TMS1600 signal levels
Problematic TMS1600 signal levels

Another day, another Microtronic

A few bytes in the ROM continued to bug me. In our discussions, Michael had brought to my attention a Microtronic on eBay.de. While I had a saved search, the Microtronic in question was subtly misspelled, which had prevented a match and excluded it from my eBay notifications. The Microtronic was in beautiful condition and came with the rare Cassette Interface board, something I was also interested in recreating. With the computer being located in Germany I enquired with the seller if they would ship to the UK, which they confirmed they would.

The listing was an auction so I would have to bide my time. I didn't want to draw attention to the lot, so waited patiently for the final day - but when I went to bid, my attempt was rejected. Despite the seller offering to ship to the UK, the setting had not been changed. I frantically messaged the seller, to no avail. However, as luck would have it, no one else bid and the auction failed.

Having failed to shift the item, the seller got back to me. They had relisted the item and updated the shipping option. Kindly, the also enabled offers, which allowed me to get the item at the base price. A few days later, a large printer box arrived at my front door. Inside, a Busch Microtronic 2090 and assorted paraphernalia.

I immediately plugged the item in, using a simple plug adapter, but was disheartened to find that the Microtronic did not turn on. Still, having successfully built a breadboard version, nothing I couldn't fix. I reached for my test equipment, and as I did so knocked the transformer slightly. The Microtronic sprung to life! My adapter was not making a good connection to the Europlug. The Microtronic worked.

Speed Test

Having my own Microtronic to hand gave me a few opportunities. The first was to validate the unexpectedly high internal oscillator speed of the TMS1600. I immediately tapped in the test program from Michael's video, and using my basic oscilloscope measured the speed. Indeed, my Microtronic also toggled the outputs at 19 Hz, matching the speed of Michael's intact machine.

Microtronic output test
Microtronic running the binary counter program at 19 Hz

Redumping the ROM, again

The second thing to do was a big step. If I removed the TMS1600 I could truly isolate the chip from any malign interference from the PCB and finally verify the ROM. My Microtronic was more or less in museum condition, and removing a 40 pin chip with basic tools is hardly ideal. Still, being able to preserve these machines in perpetuity made it seem worth the risk. I decided there was only one thing to do and ordered some 40 pin DIP sockets.

A few days later, sockets in hand, I got to work on disassembling the Microtronic. First I carefully removed the front panel, keyboard, and main PCB. Easier said than done. Circuit board free of the case, I got to work on removing the IC. It was a slow process, but with a basic soldering iron, solder sucker, wick, and flux I eventually freed pin-after-pin of the TMS1600 from the double-sided PCB.

Microtronic output test
Back of the Microtronic PCB after freeing the TMS1600

Having successfully removed the chip, I popped in my socket; careful to ensure it was pointing the right way (that is to say, to the right!). I immediately reinstalled the TMS1600. Had I broken my brand new (to me) toy? The moment of truth: the Microtronic worked.

With the chip truly isolated, I recreated Michaels dumping process. Matching the pins and tweaking the timing. The dump worked. All the suspect bytes resolved exactly as expected. All but one.

The final troublesome byte was 3/e/10. Michael and I had gone back and forth on 3/e/10. In some reads it would be 11, in some 10, and in others 00. Was it timing? Was it voltage? We were never quite sure. Logical inspection of the code shows that 3/e/10 follows an unconditional branch. Further, even the most generous of regular expressions shows that no code jumps to this address. The code itself never executes. Nevertheless, the perfect ROM dump eluded us.

(ldp 14(.*\n){1,16}|3/e/).*(call|br) 0x10

It seems logical that this byte would be 00, as were all the other unused bytes; yet I most frequently got 11. I continued to tweak the ROM dumping program, exploring different timings and protocols, with limited success. Equally, I tried tweaking the circuit. Was the lack of a connection to OSC2 problematic? No. Would a stronger pull-down on O7 clean up any problematic signalling? Yes, but it didn't fix the byte.

The timing didn't seem to have a significant effect. Very slow timing would sometimes result in the chip behaving oddly, loosing sync with the Arduino, so I continued to run the chip as fast as the Arduino could realistically handle.

Tweaking the voltage allowed me to achieve the expected result. Not just for 3/e/10 but for all other bytes. Interestingly, these dumps were at little more than 5 V. Much lower and the TMS1600 would act erratically, or not respond at all. Anything over 5.3 V, or so, resulted in the 1 in either or both positions. Curiously, throughout these experiments only 3/e/10 was affected.

The 00 at 3/e/10 seems to make the most logical sense. Despite the fact that the TMS1600 is being run ridiculously out of spec, being able to consistently extract this precise bit pattern from the chip bolsters my opinion of this fact. As such, I consider 00 at 3/e/10 the definitive ROM. Yet, the true value could very well be 11. Or any other value. It doesn't really matter. After all, as we've seen the byte is unreachable - at least in normal operation - but the fact that we've been unable to conclusively validate the ROM, so far, is mildly frustrating. As neither Michael or I are likely to be shipping our Microtronic off to Sean Riddle any time soon, not knowing is, for now, my cross to bear. I'm sure I'll return to this puzzle before too long.

Verifying the OPLA

One further opportunity available to us is to verify the OPLA. When using the O outputs to control the display all pins are required to control the individual segments on the display. This allowed us to verify the OPLA settings by ensuring the display showed the expected patterns, in particular using the built-in self-test. However, by setting the status latch (SL) low, the Microtronic also uses these pins to access the SRAM. When accessing the SRAM the OPLA is configured to set the lower four bits to the required low-nibble, but we had yet to confirm the true value of the unused high-nibble. With an actual Microtronic to hand we can hook up a logic analyser to the O outputs and set the Microtronic to step through each of the possible 16 low-nibbles of the address. One way of doing this is to fill the memory of the Microtronic SRAM with no-ops, and let the program run.

A handy no-op instruction in the Microtronic is 000. As noted on the quick reference card, this equates to the MOV instruction. The form of this instruction is 0 s d. In practice, the MOV instruction copies the value in the register s to the destination d. Copying register 0 to register 0, effectively, does nothing. Even more helpfully, the Microtronic can zero all of the SRAM with the built-in program PGM-5 as described on page 31 of the instruction manual. We have to make one minor change to this program, however. To prevent updating the display interfering with out measurements we can change the very first instruction to F02 which, as we have seen before, turns off the display.

Verifying the OPLA is then achieved by running the program, allowing the Microtronic to step through each address in sequence, as we record the state of each of the O pins using the logic analyser. This reveals that, indeed, as the Microtronic steps through each of the possible low-nibbles on O0 to 03, values 0x0 to 0xf, the state of the all the remaining O pins remains low or 0x0.