
Этой проблеме я уже посвятил две статьи. Ну, как проблеме — проблеме для меня. Никак не удавалось охватить её целиком, когнитивно и ментально промоделировать. Появление Copilot кардинально всё изменило — ментальные границы раздвинулись, и здесь я выкладываю окончательное решение для семейств микроконтроллеров Synergy и RA8 от Renesas.
Вкратце напомню, что вопрос мёртвого времени — один из важнейших для контроллеров всех типов электродвигателей и полупроводниковых преобразователей энергии.
С одной стороны, мёртвое время не может быть нулевым, поскольку полупроводники имеют собственное запаздывание реакции. С другой стороны, если оно слишком велико, падает КПД и ухудшаются динамические характеристики.
Почему пришлось вернуться к этой теме, несмотря на то, что в статье «Мёртвое время: как не ошибиться при настройке ШИМ в Renesas Synergy» вопрос уже был раскрыт?
Дело в том, что на RA8 этот приём перестал работать: попытка записать в компараторы предельные значения счётчиков не приводит к выключению ШИМ. Это оказалось неприятным сюрпризом и разрушило иллюзию о том, что внутри экосистемы одного производителя можно безболезненно переносить драйверы, основанные на прямом доступе к регистрам.
Наш проект универсального контроллера моторов на ARM Cortex-M85 получил собственный фреймворк, и настало время переходить к целевым приложениям. Но каким бы ни было это приложение, скоростью моторов нужно управлять, а значит, нужен генератор качественного ШИМ-сигнала переходящего в статический сигнал.
Микроконтроллер R7FA8M1AHECFB, установленный на нашей плате, работает на частоте 480 МГц и управляет четырьмя двигателями постоянного тока через два трёхфазных драйвера.
Программируемые трёхфазные драйверы TMC6200-TA сравнительно недороги и, главное, не требуют непрерывного ШИМ для поддержки бутстрэп-схемы, питающей верхние ключи.
Кто-то может возразить, что индивидуально управлять четырьмя моторами, имея всего шесть фаз, невозможно — и будет прав. Однако наше применение ограничивается режимами, в которых одновременно работают не более двух моторов и только в одном направлении.
Говорят, что пока копилоты остаются достаточно рискованным инструментом: по-настоящему хорошо они работают лишь в режиме автодополнения.
Надо признать, что при работе с «голыми» регистрами они действительно серьёзно лагают и могут навредить. Поручать им самостоятельный поиск документации на чип в интернете — не лучшая идея.
К сожалению, заголовочные файлы из фирменного SDK тоже мало пригодны: они перегружены огромным объёмом контекста и практически лишены комментариев, поэтому копилот «не понимает» их. Я использую такой подход: с помощью GPT индивидуально конвертирую описания регистров из PDF-даташита в .c- и .h-файлы. После этого можно смело подключать Copilot — он уже работает с понятным, структурированным исходником.
Итак, в чём же когнитивная проблема «просто взять и включить мёртвое время»?
Как уже говорилось в предыдущей статье, в семействе микроконтроллеров Synergy и RA8 от Renesas такого отдельного бита нет. И связано это с тем, что там реализовано нечто гораздо более ценное.

