Skip to content

File syn_motor_ctrl.c

File List > motor > syn_motor_ctrl.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_MOTOR_CTRL) || SYN_USE_MOTOR_CTRL

#if defined(SYN_USE_PID) && !SYN_USE_PID
#error "syn_motor_ctrl requires SYN_USE_PID=1"
#endif

#include "../util/syn_assert.h"
#include "syn_motor_ctrl.h"

/* Only include optional module headers when enabled */
#if !defined(SYN_USE_ERRLOG) || SYN_USE_ERRLOG
#include "../system/syn_errlog.h"
#endif

#include <string.h>

#define SYN_MCTRL_ERR_STALL 0x0100 
#define SYN_MCTRL_ERR_LIMIT 0x0101 
/* ── Motor output wrappers ─────────────────────────────────────────────── */

static void apply_output(SYN_MotorCtrl *ctrl, int32_t output)
{
    if (ctrl->cfg.motor.set_output != NULL) {
        ctrl->cfg.motor.set_output(ctrl->cfg.motor.ctx, output);
    }
}

static void stop_motor(SYN_MotorCtrl *ctrl)
{
    if (ctrl == NULL || ctrl->cfg.motor.coast == NULL)
        return;
    ctrl->cfg.motor.coast(ctrl->cfg.motor.ctx);
}

static void brake_motor(SYN_MotorCtrl *ctrl)
{
    if (ctrl == NULL || ctrl->cfg.motor.brake == NULL)
        return;
    ctrl->cfg.motor.brake(ctrl->cfg.motor.ctx);
}

static int32_t read_position(SYN_MotorCtrl *ctrl)
{
    if (ctrl == NULL || ctrl->cfg.read_pos == NULL)
        return 0;
    return ctrl->cfg.read_pos(ctrl->cfg.read_pos_ctx);
}

static bool limits_enabled(const SYN_MotorCtrl *ctrl)
{
    if (ctrl == NULL)
        return false;
    return (ctrl->cfg.position_min != 0 || ctrl->cfg.position_max != 0);
}

static bool at_limit(const SYN_MotorCtrl *ctrl, int32_t pos, int32_t output)
{
    if (!limits_enabled(ctrl))
        return false;

    /* At min limit and trying to go further negative */
    if (pos <= ctrl->cfg.position_min && output < 0)
        return true;
    /* At max limit and trying to go further positive */
    if (pos >= ctrl->cfg.position_max && output > 0)
        return true;

    return false;
}

/* ── API ────────────────────────────────────────────────────────────────── */

SYN_Status syn_motor_ctrl_init(SYN_MotorCtrl *ctrl, const SYN_MotorCtrl_Config *cfg)
{
    SYN_ASSERT(ctrl != NULL);
    SYN_ASSERT(cfg != NULL);
    SYN_ASSERT(cfg->read_pos != NULL);
    SYN_ASSERT(cfg->update_hz > 0);

    memset(ctrl, 0, sizeof(*ctrl));
    ctrl->cfg = *cfg;

    SYN_PID_Config pid_cfg = {
        .kp = cfg->pid_kp,
        .ki = cfg->pid_ki,
        .kd = cfg->pid_kd,
        .scale = (int32_t)1 << cfg->pid_scale,
        .out_min = cfg->output_min,
        .out_max = cfg->output_max,
        /* integral_max = 0 → auto-computed by syn_pid_init() */
        .d_filter_alpha = 200,
    };
    syn_pid_init(&ctrl->pid, &pid_cfg);

    ctrl->mode = SYN_MCTRL_MODE_IDLE;
    ctrl->state = SYN_MCTRL_STOPPED;
    ctrl->last_position = read_position(ctrl);
    ctrl->measured_position = ctrl->last_position;
    ctrl->last_update_tick = syn_port_get_tick_ms();
    ctrl->enabled = true;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;
    ctrl->ff_output = 0;
    ctrl->total_output = 0;
    ctrl->datalog = NULL;

    return SYN_OK;
}

void syn_motor_ctrl_set_output(SYN_MotorCtrl *ctrl, int32_t output)
{
    SYN_ASSERT(ctrl != NULL);

    /* Clamp to configured output range */
    if (output > ctrl->cfg.output_max)
        output = ctrl->cfg.output_max;
    if (output < ctrl->cfg.output_min)
        output = ctrl->cfg.output_min;

    ctrl->mode = SYN_MCTRL_MODE_OPEN_LOOP;
    ctrl->state = SYN_MCTRL_RUNNING;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;
    ctrl->stall_active = false;
    ctrl->pid_output = 0;
    ctrl->ff_output = 0;
    ctrl->total_output = output;

    apply_output(ctrl, output);
    ctrl->last_position = read_position(ctrl);
    ctrl->last_update_tick = syn_port_get_tick_ms();
}

