Port ESP32 - amir1387aht/MicroCS GitHub Wiki
ESP32, ESP32-S2, S3, C2, C3, C5, C6, H2 and P4: every peripheral, plus Wi-Fi and networking
from C# on the chips with a radio (Wifi, ESP-NOW, SNTP, mDNS, ping, TCP/UDP sockets, HTTP +
HTTPS) ā see NETWORK.md. Chips without Wi-Fi (H2, P4) build the same
port without it; Bluetooth is not used.
example/ is a complete ESP-IDF project. Copy it, put MicroCS in
components/MicroCS, build:
cp -r MicroCS/ports/esp32/example my_board && cd my_board # or start from your own project
git clone https://github.com/amir1387aht/MicroCS components/MicroCS
idf.py set-target esp32s3 # or esp32, esp32c2, esp32c3, esp32c6, ...
idf.py build flash monitor # press Enter, type C# at the "> " promptmy_board/
āāā CMakeLists.txt # plain IDF project file, nothing MicroCS-specific
āāā sdkconfig.defaults # 16 KB main-task stack, partitions.csv, 4 MB flash
ā # (delete an old sdkconfig after adding it)
āāā partitions.csv # 2 MB app (Wi-Fi + TLS) + LittleFS "storage" partition for your scripts
āāā main/
ā āāā CMakeLists.txt # idf_component_register(SRCS "main.c")
ā āāā main.c # ~45 lines: flash filesystem, heap, console, mcs_runtime_run()
āāā components/
āāā MicroCS/ # this repository (git clone or git submodule)
In VS Code (ESP-IDF extension): Set Espressif Device Target ā Build, Flash and Monitor.
There is no idf_component.yml to add to your project; the one in the repository root
belongs to MicroCS itself (it pulls in the joltwallet/littlefs component automatically
on the first build).
The console (mcs_esp32_console()) is UART0 and USB-Serial-JTAG at the same time, so
the prompt shows up on either USB connector of an S3/C3/C6 DevKit. The banner is printed
once at boot ā if the monitor attached later, press Enter for a fresh > .
Scripts you upload, /boot.cs, /main.cs and every file a script writes live in a
LittleFS filesystem on the chip's SPI NOR flash, so they survive resets and power cycles.
LittleFS is made for NOR flash: power-loss safe (an interrupted write leaves the old file),
wear levelling, small RAM use. Uploads by tools/mcs_remote.py are written to a temporary
file and renamed, so a reset during put never leaves a half-written /main.cs.
mcs_runtime_cfg_t cfg = MCS_RUNTIME_DEFAULTS;
bool on_flash = mcs_esp32_littlefs("storage", &cfg.fs_ops, &cfg.fs_ctx); // before allocating the VM heap
if (!on_flash) cfg.ramfs_size = 32 * 1024; // no partition: RAM disk-
Partition:
storageinexample/partitions.csv(typedata, subtypespiffs). It is formatted automatically on first boot. The example is laid out for 4 MB of flash (2 MB app with Wi-Fi and TLS, ~1.9 MB files). On bigger flash set Serial flasher config ā Flash size in menuconfig (orCONFIG_ESPTOOLPY_FLASHSIZE_8MB=yinsdkconfig.defaults) and growstorageto the end of the chip:Flash storagesizeSpace for files 4 MB 0x1F00001.9 MB 8 MB 0x5F00005.9 MB 16 MB 0xDF000013.9 MB Building without Wi-Fi (
CONFIG_MICROCS_WIFI=n)? A 1.5 MB app is enough ā useexample/partitions_nowifi.csv(CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_nowifi.csv"):factory0x180000,storage0x270000(2.4 MB) at0x190000, as the release images do.The boot log prints
MicroCS: LittleFS on flash, N of M KB used. -
TinyFS instead (built in, nothing to fetch): menuconfig ā MicroCS ā Filesystem on the "storage" partition ā TinyFS (
CONFIG_MICROCS_FS_TINYFS=y);mcs_esp32_flash_fs()mounts it. -
YAFFS2 instead: menuconfig ā MicroCS ā Filesystem on the "storage" partition ā YAFFS2 (or
CONFIG_MICROCS_FS_YAFFS2=yinsdkconfig.defaults). The yaffs2 sources are not bundled: setMICROCS_YAFFS2_DIR(environment oridf.py -DMICROCS_YAFFS2_DIR=...), keep a checkout in<project>/third_party/yaffs2, or allow the pinned download withMICROCS_FS_DOWNLOAD=1 idf.py build; thenmcs_esp32_flash_fs("storage", &cfg.fs_ops, &cfg.fs_ctx)mounts it throughmcs_esp32_partition_flash()(any data partition as anmcs_flash_t). YAFFS2 is GPLv2 (or commercial licence): linking it puts your firmware under those terms. C code then uses the files through MicroCS, notfopen(). The example callsmcs_esp32_flash_fs(), which picks whichever filesystem is configured. -
C code sees the same files (LittleFS): the partition is mounted in the ESP-IDF VFS at
/mcs(MCS_ESP32_FS_PATH), sofopen("/mcs/config.json", "r")reads what a script wrote to/config.json. -
From C#:
File.WriteAllText("/log.txt", ...),File.ReadAllText,File.AppendAllText,Directory.CreateDirectory, ... ā see FILESYSTEM.md. Free space:new DriveInfo("/").AvailableFreeSpace(bytes; alsoTotalSize). -
From the browser: open MicroCS Studio (or
tools/studio/index.html) in Chrome/Edge ā Connect ā file manager, C# editor (save + run on the board) and REPL in one page (STANDALONE.md). Connecting does not reset the board; use the console port the firmware prints on (UART bridge or the native USB-Serial-JTAG - the example serves both), at 115200 baud. -
From the PC:
python tools/mcs_remote.py --port COM18 put app.cs /main.cs + lsā/main.csthen runs at every boot.get,rm,mkdir,mvwork the same way;dfprints size, used and free. -
Wipe everything:
idf.py erase-flash(orrmthe files); the next boot formats the partition again.
Flashing a new firmware with idf.py flash writes only the app and partition table, so your
files stay ā unless you change partitions.csv so that storage moves.
The example sets no pins: GPIO, ADC, Timer, Watchdog and RTC work without any.
For UART 1/2, I2C, SPI, PWM, I2S and CAN, tell the port which GPIOs to use:
mcs_esp32_cfg_t pins = MCS_ESP32_CFG_DEFAULT; // everything -1 = unused / default
pins.uart[1] = (mcs_esp32_uart_pins_t){ 17, 18, -1, -1 }; // TX, RX, RTS, CTS
pins.i2c[0] = (mcs_esp32_i2c_pins_t){ 8, 9 }; // SDA, SCL
pins.spi[0] = (mcs_esp32_spi_pins_t){ 12, 11, 13, -1, -1 }; // SCLK, MOSI, MISO, WP, HD (QSPI)
pins.pwm[0] = 2; // LEDC channel 0 -> GPIO2
pins.i2s[0] = (mcs_esp32_i2s_pins_t){ -1, 4, 5, 6, 7 }; // MCLK, BCLK, WS, DOUT, DIN
pins.can = (mcs_esp32_can_pins_t){ 15, 16 }; // TWAI TX, RX (needs a transceiver)
mcs_esp32_hal_init(&hal, &pins);| C# | ESP-IDF driver |
|---|---|
GPIO / Pin, OnChange
|
gpio_*, ISR service ā FreeRTOS queue (safe on dual core) |
UART |
uart_driver_install / uart_read_bytes
|
I2C |
i2c_master (IDF ā„ 5.2) or the legacy driver |
SPI, SpiDevice, QSPI
|
spi_master (quad mode via spi_transaction_ext_t) |
ADC.Read(gpio) / ReadMillivolts
|
adc_oneshot + eFuse calibration |
DAC |
dac_oneshot (ESP32, S2) |
PWM |
LEDC (13-bit duty) |
LedStrip |
ws2812 driver (menuconfig ā MicroCS ā WS2812 / NeoPixel driver): RMT TX on any GPIO (up to 2 strips; none on ESP32-C2); "NEOPIXEL" = on-board RGB LED (S3 GPIO48, C3/C6/H2 GPIO8, override MCS_ESP32_RGB_LED) |
Timer |
gptimer |
I2S |
i2s_std |
CAN |
Legacy TWAI (not CAN-FD-only chips such as C5; the new TWAI driver is not wrapped yet) |
Watchdog |
task watchdog |
RTC |
gettimeofday / settimeofday
|
Hal.UniqueId |
eFuse MAC |
Console: mcs_esp32_console() (UART0 + USB-Serial-JTAG where the chip has it, input from
either), mcs_esp32_console_usb() (USB-Serial-JTAG only) or mcs_esp32_console_uart(0, 115200).
ESP-IDF's USB-Serial-JTAG driver drops received bytes that do not fit its buffer (USB itself
would wait), so MicroCS installs it with a 4 KB receive buffer (MCS_ESP32_USB_RXBUF, TX:
MCS_ESP32_USB_TXBUF) and Studio / mcs_remote.py upload with flow control (put ⦠<window>,
STANDALONE.md). If your app installs that driver itself, give it a
receive buffer of at least MCS_SHELL_PUT_WINDOW (256) bytes.
Optional C# U8g2 / U8x8 for OLED and LCD panels: menuconfig ā MicroCS ā U8g2 displays
(CONFIG_MICROCS_U8G2=y, plus the displays and fonts to build in). Step by step:
ESP32-C3 + OLED; reference: U8G2.md.
On by default wherever the chip has Wi-Fi when you build the example: menuconfig ā MicroCS ā
Wi-Fi and networking (CONFIG_MICROCS_WIFI). The ready-to-flash release images are built
without it (CONFIG_MICROCS_WIFI=n, partitions_nowifi.csv:
1.5 MB app + 2.4 MB files).
Wifi.Connect("MyNetwork", "secret-password");
Console.WriteLine(Wifi.Ip + " " + Http.Get("https://example.com/").StatusCode);-
Connect at boot: set network name / password in menuconfig
(
CONFIG_MICROCS_WIFI_SSID/_PASSWORD), or save one once from C# withWifi.Save(ssid, password)(NVS) āCONFIG_MICROCS_WIFI_AUTOCONNECTthen joins it before/main.csruns. Your own firmware callsmcs_esp32_wifi_autoconnect(ssid, password). -
RAM:
CONFIG_MICROCS_WIFI_RESERVE_KB(72 KB, 56 KB on the C2) stays outside the VM heap for the Wi-Fi driver, lwIP and TLS. The driver itself starts only when a script first uses Wi-Fi;Wifi.Off()stops it again. - Flash: Wi-Fi + TLS make the image ~1.6 MB, hence the 2 MB app partition.
-
mDNS:
idf.py add-dependency espressif/mdnsā the component is detected andMdnsappears (Net.Has("Mdns")). - HTTPS uses esp-tls with the ESP-IDF certificate bundle; ESP-NOW, SNTP and ping need nothing extra. WPA2/WPA3-Enterprise needs IDF 5.1+.
Opt-in, not in the release images: sdkconfig.ble turns on NimBLE and
CONFIG_MICROCS_BLE (C# Ble, BleService, BleConnection, BleUart; BLE.md):
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ble" set-target esp32s3 build flash monitoridf.py menuconfig ā Component config ā MicroCS ā Bluetooth LE is visible on chips
with an onboard BLE radio, even before Bluetooth is enabled. It shows the missing
prerequisites instead of silently hiding the switch:
- Component config ā Bluetooth ā Bluetooth: enable.
- Host: NimBLE - BLE only; Controller: Enabled.
- NimBLE Options: leave extended advertising off (MicroCS uses legacy advertising).
- MicroCS ā Bluetooth LE: enable the C# API; configure the default name, event queue, GATT services / attributes / value pool, scan / discovery results, subscriptions, timeouts and UART buffer. RAM reservation defaults to 48 KB. Larger pools consume more static RAM.
- Bluetooth ā NimBLE Options: set connections, preferred MTU, bonding / security and NVS persistence. The MicroCS connection table follows the NimBLE connection limit (C2: at most two). On ESP32 also match the controller's BLE connection count; on S3 / C3 allow enough controller activities for connections plus scanning / advertising.
Menuconfig choices override the corresponding MCS_BLE_* values in mcs_user_config.h.
Bonds survive reset when CONFIG_BT_NIMBLE_NVS_PERSIST=y. The supplied BLE defaults enable
this and use two connections so they are valid on C2 as well as the larger chips.
| Chip | Onboard BLE | ESP-IDF build coverage |
|---|---|---|
| ESP32, S3, C2, C3, C6, H2 | yes | 5.3.2, BLE-enabled CI builds |
| C5, C61 | yes | 5.5.2, BLE-enabled CI builds |
| S2, P4 | no | BLE unavailable; non-BLE CI builds |
Wi-Fi and BLE can coexist on chips that have both radios; H2 has BLE but no Wi-Fi.
S2 / P4 cannot run this onboard NimBLE backend (an external / hosted radio is not implemented).
C5 / C61 target defaults use partitions_large.csv: a 2.5 MB app and 1472 KB storage
on 4 MB flash (IDF 5.5 Wi-Fi + TLS + BLE exceed the older 2 MB app). S3 and older-chip
layouts are unchanged. When changing a partition table on an already flashed device,
back up scripts first: the storage offset changes; restore them after formatting the new
filesystem. In an existing C5 / C61 configuration select partitions_large.csv under
Partition Table ā Custom partition CSV file.
These are compile / simulator checks, not real-board radio validation.
Backend: mcs_port_esp32_ble.c.
The backend is mcs_port_esp32_net.c; the C# API, events, options and
limits are in docs/NETWORK.md.
The example allocates up to 192 KB for the VM with heap_caps_malloc, clamped to the largest
free block minus a 32 KB reserve for drivers and FreeRTOS, plus the Wi-Fi reserve when Wi-Fi is
built in (~90 KB on the C2); a RAM disk (a
sixth of the heap) is only used when there is no storage partition; 96 KB is plenty
for the REPL on the C3. On boards with PSRAM you can pass MALLOC_CAP_SPIRAM.
idf.py menuconfig ā Component config ā MicroCS ā Build profile. Every chip defaults
to the full build except the ESP32-C2, which defaults to Embedded (compiler + REPL +
modules, 32-bit long, no disassembler). MCU, Low-RAM and Minimal are for your own
application running precompiled images (the example's REPL needs the compiler). The HAL is
compiled in every profile.
Every other option (VM limits, stdlib switches, MCS_ESP32_FS_PATH, ā¦) goes into a
mcs_user_config.h in the project folder or main/ (template:
config/mcs_user_config.h), found automatically; Project config
header in the same menu names another file. The menuconfig choices win over the header,
except the profile: MCS_PROFILE in the header wins. See CONFIGURATION.md.
idf.py set-target esp32c2 && idf.py build flash monitor-
Memory: 272 KB SRAM (~180 KB free after boot) ā embedded profile
(
sdkconfig.defaults.esp32c2), ~90 KB VM heap with Wi-Fi (~120 KB withCONFIG_MICROCS_WIFI=n). -
printf: IDF enables the ROM "nano" formatter on the C2 by default
(
CONFIG_NEWLIB_NANO_FORMAT), which cannot print floats ā MicroCS formatsdoublewithsnprintf, so the example turns it off. The port prints a#warningif it is on with floats enabled. -
Crystal: many ESP8684 modules use a 26 MHz crystal ā set
CONFIG_XTAL_FREQ_26=y(commented line insdkconfig.defaults.esp32c2) or the UART baud rate and timers are off. -
Peripherals: GPIO 0ā20,
UART0ā1 (console on UART0, GPIO 20/19), oneI2Cbus,SPIbus 0 (SPI2),ADCon GPIO 0ā4 (12-bit),PWM= 6 LEDC channels clocked from the 60 MHz PLL divider (MCS_ESP32_LEDC_CLK_HZ),Timer= 1 general-purpose timer (MCS_ESP32_HW_TIMERSis clamped to what the chip has). Not on the C2:DAC,I2S,CAN(TWAI), USB-Serial-JTAG (the console uses UART0) ā those C# classes reportNotSupportedException. - Pins that suit an ESP8684-DevKitC: UART1 TX 7 / RX 10, I²C SDA 5 / SCL 6, SPI SCLK 4 / MOSI 3 / MISO 2, PWM 0 on GPIO 1.
The ready-to-flash release images have threads on. For your own build:
idf.py menuconfig ā Component config ā MicroCS ā Real threads on FreeRTOS tasks
(CONFIG_MICROCS_THREADS=y). The REPL keeps core 0, and C# can use core 1:
var w = Thread.RunOn(1, "/dsp.cs", "Filter", 128); // a function on core 1
Console.WriteLine(w.Result);
var loop = Thread.Every(10, "/control.cs", "Tick", 1); // OS-timed, every 10 ms on core 1jobs.cfg file jobs then run on threads by default: every 10ms /control.cs core=1
(main keeps a job on the main VM). Each thread has its own C#
heap (32 KB, taken from the VM heap) and a 12 KB task stack. Off (the default), the firmware
is single-threaded, as before. See docs/THREADS.md.
The example is built the way a user builds it (copied to a new folder, MicroCS in
components/MicroCS) with the espressif/idf:v5.3.2 image for ESP32, ESP32-S3, ESP32-C2,
ESP32-C3 and ESP32-C6 ā Wi-Fi and networking included.