Могут возникнуть ситуации, когда по той или иной причине нет возможности установить ранее заложенный в проект вид кварцевого резонатора определённой частоты, или же ситуации, когда происходит отказ кварцевого резонатора. Программист встраиваемых систем может предусмотреть развитие событий таким образом. На примере контроллера 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.
Автор: Станислав Змитрович