PROJECTS ESP8266

Hello world

Basics

Execution of program starts from function user_init(). There is no main() in ESP SDK, but it is user_init() where you are about to initialize your program, ask firmware to later call you back and return from that function.

The main point in programming the esp8266 is that unlike with generic C-program where you don't return from main() until you program is completely done and exiting, with esp8266 you return from user_init immediately after you have done you initialization and have set firmware up to call you next function in action. System will call you later again expecting you to set up next steps and return again from that function called.

The longer explanation

The program flow with esp8266 cannot be linear but it must be designed as event based. While with reqular multitasking operating systems you start from main() and you have CPU available to you all the time without need to care anything else, with esp8266 being single tasking system, you must run your code as short timeslices always returning control back to firmware so that firmware can run it's own pending housekeeping.

The previous word must is absolutely mandatory must, since if you fail to return CPU in specified small timeframe, ESP resets and restarts. You cannot prevent that restart, it's hardware watchdog that cannot be disabled. You cannot neither feed that watchdog to signal your program is alive so postponing the reset. You must always return CPU back to firmware. If you fail with it, system restarts. This probably needs some reorientation from programming model you have used to.

ESP8266 documentation says that maximum time to spend in user function without returning CPU back to system is 500 milliseconds (with interrupts enabled). Personnally I would halve that to be on safe side. With interrupts disabled, maximum allowed time spent in user function is 10 microseconds. If you fail with that, system resets. If you have complicated tasks, this requires quite a callback acrobacy for a long running tasks to not exceed that mandatory limit.

1) There's articles about wdt reset, rst_cause:4 on net people trying to solve that with fine tuned power supply, stabilizing condensators and other electronical means. Sorry to say, it's not fault of electronics, it's fault of program. I means that stole the cpu for too long time and hardware watchdog reseted system. Don't try to add components, fix your program.

2) To clarify, when talking about firmware there is not separate firmware in chip, but the firmware are those system libs you link to your code. The main loop is in there. CPU spends it's time there doing system services stuff like maintaining Wifi-connection, doing socket io and others, but giving occassionally CPU to you for short time if you ask it either by timer callback, by interrupt or by asking cpu for next time there is no higher priority system tasks running. But it expect you to return shortly back from that your function it called. Running esp's internal system does not require much clockcycles, basically you have CPU available to you as much as you need. The point is to return it back often.

3) Generally there is two architectures of cpu sharing with user tasks and system tasks on non-multitasking system. One is where cpu remains in user space, but user is obliged to call things like do_system_stuff() often enough. Other is that cpu remains in system space, but system calls back do_user_stuff() -functions when asked by user. ESP8266 NONOS SDK represents the latter.

Example Hello World!

Below is the simple Hello World! application to ESP8266. It initializes itself, waits 10 seconds (for you to replug necessary cables and to ensure the coming message is observeable to you) and prints "Hello World!". After printing it does nothing.

The example source code is alse downloadable at hello-world-example.tar.gz.

This example application uses Makefile layout described in previous page and toolchain described on it's own page. If you have successfully gone through those pages, the example should be directly compilable and flashable.

#include <c_types.h>
#include <osapi.h>
#include <user_interface.h>

/* local defines... */
#define VE_TASKPRIO_LOW    0
#define VE_TASKPRIO_MED    1
#define VE_TASKPRIO_HIGH   2

#define VE_TIMER_NOREPEAT  0
#define VE_TIMER_REPEAT    1

/* local helpers... */
#define __asize(x)  (sizeof(x) / sizeof(x[0]))
#define __d(x) { os_printf("%s:%u %s\n", __FUNCTION__, __LINE__, x); }

/* missing declarations in SDK-includes */
extern void os_printf_plus(const char *format, ...);
extern void ets_timer_setfn(os_timer_t *, os_timer_func_t *, void *);
extern void ets_timer_arm_new(os_timer_t *, uint32_t, int, int);
extern void uart_div_modify(int uart_no, int baudrate);

