6. Embedded Programming

Over this week's work I was able to program the previously made PCB board with the on-board RP2040 XIAO module and use it to create a cute little game!

Some considerations in embedded programming.

In embedded programming, there's some important considerations that are often not as critical when programming in other, more flexible and capable, computing environments. The first aspect can be software compatibility: There are plenty of different microcontrollers and architectures to choose from, making the lines of code we write not as universally compatible as in personal computers or other kinds of OS-Based computing systems, unlike those applications, the code we write for embedded systems is highly tied with the hardware being used. With that being said, more times than not, a piece of code written for an specific microcontroller will not work if directly implemented in another. Sometimes with some small modifications we can adapt the code to the new architecture, but in some other cases the modifications need to be much more exhaustive.

Another consideration is program size and resource usage: With many microcontrollers, specially when working with older architerctures (like the very common ATMEGA328p microcontroller used in the Arduino Uno board), program size can quickly get out of hand and get too big to be programmed into the chip (some libraries can get pretty big, so watch out for them!). Also, complex math-intensive programs, like the ones using lots of floating point instances, trigonometric calculations or matrix operations, (and poorly coded programs too) can rapidly fill the available RAM onboard the chip and increase the system's power consumption, resulting in crashes, calculation errors, overheating or reduced battery life (where this applies). Even when this kind of problems are less prevalent in modern microcontroller hardware, we can still get in trouble if we're not careful and conservative with how we use our hardware and write our code. In all kinds of programming, good coding pratices are always appreciated!

Programming the RP2040 using Arduino and C++!

As an Electronics Engineering student, I am very familiar with embedded programming, specially with using the Arduino platform and it's associated programming language. C++ is, after all, the programming language I feel the most comfortable using. 28

Thanks to the community behind it, the Arduino IDE is compatible with many microcontroller architectures. In this case we have to download the respective files and dependencies for the XIAO RP2040 board to work. To do this you can follow the steps shown at the Ender of the page for my 4th week's work.

While browsing Amazon I found a neat Neopixel matrix board that inmediately gave me the idea to create a small snake game for this week's work using the RP2040. Neopixel is a very interesting technology that uses a communication protocol of just one wire that allows to control a technically unlimited amount of individual RGB LED's. Using a microcontroller we can send data down the chain of connected Neopixels and change their state based on a linear location, color and brightness values.

The module I purchased for around $9.00 USD (price shown below in mexican pesos) has 64 Neopixels arranged in a 8 x 8 matrix and can be easily controlled using the Adafruit Neopixel Library.

Once the board arrived I made the following connections and uploaded to the board a test program to check everything was connected as it should. White is Ground, Gray is the 5V supply, and the purple wire carries the communication protocol coming from pin D5 on the XIAO board.

This is the test code I used, it comes as an example when the Adafruit Neopixel library is installed:


	#include Adafruit_NeoPixel.h  // Include the Adafruit NeoPixel library
	#ifdef __AVR__
	#include avr/power.h // Required for 16 MHz Adafruit Trinket, include AVR power library
	#endif
	
	// Define which pin on the Arduino is connected to the NeoPixels
	#define PIN        D5 // Suggested change for Trinket or Gemma to pin 1
	
	// Define the number of NeoPixels attached to the Arduino
	#define NUMPIXELS 64 // Size for a popular NeoPixel ring
	
	// Initialize the NeoPixel strip on the specified pin, with the specified number of pixels
	Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
	
	#define DELAYVAL 100 // Delay time (in milliseconds) between updates to the pixels
	
	void setup() {
	// These lines are for the Adafruit Trinket 5V 16 MHz. 
	// They set the clock prescaler to ensure the microcontroller operates at the correct speed.
	#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
	clock_prescale_set(clock_div_1);
	#endif
	// This is where the Trinket-specific code ends.
	
	pixels.begin(); // Initialize the NeoPixel strip object. This is required to start controlling the LEDs.
	}
	
	void loop() {
	pixels.clear(); // Turn all LEDs off to start with a clean state.
	
	// Loop over each pixel in the strip (from 0 to NUMPIXELS-1)
	for(int i=0; i<=NUMPIXELS; i++) {
		// Set the color of the current pixel to a moderately bright green.
		// The Color method takes three arguments: red, green, and blue intensity (from 0 to 255).
		pixels.setPixelColor(i, pixels.Color(0, 150, 0));
	
		pixels.show();   // Apply the color changes to the strip. This makes the update visible.
	
		delay(DELAYVAL); // Wait for a bit before moving on to the next pixel.
	}
	}

