STM32_14.MotorDriver

PWM Motor Driver with Soft Start

Sterownik silnika z funkcją miękkiego startu i kontrolowanym przyspieszeniem. Implementacja oparta na STM32G030 z wykorzystaniem HAL, PWM i maszyny stanów do sterowania kierunkiem jazdy.

STM32G0C++HALPWMMotor driverSoft start

Informacje

  • Repozytorium: STM32_14.MotorDriver
  • Płytka / obszar: STM32G030F6P6 / PWM / direction outputs
  • Kategoria: Power electronics
  • Status: Public repository

Opis techniczny

  • Najważniejszy kod znajduje się w main.cpp oraz MotorDriver.cpp: aplikacja inicjalizuje GPIO, TIM3 PWM i obiekt MotorPwm pracujący przez interfejs IMotor.
  • Sterowanie działa jako prosta maszyna stanów: stop, rideForwardRamp, rideForward, rideBackwardRamp, rideBackward.
  • Projekt zawiera rampę PercentRamp opartą o HAL_GetTick(), dzięki czemu wypełnienie PWM narasta stopniowo zamiast startować od razu z pełną mocą.
  • Wejścia kierunku jazdy są czytane z GPIO, a kod przełącza kierunki z opóźnieniem, tak żeby nie włączać obu kierunków naraz.
  • Firmware rozdziela logikę aplikacji od warstwy PWM, dzięki czemu sterowanie mocą, kierunkiem i rampą przyspieszenia jest zamknięte w czytelnych blokach.

Co dzieje się w kodzie

Wybrane najważniejsze mechanizmy z projektu.

  • main.cpp steruje całym cyklem pracy: odczytuje wejścia kierunku jazdy, prowadzi maszynę stanów i przekazuje procent mocy do abstrakcji IMotor.
  • PercentRamp jest prostą, nieblokującą rampą mocy opartą o HAL_GetTick(). Wypełnienie PWM rośnie od wartości startowej do zadanej w określonym czasie.
  • MotorPwm::MotorSpeed() mapuje procent użytkownika na realne duty PWM. Kod rozwiązuje praktyczny problem: silnik może nie ruszyć przy niskim wypełnieniu, dlatego jest dead zone i krótki „blast” startowy.
  • Kod jest już pisany bardziej obiektowo niż typowy projekt CubeMX: aplikacja używa IMotor, a szczegóły PWM są zamknięte w MotorPwm.

Kluczowe fragmenty kodu

Kod jest skrócony do części istotnych dla zrozumienia projektu.

Maszyna stanów sterowania silnikiem

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;
}

Ten fragment opisuje główną ideę sterowania: jazda nie jest prowadzona przez luźne if-y, tylko przez jawne stany. Przykładowo start jazdy do przodu przechodzi przez rideForwardRamp, gdzie uruchamiana jest rampa PWM. Dopiero po osiągnięciu zamierzonej prędkości firmware przechodzi do rideForward i aktualizuje moc przez MotorSpeed().

Rampa PWM bez blokowania programu

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;
}

Rampa liczy nową wartość na podstawie czasu systemowego, więc nie zatrzymuje pętli głównej długim HAL_Delay(). To jest ważne, bo mikrokontroler może dalej reagować na wejścia użytkownika.

Mapowanie procentu na PWM z impulsem startowym

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;
}

To jest najbardziej praktyczna część sterownika. Kod rozróżnia żądany procent od realnego wypełnienia PWM. Przy starcie daje krótki mocniejszy impuls, aby silnik fizycznie ruszył, a potem wraca do normalnego mapowania.