
Могут возникнуть ситуации, когда по той или иной причине нет возможности установить ранее заложенный в проект вид кварцевого резонатора определённой частоты, или же ситуации, когда происходит отказ кварцевого резонатора. Программист встраиваемых систем может предусмотреть развитие событий таким образом. На примере контроллера STM32F205RBT6 разработаем/напишем алгоритм определения установленного на плату кварцевого резонатора:
1) проверка подключения внешнего кварцевого резонатора/генератора;
Если не подключен:
2) инициализируем контроллер на работу от HSI с PLL;
Конец.
Если подключен:
2) инициализируем работу контроллера на HSI без PLL;
3) инициализируем LSI;
4) настраиваем TIM5 с тактированием от HSI в режиме „Input Capture mode”;
5) определяем отношение частоты HSI/LSI;
6) инициализируем работу контроллера на HSE без PLL;
7) определяем отношение частоты HSE/LSI;
8) определяем отношение частот HSI/LSI к HSE/LSI и делаем вывод какой частоты кварц подключен;
Если частота не распознана:
9) инициализируем HSI кварц под необходимую частоту с PLL.
Если частота распознана:
9) инициализируем HSE кварц под необходимую частоту с PLL.
Выбранный контроллер имеет две внутренние RC-цепочки: LSI (Low Speed Internal) и HSI (High Speed Internal). Точность выставления частоты у встроенных резонаторов, особенно у низкочастотного, оставляет желать лучшего. На точность сильно влияет температура внутри кристалла, поэтому при использовании несинхронных цифровых интерфейсов передачи данных, например, таких как CAN, может возникнуть ситуация ухода частоты (вплоть до 8% [на деле больше]), вследствие чего, принять данные будет проблематично. Поэтому их использование, в условиях изменения температуры внешней среды, не желательно. Однако, мы ими воспользуемся для анализа частоты подключенного внешнего резонатора.


Первым делом, стоит определить установлен/исправен ли резонатор:
bool isOscilatorInWorkingOrder(){ RCC->CR |= RCC_CR_HSEON; for (uint32_t i = 0; i < 10000; i++){ if(RCC->CR & RCC_CR_HSERDY){ return true; } } RCC->CR &= ~ RCC_CR_HSEON; return false; }
После выставления бита RCC_CR_HSEON включения внешнего кварца в регистре RCC_CR, необходимо подождать некоторое количество времени для его стабилизации. После того, как контроллер получит несколько тактов от кварца, выставляется бит RCC_CR_HSERDY, функция возвращает true и оставляет кварц включённым, в противном случае отключает HSE кварц и возвращает false.
Код данного характера, но для разных источников тактирования, будет выполняться некоторое количество раз, преобразую функцию к следующему виду:
bool isOscilatorInWorkingOrder(__IO uint32_t* oscReg, uint32_t oscOnBitMsk, uint32_t oscRdyBitMsk){ *oscReg |= oscOnBitMsk; for (uint32_t i = 0; i < 10000; i++){ if(*oscReg & oscRdyBitMsk){ return true; } } *oscReg &= ~oscOnBitMsk; return false; }
Так как заранее неизвестна частота тактирования низкочастотного резонатора и приблизительно понятно на какой работает высокочастотный, то следующим этапом будет определение отношения их частот. Для этого воспользуемся таймером (TIM5). Данный таймер выбран по той причине, что он единственный из представленных в контроллере, который может выбрать в качестве триггера срабатывания прерывания фронт сигнала от генератора LSI.