И тут пошёл слух, что Copilot умеет строить интерактивные графики.
Раньше, чтобы увидеть диаграммы сигналов при разных сценариях, приходилось либо долго и тщательно рисовать их вручную, либо создавать сложные модели в Simulink — и на каждом из этих этапов либо исправлять бесконечные ошибки, либо, не заметив их, тратить время на ошибочные концепции.
Первое, что мне посоветовал копилот, — это снова обратиться к регистру General PWM Timer Count Direction and Duty Setting Register (GPTCDDSR). Но сделать это глубже.
В статье «Мёртвое время. Как не ошибиться при настройке ШИМ в Renesas Synergy» я отверг работу с этим регистром. Но копилот упорно возвращал меня к нему, и мы пришли к консенсусу, что его можно использовать в двухтактном методе переключения состоя��ия ШИМ. Но надо было это проверить и опять строить графики либо модели.
Вторым шагом было попросить его сделать все нужные графики, и произошло чудо — он сразу нарисовал интерактивные графики на Python. Конечно, последовало несколько косметических улучшений вроде коррекции надписей и толщины линий, но в течение десяти минут всё было закончено.
И вот этот скрипт:
Скрипт интерактивных графиков ШИМ
import matplotlib.pyplot as plt import numpy as np from matplotlib.widgets import Slider, Button, CheckButtons from scipy.signal import argrelextrema # === НАСТРОЙКИ (константы) === TIME_WINDOW = 3.0 # длительность шкалы времени (сек) NUM_POINTS = 6000 # количество точек для дискретизации графика # === СОЗДАНИЕ ФИГУРЫ И ОСЕЙ === fig = plt.figure(figsize=(16, 10)) # Попытка развернуть окно на весь экран (работает не со всеми бэкендами) try: fig.canvas.manager.window.wm_state('zoomed') except AttributeError: # Если wm_state недоступен, попробуем другие способы try: fig.canvas.manager.window.showMaximized() except AttributeError: # Если и showMaximized недоступен, просто продолжаем без максимизации pass # Основные графики (занимают большую часть экрана) gs = fig.add_gridspec(5, 1, height_ratios=[1, 1, 1, 1, 1], left=0.1, right=0.95, top=0.92, bottom=0.25, hspace=0.3) axs = [] for i in range(5): ax = fig.add_subplot(gs[i, 0]) axs.append(ax) # Область для слайдеров (внизу) slider_area = fig.add_axes([0.1, 0.02, 0.85, 0.2]) slider_area.set_visible(False) # === СОЗДАНИЕ СЛАЙДЕРОВ === # Позиции слайдеров slider_height = 0.025 slider_spacing = 0.035 # Слайдер для периода PWM ax_pwm_period = fig.add_axes([0.15, 0.18, 0.65, slider_height]) slider_pwm_period = Slider(ax_pwm_period, 'PWM период (с)', 0.5, 2.0, valinit=1.0, valfmt='%.2f') # Слайдер для коэффициента заполнения ax_duty_cycle = fig.add_axes([0.15, 0.18 - slider_spacing, 0.65, slider_height]) slider_duty_cycle = Slider(ax_duty_cycle, 'Коэфф. заполнения', 0.01, 0.99, valinit=0.5, valfmt='%.2f') # Слайдер для мертвого времени ax_dead_time = fig.add_axes([0.15, 0.18 - 2*slider_spacing, 0.65, slider_height]) slider_dead_time = Slider(ax_dead_time, 'Мертвое время (с)', 0.01, 0.3, valinit=0.1, valfmt='%.3f') # Слайдер для времени точки останова 4-го графика ax_stop_time = fig.add_axes([0.15, 0.18 - 3*slider_spacing, 0.65, slider_height]) slider_stop_time = Slider(ax_stop_time, 'Точка останова HSw (с)', 0.5, 2.5, valinit=1.0, valfmt='%.3f') # Слайдер для шага сетки ax_grid_step = fig.add_axes([0.15, 0.18 - 4*slider_spacing, 0.65, slider_height]) slider_grid_step = Slider(ax_grid_step, 'Шаг сетки (с)', 0.05, 0.5, valinit=0.1, valfmt='%.2f') # Кнопка сброса ax_reset = fig.add_axes([0.85, 0.18 - 2*slider_spacing, 0.08, 0.06]) button_reset = Button(ax_reset, 'Сброс') # Чекбокс для инвертирования уровней точки останова ax_checkbox = fig.add_axes([0.85, 0.18 - 3*slider_spacing, 0.08, 0.04]) checkbox_invert = CheckButtons(ax_checkbox, ['Инвертировать'], [False]) # === ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ === t = np.linspace(0, TIME_WINDOW, NUM_POINTS) dt_step = t[1] - t[0] # Линии для анимации фронтов и точки останова stop_lines = [] edge_lines = [] # === ФУНКЦИЯ ДЛЯ ДОБАВЛЕНИЯ ЛИНИЙ ФРОНТОВ === def add_edge_lines(ax, rising_edges, falling_edges): # Добавляем новые линии фронтов (старые будут очищены при ax.clear()) for edge in rising_edges: if edge < len(t): ax.axvline(x=t[edge], color='blue', linestyle=':', alpha=0.7, linewidth=1.5) for edge in falling_edges: if edge < len(t): ax.axvline(x=t[edge], color='red', linestyle=':', alpha=0.7, linewidth=1.5) # === ФУНКЦИЯ ОБНОВЛЕНИЯ ГРАФИКОВ === def update_plots(): # Получаем текущие значения из слайдеров pwm_period = slider_pwm_period.val duty_cycle = slider_duty_cycle.val dead_time = slider_dead_time.val stop_time_4 = slider_stop_time.val grid_step = slider_grid_step.val # Получаем состояние чекбокса invert_levels = checkbox_invert.get_status()[0] # Расчет времени точки останова 5-го графика с учетом инвертирования if invert_levels: stop_time_5 = stop_time_4 - pwm_period / 2 # Опережение на полпериода else: stop_time_5 = stop_time_4 + pwm_period / 2 # Отставание на полпериода # === ПЕРЕСЧЕТ СИГНАЛОВ === # Треугольный счетчик f_tri = 1 / pwm_period tri_wave_norm = 1 - 2 * np.abs((t * f_tri) % 1 - 0.5) tri_wave = tri_wave_norm # От 0 до 1 # Уровень компаратора compare_level = np.full_like(t, duty_cycle) # Верхний ключ high_side = tri_wave < compare_level # Найти фронты верхнего ключа high_side_edges = np.diff(high_side.astype(int)) high_side_rising_edges = np.where(high_side_edges == 1)[0] high_side_falling_edges = np.where(high_side_edges == -1)[0] # Нижний ключ low_side = (~high_side).astype(int) # Найти фронты нижнего ключа edges = np.diff(low_side) rising_edges = np.where(edges == 1)[0] falling_edges = np.where(edges == -1)[0] # Мертвое время для нижнего ключа low_side_deadtime = np.copy(low_side) dead_samples = int(dead_time / dt_step) for rise, fall in zip(rising_edges, falling_edges): if fall - rise < dead_samples: low_side_deadtime[rise+1:fall+1] = 0 continue low_side_deadtime[rise+1:rise+1+dead_samples//2] = 0 low_side_deadtime[fall+1-dead_samples//2:fall+1] = 0 # 4-й график: копирует верхний ключ, замирает в заданном уровне custom_signal_4 = np.copy(high_side.astype(int)) stop_idx_4 = int(stop_time_4 / dt_step) if stop_idx_4 < len(custom_signal_4): if invert_levels: custom_signal_4[stop_idx_4:] = 1 # Замирает в 1 else: custom_signal_4[stop_idx_4:] = 0 # Замирает в 0 # 5-й график: копирует нижний ключ с мертвым временем, замирает в заданном уровне custom_signal_5 = np.copy(low_side_deadtime) stop_idx_5 = int(stop_time_5 / dt_step) if stop_idx_5 < len(custom_signal_5) and stop_idx_5 >= 0: if invert_levels: custom_signal_5[stop_idx_5:] = 0 # Замирает в 0 else: custom_signal_5[stop_idx_5:] = 1 # Замирает в 1 # === ОБНОВЛЕНИЕ ГРАФИКОВ === # Очищаем все графики for ax in axs: ax.clear() # График 0: Треугольник и компаратор axs[0].plot(t, tri_wave, label="Треугольник (счётчик)", linewidth=2) axs[0].plot(t, compare_level, 'r--', label="Уровень компаратора", linewidth=2) # Найти точки пересечения треугольной волны с уровнем компаратора # Ищем переходы high_side (пересечения треугольника с компаратором) intersection_indices = [] intersection_indices.extend(high_side_rising_edges) intersection_indices.extend(high_side_falling_edges) # Отмечаем точки пересечения жирными красными точками if intersection_indices: intersection_times = t[intersection_indices] intersection_levels = np.full(len(intersection_indices), duty_cycle) axs[0].scatter(intersection_times, intersection_levels, color='red', s=50, zorder=5, label='Точка компаратора') # Зеленая линия всегда показывает точку останова 4-го графика axs[0].axvline(x=stop_time_4, color='green', linestyle='--', alpha=0.7, linewidth=2, label=f'Останов HSw ({stop_time_4:.3f}с)') # Желтая линия всегда показывает точку останова 5-го графика axs[0].axvline(x=stop_time_5, color='orange', linestyle='--', alpha=0.7, linewidth=2, label=f'Останов LSw ({stop_time_5:.3f}с)') # Находим и отмечаем точки пересечения с зеленой линией (stop_time_4) if 0 <= stop_time_4 <= TIME_WINDOW: # Интерполируем значение треугольной волны в момент stop_time_4 stop_idx_4_interp = stop_time_4 / dt_step if stop_idx_4_interp < len(tri_wave) - 1: idx_low = int(stop_idx_4_interp) idx_high = idx_low + 1 frac = stop_idx_4_interp - idx_low tri_value_4 = tri_wave[idx_low] * (1 - frac) + tri_wave[idx_high] * frac axs[0].scatter([stop_time_4], [tri_value_4], color='green', s=50, zorder=6, label='Точка останова HSw') # Находим и отмечаем точки пересечения с желтой линией (stop_time_5) if 0 <= stop_time_5 <= TIME_WINDOW: # Интерполируем значение треугольной волны в момент stop_time_5 stop_idx_5_interp = stop_time_5 / dt_step if stop_idx_5_interp < len(tri_wave) - 1 and stop_idx_5_interp >= 0: idx_low = int(stop_idx_5_interp) idx_high = idx_low + 1 frac = stop_idx_5_interp - idx_low tri_value_5 = tri_wave[idx_low] * (1 - frac) + tri_wave[idx_high] * frac axs[0].scatter([stop_time_5], [tri_value_5], color='orange', s=50, zorder=6, label='Точка останова LSw') add_edge_lines(axs[0], high_side_rising_edges, high_side_falling_edges) axs[0].set_ylabel("Уровень") axs[0].legend(loc='upper right') axs[0].grid(True, alpha=0.3) axs[0].set_xticks(np.arange(0, TIME_WINDOW + grid_step, grid_step)) axs[0].set_ylim(-0.1, 1.1) # График 1: PWM High side axs[1].step(t, high_side.astype(int), where='post', linewidth=2, color='red') axs[1].axvline(x=stop_time_4, color='green', linestyle='--', alpha=0.7, linewidth=2) axs[1].axvline(x=stop_time_5, color='orange', linestyle='--', alpha=0.7, linewidth=2) add_edge_lines(axs[1], high_side_rising_edges, high_side_falling_edges) axs[1].set_ylabel("PWM High side") axs[1].set_ylim(-0.2, 1.2) axs[1].grid(True, alpha=0.3) axs[1].set_xticks(np.arange(0, TIME_WINDOW + grid_step, grid_step)) # График 2: PWM Low side axs[2].step(t, low_side_deadtime.astype(int), where='post', linewidth=2) axs[2].axvline(x=stop_time_4, color='green', linestyle='--', alpha=0.7, linewidth=2) axs[2].axvline(x=stop_time_5, color='orange', linestyle='--', alpha=0.7, linewidth=2) add_edge_lines(axs[2], high_side_rising_edges, high_side_falling_edges) axs[2].set_ylabel("PWM Low side") axs[2].set_ylim(-0.2, 1.2) axs[2].grid(True, alpha=0.3) axs[2].set_xticks(np.arange(0, TIME_WINDOW + grid_step, grid_step)) # График 3: High switch output axs[3].step(t, custom_signal_4.astype(int), where='post', linewidth=2, color='red') axs[3].axvline(x=stop_time_4, color='green', linestyle='--', alpha=0.7, linewidth=2) axs[3].axvline(x=stop_time_5, color='orange', linestyle='--', alpha=0.7, linewidth=2) add_edge_lines(axs[3], high_side_rising_edges, high_side_falling_edges) freeze_level_4 = "1" if invert_levels else "0" axs[3].set_ylabel("HSw output") axs[3].set_ylim(-0.2, 1.2) axs[3].grid(True, alpha=0.3) axs[3].set_xticks(np.arange(0, TIME_WINDOW + grid_step, grid_step)) # График 4: Low switch output axs[4].step(t, custom_signal_5.astype(int), where='post', linewidth=2) axs[4].axvline(x=stop_time_4, color='green', linestyle='--', alpha=0.7, linewidth=2) axs[4].axvline(x=stop_time_5, color='orange', linestyle='--', alpha=0.7, linewidth=2) add_edge_lines(axs[4], high_side_rising_edges, high_side_falling_edges) # Добавляем черную толстую линию для мертвого времени (когда оба сигнала = 0) deadtime_signal = (custom_signal_4 == 0) & (custom_signal_5 == 0) # Создаем линию на уровне 0.5 только там, где есть мертвое время deadtime_line = np.where(deadtime_signal, 0.5, np.nan) # NaN делает линию невидимой axs[4].plot(t, deadtime_line, linewidth=4, color='black', label='Мертвое время') freeze_level_5 = "0" if invert_levels else "1" timing_text = "опережение" if invert_levels else "отставание" axs[4].set_ylabel("LSw output") axs[4].set_ylim(-0.2, 1.2) axs[4].set_xlabel("Время (с)") axs[4].grid(True, alpha=0.3) axs[4].set_xticks(np.arange(0, TIME_WINDOW + grid_step, grid_step)) axs[4].legend(loc='upper right')# Обновляем заголовок invert_status = "Инвертированный" if invert_levels else "Обычный" title = (f'PWM: {pwm_period:.2f}с, Заполнение: {duty_cycle:.2f}, ' f'Мертвое время: {dead_time:.3f}с, Точка останова: {stop_time_4:.3f}с ({invert_status})') fig.suptitle(title, fontsize=12, fontweight='bold') # Перерисовываем fig.canvas.draw() # === ОБРАБОТЧИКИ СОБЫТИЙ === def on_slider_change(val): update_plots() def reset_sliders(event): slider_pwm_period.reset() slider_duty_cycle.reset() slider_dead_time.reset() slider_stop_time.reset() slider_grid_step.reset() # Сбрасываем чекбокс в исходное состояние checkbox_invert.set_active(0) update_plots() def on_checkbox_change(label): update_plots() # Подключаем обработчики к слайдерам slider_pwm_period.on_changed(on_slider_change) slider_duty_cycle.on_changed(on_slider_change) slider_dead_time.on_changed(on_slider_change) slider_stop_time.on_changed(on_slider_change) slider_grid_step.on_changed(on_slider_change) button_reset.on_clicked(reset_sliders) checkbox_invert.on_clicked(on_checkbox_change) # === ИНИЦИАЛИЗАЦИЯ === # Первоначальная отрисовка update_plots() plt.show()
И вот как это выглядит:

Ползунками можно регулировать все ключевые параметры моделирования двухтактного алгоритма переключения статических состояний ШИМ.
Основная задача заключалась в том, чтобы понять, в какой момент обращаться к регистру GPTCDDSR и тем самым минимизировать длительность мёртвого времени, сохраняя его минимально допустимым. Довольно быстро стало ясно, что переключение можно начинать в обеих точках излома треугольника, изображающего значение счётчика ШИМ. Именно в этих точках и возникает прерывание АЦП, поэтому дополнительных источников прерываний организовывать не требуется. На этой позитивной новости, я поручил копилоту сгенерировать функцию переключения ШИМ по описанному алгоритму — и менее чем через минуту она была готова.
Вот она:
Функция Set_pwm_enhanced
/*----------------------------------------------------------------------------------------------------- Description: Enhanced PWM switching with two-stage mode transitions for safe motor control. Implements safe switching between PWM modes (0%, 100%, PWM) using two-stage logic: - Stage 1: Force both outputs to LOW (safe intermediate state) - Stage 2: Set target output configuration (0%, 100%, or enable PWM mode) Current state is determined by reading OADTY and OBDTY bits from GTUDDTYC register. This prevents shoot-through current by ensuring safe transitions through LOW-LOW state. Parameters: motor - motor identifier (0-(DRIVER_COUNT-1)) phase - phase identifier (0-(PHASE_COUNT-1)) pwm_level - PWM duty cycle in steps (0-PWM_STEP_COUNT) output_enable - output enable flag (0=switches OFF, 1=switches enabled) Return: none -----------------------------------------------------------------------------------------------------*/ FORCE_INLINE_PRAGMA FORCE_INLINE_ATTR void _Set_pwm_enhanced(uint8_t motor, uint8_t phase, uint32_t pwm_level, uint8_t output_enable) { R_GPT0_Type *gpt_reg = g_gpt_registers[motor][phase]; // If output is being disabled, force both switches OFF (high-Z state for coast mode) if (output_enable == PHASE_OUTPUT_DISABLE) { T_gtuddtyc_bits gtuddtyc_temp = *(T_gtuddtyc_bits *)&gpt_reg->GTUDDTYC; // Both switches OFF (high-Z state) gtuddtyc_temp.OADTY = 0x2; // GTIOA = LOW (upper switch OFF) gtuddtyc_temp.OADTYF = 1; // Enable force mode for GTIOA gtuddtyc_temp.OBDTY = 0x2; // GTIOB = LOW (lower switch OFF) gtuddtyc_temp.OBDTYF = 1; // Enable force mode for GTIOB gpt_reg->GTUDDTYC = *(uint32_t *)>uddtyc_temp; // Update global state arrays for diagnostic display (Z-state: both switches OFF) g_phase_state_0_percent[motor][phase] = 0; // Not in 0% duty state g_phase_state_100_percent[motor][phase] = 0; // Not in 100% duty state g_phase_state_pwm_mode[motor][phase] = 0; // Not in PWM mode g_phase_state_Z_stage[motor][phase] = 1; // In Z-state (high impedance) return; } T_gtuddtyc_bits gtuddtyc_temp = *(T_gtuddtyc_bits *)&gpt_reg->GTUDDTYC; // Read current state from OADTY and OBDTY bits to determine transition stage uint8_t current_state_0_percent = (gtuddtyc_temp.OADTY == 0x2) && (gtuddtyc_temp.OBDTY == 0x3); // 0% duty state: GTIOA=LOW, GTIOB=HIGH uint8_t current_state_100_percent = (gtuddtyc_temp.OADTY == 0x3) && (gtuddtyc_temp.OBDTY == 0x2); // 100% duty state: GTIOA=HIGH, GTIOB=LOW uint8_t current_state_pwm_mode = (gtuddtyc_temp.OADTYF == 0) && (gtuddtyc_temp.OBDTYF == 0); // PWM mode: both force modes disabled uint8_t current_state_Z_stage = (gtuddtyc_temp.OADTY == 0x2) && (gtuddtyc_temp.OBDTY == 0x2); // Z-state: both signals LOW (high impedance) // Store current states in global arrays for diagnostic access g_phase_state_0_percent[motor][phase] = current_state_0_percent; g_phase_state_100_percent[motor][phase] = current_state_100_percent; g_phase_state_pwm_mode[motor][phase] = current_state_pwm_mode; g_phase_state_Z_stage[motor][phase] = current_state_Z_stage; if (pwm_level == 0) { // Target: 0% duty (GTIOA=LOW, GTIOB=HIGH) if (current_state_0_percent) { return; // Already in target state } if (current_state_100_percent || current_state_pwm_mode) { // Stage 1: Set both signals to LOW (first stage) gtuddtyc_temp.OADTY = 0x2; // GTIOA = LOW gtuddtyc_temp.OADTYF = 1; // Enable force mode for GTIOA gtuddtyc_temp.OBDTY = 0x2; // GTIOB = LOW gtuddtyc_temp.OBDTYF = 1; // Enable force mode for GTIOB } else if (current_state_Z_stage) { // Stage 2: Set GTIOB to HIGH to complete 0% duty transition gtuddtyc_temp.OBDTY = 0x3; // GTIOB = HIGH (lower switch ON) // GTIOA remains LOW (upper switch OFF) } } else if (pwm_level == PWM_STEP_COUNT) { // Target: 100% duty (GTIOA=HIGH, GTIOB=LOW) if (current_state_100_percent) { return; // Already in target state } if (current_state_0_percent || current_state_pwm_mode) { // Stage 1: Set both signals to LOW (first stage) gtuddtyc_temp.OADTY = 0x2; // GTIOA = LOW gtuddtyc_temp.OADTYF = 1; // Enable force mode for GTIOA gtuddtyc_temp.OBDTY = 0x2; // GTIOB = LOW gtuddtyc_temp.OBDTYF = 1; // Enable force mode for GTIOB } else if (current_state_Z_stage) { // Stage 2: Set GTIOA to HIGH to complete 100% duty transition gtuddtyc_temp.OADTY = 0x3; // GTIOA = HIGH (upper switch ON) // GTIOB remains LOW (lower switch OFF) } } else { // Target: Normal PWM operation (1 to PWM_STEP_COUNT-1) if (current_state_pwm_mode) { // Already in PWM mode, just update comparator values uint32_t comp_value = pwm_indx_to_comp[pwm_level]; gpt_reg->GTCCR[0] = comp_value; // GTCCRA - primary comparator register gpt_reg->GTCCR[2] = comp_value; // GTCCRC - secondary comparator register #if PWM_DEBUG_VALUES g_debug_gtccr0_values[motor][phase] = comp_value; // Store debug value g_debug_gtccr2_values[motor][phase] = comp_value; // Store debug value #endif return; } if (current_state_0_percent || current_state_100_percent) { // Stage 1: Set both signals to LOW (first stage) gtuddtyc_temp.OADTY = 0x2; // GTIOA = LOW gtuddtyc_temp.OADTYF = 1; // Enable force mode for GTIOA gtuddtyc_temp.OBDTY = 0x2; // GTIOB = LOW gtuddtyc_temp.OBDTYF = 1; // Enable force mode for GTIOB } else if (current_state_Z_stage) { // Stage 2: Enable PWM mode // Set PWM comparator value uint32_t comp_value = pwm_indx_to_comp[pwm_level]; gpt_reg->GTCCR[0] = comp_value; // GTCCRA - primary comparator register gpt_reg->GTCCR[2] = comp_value; // GTCCRC - secondary comparator register #if PWM_DEBUG_VALUES g_debug_gtccr0_values[motor][phase] = comp_value; // Store debug value g_debug_gtccr2_values[motor][phase] = comp_value; // Store debug value #endif // Clear force duty values and enable PWM mode gtuddtyc_temp.OADTY = 0; // Clear GTIOA force duty value gtuddtyc_temp.OBDTY = 0; // Clear GTIOB force duty value gtuddtyc_temp.OADTYF = 0; // Disable force mode for GTIOA (enable PWM) gtuddtyc_temp.OBDTYF = 0; // Disable force mode for GTIOB (enable PWM) } } // Apply the configuration gpt_reg->GTUDDTYC = *(uint32_t *)>uddtyc_temp; }
Тут не стану обманывать: это не первоначальный вариант, выданный копилотом. Это версия после множества коррекций и оптимизаций, связанных с правильной интеграцией во фреймворк, принятым мной форматированием и повышением скорости выполнения.
Но факт, что функция изначально работала правильно, и теперь можно совершенно безопасно переключаться из ШИМ в 100 % или в 0 % и даже не переживать, какую задержку будет иметь процедура обслуживания прерывания, которая перенастраивает регистры, — но, конечно, в пределах разумного.
Что имеем в результате.
Удалось получить единый обработчик прерывания для управления всеми четырьмя моторами с периодом 62.5 микросекунды и длительностью выполнения 4.5 микросекунды. В этом обработчике двухтактным методом перестраиваются 6 фаз ШИМ, производится считывание 12 аналоговых каналов АЦП, управление аналоговыми мультиплексорами, а также выполняются процедуры EMA (Exponential Moving Average, экспоненциально взвешенное скользящее среднее) фильтрации аналоговых сигналов .
