Close

Official Contest Entry Log

A project log for PIC Graphics Demo

Generate 640x480 64-color VGA graphics with an 8-bit PIC and an SRAM framebuffer

Ted YapoTed Yapo 01/02/2017 at 16:2711 Comments

OK. I got pretty fractals onto a VGA monitor using 1019 bytes of code on a PIC16F1718. Unless I get something better done by Thursday, I'm going to consider this my contest entry. I let this one run overnight - when I turned on the monitor this morning, here's what I saw:

The straight C-code takes up 582 14-bit instructions, which is equivalent to 582 * 14 / 8 = 1018.5 bytes.

The code just fit - actually, as originally written, it was one instruction over. I had to inline the SetupPeriperhals() call to shave off four instructions. Note that this code is compiled with the free Microchip XC8 compiler. The free version tells me that the code could be 232 words smaller if I used the Pro version - you have to wonder how it knows :-)

Yes, writing the whole thing in C is lazy. Perhaps I can redeem myself in the next few days. I did really want to do a hardware project, though. I'll draw up a schematic for the board as-built today for completeness.

Here's the code. I'll upload it as a file, too.

//
// vga_test.c - create first VGA frame
//
#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <pic16f1718.h>

// CONFIG1
#pragma config FOSC = INTOSC
#pragma config WDTE = OFF
#pragma config PWRTE = ON
#pragma config MCLRE = ON
#pragma config CP = OFF
#pragma config BOREN = ON
#pragma config CLKOUTEN = OFF
#pragma config FCMEN = ON

// CONFIG2
#pragma config WRT = ALL
#pragma config PPS1WAY = OFF
#pragma config ZCDDIS = ON
#pragma config PLLEN = ON
#pragma config STVREN = OFF
#pragma config BORV = LO
#pragma config LPBOR = OFF
#pragma config LVP = ON

//
// h/w interface definition
//
#define REG_OE_bar 0b00010000
#define WE_bar     0b00000001
#define OE_bar     0b00000010
#define CP_en      0b00001000
#define MR_en      0b00100000
#define CP_bar     0b00000100
#define MR_bar     0b00010000

#define VSYNC 0b10000000
#define HSYNC 0b01000000
#define RGB(r, g, b) (((r) << 4) | ((g) << 2) | (b))

#if 0 // manually inlined below to save 4 instructions
void SetupPeripherals() {
  // intosc 32 MHz
  OSCCON = 0b11110000;

  // select digital I/O
  ANSELA = 0;
  ANSELB = 0;
  ANSELC = 0;

  // set TRIS bits: all outputs
  PORTA = 0x00;
  TRISA = 0x00;
  PORTB = 0x00;
  TRISB = 0x00;
  PORTC = 0x00;
  TRISC = 0x80;
}
#endif

//
// set control lines for free-running VGA signal generation
//
void RunMode()
{
  TRISC = 0xff; // data lines all inputs
  // reset address counter, then let it rip
  LATB = WE_bar | OE_bar&0 | CP_en&0 | CP_bar&0 | MR_en   | MR_bar   ;
  LATB = WE_bar | OE_bar&0 | CP_en&0 | CP_bar&0 | MR_en&0 | MR_bar&0 ;
  LATA = REG_OE_bar&0;
}

//
// set control lines for bitbanging waveforms into SRAM, and
//   reset SRAM address counter to 0
//
void LoadMode()
{
  LATA = REG_OE_bar;
  // toggle CP with MR low to reset address counter
  LATB = WE_bar | OE_bar | CP_en | CP_bar   | MR_en | MR_bar   ;
  LATB = WE_bar | OE_bar | CP_en | CP_bar&0 | MR_en | MR_bar   ;
  LATB = WE_bar | OE_bar | CP_en | CP_bar   | MR_en | MR_bar   ;
  // bring out of reset
  LATB = WE_bar | OE_bar | CP_en | CP_bar   | MR_en | MR_bar&0 ;
  TRISC = 0x00; // data lines all outputs
}

