Decoding the NEC V20 Microcode

reenigne's decoding of the 8088 microcode in 2020 opened the doors for extremely accurate emulation of the 8088 CPU.  

Although I had added support for the NEC V20 in MartyPC, the V20 core was not cycle-accurate in terms of the V20's actual timings.  It was an 8088 in a V20's clothing - a copy-paste of my 8088 core with V20 instructions tacked on.

This was not an ideal case, but the prospect of making my V20 core cycle-exact without the microcode seemed like it might be a discouraging slog of trial and error.

So why not get the microcode, then?

I recently commissioned InfoSecDJ to take die photography of an NEC V20 CPU (actually a second-source V20 fabricated by Sharp, but a V20 nonetheless). He did an excellent job.

The NEC V20 CPU die - InfoSecDJ

This photomosaic is extremely high resolution - 5.6 Gigapixels to be exact, an astonishing 70478x80672 resolution - too large to even fit in the JPEG image format!

You can see the entire thing at full resolution here.

The rectangular region just below the center of the die is the main microcode ROM.

The V20 Microcode ROM block

The ROM array is 258x116, containing 29,928 bits. It's evenly divisible by 29, which we know is the microcode word length of the V20, so that's a good sign.  But it implies 1032 microcode words, when we were only expecting 1024.  That's a bit odd, isn't it?  We'll figure out the reason for that a bit later on.

Here's a close-in crop of the ROM array:

Microcode bits, zoomed in


The bright, horizontally running traces are part of the chip's metal layer. The yellowish dots are interconnects that connect the metal layer with the polysilicon layer beneath it.  Note the vertical bars behind the metal layer - and note that there is occasionally a gap in the polysilicon on either side of each interconnect.  

These gaps form a transistor - with the presence of a transistor indicating a 1 bit. I'll highlight the 1 bits to make that a bit easier to see.


Seeing this got me very excited - if we can visually identify the bits in the ROM, then we can extract the ROM contents.  Just one slight problem - there are 29,928 bits in the array.  That would be a tad bit tedious to extract by hand.

Luckily, there are tools available for this task.  I loaded up the ROM image in MaskRomTool by Travis Goodspeed.

Defining bit locations with MaskRomTool


MaskRomTool allowed me to draw the rows and columns that defined the locations of the bits.  Unfortunately, I found that its bit-detection methods were based on thresholding, and there wasn't enough of a difference in contrast between a bit and a non-bit to make this an effective detection mechanism.  Notice the bit histogram is very compressed toward the far axis.  This was not going to work.  Perhaps the thresholding technique would have worked better without the bright metal layer, but I didn't want to ask InfoSec DJ to attempt removing it. Another approach was needed.

Fortunately, we could use MaskRomTool to export our defined bit positions to JSON format.  I used this exported JSON file to write a Python script that extracted a square bitmap centered on each bit position, and saved it with the bit's logical column and row number in the filename. 

This gave me a 105MB ZIP file of little 42x42 pixel PNG files, each containing either a bit or a not-bit. The idea behind doing this was to train a convolutional neural network to identify the bits as either 0 or 1.

If this sounds out of your depth, I had no prior experience in training CNNs before this project, and I completed it in a single afternoon. Modern Python frameworks such as PyTorch make it that easy.

There are some good CNN tutorials out there, many of them focused on the classic problem of handwriting recognition, but we have an even simpler task. We just have to sort images into two buckets, 0s or 1s. The problem is quite literally "Hotdog or Not Hot Dog."

Before we can train our CNN, we have to have a training data set. So I created a quick and dirty Python/tkinter script so I could quickly sort bits by eye into buckets as either 0s or 1s.

The quick and dirty classification tool


Shown here is a '1' bit. Can you spot it by the transistor behind the metal layer? The buttons ended up being extraneous - you just need to hit either 1 or 0 on your keyboard to classify the bit.  In theory, you could do this 29,928 times and you'd have the job done in a few hours. I had originally intended for this to be my backup method in case the CNN training didn't work out - I had a few friends willing to volunteer to help, and the JSON logs that the "Bit Voter" produces can be merged to support distributed work with consensus. Fortunately, this was not needed.

Ultimately, I classified a little over 1,000 bits manually this way. Once they were sorted into two directories, we could now attempt to train a CNN model using the sorted images as input.

I won't go into great detail about how to write a CNN here - the source will be on my GitHub if you're  curious. I used the excellent PyTorch library. which made the whole process a lot simpler than I could have ever imagined.

