Sunday, August 28, 2011

Paint it black

Got my fingers a little dirty and started working on a couple of the playfield toys. I have not yet decided if this particular one will be an animated one...

1) White, die-cast 1957 Chrysler 300C

2) Black undercoat (purposely not 100% covering)

3) Base layer, grey (purposely not 100% covering)

4) Added rust and painted interior

5) More rust and dirt wash
6) The final car - A broken down, war beaten 'Chrysalis Motors Highwayman'
I'm thinking you cannot have a hard time guessing the theme of the game now...

Monday, August 22, 2011

As David would have said - Ch-ch-ch-changes!

I've changed my mind... I will be using a SD card for animations.
It will also be used to store the highscore etc.  The built in EEPROM has a "very" limited write/erase cycle so I figure I'd keep the internal memorycards intact and store everything on external cards to minimize the risk of my pinball machine breaking down during a tournament or whatnot.

The Chipkit 512 KB flash storage means around 120 frames of animation. That data is probably best used as common animation that needs to be readily available and the SD-card ones will be mode-specific graphics. With SPI the datarate is fast enough, and I figure I could use the natural pauses in the game to load new animations into RAM. The current game logic was actually already prepared for this so it would mean minimal changes and/or interruptions in gameplay.

I've also got basic serial communication between master (Arduino) and slave (Chipkit) going on, so the host can send "playMusic", "playAnimation" etc commands to the slave which will then perform them. I will probably do at least one rewrite of that library as time progress to further streamline the communication, but at the moment it works fine. It is prepared for (and is currently) sending batch jobs of commands, but it's uncertain if the final version will send single commands right away or batch them together for sending at the end of the frame.  There are different benefits for both versions.

I've also refactored the code to work with two different boards:
Arduino - Switches, Solenoids, Game logic and SoundFX.
Chipkit - Display, Lights, SD card and Music

With the transition to the Chipkit, it unfortunately turned out that (most likely) the voltage difference from 5V to 3.3V does not allow the display to properly function. I've been in touch with the manufacturers of both the display and the Chipkit and we've come up with the same conclusion. So, I've ordered a 3.3->5V logic translator chip now which will hopefully solve the problem. Kind of a bummer, but it will be worth the extra effort for all the sweet improvements it offers.

The game logic ... Ah, the fun part!
The ruleset is shaping up quite nicely and as it seems to me - a nice adaptation of the original concept. When reading the rules I'm quite satisfied that the gameplay will be fun as well. I guess the hardest part will be actually finding sound and graphics for the game, but if all else fails, I will do them myself.

Wednesday, August 17, 2011

Max32 + Mega2560 = Superpin!


The main bottleneck at this moment is the RAM, as I said earlier. With the Mega2560 I got 8KB of RAM, which gives me around 1 display buffer with 8 colors (´a 4096 bytes). Sure, I could do some voodoo magic to get it working, but it will be very slow to perform any writing to the buffer. With all this extra overhead the 16MHz will running at 100% just to keep up. In order to fully support doublebuffering I would like to have three buffers. One buffer that is sent with SPI to the display straight away, and two that are alternated and built "behind the scenes".

But...
I will be using BOTH boards, actually.

The Max32 will take care of the visuals (lights and display) and the Mega2560 will do solenoids, switches and game logic. Using all the available memory, this would give me around 800 KB to store data (ruleset and logic excluded) for animation - I could easily fit all the game animations there to eliminate the need for an external SD card.

The Max32 is also of more than 4x the MHz, 2x the flash and 16x the RAM!
With this extra card I've more than doubled the performance and the number of I/O pins, which will really come in handy!


1) Poor Man's Motherboard!


Sunday, August 14, 2011

Out of RAM

Ouch, Arduino. Ouch...

Time for plan B - Chipkit Max32

Those buffers...

I found out that I can improve the speed of the display rendering with about 50%.
It's quite simple really.

Currently I'm checking every frame, after every color, after pixel on/off...
But my machine will not feature 60 fps full motion video. Heck, even cinemas don't have that. You can easily see that this is a large overhead with largely redundant information.

By extracting the draw buffer (front) from the videolayer (back) I can keep drawing the frontbuffer "no questions asked" at about 1 ms per color. All the screen updates, such as text and images, happens in the backbuffer and when say a certain interval has passed the buffers are swapped. Rinse and repeat.

The beauty about this is that I remove all the heavy checks from the draw routine and can spread out the work of building a frame over a "lot" of time. Every clock cycle is crucial in this build since the Arduino is a single core, single process type of MCU.

A quick example (numbers are guesstimates, since I have not coded the backbuffer part yet):

Before:
Single frame: 18 ms (drawing) + 20 ms (building) = 38 ms, gives around 26 fps if the frame would be recreated every frame, such as during a video scene. This is very low and flickering is noticeable. Normal case is around 52 fps if the frame is created only once and drawn many times.

Single frame: 8 ms (drawing) + 2 ms (building) =   10 ms, gives around 100 fps if video was created at about 15 frames/sec. I could easily limit the drawing to 70 Hz to further free up resources without being noticeable for the player. Time that they however will feel with the game being more responsive.

I can't believe I've missed this -
I've done this a million times before in regular graphics programming...


Saturday, August 13, 2011

Shades of Sanity

Heureka!
SPI is a godsend. Everything's forgiven!

