diff --git a/README.md b/README.md index 1870f8cb06..bb88d1601c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,74 @@ +# Arduino core for ESP8266, with multi-threading + +WARNING! This is a clone of the official Arduino core for the ESP8266, with +some highly experimental cooperative multi-threading support. It will likely +make fun of you while you try to figure out why it doesn't work. + +### Motivation +Multi-threading can make it easier to write and maintain code, by avoiding +complex state machines. There are some libraries out there for simulating this, +but they typically don't have separate stacks, or they don't address the issue +with blocking calls in the ESP8266 WiFi libraries. Since I had some spare time, +and the support basically was already there in the core, I made a crude attempt +to expose it. + +### How it works +This is a case of cooperative multi-threading, meaning each thread either need +to exit its loop regularly, or call one of the `yield()` or `delay(ms)` +functions, to let the other threads run. All threads have the same priority and +are scheduled in a round-robin fashion. If a thread function exit it will be +invoked again later, just like the regular `loop()` function. There is no way +to terminate threads. + +The stack for each new thread is allocated on the heap and is by default 1024 +bytes. See `ESP8266Scheduler.h` for the function signature. + +In addition to the thread support, a number of library call sites have been +modified to avoid `esp_yield()` and just call `yield()` in a loop instead, +to avoid the blocking behavior. One should note this enables (accidental) +concurrent access to the libraries, which may be TOTALLY UNSAFE. It's probably +best if you keep your threads responsibilities completely separate. + +### Example +``` +#include + +void serialLoop() { + int num = 0; + while (true) { + Serial.printf("The number is %d\n", num++); + delay(1000); + } +} + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(115200); + Scheduler.startLoop(serialLoop); +} + +void loop() { + digitalWrite(LED_BUILTIN, HIGH); + delay(1000); + digitalWrite(LED_BUILTIN, LOW); + delay(1000); +} +``` + +### Installation +Since this is a modified version of the original core, you need to remove that +one if you have it installed and then install this one instead. Don't expect +any support on it though, especially not from the original authors. + +### Performance +Haven't checked; probably horrible. + +### Known issues +- Using ArduinoOTA is currently not super-safe since it doesn't suspend other threads, but it should not be that hard to fix. +- Since `delay()` has been replaced with `yield()` loops, the CPU will never enter any low power states. Unsure if it did that before though. + +Original README follows below. + Arduino core for ESP8266 WiFi chip =========================================== diff --git a/cores/esp8266/Arduino.h b/cores/esp8266/Arduino.h index 90e2da25db..747c053d5e 100644 --- a/cores/esp8266/Arduino.h +++ b/cores/esp8266/Arduino.h @@ -219,6 +219,9 @@ void loop(void); void yield(void); void optimistic_yield(uint32_t interval_us); +typedef void (*thread_func_t)(void*); +int spawn(thread_func_t func, void* func_arg, unsigned int stack_size); + #define digitalPinToPort(pin) (0) #define digitalPinToBitMask(pin) (1UL << (pin)) #define digitalPinToTimer(pin) (0) diff --git a/cores/esp8266/Esp.cpp b/cores/esp8266/Esp.cpp index a8e12f916f..f4c0b2208c 100644 --- a/cores/esp8266/Esp.cpp +++ b/cores/esp8266/Esp.cpp @@ -396,7 +396,6 @@ struct rst_info * EspClass::getResetInfoPtr(void) { } bool EspClass::eraseConfig(void) { - bool ret = true; const size_t cfgSize = 0x4000; size_t cfgAddr = ESP.getFlashChipSize() - cfgSize; diff --git a/cores/esp8266/cont.h b/cores/esp8266/cont.h index 46daad1007..6cf761d780 100644 --- a/cores/esp8266/cont.h +++ b/cores/esp8266/cont.h @@ -38,15 +38,28 @@ typedef struct cont_ { unsigned unused1; unsigned unused2; unsigned stack_guard1; +} cont_t; - unsigned stack[CONT_STACKSIZE / 4]; - +typedef struct cont_footer_t { unsigned stack_guard2; unsigned* struct_start; -} cont_t; +} cont_footer_t; + +typedef struct cont_static_ { + cont_t header; + unsigned stack[CONT_STACKSIZE / 4]; + cont_footer_t footer; +} cont_static_t; + +#define CONT_REQUIRED_SIZE(stack_size) (sizeof(cont_t) + stack_size + sizeof(cont_footer_t)) // Initialize the cont_t structure before calling cont_run -void cont_init(cont_t*); +void cont_init_size(cont_t*, unsigned stack_size); + +// Initialize the cont_static_t structure before calling cont_run +static inline void cont_init(cont_static_t* c) { + cont_init_size(&c->header, CONT_STACKSIZE); +} // Run function pfn in a separate stack, or continue execution // at the point where cont_yield was called diff --git a/cores/esp8266/cont_util.c b/cores/esp8266/cont_util.c index 8c36d8926e..83ada3c86a 100644 --- a/cores/esp8266/cont_util.c +++ b/cores/esp8266/cont_util.c @@ -25,27 +25,31 @@ #define CONT_STACKGUARD 0xfeefeffe -void ICACHE_RAM_ATTR cont_init(cont_t* cont) { +void ICACHE_RAM_ATTR cont_init_size(cont_t* cont, unsigned stack_size) { + unsigned* stack = (unsigned*)(cont + 1); + cont_footer_t* footer = (cont_footer_t*)(stack + (stack_size / 4)); cont->stack_guard1 = CONT_STACKGUARD; - cont->stack_guard2 = CONT_STACKGUARD; - cont->stack_end = cont->stack + (sizeof(cont->stack) / 4); - cont->struct_start = (unsigned*) cont; - + cont->stack_end = (unsigned*) footer; + footer->stack_guard2 = CONT_STACKGUARD; + footer->struct_start = (unsigned*) cont; + // fill stack with magic values to check high water mark - for(int pos = 0; pos < (int)(sizeof(cont->stack) / 4); pos++) + while(stack < (unsigned*)footer) { - cont->stack[pos] = CONT_STACKGUARD; + *stack = CONT_STACKGUARD; + stack++; } } int ICACHE_RAM_ATTR cont_check(cont_t* cont) { - if(cont->stack_guard1 != CONT_STACKGUARD || cont->stack_guard2 != CONT_STACKGUARD) return 1; + cont_footer_t* footer = (cont_footer_t*) cont->stack_end; + if(cont->stack_guard1 != CONT_STACKGUARD || footer->stack_guard2 != CONT_STACKGUARD) return 1; return 0; } int ICACHE_RAM_ATTR cont_get_free_stack(cont_t* cont) { - uint32_t *head = cont->stack; + uint32_t *head = (uint32_t*)(cont + 1); int freeWords = 0; while(*head == CONT_STACKGUARD) diff --git a/cores/esp8266/core_esp8266_main.cpp b/cores/esp8266/core_esp8266_main.cpp index 25a4f3f24f..6370c22092 100644 --- a/cores/esp8266/core_esp8266_main.cpp +++ b/cores/esp8266/core_esp8266_main.cpp @@ -75,14 +75,57 @@ void preloop_update_frequency() { extern void (*__init_array_start)(void); extern void (*__init_array_end)(void); -cont_t g_cont __attribute__ ((aligned (16))); +typedef struct thread_info_ { + cont_t* cont; + thread_func_t func; + void* func_arg; + struct thread_info_* next; + struct thread_info_* prev; +} thread_info_t; + +static void loop_wrapper(void*); + +static cont_static_t g_main_cont __attribute__ ((aligned (16))); + +thread_info_t g_thread_list = { (cont_t*) &g_main_cont, loop_wrapper, NULL, &g_thread_list, &g_thread_list }; +thread_info_t* g_current_thread = &g_thread_list; + static os_event_t g_loop_queue[LOOP_QUEUE_SIZE]; static uint32_t g_micros_at_task_start; +extern "C" cont_t* current_cont() { + return g_current_thread->cont; +} + +extern "C" int spawn(thread_func_t func, void* func_arg, unsigned int stack_size) { + stack_size = (stack_size + 0x0F) & ~0x0F; // Stack size must be a multiple of 16 + if (stack_size < 32) { + return 0; + } + cont_t* cont = (cont_t*) malloc(CONT_REQUIRED_SIZE(stack_size)); + if (!cont) { + return 0; + } + thread_info_t* thread_info = (thread_info_t*) malloc(sizeof(thread_info_t)); + if (!thread_info) { + free(cont); + return 0; + } + cont_init_size(cont, stack_size); + thread_info->cont = cont; + thread_info->func = func; + thread_info->func_arg = func_arg; + thread_info->next = &g_thread_list; + thread_info->prev = g_thread_list.prev; + g_thread_list.prev->next = thread_info; + g_thread_list.prev = thread_info; + return 1; +} + extern "C" void esp_yield() { - if (cont_can_yield(&g_cont)) { - cont_yield(&g_cont); + if (cont_can_yield(current_cont())) { + cont_yield(current_cont()); } } @@ -91,7 +134,7 @@ extern "C" void esp_schedule() { } extern "C" void __yield() { - if (cont_can_yield(&g_cont)) { + if (cont_can_yield(current_cont())) { esp_schedule(); esp_yield(); } @@ -103,16 +146,15 @@ extern "C" void __yield() { extern "C" void yield(void) __attribute__ ((weak, alias("__yield"))); extern "C" void optimistic_yield(uint32_t interval_us) { - if (cont_can_yield(&g_cont) && + if (cont_can_yield(current_cont()) && (system_get_time() - g_micros_at_task_start) > interval_us) { yield(); } } -static void loop_wrapper() { +static void loop_wrapper(void*) { static bool setup_done = false; - preloop_update_frequency(); if(!setup_done) { setup(); #ifdef DEBUG_ESP_PORT @@ -121,6 +163,11 @@ static void loop_wrapper() { setup_done = true; } loop(); +} + +static void thread_wrapper() { + preloop_update_frequency(); + g_current_thread->func(g_current_thread->func_arg); run_scheduled_functions(); esp_schedule(); } @@ -128,8 +175,9 @@ static void loop_wrapper() { static void loop_task(os_event_t *events) { (void) events; g_micros_at_task_start = system_get_time(); - cont_run(&g_cont, &loop_wrapper); - if (cont_check(&g_cont) != 0) { + g_current_thread = g_current_thread->next; + cont_run(current_cont(), thread_wrapper); + if (cont_check(current_cont()) != 0) { panic(); } } @@ -165,7 +213,7 @@ extern "C" void user_init(void) { initVariant(); - cont_init(&g_cont); + cont_init(&g_main_cont); ets_task(loop_task, LOOP_TASK_PRIORITY, g_loop_queue, diff --git a/cores/esp8266/core_esp8266_postmortem.c b/cores/esp8266/core_esp8266_postmortem.c index bfb0c06ac3..e6d4d3e707 100644 --- a/cores/esp8266/core_esp8266_postmortem.c +++ b/cores/esp8266/core_esp8266_postmortem.c @@ -32,7 +32,7 @@ extern void __real_system_restart_local(); extern void gdb_do_break(); -extern cont_t g_cont; +extern cont_t* current_cont(); // These will be pointers to PROGMEM const strings static const char* s_panic_file = 0; @@ -98,8 +98,9 @@ void __wrap_system_restart_local() { ets_puts_P(PSTR("\nSoft WDT reset\n")); } - uint32_t cont_stack_start = (uint32_t) &(g_cont.stack); - uint32_t cont_stack_end = (uint32_t) g_cont.stack_end; + cont_t* cont = current_cont(); + uint32_t cont_stack_start = (uint32_t) (cont + 1); // +1 gives the end of the cont structure + uint32_t cont_stack_end = (uint32_t) cont->stack_end; uint32_t stack_end; // amount of stack taken by interrupt or exception handler diff --git a/cores/esp8266/core_esp8266_wiring.c b/cores/esp8266/core_esp8266_wiring.c index fbbb5bfcf0..62a0e0a1e0 100644 --- a/cores/esp8266/core_esp8266_wiring.c +++ b/cores/esp8266/core_esp8266_wiring.c @@ -23,33 +23,28 @@ #include "ets_sys.h" #include "osapi.h" #include "user_interface.h" -#include "cont.h" -extern void esp_schedule(); -extern void esp_yield(); - -static os_timer_t delay_timer; static os_timer_t micros_overflow_timer; static uint32_t micros_at_last_overflow_tick = 0; static uint32_t micros_overflow_count = 0; -#define ONCE 0 #define REPEAT 1 -void delay_end(void* arg) { - (void) arg; - esp_schedule(); -} - void delay(unsigned long ms) { - if(ms) { - os_timer_setfn(&delay_timer, (os_timer_func_t*) &delay_end, 0); - os_timer_arm(&delay_timer, ms, ONCE); - } else { - esp_schedule(); + if (ms == 0) { + yield(); + } + else if (ms <= 1000 * 60 * 60) { // More precise version + unsigned long start = micros(); + unsigned long us = ms * 1000; + while (micros() - start < us) { + yield(); + } } - esp_yield(); - if(ms) { - os_timer_disarm(&delay_timer); + else { + unsigned long start = millis(); + while (millis() - start < ms) { + yield(); + } } } diff --git a/libraries/ESP8266Scheduler/ESP8266Scheduler.cpp b/libraries/ESP8266Scheduler/ESP8266Scheduler.cpp new file mode 100644 index 0000000000..d4515008f2 --- /dev/null +++ b/libraries/ESP8266Scheduler/ESP8266Scheduler.cpp @@ -0,0 +1,11 @@ +#include "ESP8266Scheduler.h" + +static void startLoopHelper(void *arg) { + reinterpret_cast(arg)(); +} + +void SchedulerClass::startLoop(SchedulerTask task, uint32_t stackSize) { + spawn(startLoopHelper, reinterpret_cast(task), stackSize); +} + +SchedulerClass Scheduler; diff --git a/libraries/ESP8266Scheduler/ESP8266Scheduler.h b/libraries/ESP8266Scheduler/ESP8266Scheduler.h new file mode 100644 index 0000000000..27866ecc90 --- /dev/null +++ b/libraries/ESP8266Scheduler/ESP8266Scheduler.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +extern "C" { + typedef void (*SchedulerTask)(void); +} + +class SchedulerClass { +public: + static void startLoop(SchedulerTask task, uint32_t stackSize = 1024); +}; + +extern SchedulerClass Scheduler; diff --git a/libraries/ESP8266Scheduler/examples/SchedulerStartLoop/SchedulerStartLoop.ino b/libraries/ESP8266Scheduler/examples/SchedulerStartLoop/SchedulerStartLoop.ino new file mode 100644 index 0000000000..b404331058 --- /dev/null +++ b/libraries/ESP8266Scheduler/examples/SchedulerStartLoop/SchedulerStartLoop.ino @@ -0,0 +1,19 @@ +#include + +static void blinkLoop() { + digitalWrite(LED_BUILTIN, HIGH); + delay(1000); + digitalWrite(LED_BUILTIN, LOW); + delay(1000); +} + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(115200); + Scheduler.startLoop(blinkLoop); +} + +void loop() { + Serial.println("Hello"); + delay(1000); +} diff --git a/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp b/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp index 7b90dc310f..decfc0ff94 100644 --- a/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp +++ b/libraries/ESP8266WiFi/src/ESP8266WiFiGeneric.cpp @@ -45,9 +45,6 @@ extern "C" { #include "WiFiUdp.h" #include "debug.h" -extern "C" void esp_schedule(); -extern "C" void esp_yield(); - // ----------------------------------------------------------------------------------------------------------------------- // ------------------------------------------------- Generic WiFi function ----------------------------------------------- @@ -426,7 +423,10 @@ void wifi_dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback void wifi_dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg); #endif -static bool _dns_lookup_pending = false; + +static uint32_t dnsLastKey; +static volatile uint32_t dnsResult; +static volatile uint32_t dnsKeyFinished; /** * Resolve the given hostname to an IP address. @@ -453,15 +453,18 @@ int ESP8266WiFiGenericClass::hostByName(const char* aHostname, IPAddress& aResul } DEBUG_WIFI_GENERIC("[hostByName] request IP for: %s\n", aHostname); - err_t err = dns_gethostbyname(aHostname, &addr, &wifi_dns_found_callback, &aResult); + uint32_t key = ++dnsLastKey; // Safe since we don't have thread preemption + err_t err = dns_gethostbyname(aHostname, &addr, &wifi_dns_found_callback, (void*)key); if(err == ERR_OK) { aResult = addr.addr; } else if(err == ERR_INPROGRESS) { - _dns_lookup_pending = true; - delay(timeout_ms); - _dns_lookup_pending = false; - // will return here when dns_found_callback fires - if(aResult != 0) { + u32 start = millis(); + while ((dnsKeyFinished != key) && ((millis() - start) < timeout_ms)) { + yield(); + } + uint32_t result = dnsResult; + if ((dnsKeyFinished == key) && (result != 0)) { + aResult = result; err = ERR_OK; } } @@ -488,13 +491,6 @@ void wifi_dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *ca #endif { (void) name; - if (!_dns_lookup_pending) { - return; - } - if(ipaddr) { - (*reinterpret_cast(callback_arg)) = ipaddr->addr; - } - esp_schedule(); // resume the hostByName function + dnsResult = ipaddr->addr; + dnsKeyFinished = (u32)callback_arg; } - - diff --git a/libraries/ESP8266WiFi/src/ESP8266WiFiScan.cpp b/libraries/ESP8266WiFi/src/ESP8266WiFiScan.cpp index a824951e65..a194623dd6 100644 --- a/libraries/ESP8266WiFi/src/ESP8266WiFiScan.cpp +++ b/libraries/ESP8266WiFi/src/ESP8266WiFiScan.cpp @@ -37,9 +37,6 @@ extern "C" { #include "debug.h" -extern "C" void esp_schedule(); -extern "C" void esp_yield(); - // ----------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------- Private functions ------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------------- @@ -96,7 +93,9 @@ int8_t ESP8266WiFiScanClass::scanNetworks(bool async, bool show_hidden) { return WIFI_SCAN_RUNNING; } - esp_yield(); + while(!ESP8266WiFiScanClass::_scanComplete) { + yield(); + } return ESP8266WiFiScanClass::_scanCount; } else { return WIFI_SCAN_FAILED; @@ -314,9 +313,7 @@ void ESP8266WiFiScanClass::_scanDone(void* result, int status) { ESP8266WiFiScanClass::_scanStarted = false; ESP8266WiFiScanClass::_scanComplete = true; - if(!ESP8266WiFiScanClass::_scanAsync) { - esp_schedule(); - } else if (ESP8266WiFiScanClass::_onComplete) { + if(ESP8266WiFiScanClass::_scanAsync && ESP8266WiFiScanClass::_onComplete) { ESP8266WiFiScanClass::_onComplete(ESP8266WiFiScanClass::_scanCount); ESP8266WiFiScanClass::_onComplete = nullptr; } diff --git a/libraries/ESP8266WiFi/src/include/ClientContext.h b/libraries/ESP8266WiFi/src/include/ClientContext.h index 639b3587ce..b40ac35fab 100644 --- a/libraries/ESP8266WiFi/src/include/ClientContext.h +++ b/libraries/ESP8266WiFi/src/include/ClientContext.h @@ -26,9 +26,6 @@ class WiFiClient; typedef void (*discard_cb_t)(void*, ClientContext*); -extern "C" void esp_yield(); -extern "C" void esp_schedule(); - #include "DataSource.h" class ClientContext @@ -126,8 +123,9 @@ class ClientContext } _connect_pending = 1; _op_start_time = millis(); - // This delay will be interrupted by esp_schedule in the connect callback - delay(_timeout_ms); + while (_connect_pending && !_is_timeout()) { + yield(); + } _connect_pending = 0; if (state() != ESTABLISHED) { abort(); @@ -161,7 +159,7 @@ class ClientContext return tcp_nagle_disabled(_pcb); } - void setTimeout(int timeout_ms) + void setTimeout(int timeout_ms) { _timeout_ms = timeout_ms; } @@ -334,15 +332,14 @@ class ClientContext void _notify_error() { - if (_connect_pending || _send_waiting) { - esp_schedule(); - } + _connect_pending = 0; + _send_pending = 0; } size_t _write_from_source(DataSource* ds) { assert(_datasource == nullptr); - assert(_send_waiting == 0); + assert(_send_pending == 0); _datasource = ds; _written = 0; _op_start_time = millis(); @@ -360,10 +357,12 @@ class ClientContext break; } - ++_send_waiting; - esp_yield(); + _send_pending = 1; + while (_send_pending && !_is_timeout()) { + yield(); + } } while(true); - _send_waiting = 0; + _send_pending = 0; return _written; } @@ -409,10 +408,7 @@ class ClientContext void _write_some_from_cb() { - if (_send_waiting == 1) { - _send_waiting--; - esp_schedule(); - } + _send_pending = 0; } err_t _sent(tcp_pcb* pcb, uint16_t len) @@ -489,7 +485,7 @@ class ClientContext (void) err; assert(pcb == _pcb); assert(_connect_pending); - esp_schedule(); + _connect_pending = 0; return ERR_OK; } @@ -538,8 +534,8 @@ class ClientContext size_t _write_chunk_size = 256; uint32_t _timeout_ms = 5000; uint32_t _op_start_time = 0; - uint8_t _send_waiting = 0; - uint8_t _connect_pending = 0; + volatile uint8_t _send_pending = 0; + volatile uint8_t _connect_pending = 0; int8_t _refcnt; ClientContext* _next; diff --git a/libraries/ESP8266WiFi/src/include/UdpContext.h b/libraries/ESP8266WiFi/src/include/UdpContext.h index cb527ab0af..e610ceaf84 100644 --- a/libraries/ESP8266WiFi/src/include/UdpContext.h +++ b/libraries/ESP8266WiFi/src/include/UdpContext.h @@ -24,8 +24,6 @@ class UdpContext; extern "C" { -void esp_yield(); -void esp_schedule(); #include "lwip/init.h" // LWIP_VERSION_ } diff --git a/package.json b/package.json index da97f8a4b7..6fe663bc68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "framework-arduinoespressif8266", + "name": "framework-arduinoespressif8266-threads", "description": "Arduino Wiring-based Framework (ESP8266 Core)", - "url": "https://github.com/esp8266/Arduino", - "version": "2.4.0-rc.1" + "url": "https://github.com/bmellstrom/esp8266-arduino-threads", + "version": "2.4.0-rc.1-threads.1" } diff --git a/tests/common.sh b/tests/common.sh index c6fa2cdeb7..0c4b507473 100755 --- a/tests/common.sh +++ b/tests/common.sh @@ -128,7 +128,7 @@ function install_platformio() pip install --user -U https://github.com/platformio/platformio/archive/develop.zip platformio platform install https://github.com/platformio/platform-espressif8266.git#feature/stage sed -i 's/https:\/\/github\.com\/esp8266\/Arduino\.git/*/' ~/.platformio/platforms/espressif8266_stage/platform.json - ln -s $TRAVIS_BUILD_DIR ~/.platformio/packages/framework-arduinoespressif8266 + ln -s $TRAVIS_BUILD_DIR ~/.platformio/packages/framework-arduinoespressif8266-threads # Install dependencies: # - esp8266/examples/ConfigFile pio lib install ArduinoJson diff --git a/tools/platformio-build.py b/tools/platformio-build.py index d3e78781aa..be84b148f5 100644 --- a/tools/platformio-build.py +++ b/tools/platformio-build.py @@ -31,7 +31,7 @@ env = DefaultEnvironment() platform = env.PioPlatform() -FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif8266") +FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif8266-threads") assert isdir(FRAMEWORK_DIR) @@ -92,4 +92,4 @@ join(FRAMEWORK_DIR, "cores", env.BoardConfig().get("build.core")) )) -env.Prepend(LIBS=libs) \ No newline at end of file +env.Prepend(LIBS=libs)