Продолжение разработки сервоконтроллера MC50. В предыдущем проекте управления DС мотором специалисты в комментариях высказали сомнения относительно способа изменять состояние линий ШИМ путём переназначения функций портов в реальном времени. Этот способ был применён в связи с оригинальной архитектурой формирователей мёртвого времени в таймерах чипов семейства Renesas Synergy.

Все статьи по открытому проекту MC50 здесь:

Чем оригинальна архитектура таймеров в Renesas Synergy

Во многих популярных микроконтроллерах мёртвое формируется отдельным узлом вносящим задержку в нарастающие и спадающие фронты исходного сигнала формируемого компараторами.

Диаграмма взятая из даташита STM32H753

В простейшем варианте для верхнего и нижнего ключей транзисторного полумоста получаем два сигнала, один с задержкой от нарастающего фронта, а другой инвертированный с задержкой от спадающего фронта исходного сигнала таймера.

В чипах Renesas Synergy подобного узла задержки нет. Вместо этого в их таймер встроен механизм автоматической корректировки значения компаратора-формирователя ШИМ нижнего ключа на основе значения компаратора-формирователя ШИМ верхнего ключа. Это позволяет более строго выдерживать симметричность импульсов ШИМ при изменении скважности.

Чем был плох предыдущий пример

Реализация была дана здесь. В контексте конкретной схемы MC50 ничем. Но в общем случае переход на функции портов ввода-вывода отключает возможность автоматического аварийного сброса всех 6 сигналов ШИМ в ноль. В MC50 за сброс в ноль напряжений на затворах ответственность несёт микросхема драйвера, там реализованы все защиты по перенапряжению, недонапряжению, перегрузкам по току и температуре. Конечно, микроконтроллер по прежнему готов реагировать на аварии, но реакция может затянуться на долгие сотни наносекунд. Хотя по правде говоря в инверторах и так существуют сквозные токи до сотни наносекунд из-за медленного рассасывания носителей в диодах мощных MOSFET.

Проблема симметричного ШИМ с мёртвым временем реализованным на задержках

Собственно проблеме не в самом ШИМ, а в том как к нему привязана выборка АЦП. В импульсных схемах измерение тока делают строго по середине импульсов, чтобы точно знать средний ток. В таймерах есть специальные средства для синхронизации выборки АЦП с генератором ШИМ. Если мёртвое время постоянно, то с синхронизацией в обычных таймерах проблем нет. Но если мёртвое время регулируется в реальном времени, то и точку выборки АЦП надо регулировать вместе с ним. В Renesas Synergy этого делать не надо, поскольку точка выборки всегда останется на своём месте. Это экономит ресурсы времени, поскольку часто на процедуру обслуживания прерываний остаются считанные микросекунды.

Новая реализация для Renesas Synergy

Новая реализация не трогает функции портов ввода-вывода. Она полагается только на функциональность самих таймеров. Однако есть существенный нюанс. Метод работает с задержкой на один или два цикла ШИМ. Но в моторах это некритично.

Сначала покажем простой, но ошибочный способ

Как всегда все упирается в неполноту документации на микроконтроллеры. Средний мануал уже переваливает за 2 тыс. страниц, но информации становится все меньше.
В регистрах таймера Renesas Synergy можно найти General PWM Timer Count Direction and Duty Setting Register. Название само за себя говорит. Это то что должно легко давать возможность менять ШИМ с 0% до 100% и обратно.

Функция управления ШИМ выглядит так:

/*-----------------------------------------------------------------------------------------------------
  Функция управления модуляцией с возможностью переключения в 0% или в 100% модуляции

  \param R_GPT
  \param pwm_lev
-----------------------------------------------------------------------------------------------------*/
static inline void _Set_gpt_pwm_level_2(R_GPTA0_Type *R_GPT, uint32_t pwm_lev)
{

  if (pwm_lev == LEVEL_0)
  {
    R_GPT->GTUDDTYC_b.OADTY = 2; // 0%
    R_GPT->GTUDDTYC_b.OBDTY = 3; // 100%
  }
  else if (pwm_lev == LEVEL_1)
  {
    R_GPT->GTUDDTYC_b.OBDTY = 2; // 0%
    R_GPT->GTUDDTYC_b.OADTY = 3; // 100%
  }
  else
  {
    R_GPT->GTUDDTYC_b.OBDTY = 1; // pwm
    R_GPT->GTUDDTYC_b.OADTY = 1; // pwm
    R_GPT->GTCCRC = pwm_indx_to_comp[pwm_lev];
  }
}