void syn_motor_ctrl_set_velocity(SYN_MotorCtrl *ctrl, int32_t units_per_sec)
{
    SYN_ASSERT(ctrl != NULL);

    ctrl->target_velocity = units_per_sec;
    ctrl->mode = SYN_MCTRL_MODE_VELOCITY;
    ctrl->state = SYN_MCTRL_RUNNING;
    ctrl->stall_active = false;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;

    syn_pid_reset(&ctrl->pid);
    ctrl->last_position = read_position(ctrl);
    ctrl->last_update_tick = syn_port_get_tick_ms();
}

void syn_motor_ctrl_set_position(SYN_MotorCtrl *ctrl, int32_t target)
{
    SYN_ASSERT(ctrl != NULL);

    /* Clamp target to soft limits */
    if (limits_enabled(ctrl)) {
        if (target < ctrl->cfg.position_min)
            target = ctrl->cfg.position_min;
        if (target > ctrl->cfg.position_max)
            target = ctrl->cfg.position_max;
    }

    ctrl->target_position = target;
    ctrl->mode = SYN_MCTRL_MODE_POSITION;
    ctrl->state = SYN_MCTRL_RUNNING;
    ctrl->stall_active = false;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;

    syn_pid_reset(&ctrl->pid);
    ctrl->last_position = read_position(ctrl);
    ctrl->last_update_tick = syn_port_get_tick_ms();
}

void syn_motor_ctrl_set_trajectory(SYN_MotorCtrl *ctrl, const SYN_MotorCtrl_Trajectory *traj)
{
    SYN_ASSERT(ctrl != NULL);
    SYN_ASSERT(traj != NULL);

    ctrl->trajectory = *traj;
    ctrl->target_position = traj->position;
    ctrl->trajectory_active = true;

    /* First call: switch mode and reset PID */
    if (ctrl->mode != SYN_MCTRL_MODE_POSITION || ctrl->state == SYN_MCTRL_STOPPED) {
        ctrl->mode = SYN_MCTRL_MODE_POSITION;
        ctrl->state = SYN_MCTRL_RUNNING;
        ctrl->stall_active = false;
        syn_pid_reset(&ctrl->pid);
        ctrl->last_position = read_position(ctrl);
        ctrl->last_update_tick = syn_port_get_tick_ms();
    }
}
void syn_motor_ctrl_move_to(SYN_MotorCtrl *ctrl, int32_t target, int32_t max_velocity,
                            int32_t acceleration)
{
    SYN_ASSERT(ctrl != NULL);
    SYN_ASSERT(max_velocity > 0);
    SYN_ASSERT(acceleration > 0);

    /* Clamp target to soft limits */
    if (ctrl->cfg.position_min != 0 || ctrl->cfg.position_max != 0) {
        if (target < ctrl->cfg.position_min)
            target = ctrl->cfg.position_min;
        if (target > ctrl->cfg.position_max)
            target = ctrl->cfg.position_max;
    }

    /* Convert per-second → per-tick in Q8 fixed-point.
     * vel_q8  = (vel_per_sec << 8) / update_hz
     * accel_q8 = (accel_per_sec² << 8) / (update_hz²)
     * Use 64-bit to avoid overflow in the shift. */
    int32_t hz = (int32_t)ctrl->cfg.update_hz;
    int32_t vel_q8 = (int32_t)(((int64_t)max_velocity << 8) / hz);
    int32_t accel_q8 = (int32_t)(((int64_t)acceleration << 8) / ((int64_t)hz * hz));
    if (vel_q8 < 1)
        vel_q8 = 1;
    if (accel_q8 < 1)
        accel_q8 = 1;

    if (ctrl->profile_active) {
        /* Mid-move retarget: update target while preserving current velocity.
         * The ramp will decelerate/reverse smoothly toward the new target. */
        ctrl->profile.target = target;
        ctrl->profile.rate = vel_q8;
        ctrl->profile.accel = accel_q8;
        ctrl->profile.done = false;
    } else {
        /* Fresh move from standstill */
        int32_t current = read_position(ctrl);
        syn_ramp_init(&ctrl->profile, current);
        syn_ramp_set_target_trapezoid_fp(&ctrl->profile, target, vel_q8, accel_q8, 8);

        ctrl->last_position = current;
        ctrl->measured_position = current;
        ctrl->last_update_tick = syn_port_get_tick_ms();
        syn_pid_reset(&ctrl->pid);
    }

    ctrl->target_position = target;
    ctrl->mode = SYN_MCTRL_MODE_POSITION;
    ctrl->state = SYN_MCTRL_RUNNING;
    ctrl->stall_active = false;
    ctrl->trajectory_active = true;
    ctrl->profile_active = true;
}

