Gametoy project


A basic button game based on Zephyr RTOS and STM32.

Find Out More

Introduction


The Concept: A Luminous and Musical Box

For several months, I had been planning a fun and educational project, particularly aimed at young children. My goal was to combine electronics, software, and woodworking into a creative endeavor.
Inspired by ideas found online, I wanted to design something achievable within my timeframe that would also look attractive and function seamlessly.

The final idea was to build a box featuring several illuminated buttons (4 to 6), a dot matrix screen for simple displays, and some software to enable games like a reaction test or a memory game (similar to Simon).
The idea of adding a piezo buzzer for sound effects came later and enhanced the experience.

I aimed for a polished final product, avoiding the unfinished look common in DIY projects. I also leveraged access to a Kick Assembly Line (KAL) at my company, Kickmaker, to use professional tools for fabrication.

You can find more information about Kickmaker directly on the website and also on the welcome to the jundle site that has multiple interesting photos and interviews about us.

Object design


Initial Planning - From Idea to Prototype

The project brainstorming began in September 2024, with the goal of having a finished product ready for Christmas 2024. I quickly settled on the idea of a fun box with illuminated buttons, a display screen, and simple games.

Key constraints shaped the design:
  • No built-in battery (very personal choice)
  • A finish product for an attractive look
  • Component compatibility with available tools
  • Use wood if possible (I love to build things with wood)
  • Easy to do

Component Sourcing

Selecting components was a critical step to save time and money.

My criteria included:
  • Reasonable price
  • Stock availability
  • Quality and technical compatibility
  • Delivery timelines

Key Components Chosen

Dot matrix screen: Ebay link. Large enough to display text, and compatible with Zephyr RTOS. Here is a great article explaining dot matrix details Link.

Illuminated buttons: Amazon link. Five colors with bright lighting, even at 5V.

Piezo buffer: Amazon link. Very classic and basic buzzer driven via a pwm.

STM32 development kit: ST link.

As I wanted to develop with Zephyr RTOS, I choose a board compatible with it, see zephyr link.

Very first prototype

Once components were received, it was time to first setup the project. At that time, I was not sure about the physical position of buttons, screens. I was not sure about what kind of games would be suitable.

Most of the time when you design a new product, you make a POC (proof of concept). For that I use a piece of cardboard, a breadboard to start wiring the components.

I put only the buttons and the screen visible from the front side, like the final product would be.

Design and fabrication


Modeling: Designing the Object

To find out the dimension of the box, I needed to create a quick 3D model. For that, I used SketchUp Make. Even though it is not a professional tool, it allowed me to work quickly because that is a software I already know.

The dimensions were adjusted to accommodate components while ensuring a sturdy and visually pleasing structure.

3D Printing

The STM32 development kit lacked pre-drilled mounting holes. I designed a custom support for the board. The first printed version was a complete fail as it was too fragile and poorly fitted.
Fortunately, a mecanical engineer from Kickmaker helped me redesign the part with precise dimensions and better printing techniques such as:
  • Orienting grooves vertically to avoid print defects.
  • Using hexagonal holes to avoid slicer filling holes with material.

The final print, correctly oriented, was perfect. Thanks, Will!

Woodworking and Box Construction

This was a very fun part for me as I love to do stuff with wood. I bought 18mm oak slices and started to draw my cuts on the board. The first step was to use a miter saw to cut pieces.

Then, I wanted to get rid of those sharp edges and smooth the surface. For that, I used a wood fillet tool.

Next step was about the whole to fit the screen into. Digging square wholes is not always easy, I could not use a CNC machine to help as well. I though about using a jigsaw but results would probably not be as good as with the router.

Then, it was time to drill holes for the buttons. I measured carefully where to dril on each piece of wood and simply use a powerful column driller.

And also, I drilled a small hole for the USB connector. It's way too large for a USB cable. Probably an oblong hole would have been prettier.

At this stage, I had all the piece of wood ready. I sanded them all and jumped to the next step.

Soldering

The soldering and assembly step were done slightly in parallel. There was quite a lot of soldering and assembling parts means glueing, and glueing means time for the glue to dry. So do not be surprised if some parts are already assembled in this chapter.

