Project

How to Create a Gaming System with the Atmel SAM4S Xplained Pro

April 01, 2016 by Robert Keim

This article provides additional details related to code development, hardware configuration, and the OLED interface.

This article provides additional details related to code development, hardware configuration, and the OLED interface.

Required Hardware/Software

Previous Article

A (Very) Simple Display Driver

At the end of the previous article, we had successfully created a new project that configures and initializes the ATSAM4SD32C microcontroller and then repeatedly displays a simple text message on the OLED display. Maybe that functionality is exciting enough for you, but I’m not quite satisfied. So, let’s continue to develop our highly primitive video game, which consists of a moving square target and a laser beam that fires when you press pushbutton 2. If the laser beam hits the target, you win. If the laser beam misses the target, you keep shooting until you win.

The first thing we need is a very simple display driver for our very simple video game. This will give us a convenient way to set and clear pixels anywhere within the OLED screen. Furthermore, we want to maintain memory in the microcontroller that corresponds to the current state of the screen. This allows us to check the state of pixels without reading data from the OLED controller; this is especially important for our particular hardware configuration because we are talking to the OLED module via SPI, and the OLED controller’s SPI interface (unlike its parallel interface) does not support read operations.

Pages and Columns

First we need to understand how the OLED pixels are arranged. Take a good look at the following two diagrams from the SSD1306 datasheet.

 

 

 

 

So what we have here are 8 “pages” of vertical pixels and 128 columns of horizontal pixels. Each page consists of 8 vertical pixels, such that the total resolution is 64 vertical pixels by 128 horizontal pixels. However, the physical size of the display is 32 vertical pixels by 128 horizontal pixels; thus, the OLED controller has enough pixel memory for 8 vertical pages, but only 4 pages can be displayed at one time.

We modify the OLED display by writing a byte via SPI. This byte controls the 8 vertical pixels corresponding to the currently chosen page and column address. The second diagram indicates the relationship between the pixels in a page and the bits within a written byte—the least significant bit in the byte corresponds to the highest pixel in the page, and the most significant bit corresponds to the lowest pixel.

Individual Pixels vs. All the Pixels

It is possible to modify only one column of one page; you do this by setting the column and page address then writing a single data byte. The ASF functions for this are ssd1306_set_page_address() and ssd1306_set_column_address(), followed by ssd1306_write_data(). I prefer, though, to modify pixels in a display buffer and then update the entire screen. I find this approach more versatile and intuitive. It may seem rather inefficient, but it’s really not too bad. One reason for this is that the OLED’s SPI interface supports fairly high clock rates. The following scope trace shows the SPI clock signal, which is currently configured to run at 5 MHz (the maximum supported by the OLED controller is 10 MHz).

 

 

So it takes only 1.6 µs to write one byte. You would think that we could update the entire screen in about (1.6 µs) × (128 columns) × (4 pages) ≈ 0.8 ms, but it actually takes much longer than this because there is about 10 µs of dead time between successive bytes. This dead time is intentionally incorporated into the ssd1306_write_data() function in order to accommodate the SSD1306’s latency, though a comment in the function indicates that the dead time can be as low as 3 µs, so I’m not sure why Atmel chose 10 µs. You can try reducing the delay in ssd1306_write_data() if you want to speed things up. Anyways, with the current delay it should take about (11.6 µs) × (128 columns) × (4 pages) ≈ 5.9 ms to update the entire screen, and the scope confirms that this is indeed the case:

 

 

Thus, we can see that the amount of time needed to update the entire screen is by no means excessive, especially if you consider that even at 30 frames per second we would have 33 ms between screen updates.

Driver Code

I use the following two-dimensional array as the display buffer:


//Preprocessor definitions
#define OLED_HEIGHT_BYTES 32/8
#define OLED_WIDTH_PIXELS 128

//Global variables
unsigned char OLED_Pixels[OLED_HEIGHT_BYTES][OLED_WIDTH_PIXELS];

These two functions modify a single pixel in the display buffer:


/*This function sets one OLED pixel identified by the Row and Column parameters.
Keep in mind that the OLED array rows are addressed as bytes (of 8 pixels), whereas
the Row parameter passed to this function is a pixel address.*/    
static inline void Set_OLED_Pixel(unsigned char Row, unsigned char Column)
{
	if (Row < 8)
	{
		OLED_Pixels[0][Column] |= (0x01 << Row);
	}
	
	else if (Row < 16)
	{
		OLED_Pixels[1][Column] |= (0x01 << (Row - 8));
	}
	
	else if (Row < 24)
	{
		OLED_Pixels[2][Column] |= (0x01 << (Row - 16));
	}
	
	else if (Row < 32)
	{
		OLED_Pixels[3][Column] |= (0x01 << (Row - 24));
	}
}