/* local types... */
enum __local_event_t {
    LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE,
    LOCAL_EVENT_SAYHELLO
};

static void ICACHE_FLASH_ATTR
__local_event_sayhello() {

    /* and finally what we are after for */
    os_printf("Hello world!\n");
}

static void ICACHE_FLASH_ATTR
__local_delay_callback(void *arg) {

    /* send the actual SAYHELLO -message */
    system_os_post(VE_TASKPRIO_LOW, LOCAL_EVENT_SAYHELLO, 0);
}

static void ICACHE_FLASH_ATTR
__local_event_waituser_to_replug_cable() {

    static os_timer_t __timer;
    
    /* since you need some time to switch back to working serial
       terminal to actually see the "Hello world", we need to delay
       actual printage. esp boots quite fast */

    /* ...so delay of 10000 milliseconds, increase if you are slow */
    os_timer_setfn(&__timer, __local_delay_callback,
		   NULL /* arg to callback */);
    os_timer_arm(&__timer, 10000, VE_TIMER_NOREPEAT);
}

static void ICACHE_FLASH_ATTR
__local_event_callback(os_event_t *ev) {

    enum __local_event_t event =
	(enum __local_event_t)ev->sig;
    
    switch(event) {
    case LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE:
	return __local_event_waituser_to_replug_cable();
    case LOCAL_EVENT_SAYHELLO:
	return __local_event_sayhello();
    }
}

void ICACHE_FLASH_ATTR
user_init() {

    /* either static or declared outside!! */
    static os_event_t __local_task_queue[8];

    /* mandatory initialization
       - baud-rate */
    uart_div_modify(0 /* uart0 */, UART_CLK_FREQ / 115200);

    /* initialize taskqueue */
    system_os_task(__local_event_callback, VE_TASKPRIO_LOW,
		   __local_task_queue, __asize(__local_task_queue));


    /* post callback */
    system_os_post(VE_TASKPRIO_LOW,
		   LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE, 0);
}

Breakdown of code

Included headers

SDK's documentation does not express in what header each of functions and variables are defined. Also the headers are not high quality. The only way is to grep the header files at /usr/local/ESP8266_NONOS_SDK to found out in where for example some function is defined. Generally you need :

There's also compiler specific headers at /usr/local/xtensa-lx106-elf/xtensa-lx106-elf/include, for example alloca.h, stdarg.h with stdint.h being probably the most useful.

The minimals for this example application are :

#include <c_types.h>
#include <osapi.h>
#include <user_interface.h>

Defs and helpers

This example Hello World contains some extra definitions and helpers that are not necessary for program execution but they make code more readable. In SDK's documentation there is lots of stuff saying to put for example 0 or 1 into timer allocation function as argument to whether it repeats or is single run only. These defines below are to make code more self explaining to you.

/* local defines... */
#define VE_TASKPRIO_LOW    0
#define VE_TASKPRIO_MED    1
#define VE_TASKPRIO_HIGH   2

#define VE_TIMER_NOREPEAT  0
#define VE_TIMER_REPEAT    1

/* local helpers... */
#define __asize(x)  (sizeof(x) / sizeof(x[0]))
#define __d(x) { os_printf("%s:%u %s\n", __FUNCTION__, __LINE__, x); }

Function definitions

SDK's headers lack lots of prototypes for functions. If you want to keep compiler warnings on with flag -Wall, these missing definitions produce obtrusive garbage into screen.

I wan't to keep -Wall messages clean, so these are missing definitions for this example application.

/* missing declarations in SDK-includes */
extern void os_printf_plus(const char *format, ...);
extern void ets_timer_setfn(os_timer_t *, os_timer_func_t *, void *);
extern void ets_timer_arm_new(os_timer_t *, uint32_t, int, int);
extern void uart_div_modify(int uart_no, int baudrate);

Main logic

user_init()

user_init() is still run at special stage on system bootup. When user_init is run, the system is not yet completely initialized. Once you return from function, system does the remaining initialization taking account the parameters you possibly have changed in user_init.