Проведём инициализацию таймера:
void timerInit(void){ RCC->APB1ENR |= RCC_APB1ENR_TIM5EN; // Включаем тактирование таймера TIM5->CCMR2 = (TIM5->CCMR2 & (~TIM_CCMR2_CC4S)) | TIM_CCMR2_CC4S_0; // Конфигурируем 4 канал как вход, IC4 маппим к TI4 TIM5->CCMR2 &= ~TIM_CCMR2_IC4F; // фильтр захвата = 0 TIM5->CCMR2 &= ~TIM_CCMR2_IC4PSC; // предделитель захвата = 0 TIM5->OR = TIM_OR_TI4_RMP_0; //через данный регистр маппим TI4 к тактирующему генератору LSI TIM5->CCER &= ~(TIM_CCER_CC4NP | TIM_CCER_CC4P); // захватываем по нарастающему фронту TIM5->CCER |= TIM_CCER_CC4E; // Включаем захват, TIM5->ARR = 0xFFFFFFFF; //выставляем максимальное разрешение счёта таймера TIM5->CR1 |= TIM_CR1_CEN; // Разрешаем счёт тиков таймером NVIC_EnableIRQ(TIM5_IRQn); //Разрешаем глобальное прерывание таймера }
Тактирование включили, счёт тиков одобрили, глобальное прерывание разрешили, однако прерывание пока ещё не произойдёт, не установили событие. Это сделаем чуть позже, сейчас напишем обработчик прерывания:
void hseCheckingTim5IRQHandler(void){ if(hseCheckingFlag){ //данное условие используется, чтобы задействовать данный обработчик только для этих целей static uint8_t interruptsCounter = 0; //счётчик количества прерываний interruptsCounter++; actCCR = TIM5->CCR4; //запоминаем количество тиков на котором произошло прерывание if(interruptsCounter == 100){ //после некоторого количества срабатываний прерываний рассчитываем период LSI в тиках основного генератора if(periodCheckingStage == hsiPeriodCheckingStage){ hsiToLsiPeriod = actCCR - prevCCR; } else { hseToLsiPeriod = actCCR - prevCCR; } lsiPeriodCounted = true; // устанавливаем флаг рассчитанного периода TIM5->DIER &= ~TIM_DIER_CC4IE; // отключаем прерывание по захвату interruptsCounter = 0; // } prevCCR = actCCR; //запоминаем количество тиков на котором произошло уже обработанное прерывание } }
Написанную функцию обработчика разместим в функции из вектора прерываний для таймера 5:
void TIM5_IRQHandler(){ hseCheckingTim5IRQHandler(); }
Определим отношение периодов генератора LSI в тиках HSI к HSE и вернём значение enum, какой частоты генератор подключен:
uint8_t checkHseFreq(void){ uint16_t hsePeriodPlusDefAccur = (hseToLsiPeriod + (hseToLsiPeriod / 100 * defaultHsiAccurancy)); uint16_t hsePeriodMinusDefAccur = (hseToLsiPeriod - (hseToLsiPeriod / 100 * defaultHsiAccurancy)); if((hsiToLsiPeriod <= hsePeriodPlusDefAccur) && (hsiToLsiPeriod >= hsePeriodMinusDefAccur)){ return HSE_FREQ_MHZ_16; } else if (((hsiToLsiPeriod / 2) <= hsePeriodPlusDefAccur) && ((hsiToLsiPeriod / 2) >= hsePeriodMinusDefAccur)) { return HSE_FREQ_MHZ_8; } else if (((hsiToLsiPeriod / 4) <= hsePeriodPlusDefAccur) && ((hsiToLsiPeriod / 4) >= hsePeriodMinusDefAccur)) { return HSE_FREQ_MHZ_4; } else { return HSE_FREQ_ISNT_RECOGNIZED; } }
Далее остаётся дело за малым, напишем основную функцию, которую будем вызывать:
uint8_t getHseFreq(void){ hseCheckingFlag = true; // Устанавливаем флаг проверки внешнего генератора if(!isOscilatorInWorkingOrder(HSE_OSCILATOR)){ // проверяем его наличие return HSE_ISNT_ENABLE; } rccReset(); //Делаем ресет основных регистров модуля RCC isOscilatorInWorkingOrder(HSI_OSCILATOR); // включаем тактирование HSI генератора isOscilatorInWorkingOrder(LSI_OSCILATOR); // включаем тактирование LSI генератора timerInit(); // инициализируем таймер lsiPeriodCounted = false; //сбрасываем флаг periodCheckingStage = hsiPeriodCheckingStage; //указываем какой вид генератора проверяем в данный момент TIM5->DIER |= TIM_DIER_CC4IE; //включаем прерывание таймера по захвату while(!lsiPeriodCounte){}; //ожидаем подсчёта периода if(!isOscilatorInWorkingOrder(HSE_OSCILATOR)){ return HSE_ISNT_ENABLE; } RCC->CFGR |= RCC_CFGR_SW_HSE; // включаем тактирование от HSE while ((RCC->CFGR & RCC_CFGR_SWS_HSE) != RCC_CFGR_SWS_HSE) {}; //Ждём стабилизации HSE lsiPeriodCounted = false; periodCheckingStage = hsePeriodCheckingStage; //указываем какой вид генератора проверяем в данный момент TIM5->DIER |= TIM_DIER_CC4IE; //включаем прерывание таймера по захвату while(!lsiPeriodCounted){}; //ожидаем подсчёта периода hseCheckingFlag = false; // сбрасываем флаг проверки внешнего генератора RCC->APB1RSTR = RCC_APB1RSTR_TIM5RST; //приводим таймер в состояние ресета return checkHseFreq(); // возвращаем enum значение частоты подключенного генаратора }
Последнее, что необходимо сделать - проинициализировать тактирование контроллера от генератора на необходимую частоту, что решается в каждом случае индивидуально.
Таким образом, полученный алгоритм проверки подключенного осциллятора сможет определить его исправность и частоту работы, а в случае отказа поможет завестись контроллеру от внутреннего источника тактирования.
Исходный код программы без большого количества комментариев к нему можно посмотреть на моём GitHub.
