STM32F411 настройка тактирования

Дипсик:
Для настройки PLL в STM32F411CEU6 с использованием внешнего кварца 25 МГц и получения тактовой частоты 100 МГц необходимо выполнить следующие шаги:


1. Настройка тактирования через PLL

Микроконтроллер STM32F411 использует PLL для умножения частоты.
Формула расчёта частоты после PLL:

fвых=fвх×PLLNPLLM×PLLPfвых​=PLLM×PLLPfвх​×PLLN​

Где:

  • PLLM — предделитель для входа PLL (делит частоту кварца перед подачей на PLL).
  • PLLN — множитель PLL.
  • PLLP — постделитель для выхода PLL (должен быть 2, 4, 6 или 8).

Для 100 МГц при кварце 25 МГц можно выбрать:

  • PLLM = 25 (частота на вход PLL: 25 МГц25=1 МГц2525МГц​=1МГц).
  • PLLN = 200 (умножение: 1 МГц×200=200 МГц1МГц×200=200МГц).
  • PLLP = 2 (итоговая частота: 200 МГц2=100 МГц2200МГц​=100МГц).

Проверка ограничений STM32F411:

  • Вход PLL (после PLLM): 1–2 МГц (у нас 1 МГц — OK).
  • VCO (PLLN × вход PLL): 100–432 МГц (у нас 200 МГц — OK).
  • Выход PLL (после PLLP): ≤ 100 МГц (у нас 100 МГц — OK).
Спойлер
#include "stm32f4xx_hal.h"

void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    // Настройка источника тактирования (HSE + PLL)
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON; // Включаем внешний кварц 25 МГц
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 25;         // Делитель для входа PLL
    RCC_OscInitStruct.PLL.PLLN = 200;        // Множитель PLL
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; // Постделитель PLLP = 2
    RCC_OscInitStruct.PLL.PLLQ = 4;          // Для USB/SDIO (необязательно)
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    // Настройка делителей для шин
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // Тактирование от PLL
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;        // HCLK = 100 МГц
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;         // APB1 = 50 МГц
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;         // APB2 = 100 МГц
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3); // Задержка FLASH = 3 (для 100 МГц)
}

Итоговая схема PLL

25 МГц (HSE) → PLLM(25) → 1 МГц → PLLN(200) → 200 МГц → PLLP(2) → 100 МГц (SYSCLK)

После настройки микроконтроллер будет работать на 100 МГц с стабильным тактированием от внешнего кварца.

1 лайк

господи, там как хошь можно извращаться

25 МГц (HSE) → PLLM(13) → 1,923076923076923 МГц → PLLN(104) → 200 МГц → PLLP(2) → 100 МГц (SYSCLK)

Ага. Вот я и извращаюсь. Наверное что то делаю не так с настройками. Схема работает на внутреннем генераторе. Системная частота 94.8 мгц.

