STM32_14.MotorDriver

PWM Motor Driver with Soft Start

Motor controller with soft start and controlled acceleration. Implementation based on STM32G030 using HAL, PWM and a state machine for direction control.

STM32G0C++HALPWMMotor driverSoft start

Project facts

  • Repository: STM32_14.MotorDriver
  • Board / area: STM32G030F6P6 / PWM / direction outputs
  • Category: Power electronics
  • Status: Public repository

Technical description

  • The key code lives in main.cpp and MotorDriver.cpp: the application initializes GPIO, TIM3 PWM and a MotorPwm object exposed through the IMotor interface.
  • Control is implemented as a small state machine: stop, rideForwardRamp, rideForward, rideBackwardRamp and rideBackward.
  • The PercentRamp logic uses HAL_GetTick() so the PWM duty cycle increases gradually instead of applying full power instantly.
  • Direction inputs are read from GPIO and output direction pins are switched with a safety delay to avoid enabling both directions at once.
  • The firmware separates application logic from the PWM layer, keeping power control, direction switching and acceleration ramping in clear blocks.

What happens in the code

Selected key mechanisms from the project code. This is not a full repository dump, but an analysis of the parts responsible for the main firmware logic.

  • main.cpp owns the main control loop: it reads direction inputs, runs the state machine and passes power percentage to the IMotor abstraction.
  • PercentRamp is a simple non-blocking power ramp based on HAL_GetTick(). PWM duty rises from a start value to the target over a defined time window.
  • MotorPwm::MotorSpeed() maps requested percent to real PWM duty. It solves a practical motor issue: low duty may not start rotation, so the code adds a dead zone and a short start boost.
  • The firmware is more structured than a typical CubeMX-only project: the application uses IMotor, while PWM details are isolated in MotorPwm.

Key code excerpts

The code is shortened to the parts that matter for understanding the project.

Motor-control state machine

Core/Src/main.cpp
enum class RideState : uint8_t
{
    Stop = 0u,
    RideForward,
    RideBackward,
    RideForwardRamp,
    RideBackwardRamp
};

switch(motor_state.motor)
{
    case RideState::RideForwardRamp:
        StartForwardRamp(now_ms);
        break;

    case RideState::RideForward:
        HandleForwardState(now_ms);
        break;

    case RideState::RideBackwardRamp:
        StartBackwardRamp(now_ms);
        break;

    case RideState::RideBackward:
        HandleBackwardState(now_ms);
        break;

    case RideState::Stop:
        HandleStopState(now_ms);
        break;
}

This snippet captures the main control idea: driving is not handled by random if statements, but through explicit states. Forward motion starts through RideForwardRamp, where the PWM ramp is started. The firmware then moves into RideForward and updates power through MotorSpeed().

Non-blocking PWM ramp

Core/Src/main.cpp
void RampStart(PercentRamp& ramp, uint8_t from, uint8_t to, uint32_t duration_ms)
{
    ramp.from = from;
    ramp.value = from;
    ramp.target = to;
    ramp.start_ms = HAL_GetTick();
    ramp.duration_ms = duration_ms;
    ramp.active = true;
}

uint8_t RampUpdate(PercentRamp& ramp)
{
    uint32_t elapsed = HAL_GetTick() - ramp.start_ms;
    if (elapsed >= ramp.duration_ms)
    {
        ramp.value = ramp.target;
        ramp.active = false;
        return ramp.value;
    }
    ramp.value = ramp.from + (elapsed * (ramp.target - ramp.from)) / ramp.duration_ms;
    return ramp.value;
}

The ramp calculates the new value from the system tick, so it does not stop the main loop with a long HAL_Delay(). This matters because the controller can still react to user inputs.

Mapping percent to PWM with start boost

Core/Src/MotorDriver.cpp
void MotorPwm::MotorSpeed(uint8_t percent)
{
    if (timer_ == nullptr)
    {
        return;
    }

    if (percent > kPercentMax)
    {
        percent = kPercentMax;
    }

    const uint32_t now_ms = HAL_GetTick();
    const uint32_t period_plus_one = __HAL_TIM_GET_AUTORELOAD(timer_) + 1u;

    // 1) Martwa strefa: STOP i reset
    if (percent < input_min_)
    {
        blast_active = 0u;
        prev_speed_percent = percent;
        __HAL_TIM_SET_COMPARE(timer_, channel_, 0u);
        return;
    }

    // 2) Kierunek zmiany (żeby anulować blast przy schodzeniu)
    const bool increasing = (percent > prev_speed_percent);

    // 3) Start blasta TYLKO na krawędzi startu (<input_min_ -> >= input_min_)
    const bool start_edge = (prev_speed_percent < input_min_) && (percent >= input_min_);

    if (start_edge)
    {
        blast_active = 1u;
        blast_end_ms = now_ms + blast_time_;
    }

    // 4) Jeśli użytkownik schodzi w dół – nie trzymaj kopa
    if (blast_active && !increasing)
    {
        blast_active = 0u;
    }

    // 5) Jeśli czas blasta minął – wyłącz
    if (blast_active && (int32_t)(now_ms - blast_end_ms) >= 0)
    {
        blast_active = 0u;
    }

    // 6) Mapowanie percent -> duty (tu liniowo dla lepszego dołu)
    float norm = (float)(percent - input_min_) / (100.0f - (float)input_min_);
    if (norm < 0.0f) norm = 0.0f;
    if (norm > 1.0f) norm = 1.0f;

    float shaped = norm;          // lepsza kontrola 0..niski zakres
    // float shaped = norm * norm; // jeśli chcesz spłaszczyć dół

    float duty_f = (float)duty_min_ + shaped * (float)(duty_max_ - duty_min_);
    if (duty_f < 0.0f) duty_f = 0.0f;
    if (duty_f > 100.0f) duty_f = 100.0f;

    // 7) Blast jako MINIMUM przez blast_time_ (nie blokuje sterowania na stałe)
    if (blast_active)
    {
        if (duty_f < (float)blast_duty_)
            duty_f = (float)blast_duty_;
    }

    // 8) Ustaw CCR z rozdzielczością (bez schodków 1%)
    uint32_t ccr = (uint32_t)(duty_f * (float)period_plus_one / 100.0f + 0.5f);
    if (ccr > period_plus_one) ccr = period_plus_one;

    __HAL_TIM_SET_COMPARE(timer_, channel_, ccr);
    prev_speed_percent = percent;
}

This is the most practical part of the driver. The code separates requested percent from real PWM duty. On startup it applies a short stronger boost so the motor can physically start, then falls back to normal mapping.