Program the ESP32 using Nim! This library builds on the esp-idf. Nim now has support for FreeRTOS & LwIP. Combined with the new ARC garbage collector makes Nim an excellent language for programming the ESP32.
See Releases for updates.
This is a Work in Progress (TM), however it's already being used in a shipping hardware project. However, it may still require understanding the underlying ESP-IDF SDK for various use cases.
Note: It's recommended to use the ESP-IDF.py v4.0 branch (as of 2020-11-24). Branch v4.1 has multiple serious bugs in I2C.
This code shows a short example of setting up an http server to toggle a GPIO pin. It uses the default async HTTP server in Nim's standard library. It still requires the code to initialize the ESP32 and WiFi or ethernet.
import asynchttpserver, asyncdispatch, net
import nesper, nesper/consts, nesper/general, nesper/gpios
constMY_PIN_A*=gpio_num_t(4)
MY_PIN_B*=gpio_num_t(5)
var level =falseprocconfig_pins() =MOTOR1_PIN.setLevel(true)
prochttp_cb*(req: Request) {.async.} =
level =not level echo"toggle my pin to: #", $level
MY_PIN_A.setLevel(level)
await req.respond(Http200, "Toggle MY_PIN_A: "&$level)
procrun_http_server*() {.exportc.} =echo"Configure pins"
{MY_PIN_A, MY_PIN_B}.configure(MODE_OUTPUT) MY_PIN_A.setLevel(lastLevel)
echo"Starting http server on port 8181"var server =newAsyncHttpServer()
waitFor server.serve(Port(8181), http_cb)
TLDR; Real reason? It's a bit of fun in a sometimes tricky field.
I generally dislike programming C/C++ (despite C's elegance in the small). When you just want a hash table in C it's tedious and error prone. C++ is about 5 different languages and I have to idea how to use half of them anymore. Rust doesn't work on half of the boards I want to program. MicroPython? ... Nope - I need speed and efficiency.
The library is currently a collection of random ESP-IDF libraries that I import using c2nim as needed. Sometimes there's a bit extra wrapping to provide a nicer Nim API.
Caveat: these features are tested as they're used for my use case. However, both Nim and the esp-idf seem designed well enough that they mostly "just work". PR's are welcome!
Supported ESP-IDF drivers with Nim'ified interfaces:
- Nim stdandard library support for most basic POSIX network API's!
- Most of the basic
FreeRTOS.hheader - NVS Flash
- UART
- SPI (don't mix half-duplex & duplex devices)
- I2C
Other things:
- Nim standard library wrapping of FreeRTOS semaphore's, mutexes, etc
- include
pthreadin your CMakeLists.txt file and use Nim's POSIX lock API's
- include
- Nim support for
xqueueand other "thread safe" data structures- Raw C Wrappers exist, see `rpcsocket_queue_mpack.nim for proper usage. Nim Channel's appear to work as well.
- Nim standard library support for FreeRTOS tasks using thread api's
- include
pthreadin your CMakeLists.txt file and use Nim's POSIX Pthread API's
- include
Things I'm not planning on (PR's welcome!)
- I2S
- PWM
- LCDs
- Built-in ADC
- Install ESP-IDF (version 4.0 is recommended for now, set the
-d:ESP_IDF_V4_0) - Install Nim 1.4+ with
asdforchoosenim - nimble install https://github.com/elcritch/nesper
- It's recommend to copy
nesper/esp-idf-examples/simplewifiexample project initially, to get the proper build steps. - Nesper wrapper API names generally match the C names directly, usually in snake case
- FreeRTOS functions usually are camel case and start with an
x, e.g.xTaskDelay - These api's are found under
nesper/esp/*ornesper/esp/net/*, e.g.nesper/esp/nvs
- Nesper Nim friendly api, usually in camel case
- These api's are found under
nesper/*, e.g.nesper/nvs
The async code really is simple Nim code:
import asynchttpserver, asyncdispatch, net
var count =0proccb*(req: Request) {.async.} =inc count
echo"req #", count
await req.respond(Http200, "Hello World from nim on ESP32\n")
# GC_fullCollect()procrun_http_server*() {.exportc.} =echo"starting http server on port 8181"var server =newAsyncHttpServer()
waitFor server.serve(Port(8181), cb)
whenisMainModule:
echo"running server"run_http_server()import nesper, nesper/consts, nesper/general, nesper/gpios
constMOTOR1_PIN*=gpio_num_t(4)
MOTOR2_PIN*=gpio_num_t(5)
procconfig_pins() =# Inputs pins use Nim's set `{}` notationconfigure({MOTOR1_PIN, MOTOR2_PIN}, GPIO_MODE_INPUT)
# or method call style:
{MOTOR1_PIN, MOTOR2_PIN}.configure(MODE_INPUT)
MOTOR1_PIN.setLevel(true)
MOTOR2_PIN.setLevel(false) import nesper, nesper/consts, nesper/general, nesper/spis
proccs_adc_pre(trans: ptr spi_transaction_t) {.cdecl.} =...proccs_unselect(trans: ptr spi_transaction_t) {.cdecl.} =...procconfig_spis() =# Setup SPI example using custom Chip select pins using pre/post callbacks let
std_hz =1_000_000.cint()
fast_hz =8_000_000.cint()
varBUS1=HSPI.newSpiBus(
mosi =gpio_num_t(32),
sclk =gpio_num_t(33),
miso =gpio_num_t(34),
dma_channel=0,
flags={MASTER})
logi(TAG, "cfg_spi: bus1: %s", repr(BUS1))
varADC_SPI=BUS1.addDevice(commandlen =bits(8),
addresslen =bits(0),
mode =0,
cs_io =gpio_num_t(-1),
clock_speed_hz = fast_hz, queue_size =1,
pre_cb=cs_adc_pre,
post_cb=cs_unselect,
flags={HALFDUPLEX})Later these can be used like:
constADC_READ_MULTI_CMD=0x80ADC_REG_CONFIG0=0x03procread_regs*(reg: byte, n: range[1..16]): SpiTrans=let read_cmd = reg orADC_READ_MULTI_CMD# does bitwise orreturnADC_SPI.readTrans(cmd=read_cmd, rxlength=bytes(n), )
procadc_read_config*(): seq[byte] =var trn =read_regs(ADC_REG_CONFIG0, 2)
trn.transmit() # preforms SPI transaction using transaction queueresult= trn.getData()
See more in the test SPI Test or the read the wrapper (probably best docs for now): spis.nim.
Nim is a flexible language which compiles to a variety of backend "host" languages, including C and C++. Like many hosted languages, it has excellent facilities to interact with the host language natively. In the embedded world this means full compatability with pre-existing libraries and toolchains, which are often complex and difficult to interface with from an "external language" like Rust or even C++. They often also require oddball compilers, ruling out LLVM based lanugages for many projects (including the ESP32 which defaults to a variant of GCC).
Nim has a few nice features for embedded work:
Language:
- High level language and semantics with low level bit fiddling and pointers
- Flexible garbage collector or manual memory management
- ARC GC allows using native-C debuggers, meaning any embedded debuggers should work too!
- ARG GC doesn't use locks, and utilizies move semantics -- it's fast
- Simple FFI's around to import and/or wrap C/C++ libraries
- Async/Event support
- Real hygenic language macros, and collections with generics!
- Very flexible and hackable standard library!
Libraries:
- Simplified network wrappers around native sockets (i.e. use
selectw/o a PhD) - Sane standard library, including JSON, datetime, crypto, ...
- Efficient compiler that eliminates un-needed code (i.e. json support using a few extra kB's)
- Package library manager
Compiler:
- Fast compilation, generated C compiles fast
- Deterministic exception handling using a non-malloc friendly goto technique
- Object-oriented like programming that's not based on vtables
There are a few cons of Nim:
- Lack of documentation for many parts of the standard library
- Understanding the differences between stack/heap based objects is a bit tricky
- Compiler options are often incompatible and can require some experimentation
- Small community (e.g. lack of some libraries)
- You likely won't get to use it at XYZ Megacorp
- It will require some pioneering!