1-7-25
Today I have reached a huge milestone in this project. Through manually uploading and dumping memory, I have gotten the MSP430FR2433 (the 16kB chip) to perform the same step computation as my desktop simulator, such that the MD5 hash of the two state files are the same.
Build Toolchain
When I left off last time, I was going to work on the build toolchain side of things. I have now gotten the system to a point where, at least on Linux, the script should go from having no dependencies downloaded to having a .elf
ready to flash. I need to have different logic at least for running the build tools on Windows, and possibly macOS too. I have not tested a complete run from no downloads to fully built, but I have tested the chain responsible for downloads and extraction and it works, at least on my machine.
I ran into a challenge with the Call Frame Information (CFI) directives that LLVM outputs into the assembly again. TI's tools do not understand them and cause the assembler to error out. This is something that I ran into before while testing, but at the time I just deleted them manually. However, I had to make a way to do the same thing automatically. I created a custom step that takes in the LazyPath
objects the build system uses and modifies the assembly file in place before it is copied to the output directory and put in the build chain. The step works fine, but does not play nice with Zig's cache system. If the step itself is not changed it works fine, but I ran into issues if I modified the step's code and didn't delete my cache. This would likely be solved by having different input and output files.
Another feature of this build script is its ability to work with multiple MCUs without having to change the script itself. I set up a build option to switch between the 3 MCUs involved in this project. This option sets an enum value, which I then convert to a string and add directly into the arguments for GCC. To my knowledge, the argument mainly chooses the appropriate linker file.
Speaking of linker files, adding them to the GCC include directory does not make them visible to GCC. I had to manually point GCC to the folder. Having to manually pass the folder enables me to make the custom ZIP file code that I modified from Zig's standard library simpler, but I have not done so yet. I also had to fall back to relying on tar
to extract the files for Linux and macOS as TI compressed them using bzip2, which Zig does not have built in support for. Using the bzip2 library to do the decompression myself was an option, but not one that I wanted to spend time on.
Getting Started With Launchpad
Now that I had my build system set up, I dove into working with the two Launchpads I had ordered. I started off with the larger MSP430FR2476. I first tried to use the included ezfet driver to talk to the board. Between issues that I will discuss next, and the fact that the driver itself is known to be experimental, it did not work. I then switched to using the tilib driver. This required me to download the library libmsp430.so
for MSPDebug to use, which TI provides on their website. I will need to incorporate this into the build system at some point. I may play around with trying to build the library from source, but it would be much easier to use TI's prebuilt version. After learning how to point MSPDebug to the file and that the 32 bit version of the library would not work on my machine, I was able to talk to the device. MSPDebug told me that my device needed an update, so I added the flag to let it do so.
The update got to ~90% before freezing for a few seconds, and then "completed". Trying to reopen the connection gave an error. I was probably worried that I had already bricked the device, and tried replugging the board. Eventually I was able to rerun the update, but the same thing happened. After many tries and a different USB cable, I did get the update to go through. The USB cable was not the issue though, as I was still running into issues with getting my system to reliably connect to the board. At first I though the issue was the voltage being supplied to the chip, but that was not it. It turns out that to reliably connect that you need to let the board be powered on for a while before you connect. I have not done experiments to find the exact time, but 30-60 seconds is probably fine.
After I had gotten the large board to work, I wanted to try the small MSP430FR2433 board too. Like the other board, it needed a firmware update to work. In my haste to get the new board to work however, I had already forgotten the need to let the board remain powered on for a time before connecting to it. I ran into the same partial upgrade issue as before, except that this time the driver wasn't able to autorecover it. At some point I switched over to my Windows install and used Code Composer Studio to recover the board, which also updated the board's firmware for me.
Going back to the larger launchpad, I used MSPDebug to flash my code onto the board and tried running it in the debugger. However, it was never reaching an infinite loop that I had set up. I went on a debugging hunt to try and find out why. To make a long story short, I found out that my code was getting reset by a watchdog timer that is automatically started on reset and must be disabled manually. I ended up figuring this out by reading TI's manual for the chip, but not before spending a lot of time manually using the debugger to step through the code, which must affect this timer differently.
Running Faster
Once I had resolved my watchdog issue, I was able to get my code to run. However, I noticed it took ~20 seconds to go through the entire board. I mainly attributed this to the fact that the MCU defaults to running at 1MHz, and decided to get it to run at its maximum 16MHz. This took quite a bit of manual reading and googling to figure out, and I didn't get it right on the first try.
The MSP430 MCUs that I use run their CPU off of a clock line called MCLK. MCLK can be sourced from a variety of inputs, but unless you want to run the CPU at <50kHz you will need to source it from DCOCLKDIV. All of the chips that I am using have a digitally controlled oscilator (DCO) that is able to generate a much higher frequency clock signal from a lower frequency input, which in this case is 32768Hz clock. This frequency can either be internally generated by the chips reference oscillator (REFO) or an external crystal. Within each of the five DCO ranges on the chips that I'm using, the DCO can generate a wide range of frequencies. The specific frequency can be changed in software. However, the correct settings to achieve a certain frequency may change based on the chip's environment. With the use of the built in frequency locked loop (FLL), users do not need to worry about manually adjusting the DCO parameters themselves. All that you need to do is configure the output frequency you want as well as if you want to have the output frequency divided down from a higher frequency for more stability.
Setting this up required doing a lot of hardware register manipulation. Zig actually has a feature called packed structs that makes tasks like this a lot easier. Packed structs let you reinterpret an integer as a series of fields that are allowed to have non-whole byte widths in a defined order.
For example: here is the packed struct corresponding to how the register CSCTL2 is layed out:
const ClockSystemControlRegister2 = packed struct(u16) {
/// FLL Multiplier.
FLLN: u10,
_unused1: u2,
/// FLL Divider.
FLLD: u3,
_unused2: u1,
};
I can then define a constant variable that points to one of these structures at the location of the register in memory:
const CSCTL2: *volatile ClockSystemControlRegister2 = @extern(*volatile ClockSystemControlRegister2, .{
.name = "CSCTL2",
});
The volatile
keyword tells Zig that writes to this register may have side effects, and the use of @extern
lets me reference the location of the register using the symbol list that TI provides in each chip's linker script. This way I do not have to hard code a selection between different addresses if they differ between the chips I use.
CSCTL2
can be used just like a normal struct from this point:
CSCTL2.FLLN = 487; // 32768 Hz * (487 + 1) is about 16MHz
CSCTL2.FLLD = 0; // Disable divider
My first go at this kept resulting in the FLL maxing out the DCO and failing to stabilize. This is because I had the divider set to /2, which meant that the FLL was trying to achieve a frequency of 32MHz behind the scenes. Disabling the divider fixed the issue and I was able to run the step function in around a second. I also enabled FRAM wait states to prevent the MCU from resetting due to accessing FRAM too fast.
Writing to the Board
After I had done this, I added code to turn off and on FRAM write protection before and after the step function. By default, writing to FRAM will be silently ignored. This is likely to prevent a bit flip or an incorrect write from destroying program code since there is no special process for writing to FRAM, unlike flash memory. Since the board lives in FRAM, I need to be able to write to there during program execution. After this change, I used MSPDebug to dump the board from memory into a file that should be equivalent to the state.bin
the desktop simulator uses to save its state. I got the MD5 hash of the state on both the MSP430 and on the desktop after being stepped once and saw that the board on the MSP430 had not updated at all. After checking the assembly code for the MSP430, I realized why. I had marked the board const
in order for the compiler to put it into FRAM, assuming that casting to remove the constant attribute would make it behave like a var
. However, Zig optimized out writes to the board. I had a problem: if I marked the board as var
the linker would fail, but marking it as const
doesn't work either.
I knew that the solution lie in the @export
builtin in Zig that allows you to customize how Zig exports symbols from your code. Marking board as var
and telling zig to place it in .rodata did not work as the linker would complain about the redefinition of .rodata to be writable. I then tried to inject my own section definition with a ;
characted to comment out the incorrect output, but all of the special characters were escaped and it didn't work. After taking a look through the linker files, I found out that there is a special section called .persistent that seems to be just what I needed. After making the board var
and telling Zig to place it there, I was able to get the match between the MSP430 and the desktop simulator that I had mentioned earlier.
Next Steps
Now that this is done, I'm going to merge hal
back into main. It definitely grew way beyond just a HAL, so it's high time to make a new branch. The first feature I plan on implementing is a loop/step reset mechanism. After a certain number of steps, or if a loop of a certain size on the board is detected, the board will be refilled with random data. I plan to do this by taking the CRC of the board, as the MSP430 processors have a built in CRC module. I also need to work on getting random data on the MSP430. My plan right now is to use a floating digital pin to get a bit of entropy at each step, and then use that entropy combined with the board state to seed one of the default Zig RNGs. After this is done, I will start working on the eInk module driver.