From e994b2439873008e11e43b04f6282448f5719186 Mon Sep 17 00:00:00 2001 From: Funkenjaeger Date: Sun, 12 Apr 2026 11:18:47 -0400 Subject: [PATCH 1/4] Add firmware/hardware emulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a native Linux emulator for hardware-free testing with the rotary-controller-python UI. It compiles the real firmware sources (`Ramps.c`, `Modbus.c`, `Scales.c`, `UARTCallback.c`) against a HAL/FreeRTOS shim layer and simulates lathe physics β€” spindle with inertia, leadscrew, carriage with half-nut engagement, and cross-slide. The emulator exposes Modbus RTU via PTY pair and TCP socket so the unmodified rotary-controller-python GUI can connect as if talking to real hardware. A two-pane ANSI terminal dashboard with sparklines provides live visualization, with keyboard controls for spindle RPM, manual axis movement, half-nut engagement, and more. All parameters are configurable via TOML file. --- .gitignore | 192 ++-------- README.md | 18 + emulator/CMakeLists.txt | 71 ++++ emulator/config/lathe.toml | 58 +++ emulator/shim/FreeRTOS.h | 34 ++ emulator/shim/cmsis_os.h | 11 + emulator/shim/cmsis_os2.h | 100 +++++ emulator/shim/core_cm4.h | 64 ++++ emulator/shim/freertos_shim.cpp | 419 +++++++++++++++++++++ emulator/shim/hal_shim.c | 298 +++++++++++++++ emulator/shim/main.h | 28 ++ emulator/shim/queue.h | 25 ++ emulator/shim/semphr.h | 24 ++ emulator/shim/stm32f4xx.h | 120 ++++++ emulator/shim/stm32f4xx_hal.h | 35 ++ emulator/shim/stm32f4xx_hal_conf.h | 24 ++ emulator/shim/stm32f4xx_hal_cortex.h | 22 ++ emulator/shim/stm32f4xx_hal_def.h | 43 +++ emulator/shim/stm32f4xx_hal_dma.h | 29 ++ emulator/shim/stm32f4xx_hal_gpio.h | 76 ++++ emulator/shim/stm32f4xx_hal_rcc.h | 81 +++++ emulator/shim/stm32f4xx_hal_tim.h | 74 ++++ emulator/shim/stm32f4xx_hal_uart.h | 70 ++++ emulator/shim/task.h | 40 ++ emulator/shim/timers.h | 35 ++ emulator/src/config.cpp | 180 +++++++++ emulator/src/config.h | 72 ++++ emulator/src/dashboard.cpp | 522 +++++++++++++++++++++++++++ emulator/src/dashboard.h | 81 +++++ emulator/src/emulator_state.h | 83 +++++ emulator/src/main.cpp | 189 ++++++++++ emulator/src/physics.cpp | 355 ++++++++++++++++++ emulator/src/physics.h | 124 +++++++ emulator/src/transport.cpp | 217 +++++++++++ emulator/src/transport.h | 64 ++++ 35 files changed, 3712 insertions(+), 166 deletions(-) create mode 100644 emulator/CMakeLists.txt create mode 100644 emulator/config/lathe.toml create mode 100644 emulator/shim/FreeRTOS.h create mode 100644 emulator/shim/cmsis_os.h create mode 100644 emulator/shim/cmsis_os2.h create mode 100644 emulator/shim/core_cm4.h create mode 100644 emulator/shim/freertos_shim.cpp create mode 100644 emulator/shim/hal_shim.c create mode 100644 emulator/shim/main.h create mode 100644 emulator/shim/queue.h create mode 100644 emulator/shim/semphr.h create mode 100644 emulator/shim/stm32f4xx.h create mode 100644 emulator/shim/stm32f4xx_hal.h create mode 100644 emulator/shim/stm32f4xx_hal_conf.h create mode 100644 emulator/shim/stm32f4xx_hal_cortex.h create mode 100644 emulator/shim/stm32f4xx_hal_def.h create mode 100644 emulator/shim/stm32f4xx_hal_dma.h create mode 100644 emulator/shim/stm32f4xx_hal_gpio.h create mode 100644 emulator/shim/stm32f4xx_hal_rcc.h create mode 100644 emulator/shim/stm32f4xx_hal_tim.h create mode 100644 emulator/shim/stm32f4xx_hal_uart.h create mode 100644 emulator/shim/task.h create mode 100644 emulator/shim/timers.h create mode 100644 emulator/src/config.cpp create mode 100644 emulator/src/config.h create mode 100644 emulator/src/dashboard.cpp create mode 100644 emulator/src/dashboard.h create mode 100644 emulator/src/emulator_state.h create mode 100644 emulator/src/main.cpp create mode 100644 emulator/src/physics.cpp create mode 100644 emulator/src/physics.h create mode 100644 emulator/src/transport.cpp create mode 100644 emulator/src/transport.h diff --git a/.gitignore b/.gitignore index 102f64e..cbc9882 100644 --- a/.gitignore +++ b/.gitignore @@ -86,171 +86,31 @@ Module.symvers Mkfile.old dkms.conf -.idea -/cmake-build-debug/ +# IDE +.idea/ /.settings/ +*.launch + +# CMake build artifacts (firmware and emulator) +/build/ +/cmake-build-*/ +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile +/.cmake/ + +# Firmware build outputs /Debug/ -/rotary-controller-f4.launch -/Release/Core/Src/freertos.cyclo -/Release/Core/Src/gpio.cyclo -/Release/Core/Src/main.cyclo -/Release/Core/Src/Modbus.cyclo -/Release/Core/Src/Ramps.cyclo -/Release/Core/Src/Scales.cyclo -/Release/Core/Src/stm32f4xx_hal_msp.cyclo -/Release/Core/Src/stm32f4xx_hal_timebase_tim.cyclo -/Release/Core/Src/stm32f4xx_it.cyclo -/Release/Core/Src/subdir.mk -/Release/Core/Src/syscalls.cyclo -/Release/Core/Src/sysmem.cyclo -/Release/Core/Src/system_stm32f4xx.cyclo -/Release/Core/Src/tim.cyclo -/Release/Core/Src/UARTCallback.cyclo -/Release/Core/Src/usart.cyclo -/Release/Core/Startup/subdir.mk -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.cyclo -/Release/Drivers/STM32F4xx_HAL_Driver/Src/subdir.mk -/Release/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/subdir.mk -/Release/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F/subdir.mk -/Release/Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/heap_4.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/portable/MemMang/subdir.mk -/Release/Middlewares/Third_Party/FreeRTOS/Source/croutine.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/event_groups.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/list.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/queue.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/stream_buffer.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/subdir.mk -/Release/Middlewares/Third_Party/FreeRTOS/Source/tasks.cyclo -/Release/Middlewares/Third_Party/FreeRTOS/Source/timers.cyclo -/Release/makefile -/Release/objects.list -/Release/objects.mk -/Release/rotary-controller-f4.list -/Release/sources.mk -/rotary-controller-f4.bin -/Makefile -/CMakeCache.txt -/cmake_install.cmake -/CMakeFiles/4.0.2/CompilerIdC/CMakeCCompilerId.c -/CMakeFiles/4.0.2/CompilerIdCXX/CMakeCXXCompilerId.cpp -/CMakeFiles/4.0.2/CMakeASMCompiler.cmake -/CMakeFiles/4.0.2/CMakeCCompiler.cmake -/CMakeFiles/4.0.2/CMakeCXXCompiler.cmake -/CMakeFiles/4.0.2/CMakeDetermineCompilerABI_C.bin -/CMakeFiles/4.0.2/CMakeDetermineCompilerABI_CXX.bin -/CMakeFiles/4.0.2/CMakeSystem.cmake -/CMakeFiles/4.0.3-dirty/CompilerIdC/CMakeCCompilerId.c -/CMakeFiles/4.0.3-dirty/CompilerIdCXX/CMakeCXXCompilerId.cpp -/CMakeFiles/4.0.3-dirty/CMakeASMCompiler.cmake -/CMakeFiles/4.0.3-dirty/CMakeCCompiler.cmake -/CMakeFiles/4.0.3-dirty/CMakeCXXCompiler.cmake -/CMakeFiles/4.0.3-dirty/CMakeDetermineCompilerABI_C.bin -/CMakeFiles/4.0.3-dirty/CMakeDetermineCompilerABI_CXX.bin -/CMakeFiles/4.0.3-dirty/CMakeSystem.cmake -/CMakeFiles/4.1.2/CompilerIdC/CMakeCCompilerId.c -/CMakeFiles/4.1.2/CompilerIdCXX/CMakeCXXCompilerId.cpp -/CMakeFiles/4.1.2/CMakeASMCompiler.cmake -/CMakeFiles/4.1.2/CMakeCCompiler.cmake -/CMakeFiles/4.1.2/CMakeCXXCompiler.cmake -/CMakeFiles/4.1.2/CMakeDetermineCompilerABI_C.bin -/CMakeFiles/4.1.2/CMakeDetermineCompilerABI_CXX.bin -/CMakeFiles/4.1.2/CMakeSystem.cmake -/CMakeFiles/4.2.1/CompilerIdC/CMakeCCompilerId.c -/CMakeFiles/4.2.1/CompilerIdCXX/CMakeCXXCompilerId.cpp -/CMakeFiles/4.2.1/CMakeASMCompiler.cmake -/CMakeFiles/4.2.1/CMakeCCompiler.cmake -/CMakeFiles/4.2.1/CMakeCXXCompiler.cmake -/CMakeFiles/4.2.1/CMakeDetermineCompilerABI_C.bin -/CMakeFiles/4.2.1/CMakeDetermineCompilerABI_CXX.bin -/CMakeFiles/4.2.1/CMakeSystem.cmake -/CMakeFiles/rotary-controller-f4.elf.dir/ASM.includecache -/CMakeFiles/rotary-controller-f4.elf.dir/build.make -/CMakeFiles/rotary-controller-f4.elf.dir/cmake_clean.cmake -/CMakeFiles/rotary-controller-f4.elf.dir/compiler_depend.make -/CMakeFiles/rotary-controller-f4.elf.dir/compiler_depend.ts -/CMakeFiles/rotary-controller-f4.elf.dir/depend.internal -/CMakeFiles/rotary-controller-f4.elf.dir/depend.make -/CMakeFiles/rotary-controller-f4.elf.dir/DependInfo.cmake -/CMakeFiles/rotary-controller-f4.elf.dir/flags.make -/CMakeFiles/rotary-controller-f4.elf.dir/link.txt -/CMakeFiles/rotary-controller-f4.elf.dir/progress.make -/CMakeFiles/clion-environment.txt -/CMakeFiles/cmake.check_cache -/CMakeFiles/CMakeConfigureLog.yaml -/CMakeFiles/CMakeDirectoryInformation.cmake -/CMakeFiles/InstallScripts.json -/CMakeFiles/Makefile.cmake -/CMakeFiles/Makefile2 -/CMakeFiles/progress.marks -/CMakeFiles/TargetDirectories.txt -/cmake-build-release/.cmake/api/v1/query/cache-v2 -/cmake-build-release/.cmake/api/v1/query/cmakeFiles-v1 -/cmake-build-release/.cmake/api/v1/query/codemodel-v2 -/cmake-build-release/.cmake/api/v1/query/toolchains-v1 -/cmake-build-release/.cmake/api/v1/reply/cache-v2-75ccddff1f9462878f34.json -/cmake-build-release/.cmake/api/v1/reply/cmakeFiles-v1-efb40c770de9d5bf104a.json -/cmake-build-release/.cmake/api/v1/reply/codemodel-v2-1d0bfa3e0f2ca88c0356.json -/cmake-build-release/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json -/cmake-build-release/.cmake/api/v1/reply/index-2025-07-25T20-06-13-0400.json -/cmake-build-release/.cmake/api/v1/reply/target-rotary-controller-f4.elf-Release-23665a0d8e0985d66bdc.json -/cmake-build-release/.cmake/api/v1/reply/toolchains-v1-516567ce22d77594ccde.json -/cmake-build-release/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c -/cmake-build-release/CMakeFiles/3.31.6/CompilerIdCXX/CMakeCXXCompilerId.cpp -/cmake-build-release/CMakeFiles/3.31.6/CMakeASMCompiler.cmake -/cmake-build-release/CMakeFiles/3.31.6/CMakeCCompiler.cmake -/cmake-build-release/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake -/cmake-build-release/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin -/cmake-build-release/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin -/cmake-build-release/CMakeFiles/3.31.6/CMakeSystem.cmake -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/ASM.includecache -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/build.make -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/cmake_clean.cmake -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/compiler_depend.make -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/compiler_depend.ts -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/depend.internal -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/depend.make -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/DependInfo.cmake -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/flags.make -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/link.txt -/cmake-build-release/CMakeFiles/rotary-controller-f4.elf.dir/progress.make -/cmake-build-release/CMakeFiles/clion-environment.txt -/cmake-build-release/CMakeFiles/clion-Release-log.txt -/cmake-build-release/CMakeFiles/cmake.check_cache -/cmake-build-release/CMakeFiles/CMakeConfigureLog.yaml -/cmake-build-release/CMakeFiles/CMakeDirectoryInformation.cmake -/cmake-build-release/CMakeFiles/Makefile.cmake -/cmake-build-release/CMakeFiles/Makefile2 -/cmake-build-release/CMakeFiles/progress.marks -/cmake-build-release/CMakeFiles/TargetDirectories.txt -/cmake-build-release/cmake_install.cmake -/cmake-build-release/CMakeCache.txt -/cmake-build-release/Makefile -/cmake-build-release/rotary-controller-f4.bin -/.cmake/api/v1/query/cache-v2 -/.cmake/api/v1/query/cmakeFiles-v1 -/.cmake/api/v1/query/codemodel-v2 -/.cmake/api/v1/query/toolchains-v1 -/.cmake/api/v1/reply/cache-v2-f5e9ca3708de3f124d35.json -/.cmake/api/v1/reply/cmakeFiles-v1-281ea38263c07c7344ed.json -/.cmake/api/v1/reply/codemodel-v2-f943dd8c247e07984a15.json -/.cmake/api/v1/reply/directory-.-Release-9ad1a58d4644cf6543fa.json -/.cmake/api/v1/reply/index-2026-01-26T01-20-50-0476.json -/.cmake/api/v1/reply/target-rotary-controller-f4.elf-Release-abbd90adbbfbd406bb3c.json -/.cmake/api/v1/reply/toolchains-v1-516567ce22d77594ccde.json +/Release/ +*.bin +*.cyclo +*.list +objects.list +objects.mk +sources.mk +makefile +subdir.mk + +# Emulator build +/emulator/build/ diff --git a/README.md b/README.md index 7a2992d..6db3508 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This repository contains the **firmware** for a rotary controller board based on * Modular firmware structure with FreeRTOS support * Supports ST‑Link V2 and Raspberry Pi + OpenOCD programming * Optimized for high-speed encoder + stepper motor control +* Includes a native lathe emulator for hardware-free testing with the Python GUI --- @@ -58,6 +59,23 @@ make clean --- +## πŸ–₯️ Lathe Emulator + +A native Linux emulator is included for hardware-free firmware testing. It compiles the real firmware sources (`Ramps.c`, `Modbus.c`, `Scales.c`, `UARTCallback.c`) against a HAL/FreeRTOS shim layer and simulates lathe physics β€” spindle with inertia, leadscrew, carriage with half-nut engagement, and cross-slide. The emulator exposes Modbus RTU via PTY pair and TCP socket so the unmodified Python GUI can connect as if talking to real hardware. + +A two-pane ANSI terminal dashboard with sparklines provides live visualization, with keyboard controls for spindle RPM, manual axis movement, half-nut engagement, and more. All parameters are configurable via TOML file. + +### Emulator Build & Run + +```bash +cd emulator +cmake -B build +cmake --build build +./build/lathe-emulator config/lathe.toml +``` + +--- + ## πŸ”§ Hardware Configuration * `.ioc` file for use with STM32CubeMX included diff --git a/emulator/CMakeLists.txt b/emulator/CMakeLists.txt new file mode 100644 index 0000000..dc9d60f --- /dev/null +++ b/emulator/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.16) +project(lathe-emulator C CXX) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Firmware source directory +set(FW_DIR ${CMAKE_SOURCE_DIR}/..) + +# --- Shim headers come FIRST so they override the real HAL/FreeRTOS --- +include_directories( + ${CMAKE_SOURCE_DIR}/shim + ${CMAKE_SOURCE_DIR}/src + ${FW_DIR}/Core/Inc +) + +# Preprocessor defines to match firmware expectations +add_definitions( + -DSTM32F411xE + -DUSE_HAL_DRIVER + -DEMULATOR_BUILD +) + +# Compiler warnings +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers) + +# --- Firmware C sources (compiled against our shim) --- +set(FW_SOURCES + ${FW_DIR}/Core/Src/Ramps.c + ${FW_DIR}/Core/Src/Modbus.c + ${FW_DIR}/Core/Src/Scales.c + ${FW_DIR}/Core/Src/UARTCallback.c +) + +# --- Shim implementations --- +set(SHIM_SOURCES + ${CMAKE_SOURCE_DIR}/shim/hal_shim.c + ${CMAKE_SOURCE_DIR}/shim/freertos_shim.cpp +) + +# --- Emulator C++ sources --- +set(EMU_SOURCES + ${CMAKE_SOURCE_DIR}/src/main.cpp + ${CMAKE_SOURCE_DIR}/src/physics.cpp + ${CMAKE_SOURCE_DIR}/src/transport.cpp + ${CMAKE_SOURCE_DIR}/src/dashboard.cpp + ${CMAKE_SOURCE_DIR}/src/config.cpp +) + +add_executable(lathe-emulator + ${FW_SOURCES} + ${SHIM_SOURCES} + ${EMU_SOURCES} +) + +target_link_libraries(lathe-emulator + pthread + m +) + +# Allow C++ files to include C headers +set_source_files_properties(${FW_SOURCES} PROPERTIES LANGUAGE C) +set_source_files_properties(${SHIM_SOURCES} ${EMU_SOURCES} PROPERTIES LANGUAGE CXX) + +# The firmware .c files must be compiled as C, but our shim headers +# use extern "C" guards so they work from both C and C++ translation units. +# Force C compilation for firmware sources specifically. +set_source_files_properties(${FW_SOURCES} ${CMAKE_SOURCE_DIR}/shim/hal_shim.c + PROPERTIES LANGUAGE C COMPILE_FLAGS "-std=gnu11" +) diff --git a/emulator/config/lathe.toml b/emulator/config/lathe.toml new file mode 100644 index 0000000..ef2935d --- /dev/null +++ b/emulator/config/lathe.toml @@ -0,0 +1,58 @@ +# Lathe Emulator Configuration +# All dimensions in metric (mm) unless noted. +# Display units are controlled by [display].units. + +[display] +units = "metric" # "metric" (mm) or "imperial" (inches) + +[spindle] +counts_per_rev = 4000 # Encoder counts per revolution +inertia_kg_m2 = 0.05 # Rotational inertia (kg*m^2) +max_torque_nm = 2.0 # Max motor torque for accel/decel +friction_nm = 0.1 # Constant friction torque +initial_rpm = 0.0 + +[leadscrew] +tpi = 8 # Threads per inch of the physical leadscrew +mm_per_step = 0.0025 # Leadscrew travel per stepper step + +[z_axis] +encoder_counts_per_mm = 400 # Z-axis DRO resolution +backlash_mm = 0.02 # Mechanical backlash +max_position_mm = 300.0 +min_position_mm = -5.0 +initial_position_mm = 0.0 +half_nut_engaged = false # Initial half-nut state + +[cross_slide] +encoder_counts_per_mm = 400 # Cross-slide DRO resolution +max_position_mm = 100.0 +min_position_mm = -5.0 +initial_position_mm = 50.0 +manual_step_mm = 0.01 # (unused, kept for reference) +jog_max_velocity_mm_s = 10.0 # Max manual jog speed (mm/s) +jog_acceleration_mm_s2 = 50.0 # Manual jog acceleration (mm/s^2) +x_up_is_negative = true # true: Up arrow = -X (away from operator) +manual_move_timeout_s = 2.0 # seconds of inactivity before auto-disabling manual move; 0 = no timeout (pure toggle) + +[servo] +max_speed = 720 # steps/s (firmware default, overridable via Modbus) +acceleration = 120 # steps/s^2 + +[modbus] +address = 17 +baud = 115200 + +[transport] +pty_enabled = true # Create a PTY pair (prints slave device path on startup) +tcp_port = 5020 +tcp_enabled = true + +[dashboard] +refresh_hz = 10 # Dashboard update rate +sparkline_seconds = 30 # History window for sparklines +log_max_lines = 200 # Event log buffer depth + +[simulation] +isr_rate_hz = 10000 # ISR tick rate (real: ~100kHz, emulator: 10kHz default) +realtime = true # true = wall-clock paced, false = fast-forward diff --git a/emulator/shim/FreeRTOS.h b/emulator/shim/FreeRTOS.h new file mode 100644 index 0000000..06d5294 --- /dev/null +++ b/emulator/shim/FreeRTOS.h @@ -0,0 +1,34 @@ +/* + * FreeRTOS kernel shim for emulator. + * Provides base types used throughout the Modbus library. + */ +#ifndef INC_FREERTOS_H +#define INC_FREERTOS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int32_t BaseType_t; +typedef uint32_t UBaseType_t; +typedef uint32_t TickType_t; + +#define pdFALSE ((BaseType_t)0) +#define pdTRUE ((BaseType_t)1) +#define pdPASS pdTRUE +#define pdFAIL pdFALSE + +#define portMAX_DELAY ((TickType_t)0xFFFFFFFFUL) + +#define configTICK_RATE_HZ 1000U + +/* portYIELD_FROM_ISR - no-op in emulator (no interrupt priority to manage) */ +#define portYIELD_FROM_ISR(xHigherPriorityTaskWoken) (void)(xHigherPriorityTaskWoken) + +#ifdef __cplusplus +} +#endif + +#endif /* INC_FREERTOS_H */ diff --git a/emulator/shim/cmsis_os.h b/emulator/shim/cmsis_os.h new file mode 100644 index 0000000..44b9a52 --- /dev/null +++ b/emulator/shim/cmsis_os.h @@ -0,0 +1,11 @@ +/* + * CMSIS-RTOS v1/v2 combined shim for emulator. + * The firmware uses cmsis_os2.h API but includes "cmsis_os.h". + * This header provides both. + */ +#ifndef __CMSIS_OS_H +#define __CMSIS_OS_H + +#include "cmsis_os2.h" + +#endif /* __CMSIS_OS_H */ diff --git a/emulator/shim/cmsis_os2.h b/emulator/shim/cmsis_os2.h new file mode 100644 index 0000000..140884c --- /dev/null +++ b/emulator/shim/cmsis_os2.h @@ -0,0 +1,100 @@ +/* + * CMSIS-RTOS v2 API shim for emulator. + */ +#ifndef __CMSIS_OS2_H +#define __CMSIS_OS2_H + +#include +#include "FreeRTOS.h" +#include "task.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Priority levels */ +typedef enum { + osPriorityNone = 0, + osPriorityIdle = 1, + osPriorityLow = 8, + osPriorityLow1 = 8+1, + osPriorityBelowNormal = 16, + osPriorityNormal = 24, + osPriorityAboveNormal = 32, + osPriorityHigh = 40, + osPriorityRealtime = 48, + osPriorityISR = 56, + osPriorityError = -1, + osPriorityReserved = 0x7FFFFFFF +} osPriority_t; + +/* Status codes */ +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4, + osErrorNoMemory = -5, + osErrorISR = -6, + osStatusReserved = 0x7FFFFFFF +} osStatus_t; + +/* Thread */ +typedef void *osThreadId_t; + +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; + void *stack_mem; + uint32_t stack_size; + osPriority_t priority; + uint32_t tz_module; + uint32_t reserved; +} osThreadAttr_t; + +typedef void (*osThreadFunc_t)(void *argument); + +osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr); +osThreadId_t osThreadGetId(void); + +/* Delay */ +osStatus_t osDelay(uint32_t ticks); + +/* Message Queue */ +typedef void *osMessageQueueId_t; + +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; + void *mq_mem; + uint32_t mq_size; +} osMessageQueueAttr_t; + +osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, const osMessageQueueAttr_t *attr); + +/* Semaphore */ +typedef void *osSemaphoreId_t; + +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; +} osSemaphoreAttr_t; + +osSemaphoreId_t osSemaphoreNew(uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr); + +/* Kernel */ +osStatus_t osKernelInitialize(void); +osStatus_t osKernelStart(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __CMSIS_OS2_H */ diff --git a/emulator/shim/core_cm4.h b/emulator/shim/core_cm4.h new file mode 100644 index 0000000..194c0ee --- /dev/null +++ b/emulator/shim/core_cm4.h @@ -0,0 +1,64 @@ +/* + * Cortex-M4 core register shim for emulator. + * Provides DWT cycle counter and CoreDebug stubs. + */ +#ifndef __CORE_CM4_H +#define __CORE_CM4_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* DWT (Data Watchpoint and Trace) */ +typedef struct { + volatile uint32_t CTRL; + uint32_t RESERVED0[5]; + volatile uint32_t CYCCNT; +} DWT_Type; + +extern DWT_Type emu_dwt; +#define DWT (&emu_dwt) + +#define DWT_CTRL_CYCCNTENA_Msk (1UL << 0) + +/* CoreDebug */ +typedef struct { + volatile uint32_t DEMCR; +} CoreDebug_Type; + +extern CoreDebug_Type emu_coreDebug; +#define CoreDebug (&emu_coreDebug) + +#define CoreDebug_DEMCR_TRCENA_Msk (1UL << 24) + +/* NVIC stubs */ +typedef enum { + NonMaskableInt_IRQn = -14, + HardFault_IRQn = -13, + MemoryManagement_IRQn = -12, + BusFault_IRQn = -11, + UsageFault_IRQn = -10, + SVCall_IRQn = -5, + PendSV_IRQn = -2, + SysTick_IRQn = -1, + TIM1_BRK_TIM9_IRQn = 24, + TIM1_UP_TIM10_IRQn = 25, + USART1_IRQn = 37, +} IRQn_Type; + +static inline void __enable_irq(void) {} +static inline void __disable_irq(void) {} +static inline void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) { (void)IRQn; (void)priority; } +static inline void NVIC_EnableIRQ(IRQn_Type IRQn) { (void)IRQn; } +static inline void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority) { + (void)IRQn; (void)PreemptPriority; (void)SubPriority; +} +static inline void HAL_NVIC_EnableIRQ(IRQn_Type IRQn) { (void)IRQn; } + +#ifdef __cplusplus +} +#endif + +#endif /* __CORE_CM4_H */ diff --git a/emulator/shim/freertos_shim.cpp b/emulator/shim/freertos_shim.cpp new file mode 100644 index 0000000..f1e9ec8 --- /dev/null +++ b/emulator/shim/freertos_shim.cpp @@ -0,0 +1,419 @@ +/* + * FreeRTOS / CMSIS-RTOS v2 shim implementation for emulator. + * + * Maps FreeRTOS primitives to POSIX threads + C++ sync primitives. + * - osThreadNew β†’ pthread_create + * - osDelay β†’ usleep + * - xTimerCreate β†’ background timer thread with condition variable + * - xSemaphore* β†’ pthread_mutex + condition variable (counting semaphore) + * - xTaskNotify β†’ per-task condition variable + notification value + * - xQueue* β†’ mutex-protected ring buffer + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "FreeRTOS.h" +#include "task.h" +#include "queue.h" +#include "timers.h" +#include "semphr.h" +#include "cmsis_os2.h" +} + +/* ======================================================================== + * Task Notification + * ======================================================================== */ + +struct TaskControlBlock { + std::mutex mtx; + std::condition_variable cv; + uint32_t notifyValue; + bool notifyPending; + pthread_t threadId; + + TaskControlBlock() : notifyValue(0), notifyPending(false), threadId(0) {} +}; + +/* Map pthread_t β†’ TaskControlBlock for task notifications */ +static std::mutex g_taskMapMutex; +static std::map g_taskMap; + +static TaskControlBlock* getOrCreateTCB(pthread_t tid) { + std::lock_guard lock(g_taskMapMutex); + auto it = g_taskMap.find(tid); + if (it != g_taskMap.end()) return it->second; + auto *tcb = new TaskControlBlock(); + tcb->threadId = tid; + g_taskMap[tid] = tcb; + return tcb; +} + +static TaskControlBlock* getCurrentTCB() { + return getOrCreateTCB(pthread_self()); +} + +extern "C" TaskHandle_t xTaskGetCurrentTaskHandle(void) { + return (TaskHandle_t)getCurrentTCB(); +} + +extern "C" BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction) { + auto *tcb = (TaskControlBlock*)xTaskToNotify; + if (!tcb) return pdFAIL; + { + std::lock_guard lock(tcb->mtx); + switch (eAction) { + case eSetValueWithOverwrite: + tcb->notifyValue = ulValue; + break; + case eSetValueWithoutOverwrite: + if (!tcb->notifyPending) tcb->notifyValue = ulValue; + break; + case eIncrement: + tcb->notifyValue++; + break; + case eNoAction: + default: + break; + } + tcb->notifyPending = true; + } + tcb->cv.notify_one(); + return pdPASS; +} + +extern "C" BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify, uint32_t ulValue, + eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken) { + if (pxHigherPriorityTaskWoken) *pxHigherPriorityTaskWoken = pdFALSE; + return xTaskNotify(xTaskToNotify, ulValue, eAction); +} + +extern "C" uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait) { + auto *tcb = getCurrentTCB(); + std::unique_lock lock(tcb->mtx); + + if (!tcb->notifyPending) { + if (xTicksToWait == portMAX_DELAY) { + tcb->cv.wait(lock, [&]{ return tcb->notifyPending; }); + } else { + auto ms = std::chrono::milliseconds(xTicksToWait); + tcb->cv.wait_for(lock, ms, [&]{ return tcb->notifyPending; }); + } + } + + uint32_t val = tcb->notifyValue; + if (tcb->notifyPending) { + if (xClearCountOnExit) { + tcb->notifyValue = 0; + } else { + if (tcb->notifyValue > 0) tcb->notifyValue--; + } + tcb->notifyPending = false; + } + return val; +} + +/* ======================================================================== + * Threads (osThreadNew) + * ======================================================================== */ + +struct ThreadArgs { + osThreadFunc_t func; + void *arg; + TaskControlBlock *tcb; +}; + +static void *threadEntry(void *rawArgs) { + auto *args = (ThreadArgs*)rawArgs; + /* Register this thread's TCB in the global map */ + { + std::lock_guard lock(g_taskMapMutex); + args->tcb->threadId = pthread_self(); + g_taskMap[pthread_self()] = args->tcb; + } + args->func(args->arg); + delete args; + return nullptr; +} + +extern "C" osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr) { + (void)attr; + auto *tcb = new TaskControlBlock(); + auto *args = new ThreadArgs{func, argument, tcb}; + + pthread_t tid; + pthread_attr_t pattr; + pthread_attr_init(&pattr); + pthread_attr_setdetachstate(&pattr, PTHREAD_CREATE_DETACHED); + int ret = pthread_create(&tid, &pattr, threadEntry, args); + pthread_attr_destroy(&pattr); + + if (ret != 0) { + delete args; + delete tcb; + return nullptr; + } + + /* Temporarily store tid so the TCB is findable even before the thread runs */ + tcb->threadId = tid; + { + std::lock_guard lock(g_taskMapMutex); + g_taskMap[tid] = tcb; + } + + return (osThreadId_t)tcb; +} + +extern "C" osThreadId_t osThreadGetId(void) { + return (osThreadId_t)getCurrentTCB(); +} + +/* ======================================================================== + * Delay + * ======================================================================== */ + +extern "C" osStatus_t osDelay(uint32_t ticks) { + /* ticks are in ms (configTICK_RATE_HZ = 1000) */ + usleep(ticks * 1000); + return osOK; +} + +/* ======================================================================== + * Software Timers + * ======================================================================== */ + +struct SoftwareTimer { + std::string name; + uint32_t periodMs; + bool autoReload; + TimerCallbackFunction_t callback; + std::atomic running; + std::atomic alive; + std::mutex mtx; + std::condition_variable cv; + std::thread worker; + + SoftwareTimer(const char *n, uint32_t period, bool reload, TimerCallbackFunction_t cb) + : name(n ? n : ""), periodMs(period), autoReload(reload), callback(cb), + running(false), alive(true) + { + worker = std::thread([this]() { timerLoop(); }); + worker.detach(); + } + + void timerLoop() { + while (alive.load()) { + { + std::unique_lock lock(mtx); + cv.wait(lock, [this]{ return running.load() || !alive.load(); }); + } + if (!alive.load()) break; + + /* Wait for the timer period */ + { + std::unique_lock lock(mtx); + bool expired = !cv.wait_for(lock, std::chrono::milliseconds(periodMs), + [this]{ return !running.load() || !alive.load(); }); + if (!alive.load()) break; + if (!running.load()) continue; /* Timer was stopped/reset during wait */ + + if (expired) { + /* Timer fired */ + if (!autoReload) running.store(false); + /* The Modbus library casts TimerHandle_t as TimerHandle_t* in callback signature */ + TimerHandle_t self = (TimerHandle_t)this; + lock.unlock(); + callback(&self); + } + } + } + } +}; + +extern "C" TimerHandle_t xTimerCreate(const char *pcTimerName, TickType_t xTimerPeriodInTicks, + UBaseType_t uxAutoReload, void *pvTimerID, + TimerCallbackFunction_t pxCallbackFunction) { + (void)pvTimerID; + auto *t = new SoftwareTimer(pcTimerName, xTimerPeriodInTicks, uxAutoReload != 0, pxCallbackFunction); + return (TimerHandle_t)t; +} + +extern "C" BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xBlockTime) { + (void)xBlockTime; + auto *t = (SoftwareTimer*)xTimer; + t->running.store(true); + t->cv.notify_all(); + return pdPASS; +} + +extern "C" BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xBlockTime) { + (void)xBlockTime; + auto *t = (SoftwareTimer*)xTimer; + t->running.store(false); + t->cv.notify_all(); + return pdPASS; +} + +extern "C" BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xBlockTime) { + (void)xBlockTime; + auto *t = (SoftwareTimer*)xTimer; + /* Reset = stop + restart, which re-arms the period */ + t->running.store(false); + t->cv.notify_all(); + /* Brief yield to let the timer thread notice the stop */ + usleep(100); + t->running.store(true); + t->cv.notify_all(); + return pdPASS; +} + +extern "C" BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken) { + if (pxHigherPriorityTaskWoken) *pxHigherPriorityTaskWoken = pdFALSE; + return xTimerReset(xTimer, 0); +} + +/* ======================================================================== + * Semaphores + * ======================================================================== */ + +struct CountingSemaphore { + std::mutex mtx; + std::condition_variable cv; + uint32_t count; + uint32_t maxCount; + + CountingSemaphore(uint32_t max, uint32_t initial) : count(initial), maxCount(max) {} +}; + +extern "C" SemaphoreHandle_t xSemaphoreCreateBinary(void) { + return (SemaphoreHandle_t) new CountingSemaphore(1, 0); +} + +extern "C" SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount) { + return (SemaphoreHandle_t) new CountingSemaphore(uxMaxCount, uxInitialCount); +} + +extern "C" BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime) { + auto *s = (CountingSemaphore*)xSemaphore; + std::unique_lock lock(s->mtx); + + if (xBlockTime == portMAX_DELAY) { + s->cv.wait(lock, [&]{ return s->count > 0; }); + } else { + if (!s->cv.wait_for(lock, std::chrono::milliseconds(xBlockTime), [&]{ return s->count > 0; })) { + return pdFAIL; + } + } + s->count--; + return pdPASS; +} + +extern "C" BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore) { + auto *s = (CountingSemaphore*)xSemaphore; + { + std::lock_guard lock(s->mtx); + if (s->count < s->maxCount) s->count++; + } + s->cv.notify_one(); + return pdPASS; +} + +/* CMSIS osSemaphoreNew maps to counting semaphore */ +extern "C" osSemaphoreId_t osSemaphoreNew(uint32_t max_count, uint32_t initial_count, + const osSemaphoreAttr_t *attr) { + (void)attr; + return (osSemaphoreId_t) new CountingSemaphore(max_count, initial_count); +} + +/* ======================================================================== + * Message Queues + * ======================================================================== */ + +struct MessageQueue { + std::mutex mtx; + std::condition_variable cvNotEmpty; + std::deque> items; + uint32_t maxItems; + uint32_t itemSize; + + MessageQueue(uint32_t max, uint32_t size) : maxItems(max), itemSize(size) {} +}; + +extern "C" QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize) { + return (QueueHandle_t) new MessageQueue(uxQueueLength, uxItemSize); +} + +extern "C" osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, + const osMessageQueueAttr_t *attr) { + (void)attr; + return (osMessageQueueId_t) new MessageQueue(msg_count, msg_size); +} + +extern "C" BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait) { + (void)xTicksToWait; + auto *q = (MessageQueue*)xQueue; + std::lock_guard lock(q->mtx); + if (q->items.size() >= q->maxItems) return pdFAIL; + std::vector item(q->itemSize); + memcpy(item.data(), pvItemToQueue, q->itemSize); + q->items.push_back(std::move(item)); + q->cvNotEmpty.notify_one(); + return pdPASS; +} + +extern "C" BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait) { + (void)xTicksToWait; + auto *q = (MessageQueue*)xQueue; + std::lock_guard lock(q->mtx); + if (q->items.size() >= q->maxItems) return pdFAIL; + std::vector item(q->itemSize); + memcpy(item.data(), pvItemToQueue, q->itemSize); + q->items.push_front(std::move(item)); + q->cvNotEmpty.notify_one(); + return pdPASS; +} + +extern "C" BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait) { + auto *q = (MessageQueue*)xQueue; + std::unique_lock lock(q->mtx); + + if (xTicksToWait == portMAX_DELAY) { + q->cvNotEmpty.wait(lock, [&]{ return !q->items.empty(); }); + } else { + if (!q->cvNotEmpty.wait_for(lock, std::chrono::milliseconds(xTicksToWait), + [&]{ return !q->items.empty(); })) { + return pdFAIL; + } + } + + auto &front = q->items.front(); + memcpy(pvBuffer, front.data(), q->itemSize); + q->items.pop_front(); + return pdPASS; +} + +extern "C" BaseType_t xQueueReset(QueueHandle_t xQueue) { + auto *q = (MessageQueue*)xQueue; + std::lock_guard lock(q->mtx); + q->items.clear(); + return pdPASS; +} + +/* ======================================================================== + * Kernel + * ======================================================================== */ + +extern "C" osStatus_t osKernelInitialize(void) { return osOK; } +extern "C" osStatus_t osKernelStart(void) { return osOK; } diff --git a/emulator/shim/hal_shim.c b/emulator/shim/hal_shim.c new file mode 100644 index 0000000..c0b71d7 --- /dev/null +++ b/emulator/shim/hal_shim.c @@ -0,0 +1,298 @@ +/* + * HAL shim implementation for emulator. + * + * Provides minimal working implementations of STM32 HAL functions + * that route through the emulator's hardware state (emulator_state.h). + */ + +#include "stm32f4xx_hal.h" +#include "emulator_state.h" +#include + +/* ======================================================================== + * Hardware register instances (emulated) + * ======================================================================== */ + +/* GPIO port instances */ +GPIO_TypeDef emu_gpioa = {0}; +GPIO_TypeDef emu_gpiob = {0}; +GPIO_TypeDef emu_gpioc = {0}; + +/* USART instance */ +USART_TypeDef emu_usart1 = {0}; + +/* RCC instance */ +RCC_TypeDef emu_rcc = {0}; + +/* DWT / CoreDebug instances */ +DWT_Type emu_dwt = {0}; +CoreDebug_Type emu_coreDebug = {0}; + +/* Timer peripheral instances (one per encoder + one for ISR + one for systick) */ +static TIM_TypeDef emu_tim1_regs = {0}; +static TIM_TypeDef emu_tim2_regs = {0}; +static TIM_TypeDef emu_tim3_regs = {0}; +static TIM_TypeDef emu_tim4_regs = {0}; +static TIM_TypeDef emu_tim9_regs = {0}; +static TIM_TypeDef emu_tim11_regs = {0}; + +/* Timer handle instances (firmware externs these) */ +TIM_HandleTypeDef htim1 = { .Instance = &emu_tim1_regs }; +TIM_HandleTypeDef htim2 = { .Instance = &emu_tim2_regs }; +TIM_HandleTypeDef htim3 = { .Instance = &emu_tim3_regs }; +TIM_HandleTypeDef htim4 = { .Instance = &emu_tim4_regs }; +TIM_HandleTypeDef htim9 = { .Instance = &emu_tim9_regs, .Init = { .Prescaler = 99, .Period = 9 } }; +TIM_HandleTypeDef htim11 = { .Instance = &emu_tim11_regs }; + +/* UART handle instance */ +UART_HandleTypeDef huart1 = { .Instance = &emu_usart1 }; + +/* ======================================================================== + * Global emulator hardware state + * ======================================================================== */ + +EmulatorHardwareState emu_hw = {0}; + +/* ======================================================================== + * HAL Init / Tick + * ======================================================================== */ + +HAL_StatusTypeDef HAL_Init(void) { + return HAL_OK; +} + +static volatile uint32_t uwTick = 0; + +void HAL_IncTick(void) { + uwTick++; + emu_hw.hal_tick = uwTick; +} + +uint32_t HAL_GetTick(void) { + return uwTick; +} + +void Error_Handler(void) { + /* In emulator, just spin (caller made a mistake) */ + while(1) {} +} + +/* ======================================================================== + * GPIO + * ======================================================================== */ + +void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { + (void)GPIOx; + (void)GPIO_Init; + /* No-op: emulator doesn't need real GPIO configuration */ +} + +void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) { + int state = (PinState == GPIO_PIN_SET) ? 1 : 0; + + /* Track step/dir/ena pins for physics model */ + if (GPIOx == GPIOA) { + if (GPIO_Pin == GPIO_PIN_0) { /* STEP_PIN */ + emu_hw.step_pin = state; + } + if (GPIO_Pin == GPIO_PIN_3) { /* SPARE_2_PIN */ + emu_hw.spare2_pin = state; + } + } else if (GPIOx == GPIOB) { + if (GPIO_Pin == GPIO_PIN_14) { /* DIR_PIN */ + emu_hw.dir_pin = state; + } + if (GPIO_Pin == GPIO_PIN_15) { /* ENA_PIN */ + emu_hw.ena_pin = state; + } + } + + /* Forward to callback if registered */ + if (emu_hw.on_gpio_write) { + emu_hw.on_gpio_write(GPIOx, GPIO_Pin, state); + } +} + +void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { + /* Read ODR, toggle the bit, write back */ + if (GPIOx->ODR & GPIO_Pin) { + GPIOx->ODR &= ~GPIO_Pin; + HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_RESET); + } else { + GPIOx->ODR |= GPIO_Pin; + HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_SET); + } +} + +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { + return (GPIOx->IDR & GPIO_Pin) ? GPIO_PIN_SET : GPIO_PIN_RESET; +} + +/* ======================================================================== + * Timer + * ======================================================================== */ + +HAL_StatusTypeDef HAL_TIM_Encoder_Init(TIM_HandleTypeDef *htim, TIM_Encoder_InitTypeDef *sConfig) { + (void)htim; + (void)sConfig; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, uint32_t Channel) { + (void)htim; + (void)Channel; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim) { + (void)htim; + /* The ISR is driven by the emulator's ISR thread, not by hardware interrupts */ + return HAL_OK; +} + +HAL_StatusTypeDef HAL_TIMEx_MasterConfigSynchronization(TIM_HandleTypeDef *htim, + TIM_MasterConfigTypeDef *sMasterConfig) { + (void)htim; + (void)sMasterConfig; + return HAL_OK; +} + +void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim) { + (void)htim; + /* Not used in emulator β€” we call SynchroRefreshTimerIsr directly */ +} + +__attribute__((weak)) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { + (void)htim; +} + +/* + * Update the virtual timer counter values from the physics model. + * Called by the physics thread before each ISR tick. + * Index mapping: 0β†’TIM1, 1β†’TIM2, 2β†’TIM3, 3β†’TIM4 + */ +void emu_update_timer_counters(void) { + emu_tim1_regs.CNT = emu_hw.scale_counters[0]; + emu_tim2_regs.CNT = emu_hw.scale_counters[1]; + emu_tim3_regs.CNT = emu_hw.scale_counters[2]; + emu_tim4_regs.CNT = emu_hw.scale_counters[3]; +} + +/* ======================================================================== + * UART + * + * The firmware uses USART_HW mode (not DMA), so the flow is: + * RX: HAL_UART_Receive_IT arms β†’ transport injects byte β†’ HAL_UART_RxCpltCallback + * TX: HAL_UART_Transmit_IT sends β†’ callback β†’ HAL_UART_TxCpltCallback + * ======================================================================== */ + +HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { + huart->pRxBuffPtr = pData; + huart->RxXferSize = Size; + huart->RxXferCount = Size; + huart->RxState = HAL_UART_STATE_BUSY_RX; + + /* Tell the transport layer we're ready for a byte */ + emu_hw.uart_rx_buf = pData; + emu_hw.uart_rx_size = Size; + emu_hw.uart_rx_armed = 1; + + return HAL_OK; +} + +HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) { + /* Route bytes to transport layer */ + if (emu_hw.on_uart_tx) { + emu_hw.on_uart_tx(pData, Size); + } + + /* Set TC flag so the busy-wait in sendTxBuffer completes */ + huart->Instance->SR |= USART_SR_TC; + huart->gState = HAL_UART_STATE_READY; + + /* Notify the Modbus task that TX is complete */ + HAL_UART_TxCpltCallback(huart); + + return HAL_OK; +} + +HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) { + /* Same as Transmit_IT for the emulator */ + return HAL_UART_Transmit_IT(huart, pData, Size); +} + +HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { + (void)huart; (void)pData; (void)Size; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart) { + (void)huart; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart) { + huart->RxState = HAL_UART_STATE_READY; + emu_hw.uart_rx_armed = 0; + return HAL_OK; +} + +uint32_t HAL_UART_GetState(UART_HandleTypeDef *huart) { + (void)huart; + return HAL_UART_STATE_READY; +} + +void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { + (void)huart; +} + +HAL_StatusTypeDef HAL_HalfDuplex_EnableTransmitter(UART_HandleTypeDef *huart) { + (void)huart; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_HalfDuplex_EnableReceiver(UART_HandleTypeDef *huart) { + (void)huart; + return HAL_OK; +} + +/* ======================================================================== + * RCC (no-ops) + * ======================================================================== */ + +HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct) { + (void)RCC_OscInitStruct; + return HAL_OK; +} + +HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency) { + (void)RCC_ClkInitStruct; (void)FLatency; + return HAL_OK; +} + +/* ======================================================================== + * Event Log + * ======================================================================== */ + +#include +#include +#include + +void emu_log_event(const char *fmt, ...) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + + int idx = emu_hw.event_log_head % 256; + int off = snprintf(emu_hw.event_log[idx], 128, "%02d:%02d:%02d.%03ld ", + tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000000); + + va_list ap; + va_start(ap, fmt); + vsnprintf(emu_hw.event_log[idx] + off, 128 - off, fmt, ap); + va_end(ap); + + emu_hw.event_log_head++; + if (emu_hw.event_log_count < 256) emu_hw.event_log_count++; +} diff --git a/emulator/shim/main.h b/emulator/shim/main.h new file mode 100644 index 0000000..27fd4d6 --- /dev/null +++ b/emulator/shim/main.h @@ -0,0 +1,28 @@ +/* + * main.h shim for emulator. + * Overrides the firmware's Core/Inc/main.h. + * Provides the same declarations the firmware code expects. + */ +#ifndef __MAIN_H +#define __MAIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stm32f4xx_hal.h" +#include "Ramps.h" + +void Error_Handler(void); + +/* Pin defines that main.h normally provides */ +#define SPARE_1_Pin GPIO_PIN_1 +#define SPARE_1_GPIO_Port GPIOA +#define USR_LED_Pin GPIO_PIN_12 +#define USR_LED_GPIO_Port GPIOB + +#ifdef __cplusplus +} +#endif + +#endif /* __MAIN_H */ diff --git a/emulator/shim/queue.h b/emulator/shim/queue.h new file mode 100644 index 0000000..44f488b --- /dev/null +++ b/emulator/shim/queue.h @@ -0,0 +1,25 @@ +/* + * FreeRTOS queue API shim for emulator. + */ +#ifndef INC_QUEUE_H +#define INC_QUEUE_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *QueueHandle_t; + +QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); +BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait); +BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait); +BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait); +BaseType_t xQueueReset(QueueHandle_t xQueue); + +#ifdef __cplusplus +} +#endif + +#endif /* INC_QUEUE_H */ diff --git a/emulator/shim/semphr.h b/emulator/shim/semphr.h new file mode 100644 index 0000000..596eee5 --- /dev/null +++ b/emulator/shim/semphr.h @@ -0,0 +1,24 @@ +/* + * FreeRTOS semaphore API shim for emulator. + */ +#ifndef INC_SEMPHR_H +#define INC_SEMPHR_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *SemaphoreHandle_t; + +SemaphoreHandle_t xSemaphoreCreateBinary(void); +SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount); +BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime); +BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore); + +#ifdef __cplusplus +} +#endif + +#endif /* INC_SEMPHR_H */ diff --git a/emulator/shim/stm32f4xx.h b/emulator/shim/stm32f4xx.h new file mode 100644 index 0000000..4c62299 --- /dev/null +++ b/emulator/shim/stm32f4xx.h @@ -0,0 +1,120 @@ +/* + * STM32F4xx device header shim for emulator. + * Provides minimal register structure stubs and includes core_cm4. + */ +#ifndef __STM32F4xx_H +#define __STM32F4xx_H + +#include +#include "core_cm4.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Undef termios macros that clash with register field names. + * This is safe because the emulator never uses termios CR1/CR2/CR3 + * from this header's context β€” transport.cpp includes termios.h + * before this header gets pulled in via stm32f4xx_hal.h. + */ +#undef CR1 +#undef CR2 +#undef CR3 + +/* Minimal USART register structure (only fields accessed by Modbus.c) */ +typedef struct { + volatile uint32_t SR; /* Status register */ + volatile uint32_t DR; /* Data register */ + volatile uint32_t BRR; /* Baud rate register */ + volatile uint32_t CR1; + volatile uint32_t CR2; + volatile uint32_t CR3; + volatile uint32_t GTPR; +} USART_TypeDef; + +#define USART_SR_TC (1U << 6) /* Transmission Complete flag */ +#define USART_SR_TXE (1U << 7) /* Transmit Data Register Empty */ + +/* GPIO port structure */ +typedef struct { + volatile uint32_t MODER; + volatile uint32_t OTYPER; + volatile uint32_t OSPEEDR; + volatile uint32_t PUPDR; + volatile uint32_t IDR; + volatile uint32_t ODR; + volatile uint32_t BSRR; + volatile uint32_t LCKR; + volatile uint32_t AFR[2]; +} GPIO_TypeDef; + +/* Timer counter register structure (only fields accessed via macros) */ +typedef struct { + volatile uint32_t CR1; + volatile uint32_t CR2; + volatile uint32_t SMCR; + volatile uint32_t DIER; + volatile uint32_t SR; + volatile uint32_t EGR; + volatile uint32_t CCMR1; + volatile uint32_t CCMR2; + volatile uint32_t CCER; + volatile uint32_t CNT; /* Counter value - this is what __HAL_TIM_GET_COUNTER reads */ + volatile uint32_t PSC; + volatile uint32_t ARR; + volatile uint32_t RCR; + volatile uint32_t CCR1; + volatile uint32_t CCR2; + volatile uint32_t CCR3; + volatile uint32_t CCR4; +} TIM_TypeDef; + +/* DMA stream structure (minimal) */ +typedef struct { + volatile uint32_t CR; + volatile uint32_t NDTR; + volatile uint32_t PAR; + volatile uint32_t M0AR; + volatile uint32_t M1AR; + volatile uint32_t FCR; +} DMA_Stream_TypeDef; + +/* RCC structure (minimal stub) */ +typedef struct { + volatile uint32_t CR; + volatile uint32_t PLLCFGR; + volatile uint32_t CFGR; + volatile uint32_t CIR; + volatile uint32_t AHB1RSTR; + volatile uint32_t AHB2RSTR; + volatile uint32_t RESERVED0[2]; + volatile uint32_t APB1RSTR; + volatile uint32_t APB2RSTR; + volatile uint32_t RESERVED1[2]; + volatile uint32_t AHB1ENR; +} RCC_TypeDef; + +/* GPIO port instances */ +extern GPIO_TypeDef emu_gpioa, emu_gpiob, emu_gpioc; +#define GPIOA (&emu_gpioa) +#define GPIOB (&emu_gpiob) +#define GPIOC (&emu_gpioc) + +/* RCC instance */ +extern RCC_TypeDef emu_rcc; +#define RCC (&emu_rcc) + +/* USART instances */ +extern USART_TypeDef emu_usart1; + +/* STM32F411xE define (expected by HAL headers) */ +#ifndef STM32F411xE +#define STM32F411xE +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_H */ diff --git a/emulator/shim/stm32f4xx_hal.h b/emulator/shim/stm32f4xx_hal.h new file mode 100644 index 0000000..34c2cdb --- /dev/null +++ b/emulator/shim/stm32f4xx_hal.h @@ -0,0 +1,35 @@ +/* + * Main HAL header shim for emulator. + * This file is what the firmware includes as "stm32f4xx_hal.h". + * It pulls in all the sub-module shim headers. + */ +#ifndef __STM32F4xx_HAL_H +#define __STM32F4xx_HAL_H + +#include +#include + +#include "stm32f4xx_hal_conf.h" +#include "stm32f4xx_hal_def.h" +#include "stm32f4xx.h" +#include "stm32f4xx_hal_gpio.h" +#include "stm32f4xx_hal_dma.h" +#include "stm32f4xx_hal_tim.h" +#include "stm32f4xx_hal_uart.h" +#include "stm32f4xx_hal_rcc.h" +#include "stm32f4xx_hal_cortex.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* HAL Init/Tick functions */ +HAL_StatusTypeDef HAL_Init(void); +void HAL_IncTick(void); +uint32_t HAL_GetTick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_H */ diff --git a/emulator/shim/stm32f4xx_hal_conf.h b/emulator/shim/stm32f4xx_hal_conf.h new file mode 100644 index 0000000..768b6e6 --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_conf.h @@ -0,0 +1,24 @@ +/* + * HAL configuration shim for emulator. + */ +#ifndef __STM32F4xx_HAL_CONF_H +#define __STM32F4xx_HAL_CONF_H + +#define HAL_MODULE_ENABLED +#define HAL_GPIO_MODULE_ENABLED +#define HAL_TIM_MODULE_ENABLED +#define HAL_UART_MODULE_ENABLED +#define HAL_DMA_MODULE_ENABLED +#define HAL_RCC_MODULE_ENABLED +#define HAL_CORTEX_MODULE_ENABLED + +/* Oscillator values (not used in emulator, but referenced by some HAL macros) */ +#define HSE_VALUE 8000000U +#define HSI_VALUE 16000000U +#define LSE_VALUE 32768U +#define LSI_VALUE 32000U + +/* Tick frequency */ +#define HAL_TICK_FREQ_1KHZ 1U + +#endif /* __STM32F4xx_HAL_CONF_H */ diff --git a/emulator/shim/stm32f4xx_hal_cortex.h b/emulator/shim/stm32f4xx_hal_cortex.h new file mode 100644 index 0000000..3cf7608 --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_cortex.h @@ -0,0 +1,22 @@ +/* + * HAL Cortex shim for emulator. + */ +#ifndef __STM32F4xx_HAL_CORTEX_H +#define __STM32F4xx_HAL_CORTEX_H + +#include "stm32f4xx_hal_def.h" +#include "core_cm4.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* All NVIC functions are already defined in core_cm4.h */ + +static inline uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb) { (void)TicksNumb; return 0; } + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_CORTEX_H */ diff --git a/emulator/shim/stm32f4xx_hal_def.h b/emulator/shim/stm32f4xx_hal_def.h new file mode 100644 index 0000000..7b904ef --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_def.h @@ -0,0 +1,43 @@ +/* + * HAL definition shim for emulator. + * Provides base types: HAL_StatusTypeDef, HAL_LockTypeDef, etc. + */ +#ifndef __STM32F4xx_HAL_DEF_H +#define __STM32F4xx_HAL_DEF_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + HAL_OK = 0x00U, + HAL_ERROR = 0x01U, + HAL_BUSY = 0x02U, + HAL_TIMEOUT = 0x03U +} HAL_StatusTypeDef; + +typedef enum { + HAL_UNLOCKED = 0x00U, + HAL_LOCKED = 0x01U +} HAL_LockTypeDef; + +#define UNUSED(X) (void)(X) + +#define __weak __attribute__((weak)) +#define __packed __attribute__((__packed__)) + +/* C11 _Noreturn is not valid in C++; map to the C++ attribute */ +#ifdef __cplusplus +#ifndef _Noreturn +#define _Noreturn [[noreturn]] +#endif +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_DEF_H */ diff --git a/emulator/shim/stm32f4xx_hal_dma.h b/emulator/shim/stm32f4xx_hal_dma.h new file mode 100644 index 0000000..5a62750 --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_dma.h @@ -0,0 +1,29 @@ +/* + * HAL DMA shim for emulator. + * Minimal stubs - DMA is not used in USART_HW mode. + */ +#ifndef __STM32F4xx_HAL_DMA_H +#define __STM32F4xx_HAL_DMA_H + +#include "stm32f4xx.h" +#include "stm32f4xx_hal_def.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define DMA_IT_HT 0x00000008U + +typedef struct { + DMA_Stream_TypeDef *Instance; +} DMA_HandleTypeDef; + +/* Macro used in UARTCallback.c to disable half-transfer interrupt */ +#define __HAL_DMA_DISABLE_IT(__HANDLE__, __INTERRUPT__) \ + do { (void)(__HANDLE__); (void)(__INTERRUPT__); } while(0) + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_DMA_H */ diff --git a/emulator/shim/stm32f4xx_hal_gpio.h b/emulator/shim/stm32f4xx_hal_gpio.h new file mode 100644 index 0000000..7a99329 --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_gpio.h @@ -0,0 +1,76 @@ +/* + * HAL GPIO shim for emulator. + */ +#ifndef __STM32F4xx_HAL_GPIO_H +#define __STM32F4xx_HAL_GPIO_H + +#include "stm32f4xx.h" +#include "stm32f4xx_hal_def.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* GPIO Pin definitions */ +#define GPIO_PIN_0 ((uint16_t)0x0001) +#define GPIO_PIN_1 ((uint16_t)0x0002) +#define GPIO_PIN_2 ((uint16_t)0x0004) +#define GPIO_PIN_3 ((uint16_t)0x0008) +#define GPIO_PIN_4 ((uint16_t)0x0010) +#define GPIO_PIN_5 ((uint16_t)0x0020) +#define GPIO_PIN_6 ((uint16_t)0x0040) +#define GPIO_PIN_7 ((uint16_t)0x0080) +#define GPIO_PIN_8 ((uint16_t)0x0100) +#define GPIO_PIN_9 ((uint16_t)0x0200) +#define GPIO_PIN_10 ((uint16_t)0x0400) +#define GPIO_PIN_11 ((uint16_t)0x0800) +#define GPIO_PIN_12 ((uint16_t)0x1000) +#define GPIO_PIN_13 ((uint16_t)0x2000) +#define GPIO_PIN_14 ((uint16_t)0x4000) +#define GPIO_PIN_15 ((uint16_t)0x8000) + +/* GPIO Pin State */ +typedef enum { + GPIO_PIN_RESET = 0U, + GPIO_PIN_SET +} GPIO_PinState; + +/* GPIO Init Structure */ +typedef struct { + uint32_t Pin; + uint32_t Mode; + uint32_t Pull; + uint32_t Speed; + uint32_t Alternate; +} GPIO_InitTypeDef; + +/* GPIO Mode definitions */ +#define GPIO_MODE_INPUT 0x00000000U +#define GPIO_MODE_OUTPUT_PP 0x00000001U +#define GPIO_MODE_OUTPUT_OD 0x00000011U +#define GPIO_MODE_AF_PP 0x00000002U +#define GPIO_MODE_AF_OD 0x00000012U +#define GPIO_MODE_ANALOG 0x00000003U + +/* GPIO Pull definitions */ +#define GPIO_NOPULL 0x00000000U +#define GPIO_PULLUP 0x00000001U +#define GPIO_PULLDOWN 0x00000002U + +/* GPIO Speed definitions */ +#define GPIO_SPEED_FREQ_LOW 0x00000000U +#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U +#define GPIO_SPEED_FREQ_HIGH 0x00000002U +#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U + +/* Function prototypes */ +void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); +void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); +void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_GPIO_H */ diff --git a/emulator/shim/stm32f4xx_hal_rcc.h b/emulator/shim/stm32f4xx_hal_rcc.h new file mode 100644 index 0000000..b3f408a --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_rcc.h @@ -0,0 +1,81 @@ +/* + * HAL RCC shim for emulator. All clock enable macros are no-ops. + */ +#ifndef __STM32F4xx_HAL_RCC_H +#define __STM32F4xx_HAL_RCC_H + +#include "stm32f4xx_hal_def.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Clock enable macros - no-ops in emulator */ +#define __HAL_RCC_GPIOA_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_GPIOB_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_GPIOC_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_GPIOD_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM1_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM2_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM3_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM4_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM9_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_TIM11_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_USART1_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_PWR_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_DMA1_CLK_ENABLE() do { } while(0) +#define __HAL_RCC_DMA2_CLK_ENABLE() do { } while(0) + +/* RCC oscillator and clock config types (for main.c SystemClock_Config - we stub these) */ +typedef struct { + uint32_t OscillatorType; + uint32_t HSEState; + uint32_t HSIState; + uint32_t LSEState; + uint32_t LSIState; + struct { + uint32_t PLLState; + uint32_t PLLSource; + uint32_t PLLM; + uint32_t PLLN; + uint32_t PLLP; + uint32_t PLLQ; + } PLL; +} RCC_OscInitTypeDef; + +typedef struct { + uint32_t ClockType; + uint32_t SYSCLKSource; + uint32_t AHBCLKDivider; + uint32_t APB1CLKDivider; + uint32_t APB2CLKDivider; +} RCC_ClkInitTypeDef; + +/* RCC constants */ +#define RCC_OSCILLATORTYPE_HSE 0x01U +#define RCC_HSE_ON 0x01U +#define RCC_PLL_ON 0x02U +#define RCC_PLLSOURCE_HSE 0x00400000U +#define RCC_PLLP_DIV2 0x00000002U +#define RCC_CLOCKTYPE_HCLK 0x02U +#define RCC_CLOCKTYPE_SYSCLK 0x01U +#define RCC_CLOCKTYPE_PCLK1 0x04U +#define RCC_CLOCKTYPE_PCLK2 0x08U +#define RCC_SYSCLKSOURCE_PLLCLK 0x08U +#define RCC_SYSCLK_DIV1 0x00U +#define RCC_HCLK_DIV1 0x00U +#define RCC_HCLK_DIV2 0x00001000U + +#define FLASH_LATENCY_3 0x03U +#define PWR_REGULATOR_VOLTAGE_SCALE1 0x0000C000U + +#define __HAL_PWR_VOLTAGESCALING_CONFIG(__REGULATOR__) do { (void)(__REGULATOR__); } while(0) + +HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct); +HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency); + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_RCC_H */ diff --git a/emulator/shim/stm32f4xx_hal_tim.h b/emulator/shim/stm32f4xx_hal_tim.h new file mode 100644 index 0000000..26ddc20 --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_tim.h @@ -0,0 +1,74 @@ +/* + * HAL TIM shim for emulator. + */ +#ifndef __STM32F4xx_HAL_TIM_H +#define __STM32F4xx_HAL_TIM_H + +#include "stm32f4xx.h" +#include "stm32f4xx_hal_def.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* TIM Handle */ +typedef struct { + TIM_TypeDef *Instance; + struct { + uint32_t Prescaler; + uint32_t CounterMode; + uint32_t Period; + uint32_t ClockDivision; + uint32_t RepetitionCounter; + uint32_t AutoReloadPreload; + } Init; +} TIM_HandleTypeDef; + +/* Encoder Init (flat fields matching real HAL) */ +typedef struct { + uint32_t EncoderMode; + uint32_t IC1Polarity; + uint32_t IC1Selection; + uint32_t IC1Prescaler; + uint32_t IC1Filter; + uint32_t IC2Polarity; + uint32_t IC2Selection; + uint32_t IC2Prescaler; + uint32_t IC2Filter; +} TIM_Encoder_InitTypeDef; + +/* Master Config */ +typedef struct { + uint32_t MasterOutputTrigger; + uint32_t MasterSlaveMode; +} TIM_MasterConfigTypeDef; + +/* Timer constants */ +#define TIM_CHANNEL_ALL 0x0000U +#define TIM_COUNTERMODE_UP 0x0000U +#define TIM_CLOCKDIVISION_DIV1 0x0000U +#define TIM_AUTORELOAD_PRELOAD_DISABLE 0x0000U +#define TIM_ENCODERMODE_TI12 0x0003U +#define TIM_ICPOLARITY_RISING 0x0000U +#define TIM_ICSELECTION_DIRECTTI 0x0001U +#define TIM_ICPSC_DIV1 0x0000U +#define TIM_TRGO_ENABLE 0x0040U +#define TIM_TRGO_RESET 0x0000U +#define TIM_MASTERSLAVEMODE_DISABLE 0x0000U + +/* Counter access macro - the critical path for reading encoder values */ +#define __HAL_TIM_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNT) + +/* Function prototypes */ +HAL_StatusTypeDef HAL_TIM_Encoder_Init(TIM_HandleTypeDef *htim, TIM_Encoder_InitTypeDef *sConfig); +HAL_StatusTypeDef HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, uint32_t Channel); +HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim); +HAL_StatusTypeDef HAL_TIMEx_MasterConfigSynchronization(TIM_HandleTypeDef *htim, TIM_MasterConfigTypeDef *sMasterConfig); +void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim); +void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim); + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_TIM_H */ diff --git a/emulator/shim/stm32f4xx_hal_uart.h b/emulator/shim/stm32f4xx_hal_uart.h new file mode 100644 index 0000000..248d33f --- /dev/null +++ b/emulator/shim/stm32f4xx_hal_uart.h @@ -0,0 +1,70 @@ +/* + * HAL UART shim for emulator. + */ +#ifndef __STM32F4xx_HAL_UART_H +#define __STM32F4xx_HAL_UART_H + +#include "stm32f4xx.h" +#include "stm32f4xx_hal_def.h" +#include "stm32f4xx_hal_dma.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* UART State */ +#define HAL_UART_STATE_RESET 0x00000000U +#define HAL_UART_STATE_READY 0x00000020U +#define HAL_UART_STATE_BUSY 0x00000024U +#define HAL_UART_STATE_BUSY_TX 0x00000021U +#define HAL_UART_STATE_BUSY_RX 0x00000022U +#define HAL_UART_STATE_BUSY_TX_RX 0x00000023U + +/* UART Handle */ +typedef struct { + USART_TypeDef *Instance; + struct { + uint32_t BaudRate; + uint32_t WordLength; + uint32_t StopBits; + uint32_t Parity; + uint32_t Mode; + uint32_t HwFlowCtl; + uint32_t OverSampling; + } Init; + uint8_t *pTxBuffPtr; + uint16_t TxXferSize; + volatile uint16_t TxXferCount; + uint8_t *pRxBuffPtr; + uint16_t RxXferSize; + volatile uint16_t RxXferCount; + DMA_HandleTypeDef *hdmatx; + DMA_HandleTypeDef *hdmarx; + volatile uint32_t gState; + volatile uint32_t RxState; + volatile uint32_t ErrorCode; +} UART_HandleTypeDef; + +/* Function prototypes */ +HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); +HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size); +HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size); +HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); +HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); +HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart); +uint32_t HAL_UART_GetState(UART_HandleTypeDef *huart); +void HAL_UART_IRQHandler(UART_HandleTypeDef *huart); +HAL_StatusTypeDef HAL_HalfDuplex_EnableTransmitter(UART_HandleTypeDef *huart); +HAL_StatusTypeDef HAL_HalfDuplex_EnableReceiver(UART_HandleTypeDef *huart); + +/* Callbacks (implemented in firmware UARTCallback.c) */ +void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); +void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); +void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart); +void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size); + +#ifdef __cplusplus +} +#endif + +#endif /* __STM32F4xx_HAL_UART_H */ diff --git a/emulator/shim/task.h b/emulator/shim/task.h new file mode 100644 index 0000000..6aebab8 --- /dev/null +++ b/emulator/shim/task.h @@ -0,0 +1,40 @@ +/* + * FreeRTOS task API shim for emulator. + */ +#ifndef INC_TASK_H +#define INC_TASK_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Task handle is an opaque pointer to our shim's internal structure */ +typedef void *TaskHandle_t; + +/* Notify actions */ +typedef enum { + eNoAction = 0, + eSetBits, + eIncrement, + eSetValueWithOverwrite, + eSetValueWithoutOverwrite +} eNotifyAction; + +/* + * Task notification API. + * These are implemented in freertos_shim.cpp using condition variables. + */ +BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction); +BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken); +uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait); + +/* Get current task handle (returns the calling thread's task handle) */ +TaskHandle_t xTaskGetCurrentTaskHandle(void); + +#ifdef __cplusplus +} +#endif + +#endif /* INC_TASK_H */ diff --git a/emulator/shim/timers.h b/emulator/shim/timers.h new file mode 100644 index 0000000..0315b84 --- /dev/null +++ b/emulator/shim/timers.h @@ -0,0 +1,35 @@ +/* + * FreeRTOS software timer API shim for emulator. + */ +#ifndef INC_TIMERS_H +#define INC_TIMERS_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *TimerHandle_t; +typedef void *xTimerHandle; /* Legacy name used by Modbus library */ + +typedef void (*TimerCallbackFunction_t)(TimerHandle_t *pxTimer); + +TimerHandle_t xTimerCreate( + const char *pcTimerName, + TickType_t xTimerPeriodInTicks, + UBaseType_t uxAutoReload, + void *pvTimerID, + TimerCallbackFunction_t pxCallbackFunction +); + +BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xBlockTime); +BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xBlockTime); +BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xBlockTime); +BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken); + +#ifdef __cplusplus +} +#endif + +#endif /* INC_TIMERS_H */ diff --git a/emulator/src/config.cpp b/emulator/src/config.cpp new file mode 100644 index 0000000..fb27ed0 --- /dev/null +++ b/emulator/src/config.cpp @@ -0,0 +1,180 @@ +/* + * Simple TOML config parser. + * Only handles flat [section] with key = value lines. + * Values: strings ("..."), integers, floats, booleans (true/false). + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include + +EmuConfig::EmuConfig() { + imperial = false; + + spindle_counts_per_rev = 4000; + spindle_inertia = 0.05; + spindle_max_torque = 2.0; + spindle_friction = 0.1; + spindle_initial_rpm = 0.0; + + leadscrew_tpi = 8.0; + leadscrew_mm_per_step = 0.0025; + + z_encoder_counts_per_mm = 400.0; + z_backlash_mm = 0.02; + z_max_mm = 300.0; + z_min_mm = -5.0; + z_initial_mm = 0.0; + z_half_nut_engaged = false; + + x_encoder_counts_per_mm = 400.0; + x_max_mm = 100.0; + x_min_mm = -5.0; + x_initial_mm = 50.0; + x_manual_step_mm = 0.01; + jog_max_velocity_mm_s = 10.0; + jog_acceleration_mm_s2 = 50.0; + x_up_is_negative = true; + manual_move_timeout_s = 2.0; + + servo_max_speed = 720; + servo_acceleration = 120; + + modbus_address = 17; + modbus_baud = 115200; + + pty_enabled = true; + tcp_port = 5020; + tcp_enabled = true; + + refresh_hz = 10; + sparkline_seconds = 30; + log_max_lines = 200; + + isr_rate_hz = 10000; + realtime = true; +} + +static std::string trim(const std::string &s) { + auto start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); +} + +bool loadConfig(const std::string &path, EmuConfig &cfg) { + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "Config: cannot open " << path << ", using defaults\n"; + return false; + } + + std::string section; + std::string line; + /* section.key β†’ value string */ + std::map kv; + + while (std::getline(file, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') continue; + + if (line[0] == '[') { + auto end = line.find(']'); + if (end != std::string::npos) + section = line.substr(1, end - 1); + continue; + } + + auto eq = line.find('='); + if (eq == std::string::npos) continue; + + std::string key = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + + /* Strip inline comments */ + auto hashPos = val.find('#'); + if (hashPos != std::string::npos && (val[0] != '"')) { + val = trim(val.substr(0, hashPos)); + } + + /* Strip quotes from strings */ + if (val.size() >= 2 && val.front() == '"' && val.back() == '"') { + val = val.substr(1, val.size() - 2); + } + + kv[section + "." + key] = val; + } + + auto getBool = [&](const std::string &k, bool def) -> bool { + auto it = kv.find(k); + if (it == kv.end()) return def; + return it->second == "true" || it->second == "1"; + }; + auto getInt = [&](const std::string &k, int def) -> int { + auto it = kv.find(k); + if (it == kv.end()) return def; + try { return std::stoi(it->second); } catch (...) { return def; } + }; + auto getDouble = [&](const std::string &k, double def) -> double { + auto it = kv.find(k); + if (it == kv.end()) return def; + try { return std::stod(it->second); } catch (...) { return def; } + }; + auto getString = [&](const std::string &k, const std::string &def) -> std::string { + auto it = kv.find(k); + if (it == kv.end()) return def; + return it->second; + }; + + cfg.imperial = (getString("display.units", "metric") == "imperial"); + + cfg.spindle_counts_per_rev = getInt("spindle.counts_per_rev", cfg.spindle_counts_per_rev); + cfg.spindle_inertia = getDouble("spindle.inertia_kg_m2", cfg.spindle_inertia); + cfg.spindle_max_torque = getDouble("spindle.max_torque_nm", cfg.spindle_max_torque); + cfg.spindle_friction = getDouble("spindle.friction_nm", cfg.spindle_friction); + cfg.spindle_initial_rpm = getDouble("spindle.initial_rpm", cfg.spindle_initial_rpm); + + cfg.leadscrew_tpi = getDouble("leadscrew.tpi", cfg.leadscrew_tpi); + cfg.leadscrew_mm_per_step = getDouble("leadscrew.mm_per_step", cfg.leadscrew_mm_per_step); + + cfg.z_encoder_counts_per_mm = getDouble("z_axis.encoder_counts_per_mm", cfg.z_encoder_counts_per_mm); + cfg.z_backlash_mm = getDouble("z_axis.backlash_mm", cfg.z_backlash_mm); + cfg.z_max_mm = getDouble("z_axis.max_position_mm", cfg.z_max_mm); + cfg.z_min_mm = getDouble("z_axis.min_position_mm", cfg.z_min_mm); + cfg.z_initial_mm = getDouble("z_axis.initial_position_mm", cfg.z_initial_mm); + cfg.z_half_nut_engaged = getBool("z_axis.half_nut_engaged", cfg.z_half_nut_engaged); + + cfg.x_encoder_counts_per_mm = getDouble("cross_slide.encoder_counts_per_mm", cfg.x_encoder_counts_per_mm); + cfg.x_max_mm = getDouble("cross_slide.max_position_mm", cfg.x_max_mm); + cfg.x_min_mm = getDouble("cross_slide.min_position_mm", cfg.x_min_mm); + cfg.x_initial_mm = getDouble("cross_slide.initial_position_mm", cfg.x_initial_mm); + cfg.x_manual_step_mm = getDouble("cross_slide.manual_step_mm", cfg.x_manual_step_mm); + cfg.jog_max_velocity_mm_s = getDouble("cross_slide.jog_max_velocity_mm_s", cfg.jog_max_velocity_mm_s); + cfg.jog_acceleration_mm_s2 = getDouble("cross_slide.jog_acceleration_mm_s2", cfg.jog_acceleration_mm_s2); + cfg.x_up_is_negative = getBool("cross_slide.x_up_is_negative", cfg.x_up_is_negative); + cfg.manual_move_timeout_s = getDouble("cross_slide.manual_move_timeout_s", cfg.manual_move_timeout_s); + + cfg.servo_max_speed = getDouble("servo.max_speed", cfg.servo_max_speed); + cfg.servo_acceleration = getDouble("servo.acceleration", cfg.servo_acceleration); + + cfg.modbus_address = getInt("modbus.address", cfg.modbus_address); + cfg.modbus_baud = getInt("modbus.baud", cfg.modbus_baud); + + cfg.pty_enabled = getBool("transport.pty_enabled", cfg.pty_enabled); + cfg.tcp_port = getInt("transport.tcp_port", cfg.tcp_port); + cfg.tcp_enabled = getBool("transport.tcp_enabled", cfg.tcp_enabled); + + cfg.refresh_hz = getInt("dashboard.refresh_hz", cfg.refresh_hz); + cfg.sparkline_seconds = getInt("dashboard.sparkline_seconds", cfg.sparkline_seconds); + cfg.log_max_lines = getInt("dashboard.log_max_lines", cfg.log_max_lines); + + cfg.isr_rate_hz = getInt("simulation.isr_rate_hz", cfg.isr_rate_hz); + cfg.realtime = getBool("simulation.realtime", cfg.realtime); + + std::cout << "Config loaded from " << path << "\n"; + return true; +} diff --git a/emulator/src/config.h b/emulator/src/config.h new file mode 100644 index 0000000..bfdcae0 --- /dev/null +++ b/emulator/src/config.h @@ -0,0 +1,72 @@ +/* + * Emulator configuration loaded from TOML file. + */ +#ifndef EMU_CONFIG_H +#define EMU_CONFIG_H + +#include + +struct EmuConfig { + /* [display] */ + bool imperial; /* false=metric, true=imperial */ + + /* [spindle] */ + int spindle_counts_per_rev; + double spindle_inertia; + double spindle_max_torque; + double spindle_friction; + double spindle_initial_rpm; + + /* [leadscrew] */ + double leadscrew_tpi; + double leadscrew_mm_per_step; + + /* [z_axis] */ + double z_encoder_counts_per_mm; + double z_backlash_mm; + double z_max_mm; + double z_min_mm; + double z_initial_mm; + bool z_half_nut_engaged; + + /* [cross_slide] */ + double x_encoder_counts_per_mm; + double x_max_mm; + double x_min_mm; + double x_initial_mm; + double x_manual_step_mm; + double jog_max_velocity_mm_s; /* max manual jog speed (mm/s) */ + double jog_acceleration_mm_s2; /* manual jog acceleration (mm/s^2) */ + bool x_up_is_negative; /* true: Up arrow = -X (away from operator) */ + double manual_move_timeout_s; /* seconds of inactivity before auto-disabling manual move; 0 = no timeout (pure toggle) */ + + /* [servo] */ + double servo_max_speed; + double servo_acceleration; + + /* [modbus] */ + int modbus_address; + int modbus_baud; + + /* [transport] */ + bool pty_enabled; + int tcp_port; + bool tcp_enabled; + + /* [dashboard] */ + int refresh_hz; + int sparkline_seconds; + int log_max_lines; + + /* [simulation] */ + int isr_rate_hz; + bool realtime; + + /* Set defaults */ + EmuConfig(); +}; + +/* Load config from file. Returns true on success. */ +bool loadConfig(const std::string &path, EmuConfig &cfg); + +#endif /* EMU_CONFIG_H */ diff --git a/emulator/src/dashboard.cpp b/emulator/src/dashboard.cpp new file mode 100644 index 0000000..1d72d30 --- /dev/null +++ b/emulator/src/dashboard.cpp @@ -0,0 +1,522 @@ +/* + * Dashboard implementation: two-pane ANSI terminal with sparklines. + */ + +#include "dashboard.h" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "emulator_state.h" +} + +/* ANSI escape helpers */ +#define ESC "\033" +#define CSI ESC "[" +#define CLEAR_SCREEN CSI "2J" +#define CURSOR_HOME CSI "H" +#define HIDE_CURSOR CSI "?25l" +#define SHOW_CURSOR CSI "?25h" +#define WRAP_OFF CSI "?7l" +#define WRAP_ON CSI "?7h" +#define BOLD CSI "1m" +#define DIM CSI "2m" +#define RESET_ATTR CSI "0m" +#define FG_GREEN CSI "32m" +#define FG_YELLOW CSI "33m" +#define FG_CYAN CSI "36m" +#define FG_RED CSI "31m" +#define FG_WHITE CSI "37m" + +static void moveTo(int row, int col) { + printf(CSI "%d;%dH", row, col); +} + +/* Clear from cursor to end of line (preserves content left of cursor) */ +static void clearToEol() { + printf(CSI "0K"); +} + +/* Unicode block characters for sparklines (8 levels) */ +static const char *spark_chars[] = { + "\u2581", "\u2582", "\u2583", "\u2584", + "\u2585", "\u2586", "\u2587", "\u2588" +}; + +std::string Dashboard::SparklineBuffer::render() const { + if (samples.empty()) return std::string(width, ' '); + + /* Take the last `width` samples (or fewer if not enough) */ + int n = std::min((int)samples.size(), width); + int start = (int)samples.size() - n; + + /* Find min/max for auto-scaling */ + double mn = samples[start], mx = samples[start]; + for (int i = start; i < (int)samples.size(); i++) { + mn = std::min(mn, samples[i]); + mx = std::max(mx, samples[i]); + } + if (mx - mn < 0.001) mx = mn + 1.0; + + std::string result; + /* Pad with spaces if fewer samples than width */ + for (int i = 0; i < width - n; i++) result += " "; + + for (int i = start; i < (int)samples.size(); i++) { + int level = (int)((samples[i] - mn) / (mx - mn) * 7.0); + level = std::max(0, std::min(7, level)); + result += spark_chars[level]; + } + return result; +} + +Dashboard::Dashboard(const EmuConfig &cfg, LathePhysics &physics, Transport &transport, + rampsSharedData_t &shared) + : cfg(cfg), physics(physics), transport(transport), shared(shared), + running(false), manual_move(false), manual_move_timer(0.0), manual_move_used(false), + spark_rpm(cfg.sparkline_seconds, cfg.refresh_hz, 30), + spark_zpos(cfg.sparkline_seconds, cfg.refresh_hz, 30), + spark_zerr(cfg.sparkline_seconds, cfg.refresh_hz, 30) +{ +} + +double Dashboard::toDisplay(double mm) const { + return cfg.imperial ? mm / 25.4 : mm; +} + +const char* Dashboard::unitSuffix() const { + return cfg.imperial ? "in" : "mm"; +} + +int Dashboard::unitPrecision() const { + return cfg.imperial ? 4 : 3; +} + +void Dashboard::run() { + running.store(true); + + /* Put terminal in raw mode */ + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); + new_tio = old_tio; + new_tio.c_lflag &= ~(ICANON | ECHO); + new_tio.c_cc[VMIN] = 0; + new_tio.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + printf(HIDE_CURSOR); + printf(WRAP_OFF); + printf(CLEAR_SCREEN); + + int interval_us = 1000000 / cfg.refresh_hz; + + while (running.load()) { + /* Update sparklines */ + spark_rpm.push(std::abs(physics.getSpindleRPM())); + spark_zpos.push(physics.getCarriageMM()); + double err = (double)(int32_t)(shared.servo.desiredSteps - shared.servo.currentSteps); + spark_zerr.push(std::abs(err)); + + draw(); + handleInput(); + usleep(interval_us); + } + + printf(WRAP_ON); + printf(SHOW_CURSOR); + printf(CLEAR_SCREEN); + printf(CURSOR_HOME); + + /* Restore terminal */ + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); +} + +void Dashboard::draw() { + /* Get terminal size */ + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + int term_w = ws.ws_col > 0 ? ws.ws_col : 100; + int term_h = ws.ws_row > 0 ? ws.ws_row : 30; + + int left_w = 40; + int right_w = term_w - left_w - 3; + if (right_w < 20) right_w = 20; + + printf(CURSOR_HOME); + + drawStatePane(1, 1, left_w); + drawLogPane(1, left_w + 2, right_w, term_h - 2); + drawStatusBar(term_h, term_w); + + fflush(stdout); +} + +void Dashboard::drawStatePane(int startRow, int startCol, int width) { + int row = startRow; + /* Helper macro for printing a line at the current row. + * Can't use variadic lambda with va_start, so use a macro. */ + #define LINE(...) do { moveTo(row++, startCol); clearToEol(); printf(__VA_ARGS__); } while(0) + + double rpm = physics.getSpindleRPM(); + double target = physics.getTargetRPM(); + const char *dir = physics.getSpindleCW() ? "CW" : "CCW"; + const char *arrow = std::abs(rpm) > 0.1 ? "\xe2\x96\xb6" : " "; + + LINE(BOLD " SPINDLE " RESET_ATTR FG_GREEN "%.0f RPM" RESET_ATTR " %s %s [target: %.0f]", + std::abs(rpm), arrow, dir, std::abs(target)); + + LINE(" RPM %s", spark_rpm.render().c_str()); + + /* Use the firmware's accumulated encoder position (what Modbus clients see). + * Note: the "phase" display aliases at low dashboard refresh rates β€” at + * 600 RPM / 4000 CPR / 10 Hz refresh, each frame spans exactly one rev + * so the phase appears frozen. This is real sampling aliasing, not a bug. */ + int32_t enc = shared.scales[0].position; + int phase = 0; + if (cfg.spindle_counts_per_rev > 0) { + phase = (int)(enc % cfg.spindle_counts_per_rev); + if (phase < 0) phase += cfg.spindle_counts_per_rev; + } + LINE(" encoder: %d phase: %d/%d", enc, phase, cfg.spindle_counts_per_rev); + + LINE("%s", ""); + + double z = toDisplay(physics.getCarriageMM()); + LINE(BOLD " Z-AXIS" RESET_ATTR " %.*f %s steps: %u", + unitPrecision(), z, unitSuffix(), shared.servo.currentSteps); + LINE(" pos %s", spark_zpos.render().c_str()); + LINE(" " FG_CYAN "\xce\x94" "err" RESET_ATTR " %s", spark_zerr.render().c_str()); + + const char *hn_str = "DISENGAGED"; + if (physics.getHalfNutState() == LathePhysics::ENGAGED) + hn_str = "ENGAGED"; + else if (physics.getHalfNutState() == LathePhysics::ENGAGING) + hn_str = FG_YELLOW "ENGAGING..." RESET_ATTR; + + LINE(" half-nut: %s backlash: %.*f", hn_str, unitPrecision(), toDisplay(0.0)); + + const char *mode_str = "OFF"; + if (shared.fastData.servoMode == 1) mode_str = "SYNC"; + else if (shared.fastData.servoMode == 2) mode_str = "JOG"; + + LINE(" mode: %s speed: %.0f stp/s", mode_str, shared.servo.currentSpeed); + + const char *dir_str = (emu_hw.dir_pin) ? "FWD" : "REV"; + const char *ena_str = (emu_hw.ena_pin == 0) ? "ON" : "OFF"; + LINE(" dir: %s enable: %s stepsToGo: %d", dir_str, ena_str, shared.servo.stepsToGo); + + LINE("%s", ""); + + double xpos = toDisplay(physics.getCrossSlideMM()); + LINE(BOLD " CROSS-SLIDE" RESET_ATTR " %.*f %s", unitPrecision(), xpos, unitSuffix()); + + LINE("%s", ""); + + /* Threading section */ + LINE(BOLD " THREADING" RESET_ATTR); + LINE(" remaining: %d traveled: --", (int)0); + LINE(" phase ref: -- cur: -- tol: --"); + + LINE("%s", ""); + + /* Scales */ + LINE(BOLD " SCALES" RESET_ATTR); + const char *scale_names[] = { "spindle", "z-axis ", "x-slide", "spare " }; + for (int i = 0; i < 4; i++) { + const char *en = shared.scales[i].syncEnable ? FG_GREEN "ON " RESET_ATTR : DIM "off" RESET_ATTR; + LINE(" [%d] %s %d/%d %s pos: %d", + i, scale_names[i], + shared.scales[i].syncRatioNum, shared.scales[i].syncRatioDen, + en, shared.scales[i].position); + } + + LINE("%s", ""); + + LINE(" MODBUS rx: %u tx: %u err: %u", + (unsigned)shared.fastData.cycles, + 0u, 0u); + LINE(" %.1f" "\xc2\xb5" "s avg tick: %uk", + (double)shared.executionCycles / 100.0, + (unsigned)(emu_hw.dwt_cyccnt / 1000)); + + std::string pty_str = transport.getPtyPath().empty() ? "none" : transport.getPtyPath(); + LINE(" PTY: %s TCP:%d: %d client", + pty_str.c_str(), cfg.tcp_port, transport.getTcpClientCount()); + + #undef LINE +} + +void Dashboard::drawLogPane(int startRow, int startCol, int width, int height) { + /* Header */ + moveTo(startRow, startCol); + printf(BOLD DIM "EVENT LOG" RESET_ATTR); + + int maxLines = height - 2; + int count = emu_hw.event_log_count; + int start = 0; + if (count > maxLines) start = count - maxLines; + + for (int i = 0; i < maxLines; i++) { + moveTo(startRow + 1 + i, startCol); + clearToEol(); + int idx = start + i; + if (idx < count) { + int ring_idx = (emu_hw.event_log_head - count + idx) % 256; + if (ring_idx < 0) ring_idx += 256; + printf(DIM "%.*s" RESET_ATTR, width, emu_hw.event_log[ring_idx]); + } + } +} + +void Dashboard::drawStatusBar(int row, int width) { + moveTo(row, 1); + printf(CSI "2K"); /* Clear entire line β€” status bar owns the full row */ + + /* Auto-disable manual move after timeout (if configured) */ + if (manual_move && manual_move_used) { + manual_move_timer += 1.0 / cfg.refresh_hz; + double timeout = cfg.manual_move_timeout_s; + if (timeout > 0.0 && manual_move_timer > timeout + && !physics.isZJogging() && !physics.isXJogging() + && !physics.isZMoveTargetActive() && !physics.isXMoveTargetActive()) { + manual_move = false; + manual_move_used = false; + emu_log_event("manual move disabled (timeout)"); + } + } + + const char *move_indicator = ""; + if (manual_move && (physics.isZJogging() || physics.isXJogging())) + move_indicator = FG_CYAN "[MANUAL active]" RESET_ATTR " "; + else if (manual_move) + move_indicator = FG_YELLOW "[MANUAL]" RESET_ATTR " "; + + const char *start_stop = (std::abs(physics.getTargetRPM()) > 0.1) ? "s[T]op" : "s[T]art"; + + /* Show Z/X position entry keys only when manual move is enabled */ + const char *zx_keys = ""; + if (manual_move) { + bool z_allowed = (physics.getHalfNutState() != LathePhysics::ENGAGED); + zx_keys = z_allowed ? " [Z]pos [X]pos arrows" : " [X]pos arrows"; + } + + printf("%s" DIM "[S]pindle RPM [D]ir [H]alf-nut [M]anual%s [E]-stop %s [Q]uit" RESET_ATTR, + move_indicator, zx_keys, start_stop); +} + +void Dashboard::handleInput() { + /* Drain all available input to avoid backlog from key repeat. + * Deduplicate: for toggle actions (H, T, D) only process the first + * occurrence in the buffer to avoid key-repeat undoing the action. */ + char buf[64]; + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n <= 0) return; + + bool had_h = false, had_t = false, had_d = false, had_e = false, had_m = false; + int last_arrow_z = 0; + int last_arrow_x = 0; + + for (ssize_t i = 0; i < n; i++) { + char c = buf[i]; + + if (c == 27 && i + 2 < n && buf[i+1] == '[') { + char arrow = buf[i+2]; + i += 2; + + if (manual_move) { + /* Left/Right = Z-axis, Up/Down = X-axis */ + int x_sign = cfg.x_up_is_negative ? -1 : 1; + if (arrow == 'C') last_arrow_z = +1; + else if (arrow == 'D') last_arrow_z = -1; + else if (arrow == 'A') last_arrow_x = x_sign; /* Up */ + else if (arrow == 'B') last_arrow_x = -x_sign; /* Down */ + manual_move_timer = 0.0; + manual_move_used = true; + } + continue; + } + + switch (c) { + case 'q': case 'Q': + running.store(false); + return; + + case 's': case 'S': + promptSpindleRPM(); + return; + + case 'd': case 'D': + if (!had_d) { had_d = true; physics.toggleDirection(); emu_log_event("spindle direction toggled"); } + break; + + case 't': case 'T': + if (!had_t) { + had_t = true; + if (std::abs(physics.getTargetRPM()) < 0.1) { + physics.setTargetRPM(500.0); + emu_log_event("spindle target -> 500 RPM"); + } else { + physics.setTargetRPM(0.0); + emu_log_event("spindle target -> 0 RPM"); + } + } + break; + + case 'h': case 'H': + if (!had_h) { had_h = true; physics.requestHalfNutToggle(); } + break; + + case 'e': case 'E': + if (!had_e) { had_e = true; physics.emergencyStop(); } + break; + + case 'm': case 'M': + if (!had_m) { + had_m = true; + manual_move = !manual_move; + manual_move_timer = 0.0; + manual_move_used = false; + emu_log_event("manual move %s", manual_move ? "enabled" : "disabled"); + } + break; + + case 'z': case 'Z': + if (manual_move && physics.getHalfNutState() != LathePhysics::ENGAGED) { + promptZPosition(); + return; + } + break; + + case 'x': case 'X': + if (manual_move) { + promptXPosition(); + return; + } + break; + + default: + break; + } + } + + /* Apply jog: one call per axis with the last direction seen */ + if (last_arrow_z != 0) physics.jogCarriage(last_arrow_z); + if (last_arrow_x != 0) physics.jogCrossSlide(last_arrow_x); +} + +void Dashboard::promptSpindleRPM() { + /* Temporarily restore terminal for line input */ + printf(SHOW_CURSOR); + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + int row = ws.ws_row > 0 ? ws.ws_row : 30; + moveTo(row, 1); + clearToEol(); + printf("Enter spindle RPM: "); + fflush(stdout); + + /* Switch to line mode briefly */ + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); + new_tio = old_tio; + new_tio.c_lflag |= ICANON | ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + char buf[32]; + if (fgets(buf, sizeof(buf), stdin)) { + double rpm = atof(buf); + physics.setTargetRPM(rpm); + emu_log_event("spindle target -> %.0f RPM", rpm); + } + + /* Back to raw mode */ + new_tio.c_lflag &= ~(ICANON | ECHO); + new_tio.c_cc[VMIN] = 0; + new_tio.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + printf(HIDE_CURSOR); + + /* Force immediate full redraw to clear the prompt. + * CLEAR_SCREEN needed because Enter may have scrolled the terminal. */ + printf(CLEAR_SCREEN); + draw(); +} + +void Dashboard::promptZPosition() { + printf(SHOW_CURSOR); + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + int row = ws.ws_row > 0 ? ws.ws_row : 30; + moveTo(row, 1); + clearToEol(); + printf("Enter Z position (%s), current %.3f: ", + unitSuffix(), toDisplay(physics.getCarriageMM())); + fflush(stdout); + + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); + new_tio = old_tio; + new_tio.c_lflag |= ICANON | ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + char buf[32]; + if (fgets(buf, sizeof(buf), stdin) && buf[0] != '\n') { + double pos = atof(buf); + /* Convert from display units to mm */ + double pos_mm = cfg.imperial ? pos * 25.4 : pos; + physics.moveCarriageTo(pos_mm); + emu_log_event("Z move to %.3f %s", pos, unitSuffix()); + manual_move_timer = 0.0; + manual_move_used = true; + } + + new_tio.c_lflag &= ~(ICANON | ECHO); + new_tio.c_cc[VMIN] = 0; + new_tio.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + printf(HIDE_CURSOR); + /* Clear the prompt area (Enter may have scrolled the terminal) */ + printf(CLEAR_SCREEN); + draw(); +} + +void Dashboard::promptXPosition() { + printf(SHOW_CURSOR); + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + int row = ws.ws_row > 0 ? ws.ws_row : 30; + moveTo(row, 1); + clearToEol(); + printf("Enter X position (%s), current %.3f: ", + unitSuffix(), toDisplay(physics.getCrossSlideMM())); + fflush(stdout); + + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); + new_tio = old_tio; + new_tio.c_lflag |= ICANON | ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + char buf[32]; + if (fgets(buf, sizeof(buf), stdin) && buf[0] != '\n') { + double pos = atof(buf); + double pos_mm = cfg.imperial ? pos * 25.4 : pos; + physics.moveCrossSlideTo(pos_mm); + emu_log_event("X move to %.3f %s", pos, unitSuffix()); + manual_move_timer = 0.0; + manual_move_used = true; + } + + new_tio.c_lflag &= ~(ICANON | ECHO); + new_tio.c_cc[VMIN] = 0; + new_tio.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + printf(HIDE_CURSOR); + printf(CLEAR_SCREEN); + draw(); +} diff --git a/emulator/src/dashboard.h b/emulator/src/dashboard.h new file mode 100644 index 0000000..b76012a --- /dev/null +++ b/emulator/src/dashboard.h @@ -0,0 +1,81 @@ +/* + * Two-pane ANSI terminal dashboard with sparklines. + */ +#ifndef EMU_DASHBOARD_H +#define EMU_DASHBOARD_H + +#include "config.h" +#include "physics.h" +#include "transport.h" +#include +#include +#include + +extern "C" { +#include "Ramps.h" +} + +class Dashboard { +public: + Dashboard(const EmuConfig &cfg, LathePhysics &physics, Transport &transport, + rampsSharedData_t &shared); + + /* Run the dashboard loop (blocking, runs on main thread). + * Returns when user presses Q. */ + void run(); + + /* Signal the dashboard to stop. */ + void requestStop() { running.store(false); } + +private: + const EmuConfig &cfg; + LathePhysics &physics; + Transport &transport; + rampsSharedData_t &shared; + + std::atomic running; + bool manual_move; /* true when manual move is active */ + double manual_move_timer; /* seconds since last arrow input */ + bool manual_move_used; /* true once user has made at least one move */ + + /* Sparkline history ring buffers */ + struct SparklineBuffer { + std::deque samples; + int max_samples; + int width; /* character width for rendering */ + + SparklineBuffer(int seconds, int hz, int w) + : max_samples(seconds * hz), width(w) {} + + void push(double val) { + samples.push_back(val); + while ((int)samples.size() > max_samples) + samples.pop_front(); + } + + std::string render() const; + }; + + SparklineBuffer spark_rpm; + SparklineBuffer spark_zpos; + SparklineBuffer spark_zerr; + + /* Unit conversion */ + double toDisplay(double mm) const; + const char* unitSuffix() const; + int unitPrecision() const; + + /* Rendering */ + void draw(); + void drawStatePane(int startRow, int startCol, int width); + void drawLogPane(int startRow, int startCol, int width, int height); + void drawStatusBar(int row, int width); + + /* Input */ + void handleInput(); + void promptSpindleRPM(); + void promptZPosition(); + void promptXPosition(); +}; + +#endif /* EMU_DASHBOARD_H */ diff --git a/emulator/src/emulator_state.h b/emulator/src/emulator_state.h new file mode 100644 index 0000000..5b03f65 --- /dev/null +++ b/emulator/src/emulator_state.h @@ -0,0 +1,83 @@ +/* + * Emulator shared state. + * Global state accessible from shim, physics, transport, and dashboard. + */ +#ifndef EMULATOR_STATE_H +#define EMULATOR_STATE_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Forward declaration */ +struct EmulatorState; + +/* + * GPIO event callback type. + * Called from HAL_GPIO_WritePin shim whenever a pin changes state. + */ +typedef void (*gpio_write_cb_t)(void *GPIOx, uint16_t pin, int state); + +/* + * UART TX callback type. + * Called when the firmware writes bytes via HAL_UART_Transmit_IT. + */ +typedef void (*uart_tx_cb_t)(const uint8_t *data, uint16_t size); + +/* + * Timer counter values written by the physics model, + * read by the firmware via __HAL_TIM_GET_COUNTER. + * Index 0-3 corresponds to TIM1-TIM4 (scales 0-3). + */ +typedef struct { + /* Physics β†’ firmware: virtual encoder counter values */ + volatile uint32_t scale_counters[4]; + + /* Firmware β†’ physics: GPIO pin states captured from step/dir/ena */ + volatile int step_pin; /* PA0 rising edge = step pulse */ + volatile int dir_pin; /* PB14: 1=forward, 0=reverse */ + volatile int ena_pin; /* PB15: 0=enabled (active low) */ + volatile int spare2_pin; /* PA3 */ + + /* UART transport hooks */ + gpio_write_cb_t on_gpio_write; + uart_tx_cb_t on_uart_tx; + void *user_data; + + /* UART RX: transport calls this to inject a byte into the firmware */ + /* (we call HAL_UART_RxCpltCallback from the shim) */ + volatile int uart_rx_armed; /* 1 = firmware has called HAL_UART_Receive_IT */ + uint8_t *uart_rx_buf; /* pointer to firmware's dataRX byte */ + uint16_t uart_rx_size; + + /* Monotonic tick counter for HAL_GetTick */ + volatile uint32_t hal_tick; + + /* DWT cycle counter (incremented by ISR thread to simulate CPU cycles) */ + volatile uint32_t dwt_cyccnt; + + /* Mutex for shared data access */ + pthread_mutex_t mutex; + + /* Event log ring buffer for dashboard */ + char event_log[256][128]; + volatile int event_log_head; + volatile int event_log_count; + +} EmulatorHardwareState; + +/* Global instance */ +extern EmulatorHardwareState emu_hw; + +/* Add an event to the log (thread-safe) */ +void emu_log_event(const char *fmt, ...); + +#ifdef __cplusplus +} +#endif + +#endif /* EMULATOR_STATE_H */ diff --git a/emulator/src/main.cpp b/emulator/src/main.cpp new file mode 100644 index 0000000..72474b0 --- /dev/null +++ b/emulator/src/main.cpp @@ -0,0 +1,189 @@ +/* + * Lathe Emulator - Main entry point. + * + * Initializes the firmware data structures, starts the physics model, + * transport layer, and ISR thread, then runs the dashboard. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "physics.h" +#include "transport.h" +#include "dashboard.h" + +extern "C" { +#include "emulator_state.h" +#include "stm32f4xx_hal.h" +#include "Ramps.h" + +/* Firmware externs */ +extern TIM_HandleTypeDef htim1, htim2, htim3, htim4, htim9, htim11; +extern UART_HandleTypeDef huart1; +extern void emu_update_timer_counters(void); +} + +static std::atomic g_running{true}; + +static void signalHandler(int sig) { + (void)sig; + g_running.store(false); +} + +/* + * ISR thread: ticks the physics model and calls the firmware's + * SynchroRefreshTimerIsr at the configured rate. + */ +static void isrThreadFunc(LathePhysics *physics, rampsHandler_t *rampsData, int rate_hz) { + auto interval = std::chrono::microseconds(1000000 / rate_hz); + auto next_tick = std::chrono::steady_clock::now(); + + /* Simulate the DWT cycle counter. At "100 MHz", each ISR tick at 10kHz = 10000 cycles. */ + uint32_t cycles_per_tick = 100000000U / (uint32_t)rate_hz; + + /* Track previous step pin state for edge detection */ + int prev_step_pin = 0; + + while (g_running.load()) { + next_tick += interval; + + /* Advance DWT cycle counter to simulate the inter-ISR interval */ + emu_hw.dwt_cyccnt += cycles_per_tick; + emu_dwt.CYCCNT = emu_hw.dwt_cyccnt; + + /* Advance physics */ + double dt = 1.0 / (double)rate_hz; + physics->tick(dt, &rampsData->shared); + + /* Call firmware ISR. + * The ISR reads DWT->CYCCNT at start and end to measure execution + * time, but since emu_dwt.CYCCNT is a plain variable both reads + * return the same value β†’ executionCycles=0. We measure the real + * wall-clock execution time and patch the result after. */ + SynchroRefreshTimerIsr(rampsData); + + /* Overwrite executionCycles with a realistic estimate. + * The real ISR takes ~5-10Β΅s on the MCU = 500-1000 cycles at 100MHz. + * Use a fixed reasonable value since we can't measure sub-Β΅s on host. */ + rampsData->shared.executionCycles = 600; + + /* Detect STEP rising edge and feed into physics */ + if (emu_hw.step_pin && !prev_step_pin) { + int dir = emu_hw.dir_pin ? 1 : -1; + physics->onStepPulse(dir); + } + prev_step_pin = emu_hw.step_pin; + + /* Increment HAL tick (~1ms per tick, approximate) */ + static int tick_divider = 0; + tick_divider++; + if (tick_divider >= rate_hz / 1000) { + tick_divider = 0; + HAL_IncTick(); + } + + /* Sleep until next tick */ + std::this_thread::sleep_until(next_tick); + } +} + +int main(int argc, char *argv[]) { + printf("=== Lathe Emulator ===\n"); + + /* Load configuration */ + EmuConfig cfg; + std::string config_path = "config/lathe.toml"; + if (argc > 1) config_path = argv[1]; + loadConfig(config_path, cfg); + + /* Initialize emulator hardware state */ + memset(&emu_hw, 0, sizeof(emu_hw)); + pthread_mutex_init(&emu_hw.mutex, nullptr); + + /* Set TC flag so Modbus sendTxBuffer busy-wait completes immediately */ + extern USART_TypeDef emu_usart1; + emu_usart1.SR = USART_SR_TC; + + /* Initialize physics model */ + LathePhysics physics(cfg); + + /* Initialize the firmware's rampsHandler_t struct */ + static rampsHandler_t rampsData; + memset(&rampsData, 0, sizeof(rampsData)); + + /* Wire timer handles to scales (same mapping as firmware's main.c) */ + extern TIM_HandleTypeDef htim1, htim2, htim3, htim4; + rampsData.shared.scales[0].timerHandle = &htim1; + rampsData.shared.scales[1].timerHandle = &htim2; + rampsData.shared.scales[2].timerHandle = &htim3; + rampsData.shared.scales[3].timerHandle = &htim4; + + /* Set synchro timer and UART handles */ + rampsData.synchroRefreshTimer = &htim9; + rampsData.modbusUart = &huart1; + + /* Apply config defaults to servo */ + rampsData.shared.servo.maxSpeed = (float)cfg.servo_max_speed; + rampsData.shared.servo.acceleration = (float)cfg.servo_acceleration; + + /* Default sync ratios */ + for (int i = 0; i < SCALES_COUNT; i++) { + rampsData.shared.scales[i].syncRatioNum = 1; + rampsData.shared.scales[i].syncRatioDen = 100; + } + + /* Initialize transport */ + Transport transport(cfg); + transport.start(); + + /* Initialize the firmware (Modbus, tasks, etc.) */ + printf("Starting firmware initialization...\n"); + RampsStart(&rampsData); + printf("Firmware initialized.\n"); + + /* Initialize servoCycles to avoid division by zero on first ISR tick. + * On real hardware updateSpeedTask() sets this within 50ms of boot, + * but the emulator's ISR thread starts immediately. + * Formula from updateSpeedTask: clock_freq / maxSpeed + * clock_freq = 100MHz / (Prescaler+1) / (Period+1) = 100000 + */ + { + extern uint16_t servoCycles; + float clock_freq = 100000000.0f / + ((float)rampsData.synchroRefreshTimer->Init.Prescaler + 1) / + (float)(rampsData.synchroRefreshTimer->Init.Period + 1); + float period = (cfg.servo_max_speed > 0) ? floorf(clock_freq / (float)cfg.servo_max_speed) : 138.0f; + if (period < 1.0f) period = 1.0f; + if (period > 65535.0f) period = 65535.0f; + servoCycles = (uint16_t)period; + } + + /* Install signal handler */ + signal(SIGINT, signalHandler); + + /* Start ISR thread */ + std::thread isrThread(isrThreadFunc, &physics, &rampsData, cfg.isr_rate_hz); + + /* Run dashboard on main thread */ + Dashboard dashboard(cfg, physics, transport, rampsData.shared); + + emu_log_event("Emulator started"); + emu_log_event("PTY: %s", transport.getPtyPath().c_str()); + if (cfg.tcp_enabled) emu_log_event("TCP: port %d", cfg.tcp_port); + + dashboard.run(); + + /* Shutdown */ + g_running.store(false); + if (isrThread.joinable()) isrThread.join(); + transport.stop(); + + printf("Emulator stopped.\n"); + return 0; +} diff --git a/emulator/src/physics.cpp b/emulator/src/physics.cpp new file mode 100644 index 0000000..acf4047 --- /dev/null +++ b/emulator/src/physics.cpp @@ -0,0 +1,355 @@ +/* + * Lathe physics model implementation. + */ + +#include "physics.h" +#include +#include +#include + +extern "C" { +#include "emulator_state.h" +#include "Ramps.h" +void emu_update_timer_counters(void); +} + +LathePhysics::LathePhysics(const EmuConfig &cfg) { + spindle_inertia = cfg.spindle_inertia; + spindle_max_torque = cfg.spindle_max_torque; + spindle_friction = cfg.spindle_friction; + spindle_counts_per_rev = cfg.spindle_counts_per_rev; + leadscrew_mm_per_step = cfg.leadscrew_mm_per_step; + leadscrew_tpi = cfg.leadscrew_tpi; + leadscrew_grid_spacing_mm = 25.4 / leadscrew_tpi; + z_counts_per_mm = cfg.z_encoder_counts_per_mm; + z_backlash_mm = cfg.z_backlash_mm; + z_max_mm = cfg.z_max_mm; + z_min_mm = cfg.z_min_mm; + x_counts_per_mm = cfg.x_encoder_counts_per_mm; + x_max_mm = cfg.x_max_mm; + x_min_mm = cfg.x_min_mm; + x_manual_step_mm = cfg.x_manual_step_mm; + + spindle_theta = 0.0; + spindle_omega = cfg.spindle_initial_rpm * 2.0 * M_PI / 60.0; + spindle_target_rpm = cfg.spindle_initial_rpm; + + leadscrew_position_mm = 0.0; + leadscrew_total_steps = 0; + + carriage_mm = cfg.z_initial_mm; + half_nut_state = cfg.z_half_nut_engaged ? ENGAGED : DISENGAGED; + half_nut_request_pending = false; + backlash_remaining = 0.0; + last_carriage_dir = 1; + + cross_slide_mm = cfg.x_initial_mm; + + z_jog_velocity = 0.0; + z_jog_target_dir = 0.0; + z_move_active = false; + z_move_target = 0.0; + x_jog_velocity = 0.0; + x_jog_target_dir = 0.0; + x_move_active = false; + x_move_target = 0.0; + jog_max_velocity = cfg.jog_max_velocity_mm_s; + jog_acceleration = cfg.jog_acceleration_mm_s2; + z_jog_idle_timer = JOG_KEY_TIMEOUT + 1.0; + x_jog_idle_timer = JOG_KEY_TIMEOUT + 1.0; +} + +void LathePhysics::tick(double dt, const void *shared_data) { + const rampsSharedData_t *shared = (const rampsSharedData_t *)shared_data; + /* --- Spindle dynamics --- */ + double target_omega = spindle_target_rpm * 2.0 * M_PI / 60.0; + double error = target_omega - spindle_omega; + + /* Proportional + bang-bang torque controller. + * Far from target: full torque (fast ramp). + * Near target: proportional torque to settle smoothly. + * At target: compensate friction exactly (no oscillation). */ + double torque_motor = 0.0; + double omega_threshold = spindle_max_torque / spindle_inertia * 0.5; /* ~20 rad/s transition band */ + + if (std::abs(error) < 0.001) { + /* At steady state: exactly counteract friction to hold speed */ + if (std::abs(spindle_omega) > 0.001) + torque_motor = (spindle_omega > 0) ? spindle_friction : -spindle_friction; + } else if (std::abs(error) < omega_threshold) { + /* Near target: proportional control + friction feedforward */ + double kp = spindle_max_torque / omega_threshold; + torque_motor = kp * error; + if (std::abs(target_omega) > 0.001) + torque_motor += (target_omega > 0) ? spindle_friction : -spindle_friction; + } else { + /* Far from target: full torque */ + torque_motor = (error > 0) ? spindle_max_torque : -spindle_max_torque; + } + + /* Friction opposes motion */ + double torque_friction = 0.0; + if (std::abs(spindle_omega) > 0.001) { + torque_friction = (spindle_omega > 0) ? -spindle_friction : spindle_friction; + } + + double alpha = (torque_motor + torque_friction) / spindle_inertia; + spindle_omega += alpha * dt; + + /* Clamp to target if we've effectively arrived */ + if (std::abs(target_omega - spindle_omega) < 0.001 && std::abs(target_omega) > 0.001) { + spindle_omega = target_omega; + } + + /* Stop fully if target is zero and speed is very low */ + if (std::abs(spindle_target_rpm) < 0.01 && std::abs(spindle_omega) < 0.1) { + spindle_omega = 0.0; + } + + spindle_theta += spindle_omega * dt; + + /* --- Half-nut engagement logic --- */ + if (half_nut_request_pending) { + if (half_nut_state == DISENGAGED || half_nut_state == ENGAGING) { + /* Request to engage. + * The leadscrew moves only when the firmware's servo is producing + * steps (sync enabled + spindle turning). Check actual servo speed. */ + bool leadscrew_moving = shared && std::abs(shared->servo.currentSpeed) > 0.1; + + if (!leadscrew_moving) { + /* Leadscrew stationary: snap carriage to nearest grid point and engage */ + snapCarriageToGrid(); + half_nut_state = ENGAGED; + half_nut_request_pending = false; + emu_log_event("half-nut ENGAGED (snap to %.3f mm)", carriage_mm); + } else { + /* Leadscrew turning: wait for phase alignment */ + if (half_nut_state != ENGAGING) { + half_nut_state = ENGAGING; + emu_log_event("half-nut ENGAGING..."); + } + if (checkPhaseAlignment()) { + half_nut_state = ENGAGED; + half_nut_request_pending = false; + emu_log_event("half-nut ENGAGED (phase match)"); + } + } + } else { + /* Currently ENGAGED: request to disengage */ + half_nut_state = DISENGAGED; + half_nut_request_pending = false; + emu_log_event("half-nut DISENGAGED"); + } + } + + /* --- Manual move integration --- */ + + /* Helper: compute jog direction for a move-to-position target. + * Returns +1/-1 while moving, 0 when arrived (with deceleration). */ + auto moveToDir = [&](double current, double target, double velocity) -> double { + double error = target - current; + double stop_dist = (velocity * velocity) / (2.0 * jog_acceleration); + if (std::abs(error) < 0.001 && std::abs(velocity) < 0.01) + return 0.0; /* arrived */ + if (std::abs(error) <= stop_dist + 0.001) + return 0.0; /* time to decelerate */ + return (error > 0) ? 1.0 : -1.0; + }; + + /* Z-axis (only when half-nut disengaged) */ + z_jog_idle_timer += dt; + if (z_jog_idle_timer > JOG_KEY_TIMEOUT) z_jog_target_dir = 0.0; + + if (half_nut_state != ENGAGED) { + /* Move-to-position overrides arrow key jog */ + double z_dir = z_jog_target_dir; + if (z_move_active) { + z_dir = moveToDir(carriage_mm, z_move_target, z_jog_velocity); + if (z_dir == 0.0 && std::abs(z_jog_velocity) < 0.01) { + z_move_active = false; + carriage_mm = z_move_target; /* snap to exact target */ + z_jog_velocity = 0.0; + emu_log_event("Z move complete: %.3f mm", carriage_mm); + } + } + + double z_target_vel = z_dir * jog_max_velocity; + double z_vel_error = z_target_vel - z_jog_velocity; + if (std::abs(z_vel_error) > 0.001) { + double accel = (z_vel_error > 0) ? jog_acceleration : -jog_acceleration; + z_jog_velocity += accel * dt; + double new_error = z_target_vel - z_jog_velocity; + if (z_vel_error * new_error < 0) z_jog_velocity = z_target_vel; + } else { + z_jog_velocity = z_target_vel; + } + carriage_mm += z_jog_velocity * dt; + carriage_mm = std::max(z_min_mm, std::min(z_max_mm, carriage_mm)); + } else { + z_jog_velocity = 0.0; + z_move_active = false; + } + + /* X-axis (always manual) */ + x_jog_idle_timer += dt; + if (x_jog_idle_timer > JOG_KEY_TIMEOUT) x_jog_target_dir = 0.0; + + { + double x_dir = x_jog_target_dir; + if (x_move_active) { + x_dir = moveToDir(cross_slide_mm, x_move_target, x_jog_velocity); + if (x_dir == 0.0 && std::abs(x_jog_velocity) < 0.01) { + x_move_active = false; + cross_slide_mm = x_move_target; + x_jog_velocity = 0.0; + emu_log_event("X move complete: %.3f mm", cross_slide_mm); + } + } + + double x_target_vel = x_dir * jog_max_velocity; + double x_vel_error = x_target_vel - x_jog_velocity; + if (std::abs(x_vel_error) > 0.001) { + double accel = (x_vel_error > 0) ? jog_acceleration : -jog_acceleration; + x_jog_velocity += accel * dt; + double new_error = x_target_vel - x_jog_velocity; + if (x_vel_error * new_error < 0) x_jog_velocity = x_target_vel; + } else { + x_jog_velocity = x_target_vel; + } + cross_slide_mm += x_jog_velocity * dt; + cross_slide_mm = std::max(x_min_mm, std::min(x_max_mm, cross_slide_mm)); + } + + /* --- Update encoder counters for firmware --- */ + emu_hw.scale_counters[0] = (uint32_t)(int32_t)getSpindleEncoderCounts(); + emu_hw.scale_counters[1] = (uint32_t)(int32_t)getCarriageEncoderCounts(); + emu_hw.scale_counters[2] = (uint32_t)(int32_t)getCrossSlideEncoderCounts(); + emu_hw.scale_counters[3] = 0; + + /* Write into the TIM counter registers */ + emu_update_timer_counters(); +} + +void LathePhysics::onStepPulse(int direction) { + /* direction: +1 or -1, from DIR pin */ + leadscrew_total_steps += direction; + leadscrew_position_mm += direction * leadscrew_mm_per_step; + + if (half_nut_state == ENGAGED) { + double move = direction * leadscrew_mm_per_step; + + /* Backlash model: direction reversal eats backlash before moving */ + if (direction != last_carriage_dir && last_carriage_dir != 0) { + backlash_remaining = z_backlash_mm; + } + + if (backlash_remaining > 0.0) { + backlash_remaining -= std::abs(move); + if (backlash_remaining <= 0.0) { + /* Backlash absorbed; apply the overshoot */ + move = -backlash_remaining * (direction > 0 ? 1.0 : -1.0); + backlash_remaining = 0.0; + } else { + move = 0.0; /* Still in backlash zone */ + } + } + + carriage_mm += move; + carriage_mm = std::max(z_min_mm, std::min(z_max_mm, carriage_mm)); + last_carriage_dir = direction; + } +} + +void LathePhysics::setTargetRPM(double rpm) { + spindle_target_rpm = rpm; +} + +void LathePhysics::toggleDirection() { + spindle_target_rpm = -spindle_target_rpm; +} + +void LathePhysics::emergencyStop() { + spindle_target_rpm = 0.0; + emu_log_event("E-STOP: spindle target -> 0"); +} + +int64_t LathePhysics::getSpindleEncoderCounts() const { + /* Convert cumulative angle to encoder counts. + * This wraps at 16-bit for TIM1 (which is how the real encoder works). */ + double counts = spindle_theta / (2.0 * M_PI) * spindle_counts_per_rev; + return (int64_t)counts; +} + +void LathePhysics::requestHalfNutToggle() { + half_nut_request_pending = true; +} + +void LathePhysics::jogCarriage(int direction) { + if (half_nut_state == ENGAGED) return; + z_move_active = false; /* arrow key jog cancels move-to-position */ + z_jog_target_dir = (double)direction; + z_jog_idle_timer = 0.0; +} + +void LathePhysics::moveCarriageTo(double target_mm) { + if (half_nut_state == ENGAGED) return; + target_mm = std::max(z_min_mm, std::min(z_max_mm, target_mm)); + z_move_target = target_mm; + z_move_active = true; + z_jog_target_dir = 0.0; /* cancel any arrow key jog */ +} + +int64_t LathePhysics::getCarriageEncoderCounts() const { + return (int64_t)(carriage_mm * z_counts_per_mm); +} + +void LathePhysics::jogCrossSlide(int direction) { + x_move_active = false; /* arrow key jog cancels move-to-position */ + x_jog_target_dir = (double)direction; + x_jog_idle_timer = 0.0; +} + +void LathePhysics::moveCrossSlideTo(double target_mm) { + target_mm = std::max(x_min_mm, std::min(x_max_mm, target_mm)); + x_move_target = target_mm; + x_move_active = true; + x_jog_target_dir = 0.0; +} + +int64_t LathePhysics::getCrossSlideEncoderCounts() const { + return (int64_t)(cross_slide_mm * x_counts_per_mm); +} + +/* --- Half-nut engagement helpers --- */ + +double LathePhysics::getLeadscrewPhase() const { + /* Phase within one revolution of the leadscrew (0.0 to 1.0) */ + double revolutions = leadscrew_position_mm / leadscrew_grid_spacing_mm; + double phase = fmod(revolutions, 1.0); + if (phase < 0.0) phase += 1.0; + return phase; +} + +double LathePhysics::getCarriageGridPhase() const { + /* Where is the carriage relative to the leadscrew thread grid? */ + double revolutions = carriage_mm / leadscrew_grid_spacing_mm; + double phase = fmod(revolutions, 1.0); + if (phase < 0.0) phase += 1.0; + return phase; +} + +void LathePhysics::snapCarriageToGrid() { + double grid = leadscrew_grid_spacing_mm; + carriage_mm = round(carriage_mm / grid) * grid; + carriage_mm = std::max(z_min_mm, std::min(z_max_mm, carriage_mm)); +} + +bool LathePhysics::checkPhaseAlignment() const { + double ls_phase = getLeadscrewPhase(); + double carr_phase = getCarriageGridPhase(); + double delta = std::abs(ls_phase - carr_phase); + if (delta > 0.5) delta = 1.0 - delta; /* wrap around */ + + /* Tolerance: within ~2% of a revolution */ + return delta < 0.02; +} diff --git a/emulator/src/physics.h b/emulator/src/physics.h new file mode 100644 index 0000000..e649590 --- /dev/null +++ b/emulator/src/physics.h @@ -0,0 +1,124 @@ +/* + * Lathe physics model. + * + * Simulates spindle with inertia, leadscrew, carriage with half-nut, and cross-slide. + */ +#ifndef EMU_PHYSICS_H +#define EMU_PHYSICS_H + +#include "config.h" +#include +#include +#include + +class LathePhysics { +public: + explicit LathePhysics(const EmuConfig &cfg); + + /* Called each ISR tick BEFORE calling into firmware. + * Updates spindle and feeds encoder counter values into emu_hw. */ + /* shared must point to the firmware's rampsSharedData_t for reading servo state */ + void tick(double dt_seconds, const void *shared_data); + + /* Called from GPIO shim when a STEP rising edge is detected. */ + void onStepPulse(int direction); + + /* --- Spindle control (from dashboard keyboard) --- */ + void setTargetRPM(double rpm); + void toggleDirection(); + void emergencyStop(); + + double getSpindleRPM() const { return spindle_omega * 60.0 / (2.0 * M_PI); } + double getTargetRPM() const { return spindle_target_rpm; } + bool getSpindleCW() const { return spindle_target_rpm >= 0; } + int64_t getSpindleEncoderCounts() const; + + /* --- Half-nut --- */ + enum HalfNutState { DISENGAGED, ENGAGING, ENGAGED }; + + void requestHalfNutToggle(); + HalfNutState getHalfNutState() const { return half_nut_state; } + + /* --- Carriage (Z-axis) manual move --- */ + double getCarriageMM() const { return carriage_mm; } + void jogCarriage(int direction); /* Arrow key jog: direction +1/-1 */ + void moveCarriageTo(double target_mm); /* Move to specific position */ + bool isZMoveTargetActive() const { return z_move_active; } + int64_t getCarriageEncoderCounts() const; + + /* --- Cross-slide (X-axis) manual move --- */ + double getCrossSlideMM() const { return cross_slide_mm; } + void jogCrossSlide(int direction); + void moveCrossSlideTo(double target_mm); + bool isXMoveTargetActive() const { return x_move_active; } + int64_t getCrossSlideEncoderCounts() const; + + /* --- Jog status --- */ + bool isZJogging() const { return std::abs(z_jog_velocity) > 0.01; } + bool isXJogging() const { return std::abs(x_jog_velocity) > 0.01; } + + /* --- Leadscrew --- */ + double getLeadscrewPositionMM() const { return leadscrew_position_mm; } + double getLeadscrewGridSpacingMM() const { return leadscrew_grid_spacing_mm; } + +private: + /* Config */ + double spindle_inertia; + double spindle_max_torque; + double spindle_friction; + int spindle_counts_per_rev; + double leadscrew_mm_per_step; + double leadscrew_tpi; + double leadscrew_grid_spacing_mm; /* 25.4 / tpi */ + double z_counts_per_mm; + double z_backlash_mm; + double z_max_mm, z_min_mm; + double x_counts_per_mm; + double x_max_mm, x_min_mm; + double x_manual_step_mm; + + /* Spindle state */ + double spindle_theta; /* cumulative angle in radians */ + double spindle_omega; /* angular velocity in rad/s */ + double spindle_target_rpm; + + /* Leadscrew state (always tracks stepper steps) */ + double leadscrew_position_mm; /* cumulative from step pulses */ + int64_t leadscrew_total_steps; + + /* Carriage state */ + double carriage_mm; + HalfNutState half_nut_state; + bool half_nut_request_pending; + double backlash_remaining; + int last_carriage_dir; /* +1 or -1, for backlash */ + + /* Cross-slide state */ + double cross_slide_mm; + + /* Manual jog state (velocity-based with acceleration) */ + double z_jog_velocity; /* current mm/s */ + double z_jog_target_dir; /* -1, 0, or +1 (arrow key jog) */ + double x_jog_velocity; /* current mm/s */ + double x_jog_target_dir; /* -1, 0, or +1 (arrow key jog) */ + + /* Move-to-position state */ + bool z_move_active; + double z_move_target; /* target position in mm */ + bool x_move_active; + double x_move_target; + double jog_max_velocity; /* mm/s from config */ + double jog_acceleration; /* mm/s^2 from config */ + double jog_timeout; /* seconds since last key event */ + double z_jog_idle_timer; /* time since last Z key */ + double x_jog_idle_timer; /* time since last X key */ + static constexpr double JOG_KEY_TIMEOUT = 0.15; /* seconds of no key β†’ stop */ + + /* Half-nut engagement helpers */ + double getLeadscrewPhase() const; + double getCarriageGridPhase() const; + void snapCarriageToGrid(); + bool checkPhaseAlignment() const; +}; + +#endif /* EMU_PHYSICS_H */ diff --git a/emulator/src/transport.cpp b/emulator/src/transport.cpp new file mode 100644 index 0000000..0c759ea --- /dev/null +++ b/emulator/src/transport.cpp @@ -0,0 +1,217 @@ +/* + * Transport implementation: PTY pair + TCP socket. + */ + +#include "transport.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "emulator_state.h" +#include "stm32f4xx_hal_uart.h" +extern UART_HandleTypeDef huart1; +void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); +} + +Transport::Transport(const EmuConfig &cfg) + : pty_enabled(cfg.pty_enabled), tcp_enabled(cfg.tcp_enabled), tcp_port(cfg.tcp_port), + pty_master_fd(-1), pty_slave_fd(-1), pty_fd(-1), + tcp_listen_fd(-1), tcp_client_fd(-1), running(false) +{ +} + +Transport::~Transport() { + stop(); +} + +bool Transport::setupPty() { + if (openpty(&pty_master_fd, &pty_slave_fd, nullptr, nullptr, nullptr) < 0) { + perror("openpty"); + return false; + } + + /* Set master to raw mode, non-blocking */ + struct termios tio; + tcgetattr(pty_master_fd, &tio); + cfmakeraw(&tio); + tcsetattr(pty_master_fd, TCSANOW, &tio); + fcntl(pty_master_fd, F_SETFL, O_NONBLOCK); + + /* Also set slave to raw mode */ + tcgetattr(pty_slave_fd, &tio); + cfmakeraw(&tio); + tcsetattr(pty_slave_fd, TCSANOW, &tio); + + pty_slave_path = ttyname(pty_slave_fd); + pty_fd = pty_master_fd; + + printf("Modbus serial: %s\n", pty_slave_path.c_str()); + return true; +} + +bool Transport::setupTcp() { + tcp_listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (tcp_listen_fd < 0) { + perror("socket"); + return false; + } + + int opt = 1; + setsockopt(tcp_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(tcp_port); + + if (bind(tcp_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(tcp_listen_fd); + tcp_listen_fd = -1; + return false; + } + + if (listen(tcp_listen_fd, 1) < 0) { + perror("listen"); + close(tcp_listen_fd); + tcp_listen_fd = -1; + return false; + } + + fcntl(tcp_listen_fd, F_SETFL, O_NONBLOCK); + printf("Modbus TCP: listening on port %d\n", tcp_port); + return true; +} + +void Transport::start() { + if (pty_enabled) setupPty(); + if (tcp_enabled) setupTcp(); + + /* Register TX callback */ + emu_hw.on_uart_tx = [](const uint8_t *data, uint16_t size) { + /* This is called from the firmware's Modbus TX path. + * We need to get a pointer to the Transport instance. + * Use the user_data field. */ + auto *self = (Transport*)emu_hw.user_data; + if (self) self->sendToClients(data, size); + }; + emu_hw.user_data = this; + + running.store(true); + poll_thread = std::thread([this]() { pollLoop(); }); +} + +void Transport::stop() { + running.store(false); + if (poll_thread.joinable()) poll_thread.join(); + if (pty_master_fd >= 0) { close(pty_master_fd); pty_master_fd = -1; } + if (pty_slave_fd >= 0) { close(pty_slave_fd); pty_slave_fd = -1; } + if (tcp_client_fd >= 0) { close(tcp_client_fd); tcp_client_fd = -1; } + if (tcp_listen_fd >= 0) { close(tcp_listen_fd); tcp_listen_fd = -1; } +} + +void Transport::sendToClients(const uint8_t *data, uint16_t size) { + if (pty_master_fd >= 0) { + ssize_t n = write(pty_master_fd, data, size); + (void)n; + } + if (tcp_client_fd >= 0) { + ssize_t n = send(tcp_client_fd, data, size, MSG_NOSIGNAL); + if (n < 0) { + close(tcp_client_fd); + tcp_client_fd = -1; + emu_log_event("TCP client disconnected"); + } + } +} + +void Transport::injectByte(uint8_t byte) { + /* Feed byte into firmware's Modbus UART RX path */ + if (emu_hw.uart_rx_armed && emu_hw.uart_rx_buf) { + *emu_hw.uart_rx_buf = byte; + emu_hw.uart_rx_armed = 0; + HAL_UART_RxCpltCallback(&huart1); + } +} + +void Transport::pollLoop() { + while (running.load()) { + struct pollfd fds[3]; + int nfds = 0; + + if (pty_master_fd >= 0) { + fds[nfds].fd = pty_master_fd; + fds[nfds].events = POLLIN; + nfds++; + } + + if (tcp_listen_fd >= 0) { + fds[nfds].fd = tcp_listen_fd; + fds[nfds].events = POLLIN; + nfds++; + } + + if (tcp_client_fd >= 0) { + fds[nfds].fd = tcp_client_fd; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + usleep(10000); + continue; + } + + int ret = poll(fds, nfds, 10 /* 10ms timeout */); + if (ret <= 0) continue; + + for (int i = 0; i < nfds; i++) { + if (!(fds[i].revents & POLLIN)) continue; + + if (fds[i].fd == pty_master_fd) { + /* Data from PTY client */ + uint8_t buf[256]; + ssize_t n = read(pty_master_fd, buf, sizeof(buf)); + if (n > 0) { + for (ssize_t j = 0; j < n; j++) { + injectByte(buf[j]); + } + } + } else if (fds[i].fd == tcp_listen_fd) { + /* New TCP connection */ + struct sockaddr_in client_addr; + socklen_t addr_len = sizeof(client_addr); + int new_fd = accept(tcp_listen_fd, (struct sockaddr*)&client_addr, &addr_len); + if (new_fd >= 0) { + if (tcp_client_fd >= 0) close(tcp_client_fd); + tcp_client_fd = new_fd; + fcntl(tcp_client_fd, F_SETFL, O_NONBLOCK); + emu_log_event("TCP client connected from %s", inet_ntoa(client_addr.sin_addr)); + } + } else if (fds[i].fd == tcp_client_fd) { + /* Data from TCP client */ + uint8_t buf[256]; + ssize_t n = recv(tcp_client_fd, buf, sizeof(buf), 0); + if (n <= 0) { + close(tcp_client_fd); + tcp_client_fd = -1; + emu_log_event("TCP client disconnected"); + } else { + for (ssize_t j = 0; j < n; j++) { + injectByte(buf[j]); + } + } + } + } + } +} diff --git a/emulator/src/transport.h b/emulator/src/transport.h new file mode 100644 index 0000000..4ab6228 --- /dev/null +++ b/emulator/src/transport.h @@ -0,0 +1,64 @@ +/* + * Modbus transport layer. + * PTY pair + TCP socket, feeding bytes into the firmware's UART shim. + */ +#ifndef EMU_TRANSPORT_H +#define EMU_TRANSPORT_H + +#include "config.h" +#include +#include +#include + +class Transport { +public: + explicit Transport(const EmuConfig &cfg); + ~Transport(); + + /* Start transport threads. */ + void start(); + + /* Stop transport threads. */ + void stop(); + + /* Send bytes from firmware TX to all connected clients. */ + void sendToClients(const uint8_t *data, uint16_t size); + + /* Get the PTY slave device path (for user to connect client). */ + std::string getPtyPath() const { return pty_slave_path; } + + /* Connection status */ + bool isPtyConnected() const { return pty_fd >= 0; } + int getTcpClientCount() const { return tcp_client_fd >= 0 ? 1 : 0; } + +private: + /* Config */ + bool pty_enabled; + bool tcp_enabled; + int tcp_port; + + /* PTY */ + int pty_master_fd; + int pty_slave_fd; + std::string pty_slave_path; + int pty_fd; /* alias for master */ + + /* TCP */ + int tcp_listen_fd; + int tcp_client_fd; + + /* Threads */ + std::atomic running; + std::thread poll_thread; + + void pollLoop(); + void injectByte(uint8_t byte); + + /* PTY setup */ + bool setupPty(); + + /* TCP setup */ + bool setupTcp(); +}; + +#endif /* EMU_TRANSPORT_H */ From d9521773207d285e5b9efb4ff6bacb23f50a684b Mon Sep 17 00:00:00 2001 From: Funkenjaeger Date: Sun, 12 Apr 2026 12:41:34 -0400 Subject: [PATCH 2/4] Remove placeholder threading display This was a placeholder for assisted threading/advanced ELS features coming in other branches; will add such features back into the emulator on those branches when ready. --- emulator/src/dashboard.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/emulator/src/dashboard.cpp b/emulator/src/dashboard.cpp index 1d72d30..78adc5e 100644 --- a/emulator/src/dashboard.cpp +++ b/emulator/src/dashboard.cpp @@ -217,13 +217,6 @@ void Dashboard::drawStatePane(int startRow, int startCol, int width) { LINE("%s", ""); - /* Threading section */ - LINE(BOLD " THREADING" RESET_ATTR); - LINE(" remaining: %d traveled: --", (int)0); - LINE(" phase ref: -- cur: -- tol: --"); - - LINE("%s", ""); - /* Scales */ LINE(BOLD " SCALES" RESET_ATTR); const char *scale_names[] = { "spindle", "z-axis ", "x-slide", "spare " }; From 1a9b593aeecaa78fe93d000311e378a03ecfd105 Mon Sep 17 00:00:00 2001 From: Funkenjaeger Date: Mon, 13 Apr 2026 18:47:38 -0400 Subject: [PATCH 3/4] Squash bugs --- .gitignore | 5 ++++- emulator/shim/freertos_shim.cpp | 10 +++++++--- emulator/src/dashboard.cpp | 7 ++++--- emulator/src/main.cpp | 8 ++++++++ emulator/src/physics.cpp | 4 +++- emulator/src/physics.h | 3 ++- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index cbc9882..e18aa78 100644 --- a/.gitignore +++ b/.gitignore @@ -113,4 +113,7 @@ makefile subdir.mk # Emulator build -/emulator/build/ +/emulator/build*/ + +# AI +.claude/ diff --git a/emulator/shim/freertos_shim.cpp b/emulator/shim/freertos_shim.cpp index f1e9ec8..0fdc448 100644 --- a/emulator/shim/freertos_shim.cpp +++ b/emulator/shim/freertos_shim.cpp @@ -232,10 +232,14 @@ struct SoftwareTimer { if (expired) { /* Timer fired */ if (!autoReload) running.store(false); - /* The Modbus library casts TimerHandle_t as TimerHandle_t* in callback signature */ - TimerHandle_t self = (TimerHandle_t)this; lock.unlock(); - callback(&self); + /* The Modbus library declares its timer callbacks as + * void cb(TimerHandle_t *pxTimer) but in real FreeRTOS + * TimerCallbackFunction_t passes the handle by value. + * The library then compares the received value against the + * stored handle, so we must pass the handle value itself + * (cast to match our typedef). */ + callback((TimerHandle_t*)this); } } } diff --git a/emulator/src/dashboard.cpp b/emulator/src/dashboard.cpp index 78adc5e..bd2df5d 100644 --- a/emulator/src/dashboard.cpp +++ b/emulator/src/dashboard.cpp @@ -204,7 +204,7 @@ void Dashboard::drawStatePane(int startRow, int startCol, int width) { if (shared.fastData.servoMode == 1) mode_str = "SYNC"; else if (shared.fastData.servoMode == 2) mode_str = "JOG"; - LINE(" mode: %s speed: %.0f stp/s", mode_str, shared.servo.currentSpeed); + LINE(" mode: %s speed: %.0f stp/s", mode_str, shared.fastData.servoSpeed); const char *dir_str = (emu_hw.dir_pin) ? "FWD" : "REV"; const char *ena_str = (emu_hw.ena_pin == 0) ? "ON" : "OFF"; @@ -351,8 +351,9 @@ void Dashboard::handleInput() { if (!had_t) { had_t = true; if (std::abs(physics.getTargetRPM()) < 0.1) { - physics.setTargetRPM(500.0); - emu_log_event("spindle target -> 500 RPM"); + double rpm = physics.getSpindleCW() ? 500.0 : -500.0; + physics.setTargetRPM(rpm); + emu_log_event("spindle target -> %.0f RPM", rpm); } else { physics.setTargetRPM(0.0); emu_log_event("spindle target -> 0 RPM"); diff --git a/emulator/src/main.cpp b/emulator/src/main.cpp index 72474b0..c82d8d4 100644 --- a/emulator/src/main.cpp +++ b/emulator/src/main.cpp @@ -128,6 +128,14 @@ int main(int argc, char *argv[]) { rampsData.synchroRefreshTimer = &htim9; rampsData.modbusUart = &huart1; + /* Adjust htim9 prescaler so the firmware's clock_freq calculation yields + * the emulator's actual ISR rate. On real hardware the timer runs at + * 100 kHz (100 MHz / (99+1) / (9+1)); the firmware derives the maximum + * step-pulse rate from that via servoCycles = clock_freq / maxSpeed. + * In the emulator the ISR fires at cfg.isr_rate_hz, so we must match: + * 100 MHz / (Prescaler+1) / (Period+1) == isr_rate_hz */ + htim9.Init.Prescaler = 100000000U / ((uint32_t)cfg.isr_rate_hz * (htim9.Init.Period + 1)) - 1; + /* Apply config defaults to servo */ rampsData.shared.servo.maxSpeed = (float)cfg.servo_max_speed; rampsData.shared.servo.acceleration = (float)cfg.servo_acceleration; diff --git a/emulator/src/physics.cpp b/emulator/src/physics.cpp index acf4047..a9396e9 100644 --- a/emulator/src/physics.cpp +++ b/emulator/src/physics.cpp @@ -33,6 +33,7 @@ LathePhysics::LathePhysics(const EmuConfig &cfg) { spindle_theta = 0.0; spindle_omega = cfg.spindle_initial_rpm * 2.0 * M_PI / 60.0; spindle_target_rpm = cfg.spindle_initial_rpm; + spindle_cw = (cfg.spindle_initial_rpm >= 0); leadscrew_position_mm = 0.0; leadscrew_total_steps = 0; @@ -114,7 +115,7 @@ void LathePhysics::tick(double dt, const void *shared_data) { /* Request to engage. * The leadscrew moves only when the firmware's servo is producing * steps (sync enabled + spindle turning). Check actual servo speed. */ - bool leadscrew_moving = shared && std::abs(shared->servo.currentSpeed) > 0.1; + bool leadscrew_moving = shared && std::abs(shared->fastData.servoSpeed) > 0.1; if (!leadscrew_moving) { /* Leadscrew stationary: snap carriage to nearest grid point and engage */ @@ -265,6 +266,7 @@ void LathePhysics::setTargetRPM(double rpm) { } void LathePhysics::toggleDirection() { + spindle_cw = !spindle_cw; spindle_target_rpm = -spindle_target_rpm; } diff --git a/emulator/src/physics.h b/emulator/src/physics.h index e649590..d34cf99 100644 --- a/emulator/src/physics.h +++ b/emulator/src/physics.h @@ -30,7 +30,7 @@ class LathePhysics { double getSpindleRPM() const { return spindle_omega * 60.0 / (2.0 * M_PI); } double getTargetRPM() const { return spindle_target_rpm; } - bool getSpindleCW() const { return spindle_target_rpm >= 0; } + bool getSpindleCW() const { return spindle_cw; } int64_t getSpindleEncoderCounts() const; /* --- Half-nut --- */ @@ -81,6 +81,7 @@ class LathePhysics { double spindle_theta; /* cumulative angle in radians */ double spindle_omega; /* angular velocity in rad/s */ double spindle_target_rpm; + bool spindle_cw; /* direction flag, survives stop/start */ /* Leadscrew state (always tracks stepper steps) */ double leadscrew_position_mm; /* cumulative from step pulses */ From e32d20a73844f4896b645088680f74033fe02a17 Mon Sep 17 00:00:00 2001 From: Funkenjaeger Date: Mon, 13 Apr 2026 22:24:48 -0400 Subject: [PATCH 4/4] Break timerHandles out of Modbus memory map The different sizes (# bytes) of the timerHandle pointers in ARM vs x86-64 caused a shift in the memory map of the emulator versus the real hardware, breaking the memory map. Those pointers don't need to be exposed via Modbus anyway, so just breaking them out of there mitigates this issue. dummy int32 replaces timerHandle in input_t typedef to maintain alignment to original memory map so SW doesn't need to change. --- Core/Inc/Ramps.h | 11 ++++++----- Core/Src/Ramps.c | 6 +++--- Core/Src/main.c | 8 ++++---- emulator/src/main.cpp | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Core/Inc/Ramps.h b/Core/Inc/Ramps.h index 4520aa2..b93ad87 100644 --- a/Core/Inc/Ramps.h +++ b/Core/Inc/Ramps.h @@ -58,11 +58,11 @@ typedef struct { } deltaPosError_t; typedef struct { - TIM_HandleTypeDef *timerHandle; - int32_t position; - int32_t speed; - int32_t syncRatioNum, syncRatioDen; - uint16_t syncEnable; + int32_t dummy; + int32_t position; // READ-ONLY (firmware-owned): absolute encoder position, updated by ISR + int32_t speed; // READ-ONLY (firmware-owned): encoder speed (counts/s), updated by updateSpeedTask + int32_t syncRatioNum, syncRatioDen; // SW write: sync ratio numerator/denominator (output steps per input count) + uint16_t syncEnable; // SW write: 0 = sync disabled, non-zero = sync enabled for this scale } input_t; typedef struct { @@ -106,6 +106,7 @@ typedef struct { // STM32 Related TIM_HandleTypeDef *synchroRefreshTimer; UART_HandleTypeDef *modbusUart; + TIM_HandleTypeDef *scaleTimers[SCALES_COUNT]; deltaPosError_t scalesDeltaPos[SCALES_COUNT]; deltaPosError_t scalesSyncDeltaPos[SCALES_COUNT]; diff --git a/Core/Src/Ramps.c b/Core/Src/Ramps.c index ef04f35..6bc1c43 100644 --- a/Core/Src/Ramps.c +++ b/Core/Src/Ramps.c @@ -92,8 +92,8 @@ void RampsStart(rampsHandler_t *rampsData) { // Initialize and start encoder timer, reset the sync flags for (int j = 0; j < SCALES_COUNT; ++j) { - initScaleTimer(rampsData->shared.scales[j].timerHandle); - HAL_TIM_Encoder_Start(rampsData->shared.scales[j].timerHandle, TIM_CHANNEL_ALL); + initScaleTimer(rampsData->scaleTimers[j]); + HAL_TIM_Encoder_Start(rampsData->scaleTimers[j], TIM_CHANNEL_ALL); } // Enable debug cycle counter @@ -239,7 +239,7 @@ void SynchroRefreshTimerIsr(rampsHandler_t *data) { for (int i = 0; i < SCALES_COUNT; i++) { data->scalesDeltaPos[i].oldPosition = data->scalesDeltaPos[i].position; - data->scalesDeltaPos[i].position = __HAL_TIM_GET_COUNTER(data->shared.scales[i].timerHandle); + data->scalesDeltaPos[i].position = __HAL_TIM_GET_COUNTER(data->scaleTimers[i]); data->scalesDeltaPos[i].delta = (int16_t) (data->scalesDeltaPos[i].position - data->scalesDeltaPos[i].oldPosition); shared->scales[i].position += data->scalesDeltaPos[i].delta; diff --git a/Core/Src/main.c b/Core/Src/main.c index 9e99885..2774f61 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -101,10 +101,10 @@ int main(void) // htim4 is used in encoder mode // htim9 is used to generate the synchro motion - RampsData.shared.scales[0].timerHandle = &htim1; - RampsData.shared.scales[1].timerHandle = &htim2; - RampsData.shared.scales[2].timerHandle = &htim3; - RampsData.shared.scales[3].timerHandle = &htim4; + RampsData.scaleTimers[0] = &htim1; + RampsData.scaleTimers[1] = &htim2; + RampsData.scaleTimers[2] = &htim3; + RampsData.scaleTimers[3] = &htim4; RampsData.synchroRefreshTimer = &htim9; RampsData.modbusUart = &huart1; RampsStart(&RampsData); diff --git a/emulator/src/main.cpp b/emulator/src/main.cpp index c82d8d4..d47f512 100644 --- a/emulator/src/main.cpp +++ b/emulator/src/main.cpp @@ -119,10 +119,10 @@ int main(int argc, char *argv[]) { /* Wire timer handles to scales (same mapping as firmware's main.c) */ extern TIM_HandleTypeDef htim1, htim2, htim3, htim4; - rampsData.shared.scales[0].timerHandle = &htim1; - rampsData.shared.scales[1].timerHandle = &htim2; - rampsData.shared.scales[2].timerHandle = &htim3; - rampsData.shared.scales[3].timerHandle = &htim4; + rampsData.scaleTimers[0] = &htim1; + rampsData.scaleTimers[1] = &htim2; + rampsData.scaleTimers[2] = &htim3; + rampsData.scaleTimers[3] = &htim4; /* Set synchro timer and UART handles */ rampsData.synchroRefreshTimer = &htim9;