void syn_motor_ctrl_move_to_scurve(SYN_MotorCtrl *ctrl, int32_t target, int32_t max_velocity,
                                   int32_t max_accel, int32_t max_jerk)
{
    SYN_ASSERT(ctrl != NULL);
    SYN_ASSERT(max_velocity > 0);
    SYN_ASSERT(max_accel > 0);
    SYN_ASSERT(max_jerk > 0);

    /* Clamp target to soft limits */
    if (ctrl->cfg.position_min != 0 || ctrl->cfg.position_max != 0) {
        if (target < ctrl->cfg.position_min)
            target = ctrl->cfg.position_min;
        if (target > ctrl->cfg.position_max)
            target = ctrl->cfg.position_max;
    }

    /* Convert per-second → per-tick.
     * The S-curve works in integer ticks, so:
     *   v_tick = v_sec / hz
     *   a_tick = a_sec / hz²
     *   j_tick = j_sec / hz³
     * Clamp to minimum of 1 to avoid zero-motion. */
    int32_t hz = (int32_t)ctrl->cfg.update_hz;
    int32_t v_tick = max_velocity / hz;
    int32_t a_tick = max_accel / (hz * hz);
    int32_t j_tick = max_jerk / (hz * hz * hz);
    if (v_tick < 1)
        v_tick = 1;
    if (a_tick < 1)
        a_tick = 1;
    if (j_tick < 1)
        j_tick = 1;

    int32_t current = read_position(ctrl);
    syn_scurve_init(&ctrl->scurve_profile, current);
    syn_scurve_set_constraints(&ctrl->scurve_profile, v_tick, a_tick, j_tick);
    syn_scurve_set_target(&ctrl->scurve_profile, target);

    ctrl->target_position = target;
    ctrl->mode = SYN_MCTRL_MODE_POSITION;
    ctrl->state = SYN_MCTRL_RUNNING;
    ctrl->stall_active = false;
    ctrl->trajectory_active = true;
    ctrl->profile_active = false;
    ctrl->scurve_active = true;

    syn_pid_reset(&ctrl->pid);
    ctrl->last_position = current;
    ctrl->measured_position = current;
    ctrl->last_update_tick = syn_port_get_tick_ms();
}

void syn_motor_ctrl_stop(SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->mode = SYN_MCTRL_MODE_IDLE;
    ctrl->state = SYN_MCTRL_STOPPED;
    ctrl->pid_output = 0;
    ctrl->ff_output = 0;
    ctrl->total_output = 0;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;
    stop_motor(ctrl);
    syn_pid_reset(&ctrl->pid);
}

void syn_motor_ctrl_estop(SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->mode = SYN_MCTRL_MODE_IDLE;
    ctrl->state = SYN_MCTRL_STOPPED;
    ctrl->pid_output = 0;
    ctrl->ff_output = 0;
    ctrl->total_output = 0;
    ctrl->trajectory_active = false;
    ctrl->profile_active = false;
    ctrl->scurve_active = false;
    brake_motor(ctrl);
    syn_pid_reset(&ctrl->pid);
}

