Build NodeMCU Firmware for ESP8266

3 minute read

NodeMCU is one of the most popular firmware for ESP8266 and ESP32 wifi SoCs, the firmware is written in c and has Lua interpreter builtin which is based on Lua 5.1.4 or Lua 5.3.

This post will show how to build basic firmware for esp8266 module.

Build Firmware

First things first, clone nodemcu-firmware and its submodules:

$ git clone --recurse-submodules https://github.com/nodemcu/nodemcu-firmware.git
$ make

In order to build firmware esp-toolchains is required, the toolchain will be downloaded to cache directory during the build process.

You may want to change the default build options to reduce memory footprint or other customizations, consult to these files:

  • app/include/user_modules.h
  • app/include/user_config.h
  • app/include/user_version.h

To enable debug message, define DEVELOP_VERSION in app/include/user_config.h.

Refer to Build Options for more details.

Flash firmware to ESP8266

The final firmware will be put in bin directory named with flash offset, use esptool to flash the firmware:

$ esptool.py --baud 230400 write_flash -fs 4MB -ff 80m \
    0x00000 bin/0x00000.bin 0x10000 bin/0x10000.bin

Other tools such as nodemcu-flasher or nodemcu-pyflasher may also be used.

Boot message after the fresh flash as follows:

NodeMCU 3.0.0.0
	branch: release
	commit: 136e09739b835d6dcdf04034141d70ab755468c6
	release: 3.0.0-release_20210201
	release DTS: 202102010145
	SSL: false
	build type: float
	LFS: 0x0 bytes total capacity
	modules: adc,bit,file,gpio,mqtt,net,node,ow,tmr,uart,wifi
 build 2021-02-26 17:29 powered by Lua 5.1.4 on SDK 3.0.1-dev(fce080e)
cannot open init.lua:

If you see cannot open init.lua, don’t worry, this is because there is no init.lua in the target board, the first program executed after startup is init.lua, you can find an example here, to upload Lua application, uploader is required, nodemcu-uploader and NodeMCU-Tool are served for this purpose:

$ sudo npm install nodemcu-tool -g
$ python3 -m pip install --user nodemcu-uploader

Upload files to esp8266:

$ nodemcu-uploader upload init.lua
opening port /dev/ttyUSB2 with 115200 baud
Preparing esp for transfer.
Transferring init.lua as init.lua
All done!

$ nodemcu-tool -p /dev/ttyUSB2 upload init.lua
[NodeMCU-Tool]~ Connected
[device]      ~ Arch: esp8266 | Version: 3.0.0 | ChipID: 0x49c76d | FlashID: 0x16405e
[NodeMCU-Tool]~ Uploading "init.lua" >> "init.lua"...
[connector]   ~ Transfer-Mode: hex
[NodeMCU-Tool]~ File Transfer complete!
[NodeMCU-Tool]~ disconnecting

Do a reset to see if it works:

Connecting to WiFi access point...
> Connection to AP(testap) established!
Waiting for IP address...
Wifi connection is ready! IP address is: 192.168.0.15
Startup will resume momentarily, you have 3 seconds to abort.
Waiting...
Running

There are many examples in the nodemcu repository for further references.

The credentials.lua and init.lua used in this example are attached here:

cat << EOF >> credentials.lua
SSID = "testap"
PASSWORD = "Passw0rd123456789l0"
EOF
cat << EOF >> init.lua
-- load credentials, 'SSID' and 'PASSWORD' declared and initialize in there
dofile("credentials.lua")

function startup()
    if file.open("init.lua") == nil then
        print("init.lua deleted or renamed")
    else
        print("Running")
        file.close("init.lua")
        -- the actual application is stored in 'application.lua'
        -- dofile("application.lua")
    end
end

-- Define WiFi station event callbacks
wifi_connect_event = function(T)
  print("Connection to AP("..T.SSID..") established!")
  print("Waiting for IP address...")
  if disconnect_ct ~= nil then disconnect_ct = nil end
end

wifi_got_ip_event = function(T)
  -- Note: Having an IP address does not mean there is internet access!
  -- Internet connectivity can be determined with net.dns.resolve().
  print("Wifi connection is ready! IP address is: "..T.IP)
  print("Startup will resume momentarily, you have 3 seconds to abort.")
  print("Waiting...")
  tmr.create():alarm(3000, tmr.ALARM_SINGLE, startup)
end

wifi_disconnect_event = function(T)
  if T.reason == wifi.eventmon.reason.ASSOC_LEAVE then
    --the station has disassociated from a previously connected AP
    return
  end
  -- total_tries: how many times the station will attempt to connect to the AP. Should consider AP reboot duration.
  local total_tries = 75
  print("\nWiFi connection to AP("..T.SSID..") has failed!")

  --There are many possible disconnect reasons, the following iterates through
  --the list and returns the string corresponding to the disconnect reason.
  for key,val in pairs(wifi.eventmon.reason) do
    if val == T.reason then
      print("Disconnect reason: "..val.."("..key..")")
      break
    end
  end

  if disconnect_ct == nil then
    disconnect_ct = 1
  else
    disconnect_ct = disconnect_ct + 1
  end
  if disconnect_ct < total_tries then
    print("Retrying connection...(attempt "..(disconnect_ct+1).." of "..total_tries..")")
  else
    wifi.sta.disconnect()
    print("Aborting connection to AP!")
    disconnect_ct = nil
  end
end

-- Register WiFi Station event callbacks
wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, wifi_connect_event)
wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, wifi_got_ip_event)
wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, wifi_disconnect_event)

print("Connecting to WiFi access point...")
wifi.setmode(wifi.STATION)
wifi.sta.config({ssid=SSID, pwd=PASSWORD})
-- wifi.sta.connect() not necessary because config() uses auto-connect=true by default
EOF