8-Bit "VGA" video graphics cards for retro computing

Back to Home page

Card B


Design files and associated documents for download:

Card B version 2 schematic diagram in PDF format
Card B version 2 PCB Gerber files in ZIP format

Introduction

Three individual video card designs, built out of 74-series derivative CMOS logic. No CPLDs or FPGAs! The cards are specifically intended to be used with old-school computer systems (Z80, 6502, et al.) and have 5V TTL/CMOS, 8-bit microprocessor-compatible control interfaces.

This page is currently under construction and serves as a scrapbook page as I develop, test and verify these designs. So far I have only fully completed card B, which is pictured here displaying the pretty Mandelbrot. The schematic diagram and the zipped Gerber files for this card can be downloaded from the links provided above.

I'm currently waiting on the boards for Card C to arrive from the PCB manufacturer. Card A is still in the PCB layout phase. Comprehensive documentation / technical details / applications information for all three cards will eventually be produced/provided and collated into a single document.


Things don't always go to plan. This is the 1st iteration of card b. I ended up making some improvements to the way the !READ and !WRITE control lines are handled, which wasn't initially as elegant as it should have been and added too much unnecessary gate propagation delay to !WRITE, which can cause memory write issues in high clock speed WDC W65C02S systems. Card B is now at version 2 and I am now satisfied with the design. It's a good thing that prototype PCBs from JLC are cheap as chips! If you're wondering what's up with U18 (bottom right of PCB), I accidentally ordered the 5.3mm wide SO package version instead of the 7.5mm, so I had to do a little pin-extension soldering on one side.



As I type this I have yet to build planned interface units to control these video cards via the "tube" port of my BBC Micro and the Expansion Interface port of my TRS-80 Model 1, amongst others, but so far I have put Card B under the command of a crusty old PIC16F877A 8-bit microcontroller (40-pin DIP) wired up on veroboard to produce the following demonstration mathematical plots and screen graphics.


Here is the complete program. You can derive the wiring diagram from the function WriteRam(). The program is written in C and it consumed 74% of the '877's ROM.
Please note that this is rather crappy code written quickly just for testing purposes; the line-drawing algorithm is poorly implemented with floating-point arithmetic when it should be integer-only.

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CCS C Compiler
// Demonstration mathematical and graphics drawing program for VGA card.
// Initial prototype code
// 21/04/2021
// Card B - 640x480x64C
// www.glensstuff.com
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#include <16f877A.h>
#include <MATH.H>                                                    // For sin and cos              
#FUSES HS,NOWDT,PUT,NOPROTECT,NOLVP,NOCPD,NOWRT,BROWNOUT
#use delay(clock=20000000)
#use fast_io(A)
#use fast_io(B)
#use fast_io(C)
#use fast_io(D)
#use fast_io(E)

#define xoffset   320;                                               // Screen center x
#define yoffset   240;                                               // Screen center y

int   C, D, E;                                                       // Bytes for ClearRAM routine  
int32 address;                                                       // Video memory address
int8  colour;                                                        // Video memory data 
int8  start;
int16 iterations;
int16 x1, y1, x2, y2;                                                // DrawLine and DrawFill coordinates
int16 xx, yy;                                                        // Working variables for line algorithm
int32 x, y;                                                          //    "                                       
float deltaX, deltaY, deltaError, error;                             //    "        
float xxx, yyy, zzz, dx, dy, dz, radians, r, n;                      // Working variables for trig. and integration

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
                                             
void WriteRam()                                                      // Routine to write pixel data to video RAM
{  
   address = (x + 1) + (y * 1024);                                   // Calculate RAM address   
   output_C(make8(address, 0));                                      // Update address bus
   output_D(make8(address, 1));                                      //    "
   output_E(make8(address, 2));                                      //    "
 
   while (!input(PIN_A4)); while (input(PIN_A4));                    // Wait for negative edge of !H_BLANK
   output_B(colour);                                                 // Put pixel data on data bus     
   output_low(pin_A0);                                               // Assert !ACCESS_RAM
   output_low(pin_A1); output_high(pin_A1);                          // Strobe !WRITE                                                
   output_high(pin_A0);                                              // Un-assert !ACCESS_RAM
}

void QuadrantA()                                                     // Bresenham's line algorithm for x2 >= x1 & y2 >= y1          
{                
   deltaX = (x2 - x1); deltaY = (y2 - y1);                            
   if (deltaX >= deltaY)   {
      deltaError = (deltaY / deltaX); error = 0; y = y1;      
      for (x = x1; x <= x2; x++) {
         WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            y++; error = error - 1;
         }
      }
   }
   else
   {
      deltaError = (deltaX / deltaY); error = 0; x = x1;
      for (y = y1; y <= y2; y++) {
         WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            x++; error = error - 1;
         }
      }         
   }   
}

void QuadrantB()                                                     // Bresenham's line algorithm for x2 < x1 & y2 >= y1
{               
   deltaX = (x1 - x2); deltaY = (y2 - y1);                             
   if (deltaX >= deltaY)   {
      deltaError = (deltaY / deltaX); error = 0; y = y1;
      for (xx = (x1 + 1); xx > x2; xx--)  {
         x = (xx - 1); WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            y++; error = error - 1;
         }
      }
   }
   else
   {
      deltaError = (deltaX / deltaY); error = 0; x = x1;
      for (y = y1; y <= y2; y++) {
         WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            x--; error = error - 1;
         }
      }         
   }   
}

void QuadrantC()                                                     // Bresenham's line algorithm for x2 < x1 & y2 < y1 
{               
   deltaX = (x1 - x2); deltaY = (y1 - y2);                            
   if (deltaX >= deltaY)   {
      deltaError = (deltaY / deltaX); error = 0; y = y1;
      for (xx = (x1 + 1); xx > x2; xx--)  {
         x = (xx - 1); WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            y--; error = error - 1;
         }
      }
   }
   else
   {
      deltaError = (deltaX / deltaY); error = 0; x = x1; 
      for (yy = (y1 + 1); yy > y2; yy--)  {
         y = (yy - 1); WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            x--; error = error - 1;
         }
      }         
   }   
}

void QuadrantD()                                                     // Bresenham's line algorithm for x2 >= x1 & y2 < y1
{                
   deltaX = (x2 - x1); deltaY = (y1 - y2);                              
   if (deltaX >= deltaY)   {
      deltaError = (deltaY / deltaX); error = 0; y = y1;
      for (x = x1; x <= x2; x++) {
         WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            y--; error = error - 1;
         }
      }
   }
   else
   {
      deltaError = (deltaX / deltaY); error = 0; x = x1;
      for (yy = (y1 + 1); yy > y2; yy--)  {
         y = (yy - 1); WriteRAM(); error = error + deltaError;
         if (error >= 0.5) {
            x++; error = error - 1;
         }
      }         
   }   
}

void DrawLine()                                             // Routine for plotting lines from start point (x1, y1) to 
{                                                           // end point (x2, y2)
   if (x2 >= x1)  {
      if (y2 >= y1)
      QuadrantA();
   
      if (y2 < y1)
      QuadrantD();
   }        
          
   if (x2 < x1)   {
      if (y2 >= y1)
      QuadrantB();
      
      if (y2 < y1)
      QuadrantC();       
   }
 
   x1 = x;  y1 = y;                                         // Set start point of the next line to the end point of line 
}                                                           // just plotted if x1 and y1 not redefined on the next call

