From 9644373f9ab357335316c700a0929f7962fb271f Mon Sep 17 00:00:00 2001 From: Vincent MANOUKIAN <10980775+manoukianv@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:37:12 +0200 Subject: [PATCH 1/8] docs: pure code cleanup, typing and Doxygen documentation --- Firmware/FFBoard/Inc/Axis.h | 471 +++++++++++++++------ Firmware/FFBoard/Inc/EffectsCalculator.h | 225 ++++++++-- Firmware/FFBoard/Inc/HidFFB.h | 115 ++++- Firmware/FFBoard/Src/Axis.cpp | 145 ++----- Firmware/FFBoard/Src/EffectsCalculator.cpp | 10 +- 5 files changed, 671 insertions(+), 295 deletions(-) diff --git a/Firmware/FFBoard/Inc/Axis.h b/Firmware/FFBoard/Inc/Axis.h index 5b044c187..32b8bb8af 100644 --- a/Firmware/FFBoard/Inc/Axis.h +++ b/Firmware/FFBoard/Inc/Axis.h @@ -2,7 +2,7 @@ * Axis.h * * Created on: 21.01.2021 - * Author: Yannick / Lidders + * Author: Yannick / Lidders / Vincent */ #ifndef SRC_AXIS_H_ @@ -10,7 +10,6 @@ #include #include #include "usb_hid_ffb_desc.h" -#include "TMC4671.h" #include "PersistentStorage.h" #include "ButtonSource.h" #include "EncoderLocal.h" @@ -34,18 +33,23 @@ #define AXIS_SPEEDLIMITER_I 0.03 #endif - +/** + * @brief Global control flags for all axes. + */ struct Control_t { - bool emergency = false; - bool usb_disabled = true; - bool update_disabled = true; - bool request_update_disabled = false; + bool emergency = false; //!< Emergency stop is active. + bool usb_disabled = true; //!< FFB is disabled by USB. + bool update_disabled = true; //!< FFB updates are disabled. + bool request_update_disabled = false; //!< A request to disable FFB updates is pending. // bool usb_update_flag = false; // bool update_flag = false; - bool resetEncoder = false; + bool resetEncoder = false; //!< A request to reset the encoder is pending. }; -struct AxisFlashAddrs +/** + * @brief Defines the flash memory addresses for axis-specific settings. + */ +struct AxisFlashAddresses { uint16_t config = ADR_AXIS1_CONFIG; uint16_t maxSpeed = ADR_AXIS1_MAX_SPEED; @@ -62,232 +66,435 @@ struct AxisFlashAddrs uint16_t postprocess1 = ADR_AXIS1_POSTPROCESS1; }; +/** + * @brief Configuration for an axis, including driver and encoder types. + */ struct AxisConfig { - uint8_t drvtype = 0; - uint8_t enctype = 0; - //bool invert = false; + uint8_t enctype = 0; //!< Encoder type ID. + uint8_t drvtype = 0; //!< Motor driver type ID. }; + +/** + * @brief Holds the physical metrics of an axis at a point in time. + */ struct metric_t { - float accel = 0; // in deg/s² - float speed = 0; // in deg/s - int32_t pos_scaled_16b = 0; // scaled position as 16b int -0x7fff to 0x7fff matching FFB ranges - float pos_f = 0; // scaled position as float. -1 to 1 range - float posDegrees = 0; // Position in degrees. Not scaled to selected range - int32_t torque = 0; // total of effect + endstop torque + float accel = 0; //!< Acceleration in deg/s². + float speed = 0; //!< Speed in deg/s. + int32_t pos_scaled_16b = 0; //!< Scaled position as a 16-bit integer (-0x7fff to 0x7fff). + float pos_f = 0; //!< Scaled position as a float (-1.0 to 1.0). + float posDegrees = 0; //!< Position in degrees, not scaled to the selected range. + int32_t torque = 0; //!< Total torque applied to the axis. }; - +/** + * @brief Holds the current and previous metrics for an axis, used for calculating derivatives. + */ struct axis_metric_t { - metric_t current; - metric_t previous; + metric_t current; //!< Current metrics. + metric_t previous; //!< Metrics from the previous update cycle. }; +/** + * @brief Represents a gear ratio for scaling encoder values. + */ struct GearRatio_t{ - uint8_t denominator = 0; - uint8_t numerator = 0; - float gearRatio = 1.0; + uint8_t denominator = 0; //!< Denominator of the gear ratio. + uint8_t numerator = 0; //!< Numerator of the gear ratio. + float gearRatio = 1.0; //!< The calculated gear ratio (numerator/denominator). }; - enum class Axis_commands : uint32_t{ power=0x00,degrees=0x01,esgain,zeroenc,invert,idlespring,axisdamper,enctype,drvtype, - pos,maxspeed,maxtorquerate,fxratio,curtorque,curpos,curspd,curaccel,reductionScaler, + pos,curtorque,curpos,curspd,curaccel, + fxratio,reductionScaler, filterSpeed, filterAccel, filterProfileId,cpr,axisfriction,axisinertia, + maxspeed, maxtorquerate, expo,exposcale }; +/** + * @brief This class represents a single FFB axis. + * It handles the motor driver, encoder, and all related calculations for force feedback effects. + */ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandler { public: + /** + * @brief Construct a new Axis object. + * @param axis The character identifier for this axis (e.g., 'x', 'y'). + * @param control A pointer to the global control structure. + */ Axis(char axis, volatile Control_t* control); virtual ~Axis(); - static ClassIdentifier info; + static ClassIdentifier info; //!< Static class identifier. const ClassIdentifier getInfo(); const ClassType getClassType() override {return ClassType::Axis;}; - virtual std::string getHelpstring() { return "FFB axis" ;} -#ifdef TMC4671DRIVER - void setupTMC4671(); -#endif + virtual std::string getHelpstring() { return "FFB axis" ;} + // Dynamic classes + /** + * @brief Sets the motor driver type. + * @param drvtype The type of the motor driver. + */ void setDrvType(uint8_t drvtype); + + /** + * @brief Sets the encoder type. + * @param enctype The type of the encoder. + */ void setEncType(uint8_t enctype); + + /** + * @brief Gets the motor driver type. + * @return The type of the motor driver. + */ uint8_t getDrvType(); + + /** + * @brief Gets the encoder type. + * @return The type of the encoder. + */ uint8_t getEncType(); + /** + * @brief Gets the encoder instance. + * @return A pointer to the encoder instance. + */ Encoder* getEncoder(); + + /** + * @brief Gets the motor driver instance. + * @return A pointer to the motor driver instance. + */ MotorDriver* getDriver(); - void usbSuspend(); // Called on usb disconnect and suspend - void usbResume(); // Called on usb resume + /** + * @brief Called on USB disconnect and suspend. + */ + void usbSuspend(); + + /** + * @brief Called on USB resume. Enables the motor driver. + */ + void usbResume(); + /** + * @brief Saves axis settings to flash memory. + * @override from PersistentStorage + */ void saveFlash() override; + + /** + * @brief Restores axis settings from flash memory. + * @override from PersistentStorage + */ void restoreFlash() override; - void prepareForUpdate(); // called before the effects are calculated - void updateDriveTorque(); //int32_t effectTorque); + /** + * @brief Prepares the axis for an update cycle. Called from the main loop before effects are calculated. + * Reads the encoder, scales the value, and updates metrics (speed, acceleration). + */ + void prepareForUpdate(); + + /** + * @brief Sends the final calculated torque to the motor driver. + */ + void updateDriveTorque(); + + /** + * @brief Triggers an emergency stop. Disables the motor driver. + * @param reset If true, resets the axis after stopping. + */ void emergencyStop(bool reset); + /** + * @brief Sets the position of the axis. + * @param val The new position value. + */ void setPos(uint16_t val); + + /** + * @brief Zeros the current encoder position. + */ void zeroPos(); + /** + * @brief Checks if FFB is globally active. + * @return true if FFB is active, false otherwise. + */ bool getFfbActive(); + /** + * @brief Scales an encoder value. + * @param angle The angle to scale. + * @param degrees The total degrees of rotation. + * @return A pair containing the scaled integer value and the float value. + */ std::pair scaleEncValue(float angle, uint16_t degrees); - float getEncAngle(Encoder *enc); - + /** + * @brief Gets the angle from the encoder. + * @param enc A pointer to the encoder. + * @return The angle in degrees. + */ + float getEncAngle(Encoder *enc); + + /** + * @brief Sets the maximum power (torque) of the motor. + * @param power The new power value. + */ void setPower(uint16_t power); - + /** + * @brief Callback for handling errors. + * @param error The error that occurred. + * @param cleared Whether the error has been cleared. + * @override from ErrorHandler + */ void errorCallback(const Error &error, bool cleared) override; - //ParseStatus command(ParsedCommand_old* cmd,std::string* reply) override; + /** + * @brief Registers the commands for this axis with the command handler. + */ void registerCommands(); - CommandStatus command(const ParsedCommand& cmd,std::vector& replies); - - ClassChooser drv_chooser; - ClassChooser enc_chooser; - + /** + * @brief Handles command line interface commands for this axis. + * @param cmd The parsed command. + * @param replies A vector of replies to be sent back. + * @return The status of the command execution. + * @override from CommandHandler + */ + CommandStatus command(const ParsedCommand& cmd,std::vector& replies) override; + + ClassChooser driverChooser; //!< Class chooser for motor drivers. + ClassChooser encoderChooser; //!< Class chooser for encoders. + + /** + * @brief Gets the last scaled encoder value. + * @return The last scaled encoder value. + */ int32_t getLastScaledEnc(); + + /** + * @brief Resets the metrics of the axis. + * @param new_pos The new position to reset to. + */ void resetMetrics(float new_pos); + + /** + * @brief Updates the metrics of the axis. + * @param new_pos The new position. + */ void updateMetrics(float new_pos); + + /** + * @brief Updates the idle spring force. + * @return The calculated idle spring force. + */ int32_t updateIdleSpringForce(); + + /** + * @brief Sets the idle spring strength. + * @param spring The new spring strength. + */ void setIdleSpringStrength(uint8_t spring); + + /** + * @brief Sets the strength and filter for an effect. + * @param val The new strength value. + * @param valToSet A reference to the value to be set. + * @param filter A reference to the biquad filter. + */ void setFxStrengthAndFilter(uint8_t val,uint8_t& valToSet, Biquad& filter); + + /** + * @brief Called before HID effects are calculated. + */ void calculateAxisEffects(bool ffb_on); - int32_t getTorque(); // current torque scaled as a 32 bit signed value - int16_t updateEndstop(); - int32_t calculateExpoTorque(int32_t torque); + /** + * @brief Gets the current torque. + * @return The current torque scaled as a 32-bit signed value. + */ + int32_t getTorque(); + /** + * @brief Calculate soft endstop effect. + */ + int16_t updateEndstop(); + + /** + * @brief Starts a force fade-in. + * @param start The starting force multiplier. + * @param fadeTime The duration of the fade-in. + */ void startForceFadeIn(float start = 0,float fadeTime = 0.5); metric_t* getMetrics(); + /** + * @brief Sets the HID FFB effect torque. + * @param torque The new FFB effect torque. + */ void setEffectTorque(int32_t torque); + + /** + * @brief Updates the total torque. + * @param totalTorque A pointer to the total torque value. + * @return true if the torque was updated, false otherwise. + */ bool updateTorque(int32_t* totalTorque); void updateSamplerate(float newSamplerate); void updateFilters(uint8_t profileId); + /** + * @brief Sets the gear ratio. + * @param numerator The numerator of the gear ratio. + * @param denominator The denominator of the gear ratio. + */ void setGearRatio(uint8_t numerator,uint8_t denominator); - static const std::vector> axis1_drivers; - static const std::vector> axis2_drivers; + static const std::vector> axis1_drivers; //!< List of available motor drivers for the first axis. + static const std::vector> axis2_drivers; //!< List of available motor drivers for the second axis. private: - // Axis damper is lower than default scale of HID Damper + // Internal constants const float AXIS_DAMPER_RATIO = INTERNAL_SCALER_DAMPER * INTERNAL_AXIS_DAMPER_SCALER / 255.0; const float AXIS_INERTIA_RATIO = INTERNAL_SCALER_INERTIA * INTERNAL_AXIS_INERTIA_SCALER / 255.0; - AxisFlashAddrs flashAddrs; - volatile Control_t* control; + // Private methods + /** + * @brief Sets the degrees of rotation for the axis. + * @param degrees The new range of rotation. + */ + void setDegrees(uint16_t degrees); + /** + * @brief Returns the current power setting of the axis. + * @return The power value. + */ + uint16_t getPower(); + /** + * @brief Returns the calculated torque scaler. + * @return The torque scaler value. + */ + bool isInverted(); - //TIM_HandleTypeDef *timer_update; - AxisConfig conf; + void updateTorqueScaler(); + float getTorqueScaler(); - std::unique_ptr drv = std::make_unique(); // dummy - std::shared_ptr enc = nullptr; + /** + * @brief Sets the ratio between game effects and endstop force. + * @param val The new ratio value (0-255). + */ + void setFxRatio(uint8_t val); + /** + * @brief Sets the exponential torque curve. + * @param val The new expo value. + */ + void setExpo(int val); - bool outOfBounds = false; + int32_t calculateExpoTorque(int32_t torque); + /** + * @brief Decodes the axis configuration from a 16-bit integer stored in flash. + * @param val The 16-bit encoded configuration value. + */ static AxisConfig decodeConfFromInt(uint16_t val); + /** + * @brief Encodes the axis configuration into a 16-bit integer for flash storage. + * @param conf The AxisConfig struct to encode. + * @return The encoded 16-bit value. + */ static uint16_t encodeConfToInt(AxisConfig conf); - const Error outOfBoundsError = Error(ErrorCode::axisOutOfRange,ErrorType::warning,"Axis out of bounds"); + // Member variables + AxisFlashAddresses flashAddresses; //!< Flash memory addresses for this axis. + volatile Control_t* control; //!< Pointer to the global control structure. + AxisConfig conf; //!< Configuration for this axis (driver and encoder types). + char axis; //!< Axis identifier ('X', 'Y', 'Z'). - float forceFadeTime = 1.0; - float forceFadeCurMult = 1.0; + std::unique_ptr drv = std::make_unique(); //!< Unique pointer to the active motor driver. + std::shared_ptr enc = nullptr; //!< Shared pointer to the active encoder. -#ifdef TMC4671DRIVER - TMC4671Limits tmclimits = TMC4671Limits({.pid_torque_flux_ddt = 32767, - .pid_uq_ud = 30000, - .pid_torque_flux = 30000, - .pid_acc_lim = 2147483647, - .pid_vel_lim = 2147483647, - .pid_pos_low = -2147483647, - .pid_pos_high = 2147483647}); -#endif - float encoderOffset = 0; // Offset for absolute encoders - uint16_t degreesOfRotation = 900; // How many degrees of range for the full gamepad range - uint16_t lastdegreesOfRotation = degreesOfRotation; // Used to store the previous value - uint16_t nextDegreesOfRotation = degreesOfRotation; // Buffer when changing range + bool outOfBounds = false; //!< Flag indicating if the axis is out of its valid range. + const Error outOfBoundsError = Error(ErrorCode::axisOutOfRange,ErrorType::warning,"Axis out of bounds"); //!< Error object for out-of-bounds condition. + + // Force fade-in effect + float forceFadeTime = 1.0; //!< Duration of the force fade-in in seconds. + float forceFadeCurMult = 1.0; //!< Current multiplier for the force fade-in. + + uint16_t degreesOfRotation = 900; //!< Current degrees of rotation. + uint16_t lastdegreesOfRotation = degreesOfRotation; //!< Previous degrees of rotation (for smooth transitions). + uint16_t nextDegreesOfRotation = degreesOfRotation; //!< Target degrees of rotation. // Limiters - uint16_t maxSpeedDegS = 0; // Set to non zero to enable. example 1000. 8b * 10? - //float maxAccelDegSS = 0; - uint32_t maxTorqueRateMS = 0; // 8b * 128? - float speedLimiterP = AXIS_SPEEDLIMITER_P; - float speedLimiterI = AXIS_SPEEDLIMITER_I; + uint16_t maxSpeedDegS = 0; //!< Maximum speed in degrees per second. 0 to disable. + uint32_t maxTorqueRateMS = 0; //!< Maximum torque rate of change per millisecond. 0 to disable. - float spdlimitreducerI = 0; - //float acclimitreducerI = 0; - //const uint8_t accelFactor = 10.0; // Conversion factor between internal and external acc limit + float speedLimiterP = AXIS_SPEEDLIMITER_P; //!< Proportional term for the speed limiter. + float speedLimiterI = AXIS_SPEEDLIMITER_I; //!< Integral term for the speed limiter. - void setDegrees(uint16_t degrees); + // Speed limiter PID + float spdlimitreducerI = 0; - uint16_t getPower(); - float getTorqueScaler(); - bool isInverted(); - char axis; + // Axis metrics + axis_metric_t metric; //!< Current and previous physical metrics of the axis. + float _lastSpeed = 0; //!< Instantaneous speed from the last cycle. + // Torque components + int32_t effectTorque = 0; //!< Torque from HID FFB effects. + int32_t axisEffectTorque = 0; //!< Torque from mechanical effects. - // Merge normalized - axis_metric_t metric; - float _lastSpeed = 0; - int32_t effectTorque = 0; - int32_t axisEffectTorque = 0; - uint8_t fx_ratio_i = 204; // Reduce effects to a certain ratio of the total power to have a margin for the endstop. 80% = 204 - uint16_t power = 5000; - float torqueScaler = 0; // power * fx_ratio as a ratio between 0 & 1 - float effect_margin_scaler = 0; - bool invertAxis = true; // By default most motors and encoders count up CCW while gamepads are counting up CW. - uint8_t endstopStrength = 127; // Sets how much extra torque per count above endstop is added. High = stiff endstop. Low = softer - const float endstopGain = 25; // Overall max endstop intensity + // Power and scaling + uint16_t power = 5000; //!< Maximum motor power/torque. + uint8_t fx_ratio_i = 204; //!< Ratio of HID effects vs. endstop effects (0-255). + float effect_margin_scaler = 0; //!< Scaler for HID effects based on effectRatio. + float torqueScaler = 0; //!< Final torque scaler based on power. + // Axis configuration + bool invertAxis = true; //!< Invert axis direction. + uint8_t endstopStrength = 127; //!< Stiffness of the endstop effect. + const float endstopGain = 25; //!< Overall maximum endstop intensity. - uint8_t idlespringstrength = 127; - int16_t idlespringclip = 0; - float idlespringscale = 0; - bool motorWasNotReady = true; + // Idle spring effect + uint8_t idlespringstrength = 127; //!< Strength of the idle spring. + int16_t idlespringclip = 0; //!< Maximum force for the idle spring. + float idlespringscale = 0; //!< Scaler for the idle spring force. + bool motorWasNotReady = true; //!< Flag to detect motor readiness transition. + // Filters // TODO tune these and check if it is really stable and beneficial to the FFB. index 4 placeholder - const std::array filterSpeedCst = { {{ 40, 55 }, { 70, 55 }, { 120, 55 }, {180, 55}} }; - const std::array filterAccelCst = { {{ 40, 30 }, { 55, 30 }, { 70, 30 }, {120, 55}} }; - const biquad_constant_t filterDamperCst = {60, 55}; - const biquad_constant_t filterFrictionCst = {50, 20}; - const biquad_constant_t filterInertiaCst = {20, 20}; - uint8_t filterProfileId = 1; // Default medium (1) as this is the most common encoder resolution and users can go lower or higher if required. - float filter_f = 1000; // 1khz default. should be set at runtime once the actual rate is known - const int32_t intFxClip = 20000; - uint8_t damperIntensity = 30; - - uint8_t frictionIntensity = 0; - uint8_t inertiaIntensity = 0; - + const std::array filterSpeedCst = { {{ 40, 55 }, { 70, 55 }, { 120, 55 }, {180, 55}} }; //!< Speed filter profiles. + const std::array filterAccelCst = { {{ 40, 30 }, { 55, 30 }, { 70, 30 }, {120, 55}} }; //!< Acceleration filter profiles. + const biquad_constant_t filterDamperCst = {60, 55}; //!< Damper filter constants. + const biquad_constant_t filterFrictionCst = {50, 20}; //!< Friction filter constants. + const biquad_constant_t filterInertiaCst = {20, 20}; //!< Inertia filter constants. + uint8_t filterProfileId = 1; //!< Currently selected filter profile ID. + float filter_f = 1000.0; // 1khz + const int32_t intFxClip = 20000; //!< Clipping value for internal effects. + + // Internal effects intensity + uint8_t damperIntensity = 30; //!< Intensity of the internal damper effect. + uint8_t frictionIntensity = 0; //!< Intensity of the internal friction effect. + uint8_t inertiaIntensity = 0; //!< Intensity of the internal inertia effect. + + // Biquad filter instances Biquad speedFilter = Biquad(BiquadType::lowpass, filterSpeedCst[filterProfileId].freq/filter_f, filterSpeedCst[filterProfileId].q/100.0, 0.0); Biquad accelFilter = Biquad(BiquadType::lowpass, filterAccelCst[filterProfileId].freq/filter_f, filterAccelCst[filterProfileId].q/100.0, 0.0); - Biquad damperFilter = Biquad(BiquadType::lowpass, filterDamperCst.freq/filter_f, filterDamperCst.q / 100.0, 0.0); // enable on class constructor - Biquad frictionFilter = Biquad(BiquadType::lowpass, filterFrictionCst.freq/filter_f, filterFrictionCst.q / 100.0, 0.0); // enable on class constructor - Biquad inertiaFilter = Biquad(BiquadType::lowpass, filterInertiaCst.freq/filter_f, filterInertiaCst.q / 100.0, 0.0); // enable on class constructor - - - void setFxRatio(uint8_t val); - void updateTorqueScaler(); - - void setExpo(int val); - - - GearRatio_t gearRatio; - - int expoValInt = 0; // expo v = val*2 => v<0 ? 1/-v : v - float expo = 1; - float expoScaler = 50; // 0.28 to 3.54 - + Biquad damperFilter = Biquad(BiquadType::lowpass, filterDamperCst.freq/filter_f, filterDamperCst.q / 100.0, 0.0); + Biquad frictionFilter = Biquad(BiquadType::lowpass, filterFrictionCst.freq/filter_f, filterFrictionCst.q / 100.0, 0.0); + Biquad inertiaFilter = Biquad(BiquadType::lowpass, filterInertiaCst.freq/filter_f, filterInertiaCst.q / 100.0, 0.0); + + // Post-processing + GearRatio_t gearRatio; //!< Gear ratio between encoder and axis. + int expoValInt = 0; //!< Raw integer value for the expo curve. + float expo = 1; //!< Calculated exponent for the torque curve. + float expoScaler = 50; //!< Scaler for the expo calculation : 0.28 to 3.54 }; #endif /* SRC_AXIS_H_ */ diff --git a/Firmware/FFBoard/Inc/EffectsCalculator.h b/Firmware/FFBoard/Inc/EffectsCalculator.h index feff5f0a0..5e43c9285 100644 --- a/Firmware/FFBoard/Inc/EffectsCalculator.h +++ b/Firmware/FFBoard/Inc/EffectsCalculator.h @@ -8,7 +8,6 @@ #ifndef EFFECTSCALCULATOR_H_ #define EFFECTSCALCULATOR_H_ -//#include "ClassChooser.h" #include "ffb_defs.h" #include "PersistentStorage.h" #include "CommandHandler.h" @@ -27,7 +26,9 @@ class Axis; struct metric_t; -// default effect gains +/** + * @brief Default effect gains. + */ struct effect_gain_t { uint8_t friction = 254; uint8_t spring = 64; @@ -35,24 +36,33 @@ struct effect_gain_t { uint8_t inertia = 127; }; +/** + * @brief Default effect scalers. + */ struct effect_scaler_t { - float friction = 1.0; //0.4 * 40; + float friction = 1.0; float spring = 16.0; - float damper = 4.0; //2 * 40 * 2 - float inertia = 2.0;//0.5 * 40; + float damper = 4.0; + float inertia = 2.0; }; +/** + * @brief Biquad filter constants for different effect types. + */ struct effect_biquad_t { - biquad_constant_t constant = { 500, 70 }; - biquad_constant_t friction = { 50, 20 }; - biquad_constant_t damper = { 30, 40 }; - biquad_constant_t inertia = { 15, 20 }; + biquad_constant_t constant = { 500, 70 }; + biquad_constant_t friction = { 50, 20 }; + biquad_constant_t damper = { 30, 40 }; + biquad_constant_t inertia = { 15, 20 }; }; +/** + * @brief Holds statistics for an effect type. + */ struct effect_stat_t { - std::array current={0}; - std::array max={0}; - uint16_t nb=0; + std::array current={0}; //!< Current force for each axis. + std::array max={0}; //!< Maximum force for each axis. + uint16_t nb=0; //!< Number of samples. }; enum class EffectsCalculator_commands : uint32_t { @@ -62,6 +72,9 @@ enum class EffectsCalculator_commands : uint32_t { monitorEffect, effectsDetails, effectsForces, }; +/** + * @brief This class calculates the final torque for each axis based on the received HID FFB effects. + */ class EffectsCalculator: public PersistentStorage, public CommandHandler, cpp_freertos::Thread { @@ -73,93 +86,215 @@ class EffectsCalculator: public PersistentStorage, const ClassIdentifier getInfo(); const ClassType getClassType() override {return ClassType::Internal;}; - void saveFlash(); - void restoreFlash(); + /** + * @brief Saves effect settings to flash memory. + * @override from PersistentStorage + */ + void saveFlash() override; + /** + * @brief Restores effect settings from flash memory. + * @override from PersistentStorage + */ + void restoreFlash() override; + /** + * @brief Checks if the effects calculator is active. + * @return true if active, false otherwise. + */ bool isActive(); + /** + * @brief Sets the active state of the effects calculator. + * @param active true to activate, false to deactivate. + */ void setActive(bool active); + /** + * @brief Calculates the combined force of all active effects for each axis. + * @param axes A vector of unique pointers to the axis objects. + */ void calculateEffects(std::vector> &axes); + /** + * @brief Sets the filters for a specific effect. + * @param effect A pointer to the FFB_Effect object. + */ virtual void setFilters(FFB_Effect* effect); + /** + * @brief Sets the global gain for all effects. + * @param gain The gain value (0-255). + */ void setGain(uint8_t gain); + /** + * @brief Gets the global gain. + * @return The global gain value. + */ uint8_t getGain(); + /** + * @brief Logs an effect type as active or inactive. + * @param type The effect type ID. + * @param remove true to log as inactive, false as active. + */ void logEffectType(uint8_t type,bool remove = false); - //void setDirectionEnableMask(uint8_t mask); + /** + * @brief Calculates statistics for an effect type. + * @param type The effect type ID. + * @param force The calculated force value. + * @param axis The axis ID. + */ void calcStatsEffectType(uint8_t type, int32_t force,uint8_t axis); + /** + * @brief Logs the state of an effect (active, inactive, etc.). + * @param type The effect type ID. + * @param state The state value. + */ void logEffectState(uint8_t type,uint8_t state); + /** + * @brief Resets the log of active effects. + * @param reinit If true, reinitializes the log. + */ void resetLoggedActiveEffects(bool reinit); + /** + * @brief Finds a free effect slot in the effects array. + * @param type The effect type ID. + * @return The index of the free slot, or -1 if none found. + */ int32_t find_free_effect(uint8_t type); + /** + * @brief Frees an effect slot. + * @param idx The index of the slot to free. + */ void free_effect(uint16_t idx); - CommandStatus command(const ParsedCommand& cmd,std::vector& replies); + /** + * @brief Handles CLI commands for the effects calculator. + * @param cmd The parsed command. + * @param replies A vector of replies to be sent back. + * @return The status of the command execution. + * @override from CommandHandler + */ + CommandStatus command(const ParsedCommand& cmd,std::vector& replies) override; virtual std::string getHelpstring() { return "Controls internal FFB effects"; } static const uint32_t max_effects = MAX_EFFECTS; - std::array effects; // Main effects storage + std::array effects; //!< Main effects storage array. - // Thread impl - void Run(); + /** + * @brief Main thread function for the effects calculator. + */ + void Run() override; - void updateSamplerate(float newSamplerate); // Must be called if update rate is changed to update filters and effects + /** + * @brief Updates the internal sample rate and recalculates filters. + * @param newSamplerate The new sample rate in Hz. + */ + void updateSamplerate(float newSamplerate); protected: private: - //uint8_t directionEnableMask = 0; // Filters - effect_biquad_t filter[2]; // 0 is the default profile and the custom for CFFilter, CUSTOM_PROFILE_ID is the custom slot - uint8_t filterProfileId = 0; - uint32_t calcfrequency = 1000; // HID frequency 1khz - const float qfloatScaler = 0.01; + effect_biquad_t filter[2]; //!< Filter profiles: 0 is default, CUSTOM_PROFILE_ID is custom. + uint8_t filterProfileId = 0; //!< Currently selected filter profile ID. + uint32_t calcfrequency = 1000; //!< Calculation frequency in Hz (default 1kHz). + const float qfloatScaler = 0.01; //!< Scaler for Q factor. - // Rescale factor for conditional effect to boost or decrease the intensity - uint8_t global_gain = 0xff; - effect_gain_t gain; - effect_scaler_t scaler; - uint8_t frictionPctSpeedToRampup = 25; // define the max value of the range (0..5% of maxspeed) where torque is rampup on friction + // Intensity and scaling + uint8_t global_gain = 0xff; //!< Global gain for all effects. + effect_gain_t gain; //!< Individual effect type gains. + effect_scaler_t scaler; //!< Individual effect type scalers. + uint8_t frictionPctSpeedToRampup = 25; //!< Friction ramp-up threshold (percentage of max speed). // FFB status - bool effects_active = false; // If FFB is on - uint32_t effects_used = 0; - std::array effects_stats; // [0..12 effect types] - std::array effects_statslast; // [0..12 effect types] - bool isMonitorEffect = false; + bool effects_active = false; //!< true if FFB is active. + uint32_t effects_used = 0; //!< Mask of currently used effect slots. + std::array effects_stats; //!< Statistics for each effect type (current cycle). + std::array effects_statslast; //!< Statistics from the previous cycle. + bool isMonitorEffect = false; //!< Flag for effect monitoring. + /** + * @brief Calculates the force component for a specific effect on a specific axis. + */ int32_t calcComponentForce(FFB_Effect *effect, int32_t forceVector, std::vector> &axes, uint8_t axis); + /** + * @brief Calculates the force for a non-conditional effect (e.g., Constant Force, Sine). + */ int32_t calcNonConditionEffectForce(FFB_Effect* effect); + /** + * @brief Returns the speed ramp-up factor for friction. + */ float speedRampupPct(); + /** + * @brief Calculates the force for a conditional effect (e.g., Spring, Damper, Friction). + */ int32_t calcConditionEffectForce(FFB_Effect *effect, float metric, uint8_t gain, uint8_t idx, float scale, float angle_ratio); + /** + * @brief Calculates the envelope magnitude for an effect. + */ int32_t getEnvelopeMagnitude(FFB_Effect *effect); + /** + * @brief Lists the currently used effects as a string. + */ std::string listEffectsUsed(bool details = false,uint8_t axis = 0); - //std::string listForceEffects(); + /** + * @brief Checks and updates filter coefficients. + */ void checkFilterCoeff(biquad_constant_t *filter, uint32_t freq,uint8_t q); + /** + * @brief Updates filter settings for a specific effect type. + */ void updateFilterSettingsForEffects(uint8_t type_effect); }; /** - * Helper interface class for common effects calculator related control functions + * @brief Helper interface class for common effects calculator related control functions. */ class EffectsControlItf{ public: - virtual void set_FFB(bool state) = 0; // Enables or disables FFB + /** + * @brief Enables or disables FFB globally. + */ + virtual void set_FFB(bool state) = 0; virtual void stop_FFB(){set_FFB(false);}; virtual void start_FFB(){set_FFB(true);}; + /** + * @brief Resets all FFB effects. + */ virtual void reset_ffb() = 0; - virtual uint32_t getConstantForceRate(); // Returns an estimate of the constant force effect update rate in hz - virtual uint32_t getRate(); // Returns an estimate of the overall effect update speed in hz + /** + * @brief Returns the estimated constant force update rate in Hz. + */ + virtual uint32_t getConstantForceRate(); + /** + * @brief Returns the estimated overall effect update speed in Hz. + */ + virtual uint32_t getRate(); + /** + * @brief Checks if FFB is active. + */ virtual bool getFfbActive() = 0; + /** + * @brief Sets the global FFB gain. + */ virtual void set_gain(uint8_t gain) = 0; + /** + * @brief Event handler for constant force updates. + */ virtual void cfUpdateEvent(); + /** + * @brief Event handler for effect updates. + */ virtual void fxUpdateEvent(); - virtual void updateSamplerate(float newSamplerate) = 0; // Should be called when update loop rate is changed + /** + * @brief Updates the sample rate for filters and effects. + */ + virtual void updateSamplerate(float newSamplerate) = 0; private: - FastMovingAverage fxPeriodAvg{5}; - FastMovingAverage cfUpdatePeriodAvg{5}; + FastMovingAverage fxPeriodAvg{5}; //!< Average period between effect updates. + FastMovingAverage cfUpdatePeriodAvg{5}; //!< Average period between constant force updates. - uint32_t lastFxUpdate = 0; - uint32_t lastCfUpdate = 0; + uint32_t lastFxUpdate = 0; //!< Time of last effect update. + uint32_t lastCfUpdate = 0; //!< Time of last constant force update. }; #endif /* EFFECTSCALCULATOR_H_ */ diff --git a/Firmware/FFBoard/Inc/HidFFB.h b/Firmware/FFBoard/Inc/HidFFB.h index 7dae12394..3e776958e 100644 --- a/Firmware/FFBoard/Inc/HidFFB.h +++ b/Firmware/FFBoard/Inc/HidFFB.h @@ -20,54 +20,139 @@ #define Z_AXIS_ENABLE 4 #define DIRECTION_ENABLE(AXES) (1 << AXES) +/** + * @brief This class handles HID FFB reports from the USB host. + * It decodes the reports and updates the effects in the EffectsCalculator. + */ class HidFFB: public UsbHidHandler, public EffectsControlItf { public: + /** + * @brief Construct a new HidFFB object. + * @param ec Shared pointer to the EffectsCalculator. + * @param axisCount Number of FFB axes. + */ HidFFB(std::shared_ptr ec,uint8_t axisCount); virtual ~HidFFB(); + /** + * @brief Handles HID OUT reports. + * @override from UsbHidHandler + */ void hidOut(uint8_t report_id, hid_report_type_t report_type,const uint8_t* buffer, uint16_t bufsize) override; + /** + * @brief Handles HID GET reports. + * @override from UsbHidHandler + */ uint16_t hidGet(uint8_t report_id, hid_report_type_t report_type,uint8_t* buffer, uint16_t reqlen) override; - bool getFfbActive(); + /** + * @brief Checks if FFB is active. + * @return true if active, false otherwise. + */ + bool getFfbActive() override; + /** + * @brief Sends a HID report. + */ static bool HID_SendReport(uint8_t *report,uint16_t len); - void reset_ffb(); - void start_FFB(); - void stop_FFB(); - void set_FFB(bool state); - void set_gain(uint8_t gain); + /** + * @brief Resets all FFB effects. + */ + void reset_ffb() override; + /** + * @brief Starts FFB. + */ + void start_FFB() override; + /** + * @brief Stops FFB. + */ + void stop_FFB() override; + /** + * @brief Enables or disables FFB. + * @param state true to enable, false to disable. + */ + void set_FFB(bool state) override; + /** + * @brief Sets the global FFB gain. + * @param gain The gain value (0-255). + */ + void set_gain(uint8_t gain) override; + /** + * @brief Sends a status report for a specific effect. + */ void sendStatusReport(uint8_t effect); + /** + * @brief Sets the direction enable mask. + */ void setDirectionEnableMask(uint8_t mask); - void updateSamplerate(float newSamplerate); + /** + * @brief Updates the internal sample rate. + * @param newSamplerate The new sample rate in Hz. + */ + void updateSamplerate(float newSamplerate) override; private: // HID std::shared_ptr effects_calc; - std::array& effects; // Must be passed in constructor + std::array& effects; //!< Reference to the effects array in EffectsCalculator. + + /** + * @brief Handles the creation of a new effect. + */ void new_effect(FFB_CreateNewEffect_Feature_Data_t* effect); + /** + * @brief Handles freeing an effect slot. + */ void free_effect(uint16_t id); + /** + * @brief Decodes the FFB control command. + */ void ffb_control(uint8_t cmd); + /** + * @brief Decodes the set effect report. + */ void set_effect(FFB_SetEffect_t* effect); + /** + * @brief Decodes the set condition report. + */ void set_condition(FFB_SetCondition_Data_t* cond); + /** + * @brief Decodes the set envelope report. + */ void set_envelope(FFB_SetEnvelope_Data_t* report); + /** + * @brief Decodes the set ramp report. + */ void set_ramp(FFB_SetRamp_Data_t* report); + /** + * @brief Decodes the set constant force report. + */ void set_constant_effect(FFB_SetConstantForce_Data_t* effect); + /** + * @brief Decodes the set periodic report. + */ void set_periodic(FFB_SetPeriodic_Data_t* report); + /** + * @brief Decodes the effect operation report (start/stop/pause). + */ void set_effect_operation(FFB_EffOp_Data_t* report); + /** + * @brief Updates the filters for an effect. + */ void set_filters(FFB_Effect* effect); - uint8_t directionEnableMask; // Has to be adjusted if bit is not last bit after axis enable bits - uint16_t used_effects = 0; - bool ffb_active = false; - FFB_BlockLoad_Feature_Data_t blockLoad_report; - FFB_PIDPool_Feature_Data_t pool_report; + uint8_t directionEnableMask; //!< Mask for enabled directions. + uint16_t used_effects = 0; //!< Number of currently used effect slots. + bool ffb_active = false; //!< true if FFB is active. + FFB_BlockLoad_Feature_Data_t blockLoad_report; //!< HID Feature report: Block Load. + FFB_PIDPool_Feature_Data_t pool_report; //!< HID Feature report: PID Pool. - reportFFB_status_t reportFFBStatus; + reportFFB_status_t reportFFBStatus; //!< Holds the current FFB status. - uint8_t axisCount; + uint8_t axisCount; //!< Number of FFB axes. }; #endif /* HIDFFB_H_ */ diff --git a/Firmware/FFBoard/Src/Axis.cpp b/Firmware/FFBoard/Src/Axis.cpp index 41f487b28..54b2b83a0 100644 --- a/Firmware/FFBoard/Src/Axis.cpp +++ b/Firmware/FFBoard/Src/Axis.cpp @@ -86,30 +86,30 @@ const std::vector> Axis::axis2_drivers = /** * Axis class manages motor drivers and passes effect torque to the motor drivers */ -Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_AXIS), drv_chooser(MotorDriver::all_drivers),enc_chooser{Encoder::all_encoders} +Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_AXIS), driverChooser(MotorDriver::all_drivers),encoderChooser{Encoder::all_encoders} { this->axis = axis; this->control = control; if (axis == 'X') { - drv_chooser = ClassChooser(axis1_drivers); + driverChooser = ClassChooser(axis1_drivers); setInstance(0); - this->flashAddrs = AxisFlashAddrs({ADR_AXIS1_CONFIG, ADR_AXIS1_MAX_SPEED, ADR_AXIS1_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS1_CONFIG, ADR_AXIS1_MAX_SPEED, ADR_AXIS1_MAX_ACCEL, ADR_AXIS1_ENDSTOP, ADR_AXIS1_POWER, ADR_AXIS1_DEGREES,ADR_AXIS1_EFFECTS1,ADR_AXIS1_EFFECTS2,ADR_AXIS1_ENC_RATIO, ADR_AXIS1_SPEEDACCEL_FILTER,ADR_AXIS1_POSTPROCESS1}); } else if (axis == 'Y') { - drv_chooser = ClassChooser(axis2_drivers); + driverChooser = ClassChooser(axis2_drivers); setInstance(1); - this->flashAddrs = AxisFlashAddrs({ADR_AXIS2_CONFIG, ADR_AXIS2_MAX_SPEED, ADR_AXIS2_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS2_CONFIG, ADR_AXIS2_MAX_SPEED, ADR_AXIS2_MAX_ACCEL, ADR_AXIS2_ENDSTOP, ADR_AXIS2_POWER, ADR_AXIS2_DEGREES,ADR_AXIS2_EFFECTS1,ADR_AXIS2_EFFECTS2, ADR_AXIS2_ENC_RATIO, ADR_AXIS2_SPEEDACCEL_FILTER,ADR_AXIS2_POSTPROCESS1}); } else if (axis == 'Z') { setInstance(2); - this->flashAddrs = AxisFlashAddrs({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, ADR_AXIS3_ENDSTOP, ADR_AXIS3_POWER, ADR_AXIS3_DEGREES,ADR_AXIS3_EFFECTS1,ADR_AXIS3_EFFECTS2,ADR_AXIS3_ENC_RATIO, ADR_AXIS3_SPEEDACCEL_FILTER,ADR_AXIS3_POSTPROCESS1}); } @@ -166,10 +166,10 @@ void Axis::registerCommands(){ * Read parameters from flash and restore settings */ void Axis::restoreFlash(){ - //NormalizedAxis::restoreFlash(); + // TODO: This seems to be a remnant of a previous architecture (NormalizedAxis). Confirm and remove. // read all constants uint16_t value; - if (Flash_Read(flashAddrs.config, &value)){ + if (Flash_Read(flashAddresses.config, &value)){ this->conf = Axis::decodeConfFromInt(value); }else{ pulseErrLed(); @@ -178,31 +178,31 @@ void Axis::restoreFlash(){ setDrvType(this->conf.drvtype); setEncType(this->conf.enctype); - if (Flash_Read(flashAddrs.maxSpeed, &value)){ + if (Flash_Read(flashAddresses.maxSpeed, &value)){ this->maxSpeedDegS = value; }else{ pulseErrLed(); } -// -// if (Flash_Read(flashAddrs.maxAccel, &value)){ -// this->maxTorqueRateMS = value; -// }else{ -// pulseErrLed(); -// } + + if (Flash_Read(flashAddresses.maxAccel, &value)){ + this->maxTorqueRateMS = value; + }else{ + pulseErrLed(); + } uint16_t esval, power; - if(Flash_Read(flashAddrs.endstop, &esval)) { + if(Flash_Read(flashAddresses.endstop, &esval)) { fx_ratio_i = esval & 0xff; endstopStrength = (esval >> 8) & 0xff; } - if(Flash_Read(flashAddrs.power, &power)){ + if(Flash_Read(flashAddresses.power, &power)){ setPower(power); } uint16_t deg_t; - if(Flash_Read(flashAddrs.degrees, °_t)){ + if(Flash_Read(flashAddresses.degrees, °_t)){ this->degreesOfRotation = deg_t & 0x7fff; this->invertAxis = (deg_t >> 15) & 0x1; setDegrees(degreesOfRotation); @@ -210,25 +210,25 @@ void Axis::restoreFlash(){ uint16_t effects; - if(Flash_Read(flashAddrs.effects1, &effects)){ + if(Flash_Read(flashAddresses.effects1, &effects)){ setIdleSpringStrength(effects & 0xff); setFxStrengthAndFilter((effects >> 8) & 0xff,damperIntensity,damperFilter); }else{ setIdleSpringStrength(idlespringstrength); // Use default } - if(Flash_Read(flashAddrs.effects2, &effects)){ + if(Flash_Read(flashAddresses.effects2, &effects)){ setFxStrengthAndFilter(effects & 0xff,frictionIntensity,frictionFilter); setFxStrengthAndFilter((effects >> 8) & 0xff,inertiaIntensity,inertiaFilter); } uint16_t ratio; - if(Flash_Read(flashAddrs.encoderRatio, &ratio)){ + if(Flash_Read(flashAddresses.encoderRatio, &ratio)){ setGearRatio(ratio & 0xff, (ratio >> 8) & 0xff); } uint16_t filterStorage; - if (Flash_Read(flashAddrs.speedAccelFilter, &filterStorage)) + if (Flash_Read(flashAddresses.speedAccelFilter, &filterStorage)) { uint8_t profile = filterStorage & 0xFF; this->filterProfileId = profile; @@ -239,31 +239,31 @@ void Axis::restoreFlash(){ } uint16_t pp1; - if(Flash_Read(flashAddrs.postprocess1, &pp1)){ + if(Flash_Read(flashAddresses.postprocess1, &pp1)){ setExpo((int8_t)(pp1 & 0xff)); } } // Saves parameters to flash. void Axis::saveFlash(){ - //NormalizedAxis::saveFlash(); - Flash_Write(flashAddrs.config, Axis::encodeConfToInt(this->conf)); - Flash_Write(flashAddrs.maxSpeed, this->maxSpeedDegS); -// Flash_Write(flashAddrs.maxAccel, (uint16_t)(this->maxTorqueRateMS)); - - Flash_Write(flashAddrs.endstop, fx_ratio_i | (endstopStrength << 8)); - Flash_Write(flashAddrs.power, power); - Flash_Write(flashAddrs.degrees, (degreesOfRotation & 0x7fff) | (invertAxis << 15)); - Flash_Write(flashAddrs.effects1, idlespringstrength | (damperIntensity << 8)); - Flash_Write(flashAddrs.effects2, frictionIntensity | (inertiaIntensity << 8)); - Flash_Write(flashAddrs.encoderRatio, gearRatio.numerator | (gearRatio.denominator << 8)); + // TODO: This seems to be a remnant of a previous architecture (NormalizedAxis). Confirm and remove. + Flash_Write(flashAddresses.config, Axis::encodeConfToInt(this->conf)); + Flash_Write(flashAddresses.maxSpeed, this->maxSpeedDegS); + Flash_Write(flashAddresses.maxAccel, (uint16_t)(this->maxTorqueRateMS)); + + Flash_Write(flashAddresses.endstop, fx_ratio_i | (endstopStrength << 8)); + Flash_Write(flashAddresses.power, power); + Flash_Write(flashAddresses.degrees, (degreesOfRotation & 0x7fff) | (invertAxis << 15)); + Flash_Write(flashAddresses.effects1, idlespringstrength | (damperIntensity << 8)); + Flash_Write(flashAddresses.effects2, frictionIntensity | (inertiaIntensity << 8)); + Flash_Write(flashAddresses.encoderRatio, gearRatio.numerator | (gearRatio.denominator << 8)); // save CF biquad uint16_t filterStorage = (uint16_t)this->filterProfileId & 0xFF; - Flash_Write(flashAddrs.speedAccelFilter, filterStorage); + Flash_Write(flashAddresses.speedAccelFilter, filterStorage); // Postprocessing - Flash_Write(flashAddrs.postprocess1, expoValInt & 0xff); + Flash_Write(flashAddresses.postprocess1, expoValInt & 0xff); } @@ -375,16 +375,6 @@ void Axis::setPower(uint16_t power) { this->power = power; updateTorqueScaler(); -#ifdef TMC4671DRIVER - // Update hardware limits for TMC for safety - TMC4671 *drv = dynamic_cast(this->drv.get()); - if (drv != nullptr) - { - //tmclimits.pid_uq_ud = power; - //tmclimits.pid_torque_flux = power; - drv->setTorqueLimit(power); - } -#endif } @@ -393,12 +383,13 @@ void Axis::setPower(uint16_t power) */ void Axis::setDrvType(uint8_t drvtype) { - if (!drv_chooser.isValidClassId(drvtype)) + if (!driverChooser.isValidClassId(drvtype)) { return; } cpp_freertos::CriticalSection::Enter(); - this->drv.reset(drv_chooser.Create((uint16_t)drvtype)); + MotorDriver* drv = driverChooser.Create((uint16_t)drvtype); + this->drv.reset(drv); if (drv == nullptr) { cpp_freertos::CriticalSection::Exit(); @@ -410,12 +401,7 @@ void Axis::setDrvType(uint8_t drvtype) if(!this->drv->hasIntegratedEncoder()){ this->drv->setEncoder(this->enc); } -#ifdef TMC4671DRIVER - if (dynamic_cast(drv.get())) - { - setupTMC4671(); - } -#endif + if (!tud_connected()) { control->usb_disabled = false; @@ -428,25 +414,6 @@ void Axis::setDrvType(uint8_t drvtype) cpp_freertos::CriticalSection::Exit(); } -#ifdef TMC4671DRIVER -// Special tmc setup methods -void Axis::setupTMC4671() -{ - TMC4671 *drv = static_cast(this->drv.get()); -// drv->setAxis(axis); - drv->setExternalEncoderAllowed(true); - drv->restoreFlash(); - tmclimits.pid_torque_flux = getPower(); - drv->setLimits(tmclimits); - //drv->setBiquadTorque(TMC4671Biquad(tmcbq_500hz_07q_25k)); - - - // Enable driver - - drv->setMotionMode(MotionMode::torque); - drv->Start(); // Start thread -} -#endif /** @@ -454,11 +421,11 @@ void Axis::setupTMC4671() */ void Axis::setEncType(uint8_t enctype) { - if (enc_chooser.isValidClassId(enctype) && !drv->hasIntegratedEncoder()) + if (encoderChooser.isValidClassId(enctype) && !drv->hasIntegratedEncoder()) { this->conf.enctype = (enctype); - this->enc = std::shared_ptr(enc_chooser.Create(enctype)); // Make new encoder + this->enc = std::shared_ptr(encoderChooser.Create(enctype)); // Make new encoder if(drv && !drv->hasIntegratedEncoder()) this->drv->setEncoder(this->enc); }else{ @@ -747,11 +714,6 @@ bool Axis::updateTorque(int32_t* totalTorque) { float speedreducer = (float)((metric.current.speed*torqueSign) - (float)maxSpeedDegS) * ((float)0x7FFF / maxSpeedDegS); spdlimitreducerI = clip( spdlimitreducerI + ((speedreducer * speedLimiterI) * torqueScaler),0,power); - // Accel limit. Not really useful. Maybe replace with torque slew rate limit? -// float accreducer = (float)((metric.current.accel*torqueSign) - (float)maxAccelDegSS) * getAccelScalerNormalized(); -// acclimitreducerI = clip( acclimitreducerI + ((accreducer * 0.02) * torqueScaler),0,power); - - // Only reduce torque. Don't invert it to prevent oscillation float torqueReduction = speedreducer * speedLimiterP + spdlimitreducerI;// accreducer * 0.025 + acclimitreducerI if(torque > 0){ @@ -764,9 +726,9 @@ bool Axis::updateTorque(int32_t* totalTorque) { } // Torque slew rate limiter if(maxTorqueRateMS > 0){ - torque = clip(torque, metric.previous.torque - maxTorqueRateMS,metric.previous.torque + maxTorqueRateMS); + torque = clip(torque, metric.previous.torque - (int32_t)maxTorqueRateMS,metric.previous.torque + (int32_t)maxTorqueRateMS); } -// if(torque - metric.previous.torque) + if(outOfBounds){ torque = 0; } @@ -864,14 +826,6 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& case Axis_commands::degrees: handleGetSetFunc(cmd, replies, degreesOfRotation, &Axis::setDegrees,this); -// if (cmd.type == CMDtype::get) -// { -// replies.emplace_back(degreesOfRotation); -// } -// else if (cmd.type == CMDtype::set) -// { -// setDegrees(cmd.val); -// } break; case Axis_commands::esgain: @@ -940,7 +894,7 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& case Axis_commands::enctype: if(cmd.type == CMDtype::info){ - enc_chooser.replyAvailableClasses(replies,this->getEncType()); + encoderChooser.replyAvailableClasses(replies,this->getEncType()); }else if(cmd.type == CMDtype::get){ replies.emplace_back(this->getEncType()); }else if(cmd.type == CMDtype::set){ @@ -950,7 +904,7 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& case Axis_commands::drvtype: if(cmd.type == CMDtype::info){ - drv_chooser.replyAvailableClasses(replies,this->getDrvType()); + driverChooser.replyAvailableClasses(replies,this->getDrvType()); }else if(cmd.type == CMDtype::get){ replies.emplace_back(this->getDrvType()); }else if(cmd.type == CMDtype::set){ @@ -1043,13 +997,6 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& if(this->getEncoder() != nullptr){ cpr = this->getEncoder()->getCpr(); } -//#ifdef TMC4671DRIVER // CPR should be consistent with position. Maybe change TMC to prescale to encoder count or correct readout in UI -// TMC4671 *tmcdrv = dynamic_cast(this->drv.get()); // Special case for TMC. Get the actual encoder resolution -// if (tmcdrv && tmcdrv->hasIntegratedEncoder()) -// { -// cpr = tmcdrv->getEncCpr(); -// } -//#endif replies.emplace_back(cpr); }else{ return CommandStatus::ERR; diff --git a/Firmware/FFBoard/Src/EffectsCalculator.cpp b/Firmware/FFBoard/Src/EffectsCalculator.cpp index 5684fd975..1fe388673 100644 --- a/Firmware/FFBoard/Src/EffectsCalculator.cpp +++ b/Firmware/FFBoard/Src/EffectsCalculator.cpp @@ -104,11 +104,13 @@ An inertia condition uses axis acceleration as the metric. void EffectsCalculator::calculateEffects(std::vector> &axes) { for (auto &axis : axes) { - axis->setEffectTorque(0); axis->calculateAxisEffects(isActive()); } if(!isActive()){ + for (auto &axis : axes) { + axis->setEffectTorque(0); + } return; } @@ -159,8 +161,8 @@ void EffectsCalculator::calculateEffects(std::vector> &axe // Apply summed force to axes for(uint8_t i=0 ; i < axisCount ; i++) { - int32_t force = clip(forces[i], -0x7fff, 0x7fff); // Clip - axes[i]->setEffectTorque(force); + forces[i] = clip(forces[i], -0x7fff, 0x7fff); // Clip at effects summ + axes[i]->setEffectTorque(forces[i]); } effects_statslast = effects_stats; @@ -715,7 +717,7 @@ std::string EffectsCalculator::listEffectsUsed(bool details,uint8_t axis){ } else { bool firstItem = true; - for (int i=0;i < 12; i++) { + for (int i=0; i< 12; i++) { if (!firstItem) effects_list += ", "; effects_list += "{\"max\":" + std::to_string(effects_stats[i].max[axis]); effects_list += ", \"curr\":" + std::to_string(effects_stats[i].current[axis]); From 67e0270482e7516d80967ad75b99419325fe7768 Mon Sep 17 00:00:00 2001 From: Vincent MANOUKIAN <10980775+manoukianv@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:39:57 +0200 Subject: [PATCH 2/8] feat: add torque slew rate limiter and driver calibration --- Firmware/FFBoard/Inc/Axis.h | 16 ++++++- Firmware/FFBoard/Inc/MotorDriver.h | 69 +++++++++++++++++++++++--- Firmware/FFBoard/Src/Axis.cpp | 72 ++++++++++++++++++++++++---- Firmware/FFBoard/Src/MotorDriver.cpp | 6 +++ Firmware/scripts/memory_map.csv | 11 ++--- 5 files changed, 150 insertions(+), 24 deletions(-) diff --git a/Firmware/FFBoard/Inc/Axis.h b/Firmware/FFBoard/Inc/Axis.h index 32b8bb8af..958c4bae1 100644 --- a/Firmware/FFBoard/Inc/Axis.h +++ b/Firmware/FFBoard/Inc/Axis.h @@ -54,6 +54,7 @@ struct AxisFlashAddresses uint16_t config = ADR_AXIS1_CONFIG; uint16_t maxSpeed = ADR_AXIS1_MAX_SPEED; uint16_t maxAccel = ADR_AXIS1_MAX_ACCEL; + uint16_t maxSlewRateDrv = ADR_AXIS1_MAX_SLEWRATE_DRV; uint16_t endstop = ADR_AXIS1_ENDSTOP; uint16_t power = ADR_AXIS1_POWER; @@ -109,7 +110,9 @@ enum class Axis_commands : uint32_t{ pos,curtorque,curpos,curspd,curaccel, fxratio,reductionScaler, filterSpeed, filterAccel, filterProfileId,cpr,axisfriction,axisinertia, - maxspeed, maxtorquerate, + maxspeed,slewrate, + calibrate_maxSlewRateDrv, + maxSlewRateDrv, expo,exposcale }; @@ -400,6 +403,12 @@ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandle int32_t calculateExpoTorque(int32_t torque); + /** + * @brief Applies the torque slew rate limiter to the torque. + * @param torque A reference to the torque value to be modified. + */ + void applyTorqueSlewRateLimiter(int64_t& torque); + /** * @brief Decodes the axis configuration from a 16-bit integer stored in flash. * @param val The 16-bit encoded configuration value. @@ -433,6 +442,7 @@ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandle uint16_t nextDegreesOfRotation = degreesOfRotation; //!< Target degrees of rotation. // Limiters + uint16_t maxSlewRate_Driver = MAX_SLEW_RATE; //!< Maximum slew rate as measured by the driver (in units/ms). uint16_t maxSpeedDegS = 0; //!< Maximum speed in degrees per second. 0 to disable. uint32_t maxTorqueRateMS = 0; //!< Maximum torque rate of change per millisecond. 0 to disable. @@ -467,6 +477,10 @@ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandle float idlespringscale = 0; //!< Scaler for the idle spring force. bool motorWasNotReady = true; //!< Flag to detect motor readiness transition. + // Slew rate calibration tracking: true when Axis requested a calibration and + // is waiting for the driver to finish measuring the max slew rate. + bool awaitingSlewCalibration = false; + // Filters // TODO tune these and check if it is really stable and beneficial to the FFB. index 4 placeholder const std::array filterSpeedCst = { {{ 40, 55 }, { 70, 55 }, { 120, 55 }, {180, 55}} }; //!< Speed filter profiles. diff --git a/Firmware/FFBoard/Inc/MotorDriver.h b/Firmware/FFBoard/Inc/MotorDriver.h index 0446ac3ba..0141f7ec1 100644 --- a/Firmware/FFBoard/Inc/MotorDriver.h +++ b/Firmware/FFBoard/Inc/MotorDriver.h @@ -14,7 +14,14 @@ #include "Encoder.h" #include "memory" +#define MAX_SLEW_RATE 65535 + class Encoder; + +/** + * @brief Base class for all motor drivers. + * This class defines the interface for controlling a motor and optionally its integrated encoder. + */ class MotorDriver : public ChoosableClass{ public: MotorDriver(){}; @@ -25,25 +32,75 @@ class MotorDriver : public ChoosableClass{ const ClassType getClassType() override {return ClassType::Motordriver;}; static const std::vector> all_drivers; + /** + * @brief Sends a torque command to the motor. + * @param power The torque value (signed 16-bit). + */ virtual void turn(int16_t power); + /** + * @brief Stops the motor (torque = 0). + */ virtual void stopMotor(); + /** + * @brief Starts the motor driver. + */ virtual void startMotor(); + /** + * @brief Triggers an emergency stop. + * @param reset If true, resets the driver after stopping. + */ virtual void emergencyStop(bool reset = false); - virtual bool motorReady(); // Returns true if the driver is active and ready to receive commands + /** + * @brief Checks if the driver is ready to receive torque commands. + * @return true if ready, false otherwise. + */ + virtual bool motorReady(); - virtual Encoder* getEncoder(); // Encoder is managed by the motor driver. Must always return an encoder + /** + * @brief Gets the encoder managed by this driver. + * @return A pointer to the encoder instance. + */ + virtual Encoder* getEncoder(); /** - * Can pass an external encoder if driver has no integrated encoder - * This allows a driver to get an external encoder assigned if it requires one and has the capability of using external encoders + * @brief Sets an external encoder for drivers that don't have an integrated one. + * @param encoder A shared pointer to the external encoder. */ virtual void setEncoder(std::shared_ptr& encoder){drvEncoder = encoder;} - virtual bool hasIntegratedEncoder(); // Returns true if the driver has an integrated encoder. If false the axis will pass one to the driver + + /** + * @brief Checks if the driver has an integrated encoder. + * @return true if integrated, false if external. + */ + virtual bool hasIntegratedEncoder(); + + /** + * @brief Gets the hardware measured maximum slew rate of the driver. + * @return The maximum slew rate in units/ms. + */ + virtual uint32_t getDrvSlewRate() { return MAX_SLEW_RATE; } + + /** + * @brief Starts a calibration procedure to measure the driver's max slew rate. + * @return true if calibration started successfully. + */ + virtual bool startSlewRateCalibration() { return false; } + + /** + * @brief Performs initial setup and configuration of the driver. + */ + virtual void setupDriver() {} + + /** + * @brief Sets the maximum power/current limit for the driver. + * @param power The power limit value. + */ + virtual void setPowerLimit(uint16_t power) {} protected: - std::shared_ptr drvEncoder = std::make_shared(); // Dummy encoder + std::shared_ptr drvEncoder = std::make_shared(); //!< Pointer to the encoder (integrated or external). }; diff --git a/Firmware/FFBoard/Src/Axis.cpp b/Firmware/FFBoard/Src/Axis.cpp index 54b2b83a0..b22ae92e8 100644 --- a/Firmware/FFBoard/Src/Axis.cpp +++ b/Firmware/FFBoard/Src/Axis.cpp @@ -94,7 +94,7 @@ Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_ { driverChooser = ClassChooser(axis1_drivers); setInstance(0); - this->flashAddresses = AxisFlashAddresses({ADR_AXIS1_CONFIG, ADR_AXIS1_MAX_SPEED, ADR_AXIS1_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS1_CONFIG, ADR_AXIS1_MAX_SPEED, ADR_AXIS1_MAX_ACCEL, ADR_AXIS1_MAX_SLEWRATE_DRV, ADR_AXIS1_ENDSTOP, ADR_AXIS1_POWER, ADR_AXIS1_DEGREES,ADR_AXIS1_EFFECTS1,ADR_AXIS1_EFFECTS2,ADR_AXIS1_ENC_RATIO, ADR_AXIS1_SPEEDACCEL_FILTER,ADR_AXIS1_POSTPROCESS1}); } @@ -102,14 +102,14 @@ Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_ { driverChooser = ClassChooser(axis2_drivers); setInstance(1); - this->flashAddresses = AxisFlashAddresses({ADR_AXIS2_CONFIG, ADR_AXIS2_MAX_SPEED, ADR_AXIS2_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS2_CONFIG, ADR_AXIS2_MAX_SPEED, ADR_AXIS2_MAX_ACCEL, ADR_AXIS2_MAX_SLEWRATE_DRV, ADR_AXIS2_ENDSTOP, ADR_AXIS2_POWER, ADR_AXIS2_DEGREES,ADR_AXIS2_EFFECTS1,ADR_AXIS2_EFFECTS2, ADR_AXIS2_ENC_RATIO, ADR_AXIS2_SPEEDACCEL_FILTER,ADR_AXIS2_POSTPROCESS1}); } else if (axis == 'Z') { setInstance(2); - this->flashAddresses = AxisFlashAddresses({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, ADR_AXIS3_MAX_SLEWRATE_DRV, ADR_AXIS3_ENDSTOP, ADR_AXIS3_POWER, ADR_AXIS3_DEGREES,ADR_AXIS3_EFFECTS1,ADR_AXIS3_EFFECTS2,ADR_AXIS3_ENC_RATIO, ADR_AXIS3_SPEEDACCEL_FILTER,ADR_AXIS3_POSTPROCESS1}); } @@ -145,7 +145,7 @@ void Axis::registerCommands(){ registerCommand("drvtype", Axis_commands::drvtype, "Motor driver type get/set/list",CMDFLAG_GET | CMDFLAG_SET | CMDFLAG_INFOSTRING); registerCommand("pos", Axis_commands::pos, "Encoder position",CMDFLAG_GET); registerCommand("maxspeed", Axis_commands::maxspeed, "Speed limit in deg/s",CMDFLAG_GET | CMDFLAG_SET); - registerCommand("maxtorquerate", Axis_commands::maxtorquerate, "Torque rate limit in counts/ms",CMDFLAG_GET | CMDFLAG_SET); + registerCommand("slewrate", Axis_commands::slewrate, "Torque rate limit in counts/ms",CMDFLAG_GET | CMDFLAG_SET); registerCommand("fxratio", Axis_commands::fxratio, "Effect ratio. Reduces game effects excluding endstop. 255=100%",CMDFLAG_GET | CMDFLAG_SET); registerCommand("curtorque", Axis_commands::curtorque, "Axis torque",CMDFLAG_GET); registerCommand("curpos", Axis_commands::curpos, "Axis position",CMDFLAG_GET); @@ -190,6 +190,13 @@ void Axis::restoreFlash(){ pulseErrLed(); } + // save the max torque for the slew rate + if (Flash_Read(flashAddresses.maxSlewRateDrv, &value)){ + this->maxSlewRate_Driver = value; + }else{ + pulseErrLed(); + } + uint16_t esval, power; if(Flash_Read(flashAddresses.endstop, &esval)) { @@ -250,6 +257,7 @@ void Axis::saveFlash(){ Flash_Write(flashAddresses.config, Axis::encodeConfToInt(this->conf)); Flash_Write(flashAddresses.maxSpeed, this->maxSpeedDegS); Flash_Write(flashAddresses.maxAccel, (uint16_t)(this->maxTorqueRateMS)); + Flash_Write(flashAddresses.maxSlewRateDrv, (uint16_t)(this->maxSlewRate_Driver)); Flash_Write(flashAddresses.endstop, fx_ratio_i | (endstopStrength << 8)); Flash_Write(flashAddresses.power, power); @@ -396,6 +404,7 @@ void Axis::setDrvType(uint8_t drvtype) return; } this->conf.drvtype = drvtype; + this->maxTorqueRateMS = drv->getDrvSlewRate(); // Pass encoder to driver again if(!this->drv->hasIntegratedEncoder()){ @@ -725,9 +734,7 @@ bool Axis::updateTorque(int32_t* totalTorque) { torque -= torqueReduction; } // Torque slew rate limiter - if(maxTorqueRateMS > 0){ - torque = clip(torque, metric.previous.torque - (int32_t)maxTorqueRateMS,metric.previous.torque + (int32_t)maxTorqueRateMS); - } + applyTorqueSlewRateLimiter(*(int64_t*)&torque); // Temporary cast for logic implementation if(outOfBounds){ torque = 0; @@ -754,6 +761,22 @@ bool Axis::updateTorque(int32_t* totalTorque) { return (torqueChanged); } +void Axis::applyTorqueSlewRateLimiter(int64_t& torque) +{ + // Limits the rate of change of the torque (slew rate), to smooths out sudden changes in torque. + // Essential for a natural feel and to prevent "clanking" noises. + if(maxTorqueRateMS == 0) { + return; // Limiter is disabled + } + + // This prevents sudden torque jumps, resulting in a smoother feel. + const int64_t previousTorque = metric.previous.torque; + const int64_t maxTorqueChange = maxTorqueRateMS; + + // The torque is clipped to be within the range of [previous torque - limit, previous torque + limit]. + torque = clip(torque, previousTorque - maxTorqueChange, previousTorque + maxTorqueChange); +} + void Axis::updateSamplerate(float newSamplerate){ this->filter_f = newSamplerate; this->updateFilters(this->filterProfileId); // Recalculate filters @@ -932,8 +955,39 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& handleGetSet(cmd, replies, this->maxSpeedDegS); break; - case Axis_commands::maxtorquerate: - handleGetSet(cmd, replies, this->maxTorqueRateMS); + case Axis_commands::slewrate: + { + if(cmd.type == CMDtype::get){ + // If driver has a more restrictive calibrated value, update the axis limit + if(maxSlewRate_Driver < this->maxTorqueRateMS) { + this->maxTorqueRateMS = maxSlewRate_Driver; + } + replies.emplace_back(this->maxTorqueRateMS); + }else if(cmd.type == CMDtype::set){ + this->maxTorqueRateMS = clip(cmd.val, 0, maxSlewRate_Driver); + } + } + break; + + case Axis_commands::calibrate_maxSlewRateDrv: + { + if(cmd.type == CMDtype::get){ + // Start calibration on driver and set awaiting flag if start is OK + if (drv->startSlewRateCalibration()) { + this->awaitingSlewCalibration = true; + } else { + // Inform user that calibration can't started + CommandHandler::broadcastCommandReply(CommandReply("Slew rate calibration unsupported",1), (uint32_t)Axis_commands::calibrate_maxSlewRateDrv, CMDtype::get); + } + replies.emplace_back(1); // ack + } + break; + } + + case Axis_commands::maxSlewRateDrv: + if (cmd.type == CMDtype::get) { + replies.emplace_back(maxSlewRate_Driver); + } break; case Axis_commands::fxratio: diff --git a/Firmware/FFBoard/Src/MotorDriver.cpp b/Firmware/FFBoard/Src/MotorDriver.cpp index b7325de9d..cd27754cc 100644 --- a/Firmware/FFBoard/Src/MotorDriver.cpp +++ b/Firmware/FFBoard/Src/MotorDriver.cpp @@ -81,6 +81,12 @@ const ClassIdentifier MotorDriver::getInfo(){ return info; } +/** + * Setup driver when is selected + */ +void MotorDriver::setupDriver(){ + +} /** * Turn the motor with positive/negative power. diff --git a/Firmware/scripts/memory_map.csv b/Firmware/scripts/memory_map.csv index ccaca03f4..ce1383a95 100644 --- a/Firmware/scripts/memory_map.csv +++ b/Firmware/scripts/memory_map.csv @@ -62,6 +62,7 @@ Section comment,Name,Value,Comment on key,VirtAddVarTab,exportableFlashAddresses ,ADR_AXIS1_DEGREES,0x303,,1,1 ,ADR_AXIS1_MAX_SPEED,0x304,// Store the max speed,1,1 ,ADR_AXIS1_MAX_ACCEL,0x305,// Store the max accel,1,1 +,ADR_AXIS1_MAX_SLEWRATE_DRV,0x306,// Max slew rate for drv,1,1 ,ADR_AXIS1_ENDSTOP,0x307,"// 0-7 endstop margin, 8-15 endstop stiffness",1,1 ,ADR_AXIS1_EFFECTS1,0x308,"// 0-7 idlespring, 8-15 damper",1,1 ,ADR_AXIS1_SPEEDACCEL_FILTER,0x309,// Speed/Accel filter Lowpass profile,1,1 @@ -88,18 +89,13 @@ Section comment,Name,Value,Comment on key,VirtAddVarTab,exportableFlashAddresses ,ADR_AXIS2_DEGREES,0x343,,1,1 ,ADR_AXIS2_MAX_SPEED,0x344,// Store the max speed,1,1 ,ADR_AXIS2_MAX_ACCEL,0x345,// Store the max accel,1,1 +,ADR_AXIS2_MAX_SLEWRATE_DRV,0x346,// Max slew rate for drv,1,1 ,ADR_AXIS2_ENDSTOP,0x347,"// 0-7 endstop margin, 8-15 endstop stiffness",1,1 ,ADR_AXIS2_EFFECTS1,0x348,"// 0-7 idlespring, 8-15 damper",1,1 ,ADR_AXIS2_SPEEDACCEL_FILTER,0x349,// Speed/Accel filter Lowpass profile,1,1 ,ADR_AXIS2_ENC_RATIO,0x34A,// Store the encoder ratio for an axis,1,1 ,ADR_AXIS2_EFFECTS2,0x34B,"// 0-7 Friction, 8-15 Inertia",1,1 ,ADR_AXIS2_POSTPROCESS1,0x34C,// 0-7 expo curve,1,1 -,,,,, -,,,,, -,,,,, -,,,,, -,,,,, -,,,,, // TMC2,,,,, ,ADR_TMC2_MOTCONF,0x360,// 0-2: MotType 3-5: PhiE source 6-15: Poles,1,1 ,ADR_TMC2_CPR,0x361,,1,1 @@ -120,14 +116,13 @@ Section comment,Name,Value,Comment on key,VirtAddVarTab,exportableFlashAddresses ,ADR_AXIS3_DEGREES,0x383,,1,1 ,ADR_AXIS3_MAX_SPEED,0x384,// Store the max speed,1,1 ,ADR_AXIS3_MAX_ACCEL,0x385,// Store the max accel,1,1 +,ADR_AXIS3_MAX_SLEWRATE_DRV,0x386,// Max slew rate for drv,1,1 ,ADR_AXIS3_ENDSTOP,0x387,"// 0-7 endstop margin, 8-15 endstop stiffness",1,1 ,ADR_AXIS3_EFFECTS1,0x388,"// 0-7 idlespring, 8-15 damper",1,1 ,ADR_AXIS3_SPEEDACCEL_FILTER,0x389,// Speed/Accel filter Lowpass profile,1,1 ,ADR_AXIS3_ENC_RATIO,0x38A,// Store the encoder ratio for an axis,1,1 ,ADR_AXIS3_EFFECTS2,0x38B,"// 0-7 Friction, 8-15 Inertia",1,1 ,ADR_AXIS3_POSTPROCESS1,0x38C,// 0-7 expo curve,1,1 -,,,,, -,,,,, // TMC3,,,,, ,ADR_TMC3_MOTCONF,0x3A0,// 0-2: MotType 3-5: PhiE source 6-15: Poles,1,1 ,ADR_TMC3_CPR,0x3A1,,1,1 From daee6973cc4e5e740dd94977e19404076b2d9312 Mon Sep 17 00:00:00 2001 From: Vincent MANOUKIAN <10980775+manoukianv@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:42:08 +0200 Subject: [PATCH 3/8] feat: refactor torque calculation into a 7-step pipeline --- Firmware/FFBoard/Src/Axis.cpp | 288 ++++++++++++++++++++-------------- 1 file changed, 173 insertions(+), 115 deletions(-) diff --git a/Firmware/FFBoard/Src/Axis.cpp b/Firmware/FFBoard/Src/Axis.cpp index b22ae92e8..73708df60 100644 --- a/Firmware/FFBoard/Src/Axis.cpp +++ b/Firmware/FFBoard/Src/Axis.cpp @@ -2,17 +2,32 @@ * Axis.cpp * * Created on: 31.01.2020 - * Author: Yannick + * Author: Yannick, Vincent + * */ #include "Axis.h" #include "voltagesense.h" + +// Load the driver is they are declared in targer_constants.h +#ifdef TMC4671DRIVER #include "TMC4671.h" +#endif +#ifdef PWMDRIVER #include "MotorPWM.h" -#include "VescCAN.h" +#endif +#ifdef ODRIVE #include "ODriveCAN.h" +#endif +#ifdef VESC +#include "VescCAN.h" +#endif +#ifdef SIMPLEMOTION #include "MotorSimplemotion.h" +#endif +#ifdef RMDCAN #include "RmdMotorCAN.h" +#endif #include "critical.hpp" ////////////////////////////////////////////// @@ -109,16 +124,14 @@ Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_ else if (axis == 'Z') { setInstance(2); - this->flashAddresses = AxisFlashAddresses({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, ADR_AXIS3_MAX_SLEWRATE_DRV, + this->flashAddresses = AxisFlashAddresses({ADR_AXIS3_CONFIG, ADR_AXIS3_MAX_SPEED, ADR_AXIS3_MAX_ACCEL, ADR_AXIS3_MAX_SLEWRATE_DRV, ADR_AXIS3_ENDSTOP, ADR_AXIS3_POWER, ADR_AXIS3_DEGREES,ADR_AXIS3_EFFECTS1,ADR_AXIS3_EFFECTS2,ADR_AXIS3_ENC_RATIO, ADR_AXIS3_SPEEDACCEL_FILTER,ADR_AXIS3_POSTPROCESS1}); } - - restoreFlash(); // Load parameters CommandHandler::registerCommands(); // Internal commands registerCommands(); - updateTorqueScaler(); // In case no flash setting has been loaded yet + restoreFlash(); // Load parameters } Axis::~Axis() @@ -184,6 +197,7 @@ void Axis::restoreFlash(){ pulseErrLed(); } + // save the max torque for the slew rate if (Flash_Read(flashAddresses.maxAccel, &value)){ this->maxTorqueRateMS = value; }else{ @@ -198,20 +212,20 @@ void Axis::restoreFlash(){ } - uint16_t esval, power; - if(Flash_Read(flashAddresses.endstop, &esval)) { - fx_ratio_i = esval & 0xff; - endstopStrength = (esval >> 8) & 0xff; + uint16_t endstopRawValue, power; + if(Flash_Read(flashAddresses.endstop, &endstopRawValue)) { + setEffectRatio(endstopRawValue & 0xff); + endstopStrength = (endstopRawValue >> 8) & 0xff; } if(Flash_Read(flashAddresses.power, &power)){ setPower(power); } - uint16_t deg_t; - if(Flash_Read(flashAddresses.degrees, °_t)){ - this->degreesOfRotation = deg_t & 0x7fff; - this->invertAxis = (deg_t >> 15) & 0x1; + uint16_t degreesRawValue; + if(Flash_Read(flashAddresses.degrees, °reesRawValue)){ + this->degreesOfRotation = degreesRawValue & 0x7fff; + this->invertAxis = (degreesRawValue >> 15) & 0x1; setDegrees(degreesOfRotation); } @@ -221,7 +235,7 @@ void Axis::restoreFlash(){ setIdleSpringStrength(effects & 0xff); setFxStrengthAndFilter((effects >> 8) & 0xff,damperIntensity,damperFilter); }else{ - setIdleSpringStrength(idlespringstrength); // Use default + setIdleSpringStrength(idleSpringStrength); // Use default } if(Flash_Read(flashAddresses.effects2, &effects)){ @@ -259,10 +273,10 @@ void Axis::saveFlash(){ Flash_Write(flashAddresses.maxAccel, (uint16_t)(this->maxTorqueRateMS)); Flash_Write(flashAddresses.maxSlewRateDrv, (uint16_t)(this->maxSlewRate_Driver)); - Flash_Write(flashAddresses.endstop, fx_ratio_i | (endstopStrength << 8)); + Flash_Write(flashAddresses.endstop, effectRatio | (endstopStrength << 8)); Flash_Write(flashAddresses.power, power); Flash_Write(flashAddresses.degrees, (degreesOfRotation & 0x7fff) | (invertAxis << 15)); - Flash_Write(flashAddresses.effects1, idlespringstrength | (damperIntensity << 8)); + Flash_Write(flashAddresses.effects1, idleSpringStrength | (damperIntensity << 8)); Flash_Write(flashAddresses.effects2, frictionIntensity | (inertiaIntensity << 8)); Flash_Write(flashAddresses.encoderRatio, gearRatio.numerator | (gearRatio.denominator << 8)); @@ -271,7 +285,7 @@ void Axis::saveFlash(){ Flash_Write(flashAddresses.speedAccelFilter, filterStorage); // Postprocessing - Flash_Write(flashAddresses.postprocess1, expoValInt & 0xff); + Flash_Write(flashAddresses.postprocess1, expoValue & 0xff); } @@ -315,6 +329,7 @@ void Axis::prepareForUpdate(){ return; } + // TODO: The motorReady() check was commented out. Review if this is still the desired behavior or if it should be restored. //if (!drv->motorReady()) return; float angle = getEncAngle(getEncoder()); @@ -347,6 +362,7 @@ void Axis::prepareForUpdate(){ }else if(abs(scaledEnc) <= 0x7fff) { outOfBounds = false; + // TODO: This error clearing seems to have been moved to the errorCallback. Confirm this is correct and remove this line. //ErrorHandler::clearError(outOfBoundsError); } @@ -358,6 +374,7 @@ void Axis::prepareForUpdate(){ this->updateMetrics(angle); + //this->updateHandsOffState(); } @@ -382,7 +399,11 @@ void Axis::updateDriveTorque(){ void Axis::setPower(uint16_t power) { this->power = power; - updateTorqueScaler(); + torqueScaler = ((float)power / (float)0x7fff); + if (drv != nullptr) + { + drv->setPowerLimit(power); + } } @@ -411,6 +432,7 @@ void Axis::setDrvType(uint8_t drvtype) this->drv->setEncoder(this->enc); } + drv->setupDriver(); if (!tud_connected()) { control->usb_disabled = false; @@ -539,16 +561,16 @@ int32_t Axis::getLastScaledEnc() { * Changes intensity of idle spring when FFB is off */ int32_t Axis::updateIdleSpringForce() { - return clip((int32_t)(-metric.current.pos_scaled_16b*idlespringscale),-idlespringclip,idlespringclip); + return clip((int32_t)(-metric.current.pos_scaled_16b*idleSpringScale),-idleSpringClip,idleSpringClip); } /* * Set the strength of the spring effect if FFB is disabled */ void Axis::setIdleSpringStrength(uint8_t spring){ - idlespringstrength = spring; - idlespringclip = clip((int32_t)spring*35,0,10000); - idlespringscale = 0.5f + ((float)spring * 0.01f); + idleSpringStrength = spring; + idleSpringClip = clip((int32_t)spring*35,0,10000); + idleSpringScale = 0.5f + ((float)spring * 0.01f); } /** @@ -562,26 +584,27 @@ void Axis::setFxStrengthAndFilter(uint8_t val,uint8_t& valToSet, Biquad& filter) } /** - * Called before HID effects are calculated - * Should calculate always on and idle effects specific to the axis like idlespring and friction + * Calculates the internal mechanical effects (damper, friction, inertia) that are always active. + * Called before HID effects are calculated. + * Also calculates idle spring when FFB is inactive. */ -void Axis::calculateAxisEffects(bool ffb_on){ - axisEffectTorque = 0; +void Axis::calculateMechanicalEffects(bool ffb_on){ + mechanicalEffectTorque = 0; if(!ffb_on){ - axisEffectTorque += updateIdleSpringForce(); + mechanicalEffectTorque += updateIdleSpringForce(); } // Always active damper if(damperIntensity != 0){ float speedFiltered = (metric.current.speed) * (float)damperIntensity * AXIS_DAMPER_RATIO; - axisEffectTorque -= damperFilter.process(clip(speedFiltered, -intFxClip, intFxClip)); + mechanicalEffectTorque -= damperFilter.process(clip(speedFiltered, -internalFxClip, internalFxClip)); } // Always active inertia if(inertiaIntensity != 0){ float accelFiltered = metric.current.accel * (float)inertiaIntensity * AXIS_INERTIA_RATIO; - axisEffectTorque -= inertiaFilter.process(clip(accelFiltered, -intFxClip, intFxClip)); + mechanicalEffectTorque -= inertiaFilter.process(clip(accelFiltered, -internalFxClip, internalFxClip)); } // Always active friction. Based on effectsCalculator implementation @@ -590,12 +613,12 @@ void Axis::calculateAxisEffects(bool ffb_on){ float speedRampupCeil = 4096; float rampupFactor = 1.0; if (fabs (speed) < speedRampupCeil) { // if speed in the range to rampup we apply a sine curve - float phaseRad = M_PI * ((fabs (speed) / speedRampupCeil) - 0.5);// we start to compute the normalized angle (speed / normalizedSpeed@5%) and translate it of -1/2PI to translate sin on 1/2 periode - rampupFactor = ( 1 + sin(phaseRad ) ) / 2; // sin value is -1..1 range, we translate it to 0..2 and we scale it by 2 + float phaseRad = M_PI * ((fabsf (speed) / speedRampupCeil) - 0.5f);// we start to compute the normalized angle (speed / normalizedSpeed@5%) and translate it of -1/2PI to translate sin on 1/2 periode + rampupFactor = ( 1.0f + sinf(phaseRad ) ) / 2.0f; // sin value is -1..1 range, we translate it to 0..2 and we scale it by 2 } int8_t sign = speed >= 0 ? 1 : -1; float force = (float)frictionIntensity * rampupFactor * sign * INTERNAL_AXIS_FRICTION_SCALER * 32; - axisEffectTorque -= frictionFilter.process(clip(force, -intFxClip, intFxClip)); + mechanicalEffectTorque -= frictionFilter.process(clip(force, -internalFxClip, internalFxClip)); } } @@ -603,9 +626,9 @@ void Axis::calculateAxisEffects(bool ffb_on){ /** * Changes the ratio of effects to endstop strength. 255 = same strength, 0 = no effects */ -void Axis::setFxRatio(uint8_t val) { - fx_ratio_i = val; - updateTorqueScaler(); +void Axis::setEffectRatio(uint8_t val) { + effectRatio = val; + effectRatioScaler = ((float)effectRatio/255.0); } /** @@ -635,8 +658,8 @@ void Axis::updateMetrics(float new_pos) { // pos is degrees // compute speed and accel from raw instant speed normalized float currentSpeed = (new_pos - metric.previous.posDegrees) * this->filter_f; // deg/s metric.current.speed = speedFilter.process(currentSpeed); - metric.current.accel = accelFilter.process((currentSpeed - _lastSpeed))* this->filter_f; // deg/s/s - _lastSpeed = currentSpeed; + metric.current.accel = accelFilter.process((currentSpeed - previousFrameSpeed))* this->filter_f; // deg/s/s + previousFrameSpeed = currentSpeed; } @@ -647,7 +670,7 @@ uint16_t Axis::getPower(){ } /** - * Calculates an exponential torque correction curve + * Calculates an exponential torque correction curve and scale for FFBEffect */ int32_t Axis::calculateExpoTorque(int32_t torque){ float torquef = (float)torque / (float)0x7fff; // This down and upscaling may introduce float artifacts. Do this before scaling down. @@ -658,17 +681,29 @@ int32_t Axis::calculateExpoTorque(int32_t torque){ } } -void Axis::updateTorqueScaler() { - effect_margin_scaler = ((float)fx_ratio_i/255.0); - torqueScaler = ((float)power / (float)0x7fff); -} +int64_t Axis::calculateFFBTorque() { -float Axis::getTorqueScaler(){ - return torqueScaler; -} + int64_t torque = this->ffbEffectTorque; + // 1. Game Clipping detection + // If the game sends more than the theoretical maximum (+/- 32767), the signal is clipped at the source. + if(abs(torque) >= 0x7fff){ + pulseClipLed(); // Visual alert: game signal is clipping + } -int32_t Axis::getTorque() { return metric.previous.torque; } + // 2. Apply Expo (Linearization or sensation curve) + if(expo != 1){ + torque = calculateExpoTorque(torque); + } + + // 3. Game specific gain (effectRatioScaler) + // Scale the FFB from game only (allows lowering game effects without lowering endstops) + torque = (int64_t)((float)torque * effectRatioScaler); + + return torque; +} + +int32_t Axis::getTorque() { return metric.current.torque; } bool Axis::isInverted() { return invertAxis; @@ -677,20 +712,21 @@ bool Axis::isInverted() { /** * Calculate soft endstop effect */ -int16_t Axis::updateEndstop(){ - int8_t clipdir = cliptest(metric.current.pos_scaled_16b, -0x7fff, 0x7fff); - if(clipdir == 0){ +int32_t Axis::calculateEndstopTorque(){ + // TODO Check the type int8_t and the range clipping is -0x7fff..0x7fff + int8_t clipDirection = cliptest(metric.current.pos_scaled_16b, -0x7fff, 0x7fff); + if(clipDirection == 0){ return 0; } - float addtorque = clipdir*metric.current.posDegrees - (float)this->degreesOfRotation/2.0; // degress of rotation counts total range so multiply by 2 - addtorque *= (float)endstopStrength * endstopGain; // Apply endstop gain for stiffness. - addtorque *= -clipdir; + float endstopTorque = clipDirection*metric.current.posDegrees - (float)this->degreesOfRotation/2.0; // degress of rotation counts total range so multiply by 2 + endstopTorque *= (float)endstopStrength * endstopGain; // Apply endstop gain for stiffness. + endstopTorque *= -clipDirection; - return clip(addtorque,-0x7fff,0x7fff); + return clip(endstopTorque,-0x7fff,0x7fff); } -void Axis::setEffectTorque(int32_t torque) { - effectTorque = torque; +void Axis::setFfbEffectTorque(int64_t torque) { + this->ffbEffectTorque = torque; } /** pass in ptr to receive the sum of the effects + endstop torque @@ -699,68 +735,62 @@ void Axis::setEffectTorque(int32_t torque) { bool Axis::updateTorque(int32_t* totalTorque) { - if(abs(effectTorque) >= 0x7fff){ - pulseClipLed(); - } - - // Scale effect torque - int32_t torque = effectTorque; // Game effects - if(expo != 1){ - torque = calculateExpoTorque(torque); - } - torque *= effect_margin_scaler; - torque += axisEffectTorque; // Independent effects - torque += updateEndstop(); - torque *= torqueScaler; // Scale to power - - - // TODO speed and accel limiters - if(maxSpeedDegS > 0){ - - float torqueSign = torque > 0 ? 1 : -1; // Used to prevent metrics against the force to go into the limiter - // Speed. Mostly tuned... - //spdlimiterAvg.addValue(metric.current.speed); - float speedreducer = (float)((metric.current.speed*torqueSign) - (float)maxSpeedDegS) * ((float)0x7FFF / maxSpeedDegS); - spdlimitreducerI = clip( spdlimitreducerI + ((speedreducer * speedLimiterI) * torqueScaler),0,power); - - // Only reduce torque. Don't invert it to prevent oscillation - float torqueReduction = speedreducer * speedLimiterP + spdlimitreducerI;// accreducer * 0.025 + acclimitreducerI - if(torque > 0){ - torqueReduction = clip(torqueReduction,0,torque); - }else{ - torqueReduction = clip(-torqueReduction,torque,0); - } - - torque -= torqueReduction; - } - // Torque slew rate limiter - applyTorqueSlewRateLimiter(*(int64_t*)&torque); // Temporary cast for logic implementation - + // STEP 1: Process FFB torque from the game (via helper function) + // (Reconstructed by CMSIS Spline, Expo applied, and scaled by FFB ratio) + int64_t torque = calculateFFBTorque(); + + // STEP 2: Mix in local mechanical effects + // (Damper, Friction, Inertia generated locally at high frequency) + torque += mechanicalEffectTorque; + + // STEP 3: Add endstops + // Note: Historically endstops are added before the Master Scaler. + // If hard endstops (like Simucube) are desired, this should be moved to STEP 5. + torque += calculateEndstopTorque(); + + // STEP 4: Master Volume Scaling + // Map the virtual signal (+/- 32767) to the physical power limit ("power") + torque = (int64_t)((float)torque * torqueScaler); + + // STEP 5: Safety limits (Fade-in, Out of bounds) + // Applied BEFORE dynamic limiters so that abrupt cuts are smoothed by the Slew Rate. if(outOfBounds){ torque = 0; } - // Fade in - if(forceFadeCurMult < 1){ - torque = torque * forceFadeCurMult; - forceFadeCurMult += forceFadeTime / this->filter_f; // Fade time + // Apply a fade-in effect for a smooth force ramp-up on startup or recovery. + // Increases forceFadeMultiplier progressively based on forceFadeDuration and sample rate. + if(forceFadeMultiplier < 1.0f){ + torque = (int64_t)((float)torque * forceFadeMultiplier); + forceFadeMultiplier += forceFadeDuration / this->filter_f; } - // Torque calculated. Now sending to driver - torque = (invertAxis) ? -torque : torque; - metric.current.torque = torque; - torque = clip(torque, -power, power); + // STEP 6: Dynamic limiters (Speed & Slew Rate) + // CRITICAL: Slew Rate compares the target value with "metric.previous.torque" + // (which is the clipped physical torque from the previous cycle). + // It MUST be applied on the final scaled torque! + torque -= applySpeedLimiterTorque(torque); + applyTorqueSlewRateLimiter(torque); - bool torqueChanged = metric.current.torque != metric.previous.torque; + // STEP 7: Axis inversion and final hardware clipping + torque = (invertAxis) ? -torque : torque; - if (abs(torque) == power){ - pulseClipLed(); + int32_t torqueAfterClipping = clip((int32_t)torque, -power, power); + + if (torqueAfterClipping != torque){ + pulseClipLed(); // Visual alert: MOTOR cannot provide requested power (Hardware clipping) } - *totalTorque = torque; - return (torqueChanged); + // Store the actually applied torque for the next iteration (used by the slew rate limiter). + metric.current.torque = torqueAfterClipping; + + // return result + *totalTorque = torqueAfterClipping; + + return (metric.current.torque != metric.previous.torque); } + void Axis::applyTorqueSlewRateLimiter(int64_t& torque) { // Limits the rate of change of the torque (slew rate), to smooths out sudden changes in torque. @@ -777,6 +807,34 @@ void Axis::applyTorqueSlewRateLimiter(int64_t& torque) torque = clip(torque, previousTorque - maxTorqueChange, previousTorque + maxTorqueChange); } +int64_t Axis::applySpeedLimiterTorque(int64_t& torque){ + // Speed Limiter: A PI controller to reduce torque when speed exceeds maxSpeedDegS. + // The limiter only acts when torque is applied in the direction of movement. + + // if limiter is disabled, return + if(maxSpeedDegS <= 0) { + return 0; + } + + int64_t resultTorque = 0; + + float torqueSign = torque > 0 ? 1 : -1; // Used to prevent metrics against the force to go into the limiter + // Speed. Mostly tuned... + //spdlimiterAvg.addValue(metric.current.speed); + float speedreducer = (float)((metric.current.speed*torqueSign) - (float)maxSpeedDegS) * ((float)0x7FFF / maxSpeedDegS); + speedLimitReducerI = clip( speedLimitReducerI + ((speedreducer * speedLimiterI) * torqueScaler),0,power); + + // Only reduce torque. Don't invert it to prevent oscillation + float torqueReduction = speedreducer * speedLimiterP + speedLimitReducerI;// accreducer * 0.025 + acclimitreducerI + if(torque > 0){ + resultTorque = clip(torqueReduction,0,torque); + }else{ + resultTorque = clip(-torqueReduction,torque,0); + } + + return resultTorque; +} + void Axis::updateSamplerate(float newSamplerate){ this->filter_f = newSamplerate; this->updateFilters(this->filterProfileId); // Recalculate filters @@ -797,8 +855,8 @@ void Axis::updateFilters(uint8_t profileId){ * Starts fading in force from start to 1 over fadeTime */ void Axis::startForceFadeIn(float start,float fadeTime){ - this->forceFadeTime = fadeTime; - this->forceFadeCurMult = clip(start, 0, 1); + this->forceFadeDuration = fadeTime; + this->forceFadeMultiplier = clip(start, 0, 1); } @@ -809,9 +867,9 @@ void Axis::setDegrees(uint16_t degrees){ degrees &= 0x7fff; if(degrees == 0){ - nextDegreesOfRotation = lastdegreesOfRotation; + nextDegreesOfRotation = previousDegreesOfRotation; }else{ - lastdegreesOfRotation = degreesOfRotation; + previousDegreesOfRotation = degreesOfRotation; nextDegreesOfRotation = degrees; } } @@ -819,7 +877,7 @@ void Axis::setDegrees(uint16_t degrees){ void Axis::setExpo(int val){ val = clip(val, -127, 127); - expoValInt = val; + expoValue = val; if(val == 0){ expo = 1; // Explicitly force expo off return; @@ -874,7 +932,7 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& case Axis_commands::idlespring: if (cmd.type == CMDtype::get) { - replies.emplace_back(idlespringstrength); + replies.emplace_back(idleSpringStrength); } else if (cmd.type == CMDtype::set) { @@ -992,9 +1050,9 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& case Axis_commands::fxratio: if(cmd.type == CMDtype::get){ - replies.emplace_back(this->fx_ratio_i); + replies.emplace_back(this->effectRatio); }else if(cmd.type == CMDtype::set){ - setFxRatio(cmd.val); + setEffectRatio(cmd.val); } break; @@ -1058,7 +1116,7 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector& break; case Axis_commands::expo: - handleGetSetFunc(cmd, replies, expoValInt, &Axis::setExpo, this); // need to also provide the expoScaler constant + handleGetSetFunc(cmd, replies, expoValue, &Axis::setExpo, this); // need to also provide the expoScaler constant break; case Axis_commands::exposcale: From a8dba6b06592430217389d95e394af339d99b4e8 Mon Sep 17 00:00:00 2001 From: Vincent MANOUKIAN <10980775+manoukianv@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:43:32 +0200 Subject: [PATCH 4/8] feat: add ARM CMSIS-DSP library infrastructure and optimized filters --- .github/workflows/build-firmware.yml | 2 + .gitmodules | 3 + Firmware/FFBoard/Inc/Filters.h | 23 +- Firmware/FFBoard/Inc/ffb_defs.h | 1 + Firmware/FFBoard/Src/Filters.cpp | 121 +++++++-- Firmware/Libraries/CMSIS-DSP | 1 + Firmware/Makefile | 24 +- Firmware/Targets/F407VG/.cproject | 316 ++++++++++++++++++++++-- Firmware/Targets/F407VG/.project | 5 + Firmware/Targets/F407VG_DISCO/.cproject | 18 +- Firmware/Targets/F411RE/.cproject | 4 + 11 files changed, 463 insertions(+), 55 deletions(-) create mode 160000 Firmware/Libraries/CMSIS-DSP diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index d3e3377b1..e31bb5ccc 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -32,6 +32,8 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 + with: + submodules: 'recursive' - {uses: ./.github/actions/build-firmware, with: {target: '${{ matrix.target }}', path: 'Output'}} diff --git a/.gitmodules b/.gitmodules index ed7c594be..313b67a9d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = Hardware url = https://github.com/Ultrawipf/OpenFFBoard-hardware.git branch = master +[submodule "Firmware/Libraries/CMSIS-DSP"] + path = Firmware/Libraries/CMSIS-DSP + url = https://github.com/ARM-software/CMSIS-DSP.git diff --git a/Firmware/FFBoard/Inc/Filters.h b/Firmware/FFBoard/Inc/Filters.h index 83a31bf17..92c2a7959 100644 --- a/Firmware/FFBoard/Inc/Filters.h +++ b/Firmware/FFBoard/Inc/Filters.h @@ -2,13 +2,17 @@ * Filters.h * * Created on: Feb 13, 2020 - * Author: Yannick + * Author: Yannick, Vincent */ #ifndef FILTERS_H_ #define FILTERS_H_ #include "cppmain.h" +#ifdef USE_DSP_FUNCTIONS +#include "arm_math.h" +#endif + #ifdef __cplusplus // Frequency in hz, q in float q*100. Example: Q 0.5 -> 50 @@ -40,14 +44,27 @@ class Biquad{ float getFc() const; void setQ(float Q); float getQ() const; + void setPeakGain(float peakGainDB); void calcBiquad(void); -protected: +#ifdef USE_DSP_FUNCTIONS + const float* getCoeffs() const { return pCoeffs; } +#endif +protected: BiquadType type; - float a0, a1, a2, b1, b2; float Fc, Q, peakGain; + +#ifdef USE_DSP_FUNCTIONS + // CMSIS-DSP instance + arm_biquad_casd_df1_inst_f32 S; + float32_t pCoeffs[5]; + float32_t pState[4]; // For a single biquad stage (DF1 float requires 4 states) +#else float z1, z2; + float a0, a1, a2, b1, b2; +#endif + }; diff --git a/Firmware/FFBoard/Inc/ffb_defs.h b/Firmware/FFBoard/Inc/ffb_defs.h index f6f0a8eef..aa58dd978 100644 --- a/Firmware/FFBoard/Inc/ffb_defs.h +++ b/Firmware/FFBoard/Inc/ffb_defs.h @@ -11,6 +11,7 @@ #include "cppmain.h" #include "Filters.h" #include "constants.h" // For #define MAX_AXIS + #define FFB_ID_OFFSET 0x00 #define MAX_EFFECTS 40 diff --git a/Firmware/FFBoard/Src/Filters.cpp b/Firmware/FFBoard/Src/Filters.cpp index a9c59330a..1eb2644d6 100644 --- a/Firmware/FFBoard/Src/Filters.cpp +++ b/Firmware/FFBoard/Src/Filters.cpp @@ -2,20 +2,33 @@ * Filters.cpp * * Created on: Feb 13, 2020 - * Author: Yannick + * Author: Yannick, Vincent * * Based on http://www.earlevel.com/main/2012/11/26/biquad-c-source-code/ */ #include "Filters.h" - #include +#ifdef USE_DSP_FUNCTIONS +#include "arm_math.h" +#endif Biquad::Biquad(){ - z1 = z2 = 0.0; +#ifdef USE_DSP_FUNCTIONS + // Clear state + memset(pState, 0, sizeof(pState)); + // Initialize the CMSIS-DSP biquad instance + arm_biquad_cascade_df1_init_f32(&S, 1, pCoeffs, pState); +#else + z1 = 0.0f; + z2 = 0.0f; +#endif } Biquad::Biquad(BiquadType type, float Fc, float Q, float peakGainDB) { +#ifdef USE_DSP_FUNCTIONS + arm_biquad_cascade_df1_init_f32(&S, 1, pCoeffs, pState); +#endif setBiquad(type, Fc, Q, peakGainDB); } @@ -49,13 +62,23 @@ float Biquad::getQ() const { return this->Q; } +void Biquad::setPeakGain(float peakGainDB) { + this->peakGain = peakGainDB; + calcBiquad(); +} + /** * Calculates one step of the filter and returns the output */ float Biquad::process(float in) { - float out = in * a0 + z1; + float out; +#ifdef USE_DSP_FUNCTIONS + arm_biquad_cascade_df1_f32(&S, &in, &out, 1); +#else + out = in * a0 + z1; z1 = in * a1 + z2 - b1 * out; z2 = in * a2 - b2 * out; +#endif return out; } @@ -72,11 +95,18 @@ void Biquad::setBiquad(BiquadType type, float Fc, float Q, float peakGainDB) { * Updates parameters and resets the biquad filter */ void Biquad::calcBiquad(void) { + float norm; + float K, V; +#ifdef USE_DSP_FUNCTIONS + float a0 = 0.0f, a1 = 0.0f, a2 = 0.0f, b1 = 0.0f, b2 = 0.0f; + V = powf(10, fabsf(peakGain) / 20.0f); + K = arm_sin_f32(PI * Fc) / arm_cos_f32(PI * Fc); +#else z1 = 0.0; z2 = 0.0; - float norm; - float V = pow(10, fabs(peakGain) / 20.0); - float K = tan(M_PI * Fc); + V = pow(10, fabs(peakGain) / 20.0); + K = tan(M_PI * Fc); +#endif switch (this->type) { case BiquadType::lowpass: norm = 1 / (1 + K / Q + K * K); @@ -134,41 +164,84 @@ void Biquad::calcBiquad(void) { break; case BiquadType::lowshelf: if (peakGain >= 0) { // boost - norm = 1 / (1 + sqrt(2) * K + K * K); - a0 = (1 + sqrt(2*V) * K + V * K * K) * norm; + float sqrt2, sqrt2V; +#ifdef USE_DSP_FUNCTIONS + arm_sqrt_f32(2, &sqrt2); + arm_sqrt_f32(2*V, &sqrt2V); +#else + sqrt2 = sqrt(2); + sqrt2V = sqrt(2*V); +#endif + norm = 1 / (1 + sqrt2 * K + K * K); + a0 = (1 + sqrt2V * K + V * K * K) * norm; a1 = 2 * (V * K * K - 1) * norm; - a2 = (1 - sqrt(2*V) * K + V * K * K) * norm; + a2 = (1 - sqrt2V * K + V * K * K) * norm; b1 = 2 * (K * K - 1) * norm; - b2 = (1 - sqrt(2) * K + K * K) * norm; + b2 = (1 - sqrt2 * K + K * K) * norm; } else { // cut - norm = 1 / (1 + sqrt(2*V) * K + V * K * K); - a0 = (1 + sqrt(2) * K + K * K) * norm; + float sqrt2, sqrt2V; +#ifdef USE_DSP_FUNCTIONS + arm_sqrt_f32(2, &sqrt2); + arm_sqrt_f32(2*V, &sqrt2V); +#else + sqrt2 = sqrt(2); + sqrt2V = sqrt(2*V); +#endif + norm = 1 / (1 + sqrt2V * K + V * K * K); + a0 = (1 + sqrt2 * K + K * K) * norm; a1 = 2 * (K * K - 1) * norm; - a2 = (1 - sqrt(2) * K + K * K) * norm; + a2 = (1 - sqrt2 * K + K * K) * norm; b1 = 2 * (V * K * K - 1) * norm; - b2 = (1 - sqrt(2*V) * K + V * K * K) * norm; + b2 = (1 - sqrt2V * K + V * K * K) * norm; } break; case BiquadType::highshelf: if (peakGain >= 0) { // boost - norm = 1 / (1 + sqrt(2) * K + K * K); - a0 = (V + sqrt(2*V) * K + K * K) * norm; + float sqrt2, sqrt2V; +#ifdef USE_DSP_FUNCTIONS + arm_sqrt_f32(2, &sqrt2); + arm_sqrt_f32(2*V, &sqrt2V); +#else + sqrt2 = sqrt(2); + sqrt2V = sqrt(2*V); +#endif + norm = 1 / (1 + sqrt2 * K + K * K); + a0 = (V + sqrt2V * K + K * K) * norm; a1 = 2 * (K * K - V) * norm; - a2 = (V - sqrt(2*V) * K + K * K) * norm; + a2 = (V - sqrt2V * K + K * K) * norm; b1 = 2 * (K * K - 1) * norm; - b2 = (1 - sqrt(2) * K + K * K) * norm; + b2 = (1 - sqrt2 * K + K * K) * norm; } else { // cut - norm = 1 / (V + sqrt(2*V) * K + K * K); - a0 = (1 + sqrt(2) * K + K * K) * norm; + float sqrt2, sqrt2V; +#ifdef USE_DSP_FUNCTIONS + arm_sqrt_f32(2, &sqrt2); + arm_sqrt_f32(2*V, &sqrt2V); +#else + sqrt2 = sqrt(2); + sqrt2V = sqrt(2*V); +#endif + norm = 1 / (V + sqrt2V * K + K * K); + a0 = (1 + sqrt2 * K + K * K) * norm; a1 = 2 * (K * K - 1) * norm; - a2 = (1 - sqrt(2) * K + K * K) * norm; + a2 = (1 - sqrt2 * K + K * K) * norm; b1 = 2 * (K * K - V) * norm; - b2 = (V - sqrt(2*V) * K + K * K) * norm; + b2 = (V - sqrt2V * K + K * K) * norm; } break; } - return; +#ifdef USE_DSP_FUNCTIONS + // Store coefficients in the format required by CMSIS-DSP: {b0, b1, b2, -a1, -a2} + // Note the negated feedback coefficients a1 and a2. + pCoeffs[0] = a0; + pCoeffs[1] = a1; + pCoeffs[2] = a2; + pCoeffs[3] = -b1; + pCoeffs[4] = -b2; + + // Reset state + memset(pState, 0, sizeof(pState)); +#endif } diff --git a/Firmware/Libraries/CMSIS-DSP b/Firmware/Libraries/CMSIS-DSP new file mode 160000 index 000000000..1ba19d14a --- /dev/null +++ b/Firmware/Libraries/CMSIS-DSP @@ -0,0 +1 @@ +Subproject commit 1ba19d14a6491169b4d2873e1a0a59486c7636c6 diff --git a/Firmware/Makefile b/Firmware/Makefile index b5e4fb33c..a3b53944a 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -104,7 +104,6 @@ $(TARGET_DIR)/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F \ $(TARGET_DIR)/Drivers/CMSIS/Device/ST/STM32F4xx/Include \ $(TARGET_DIR)/Drivers/CMSIS/Include - C_INCLUDES := $(addprefix -I, $(C_INCLUDES)) ####################################### @@ -152,10 +151,29 @@ AS_DEFS = C_DEFS = \ -DSTM32_THREAD_SAFE_STRATEGY=4 \ -DUSE_HAL_DRIVER + ifeq ($(MCU_TARGET),F411RE) -C_DEFS += -DSTM32F411xE +C_DEFS += -DSTM32F411xE -DUSE_DSP_FUNCTIONS else -C_DEFS += -DSTM32F407xx +C_DEFS += -DSTM32F407xx -DUSE_DSP_FUNCTIONS +endif + +# DSP library configuration +ifneq (,$(filter -DUSE_DSP_FUNCTIONS,$(C_DEFS))) +DSP_LIB_PATH = Libraries/CMSIS-DSP +C_INCLUDES += -I$(DSP_LIB_PATH)/Include -I$(DSP_LIB_PATH)/PrivateInclude +C_DEFS += -DARM_MATH_LOOPUNROLL -DARM_MATH_CORTEX_M4 + +# Add required CMSIS-DSP source modules +C_SOURCES += $(wildcard $(DSP_LIB_PATH)/Source/BasicMathFunctions/arm_*.c) \ + $(wildcard $(DSP_LIB_PATH)/Source/CommonTables/arm_*.c) \ + $(wildcard $(DSP_LIB_PATH)/Source/FastMathFunctions/arm_*.c) \ + $(wildcard $(DSP_LIB_PATH)/Source/FilteringFunctions/arm_*.c) \ + $(wildcard $(DSP_LIB_PATH)/Source/ControllerFunctions/arm_*.c) \ + $(wildcard $(DSP_LIB_PATH)/Source/InterpolationFunctions/arm_*.c) + +# Filter out unnecessary or incompatible files +C_SOURCES := $(filter-out %_f16.c %_f64.c %_mve.c, $(C_SOURCES)) endif # compile gcc flags diff --git a/Firmware/Targets/F407VG/.cproject b/Firmware/Targets/F407VG/.cproject index 81af2569f..de32c7675 100644 --- a/Firmware/Targets/F407VG/.cproject +++ b/Firmware/Targets/F407VG/.cproject @@ -25,11 +25,11 @@