//
// bitbang a number of identical bytes into sequential SRAM addresses
//
void write_SRAM_bytes(uint8_t value, uint8_t count)
{
  PORTC = value;
  LATB = WE_bar | OE_bar | CP_en | CP_bar | MR_en | MR_bar&0 ;
  do {
    // toggle WE to write data
    LATB = WE_bar&0 | OE_bar | CP_en | CP_bar   | MR_en | MR_bar&0 ;
    LATB = WE_bar   | OE_bar | CP_en | CP_bar   | MR_en | MR_bar&0 ;
    // toggle CP to advance address
    LATB = WE_bar   | OE_bar | CP_en | CP_bar&0 | MR_en | MR_bar&0 ;
    LATB = WE_bar   | OE_bar | CP_en | CP_bar   | MR_en | MR_bar&0 ;
  } while (--count);
}

void GenerateLine(uint8_t vsync, uint8_t rgb, uint8_t count)
{
  do {
    write_SRAM_bytes( vsync | HSYNC   | rgb&0 , 16);  // front porch
    write_SRAM_bytes( vsync | HSYNC&0 | rgb&0 , 96);  // sync pulse
    write_SRAM_bytes( vsync | HSYNC   | rgb&0 , 48);  // back porch
    write_SRAM_bytes( vsync | HSYNC   | rgb   , 200); // video
    write_SRAM_bytes( vsync | HSYNC   | rgb   , 200); // video
    write_SRAM_bytes( vsync | HSYNC   | rgb   , 240); // video
  } while (--count);
}

#define S 12
#define FP(x) ((int16_t)((x) * (1<<S)))

//#define WHOLE_SET

#ifdef WHOLE_SET
#define ASPECT   (640./480.)
#define WIDTH     2.5
#define IMAG_MIN  FP(-1.25)
#define IMAG_STEP FP(WIDTH / 480.)
#define REAL_MIN  FP(-2.5)
#define REAL_STEP FP(ASPECT * WIDTH / 640.)
#define ESCAPE_RADIUS FP(4.)
#define MAXITER   255
#else
#define ASPECT   (640./480.)
#define WIDTH     0.125
#define IMAG_MIN  FP(-.0625)
#define REAL_MIN  FP(-1.5)
#define IMAG_STEP FP(WIDTH / 480.)
#define REAL_STEP FP(ASPECT * WIDTH / 640.)
#define ESCAPE_RADIUS FP(4.)
#define MAXITER   255
#endif

void GenerateFrame()
{
  GenerateLine( VSYNC   , 0,  33);  // V back porch

  int16_t dc = REAL_STEP;
  int16_t dd = IMAG_STEP;
  int16_t d = IMAG_MIN;  
  int16_t row = 480;
  do {
    write_SRAM_bytes( VSYNC | HSYNC   | 0 , 16);  // H front porch
    write_SRAM_bytes( VSYNC | HSYNC&0 | 0 , 96);  // H sync pulse
    write_SRAM_bytes( VSYNC | HSYNC   | 0 , 48);  // H back porch

    int16_t c = REAL_MIN;
    int16_t col = 640;
    do {
      int16_t a = 0;
      int16_t b = 0;
      uint8_t iter = MAXITER;
      do {
        int32_t aa32 = ((int32_t)a * (int32_t)a);
        if (aa32 & 0xf8000000){
          break;
        }
        int16_t aa = aa32 >> S;

        int32_t bb32 = ((int32_t)b * (int32_t)b);
        if (bb32 & 0xf8000000){
          break;
        }
        int16_t bb = bb32 >> S;

        if (aa > ESCAPE_RADIUS ||
            bb > ESCAPE_RADIUS ||
            aa + bb > ESCAPE_RADIUS){
          break;
        }
        b = (((int32_t)a * (int32_t)b) >> (S-1)) + d;
        a = aa - bb + c;
      } while(--iter);

      uint8_t red, green, blue;
      red = (iter & 3);
      green = ((iter & 0x0c) >> 2);
      blue = ((iter & 0x30) >> 4);
      write_SRAM_bytes( VSYNC | HSYNC   | RGB(red, green, blue), 1); // one pixel

      c += dc;
    } while (--col);
    d += dd;
  } while (--row);

  GenerateLine( VSYNC   , 0,  10);  // V front porch
  GenerateLine( VSYNC&0 , 0,  2);   // V sync pulse
  write_SRAM_bytes( VSYNC | HSYNC | 0, 2);     // end of vsync; resets counter
}

