12-22-24

Over the past few days I've played around with the Zig build system as well as GCC/binutils, I've gotten to a point now where I'm able to generate the assembly for the step function, assemble it, and link it (with a generic linker script).

build.zig

Before I started diving deeper into assembly and linking, I first wanted to work on getting Zig's build system to build the assembly file automatically. I was able to do this manually from the command line thanks to some prior work from GitHub user justinbalexander. The repo, msp430-zig, provides a Makefile that turns Zig source into an MSP430 assembly file that can then be assembled by GNU binutils for MSP430. This line in the makefile was just what I needed, and after updating it to the current version of Zig (the last commit on the Makefile is over six years old), I ended up with something like this:

zig build-obj -O ReleaseSmall -target msp430-freestanding -femit-asm=msp430-test.s msp430-test.zig

The -O ReleaseSmall is important, using -O ReleaseFast produces very large files that seem to be filled with debug information. When writing basic code to see the compiler in action, I used @ptrFromInt to manually point to some address in memory, being sure to manually specify the type with the volatile keyword. Without this keyword, Zig assumes that nothing actually happens in the program and optimizes everything away. Adding this keyword forces Zig to not assume this, which makes testing a lot more convenient.

The next step in this process was to turn this command into something that could be ran in the way that you can run zig build run to build and run the desktop simulator. I took a look at tutorials, existing code, and even the build system's internals. The code that I added consists of three parts:

  1. Build the object (assembly file)
  2. Copy the object to the install directory
  3. A named step to trigger the previous two steps in order
    Each of these parts is known as a step in the build.zig file. Steps can be told to depend on one another to ensure correct execution order. So step three of this process depends on step two, which in turn depends on step one. That way when step three is run from the command line, steps one and two will run in that order.

Building the Object

Step one in this process is very similar to how the desktop executable's code is built. In fact, I used the provided code for doing that in build.zig as a guide and modified it to make it work in this context. Both the addExecutable step used for the desktop build and the addObject step used for the assembly build take four options:

  1. A name
  2. A root source file
  3. The target for the build
  4. Optimization settings
    The first two are fairly easy. I chose gol_card for the name and pointed the step at src/main-embedded.zig. The optimization is only slightly harder, being permanently coded to std.builtin.OptimizeMode.ReleaseSmall. The hardest part is the build target. Normally you are able to use b.standardTargetOptions to allow the user to pick what target they build for. However in this case, we want to lock the build down to only one target. This requires two steps. First, a query for the target platform must be built. This can be done by specifying two strings. The first string, known sometimes as a triple, contains the target architecture, OS, and ABI. Since we are not using an OS or ABI in this project, the string msp430-freestanding is used, turning it into a doubble instead of a triple. The next string specifies any special CPU features that LLVM can take advantage of. The msp430-zig repo mentions this when discussing support for hardware multipliers. I took a look at LLVM's source and came across this file that lists every feature supported by LLVM. At the moment, there are only the features for hardware multipliers (hwmult32 in our case) and one that enables the use of the extended memory extension. This extension may come into play while testing this on real hardware, which I will get into later. For now, I use the string msp430+hwmult32. It turns out that the code as of now does not actually use the multiplier, but I tried out some test code that did and managed to link it with the correct library as mention in the msp430-zig repo.

Copying The Object

One the assembly file has been built, we need to copy it to the output directory. It seems that Zig keeps output files from builds in the .zig-cache directory and will only copy over the files that you specify to the build output directory. This can be done by creating a step to copy the file using b.addInstallFile and giving it a LazyPath to the eventual location of the assembly file as well as the name of the copy, which I just chose to be gol_card.s.

Named Step

The last step is to give the previous two steps a name so that they can be run from the command line. This can be done with b.step.

Upgrading Zig

