THE BELL

Есть те, кто прочитали эту новость раньше вас.
Подпишитесь, чтобы получать статьи свежими.
Email
Имя
Фамилия
Как вы хотите читать The Bell
Без спама

Режим захвата - это особый режим работы таймера, суть которого в следующем, при изменении логического уровня на определённом выводе микроконтроллера, значение счётного регистра записывается в другой регистр, который именуют регистром захвата.

Для чего это надо?
С помощью этого режима можно измерить длительность импульса или период сигнала.

Режим захвата у STM32 обладает некоторыми особенностями:

  • возможность выбрать какой фронт будет активным
  • возможность изменить частоту входного сигнала с помощью предделителя (1,2,4,8)
  • каждый канал захвата оснащён встроенным входным фильтром
  • источником сигнала захвата может служить другой таймер
  • для каждого канала предусмотрено по два флага, первый выставляется если произошёл захват, второй если произошёл захват при установленном первом флаге

Для настройки режима захвата предназначены регистры CCMR1 (для 1 и 2 канала) и CCMR2 (для 3 и 4), а также регистры CCER , DIER .

Давайте рассмотрим подробнее битовые поля регистра CCMR2 , отвечающие за настройку 4 канала таймера, именно его мы будем настраивать в примере. Ещё хотелось бы отметить, что в этом же регистре находятся битовые поля, которые используются при настройке таймера в режиме сравнения.

CC4S - определяет направление работы четвёртого канала(вход или выход). При настройке канала как вход сопоставляет ему сигнал захвата

  • 00 - канал работает как выход
  • 01 - канал работает как вход, сигнал захвата - TI4
  • 10 - канал работает как вход, сигнал захвата - TI3
  • 11 - канал работает как вход, сигнал захвата - TRC
IC4PSC – определяют коэффициент деления, для сигнала захвата
  • 00 - делитель не используется, сигнал захвата IC1PS формируется по каждому событию
  • 01 - сигнал захвата формируется по каждому второму событию
  • 10 - сигнал захвата формируется по каждому четвёртому событию
  • 11 - сигнал захвата формируется по каждому восьмому событию
IC4F - предназначен для настройки входного фильтра, кроме количества выборок, в течение которых микроконтроллер не будет реагировать на входные сигналы, также можно настроить частоту выборок. По сути мы настраиваем время задержки с момента прихода фронта до "подтверждающей" выборки.

Теперь давайте рассмотрим регистр 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) { } }

Таймеры в 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 таймер нужно:

  1. Установить предделитель чтоб таймер не тикал быстро (TIMx_PSC )
  2. Задать предел до которого таймер должен дотикать перед своим сбросом (TIMx_ARR )
  3. Включить отсчет битом CEN в регистре TIMx_CR1
  4. Включить прерывание по переполнению битом 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 таймеры и их практическое применение.

В STM32 есть множество очень удобных и гибких в настройке таймеров. Даже у самого младшего микроконтроллера (STM32F030F4P6) есть 4 таких таймера.

8. Настроим проект - добавим нужные файлы

Чтобы использовать таймер, нам потребуется подключить файл библиотеки периферии stm32f10x_tim.c. Точно так же, правой кнопкой щёлкаем в Workspace (окно слева) по группе StdPeriphLib, Add –> Add files, файл LibrariesSTM32F10x_StdPeriph_Driversrcstm32f10x_tim.c.

