Disassembling the Triad T2556: Video Controller

Part Eight in a Series on the Triad T2556

Alex King
8 min readJul 28, 2020
Photo by Florian Klauer on Unsplash

Right. So hopefully by now you read the previous intro and know a bit about Z80 assembler. It’ll make these next few articles a lot more easy if you have! It’ll also help if you’ve read the schematizing article that explains some of the hardware.

To make things more manageable, I’ve broken down the assembly listings into pages, and then subdivided each page into functional blocks.

Each of these blocks build on the last. So — for example — I’ll go into detail on performing operations on the input-output bus once, but will then assume that you know what I’m talking about and won’t go into as much detail next time.

You might need to refer back to earlier blocks if stuff doesn’t make sense, but I promise that it will eventually.

As always, if you have questions, comments, or have ideas on how this can be described better, then I’d love to hear from you.

The additional commentary that exist in the code listings have been stripped out of this version of the assembly code for brevity, but we still have labels to work with which should be enough.

Let’s start from location 00000h (called start here), which is where the processor will begin executing code after a reset or power-on.

BLOCK 1A: The first two instructions load register a(also known as the accumulator) with the value of 08h, and writes it out to the device called #THINGS. You can see that I did a global substitution for all of the device IDs to something that better aligns with what was labeled on the schematic. Just to make things a bit easier.

What does this actually do? It’s initializing some of the hardware! 08h in binary is 00001000, so it’s setting just one bit on that hardware latch. You can take a look back at the schematic to (kinda) see what this does.

BLOCK 1B: Here we are reading the state of the #DIP4 device and set the hl register pair to point to one of two addresses in ROM depending on whether a particular bit in the value is set or clear. In Python, the equivalent is something like this:

hl = VIDEO_CFG_HSP_44_start
if (in(DIP4) & 0x80) == 0:
hl = VIDEO_CFG_HSP_4F_start

It’s obviously not quite as neat as what we’d do in a high level language, but this is old timey assembler when instructions were a bit more limited.

BLOCK 1C: Before we get too far into the code, we should take a step back and refresh our memory on some of the hardware, since the firstout instruction here implies that this block has something to do with writing to the video controller.

From the schematic, we know that address line A0 connects to the Register Select pin on the controller. The datasheet says this about that line:

Register Select (RS) — The RS line is. a high-impedance TTL MOS compatible input which selects either the address register (RS = 0) or one of the data register (RS = 1) or the internal register file.

From looking at the schematic, we can see that #VIDEO ADDR is 016h, and #VIDEO DATA is 017h. Easy.

Digging into the datasheet a bit more, this device is programmed by writing the address of the register we want to access to #VIDEO ADDR and then either reading or writing #VIDEO DATA appropriately. This is what the datasheet has to say about the registers:

Okay, let’s get back to the code. As we begin the instruction that is labeled configure_video_loop, there are a few registers initialized for us:

  • Register a is initialized to 0;
  • Register c is initialized to 017h;
  • Register pair hl is now pointing to one of two possible locations in memory.

First we write the contents of a to #VIDEO ADDR, then we load register b with whatever memory address hl points to. This value is written to the I/O device pointed to by register c (017h), which we know to be #VIDEO DATA.

Now hl is incremented so that it points to the next location in memory, and register a is incremented as well.

Next, register a is compared to 010h and if it’s not then the loop is repeated.

So this block programs the first sixteen registers on the video controller, according to whatever is stored in the source data table!

Oh. Here are the two versions of the table to choose between:

Each of these defb instructions represents one byte of storage.

The only difference between the two is that the horizontal sync position as stored in register 2 will end up with either a value of 044h or 04fh (depending on that switch setting). In case you’re interested, this is what the datasheet has to say about register 2:

Horizontal Sync Position Register (R2) — This 8-bit write- only register controls the HS position. The horizontal sync position defines the horizontal sync delay (front porch) and the horizontal scan delay (back porch). When the programmed value of this register is increased, the display on the CRT screen is shifted to the left. When the programmed value is decreased the display is shifted to the right. Any 8-bit number may be programmed as long as the sum of the contents of R2 and R3 are less than the contents of RO. R2 must be greater than R1.