// Версия 4
#include <LiquidCrystal.h>            // библиотека для работы с экраном
LiquidCrystal lcd(PB8, PB7, PB6, PB5, PB4, PB3, PA15, PA12, PB13, PB12); // (RS,E,D0-D7)
volatile bool flag_Led = 0;
uint32_t tim2_value;
//--------------------
void setup()
{
  lcd.begin(20, 4);
  //--- Настройка Таймера 2
  // Настройка системной шины
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сбрасываем биты PPRE1
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV1; // Устанавливаем делитель APB1 = 1 (до настройки PLL)
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Включаем тактирование таймера TIM2
  TIM2->CR1 &= ~TIM_CR1_CEN;        // Останавливаем таймер перед настройкой
  TIM2->PSC = 0;                    // Предделитель (PSC) = 0
  TIM2->ARR = 0xFFFFFFFF;           // Максимальное значение счётчика (32 бита)
  TIM2->CNT = 0;                    // Сбрасываем текущее значение счётчика
  TIM2->EGR |= TIM_EGR_UG;          // Принудительное обновление регистров таймера
  TIM2->CR1 |= TIM_CR1_CEN;         // Запускаем таймер
  //--- Настройка прерывания на PB0
  pinMode(PB0, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PB0), watchdogHandler, RISING);
  //--- Настройка системной частоты на 100 МГц через PLL
  RCC->CR |= RCC_CR_HSEON;                       // Включаем HSE
  while (!(RCC->CR & RCC_CR_HSERDY));            // Ждем готовности HSE
  RCC->PLLCFGR = (25 << RCC_PLLCFGR_PLLM_Pos) |  // PLLM=25 (25 МГц / 25 = 1 МГц)
                 (200 << RCC_PLLCFGR_PLLN_Pos) | // PLLN=200 (1 МГц × 200 = 200 МГц)
                 RCC_PLLP_DIV2;                  // PLLP=2 (200 МГц / 2 = 100 МГц)
  RCC->CR |= RCC_CR_PLLON;                       // Включаем PLL
  while (!(RCC->CR & RCC_CR_PLLRDY));            // Ждем готовности PLL
  // Переключение системного такта на PLL
  RCC->CFGR = (2 << RCC_CFGR_SW_Pos); // SW=2 (PLL)
  uint32_t timeout = 100000;          // Таймаут для проверки переключения
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) 
  {
    timeout--;
    if (timeout == 0) break; // Выход при таймауте
  }
  //Настройка делителей тактов
  RCC->CFGR &= ~RCC_CFGR_HPRE;      // Сброс HPRE (AHB делитель)
  RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // HCLK = 100 МГц (AHB = SYSCLK / 1)
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сброс PPRE1 (APB1 делитель)
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 50 МГц (AHB / 2)
}
//---------------------
void loop()
{
  if (flag_Led)
  {
    // Вывод значения таймера TIM2 на LCD
    lcd.setCursor(0, 0);lcd.print("SYS=");lcd.print(tim2_value);
    flag_Led = 0;
  }
}
//--------------------
void watchdogHandler()
{
  flag_Led = 1;
  tim2_value = TIM2->CNT; // Сохраняем текущее значение таймера
  TIM2->CNT = 0;          // Сброс счетчика
}

в какой строке, по твоему, ты переключаешь тактирование PLL на HSE?
вот @andycat так делает

RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE; // PLL source set HSE

Ну я думаю это вставить в 32 строку. Но это тоже не помогло.

// Версия 5
#include <LiquidCrystal.h>            // библиотека для работы с экраном
LiquidCrystal lcd(PB8, PB7, PB6, PB5, PB4, PB3, PA15, PA12, PB13, PB12); // (RS,E,D0-D7)
volatile bool flag_Led = 0;
uint32_t tim2_value;
//--------------------
void setup()
{
  lcd.begin(20, 4);
  //--- Настройка Таймера 2
  // Настройка системной шины
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сбрасываем биты PPRE1
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV1; // Устанавливаем делитель APB1 = 1 (до настройки PLL)
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Включаем тактирование таймера TIM2
  TIM2->CR1 &= ~TIM_CR1_CEN;        // Останавливаем таймер перед настройкой
  TIM2->PSC = 0;                    // Предделитель (PSC) = 0
  TIM2->ARR = 0xFFFFFFFF;           // Максимальное значение счётчика (32 бита)
  TIM2->CNT = 0;                    // Сбрасываем текущее значение счётчика
  TIM2->EGR |= TIM_EGR_UG;          // Принудительное обновление регистров таймера
  TIM2->CR1 |= TIM_CR1_CEN;         // Запускаем таймер
  //--- Настройка прерывания на PB0
  pinMode(PB0, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PB0), watchdogHandler, RISING);
  //--- Настройка системной частоты на 100 МГц через PLL
  RCC->CR |= RCC_CR_HSEON;                       // Включаем HSE
  while (!(RCC->CR & RCC_CR_HSERDY));            // Ждем готовности HSE
  // Настройка PLL
  RCC->PLLCFGR = 0; // Сброс PLLCFGR
 RCC->PLLCFGR = (25 << RCC_PLLCFGR_PLLM_Pos) |   // PLLM=25 (25 МГц / 25 = 1 МГц)
                 (200 << RCC_PLLCFGR_PLLN_Pos) | // PLLN=200 (1 МГц × 200 = 200 МГц)
                 RCC_PLLP_DIV2;                  // PLLP=2 (200 МГц / 2 = 100 МГц)
  RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE;        // Источник HSE
  RCC->CR |= RCC_CR_PLLON;                       // Включаем PLL
  while (!(RCC->CR & RCC_CR_PLLRDY));            // Ждем готовности PLL
  // Переключение системного такта на PLL
  RCC->CFGR = (2 << RCC_CFGR_SW_Pos); // SW=2 (PLL)
  uint32_t timeout = 100000;          // Таймаут для проверки переключения
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) 
  {
    timeout--;
    if (timeout == 0) break; // Выход при таймауте
  }
  // Настройка делителей тактов
  RCC->CFGR &= ~RCC_CFGR_HPRE;      // Сброс HPRE (AHB делитель)
  RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // HCLK = 100 МГц (AHB = SYSCLK / 1)
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сброс PPRE1 (APB1 делитель)
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 50 МГц (AHB / 2)
}
//---------------------
void loop()
{
  if (flag_Led)
  {
    // Вывод значения таймера TIM2 на LCD
    lcd.setCursor(0, 0);lcd.print("SYS=");lcd.print(tim2_value);
    flag_Led = 0;
  }
}
//--------------------
void watchdogHandler()
{
  flag_Led = 1;
  tim2_value = TIM2->CNT; // Сохраняем текущее значение таймера
  TIM2->CNT = 0;          // Сброс счетчика
}