SYN_MotorCtrl_State syn_motor_ctrl_update(SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);

    if (ctrl->mode == SYN_MCTRL_MODE_IDLE || !ctrl->enabled) {
        return ctrl->state;
    }

    uint32_t now = syn_port_get_tick_ms();
    uint32_t dt_ms = now - ctrl->last_update_tick;
    if (dt_ms == 0)
        dt_ms = 1;

    /* ── Read feedback ──────────────────────────────────────────── */
    int32_t current_pos = read_position(ctrl);
    int32_t delta = current_pos - ctrl->last_position;
    ctrl->last_position = current_pos;
    ctrl->measured_position = current_pos;

    /* Velocity = delta × update_hz (units per second) */
    ctrl->measured_velocity = delta * (int32_t)ctrl->cfg.update_hz;

    /* ── Open-loop: skip PID, maintain current output ──────────── */
    if (ctrl->mode == SYN_MCTRL_MODE_OPEN_LOOP) {
        ctrl->last_update_tick = now;
        return ctrl->state;
    }

    /* ── Advance built-in profile (move_to) ─────────────────────── */
    if (ctrl->profile_active) {
        int32_t prev_vel = ctrl->trajectory.velocity;
        int32_t prev_profile_pos = syn_ramp_value(&ctrl->profile);
        int32_t profile_pos = syn_ramp_update(&ctrl->profile);
        int32_t profile_vel = (profile_pos - prev_profile_pos) * (int32_t)ctrl->cfg.update_hz;
        int32_t profile_accel = (profile_vel - prev_vel) * (int32_t)ctrl->cfg.update_hz;

        ctrl->trajectory.position = profile_pos;
        ctrl->trajectory.velocity = profile_vel;
        ctrl->trajectory.acceleration = profile_accel;
        ctrl->target_position = profile_pos;

        if (syn_ramp_done(&ctrl->profile)) {
            ctrl->profile_active = false;
            ctrl->trajectory_active = false;
        }
    }

    /* ── Advance built-in S-curve profile (move_to_scurve) ─────── */
    if (ctrl->scurve_active) {
        int32_t hz = (int32_t)ctrl->cfg.update_hz;
        syn_scurve_update(&ctrl->scurve_profile);

        ctrl->trajectory.position = syn_scurve_position(&ctrl->scurve_profile);
        ctrl->trajectory.velocity = syn_scurve_velocity(&ctrl->scurve_profile) * hz;
        ctrl->trajectory.acceleration = syn_scurve_acceleration(&ctrl->scurve_profile) * hz * hz;
        ctrl->target_position = ctrl->trajectory.position;

        if (syn_scurve_done(&ctrl->scurve_profile)) {
            ctrl->scurve_active = false;
            ctrl->trajectory_active = false;
        }
    }

    /* ── Compute feedforward ────────────────────────────────────── */
    int32_t ff = 0;
    if (ctrl->trajectory_active && (ctrl->cfg.ff_kv != 0 || ctrl->cfg.ff_ka != 0)) {
        int32_t ff_scale = (ctrl->cfg.ff_scale > 0) ? (int32_t)1 << ctrl->cfg.ff_scale : 1;
        ff = (ctrl->cfg.ff_kv * ctrl->trajectory.velocity +
              ctrl->cfg.ff_ka * ctrl->trajectory.acceleration) /
             ff_scale;

        /* Clamp FF to leave headroom for PID correction.
         * FF gets at most 90% of the output range — PID needs room to work. */
        int32_t ff_max = (ctrl->cfg.output_max * 9) / 10;
        int32_t ff_min = (ctrl->cfg.output_min * 9) / 10;
        if (ff > ff_max)
            ff = ff_max;
        if (ff < ff_min)
            ff = ff_min;
    }
    ctrl->ff_output = ff;

    /* ── Compute PID ────────────────────────────────────────────── */
    int32_t pid_out;

    if (ctrl->mode == SYN_MCTRL_MODE_VELOCITY) {
        /* Velocity: setpoint is target velocity, measured is actual */
        pid_out = syn_pid_update(&ctrl->pid, ctrl->target_velocity, ctrl->measured_velocity, dt_ms);
    } else {
        /* Position mode (includes trajectory tracking) */
        int32_t error = ctrl->target_position - current_pos;

        /* Check deadband (only when trajectory is not actively driving) */
        if (!ctrl->trajectory_active) {
            int32_t abs_error = (error < 0) ? -error : error;
            if (abs_error <= ctrl->cfg.position_deadband) {
                if (ctrl->state != SYN_MCTRL_ON_TARGET) {
                    ctrl->state = SYN_MCTRL_ON_TARGET;
                    ctrl->pid_output = 0;
                    ctrl->ff_output = 0;
                    ctrl->total_output = 0;
                    stop_motor(ctrl);
                    syn_pid_reset(&ctrl->pid);

                    if (ctrl->on_target != NULL) {
                        ctrl->on_target(ctrl, ctrl->on_target_ctx);
                    }
                }
                return ctrl->state;
            }
        }

        ctrl->state = SYN_MCTRL_RUNNING;
        pid_out = syn_pid_update(&ctrl->pid, ctrl->target_position, current_pos, dt_ms);
    }

    ctrl->pid_output = pid_out;

    /* ── Combine feedforward + feedback ─────────────────────────── */
    int32_t output = pid_out + ff;

    /* Clamp combined output — with FF-aware anti-windup.
     * If the combined output saturates, the PID integrator is frozen
     * to prevent windup. This is critical when FF consumes most of
     * the output headroom (e.g., during high-speed cruise). */
    bool saturated = false;
    if (output > ctrl->cfg.output_max) {
        output = ctrl->cfg.output_max;
        saturated = true;
    }
    if (output < ctrl->cfg.output_min) {
        output = ctrl->cfg.output_min;
        saturated = true;
    }

    if (saturated) {
        /* Freeze integrator — don't let it accumulate while we're clipping */
        int32_t max_pid = ctrl->cfg.output_max - ff;
        int32_t min_pid = ctrl->cfg.output_min - ff;
        if (max_pid < min_pid) {
            int32_t t = max_pid;
            max_pid = min_pid;
            min_pid = t;
        }
        /* Clamp the integrator to keep PID output within available headroom */
        if (ctrl->pid.integral > 0 && pid_out > max_pid) {
            ctrl->pid.integral -= (pid_out - max_pid) * ctrl->pid.cfg.scale;
        }
        if (ctrl->pid.integral < 0 && pid_out < min_pid) {
            ctrl->pid.integral -= (pid_out - min_pid) * ctrl->pid.cfg.scale;
        }
    }

    ctrl->total_output = output;

    /* ── Enforce soft position limits ───────────────────────────── */
    if (at_limit(ctrl, current_pos, output)) {
        ctrl->state = SYN_MCTRL_LIMIT;
        ctrl->pid_output = 0;
        ctrl->ff_output = 0;
        ctrl->total_output = 0;
        stop_motor(ctrl);
        syn_pid_reset(&ctrl->pid);
#if !defined(SYN_USE_ERRLOG) || SYN_USE_ERRLOG
        if (ctrl->cfg.errlog != NULL) {
            syn_errlog_record(ctrl->cfg.errlog, SYN_MCTRL_ERR_LIMIT, SYN_ERR_WARNING,
                              (uint32_t)current_pos);
        }
#endif
        return ctrl->state;
    }

    if (ctrl->state != SYN_MCTRL_STALLED) {
        ctrl->state = SYN_MCTRL_RUNNING;
    }

    /* ── Apply to motor ─────────────────────────────────────────── */
    apply_output(ctrl, output);

    /* ── Tuning capture ─────────────────────────────────────────── */