This is what a training run looks like. 

 [Epoch 01] train: loss=0.6945 acc=0.7273 f1=0.0164 | val: loss=0.6924 acc=0.7876 f1=0.0000  
   val precision=0.0000 recall=0.0000 cm=[[178, 0], [48, 0]]  
 [Epoch 02] train: loss=0.6860 acc=0.7151 f1=0.3826 | val: loss=0.6257 acc=0.7965 f1=0.0729  
   val precision=0.5000 recall=0.0394 cm=[[178, 0], [46, 2]]  
 [Epoch 03] train: loss=0.3751 acc=0.8914 f1=0.7213 | val: loss=0.3900 acc=0.7655 f1=0.6327  
   val precision=0.4661 recall=1.0000 cm=[[125, 53], [0, 48]]  
 [Epoch 04] train: loss=0.1159 acc=0.9523 f1=0.9139 | val: loss=0.0495 acc=0.9912 f1=0.9773  
   val precision=0.9773 recall=0.9773 cm=[[177, 1], [1, 47]]  
 [Epoch 05] train: loss=0.0251 acc=0.9945 f1=0.9888 | val: loss=0.0460 acc=0.9867 f1=0.9744  
   val precision=0.9514 recall=1.0000 cm=[[175, 3], [0, 48]]  
 [Epoch 06] train: loss=0.0319 acc=0.9933 f1=0.9802 | val: loss=0.0438 acc=0.9823 f1=0.9659  
   val precision=0.9350 recall=1.0000 cm=[[174, 4], [0, 48]]  
 [Epoch 07] train: loss=0.0185 acc=0.9945 f1=0.9212 | val: loss=0.0274 acc=0.9956 f1=0.9891  
   val precision=0.9792 recall=1.0000 cm=[[177, 1], [0, 48]]  
 [Epoch 08] train: loss=0.0141 acc=0.9956 f1=0.9913 | val: loss=0.0271 acc=0.9956 f1=0.9891  
   val precision=0.9792 recall=1.0000 cm=[[177, 1], [0, 48]]  
 [Epoch 09] train: loss=0.0101 acc=0.9978 f1=0.9940 | val: loss=0.0447 acc=0.9867 f1=0.9735  
   val precision=0.9488 recall=1.0000 cm=[[175, 3], [0, 48]]  
 [Epoch 10] train: loss=0.0110 acc=0.9967 f1=0.9907 | val: loss=0.0437 acc=0.9912 f1=0.9773  
   val precision=0.9773 recall=0.9773 cm=[[177, 1], [1, 47]]  
 Early stopping: no val F1 improvement >= 0.0 for 3 epoch(s).  
 Best val F1: 0.9891  

If you have a CUDA-capable GPU, training is rather quick - this only took a few minutes.