Some of the display needed a bit of heating gun to be well aligned as you can see on the picture.
I had to solder the piezo on the prototyping board shipped with the STM32 discovery board. It's not that easy to see so it is highlighted on the left picture with the red circle.

Then, we needed to cut wires with the correct length. To know the cable length needed, I used the very first prototype. See the last picture to understand. Ideally, I wanted to have the development board flag on the back panel and the display + button assembly vertically on one side. This configuration happens if you simply open the box. I wanted all my cable to be long enough to hold that position.

I cut all the wires accordingly, respecting the color code so it's easier to manage. I solder 4 wires on each button (Even though 3 could have done the job because a ground for LED or button is the same). I soldered then all wires on the prototyping board. This step was quite fast, with some help from my wife we did all that in a couple of hours.

Front panel Assembly

At this stage, we could start the front panel assembly. So that means inserting the display and have it well aligned with the front panel surface.
And then, inserting all the buttons with their nut.

Glueing parts

Once all pieces were okay to be assembled, it was time to glue them together.

First, I had to glue the screen. I did not used any support. Ideally this would have been better to do so, more robust but I was very careful when creating the square hole for the screen. It was already holding the screen very well without glue so this was just extra precaution.
The plan was to leave the possibility to open the box again either for future repair or future updates that may occur. The bottom part was only glued to the top side where the USB cable goes in. The top part with the screen and buttons were glued with the 3 remaining sides.
Lastly, all 2 mains assembly (front panel and back panel) were merged together, fixed using 3 discrete screws, two on the side and one on the back.

All box were flashed with the code, which was not 100% finished at the time but enough to detect if any soldering mistakes were done during that process. We did not face any issue on the 4 boxes.

That was a really satisfying step !

Finishing

For the finish, I applied Rubio Monocoat Oil Plus 2C, achieving a professional look.

On the left, the box with Monocoat, on the right, plain wood without any finish yet.

There is not much of a difference on this photo because the photo is not the best on ever taken. In real life, that is more visible.

Electronics


Schematic

The schematic is just below. The screen is powered via a +5v supply, using SPI communication (MOSI / MISO / CLK / CS).
Each button has an integrated LED that can be powered by an IO as LED are not drawning that much current.
Lastly, the piezo buzzer is driven via a PWM signal only.

Prior to order all components, I had this kind of schematic in mind. You need to be very careful about the current consummed by each parts and if your USB supply is enough to power up everything. Of course USB standard can be powerful enough to power all these components but what matters is the USB chip use on this specific discovery board. I made all the calculation in advance so I knew it should not be a problem, even with the display with all its LEDs on.

The only part I was blind is if LEDs inside the buttons will be visible enough to be powered directly by GPIO of the STM32 MCU. I was one of the first try when i received components. Plan be would have been to design a small dedicated circuit for that otherwise.

Software


Quick start of existing project

If you want to have a look at the code, try to compile it and play with it. The best way is simply to follow the README on the gametoy project page.
I have been working on Windows, however, it's probably working on Linux or MacOS as well.

If you have a different board than mine, you'll have to do some adjustments on the pinout to make it work (mostly in the dts file overlay), it should not be very complicated.

If you found any issue, have any remarks, feel free to open an issue and / or create a pull request with interesting features to add.
The rest of this chapter explain the philosophy, how I have worked so far to build this software. If you have a similar project using zephyr, I hope you can find inspiration to help you develop with this RTOS.

Setting up the development environment

To start working with Zephyr RTOS, follow these steps:
  1. Create a working directory.
  2. Install the Zephyr environment by following the Getting Started Guide.
  3. Create a new project named gametoy and copy the samples/basic/blinky project into it.
This initial step ensures that we can compile and flash a board by making an LED blink. At this point, we can begin developing and validating each function of our game using a breadboard and Dupont wires, avoiding the need for soldering.

Target configuration

When compiling, we use the keyword stm32f051, which refers to a specific target. A target may consist of multiple boards (a main board and a daughter board). Each target in Zephyr has a directory with a DTS file that defines its hardware.

