Skip to content

Commit c24d3b2

Browse files
authored
Add sample to demonstrate basic dual-core operation (SmingHub#2949)
This PR adds a sample application to show how to run code on the second CPU of an ESP32, RP2040 and RP2350 devices. Issue SmingHub#2943 highlights the sort of problem which occurs with the ESP32 when attempting to use timers to run code very frequently, where the CPU time required to service an interrupt and get the code to run is too long. The problem is the overhead impose by FreeRTOS and the nature of multitasking. A solution to this is to run time-sensitive code on the second CPU. Polled timers are a good choice here as they don't use interrupts at all.
1 parent c898bd9 commit c24d3b2

File tree

5 files changed

+185
-0
lines changed

5 files changed

+185
-0
lines changed

samples/Basic_Dual_Core/Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#####################################################################
2+
#### Please don't change this file. Use component.mk instead ####
3+
#####################################################################
4+
5+
ifndef SMING_HOME
6+
$(error SMING_HOME is not set: please configure it as an environment variable)
7+
endif
8+
9+
include $(SMING_HOME)/project.mk

samples/Basic_Dual_Core/README.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Dual Core
2+
=========
3+
4+
.. highlight:: text
5+
6+
Basic example of using second core of ESP32, RP2040 and RP2350 chips.
7+
8+
.. important::
9+
10+
Sming is NOT thread-safe! In general, only re-entrant code and code intended to be called from interrupt context is safe to use from multiple cores.
11+
12+
If code is timing sensitive then it should be run from IRAM. For RP2040 it *must* be in IRAM.
13+
14+
Flash and filing system access must only be done from the main Sming application.
15+
16+
17+
Required steps
18+
--------------
19+
20+
For the RP2040 (and RP2350) Sming builds are 'bare metal' so there is no operating system to consider.
21+
22+
For the ESP32 we require FreeRTOS so the following steps are necessary:
23+
24+
- Override IDF SDK config settings as in ``esp-dual-core.cfg``.
25+
- Add line to project's ``component.mk``: *SDK_CUSTOM_CONFIG := esp32-dual-core.cfg*
26+
- Add application code to run on second core and call to start for that core
27+
- Run `make sdk-config-clean` to ensure custom configuration values are picked up
28+
29+
The custom configuration enables dual-core operation (``CONFIG_FREERTOS_UNICORE=n``) and disables the idle task for the second core (``CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n``). This ensures that code will run uninterrupted.
30+
31+
Task stats shows this when running::
32+
33+
# | Core | Prio | Handle | Run Time | % Time | Name
34+
1 | 0 | 24 | 3ffaee9c | 0 | 0% | ipc0
35+
3 | 0 | 22 | 3ffaf568 | 3226 | 0% | esp_timer
36+
5 | 0 | 0 | 3ffb8974 | 1996774 | 49% | IDLE0
37+
2 | 1 | 24 | 3ffaf404 | 0 | 0% | ipc1
38+
8 | 1 | 18 | 3ffbdf44 | 33156 | 0% | Sming
39+
7 | 1 | 5 | 3ffb9d24 | 1966844 | 49% | Sming2
40+
6 | 1 | 0 | 3ffb9118 | 0 | 0% | IDLE1
41+
42+
43+
Bare Metal
44+
----------
45+
46+
Because we rely on the IDF and its dependency on FreeRTOS, the above approach also ensures that calls such as ``System.queueCallback`` will work. This is the recommended way to communicate between code running on different cores.
47+
48+
The default mode for Sming runs without FreeRTOS on the second core (*CONFIG_FREERTOS_UNICORE=y*) which provides a smaller set of tasks and thus lower system memory usage::
49+
50+
# | Core | Prio | Handle | Run Time | % Time | Name
51+
1 | 0 | 22 | 3ffaf470 | 29 | 0% | esp_timer
52+
4 | 0 | 18 | 3ffb778c | 11016 | 0% | Sming
53+
3 | 0 | 0 | 3ffafdb0 | 1988955 | 49% | IDLE
54+
55+
In this state it is technically possible to get code running on the second core by hooking a low-level startup routine. The application would need to handle stack/heap allocation and code would be far more limited in what it can safely do.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#include <SmingCore.h>
2+
#include <Services/Profiling/TaskStat.h>
3+
4+
#ifdef ARCH_RP2040
5+
#include <pico/multicore.h>
6+
#endif
7+
8+
#ifndef WIFI_SSID
9+
#define WIFI_SSID "PleaseEnterSSID"
10+
#define WIFI_PWD "PleaseEnterPass"
11+
#endif
12+
13+
namespace
14+
{
15+
Profiling::TaskStat taskStat(Serial);
16+
SimpleTimer statTimer;
17+
18+
const uint8_t outputPin = 22;
19+
bool nextOutputState;
20+
21+
const unsigned periodMicroseconds = 100;
22+
const unsigned timerInterval = periodMicroseconds / 2;
23+
const unsigned countsPerSecond = 1'000'000 / timerInterval;
24+
25+
void handleNotification(uint32_t param)
26+
{
27+
debug_i("NOTIFY %u", param);
28+
}
29+
30+
// Code to run on second CPU
31+
void IRAM_ATTR app2_main()
32+
{
33+
static unsigned count;
34+
35+
// We're going to toggle an IO pin very fast to evaluate jitter
36+
pinMode(outputPin, OUTPUT);
37+
38+
// Using a polled timer has virtually zero overhead as it accesses timing hardware directly
39+
PeriodicFastUs timer(timerInterval);
40+
41+
for(;;) {
42+
if(!timer.expired()) {
43+
continue;
44+
}
45+
46+
// NB. Can use SDK GPIO calls or direct hardware access if required
47+
digitalWrite(outputPin, nextOutputState);
48+
nextOutputState = !nextOutputState;
49+
50+
if(count % countsPerSecond == 0) {
51+
System.queueCallback(handleNotification, count);
52+
53+
debug_i("TICK");
54+
55+
// ESP32: This is required if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
56+
// WDT.alive();
57+
}
58+
++count;
59+
}
60+
}
61+
62+
} // namespace
63+
64+
void init()
65+
{
66+
Serial.begin(SERIAL_BAUD_RATE);
67+
Serial.systemDebugOutput(true);
68+
69+
Serial.println(_F("Dual-core demo."));
70+
71+
#ifndef DISABLE_WIFI
72+
// Station - WiFi client
73+
WifiStation.enable(true);
74+
WifiStation.config(_F(WIFI_SSID), _F(WIFI_PWD));
75+
#endif
76+
77+
const unsigned stackSize = 2048;
78+
79+
#ifdef ARCH_ESP32
80+
81+
/*
82+
Provide some periodic status so we can see what tasks are active.
83+
Note that this causes the CPU to stall for about 30ms so by default leave it disabled.
84+
*/
85+
statTimer.initializeMs<2000>([]() { taskStat.update(); });
86+
// statTimer.start();
87+
88+
/*
89+
Create task running on second core.
90+
As this is the only task allocated to the second CPU (idle task disabled)
91+
it will run without interference from the scheduler.
92+
*/
93+
const unsigned priority = 5;
94+
xTaskCreatePinnedToCore(TaskFunction_t(app2_main), "Sming2", stackSize, nullptr, priority, nullptr, 1);
95+
96+
#elif defined(ARCH_RP2040)
97+
98+
/*
99+
For RP2040 code runs without any OS interference ('bare metal')
100+
*/
101+
102+
static uint32_t stack[stackSize / 4];
103+
multicore_launch_core1_with_stack(app2_main, stack, stackSize);
104+
105+
#endif
106+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
COMPONENT_SOC := \
2+
esp32 \
3+
rp2040 \
4+
rp2350
5+
6+
DISABLE_NETWORK := 1
7+
8+
# Required for ESP32
9+
SDK_CUSTOM_CONFIG := esp32-dual-core.cfg
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Enable dual-core operation
2+
CONFIG_FREERTOS_UNICORE=n
3+
4+
# If watchdog is required, code will need to suspend periodically via `WDT.alive`.
5+
# Leaving it disabled ensures code runs uninterrupted on second core.
6+
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n

0 commit comments

Comments
 (0)