Но осциллограмма переключения сигналов на выходе выглядит печально:

Этот регистр правильно устанавливает 0% и 100% ШИМ, но совершенно не учитывает мёртвое время, несмотря на то, что оно было задано в соответствующих регистрах.

Теперь правильный способ

Правильный способ должен обязательно проверять на какой фазе счётчика мы находимся в момент прерывания: нарастания или спада и какое было состояние ШИМ до этого.

Диаграмма с разными сценариями загрузки компараторов счётчика дана ниже.

Алгоритм действий с таймером при инициализации следующий:

  • Конфигурирование таймера для генерации симметричного ШИМ сигнала с назначением мёртвого времени одинакового для переднего и заднего фронтов

  • При симметричном ШИМ таймер сначала нарастает потом спадает. Сигнал на верхнем ключе в 1 когда таймер превышает значение компаратора.

  • Компараторы загружаются в режиме буферизации. Загруженное сейчас значение вступает в силу только со старта следующего цикла

  • Загружаем только компаратор канала A (верхнего ключа). Компаратор B (нижнего ключа) вычисляется автоматически формирователем мёртвого времени.

  • Отдельно программируем компаратор генерации триггера начала выборки АЦП. Срабатывание компаратор происходит на пару микросекунд раньше начала очередного цикла таймера. Таким образом сам момент выборки приходится точно на центр импульса ШИМ.

  • По окончании преобразований АЦП вызывается прерывание. В этом прерывании производятся все основные действия по расчёту и записи новых значений в компараторы таймеров для следующего цикла ШИМ.

Инициализация таймера выглядит так:

/*-----------------------------------------------------------------------------------------------------
  Конфигурирование таймера для генерации ШИМ сигнала
  Формируется симметричный ШИМ.
  Таймер сначала нарастает потом спадает
  Используется встроенный формирователь мертвого времени.
  Загружаем только компаратор канала A (верхнего канала). Компаратор B вычисляется автоматически.
  Перезагрузка компаратора происходит при переходе таймера через 0 и через пик

  Учитываем время закрытия силовых транзисторов
  Для CSD18540Q5B время закрытия = 20 ns, время открытия 6    ns. Diode reverse recovery time = 82 ns
  Для STL320N4LF8 время закрытия = 89 ns, время открытия 12.5 ns. Diode reverse recovery time = 60 ns
  Микросхема драйвера  TMC6200 вносит внутреннюю задержку между закрытием одного плеча и открытием другого в 75 ns
  Рекомендуется иметь задержку на 30% больше чем специфицировано время выключчения для транзистра

  Один такт таймера равен 1/120 = 8.3 ns

  \param R_GPT      - указатель на структура таймера
-----------------------------------------------------------------------------------------------------*/
static void _PWM_triangle_2buffered_init(R_GPTA0_Type *R_GPT)
{
  R_GPT->GTWP_b.PRKEY  = 0xA5;    // Разрешаем запись в бит WP этого регистра
  R_GPT->GTWP_b.WP     = 0;       // 0: Enable writes to the register Разрешаем запись в остальные регистры таймера

  R_GPT->GTCR_b.CST    = 0;       // Останавливаем счет

  R_GPT->GTCNT         = 0;       // Обнуляем таймер
  R_GPT->GTPR          = gpt_pwm_period; // Устанавливаем регистр задающий верхний предел таймера
  R_GPT->GTIOR         = 0;       // Очищаем настройки выходов. Все запрещены

  // Отклчючаем все флаги счета внешних импульсов
  R_GPT->GTUPSR        = 0;       // General PWM Timer Up Count Source Select Register
  R_GPT->GTDNSR        = 0;       // General PWM Timer Down Count Source Select Register

  R_GPT->GTUDDTYC_b.UDF= 1;       // Сразу вступает в силу установка инкремента счетчика
  R_GPT->GTUDDTYC_b.UD = 1;       // Счетчик инкрементируется


  R_GPT->GTCR_b.TPCS   = 0;       // Timer Prescaler Select. 0 0 0: PCLKD/1
  R_GPT->GTCR_b.MD     = 5;       // 101: Triangle-wave PWM mode 2 (32-bit transfer at crest and trough) (single buffer or double buffer possible)


  R_GPT->GTINTAD_b.GRPABH = 1;    // 1: Enable same time output level high disable request
                                  // Если оба входа установятся в 1 то будет выдан сигнал запрещения выходов

  R_GPT->GTSSR_b.CSTRT = 1;       // Разрешаем програмный запуск  от регитсра GTSTR
  R_GPT->GTPSR_b.CSTOP = 1;       // Разрешаем програмный останов от регитсра GTSTP
  R_GPT->GTCSR_b.CCLR  = 1;       // Разрешаем програмный сброс  от регитсра GTCTR
  R_GPT->GTST          = 0;       // Очищаем статус


  R_GPT->GTDTCR_b.TDE  = 1;       // Use GTDVU and GTDVD to set the compare match value for negative-phase waveform with dead time automatically in GTCCRB.
  R_GPT->GTDTCR_b.TDBUE= 0;       // 0: Disable GTDVU buffer operation
  R_GPT->GTDTCR_b.TDBDE= 0;       // 0: Disable GTDVD buffer operation
  R_GPT->GTDTCR_b.TDFER= 1;       // 1: Automatically set the value written to GTDVU to GTDVD
  R_GPT->GTDVU_b.GTDVU = PWM_DEAD_TIME_VAL;

  R_GPT->GTBER_b.BD    = 0;       // Снимаем запрещение буфферизированной записи в регистры GTCCR, GTPR, GTADTR, GTDV

  R_GPT->GTBER_b.CCRA  = 1;       // GTCCRA Buffer Operation. 01:  Single buffer operation (GTCCRA ↔ GTCCRC)
  R_GPT->GTCCRA        = MIN_PWM_COMPARE_VAL; // Загружаем  значение в компаратор A. Устанавливаем минимальную длительность начального импульса
  R_GPT->GTCCRC        = MIN_PWM_COMPARE_VAL; // Загружаем первое буфферизированное  значение в компаратор A.
  R_GPT->GTIOR_b.GTIOA = 0x7;     // Set initial output low, Retain output at cycle end, Toggle output at GTCCRA compare match
  R_GPT->GTIOR_b.GTIOB = 0x1B;    // Set initial output high, Retain output at cycle end, Toggle output at GTCCRB compare match

  R_GPT->GTIOR_b.OADFLT= 0;       // 0: Output low on GTIOCA pin when counting is stopped
  R_GPT->GTIOR_b.OBDFLT= 0;       // 0: Output low on GTIOCB pin when counting is stopped

  R_GPT->GTIOR_b.OADF  = 2;       // 1 0: Set GTIOCA pin to 0 on output disable
  R_GPT->GTIOR_b.OBDF  = 2;       // 1 0: Set GTIOCB pin to 0 on output disable

  R_GPT->GTIOR_b.OAE   = 1;       // GTIOCA Pin Output Enable
  R_GPT->GTIOR_b.OBE   = 1;       // GTIOCB Pin Output Enable

  __DSB();                        // Ожидаем пока все данные будут записаны в периферию
}