Since we are using a breadboard and adding new components (screen, buttons, LEDs, buzzer), we can create a new target or use an overlay file to extend the base DTS configuration. This is especially useful for development kits, as it allows us to add connections without modifying the hardware itself.

Screen

The driver for the MAX7219 already exists in Zephyr. This chip controls an 8x8 dot matrix, and multiple units can be chained together. I used a pre-assembled screen with four MAX7219 chips connected via SPI:


&spi1 {
    pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7 &spi1_nss_pa4>;
    pinctrl-names = "default";
    status = "okay";

    max7219_8x32: max7219@0 {
        compatible = "maxim,max7219";
        reg = <0>;
        spi-max-frequency = <1000000>;
        num-cascading = <4>;
        intensity = <1>;
        scan-limit = <7>;
    };
};

The screen is powered by 5V, which is conveniently provided by the development kit.

Buttons

Each button has two pins: one connected to ground and the other to a GPIO pin. Pressing the button connects the signal to ground, a common mechanism for buttons. Here is the overlay configuration for five buttons:


buttons {
    compatible = "gpio-keys";

    red_button: red_button {
        gpios = <&gpioc 7 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };

    white_button: white_button {
        gpios = <&gpiob 6 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };

    blue_button: blue_button {
        gpios = <&gpiob 4 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };

    yellow_button: yellow_button {
        gpios = <&gpiod 2 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };

    green_button: green_button {
        gpios = <&gpioa 15 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
    };
};


Button LEDs

Each LED has two pins: one connected to a GPIO signal and the other to ground. Setting the GPIO high turns the LED on. Here is the overlay configuration:


leds {
    red_led: red_led {
        gpios = <&gpioc 6 GPIO_ACTIVE_HIGH>;
    };

    white_led: white_led {
        gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>;
    };

    blue_led: blue_led {
        gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>;
    };

    yellow_led: yellow_led {
        gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;
    };

    green_led: green_led {
        gpios = <&gpioa 14 GPIO_ACTIVE_HIGH>;
    };
};


Piezo buzzer

The piezo buzzer is controlled using PWM (Pulse Width Modulation). A square wave signal at a specific frequency determines the note played, while the duty cycle controls volume.


pwmleds: pwmleds {
    compatible = "pwm-leds";
    status = "okay";

    music_pwm_led: music_pwm_led {
        pwms = <&pwm 1 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
    };
};

&timers1 {
    status = "okay";
    pwm: pwm {
        compatible = "st,stm32-pwm";
        status = "okay";
        #pwm-cells = < 0x3 >;
        pinctrl-0 = <&tim1_ch1_pa8>;
        pinctrl-names = "default";
    };
};


Software Architecture

To organize my source code, I created a lib folder where I store reusable macro software components. For example, I developed a component called gtp_sound that allows me to generate sounds, beeps, and full melodies. This component can be used by any other module or game to add sound effects.

An additional advantage of this structure is the ability to create a dedicated test program to refine and validate this software module.

On the right, you can find a small diagram of all software components. Starting from bottom to top, the blue ones are already developed on Zephyr RTOS.

Then, you have three components to deal with buttons, display and sounds. The gametoy project is really only dealing with these 3 main features.

On top of that, I have design two module, menu and game because all games have things in common (useful functions, how to quit a game, how to display points, etc...). All games must appear on the menu when the product starts.

How to make music with a piezo buzzer ?

To create music, sounds, I have integrated a piezo buzzer. You need to feed this component with a PWM (pulse width modulation) signal.
Each tone is a different frequency, if you play the exact frequency for the expected amount of time, you can reproduce a lot of different sounds.

First, i tried some arduino code I found on github, This was not fully working as my hardware is not able to play pwm at a lower frequency than 740Hz.

I kept the header with all predefined tones and asked chatgpt to generate me some basic melody such as merry christmas. I did not use the audio result because you could barely guess it was merry christmas. Some notes were completely wrong. LLM are quite good sometimes but in autumn 2024, they were not really good to generate a music in your code.

Lastly, I simply find the music partition online and manually code the notes and duration I heard. I ended up with a code similar to that.

For the resulting audio, please have a look at the video at the bottom of this page.