Ещё нужно включить использование заголовка к этому файлу. Открываем stm32f10x_conf.h (правой кнопкой по названию этого файла в коде, «Open stm32f10x_conf.h». Раскомментируем строчку #include «stm32f10x_tim.h».

9. Добавим таймер

Задержка пустым циклом - это кощунство, тем более на таком мощном кристалле как STM32, с кучей таймеров. Поэтому сделаем эту задержку с помощью таймера.

В STM32 есть разные таймеры, отличающиеся набором свойств. Самые простые - Basic timers, посложнее - General purpose timers, и самые сложные - Advanced timers. Простые таймеры ограничиваются просто отсчётом тактов. В более сложных таймерах появляется ШИМ. Самые сложные таймеры, к примеру, могут сгенерировать 3–фазный ШИМ с прямыми и инверсными выходами и дедтаймом. Нам хватит и простого таймера, под номером 6.

Немного теории

Всё, что нам требуется от таймера - досчитывать до определённого значения и генерировать прерывание (да, мы ещё и научимся использовать прерывания). Таймер TIM6 тактируется от системной шины, но не напрямую а через прескалер - простой программируемый счётчик–делитель (подумать только, в СССР выпускались специальные микросхемы–счётчики, причём программируемые были особым дефицитом - а теперь я говорю о таком счётчике просто между делом). Прескалер можно настраивать на любое значение от 1 (т.е. на счётчик попадёт полная частота шины, 24МГц) до 65536 (т.е. 366 Гц).

Тактовые сигналы в свою очередь, увеличивают внутренний счётчик таймера, начиная с нуля. Как только значение счётчика доходит до значения ARR - счётчик переполняется, и возникает соответствующее событие. По наступлению этого события таймер снова загружает 0 в счётчик, и начинает считать с нуля. Одновременно он может вызвать прерывание (если оно настроено).

На самом деле процесс немного сложнее: есть два регистра ARR - внешний и внутренний. Во время счёта текущее значение сравнивается именно со внутренним регистром, и лишь при переполнении внутренний обновляется из внешнего. Таким образом, можно безопасно менять ARR во время работы таймера - в любой момент.

Код

Код будет очень похож на предыдущий, т.к. инициализация всей периферии происходит однотипно - за тем лишь исключением, что таймер TIM6 висит на шине APB1. Поэтому включение таймера: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

Теперь заводим структуру типа TIM_TimeBaseInitTypeDef, инициализируем её (TIM_TimeBaseStructInit), настраиваем, передаём её в функцию инициализации таймера (TIM_TimeBaseInit) и наконец включаем таймер (TIM_Cmd).

TIM_TimeBaseInitTypeDef TIM_InitStructure; // Заводим структуру TIM_TimeBaseStructInit(&TIM_InitStructure); // Инициализация структуры TIM_InitStructure.TIM_Prescaler = 24000; // Предделитель TIM_InitStructure.TIM_Period = 1000; // Период таймера TIM_TimeBaseInit(TIM6, &TIM_InitStructure); // Функция настройки таймера TIM_Cmd(TIM6, ENABLE); // Включение таймера

Что за магические числа? Как мы помним, на шине присутствует тактовая частота 24МГц (при наших настройках проекта). Настроив предделитель таймера на 24000, мы поделим эту частоту на 24 тысячи, и получим 1кГц. Именно такая частота попадёт на вход счётчика таймера.

Значение же в счётчике - 1000. Значит, счётчик переполнится за 1000 тактов, т.е. ровно за 1 секунду.

После этого у нас действительно появляется работающий таймер. Но это ещё не всё.

10. Разберёмся с прерываниями

Окей, прерывания. Для меня когда–то (во времена PIC) они были тёмным лесом, и я старался вообще их не использовать - да и не умел, на самом деле. Однако, в них заключена сила, игнорировать которую вообще недостойно. Правда, прерывания в STM32 - ещё более сложная штука, особенно механизм их вытеснения; но об этом позже.

Как мы заметили раньше, таймер генерирует прерывание в момент переполнения счётчика - если включена вообще обработка прерываний этого прибора, конкретно это прерывание включено и сброшено предыдущее такое же. Анализируя эту фразу, понимаем что нам нужно:

  1. Включить вообще прерывания таймера TIM6;
  2. Включить прерывание таймера TIM6 на переполнение счётчика;
  3. Написать процедуру–обработчик прерывания;
  4. После обработки прерывания сбросить его.

Включение прерываний

Честно говоря, тут вообще ничего сложного. Первым делом включаем прерывания TIM6: NVIC_EnableIRQ(TIM6_DAC_IRQn); Почему такое название? Потому что в ядре STM32 прерывания от TIM6 и от ЦАП имеют одинаковый номер. Не знаю, почему так сделано - экономия, нехватка номеров или просто какая–то наследная штука - в любом случае, никаких проблем это не принесёт, потому что в этом проекте не используется ЦАП. Даже если в нашем проекте использовался бы ЦАП - мы могли бы при входе в прерывание узнавать, кто конкретно его вызвал. Практически все другие таймеры имеют единоличное прерывание.

Настройка события–источника прерываний: TIM_ITConfig(TIM6, TIM_DIER_UIE, ENABLE); - включаем прерывание таймера TIM6 по событию TIM_DIER_UIE, т.е. событие обновления значения ARR. Как мы помним из картинки, это происходит одновременно с переполнением счётчика - так что это именно то событие, которое нам нужно.

На текущий момент код таймерных дел таков:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_TimeBaseStructInit(&TIM_InitStructure); TIM_InitStructure.TIM_Prescaler = 24000; TIM_InitStructure.TIM_Period = 1000; TIM_TimeBaseInit(TIM6, &TIM_InitStructure); TIM_Cmd(TIM6, ENABLE); NVIC_EnableIRQ(TIM6_DAC_IRQn); TIM_ITConfig(TIM6, TIM_DIER_UIE, ENABLE);

Обработка прерываний

Сейчас запускать проект нельзя - первое же прерывание от таймера не найдёт свой обработчик, и контроллер повиснет (точнее, попадёт в обработчик HARD_FAULT, что по сути одно и то же). Нужно его написать.

Немного теории

Он должен иметь совершенно определённое имя, void TIM6_DAC_IRQHandler(void). Это имя, так называемый вектор прерывания, описано в файле startup (в нашем проекте это startup_stm32f10x_md_vl.s - можете сами увидеть, 126 строка). На самом деле вектор - это адрес обработчика прерывания, и при возникновении прерывания ядро ARM лезет в начальную область (в которую транслирован файл startup - т.е. его местоположение задано совершенно жёстко, в самом начале флеш–памяти), ищет там вектор и переходит в нужное место кода.

Проверка события

Первое что мы должны сделать при входе в такой обработчик - проверить, какое событие вызвало прерывание. Сейчас у нас всего одно событие, а в реальном проекте на одном таймере вполне могут быть несколько событий. Поэтому проверяем событие, и выполняем соответствующий код.

В нашей программе эта проверка будет выглядеть так: if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) - всё понятно, функция TIM_GetITStatus проверяет наличие указанного события у таймера, и возвращает 0 или 1.