Полная инициализация всех 3 каналов выглядит так:

/*-----------------------------------------------------------------------------------------------------
  Инициализация таймеров GPT0, GPT1, GPT2 (согласно обозначению в даташите) или R_GPTA0, R_GPTA1, R_GPTA2 (согласно обозначению в хидере)

  Настройка ШИМ на треугольный режим с перегрузкой на впадине и на пике

  Таймер тактируется частотой PCLKD = 120 МГц

  \param freq       - частота ШИМ

  \return uint32_t
-----------------------------------------------------------------------------------------------------*/
uint32_t PWM_3ph_triangle_2buffered_init(uint32_t pwm_freq)
{
  gpt_pwm_period =(PCLKD_FREQ / pwm_freq)- 1;

  if (gpt_pwm_period < PWM_STEP_COUNT)
  {
    // Не даем ошибочно выбрать слишком высокую частоту ШИМ
    APPLOG("Wrong PWM frequency value %d", pwm_freq);
    return RES_ERROR;
  }

  // Заполняем массив загрузочными данными компаратор для разных значений модуляции
  for (uint32_t i=0; i < PWM_STEP_COUNT; i++)
  {
    int32_t comp_val =(gpt_pwm_period * (PWM_STEP_COUNT - i)) / PWM_STEP_COUNT;

    // Не даем прекратиться PWM и не даем импульсу стать слишком коротким
    if (comp_val < MIN_PWM_COMPARE_VAL)
    {
      comp_val = MIN_PWM_COMPARE_VAL;
    }
    else if ((gpt_pwm_period - comp_val) < MIN_PWM_COMPARE_VAL)
    {
      comp_val = gpt_pwm_period - MIN_PWM_COMPARE_VAL;
    }
    pwm_indx_to_comp[i] = comp_val;

  }

  R_MSTP->MSTPCRD_b.MSTPD5 = 0;    / Разрешаем работу модулей GPT ch7-ch0

  _PWM_triangle_2buffered_init(R_GPTA0);
  _PWM_triangle_2buffered_init(R_GPTA1);
  _PWM_triangle_2buffered_init(R_GPTA2);
  _PWM_set_ADC_trigger();

  return RES_OK;
}

