Public Access
The main loop rests between passes (#40)
It made 50,000 passes a second and kept core 1 100 % busy at rest. Keys are buffered by the keyboard controller, the consoles and the radio have their own tasks, and no Service ticks more often than every 50 ms, so the loop now rests 5 ms after a pass with the screen on and 20 ms with it off; never during a serial file transfer. Safe Mode's loop too. Screen off: 50 passes a second and core 1 at 1 %; screen on: 167 and 10 %. The chip settles 4 C cooler (34.3 against 38.3). GNSS, Gemini, an upload, the Sweep and the radio's interrupt all checked at the new pace. `tasks` shows the loop's passes; Debug Builds: `loop spin on|off`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -107,7 +107,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve
|
||||
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
||||
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
||||
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states |
|
||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, and each core's load |
|
||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
||||
| `net` | Bytes each network service has read and written since boot |
|
||||
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
||||
| `log level <0-5>` | ESP-IDF log level |
|
||||
@@ -122,6 +122,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve
|
||||
| `lora inject <hex> [rssi] [snr]` | Debug Builds: a packet into the Scanner as if received (nothing is sent) |
|
||||
| `crash` | The last crash: which firmware, why, task, PC and backtrace (from the core dump in flash) |
|
||||
| `coredump erase` | Forgets the core dump |
|
||||
| `loop spin on` / `loop spin off` | Debug Builds: make the main loop spin without resting, to compare load and radio noise |
|
||||
| `crash abort` / `crash wdt` | Debug Builds: crash on purpose, or hang the main loop until the watchdog fires |
|
||||
| `help` | Lists the commands |
|
||||
|
||||
|
||||
@@ -90,3 +90,25 @@ The App has five views, not four: Q121's network view is one of its own (Overvie
|
||||
- **`tasks` on the console** sampled twice inside one command at first, a quarter second apart, and showed the loop at 1 %: it was asleep in the command's own wait. It now samples, lets the loop run for a second, and prints.
|
||||
- **Low stack, flagged:** `IDLE0` (232 bytes left), `IDLE1` (328 to 352) and `spk_task` (256 to 264), all the framework's own tasks.
|
||||
- **Cost:** 15.6 KB of flash for the App and the counters (1,742,723 bytes, release). Nothing while it's closed; about 2 KB of history and samples while it's open.
|
||||
|
||||
## The main loop rests (issue #40)
|
||||
|
||||
The loop polled the keyboard, ticked the Services, ran the consoles and redrew when needed, then came straight back: 50,000 passes a second, and core 1 100 % busy with the device idle and the screen off.
|
||||
|
||||
Nothing needs that. The keyboard controller buffers key events; the consoles and the radio have their own tasks or interrupts; no Service asks for a tick more often than every 50 ms. So after each pass the loop now rests: **5 ms with the screen on, 20 ms with it off**, and not at all during a serial file transfer (`sd put`), which reads its bytes from the loop. Safe Mode's loop rests 5 ms too. Debug Builds have `loop spin on|off` to bring the old behaviour back for comparison.
|
||||
|
||||
### Measured (2026-10-06, Debug Build, Wi-Fi connected, GNSS on, on USB power)
|
||||
|
||||
| | Spinning | Resting |
|
||||
|---|---|---|
|
||||
| Passes a second, screen off | 50,160 | 50 |
|
||||
| Core 1 load, screen off | 100 % | 1 % |
|
||||
| Passes a second, screen on (Launcher) | 1,203 | 167 |
|
||||
| Core 1 load, screen on | 62 % | 10 % |
|
||||
| Chip temperature at rest, settled | 38.3 C | 34.3 C |
|
||||
| A 1.8 MB upload over the Debug Console | about 230 KB/s | 288 KB/s |
|
||||
|
||||
- Still working at this pace: GNSS (a 3D Fix, 22 satellites), a Gemini fetch (52 KB), the upload read back by SHA-256, the Sweep (still 606 to 610 ms a pass), the radio's DIO1 interrupt.
|
||||
- **Not measured:** the current drawn (no meter on the battery line), and how typing feels on the real keyboard: a key now waits up to 5 ms for the loop, 20 ms if it's the one that wakes the screen.
|
||||
- **The radio's noise floor didn't move** (-97 to -99 dBm at 125 kHz either way): the spinning loop wasn't the source (issue #20).
|
||||
- **Not done:** real sleep. The framework is built without power management (`CONFIG_PM_ENABLE` is off), so an idle core only halts until the next interrupt. Automatic light sleep would need the framework rebuilt with it, Wi-Fi in modem sleep, and the USB serial port's behaviour checked. A next step if battery life calls for it.
|
||||
|
||||
+38
-1
@@ -390,10 +390,17 @@ static std::vector<TaskSample> tasksBefore;
|
||||
static uint32_t tasksTotal = 0, tasksDueMs = 0;
|
||||
static bool tasksPending = false;
|
||||
|
||||
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
|
||||
#ifdef RORO_DEBUG
|
||||
static bool loopSpin = false; // `loop spin on`: no rest between passes, to compare load and radio noise
|
||||
#endif
|
||||
static uint32_t tasksPasses = 0;
|
||||
|
||||
static void tasksStep() {
|
||||
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
|
||||
tasksPending = false;
|
||||
system_info::printTasks(console, tasksBefore, tasksTotal);
|
||||
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
|
||||
std::vector<TaskSample>().swap(tasksBefore);
|
||||
}
|
||||
|
||||
@@ -421,6 +428,7 @@ static const char* const kHelp =
|
||||
#ifdef RORO_DEBUG
|
||||
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
||||
"wifi ip ... try <seconds> | wifi ip keep a trial IP setting: back to the previous one unless kept\n"
|
||||
"loop spin on|off make the main loop spin without resting, to compare load and radio noise\n"
|
||||
"lora inject <hex> [rssi] [snr] a packet into the LoRa Scanner as if received (nothing is sent)\n"
|
||||
"coredump get (Debug Console only) send the raw core dump: use scripts/rdbg.py coredump\n"
|
||||
"reset (Debug Console only) restart at once, even if the main loop is stuck\n"
|
||||
@@ -448,9 +456,16 @@ static void runCommand(String line) {
|
||||
console.printf("update: %s\n", update->onProbation() ? "on probation" : "confirmed");
|
||||
system_info::printSlots(console, nvs);
|
||||
}
|
||||
#ifdef RORO_DEBUG
|
||||
if (line == "loop spin on" || line == "loop spin off") {
|
||||
loopSpin = line.endsWith("on");
|
||||
console.printf("loop: %s\n", loopSpin ? "spinning, no rest" : "resting between passes");
|
||||
}
|
||||
#endif
|
||||
if (line == "tasks") { // sampled now, printed a second later by tasksStep(): the loop must run in between
|
||||
tasksTotal = system_info::sampleTasks(tasksBefore);
|
||||
tasksDueMs = millis() + 1000;
|
||||
tasksPasses = loopPasses;
|
||||
tasksPending = true;
|
||||
}
|
||||
if (line == "net") { // bytes each service has read and written since boot (S1, Q121)
|
||||
@@ -778,8 +793,30 @@ static void loopSafeMode() {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// How long the main loop rests after a pass (issue #40). Spinning, it made 50,000 passes a
|
||||
// second and kept core 1 100 % busy for nothing: keys are buffered by the keyboard controller,
|
||||
// the consoles and the radio have their own tasks, and no Service asks for a tick more often than
|
||||
// every 50 ms. Shorter with the screen on, so a key or a redraw never waits long.
|
||||
constexpr uint32_t kLoopRestScreenOnMs = 5;
|
||||
constexpr uint32_t kLoopRestScreenOffMs = 20;
|
||||
static void loopPass();
|
||||
|
||||
void loop() {
|
||||
if (safeMode) return loopSafeMode();
|
||||
loopPasses++;
|
||||
if (safeMode) {
|
||||
loopSafeMode();
|
||||
delay(kLoopRestScreenOnMs);
|
||||
return;
|
||||
}
|
||||
loopPass();
|
||||
#ifdef RORO_DEBUG
|
||||
if (loopSpin) return;
|
||||
#endif
|
||||
if (upload.active()) return; // a serial file transfer: every byte is read promptly
|
||||
delay(power->screen() == ScreenState::Off ? kLoopRestScreenOffMs : kLoopRestScreenOnMs);
|
||||
}
|
||||
|
||||
static void loopPass() {
|
||||
#ifdef RORO_TEST_CRASH
|
||||
// Test builds only (never in a release): crash during Probation to exercise Rollback.
|
||||
if (millis() > 5000) abort();
|
||||
|
||||
Reference in New Issue
Block a user