After all of this, I created a test main-embedded.zig and ran the build script to test everything out. However, I ran into issues with the resulting path of the assembly file not being populated in memory by the time the file copy step ran. What was even stranger was that the code would successfully generate the assembly in the root of the project directory when the command was run, but if the assembly file was deleted and the build was ran from cache it would not. In this case, the assembly file does not appear to even exist in the cache directory. This lead me to dig into the internals of the build system, specifically the process for handling assembly files. I eventually realized that the version of Zig that I was using, 0.13.0, was likely improperly handling the assembly files. Fortunately, a commit to fix this behavior already exists. The problem is that this commit is not included in any new tagged release of Zig (0.13.0 is the newest tagged version at this time). This required me to upgrade my Zig to a nightly build. I downloaded the newest nightly build from Zig's website, which ended up being this commit. After downloading the archive, all I needed to do was extract the folder inside. The folder is a complete installation of Zig and its standard library, and all I needed to do to start using it was to specify the new Zig binary when running commands instead of the one in my PATH.

Before I started working on my assembly problem again, I wanted to make sure my desktop simulator build still worked. There was at least one error that was causing my code not to compile, but they were all very easy to fix. What was not so easy was raylib refusing to build. One of the commits included in my nightly build but not in version 0.13.0 had deprecated a build feature that raylib was using. The fix was a very simple find and replace, but that meant I needed to switch how I was getting my dependencies. I ended up pulling raylib-zig and raylib into my project as git submodules, and then manually changed my build.zig.zon and raylib-zig's to point to the local copies of their dependencies. In the actual raylib project, I made the changes necessary to build it with the nightly build of Zig. While raylib is a pure C project, Zig can actually be used as a build system for C. Raylib supports building using Zig; I only made changes to the build.zig that was already there. Once these changes were made, everything was working fine\

Assembly and Linking

Now that we can produce assembly code, it's time to turn that code into a file that we can flash onto the microcontroller. As I mentioned previously, the two ways that I was considering for doing this were by using a TI fork of GCC and binutils or just building binutils for the MSP430 from upstream. I tried both of these out, and for now I'm going to go with the TI version of GCC and binutils. While it is older, I didn't notice any difference in size in the output of either program. By using TI's prebuilt package, I can set up a script to download and extract everything I need and be ready to go. I was also unable to link the program when adding the code to step the simulation when using the upstream binutils. This is because the step code depends on calls to memcpy, which upstream binutils does not have an implementation for. TI's toolchain comes with such an implementation and links it automatically.

I also noticed that LLVM emits certain directives in the assembly that the assembler does not support and causes it to fail. These directives can be removed with a regex, but I also wonder if there is a way to make the assembler ignore them.

What's Next

There's still more work to do in this area. I will create a script that will check if the toolchain is installed and download it if it isn't. This reduces the chances that manual setup will be needed to build code for the microcontroller on another Linux machine. I also need to tie this script, as well as the actual assembly and linking, into the Zig build system.

I have also ordered the parts I need to create a hardware prototype of the card. I ordered the eInk display I plan in breakout board form. It's technically a Raspberry Pi HAT, but it has a connector that allows it to be used with any microcontroller fairly easily. I have also ordered two of TI's Launchpads to prototype with. One contains the MSP430FR2433 that I plan on using in the project. However, I also ordered one containing an MSP430FR2476. These two chips are very similar, except that the 2476 has 64kB of FRAM and 8kB RAM. I mentioned before that special instructions are needed to access certain portions of memory. This chip has enough FRAM that these instructions might come into play. After reading TI's user guide for the chip, I think it is possible to get away without ever using the extension since my entire program and data will fit within the range where the extension instructions are not needed.

I actually do not plan on using this specific chip in my design. I ordered a board with it as very close to the MSP430FR2475, which has 32kB of FRAM and 6kB ram. Fitting the state of the game, the program code, and the various tables and other data needed for the program to run in a bit less than 16kB is going to be a tight fit. If I find myself unable to, I want to avoid waiting for another Launchpad to come in to continue testing. I really hope to fit everything in 16kB, but I am preparing in case I cannot.

I have also taken a look at the library given by the eInk vendor. It's written in C, but I plan on porting it over to Zig. None of the code looked particularly complicated from what I saw. There are a few arrays of data in the code that will need to be stored, but they should not take up much space.

I also still need to write about my choice to use Zig as well as the reasoning behind the hardware decisions I am making.

It's been about a month since I started writing the code for the simulator. The concept of a advanced PCB business card has been floating around in my mind since late spring/early summer. Now that I've finally pulled the trigger on these parts, I feel that I'm getting closer to actually finishing this project. There's still a lot to do, namely the custom hardware design, but I'm excited to see how this project plays out.