Skip to content

ESP8266 Design Notes

Gordon Williams edited this page Feb 23, 2017 · 17 revisions

THIS WIKI IS OUT OF DATE. check out the README.md on GitHub for docs on Espruino internals, or see www.espruino.com for searchable, up to date docs on using Espruino.

ESP8266 Espruino Design Notes

The contents of this page has mostly been moved to https://github.com/espruino/EspruinoDocs/blob/master/boards/Esp8266_12.md

This page contains a number of random design notes for the esp8266 port of espruino.

Flash map and access

The esp8266 modules come in a number of forms with varying flash chip sizes. The flash layout for each size is slightly different. There are a number of random tidbits to know about flash layout:

  • There are two binary formats in use: the non-OTA and the OTA update format. The non-OTA has no 2nd stage bootloader, starts at 0x0000 in flash, and has two binary blobs: one for data and one for instructions (+ read-only data). The OTA format has a 2nd stage bootloader at 0x0000 and then a single binary blob for the first firmware image at 0x1000. There is a 2nd binary blob up after the first one to provide the two firmware images necessary for a safe OTA update. All this is described in the Espressif IOT SDK User Manual.
  • The hardware can memory-map 1MBytes of flash, but it has to be read on word (4-byte) boundaries, it cannot be written as far as I know
  • Every memory map has a 16KB "user param" area that is reserved for applications to store non-volatile settings and such. This is used as the "save to flash" area in Espruino. Currently the number of variables is set to 1023, which uses 12KB and in addition the saved data is run-length encoded. Therefore at most 12KB of this flash area really need to be reserved. It is not known whether there's a chance to increase the number of variables at this point.
  • Every memory maps also has a 16KB "system param" area in which the SDK stores settings, including RF parameters and wifi settings
  • Finally there is an unused 4KB area just before the 2nd firmware in the larger memory maps, this "mirrors" the bootloader area but is not used.
Flash size FW size FW#1 start FW #2 start Save to flash System param SPIFFs Free
512KB 480KB 0x000000 N/A 0x78000 0x7C000 N/A N/A
1MB 492KB 0x001000 0x81000 0x7C000 0xFC000 N/A 0x80000 (4KB)
2MB 492KB 0x001000 0x81000 0x7C000 0x1FC000 0x100000 (1MB) 0x80000 (4KB)
4MB 492KB 0x001000 0x81000 0x7C000 0x3FC000 0x100000 (3MB) 0x80000 (4KB)

System time

The esp8266 has two notions of system time implemented in the SDK by system_get_time() and system_get_rtc_time(). The former has 1us granularity and comes off the CPU cycle counter, the latter has approx 57us granularity (need to check) and comes off the RTC clock. Both are 32-bit counters and thus need some form of roll-over handling in software to produce a JsSysTime.

It seems pretty clear from the API and the calibration concepts that the RTC runs off an internal RC oscillator or something similar and the SDK provides functions to calibrate it WRT the crystal oscillator.

The RTC timer is preserved when the chip goes into light sleep mode, when it does a software restart (WDT, exception, or reset call) but it is lost after a deep sleep or an external reset input.

The implementation uses the system timer for jshGetSystemTime() and related functions and uses the rtc timer only at start-up to initialize the system timer to the best guess available for the current date-time.

From a JavaScript perspective, we can get and set the system time using the JS functions called getTime() and setTime(). These get and take a time in seconds (float).

Timers

The esp8266 has a number of timers that can be used. The simplest to use is via os_timer_arm() and related functions. This has millisecond granularity and schedules a task, so it does not have very high priority nor resolution. There is a way to change the timer to have microsecond resolution, but it's not well documented what the side-effects are nor whether this does anything to the priority. Alternatively one can attach a callback to a hardware timer via hw_timer_arm() et. al. and that can fire either at NMI level or at FRC1 level (whatever that is). Clearly there will be task scheduling issues that need to be investigated, i.e., the callback will be limited in what it can safely call.

The current plan is to use the hw_timer at NMI level and see whether this can be made to work reliably.

Main loop processing

Espruino has the concept of a "main loop" which is executed to perform an iteration of work. Since the ESP8266 needs control to be returned to itself to do work, the current design and implementation take advantage of this. The ESP8266 has the concept of "task queues". The idea behind this is that a task queue is a queue of work items that should be run when the ESP8266 finds time to do them. When the Espruino code is loaded, a task is added to the work queue to run one instance of the main loop. Control then returns back to the ESP8266. When it can, the ESP8266 then runs this task ... which runs one instance of the main loop ... and when that instance completes, a new task is added to the task queue and control returns back to ESP8266. In effect, this means that either the ESP8266 is in the Espruino code ... or it has returned from the Espruino code and there is a task awaiting servicing which, when run, will cause a return to the Espruino code. This seems to be working satisfactorily for just now but principles and theory suggest that if an instance of calling the main loop "takes too long" then that could starve the return to the ESP8266 which may result in (worst case) a watch dog timer halt occurring.

I2C Implementation

The I2C interface is implemented in software because the esp8266 does not have hardware support for i2c (contrary to what the datasheet seems to imply). The software implementation has the following limitations: it operates at approx 100Khz, it is master-only, and it does not support clock stretching (a method by which slaves can slow down the master). The I2C interface can be bound to almost any I/O pin pair, but you should avoid gpio15 because it needs to be pulled-down at boot time and the I2C bus needs pull-up resistors. The pins chosen for I2C are configured to be open-drain outputs and an external pull-up resistor is required on each of the two pins. Remember that esp8266 pins are not 5v compatible...

