FreeRTOS Differential Drive Closed-Loop Control: Inverse Kinematics, PID Speed Control, and PWM Motor Driving on STM32

This article breaks down a FreeRTOS closed-loop control pipeline for a differential drive chassis: it first converts linear and angular velocity commands through inverse kinematics, then uses dual-wheel PID controllers to track the target wheel speeds, and finally drives the motors through PWM and direction pins. It solves the practical gap between “high-level motion commands” and “actual motor actuation.” Keywords: FreeRTOS, PID closed loop, PWM motor driving.

Technical Specifications at a Glance

Parameter Description
Runtime Environment STM32 + FreeRTOS
Implementation Language C
Chassis Model Two-wheel differential drive
Control Period 10 ms
Velocity Inputs vx_mmps, wz_mradps
Outputs Left and right wheel PWM and direction control
Core Dependencies HAL / timer PWM, GPIO, math library lroundf
Protocols / Interfaces Task notifications, driver interface, wheel speed feedback interface
Star Count Not provided in the source

This control pipeline maps abstract motion commands to actual motor actuation

In a FreeRTOS control task, robot motion does not come from a single algorithm. It comes from a continuous data path: the upper layer issues velocity commands, and the lower layer completes inverse kinematics, speed regulation, and drive output. This design separates the workflow into three independent modules: “compute the target,” “track the target,” and “execute the target,” which makes the system easier to maintain and replace.

For a differential drive chassis, the upper layer usually cares only about the robot’s forward velocity and yaw rate. It does not directly specify left and right wheel speeds. The control task converts that higher-level motion intent into PWM control values that the motors can actually execute.

Inverse kinematics distributes chassis velocity to the left and right wheels

A differential drive model has no lateral velocity degree of freedom. Its inputs are typically only linear velocity vx_mmps and angular velocity wz_mradps. The controller first converts wz_mradps to rad/s, then combines it with half of the wheelbase to compute the target linear speed for each wheel.

static void diff_inverse_kinematics_mmps_mradps(int16_t vx_mmps,
                                                int16_t wz_mradps,
                                                float* vL,
                                                float* vR)
{
    float wz_rad = (float)wz_mradps / 1000.0f;      // Convert angular velocity units
    float half_base = WHEEL_BASE_MM * 0.5f;         // Compute half the wheelbase

    float vL_val = (float)vx_mmps - wz_rad * half_base; // Target speed for the left wheel
    float vR_val = (float)vx_mmps + wz_rad * half_base; // Target speed for the right wheel

    vL_val = clampf(vL_val, -MAX_WHEEL_SPEED_MMPS, MAX_WHEEL_SPEED_MMPS); // Speed limiting protection
    vR_val = clampf(vR_val, -MAX_WHEEL_SPEED_MMPS, MAX_WHEEL_SPEED_MMPS); // Speed limiting protection

    *vL = vL_val;
    *vR = vR_val;
}

This code maps robot-level velocity commands to left and right wheel target speeds, while applying safety limits before the values enter the control loop.

Its physical meaning is straightforward: during straight motion, both wheels run at the same speed; during a turn, the outer wheel runs faster and the inner wheel runs slower. The benefit of this design is that if you later change the wheelbase or chassis dimensions, you only need to update the kinematic parameters instead of rewriting the PID or driver layer.

PID closed-loop control converts target wheel speed into stable output

The result of inverse kinematics is only the speed that the wheels should reach. In reality, the motor response is affected by load, friction, and power supply variation, so it cannot follow the target precisely on its own. That is why a speed closed loop is necessary: it continuously drives the measured value toward the reference.

Each wheel maintains its own independent PID controller. The proportional term provides a fast response to the current error, the integral term removes steady-state error, and the derivative term suppresses oscillation caused by rapid changes. In the original implementation, you can set the derivative term to zero during early tuning to simplify debugging.

The PID update logic must handle both integral limiting and output limiting

static void PID_Init(PID_t* pid, float kp, float ki, float kd,
                     float i_limit, float out_limit)
{
    pid->kp = kp;
    pid->ki = ki;
    pid->kd = kd;
    pid->i_acc = 0.0f;          // Clear the initial integral value
    pid->prev_err = 0.0f;       // Clear the previous error
    pid->i_limit = i_limit;
    pid->out_limit = out_limit;
}

static int16_t PID_Update(PID_t* pid, float ref, float meas, float dt)
{
    float err = ref - meas;                         // Current speed error
    float p_out = pid->kp * err;                   // Proportional output

    pid->i_acc += pid->ki * err * dt;              // Accumulate the integral of the error
    pid->i_acc = clampf(pid->i_acc, -pid->i_limit, pid->i_limit); // Prevent integral windup

    float d_out = 0.0f;
    if (pid->kd != 0.0f && dt > 0.0f) {
        d_out = pid->kd * (err - pid->prev_err) / dt; // Compute the derivative term
    }
    pid->prev_err = err;

    float out = p_out + pid->i_acc + d_out;        // Combine the control output
    out = clampf(out, -pid->out_limit, pid->out_limit); // Limit the total output

    return clampi16((int16_t)lroundf(out), PWM_MIN, PWM_MAX); // Convert to a PWM value
}

This code generates a protected PWM control value from the difference between the target wheel speed and the measured wheel speed, preventing integral runaway and output overshoot.