The idea is that we want to maximize our accuracy - but reaching 1.0 may not be feasible, and maybe not even desirable (there's a thing called overfitting).  Sometimes going on for longer just makes things worse, so we end training if we're not seeing a steady improvement. 

The output of the training is a neural network model - we can then use this model to run inference on an entire input data set. Inference is just a fancy word for applying our model to actually do what we trained it to do - predict whether a given image contains a 0 bit or 1 bit.

Before we move on - a quick note to head off any potential controversies. CNNs loosely fall in the broader scope of AI from a computer science perspective, but we are not using "AI" in the modern, controversial sense that typically refers to a large language model (LLM). 

When we run an inference pass, we get a confidence score for each pixel. We can use this confidence score to mark bits the model is less confident about, under some specific threshold (I used < 99% here).  Here's the result of the first run, with ambiguous bits colored red:


The first output of our bit-classification CNN

I took all the ambiguous bits and manually sorted them back into the training folders, then re-ran the training, repeating until I got this result:

The final "good enough" inference run

This was pretty good - only 4 bits remain ambiguous, and it was faster just to manually verify them than to train another model. 

Great, we have our 29k microcode bits and we saved hours of tedious manual labor (in exchange for hours of writing a training script in Python, but at least that is reusable!).

We still have to turn this rectangular blob of bits into a list of 29-bit microcode words.  In other words, we need to reorganize the bitmap until it is 29x1032 instead of 258x116.  How exactly to go about doing that is not obvious, but we can put it aside for the moment until we've decoded the matching decoder PLA

The decode or "activation" PLA sits above the main microcode ROM block, with some intermediate circuitry sandwiched in between.

The decode PLA

The job of this PLA is to take 13 logical inputs from the left side (each input has a twin inverted signal, for a total of 26 input lines), and activate one column of the microcode array beneath it if the input signals match that column of the PLA.

What do we mean by matching?

A closer zoom might be informative:

The decode PLA, zoomed in

We have a similar arrangement of vertical metal wires, punctuated by interconnects, and gaps in the substrate forming transistors.  In this case, these transistors form logical AND gates.  Unlike the microcode ROM, only one transistor is ever attached to an interconnect, facing either the normal or inverted signal of an input pair.  This allows the PLA to test for a 0, a 1, or to not care about that input signal at all (the case where no interconnect is present).  This creates a sort of maskable Boolean logic.  Since all the gates are tied together, the corresponding column of microcode will only be activated if all the gates match. 

InfoSecDJ has an interesting analysis of the V20's PLA circuitry here that is worth a read.

This matching logic is very clever - almost certainly 8 of the 13 input lines are the opcode byte itself for a given instruction.  Setting "don't care" bits in the PLA allows entire ranges of instructions to share the same microcode implementation, which is hugely important for efficiency's sake so that the microcode ROM doesn't take up the entire CPU die.

What's being stored here is not exactly a set of bits, but instead logic - the simple AND logic can be represented as pairs of bits, and we can extract those bits the same way we extracted the microcode ROM - drawing rows and columns in MaskRomTool, exporting the bits as images, manually sorting a thousand of them, then feeding that to our hotdog CNN.

This is our result:

The final PLA extraction


Manually decoding the first few columns of the decode PLA is promising. We essentially have:

00?00???0??00
00?00???10?00
00?000??11100

where a ? means we "don't care" about the value of the bit in that position, allowing for a range of opcodes to match.

Luckily, it appears that the 8 bits of the instruction opcode are represented in the 8 inputs starting at the fourth input.

The first row will match 64 different opcodes, starting with 00,01,02,03,08,09,0a,0b,10,11,12,13, and so on. This happens to correspond with the 8088 ISA's general ALU opcodes, which all share the same microcode on the 8088.  That's a good sign.

The second line will match 04,05,0c,0d,14,15,1c,1d, and so on, corresponding with general ALU opcodes that take an immediate operand. This is surely more than coincidence.  As it turns out, the matching logic for the most part is laid out in a reassuringly numerical order.

One thing to note is that there are only 257 activation lines for 1032 microcode words. This means that the "entry points" into the microcode ROM for a given instruction have to be at addresses evenly divisible by 4 - this was the case on the 8088 as well, so it's not weird to see.

Each column of the microcode contains 4 words. 

The 4:1 microcode column multiplexers

Between the two blocks of microcode, a 4-way multiplexer allows two logical inputs to select one of the four words currently activated by the decode PLA.  These inputs are pulled from the two low-order bits of the microcode program counter.

Given a rectangular block of microcode ROM bits, the question becomes how that block is addressed to produce a linear arrangement of microcode words. There are a number of possible permutations - from the ROM array itself, we could read from the top or bottom, and within each multiplexed row, we have a similar choice. It's also possible that lines are swapped or interleaved, so some trial and error may be necessary to produce something that looks reasonable. As it turns out, we read the microcode words in order from each column from the bottom-up.

We can tell we're on the right track if the patterns of the resulting word bitmap align with known fields. This is extremely useful if you happen to know what those known fields are. Thankfully, I had something of a clue from court documents from NEC and Intel's infamous lawsuit over the V20 microcode.  Although we will see later that this diagram is not quite correct, it gave us enough of a picture to get started.

The NEC V20 microcode word format from court documents

Here you can see me musing about the reasonableness of a certain word extraction on Discord.

Working on V20 word extraction

Thankfully, finding a correct word extraction did not take long at all.

Here's what all the microcode words look like once extracted, split into five columns (each column continues from the bottom of the column to its left).  I've colorized the various sections of the microcode for visual interest. I attempted to use a colorblind-friendly palette (Okabe & Ito), but apologies if this information is not visible to everyone.

The V20 microcode areas, indicated by color

Approximately two-thirds of the V20 microcode is dedicated to implementing the Intel 80186 ISA. The remainder is dedicated to either implementing NEC's own extended instruction set in the 0Fh-prefixed opcode space, or implementing the 8080 instruction set used by NEC's 8080 emulation mode. The latter accounts for nearly 1/5 of the total ROM area.

Given that we know where instructions are, and we have a rough picture of the form of microcode words, we can start determining by deduction what the various values of the source and destination operands mean.  This is very similar to solving a crossword. You start at certain logical anchors - such as instructions that work with specific registers - note down guesses and assumptions as you work, and either validate or reject them. 

The first four values of the first source field turn out to be segment registers - ES, CS, SS and DS. The last eight values turn out to be AX, CX, DX, BX, SP, BP, SI and DI.

I started tracking my work using an Excel spreadsheet to decode the fields and perform lookups of various field values which I kept on additional sheets.

Decoding the V20 Microcode with an Excel Spreadsheet

The Main Decoding Effort

It was at this point that I decided I could use some help. reenigne was busy with his own decoding of the 80386 microcode, so I enlisted the help of the members of the Vintage Computer Federation forums.

The thread I posted resulted in a productive collaboration with veteran VCF user dreNorterR (who had previously decoded the 80186 microcode, and so had plenty of experience in such matters). I'm not going to recount the entire thread - if you're curious to see two nerds puzzle out the secrets of the V20, it's worth a read. You can see how various fields were reverse-engineered and how we bounced ideas off each other to put the puzzle pieces together. 

Unlocking the Group Decode ROM

Just staring at the microcode itself can leave one puzzled as to how certain instructions work, since the microcode itself in many cases doesn't have enough context to govern how the instructions must actually behave. 

On the 8088, a separate Group Decode ROM (GDR) PLA provides additional signals that are activated by different opcodes. The 8088 GDR emits 15 signals per opcode that provide the missing context for the microcode instructions. I recommend reading Ken Shirriff's excellent blog post on the 8088 Group Decode ROM.

Being intimately familiar with the 8088, I was sure that the V20 must also have a GDR. It wasn't difficult to spot, being a large block of PLA circuitry. Many of the signals the V20's GDR emits are identical in purpose to the 8088's signals, which made decoding it more or less straightforward.

The NEC V20's Group Decode ROM PLA

Decoding the first few lines of the GDR show us some familiar patterns.
01 111100?? 00100000001000 PREFIXES  f0,f1,f2,f3
01 1111010? 00100000001000 HLT,CMC   f4,f5
01 1111?0?? 00100000000100           f0,f1,f2,f3,f8,f9,fa,fb
01 1111??0? 00100000000010           f0,f1,f4,f5,f8,f9,fc,fd
01 1111?0?0 00100000000001           f0,f2,f8,fa
01 1111?100 00100000000001 HLT,STD   f4,fc
01 00001111 00100000000001 EXT PFX   0f
01 001??110 00100000000000           26,2e,36,3e
01 01100100 00100000000001 REPNC     64
01 0110010? 00100000001000 REPX      64,65
01 0100???? 00000000011100 INC/DEC   40,41,42,43,44,45,46,47,48,49,4a,4b,4c,4d,4e,4f
01 1111?11? 01000100011011 GRP       f6,f7,fe,ff
The first mask column match matches opcodes F0, F1, F2 and F3, all of which are instruction prefixes - later on we can see the 0F opcode extension explicitly matched as well. 

The True V20 Microcode Word Format


One interesting development occurred during the VCF forum collaboration - dreNorteR discovered microcode word formats that were not mentioned in the famous court documents.

As it turns out, the frequently reproduced diagram was incomplete.  The left side of the microcode word can take two forms, one of which encodes an inline constant value.  The right-hand side of the microcode word has four total forms, not three.

The final NEC V20 microcode word format

This division can be seen in the die photography quite clearly - 17 outputs of the microcode ROM exit the ROM in one direction, with the remaining lines exiting in the opposite direction, so clearly they had different functional divisions.

Fields like 'F', 'W', and 'E', which were left unexplained in the old diagram now have known meanings. It might have been reasonable to assume 'F' was "Update Flags" in correlation with the 8088's F field, but it actually has an entirely different meaning - Fetch.

V30

NEC had an advantage over Intel that allowed them to make a rather clever optimization. The NEC V20, like the 8088, has an 8-bit bus.  The corresponding chip to the fully 16-bit 8086 is NEC's V30 CPU.

Intel designed the 8086 first, and the 8088 was derived later. This meant that the 8088 and 8086 could not be designed with the same microcode mask. NEC was able to design the V20 and the V30 at the same time, and could simply switch two lines of microcode using the metal layer that fit on top of the main microcode ROM.

In the image below, within the indicated circle, one side or arm of the metal structure was cut depending on the CPU type being fabricated. This meant both CPUs could share the same microcode mask. This also explains the discrepancy in microcode word count originally noticed!

 
The metal layer V20 vs V30 switch

If we zoom in, you can clearly see that a trace on the left side is cut on this V20 die where it connects to the thick post at top center.  On a V30, the opposite side would be cut instead.

A zoomed-in view of the V20/V30 selection circuitry

Work Still To Do

The V20's microcode has not been 100% decoded - there are still some unidentified source values to puzzle out. There's enough of it decoded that I feel confident in beginning work on a microcode-based implementation of the NEC V20 CPU in my emulator, MartyPC.  I have a hunch that the remaining mysteries will reveal themselves when we're faced with using what we know to replicate actual CPU behavior against my hardware-generated NEC V20 test suite.

Artifacts

All the resources used for decoding and my latest microcode spreadsheet are available on GitHub here.









Comments

Popular posts from this blog

Executable Emoji

60fps Video on a CGA? - The GlyphBlaster

PC Floppy Copy Protection: Vault Prolok