Timer microsecond granularity is settable only as first thing in user_init. This is explained more in detail in separate page. Also, wireless (if it is set to autoconnect, which is default) is not yet available in user_init, but esp8266 connects to wlan station next after you return from user_init.

In our example we first ensure serial connections parameters so that we can actually see what we a going to print out at end. When considering serial output, os_printf() might not be working while still in user_init stage. os_printf works after returning from user_init, but printing in user_init seems to be dependent to previous stage, ie. was system for example reseted or booted from cold.

    /* mandatory initialization
       - baud-rate */
    uart_div_modify(0 /* uart0 */, UART_CLK_FREQ / 115200);

As a second task in user_init we set up message queue, and message callback function that receives to messages we are going to send.

Posting tasks is crucial function in esp8266 programming. Since we need to actively return cpu back to system, task queues are the way to say system. "I return the CPU back to you, but call me back with this message once you have done you pending tasks."

Task queue initialization needs permanent user allocated struct os_event_t structure for messages that it will contain. Sizing, how many messages can be in queue is up to you. Most of time you need just one slot, if you are going to post several messages to queue before returning from callback, you need several slots.

    /* either static or declared outside!! */
    static os_event_t __local_task_queue[8];

1) Please take notice on static keyword in front of definition of __local_task_queue. Unlike generic C-program, we return from that user_init function still leaving our program running. If there is no static -keyword, __local_task_queue would be allocated from stack, and as stack being rewinded on return from it's allocation functions scope this allocation would be destroyed and overwritten on call to next function.

2) Of course you can define __local_task_queue outside of function on global level to also make it to go data segment and permanent. Personally I like to put these inside to function since it tells viewer that it's meant to be touched only from there.

2) Dynamic memory allocation by os_malloc should not be used. Allocation and freeing different sized objects leads rapidly to fragmentation of limited amount of RAM on esp8266. Unlike full operating systems, esp has no option to brk() more virtual memory for allocations. Use compile time .bss allocations and buffers, either declared as static inside function or as allocation on global level.

system_os_task() takes parameters callback (with type void (*)(os_event_t)) that will be called when system dispatches message. Priority of queue meaning higher priority queues will be emptied first before system going to dispatch lower priorities. User allocated memory area for pending messages and how many slots are in queue in total

    /* initialize taskqueue */
    system_os_task(__local_event_callback, VE_TASKPRIO_LOW,
		   __local_task_queue, __asize(__local_task_queue));

There can be three queues each with different priority. 0 is lowest, 2 is highest. SDK does not have constants for these, we defined VE_TASKPRIO_LOW to VE_TASKPRIO_HIGH on top of the source file to make code more readable.

Macro __asize(x) is again defined on top of the file as total size of x areas divider by size of x[0]. Ie., how many slots are in array. Of course in this case you could have written directy "8" but this is more elegant.

As final thing before returning from user_init, we send ourselves message "Call me back with argument LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE" That was again defined at top of our source file.

    /* post callback */
    system_os_post(VE_TASKPRIO_LOW, 
		   LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE, 0);

system_os_post takes three parameters. First is to which priority queue message goes, (By rule of thumb, use the lowest priority queue) and message type and message param. Message type and param are dedicate to user, you can use them as you will. Both are specified as uint32_t

This is again a bit weirdness in ESP's SDK. Common practice would suggest that message type would be int meaning something defined for enum, and message param would be more sensible as void * meaning pointer to data as argument to callback. In esp's case you need to cast them back forth.

user_init() as complete is :

void ICACHE_FLASH_ATTR
user_init() {

    /* either static or declared outside!! */
    static os_event_t __local_task_queue[8];

    /* mandatory initialization
       - baud-rate */
    uart_div_modify(0 /* uart0 */, UART_CLK_FREQ / 115200);

    /* initialize taskqueue */
    system_os_task(__local_event_callback, VE_TASKPRIO_LOW,
		   __local_task_queue, __asize(__local_task_queue));


    /* post callback */
    system_os_post(VE_TASKPRIO_LOW, 
		   LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE, 0);
}

