Go Down

Topic: The DHG - Dynamic Hi resolution Graphics driver (Read 1 time) previous topic - next topic

janost

Feb 07, 2014, 10:04 pm Last Edit: Feb 07, 2014, 10:36 pm by janost Reason: 1
I got feed up with not having hires graphics with tvout on the ATmega328 so I came up with my own platform.

The DHG, dynamic hires graphics.
It gives a true 176x184 pixel resolution graphics on the 328 without having a 4k videomemory.

How?
It's RAMtiled at a resolution of 22x23 tiles.

That makes for the full resolution in 88x88pixels or 11x11tiles.
The thing is that not all of the pixels in a screen is with in this square.

My solution put's out tiles in a much larger screen just where they are needed.
Not in a square format.

It dynamically allocates tiles for text, graphics and sprites from a pool and put's them all over the larger screen.
This means you have the full resolution on the screen but only where it's needed.

At the moment it only has 128 tiles so if you try to fill the screen, it wont work.
But a reasonable amount of text, sprites and gfx works really great.

I'll show you more.

strykeroz


I'll show you more.
Please do. This sounds very interesting.  Are you using some kind of memory external to the 328 to cater for swapping out tiles or is 11x11 the theoretical limit?

Will this leave much RAM for the rest of your project, or is the idea to have a standalone 328 that just drives the display with data received from another Arduino/328 project?

Geoff
"There is no problem so bad you can't make it worse"
- retired astronaut Chris Hadfield

janost

#2
Feb 08, 2014, 12:26 am Last Edit: Feb 08, 2014, 01:23 am by janost Reason: 1
No external memory.

It uses 506bytes for videomemory and 1024bytes for tilememory.
It leaves 500bytes for the application.

It's kind of hard to explain but it only places tiles on the screen where there are pixels and blank in between.
The second picture shows a spaceship, missile and the player ship.

They use a total of 8+1+4=13 tiles but still in full 176x184 resolution on the screen.
You just specify where you want to draw them.

The tiles can be overlapping, it will just "or" the new pixels in to the tile on the screen.
So the screen will appear to be a hires bitmap.

The driver takes care of the allocating the tiles and can set points, draw lines, rectangles and circles and even sprites on the screen.

The example draws 2 rotating lines in full resolution on a 176x184 screen.

Code: [Select]

// DHG - Dynamic Hi resolution Graphics driver by Jan Ostman
// This driver allocates tileRAM for pixels dynamically

// This is not complete yet but only here for inspiration

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

//----------------- VideoBlaster definitions -----------------

#define DOTCLK 1   // Pixel clock (0 for 8MHz, 1 for 4MHz)
#define HSYNC 132  // Hsync frequency (divided from Fcpu)
#define LINES 261  // Lines per field -1 (261 for NTSC, 311 for PAL)
#define SYNCPIN 4  // Pin in PORTD that is connected for sync

volatile byte VBE=0;     // Video blanking status. If this is not zero you should sleep to keep the video smooth

#define WAIT_VBE while (VBE) sleep_cpu();
#define WAIT_NOTVBE while (!VBE) {};

unsigned int scanline=0; // Dont touch, not volatile
unsigned int videoptr=0; // Dont touch, not volatile
byte row;                // Dont touch, not volatile

#define TILEALLOC 1      // This reserves extra tiles for BG or chars on screen not used by DHG
                        // 1 tile is always reserved for blank screen tiles
                       
char videomem[528];
char DHGmem[1032];
uint8_t nextfreetile=TILEALLOC;

#define SYNCDELAY   // This makes up the sync pulse low time delay
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n"); \
   asm("nop\n nop\n nop\n nop\n nop\n nop\n nop\n nop\n");
   
const byte MSPIM_SCK = 4;  // This is needed for the hardware to work
const byte MSPIM_SS = 5;   // This is needed for the hardware to work

//--------------------------------------------------------------