вы не находите противоречий?
переключать источник на выключенный PLL, а только потом его включать?

STM32 не дура, сразу валится в Error_Handler() и включает внутренний HSI, шоб совсем не сдохнуть))

Может и были противоречия в этих строках но похоже она слишком умная. Все равно работать не хочет.

// Версия 6
#include <LiquidCrystal.h>            // библиотека для работы с экраном
LiquidCrystal lcd(PB8, PB7, PB6, PB5, PB4, PB3, PA15, PA12, PB13, PB12); // (RS,E,D0-D7)
volatile bool flag_Led = 0;
uint32_t tim2_value;
//--------------------
void setup()
{
  lcd.begin(20, 4);
  //--- Настройка Таймера 2
  // Настройка системной шины
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сбрасываем биты PPRE1
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV1; // Устанавливаем делитель APB1 = 1 (до настройки PLL)
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Включаем тактирование таймера TIM2
  TIM2->CR1 &= ~TIM_CR1_CEN;        // Останавливаем таймер перед настройкой
  TIM2->PSC = 0;                    // Предделитель (PSC) = 0
  TIM2->ARR = 0xFFFFFFFF;           // Максимальное значение счётчика (32 бита)
  TIM2->CNT = 0;                    // Сбрасываем текущее значение счётчика
  TIM2->EGR |= TIM_EGR_UG;          // Принудительное обновление регистров таймера
  TIM2->CR1 |= TIM_CR1_CEN;         // Запускаем таймер
  //--- Настройка прерывания на PB0
  pinMode(PB0, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PB0), watchdogHandler, RISING);
  //--- Настройка системной частоты на 100 МГц через PLL
  RCC->CR |= RCC_CR_HSEON;                       // Включаем HSE
  while (!(RCC->CR & RCC_CR_HSERDY));            // Ждем готовности HSE
  // Настройка PLL
  RCC->PLLCFGR = 0;                              // Сброс PLLCFGR
 RCC->PLLCFGR = (25 << RCC_PLLCFGR_PLLM_Pos) |   // PLLM=25 (25 МГц / 25 = 1 МГц)
                 (200 << RCC_PLLCFGR_PLLN_Pos) | // PLLN=200 (1 МГц × 200 = 200 МГц)
                 RCC_PLLP_DIV2;                  // PLLP=2 (200 МГц / 2 = 100 МГц)
  RCC->CR |= RCC_CR_PLLON;                       // Включаем PLL
  RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE;        // Источник HSE
  while (!(RCC->CR & RCC_CR_PLLRDY));            // Ждем готовности PLL
  // Переключение системного такта на PLL
  RCC->CFGR = (2 << RCC_CFGR_SW_Pos);            // SW=2 (PLL)
  uint32_t timeout = 100000;                     // Таймаут для проверки переключения
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) 
  {
    timeout--;
    if (timeout == 0) break; // Выход при таймауте
  }
  // Настройка делителей тактов
  RCC->CFGR &= ~RCC_CFGR_HPRE;      // Сброс HPRE (AHB делитель)
  RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // HCLK = 100 МГц (AHB = SYSCLK / 1)
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сброс PPRE1 (APB1 делитель)
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 50 МГц (AHB / 2)
}
//---------------------
void loop()
{
  if (flag_Led)
  {
    // Вывод значения таймера TIM2 на LCD
    lcd.setCursor(0, 0);lcd.print("SYS=");lcd.print(tim2_value);
    flag_Led = 0;
  }
}
//--------------------
void watchdogHandler()
{
  flag_Led = 1;
  tim2_value = TIM2->CNT; // Сохраняем текущее значение таймера
  TIM2->CNT = 0;          // Сброс счетчика
}