Once everything seemed to be working properly, I could start programming my snake game.

The first thing to do is to convert the linear index (0 to 63) of Neopixels into positive cartesian XY coordinates and viceversa, to then, be able to draw a pixel on it. This makes creating the game much easier!

To do this I created the 'DrawPixel' function, which recieves the desired XY coordinates and the RGB values for the selected pixel:


void DrawPixel(int x, int y, int r, int g, int b) {
	int pixelNum = y * COLS + x;  // Calculate the linear index for the 2D position
	strip.setPixelColor(pixelNum, strip.Color(r, g, b));  // Set the color of the pixel
}

This function right here, is the building block for the whole game. The instruction '.setPixelColor' is included on the Adafruit Neopixel library.

Then, I needed a way to control and move said point along the screen. In my case I used the serial port and some keystrokes on the computer keyboard to control the pixel's movement; then, interpreted the serial character input as different numbers that can be sent to a different function that adds or subtracts coordinate values limited by the size of the matrix, hence, moving the snake's head along the matrix on user's command. On the other hand, the TurnOFF function serves as a way to clear everything that is displayed on screen.


void loop() {
	if (Serial.available()) {  // Check if there is an input from the serial
		DirectionInput = Serial.read();  // Read the input direction
	}
	
	// Convert the input character into a direction for the snake to move
	if (DirectionInput == 'w') {
		DirectionOutput = 4;  // Move up
	} else if (DirectionInput == 's') {
		DirectionOutput = 3;  // Move down
	} else if (DirectionInput == 'a') {
		DirectionOutput = 2;  // Move left
	} else if (DirectionInput == 'd') {
		DirectionOutput = 1;  // Move right
	}
	
	SnakeHeadMove(DirectionOutput);  // Move the snake based on the direction
	delay(GameSpeed);  // Control the speed of the game
}

// Function to move the snake's head
void SnakeHeadMove(int Direction) {
  TurnOFF();  // Turn off all LEDs before redrawing

  // Move the head of the snake
  switch (Direction) {
    case 1: HeadX = (HeadX + 1) % COLS; break;         
    case 2: HeadX = (HeadX - 1 + COLS) % COLS; break;  
    case 3: HeadY = (HeadY + 1) % ROWS; break;         
    case 4: HeadY = (HeadY - 1 + ROWS) % ROWS; break;  
  }
}

//Removes everything from the matrix
void TurnOFF() {
	for (int i = 0; i < NUM_NEOPIXELS; i++) {
	  strip.setPixelColor(i, strip.Color(0, 0, 0));  // Set each LED to black (off)
	}
	strip.show();  // Apply the changes
  }

Then, is time to have a way of generating a random apple, and being able to detect if the snake ate it to increase the game's score and eventually, the snake's length. To do this we can use the 'random' function to generate both random X and Y coordinates, limited of course to the size of the matrix. After this is done, we draw the pixel red on the resulting location.

To detect a colision of the snake with the apple, meaning the snake ate the apple, we constantly check for a coincidence between the head's coordinates and the apple's coordinates, if a colision is detected, we generate a new apple and add one to the score.


void generateApple() {
	appleX = random(0, COLS);  // Generate a random X position
	appleY = random(0, ROWS);  // Generate a random Y position
	DrawPixel(appleX, appleY, 255, 0, 0);  // Draw the apple in red
	strip.show();  // Show the apple on the LED matrix
	}

//This is running on the main loop!
// Check if the snake has eaten an apple
if (HeadX == appleX && HeadY == appleY) {
	generateApple();  // Place a new apple
	snakeLength++;  // Increase the length of the snake
	generateSuperApple();  // Possibly generate a super apple
}

