Программирование STM32. Часть 4: Настройка RCC

Ответить
Oleg
Сообщения: 186
Зарегистрирован: 12 июл 2023, 12:09
Программирование STM32. Часть 4: Настройка RCC

Сообщение Oleg »

Регистры CR и CFGR

Открываем Reference manual на микроконтроллер STM32F103x8 и переходим к разделу 7: Low-, medium-, high- and XL-density reset and clock control (RCC). RCC имеет довольно много регистров, целых 10 штук. Однако, для настройки источника тактирования и делителей шин нам понадобится только 2 из них.
Биты регистра CR.jpg
Описание основных битов регистра:

PLLRDY — флаг готовности PLL. Устанавливается аппаратно и сигнализирует о том, что PLL заблокирован.

PLLON — Включить PLL. Устанавливается и сбрасывается программно. При переходе в режим Stop или Standby сбрасывается аппаратно. Этот бит не может быть сброшен, если PLL используется как источник системного тактирования.

CSSON — включить систему CSS

HSEBYP — Если вместо кварцевого резонатора HSE мы хотим использовать внешний прямоугольный тактовый сигнал, то этот бит нужно установить в 1

HSERDY — Флаг готовности генератора HSE. Аппаратно устанавливается в 1 после успешного запуска и стабилизации частоты HSE-генератора

HSEON — Запустить HSE генератор. Устанавливается и сбрасывается программно. При переходе в режим Stop или Standby сбрасывается аппаратно и HSE генератор останавливается. Этот бит не может быть сброшен, если HSE используется как источник системного тактирования.

HSIRDY — то же самое, что и HSERDY, только для встроенного RC-генератора HSI

HSION — то же самое, что и HSEON, только для встроенного RC-генератора HSI
Биты регистра CFGR.jpg
Описание основных битов регистра:

MCO — подача тактового сигнала на MCO-пин микроконтроллера.

0xx: Функция отключена
100: Выбран System clock (SYSCLK)
101: Выбран сигнал с HSI
110: Выбран сигнал с HSE
111: Выбран сигнал с PLL, который поделен на 2

PLLMUL — коэффициент умножения PLL. Эти биты могут быть записаны программно только при отключенном PLL

0000: Входную частота PLL умножить на 2
0001: —//— на 3
0010: —//— на 4
0011: —//— на 5
0100: —//— на 6
0101: —//— на 7
0110: —//— на 8
0111: —//— на 9
1000: —//— на 10
1001: —//— на 11
1010: —//— на 12
1011: —//— на 13
1100: —//— на 14
1101: —//— на 15
1110: —//— на 16
1111: —//— на 16

Два последних значения соответствуют одинаковому коэффициенту умножения.

PLLXTPRE — Делитель частоты с HSE генератора перед подачей на PLL. Этот бит не может быть изменен, если PLL запущен. При установке в 1 частота HSE будет поделена на 2, если 0, то делитель отключен.

PLLSRC — Источник входной частоты PLL. Не может быть изменен, если PLL запущен.

0: частота HSI генератора поделенная на 2
1: частота HSE генератора. Делитель может быть выбран PLLXTPRE битом.

PPRE2 — Делитель шины APB2 prescaler

0xx: HCLK без деления
100: HCLK / 2
101: HCLK / 4
110: HCLK / 8
111: HCLK / 16

PPRE1 — Делитель шины APB1 prescaler. Частота шины APB1 не должна превышать 36 МГц.

0xx: HCLK без деления
100: HCLK / 2
101: HCLK / 4
110: HCLK / 8
111: HCLK / 16

HPRE — AHB prescaler

0xxx: SYSCLK без деления
1000: SYSCLK / 2
1001: SYSCLK / 4
1010: SYSCLK / 8
1011: SYSCLK / 16
1100: SYSCLK / 64
1101: SYSCLK / 128
1110: SYSCLK / 256
1111: SYSCLK / 512

SWS — Состояние переключателя тактирования системы. Устанавливается аппаратно и указывает на текущий источник тактирования.

00: HSI генератор используется как источник тактирования системы
01: HSE генератор используется как источник тактирования системы
10: PLL используется как источник тактирования системы

SW — Переключатель источника тактирования системы. Изменяется программно для выбора источника SYSCLK. Устанавливается аппаратно для принудительного переключения на HSI генератор переходе в режим Stop или Standby или в случае срыва генерации HSE, который используется в качестве источника SYSCLK (только если активна система CSS)

