Week 3: Embedded Programming

Week 3 was about embedded programming, which mostly involved testing embedded programming in a simulator. I used the simulator to test out a little Rust DSL I wrote, so I’ll be able to occasionally send a message giving a new equation like t sin y 0.5 mul add to different devices and, assuming they know about a global clock t and their location y, they could evaluate it and use the values to control their parameters.

I’m getting ahead of myself. We started with the group assignment, where we looked at physical devices.

Group Assignment 🔗

Thursday afternoon, we met in the CBA lab for the group assignment. Quentin brought a bag of fun things and each used one of the microcontrollers we’re most likely to use. An RP2040 (powerful one with a friendly datasheet made by Raspberry Pi), an ESP32 (which is a good chip with wifi and newer ones have Bluetooth and don’t have the long wire that is its antenna), and a tiny little ATTiny. I think there was a SAMD board too.

Seeed produces little chips with a lot of the capacitors and resistors you need, and you don’t need to mess with surface mounting the tiny little microcontrollers. But if you need more pins, you can find layouts by other folks. (Also at recitation, Anthony warned us about some flaws of seeed’s RP4020 chip: if you plug in a battery and the USB at the same time, bad things can happen. So it might be a little better to just stick with USB charging in that case.)

So, generally, you need a programmer chip so you can put a bootloader on the chip. (Usually?) there are special pins for the programmers. But! Some controllers are fancy, and either have a bootloader in silicone (e.g. RP2040) or at the very least have USB capabilities so you don’t need to plug into the pins. Quentin also has special boards that can help you flash even the old controllers.

(thanks for the picture, Michelle!)

The Arduino IDE is a useful way to install toolchains, even if you aren’t using Arduino boards or the IDE to code. So Quentin showed plugging in the ports (I think the ATTiny had serial pins, others could go straight through the USB). Some of the devices supported UF2, which turned the microcontroller into a USB drive that you could just drop programs onto.

In the last few minutes, Quentin showed how to put MicroPython on the device, and subsequently being able to interactively ask it to do math through the REPL.

Individual Assignment 🔗

I thought it would be good to use this week to get a better idea of my final project, so I know which microcontroller to check out.

Tangent: Final Project 🔗

(I originally wrote a lot here about the final project, but now that is on final.)

Back to this week 🔗

Okay, that took a bit of time. So that tells me that I probably care about the ESP32 series. So let’s check out that datasheet.

Reading a data-sheet ✅ 🔗

First of all, there’s a bunch of ESP32s. I went ahead and checked out the newest one, the ESP32-C6. It looks like Seeed has chips with these that aren’t much different in price than the ESP32-C3. I might also check out the ESP32-S3 for the main computer, which is a little more expensive but also has better specs.

I started flipping through the datasheet. One thing I realize is that this document is only 80 pages long instead of the 644 pages of the RP2040 datasheet. Eventually, I click on a link and end up in the ESP32-C6 Technical Reference Manual (without realizing it), which has 1360 pages. A lot of those 1300 pages are about precisely the format of the data sent.

I flip through the datasheet some more. At one point they talk about power management. Apparently, the ESP32 can switch to a different CPU for low-power mode. I wonder if this could be useful for the mode where it’s just playing lights, instead of being wirelessly programmable. (aside, it looks like the ESP32-C6 puts things like “LED PWM” in modem-sleep and light-sleep mode, but the ESP32-S3 lumps the LED PWM in with its high-power components. I don’t know if I’ll actually use LED PWM for modulating LEDs but it’s something to keep in mind).

There are a lot of different sizes of memory, but I think one I care about is the “512KB of high-performance SRAM for data and instructions”. I’m a little mindful of this, because I want to try embedded Rust for the first time, and I’m used to not worrying about the size of my programs. That said, I’m hoping that representing the display values with little functions won’t take up too much space.

It’s also cool to realize that there are specialized parts on microcontrollers for things like AES or random number generators. I wonder if that’s why I sometimes have to install special packages in brew instead of pip/cargo when I’m interfacing with audio or OpenSSL or…

There’s a page with the “absolute maximum ratings” that lists the maximum/minimum mA/V/temperature the device can take.

The end of the datasheet has a consolidated pin overview, which seems like it could be useful and complete because I’ve heard the stylish colorful one we see is only a subset of their features.

Cool! So I know now where I would find specs for data if a library was not in Rust or something (in the technical reference). I also know where to find the complete pin overview.

Okay, so I count that as reading a datasheet as ✅.

Simulated Embedded Programming 🔗

I started gradually here. I first replicated the video. I choose the MicroPython-flashed controller, copy the MicroPython script, connect a button and the LED (with its resistor), and yep, it does what the video does. A little later I came back and needed to make an LED glow and realized I didn’t know what I had just learned, but I think it sunk in the second time.

You can see in the IO that the button does turn on-off-on-off-on-off really fast. I’m not sure if this is something with the app, or if it’s actually being realistic. From recitation, we learned that dropping in a capacitor can help smooth out signals.