/*This function clears one OLED pixel identified by the Row and Column parameters.
Keep in mind that the OLED array rows are addressed as bytes (of 8 pixels), whereas
the Row parameter passed to this function is a pixel address.*/
static inline void Clear_OLED_Pixel(unsigned char Row, unsigned char Column)
{
	if (Row < 8)
	{
		OLED_Pixels[0][Column] &= ~(0x01 << Row);
	}
	
	else if (Row < 16)
	{
		OLED_Pixels[1][Column] &= ~(0x01 << (Row - 8));
	}
	
	else if (Row < 24)
	{
		OLED_Pixels[2][Column] &= ~(0x01 << (Row - 16));
	}
	
	else if (Row < 32)
	{
		OLED_Pixels[3][Column] &= ~(0x01 << (Row - 24));
	}
}

Note that I declared these as “inline” functions so that they will execute more quickly.

Here is the function that I use to clear all the pixels in the display buffer:


static void Clear_OLED_Array(void)
{
	unsigned char x, y;
		
	for (x = 0; x < OLED_HEIGHT_BYTES; x++)
	{
		for (y = 0; y < OLED_WIDTH_PIXELS; y++)
		{
			OLED_Pixels[x][y] = 0;
		}
	}
}

Note that these three functions do not actually update the OLED screen; they merely modify the buffer. The screen is updated by copying the contents of the display buffer to the memory in the OLED controller:


static void Update_OLED_Display(void)
{
	unsigned char Row, Column;
	
	ssd1306_set_page_address(0);
	ssd1306_set_column_address(0);
	
	for (Row = 0; Row < OLED_HEIGHT_BYTES; Row++)
	{
		for (Column = 0; Column < OLED_WIDTH_PIXELS; Column++)
		{
			ssd1306_write_data(OLED_Pixels[Row][Column]);
		}
	}
}

This function writes all the bytes from the display buffer to the OLED controller via SPI, according to the following pattern: page 0, column 0; page 0, column 1; . . . ; page 0, column 127; page 1, column 0; . . . ; page 1, column 127; page 2, column 0; . . . ; page 2, column 127; page 3, column 0; . . . ; page 3, column 127. Keep in mind that these bytes are written one after another, without any intervening memory-address information. Thus, we need to make sure that the OLED controller interprets this data correctly. To do this, we configure it for “horizontal addressing mode”:

 

 

When operating in this mode, the OLED controller will automatically update its address pointer in a way that is consistent with how we are sending out the contents of the display buffer.

A Moving Target

The target is a 4-pixel-by-4-pixel square that repeatedly moves from left to right, as follows:

There is a 30 ms delay between the one-pixel-to-the-right target movements. This scope capture shows the data packets transmitted to the OLED controller; as we saw above, the packets (i.e., data bytes for updating the entire screen) are about 6 ms wide, and now we have 30 ms between packets.

 

 

The following code excerpt gives you the entire main() function. The moving-target functionality is in the infinite while loop. Refer to the comments for important details.


