- Лоботрясы

Поиск
Перейти к контенту

Главное меню:

Статьи по STM32

Читать в Яндекс.Подписках



    STM32. Таймеры общего назначения на микроконтроллерах Cortex-M0. Часть 2
Просмотров: 9280
     Пример работы, немного о прерываниях и куча Сишного кода...

     В прошлой части у нас было знакомство со структурой таймеров общего назначения и их рабочими регистрами, а вот теперь рассмотрим подробно примеры работы с таймерами (используя CooCox IDE).
     В общем, захотелось мне поиграться с возможностями 32-х битного таймера TIM2, сделав простейший таймер, отсчитывающий время в диапазоне 1 с…60 минут с любым устанавливаемым порогом времени в этом промежутке.
     Немного размышлений: пусть таймер работает с частотой 1 кГц (1000 Гц), тогда, установив в регистр автоперезагрузки TIMx_ARR значение 1000, получу прерывание (при его разрешении, естественно!) раз в секунду, а если запишу 60000 – получу прерывание раз в минуту, даже если запишу в регистр TIMx_ARR число 3600000 (прерывание раз в час!) – он и это осилит, т.к. таймер – 32-х разрядный (максимальное значение – 0xFFFFFFFF или 4294967296 в десятичном представлении).
     А теперь как можно получить все эти настройки. Микроконтроллер тактируется кварцем на 8 МГц, при этом, при создании нового проекта в CooCox, системная частота SYSCLK по умолчанию (в файле system_stm32f0xx_temp.cсоставляет 48 МГц (максимально возможное значение для STM32F031F4P6), а предделители шин AHB и APB1 имеют коэффициент деления «1» (что за магические такие SYSCLK или AHB и APB1, можете почитать в статье про организацию тактирования). Соответственно (да вы просто капитан очевидность!) частота шины APB1 (от которой и тактируется наш таймер TIM2) тоже составляет 48 МГц.
     Чтобы получить частоту работы таймера TIM2 в 1 кГц, необходимо частоту шины APB1 разделить на 48000 (48 МГц/1 кГц), что наш 16-ти разрядный предделитель TIMx_PSC вполне позволяет сделать. Правда, в него нужно будет записать число 47999, т.к. для получения тактовой частоты счётчика в даташите приводится такой вот несложный расчёт:
fCK_CNT= fCK_PSC/ (PSC[15:0] + 1)
где fCK_PSC – тактовая частота до предделителя TIMx_PSC
      PSC – содержимое предделителя TIMx_PSC

     Немного о прерываниях…
     И, раз я решил в программе задействовать прерывание таймера по переполнению, то пройдусь немного и по этой важной функции микроконтроллера. Функции для работы с системой прерываний можно найти в файле core_cm0.(этот файл содержит в себе описание ядра микроконтроллера – Cortex-M0).
     В этом файле есть такая структура, с помощью которой обеспечивается доступ к контроллеру вложенных векторизованных прерываний (или по буржуйски – Nested Vectored Interrupt Controller (NVIC)) – блоку в ядре микроконтроллера STM32 (а впрочем, и всех ARM), обеспечивающему работу системы прерываний.
 
typedef struct
{
  __IO uint32_t   ISER[1]; // Interrupt Set Enable Register          
  __IO uint32_t   ICER[1]; // Interrupt Clear Enable Register         
  __IO uint32_t   ISPR[1]; // Interrupt Set Pending Register          
  __IO uint32_t   ICPR[1]; // Interrupt Clear Pending Register        
  __IO uint32_t   IP[8];    // Interrupt Priority
NVIC_Type;
 
     В принципе, более подробно блок прерываний рассмотрю отдельно и чуть позже, но вот особенности работы с ним затрону уже сейчас (так как уже надо :-)
     Так вот, в файле core_cm0.описаны несколько полезных функций, позволяющих работать с вышеприведенной структурой и, соответственно с регистрами, обеспечивающими работу с блоком прерываний.
void NVIC_EnableIRQ(IRQn_Type   IRQn) – функция разрешения прерывания, где IRQn – номер внешнего прерывания;
void NVIC_DisableIRQ(IRQn_Type   IRQn) а это, естественно, запрет прерывания;
… ещё несколько всяких функций …
void NVIC_SetPriority(IRQn_Type   IRQn, uint32_t    priority) установка приоритета прерывания.

     Во всех этих функциях IRQn  это переменная по шаблону (типу перечисления) IRQn_Type, который объявлен в файле stm32f0xx.h. Эта переменная может принимать один из объявленных в IRQn_Type номеров (векторов) прерываний, которые представлены в виде перечислений: TIM2_IRQn, TIM3_IRQn, IM14_IRQn и т.д. все основные блоки периферии микроконтроллера (АЦП, USARTI2C, внешние прерывания…), имеющие возможность вызова прерывания.
     Поэтому, например, разрешение прерывания по переполнению таймера TIM2 будет выглядеть так:

NVIC_EnableIRQ(TIM2_IRQn);

     В общем, с прерываниями более-менее разобрались (я уж точно!)…
     Ах ну да, самое же главное – процедура обработки прерывания! Для нашего таймера будет иметь такой вид:

void TIM2_IRQHandler (void)
{           
     что-то там важное и необходимое :-)
}
     Откуда я взял что в моём случае именно TIM2_IRQHandler? Взял из таблицы векторов прерываний в файле startup_stm32f0xx.(Боже, чего только в STM32 и по каким файлам не разбросано?!)
 
     В итоге, чтобы запустить таймер и начать с ним работать, потихоньку насобирался такой вот код:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //Включение тактирования таймера TIM2 (подключение к системе тактирования)
NVIC_EnableIRQ(TIM2_IRQn ); // Включение прерывания от TIM2
TIM2->PSC = 47999; // Установка предделителя
TIM2->ARR = 1000; // Установка значения регистра автоперезагрузки (до которого будет вестись счёт)
TIM2->DIER |= TIM_DIER_UIE// Разрешение прерывания по переполнению
TIM2->CR1 |= TIM_CR1_CEN; // Включение тактирования таймера (разрешение подачи тактового сигнала)
 
     И обработчик прерывания для таймера:
void TIM2_IRQHandler(void)
{
   TIM2->SR &=~ TIM_SR_UIF; // Сбрасываем флаг прерывания

}
 
     А вот и наконец сам пример реализации таймера, который я сделал в качестве закрепления изучаемого материала. Программа и схема вполне рабочие и могут быть основой для готового устройства, единственно, я не стал предусматривать исполнительный выход для подачи сигнала на реле или зуммер, т.к. меня интересовало не готовое устройство, а заигрывания с 32-х битным таймером :-) Я намеренно решил усложнить себе задачу, и сделать организацию работы таймера как можно более замысловатой, зато иметь возможность поработать с большим количеством различных битов статуса и настройки (и тем не менее, добиться работоспособности!).
     Схемка:
     Краткие пояснения к схеме и программе работы. Таймер TIM3 используется для организации динамической индикации четырёхразрядного индикатора, обеспечивая прерывание по переполнению счётчика каждые 2 мс. Для настройки таймера используются 2 кнопочки, реализующие 4 функции: выбор редактируемого разряда, установка редактируемого разряда, пуск/пауза таймера и возврат в режим настроек.
     За выполнение первых двух режимов отвечают кратковременные нажатия на кнопочки SB1 и SB2 соответственно, а за выбор 2-х последних режимов отвечают длительные нажатия на те же самые кнопочки. Код работы кнопок (buttons.hможно найти в предложенном на растерзание скачивание проекте, приводить его в примере работы я не стал, т.к. не в нём дело, да и основан он на коде с сайта http://chipenable.ru.
     Благодаря наличию у STM32F050F4P6 (STM32F031F4P6) 32-х разрядного таймера (даже у STM32F103C8 такого нет!), я спокойно получаю значение для регистра автоперезагрузки TIM2, позволяющее вызвать функцию-обработчик прерывания по переполнению даже через час.
     В коде программы за организацию сего безобразия отвечает вот эта строчка, где значения минут и секунд я устанавливаю уже кнопочками (для понимания данной строчки отсылаю к словесной тираде в самом начале сегодняшнего повествования):
TIM2->ARR = ref_minute*60000 + ref_second*1000;
 
     После установки нужного интервала времени и пуска таймера, индикация показаний времени идёт по убывающей до нуля. Как же тогда программа узнаёт, что уже прошла секунда текущего времени, если я сделал срабатывание TIM2 только по истечении заданного времени? Несложно. Просто в коде программы я постоянно проверяю содержимое счётного регистра на предмет счёта очередных 1000 импульсов (частота работы таймера у нас ведь 1000 Гц):
TIM2->CNT% 1000
 
Сам код. Не пугаться, он длинный!
#include <stm32f0xx.h>
#include <stdint.h>
#include <system_stm32f0xx.h>
#include <stm32f0xx_gpio.h>
#include <stm32f0xx_rcc.h>
#include "buttons.h"

unsigned char start_timer=0; // Флаг запуска таймера
unsigned char minute, second=0; // Переменные для определения отсчитанного времени
unsigned char ref_minute=0, ref_second=1; // Переменные для установки времени срабатывания таймера
unsigned char temp_array[4]; // Вспомогательный буфер для формирования переменых времени
unsigned long My_ARR_Value = 1000; //Начальное значение
unsigned int last_CNT; // Последнее сохранённое значение счётчика (чтобы постоянно не попадать в режим счёта секунд)
unsigned char pause; // Флаг постановки таймера на паузу

char push_button; //Переменная для получения кода нажатой кнопки

//************************LED-индикатор**************************
#define SEG_GPIO     GPIOA //Порт управления сегментами
#define RCC_SEG_GPIO   RCC_AHBENR_GPIOAEN //RCC_AHBENR_GPIOBEN, RCC_AHBENR_GPIOCEN... - включение тактирования
#define DIG_GPIO     GPIOA //Порт управления разрядами
#define RCC_DIG_GPIO    RCC_AHBENR_GPIOAEN //RCC_AHBENR_GPIOBEN, RCC_AHBENR_GPIOCEN... - включение тактирования

// Выводы порта, к которому подключены сегменты LED-индикатора
#define SEG_A    0
#define SEG_B    1
#define SEG_C    2
#define SEG_D    3
#define SEG_E    4
#define SEG_F    5
#define SEG_G    6
#define ALL_SEG    ((unsigned int)((1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(1<<SEG_G)))

//Выводы управления разрядами
#define DIG_1    9
#define DIG_2    10
#define DIG_3    13
#define DIG_4    14
#define ALL_DIG    ((unsigned int)((1<<DIG_1)|(1<<DIG_2)|(1<<DIG_3)|(1<<DIG_4)))

#define BLINK_LEVEL   100 // Установка частоты мигания выбираемого разряда

volatile unsigned char *digit_12, *digit_34; // Указатели для организации работы LED-индикаторов
unsigned char next_digit; // Переменная для организации динамической индикации LED-индикаторов
volatile unsigned char digit; // Выбор разряда LED для установки
unsigned char blinking; // Флаг разрешения мигания разряда
unsigned char blink_count; // Счётчик для обеспечения мигания разряда

//Знакогенератор цифр LED-индикатора
static unsigned int LED_array[] = {
(1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(0<<SEG_G), //0
(0<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(0<<SEG_G), //1
(1<<SEG_A)|(1<<SEG_B)|(0<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(0<<SEG_F)|(1<<SEG_G), //2
(1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(1<<SEG_G), //3
(0<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //4
(1<<SEG_A)|(0<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //5
(1<<SEG_A)|(0<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //6
(1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(0<<SEG_D)|(0<<SEG_E)|(0<<SEG_F)|(0<<SEG_G), //7
(1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(1<<SEG_E)|(1<<SEG_F)|(1<<SEG_G), //8
(1<<SEG_A)|(1<<SEG_B)|(1<<SEG_C)|(1<<SEG_D)|(0<<SEG_E)|(1<<SEG_F)|(1<<SEG_G) }; //9

//===============================================================
//                Динамическая индикация (Вставить содержимое в прерывание TIM3)
//===============================================================
void LED_display (void)
{
     DIG_GPIO->ODR &=~ ALL_DIG; // Погасить все индикаторы
     SEG_GPIO->ODR &=~ ALL_SEG;
     switch (next_digit)
     {
          case 0: if(digit!=1)     DIG_GPIO->ODR |= (1<<DIG_1); // Подключаю нужный индикатор
          if((digit==1)&&blinking)     DIG_GPIO->ODR |= (1<<DIG_1);
          SEG_GPIO->ODR |= LED_array[*digit_12/10];     break;
          case 1: if(digit!=2)     DIG_GPIO->ODR |= (1<<DIG_2);
          if((digit==2)&&blinking)     DIG_GPIO->ODR |= (1<<DIG_2);
          SEG_GPIO->ODR |= LED_array[*digit_12%10];     break;
          case 2: if(digit!=3)     DIG_GPIO->ODR |= (1<<DIG_3);
          if((digit==3)&&blinking)     DIG_GPIO->ODR |= (1<<DIG_3);
          SEG_GPIO->ODR |= LED_array[*digit_34/10];     break;
          case 3: if(digit!=4)     DIG_GPIO->ODR |= (1<<DIG_4);
          if((digit==4)&&blinking)     DIG_GPIO->ODR |= (1<<DIG_4);
          SEG_GPIO->ODR |= LED_array[*digit_34%10];     break;
     }
     if (++next_digit >= 5)     next_digit=0;

     if(++blink_count == BLINK_LEVEL)
     {
          blink_count=0;
          blinking ^= 1;
     }
}

//===============================================================
//                                            Инициализация LED-дисплея
//===============================================================
void LED_Init (void)
{
     GPIO_InitTypeDef GPIO_InitStruct;

     RCC->AHBENR |= RCC_SEG_GPIO|RCC_DIG_GPIO; //Включение тактирования портов GPIO
     GPIO_InitStruct.GPIO_Pin = ALL_SEG; //Какие выводы конфигурируем
     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT; //Выход
     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz; //Устанавливаем скорость работы модуля GPIO
     GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; //Двухтактный выход
     GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL; //Без подтягивающих резисторов
     GPIO_Init(SEG_GPIO, &GPIO_InitStruct);
     GPIO_InitStruct.GPIO_Pin = ALL_DIG; // Какие выводы конфигурируем
     GPIO_Init(DIG_GPIO, &GPIO_InitStruct);
}
//************************LED-индикатор**************************
//===============================================================
//                                           Обработчик прерывания по TIM2
//===============================================================
void TIM2_IRQHandler(void)
{
     static char x;
     TIM2->SR &=~ TIM_SR_UIF; // Сбрасываем флаг прерывания

     if(x==1) //Прерывание происходит и при запуске! (при событии обновления счётчика!) Поэтому пропускаем сие прискорбное событие...
     { //В общем, попадаем сюда по окончании счёта нашего таймера! Можете вставить сюда что-нибудь своё (например код автоматического смыва унитаза)
          minute=second=0; // LED-индикатор показывает нули (счёт окончен!)
          start_timer=0;
          TIM2->CR1 &=~ TIM_CR1_CEN; // Выключаем тактирование таймера
     }
     if(x==0)    x=1;
}

//===============================================================
//                                             Обработчик прерывания по TIM3
//===============================================================
void TIM3_IRQHandler(void)
{
     TIM3->SR &=~ TIM_SR_UIF; // Сбрасываем флаг прерывания

     LED_display (); // Динамическая индикация
     BUTTON_Scan(); // Опрос кнопок
}
//===============================================================

int main(void)
{
     // Initialises the system clock
     SystemInit();
     // Инициализация кнопок
     BUTTON_Init();

     // TIM2 - для получения выдержек времени, TIM3 - для организации динамической индикации
     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //Включение тактирования таймера TIM2
     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //Включение тактирования таймера TIM3

     // Инициализация TIM2
     NVIC_EnableIRQ(TIM2_IRQn); // Включение прерывания от TIM2
     TIM2->PSC = 47999;
     TIM2->ARR = My_ARR_Value;
     TIM2->DIER |= TIM_DIER_UIE;  // Разрешение прерывания по переполнению

     // Инициализация TIM3
     NVIC_EnableIRQ(TIM3_IRQn); // Включение прерывания от TIM3
     TIM3->PSC = 4799; // Частота тактирования таймера - 10 кГц
     TIM3->ARR = 20; // Обновление таймера каждые 2 мс
     TIM3->DIER |= TIM_DIER_UIE; // Разрешение прерывания по переполнению
     TIM3->CR1 |= TIM_CR1_CEN; // Включение тактирования таймера

     // Индикаторы показывают текущее значение периода работы таймера
     digit_12 = &ref_minute;
     digit_34 = &ref_second;

     LED_Init(); // Инициализация LED

     while(1)
     {
          push_button = BUTTON_GetKey();
          if(push_button) // Одна из кнопок нажата
          {
               // Настраивать время таймера можно только во время остановки таймера!
               if(!start_timer)
               {
                    temp_array[0] = ref_minute/10; temp_array[1] = ref_minute%10;
                    temp_array[2] = ref_second/10; temp_array[3] = ref_second%10;
                    if(push_button == SELECT)
                    {
                         digit++;
                         if(digit == 5)     digit=0;
                    }

                    if(push_button == ENTER)
                    {
                         switch (digit)
                         {
                              case 1: if(++temp_array[0] >= 6)     temp_array[0] = 0;
                                          break;
                              case 2: if(++temp_array[1] > 9)     temp_array[1] = 0;
                                          break;
                              case 3: if(++temp_array[2] >= 6)     temp_array[2] = 0;
                                          break;
                              case 4: if(++temp_array[3] > 9)     temp_array[3] = 0;
                                          break;
                              default: break;
                         }
                         ref_minute = 10*temp_array[0]+temp_array[1]; ref_second = 10*temp_array[2]+temp_array[3]; //Сохранили настройки
                    }
               }

               if(push_button == START) // Пуск/Стоп
               {
                    if(!start_timer) // Пуск таймера
                    {
                         digit=0;
                         TIM2->CNT = 0;
                         TIM2->ARR = ref_minute*60000 + ref_second*1000; // Обновляю значение регистра автоперезагрузки
                         TIM2->CR1 |= TIM_CR1_CEN; //Включение тактирования таймера TIM2
                         minute = ref_minute; second = ref_second;
                         digit_12 = &minute;
                         digit_34 = &second;
                         start_timer=1;
                    }
                    else // Режим паузы таймера
                    {
                         if(!pause)
                         {
                              pause=1;
                              TIM2->CR1 &=~ TIM_CR1_CEN; // Остановка тактирования
                         }
                         else
                         {
                              pause=0;
                              TIM2->CR1 |= TIM_CR1_CEN; //Включение тактирования таймера TIM2
                         }
                    }
               }

               if(push_button == RESET) // Возврат в режим настроек
               {
                    digit=0;
                    TIM2->CR1 &=~ TIM_CR1_CEN; // Остановка тактирования TIM2
                    start_timer=0;
                    digit_12 = &ref_minute;
                    digit_34 = &ref_second;
               }
          }

          // Постоянно проверяю содержимое счётного регистра TIM2_CNT на предмет окончания очередной секунды
          if ((start_timer)&&(last_CNT!=TIM2->CNT)&&(TIM2->CNT!=0)&&((TIM2->CNT % 1000)==0)) //Если прошла очередная секунда
          {
               last_CNT=TIM2->CNT;
               if(second==0) { second=60; minute--; }
               second--;
          }
     }
}

     Небольшое фото проволочной икебаны процесса (правда, к моей очередной радости, крайний справа сегмент индикатора исдох – ничто не вечно из Китая под Луной). Поставил на паузу отсчёт времени на 3 мин 37 сек. Если разрожусь, то ещё и видео работы скоро выложу.
     В моём тестовом варианте таймера, как видно по схеме, я задействовал выводы PA13 и PA14, являющиеся выводами интерфейса SWD (ну, программируем мы по ним :-)
     Сконфигурировав их в программе в качестве выходов, и записав свой код в микроконтроллер, я был немало удивлён, обнаружив невозможность произвести повторное перепрограммирование своего многострадального микроконтроллера. Притом, что даже попытка программирования из встроенного загрузчика (Bootloader) меня огорчила ещё больше (видать, где перестарался с конфигурацией). Сразу вспомнились AVR-ры, где сделав вывод RESET рабочей лапкой (пощаманив с фьюзами), без параллельного программатора хрен уже перепрограммируешь микроконтроллеры. Но поиск в интернете сразу же помог мне вздохнуть спокойно, о чём спешу поделиться и с вами (попутно обновив этой крайне нужной информацией предыдущую статью про работу с портами!).
     Ну так вот, чтобы заставить прохлаждающийся STM32 снова работать вам во благо, необходимо воспользоваться программой STM32 ST-LINK Utility (которая обеспечивает работу программатора/отладчика ST-Link и которую вы скорее всего уже скачали ранее).
     Открыв программу (и подключив, естественно, программатор ST-Link с программируемым микроконтроллером), зайдите в меню Target >  Settingsи выберите в окошечке Mode режим Connect Under Reset
     После этого клацните мышью OK и нажмите (ручками!) на плате с микроконтроллером кнопочку RESET и отпустите её. Ура, микроконтроллер подключён! А дальше, я думаю, вы уже разберётесь что делать (стирать FLASH-память или загружать новую прошивку).
 
     А обещанный обзор библиотеки TIM среды CooCox оставлю на третью часть, ибо Бобик сдох…
Also to be continued…


     Скачать:

Опубликовано 7.02.2015
©Igoryosha, 2015
 
 
Назад к содержимому | Назад к главному меню