I hop over to falstad now and try to get my bearings. The program starts with a Serial.begin(115200). I remember reading and then forgetting what this meant, so I looked it up again. It’s Baud, and apparently here are some common settings. Oh, DMX is a really big number on that table, I’ve been hearing about DMX more often as I’ve hung out with lighting folks, that’s fun. Anyways…

I try to replicate an example from recitation with the button and capacitor slowing it down.

First, I set up a button with no capacitor.

And attach a scope, I guess to the weird dead-end wire. I’m kind of confused by that, but it outputs the square waves.

(Sadly it’s not the jittery behavior I saw in WOKWI and am aiming to smooth out, but it’ll do).

Now I attach a capacitor, which changes the shape of the curve into a little shark fin. The shape of the fin changes depending on what size capacitor I use.

I go back to WOKWI and realize they have a Rust version of the chip. The ESP-C6 example isn’t building, but the ESP-C3 one is. I add a not-very-exciting second LED light, mostly to force myself to look at the code: the libraries are a lot of esp-specific ones, and I see in the Cargo.toml they use features to say which chip you’re on. I also know the #![no_std] at the top of the file means I won’t get to use the Rust standard library, and that will be very interesting to navigate.

So thinking a little more, the thing I want to test today is whether I can serialize a simple function and then evaluate it in an embedded environment. I normally use the evalexpr library, which has ways to parse a string like “x + y” and build up variable contexts and such. When I try to include evalexpr in Cargo.toml, it immediately starts complaining about the standard library missing. (I realized later that there are templates in WOKWI that include the std, but I don’t think I’ll need more than core.)

I first poke to see if there are feature flags and think about forking the project. But… it’s.. not too hard to write a Reverse Polish notation parser for a calculator (I don’t need it to define functions, or be Turing complete, or…). The function will be sent from another one of the programs within my control, so I can make sure that it’s always sent perfectly, and if needed, I could use one of the beefier machines (web server, laptop) to provide the user a more detailed error messages. And Wikipedia linked to a cute little algorithm to turn infix into RPN, which I can try out.

I put the basic DSL together here with things like addition and atan. I copied it into one of the simplified Rust templates. I needed to include something to do math (I choose micromath) because apparently no_std gets rid of things like trigonometry, which we love to use in live coding.

I copied over the DSL, then added a few more LEDs to the example template, and then drop in some code to control those LEDs using this code.

let maybe_expr = TinyExpr::from_str("x sin y 0.5 mul add");

if let Ok(expr) = maybe_expr {
    let mut time = 0.0;
    loop {
        delay.delay_millis(10);

        if expr.eval(time, -1.0).unwrap_or(0.0) > 0.0 {
            led1.set_high();
        } else {
            led1.set_low();
        }
        
        if expr.eval(time, 0.0).unwrap_or(0.0) > 0.0 {
            led2.set_high();
        } else {
            led2.set_low();
        }

        if expr.eval(time, 1.0).unwrap_or(0.0) > 0.0 {
            led3.set_high();
        } else {
            led3.set_low();
        }

        time += 0.01;
    }
} else {
    // hrm, hack because I don't have `.unwrap()` or anything, should find something better like just crashing..
    loop {
        println!("failed to initialize :(");
 delay.delay_millis(10000);
 }
}

So now every 10 milliseconds, it’ll check sin(time) + 0.5 * y > 0.0, where y is -1.0, 0.0, or 1.0 depending on the LED. The idea is that I can encode the message x sin y 0.5 mul add and send it wirelessly from the main computer to the components. (This is really similar to how I think about this, where each shape knows its location, and has a function that uses its location and time to decide its orientation.)

Implementing things in embedded Rust was interesting. A few of the things I reach for (like unwrap or print statements) need to be done differently, and I’m still not totally sure how to do that. ChatGPT answered my confused questions (but I authored the code). For debugging, I ended up just removing the #![no_std] flag and printing things.

Another thing missing is variable-length lists (e.g. Vec), which you can get by including another package. I need to check in on the trade-offs first. In the meantime, I have a fixed number of tokens I accept (64), and set input variables (I only included x and y, but I should at least also include t to represent the clock.) While it would be nice for both of these to be flexible, I imagine I’ll run into other factors when I get to the wireless message size and how slow it is to evaluate long functions. I’ll wait to do some profiling once I have everything hooked up and then make some decisions.

Appendix: Homework description 🔗

group assignment:
    demonstrate and compare the toolchains and development workflows
        for alternative embedded architectures
individual assignment:
    browse through the data sheet for your microcontroller
        write a program for a microcontroller to interact
            (with local input &/or output devices) and communicate
            (with remote wired or wireless connections), and simulate its operation
extra credit: test it on a development board
extra credit: try different languages &/or development environments

You've found my documentation of the Fall 2024 class for how to make (almost) anything at MIT. At the time of writing, I am a first year MAS student (MIT Media Lab) in the Future Sketches lab. When I'm not making almost anything, I'm making specific other things, like making cool-looking things with code, which I sometimes pen plot or live code.