#if !defined(SYN_USE_DATALOG) || SYN_USE_DATALOG
    if (ctrl->datalog != NULL) {
        SYN_MotorCtrl_Sample sample = {
            .tick_ms = now,
            .target_pos = ctrl->target_position,
            .measured_pos = current_pos,
            .target_vel =
                ctrl->trajectory_active ? ctrl->trajectory.velocity : ctrl->target_velocity,
            .measured_vel = ctrl->measured_velocity,
            .ff_output = ff,
            .pid_output = pid_out,
            .total_output = output,
        };
        syn_datalog_write(ctrl->datalog, SYN_MCTRL_DATALOG_ID, &sample, sizeof(sample));
    }
#endif

    /* ── Accumulate move metrics ────────────────────────────────── */
    {
        int32_t pos_error = ctrl->target_position - current_pos;
        int32_t abs_err = (pos_error < 0) ? -pos_error : pos_error;
        int32_t abs_out = (output < 0) ? -output : output;

        if (abs_err > ctrl->metrics.max_error)
            ctrl->metrics.max_error = abs_err;

        ctrl->metrics.error_sq_sum += (int64_t)pos_error * pos_error;

        if (abs_out > ctrl->metrics.peak_output)
            ctrl->metrics.peak_output = abs_out;

        /* Overshoot: position went past target */
        if (ctrl->mode == SYN_MCTRL_MODE_POSITION) {
            int32_t beyond = current_pos - ctrl->target_position;
            /* Overshoot direction depends on which side we approached from.
             * Use abs(beyond) if error has changed sign (we crossed target). */
            int32_t abs_beyond = (beyond < 0) ? -beyond : beyond;
            if (abs_err < ctrl->metrics.max_error && abs_beyond > ctrl->metrics.overshoot) {
                /* Only count as overshoot once we've been closer before */
                ctrl->metrics.overshoot = abs_beyond;
            }
        }

        /* Settle time: first time within deadband */
        if (ctrl->metrics.settle_tick == 0 && abs_err <= ctrl->cfg.position_deadband) {
            ctrl->metrics.settle_tick = now;
        }

        ctrl->metrics.sample_count++;
    }

    /* ── Stall detection ────────────────────────────────────────── */
    if (ctrl->cfg.stall_timeout_ms > 0) {
        int32_t abs_delta = (delta < 0) ? -delta : delta;
        int32_t abs_output = (output < 0) ? -output : output;

        if (abs_output > (ctrl->cfg.output_max / 4) && abs_delta <= ctrl->cfg.stall_threshold) {
            if (!ctrl->stall_active) {
                ctrl->stall_active = true;
                ctrl->stall_start_tick = now;
            } else if ((now - ctrl->stall_start_tick) >= ctrl->cfg.stall_timeout_ms) {
                ctrl->state = SYN_MCTRL_STALLED;
                ctrl->mode = SYN_MCTRL_MODE_IDLE;
                ctrl->trajectory_active = false;
                ctrl->profile_active = false;
                ctrl->scurve_active = false;
                stop_motor(ctrl);
                syn_pid_reset(&ctrl->pid);

#if !defined(SYN_USE_ERRLOG) || SYN_USE_ERRLOG
                if (ctrl->cfg.errlog != NULL) {
                    syn_errlog_record(ctrl->cfg.errlog, SYN_MCTRL_ERR_STALL, SYN_ERR_ERROR,
                                      (uint32_t)current_pos);
                }
#endif

                if (ctrl->on_stall != NULL) {
                    ctrl->on_stall(ctrl, ctrl->on_stall_ctx);
                }
            }
        } else {
            ctrl->stall_active = false;
        }
    }

    ctrl->last_update_tick = now;
    return ctrl->state;
}

