For a university module, we had to implement "Tic Tac Toe Extended" (also called "Ultimate Tic Tac Toe") in MATLAB and run it on a microcontroller.
The intention was very likely to use an Arduino board, and control the hardware directly using the matlab board support packages.
I wanted instead to create a more "industrial" solution, which means developing just the core logic in MATLAB, exporting it to C, and then embedding it into a host firmware that controls the hardware.
Official guidance was that either way was ok, so long as any part was still developed in MATLAB (it's a MATLAB course, after all).
Rules:
"Ultimate Tic Tac Toe" is played on a grid of 3x3 Tic Tac Toe sub-boards, winning a sub-board marks it for the player, the goal is to win 3 sub-boards in a horizontal, vertical or diagonal line.
Hardware:
3x3 boards, with 3x3 cells each, means 81 cells total. Each cell is a tri-state, it can be Empty, Player1 or Player2. This makes using LEDs impractical, you would need to solder 162 LEDs by hand (and connect them to IO multiplexers). So, the easy way out was to use 16x16 neopixel matrix.
For input, we could have soldered a 3x3 button matrix, instead I used a USB keyboard connected to the MCU.
I had a few ESP32-S3 devboards lying around, I like them, and they work well with Rust (see Software), so that's what I used.
Just for comparison, ESP32-S3 has a dual-core XTENSA 240 MHz CPU, 512kB internal SRAM, 384kB internal ROM, and the module has an additional 16MB Flash and 8MB PSRAM, which I didn't end up needing. (The firmware ended up at 136kB)
An Arduino Due would have had an Atmel SAM3X8E ARM Cortex-M3, clocked at 84 MHz, with 96kB SRAM and 512kB Flash.
(The era of 8-bit microcontrollers is over. Back then, when I was in school, we used 8 bit AVRs and PICs. Honestly, I miss them.)
Software:
Over the last two years I've been learning Rust, big fan, great language, even better eco-system. Developing 8-bit MCU firmware in C was fun, 15 years ago, but now I'd rather use something ... better. A while ago I was forced to develop STM32 firmware in C using their HAL, which was painful.
So, Rust. Embedded Rust has done great leaps in the last year or so. ``embedded-hal`` was just stabilized at ``1.0.0``, before that, the breaking changes were a little annoying. If you want to get started in the embedded space using Rust, now is a great time!
The ESP32-S3 has a USB-OTG peripheral, which means it can implement a USB host or device. The dev-board has a USB-C connector. Unfortunately, software support isn't that great. ``ESP-IDF`` has USB Host support, but I wanted to use ``embassy`` with ``no_std``. There is a crate ``embassy-usb``, but it only implements the device side. The open source library ``tinyusb`` supports host on the esp32-s3, but it also depends on IDF internally. I ended up forking tinyusb and removing the IDF dependency, making it run on bare-metal.
The Neopixel matrix uses WS2812, which is a proprietary one-wire protocol for controlling daisy chained RGB LEDs. It's not SPI but it can be emulated using SPI, and there was an existing library I could use (``ws2812_spi``)
Integrating MATLAB:
I exported the function to C using matlab code and the ``codegen`` command. Then I used the ``cc`` crate in a custom build.rs to compile and link it into the main rust program.
// Set up DMA for SPIlet (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(1, 4 * 1024);let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).map_err(|err| error_with_location!("Failed to create DMA RX buffer: {:?}", err))?;let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).map_err(|err| error_with_location!("Failed to create DMA TX buffer: {:?}", err))?;let spi: esp_hal::spi::master::SpiDmaBus<'_, esp_hal::Blocking> =esp_hal::spi::master::Spi::new(peripherals.SPI2,esp_hal::spi::master::Config::default().with_frequency(Rate::from_khz(4_500)),)?.with_mosi(peripherals.GPIO21).with_dma(peripherals.DMA_CH1).with_buffers(dma_rx_buf, dma_tx_buf);static NEOPIXEL_SIGNAL: StaticCell<Signal<CriticalSectionRawMutex, Box<[RGB8]>>> =StaticCell::new();let neopixel_signal = &*NEOPIXEL_SIGNAL.init(Signal::new());