Как-то через опу сначала настраивать экранчик, таймеры, а потом уже тактирование.
Надо:

  1. Настроить тактирование. ОБЯЗАТЕЛЬНО дождаться сигнала готовности( PLL штука по меркам МК ОЧЕНЬ ДОЛГО устаканивается)
  2. Включаем тактирование нужного модуля
  3. Настраиваем модуль.

Иной порядок приводит к непредсказуемым багам. Коттрые ни один дебаг не в состоянии отловить- частота-то неизвестна в конкретный момент!

мне после этого надоело что то объяснять.

1 лайк

Вроде делаю порядок все по правильному коду. Порядок настройки такой:

  1. Включение HSE и проверка его готовности.
  2. Настройка параметров PLL (PLLM, PLLN, PLLP) до включения PLL .
  3. Включение PLL и проверка его готовности.
  4. Переключение системного такта на PLL .
  5. Настройка делителей частот шин (AHB, APB1).
// Версия 7
#include <LiquidCrystal.h>            // библиотека для работы с экраном
LiquidCrystal lcd(PB8, PB7, PB6, PB5, PB4, PB3, PA15, PA12, PB13, PB12); // (RS,E,D0-D7)
volatile bool flag_Led = 0;
uint32_t tim2_value;
//--------------------
void setup()
{
  //--- Настройка системной частоты на 100 МГц через PLL
  RCC->CR |= RCC_CR_HSEON;                       // Включаем HSE
  while (!(RCC->CR & RCC_CR_HSERDY));            // Ждем готовности HSE
  RCC->PLLCFGR = 0;                              // Сброс PLLCFGR
  RCC->PLLCFGR =                                 // Задаем параметры PLL ДО ВКЛЮЧЕНИЯ:
      (25 << RCC_PLLCFGR_PLLM_Pos) |             // PLLM=25 (25 МГц / 25 = 1 МГц)
      (200 << RCC_PLLCFGR_PLLN_Pos) |            // PLLN=200 (1 МГц × 200 = 200 МГц)
      RCC_PLLP_DIV2;                             // PLLP=2 (200 МГц / 2 = 100 МГц)
  RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE;        // Указываем источник HSE
  RCC->CR |= RCC_CR_PLLON;                       // Включаем PLL
  while (!(RCC->CR & RCC_CR_PLLRDY));            // Ждем готовности
  // Переключение системного такта на PLL
  RCC->CFGR = (2 << RCC_CFGR_SW_Pos);            // SW=2 (PLL)
  uint32_t timeout = 100000;                     // Таймаут для проверки переключения
  while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) 
  {
    timeout--;
    if (timeout == 0) break;                     // Выход при таймауте
  }
  // Настройка делителей тактов
  RCC->CFGR &= ~RCC_CFGR_HPRE;      // Сброс HPRE (AHB делитель)
  RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // HCLK = 100 МГц (AHB = SYSCLK / 1)
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сброс PPRE1 (APB1 делитель)
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 50 МГц (AHB / 2)
  //Настройка экрана.
  lcd.begin(20, 4);
  //--- Настройка Таймера 2
  // Настройка системной шины
  RCC->CFGR &= ~RCC_CFGR_PPRE1;     // Сбрасываем биты PPRE1
  RCC->CFGR |= RCC_CFGR_PPRE1_DIV1; // Устанавливаем делитель APB1 = 1 (до настройки PLL)
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Включаем тактирование таймера TIM2
  TIM2->CR1 &= ~TIM_CR1_CEN;        // Останавливаем таймер перед настройкой
  TIM2->PSC = 0;                    // Предделитель (PSC) = 0
  TIM2->ARR = 0xFFFFFFFF;           // Максимальное значение счётчика (32 бита)
  TIM2->CNT = 0;                    // Сбрасываем текущее значение счётчика
  TIM2->EGR |= TIM_EGR_UG;          // Принудительное обновление регистров таймера
  TIM2->CR1 |= TIM_CR1_CEN;         // Запускаем таймер
  //--- Настройка прерывания на PB0
  pinMode(PB0, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PB0), watchdogHandler, RISING);
}
//---------------------
void loop()
{
  if (flag_Led)
  {
    // Вывод значения таймера TIM2 на LCD
    lcd.setCursor(0, 0);lcd.print("SYS=");lcd.print(tim2_value);
    flag_Led = 0;
  }
}
//--------------------
void watchdogHandler()
{
  flag_Led = 1;
  tim2_value = TIM2->CNT; // Сохраняем текущее значение таймера
  TIM2->CNT = 0;          // Сброс счетчика
}