Модуль с тестом разных сценариев переключения состояния ШИМ выглядит так:


// Структура описания варианта тестирования ШИМ
typedef struct
{
    uint32_t start_pwm;  // Начальное сосотояние ШИМ
    uint32_t end_pwm;    // Конечное сосотояние ШИМ

} T_pwm_test_mode;


#define M___0    0
#define M___1    PWM_STEP_COUNT
#define M_PWM   (PWM_STEP_COUNT/10)

// .............................................................
// Массив вариантов тестирования ШИМ
// .............................................................
T_pwm_test_mode pwm_test_steps[] =
{
  {M___0            , M___1   }, // N0:  0 %   -> 100 %
  {M___1            , M___0   }, // N1:  100 % -> 0 %

  {M___0            , M_PWM   }, // N2:  0 %   -> PWM
  {M_PWM            , M___0   }, // N3:  PWM   -> 0 %

  {M___1            , M_PWM   }, // N4:  100%  -> PWM
  {M_PWM            , M___1   }, // N5:  PWM   -> 100 %
};

uint32_t test_step = 0; // Переменная задающая вариант тестирования.
                        // Менять значение переменной можно из окна Live Watch отладчика в реальном времени

uint8_t  test_flag;     // Переменная вызывающая генерацию импульса на светодиоде


/*-----------------------------------------------------------------------------------------------------
  Функция вызываемая в обработчике прерываний по окончании выборок АЦП
  Вызывается синхронно с ШИМ с частотой ШИМ


  \param void
-----------------------------------------------------------------------------------------------------*/
static void Test_BLDC_ISR_handler(void)
{
  // Функции измерения
  Hall_3PH_capture_bitmask();
  Measure_instant_phases_current();
  Measure_overall_BLDC_motor_current();
  Measure_DC_bus_voltage_current();
  Measure_servo_sensor_speed();
  Hall_3PH_measure_speed_and_direction();

  if (test_flag)
  {
    test_flag = 0;
    RS485_LED = 1;              // Генерация синхроимпульса на светодиоде платы сервоконтроллера
  }
  Set_gpt0_pwm_level();         // Обновление регистра копаратора фазы U
  Set_gpt1_pwm_level();         // Обновление регистра копаратора фазы V
  Set_gpt2_pwm_level();         // Обновление регистра копаратора фазы W

  RS485_LED = 0;

  App_set_flags(TEST_FLAG);     // Выставляется флаг задаче тестирования для уведомления
                                // об окончании очередного цикла обновления регистров комапараторов
}


/*-----------------------------------------------------------------------------------------------------
  Выполнение теста переключения из одного состояния ШИМ в другое

  \param void
-----------------------------------------------------------------------------------------------------*/
void Exec_test_step(void)
{
  uint32_t actual_events;
  uint32_t start_pwm ;
  uint32_t end_pwm   ;

  start_pwm = pwm_test_steps[test_step].start_pwm;
  end_pwm   = pwm_test_steps[test_step].end_pwm;

  Post_phase_pwm_level(PHASE_U, start_pwm);      // Устанавливаем начальное состояние ШИМ для заданного варианта теста

  for (uint32_t i=0;i<5;i++)
  {
    App_wait_flags(TEST_FLAG,&actual_events, 10); // Пропускаем нечетное количество циклов
  }

  Post_phase_pwm_level(PHASE_U, end_pwm);         // Устанавливаем конечное состояние ШИМ для заданного варианта теста

  test_flag = 1;                                  // Сигнализируем о необходимости в ближайшем цикле сформировать синхроимпульс на  светодиоде
                                                  // Синхроимпульс нужен для осциллографа для привязки к моменту смены состояний ШИМ

  for (uint32_t i=0;i<5;i++)
  {
    App_wait_flags(TEST_FLAG,&actual_events, 10); // Пропускаем нечетное количество циклов
  }
}