1) 8 + 1 colors at 18.868 ms, about 52 frames/sec.

The new code is approximately 2.5 x faster, which may not sound a lot - but that's what separates a boring or flickering display from a nice, crisp multicolor display. The main time thief is the "if ( *f++ >= lx)" part - the code runs at about 128 frames/sec with a fixed value instead of "b", so I figure it will be possible to further improve the code. For reference, old Williams games used 4+1 shades, newer Stern games use 16 shades.  Considering the hardware used, I'm quite satisfied with the current performance.


Feel free to look upon and possible improve the code:

void writeDisplay()
{
/*
Write all shades in one go. (i.e around 2.3 ms per color)
*/


register byte x,y,lx,b;
    register byte *f;


cli(); //Disable interrupts 

for (lx=1; lx < DISPLAY_MAX_BRIGHTNESS+1;lx++) //repeat for each color/shade
{   
   f = &frame[0][0];


//row 1  -------------
bitClear(PORTF, 2);          //set 4th bit of PORTF to LOW          
for (x=0; x < 16; x++)   //send full 128 bit row using SPI
{
if ( *f++ >= lx) b  = B10000000; else b=0;
if ( *f++ >= lx) b |= B01000000;
if ( *f++ >= lx) b |= B00100000;
if ( *f++ >= lx) b |= B00010000;
if ( *f++ >= lx) b |= B00001000;
if ( *f++ >= lx) b |= B00000100;
if ( *f++ >= lx) b |= B00000010;
if ( *f++ >= lx) b |= B00000001;

SPI.transfer(b);
}   
   bitSet(PORTF, 2);            //set 4th bit of PORTF to HIGH //Column latch
      bitSet(PORTF, 4);          //set 5th bit of PORTF to HIGH (mark first row)  
bitClear(PORTF, 5);          //set 6th bit of PORTF to LOW     
        bitClear(PORTF, 3);          //set 4th bit of PORTF to LOW
        bitSet(PORTF, 3);            //set 4th bit of PORTF to HIGH      
bitSet(PORTF, 5);            //set 6th bit of PORTF to HIGH


//row 2-31      ------------
for (y=0; y < DISPLAY_MAX_ROWS-2; y++)
{

   bitClear(PORTF, 2);          //set 4th bit of PORTF to LOW          
for (x=0; x < 16; x++)   //send full 128 bit row using SPI
{
if ( *f++ >= lx) b  = B10000000; else b=0;
if ( *f++ >= lx) b |= B01000000;
if ( *f++ >= lx) b |= B00100000;
if ( *f++ >= lx) b |= B00010000;
if ( *f++ >= lx) b |= B00001000;
if ( *f++ >= lx) b |= B00000100;
if ( *f++ >= lx) b |= B00000010;
if ( *f++ >= lx) b |= B00000001;

SPI.transfer(b);
}   
  bitSet(PORTF, 2);            //set 4th bit of PORTF to HIGH //Column latch
    bitClear(PORTF, 4);          //set 5th bit of PORTF to LOW (the rest of the rows are not leading)
  bitClear(PORTF, 5);          //set 6th bit of PORTF to LOW    
       bitClear(PORTF, 3);          //set 4th bit of PORTF to LOW
      bitSet(PORTF, 3);            //set 4th bit of PORTF to HIGH
bitSet(PORTF, 5);            //set 6th bit of PORTF to HIGH
}
 
 
//row 32 ---------------
   bitClear(PORTF, 2);          //set 4th bit of PORTF to LOW          
for (x=0; x < 15; x++)   //send full 128 bit row using SPI
{
if ( *f++ >= lx) b  = B10000000; else b=0;
if ( *f++ >= lx) b |= B01000000;
if ( *f++ >= lx) b |= B00100000;
if ( *f++ >= lx) b |= B00010000;
if ( *f++ >= lx) b |= B00001000;
if ( *f++ >= lx) b |= B00000100;
if ( *f++ >= lx) b |= B00000010;
if ( *f++ >= lx) b |= B00000001;
SPI.transfer(b);
}   
if ( *f++ >= lx) b  = B10000000; else b=0;
if ( *f++ >= lx) b |= B01000000;
if ( *f++ >= lx) b |= B00100000;
if ( *f++ >= lx) b |= B00010000;
if ( *f++ >= lx) b |= B00001000;
if ( *f++ >= lx) b |= B00000100;
if ( *f++ >= lx) b |= B00000010;
if ( *f  >= lx) b |= B00000001;
SPI.transfer(b);
  bitSet(PORTF, 2);            //set 4th bit of PORTF to HIGH //Column latch
      bitClear(PORTF, 4);          //set 5th bit of PORTF to LOW (the rest of the rows are not leading)
bitClear(PORTF, 5);          //set 6th bit of PORTF to LOW
        bitClear(PORTF, 3);          //set 4th bit of PORTF to LOW
        bitSet(PORTF, 3);            //set 4th bit of PORTF to HIGH
bitSet(PORTF, 5);            //set 6th bit of PORTF to HIGH
} //End of color/shade

sei(); //enable interrupts.


bitClear(PORTF, 5);          //set 6th bit of PORTF to LOW
}



Friday, August 12, 2011

The new code...

My code is now full of commands similar to this:

*b = ((*b   << 1) | (*p++ | layer)?1:0); 


My head hurts...