diff --git a/README.md b/README.md index 6657247..b409bb1 100644 --- a/README.md +++ b/README.md @@ -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 [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 | diff --git a/docs/milestones/S1.md b/docs/milestones/S1.md index 060cf9d..5b27276 100644 --- a/docs/milestones/S1.md +++ b/docs/milestones/S1.md @@ -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. diff --git a/src/main.cpp b/src/main.cpp index f367f80..6e4d2b4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -390,10 +390,17 @@ static std::vector 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(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().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 | 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 [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();