int gtp_sound_play_merry_christmas()
{
	if (k_sem_take(&merry_christmas_start, K_NO_WAIT) != 0) {
		return 0;
	}

	/* source: https://gmajormusictheory.org/Freebies/Sing/WeWishYouAMerry/WeWishYouAMerry.pdf */

	static const uint16_t melody[] = {
		NOTE_D6,                                        // Meas 01
		NOTE_G6,  NOTE_G6, NOTE_A6,  NOTE_G6, NOTE_FS6, // Meas 02
		NOTE_E6,  NOTE_E6, NOTE_E6,                     // Meas 03
		NOTE_A6,  NOTE_A6, NOTE_B6,  NOTE_A6, NOTE_G6,  // Meas 04
		NOTE_FS6, NOTE_D6, NOTE_D6,                     // Meas 05
		NOTE_B6,  NOTE_B6, NOTE_C6,  NOTE_B6, NOTE_A6,  // Meas 06
		NOTE_G6,  NOTE_E6, NOTE_D6,  NOTE_D6,           // Meas 07
		NOTE_E6,  NOTE_A6, NOTE_FS6,                    // Meas 08
		NOTE_G6,                                        // Meas 09
	};

	// Tone durations in ms
	static const int noteDurations[] = {
		500,                     // Meas 01
		350, 250, 250, 250, 350, // Meas 02
		350, 300, 350,           // Meas 03
		350, 250, 250, 250, 250, // Meas 04
		350, 350, 300,           // Meas 05
		350, 250, 250, 250, 350, // Meas 06
		350, 350, 400, 400,      // Meas 07
		400, 450, 450,           // Meas 08
		900,                     // Meas 09
	};

	play_song(melody, noteDurations, sizeof(melody) / sizeof(melody[0]));

	return SONG_WELL_FINISHED;
}


Software Design Constraints

The biggest constraint is RAM usage. The STM32F0 has only 8KB of RAM, so careful software design is needed to avoid running out of memory.

The following optimization strategies were used:
  • Minimizing the number of threads. Currently, there are only four:
    • One thread for LED management (especially blinking).
    • One thread for handling the display.
    • One thread for Zephyr work (used for button press callbacks).
    • The main thread handles everything else, including games and music.
  • Since all games run in the main thread, mutex-based mechanisms ensure proper execution control.
While this design is efficient, it results in a less elegant main.c, which some purists might critique.

New games can be created easily using the same principles, allowing creativity in game design.

Final result


Full video

This is a video of the final result. Refer to the following timecode if needed:

00:10s - Setup
00:35s - Gametoy menu
00:48s - Reactivity game
01:17s - Traffic catch game
01:58s - Memory game
02:41s - Dual speed game
03:20s - Simple sound game
03:44s - Merry Christmas song
Video thumbnail

Reactivity game

This game is called reactivity game. When the game starts, on of the button will light up. You have to press it as fast as you can. The game lasts for 10 rounds. Internally the game measure the reaction time between the lighting up of the button and the pressing of the button.

Your final score is the addition of all the reaction times.

Pressing a wrong button will add a penalty of 2s to the final score.

Memory game

This game is called memory game. It is very similar to the famous Simon game.

The game consists of five colored buttons, each producing a unique tone when pressed. The device generates a random sequence, starting with one color, and the player must repeat it correctly. With each successful round, the sequence grows longer, increasing the difficulty. If the player makes a mistake, the game ends, and they must start over. The goal is to achieve the highest possible sequence without errors.

Traffic catch game

This game is called traffic catch game. You play a car, represented with a 2x2 square on the left of the screen, objects are coming from the right, you need to catch as many objects as possible going up and down the road.

The game ends after 30s. The score is the number of objects caught.

Dual speed game

This game is called dual speed game. You need two players, player on the left is using the blue button, player on the right is using the red button. The goal is to press a maximum of time your respective button. Each press will switch a pixel towards the center, till both sides are hit, then doing this on the upper row, and so on.

The game ends when all pixels are hits. The score is the number of presses.

Let's Get In Touch!


Any question ? Related to this project ? A professional opportunity ? Do you want to know more about Kickmaker ? Give us a call or send us a message via email or LinkedIn and we will get back to you as soon as possible!