00: HSI выбран в качестве источника системного тактирования
01: HSE выбран в качестве источника системного тактирования
10: PLL выбран в качестве источника системного тактирования

Коэффициенты

Ранее мы рассмотрели вариант тактирования системы тактирования от HSE генератора через PLL, для удобства скопирую это сюда:

Кварц HSE на 8 МГц
PLLXTPRE: без деления
PLLSRC: HSE генератор
PLLMUL = 9
SW = PLLCLK
AHB Prescaler = 1
APB1 Prescaler = 2
APB2 Prescaler = 1

И картинку тоже:
Схема прохождения тактового сигнала при использовании PLL совместно с HSE.jpg
Регистры в CMSIS

Во одной из тем мы учились подключать библиотеку CMSIS к IAR-у, сейчас нам понадобится этот проект, так как мы переходим к практике. Но перед этим немного поговорим о том, как устроено обращение к регистрам периферии в CMSIS.

Каждый экземпляр периферии является структурой, в которой находятся все регистры, относящиеся к данному устройству. Почти во всех случаях имя структуры совпадает с именем периферийного модуля. Для микроконтроллера STM32F103C8 все структуры периферийных модулей объявлены в файле stm32f103xb.h:

Код: Выделить всё

#define TIM2                ((TIM_TypeDef *)TIM2_BASE)
#define TIM3                ((TIM_TypeDef *)TIM3_BASE)
#define TIM4                ((TIM_TypeDef *)TIM4_BASE)
#define RTC                 ((RTC_TypeDef *)RTC_BASE)
#define WWDG                ((WWDG_TypeDef *)WWDG_BASE)
#define IWDG                ((IWDG_TypeDef *)IWDG_BASE)
#define SPI2                ((SPI_TypeDef *)SPI2_BASE)
#define USART2              ((USART_TypeDef *)USART2_BASE)
#define USART3              ((USART_TypeDef *)USART3_BASE)
#define I2C1                ((I2C_TypeDef *)I2C1_BASE)
#define I2C2                ((I2C_TypeDef *)I2C2_BASE)
#define USB                 ((USB_TypeDef *)USB_BASE)
#define CAN1                ((CAN_TypeDef *)CAN1_BASE)
#define BKP                 ((BKP_TypeDef *)BKP_BASE)
#define PWR                 ((PWR_TypeDef *)PWR_BASE)
#define AFIO                ((AFIO_TypeDef *)AFIO_BASE)
#define EXTI                ((EXTI_TypeDef *)EXTI_BASE)
#define GPIOA               ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *)GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *)GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *)GPIOE_BASE)
#define ADC1                ((ADC_TypeDef *)ADC1_BASE)
#define ADC2                ((ADC_TypeDef *)ADC2_BASE)
#define ADC12_COMMON        ((ADC_Common_TypeDef *)ADC1_BASE)
#define TIM1                ((TIM_TypeDef *)TIM1_BASE)
#define SPI1                ((SPI_TypeDef *)SPI1_BASE)
#define USART1              ((USART_TypeDef *)USART1_BASE)
#define SDIO                ((SDIO_TypeDef *)SDIO_BASE)
#define DMA1                ((DMA_TypeDef *)DMA1_BASE)
#define DMA1_Channel1       ((DMA_Channel_TypeDef *)DMA1_Channel1_BASE)
#define DMA1_Channel2       ((DMA_Channel_TypeDef *)DMA1_Channel2_BASE)
#define DMA1_Channel3       ((DMA_Channel_TypeDef *)DMA1_Channel3_BASE)
#define DMA1_Channel4       ((DMA_Channel_TypeDef *)DMA1_Channel4_BASE)
#define DMA1_Channel5       ((DMA_Channel_TypeDef *)DMA1_Channel5_BASE)
#define DMA1_Channel6       ((DMA_Channel_TypeDef *)DMA1_Channel6_BASE)
#define DMA1_Channel7       ((DMA_Channel_TypeDef *)DMA1_Channel7_BASE)
#define RCC                 ((RCC_TypeDef *)RCC_BASE)
#define CRC                 ((CRC_TypeDef *)CRC_BASE)
#define FLASH               ((FLASH_TypeDef *)FLASH_R_BASE)
#define OB                  ((OB_TypeDef *)OB_BASE)
#define DBGMCU              ((DBGMCU_TypeDef *)DBGMCU_BASE)
Рассмотрим, как выполняется обращение к регистрам из программы на Си. Например, нам надо в регистре RCC_CR установить бит HSEON. Это можно сделать одним из следующих способов:

Код: Выделить всё

RCC->CR |= RCC_CR_HSEON_Msk;
или так:

Код: Выделить всё

RCC->CR |= (1 << RCC_CR_HSEON_Pos);
Думаю, те, кто раньше программировал для микроконтроллеров AVR увидят в этих записях что-то знакомое. Рассмотрим первый случай:
123.jpg
123.jpg (7.02 КБ) 361 просмотр
Сначала идет имя периферийного модуля, в нашем случае «RCC». Затем символ «->», после чего имя регистра «CR». RCC_CR_HSEON_Msk представляет собой вот такой #define:

Код: Выделить всё

#define RCC_CR_HSEON_Msk    (1<<16)
где 16 — номер бита HSEON в регистре CR (см. рис. 1). RCC_CR_HSEON_Msk есть ни что иное, как битовая маска, имя которой состоит из названия периферийного модуля, имени регистра и бита, а так же постфикса _Msk. В CMSIS есть еще один #define, который является синонимом RCC_CR_HSEON_Msk:

Код: Выделить всё

#define RCC_CR_HSEON    RCC_CR_HSEON_Msk
По факту все то же самое, только без _Msk.

Второй случай выглядит аналогичным образом:
321.jpg
321.jpg (7.43 КБ) 361 просмотр
где

Код: Выделить всё

#define RCC_CR_HSEON_Pos    16
То есть, RCC_CR_HSEON_Pos является позицией бита в регистре, о чем говорит постфикс _Pos.

А как быть с параметрами, которые имеют несколько битов? К примеру в регистре CFGR мы хотим установить значение множителя PLL равное девяти, имеющее код 0111 (см. рис. 2 биты PLLMUL). Тут вот такое решение:

Код: Выделить всё

RCC->CFGR |= RCC_CFGR_PLLMULL_0 | RCC_CFGR_PLLMULL_1 | RCC_CFGR_PLLMULL_2;
RCC->CFGR &= ~(RCC_CFGR_PLLMULL_3);
Первой строчкой мы устанавливаем биты 0, 1 и 2 PLLMUL в единицы, второй строчкой сбрасываем бит 3 в ноль, в итоге получаем 0111. Однако, если мы уверены, что все биты PLLMUL изначально установлены в нули, то вторую строчку мы можем пропустить:

Код: Выделить всё

RCC->CFGR |= RCC_CFGR_PLLMULL_0 | RCC_CFGR_PLLMULL_1 | RCC_CFGR_PLLMULL_2;
А есть и еще один вариант: значение 0111 в десятичном виде является числом 7. Тогда можно сделать вот так:

Код: Выделить всё

RCC->CFGR |= (7 << RCC_CFGR_PLLMULL_Pos);
Но и тут надо помнить, что такая запись справедлива только в случае, если изначальное значение всех битов PLLMUL равны нулям.

Настройка

Для правильной настройки системы тактирования в микроконтроллерах STM32 необходимо соблюдать определенную последовательность запуска блоков. Если мы хотим переключить систему с внутреннего RC-генератора на HSE через PLL, то необходимо провести следующие операции вот в таком порядке:

Запустить генератор HSE
Настроить PLL
Запустить PLL
Настроить количество циклов ожидания FLASH
Настроить делители шин
Переключиться на работу от PLL

Так, вроде все понятно, хотя… А что это за 4-й пункт? Что за циклы ожидания FLASH? Вот здесь кроется один неочевидный момент. Дело в том, что FLASH-память микроконтроллера, в которой хранится управляющая программа, может работать на максимальной частоте 24 МГц. Обмен данными с FLASH осуществляется через шину AHB (см. рис. 3). А если частота шины AHB выше 24 МГц, но необходимо ввести циклы ожидания обращений к этой памяти, примем, чем выше частота, тем больше этих циклов надо:

ноль циклов ожидания, если 0 < SYSCLK ≤ 24 MHz
один цикл ожидания, если 24 MHz < SYSCLK ≤ 48 MHz
два цикла ожидания, если 48 MHz < SYSCLK ≤ 72 MHz

Надеюсь, с этим все ясно.

Хочу отметить, что порядок настройки может быть и таким:

Настроить количество циклов ожидания FLASH
Настроить делители шин
Запустить генератор HSE
Настроить PLL
Запустить PLL
Переключиться на работу от PLL

