This is not strictly a 6502 circuit, though it is designed to work with my 6502 computers - however I thought it might be of interest anyway, and I apologise if it's off-topic or otherwise not interesting.
As I mentioned in another thread, I wanted some basic VGA output for one of my computers, and wanted to build something much simpler than my past video circuits which have all been graphical. This is what I came up with, using some GAL SPLDs (ATF16V8) to reduce the chip count:
Attachment:
vgatextschematic.png [ 114.52 KiB | Viewed 2492 times ]
The circuit has three ATF16V8 PLDs, two 74HCT590 8-bit tristate counters, three 74AHC574 8-bit tristate registers, a 71256-SA12 32K SRAM module of which 4K gets used, an AT28C256 32K EEPROM module of which 4K gets used, a 74HCT166 parallel-in serial-out shift register, and the VGA oscillator (25.175MHz). It fits on two rows of breadboard:
Attachment:
20231223_133909.jpg [ 698.9 KiB | Viewed 2490 times ]
It is black and white only, with no support for character attributes, but I may add this later. The font is one of the standard VGA fonts, 8x16, in a 640x480 resolution mode - this existed on PCs but was not so common, they usually used 9x16 characters in a 720x400 resolution mode instead. So we get 80x30 characters on the screen:
Attachment:
20231222_061117.jpg [ 1.23 MiB | Viewed 2490 times ]
Regarding its operation, I'm not sure how much detail to go into, but I'll write what comes to mind and do ask if anything is unclear.
At a high level it reads a byte from RAM once per eight VGA pixels, and shifts it out through the shift register. Hence it does this at an eighth of the pixel clock frequency, which is about 3MHz. The RAM is plenty fast enough to do write operations in between these reads, when they are necessary. Unlike my previous circuits, this one is not tied no the same clock as the CPU - I let them run on separate clocks. Any writes from the CPU are captured in storage registers, forming a 1-deep FIFO, and processed by the video circuit when the time is right. It's up to the software code to not write more frequently than the circuit can support, but this would actually be quite hard to do with a 6502, you'd need a very fast clock speed and a very simple data stream.
The "control" PLD controls the timing signals for the various components, within each block of eight horizontal pixels - it tells the counters and RAM when they can output to the buses, tells the shift register when it should load the next 8 bits of data, and watches out for writes from the computer and, if one is pending, it arranges for the storage registers to output to the buses instead of the counters, and sends the RAM a write-enable pulse at the right time.
The "horizontal" and "vertical" PLDs work in tandem with one of the counters each, to generate the sync signals for the monitor, as well as the reset signals for the counters and an "on" signal which shows when the monitor is not in a blanking period, hence we should output visible pixel data.
In more detail, here's the code for the control PLD:
Code:
Name vgatextctl ;
PartNo 17.00.A ;
Date 21/12/2023 ;
Revision 00 ;
Designer George Foot ;
Company gfoot360 ;
Assembly None ;
Location None ;
Device g16v8 ;
pin 1 = CLK;
/* inputs */
pin 2 = CLK1;
pin 3 = !WR;
pin [4..8] = [ ENA4..1, !ENA0 ];
/* outputs */
pin 12 = C0; /* clk/2, HCPR */
pin 13 = C1; /* clk/4 */
pin 14 = C2; /* clk/8, HCPC, /COE */
pin 15 = !XOE; /* OE for write ops */
pin 16 = !RAMWE; /* RAM's /WE */
pin 17 = !SRPE; /* shift reg parallel enable */
pin 18 = WP; /* write pending */
pin 19 = PREVWR; /* WR from previous cycle */
nC0 = !C0;
nC1 = C0 & !C1 # !C0 & C1;
nC2 = C0 & C1 & !C2 # !C0 & C2 # !C1 & C2;
Field C = [ C2..0 ];
Field nC = [ nC2..0 ];
C.d = nC;
XOE.d = WP & nC:5 # XOE & nC:[4..7];
RAMWE.d = XOE & nC:6;
SRPE = !CLK1 & C:2 # CLK1 & C:3;
Field ENA = [ENA4..0];
PREVWR.d = WR & ENA:&;
WP.d = WP & !(C:7 & XOE) # PREVWR & !WR;
The control PLD counts up from 0 to 7, based on the VGA pixel clock, using macrocells C0,C1,C2 - grouped together as field C. The temporary field nC is used to track the "next" value for C, as it can be useful to reuse this in other registered pin calculations as often their states need to correspond to the next value, rather than the current value. This is just a nicety in the source file though, it doesn't use any hardware resources.
The high bit of this count, C2, forms a "counter output enable" signal (COE) which allows the counters - and RAM - to output their data. Thus the counters drive the V and H buses, the and RAM looks up the data there and outputs it to the video data (VD) bus. The ROM picks this up along with the bottom four bits of the V bus to look up a row of data from the relevant character definition, and provides this to the shift register. The shift register's parallel-enable pin SRPE is pulled low for one tick out of every eight, by the control PLD. Originally this was at the end of COE's low phase, but I moved it back a tick or two due to artifacts appearing. SRPE is not registered because it needs to straddle a rising edge of the pixel clock - I want to set it halfway through a clock period, and clear it halfway through the next one.
During write operations, the control PLD spots that a write is occuring based on the input WE signal being asserted along with all five enable inputs being asserted. This state is latched until the eight-pixel cycle gets to a point where the write can take place. When this is reached, the storage registers' output-enable (XOE) is activated by the control PLD, so that they write to the video address and data buses. It used to do this immediately after COE was unasserted, but I changed it to leave a one clock gap with nothing driving the buses. One clock cycle later, the control PLD activates the RAM's write-enable signal for a further clock cycle, and then the "write pending" signal is cleared. This activation of XOE and RAMWE only happens during cycles where there is data to be written.
To illustrate how the timing for reads and writes work together, here is the simulator script to test the PLD:
Code:
ORDER: CLK, CLK1, %1, WR, %1, ENA, %2, C, %1, PREVWR, WP, %2, XOE, RAMWE, %1, SRPE;
VECTORS:
$msg "Read";
P0 0 11111 '0' 0 0 11 0
00 0 11111 "0" L L LL L
C1 0 11111 "1" L L LL L
C1 0 11111 "2" L L LL L
00 0 11111 "2" L L LL H
C1 0 11111 "3" L L LL H
00 0 11111 "3" L L LL L
C1 0 11111 "4" L L LL L
C1 0 11111 "5" L L LL L
C1 0 11111 "6" L L LL L
C1 0 11111 "7" L L LL L
C1 0 11111 "0" L L LL L
$msg "Write";
C1 1 11111 "1" H L LL L
C1 0 11111 "2" L H LL L
00 0 11111 "2" L H LL H
C1 0 11111 "3" L H LL H
00 0 11111 "3" L H LL L
C1 0 11111 "4" L H LL L
C1 0 11111 "5" L H HL L
C1 0 11111 "6" L H HH L
C1 0 11111 "7" L H HL L
C1 0 11111 "0" L L LL L
Moving on to the horizontal PLD:
Code:
Name vgatexthoriz ;
PartNo 17.00.B ;
Date 21/12/2023 ;
RevisiON 00 ;
Designer George Foot ;
Company gfoot360 ;
Assembly None ;
LocatiON None ;
Device g16v8 ;
pin 1 = CLK; /* C2 */
/* 74HCT590 8-bit counter, CPR=C2, CPC=!C2, MRC=!HR */
pin [2..8] = [H0..6];
pin 9 = VON;
/* 0 spare inputs */
pin 17 = !HR;
pin 18 = !ON;
pin 19 = !HSYNC;
/* 5 spare I/Os */
Field H = [H6..0];
HR.d = H:62;
ON.d = H:63 & VON # ON & !(H:4f);
HSYNC.d = H:51 # HSYNC & !(H:5d);
It is fairly straightforward - the external counter is providing a 7-bit count value, and based on various values for this count, we do different things. The 74HC590s have asynchronous reset, which is annoying; but they also have separate clock pins for counting and outputting the latest count. The async reset means we don't want the counter to be clocked at the same point as the PLD, because then we'd be transitioning the MR input to the counter at the same point that it's being clocked. So we drive the PLD from a different signal that's in sync with the counter's register clock instead. This means that outputs from the PLD and the counter change at roughly the same time. The counter internally counts up to the next value in the meantime but the outside world - including the PLD - doesn't see that until the CPR tick.
The upshot of this is that we want the counter to be internally, asynchronously reset when it has just published the final count value for the row, which is 99 ($63) for us. To do this, the PLD needs to make the decision to assert VR - the reset signal - when the published count is one less than that, i.e. $62. This was confusing to think about but made sense in the end.
The trigger value for the ON signal is, however, $63 because we want this to be activated on the cycle when the counter starts outputting zero - so we want to set it in the transition from the cycle where the counter was outputting $63, which is also the cycle when it will be reset (I could have used that instead of a specific value here). ON gets turned off when the horizontal count is about to tick over to 80 characters ($50) so the trigger value for that is $4F.
The trigger values for HSYNC are similarly one less than you might expect - they are the values the counter will show on the cycle before HSYNC should start or end.
The vertical PLD is similar to the horizontal one, but has to deal with much larger count values:
Code:
Name vgatextvert;
PartNo 17.00.C ;
Date 21/12/2023 ;
Revision 01 ;
Designer George Foot ;
Company gfoot360 ;
Assembly None ;
Location None ;
Device g16v8 ;
pin 1 = CLK; /* hsync */
/* 74HCT590 8-bit counter, e.g. CPR=hsync, CPC=!hsync, /MRC=!vr */
pin [2..9] = [V0..7];
/* 0 spare inputs */
pin 12 = !VR;
pin 13 = !VSYNC;
pin 14 = VON;
pin 15 = V8int;
pin 16 = V8;
pin 17 = !V8OE;
/* 2 spare I/Os */
Field V = [V7..0];
VR.d = V8int & V:de # !VON & V:2b;
!VON.d = VON & V8int & V:df # !VON & !(V:[2c..2f]);
V8int.d = VON & (V:ff # V8int);
VSYNC.d = !VON & V:[09..0a];
V8 = V8int;
V8.oe = V8OE;
There are 525 scanlines on the screen, but the counter can only count up to 255 before wrapping. To count up to 525, it needs to go through three phases:
- Phase 1 - scanlines 0-255 - VON is set, V8int is unset; but is set at the very end of this phase
- Phase 2 - scanlines 256-479 - VON is set, V8int is set; we count up to 223 and then reset the counter
- Phase 3 - scanlines 480-524 - VON is not set as this is the blanking period; we count up to 45 then reset the counter, also generating VSYNC in the right place
VON is output for the horziontal PLD to read, and blend with its own blanking signal. The blanking is done by disabling the character ROM, whose data bus has pull-down resistors, so the shift register outputs black from that point on.
V8int is for internal use, but we also need this signal externally. However it needs to be tristate as it's going on to the video address bus which is sometimes driven by the write circuit instead. This PLD doesn't allow us to individually set output-enable for a registered pin; however, it does allow us to do it for a combinational pin. So we copy the state from V8int into V8, and set that pin up to be controllable by an external signal.
We can't use the PLD's general output-enable pin for this purpose because there are other output signals on this PLD such as VSYNC which must be active all the time, not just during the read portion of each 8-pixel cycle.
---
Talking of output-enables, an important factor in this circuit is that the outputs from the 74HC590s are used to determine the next states for the PLDs, but these outputs are also not always enabled - they are only enabled when COE is low. This means that it's important for the horizontal and vertical PLDs' clock inputs to have their rising edges during that period. For the horizontal one that's straightforward enough, but for the vertical one it's less obvious because its clock signal comes from the horizontal one. As the horizontal one is clocked by the end of the COE signal, this means COE is not asserted when the vertical one is clocked. However I delayed the XOE signal by one clock tick so that, at this point, the bus would at least float, which is sufficient. I ought to go back and find a better way to do that really.
There is also a bug, it seems, with write operations where sometimes - maybe one it a hundred - they don't go through, or perhaps write the wrong data. I don't think they are writing the wrong addresses. It appears mostly worked-around by making the software execute write operations twice, which I guess reduces the chance to one in ten thousand that it fails! I haven't worked out what actually causes it yet though, there are several places it could be going wrong en route from the computer to the video RAM, and if the problem persists then I will look into it more.
You can also tell how much trouble I had with a circuit by how many passive components it has in it. There are quite a few decoupling capacitors in this one, and there's even one straddling across the shift register. This is because the shift register was giving me a lot of problems. I think this one is actually broken because if I connect its pin 1 (serial input) high then it seems to bleed this into the other data, even though it should be getting loaded before this value has time to propagate to the output; similarly if I connect it low, everything goes black. It is an input so I didn't want to leave it unconnected, and I found that connecting it high through a large resistor seemed to work.
I also had trouble with the clock edges, even though it is physically right next to the oscillator. I added parallel termination to the clock line at the shift register's clock input pin - I think this is 220 ohms, it is a value I've used before for this exact same purpose and it fixed a lot of flickering and sparkling around the text.
At some point I would like to add colour support, and perhaps get rid of the ROM and let the font definition live in RAM with the framebuffer. I have a scheme in mind to hit the RAM four times during the eight pixel cycle, to do this - once to read the attribute byte, once to read the character index, once to read the line of pixels to from the character, and potentially a fourth time to write new data into RAM. This would involve three different possible sources for address data for the RAM, rather than just two, but I think it should be fairly practical to achieve and maybe a cleaner solution than having a separate RAM IC for storing attribute data.
I also thought about some kind of hybrid text and graphics support, but haven't fleshed that idea out properly yet - I'd only want to do it if it can be done without too much complexity.
In any case it looks like the circuit as it stands should satisfy my immediate needs, so I don't know if/when I'd come back and make those changes.