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 project is fairly stable and even being used in shipping hardware project. The documentation is rough and primarily only includes this README. As such, it may still require understanding the underlying ESP-IDF SDK for various use cases. However, it is useable and several people unfamiliar with ESP-IDF and embedded programming have added wrappers for esp-idf modules.
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.
git clone -b release/v4.0 --recursive https://github.com/espressif/esp-idf.git
-d:ESP_IDF_V4_0
or -d:ESP_IDF_V4_1
nimble install https://github.com/elcritch/nesper
or for the devel branch nimble install 'https://github.com/elcritch/nesper@#devel'
)nimble init --git esp32_nim_example
requires "nesper >= 0.6.1"
# includes nimble tasks for building Nim esp-idf projects
include nesper/build_utils/tasks
bin
option like bin = @["src/esp32_nim_example"]
as this will override the nimble esp_build
and result in a broken idf.py build. nimble esp_setup
to setup the correct files for building an esp32/esp-idf project nimble esp_build
to build the esp-idf projectidf.py -p </dev/ttyUSB0> flash monitor
Notes:
nimble esp_build
will both compile the Nim code and then build the esp-idf projectnimble esp_compile
to check your Nim code worksnimble esp_build --clean
to force a full Nim recompilenimble esp_build --dist-clean
to force a full Nim recompileThis 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
const
MY_PIN_A* = gpio_num_t(4)
MY_PIN_B* = gpio_num_t(5)
var
level = false
proc config_pins() =
MOTOR1_PIN.setLevel(true)
proc http_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)
proc run_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 no 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:
FreeRTOS.h
header Other things:
pthread
in your CMakeLists.txt file and use Nim's POSIX lock API'sxqueue
and other "thread safe" data structures
pthread
in your CMakeLists.txt file and use Nim's POSIX Pthread API'sThings I'm not planning on (PR's welcome!)
This is the more manual setup approach:
nesper/esp-idf-examples/simplewifi
example project initially, to get the proper build steps.
git clone https://github.com/elcritch/nesper
cp -Rv nesper/esp-idf-examples/simplewifi/ ./nesper-simplewifi
cd ./nesper-simplewifi/
make build
(also make esp_v40
or make esp_v41
) x
, e.g. xTaskDelay
nesper/esp/*
or nesper/esp/net/*
, e.g. nesper/esp/nvs
nesper/*
, e.g. nesper/nvs
The async code really is simple Nim code:
import asynchttpserver, asyncdispatch, net
var count = 0
proc cb*(req: Request) {.async.} =
inc count
echo "req #", count
await req.respond(Http200, "Hello World from nim on ESP32\n")
# GC_fullCollect()
proc run_http_server*() {.exportc.} =
echo "starting http server on port 8181"
var server = newAsyncHttpServer()
waitFor server.serve(Port(8181), cb)
when isMainModule:
echo "running server"
run_http_server()
import nesper, nesper/consts, nesper/general, nesper/gpios
const
MOTOR1_PIN* = gpio_num_t(4)
MOTOR2_PIN* = gpio_num_t(5)
proc config_pins() =
# Inputs pins use Nim's set `{}` notation
configure({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
proc cs_adc_pre(trans: ptr spi_transaction_t) {.cdecl.} = ...
proc cs_unselect(trans: ptr spi_transaction_t) {.cdecl.} = ...
proc config_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()
var BUS1 = 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))
var ADC_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:
const
ADC_READ_MULTI_CMD = 0x80
ADC_REG_CONFIG0 = 0x03
proc read_regs*(reg: byte, n: range[1..16]): SpiTrans =
let read_cmd = reg or ADC_READ_MULTI_CMD # does bitwise or
return ADC_SPI.readTrans(cmd=read_cmd, rxlength=bytes(n), )
proc adc_read_config*(): seq[byte] =
var trn = read_regs(ADC_REG_CONFIG0, 2)
trn.transmit() # preforms SPI transaction using transaction queue
result = trn.getData()
See more in the test SPI Test or the read the wrapper (probably best docs for now): spis.nim.
import nesper, nesper/esp_vfs_fat
var
base_path : cstring = "/spiflash"
s_wl_handle : wl_handle_t = WL_INVALID_HANDLE
mount_config = esp_vfs_fat_mount_config_t(format_if_mount_failed: true,
max_files: 10, allocation_unit_size: 4096)
err = esp_vfs_fat_spiflash_mount(base_path, "storage", mount_config.addr, s_wl_handle.addr)
if err != ESP_OK:
echo "Failed to mount FATFS."
else:
echo "FATFS mounted successfully!"
writeFile("/spiflash/hello.txt", "Hello world!")
echo readFile("/spiflash/hello.txt") # Hello world!
Notice: file extension of files on FAT filesystem is limited to maximum of 3 characters.
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:
Libraries:
select
w/o a PhD)Compiler:
There are a few cons of Nim: