Part I: the platform
7-segment displays are pure magic! For this project, my aim was to build a simple platform to play with displays. What can be simpler than a single display, a key and a MCU? Regarding the MCU, a while ago I purchased some of the new tinyAVR-0 and -1 from Microchip, attracted by their generous amount of peripherals on board, so they were the first candidate for my project. Surely it helped that each pin of the MCU can source up to 40 mA of current. Current is never enough with LEDs :-)
If the platform is simple, the problem was to find an application that may be worth the effort to build new hardware and write some clever software.
My first idea was to build an electronic game.
In my teen years, I grown up with the early, hand-held electronic games. Most of them where based on LEDs, keys and buzzers, all cool parts that would be nice to reuse in new, stranger projects. May I find a way to use a display in a game without adding too much parts to the recipe? Unfortunately, may early ideas where too complex or required some serious effort to build compact, efficient power sources. I decided then to try something more traditional and build a modular 7-segment display, with a twist.
I got some components from my minuscule warehouse, got a suitable MCU with a breadboard adapter then I made a quick breadboard prototype to test the concepts (and the toolchain, more on that later).
The MCU is one of the new tinyAVR series-0 cores from Microchip. For the breadboard, I adopted a ATTiny3216-SN (SOIC-20), just because when I develop a new project I tend to use the biggest part before scaling down the BOM.
I wrote some quick code to test the LED interface (really to test the toolchain) when the pattern running on the display captured my attention and suddenly I realised that I got my game!
Some furious thinking later, I warmed up KiCad and jotted down a refined schematic. Some iteration later, I reached a satisfactory architecture which I could use, after a minimal re-routing, to produce PCBs for both my ideas. Please, let me introduce you the Stranger Segs architecture.
{gallery}Stranger Segs on KiCad R1 2205 |
---|
The schematics |
PCB Top layer: SMT components, on the bottom layer I'll mount TH components. |
3D preview (top): some packages are missing but it delivers the final idea. |
3D preview (bottom): as above, but the layer where UI resides. |
At the core, there is a tinyAVR-1 MCU. You may use any model with an adequate flash space for your application but the package must be a QFN-20. I have chosen a ATtiny416-MN. The key is a Omron B3F-1000, small and economical. The display is the biggest variant of the Wurth Electronics WL-T7DS. Use the red variant or is very likely that the current-limiting resistor should be changed. To use less space and fit all the logic under the display, I used resistor arrays. Connectors may be soldered or not depending on your application. To program the module, I decided to use Tag Connect cables: the connector is printed on the PCB and is really small. Saves cost and space.
I plan to use that platform to build a game to revive the soul of electronic games from early Eighties and a composable, smart module for general purpose displays. Because two applications are better than one :-)
Part II: the game
Let me introduce you the Speed Racer Game, built on the Stranger Segs platform!
Did you remember electric toy cars that loops around an electrified circuit? Well, the display is your circuit!
The key is your steering wheel (sort of): when you click the key at the right moment, the car does a deviation (think of a chicane): traces a nice eight-loop and increases its speed.
When is it the right moment? It depends on your speed. Try to steer (click) just a bit before your car surpasses the waypoint. Each time you steer at the right moment, you’ll earn a point. But if you steer at the bad moment, the car exits the circuits and crashes. The game is paused, the number of remaining cars flashes for a while then the game resumes at low speed.
At the end of the game, your score is displayed until the key is pressed. The game will then restart at low speed and 0 points.
Not so simple as you may think.
In the video, I left in view the power source and the current measurements. Notice how the consumption of the module is dominated by the display to the point that a single LED always on consumes more than 7 LEDs multiplexed. I’ll update the application software to always use the multiplexed drive instead of the simple, direct drive. More on that in the next section.
The software
The application is divided in five functional modules:
- A general purpose timer and real time clock.
- The display driver.
- The key handler.
- The application logic (the game).
- Utilities.
There is also a library of tests to exercise each module alone or in combination with other modules.
Let’s discuss each module in detail.
The timer
Each game (each application, indeed) needs to keep track of the elapsed time. What better than an RTC for that? The RTC of new tinyAVRs is very flexible. I decided to use the overflow counter as a ticks counter and the periodic interval as a seconds counter. I’ve also baked in a general purpose interval generator used in the game to animate the display.
Notice that time accuracy is not great:
- to keep the BOM shorter no external crystal were used;
- the RTC uses the internal low frequency oscillator, which isn’t very precise;
- 32.768 kHz doesn’t divide evenly to obtain a 20 ms tick.
In my opinion, not a problem for such a game.
The key handler
Keys are notoriously stiff devices, prone to generate bouncing state across changes. Since this game depends on the timing of key presses, I decided to use the port change interrupt to measure exactly when the key is pressed.
When the key is pressed, the IRQ handler register the condition and disables itself. Then kicks in the debounce task: a simple wait of a long interval (60 ms) before enabling the port change interrupt again, this time to detect the key release.
The display driver
The 7-segment display is a common-cathode device, driven directly by the MCU GPIO ports. A current-limiting resistor is in series to each GPIO pin ensures that the current limit of each port is never reached. Since the MCU is powered by a mere 3.3V, I decided for a 120 Ohm resistor for about 10 mA of current for each LED.
To keep current consumption low, I’ve added a multiplexer that turns on each LED alone, in sequence, at a frequency of 200 Hz. This ensures that, at every time, the current consumed is that of a single LED. The frequency chosen is invisible to the human eye.
You may drive a single segment or multiple segments, depicting an alphanumeric symbol. The API is very simple: you pass to the driver the ASCII code of the segment to turn on or (for multiple segments) the ASCII code of the character to display. Since the segments are only seven, not all alphabetic characters may be displayed. Not such a limitation, if you are a bit creative with the text to display.
The LED multiplexer is driven by the timer B, always dedicated to the display driver. When all segment are off, the multiplexer is turned off to save current and CPU cycles. The tables to translate character codes to segment masks are stored in read-only memory (in flash).
The game logic
The game is built around three loops:
- The “insert coin” loop, where a demo animation is displayed until the key is pressed.
- The game loop, where the player interacts with the game.
- The “game over” loop, where the score animation is displayed until the key is pressed.
The first and the second loops are based both on the core game engine. Since the number of states that the game may assume is limited (the display has only 7 segments!) I decided to go for a state-driven machine. In reality, it’s a sort of a virtual machine with a very compact instruction set which encodes the animation states and instructions to update the game state according to two events: a key press and a timer tick.
//
// FILE: game_vm.cpp
// COPYRIGHT: 2022 Gabriele Falcioni
//
// AUTHOR: Gabriele Falcioni
// DATE: 29/05/2022
//
// PROJECT: Stranger Segs - Racing Game application
// HARDWARE: ATtiny3216
// TOOL CHAIN: Microchip avr-gcc 5.4.0, ATtiny_DFP 3.0.151
//
/* Please, notice: this is only a redacted snippet of the full file */
enum vm_opcode: uint8_t {
op_jump = 0, // PC = next_pc(), fetch, exec tick
op_a, // turn on segment a, increment PC, steer (vm_next != 0), pause
op_b, // turn on segment b, increment PC, steer (vm_next != 0), pause
op_c, // turn on segment c, increment PC, steer (vm_next != 0), pause
op_d, // turn on segment d, increment PC, steer (vm_next != 0), pause
op_e, // turn on segment e, increment PC, steer (vm_next != 0), pause
op_f, // turn on segment f, increment PC, steer (vm_next != 0), pause
op_g, // turn on segment g, increment PC, steer (vm_next != 0), pause
// on failed steer: decrement crashes, reset speed, PC = 0, fetch, stop VM
// on successful steer: increment score, recompute speed, PC = next_pc(), fetch, exec_tick
};
constexpr uint8_t vm_instruction(const vm_opcode on_tick, const int8_t next_pc) {
return (on_tick << 5) | next_pc;
}
inline vm_opcode get_opcode(uint8_t instruction) {
return static_cast<vm_opcode>(instruction >> 5);
}
inline uint8_t get_next_pc(uint8_t instruction) {
return instruction & 0x01F;
}
const uint8_t vm_program[] PROGMEM = {
/* 0: */ vm_instruction(op_b, 7), // on steer, goto 7
/* 1: */ vm_instruction(op_c, 0),
/* 2: */ vm_instruction(op_d, 0),
/* 3: */ vm_instruction(op_e, 21), // on steer, goto 21
/* 4: */ vm_instruction(op_f, 0),
/* 5: */ vm_instruction(op_a, 0),
/* 6: */ vm_instruction(op_jump, 0), // goto 0
/* 7: */ vm_instruction(op_g, 0),
/* 8: */ vm_instruction(op_e, 0),
/* 9: */ vm_instruction(op_d, 0),
/* 10: */ vm_instruction(op_c, 17), // on steer, goto 17
/* 11: */ vm_instruction(op_b, 0),
/* 12: */ vm_instruction(op_a, 0),
/* 13: */ vm_instruction(op_f, 25), // on steer, goto 25
/* 14: */ vm_instruction(op_e, 0),
/* 15: */ vm_instruction(op_d, 0),
/* 16: */ vm_instruction(op_jump, 10), // goto 10
/* 17: */ vm_instruction(op_g, 0),
/* 18: */ vm_instruction(op_f, 0),
/* 19: */ vm_instruction(op_a, 0),
/* 20: */ vm_instruction(op_jump, 0), // goto 0
/* 21: */ vm_instruction(op_g, 0),
/* 22: */ vm_instruction(op_b, 0),
/* 23: */ vm_instruction(op_a, 0),
/* 24: */ vm_instruction(op_jump, 13), // goto 13
/* 25: */ vm_instruction(op_g, 0),
/* 26: */ vm_instruction(op_jump, 1), // goto 1
};
/* ... skip some code up to the core of VM ... */
vm_result exec() {
const vm_opcode op = get_opcode(instruction);
switch (op) {
case op_jump:
pc = get_next_pc(instruction);
fetch();
return vm_exec;
case op_a:
display::show_segment('a');
pc += 1;
return vm_pause;
case op_b:
display::show_segment('b');
pc += 1;
return vm_pause;
case op_c:
display::show_segment('c');
pc += 1;
return vm_pause;
case op_d:
display::show_segment('d');
pc += 1;
return vm_pause;
case op_e:
display::show_segment('e');
pc += 1;
return vm_pause;
case op_f:
display::show_segment('f');
pc += 1;
return vm_pause;
case op_g:
display::show_segment('g');
pc += 1;
return vm_pause;
}
// invalid opcode!
// display an 'E' (for ERROR) with the dot turned on and wait forever.
display::show_char('E' + 128);
for (;;) idle();
}
vm_result steer() {
pc = get_next_pc(instruction);
if (pc) {
score += 1;
update_speed();
fetch();
return vm_exec;
} else {
if (cars > 0) --cars;
speed = 0;
update_ticks();
return vm_stop;
}
}
The API includes a cheat mode where the VM suggest when a steer is a valid move to do. That API was used in the “insert coin” mode to simulate key presses at the right moment and move the gameplay forward until the demo finishes.
The game speed starts low then increases at each valid steer. The speed increase is non-linear: is low at first, then speeds up until a maximum is reached. At maximum speed, every 80 ms, the player has a time window of only 40 ms to press the key.
It’s may seem odd but a single 7-segment display is enough to print messages. The game prints “Press to play” in demo mode and prints the earned score followed by “Press to play again” at the end of game. I’ve included some animation effects: just before the game begins, a count down is started from 3 to 0. The 0 will flash briefly. When the car crashes, the number of remaining cars is printed with the usual flash effect.
Further enhancements
The game code is pretty rough but works well. The size of flash used by the game is around 3 KB. The CPU is running at only 3.3 MHz. Sure, some polish is required and some optimisation may help to reduce code size (I expect something around 2.5 KB), pretty enough to think to add sound to the game. Incidentally, the timer A is free and may be used for some S/FX, courtesy of the PWM generators. And some mighty chip tune, for the brave driver :-) Power consumption may be further reduced putting the core to sleep when idle.
I’ll post the code for the project after some polishing.
In the next post, I'll share some thoughts and more details regarding the other application. Stay tuned!