The most important engineering detail here is not the formula itself, but the limiting strategy. Without i_limit, the integral term keeps accumulating under persistent error. As the system gets close to the target, the controller may still output too much control effort, which leads to obvious overshoot and oscillation.

The PWM driver layer converts logical control values into hardware behavior

The output of the PID controller is still only a logical value. To actually drive the motor, the system must coordinate the direction pins and timer PWM. Typical two-line H-bridge drivers, such as TB6612 and L298, use a “direction + duty cycle” control scheme.

In this implementation, Motor_ApplyPwm(pwmL, pwmR) is the execution entry point. It accepts logical PWM values in the range -1000~1000. The sign determines the direction, and the absolute value determines the duty cycle. This lets the upper layer focus only on control values instead of low-level GPIO states.

static void motor_apply_dir_2pin(int16_t pwm, GPIO_TypeDef* in1_port, uint16_t in1_pin,
                                 GPIO_TypeDef* in2_port, uint16_t in2_pin)
{
    if (pwm > 0) {
        HAL_GPIO_WritePin(in1_port, in1_pin, GPIO_PIN_SET);   // Forward direction
        HAL_GPIO_WritePin(in2_port, in2_pin, GPIO_PIN_RESET);
    } else if (pwm < 0) {
        HAL_GPIO_WritePin(in1_port, in1_pin, GPIO_PIN_RESET); // Reverse direction
        HAL_GPIO_WritePin(in2_port, in2_pin, GPIO_PIN_SET);
    } else {
        HAL_GPIO_WritePin(in1_port, in1_pin, GPIO_PIN_RESET); // Stop with brake/coast behavior
        HAL_GPIO_WritePin(in2_port, in2_pin, GPIO_PIN_RESET);
    }
}

This code switches the motor direction pins based on the sign of the PWM value, implementing forward rotation, reverse rotation, and stop control.

The full control flow should run at a stable fixed period

In each 10 ms control cycle, the task usually runs in a fixed sequence: read the velocity command, execute inverse kinematics, obtain wheel speed feedback, update the dual PID controllers, and apply PWM. This sequence forms a stable speed closed loop.

AI Visual Insight: This diagram shows the serial control chain of a differential drive chassis, from upper-layer velocity commands to actuator output. The target velocity first goes through left/right wheel distribution, then combines with wheel speed feedback and enters a dual-channel PID controller, which finally outputs left and right PWM signals to the motor driver. The diagram highlights the standard discrete closed-loop structure built from the reference value, feedback value, and control output, which maps naturally to a periodic FreeRTOS control task.

A typical periodic task framework looks like this

void ControlTask_10ms(void)
{
    float refL, refR, measL, measR;

    diff_inverse_kinematics_mmps_mradps(cmd_vx, cmd_wz, &refL, &refR); // Compute the target wheel speeds
    Motor_GetWheelSpeedMmps(&measL, &measR);                           // Read the actual wheel speeds

    int16_t pwmL = PID_Update(&pid_left, refL, measL, 0.01f);         // Closed-loop control for the left wheel
    int16_t pwmR = PID_Update(&pid_right, refR, measR, 0.01f);        // Closed-loop control for the right wheel

    Motor_ApplyPwm(pwmL, pwmR);                                       // Send the output to the driver layer
}

This code connects inverse kinematics, feedback acquisition, PID computation, and PWM output into a deterministic 10 ms control cycle.

This modular design improves system scalability

The greatest value of this solution is not only that it makes a two-wheel robot move, but that it establishes clear software boundaries. The inverse kinematics layer handles only mathematical mapping, the PID layer handles only error convergence, and the driver layer handles only hardware execution. The three layers remain loosely coupled.

That means if you later replace simulated wheel speed feedback with encoder feedback, or move from a two-wheel differential drive to another chassis structure, you can constrain the required changes to the corresponding module without breaking the overall FreeRTOS control task framework.

FAQ

Q1: Why can’t the upper layer send motor PWM directly instead of performing inverse kinematics first?

Because the upper layer describes the robot’s overall motion goal, such as forward velocity and yaw rate, while the motor actuators require wheel-level targets such as left and right wheel speed or PWM. Inverse kinematics converts a robot-level goal into wheel-level targets, which is a necessary bridge in chassis control.

Q2: Why is integral limiting required in a PID controller?

The integral term accumulates historical error. If the motor cannot reach the target for an extended period, the integral term keeps growing and causes integral windup. Even after the error decreases, the controller may still output excessive control effort, leading to overshoot, oscillation, and poor stability. That is why limiting is an engineering necessity.

Q3: Can simulated wheel speed feedback directly replace a real encoder?

No, not for long-term use. Simulated feedback works well during early integration and debugging because it helps verify closed-loop logic, interfaces, and parameter trends. In a real deployment, however, you still need encoder or Hall sensor feedback. Otherwise, the system cannot reflect real friction, load disturbances, or mechanical offsets, and control accuracy remains limited.

AI Readability Summary

This article reconstructs the execution pipeline of a differential drive chassis inside a FreeRTOS control task. It focuses on inverse kinematics, dual-wheel PID closed-loop regulation, and PWM driver interface design. It explains how target velocity becomes executable motor control output and provides a modular implementation approach suitable for STM32 and embedded systems.