NodeMCU from Scratch: Building and Flashing the Lua Firmware Yourself

NodeMCU from Scratch: Building and Flashing the Lua Firmware Yourself
By Matthias Petermann / on 18.12.2025

Technical Context: The ESP8266

The ESP8266 is an inexpensive WiFi-enabled microcontroller from Espressif that has been a mainstay of hobbyist, IoT, and prototyping projects for many years.

Technically, it is a 32-bit microcontroller with an integrated WiFi stack, external flash storage, and sufficient processing power for typical tasks such as sensing, actuator control, or simple network protocols (e.g. MQTT or HTTP).

One important point is that the ESP8266 is not tied to a particular runtime environment. Depending on the use case, different firmwares can be used, including AT firmware, MicroPython, or NodeMCU.

In this context, the ESP8266 provides the hardware foundation for the following build and flashing process.


Starting Point

The ESP8266 boards used here originally came with NodeMCU preinstalled. Without giving it much thought, I later flashed them with MicroPython, mainly for pragmatic reasons. That was sufficient for quick experiments and worked reliably.

Only after gaining some distance did NodeMCU come back into focus. Looking at it again made it clear that the firmware is fully open source and can actually be built from source.

This led less to a specific project than to a straightforward question:

How difficult is it to build such firmware yourself – and what do you gain by doing so?

Besides providing better control over the build and modules, Lua also plays a role. I now find the language pleasantly direct and well suited to embedded contexts, without this being intended as a fundamental comparison with Python.


Motivation

NodeMCU is often used as a ready-made environment: flash the image, write Lua scripts, and you are done.

That is perfectly sufficient for many scenarios. I was deliberately interested in the step before that: the path from source code to running firmware on real hardware.

ℹ️ Why build it yourself?

Not out of necessity, but out of interest:

  • Which modules are actually included in the image?
  • How is the firmware binary created?
  • How reproducible is the build process?

Especially with embedded systems, this understanding provides a solid foundation for later experiments and troubleshooting.


Preparation: Toolchain and Source Code

$ sudo apt install python-is-python3 esptool cu

$ mkdir nodemcu
$ cd nodemcu
$ git clone https://github.com/nodemcu/nodemcu-firmware.git
$ cd nodemcu-firmware
$ git submodule update --init --recursive

Configuration

NodeMCU has a modular architecture. The modules determine which functionality will later be available in the Lua REPL and in scripts. The following file can be used to specify which Lua modules are included in the image:

$ nano app/include/user_modules.h
πŸ“ Modular Firmware
The modular structure allows the NodeMCU firmware to be adapted to different use cases – from a simple sensor node to more complex, connected setups.

Overview: NodeMCU Modules (user_modules.h)

Module Default Category Purpose / typical use
adc βœ“ Hardware Analog-to-digital converter
bit βœ“ Core Bit operations
dht βœ“ Sensor DHT11 / DHT22 temperature & humidity
file βœ“ Core Access to flash filesystem
gpio βœ“ Hardware Digital inputs / outputs
i2c βœ“ Bus IΒ²C communication
mqtt βœ“ Network MQTT client
net βœ“ Network TCP / UDP sockets
node βœ“ System System functions (restart, heap, etc.)
ow βœ“ Bus OneWire (e.g. DS18B20)
spi βœ“ Bus SPI communication
tmr βœ“ Timing Timers, delays
uart βœ“ Communication Serial interface
wifi βœ“ Network WiFi configuration & status
ads1115 βœ— Sensor External ADC
adxl345 βœ— Sensor Accelerometer
bme280 βœ— Sensor Temperature / humidity / pressure
bmp085 βœ— Sensor Barometric pressure sensor
http βœ— Network HTTP client
mdns βœ— Network mDNS / Zeroconf
pwm βœ— Hardware Pulse-width modulation
pwm2 βœ— Hardware Extended PWM
sntp βœ— Time Network time synchronization
tls βœ— Security TLS / SSL
websocket βœ— Network WebSocket client
ws2812 βœ— Hardware RGB LEDs (NeoPixel)
u8g2 βœ— Display Graphics displays
enduser_setup βœ— Provisioning WiFi setup via captive portal

Run the Build

The actual build is then uneventful:

$ make
πŸ’‘ Where is the completed firmware?

After a successful build, NodeMCU places the generated firmware images in the nodemcu-firmware/bin/ directory.

Typically, it contains at least two files:

  • 0x00000.bin – bootloader and initial code
  • 0x10000.bin – the actual NodeMCU firmware

These images are later written to the corresponding flash offsets of the ESP8266.


Check the Hardware and Prepare for Flashing

$ sudo esptool --port /dev/ttyUSB0 flash_id
$ sudo esptool --port /dev/ttyUSB0 erase_flash
πŸ“ A Clean Start
Especially with custom builds, a complete erase is useful. Otherwise, remnants in flash can cause difficult-to-understand effects.

Flash the Firmware