SPI Implementation

ESP8266 SPI hardware support can be considered complex. Fortunately, existing Github based libraries exist that can be leveraged to encapsulate these complexities. The library used in Espruino is MetalPhreak/ESP8266_SPI_Driver.

The hardware SPI interface is implemented through a number of functions found is jshardware.c. The first one we will look at is called jshSPISetup. When invoked, it passes in the identity of the hardware device and configuration information. For our hardware consideration, we should check that the device is EV_SPI1. From the configuration information, the immediate thing we need is the "baudrate" which is taken as the measure of desired SPI clock speed.

GPIO Pins

The esp8266 GPIO pins support totem-pole and open-drain outputs, and they support a weak internal pull-up resistor (in the 20KOhm-50KOhm range). The Espruino D0 through D15 pins map directory to GPIO0 through GPIO15 on the esp8266. Remember that GPIO6 through GPIO11 are used for the external flash chip and are therefore not really available. Also, gpio0 and gpio2 must be pulled-up at boot and gpio15 must be pulled-down at boot.

The current implementation does not support hardware PWM as the timers are used elsewhere. The esp8266 ADC function is available on any pin (D0-D15) but really uses a separate pin on the esp8266 (this should probably be changed to an A0 pin?).

Networking

The ESP8266 board implements the JsNetwork contract of core Espruino. What this means is that ESP8266 implements the following exposed functions:

  • int accept(JsNetwork *net, int serverSocket)
  • bool checkError(JsNetwork *net)
  • void closeSocket(JsNetwork *net, int socket)
  • int createSocket(JsNetwork *net, uint32_t ipAddress, unsigned short port)
  • void gethostbyname(JsNetwork *net, char *hostName, uint32_t *ipAddress)
  • void idle(JsNetwork *net)
  • int recv(JsNetwork *net, int socket, void *buffer, size_t length)
  • int send(JsNetwork *net, int socket, void *buffer, size_t length)

To understand these functions, we need to realize that Espruino thinks in terms of a logical concept called a "socket". A socket is simply an integer key value that is used to represent a network connection. For example, imagine an ESP8266 connected via TCP to a partner ... within Espruino, that whole connection is represented by its socket identifier and if we wish to send data to that partner, Espruino would execute a send() to that socket. Since the socket identifier is returned from a call to createSocket this shields Espruino from the internal details. All Espruino sees is the socket identifier (often called a socket handle). It is assumed that the implementation of createSocket maintains what ever state is needed so that subsequent references to the socket identifier result in the correct communications with the correct partner.

In ESP8266 land, there is no concept of a socket. Instead, ESP8266 works with the notion of a struct espconn to hold the state of a connection. And it is here that we can now delve into the implementation of the ESP8266 board functions.

Within libs/network/esp8266/network_esp8266.c we have the core implementation. The primary story here is that we have a structure called struct socketData. An instance of this contains details of a connection currently active from the ESP8266. There is an array of these structures stored in the local static called socketArray. There will be an element in this array for each active Espruino logical socket. For example, if Espruino asks for a new socket with a call to createSocket, ESP8266 will find an un-used element in the socketArray and associate that element with the socket identifier that is returned. When subsequent calls come back in which refer to the same socket identifier, that is mapped back to the same socketArray element and we now have achieved the concept of a one-to-one mapping between an Espruino Socket and a struct socketData. Contained within the struct socketData is the struct espconn which is used be the ESP8266 SDK to own a connection to a partner.

Since Espruino calls into the network layer and there is never an instance where the network layer calls "out" to Espruino, we have to do some work to accommodate that model. For example, when data arrives from a TCP partner, our logic has to squirrel away that data so that, in the future, when Espruino issues a recv() request on the socket, the data that was previously supplied is made available. Similarly, the send() request causes a send of data only if we are not already in the process of sending. This brings us to some interesting semantics of send() and recv().

When Espruino calls send() it is effectively saying "Please send this buffer of data of this specific size and tell me how much you actually sent". Pause on that a moment and think about it. With this interesting semantic, the ESP8266 can send all, some or ... none of the data. Sending less than the full amount is NOT considered an error ... instead, it basically asks Espruino to try and send again in the future and maybe we will be able to send more.

The recv() call is similar. When invoked from Espruino, it is effectively asking "Do you have any data that I can consume? If so, fill in this buffer and tell me how much you supplied". Again, this can fill in all, some or none of the buffer supplied as a function of data it has previously received. As you can imagine, there are many permutations here. It is important to note that Espruino is effectively polling the ESP8266 environment all the time saying: Any data now? How about now? Any here? Now? This means that we have to be as efficient as sensibly possible in determining that we have no data.

Imagine that a partner has sent data to the ESP8266. This means that the ESP8266 has to remember that data until Espruino has issued a recv() and it is all consumed (there might be many recv() calls needed to consume it all). To achieve this, when the data arrives at ESP8266 a memory buffer if allocated to hold that data. Currently it is a malloc() buffer that is not particularly well managed and can be the source of dramatic future improvement.