Now this is done, we have now to expand the SnakeHeadMove function to allow the snake to have a tail and for it to grow in size. We accomplish this by storing every known past position the snake's head has been (up to its current length) in a two dimensional matrix, that then is sweeped by a for cycle that draws the pixels for the snake's tail accordingly. Having a tail presents a new problem: We have to make sure that the apples do not generate over the snake in any given moment, so an improvement on the generateApple function is also necessary.


void SnakeHeadMove(int Direction) {
	TurnOFF();  // Turn off all LEDs before redrawing
	
	// Move the head of the snake
	switch (Direction) {
		case 1: HeadX = (HeadX + 1) % COLS; break;         
		case 2: HeadX = (HeadX - 1 + COLS) % COLS; break;  
		case 3: HeadY = (HeadY + 1) % ROWS; break;         
		case 4: HeadY = (HeadY - 1 + ROWS) % ROWS; break;  
	}
	
	// Update the positions of the snake's segments to follow the head
	for (int i = snakeLength - 1; i > 0; i--) {
		snakeTrail[i][0] = snakeTrail[i - 1][0];
		snakeTrail[i][1] = snakeTrail[i - 1][1];
	}
	snakeTrail[0][0] = HeadX;  // Update the head's position in the trail
	snakeTrail[0][1] = HeadY;
	
	// Draw the snake on the LED matrix
	for (int i = 0; i < snakeLength; i++) {
		if(i == 0){
		DrawPixel(snakeTrail[i][0], snakeTrail[i][1], 255, 200, 0);  // Draw the head in a different color     
		}
		else{
		DrawPixel(snakeTrail[i][0], snakeTrail[i][1], SnakeR, SnakeG, SnakeB);  // Draw the body
		}
	}
	DrawPixel(appleX, appleY, 255, 0, 0);  // Draw the apple in red
	strip.show();  // Update the NeoPixels to display the changes
	}

void generateApple() {
	bool appleOnSnake = true;
	while (appleOnSnake) {
		appleOnSnake = false;
		appleX = random(0, COLS);  // Generate a random X position
		appleY = random(0, ROWS);  // Generate a random Y position
	
		// Check if the apple's position overlaps with the snake's segments
		for (int i = 0; i < snakeLength; i++) {
		if (snakeTrail[i][0] == appleX && snakeTrail[i][1] == appleY) {
			appleOnSnake = true;  // If the apple is on the snake, find a new position
			break;
		}
		}
	}
	DrawPixel(appleX, appleY, 255, 0, 0);  // Draw the apple in red
	strip.show();  // Show the apple on the LED matrix
	}

By using the appleOnSnake flag, we can allow the random coordinate generation to iterate until it doesn't find a coincidence with any of the pixels occupied by the snake.

We now have a tail! So let's allow the program to detect collisions from the snake with any part of itself, meaning, its game over! To do this I created a new function that returns a boolean flag according to its coincidence analysis:


// Function to check if the snake has collided with itself
bool checkSelfCollision() {
	for (int i = 1; i < snakeLength; i++) {  // Start from 1 to exclude the head
	if (snakeTrail[i][0] == HeadX && snakeTrail[i][1] == HeadY) {
		return true;  // Collision detected
	}
	}
	return false;  // No collision
}

Now, let's use that function to create a way for the player to lose and restart the game! The BlinkRed function will flash the whole matrix red three times when triggered, while the restartGame function will reset every variable to allow the game to start from zero.



//This is running on the main loop
// Check for collision with itself
  if (checkSelfCollision()) {
    blinkRed(3, 500);  // Flash the screen red 3 times
    restartGame();     // Restart the game
  }

// Function to blink all LEDs in red a specified number of times
void blinkRed(int blinkCount, int blinkDuration) {
	for (int i = 0; i < blinkCount; i++) {
		// Turn all LEDs to red
		for (int j = 0; j < NUM_NEOPIXELS; j++) {
		strip.setPixelColor(j, strip.Color(255, 0, 0));  // Set to red
		}
		strip.show();
		delay(blinkDuration);

		// Turn off all LEDs
		for (int j = 0; j < NUM_NEOPIXELS; j++) {
		strip.setPixelColor(j, strip.Color(0, 0, 0));  // Set to off
		}
		strip.show();
		delay(blinkDuration);
	}
}