void setupVideoBlaster() {
 pinMode (MSPIM_SS, OUTPUT);  //A must for MSMSPI VIDEO to work
 pinMode (MSPIM_SCK, OUTPUT); //A must for MSMSPI VIDEO to work
 pinMode(2, OUTPUT); //Set D2 as output for Sync. A must for MSMSPI VIDEO to work
 UBRR0 = 0;
 UCSR0A = _BV (TXC0);
 UCSR0C = _BV (UMSEL00) | _BV (UMSEL01);
 UCSR0B = _BV (TXEN0);
 UBRR0 = DOTCLK;
 cli();
 TCCR0A = 0;
 TCCR0B = 0;
 TCNT0  = 0;
 OCR0A = HSYNC;  
 TCCR0A |= (1 << WGM01);
 TCCR0B |= (1 << CS01) | (0 << CS00);  
 TIMSK0 |= (1 << OCIE0A);
 set_sleep_mode (SLEEP_MODE_IDLE);
 sei();

 pinMode(1, OUTPUT); //A must for MSMSPI VIDEO to work
 digitalWrite(1,LOW); //A must for MSMSPI VIDEO to work

 for (int i=0;i<528;i++) {
   videomem[i]=0;
 }
}

ISR(TIMER0_COMPA_vect){    //Video interrupt. This is called at every line in the frame.
 byte c=8;  //Back porch
 byte d=4;  //Left Blank
 byte p=22; //Chars per row
 PORTD=0;
   if ((scanline>2)&&(scanline<40)||(scanline>231)) {
   SYNCDELAY
   PORTD = SYNCPIN;
   VBE=0;
 }

if (scanline<3) {
   SYNCDELAY  
   PORTD =0;
   videoptr=0;  
   row=0;    
 }  
 
 if ((scanline>39)&&(scanline<232)) {
   SYNCDELAY  
   PORTD = SYNCPIN;
   while (c--) {
    asm("nop\n");
    asm("nop\n");
   }  
   UCSR0B = _BV(TXEN0);
   while (d--) {
    while ((UCSR0A & _BV (UDRE0)) == 0) {}
    UDR0 = 0;
   }
   while (p--) {
    UDR0 = DHGmem[videomem[videoptr++]*8+(row&7)];
    //while ((UCSR0A & _BV (UDRE0)) == 0)
    // {}
    //videoptr++;
   }  
   while ((UCSR0A & _BV (UDRE0)) == 0)
     {}  
   UCSR0B = 0;
   row++;
   videoptr=(row>>3)*22;
   VBE=1;
   }
 
 scanline++;
 if (scanline>LINES) {
   scanline=0;
 }
}

void Dclr() {
 for (uint16_t z=22;z<528;z++) {
  if (videomem[z]) {
    uint16_t pixelptr=videomem[z]<<3;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    DHGmem[pixelptr++]=0;
    videomem[z]=0;
  }
 }
 nextfreetile=TILEALLOC;
}

void Dpset(uint8_t x,uint8_t y) {
 if (nextfreetile<129) {
 if ((x<176)&&(y<184)) {
 uint16_t tileptr;
 uint16_t pixeltile=22+(((y>>3)*22)+(x>>3));
 if (videomem[pixeltile]) {
  tileptr=videomem[pixeltile]<<3;
  DHGmem[tileptr+(y&7)]|= (128>>(x&7));
 }
 else {
  videomem[pixeltile]=nextfreetile;
  tileptr=nextfreetile<<3;
  DHGmem[tileptr+(y&7)]|= (128>>(x&7));
  nextfreetile++;
 }
}
}
}

