STM32 ШИМ на стандартных контактах GPIO
Я использую STM32F407ZET6 с поддержкой ядра Arduino в PlatformIO. Процессор имеет внешний кварцевый резонатор на 8 МГц, работающий на частоте 168 МГц. У меня есть проект, в котором я уже использую UART1, I2C1, SPI2 и SDIO (частота 1 Гц, используя этот пример в качестве основы для кода), а также 14 аналоговых входов. У меня есть 14 выходных контактов, которые я хочу использовать для ШИМ с частотой 200 Гц для управления несколькими высокоскоростными синхронизаторами (HSD).
Поскольку у меня нет доступных выводов с поддержкой ШИМ, я попытался добиться этого, используя аппаратный таймер с интервалом 50 мкс, чтобы обеспечить разрешение ШИМ 100. В принципе, это работает, но, как мне кажется, частые прерывания создают проблемы для других периферийных устройств, особенно для SDIO, который, похоже, прекращает запись на SD. Я перепробовал несколько разных таймеров, пытаясь решить проблемы с SDIO. Без включённого таймера запись данных с SD на частоте 1 Гц, похоже, работает стабильно.
Другой подход, который, на мой взгляд, может сработать, — это использование аппаратного таймера с DMA для записи в регистры портов в надежде, что это не нарушит работу других периферийных устройств. Возможно ли это, и может ли кто-нибудь дать мне советы? Как этого добиться? Я потратил несколько часов, пытаясь реализовать обновление GPIO для портов F и G, но безуспешно.
Обновление: Следующий код, безусловно, нуждается в доработке, но он работает. Любые предложения по улучшению приветствуются.
В чем я не уверен, так это в использовании аналоговых контактов на тех же портах и в том, представляет ли это проблему.
Кроме того, вызовет ли использование digitalWrite() на каком-либо из неиспользуемых выводов в коде ниже проблемы? Стоит ли обновлять какие-либо цифровые выводы, независимо от того, есть ли ШИМ-сигнал или нет, используя код ниже?
#include "Arduino.h"
#include "stm32f4xx_hal.h"
// Контакты для обновления
const uint16_t GPIOG_PINS[] = {GPIO_PIN_10, GPIO_PIN_9, GPIO_PIN_6, GPIO_PIN_5, GPIO_PIN_4, GPIO_PIN_3, GPIO_PIN_2};
const uint8_t NUM_PINS_G = sizeof(GPIOG_PINS) / sizeof(GPIOG_PINS[0]);
const uint16_t GPIOF_PINS[] = {GPIO_PIN_15, GPIO_PIN_14, GPIO_PIN_13, GPIO_PIN_12, GPIO_PIN_2, GPIO_PIN_1, GPIO_PIN_0};
const uint8_t NUM_PINS_F = sizeof(GPIOF_PINS) / sizeof(GPIOF_PINS[0]);
// Буфер DMA для нескольких контактов
uint16_t pwmBufferG[100];
uint16_t pwmBufferF[100];
// Независимое отслеживание рабочего цикла
uint8_t dutyCycles[14] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Дескрипторы таймера и DMA
TIM_HandleTypeDef htim8;
TIM_HandleTypeDef htim1;
// Настройка буфера ШИМ
void updatePWMDutyCycle(uint8_t pinIndex, uint8_t dutyCycle)
{
if (pinIndex >= NUM_PINS_G + NUM_PINS_F)
return; // Убедитесь, что индекс правильный
dutyCycles[pinIndex] = dutyCycle; // Сохраняем новый рабочий цикл
for (int i = 0; i < 100; i++)
{
if (pinIndex < NUM_PINS_G)
{
if (i < dutyCycle)
{
pwmBufferG[i] |= GPIOG_PINS[pinIndex]; // Установить высокий уровень контакта
}
else
{
pwmBufferG[i] &= ~GPIOG_PINS[pinIndex]; // Установить вывод на низкий уровень
}
}
else
{
if (i < dutyCycle)
{
pwmBufferF[i] |= GPIOF_PINS[pinIndex - NUM_PINS_G]; // Установить высокий уровень вывода
}
else
{
pwmBufferF[i] &= ~GPIOF_PINS[pinIndex - NUM_PINS_G]; // Установить вывод на низкий уровень
}
}
}
}
void configureDMA()
{
__HAL_RCC_DMA2_CLK_ENABLE();
// Первый DMA (Поток 1, Канал 7)
static DMA_HandleTypeDef hdma;
hdma.Instance = DMA2_Stream1;
hdma.Init.Channel = DMA_CHANNEL_7;
hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma.Init.PeriphInc = DMA_PINC_DISABLE;
hdma.Init.MemInc = DMA_MINC_ENABLE;
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma.Init.Mode = DMA_CIRCULAR;
hdma.Init.Priority = DMA_PRIORITY_HIGH;
hdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma);
__HAL_LINKDMA(&htim8, hdma[TIM_DMA_ID_UPDATE], hdma);
HAL_DMA_Start(&hdma, (uint32_t)pwmBufferG, (uint32_t)&GPIOG->ODR, 100);
// Второй DMA (поток 5, канал 6)
static DMA_HandleTypeDef hdma_tim1_up;
hdma_tim1_up.Instance = DMA2_Stream5;
hdma_tim1_up.Init.Channel = DMA_CHANNEL_6;
hdma_tim1_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim1_up.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim1_up.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim1_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_tim1_up.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_tim1_up.Init.Mode = DMA_CIRCULAR;
hdma_tim1_up.Init.Priority = DMA_PRIORITY_HIGH;
hdma_tim1_up.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_tim1_up);
__HAL_LINKDMA(&htim1, hdma[TIM_DMA_ID_UPDATE], hdma_tim1_up);
HAL_DMA_Start(&hdma_tim1_up, (uint32_t)pwmBufferF, (uint32_t)&GPIOF->ODR, 100);
}
void configureTimer()
{
__HAL_RCC_TIM8_CLK_ENABLE();
htim8.Instance = TIM8;
htim8.Init.Prescaler = 84 - 1; // 84МГц / 84 = 1МГц
htim8.Init.CounterMode = TIM_COUNTERMODE_UP;
htim8.Init.Period = 100 - 1; // 1МГц / 100 = 10 кГц
htim8.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim8.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim8);
HAL_TIM_Base_Start(&htim8);
__HAL_TIM_ENABLE_DMA(&htim8, TIM_DMA_UPDATE);
__HAL_RCC_TIM1_CLK_ENABLE();
htim1.Instance = TIM1;
htim1.Init.Prescaler = 84 - 1; // 84МГц / 84 = 1МГц
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 100 - 1; // 1МГц / 100 = 10 кГц
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim1);
HAL_TIM_Base_Start(&htim1);
__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
}
void setupGPIO()
{
__HAL_RCC_GPIOG_CLK_ENABLE();
GPIO_InitTypeDef GPIOG_InitStruct = {0};
GPIOG_InitStruct.Pin = GPIO_PIN_10 | GPIO_PIN_9 | GPIO_PIN_6 | GPIO_PIN_5 | GPIO_PIN_4 | GPIO_PIN_3 | GPIO_PIN_2;
GPIOG_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIOG_InitStruct.Pull = GPIO_NOPULL;
GPIOG_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOG, &GPIOG_InitStruct);
__HAL_RCC_GPIOF_CLK_ENABLE();
GPIO_InitTypeDef GPIOF_InitStruct = {0};
GPIOF_InitStruct.Pin = GPIO_PIN_15 | GPIO_PIN_14 | GPIO_PIN_13 | GPIO_PIN_12 | GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_0;
GPIOF_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIOF_InitStruct.Pull = GPIO_NOPULL;
GPIOF_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOF, &GPIOF_InitStruct);
}
void setup()
{
// Инициализируем последовательный порт для отладки (необязательно)
Serial.begin(115200);
Serial.println("STM32 Software PWM Started");
setupGPIO();
for (int i = 0; i < NUM_PINS_G + NUM_PINS_F; i++)
{
updatePWMDutyCycle(i, 50);
}
configureDMA();
configureTimer();
Serial.println("Updated PWM duty cycles");
}
void loop()
{
}
Обновление 2:
С учётом предложений liaifat85, а также с использованием BSRR (Bit Set Reset Register), я обновил код ниже. После прочтения этого вопроса я считаю, что использование BSRR в данном случае предпочтительнее. Я проверил работоспособность, изменив скважность и проверив выходы осциллографом.
#include "Arduino.h"
#include "stm32f4xx_hal.h"
// Контакты для обновления
const uint16_t GPIOG_PINS[] = {GPIO_PIN_10, GPIO_PIN_9, GPIO_PIN_6, GPIO_PIN_5, GPIO_PIN_4, GPIO_PIN_3, GPIO_PIN_2}; // Выходы от 1 до 7
const uint8_t NUM_PINS_G = sizeof(GPIOG_PINS) / sizeof(GPIOG_PINS[0]);
const uint16_t GPIOF_PINS[] = {GPIO_PIN_15, GPIO_PIN_14, GPIO_PIN_13, GPIO_PIN_12, GPIO_PIN_2, GPIO_PIN_1, GPIO_PIN_0}; // Выходы с 8 по 14
const uint8_t NUM_PINS_F = sizeof(GPIOF_PINS) / sizeof(GPIOF_PINS[0]);
// Буфер DMA для нескольких контактов
uint32_t pwmBufferG[100] = {0};
uint32_t pwmBufferF[100] = {0};
// Независимое отслеживание рабочего цикла
uint8_t dutyCycles[14] = {0};
// Дескрипторы таймера и DMA
TIM_HandleTypeDef htim8;
TIM_HandleTypeDef htim1;
// Настройка буфера ШИМ
void updatePWMDutyCycle(uint8_t pinIndex, uint8_t dutyCycle)
{
if (pinIndex >= NUM_PINS_G + NUM_PINS_F)
return; // Убедитесь, что индекс правильный
dutyCycles[pinIndex] = dutyCycle; // Сохраняем новый рабочий цикл [cite: 6]
for (int i = 0; i < 100; i++)
{
uint32_t setMask;
uint32_t resetMask;
if (pinIndex < NUM_PINS_G)
{
setMask = GPIOG_PINS[pinIndex];
resetMask = GPIOG_PINS[pinIndex] << 16; // Значение сброса BSRR — это вывод, сдвинутый влево на 16
if (i < dutyCycle)
{
pwmBufferG[i] |= setMask; // Установить высокий уровень контакта
pwmBufferG[i] &= ~resetMask;
}
else
{
pwmBufferG[i] |= resetMask; // Установить вывод на низкий уровень
pwmBufferG[i] &= ~setMask;
}
}
else
{
setMask = GPIOF_PINS[pinIndex - NUM_PINS_G];
resetMask = GPIOF_PINS[pinIndex - NUM_PINS_G] << 16; // Значение сброса BSRR — это вывод, сдвинутый влево на 16
if (i < dutyCycle)
{
pwmBufferF[i] |= setMask; // Установить высокий уровень контакта
pwmBufferF[i] &= ~resetMask;
}
else
{
pwmBufferF[i] |= resetMask; // Установить вывод на низкий уровень
pwmBufferF[i] &= ~setMask;
}
}
}
}
void configureDMA()
{
__HAL_RCC_DMA2_CLK_ENABLE();
// Первый DMA (Поток 1, Канал 7)
static DMA_HandleTypeDef hdma;
hdma.Instance = DMA2_Stream1;
hdma.Init.Channel = DMA_CHANNEL_7;
hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma.Init.PeriphInc = DMA_PINC_DISABLE;
hdma.Init.MemInc = DMA_MINC_ENABLE;
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma.Init.Mode = DMA_CIRCULAR;
hdma.Init.Priority = DMA_PRIORITY_HIGH;
hdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma);
htim8.hdma[TIM_DMA_ID_UPDATE] = &hdma;
HAL_DMA_Start(&hdma, (uint32_t)pwmBufferG, (uint32_t)&GPIOG->BSRR, 100);
// Второй DMA (поток 5, канал 6)
static DMA_HandleTypeDef hdma_tim1_up;
hdma_tim1_up.Instance = DMA2_Stream5;
hdma_tim1_up.Init.Channel = DMA_CHANNEL_6;
hdma_tim1_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim1_up.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim1_up.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim1_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_tim1_up.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_tim1_up.Init.Mode = DMA_CIRCULAR;
hdma_tim1_up.Init.Priority = DMA_PRIORITY_HIGH;
hdma_tim1_up.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_tim1_up);
htim1.hdma[TIM_DMA_ID_UPDATE] = &hdma;
HAL_DMA_Start(&hdma_tim1_up, (uint32_t)pwmBufferF, (uint32_t)&GPIOF->BSRR, 100);
}
void configureTimer()
{
__HAL_RCC_TIM8_CLK_ENABLE();
htim8.Instance = TIM8;
htim8.Init.Prescaler = 84 - 1; // 84МГц / 84 = 1МГц
htim8.Init.CounterMode = TIM_COUNTERMODE_UP;
htim8.Init.Period = 100 - 1; // 1МГц / 100 = 10 кГц
htim8.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim8.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim8);
HAL_TIM_Base_Start(&htim8);
__HAL_TIM_ENABLE_DMA(&htim8, TIM_DMA_UPDATE);
__HAL_RCC_TIM1_CLK_ENABLE();
htim1.Instance = TIM1;
htim1.Init.Prescaler = 84 - 1; // 84МГц / 84 = 1МГц
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 100 - 1; // 1МГц / 100 = 10 кГц
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim1);
HAL_TIM_Base_Start(&htim1);
__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
}
void setupGPIO()
{
__HAL_RCC_GPIOG_CLK_ENABLE();
GPIO_InitTypeDef GPIOG_InitStruct = {0};
GPIOG_InitStruct.Pin = GPIO_PIN_10 | GPIO_PIN_9 | GPIO_PIN_6 | GPIO_PIN_5 | GPIO_PIN_4 | GPIO_PIN_3 | GPIO_PIN_2;
GPIOG_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIOG_InitStruct.Pull = GPIO_NOPULL;
GPIOG_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOG, &GPIOG_InitStruct);
__HAL_RCC_GPIOF_CLK_ENABLE();
GPIO_InitTypeDef GPIOF_InitStruct = {0};
GPIOF_InitStruct.Pin = GPIO_PIN_15 | GPIO_PIN_14 | GPIO_PIN_13 | GPIO_PIN_12 | GPIO_PIN_2 | GPIO_PIN_1 | GPIO_PIN_0;
GPIOF_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIOF_InitStruct.Pull = GPIO_NOPULL;
GPIOF_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOF, &GPIOF_InitStruct);
}
void setup()
{
// Инициализируем последовательный порт для отладки (необязательно)
Serial.begin(115200);
setupGPIO();
for (int i = 0; i < NUM_PINS_G + NUM_PINS_F; i++)
{
updatePWMDutyCycle(i, 50);
}
configureDMA();
configureTimer();
}
void loop()
{
}
@Joe Mann, 👍0
Обсуждение1 ответ
pwmBufferG и pwmBufferF должны быть инициализированы нулем, чтобы избежать неопределенного поведения. DMA правильно связан с TIM8 и TIM1, но синтаксис связи неверен. Вместо __HAL_LINKDMA(&htim8, hdma[TIM_DMA_ID_UPDATE], hdma); используйте htim8.hdma[TIM_DMA_ID_UPDATE] = &hdma; Вы используете DMA2_Stream1 для TIM8, но у STM32F407 DMA2_Stream1 не сопоставлен с TIM8.
DMA2_Stream5 не сопоставлен с обновлением TIM1.
Необходимо использовать DMA2_Stream2 для TIM8_UP и DMA2_Stream5 для TIM1_UP.
Надеюсь, эти исправления решат существующие проблемы. Следующая информация может оказаться вам полезной.
https://controllerstech.com/pwm-with-dma-in-stm32/
Если вы хотите сделать собственную плату на базе STM32, вы можете посмотреть здесь: https://www.pcbway.com/blog/25/Tutorial__How_to_Design_Your_Own_Custom_STM32_Microcontroller_Board.html
Согласно странице 311 RM009 (справочное руководство STM32F407), TIM8_UP связан с потоком 1, каналом 7, а TIM1_UP связан с потоком 5, каналом 6. Я попробую эти изменения., @Joe Mann
- Почему мой код прерывания не работает?
- Использовать выводы PWM в качестве обычных цифровых входов/выходов?
- Как Arduino Uno может поддерживать до 12 сервоприводов, если у него всего 6 цифровых выводов ШИМ?
- В Arduino Uno можно использовать цифровые контакты с ШИМ для чтения аналоговых данных. Возможно ли это также с Wemos D1 Mini?
- NodeMCU - Vin контакт как выход 5V?
- Использовать все контакты как цифровые входы/выходы
- Что представляют собой AREF, IOREF и немаркированный контакт рядом с IOREF на Uno R3?
- Установите частоту ШИМ на 25 кГц.
@jsotola - если у кого-то есть ответ на любой из этих вопросов, я буду признателен., @Joe Mann
@jstola. Я отредактировал пост. Надеюсь, теперь он понятнее., @Joe Mann
Как насчёт использования PCA9685 - I2C - 16 каналов, 12-битный ШИМ? С DMA, вероятно, по одному каналу на порт и использованием BSRR (однако для 256 шагов потребуется 1 КБ ОЗУ на порт, и изменение значений может быть затруднительным — но можно просто установить один бит, когда устройство должно запускаться, и один бит, когда оно должно очищаться)., @KIIV
Это возможно, но на данном этапе я не хочу проводить ещё одну модернизацию оборудования. ШИМ-модуляция была бы неплоха, но я не знаю, стоит ли снова её менять, если только это не будет абсолютно необходимо., @Joe Mann
Это зависит от того, что обойдется дороже — новая доработка или ваши усилия по ее обходу., @KIIV
Я делаю это в свободное время, так что в этом отношении мне это ничего не стоит., @Joe Mann