Message callback

Message callback is just simple switch-case block that extracts message type from argument it receives and calls function specific to received type.

In case some of task you need to do is slow or asynchronous, system message loop is tool for notifying their completion. Things like connection wireless, receiving (parsed) data from net, having packet sent to net consume too much time to wait their completing in busy / micro sleep loop. Your program needs to post system message to be catched on message loop when something asynchronous happen.

For example when receiving data from net, system calls your net io callback when there is data waiting. You buffer that data in espconn_recv_callback, until you have complete message ready. Once you have message ready, you post message something like LOCAL_EVENT_WE_HAVE_MESSAGE_PLEASE_PROCESS_IT

Of cource when initializing your net receiving socket, you can also provide direct callback to it to be called when there's message ready. Using system message queue just has advantage of fullfill requirement to give cpu back to system, althought using it creates some overhead and complexity to program.

So, in this case we select just two messages. To remind, they were defined in top of the source file. Of course using enum is oblicatory, but more readable than just using plain numbers.

/* local types... */
enum __local_event_t {
    LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE,
    LOCAL_EVENT_SAYHELLO
};

In user_init we did send LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE so message loop calls matching function.

static void ICACHE_FLASH_ATTR
__local_event_callback(os_event_t *ev) {

    enum __local_event_t event =
	(enum __local_event_t)ev->sig;
    
    switch(event) {
    case LOCAL_EVENT_WAIT_USER_TO_REPLUG_CABLE:
	return __local_event_waituser_to_replug_cable();
    case LOCAL_EVENT_SAYHELLO:
	return __local_event_sayhello();
    }
}

Delaying printing of the Hello Text

In our example we don't print "Hello World!" immediately. ESP8266 boots up quite fast, so fast that you don't necessarily be fast enough to check your serial communication program is ready and receiving. The 10 second delay to gives you time to not miss that hello text while setting up connection.

Also this gives fine opportunity to demonstrate the timer

The os_timer_setfn initializes user allocated timer structure, setting function to be called and optional void * argument to be passed to callback. Normally you might pass some pointer to structure, in this case we have nothing important, so we pass NULL.

Once timer is initialized, you arm it with os_timer_arm for timeout as millisends and flag for whether this is repeating or one time only timer as third argument. You can have multiple timers active at same time, also you can re-arm timer again if it was only once -type timer, and you can disarm your repeating timer anywhere you want.

Again, there is no defines in SDK's headers for repeating and non-repeating timer. Nonrepeating is 0 and repeating is 1. Constants for example are defined at top of the source.

static void ICACHE_FLASH_ATTR
__local_event_waituser_to_replug_cable() {

    static os_timer_t __timer;
    
    /* since you need some time to switch back to working serial
       terminal to actually see the "Hello world", we need to delay
       actual printage. esp boots quite fast */

    /* ...so delay of 10000 milliseconds, increase if you are slow */
    os_timer_setfn(&__timer, __local_delay_callback,
		   NULL /* arg to callback */);
    os_timer_arm(&__timer, 10000, VE_TIMER_NOREPEAT);
}

Timer was set to call specified function after 10k milliseconds. Of course it could have been set to invoke final __local_event_sayhello -function directly, but to live like teach, __local_delay_callback dispatches yet again message through message queue.

static void ICACHE_FLASH_ATTR
__local_delay_callback(void *arg) {

    /* send the actual SAYHELLO -message */
    system_os_post(VE_TASKPRIO_LOW, LOCAL_EVENT_SAYHELLO, 0);
}

The "Hello World"

Once __local_delay_callback dispatched message __local_event_callback catched it and finally called the function for what we are here. The __local_event_sayhello.

static void ICACHE_FLASH_ATTR
__local_event_sayhello() {

    /* and finally what we are after for */
    os_printf("Hello world!\n");
}