---
url: https://mikrojs.dev/develop/custom-firmware.md
description: >-
  Build Mikro.js firmware with native modules, different settings or custom
  startup code
---

# Custom Firmware

Build custom firmware when the official Mikro.js firmware lacks something an app needs: a native module, different ESP-IDF settings, a bigger flash chip, or custom startup code. A firmware project is a small npm package, and you don't need the Mikro.js repository.

## Prerequisites

* [Node.js](https://nodejs.org/) >= 24
* [pnpm](https://pnpm.io/installation) or npm
* ESP-IDF >= 6.1, installed with [EIM](https://docs.espressif.com/projects/idf-im-ui/en/latest/):

  ```sh
  eim install -i v6.1 -t all -n true
  ```

## Create a project

`package.json` depends on `@mikrojs/firmware`, on `mikro` for the `mikro idf` command, and on the packages whose native modules you want. `@mikrojs/firmware` has to be a direct dependency: `mikro idf` resolves it from the project, which package managers such as pnpm allow only for the project's own dependencies.

```json
{
  "name": "my-firmware",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@mikrojs/firmware": "^0.1.0",
    "@my-scope/epaper": "^0.1.0",
    "mikro": "^0.1.0"
  }
}
```

`CMakeLists.txt`:

```cmake
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# The native modules to compile in, by the names that apps import
set(MIKROJS_NATIVE_MODULES "@my-scope/epaper/panel")

if(NOT DEFINED MikroFirmware_DIR)
    message(FATAL_ERROR "Build with `mikro idf`, which tells CMake where @mikrojs/firmware is")
endif()
find_package(MikroFirmware REQUIRED COMPONENTS esp32 NO_DEFAULT_PATH)

project(my-firmware)
```

[`mikro idf`](/cli#mikro-idf) resolves `@mikrojs/firmware` from the project and passes its folder to CMake as `MikroFirmware_DIR`, so build with `mikro idf`, not plain `idf.py`. `NO_DEFAULT_PATH` makes CMake use only that folder, never another copy installed elsewhere on the system. `COMPONENTS esp32` selects the chip family.

Separate several native modules with `;`. The build stops if an entry is not an installed [native module](./native-modules). Without `MIKROJS_NATIVE_MODULES`, the build is the official Mikro.js firmware with the project's settings.

The firmware takes the name of the project's package. The device reports it as `sys.board.name`, `mikro fw pack` names the archive after it, and `mikro flash --board` finds a [board package](./creating-boards)'s image by it. To name it otherwise, set `MIKROJS_BOARD_NAME`, and `MIKROJS_BOARD_DESCRIPTION` for the description `mikro flash` shows. A name has at most 63 characters and the form of a package name, optionally followed by `/<board>`: `@acme/boards/t-display`. You can also set the variables in the environment or with `-D`, which take priority over `set()`.

An app can be its own firmware project: add `@mikrojs/firmware` to the app's dependencies, and put `CMakeLists.txt` next to its `package.json`. [`examples/chip-temperature`](https://github.com/mikrojs/mikro/tree/main/examples/chip-temperature) is set up this way; `pn create mikro --firmware` scaffolds one. ESP-IDF writes `sdkconfig`, `managed_components/` and `dependencies.lock` into the project folder, and the build into `.mikro/`, so add them to `.gitignore`.

## Build and flash

In the project folder, install the dependencies, set the chip, and build:

```sh
pn install
pn mikro idf set-target esp32c6
pn mikro idf build flash monitor
```

[`mikro idf`](/cli#mikro-idf) passes its arguments to ESP-IDF's `idf.py`. When ESP-IDF is not active in the shell, it runs `idf.py` through EIM, which activates ESP-IDF first.

An IDE that runs CMake itself, such as the ESP-IDF extension for VS Code, has to pass `MikroFirmware_DIR` too. Run `pn mikro idf reconfigure` once; the `MikroFirmware_DIR` line in `.mikro/build-fw/CMakeCache.txt` (`.mikro/build-fw-<folder>/` for a firmware project in a folder of the app) then has the folder. Add `-DMikroFirmware_DIR=<folder>` to the CMake arguments in the IDE's settings. After you update `@mikrojs/firmware`, check the folder again, because it can change.

::: tip The build says "qjsc not found"
pnpm skipped the build script of `@mikrojs/quickjs`, which builds the QuickJS bytecode compiler. Run `pnpm approve-builds`, select `@mikrojs/quickjs`, and install again.
:::

## Change ESP-IDF settings

Put the settings in a `sdkconfig.defaults` file in the project. They override the firmware package's defaults. ESP-IDF reads the file only when it creates `sdkconfig`, so after you change it, delete `sdkconfig` and run `mikro idf set-target` again.

For example, an app that does all its networking over a cellular modem can leave WiFi out, which frees about 20 KB of internal RAM:

```ini
CONFIG_MIKROJS_WIFI=n
```

On chips with USB-Serial/JTAG (ESP32-C3, ESP32-C5, ESP32-C6, ESP32-S3), a battery-powered device can enter light sleep between timers. This is experimental:

```ini
CONFIG_MIKROJS_AUTO_LIGHT_SLEEP=y
```

The option also turns on ESP-IDF power management, which lowers the CPU clock while the app waits. The device stays awake while a USB host is connected, so development over USB works as before. It also stays awake while the app has a `Pwm`, `Uart`, `NeoPixel` or `I2s` handle open. A `main.cpp` that doesn't call `MIK_Main()` gets no light sleep unless it calls `esp_pm_configure()` itself. Light sleep has two limits. The device can miss a GPIO edge that happens while it sleeps. If you connect a USB host while the device sleeps, the host may not detect the device until you reset it.

At the start of every boot, the firmware waits 500 ms for the recovery trigger (`mikro deploy --recover` or a double reset, see [Recovering a crash-looping device](/troubleshooting#recovering-a-crash-looping-device)). A wake from deep sleep is a boot too, so an app that wakes often can skip the window. The double reset needs a window long enough to press reset twice, and `--recover` needs it to outlast the host's reconnect after the reset, so a short window is in practice the same as none. Without the window, `--recover` and the double reset do not work, and a crash-looping app has to be erased from the ROM bootloader.

```ini
CONFIG_MIKROJS_RECOVERY_WINDOW_MS=0
```

## Use a bigger flash chip

The official firmware's partition table is for 4 MB of flash. `mikro flash` stretches `user`, the partition that holds the app and its files, to the end of a bigger chip for prebuilt images: the generic firmware, a [board package](./creating-boards)'s image and `--from` firmware. A build flashed with `--build-dir` or `pn mikro idf flash` uses the flash size in its `sdkconfig` and the table as you wrote it. For a bigger chip, set its size in `sdkconfig.defaults`, and add a `partitions.csv` that gives the extra space to `user`. For 8 MB:

```ini
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
```

```csv
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 0x280000,
user,     data, littlefs,      ,  0x570000,
```

For 16 MB, use `CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y`, `CONFIG_ESPTOOLPY_FLASHSIZE="16MB"` and a `user` size of `0xD70000`. Keep the names and types of `user` and `factory`: the firmware looks the filesystem up by the name `user`.

::: warning Shrinking the user partition reformats it
If the new `user` partition is smaller than the one on the device, the device reformats it on the next boot, and you need to deploy the app again. `storageUsage().total` shows the size on the device. A build laid out for 4 MB is smaller on an 8 MB or 16 MB module that has run the generic firmware, which uses all of its flash. `mikro flash --build-dir` refuses such a flash unless you pass `--force`.
:::

Flash the new table over USB with `pn mikro idf flash`. On its first boot, the device grows the filesystem to fill the new `user` partition and keeps any existing files.

## Custom startup code

The firmware starts with `MIK_Main()`, which sets up NVS, the filesystem and the JavaScript runtime, and runs the REPL and the deploy protocol. To run code before it, add a `main/` folder:

```cpp
// main/main.cpp
#include "mikrojs_esp32.h"

extern "C" void app_main(void) {
    // Setup code here
    MIK_Main();
}
```

```cmake
# main/CMakeLists.txt
idf_component_register(SRCS "main.cpp"
    PRIV_REQUIRES spi_flash mikrojs littlefs esp_driver_uart esp_driver_usb_serial_jtag
    INCLUDE_DIRS "")
```

To change what `MIK_Main()` itself does, start from [its source](https://github.com/mikrojs/mikro/blob/main/packages/%40mikrojs/firmware/components/mikrojs/mik_main.cpp).

Mikro.js keeps a 68-byte structure in ESP-IDF's [custom app description](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html#adding-a-custom-structure-to-an-application) (`.rodata_custom_desc`), where `mikro flash` writes a board's name into the generic firmware. A structure of your own shares that section, and the link order decides which of the two comes first, so its offset in the image isn't fixed: give it a marker of its own to find it by.

## Custom firmware and the CLI

The CLI comes with the official Mikro.js firmware, but it never flashes that over custom firmware unless you ask it to. After a CLI upgrade, it asks you to rebuild and flash the custom firmware instead. To switch a device back to the official Mikro.js firmware, run `mikro flash --force`.

## Share a build

Others can flash the firmware without building it. In the project folder, build the firmware and pack it:

```sh
pn mikro fw pack
```

This writes `mikro-fw-my-firmware-esp32c6.tar.gz`, named after the firmware and the chip, to the current folder: `@` is dropped and `/` becomes `-`, so `@acme/devboard` gives `mikro-fw-acme-devboard-esp32c6.tar.gz`. Put it where others can download it, such as a GitHub release, and flash it by its URL:

```sh
pn mikro flash --from https://github.com/my-org/my-firmware/releases/download/v1.0.0/mikro-fw-my-firmware-esp32c6.tar.gz
```

To make firmware for a development board that apps can install with npm and flash with `mikro flash`, publish it as a [board package](./creating-boards).
