LabNotes: PETI OS
Last Updated: 2021-05-13 08:00:00 -0500
While calling it an operating system is overstating the complexity involved dramatically, the fact remains that I have had a very productive week in that the two most convoluted portions of the PETI firmware is now in place - the two systems which manage the central game state object and decide what is drawn on the screen and when. Step on through and we’ll talk about how that was done, and the pitfalls I found along the way.
Scenes, Modes, and Extra Libraries
To the surprise of precisely nobody, the most complicated part of PETI was, is, and will likely always continue to be its graphical system, though calling it a graphical system is overstating its capabilities. PETI uses a text-based multi-font character system to divide the screen up into a series of lines of character-based text of certain widths based on which font is being used.
The original plan up until about noon on tuesday was to always use the same arrangement of lines - one each on the top and bottom of 8x12 text, and 6 in the middle using the larger 16x16 font.
However, when I started working on the display code I realized it would be fairly easy to do this completely differently, and so I implemented the scene system in such a way that all the following statements are true:
- A single display call can be made from the main loop on each trip around itself to update the display, period.
- That display call within the scenes_manager module/library then determines based on a global value what scene we’re in.
- The scene, which lives in its own code document/lib, knows what output mode it needs to be in, and calls a function that lives in
/lib/display/display.cthat accepts the mode and a frame object as a variable, making sure the scene is drawn the right way.
This means you can also do the following things without ever editing main.c again:
- Add up to a total of 256 scenes by implementing their code and making sure the scene manager code’s switch statement can reach it based on the right scene number, and;
- Add up to a total of 256 possible display modes, up to the upper bound of what will fit in the system memory, by adding the right display function to display.c.
This was chiefly done by implementing three new display modes:
MODE_DEMO, which simply prints the boot screen,
MODE_MENU, which will print 10 rows of 8x12 text, 16 characters wide, centered vertically on the display, and;
MODE_GAME, which obeys the rules determined bove.
This part, as it turns out, was the easy part.
New Scene Implementation: calendar_menu.c
Another part of the code changes this week - which I glossed over up until now - was redesigning the time tracking completely to use the RTC_C peripheral. This exposes an accurate-to-the-second, adjusting-for-the-weird-rules-of-time clock that can be set to run constantly in the background and, to the best of my understanding, runs independantly of the system clock, meaning it won’t need to be corrected if I ever decide to raise the system clock nearer to the peak of what it can do.
This meant a couple of things - we don’t have to waste time counting the timer interrupts when the timer wakes us up once per second to do things, for example. It also meant I needed a way to set the RTC. In initial testing - and to set up the calendar - I passed in an arbitrary date and time, but it would be handy to have the user set the calendar. In fact, this was always an intended part of the boot process, and the game wouldn’t normally let you enter gameplay without a set time.
Turns out that this is actually also super easy. All that’s needed is a function to hold the glue logic, and glue logic that interrogates the various input registers, holds some numbers in memory, then dumps them into the RTC and spits you out to the game’s main scene -
With one complication - the RTC is expecting its arguments in Binary Coded Decimal (BCD) format. This can be confusing at first, but fortunately the DriverLib convenience library for the MSP430 Family exposes functions not just to manage the conversion but to do the conversion in hardware within the RTC peripheral itself, for whatever performance bump that’s worth. Once that’s sorted out it’s actually fairly simple to get a clean set of inputs.
This was implemented extremely quickly, which meant I got extremely cocky immediately afterward.
Detour: Implementing the Game State Machine
When I say I implemented the game state I’m being a little disengenous. While all the functions that you’ll see in the main.c file are defined, a lot of those functions have child functions that aren’t defined yet, commented out because they weren’t strictly needed for the tech demo. In essence, all you actually have are two functions: an initialize function that stands up the struct that holds the game state and another that updates it and some various global integers that are controlling things, which is called on every trip around the main loop.
The child functions that control a lot of the game functionality are missing - right now, all that exists is a cutout function that sets a special evolution time if the active
stage happens to be the egg state, and the evolution detection logic doesn’t handle the evolution at all, but drops the user back into
SCENEADDR_demo_mode when the egg would normally hatch. This was enough of the logic in one place to test the concept.
New Scene Implementation: main_game.c
The scene described at
main_game.c is going to be some of the most frequently-run code on the entire device, second only to the ISR for the
TIMER_A interrupt and all of the functions called in the main loop. This is because it controls the drawing of the main game display, as well as managing state related to that, such as cursor position for the two main menu bars (also not yet implemented, because to implement them I would have to implement menus for them to go to).
This sounds very simple, and is, but pushed the limits of what I could do with my current understanding of C as a language, embarassingly enough.
First Issue: main_game animations are complex
There are two concepts important to understand about how animations are handled on the main_game screen:
- Each “phase” of life (egg, child, adolescent, adult, and senior) has its own unique
metanimationwhich is four frames of animation data stored as arrays of strings, telling the functions in main_game where to draw the character tiles
- Each “stage” or “species” of PETI has a
sizevalue that further instructs the game on how to understand that stage’s animation property, and;
- Each stage also has an
animation, consisting of two strings (an A frame and a B frame) of text which are the actual characters to reference from the 16x16 tileset to draw that character.
This introduces a lot of complexity. For each of the six lines of text that describe the “playing field” of the game’s main screen, the function has to:
- iterate over each character/cell of the output register for that line, and;
- check the value in the currently-active frame of the
metanimationto determine whether to put a space or a character, and;
- if a character, check which character we are on in
- modulate various counters based on that
sizevalue mentioned above, and;
- set a flag that says whether to redraw this line on the LCD or not.
This complexity, while allowing a great amount of versatility (and eventually allowing us to make use of
special metanimations without expanding the already bloated fonts, necessarily lead to some craziness just in getting compilation down.
Second Issue: Neither Arrays, nor Structs, nor Pointers Worked Inutitively
I have a far better understanding of it now, but initally I was running into massive problems even getting the metanimation and animation values /into/ the function that updates the text in that line. This came from a massive pile of bad pointer juggling, and it got so bad that I ended up asking for help on the mentoring channel of Clickspring’s Patron Discord (Patreon Link) and being taught the right way around to both constructing the arrays of structs that hold all of this data as well as getting my pointer situation sorted out.
Finally, compileable code that also doesn’t just write garbage to the screen, but there’s an issue - all the rows on the screen that are getting characters written to them are getting the same characters written to them - and in my case, only the back half of the multi-character sprite is being shown. What gives?
Third Issue: Divide-By… Eight?
The issue turned out to be extremely simple and was noticed by XerTheSquirrel of SquirrelJME Fame (full disclosure; I am a maintainer on that org, though if you loo you’ll see I do no actual work on the JME) - I was dividing the character index by 8 and needed to be dividing by 16 - the width of the font, rather than the number of cells in the desired output. Oops!
This lead to two funny coincidences:
- The tracking would work right the first time because of course it did, but;
- when we moved on to the next row we’d over-index it’s array and overwrite the previous row with the wrong characterset, and;
- this only happened instead of other stuff breaking because of coincidences about where the compiler was putting things in-memory.
That sorted, we now have… a pulsing egg that actually works!
The Problem I Skipped: What do you mean, out of memory?
For legibility reasons, I’ve also skipped over a problem I ran into during the implmentation of main_game that had less to do with the coding of the functions themselves and more to do with how I was building the structures that hold permanent game state information like:
- the three fonts, and;
- the array of structs that holds all the information about each evolutionary stage of the game, and;
- the array of structs that holds all of the normal metanimations (and eventually, its cousin for special metanimations).
The summary: I forgot about a very important phrase:
#PRAGMA PERSISTENT. The compiler expects this declaration before any symbol definition or assignment that you want to have be relatively static for the full operation of the device. Making this declaration tells the compiler that you will not be modifying (or at least not frequently modifying) the object and would you please put the silly thing in FRAM.
Because I did not include this pragma I could not get the code to fit onto the device and was tearing my hair out until I remembered running into the same issue when I was defining the splash screen bitmap, which exceeds the operating RAM of the device itself and therefore has to be read into the SPI bus very carefully when displaying it.
What comes next?
A lot, actually. Technically the active codebranch at the PETI repo has a version number instead of being POC-SOMEDATE, but the game is hardly pleasurable to play in its current state. Before I go any further with defining the systems that keep track of hunger, force evolution, play minigames, and so on, though, there is a very important task to do.
All of this stuff has to be documented, the codebase has to be linted and recommented, and I’ve gotta actually merge the changes into master.
Oh, and somewhere in there I need to put the breadboard-prototype of the back plane into practice, which I’ll probably do as a Tuesday night stream.
PETI is a major project intended to design and construct a virtual pet from Open Source Hardware and Software. If you would like to support the development of this, or any of the other projects I’m working on for Arcana Labs, and you wanted to show your support financially, your best avenue is via my Github Sponsors account or by making a one-time donation to Arcana Labs via Ko-Fi.com or through other avenues detailed here.