// Function to restart the game after the snake collides with itself
void restartGame() {
  HeadX = 4;  // Reset the head position to the center
  HeadY = 4;
  snakeLength = 1;  // Reset the snake length
  DirectionInput = 0;  // Clear the direction input
  DirectionOutput = 0;  // Clear the direction output
  GameSpeed = 400;  // Reset the game speed
  Serial.flush();  // Clear the serial buffer
  DirectionOutput = 0;

  // Reinitialize the snake's trail
  for (int i = 0; i < snakeLength; i++) {
    snakeTrail[i][0] = HeadX - i;
    snakeTrail[i][1] = HeadY;
  }
  SnakeR = 0;  // Reset the snake color to green
  SnakeG = 255;
  SnakeB = 0;
  generateApple();  // Generate a new apple
}

We now have a working snake game, but I wanted to add some more features! This piece of code running inside the main loop increases the game speed and changes the snake's tail color if a score of 5 or 7 is exceeded.


// Change the game's difficulty and the snake's color based on its length
  if (snakeLength > 4) {
    SnakeR = 0;
    SnakeG = 0;
    SnakeB = 255;
    GameSpeed = 250;  // Increase the game speed
  } else if (snakeLength > 7) {
    SnakeR = 0;
    SnakeG = 255;
    SnakeB = 255;
    GameSpeed = 150;  // Further increase the game speed
  }

Another fun feature to add is some kind of 'Super Apple'! The Super Apple, if eaten, will grant three points instead of just one, but there's a catch: This apple only has a one in six chance to appear, and once it does, it blinks white for less than a second to indicate its position to then become invisible! If you go through the point it appeared though, you will be granted those extra three points. All this is done using this new function and the same logic implemented with the normal apple.


// Function to generate a super apple occasionally
void generateSuperApple() {
	bool appleOnSnake = true;
	int AppleGeneration = random(1, 6);  // Random chance to generate a super apple

	if (AppleGeneration == 3) {  // If the condition meets, generate a super apple
	while (appleOnSnake) {
		appleOnSnake = false;
		SappleX = random(0, COLS);  // Generate a random X position for the super apple
		SappleY = random(0, ROWS);  // Generate a random Y position for the super apple

		// Check if the super apple's position overlaps with the snake's segments
		for (int i = 0; i < snakeLength; i++) {
		if (snakeTrail[i][0] == SappleX && snakeTrail[i][1] == SappleY) {
			appleOnSnake = true;  // If the super apple is on the snake, find a new position
			break;
		}
		}
	}
	DrawPixel(SappleX, SappleY, 255, 255, 255);  // Draw the super apple in white
	strip.show();  
	delay(100);
	}
}

The full code, together with the initial declaration of variables, import of the Neopixel library and the hardware configuration done in the Setup function, should look like the following and result in the game previously described.


#include Adafruit_NeoPixel.h
#include stdlib.h

// Define the pin connected to the NeoPixels and the total number of NeoPixels
#define PIN_NEOPIXEL D5   
#define NUM_NEOPIXELS 64  
#define ROWS 8            
#define COLS 8            
int PanelBrightness = 25;  // Control the brightness of the panel

// Create an Adafruit_NeoPixel object for controlling the NeoPixel LEDs
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_NEOPIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

// Initial position of the snake's head
int HeadX = 4;  
int HeadY = 4;  
int GameSpeed = 400;  // Control the speed of the game
int DirectionInput = 0;  // Stores the input direction from the user
int DirectionOutput = 0;  // Stores the output direction for the snake to move
int SnakeR, SnakeG, SnakeB;  // Color of the snake

int appleX, appleY;  // Position of the regular apple
int SappleX, SappleY;  // Position of the super apple

int snakeLength = 1;    // Initial length of the snake
int snakeTrail[64][2];  // Stores the positions of the snake's segments