Очистка флага UIF

Второй шаг - очистка флага прерывания. Вернитесь к картинке: самый последний график UIF это и есть флаг прерывания. Если его не очистить, следующее прерывание не сможет вызваться, и контроллер опять упадёт в HARD_FAULT (да что же такое!).

Выполнение действий в прерывании

Будем просто переключать состояние светодиода, как и в первой программе. Разница в том, что теперь наша программа делает это более сложно! На самом деле, так писать гораздо правильнее.

If(state) GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET); else GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); state = 1 - state;

Используем глобальную переменную int state=0;

11. Весь код проекта с таймером

#include "stm32f10x_conf.h" int state=0; void TIM6_DAC_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); if(state) GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET); else GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); state = 1 - state; } } void main() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_StructInit(&GPIO_InitStructure); GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_TimeBaseStructInit(&TIM_InitStructure); TIM_InitStructure.TIM_Prescaler = 24000; TIM_InitStructure.TIM_Period = 1000; TIM_TimeBaseInit(TIM6, &TIM_InitStructure); TIM_Cmd(TIM6, ENABLE); NVIC_EnableIRQ(TIM6_DAC_IRQn); TIM_ITConfig(TIM6, TIM_DIER_UIE, ENABLE); while(1) { } }

Архив с проектом таймера.

Ну и к слову, таймер умеет переключать ногу и сам, без прерываний и ручной обработки. Это будет наш третий проект.

Весь цикл:

1. Порты ввода–вывода

2. Таймер и прерывания

3. Выходы таймера

4. Внешние прерывания и NVIC

5. Ставим FreeRTOS

Post Views: 235

THE BELL

Есть те, кто прочитали эту новость раньше вас.
Подпишитесь, чтобы получать статьи свежими.
Email
Имя
Фамилия
Как вы хотите читать The Bell
Без спама