Спасибо всем за помощь. Действительно настраивается легко. Теперь все работает правильно. Оказалось виноват внешний кварцевый генератор. Кварц возбуждался на 1 гармонике на 25 мгц. А должен был на 4 гармонике и получилось бы 100 мгц.Поскольку кварц маленький и паять его не охота то вместо него поставил другой кварц с выходом TTL на 3.3 вольта и подключил его к выводу PA0 и все заработало. Я почему то думал что если у кварца 4 вывода то это генератор с внешним питанием. Оказалось нет. Посмотрел на схему платы и очень удивился этому. Теперь частота стабильна 100 мгц +/- 1 Гц. Правильный код в предыдущем сообщении.

Сбил с толку, однако)
А PA0 это input frequence? Подглядел, это вход таймера вроде.

Но вход внеш.кварца 4-26МГц. Как он может выдавать 100МГц?

тут, по моему продолжение потока сознания, нихрена не относящиеся не начальному вопросу, не к последствиям решения
чего одно это стоит

а из позитива и всем за науку, это

че делал, как подключал, одни загадки и обрывки кода.
и то из сетей похоже…
ладно срослось и ладно, “вопрос решен” сам за себя говорит…

2 лайка

Ну поскольку человеку надоело что то объяснять так зачем спрашивать что да как получилось, если на простейшие и легчайшие вопросы отвечать лень. А вот про потоки сознания поговорить это очень челу важно. Вам бы в предсказатели податься. Там можно вообще ничего не делать а только говорить.

так я там и работаю.

и встречный совет, научитесь четко выражать свои мысли.

Комментарии по тактированию от внешнего генератора. Проверил на платах STM32F411 и STM32G431. в обоих вариантах все работает и переключается как на внешний генератор так и на внутренний. И дело оказывается было не в коде. Даже в исправном коде на PLL не переходит.
Неправильный вариант. Я настройки тактирования прописывал в Setup напрямую.
Правильный вариант (рабочий). Настройки генератора надо было прописать сюда void SystemClock_Config(void) {} а уже из setup вызывать SystemClock_Config(); В чем разница я не знаю но это у меня заработало только в таком варианте причем сразу.

Перед вызовом setup вызываются скрытые init() и прочие которые собственно настраивают камень под нужды программы. Работает main.cpp как и положено, но неофитам это не видно. Поэтому выход из loop, в частности, просто пребрасывает в начало loop.

Спойлер main.cpp
#include <Arduino.h>

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}