Stm32 таймеры комплиментарные каналы. STM32: базовые таймеры
Режим захвата - это особый режим работы таймера, суть которого в следующем, при изменении логического уровня на определённом выводе микроконтроллера, значение счётного регистра записывается в другой регистр, который именуют регистром захвата.
Для чего это надо?
С помощью этого режима можно измерить длительность импульса или период сигнала.
Режим захвата у STM32 обладает некоторыми особенностями:
- возможность выбрать какой фронт будет активным
- возможность изменить частоту входного сигнала с помощью предделителя (1,2,4,8)
- каждый канал захвата оснащён встроенным входным фильтром
- источником сигнала захвата может служить другой таймер
- для каждого канала предусмотрено по два флага, первый выставляется если произошёл захват, второй если произошёл захват при установленном первом флаге
Для настройки режима захвата предназначены регистры CCMR1 (для 1 и 2 канала) и CCMR2 (для 3 и 4), а также регистры CCER , DIER .
Давайте рассмотрим подробнее битовые поля регистра CCMR2
, отвечающие за настройку 4 канала таймера, именно его мы будем настраивать в примере. Ещё хотелось бы отметить, что в этом же регистре находятся битовые поля, которые используются при настройке таймера в режиме сравнения.
CC4S
- определяет направление работы четвёртого канала(вход или выход). При настройке канала как вход сопоставляет ему сигнал захвата
- 00 - канал работает как выход
- 01 - канал работает как вход, сигнал захвата - TI4
- 10 - канал работает как вход, сигнал захвата - TI3
- 11 - канал работает как вход, сигнал захвата - TRC
- 00 - делитель не используется, сигнал захвата IC1PS формируется по каждому событию
- 01 - сигнал захвата формируется по каждому второму событию
- 10 - сигнал захвата формируется по каждому четвёртому событию
- 11 - сигнал захвата формируется по каждому восьмому событию
Теперь давайте рассмотрим регистр CCER
.
CC4E
- включает/выключает режим захвата.
CC4P
- определяет фронт по которому будет производиться захват, 0 - передний, 1 - задний.
И регистр DIER
.
CC4DE
- разрешает формировать запрос к DMA.
CC4IE
- разрешает прерывание по захвату.
После того как произошёл захват формируется событие захвата, которое устанавливает соответствующий флаг. Это может привести к генерации прерывания и запросу DMA
, если они разрешены в регистре DIER
. Кроме того, событие захвата может быть сформировано программно, установкой битового поля в регистре генерации событий EGR
:
Битовые поля CC1G, CC2G, CC3G и CC4G
позволяют генерировать событие в соответствующем канале захвата/сравнения.
Кстати, CCR1, CCR2, CCR3 и CCR4 - регистры захвата, в которых сохраняется значение таймера по сигналу захвата.
Для того чтобы контролировать формирование сигнала захвата, в регистре SR
для каждого канала выделено по два флага.
CC4IF
- устанавливается когда формируется сигнал захвата, сбрасываются эти флаги программно или чтением соответствующего регистра захвата/сравнения.
CC4OF
- устанавливается если флаг CC4IF не был очищен, а пришёл очередной сигнал захвата. Этот флаг очищается программно записью нуля.
Теперь давайте применим полученные знания на практике, с генератора сигналов на вход TIM5_CH4 подадим синусоиду с частотой 50Гц и попробуем измерить её период. Для того чтобы ускорить процесс предлагаю использовать DMA. Какой вывод МК соответствует 4 каналу TIM5 можно найти в даташите на МК в разделе Pinouts and pin description
.
Для DMA
необходим адрес регистра CCR4
, вот как его найти. Открываем RM0008
и в таблице Register boundary addresses
находим начальный адрес TIM5.
смещение для регистра CCR4 можно найти в том же документе в разделе register map .
#include "stm32f10x.h" #define TIM5_CCR4_Address ((u32)0x40000C00+0x40) #define DMA_BUFF_SIZE 2 uint16_t buff;//Буфер uint16_t volatile T; void DMA2_Channel1_IRQHandler (void) { T = (buff > buff) ? (buff - buff) : (65535+ buff - buff); DMA2->IFCR |= DMA_IFCR_CGIF1; } void Init_DMA(void) { RCC->AHBENR |= RCC_AHBENR_DMA2EN; //Разрешаем тактирование первого DMA модуля DMA2_Channel1->CPAR = TIM5_CCR4_Address; //Указываем адрес периферии - регистр результата преобразования АЦП для регулярных каналов DMA2_Channel1->CMAR = (uint32_t)buff; //Задаем адрес памяти - базовый адрес массива в RAM DMA2_Channel1->CCR &= ~DMA_CCR1_DIR; //Указываем направление передачи данных, из периферии в память DMA2_Channel1->CNDTR = DMA_BUFF_SIZE; //Количество пересылаемых значений DMA2_Channel1->CCR &= ~DMA_CCR1_PINC; //Адрес периферии не инкрементируем после каждой пересылки DMA2_Channel1->CCR |= DMA_CCR1_MINC; //Адрес памяти инкрементируем после каждой пересылки. DMA2_Channel1->CCR |= DMA_CCR1_PSIZE_0; //Размерность данных периферии - 16 бит DMA2_Channel1->CCR |= DMA_CCR1_MSIZE_0; //Размерность данных памяти - 16 бит DMA2_Channel1->CCR |= DMA_CCR1_PL; //Приоритет - очень высокий DMA2_Channel1->CCR |= DMA_CCR1_CIRC; //Разрешаем работу DMA в циклическом режиме DMA2_Channel1->CCR |= DMA_CCR1_TCIE;//Разрешаем прерывание по окончанию передачи DMA2_Channel1->CCR |= DMA_CCR1_EN; //Разрешаем работу 1-го канала DMA } int main(void) { Init_DMA(); //включаем тактирование порта А, альтернативных функций и таймера RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_TIM5EN; TIM5->PSC = 56000-1;//новая частота 1Khz TIM5->CCMR2 |= TIM_CCMR2_CC4S_0;//выбираем TI4 для TIM5_CH4 TIM5->CCMR2 &= ~(TIM_CCMR2_IC4F | TIM_CCMR2_IC4PSC);//не фильтруем и делитель не используем TIM5->CCER &= ~TIM_CCER_CC4P;//выбираем захват по переднему фронту TIM5->CCER |= TIM_CCER_CC4E;//включаем режим захвата для 4-го канала TIM5->DIER |= TIM_DIER_CC4DE;//разрешаем формировать запрос к DMA //TIM5->DIER |= TIM_DIER_CC4IE; //разрешаем прерывание по захвату TIM5->CR1 |= TIM_CR1_CEN; //включаем счётчик //NVIC->ISER |= NVIC_ISER_SETENA_18; //TIM5 Interrupt NVIC->ISER |= NVIC_ISER_SETENA_24; //DMA Interrupt while(1) { } }
В любом современном контроллере есть таймеры
. В этой статье речь пойдёт о простых (базовых) таймерах stm32f4 discovery
.
Это обычные таймеры. Они 16 битные с автоматической перезагрузкой. Кроме того имеется 16 битный программируемый делитель частоты
. Есть возможность генерирования прерывания по переполнению счётчика
и/или запросу DMA.
Приступим. Как и раньше я пользуюсь Eclipse + st-util в ubuntu linux
Первым делом подключаем заголовки:
#include
Ничего нового в этом нет. Если не ясно откуда они берутся либо читайте предыдущие статьи, либо открывайте файл и читайте.
Определим две константы. Одну для обозначения диодов, другую массив из техже диодов:
Const uint16_t LEDS = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; // все диоды const uint16_t LED = {GPIO_Pin_12, GPIO_Pin_13, GPIO_Pin_14, GPIO_Pin_15}; // массив с диодами
Скорее всего уже знакомая вам функция-инициализации периферии (то есть диодов) :
Void init_leds(){ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); // разрешаем тактирование GPIO_InitTypeDef gpio; // структура GPIO_StructInit(&gpio); // заполняем стандартными значениями gpio.GPIO_OType = GPIO_OType_PP; // подтяжка резисторами gpio.GPIO_Mode = GPIO_Mode_OUT; // работаем как выход gpio.GPIO_Pin = LEDS; // все пины диодов GPIO_Init(GPIOD, &gpio);
Функция инициализатор таймера:
Void init_timer(){ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); // включаем тактирование таймера /* Другие параметры структуры TIM_TimeBaseInitTypeDef * не имеют смысла для базовых таймеров. */ TIM_TimeBaseInitTypeDef base_timer; TIM_TimeBaseStructInit(&base_timer); /* Делитель учитывается как TIM_Prescaler + 1, поэтому отнимаем 1 */ base_timer.TIM_Prescaler = 24000 - 1; // делитель 24000 base_timer.TIM_Period = 1000; //период 1000 импульсов TIM_TimeBaseInit(TIM6, &base_timer); /* Разрешаем прерывание по обновлению (в данном случае - * по переполнению) счётчика таймера TIM6. */ TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); TIM_Cmd(TIM6, ENABLE); // Включаем таймер /* Разрешаем обработку прерывания по переполнению счётчика * таймера TIM6. это же прерывание * отвечает и за опустошение ЦАП. */ NVIC_EnableIRQ(TIM6_DAC_IRQn); }
Я прокомментировал код, так-что думаю всё ясно.
Ключевыми параметрами тут являются делитель (TIM_Prescaler) и период (TIM_Period) таймера. Это параметры, которые собственно и настраивают работу таймера.
К примеру, если у вас на STM32F4 DISCOVERY тактовая частота установлена в 48МГц, то на таймерах общего назначения частота 24МГц. Если установить делитель (TIM_Prescaler) в 24000 (частота счёта = 24МГц/24000 = 1КГц), а период (TIM_Period) в 1000, то таймер будет отсчитывать интервал в 1с.
Обратите внимание, что всё зависит от тактовой частоты. Её вы должны выяснить точно.
Так же отмечу, что на высоких частотах переключение светодиода по прерыванию существенно искажает значение частоты. При значении в 1МГц на выходе я получал примерно 250КГц, т.е. разница не приемлима. Такой результат видимо получается из-за затрат времени на выполнение прерывания.
Глобальная переменная - флаг горящего диода:
U16 flag = 0;
Обработчик прерывания, которое генерирует таймер. Т.к. этоже прерывание генерируется и при работе ЦАП, сначала проверяем, что сработало оно именно от таймера:
Void TIM6_DAC_IRQHandler(){ /* Так как этот обработчик вызывается и для ЦАП, нужно проверять, * произошло ли прерывание по переполнению счётчика таймера TIM6. */ if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) { flag++; if (flag>3) flag = 0; /* Очищаем бит обрабатываемого прерывания */ TIM_ClearITPendingBit(TIM6, TIM_IT_Update); GPIO_Write(GPIOD, LED); // зажигаем слудующий диод } }
Функция main:
Int main(){ init_leds(); init_timer(); do { } while(1); }
Цикл оставляем пустым. Счётчик выполняет свою работу асинхронно, а прерывание на то и прерывание, чтобы не зависеть от выполняемой в данный момент операции.
Таймеры в STM32, как в принципе и вся периферия, являются очень навороченными. От обилия разных функций, которые могут выполнять таймеры может даже закружиться голова. Хотя, казалось бы, таймер он на то и таймер, чтобы просто считать. Но на деле все гораздо круче)
Мало того, что таймеры обладают такими широкими возможностями, так их еще несколько у каждого контроллера. И даже не два и не три, а больше! В общем, нахваливать все это можно бесконечно. Давайте уже разбираться, что и как работает. Итак, микроконтроллер STM32F103CB имеет:
- 3 таймера общего назначения (TIM2, TIM3, TIM4)
- 1 более продвинутый таймер с расширенными возможностями (TIM1)
- 2 WDT (WatchDog Timer)
- 1 SysTick Timer
Собственно таймеры общего назначения и таймер TIM1 не сильно отличаются друг от друга, так что ограничимся рассмотрением какого-нибудь одного таймера. К слову я остановил свой выбор на TIM4. Без особой причины, просто так захотелось =). Таймеры имеют 4 независимых канала, которые могут использоваться для:
- Захвата сигнала
- Сравнения
- Генерации ШИМ
- Генерации одиночного импульса
- Переполнение
- Захват сигнала
- Сравнение
- Событие-триггер
При наступлении любого из этих событий таймеры могут генерировать запрос к DMA (DMA – прямой доступ к памяти, уже скоро мы будем разбираться и с ним =)). Теперь немного подробнее о каждом из режимов работы таймеров.
Режим захвата сигнала. Очень удобно при работе таймера в этом режиме измерять период следования импульсов. Смотрите сами: приходит импульс, таймер кладет свое текущее значение счетчика в регистр TIM_CCR. По-быстрому забираем это значение и прячем в какую-нибудь переменную. Сидим, ждем следующий импульс. Опа! Импульс пришел, таймер снова сует значение счетчика в TIM_CCR , и нам остается только вычесть из этого значения то, которое мы предварительно сохранили. Это, наверное, самое простое использование этого режима таймера, но очень полезное. Отлавливать можно как передний фронт импульса, так и задний, так что возможности довольно велики.
Режим сравнения. Тут просто подключаем какой-нибудь канал таймера к соответствующему выводу, и как только таймер досчитает до определенного значения (оно в TIM_CCR ) состояние вывода изменится в зависимости от настройки режима (либо выставится в единицу, либо в ноль, либо изменится на противоположное).
Режим генерации ШИМ. Ну тут все скрыто в названии) В этом режиме таймер генерирует ШИМ! Наверно нет смысла что-то писать тут еще сейчас. Скоро будет примерчик как раз на ШИМ, там и поковыряем поподробнее.
Режим Dead-Time. Суть режима в том, что между сигналами на основном и комплементарном выводах таймера появляется определенная задержка. В интернете есть довольно много информации о том, где это можно и нужно применять.
Ну вот в принципе ооочень кратко об основных режимах работы таймера. Если будут вопросы про другие режимы, более специфические, пишите в Комментарии 😉
Надо бы потихоньку написать программку для работы с таймерами. Но сначала посмотрим, что есть в библиотеке Standard Peripheral Library. Итак, за таймеры несут ответственность файлы – stm32f10x_tim.h и stm32f10x_tim.c . Открываем первый и видим, что структура файла повторяет структуру файла для работы с GPIO, который мы рассматривали в предыдущей статье. Здесь описаны структуры и поля структур, которые нужны для конфигурирования таймеров. Правда здесь уже не одна, а несколько структур (режимов, а соответственно и настроек то у таймеров побольше, чем у портов ввода-вывода). Все поля структур снабжены комментариями, так что не должно тут возникать никаких проблем. Ну вот, например:
uint16_t TIM_OCMode; // Specifies the TIM mode. |
Здесь будем задавать режим работы таймера. А вот еще:
uint16_t TIM_Channel; // Specifies the TIM channel. |
Здесь выбираем канал таймера, ничего неожиданного) В общем все довольно прозрачно, если что спрашивайте =) С первым файлом понятно. А в файле stm32f10x_tim.c – готовые функции для работы с таймерами. Тоже все в целом ясно. Мы уже использовали библиотеку для работы с GPIO, теперь вот работаем с таймерами, и очевидно, что для разной периферии все очень похоже. Так что давайте создавать проект и писать программу.
Итак, запиливаем новый проект, добавляем все необходимые файлы:
Пишем код:
Необходимо отметить, что в поле TIM_Prescaler нужно записывать значение, на единицу меньшее, чем то, которое мы хотим получить.
/****************************timers.c*******************************/ #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" #include "stm32f10x_tim.h" //При таком предделителе у меня получается один тик таймера на 10 мкс #define TIMER_PRESCALER 720 /*******************************************************************/ //Переменная для хранения предыдущего состояния вывода PB0 uint16_t previousState; GPIO_InitTypeDef port; TIM_TimeBaseInitTypeDef timer; /*******************************************************************/ void initAll() { //Включаем тактирование порта GPIOB и таймера TIM4 //Таймер 4 у нас висит на шине APB1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE) ; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE) ; //Тут настраиваем порт PB0 на выход //Подробнее об этом в статье про GPIO GPIO_StructInit(& port) ; port.GPIO_Mode = GPIO_Mode_Out_PP; port.GPIO_Pin = GPIO_Pin_0; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, & port) ; //А тут настройка таймера //Заполняем поля структуры дефолтными значениями TIM_TimeBaseStructInit(& timer) ; //Выставляем предделитель timer.TIM_Prescaler = TIMER_PRESCALER - 1 ; //Тут значение, досчитав до которого таймер сгенерирует прерывание //Кстати это значение мы будем менять в самом прерывании timer.TIM_Period = 50 ; //Инициализируем TIM4 нашими значениями TIM_TimeBaseInit(TIM4, & timer) ; } /*******************************************************************/ int main() { __enable_irq() ; initAll() ; //Настраиваем таймер для генерации прерывания по обновлению (переполнению) TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE) ; //Запускаем таймер TIM_Cmd(TIM4, ENABLE) ; //Разрешаем соответствующее прерывание NVIC_EnableIRQ(TIM4_IRQn) ; while (1 ) { //Бесконечно тупим) Вся полезная работа – в прерывании __NOP() ; } } /*******************************************************************/ //Если на выходе был 0.. timer.TIM_Period = 50 ; TIM_TimeBaseInit(TIM4, & timer) ; //Очищаем бит прерывания TIM_ClearITPendingBit(TIM4, TIM_IT_Update) ; } else { //Выставляем ноль на выходе timer.TIM_Period = 250 ; TIM_TimeBaseInit(TIM4, & timer) ; TIM_ClearITPendingBit(TIM4, TIM_IT_Update) ; } } |
В этой программе мы смотрим, что было на выходе до момента генерации прерывания – если ноль, выставляем единицу на 0.5 мс. Если была единица – ставим ноль на 2.5 мс. Компилируем и запускаем отладку =)
Небольшое, но очень важное отступление… Наш пример, конечно, будет работать и для теста он вполне сгодится, но все-таки в “боевых” программах нужно следить за оптимальностью кода как с точки зрения его объема, так и с точки зрения производительности и расхода памяти. В данном случае нет никакого смысла использовать структуру timer, а также вызывать функцию TIM_TimeBaseInit() каждый раз при смене периода. Правильнее менять всего лишь одно значение в одном регистре, а именно в регистре TIMx->ARR (где х – это номер таймера). В данном примере код трансформируется следующим образом:
/*******************************************************************/ void TIM4_IRQHandler() { //Если на выходе был 0.. if (previousState == 0 ) { //Выставляем единицу на выходе previousState = 1 ; GPIO_SetBits(GPIOB, GPIO_Pin_0) ; //Период 50 тиков таймера, то есть 0.5 мс TIM4-> ARR = 50 ; } else { //Выставляем ноль на выходе previousState = 0 ; GPIO_ResetBits(GPIOB, GPIO_Pin_0) ; //А период теперь будет 250 тиков – 2.5 мс TIM4-> ARR = 250 ; } TIM_ClearITPendingBit(TIM4, TIM_IT_Update) ; } /****************************End of file****************************/ |
Итак, продолжаем, на пути у нас очередные грабли) А именно ошибка:
..\..\..\SPL\src\stm32f10x_tim.c(2870): error: #20: identifier “TIM_CCER_CC4NP” is undefined
Не так страшно как может показаться, идем в файл stm32f10x.h, находим строки
Вот теперь все собирается, можно отлаживать. Включаем логический анализатор. В командной строке пишем: la portb&0x01 и наблюдаем на выходе:
Что хотели, то и получили) Другими словами все работает правильно. В следующей статье поковыряем режим генерации ШИМ, оставайтесь на связи 😉
Не пропустите хорошую статью про таймеры в целом – .
Собственно, поэтому давайте сразу же переходить к программированию. Возьмем любой из базовых таймеров микроконтроллера STM32F3 , произведем его минимальную настройку и попытаемся сгенерировать прерывания через равные промежутки времени. Максимально простой пример 😉
Итак, из Standard Peripheral Library нам понадобятся парочка файлов, в которых реализовано взаимодействие с регистрами таймеров:
#include "stm32f30x_gpio.h" #include "stm32f30x_rcc.h" #include "stm32f30x_tim.h" #include "stm32f30x.h" /*******************************************************************/ TIM_TimeBaseInitTypeDef timer; /*******************************************************************/ |
Минимальная инициализация таймера выглядит следующим образом. Кстати заодно настроим одну из ножек контроллера на работу в режиме выхода. Это нужно всего лишь для того, чтобы мигать светодиодиком 😉
/*******************************************************************/ void initAll() { // Тактирование - куда ж без него RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE, ENABLE) ; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE) ; // На этом выводе у нас синий светодиод (STM32F3Discovery) gpio.GPIO_Mode = GPIO_Mode_OUT; gpio.GPIO_Pin = GPIO_Pin_8; gpio.GPIO_OType = GPIO_OType_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOE, & gpio) ; // А вот и долгожданная настройка таймера TIM2 TIM_TimeBaseStructInit(& timer) ; timer.TIM_Prescaler = 7200 ; timer.TIM_Period = 20000 ; TIM_TimeBaseInit(TIM2, & timer) ; /*******************************************************************/ |
Тут стоит уделить внимание двум непонятно откуда взявшимся числам – 7200 и 20000 . Сейчас разберемся что это 😉 Таймер у меня тактируется частотой 72 МГц . Prescaler, он же предделитель, нужен для того, чтобы эту частоту делить) Таким образом, получаем 72 МГц / 7200 = 10 КГц . Значит один “тик” таймера соответствует (1 / 10000) секунд , что равняется 100 микросекундам. Период таймера – это величина, досчитав до которой программа улетит на обработчик прерывания по переполнению таймера. В нашем случае таймер дотикает до 20000 , что соотвествует (100 * 20000) мкс или 2 секундам. То есть светодиод (который мы зажигаем и гасим в обработчике прерывания) будет мигать с периодом 4 секунды (2 секунды горит, 2 секунды не горит =)). Теперь с этим все понятно, продолжаем…
В функции main() вызываем функцию инициализации, а также включаем прерывания и таймер. В цикле while(1) кода и того меньше – он просто пуст 😉
/*******************************************************************/ int main() { __enable_irq() ; initAll() ; TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE) ; TIM_Cmd(TIM2, ENABLE) ; NVIC_EnableIRQ(TIM2_IRQn) ; while (1 ) { } } /*******************************************************************/ |
Все, осталось написать пару строк для обработчика прерываний, и дело сделано:
/*******************************************************************/ void TIM2_IRQHandler() { TIM_ClearITPendingBit(TIM2, TIM_IT_Update) ; if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_8) == 1 ) { GPIO_ResetBits(GPIOE, GPIO_Pin_8) ; } else { GPIO_SetBits(GPIOE, GPIO_Pin_8) ; } } /*******************************************************************/ |
Прошив программу в контроллер, наблюдаем мигающий синий светодиод, следовательно программа функционирует верно! В принципе на этом все на сегодня, такая вот получилась краткая статейка)
Таймеры — это такая периферия контроллера STM32 позволяющая нам очень точно отсчитывать интервалы времени. Это пожалуй одна из самых важных и наиболее используемых функций, однако есть и другие. Следует начать с того, что в контроллерах STM32 существуют таймеры разной степени крутости. Самые простые это Basic
timers
. Они хороши тем, что очень просто настраиваются и управляются при помощи минимума регистров. Все что они умеют это отсчитывать временные интервалы и генерировать когда таймер дотикает до заданного значения. Следующая группа (general-purpose timers
) гораздо круче первой, они умеют генерировать ШИМ, умеют считать испульсы поступающие на определённые ножки, можно подключать энкодер итд. И самый крутой таймер это advanced-control timer
, думаю что его я использовать не буду еще очень долго так как мне пока без надобности управлять трехфазным электродвигателем. Начать знакомство с таймерами следует с чего попроще, я решил взяться за Basic таймеры. Задача которую я себе поставил: Заставить таймер генерить прерывания каждую секунду.
Первым делом отмечу, что Basic таймеры (TIM6 и TIM7) прицеплены к шине APB1, поэтому в случае если частота тактовых импульсов на ней будет меняться, то и таймеры начнут тикать быстрее или медленнее. Если ничего не менять в настройках тактирования и оставить их по умолчанию, то частота APB1 составляет 24МГц при условии что подключен внешний кварц на частоту 8 МГц. Вообще система тактирования у STM32 очень замысловатая и я попробую про неё нормально написать отдельный пост. А пока просто воспользуемся теми настройками тактирования которые задаются кодом автоматически сгенерированым CooCox’ом. Начать стоит с самого главного регистра — TIMx_CNT (здесь и далее x — номер basic таймера 6 или 7). Это счётный 16-ти битный регистр, занимающийся непосредственно счётом времени. Каждый раз когда с шины APB1 приходит тактовый импульс, содержимое этого регистра увеличивается на едницу. Когда регистр переполняется, все начинается с нуля. При нашей дефолтной частоте шины APB1, таймер за одну секунду тикнет 24 млн раз! Это очень дофига, и поэтому у таймера есть предделитель, управлять которым мы можем при помощи регистра TIMx_PSC . Записав в него значение 24000-1 мы заставим счётный регистр TIMx_CNT увеличивать свое значение каждую милисекунду (Частоту APB1 делим на число в регистре предделителе и получаем сколько раз в секунду увеличивается счётчик). Единицу нужно вычесть потому, что если в регистре ноль то это означает, что включен делитель на единицу. Теперь, когда счётный регистр дотикает до 1000 мы можем точно заявить, что прошла ровно одна секунда! И че теперь опрашивать счётный регистр и ждать пока там появится 1000? Это не наш метод, ведь мы можем заюзать ! Но вот беда, прерывание у нас всего одно, и возникает оно когда счётчик обнулится. Для того чтоб счётчик обнулялся досрочно, а не когда дотикает до 0xFFFF, служит регистр TIMx_ARR . Записываем в него то число до которого должен досчитывать регистр TIMx_CNT перед тем как обнулиться. Если мы хотим чтоб прерывание возникало раз в секунду, то нам нужно записать туда 1000. По части непосредственно отсчёта времени это все, но таймер сам по себе тикать не начнет. Его нужно включить установив бит CEN в регистре TIMx_CR1 . Этот бит разрешает начать отсчёт, соответственно если его сбросить то отсчет остановится (ваш К.О.). В регистре есть и другие биты но они нам не особо интересны. Зато интересен нам еще один бит, но уже в регистре TIMx_DIER . Называется он UIE, установив его мы разрешаем таймеру генерить прерывания при сбросе счётного регистра. Вот собственно и всё, даже не сложней чем в каких-нибудь AVRках. Итак небольше резюме: Чтоб заюзать basic таймер нужно:
- Установить предделитель чтоб таймер не тикал быстро (TIMx_PSC )
- Задать предел до которого таймер должен дотикать перед своим сбросом (TIMx_ARR )
- Включить отсчет битом CEN в регистре TIMx_CR1
- Включить прерывание по переполнению битом UIE в регистре TIMx_DIER
Вот такая нехитрая последовательность. А теперь настало время достать и попробовать в миллионный раз мигнуть этим несчастными светодиодами, но уже с помощью таймера 🙂
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" int main() { GPIO_InitTypeDef PORT; //Включаем порт С и таймер 6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); // Настроим ноги со светодиодами на выход PORT.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_8); PORT.GPIO_Mode = GPIO_Mode_Out_PP; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOC, &PORT); TIM6->PSC = 24000 - 1; // Настраиваем делитель что таймер тикал 1000 раз в секунду TIM6->ARR = 1000 ; // Чтоб прерывание случалось раз в секунду TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания while(1) { //Программа ничего не делает в пустом цикле } } // Обработчик прерывания TIM6_DAC void TIM6_DAC_IRQHandler(void) { TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF GPIOC->ODR^=(GPIO_Pin_9 | GPIO_Pin_8); //Инвертируем состояние светодиодов }
Стоит добавить небольшое примечание к обработчику прерывания. Дело в том, что он у нас используется сразу двумя блоками периферии: таймером 6 и DAC’ом. Это означает, что если вы будете писать программу которая разрешает прерывания от обоих этих периферийных устройств, то в теле обработчика необходимо проверять кто же из них вызвал прерывание. В нашем случае я не стал этого делать, так как ни каких прерываний от DAC возникнуть не может. Он не настроен, а по дефолту прерывания запрещены. В следующий раз рассмотрим general-purpose таймеры и их практическое применение.