int main() {

  // this call inlined here to shave off instructions
  // SetupPeripherals();

  // intosc 32 MHz
  OSCCON = 0b11110000;

  // select digital I/O
  ANSELA = 0;
  ANSELB = 0;
  ANSELC = 0;

  // set TRIS bits: all outputs
  PORTA = 0x00;
  TRISA = 0x00;
  PORTB = 0x00;
  TRISB = 0x00;
  PORTC = 0x00;
  TRISC = 0x80;

  LoadMode();
  GenerateFrame();
  RunMode();

  while(1){
    continue;
  }

  return 0;
}


Discussions

Yann Guidon / YGDES wrote 01/03/2017 at 01:43 point

if (aa32 & 0xf8000000){

=>

if (aa32 >> 24)

or better, test the high byte of aa32

  Are you sure? yes | no

Ted Yapo wrote 01/03/2017 at 02:05 point

Yes, good call.

The 0xf800... should actually be calculated/parameterized by S, the location of the binary point in the 16-bit fixed point values.  I had been meaning to fix it for some time, but never got around to it, then got distracted by the hardware.  But, instead of parameterizing it, if you leave the constant as-is, you can probably cut a few instructions here.

Then again, a good C-compiler should do this optimization for you.  But probably not the free XC8 one.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 01/03/2017 at 02:57 point

as ever, when optimising : ASSUME NOTHING :-)

  Are you sure? yes | no

Dr. Cockroach wrote 01/03/2017 at 00:55 point

Fantastic :-)

  Are you sure? yes | no

Yann Guidon / YGDES wrote 01/02/2017 at 23:29 point

AWESOME !!!!!!!!!!!!

  Are you sure? yes | no

esot.eric wrote 01/02/2017 at 18:40 point

Beautious! Can't believe that short scroll through code pulled that together. I've gotta look at this more closely when my brain's back.

  Are you sure? yes | no

Ted Yapo wrote 01/02/2017 at 21:41 point

Thanks :-)

It's a little convoluted since the frame generation logic is intertwined with the fractal computation, but it works.  I was pretty psyched that the hardware just fell together at the end - I feared all sorts of issues running a board like this at 25.175 MHz, but the signals all look pretty good.  And it was an ultra lucky bonus that I was within a few instructions of 1kB with my first try.  Much smaller and I would have wanted to cram more functionality in there; much bigger, and I'd have some serious optimization to do.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 01/03/2017 at 00:09 point

you can still inline a few setup functions :-)

  Are you sure? yes | no

Ted Yapo wrote 01/03/2017 at 00:14 point

@Yann Guidon / YGDES yes, yes I can.  I had two others inlined at one point, and it saves a few instructions, but looks ugly.  Unless I can think of something to do with the extra few bytes, I'll spend them on pretty code :-)

The compiler should do this anyway - the free version is just stupid.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 01/03/2017 at 00:56 point

maybe manage  MAXITER ?

  Are you sure? yes | no

Ted Yapo wrote 01/03/2017 at 01:19 point

@Yann Guidon / YGDES you mean reduce MAXITER?  It will make things go faster, but reduce detail near the edges of the set.

I just did some experiments - my desktop generates the same image in about 100 ms, so what-if experiments are painless :-) 

Going to MAXITER = 128 doesn't show much difference, but by 64, detail is visibly lost.  Even speeding this thing 2x isn't going to make it interactive, though.  It takes a few hours - I'm going to actually time it soon.

  Are you sure? yes | no