Introduction
I know, I know. I have said that this machine is going to be simple. And simple means no video. But, what if?
This is that page. Lets investigate the consequences!
Sharing Memory
First off, if we have video output of any sorts, that means the video circuit needs to read memory. The CPU also needs to read memory. They need to share the available bandwidth. With core memory, running at 2MHz, each access takes 500ns, but reads and writes come in pairs. So really an access takes 1us. Our memory is 16 bits wide, which leads to a 2MByte/s memory bandwidth. This is what we need to share.
Let’s look at the CPU timing diagram:
The CPU normally takes 5 clock cycles (phases) per instruction. Six for SWAP/ISWAP. To unify the naming, a normal instruction has phase 0,1,2,4,5 while SWAP and ISWAP has 0,1,2,3,4,5. That is, phase 3 is skipped for all but SWAP/ISWAP instructions.
Of these phases, the CPU does a read in phase 0 and 2 and a write in phase 1 and 5. (The diagram doesn’t quite say this, it states that phase 4 can be a write as well, but that’s not true with the implementation I have at the moment. It certainly doesn’t have to be true.)
This leaves phases 3 and 4 without memory accesses. If we want to share memory between the CPU and the screen, we would need a reliable bandwidth for both. So, we could either say that the screen gets one phase (phase 4) to do its thing, or we equalize instruction execution time to constant 6 clocks, and the screen gets phases 3 and 4 to get to the memory.
I will chose the later, we’ll see why in a second. So, one of the important changes is as follows:
The CPU needs to change to include phase 3 in every instruction.
This is easy to do, in fact would probably simplify the control state machine.
With that change, the CPU gets 2/3rd of the memory bandwidth, the screen gets the rest: 1/3rd. In absolute terms, that’s 0.66MByte/s of memory bandwidth.
Choosing a video standard
What is that memory bandwidth enough for? Remember, we are building this out of discrete transistors so flops, even latches are really expensive. We can’t afford any FIFO buffers or other such structures to equalize memory access patterns, which means that the pixel clock rate must be less than what this 0.66MBps memory bandwidth can support. If we used only one bit per pixel (black and white image), this corresponds to a pixel clock rate of 5.33MHz. According to the TinyVGA page, the slowest pixel clock rate for VGA is 25MHz. That’s of course for 640 horizontal resolution, if we cut that in half (320 pixels), we get 12.5MHz. Still way too high. We need something slower, and there is something slower: old TV standards. Let’s look at them!
NTSC timing
NTSC (PAL too but I’m going to work with NTSC, because ‘Murika), being an analog standard doesn’t have a definite pixel clock rate. Instead, it has a horizontal sync rate and an active period for the signal. Finding accurate information on this is surprisingly hard, but I’m going to heavily rely on the following two documents:
- https://www.technicalaudio.com/pdf/Grass_Valley/Grass_Valley_NTSC_Studio_Timing.pdf
- https://pub.smpte.org/pub/st170/st0170-2004_stable2010.pdf
From these, the line-rate is: 63.5us, this is from sync to sync. The active period is: 52.6us.
With our 5.33MHz pixel clock rate, we can fit 280 pixels into the active period. If we include some border on each side, that would work out to be a 256 pixel horizontal resolution.
NTSC has a total of 525 scan lines, of which 485 are active. That is split in half due to interlace, so really we deal with 242.5 (yes, half a line) active lines per field. The field rate is 60Hz. Now, if we wanted to have roughly the same amount of horizontal and vertical border, we would end up with 220 lines in our display and a final resolution of 256×220.
Such a screen would occupy 7040 bytes (6.875kBytes) of the 32kByte we have available. Not a small fraction, but maybe something we can live with.
To make a TV set happy, we would need to generate a standards compliant video signal. This includes:
Horizontal blanking: 10.9us +/-0.2us: this includes front-porch, back-porch, color-burst and sync pulse
For NTSC there are 485 active lines, a total of 525 lines, with a refresh rate of ~60Hz.
The vertical sync is:
Field-one:
3 lines of black (with middle-sync)
3 lines of v-sync (with inverted sync both in the end and the middle)
3 lines of black (with middle-sync)
11 lines of blank (with normal sync)
Field-two has
3.5 lines of black (with middle-sync)
3 lines of v-sync (with inverted sync both in the end and the middle)
2.5 lines of black (with middle-sync)
11 lines of blank (with normal sync)
this ignores that the last line and the first line has only half-line info. I assume black there already
Clocks
As we’ve said, the pixel clock rate is 5.33MHz, but our system clock is 2MHz. This 2MHz clock is generated from an 8MHz one due to timing reasons (for the core memory), but how do we get 5.333MHz out of these?
This is not terribly difficult if we use both edges of our 8MHz clock:
We would also need to synchronize the CPU state-machine to the 5.33MHz clock somehow, but the clock themselves seem to be relatively easy to come by.
Horizontal timing
With the 5.333MHz clock, timing for the rest of the NTSC signals comes out as follows:
Front-porch: 8 cycles
HSync: 25.066 cycles
Back-porch: 25.066 cycles
Active portion: 280.5333 cycles
After rounding, this becomes:
Front-porch: 8 cycles
HSync: 25 cycles
Back-porch: 25 cycles
Active portion: 280 cycles
Which is 0.666 cycles short over 338 clock cycles. Or, the proper crystal rate would be 7.98425196851965MHz. That delta is 2000ppm, so maybe relevant? Probably no
To generate this timing, we need to count. And counters are really really expensive. So, I will settle for just two counters: one for the horizontal timing and one for the vertical. All the synchronization events will be derived from these two counters by (fixed) comparators. We won’t have any programmability, the timing is going to fixed to the NTSC standard. To simplify the comparison further, I will have to make sure that all timing events are divisible by 8. After that, the events become even more quantized:
Front-porch: 8 cycles
HSync: 24 cycles
Back-porch: 24 cycles
Front blank: 10 cycles
Active screen: 256 cycles <<- start counter here
Back blank: 16 cycles
This will make the active portion more aligned to the left, but all but the total count is divisible by 8.
So, we would have a counter, running at 5.333MHz, counting from 0 to 337. This is a 9-bit counter. The comparisons happen as follows:
TC comparator for 337 (sync reset)
Coarse trigger: (bottom 3 bits all 1)
Top-6-bit comparators:
31 – end of active
33 – end of back-blank
34 – end of front-porch
37 – end of hsync
40 – end of back-porch
For VSync we’ll need to add sync pulses in the center of the scan-line as well. These pulses are just as wide as normal syncs (so 3 counts, starting at count 14):
14 – start of VSync serration
17 – end of VSync serration
This seems to be large AND gate (31), and a 3-bit decoder with proper enables for all the rest. Some cleverness might be needed for the VSync pulses. We could also reset the horizontal counter in some form during VSync if that’s easier.
Vertical timing
For NTSC there are 485 active lines, a total of 525 lines, with a refresh rate of ~60Hz.
The vertical sync is:
Field-one:
3 lines of black (with middle-sync)
3 lines of v-sync (with inverted sync both in the end and the middle)
3 lines of black (with middle-sync)
11 lines of blank (with normal sync)
Field-two has:
3.5 lines of black (with middle-sync)
3 lines of v-sync (with inverted sync both in the end and the middle)
2.5 lines of black (with middle-sync)
11 lines of blank (with normal sync)
this ignores that the last line and the first line has only half-line info. I assume black there already
For 525 scan-lines, one needs a 10-bit counter. The various events on this counter work out as follows:
– 200 active lines (we start the counter here)
– 31 v-blank
– 3 v-blank with middle-sync
– 3 v-sync with middle-sync
– 3 v-blank with middle-sync
– 11+31 v-blank
– 200 active lines (second field, repeat of first)
– 31 v-blank
– 3.5 v-blank with middle-sync
– 3 v-sync with middle-sync
– 2.5 v-blank with middle-sync
– 11+31 v-blank
We ‘tick’ the vertical counter every time we get an HSync from the horizontal engine. This means that the serrations are going to count as double scan-lines (and go by twice as fast). With that adjustment we get:
– 200 active lines (we start the counter here)
– 31 v-blank
– 6 v-blank with middle-sync
– 6 v-sync with middle-sync
– 6 v-blank with middle-sync
– 11+31 v-blank
– 200 active lines (second field, repeat of first)
– 31 v-blank
– 7 v-blank with middle-sync
– 3 v-sync with middle-sync
– 5 v-blank with middle-sync
– 11+31 v-blank
We will also do another trick: we have a single-bit field-identifier, which determines if we’re in the first or the second field in the above, i.e. the comparison values for two of the timing values (adding and subtracting one each). This allows us to reset the counter for every field and – as a consequence – use the bits form the vertical counter as part of the memory address generation (bottom 7 bits coming from the horizontal counter), so with that:
– 200 active lines (we start the counter here)
– 31 v-blank
– 6 or 7 v-blank with middle-sync
– 6 v-sync with middle-sync
– 6 or 5 v-blank with middle-sync
– 11+31 v-blank
In binary values:
– 0b0_1100_0111 : end of active lines (at count 199 to allow for sync reset)
– 0b0_1110_0110 : end of v-blank
– 0b0_1110_110? : end of v-blank with middle-sync. ? is zero for even, one for odd fields
– 0b0_1111_001? : end of v-sync with middle-sync. ? is zero for even, one for odd fields
– 0b0_1111_1000 : end of v-blank with middle-sync. This is where we re-align, so no ? needed anymore (thank God)
– 0b1_0000_1110 : end of field: reset counter and flip field-bit
We need to decode 6 values, I think it’s a custom ’74ls139′-style thing, but we can rely on the counter providing both inverted and non-inverted values so, really it’s just a bunch of AND gates.
This creates the timing generator.
Video logic
The address is generated from bits 7-4 (inclusive) of the horizontal and bits 7-0 of the vertical counters. This is a 12-bit address, so we’re mocking around in a 4kWord space. The top 4 address bits are statically generated by a latch (video RAM base address register). Every instruction has 2 cycles worth of time to read-refresh the addressed word from core memory. This is fed into a 16-bit shift-register, which is shifted out using our 5.333MHz pixel clock. The shifted data is AND-ed with the blanking signal and then combined with the porch and sync signals to generate the proper analog video signal. And, well, basically that’s it.
Overall, except for the annoyance of the large counters (a 10-bit and a 9-bit one) and a 16-bit shift register, this really isn’t that bad.
Digital Winter
Well, snow, at least. This happens in old CGA displays because of the CPU and the video engine sharing the memory bus without proper lock-out. We don’t have that problem in the Disintegrated machine.We have something else to worry about.
Look back at the timing diagram at the top! The CPU reads the memory in phase 2 and writes it in phase 5. This means that – with core memory reads being destructive – that during phase 3 and 4, the memory location the CPU is accessing contains all 0-s. Not the old value, not the new value, 0-s. If this location happens to be the same one that the video output reads in phase 3, it would read – again, neither the old nor the new value – but 0. Or 16 consecutive black pixels. Every time such a collision happens, we would see a momentary display corruption. Black snow, if you wish.
How to prevent it? Well, the obvious thing is not to touch the video memory in inopportune times. Do screen updates during horizontal or vertical blanking/retrace periods or to regions of the screen that are far away from the ‘ray’. To facilitate this, we need to expose the state of the video logic to SW. It’s probably too granular to do anything within a scan line, so we need to know only the current scan-line and field index. This can be done using a single, read-only I/O location.
On top of this, generating an interrupt every time (if enabled) the active portion of the screen is drawn (after scan-line 200) is useful. This not only makes it easy to time screen updates, but gives us a periodic interrupt for other functions, such as context switches (with our fancy new MMU). To do this, we need an interrupt enable bit (this can be written in the same I/O location), as well as a sticky interrupt-pending bit, that is set whenever an interrupt is requested and cleared when the I/O location is written.
Conclusion
This circuit doesn’t seem bad in terms of complexity. I’m still not convinced it’s worth the bother, but I don’t want to build a machine that never allows for an extension of this sort. In particular, what needs to happen in the CPU and memory system to enable video output in the future?
The two important things is that the CPU needs to run in 6-cycle mode; this can be done by a jumper setting or something. We also need a way to ‘steal’ the bus from the CPU in cycle 3 and 4. So, the CPU needs to expose the phase it’s in. The bus-stealing can be done externally using muxes. We need to do them anyway, including them in the CPU is a bit premature.
The memory doesn’t seem to need any modifications.
