Guidelines for writing code for the ESP8266

Espressif’s ESP8266 WiFi SoC is an increasingly popular chip for Internet of Things projects, both hobby and professional, because it combines a capable MCU with a WiFi radio in a single chip for an amazingly low cost. It’s received a lot of attention in many blogs but I wanted to give a little bit more technical depth on the MCU architecture and my best practices for programming it. There are some idiosyncrasies which have to be taken into account when programming to get the most out of the SoC.

Chip Architecture

Decapped ESP8266 die
Decapped ESP8266 with radio peripherals annotated. Credit: swimmmerdude

Code Memory

The ESP8266 has on die program ROM which includes some library code and a first stage boot loader. All the rest of the code must be stored in external SPI flash. 32kb of code can be loaded into dedicated code RAM. The rest of the code is fetched over SPI and cached in additional 32kb of RAM. Cached code can be fast as long as there isn’t too much churn. The ICACHE_FLASH_ATTR decorator is used to locate code in the SPI flash memory instead of the core 32kb of RAM.

Processor Architecture

The SoC uses a Tensilica Xtensa lx106 MCU which is a 32bit processor with 16 bit instructions but is not ARM. It is Harvard architecture which most significantly means that instruction memory and data memory are completely separate.

Peripherals

All of the MCU peripherals (UART, I2S, Radio, etc.) seem to have been developed custom by Espressif and are a bit idiosyncratic so they take some time to get used to.

One of the most significant speed limitations is that all register reads / writes are surprisingly slow so they should be kept to a minimum.

DMA: So far the only peripheral which has any kind of DMA which has been figured out is the I2S peripheral. With some clever tricks, I2S can also be used for SPI.

Programming Best Practices

Code structure

  • All application code must be in either a task or a timer
  • All tasks and timers should complete in less than 2ms and must complete in less than 500ms or the watchdog timer will reset the MCU.
  • Reoccurring timers should not be scheduled more often than every 5ms.
  • There are 3 user task levels: 0, 1, 2.
    • All level 2 tasks queued run before any queued level 1 tasks and so on.
    • Task level 2 is reserved for HAL functions
  • Timers which are due are higher priority than tasks.
  • Tasks and timers do not preempt each other. A new task or timer cannot start until the running task / timer has ended.
  • Interrupts must not be locked out for more than 10μs at a time or WiFi will crash.
    • No ISR can run for more than 10μs.
    • This is likely the cause of the mysterious lmac.c ### / mac ### errors which many people have encountered. They happen when the MCU can’t service radio interrupts fast enough.
  • Register reading and writing is surprisingly slow, cache where possible, etc.

Performance and Memory

  • Application code should have the ICACHE_FLASH_ATTR decorator unless it is executed very often.
  • All interrupt handlers must not have the ICACHE_FLASH_ATTR decorator and any code which executes very often should not have the decorator.
  • The Espressif is not especially fast and fetching code is relatively slow, however, it has a relatively large amount of RAM so when making computation/code-size/RAM use trade offs, generally take the path that uses more RAM.
  • Heep (malloc and free) is available but should be used sparingly and cautiously.
  • Code size:
    • Since the Espressif uses external SPI flash, there is effectively unlimited code memory.
    • However, loading code from SPI flash is relatively slow and there is a limited code cache in local chip RAM. Hence inner loops and code which executes often should be kept relatively small to reduce cache thrashing.