int main (void)
{	
	//clock configuration and initialization
	sysclk_init();
	
	/*Disable the watchdog timer and configure/initialize
	port pins connected to various components incorporated 
	into the SAM4S Xplained development platform, e.g., the 
	NAND flash, the OLED interface, the LEDs, the SW0 pushbutton.*/  
	board_init();

	//initialize SPI and the OLED controller
	ssd1306_init();
	
	/*These two statements configure the OLED module for
	"horizontal addressing mode."*/
	ssd1306_write_command(SSD1306_CMD_SET_MEMORY_ADDRESSING_MODE);
	ssd1306_write_command(0x00);
	
	Configure_Pushbutton2();
	
	/*This function clears all the pixels in the array; it does not
	update the OLED display.*/
	Clear_OLED_Array();
	
	Update_OLED_Display();
		
	/*These variables hold the row and column address 
	corresponding to the top-left pixel of the 
	4-pixel-by-4-pixel square target. The row address 
	is set to 14 because we want the target to be in the 
	middle of the display, and the column address is set 
	to 0 so that the target starts from the left-hand 
	edge of the screen.*/
	TargetRow = 14;
	TargetColumn = 0;
	
	while (1)
	{
		//form the 4-pixel-by-4-pixel square target
		Set_OLED_Pixel(TargetRow, TargetColumn);
		Set_OLED_Pixel(TargetRow, TargetColumn+1);
		Set_OLED_Pixel(TargetRow, TargetColumn+2);
		Set_OLED_Pixel(TargetRow, TargetColumn+3);
		Set_OLED_Pixel(TargetRow+1, TargetColumn);
		Set_OLED_Pixel(TargetRow+1, TargetColumn+1);
		Set_OLED_Pixel(TargetRow+1, TargetColumn+2);
		Set_OLED_Pixel(TargetRow+1, TargetColumn+3);
		Set_OLED_Pixel(TargetRow+2, TargetColumn);
		Set_OLED_Pixel(TargetRow+2, TargetColumn+1);
		Set_OLED_Pixel(TargetRow+2, TargetColumn+2);
		Set_OLED_Pixel(TargetRow+2, TargetColumn+3);
		Set_OLED_Pixel(TargetRow+3, TargetColumn);
		Set_OLED_Pixel(TargetRow+3, TargetColumn+1);
		Set_OLED_Pixel(TargetRow+3, TargetColumn+2);
		Set_OLED_Pixel(TargetRow+3, TargetColumn+3);
		
		Update_OLED_Display();
		
		delay_ms(30);
		
		//clear the previous target
		Clear_OLED_Pixel(TargetRow, TargetColumn);
		Clear_OLED_Pixel(TargetRow, TargetColumn+1);
		Clear_OLED_Pixel(TargetRow, TargetColumn+2);
		Clear_OLED_Pixel(TargetRow, TargetColumn+3);
		Clear_OLED_Pixel(TargetRow+1, TargetColumn);
		Clear_OLED_Pixel(TargetRow+1, TargetColumn+1);
		Clear_OLED_Pixel(TargetRow+1, TargetColumn+2);
		Clear_OLED_Pixel(TargetRow+1, TargetColumn+3);
		Clear_OLED_Pixel(TargetRow+2, TargetColumn);
		Clear_OLED_Pixel(TargetRow+2, TargetColumn+1);
		Clear_OLED_Pixel(TargetRow+2, TargetColumn+2);
		Clear_OLED_Pixel(TargetRow+2, TargetColumn+3);
		Clear_OLED_Pixel(TargetRow+3, TargetColumn);
		Clear_OLED_Pixel(TargetRow+3, TargetColumn+1);
		Clear_OLED_Pixel(TargetRow+3, TargetColumn+2);
		Clear_OLED_Pixel(TargetRow+3, TargetColumn+3);
		
		/*Move the target one pixel to the right. If the 
		right edge of the target has reached the last
		display column, return the target to the left-hand
		edge of the screen.*/
		TargetColumn++;
		if ( (TargetColumn+3) == OLED_WIDTH_PIXELS)
		{
			TargetColumn = 0;
		}
	}
}

Firing the Laser

You fire the laser by pressing pushbutton 2. We need to enable the port pin connected to pushbutton 2 and configure its interrupt (the pushbutton functionality is interrupt-driven).


static void Configure_Pushbutton2(void)
{
	/*Enable the clock for PIOC, which is one of the parallel 
	input/output controllers. The pushbutton 2 signal is connected
	to a pin included in PIOC.*/
	pmc_enable_periph_clk(ID_PIOC);
	
	/*Configure the integrated debounce functionality for the pin
	connected to pushbutton 2.*/ 
	pio_set_debounce_filter(PIOC, PIN_PUSHBUTTON_2_MASK, 10);
	
	/*This assigns an interrupt handler function for the pushbutton 2 
	interrupt. The third parameter consists of attributes used in the
	process of configuring the interrupt. In this case the attributes
	parameter indicates that the pin's internal pull-up is active, that 
	the pin's debounce filter is active, and that the interrupt fires on 
	the falling edge.*/ 
	pio_handler_set(PIOC, ID_PIOC, PIN_PUSHBUTTON_2_MASK, PIN_PUSHBUTTON_2_ATTR, PushButton2_InterruptHandler);
	
	//enable PIOC interrupts in the NVIC (nested vectored interrupt controller)
	NVIC_EnableIRQ((IRQn_Type) ID_PIOC);
	
	//15 is the lowest priority, 0 is the highest priority
	pio_handler_set_priority(PIOC, (IRQn_Type) ID_PIOC, 0);
	
	/*Enable the pushbutton 2 interrupt source. Note that it is necessary
	to both "enable the interrupt" and "enable the interrupt in the NVIC."*/
	pio_enable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK);
}