Просто надо помнить, что мы сидим на ветке, которую пилим, и важно все отпилить и подставить в нужной последовательности, чтоб не упасть. В случае с STM32, «упасть» не получится, так как в этих микроконтроллерах реализована аппаратная защита от неправильной последовательности действий: мы не сможем остановить генератор, от которого сейчас работаем или переключиться на источник тактового сигнала, если он еще не запущен. Так что в худшем случае у нас просто ни чего не получится, что то же является не очень приятным.

Итак, поехали! Возьмем за основу проект, который мы создали ранее, когда подключали CMSIS: https://github.com/DiMoonElec/stm32f103c8_empty_project.

Создадим функцию, в которую будем добавлять код инициализации:

Код: Выделить всё

int ClockInit(void)
{
}
Первым делом запускаем генератор HSE:

Код: Выделить всё

RCC->CR |= (1<<RCC_CR_HSEON_Pos); //Запускаем генератор HSE
После этого нам надо дождаться установки флага HSERDY, который указывает на успешный запуск генератора. Можно это сделать по-простому с помощью цикла while() {}, но тогда мы рискуем зависнуть в нем навсегда, если что-то случится с кварцевым резонатором. Хотелось бы иметь возможность каким-то образом сигнализировать о невозможности запуска. Вот моя реализация:

Код: Выделить всё

  __IO int StartUpCounter;

  //Ждем успешного запуска или окончания тайм-аута
  for(StartUpCounter=0; ; StartUpCounter++)
  {
    //Если успешно запустилось, то 
    //выходим из цикла
    if(RCC->CR & (1<<RCC_CR_HSERDY_Pos))
      break;
    
    //Если не запустилось, то
    //отключаем все, что включили
    //и возвращаем ошибку
    if(StartUpCounter > 0x1000)
    {
      RCC->CR &= ~(1<<RCC_CR_HSEON_Pos); //Останавливаем HSE
      return 1;
    }
  }
Тут реализован тайм-аут на запуск HSE. Если генератор успел запуститься до возникновения условия if(StartUpCounter > 0x1000), то мы выходим из цикла for() с помощью инструкции break.

Так, HSE запустили. Переходим к настройке PLL:

Код: Выделить всё

  //Настраиваем PLL
  RCC->CFGR |= (0x07<<RCC_CFGR_PLLMULL_Pos) //PLL множитель равен 9
            | (0x01<<RCC_CFGR_PLLSRC_Pos); //Тактирование PLL от HSE
Тут просто настраиваем коэффициент умножения и выбираем источник тактирования PLL. Далее, запускаем PLL:

Код: Выделить всё

RCC->CR |= (1<<RCC_CR_PLLON_Pos); //Запускаем PLL
После этого ждем успешного запуска. Тут ожидание реализовано точно так же, как и для HSE:

Код: Выделить всё

  //Ждем успешного запуска или окончания тайм-аута
  for(StartUpCounter=0; ; StartUpCounter++)
  {
    //Если успешно запустилось, то 
    //выходим из цикла
    if(RCC->CR & (1<<RCC_CR_PLLRDY_Pos))
      break;
    
    //Если по каким-то причинам не запустился PLL, то
    //отключаем все, что включили
    //и возвращаем ошибку
    if(StartUpCounter > 0x1000)
    {
      RCC->CR &= ~(1<<RCC_CR_HSEON_Pos); //Останавливаем HSE
      RCC->CR &= ~(1<<RCC_CR_PLLON_Pos); //Останавливаем PLL
      return 2;
    }
  }
После этого настраиваем FLASH и делители:

Код: Выделить всё

  //Устанавливаем 2 цикла ожидания для Flash
  //так как частота ядра у нас будет 48 MHz < SYSCLK <= 72 MHz
  FLASH->ACR |= (0x02<<FLASH_ACR_LATENCY_Pos); 
  
  //Делители
  RCC->CFGR |= (0x00<<RCC_CFGR_PPRE2_Pos) //Делитель шины APB2 отключен (оставляем 0 по умолчанию)
            | (0x04<<RCC_CFGR_PPRE1_Pos) //Делитель нишы APB1 равен 2
            | (0x00<<RCC_CFGR_HPRE_Pos); //Делитель AHB отключен (оставляем 0 по умолчанию)
Ну и торжественный момент переключение на работу от PLL:

Код: Выделить всё

RCC->CFGR |= (0x02<<RCC_CFGR_SW_Pos); //Переключаемся на работу от PLL
Ждем завершения переключения:

Код: Выделить всё

  //Ждем, пока переключимся
  while((RCC->CFGR & RCC_CFGR_SWS_Msk) != (0x02<<RCC_CFGR_SWS_Pos))
  {
  }
Поздравляю! Мы запустились от PLL! После этого можно отключить RC-генератор HSI, так как он нам больше не нужен:

Код: Выделить всё

  //После того, как переключились на
  //внешний источник такирования
  //отключаем внутренний RC-генератор
  //для экономии энергии
  RCC->CR &= ~(1<<RCC_CR_HSION_Pos);
Приведу весь код функции переключения на работу от PLL:

Код: Выделить всё

//Настраиваем тактирование системы от внешнего кварца
//через PLL на саксимально возможных частотах.
//Внешний кварц должен быть на 8МГц
//Возвращает:
//  0 - завершено успешно
//  1 - не запустился кварцевый генератор
//  2 - не запустился PLL
int ClockInit(void)
{
  __IO int StartUpCounter;
  
  ////////////////////////////////////////////////////////////
  //Запускаем кварцевый генератор
  ////////////////////////////////////////////////////////////
  
  RCC->CR |= (1<<RCC_CR_HSEON_Pos); //Запускаем генератор HSE
  
  //Ждем успешного запуска или окончания тайм-аута
  for(StartUpCounter=0; ; StartUpCounter++)
  {
    //Если успешно запустилось, то 
    //выходим из цикла
    if(RCC->CR & (1<<RCC_CR_HSERDY_Pos))
      break;
    
    //Если не запустилось, то
    //отключаем все, что включили
    //и возвращаем ошибку
    if(StartUpCounter > 0x1000)
    {
      RCC->CR &= ~(1<<RCC_CR_HSEON_Pos); //Останавливаем HSE
      return 1;
    }
  }
  
  ////////////////////////////////////////////////////////////
  //Настраиваем и запускаем PLL
  ////////////////////////////////////////////////////////////
  
  //Настраиваем PLL
  RCC->CFGR |= (0x07<<RCC_CFGR_PLLMULL_Pos) //PLL множитель равен 9
            | (0x01<<RCC_CFGR_PLLSRC_Pos); //Тактирование PLL от HSE
  
  
  RCC->CR |= (1<<RCC_CR_PLLON_Pos); //Запускаем PLL
  
  //Ждем успешного запуска или окончания тайм-аута
  for(StartUpCounter=0; ; StartUpCounter++)
  {
    //Если успешно запустилось, то 
    //выходим из цикла
    if(RCC->CR & (1<<RCC_CR_PLLRDY_Pos))
      break;
    
    //Если по каким-то причинам не запустился PLL, то
    //отключаем все, что включили
    //и возвращаем ошибку
    if(StartUpCounter > 0x1000)
    {
      RCC->CR &= ~(1<<RCC_CR_HSEON_Pos); //Останавливаем HSE
      RCC->CR &= ~(1<<RCC_CR_PLLON_Pos); //Останавливаем PLL
      return 2;
    }
  }
  
  ////////////////////////////////////////////////////////////
  //Настраиваем FLASH и делители
  ////////////////////////////////////////////////////////////
  
  //Устанавливаем 2 цикла ожидания для Flash
  //так как частота ядра у нас будет 48 MHz < SYSCLK <= 72 MHz
  FLASH->ACR |= (0x02<<FLASH_ACR_LATENCY_Pos); 
  
  //Делители
  RCC->CFGR |= (0x00<<RCC_CFGR_PPRE2_Pos) //Делитель шины APB2 отключен
            | (0x04<<RCC_CFGR_PPRE1_Pos) //Делитель нишы APB1 равен 2
            | (0x00<<RCC_CFGR_HPRE_Pos); //Делитель AHB отключен
  
  
  RCC->CFGR |= (0x02<<RCC_CFGR_SW_Pos); //Переключаемся на работу от PLL
  
  //Ждем, пока переключимся
  while((RCC->CFGR & RCC_CFGR_SWS_Msk) != (0x02<<RCC_CFGR_SWS_Pos))
  {
  }
  
  //После того, как переключились на
  //внешний источник такирования
  //отключаем внутренний RC-генератор
  //для экономии энергии
  RCC->CR &= ~(1<<RCC_CR_HSION_Pos);
  
  //Настройка и переклбючение сисемы
  //на внешний кварцевый генератор
  //и PLL запершилось успехом.
  //Выходим
  return 0;
}

Ответить