void setup() {
	Serial.begin(115200);                    // Initialize serial communication
	strip.begin();                           // Initialize the NeoPixel strip
	strip.show();                            // Turn off all the pixels initially
	strip.setBrightness(PanelBrightness);    // Set the brightness of the strip
	// Initialize the snake's trail
	for (int i = 0; i < snakeLength; i++) {
	snakeTrail[i][0] = HeadX;
	snakeTrail[i][1] = HeadY;
	}
	randomSeed(analogRead(0));  // Seed for random number generation
	generateApple();  // Place the first apple
	// Set initial color of the snake
	SnakeR = 0;
	SnakeG = 255;
	SnakeB = 0;
}

void loop() {
	if (Serial.available()) {  // Check if there is an input from the serial
	DirectionInput = Serial.read();  // Read the input direction
	}

	// Convert the input character into a direction for the snake to move
	if (DirectionInput == 'w') {
	DirectionOutput = 4;  // Move up
	} else if (DirectionInput == 's') {
	DirectionOutput = 3;  // Move down
	} else if (DirectionInput == 'a') {
	DirectionOutput = 2;  // Move left
	} else if (DirectionInput == 'd') {
	DirectionOutput = 1;  // Move right
	}

	SnakeHeadMove(DirectionOutput);  // Move the snake based on the direction
	delay(GameSpeed);  // Control the speed of the game
	
	// Check if the snake has eaten an apple
	if (HeadX == appleX && HeadY == appleY) {
	generateApple();  // Place a new apple
	snakeLength++;  // Increase the length of the snake
	generateSuperApple();  // Possibly generate a super apple
	}

	// Check if the snake has eaten a super apple
	if (HeadX == SappleX && HeadY == SappleY) {
	generateApple();  // Place a new apple
	snakeLength += 3;  // Increase the length of the snake significantly
	SappleX = -1;  // Invalidate the super apple's position
	SappleY = -1;
	}

	// Check for collision with itself
	if (checkSelfCollision()) {
	blinkRed(3, 500);  // Flash the screen red 3 times
	restartGame();     // Restart the game
	}

	// Change the game's difficulty and the snake's color based on its length
	if (snakeLength > 4) {
	SnakeR = 0;
	SnakeG = 0;
	SnakeB = 255;
	GameSpeed = 250;  // Increase the game speed
	} else if (snakeLength > 7) {
	SnakeR = 0;
	SnakeG = 255;
	SnakeB = 255;
	GameSpeed = 150;  // Further increase the game speed
	}
}

// Function to draw a pixel (part of the snake or an apple) on the LED matrix
void DrawPixel(int x, int y, int r, int g, int b) {
	int pixelNum = y * COLS + x;  // Calculate the linear index for the 2D position
	strip.setPixelColor(pixelNum, strip.Color(r, g, b));  // Set the color of the pixel
}

// Function to move the snake head and its trail
void SnakeHeadMove(int Direction) {
	TurnOFF();  // Turn off all LEDs before redrawing

	// Move the head of the snake
	switch (Direction) {
	case 1: HeadX = (HeadX + 1) % COLS; break;         
	case 2: HeadX = (HeadX - 1 + COLS) % COLS; break;  
	case 3: HeadY = (HeadY + 1) % ROWS; break;         
	case 4: HeadY = (HeadY - 1 + ROWS) % ROWS; break;  
	}

	// Update the positions of the snake's segments to follow the head
	for (int i = snakeLength - 1; i > 0; i--) {
	snakeTrail[i][0] = snakeTrail[i - 1][0];
	snakeTrail[i][1] = snakeTrail[i - 1][1];
	}
	snakeTrail[0][0] = HeadX;  // Update the head's position in the trail
	snakeTrail[0][1] = HeadY;

	// Draw the snake on the LED matrix
	for (int i = 0; i < snakeLength; i++) {
	if(i == 0){
		DrawPixel(snakeTrail[i][0], snakeTrail[i][1], 255, 200, 0);  // Draw the head in a different color     
	}
	else{
		DrawPixel(snakeTrail[i][0], snakeTrail[i][1], SnakeR, SnakeG, SnakeB);  // Draw the body
	}
	}
	DrawPixel(appleX, appleY, 255, 0, 0);  // Draw the apple in red
	strip.show();  // Update the NeoPixels to display the changes
}