The laser is shown as a vertical line that quickly extends upward from the bottom-center of the screen (i.e., row = 31, column = 63). If the laser hits the target, the program generates the “debris effect,” i.e., the square disappears and is replaced by four pixels that expand outward (one from each corner of the square). I realize that this doesn’t satisfy conservation of mass, considering that the original target had a total of 16 pixels; we’ll just assume that the other 12 pixels were vaporized into invisibly small particles.

Here is the pushbutton 2 interrupt handler, which includes all the laser functionality. Again, the comments are essentially a continuation of the information in the article itself, so don’t forget about them.


static void PushButton2_InterruptHandler(uint32_t id, uint32_t mask)
{	
	/*Confirm that the source of the interrupt is pushbutton 2. This seems 
	unnecessary to me, but it is included in Atmel's example code.*/
	if ((id == ID_PIOC) && (mask == PIN_PUSHBUTTON_2_MASK))
	{
		/*Disable the pushbutton 2 interrupt to prevent spurious
		interrupts caused by switch bounce. I probed the pushbutton
		signal, and it looks like the spurious transitions occur at the end
		of the signal's active-low state, so disabling the interrupt as
		soon as we enter the interrupt handler effectively suppresses
		the spurious interrupt requests. For some reason I was not able
		to completely resolve this problem by means of the pin's integrated
		debounce filter.*/
		pio_disable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK);
		
		//these next 8 groups of statements create the laser beam
		Set_OLED_Pixel(31, 63);
		Set_OLED_Pixel(30, 63);
		Set_OLED_Pixel(29, 63);
		Set_OLED_Pixel(28, 63);
		Update_OLED_Display();
		delay_ms(10);
		
		Set_OLED_Pixel(27, 63);
		Set_OLED_Pixel(26, 63);
		Set_OLED_Pixel(25, 63);
		Set_OLED_Pixel(24, 63);
		Update_OLED_Display();
		delay_ms(10);
		
		Set_OLED_Pixel(23, 63);
		Set_OLED_Pixel(22, 63);
		Set_OLED_Pixel(21, 63);
		Set_OLED_Pixel(20, 63);
		Update_OLED_Display();
		delay_ms(10);
				
		Set_OLED_Pixel(19, 63);
		Set_OLED_Pixel(18, 63);
		Set_OLED_Pixel(17, 63);
		Set_OLED_Pixel(16, 63);
		Update_OLED_Display();
		delay_ms(10);
						
		Set_OLED_Pixel(15, 63);
		Set_OLED_Pixel(14, 63);
		Set_OLED_Pixel(13, 63);
		Set_OLED_Pixel(12, 63);
		Update_OLED_Display();
		delay_ms(10);
			
		Set_OLED_Pixel(11, 63);
		Set_OLED_Pixel(10, 63);
		Set_OLED_Pixel(9, 63);
		Set_OLED_Pixel(8, 63);
		Update_OLED_Display();
		delay_ms(10);
						
		Set_OLED_Pixel(7, 63);
		Set_OLED_Pixel(6, 63);
		Set_OLED_Pixel(5, 63);
		Set_OLED_Pixel(4, 63);
		Update_OLED_Display();
		delay_ms(10);
		
		Set_OLED_Pixel(3, 63);
		Set_OLED_Pixel(2, 63);
		Set_OLED_Pixel(1, 63);
		Set_OLED_Pixel(0, 63);
		Update_OLED_Display();
		delay_ms(10);
		
		/*Does the target currently include the laser-beam column?
		If so, clear the OLED array to remove the target and the laser,
		then create the debris effect. After that, clear the OLED screen
		and return the target to its starting position.*/ 
		if (TargetColumn >= 60 && TargetColumn <= 63)
		{
			Clear_OLED_Array();
			
			Set_OLED_Pixel(TargetRow-1, TargetColumn-1);
			Set_OLED_Pixel(TargetRow-1, TargetColumn+4);
			Set_OLED_Pixel(TargetRow+4, TargetColumn-1);
			Set_OLED_Pixel(TargetRow+4, TargetColumn+4);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-1, TargetColumn-1);
			Clear_OLED_Pixel(TargetRow-1, TargetColumn+4);
			Clear_OLED_Pixel(TargetRow+4, TargetColumn-1);
			Clear_OLED_Pixel(TargetRow+4, TargetColumn+4);
			
			Set_OLED_Pixel(TargetRow-2, TargetColumn-2);
			Set_OLED_Pixel(TargetRow-2, TargetColumn+5);
			Set_OLED_Pixel(TargetRow+5, TargetColumn-2);
			Set_OLED_Pixel(TargetRow+5, TargetColumn+5);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-2, TargetColumn-2);
			Clear_OLED_Pixel(TargetRow-2, TargetColumn+5);
			Clear_OLED_Pixel(TargetRow+5, TargetColumn-2);
			Clear_OLED_Pixel(TargetRow+5, TargetColumn+5);

			Set_OLED_Pixel(TargetRow-3, TargetColumn-3);
			Set_OLED_Pixel(TargetRow-3, TargetColumn+6);
			Set_OLED_Pixel(TargetRow+6, TargetColumn-3);
			Set_OLED_Pixel(TargetRow+6, TargetColumn+6);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-3, TargetColumn-3);
			Clear_OLED_Pixel(TargetRow-3, TargetColumn+6);
			Clear_OLED_Pixel(TargetRow+6, TargetColumn-3);
			Clear_OLED_Pixel(TargetRow+6, TargetColumn+6);
		
			Set_OLED_Pixel(TargetRow-4, TargetColumn-4);
			Set_OLED_Pixel(TargetRow-4, TargetColumn+7);
			Set_OLED_Pixel(TargetRow+7, TargetColumn-4);
			Set_OLED_Pixel(TargetRow+7, TargetColumn+7);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-4, TargetColumn-4);
			Clear_OLED_Pixel(TargetRow-4, TargetColumn+7);
			Clear_OLED_Pixel(TargetRow+7, TargetColumn-4);
			Clear_OLED_Pixel(TargetRow+7, TargetColumn+7);
			
			Set_OLED_Pixel(TargetRow-5, TargetColumn-5);
			Set_OLED_Pixel(TargetRow-5, TargetColumn+8);
			Set_OLED_Pixel(TargetRow+8, TargetColumn-5);
			Set_OLED_Pixel(TargetRow+8, TargetColumn+8);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-5, TargetColumn-5);
			Clear_OLED_Pixel(TargetRow-5, TargetColumn+8);
			Clear_OLED_Pixel(TargetRow+8, TargetColumn-5);
			Clear_OLED_Pixel(TargetRow+8, TargetColumn+8);
			
			Set_OLED_Pixel(TargetRow-6, TargetColumn-6);
			Set_OLED_Pixel(TargetRow-6, TargetColumn+9);
			Set_OLED_Pixel(TargetRow+9, TargetColumn-6);
			Set_OLED_Pixel(TargetRow+9, TargetColumn+9);
			Update_OLED_Display();
			delay_ms(200);
			
			Clear_OLED_Pixel(TargetRow-6, TargetColumn-6);
			Clear_OLED_Pixel(TargetRow-6, TargetColumn+9);
			Clear_OLED_Pixel(TargetRow+9, TargetColumn-6);
			Clear_OLED_Pixel(TargetRow+9, TargetColumn+9);
			
			Set_OLED_Pixel(TargetRow-7, TargetColumn-7);
			Set_OLED_Pixel(TargetRow-7, TargetColumn+10);
			Set_OLED_Pixel(TargetRow+10, TargetColumn-7);
			Set_OLED_Pixel(TargetRow+10, TargetColumn+10);
			Update_OLED_Display();
			delay_ms(500);
			
			Clear_OLED_Array();
			Update_OLED_Display();
			delay_ms(1000);
			
			TargetColumn = 0;
		}
		
		/*If the laser beam missed the target, clear the laser beam pixels.
		Nothing else needs to be done; the target will continue moving
		when execution returns to the infinite loop in main().*/
		else
		{
			OLED_Pixels[0][63] = 0;
			OLED_Pixels[1][63] = 0;
			OLED_Pixels[2][63] = 0;
			OLED_Pixels[3][63] = 0;
		}
		
		//reenable the pushbutton 2 interrupt
		pio_enable_interrupt(PIOC, PIN_PUSHBUTTON_2_MASK);
	}
}

Results

You can use the following link to download all the source and project files.

OLEDTargetPractice_Part2.zip

And here is the code in action. It makes you wonder why people buy video games, when it’s so easy to make them yourself!

Give this project a try for yourself! Get the BOM.

1 Comment