void Ddrawline(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) {
 int e;
 signed int dx,dy,j, temp;
 signed char s1,s2, xchange;
 signed int x,y;
 x = x0;
 y = y0;
 if (x1 < x0) {
    dx = x0 - x1;
    s1 = -1;
 }
 else if (x1 == x0) {
    dx = 0;
    s1 = 0;
 }
 else {
    dx = x1 - x0;
    s1 = 1;
 }
 if (y1 < y0) {
    dy = y0 - y1;
    s2 = -1;
 }
 else if (y1 == y0) {
    dy = 0;
    s2 = 0;
 }
 else {
    dy = y1 - y0;
    s2 = 1;
 }
 xchange = 0;    
 if (dy>dx) {
    temp = dx;
    dx = dy;
    dy = temp;
    xchange = 1;
 }
 e = ((int)dy<<1) - dx;  
 for (j=0; j<=dx; j++) {
    Dpset(x,y);
    if (e>=0) {
       if (xchange==1) x = x + s1;
       else y = y + s2;
          e = e - ((int)dx<<1);
       }
       if (xchange==1)
          y = y + s2;
       else
          x = x + s1;
          e = e + ((int)dy<<1);
    }
}

void Ddrawrect(uint8_t x0, uint8_t y0, uint8_t w, uint8_t h) {
Ddrawline(x0,y0,x0+w,y0);
Ddrawline(x0,y0,x0,y0+h);
Ddrawline(x0+w,y0,x0+w,y0+h);
Ddrawline(x0,y0+h,x0+w,y0+h);
}

uint8_t BX=0;
uint8_t BY=10;
int8_t DX=1;
int8_t DY=-1;

uint8_t BX2=38;
uint8_t BY2=90;
int8_t DX2=1;
int8_t DY2=1;

uint8_t BX3=15;
uint8_t BY3=9;
int8_t DX3=1;
int8_t DY3=1;

uint8_t BX4=130;
uint8_t BY4=50;
int8_t DX4=1;
int8_t DY4=1;

void setup () {
setupVideoBlaster();
}

void loop () {
 WAIT_VBE
 Dclr();  // call this to clear the screen before drawing anything
 BX+=DX;
 BY+=DY;
 BX2+=DX2;
 BY2+=DY2;
 BX3+=DX3;
 BY3+=DY3;
 BX4+=DX4;
 BY4+=DY4;
 if ((BX>145)||(BX<1)) DX=-DX;
 if ((BY>153)||(BY<1)) DY=-DY;
 if ((BX2>145)||(BX2<1)) DX2=-DX2;
 if ((BY2>153)||(BY2<1)) DY2=-DY2;
 if ((BX3>145)||(BX3<1)) DX3=-DX3;
 if ((BY3>153)||(BY3<1)) DY3=-DY3;
 if ((BX4>145)||(BX4<1)) DX4=-DX4;
 if ((BY4>153)||(BY4<1)) DY4=-DY4;
 Ddrawline(BX,BY,BX2,BY2);
 Ddrawline(BX3,BY3,BX4,BY4);
 WAIT_NOTVBE
}

This is a great compromise! I suppose you could think of it as a sprite engine, in a way. (I saw another project that did something similar, and acted like the character mapped display in the old VIC-20 I had.) Very cool.
Embedded Software Engineer
UNO | Leonardo | Due | Teensy | BASIC Stamp
http://www.subethasoftware.com

PaulRB

Excellent! Reminds me of the way the graphics worked on the Sinclair ZX80/81. They only had 1K RAM and a 4MHz Z80 CPU, and no graphics processor, so the CPU did the graphics as well. There was a "FAST" mode you could use which would use the full speed of the CPU to execute the BASIC program, but the screen went crazy (no line/frame sync signals being produced) until you changed back to "SLOW" mode.

Paul


At the moment it only has 128 tiles so if you try to fill the screen, it wont work.
But a reasonable amount of text, sprites and gfx works really great.


I plan to try out your code soon. If the tiles are not being used, to they consume memory? I see great potential in a "Sprite Graphics" type system like this, for games without tons of background graphics. For instance, if a program was required to only use bitmap sprite data, stored in PROGMEM (too slow to use directly?), and the engine just kept an array of how many things it was displaying and where they were supposed to be drawn, it would free up a ton of memory.