// Function to turn off all LEDs
void TurnOFF() {
	for (int i = 0; i < NUM_NEOPIXELS; i++) {
	strip.setPixelColor(i, strip.Color(0, 0, 0));  // Set each LED to black (off)
	}
	strip.show();  // Apply the changes
}

// Function to generate a new position for the apple
void generateApple() {
	bool appleOnSnake = true;
	while (appleOnSnake) {
	appleOnSnake = false;
	appleX = random(0, COLS);  // Generate a random X position
	appleY = random(0, ROWS);  // Generate a random Y position

	// Check if the apple's position overlaps with the snake's segments
	for (int i = 0; i < snakeLength; i++) {
		if (snakeTrail[i][0] == appleX && snakeTrail[i][1] == appleY) {
		appleOnSnake = true;  // If the apple is on the snake, find a new position
		break;
		}
	}
	}
	DrawPixel(appleX, appleY, 255, 0, 0);  // Draw the apple in red
	strip.show();  // Show the apple on the LED matrix
}

// Function to check if the snake has collided with itself
bool checkSelfCollision() {
	for (int i = 1; i < snakeLength; i++) {  // Start from 1 to exclude the head
	if (snakeTrail[i][0] == HeadX && snakeTrail[i][1] == HeadY) {
		return true;  // Collision detected
	}
	}
	return false;  // No collision
}

// Function to blink all LEDs in red a specified number of times
void blinkRed(int blinkCount, int blinkDuration) {
	for (int i = 0; i < blinkCount; i++) {
	// Turn all LEDs to red
	for (int j = 0; j < NUM_NEOPIXELS; j++) {
		strip.setPixelColor(j, strip.Color(255, 0, 0));  // Set to red
	}
	strip.show();
	delay(blinkDuration);

	// Turn off all LEDs
	for (int j = 0; j < NUM_NEOPIXELS; j++) {
		strip.setPixelColor(j, strip.Color(0, 0, 0));  // Set to off
	}
	strip.show();
	delay(blinkDuration);
	}
}

// Function to generate a super apple occasionally
void generateSuperApple() {
	bool appleOnSnake = true;
	int AppleGeneration = random(1, 6);  // Random chance to generate a super apple

	if (AppleGeneration == 3) {  // If the condition meets, generate a super apple
	while (appleOnSnake) {
		appleOnSnake = false;
		SappleX = random(0, COLS);  // Generate a random X position for the super apple
		SappleY = random(0, ROWS);  // Generate a random Y position for the super apple

		// Check if the super apple's position overlaps with the snake's segments
		for (int i = 0; i < snakeLength; i++) {
		if (snakeTrail[i][0] == SappleX && snakeTrail[i][1] == SappleY) {
			appleOnSnake = true;  // If the super apple is on the snake, find a new position
			break;
		}
		}
	}
	DrawPixel(SappleX, SappleY, 255, 255, 255);  // Draw the super apple in white
	strip.show();  
	delay(100);
	}
}

// Function to restart the game after the snake collides with itself
void restartGame() {
	HeadX = 4;  // Reset the head position to the center
	HeadY = 4;
	snakeLength = 1;  // Reset the snake length
	DirectionInput = 0;  // Clear the direction input
	DirectionOutput = 0;  // Clear the direction output
	GameSpeed = 400;  // Reset the game speed
	Serial.flush();  // Clear the serial buffer
	DirectionOutput = 0;

	// Reinitialize the snake's trail
	for (int i = 0; i < snakeLength; i++) {
	snakeTrail[i][0] = HeadX - i;
	snakeTrail[i][1] = HeadY;
	}
	SnakeR = 0;  // Reset the snake color to green
	SnakeG = 255;
	SnakeB = 0;
	generateApple();  // Generate a new apple
}					

When played, It should look like this!

Thank you for taking a look into my 6th week's progress!

Useful links