My guess is that one version of the configuration is designed to be used with 4x3 monitors, and the other for widescreen displays like the T2556.

Why does the code use out (c), b rather than out(017h), b? The truth is that although the Z80 has a lot of instructions, it doesn’t have an instruction for every permutation of each instruction’s parameters. You can see here that if we want to post the value of the b register with out, we have to use c as the target address.

If you like, you can go through the values that are assigned to each of the registers to see how the display gets configured.

BLOCK 1D: Now that the video controller is configured, the next thing to do will be to jump over the configuration data and run a test to see if it looks as though it took programming okay. That leads us to…

BLOCK 1E: Okay, so the video controller is supposed to be configured, let’s see if everything looks right. The first thing we do is grab the content of register 14 (that’s 0eh). Based on those two tables, it should be the same as what we wrote (00h). If it’s not, then jump into the failure code.

BLOCK 1F: Now assuming that the previous test worked, we’ll repeat that exercise but look at register 15. If the value is (again) the same as the one we configured, then… success! Go to the next step in configuration with code at video_crt_check_pass.

BLOCK 1G: This is sad panda time. At least one of the two registers didn’t return an expected value. If we can’t display anything to the screen, then the machine is more or less dead and has to be serviced.

To understand this code better, let’s take a look at the green bracketed section in block 1G. Here, b is initialized to 0f0h and then we run djnz l005fh. djnz is short for decrement and then jump if not zero, but what is it that is being decremented? A quick google search says that it’s decrementing the b register. I suppose I should have known, right?

This page on Z80 Heaven says that djnz takes 13 t-states to execute if the outcome is non-zero. We can think of a t-state as a clock cycle, so on a 4MHz cpu, that’s 13/4,000,000 of a second. The CPU will drop out of the loop after 0f0h (240 decimal) passes, which is (240*13)/4,000,000 or .78 milliseconds. We could count the extra 8 t-states that are required for the final zero-outcome, but in this case we don’t need to be super accurate.

Immediately after the loop terminates, we do a dec c to subtract one from the c register, and then — if c is still greater than zero — jump relative non-zero to the outside of the yellow loop. Scrolling up to the top of block 1G, we see that c is initialized to 0c8h, so we will navigate that little delay loop 200 (0c8h) times.

There are just a couple of other things going on in here. At the beginning of the block, a is initialized to 0a8h, and the first couple of instructions inside that outer loop are xor 010h and out (#THINGS),a. The xor will cause one bit to get flipped in the a register, and the out instruction will load this new value into the #THINGS device.

Because you’re observant, you already noticed that the #THINGS configuration changed from 08h when the program started to 0a8h as we start flipping that one bit. Why is that?

Time to recap.

We flip that bit on the #THINGS device, then we wait .78 milliseconds. This process is repeated 200 hundred times.

Looking at the schematic, it turns out that the bit on the things device is connected to the loudspeaker.

So this code is generating a short tone! 1/0.78ms is about 1280Hz… give or take a bit for the other machine cycles that we didn't include in our calculations.

BLOCK 1H: Gee. This looks similar, don’t you think? The only difference here is that b is initialized with 0ffh, so the delay between state changes will be longer and the pitch lower.

At the end of block 1H, we’ll follow that red brace back to video_crt_check_fail, and do so ad infinitum giving a two-tone alarm sound.

Finally…

You might wonder: why is this tone generation code repeated? Why not use a subroutine to avoid all of that repetition? I know that I did.

The answer is pretty simple. At this point in program execution, there is no stack. The stack will store the return address for the call, and without a stack, subroutines are more or less impossible. To get a stack set up, we will need to confirm that we have working RAM. That will happen next

--

--

Alex King

Software and Technology Nerd, DevOps Ninja, Maker of Things, Aerospace Enthusiast. https://orc.works/