From 5a5b4b61739a291378866739a744698ca4722a13 Mon Sep 17 00:00:00 2001 From: Greg Cormier Date: Sat, 11 Jul 2026 08:54:24 -0400 Subject: [PATCH] Simulator speed and responsiveness overhaul Consolidates the optimizer work into a single change: - Event-driven master clock that skips provably idle ticks. - Per-tick micro-fixes in the hardware simulator hot path: lazy sim_time, early bail in print_steps, timer-enabled bitmask and direct ISR dispatch. - Serial polling syscall diet: one-time raw setup plus non-blocking read. - Yield the host CPU instead of busy-spinning the grbl thread. - Default to -O3 Release builds, with the cross-thread data races that optimization exposed fixed. - Add -v/--version reporting build-time info. - Fix chunky/unresponsive sender realtime view in socket mode. grbl/block output is byte-identical to before; -r sampling drift is bounded. --- .gitignore | 2 +- CMakeLists.tpl | 21 +++- CMakeLists.txt | 33 +++++- OPTIMIZATION_PLAN.md | 228 +++++++++++++++++++++++++++++++++++++++++ src/build_info.h.in | 10 ++ src/driver.c | 51 +++++++-- src/driver.h | 5 + src/eeprom.c | 11 +- src/grbl_interface.c | 65 +++++++++--- src/main.c | 101 +++++++++++++----- src/mcu.c | 177 ++++++++++++++++++++++++++------ src/mcu.h | 54 ++++++---- src/platform_linux.c | 99 ++++++++++-------- src/platform_windows.c | 27 +++++ src/serial.c | 7 ++ src/simulator.c | 78 +++++++++++--- src/simulator.h | 5 +- 17 files changed, 805 insertions(+), 169 deletions(-) create mode 100644 OPTIMIZATION_PLAN.md create mode 100644 src/build_info.h.in diff --git a/.gitignore b/.gitignore index 4fcd7bd..a241983 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,7 @@ $RECYCLE.BIN/ # Build folder -build +build* # # ========================= diff --git a/CMakeLists.tpl b/CMakeLists.tpl index a6d01be..3967c1d 100644 --- a/CMakeLists.tpl +++ b/CMakeLists.tpl @@ -6,6 +6,20 @@ add_compile_definitions(F_CPU=16000000) %build_flags% +# Capture the effective build-time flags for the -v/--version output. +string(TOUPPER "${CMAKE_BUILD_TYPE}" _sim_build_type) +if(_sim_build_type STREQUAL "") + set(SIM_BUILD_TYPE "None") +else() + set(SIM_BUILD_TYPE "${CMAKE_BUILD_TYPE}") +endif() +string(STRIP "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${_sim_build_type}}" SIM_C_FLAGS) +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../src/build_info.h.in + ${CMAKE_CURRENT_BINARY_DIR}/build_info.h + @ONLY +) + include(../src/grbl/CMakeLists.txt) if (WIN32) @@ -53,13 +67,16 @@ add_executable(grblHAL_sim ${platform_SRC} ) -target_link_libraries(grblHAL_sim PRIVATE +target_link_libraries(grblHAL_sim PRIVATE m grbl ${platform_LIB} ) -add_executable(grblHAL_validator +# Generated build_info.h lands in the build dir. +target_include_directories(grblHAL_sim PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_executable(grblHAL_validator ../src/eeprom.c ../src/grbl_eeprom_extensions.c ../src/validator.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d1c19e9..d9b3d13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,34 @@ cmake_minimum_required(VERSION 3.10) project(grblHAL-sim C) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel) +endif() + +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + # ISR handlers run on a separate host thread; keep type-punning-heavy + # embedded code out of strict-aliasing territory. + add_compile_options(-fno-strict-aliasing) +endif() + add_compile_definitions(F_CPU=16000000) #add_compile_definitions(SQUARING_ENABLED=1) +# Capture the effective build-time flags for the -v/--version output. +string(TOUPPER "${CMAKE_BUILD_TYPE}" _sim_build_type) +if(_sim_build_type STREQUAL "") + set(SIM_BUILD_TYPE "None") +else() + set(SIM_BUILD_TYPE "${CMAKE_BUILD_TYPE}") +endif() +string(STRIP "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${_sim_build_type}}" SIM_C_FLAGS) +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/src/build_info.h.in + ${CMAKE_CURRENT_BINARY_DIR}/build_info.h + @ONLY +) + include(src/grbl/CMakeLists.txt) if (WIN32) @@ -16,6 +41,7 @@ if (WIN32) set(platform_LIB ws2_32 + winmm # timeBeginPeriod/timeEndPeriod (1 ms Sleep granularity) ) endif(WIN32) @@ -51,13 +77,16 @@ add_executable(grblHAL_sim ${platform_SRC} ) -target_link_libraries(grblHAL_sim PRIVATE +target_link_libraries(grblHAL_sim PRIVATE m grbl ${platform_LIB} ) -add_executable(grblHAL_validator +# Generated build_info.h lands in the build dir. +target_include_directories(grblHAL_sim PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_executable(grblHAL_validator src/eeprom.c src/grbl_eeprom_extensions.c src/validator.c diff --git a/OPTIMIZATION_PLAN.md b/OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..adbc092 --- /dev/null +++ b/OPTIMIZATION_PLAN.md @@ -0,0 +1,228 @@ +# grblHAL-sim performance plan (x64, code-only, no functional change) + +Goal: make the simulator more responsive and able to hold real-time (and faster) +pace, like a real MCU that never lags its own clock. Functionality and +simulation accuracy (exact tick at which every IRQ fires) must be preserved. + +## Measured baseline (2026-07-10, WSL2 x64) + +Workload: `printf '$X\nG1X10F600\nG1X0\n\x06'` piped to `grblHAL_sim -t 0 -n -g /dev/null` +(~3.1 s of simulated time: 1 s serial-input warmup + streaming + 2 s of motion). + +- Current build has **no `CMAKE_BUILD_TYPE`** → compiled at **-O0**. Wall: **1.55 s**, CPU: 2.8 s (both threads pegged). +- Same code at `-O3` (`-DCMAKE_BUILD_TYPE=Release`): Wall: **0.87 s** → **1.8× end-to-end, zero code change**. +- gprof of the `-O2` build (sim thread dominates samples; grbl thread visible via call counts): + +| % time | function | calls | note | +|---|---|---|---| +| 31% | `mcu_master_clock` | 23.7 M | once per simulated CPU tick | +| 6.4% | `sim_loop` | — | incl. per-tick float division for `sim_time` | +| 5.9% + 2.5% | `print_steps` + `grbl_per_tick` | 23.7 M | called every tick even when step reporting is off | +| ~3% | `plan_get_current_block` | 29.7 M | 24 M of these from `print_steps` | +| ~25% | `protocol_exec_rt_system`, `task_execute`, `serialGetC`, `st_prep_buffer`, … | ~5 M each | grbl thread busy-spinning its main loop | + +Key structural fact: `sim_loop` does one full loop iteration per simulated tick — +16 M iterations per simulated second — even when the next interesting event +(timer expiry, serial byte) is thousands of ticks away. + +## Ordered plan (biggest win first) + +Status: item #1 done (branch `optimizer`, 9afb4dc). Item #2 done (branch +`optimizer2`): benchmark 0.90 s → 0.02 s; stepper ISR interval trace verified +identical to per-tick simulation (32 857 firings, 0 mismatches). Implementing +#2 exposed and fixed three latent wall-vs-sim-time bugs: input fed before the +grbl thread's boot flushed the rx stream (now paced by a main-loop heartbeat, +at most one byte per pass — matching real hardware, where the main loop runs +many times per serial byte); ^F exit aborting mid-G4-dwell and truncating +output (exit now requires true quiescence, confirmed across a wall-clock +pause); eeprom_close() crashing when the grbl thread had not opened the file. + +All five items are now consolidated on branch `finaloptimize`, +merged one at a time from `optimizer`/`optimizer2`..`optimizer5`: +- #3 done (branch `optimizer3`): per-tick micro-fixes — lazy `sim_time`, + early bail in `print_steps` before `plan_get_current_block()`, and a + timer-enabled bitmask + `gpio_irq_pending` flag + direct `isr[]` dispatch in + `mcu_master_clock`. Merged with #2's event-skip: the two share the `timer[]` + and GPIO state cleanly (all `timer[]` enables now route through + `mcu_timer_enable()` so the mask stays a superset of live timers). +- #4 done (branch `optimizer4`): `sched_yield()` in the grbl realtime hook and + the `driver_delay_ms` spin loop; kept alongside #2's `grbl_pulse` heartbeat. +- #5 done (branch `optimizer5`): one-time raw terminal setup + non-blocking + `read()` for stdin/socket serial polling. + +Regression across the whole merge: grbl response output and block output stay +byte-identical to the pre-merge (item #1) build for the plan workload. The +`-r 0.01` sampled step trace drifts only in the last timestamp digit (<1e-4 s) +and by at most ±1 step at a sample boundary — a sampling-window artifact of the +lazy/event-driven timing, not motion divergence: final position and the +full-resolution ISR step trace are unchanged. + +### 1. Build at -O3 by default (trivial, measured 1.8×; -O2 measured ~20% slower) +`CMakeLists.txt`: default `CMAKE_BUILD_TYPE` to `Release` when unset (keep it +overridable). Add `-fno-strict-aliasing` (negligible cost; removes a UB class +common in embedded-style code). + +Safety notes (verified 2026-07-10): +- The real optimization hazard in this codebase is not -O3-specific: it is + cross-thread sharing of plain variables (embedded ISR idioms running on OS + threads). Those are equally exposed at -O1/-O2, so lowering the level buys + no safety — fix the variables instead (see item #4). Empirically checked: + the -O3 binary still re-reads `delay.ms` from memory in the + `driver_delay_ms` spin loop (disassembly verified) and a `G4 P1` dwell + completes; grbl core stream ring-buffer head/tail are already `volatile`, + which is sound on x86-64 TSO for SPSC use. Still land the `volatile` fixes + in item #4 in the same PR as this flag change — today's behavior is + compiler luck, not a contract. +- **Defer LTO** until item #4's shared-state audit is done: several racy + variables (e.g. `ticks` read via the `hal.get_elapsed_ticks` pointer) are + currently protected only by cross-TU call boundaries, which LTO dissolves. +- Skip `-march=native`, or pair it with `-ffp-contract=off`: GCC's default + FMA contraction would change planner float rounding and break the + byte-identical output diffs used as the regression harness. Plain -O3 does + not alter FP semantics (no -ffast-math). + +### 2. Event-driven clock: batch-advance `masterclock` to the next event (order-of-magnitude) +Replace per-tick iteration in `sim_loop`/`mcu_master_clock` with: +compute `next_event = min(` ticks-to-expiry of each enabled timer (incl. +prescaler), systick expiry, `next_byte_tick`, `target_ticks` `)`, jump +`masterclock` forward in one step, then run the event's ISR **at exactly the +same tick value it fires today**. Recompute after every event (ISRs reprogram +timers). + +- This is the change that makes it behave like a real MCU: today the sim tops + out around a few × real time and can *fall behind* real time under load + (jitter, laggy status/jog response). Event skipping removes the per-tick tax + entirely; idle/waiting periods become free. +- Care needed (accuracy contract): + - timer reload semantics in `mcu.c` (`value==0 → reload without IRQ`, IRQ on 1→0), + prescaler behavior, `enable`/`irq_enable` flags; + - GPIO pin-change IRQs are raised outside the clock (from `on_byte` / + `mcu_gpio_in`) and serviced on the *next* tick — after any GPIO input + change, the next event must be "now + 1 tick"; + - `sim.on_tick` (→ `print_steps`) only does work when a block changes or + `sim_time` crosses `next_print_time`; make the print threshold an event so + `-r` output timestamps stay identical. +- **Verification (required):** instrument old and new builds to log + `(masterclock, irq_number)` for every ISR dispatch; run several g-code + workloads (incl. homing-style limit toggles and `-r 0.01` step reporting) and + diff the traces byte-for-byte, plus diff serial/block/step output files. + +### 3. Per-tick micro-fixes (low-risk; independent of #2, also its fallback — ~1.5–2× on sim thread) +- `simulator.c:67`: delete the per-tick `sim.sim_time = (float)masterclock/(float)F_CPU` + (a u64→float convert + divide, 16 M/s). Compute lazily in `print_steps` from + `sim.masterclock`. Keep the same float rounding if byte-identical `-r` output matters. +- `grbl_interface.c print_steps`: return on `next_print_time == 0.0` (the + default) *before* calling `plan_get_current_block()`; keep the + `exit_REQ`/`state_get()` check first. Removes ~24 M calls/s. +- `mcu.c mcu_master_clock`: keep a bitmask of enabled timers and a global + `gpio_irq_pending` flag (set in `mcu_gpio_in`, cleared when serviced) so an + idle tick tests two words instead of scanning 3 timers + 10 GPIO ports; + replace the timer `switch` with `isr[Timer0_IRQ + i]()`. + +### 4. Stop the grbl thread from burning a full host core +grbl's main loop and `driver_delay_ms`'s `while(delay.ms);` spin at host speed +(~5.3 M polls of `protocol_execute_realtime` in a 0.9 s run; process CPU ≈ 2× +wall). On real hardware that spin is authentic; on the host it steals cycles +from the sim thread and hurts interactive responsiveness. +- Conservative: `sched_yield()` in `sim_process_realtime` (`driver.c`) and in + the `delay.ms` wait loop. +- Better: µs-scale adaptive sleep only when rx buffer empty, no delay pending, + and state idle — but measure that high step rates (planner starvation) are + unaffected before keeping it. +- While there: `delay.ms` is read in a spin loop but `delay` is not `volatile` + — works today at -O3 by luck; make it `volatile` (needed anyway once builds + default to -O3, item #1). + +### 5. Serial polling syscall diet +`platform_linux.c platform_poll_stdin` does `tcgetattr` + 2× `tcsetattr` + +`select` + `getchar` per poll, once per simulated byte-time (~11.5 k/s × speedup). +Set the terminal raw **once** in `platform_init()`, restore in +`platform_terminate()` + `atexit`; poll with one non-blocking `read()`. +Same for `sim_socket_in` (`main.c`): put the accepted socket in `O_NONBLOCK` +and use plain `read()`; keep `select` only for detecting new connections. +Matters more as items 1–3 raise the achievable speedup ceiling. + +### 6. Smaller / later (measure first) +- ~~Shrink the 100 ms control frame in `sim_loop` once the tick loop is cheap — + lowers worst-case interactive latency/jitter at `-t 1`.~~ Done as part of the + interactive-latency work below (100 ms → 20 ms). +- `fflush` per line in `print_steps` only matters with `-r`; leave unless profiled. + +## Interactive latency: chunky/unresponsive sender realtime view (2026-07-11, branch `alloptimized`) + +Symptom: gSender's realtime view (DRO/visualizer) is chunky or intermittently +unresponsive when connected to the Windows simulator over `-p `. Root +cause was three independent, compounding problems — none of them in the tick +loop the earlier items optimized: + +1. **Structural response latency from the 100 ms control frame.** Serial input + is only polled during a frame's simulation burst; a `?` arriving while + `sim_loop` sleeps out the rest of the frame waits for the next one. + Measured `?`→status-report latency at `-t 1` (40 polls at 250 ms cadence, + Linux build, loopback): **p50 46 ms, p90 51 ms, max 90 ms**. On Windows, + `Sleep()`'s default 15.6 ms granularity added a scheduler quantum of jitter + on top. +2. **Console output blocks the simulator thread.** In socket mode, + `printBlock()` still wrote one line per planned block to stdout (with + `fflush`) from the sim thread's per-byte hook. On Windows that console is + conhost — slow per write, and **completely blocked while the user has a + QuickEdit text selection active** (a stray click in the console window + freezes the entire simulator: input polling, output draining, everything). + `_kbhit()` was also being called once per byte slot (~11.5 k conhost + round-trips per simulated second). +3. **Nagle + delayed ACK on the response socket.** `sim_socket_out` flushed on + both `\r` and `\n`, so every `"...\r\n"` line went out as two `send()`s: the + body and a lone `\n` byte. With Nagle enabled (no `TCP_NODELAY` anywhere) + the trailing byte waits for the peer's ACK, which Windows delays up to + ~200 ms — typically until gSender's *next* `?` poll. A line parser that + splits on `\n` therefore sees each status report roughly one poll period + late, quantizing the realtime view to the poll rate. + +Fixes (all landed together): +- `main.c sim_socket_in` (both platforms): set `TCP_NODELAY` on the accepted socket. +- `simulator.c sim_socket_out`: flush on `\n` or buffer-full only — one + `send()` per line. Verified post-fix: every report arrives as a single + segment ending `\r\n`. +- `simulator.c sim_loop`: control frame 100 ms → 20 ms (worst-case added input + latency ≈ frame length; per-frame bookkeeping is cheap since item #2). +- `platform_windows.c platform_init/terminate`: `timeBeginPeriod(1)` / + `timeEndPeriod(1)` so 20 ms frame sleeps are accurate (links `winmm`), and + QuickEdit mode disabled on the console (restored on exit) so clicking the + window can no longer freeze the sim. +- `grbl_interface.c grbl_per_byte`: in socket mode, block output is only + printed when `-b` redirected it away from stdout (**behavior change**: with + `-p` and no `-b`, block lines are no longer printed — pass `-b ` to + keep them), and the console-key poll (`_kbhit`) is throttled to every 128 + byte slots (~11 ms sim time). +- `driver.c`: Windows `sim_yield()` upgraded from no-op to `SwitchToThread()`, + matching the Linux `sched_yield()` from item #4. + +Measured result (same 40-poll harness, Linux build): + +| `?` → report latency | before | after | +|---|---|---| +| p50 | 46 ms | 6.8 ms | +| mean | 45 ms | 7.8 ms | +| p90 | 51 ms | 9.7 ms | +| max | 90 ms | 27 ms | + +The Windows-only wins (delayed-ACK stalls, QuickEdit freezes, Sleep +granularity) come on top of this and could not be measured from WSL — verify +by eye in gSender on the Windows build. + +Regression (plan workload, `-t 0`): serial and block output byte-identical +pre/post; `-r 0.01` step trace shows only the already-documented +sampling-boundary artifact (last-digit timestamps, ±1 step at sample +boundaries; final position identical). `-t 1` pacing verified: 5.07 s wall for +5.07 s of simulated activity. + +## Out of scope but observed +- `mcu.c mcu_gpio_in`: `changed`/`bitflag` are `uint8_t` while ports are 16-bit — + pin-change IRQs can never fire for pins 8–15. Latent correctness bug (all + current masks are 8-bit), not a perf item. Worth a separate fix/issue. + +## Benchmark & regression harness for every item +- Speed: fixed `EEPROM.DAT`, the workload above, `time` at `-t 0`; also a long + job (many short segments) to stress the stepper ISR rate. +- Correctness: byte-diff of serial output, block output, `-r 0.01` step trace, + and (for #2) the ISR `(tick, irq)` trace. diff --git a/src/build_info.h.in b/src/build_info.h.in new file mode 100644 index 0000000..0b8303e --- /dev/null +++ b/src/build_info.h.in @@ -0,0 +1,10 @@ +/* Generated by CMake from build_info.h.in - do not edit directly. */ +#ifndef _BUILD_INFO_H_ +#define _BUILD_INFO_H_ + +#define SIM_BUILD_TYPE "@SIM_BUILD_TYPE@" +#define SIM_C_FLAGS "@SIM_C_FLAGS@" +#define SIM_COMPILER "@CMAKE_C_COMPILER_ID@ @CMAKE_C_COMPILER_VERSION@" +#define SIM_TARGET "@CMAKE_SYSTEM_NAME@ @CMAKE_SYSTEM_PROCESSOR@" + +#endif diff --git a/src/driver.c b/src/driver.c index 1f0f79c..191fa61 100644 --- a/src/driver.c +++ b/src/driver.c @@ -20,10 +20,30 @@ */ #include +#include + +// On the host, the grbl thread and the simulator thread share one (or few) real +// CPU cores. grbl's main loop and driver_delay_ms's spin loop run at full host +// speed, so without cooperation they starve the simulator thread and burn a +// whole core (process CPU ~= 2x wall). Yielding the host CPU here does NOT change +// simulation timing: the simulated tick at which every event fires is driven by +// the simulator thread's clock/ISRs, not by how often the grbl thread polls. +// It only lets the scheduler hand cycles to the simulator thread when the grbl +// thread would otherwise busy-spin. +#ifndef WIN32 +#include +#define sim_yield() sched_yield() +#else +// SwitchToThread() hands the rest of the timeslice to another ready thread +// (the simulator thread) and returns immediately when none is waiting - the +// Windows equivalent of sched_yield(). windows.h comes in via platform.h. +#define sim_yield() SwitchToThread() +#endif #include "mcu.h" #include "driver.h" #include "serial.h" +#include "simulator.h" #include "eeprom.h" #include "grbl_eeprom_extensions.h" #include "platform.h" @@ -34,10 +54,12 @@ #define SQUARING_ENABLED 0 #endif +// NOTE: probe_invert, ticks and delay are shared between the grbl thread and the +// simulator thread's ISRs; they need volatile/_Atomic so stores are visible across threads. static spindle_id_t spindle_id; -static bool probe_invert; -static uint32_t ticks = 0; -static delay_t delay = { .ms = 1, .callback = NULL }; // NOTE: initial ms set to 1 for "resetting" systick timer on startup +static volatile bool probe_invert; +static _Atomic uint32_t ticks = 0; +static volatile delay_t delay = { .ms = 1, .callback = NULL }; // NOTE: initial ms set to 1 for "resetting" systick timer on startup static on_execute_realtime_ptr on_execute_realtime; void SysTick_Handler (void); @@ -55,7 +77,8 @@ static void driver_delay_ms (uint32_t ms, void (*callback)(void)) if((delay.ms = ms) > 0) { systick_timer.enable = 1; if(!(delay.callback = callback)) - while(delay.ms); + while(delay.ms) + sim_yield(); // hand the host CPU to the simulator thread while waiting; delay.ms is decremented by SysTick on the sim thread } else if(callback) callback(); } @@ -112,7 +135,7 @@ static void stepperWakeUp (void) { timer[STEPPER_TIMER].load = 5000; timer[STEPPER_TIMER].value = 0; - timer[STEPPER_TIMER].enable = 1; + mcu_timer_enable(STEPPER_TIMER, true); // enable + keep timer_enabled_mask in sync // hal.stepper_interrupt_callback(); // start the show } @@ -122,7 +145,7 @@ static void stepperGoIdle (bool clear_signals) { timer[STEPPER_TIMER].value = 0; timer[STEPPER_TIMER].load = 0; - timer[STEPPER_TIMER].enable = 0; + mcu_timer_enable(STEPPER_TIMER, false); // disable + keep timer_enabled_mask in sync if(clear_signals) { set_step_outputs((axes_signals_t){0}); @@ -135,7 +158,7 @@ static void stepperCyclesPerTick (uint32_t cycles_per_tick) { timer[STEPPER_TIMER].load = cycles_per_tick; timer[STEPPER_TIMER].value = 0; - timer[STEPPER_TIMER].enable = 1; + mcu_timer_enable(STEPPER_TIMER, true); // enable + keep timer_enabled_mask in sync } // "Normal" version: Sets stepper direction and pulse pins and starts a step pulse a few nanoseconds later. @@ -413,7 +436,14 @@ bool driver_setup (settings_t *settings) // ensures hardware simulator gets some cycles in "parallel" void sim_process_realtime (uint_fast16_t state) { - //platform_sleep(0); // yield needed? or simply trust the OS's thread scheduler... + // heartbeat for the simulator thread: the boot sequence (which flushes the + // input stream) is done and the main loop has run since the last beat. + // The serial feed is paced against it so a burst of simulated time cannot + // deliver input faster than the grbl thread consumes it - mirroring real + // hardware, where the main loop runs many times between two serial bytes. + sim.grbl_pulse++; + + sim_yield(); // yield to the simulator thread instead of spinning grbl's main loop at full host speed on_execute_realtime(state); } @@ -422,6 +452,11 @@ uint32_t millis (void) return ticks; } +bool driver_delay_pending (void) +{ + return delay.ms != 0; +} + bool driver_init () { mcu_reset(); diff --git a/src/driver.h b/src/driver.h index ea6b4df..a3e51f9 100644 --- a/src/driver.h +++ b/src/driver.h @@ -60,3 +60,8 @@ #define PROBE_BIT (1< + +// true while a hal.delay_ms() countdown is running (e.g. the grbl thread blocked in a G4 dwell) +bool driver_delay_pending (void); diff --git a/src/eeprom.c b/src/eeprom.c index 51721be..dcf522d 100644 --- a/src/eeprom.c +++ b/src/eeprom.c @@ -54,9 +54,10 @@ static FILE *eeprom_create_empty_file (void) return fp; } +static FILE* EEPROM_FP = NULL; + static FILE *eeprom_fp (void) { - static FILE* EEPROM_FP = NULL; static int tried = 0; if (!EEPROM_FP && !tried) { @@ -72,8 +73,12 @@ static FILE *eeprom_fp (void) void eeprom_close (void) { - FILE* fp = eeprom_fp(); - fclose(fp); + // NOTE: may run from the exit handler while the grbl thread never got as + // far as opening the file - do not open (or close) it on its behalf here. + if (EEPROM_FP) { + fclose(EEPROM_FP); + EEPROM_FP = NULL; + } } uint8_t eeprom_get_char(uint32_t addr ) diff --git a/src/grbl_interface.c b/src/grbl_interface.c index d491cfa..738e865 100644 --- a/src/grbl_interface.c +++ b/src/grbl_interface.c @@ -39,6 +39,18 @@ double next_print_time; static void print_steps(bool force); static void printBlock(void); +//True when the grbl thread has nothing left to do: no queued input, no planned +//or executing motion, no running delay (G4 dwell) and no response bytes still +//on their way out. The state check alone misses dwells, which run in STATE_IDLE. +static bool grbl_is_idle (void) +{ + return state_get() < STATE_HOMING && + plan_get_current_block() == NULL && + !driver_delay_pending() && + hal.stream.get_rx_buffer_count() == 0 && + !(uart.tx_irq_enable || uart.tx_flag); +} + void grbl_app_init (void) { //setup local tacking vars @@ -59,6 +71,13 @@ void grbl_per_tick (void) void grbl_per_byte (void) { if(sim.socket_fd) { + // In socket mode the console keyboard only toggles simulated pins, so + // polling it every byte slot (~11.5k conhost round-trips per simulated + // second on Windows) is wasted syscalls. Every 128 slots (~11 ms of + // sim time at 115200 baud) is far below human reaction time. + static uint_fast8_t kbd_divider = 0; + + if((++kbd_divider & 0x7f) == 0) switch (platform_poll_stdin()) { case 'e': @@ -133,7 +152,13 @@ void grbl_per_byte (void) break; } -// if(args.block_out_file != stdout) + // printBlock() runs on the sim thread; in socket mode its default + // stdout target is a live console, and a console write that stalls + // (Windows QuickEdit text selection blocks writers outright, and + // conhost rendering is slow even when healthy) freezes the entire + // simulator - serial polling included. Only print when -b redirected + // block output to a file. + if(args.block_out_file != stdout) printBlock(); //maybe print newest block } else printBlock(); //maybe print newest block @@ -147,27 +172,41 @@ void grbl_app_exit (void) //show current position in steps static void print_steps (bool force) -{ +{ static plan_block_t* printed_block = NULL; - plan_block_t* current_block = plan_get_current_block(); - int ocr = 0; - //Allow exit when idle. Prevents aborting before all streamed commands have run - if (sim.exit == exit_REQ && state_get() < STATE_HOMING ) - sim.exit = exit_OK; + //(kept first - order matters: this must run every tick regardless of -r). + if (sim.exit == exit_REQ && grbl_is_idle()) { + //The grbl thread runs on wall time and may only look idle for a moment, + //e.g. between the hal.delay_ms() slices of a G4 dwell or between draining + //the input and planning a move. Give it real time to show new activity + //before trusting the snapshot; simulated time is no yardstick for this + //since it can run far ahead of the grbl thread's progress. + platform_sleep(1000); + if (grbl_is_idle()) + sim.exit = exit_OK; + } if (next_print_time == 0.0) - return; //no printing + return; //no printing - bail before the (per-tick) plan_get_current_block() + + plan_block_t* current_block = plan_get_current_block(); + int ocr = 0; + + // Derive sim time lazily from masterclock. This is byte-identical to the value + // formerly stored in sim.sim_time: the same (float)masterclock/(float)F_CPU + // float division, assigned to a double, so -r timestamps are unchanged. + double sim_time = (float)sim.masterclock / (float)F_CPU; #ifdef VARIABLE_SPINDLE if(SPINDLE_TCCRA_REGISTER >= 127) ocr = SPINDLE_OCR_REGISTER; #endif if (current_block != printed_block) { - //new block. + //new block. if (block_number) //print values from the end of prev block - fprintf(args.step_out_file, "%12.5f %d, %d, %d, %d\n", sim.sim_time, sys.position[X_AXIS], sys.position[Y_AXIS], sys.position[Z_AXIS],ocr); + fprintf(args.step_out_file, "%12.5f %d, %d, %d, %d\n", sim_time, sys.position[X_AXIS], sys.position[Y_AXIS], sys.position[Z_AXIS],ocr); printed_block = current_block; if (current_block == NULL) @@ -176,11 +215,11 @@ static void print_steps (bool force) fprintf(args.step_out_file, "# block number %d\n", block_number++); } //print at correct interval while executing block - else if ((current_block && sim.sim_time>=next_print_time) || force ) { - fprintf(args.step_out_file, "%12.5f %d, %d, %d, %d\n", sim.sim_time, sys.position[X_AXIS], sys.position[Y_AXIS], sys.position[Z_AXIS], ocr); + else if ((current_block && sim_time>=next_print_time) || force ) { + fprintf(args.step_out_file, "%12.5f %d, %d, %d, %d\n", sim_time, sys.position[X_AXIS], sys.position[Y_AXIS], sys.position[Z_AXIS], ocr); fflush(args.step_out_file); //make sure the simulation time doesn't get ahead of next_print_time - while (next_print_time <= sim.sim_time) + while (next_print_time <= sim_time) next_print_time += args.step_time; } } diff --git a/src/main.c b/src/main.c index 38224ee..7843477 100644 --- a/src/main.c +++ b/src/main.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #ifdef WIN32 #include @@ -35,11 +36,13 @@ #else #include #include +#include #endif #include "simulator.h" #include "eeprom.h" #include "grbl_interface.h" +#include "build_info.h" #include "grbl/grbllib.h" @@ -70,6 +73,7 @@ void print_usage(const char* badarg) " -p : port to open raw telnet communication.\n" " -c : character to print before each line from grbl. default = '#'\n" " -n : no comments before grbl response lines.\n" + " -v : print version and build information.\n" " -h : this help.\n" "\n" " and can be specifed with option flags or positional parameters\n" @@ -79,6 +83,21 @@ void print_usage(const char* badarg) progname); } +void print_version(void) +{ + printf("grblHAL simulator\n" + " Build type : %s\n" + " Compiler : %s\n" + " Target : %s\n" + " C flags : %s\n" + " Built : %s %s\n", + SIM_BUILD_TYPE, + SIM_COMPILER, + SIM_TARGET, + SIM_C_FLAGS[0] ? SIM_C_FLAGS : "(none)", + __DATE__, __TIME__); +} + //wrapper for thread interface PLAT_THREAD_FUNC(grbl_main_thread, exit) { @@ -99,6 +118,10 @@ uint8_t sim_socket_in() if(sim.socket_fd == INVALID_SOCKET) { sim.socket_fd = accept(socket_fd, NULL, NULL); setsockopt(sim.socket_fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&t, sizeof(t)); + // Disable Nagle: senders (gSender & co) poll with single-byte '?' and + // responses are small; Nagle + delayed ACK adds up to ~200 ms per line. + BOOL nodelay = TRUE; + setsockopt(sim.socket_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&nodelay, sizeof(nodelay)); u_long mode = 1; ioctlsocket(sim.socket_fd, FIONBIO, &mode); // set non-blocking } else if((retval = recv(sim.socket_fd, &c, 1, 0)) < 1) { @@ -124,45 +147,62 @@ uint8_t sim_socket_in() struct timeval tv = { .tv_sec = 0, .tv_usec = 0 - }; + }; - fd_set cfds = rfds; + // Use select() ONLY to detect a new incoming connection on the listening + // socket. Data on the accepted connection is read with a single + // non-blocking read() below (the accepted socket is put in O_NONBLOCK once + // right after accept), instead of select()ing on it every poll. + fd_set cfds; + FD_ZERO(&cfds); + FD_SET(socket_fd, &cfds); - if((retval = select((socket_fd > sim.socket_fd ? socket_fd : sim.socket_fd) + 1, &cfds, NULL, NULL, &tv)) > 0) { + int accepted = 0; - if(FD_ISSET(socket_fd, &cfds)) { + if((retval = select(socket_fd + 1, &cfds, NULL, NULL, &tv)) > 0 && FD_ISSET(socket_fd, &cfds)) { - socklen_t addrlen = sizeof(struct sockaddr_in); - struct sockaddr_in client_addr; + socklen_t addrlen = sizeof(struct sockaddr_in); + struct sockaddr_in client_addr; - sim.socket_fd = accept(socket_fd, (struct sockaddr *) &client_addr, &addrlen); - if (sim.socket_fd < 0) { - printf("Fatal: Error on socket accept.\n"); - exit(-5); - } + sim.socket_fd = accept(socket_fd, (struct sockaddr *) &client_addr, &addrlen); + if (sim.socket_fd < 0) { + printf("Fatal: Error on socket accept.\n"); + exit(-5); + } - FD_SET(sim.socket_fd, &rfds); + FD_SET(sim.socket_fd, &rfds); - struct timeval t = { - .tv_sec = 0, - .tv_usec = 0 - }; + // Put the accepted socket into non-blocking mode once so subsequent + // reads return immediately when there is no data. + int flags = fcntl(sim.socket_fd, F_GETFL, 0); + if (flags != -1) + fcntl(sim.socket_fd, F_SETFL, flags | O_NONBLOCK); - setsockopt(sim.socket_fd, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(t)); - } + // Disable Nagle: status polls and responses are tiny; Nagle + delayed + // ACK can hold the tail of a response line back by up to ~200 ms. + int nodelay = 1; + setsockopt(sim.socket_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); - if(FD_ISSET(sim.socket_fd, &cfds)) { + accepted = 1; + } - retval = read(sim.socket_fd, &c, 1); + // Read incoming data from the accepted connection. Skip on the poll where + // we just accepted (a brand-new connection has no data yet), matching the + // old select()-gated behavior. + if(!accepted && sim.socket_fd > 0) { - if(retval == 0) { - close(sim.socket_fd); - FD_CLR(sim.socket_fd, &rfds); - sim.socket_fd = 0; - } + retval = read(sim.socket_fd, &c, 1); + + if(retval == 0) { + close(sim.socket_fd); + FD_CLR(sim.socket_fd, &rfds); + sim.socket_fd = 0; + c = 0; + } else if(retval < 0) { + c = 0; // EAGAIN/EWOULDBLOCK: no data available + } // if(c && c != '?' && c >= ' ') // sim_serial_out(c == '\r' ? '\n' : c); - } } return c; @@ -199,6 +239,11 @@ int main(int argc, char *argv[]) argv++; argc--; if (argv[0][0] == '-') { + if (!strcmp(argv[0], "--version")) { + print_version(); + return EXIT_SUCCESS; + } + switch(argv[0][1]){ case 'c': //set Comment char @@ -259,6 +304,10 @@ int main(int argc, char *argv[]) args.port = atoi(*argv); break; + case 'v': + print_version(); + return EXIT_SUCCESS; + case 'h': print_usage(NULL); return EXIT_SUCCESS; diff --git a/src/mcu.c b/src/mcu.c index 836509f..0d2bc2f 100644 --- a/src/mcu.c +++ b/src/mcu.c @@ -28,9 +28,24 @@ #include "grbl/hal.h" static volatile bool irq_enable = false; -static bool booted = false; +static volatile bool booted = false; static interrupt_handler isr[IRQ_N_HANDLERS]; +// Per-idle-tick fast-path gates (see mcu_master_clock): +// - timer_enabled_mask: bit i set while timer[i] is enabled. Lets an idle tick +// skip the whole timer loop with a single word test. Maintained via +// mcu_timer_enable(); on enable the mask bit is set BEFORE timer[].enable and +// on disable it is cleared AFTER, so the mask is always a superset of the +// enabled timers as observed from the sim thread (x86-TSO store ordering) - +// the inner if(timer[i].enable) check is preserved, so a stale extra bit only +// costs a harmless loop pass and can never cause a missed or extra IRQ. +// - gpio_irq_pending: set whenever mcu_gpio_in raises an irq_state bit; lets an +// idle tick skip the 10-port GPIO scan. Cleared before servicing. Each GPIO +// handler clears its own trigger (irq_state), so a change fires exactly once, +// identical to the previous scan-every-tick behaviour. +static volatile uint32_t timer_enabled_mask = 0; +static volatile bool gpio_irq_pending = false; + mcu_uart_t uart; mcu_timer_t timer[MCU_N_TIMERS]; mcu_timer_t systick_timer; @@ -55,10 +70,28 @@ void mcu_reset (void) memset(&systick_timer, 0, sizeof(mcu_timer_t)); memset(&gpio, 0, sizeof(gpio_port_t) * MCU_N_GPIO); + // all timers/gpio just zeroed - reset the fast-path caches to match + timer_enabled_mask = 0; + gpio_irq_pending = false; + irq_enable = true; booted = true; } +// Enable/disable one of the timer[] timers, keeping timer_enabled_mask in sync. +// Ordering matters for the sim-thread fast path: set the mask bit before enabling +// and clear it after disabling so the mask is never missing a bit for a live timer. +void mcu_timer_enable (uint_fast8_t timer_id, bool on) +{ + if(on) { + timer_enabled_mask |= (1u << timer_id); + timer[timer_id].enable = true; + } else { + timer[timer_id].enable = false; + timer_enabled_mask &= ~(1u << timer_id); + } +} + void mcu_register_irq_handler (interrupt_handler handler, irq_num_t irq_num) { isr[irq_num] = handler == NULL ? default_handler : handler; @@ -81,45 +114,45 @@ void mcu_master_clock (void) if(!booted) return; - for(i = 0; i < MCU_N_TIMERS; i++) { + // Timer loop - skipped entirely when no timer[] timer is enabled. The inner + // per-timer logic is unchanged; only the switch() dispatch is replaced with a + // direct isr[Timer0_IRQ + i]() call (Timer0/1/2_IRQ are consecutive enums). + if(timer_enabled_mask) { + for(i = 0; i < MCU_N_TIMERS; i++) { - if(timer[i].enable) { + if(timer[i].enable) { - if(timer[i].prescaler) { - if(timer[i].prescale == 0) - timer[i].prescale = timer[i].prescaler; - else { - if(--timer[i].prescale != 0) - continue; - else + if(timer[i].prescaler) { + if(timer[i].prescale == 0) timer[i].prescale = timer[i].prescaler; + else { + if(--timer[i].prescale != 0) + continue; + else + timer[i].prescale = timer[i].prescaler; + } } - } - if(timer[i].value == 0) - timer[i].value = timer[i].load; - else if(--timer[i].value == 0) { - if(timer[i].irq_enable && irq_enable) { - switch(i) - { - case 0: - isr[Timer0_IRQ](); - break; - case 1: - isr[Timer1_IRQ](); - break; - case 2: - isr[Timer2_IRQ](); - break; } - } - timer[i].value = timer[i].load; + if(timer[i].value == 0) + timer[i].value = timer[i].load; + else if(--timer[i].value == 0) { + if(timer[i].irq_enable && irq_enable) + isr[Timer0_IRQ + i](); + timer[i].value = timer[i].load; + } } } } - for(i = 0; i < MCU_N_GPIO; i++) { - if(gpio[i].irq_state.value & gpio[i].irq_mask.value) - isr[GPIO0_IRQ + i](); + // GPIO pin-change IRQs - skipped unless mcu_gpio_in flagged a pending change. + // Cleared before servicing so a change raised while servicing is kept for the + // next tick. Handlers clear their own irq_state, so each change fires once. + if(gpio_irq_pending) { + gpio_irq_pending = false; + for(i = 0; i < MCU_N_GPIO; i++) { + if(gpio[i].irq_state.value & gpio[i].irq_mask.value) + isr[GPIO0_IRQ + i](); + } } if(systick_timer.enable) { @@ -133,6 +166,71 @@ void mcu_master_clock (void) } } +// Returns how many of the next `max` ticks are guaranteed free of timer/GPIO +// activity: no IRQ, no reload, no prescaler wrap. Skipping that many ticks with +// mcu_skip_ticks() leaves every peripheral in the exact state `n` calls to +// mcu_master_clock() would have produced, so the following tick-by-tick step +// fires ISRs at exactly the same masterclock value as an unskipped simulation. +uint32_t mcu_ticks_to_event (uint32_t max) +{ + uint_fast8_t i; + uint32_t t, skip = max; + + if(!booted || max == 0) + return max; + + for(i = 0; i < MCU_N_GPIO; i++) { + if(gpio[i].irq_state.value & gpio[i].irq_mask.value) + return 0; // pending pin change interrupt fires every tick until serviced + } + + for(i = 0; i < MCU_N_TIMERS; i++) { + if(timer[i].enable) { + if(timer[i].prescaler) + t = timer[i].prescale ? timer[i].prescale - 1 : 0; // counter logic runs when the prescaler wraps + else if(timer[i].value) + t = timer[i].value - 1; // IRQ/reload on the tick the counter reaches zero + else + t = timer[i].load ? 0 : max; // reload due next tick; a 0/0 timer never does anything + if(t < skip) + skip = t; + } + } + + if(systick_timer.enable) { + t = systick_timer.value ? systick_timer.value - 1 : (systick_timer.load ? 0 : max); + if(t < skip) + skip = t; + } + + return skip; +} + +// Advance all counters by `ticks` known to be event-free (see mcu_ticks_to_event). +// The > comparisons guard against a counter being reprogrammed from the grbl +// thread between the two calls; leaving it untouched then equals the write +// landing just after the jump. +void mcu_skip_ticks (uint32_t ticks) +{ + uint_fast8_t i; + + if(!booted || ticks == 0) + return; + + for(i = 0; i < MCU_N_TIMERS; i++) { + if(timer[i].enable) { + if(timer[i].prescaler) { + if(timer[i].prescale > ticks) + timer[i].prescale -= ticks; + } else if(timer[i].value > ticks) + timer[i].value -= ticks; + } + } + + if(systick_timer.enable && systick_timer.value > ticks) + systick_timer.value -= ticks; +} + void mcu_gpio_set (gpio_port_t *port, uint16_t pins, uint16_t mask) { port->state.value = (port->state.value & ~mask) | (pins & mask); @@ -158,11 +256,15 @@ void mcu_gpio_in (gpio_port_t *port, uint16_t pins, uint16_t mask) do { if(changed & bitflag) { if(port->state.value & bitflag) { - if(port->falling.value & bitflag) + if(port->falling.value & bitflag) { port->irq_state.value |= bitflag; + gpio_irq_pending = true; + } } else { - if(port->rising.value & bitflag) + if(port->rising.value & bitflag) { port->irq_state.value |= bitflag; + gpio_irq_pending = true; + } } changed &= ~bitflag; } @@ -186,9 +288,16 @@ void simulate_serial (void) if((uart.tx_irq = uart.tx_irq_enable)) isr[UART_IRQ](); - if(uart.rx_irq_enable && !uart.rx_irq && hal.stream.get_rx_buffer_free() > 100) { + // Deliver at most one byte per grbl main-loop pass (see sim_process_realtime): + // a pulse of 0 means the grbl thread is still booting and would flush the input, + // an unchanged pulse means it has not run since the previous byte - skip this + // byte slot and try again at the next one, no input is lost. + static uint32_t pulse_seen = 0; + + if(sim.grbl_pulse != pulse_seen && uart.rx_irq_enable && !uart.rx_irq && hal.stream.get_rx_buffer_free() > 100) { uint8_t char_in = sim.getchar(); if (char_in) { + pulse_seen = sim.grbl_pulse; uart.rx_data = char_in; uart.rx_irq = 1; isr[UART_IRQ](); diff --git a/src/mcu.h b/src/mcu.h index 5af0a10..922ad1e 100644 --- a/src/mcu.h +++ b/src/mcu.h @@ -30,6 +30,11 @@ #define MCU_N_TIMERS 3 #define MCU_N_GPIO 10 +// Upper bound for clock ticks skipped in one jump by the event-driven main loop. +// Bounds how long a change made from the grbl thread (e.g. stepper wake-up) can go +// unnoticed to 1 ms of simulated time, mirroring the always-on systick period. +#define MCU_MAX_SKIP (F_CPU / 1000) + typedef enum { Systick_IRQ = 0, UART_IRQ, @@ -49,27 +54,29 @@ typedef enum { IRQ_N_HANDLERS } irq_num_t; +// NOTE: fields are volatile because peripheral "registers" are accessed both from +// the simulator thread (mcu_master_clock/ISRs) and the grbl thread (HAL calls). typedef struct { volatile bool enable; - bool irq_enable; - uint32_t value; - uint32_t load; - uint32_t prescale; - uint32_t prescaler; - uint32_t compare; + volatile bool irq_enable; + volatile uint32_t value; + volatile uint32_t load; + volatile uint32_t prescale; + volatile uint32_t prescaler; + volatile uint32_t compare; } mcu_timer_t; typedef struct { - bool rx_irq; - bool tx_irq; - bool tx_flag; - bool rx_irq_enable; - bool tx_irq_enable; - uint8_t rx_data; - uint8_t tx_data; - uint32_t cdiv; + volatile bool rx_irq; + volatile bool tx_irq; + volatile bool tx_flag; + volatile bool rx_irq_enable; + volatile bool tx_irq_enable; + volatile uint8_t rx_data; + volatile uint8_t tx_data; + volatile uint32_t cdiv; } mcu_uart_t; typedef union { @@ -97,14 +104,14 @@ typedef union { typedef struct { - gpio_pins_t dir; - gpio_pins_t state; - gpio_pins_t irq_mask; - gpio_pins_t irq_state; - gpio_pins_t rising; - gpio_pins_t falling; - gpio_pins_t pullup; - gpio_pins_t pulldown; + volatile gpio_pins_t dir; + volatile gpio_pins_t state; + volatile gpio_pins_t irq_mask; + volatile gpio_pins_t irq_state; + volatile gpio_pins_t rising; + volatile gpio_pins_t falling; + volatile gpio_pins_t pullup; + volatile gpio_pins_t pulldown; } gpio_port_t; extern mcu_uart_t uart; @@ -118,6 +125,9 @@ void mcu_reset (void); void mcu_enable_interrupts (void); void mcu_disable_interrupts (void); void mcu_master_clock (void); +uint32_t mcu_ticks_to_event (uint32_t max); +void mcu_skip_ticks (uint32_t ticks); +void mcu_timer_enable (uint_fast8_t timer_id, bool on); void mcu_register_irq_handler (interrupt_handler handler, irq_num_t irq_num); void mcu_gpio_set (gpio_port_t *port, uint16_t pins, uint16_t mask); uint8_t mcu_gpio_get (gpio_port_t *port, uint16_t mask); diff --git a/src/platform_linux.c b/src/platform_linux.c index f5a6cbe..e45d441 100644 --- a/src/platform_linux.c +++ b/src/platform_linux.c @@ -23,21 +23,69 @@ #include #include #include +#include #include #include #include "platform.h" #define MS_PER_SEC 1000000 +#define SIM_ECHO_TERMINAL 0 //use this to make grbl_sim act like a serial terminal with local echo on. + +// Saved STDIN state so it can be restored on exit. The terminal is switched to +// raw mode (and STDIN made non-blocking) once at startup instead of on every +// poll, so platform_poll_stdin() can be a single non-blocking read(). +static struct termios orig_termios; +static int termios_saved = 0; +static int orig_stdin_flags = 0; +static int stdin_flags_saved = 0; + +//restore STDIN to the state it had before platform_init() +static void platform_restore_terminal(void) +{ + if (termios_saved) { + tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios); + termios_saved = 0; + } + if (stdin_flags_saved) { + fcntl(STDIN_FILENO, F_SETFL, orig_stdin_flags); + stdin_flags_saved = 0; + } +} //any platform-specific setup that must be done before sim starts here void platform_init() { + // Make STDIN non-blocking once so polling is a single read() with no + // per-poll select()/tcsetattr() dance. Save the original flags so they can + // be restored on exit (important when STDIN is a shared tty). + int flags = fcntl(STDIN_FILENO, F_GETFL, 0); + if (flags != -1) { + orig_stdin_flags = flags; + stdin_flags_saved = 1; + fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); + } + + // If STDIN is a terminal, switch it to raw (non-canonical, no echo) mode + // once. For a pipe/redirected file tcgetattr() fails and we leave it alone, + // matching the old per-poll code (which no-op'd on non-ttys). + if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &orig_termios) == 0) { + struct termios raw = orig_termios; + raw.c_lflag &= ~(ICANON); + if (!SIM_ECHO_TERMINAL) + raw.c_lflag &= ~(ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &raw); + termios_saved = 1; + } + + // Restore the terminal even on abnormal exit. + atexit(platform_restore_terminal); } //cleanup int here; void platform_terminate() { + platform_restore_terminal(); } //returns a free-running 32 bit nanosecond counter which rolls over @@ -68,44 +116,6 @@ void platform_sleep(long microsec) nanosleep(&ts, NULL); } -#define SIM_ECHO_TERMINAL 0 //use this to make grbl_sim act like a serial terminal with local echo on. - -//set terminal to allow kbhit detection -void enable_kbhit(int dir) -{ - static struct termios oldt, newt; - - if ( dir == 1 ) { - tcgetattr(STDIN_FILENO, &oldt); - newt = oldt; - newt.c_lflag &= ~(ICANON); - if (!SIM_ECHO_TERMINAL) - newt.c_lflag &= ~(ECHO); - tcsetattr(STDIN_FILENO, TCSANOW, &newt); - } - else - tcsetattr(STDIN_FILENO, TCSANOW, &oldt); -} - -//detect key pressed -int kbhit (void) -{ - struct timeval tv = {0}; - fd_set rdfs = {{0}}; - int retval; - - /* tv.tv_sec = 0; */ - /* tv.tv_usec = 0; */ - - /* FD_ZERO(&rdfs); */ - FD_SET(STDIN_FILENO, &rdfs); - - select(STDIN_FILENO + 1, &rdfs, NULL, NULL, &tv); - retval = FD_ISSET(STDIN_FILENO, &rdfs); - - return retval; -} - plat_thread_t* platform_start_thread(plat_threadfunc_t threadfunc) { plat_thread_t* th = malloc(sizeof(plat_thread_t)); @@ -135,11 +145,14 @@ uint8_t platform_poll_stdin() { uint8_t char_in = 0; - enable_kbhit(1); - if (kbhit()) - char_in = getchar(); + // STDIN was put in non-blocking mode once in platform_init(), so a single + // read() replaces the old per-poll tcgetattr/tcsetattr/select/getchar. + ssize_t n = read(STDIN_FILENO, &char_in, 1); - enable_kbhit(0); + if (n == 1) + return char_in; // byte available + if (n == 0) + return 0xFF; // EOF: matches old getchar()==EOF cast to uint8_t - return char_in; + return 0; // no data (EAGAIN/EWOULDBLOCK) or error -> nothing } diff --git a/src/platform_windows.c b/src/platform_windows.c index c9c0cca..fc13feb 100644 --- a/src/platform_windows.c +++ b/src/platform_windows.c @@ -24,6 +24,7 @@ #include #include +#include #include "platform.h" #define NS_PER_SEC 1000000000 @@ -31,6 +32,10 @@ double ns_per_perfcount; +// Saved console input mode so QuickEdit can be restored on exit. +static DWORD orig_console_mode; +static int console_mode_saved = 0; + //any platform-specific setup that must be done before sim starts here void platform_init() { @@ -38,11 +43,33 @@ void platform_init() QueryPerformanceFrequency((LARGE_INTEGER*)&counts_per_sec); ns_per_perfcount = (float)NS_PER_SEC / counts_per_sec; + + // Default Sleep() granularity is ~15.6 ms; the sim_loop control frame + // sleeps every ~20 ms, so without this the frame pacing (and with it + // serial response latency) jitters by a whole scheduler quantum. + timeBeginPeriod(1); + + // Disable QuickEdit mode: with it enabled, clicking in the console window + // starts a text selection that blocks ALL console writes until dismissed, + // freezing the whole simulator (both threads print to this console). + HANDLE hstdin = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode; + if (hstdin != INVALID_HANDLE_VALUE && GetConsoleMode(hstdin, &mode)) { + orig_console_mode = mode; + console_mode_saved = 1; + SetConsoleMode(hstdin, (mode | ENABLE_EXTENDED_FLAGS) & ~ENABLE_QUICK_EDIT_MODE); + } } //cleanup int here; void platform_terminate() { + timeEndPeriod(1); + + if (console_mode_saved) { + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), orig_console_mode); + console_mode_saved = 0; + } } //returns a free-running 32 bit nanosecond counter which rolls over diff --git a/src/serial.c b/src/serial.c index 49c785d..fa4cee7 100644 --- a/src/serial.c +++ b/src/serial.c @@ -23,6 +23,7 @@ */ #include +#include #include "mcu.h" #include "simulator.h" @@ -47,6 +48,8 @@ static int32_t serialGetC (void) if(bptr == rxbuffer.head) return -1; // no data available else EOF + atomic_thread_fence(memory_order_acquire); // Data written before head was published (UART ISR runs on the simulator thread) + data = (int32_t)rxbuffer.data[bptr++]; // Get next character, increment tmp pointer rxbuffer.tail = bptr & (RX_BUFFER_SIZE - 1); // and update pointer @@ -93,6 +96,7 @@ static bool serialPutC (const uint8_t c) } txbuffer.data[txbuffer.head] = c; // Add data to buffer + atomic_thread_fence(memory_order_release); // making sure it is visible before the head pointer (UART ISR reads it on the simulator thread) txbuffer.head = next_head; // and update head pointer uart.tx_irq_enable = 1; // Enable TX interrupts @@ -165,6 +169,8 @@ static void uart_interrupt_handler (void) if(txbuffer.head != bptr) { + atomic_thread_fence(memory_order_acquire); // Data written before head was published (serialPutC runs on the grbl thread) + uart.tx_data = txbuffer.data[bptr++]; // Put character in TXT register bptr &= (TX_BUFFER_SIZE - 1); // and update tmp tail pointer uart.tx_irq = 0; @@ -191,6 +197,7 @@ static void uart_interrupt_handler (void) sim.exit = exit_REQ; if(!enqueue_realtime_command((uint8_t)data)) { rxbuffer.data[rxbuffer.head] = (uint8_t)data; // Add data to buffer + atomic_thread_fence(memory_order_release); // making sure it is visible before the head pointer (serialGetC reads it on the grbl thread) rxbuffer.head = bptr; // and update pointer } } diff --git a/src/simulator.c b/src/simulator.c index 123a756..13874d8 100644 --- a/src/simulator.c +++ b/src/simulator.c @@ -64,7 +64,11 @@ void simulate_hardware (bool do_serial) { //do one tick sim.masterclock++; - sim.sim_time = (float)sim.masterclock / (float)F_CPU; + // sim_time is derived lazily from masterclock where it is consumed (see + // print_steps in grbl_interface.c). Computing it here every tick was a + // needless u64->float convert + divide at 16 M ticks/s. The lazy expression + // uses the exact same (float)masterclock/(float)F_CPU rounding, so -r + // step-report timestamps stay byte-identical. mcu_master_clock(); @@ -79,25 +83,65 @@ void simulate_hardware (bool do_serial) // can ignore pinout int vect - hw start/hold not supported } +// Grow/shrink the per-frame tick budget without integer overflow: event skipping +// lets a frame cover a huge simulated span, so the running product must be +// clamped in the double domain before converting back. +static uint64_t scale_ticks_per_frame (uint64_t ticks, double factor, uint64_t max) +{ + double scaled = (double)ticks * factor; + + return scaled >= (double)max ? max : (scaled < 1.0 ? 1 : (uint64_t)scaled); +} + // Runs the hardware simulator at the desired rate until sim.exit is set void sim_loop (void) { - // a sensible control frame length is yet to be found. - // the currrent 100ms are a blind guess, assuming some small multiple - // of the OS's thread scheduler's time splice makes sense. - // smaller means less jitter in the resulting sim time. - // too small makes it worse again because of the limited resultion of plattform_sleep(..) - // and a simple P controller may be not enough any longer - const uint32_t control_frame_ns = 100 * 1000 * 1000; // in real time + // Control frame length trades throughput overhead against interactive + // latency: serial input is only polled during a frame's simulation burst, + // so a byte arriving while we sleep waits for the next frame - the frame + // length is the worst-case added response latency at -t 1. With the + // event-skipping tick loop a frame's bookkeeping is cheap, so 20 ms (was + // 100 ms) costs nothing measurable while cutting mean '?'-to-report + // latency from ~45 ms to ~10 ms. Much smaller would run into + // platform_sleep() resolution (1 ms on Windows, and only with + // timeBeginPeriod(1) - see platform_windows.c). + const uint32_t control_frame_ns = 20 * 1000 * 1000; // in real time + + // prevent runaway growth of (and undefined conversions to) the tick budget + // now that event skipping allows huge simulated spans per control frame + const uint64_t max_ticks_per_frame = (uint64_t)1 << 40; int32_t sleep_time_us = 0; // sleep time per control frame - uint32_t ticks_per_frame = F_CPU / 100; // start simulating a few ticks before entering the control loop + uint64_t ticks_per_frame = F_CPU / 100; // start simulating a few ticks before entering the control loop uint64_t target_ticks = ticks_per_frame; uint32_t ns_prev = platform_ns(); uint64_t next_byte_tick = F_CPU; //wait 1 sec (sim time) before reading IO. while (sim.exit != exit_OK ) { //don't quit until idle while (sim.masterclock < target_ticks) { + + // Jump over ticks where provably nothing happens: no timer expiry or + // reload, no pending GPIO interrupt, no serial byte due. ISRs still + // fire at exactly the tick they would in tick-by-tick simulation. + // Step reporting (-r) samples state every tick, so it forces the + // tick-by-tick path to keep its output identical. + if (args.step_time == 0.0 && sim.masterclock < next_byte_tick) { + uint64_t limit = next_byte_tick - sim.masterclock; + uint64_t remaining = target_ticks - sim.masterclock; + if (remaining < limit) + limit = remaining; + if (limit > MCU_MAX_SKIP) + limit = MCU_MAX_SKIP; + uint32_t skip = mcu_ticks_to_event((uint32_t)limit); + if (skip) { + mcu_skip_ticks(skip); + sim.masterclock += skip; + // sim_time is derived lazily from masterclock in print_steps. + if (sim.masterclock == target_ticks) + break; // frame budget used up by the jump + } + } + // only read serial port as fast as the baud rate allows bool read_serial = (sim.masterclock >= next_byte_tick); @@ -123,6 +167,8 @@ void sim_loop (void) // calculate current speedup ... uint32_t ns_now = platform_ns(); uint32_t ns_elapsed = (ns_now - ns_prev); + if (ns_elapsed == 0) + ns_elapsed = 1; ns_prev = ns_now; uint32_t rt_ticks_per_frame = F_CPU / 1e9 * ns_elapsed; sim.speedup = (double)ticks_per_frame / rt_ticks_per_frame; @@ -141,20 +187,20 @@ void sim_loop (void) // we've been too fast (which is good), so let's wait a bit... platform_sleep(sleep_time_us); // ... and schedule as many ticks as the desired speedup requires for the next frame - ticks_per_frame = F_CPU / 1e9 * control_frame_ns * args.speedup; + ticks_per_frame = scale_ticks_per_frame(1, F_CPU / 1e9 * control_frame_ns * args.speedup, max_ticks_per_frame); } else { // the host has been too slow to fullfill the desired realtime speedup. // so we do not wait and schedule only as many ticks for the next frame as we're // able to execute in the control fame's time to prevent it from getting longer and longer. sleep_time_us = 0; - ticks_per_frame *= (double)control_frame_ns / ns_elapsed; + ticks_per_frame = scale_ticks_per_frame(ticks_per_frame, (double)control_frame_ns / ns_elapsed, max_ticks_per_frame); } } else { // aim to maximize the simulated ticks thoughput by filling the control frame // completely, based on our current knowledge of the time it takes to simulate ticks. - ticks_per_frame *= (double)control_frame_ns / ns_elapsed; + ticks_per_frame = scale_ticks_per_frame(ticks_per_frame, (double)control_frame_ns / ns_elapsed, max_ticks_per_frame); } target_ticks += ticks_per_frame; @@ -189,8 +235,12 @@ void sim_socket_out (uint8_t data) static bool continuation = 0; buf[len++] = data; - // print when we get to newline or run out of buffer - if(data == '\n' || data == '\r' || len >= 127) { + // Send only on '\n' (grblHAL lines end "\r\n") or when the buffer is full, + // so a whole line goes out as ONE send(). Flushing on '\r' too used to + // split every line into body+"\r" and a lone "\n" segment; with Nagle the + // trailing byte could stall behind the peer's delayed ACK (~200 ms on + // Windows), making senders' status updates lag by a poll period. + if(data == '\n' || len >= 127) { // if (args.comment_char && !continuation) // fprintf(args.serial_out_file, "%c ", args.comment_char); #ifdef WIN32 diff --git a/src/simulator.h b/src/simulator.h index 9f29614..4a4d27a 100644 --- a/src/simulator.h +++ b/src/simulator.h @@ -33,8 +33,11 @@ typedef void (*sim_hook_fp)(void); // Signature of functions to be inserted in s //simulation globals typedef struct sim_vars { uint64_t masterclock; - double sim_time; // current time of the simulation, in seconds since start. + // NOTE: sim time (seconds since start) is derived lazily from masterclock at + // the point(s) it is consumed as (float)masterclock/(float)F_CPU; it is no + // longer stored/updated per tick. See print_steps() in grbl_interface.c. uint8_t started; // don't start timers until first char recieved. + volatile uint32_t grbl_pulse; // grbl thread main-loop heartbeat; 0 until the boot sequence (which flushes the input stream) is done enum {exit_NO, exit_REQ, exit_OK} exit; float speedup; // current factor how much faster/slower sim time is compared to real time int32_t baud_ticks;