void syn_motor_ctrl_on_stall(SYN_MotorCtrl *ctrl, SYN_MotorCtrl_StallCallback cb, void *ctx)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->on_stall = cb;
    ctrl->on_stall_ctx = ctx;
}

void syn_motor_ctrl_on_target(SYN_MotorCtrl *ctrl, SYN_MotorCtrl_TargetCallback cb, void *ctx)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->on_target = cb;
    ctrl->on_target_ctx = ctx;
}

void syn_motor_ctrl_set_gains(SYN_MotorCtrl *ctrl, int32_t kp, int32_t ki, int32_t kd)
{
    SYN_ASSERT(ctrl != NULL);
    syn_pid_set_gains(&ctrl->pid, kp, ki, kd);
}

void syn_motor_ctrl_set_ff_gains(SYN_MotorCtrl *ctrl, int32_t ff_kv, int32_t ff_ka)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->cfg.ff_kv = ff_kv;
    ctrl->cfg.ff_ka = ff_ka;
}

void syn_motor_ctrl_clear_stall(SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);
    if (ctrl->state == SYN_MCTRL_STALLED) {
        ctrl->state = SYN_MCTRL_STOPPED;
        ctrl->stall_start_tick = 0;
        ctrl->stall_active = false;
        ctrl->trajectory_active = false;
        ctrl->profile_active = false;
        ctrl->scurve_active = false;
        syn_pid_reset(&ctrl->pid);
    }
}

void syn_motor_ctrl_set_datalog(SYN_MotorCtrl *ctrl, SYN_DataLog *log)
{
    SYN_ASSERT(ctrl != NULL);
    ctrl->datalog = log;
}

void syn_motor_ctrl_reset_metrics(SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);
    memset(&ctrl->metrics, 0, sizeof(ctrl->metrics));
    ctrl->metrics.move_start_tick = syn_port_get_tick_ms();
}

#include "../util/syn_qmath.h"

int32_t syn_motor_ctrl_rms_error(const SYN_MotorCtrl *ctrl)
{
    SYN_ASSERT(ctrl != NULL);
    if (ctrl->metrics.sample_count == 0)
        return 0;
    int64_t mean_sq = ctrl->metrics.error_sq_sum / (int64_t)ctrl->metrics.sample_count;
    if (mean_sq <= 0)
        return 0;
    return (int32_t)syn_isqrt64((uint64_t)mean_sq);
}

#endif /* SYN_USE_MOTOR_CTRL */