Following up on my previous posts where I was testing basic async servers and such. I've almost done porting my companies firmware over to Nim based on the nesper library.. and it's great!
The firmware is about 10k lines of Nim code (~2.2k lines in the app and ~8.7k being the Nim imports for the various esp-idf C/C++ libraries). It compiles the Nim code in a few seconds and produces a final binary that's 810kB. That's using MPack, JSON, RPC, Net, Sockets and other libraries with lots of sequences, strings, and tables all over and not optimizing so far at all. I was worried with C++ that 1.5mb might be tight... I also have about 170kB of free RAM after setting up 3 sockets, all the SPI's, JSON parsing. Multi-core / multi-threading has mostly just worked with the channel's and locks as well. It's been a joy.
I'm curious how far one could push it with a GUI stuff... ;)
There are a few pain points I've run into, like figuring out whether to use destructors vs finalizers to automatically free ESP-IDF resources. I've also run into a few more crashes than expected when I don't properly initialize some piece of memory when using ref's (though the stack traces with some repr printing have made it trivial to trace down). The only other oddity is nameless C unions with structs are a bit tricky (though workable).
Next step: encryption/tls on the sockets? Eih. ESP-IDF uses mbed TLS, but not sure how that'll work with the net libraries.
@moigagoo thanks for asking! It's tricky when you don't even realize which details aren't common knowledge.
For a complete noob in the area, could you please add a little bit more information about what the firmware does and what it's for? All the things you wrote sound very exciting, and it's a shame a can't understand most of it. I'd really like to share your joy :-)
That's a bit tricky. I can't detail all the fun magic of the firmware due to proprietary stuff. However, overall it's a firmware to run high speed data collection. It uses a high speed ADC (analog digital converter) which is what converts voltages to digital readings. It also uses a DAC (digital to analog) that produces an output voltage. The firmware essentially creates a voltage output function and records the responses. The ADC we use runs at about 100kHz, not super fast but fast enough to be tricky.
That means the ESP32 needs to be able to communicate with the ADC every 8-100us consistently for up to a minute or so and can't be automated in hardware (in this case). For context on the ESP32 most operations take 5-40ns each, though a floating point division can take 200-600ns, so you need pretty fast code to keep up as operations can easily add up. It also needs to communicate with the ADC and DAC over a standard called SPI that uses 4 wires to communicate, but that can take a 2-6us just by itself. Oi! So you really need low level fast code for this. In this case I pre-allocate the memory, and convert raw bytes to floats later. It's a bit trickier than perhaps average Nim code, but not really hard either. Profiling is the key here.
For our companies use case, we want to collect a long stream of data at moderately fast speeds, so the code can't be interrupted for items like servicing network requests. However, TCP/IP stacks can weird out if you pause them for 30+ seconds. So I chose the ESP32 for this project since it has two processor cores! It was designed for this purpose. Nim fits this bill perfectly since communicating between "threads" in regular C/C++ can be a pain. You end up needing to figure out who owns what memory, who frees it, etc. Dealing with locks is also a pain, but with Nim it's trivial to write a template that ensure you acquire and release locks correctly. It's also useful for using the various hardware API's for the ESP32 (e.g. ensure the SPI commands clean up properly). It's saved me a lot of headaches on that angle so far.
The last point that is sparking joy for me is how Nim handles all of this in a few hundred kB of flash (e.g. program disk size). C++ code tends to bloat really quickly when you convenient standard library things. The code for a C++11 RPC server I tried previously that provided a nice interface that automatically de-serialized JSON was a few megabytes of just source code. I copied and tweaked the nim-json-rpc library and got a similar interface in just a few hundreds lines of Nim macros! You can write compact C++ code but you tend to loose a lot of the higher level niceties (e.g. exceptions) on embedded due to program size limitations. 1 megabyte of flash storage normally goes pretty quickly.
That means I can write a lot of "nice to have features" in the firmware (like pretty printing error codes) that normally I'd have to be fairly picky in using. I can also fit a lot more RPC calls that allow the primary controller (running Elixir/Nerves) to investigate possible hardware issues/failures. In normal C code, you'd also need to spend a lot of effort serializing your data so it quickly becomes less worthwhile to add these interfaces.
Hope that's explains a bit! Sorry it's a bit wordy, to quote Blaise Pascal, if I had more time I'd make it shorter. ;)
By baremetal, do you mean Arduino? I believe esp Arduino actually lives on top of FreeRTOS -- it just hides it. If so you might be able to use printf in a panic override. I do miss some of the Arduino API's for SPI/I2C that are just much simpler than the raw esp-idf/freertos ones.
So I'm using makeEspArduino + this library to power my LED matrix + https://github.com/espressif/arduino-esp32. I can see references to FreeRTOS there so quite possible that I am indeed using FreeRTOS without even knowing, that's a shame.
Are you mostly doing toy projects or work stuff?
Toy project that I hope I can turn into a hobby product :)
Some more context: Smaller embedded systems like these don't have an operating system as we know it. They don't even have the ability to "launch programs". Instead, you statically link your application code with the libraries provided by the vendor — everything from malloc to I/O to storage to networking — producing a raw binary executable. That gets written to the device's firmware flash storage. Then when the device powers up, the CPU literally just jumps to the entry point of the firmware and starts running your code.
For obvious reasons these systems tend to be programmed in C or C++. But there's a lot of interest in using newer languages that compile to native code without needing a big runtime library, primarily Rust.
(And actually there is a stripped-down version of Python called "MicroPython" or "CircuitPython" that runs on a lot of these systems. Not the really dinky 8-bit ones, but the ones with 32-bit CPUs.)
There are a few pain points I've run into, like figuring out whether to use destructors vs finalizers to automatically free ESP-IDF resources.
Could you elaborate? I've only been using destructors (=destroy functions) in my code. I've been assuming that finalizers are older/deprecated. If there are any benefits of finalizers, I'd love to know.
For obvious reasons these systems tend to be programmed in C or C++. But there's a lot of interest in using newer languages that compile to native code without needing a big runtime library, primarily Rust.
@snej thanks for expanding the context. Maybe there needs to be a "embedded Nim" website. ;) I was going to include reasons I didn't choose Rust, but felt it was already too long. Rust has a lot of interest and momentum in embedded, but has some drawbacks.
The primary being drawback being that Rust relies on LLVM, which doesn't support many embedded targets in particular the ESP32. There's experimental Xtense/ESP32 support for LLVM, but then also have to figure out how to use the existing support libraries for the target (if possible). There's a lot of C code and weird build systems in the embedded world. Nim is excellent in this regard! Pre-compile to C and plug it into the existing system and hit the ground running with debuggers, real time OSes, etc.
The other big impediment is that the Rust standard library is very large and monolithic which isn't good for embedded (it hardcodes a lot of OS stuff I guess). There's been big improvements since I last toyed with Rust in that area, but I believe Nim's approach of only compiling what's strictly required/called results in lighter/smaller compiled code for embedded (not faster code per se).
So Nim provides many of the benefits of Rust, but is usable on most any embedded device or ecosystem. I find Nim less tedious to program too.
Could you elaborate? I've only been using destructors (=destroy functions) in my code. I've been assuming that finalizers are older/deprecated. If there are any benefits of finalizers, I'd love to know.
@snej, destructor's only appear to work for objects though the docs say a finalizer will be auto-generated for refs of objects with destructors. Finalizers probably won't "go away" soon since they're used for ref's but might not be used directly as much. More importantly, a recent post Returning objects from func with ARC did a good job showing how =destroy & =copy both get called and can result in a piece of "data" being destroyed multiple times. In this case, I want to called a special cleanup function for a C data structure but only once at the end, so finalizers made more sense. Though you could setup =destroy/=sink/=copy to do it too only once but that's beyond me at the moment...
Right now you can't make destructors for refs directly, but you can do this (and IIRC this is the "right" way of doing it):
type
MyObj = object
MyRef = ref MyObj
proc `=destroy`(x: var MyObj) =
echo "Destroying myobj!"
block:
let a = MyRef()
I don't have any in-depth ESP experience, so forgive me if I'm asking a stupid question, but what do you mean by " (and it's baked into Nim's stdlib now!)" regarding FreeRTOS and nim?
Good question, the ESP32's use FreeRTOS which is a real time operating system (RTOS). Nim's standard library has a freertos option now, so you can do --os:freertos. It also configures the stdlib's network code to use LwIP which is an embedded TCP/IP stack. What this means is that you can use Nim's nice set of networking libraries, like asyncnet, selectors, etc. It gives a nice modern high level API for networking on any device supporting FreeRTOS & LwIP.
FreeRTOS accounts for somewhere around 30-60% of the RTOS market share. Amazon purchased it a few years ago and re-licensed it under a more liberal MIT license (before it was either GPL/Commercial) and use it as the basis of their AWS IoT platforms.
"right" way of doing it
But introducing the Value Type just because the destructor needs it is a bit ugly. For gintro we use code like
grep -A14 "ToggleButton\* =" ~/.nimble/pkgs/gintro-#head/gintro/gtk4.nim
ToggleButton* = ref object of Button
when defined(gcDestructors):
proc `=destroy`*(self: var typeof(ToggleButton()[])) =
Are there disadvantages, may ToggleButton()[] allocate a temporary object?
More importantly, a recent post Returning objects from func with ARC did a good job showing how =destroy & =copy both get called and can result in a piece of "data" being destroyed multiple times.
I found it to be almost indecipherable, I would appreciate specific bug reports.
In theory, both finalizers and =destroy are on equal footing. Arguably finalizers on ARC have spec bug though (need to write an RFC, relax, it probably does not affect your code).
I found it to be almost indecipherable, I would appreciate specific bug reports.
My bad, writing while tired... There isn't a bug, but rather I don't know how to ensure that "move" works properly for my use case. As I understand it the docs say that a default =sink will be generated that does a copy/destroy unlike say seq's that will move the data pointer to a new object.
Say I have an object that contains a number, then I put the object in an array the default sink action would be to copy the number into a new object and destroy the old object, right? That's fine generally, and appears to happen in cases "four" and "seven" of the example in this post where =destroy gets called twice with the same values.
However if the number in the object is a pointer or resource handle then that'll cause issues. Perhaps if the resource is a Nim ref it would handle it properly, but I'm wrapping an allocated resource from a C library. Obviously you can implement strings and seq's using =sink and moving the same pointer without issue, but from only the docs I'm not confident in how to do it. So I went with ref object since I'm confident the finalizer will only be called once and I can then safely call delete(myResource) at that time.
So for my case, =destroy by itself isn't equivalent to a finalizer, but it'd require proper move behavior which (I think?) requires a custom =sink? Though @Steven_Salweski, your method might work nicely. I still use a ref, but can still make the destroy operation for it without an explicit object.
Good question, the ESP32's use FreeRTOS which is a real time operating system (RTOS). Nim's standard library has a freertos option now, so you can do --os:freertos. It also configures the stdlib's network code to use LwIP which is an embedded TCP/IP stack. What this means is that you can use Nim's nice set of networking libraries, like asyncnet, selectors, etc. It gives a nice modern high level API for networking on any device supporting FreeRTOS & LwIP.
FreeRTOS accounts for somewhere around 30-60% of the RTOS market share. Amazon purchased it a few years ago and re-licensed it under a more liberal MIT license (before it was either GPL/Commercial) and use it as the basis of their AWS IoT platforms.
Thanks, this is really cool, I will have to tinker with it sometime. I have an ESP somewhere in a box...:)
Thanks a lot! Now I want to wet my feet in microcontroller programming with Nim :-) Any tips or tutorials for a beginner?
Oops, sorry missed your question. Nesper is based on the ESP32 programming sdk (called ESP-IDF). They have a pretty good programming guide though you might read through a shorter guide overall. Follow those, then follow the instructions in the Nesper readme. Sparkfun has some good overalls for embedded circuits/microcontrollers.