
Этой проблеме я уже посвятил две статьи. Ну, как проблеме — проблеме для меня. Никак не удавалось охватить её целиком, когнитивно и ментально промоделировать. Появление 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, экспоненциально взвешенное скользящее среднее) фильтрации аналоговых сигналов .