< Return to Hobbies

Building an Embedded Rust Stack for ESP32

I wanted to take my Rust skills into the Embedded eco-system and for a good long time, spent way too much time trying to re-invent a certain wheel (nothing to do with Rust, though) - more to do with choosing the 'right' MCU SOC for the job.
RTC module is the smaller board at the top (which has a small battery) and connects via I2C with the ESP32 that's on the smaller board in the M.2 socket.
My biggest issue is I wanted it to be capable of everything and anything, but in the embedded world, constraints are always part of the designing stage.  It's hard to say optimise for work-horse performance and say efficiency in terms of battery life; keeping quiescent draw to a minimum but running many sensors at the same time etc. You always have to make a compromise.  However, extendability and future expansion are important. 
I think I'll detail the journey that ultimately led me to my current choice in another blog post, however I've come to simply embrace the Sparkfun MicroMod "framework".  Benefits are obvious:
  1. Designing and planning have already been done by a reputable team.
  2. MCU agonistic pin-out planning has already been done.  Every Sparkfun Hookup guide details the pin-outs etc.
  3. Sparkfun share all their designs in Eagle (now part of Autodesk), which is great as I have been using Eagle for almost 21 years!
The Sparkfun MicroMod eco-system boasts a wide range of MCU options as well, including STM32, RP2040 and even Nordic's nRF52840.
I'm quite fond of Nordic's nRF52840 as I've taken a deep dive in to RAK's Wisblock firmware for the RAK4631 and made tweaks to create a "dummy" Helium Console sensor device. Troubleshooting LoraWAN connectivity in the Wisblock codebase helped me take a deep-dive into FreeRTOS and its interrupt handling with mutexes/semaphores etc.

Initial steps

My first attempt at setting up the basics has already been shared on Github, not so much by intentional planning, but because someone in the Discord #embedded channel wanted to take a look.
I would like to give it a better name than "rust-esp32-xtensa-blinky", but this codebase is sort of a "Blinky" like project, well for more advanced basics.  If you dare to take a look at the code, please bare with me - there's A LOT of cleaning up that I mean to do.
A large portion of the code in main.rs needs to shift into separate modules, and code in the sensors section needs to separate out the I2C related code from the actual sensor code.
Check out the README on Github for all the details, but the coolest bit that I've got working so far is SNTP (Network time protocol) and fallback to the RTC (Real-time clock).
At first boot, the ESP32's local time starts at the "UNIX time since epoch" timestamp of 1970, and it will first copy the RTC time, to the system clock.  This only happens once as I've got a conditional checking for the 1970's date.
I (13303) rust_esp32_blinky: Reading RTC Sensor I (13303) esp_idf_svc::sntp: Initializing I (13303) esp_idf_svc::sntp: Initialization complete I (13313) rust_esp32_blinky: SNTP initialized, waiting for status! W (13813) rust_esp32_blinky: SNTP attempted connection 200000 times. Now quitting. _time: [66, 1, 31, 11, 8, 16, 11, 22] I (13813) rust_esp32_blinky: Updated System Clock from RTC time: 1970-01-01T00:00:13+00:00 / sec: 1668598261, usec: 0 I (13823) rust_esp32_blinky: timestamp: 1668598261 _time: [67, 2, 31, 11, 8, 16, 11, 22] I (14833) rust_esp32_blinky: Local time: Wednesday 2022-11-16 11:31:02 UTC Universal time: RTC time: Wednesday 2022-11-16 11:31:02 UTC Time zone: System clock synchronized: NTP service: RTC in local TZ: NTP status: SNTP_SYNC_STATUS_RESET
SNTP's update has a callback, which then (i) updates local system time and (ii) the main loop monitors a global flag (via a Mutex lock) and also updates the RTC clock.
I (8813) rust_esp32_blinky: Reading RTC Sensor I (8813) esp_idf_svc::sntp: Initializing I (8823) esp_idf_svc::sntp: Initialization complete I (8823) rust_esp32_blinky: SNTP: Enabled I (8833) rust_esp32_blinky: SNTP initialized, waiting for status! W (9323) rust_esp32_blinky: SNTP attempted connection 200000 times. Now quitting. _time: [83, 34, 22, 12, 4, 15, 11, 22] I (6447) rust_esp32_blinky: Updated System Clock from RTC time: 1970-01-01T00:00:05+00:00 / sec: 1668 514953, usec: 0 I (6457) rust_esp32_blinky: timestamp: 1668514953 _time: [83, 34, 22, 12, 4, 15, 11, 22] I (7457) rust_esp32_blinky: Local time: Tuesday 2022-11-15 12:22:34 UTC Universal time: RTC time: Tuesday 2022-11-15 12:22:34 UTC Time zone: System clock synchronized: NTP service: RTC in local TZ: NTP status: SNTP_SYNC_STATUS_RESET // later on... I (10547) rust_esp32_blinky: Updated RTC Clock from local time: 2022-11-15T12:22:38+00:00 I (10547) rust_esp32_blinky: [OK]: Updated RTC Clock from Local time _time: [1, 39, 22, 12, 4, 15, 11, 22] I (11557) rust_esp32_blinky: Local time: Tuesday 2022-11-15 12:22:39 UTC Universal time: RTC time: Tuesday 2022-11-15 12:22:39 UTC Time zone: System clock synchronized: NTP service: RTC in local TZ: NTP status: SNTP_SYNC_STATUS_COMPLETED
I now need to add another interrupt (or more than one possibly), to ensure that (i) SNTP updates continue to occur and (ii) if the System time ever drifts behind the RTC, we perform a fallback to RTC, but still treat SNTP time as canonical. Sounds like a complex dance, and it is.  Testing it is harder too.