I suspect PROGMEM might be too slow, so RAM would have to be allocated for the sprites that were in use. I might have eight bitmaps for an Asteroids ship, and display frame 1 at X,Y, using up only the RAM/overhead for that bitmap.

Interesting concept. I just started playing with Arduino video output last weekend, and have much to learn. (I am documenting my attempts to make a Pac-Man clone at www.subethasoftware.com)
Embedded Software Engineer
UNO | Leonardo | Due | Teensy | BASIC Stamp
http://www.subethasoftware.com

IWould it be possible for the user to only allocate memory for tiles that are needed? Something like this:

The code would define the bitmap data for the sprites...

// 8x8 spaceship... A box because I am lazy.
#define SPACEBOXW 8
#define SPACEBOXW 8
PROGMEM const uint8_t spacebox[] = {
  0b11111111,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b11111111
};

Then, assuming doing PROGMEM reads might be too slow for video, RAM would be allocated for that tile/sprite. Maybe a structure like this that could contain info on where the tile is supposed to be drawn, and information about the tile size:

typedef struct {
  uint8_t x, y;
  uint8_t w, h;
  uint8_t *dataPtr;
} TILE;

Then something like this (maybe hidden in a function to make it easier):

TILE spacebox;
// RAM to hold the copy of the sprite data
uint8_t spaceboxRam[sizeof(spacebox)];
// Copy PROGMEM data to RAM buffer
memcpy_PF(spaceboxRam, spacebox, sizeof(spacebox));
// Update tile object to know where RAM buffer is.
spacebox.dataPtr = spaceBoxRam;
spacebox.x = 255; // Maybe 255 means "do not display"
spacebox.y = 255;

Then some interface to add/remove it to the stuff the video engine is drawing on the screen.

addTile(&spacebox);

Then in the code loop, the engine would go through all the tiles that have been added and display them. The use would do stuff to move them just by modifying the object:

spacebox.x = 50;
spacebox.y = 50;

For a game, such as Space Invaders, where there are five rows of eleven types of aliens, there would be RAM for five tiles buffers allocated, and there would be 55 (11x5) TILE structures to track each invader.

For animation, the tile would just have the pointer changed to point to the next frame. (A clever function could be written to let the user pass in an array of frames and have it actually handle the animation directly.)

spacebox.dataPtr = spaceboxRam2;

(I would be using an array of frames, most likely, and be setting them to something like spaceboxRam[framNum]);

NOTE: This would have to be done between interrupts so it didn't mess anything up, I suspect?

Just some quick thoughts. Displaying full screens of text this would wouldn't necessarily be practical, but it might be great for games. I have about 220 bytes free for my Pac-Man demo, using the low resolution of TVout. It would be fun to see what could be done in an environment with more RAM to play with, and higher resolution to boot!
Embedded Software Engineer
UNO | Leonardo | Due | Teensy | BASIC Stamp
http://www.subethasoftware.com

janost

#7
Feb 14, 2014, 06:31 pm Last Edit: Feb 14, 2014, 06:56 pm by janost Reason: 1
No. PROGMEM is not slow but it kind of defeats the purpose of dynamic allocation.
With progmem it becomes ROM tiled or what is in everyday called a char-rom :)
My VideoBlaster platform runs with ROMtiles or text chars.

Your are in charge of your own program so if it doesn't require all 128 tiles then just lower the amount.
Change the size of DHGmem and lower the test for maxtiles in the Dpset function and it should work.

The thing with sprites (like the DHG takes care of) is that, say an 8x8 sprite, takes a single tile when at a position multiple of 8.
Once its moved a single pixel to the left or down it takes 2 or 4 tiles because of the boundary.

There isn't much time in the pixelshifting for or-ing pixels so the data really needs to be prepared on a linebasis.
And then there isn't time to prepare the next line.

For your space invaders example, you poke out a number of fixed tilespointers in videomem.
Then you animate them by changing data directly in the DHGmem.

Go Up