Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions Firmware/FFBoard/Inc/Axis.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ 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;
Expand Down Expand Up @@ -114,12 +113,9 @@ struct GearRatio_t{

enum class Axis_commands : uint32_t{
power=0x00,degrees=0x01,esgain,zeroenc,invert,idlespring,axisdamper,enctype,drvtype,
pos,curtorque,curpos,curspd,curaccel,
fxratio,reductionScaler,
pos,fxratio,curtorque,curpos,curspd,curaccel,reductionScaler,
filterSpeed, filterAccel, filterProfileId,cpr,axisfriction,axisinertia,
maxspeed,slewrate,
calibrate_maxSlewRateDrv,
maxSlewRateDrv,
maxspeed,maxtorquerate,
expo,exposcale
};

Expand Down Expand Up @@ -419,11 +415,6 @@ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandle
* @return torque update to apply to reduced de speed.
*/
int32_t applySpeedLimiterTorque(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(int32_t& torque);
/**
* @brief Decodes the axis configuration from a 16-bit integer stored in flash.
* @param val The 16-bit encoded configuration value.
Expand Down Expand Up @@ -459,8 +450,7 @@ class Axis : public PersistentStorage, public CommandHandler, public ErrorHandle
uint16_t previousDegreesOfRotation = degreesOfRotation; //!< Previous degrees of rotation (for smooth transitions).
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).
// Axis parameters
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.

Expand Down Expand Up @@ -498,9 +488,6 @@ 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
Expand Down
17 changes: 2 additions & 15 deletions Firmware/FFBoard/Inc/MotorDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,10 @@ class MotorDriver : public ChoosableClass{

virtual void setPowerLimit(uint16_t power){}; // specific motor driver manager power, this is used to send powerLimit to the driver, like TMC4671.

//
/**
* Slew rate calibration interface (no-op by default)
* If driver can't calibration the max slew rate will by MAX_SLEW_RATE (65535)
* @return false is the driver not implemented this process or true if calibration start
* Check if any calibration process is currently active.
*/
virtual bool startSlewRateCalibration() { return false; };
/**
* Check if calibration is in process
* @return the state of calibration process
*/
virtual bool isSlewRateCalibrationInProgress() { return false; };
/**
* Get the value of the Slew Rate after a calibration
* @return
*/
virtual uint16_t getDrvSlewRate() { return MAX_SLEW_RATE; };
virtual bool isCalibrationInProgress() { return false; };

virtual bool motorReady(); // Returns true if the driver is active and ready to receive commands

Expand Down
4 changes: 4 additions & 0 deletions Firmware/FFBoard/Inc/flash_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ void Flash_Dump(std::vector<std::tuple<uint16_t,uint16_t>> *result,bool includeA
bool Flash_Format();


#ifdef COGGING_TABLE_FLASH_START_ADDRESS
bool Flash_WriteCoggingTable(uint8_t table_idx, void* data);
bool Flash_ReadCoggingTable(uint8_t table_idx, void* data);
#endif

bool OTP_Write(uint16_t adroffset,uint64_t dat);
bool OTP_Read(uint16_t adroffset,uint64_t* dat);
Expand Down
141 changes: 48 additions & 93 deletions Firmware/FFBoard/Src/Axis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,22 @@ Axis::Axis(char axis,volatile Control_t* control) :CommandHandler("axis", CLSID_
{
driverChooser = ClassChooser<MotorDriver>(axis1_drivers);
setInstance(0);
this->flashAddresses = AxisFlashAddresses({ADR_AXIS1_CONFIG, ADR_AXIS1_MAX_SPEED, ADR_AXIS1_MAX_ACCEL, ADR_AXIS1_MAX_SLEWRATE_DRV,
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')
{
driverChooser = ClassChooser<MotorDriver>(axis2_drivers);
setInstance(1);
this->flashAddresses = AxisFlashAddresses({ADR_AXIS2_CONFIG, ADR_AXIS2_MAX_SPEED, ADR_AXIS2_MAX_ACCEL, ADR_AXIS2_MAX_SLEWRATE_DRV,
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->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_ENDSTOP, ADR_AXIS3_POWER, ADR_AXIS3_DEGREES,ADR_AXIS3_EFFECTS1,ADR_AXIS3_EFFECTS2,ADR_AXIS3_ENC_RATIO,
ADR_AXIS3_SPEEDACCEL_FILTER,ADR_AXIS3_POSTPROCESS1});
}
Expand Down Expand Up @@ -171,7 +171,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("slewrate", Axis_commands::slewrate, "Torque rate limit in counts/ms",CMDFLAG_GET | CMDFLAG_SET);
registerCommand("maxtorquerate", Axis_commands::maxtorquerate, "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);
Expand Down Expand Up @@ -210,19 +210,13 @@ void Axis::restoreFlash(){
pulseErrLed();
}

// save the max torque for the slew rate
if (Flash_Read(flashAddresses.maxAccel, &value)){
this->maxTorqueRateMS = value;
}else{
pulseErrLed();
}

// save the max torque for the slew rate
if (Flash_Read(flashAddresses.maxSlewRateDrv, &value)){
this->maxSlewRate_Driver = value;
}else{
pulseErrLed();
}



uint16_t endstopRawValue, power;
Expand Down Expand Up @@ -284,8 +278,6 @@ 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, effectRatio | (endstopStrength << 8));
Flash_Write(flashAddresses.power, power);
Flash_Write(flashAddresses.degrees, (degreesOfRotation & 0x7fff) | (invertAxis << 15));
Expand Down Expand Up @@ -364,7 +356,7 @@ void Axis::prepareForUpdate(){
int32_t scaledEnc;
std::tie(scaledEnc,std::ignore) = scaleEncValue(angle, degreesOfRotation);

if (abs(scaledEnc) > 0xffff && drv->motorReady()){
if (abs(scaledEnc) > 0xffff && drv->motorReady() && !drv->isCalibrationInProgress()){
// We are way off. Shut down
drv->stopMotor();
pulseErrLed();
Expand Down Expand Up @@ -400,10 +392,9 @@ void Axis::errorCallback(const Error &error, bool cleared){


void Axis::updateDriveTorque(){
// totalTorque = effectTorque + endstopTorque
int32_t totalTorque;
bool torqueChanged = updateTorque(&totalTorque);
if (torqueChanged && drv->motorReady()){
updateTorque(&totalTorque);
if (drv->motorReady()){
// Send to motor driver
drv->turn(totalTorque);
}
Expand All @@ -429,33 +420,45 @@ void Axis::setDrvType(uint8_t drvtype)
{
return;
}
cpp_freertos::CriticalSection::Enter();
MotorDriver* drv = driverChooser.Create((uint16_t)drvtype);
this->drv.reset(drv);
if (drv == nullptr)

// Create the driver outside of the critical section to avoid FreeRTOS issues (semaphore take with interrupts disabled)
MotorDriver* drv_new = driverChooser.Create((uint16_t)drvtype);

if (drv_new == nullptr)
{
cpp_freertos::CriticalSection::Exit();
return;
}

// Pre-initialization outside of critical section
if(!drv_new->hasIntegratedEncoder()){
drv_new->setEncoder(this->enc);
}
drv_new->setupDriver();

// Use critical section only for atomic replacement of the driver pointer
MotorDriver* old_drv_ptr = nullptr;
cpp_freertos::CriticalSection::Enter();
old_drv_ptr = this->drv.release(); // Detach the old driver safely
this->drv.reset(drv_new); // Attach the new driver
this->conf.drvtype = drvtype;
this->maxTorqueRateMS = drv->getDrvSlewRate();

// Pass encoder to driver again
if(!this->drv->hasIntegratedEncoder()){
this->drv->setEncoder(this->enc);
cpp_freertos::CriticalSection::Exit();

// Delete the old driver outside of the critical section to avoid blocking destructors or FreeRTOS issues
if (old_drv_ptr != nullptr) {
delete old_drv_ptr;
}

drv->setupDriver();
// Perform SPI communications (start/suspend) outside of the critical section
if (!tud_connected())
{
control->usb_disabled = false;
this->usbSuspend();
}
else
{
drv->startMotor();
drv_new->startMotor();
}
cpp_freertos::CriticalSection::Exit();
}


Expand Down Expand Up @@ -604,6 +607,10 @@ void Axis::setFxStrengthAndFilter(uint8_t val,uint8_t& valToSet, Biquad& filter)
void Axis::calculateMechanicalEffects(bool ffb_on){
mechanicalEffectTorque = 0;

if (outOfBounds || drv->isCalibrationInProgress() || !drv->motorReady()) {
return;
}

if(!ffb_on){
mechanicalEffectTorque += updateIdleSpringForce();
}
Expand Down Expand Up @@ -780,59 +787,38 @@ bool Axis::updateTorque(int32_t* totalTorque) {
torque = 0;
}

// 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 = (int32_t)((float)torque * forceFadeMultiplier);
forceFadeMultiplier += forceFadeDuration / this->filter_f;
}

// STEP 6: Dynamic limiters (Speed)
// Must be applied while torque is still in the Game Coordinate System (matching current.speed).
torque -= applySpeedLimiterTorque(torque);

// STEP 7: Axis inversion and Slew Rate Limiter
// Axis inversion must be done BEFORE Slew Rate because previous.torque is in hardware CS.
torque = (invertAxis) ? -torque : torque;
// Torque slew rate limiter
if(maxTorqueRateMS > 0){
torque = clip<int32_t,int32_t>(torque, metric.previous.torque - maxTorqueRateMS,metric.previous.torque + maxTorqueRateMS);
}

// 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 physical torque after inversion!
applyTorqueSlewRateLimiter(torque);
if(invertAxis){
torque = -torque;
}

// STEP 8: Final hardware clipping
int32_t torqueAfterClipping = clip<int32_t, int32_t>((int32_t)torque, -power, power);

if (torqueAfterClipping != torque){
pulseClipLed(); // Visual alert: MOTOR cannot provide requested power (Hardware clipping)
pulseClipLed();
}

// Store the actually applied torque for the next iteration (used by the slew rate limiter).
metric.current.torque = torqueAfterClipping;
metric.previous.torque = torqueAfterClipping;
metric.current.torque = torqueAfterClipping;

// return result
*totalTorque = torqueAfterClipping;

return (metric.current.torque != metric.previous.torque);
}


void Axis::applyTorqueSlewRateLimiter(int32_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 int32_t previousTorque = metric.previous.torque;
const int32_t maxTorqueChange = maxTorqueRateMS;

// The torque is clipped to be within the range of [previous torque - limit, previous torque + limit].
torque = clip<int32_t>(torque, previousTorque - maxTorqueChange, previousTorque + maxTorqueChange);
}

int32_t Axis::applySpeedLimiterTorque(int32_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.
Expand Down Expand Up @@ -1057,39 +1043,8 @@ CommandStatus Axis::command(const ParsedCommand& cmd,std::vector<CommandReply>&
handleGetSet(cmd, replies, this->maxSpeedDegS);
break;

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<uint32_t,uint32_t>(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);
}
case Axis_commands::maxtorquerate:
handleGetSet(cmd, replies, this->maxTorqueRateMS);
break;

case Axis_commands::fxratio:
Expand Down
Loading
Loading