diff --git a/.gitignore b/.gitignore index 102f64e..e18aa78 100644 --- a/.gitignore +++ b/.gitignore @@ -86,171 +86,34 @@ 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*/ + +# AI +.claude/ 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 33d504d..0ae5414 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 @@ -237,7 +237,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/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..0fdc448 --- /dev/null +++ b/emulator/shim/freertos_shim.cpp @@ -0,0 +1,423 @@ +/* + * 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); + lock.unlock(); + /* 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); + } + } + } + } +}; + +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..bd2df5d --- /dev/null +++ b/emulator/src/dashboard.cpp @@ -0,0 +1,516 @@ +/* + * 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.fastData.servoSpeed); + + 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", ""); + + /* 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) { + 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"); + } + } + 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..d47f512 --- /dev/null +++ b/emulator/src/main.cpp @@ -0,0 +1,197 @@ +/* + * 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.scaleTimers[0] = &htim1; + rampsData.scaleTimers[1] = &htim2; + rampsData.scaleTimers[2] = &htim3; + rampsData.scaleTimers[3] = &htim4; + + /* Set synchro timer and UART handles */ + 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; + + /* 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..a9396e9 --- /dev/null +++ b/emulator/src/physics.cpp @@ -0,0 +1,357 @@ +/* + * 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; + spindle_cw = (cfg.spindle_initial_rpm >= 0); + + 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->fastData.servoSpeed) > 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_cw = !spindle_cw; + 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..d34cf99 --- /dev/null +++ b/emulator/src/physics.h @@ -0,0 +1,125 @@ +/* + * 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_cw; } + 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; + bool spindle_cw; /* direction flag, survives stop/start */ + + /* 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 */