void DrawFill()                                             // Routine for drawing a rectangular fill.  
{                                                           // (x1, y1) = upper left corner. (x2, y2) = lower left corner
   for (y = y1; y <= y2; y++) {   
      for (x = x1; x <= x2; x++) {
         WriteRAM();
      }
   }
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void ClearRAM()                                                         // Fast routine for clearing video memory
{                                                                       // Does not wait for !H_SYNC

   C = 0; D = 0; E = 0; output_C(C); output_D(D); output_E(E);          // Zero address bus
   output_B(0);                                                         // Data bus = 0x00 for black  
   output_low(pin_A0);                                                  // Assert !ACCESS_RAM
                                                                        // Video is hardware blanked when !ACCESS_RAM low 
   while (E < 8)  {
      output_low(pin_A1); output_high(pin_A1);                          // Strobe !WRITE 
      
      C++; output_C(C);                                                 // Increment through and update address      
      if (C == 0) {                                                     //    "
         D++; output_D(D);                                              //    "                    
         if (D == 0)   {                                                //    "
            E++; output_E(E);                                           //    "
         }                                                              
      }                                                                                             
   }                                                                    
   output_high(pin_A0);                                                 // Un-assert !ACCESS_RAM
}

void DrawPolarRoses()                                                   // Iterative overlaying polar rose plots for the 
{                                                                       // form r = a sin(n(angle)), stepping n
   start = 0; colour = 53;
   for (n = 1; n <= 6; n = n + 0.5) {
      for (radians = 0; radians <= 6.2832; radians = radians + 0.05) {  // Step angle, range 0 through 360 (2pi) degrees 
         r = 200 * sin(n * radians);                                    // Calculate r for polar rose function    
      
         x2 = (sin(radians) * r) + xoffset;                             // Polar coordinates to Cartesian coordinates, x 
         y2 = (cos(radians) * r) + yoffset;                             // Polar coordinates to Cartesian coordinates, y
   
         if (start == 0)   {
            start = 1; x1 = x2; y1 = y2;                                // Set initial starting coordinate for DrawLine                          
         }
         else   
            DrawLine(); 
      }
      colour++;                                                         // Plot next rose in next colour of palette
   }
}

void DrawLinePattern()
{
   colour = 0x0B; x2 = 0; y2 = 0;
   while (x2 < 639)  {
      x1 = 0; y1 = 479; DrawLine(); x2 = x2 + 20;
   }   
   colour = 0x0C;                                        
   While (y2 < 479)  {
      x1 = 0; y1 = 479; DrawLine(); y2 = y2 + 20;
   }  
   x1 = 0; y1 = 479; y2 = 479; DrawLine();
   colour = 0x3C; x2 = 639; y2 = 0; start = 0;                                        
   while (start <= 31)  {
      x1 = 639; y1 = 479; DrawLine(); x2 = x2 -20; start++;
   }  
   colour = 0x0F; x2 = 0;                                          
   while (y2 <479)   {
      x1 = 639; y1 = 479; DrawLine(); y2 = y2 + 20;
   }  
   x1 = 639; y1 = 479; y2 = 479; DrawLine();
}
    
void DrawCascadingSquares()
{  
   colour = 0; x1 = 0;     y1 = 0;
   while (colour <= 63) {
      x2 = x1 + 72;  y2 = y1 + 38;  DrawFill();
      x1 = x1 + 9;   y1 = y1 + 7;   colour++;   
   }
   colour = 0; x1 = 567;   y1 = 0;
   while (colour <=63)  {
      x2 = x1 + 72;  y2 = y1 + 38;  DrawFill();
      x1 = x1 - 9;   y1 = y1 + 7;   colour++;
   }
}

void DrawChequerBoard()
{ 
   colour = 1;       
   for (x1 = 120; x1 <= 480; x1 = x1 + 40)   {
      x2 = x1 + 39; colour = 1 - colour;
      for (y1 = 40; y1 <= 400;   y1 = y1 + 40)  {
         y2 = y1 + 39;  DrawFill(); colour = 1 - colour;  
      }
   }
}

void DrawSinXdivXfunction()                                          // Iterative sin(x)/x plot with stepped variables in      
{                                                                    // isometric projection

   colour = 0x3F; start = 0;  dy = 1; dz = 10;                       // Plotting colour = white and initial conditions
   for (zzz = -100; zzz <= 100; zzz = zzz + 10) {                    // Step z
      start = 0;      
      for (radians = -5; radians <= 5; radians = radians + 0.1)   {  // Step radians
         xxx = (radians * 20);                                       // Compute x coordinate 
         yyy = sin(radians * dy);                                    // Compute y coordinate 
         yyy = -(dz * (yyy / (radians * dy)));                       //    "
     
         x2 = (xxx - zzz) + xoffset;                                 // Isometric projection transform, x axis, 45 degrees 
         y2 = (yyy - (xxx + zzz)) + yoffset;                         // Isometric projection transform, y axis, 45 degrees 
                                                                     // Sin(45) = cos(45) so trig. omitted
         
         if (start == 0)   {
            start = 1; x1 = x2; y1 = y2;                             // Set initial starting coordinate for DrawLine                          
         }
         else   
            DrawLine();                                              
      }
      dy = dy + 0.2;                               // Linear step increase of ripple multiplier for next iteration
      dz = dz * 1.12;                              // Exponential step increase of amplitude multiplier for next iteration 
   }
}

void DrawLandscape()
{
   colour = 0x10; x1 = 0; y1 = 0;   x2 = 639; y2 = 119; DrawFill();     // Draw blue and green screen fill 
   colour = 0x20; x1 = 0; y1 = 120; x2 = 639; y2 = 239; DrawFill();     //    "
   colour = 0x30; x1 = 0; y1 = 240; x2 = 639; y2 = 359; DrawFill();     //    "
   colour = 0x04; x1 = 0; y1 = 360; x2 = 639; y2 = 399; DrawFill();     //    "
   colour = 0x08; x1 = 0; y1 = 400; x2 = 639; y2 = 439; DrawFill();     //    "
   colour = 0x0C; x1 = 0; y1 = 439; x2 = 639; y2 = 479; DrawFill();     //    "
   colour = 0x03;                                                       // Draw red border
   x1 = 0;     y1 = 0;     x2 = 639;   y2 = 3;     DrawFill();          //    "
   x1 = 636;   y1 = 0;     x2 = 639;   y2 = 479;   DrawFill();          //    "
   x1 = 0;     y1 = 476;   x2 = 639;   y2 = 479;   DrawFill();          //    "
   x1 = 0;     y1 = 0;     x2 = 3;     y2 = 479;   DrawFill();          //    "
   colour = 0x0F;                                                       // Draw sun
   for (radians = 0; radians <= 6.28; radians = radians + 0.05)   {     //    "
      x1 = 100; y1 = 100;                                               //    "                                    
      x2 = 100 + (50 * sin(radians)); y2 = 100 + (50 * cos(radians));   //    "
      DrawLine();                                                       //    "
   }
}

void ComputeAndPlotLorenzAttractor()
{
   iterations = 0; start = 0; colour = 0x3F;   
   xxx = 1; yyy = 0; zzz = 20;                                          // Initial conditions
                                                                      
   while (iterations < 7000)   {                                           
      dx = (-10 * xxx) + (10 * yyy);                                    // Compute x
      dy = (28 * xxx) - yyy - (xxx * zzz);                              // Compute y
      dz = (-2.67 * zzz) + (xxx * yyy);                                 // Compute z   
 
      xxx = xxx + (dx * 0.01);                                          // Integration step x
      yyy = yyy + (dy * 0.01);                                          // Integration step y
      zzz = zzz + (dz * 0.01);                                          // Integration step z
   
      x2 = (xxx * 12) + xoffset;                                        // Line plotting coordinate x
      y2 = (-zzz * 8) + 450;                                            // Line plotting coordinate y
   
      if (start == 0)   {
         start = 1; x1 = x2; y1 = y2;                                   // Set initial starting coordinate for DrawLine
      }
      else  {   
         DrawLine(); 
         iterations++;
      }
   }
}

void WipeOut()
{
   x1 = 0; y1 = 0; x2 = 639; y2 = 479; colour = 0x00;
   DrawFill();
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void MAIN()
{
   delay_ms(3000);
   set_tris_A(0x10); set_tris_B(0xC0); set_tris_C(0x00);                // Setup IO ports
   set_tris_D(0x00); set_tris_E(0x00);                                  //    "  
   output_high(pin_A0); output_high(pin_A1); output_high(pin_A2);       // !ACCESS_RAM, !WRITE & !READ = high                 
                                   
   ClearRAM();    
   DrawPolarRoses();
   DrawLinePattern();
   DrawCascadingSquares();
   DrawChequerBoard();
   DrawSinXdivXfunction();
   delay_ms(1000);
   DrawLandscape();
   ComputeAndPlotLorenzAttractor();
   delay_ms(2000);
   WipeOut();
}

Here is a teaser photo of Card C displaying a 320 x 240 pixel bitmap image. The wide-screen monitor is in 4:3 mode.