$ sudo esptool --port /dev/ttyUSB0 write_flash 0x0 0x00000.bin 0x10000 0x10000.bin

Serial Console

πŸ’‘ Serial Communication

A serial terminal can be used to communicate with the firmware. Depending on the tool and terminal settings, input and output may behave with varying degrees of reliability.

For simple tests and interactive work, picocom has proven in practice to be a robust and straightforward solution.

$ sudo apt install picocom
$ sudo usermod -a -G dialout mpeterma
$ picocom -b 115200 /dev/ttyUSB0
...
NodeMCU 3.0.0.0
modules: adc,bit,dht,file,gpio,i2c,mqtt,net,node,ow,spi,tmr,uart,wifi
powered by Lua 5.1.4
cannot open init.lua:
>
ℹ️ REPL: Interactive Work with NodeMCU

After connecting through the serial console, you arrive at a REPL (Read–Eval–Print Loop) provided by the NodeMCU firmware.

In this mode, Lua commands can be entered directly and executed immediately. This is well suited to initial tests, debugging, and gradually trying out hardware functions.

A simple example is switching a GPIO pin on and off, with an LED connected to it, for example:

Changes take effect immediately, without having to flash the firmware again.

> gpio.mode(4, gpio.OUTPUT)   -- GPIO4 als Ausgang konfigurieren
> gpio.write(4, gpio.LOW)     -- LED einschalten
> gpio.write(4, gpio.HIGH)    -- LED ausschalten
NodeMCU board with lit LED

A First Program: init.lua

As a first step, no external tool is used. Instead, the program is created directly in the REPL of the NodeMCU firmware.

a = file.open("init.lua", "w")
a.write("print('Hello World')")
a.close()
node.restart()

The following happens:

  • Lua commands are executed directly on the device through the REPL.
  • file.open(…) creates a file named init.lua in the ESP8266’s flash storage.
  • a.write(…) writes the program code directly to this file.
  • After the file is closed, the device is restarted.
  • On restart, NodeMCU automatically loads the init.lua file and executes its contents:
Hello World
ℹ️ init.lua

init.lua is NodeMCU’s standard entry point. If this file exists in flash, it is loaded and executed automatically at startup.

For simple experiments, the entire program can be placed directly in this file. For more complex projects, it is common to split the logic across multiple files or modules and load them from init.lua.


Upload Using a Tool

Creating files directly through the REPL is practical for initial tests, but scales only to a limited extent. As soon as programs become larger or consist of multiple files, typing code through the serial console quickly becomes unwieldy.

For real projects, it is therefore much more sensible to edit Lua files locally and then transfer them to the device in one batch.

ℹ️ Why use an upload tool?

An upload tool enables:

  • Editing code with an editor, version control, and syntax highlighting
  • Transferring multiple files in a single step
  • Reproducible updates without manual REPL input

The actual firmware image remains unchanged; only files in the flash filesystem are updated.

On my Debian system, nodemcu-uploader is not available in the package repositories. Using a Python virtual environment (venv) ensures that the tool is installed in isolation without affecting the system Python or other packages.

sudo apt install python3-pip python3-venv

mkdir ~/Apps/nodemcu-uploader
cd ~/Apps/nodemcu-uploader
python3 -m venv venv
source venv/bin/activate

pip install --upgrade pip
pip install nodemcu-uploader

Example: Blinking an LED on GPIO4

As a somewhat more realistic example, here is a small Lua program that periodically switches an LED on GPIO4 (D2 on many NodeMCU boards) on and off.

-- init.lua

local pin = 4
gpio.mode(pin, gpio.OUTPUT)

while true do
  gpio.write(pin, gpio.HIGH)
  tmr.delay(500000)  -- 500 ms
  gpio.write(pin, gpio.LOW)
  tmr.delay(500000)  -- 500 ms
end

The file is then transferred to the device:

nodemcu-uploader --port /dev/ttyUSB0 --baud 115200 upload init.lua

After restarting the ESP8266, the program runs automatically and the LED starts blinking.

πŸ“ Note on the Infinite Loop

Infinite loops using tmr.delay() block the Lua interpreter and are suitable only for simple tests.

In production applications, timer callbacks (tmr.create()) or event-based approaches are used instead.


Conclusion

NodeMCU can be built from source, flashed, and run on real hardware without major obstacles. The entire path, from cloning the repository through the build to interactive work in the REPL, is transparent and easy to follow.

The real value lies less in the result than in the process: you understand which modules are part of the firmware, how the image is created, and how logic and firmware can be cleanly separated. Especially with embedded systems, this knowledge provides a solid foundation for later experiments, extensions, or troubleshooting.

In this context, Lua proves to be a pleasantly direct language, particularly in combination with the REPL and NodeMCU’s simple file structure. A single init.lua is enough for small experiments, while structure and tooling can easily be expanded for larger projects.

NodeMCU is therefore not merely a β€œready-made runtime environment”, but a manageable system, suitable both for quick tests and as a foundation for further ideas and projects.