/*-----------------------------------------------------------------------------------------------------
  Задача тестирования переключения состояния ШИМ

  \param void
-----------------------------------------------------------------------------------------------------*/
static void _Test_PWM(void)
{
  uint32_t actual_events;
  uint32_t res;

  Hall_3PH_reinit();
  Rot_Encoders_capturing_init(RPM_MEASURE_OVERFL_TIME);  // Инициализируем измерение скрости вращения двигателя по сигналам с датчиков Холла

  if (PWM_3ph_triangle_2buffered_init(wvar.pwm_frequency) != RES_OK) return;
  PWM_3ph_start();

  ADC_init(Test_BLDC_ISR_handler);                       // Инициализируем ADC работающий синхронно с PWM мотора

  MC50_3Ph_PWM_pins_init();

  do
  {
    res = App_wait_flags(TEST_FLAG,&actual_events, 10);
    if (res == TX_SUCCESS)
    {
      Exec_test_step();
    }

  } while (1);
}

И наконец сама функция переключения состояния ШИМ для одного канала выглядит так:


#define COMP_STATE_0    gpt_pwm_period
#define COMP_STATE_1    0

#define COUNT_DIR_DOWN  0

#define LEVEL_0         0
#define LEVEL_1         PWM_STEP_COUNT

/*-----------------------------------------------------------------------------------------------------
  Функция вызываемая из обработчика прерывания синхронно с треугольным ШИМ на подъеме или спаде

  \param R_GPT
  \param pwm_lev
-----------------------------------------------------------------------------------------------------*/
static inline void _Set_gpt_pwm_level(R_GPTA0_Type *R_GPT, uint32_t pwm_lev)
{
  uint32_t comp_now  = R_GPT->GTCCRA;
  uint32_t count_dir =(R_GPT->GTST) & BIT(15); // Если счет идет вниз, то значение будет равно 0

  if (pwm_lev == LEVEL_0)
  {
    if (comp_now != COMP_STATE_1)
    {
      //============================
      // pwm -> 0
      //============================
      if (count_dir == COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = COMP_STATE_0;
      }
    }
    else
    {
      //============================
      // 1 -> 0
      //============================
      if (count_dir != COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = gpt_pwm_period >> 1;
      }
    }
  }
  else if (pwm_lev == LEVEL_1)
  {
    if (comp_now != COMP_STATE_0)
    {
      //============================
      // pwm -> 1
      //============================
      if (count_dir == COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = COMP_STATE_1;
      }
    }
    else
    {
      //============================
      // 0 -> 1
      //============================
      if (count_dir != COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = gpt_pwm_period >> 1;
      }
    }
  }
  else
  {
    uint32_t comp_val = pwm_indx_to_comp[pwm_lev];
    if (comp_now == COMP_STATE_0)
    {
      //============================
      // 0 -> pwm
      //============================
      if (count_dir == COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = comp_val;
      }
    }
    else if  (comp_now == COMP_STATE_1)
    {
      //============================
      // 1 -> pwm
      //============================
      if (count_dir != COUNT_DIR_DOWN)
      {
        R_GPT->GTCCRC = comp_val;
      }
    }
    else
    {
      //============================
      // pwm -> pwm
      //============================
      R_GPT->GTCCRC = comp_val;
    }
  }
}

/*-----------------------------------------------------------------------------------------------------


  \param pwm_lev
-----------------------------------------------------------------------------------------------------*/
void Set_gpt0_pwm_level(void)
{
  _Set_gpt_pwm_level(R_GPTA0, gpt0_pwm_lev);
}

/*-----------------------------------------------------------------------------------------------------


  \param pwm_lev
-----------------------------------------------------------------------------------------------------*/
void Set_gpt1_pwm_level(void)
{
  _Set_gpt_pwm_level(R_GPTA1, gpt1_pwm_lev);
}

/*-----------------------------------------------------------------------------------------------------


  \param pwm_lev
-----------------------------------------------------------------------------------------------------*/
void Set_gpt2_pwm_level(void)
{
  _Set_gpt_pwm_level(R_GPTA2, gpt2_pwm_lev);
}

Исследования данного метода предполагает наличие 2-канального осциллографа от 200 Мгц с входом внешней синхронизации или 4-х канального осциллографа. Сигнал синхронизации отмечающий место смены состояний ШИМ выводится на светодиод LED2. Для простоты кода переменная с номером теста меняется прямо из окна Live Watch отладчика IAR Workbench. Тест не планируется вводить в основную ветку проекта MC50.

Резюме

Новый способ оказался более коротким чем предыдущий с перепрограммирование портов. Он требует всего одну запись в периферийный регистр. Однако за это пришлось пожертвовать временем реакции на команду изменения состояния ШИМ. Состояние изменится через один или два цикла ШИМ после регистрации запроса из основной задачи.