Реклама ⓘ
Главная » Микроконтроллеры
Призовой фонд
на апрель 2024 г.
1. 100 руб.
От пользователей

Похожие статьи:


Реклама ⓘ

IAR и STM32 CORTEX M0. Часть 0x06, Таймеры, ШИМ, прерывания

Таймеры и прерывания — базовые вещи, их раскурку нужно начинать как можно раньше, потому что без первых невозможно делать адекватные паузы, а без вторых программа быстро превращается в неповоротливое создание, зависающее в бессмысленных циклах, когда можно было бы делать что-то полезное. В данной статье будем рассматривать, в основном, TIM3, потому что это единственный таймер, у которого доступно оба канала CH1 и CH2 (будет полезно при подключении энкодера).

1. Подключаем библиотеку USART...
Какой такой USART, спросите вы? Ну обыкновенный... Вы же как-то прошивали микроконтроллер. Как нет? В прошлых статьях точно такое было! Значит, у вас есть уже связь мк с компьютером. Так почему бы не использовать это для наглядности наших экспериментов, чтобы на экран выводилась всякая полезная инфа? Ничего страшного, мы USART еще не проходили, но пока можно рассматривать этот интерфейс, как черный ящик, вам даже подключать ничего не придется, после прошивки достаточно будет убрать перемычку с boot0 и запустить любую терминальную программу. Во всех подробностях мы рассмотрим этот интерфейс в следующей статье, а пока просто подключим библиотеку (в конце статьи архив со всем необходимым).

Создаем проект, как обычно, в папке src создаем папку lib, куда закидываем usart.c. Заголовочные файлы def.h (для работы с портами) и usart.h помещаем в папочку inc. После этого добавляем файлы в проект, в результате должно получится дерево там такое... и мужик в пиджаке

Протестировать очень просто. Напишите простейшую программу:

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"


void main(void) {
  
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
 
 usart1_init(833); //Baudrate 9600
 usart1_tx_init();
 usart1_tx_str("########################\r\n");
 usart1_tx_str("#Just any test from cxem.net#\r\n");
 usart1_tx_str("#########################");
 
 while (1);
 
}

После компиляции и прошивки запустите терминальную программу. Теперь подключитесь к нужному COM-порту (тому самому, что вы использовали в программе Flash Loader при прошивке), проверьте, что перемычка с Boot0 на вашей тестовой плате снята, нажмите reset, в результате должна появиться такая вот надпись:

Получилось? Отлично, идем дальше!

2. Инициализация таймера
Начинаем, как обычно, с тактования. Открываем уже знакомую нам блок-схему из даташита — Block diagram:

Видно, что тактование идет от двух шин: AHB и APB. Мы уже разобрались с шиной AHB в прошлой статье:

RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

...а вот с APB все немного сложнее, хоть и похоже. Начинаем в IAR набирать RCC->APB

И вот вопрос, APB1ENR или APB2ENR? Открываем заголовочный файл stm32f0xx.h, находим секцию Bit definition for RCC_APB...

Видно, что таймер TIM3 подключается к APB1, потому:

RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;

Все, тактование таймера есть, осталось немного его настроить и подключить прерывания.

Поскольку частота камня STM32F030 ни через какие делители до сих пор не проходила, то она равна 8 МГц. Делитель на таймере настроим таким образом, чтобы период был равен 1 мс. Для этого 8 000 000 надо поделить на 8000. Поскольку 0 у нас — это "положительное" число, т.е. отсчет ведется с него, то делитель равен (8000 - 1) = 7999

 TIM3->PSC = 7999;

Пусть у нас прерывание будет каждую секунду, тогда нужно, чтобы в регистре ARR (отвечает, до скольки считать таймеру) было 1000. Поскольку отсчет идет с 0, то на единицу меньше — 999

 TIM3->ARR = 999;

Теперь разрешим прерывания по переполнению счетчика и запустим сам таймер:

 TIM3->DIER |= TIM_DIER_UIE;
 TIM3->CR1 |= TIM_CR1_CEN;

И вот, прежде чем двигаться дальше, неплохо бы сделать краткое отступление — как эти самые прерывания запускать и как их использовать.

3. Прерывания
Не буду растекаться мыслью по древу, для чего нужны прерывания и почему они важны, а начну сразу с того, что по умолчанию прерывания включены глобально. То есть, при желании можно отключить их все командой __disable_irq(). Включить обратно — __enable_irq().

Хотя прерывания и включены глобально, для каждого интерфейса их нужно разрешать отдельно. Делается это при помощи команды NVIC_EnableIRQ(). В качестве аргумента функция принимает номер прерывания, которое соответствует интерфейсу. Номера эти можно посмотреть все в том же файле stm32f0xx.h:

Номер прерывания таймера TIM3 объявлен константой TIM3_IRQn. Теперь мы знаем, как разрешить прерывания для TIM3:

 NVIC_EnableIRQ(TIM3_IRQn);

Но и это не все. Следующий уровень — разрешение прерываний на уровне интерфейса (в данном случае, регистр TIM3_DIER). Так, за прерывания по обновлению (переполнению) счетчика отвечает бит UIE (Update Interrupt Enable). Аналогично, битами BIE, TIE (Break, Trigger) и т.д. разрешаются при необходимости остальные прерывания:

Мы чуть ранее условились, что прерывания будем ловить по переполнению счетчика, потому команда и была такой:

TIM3->DIER |= TIM_DIER_UIE;

Теперь рассмотрим обработчики — что делать-то будем по прерыванию? Соответствующие функции объявлены в файле startup_stm32f0xx.s (папка startup), секция External Interrupts:

Таким образом, обработка прерывания будет проходить внутри функции void TIM3_IRQHandler (void):

void TIM3_IRQHandler (void) {
  if ((TIM3->SR & TIM_SR_UIF) == TIM_SR_UIF) {  /* (1) */
    TIM3->SR &= ~TIM_SR_UIF;                    /* (2) */
    usart_tx_num(timer_sec++, 4);               /* (3) */
    usart1_tx_str("\r\n");                      
  }
}

Сначала мы проверяем (1), что прерывание произошло по событию переполнения счетчика, за это отвечает флаг Update Interrupt Flag (UIF), все флаги прерываний можно посмотреть в RM, либо том же stm32f0xx.h (секция Bit definition for TIM_SR register). Далее нужно этот флаг сбросить (2), иначе при выходе из обработчика прерываний мы тут же попадем туда снова. Ну и пусть у нас результат выводится на терминал по USART (3). Если мы все сделаем правильно, то каждую секунду на экран будет выводиться обновленное содержимое переменной timer_sec.

Прерывания — весьма обширная тема, там вам и приоритеты, и прочий фарш, который мы обязательно разберем... Как-нибудь потом... А пока хватит, возвращаемся к таймерам.

4. Простейший пример работы
Итак, мы получили минимально работающий код, подведем промежуточный итог:

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"

volatile int timer_sec;

/* Обработка прерываний таймера TIM3*/
/* (1) Проверка флага UIF */
/* (2) Сброс флага UIF */
/* (3) Вывести в терминал переменную и увеличить ее на 1 */
/* (4) Вывести в терминал перенос строки */

void TIM3_IRQHandler (void) {
  if ((TIM3->SR & TIM_SR_UIF) == TIM_SR_UIF) { /* (1) */
    TIM3->SR &= ~TIM_SR_UIF;                   /* (2) */
    usart_tx_num(timer_sec++, 4);              /* (3) */
    usart1_tx_str("\r\n");                     /* (4) */
  }
}

void main(void) {
  
 /* (1) Подключаем тактование GPIOA */
 /* (2) Подключаем тактование таймера TIM3 */
 /* (3) Инициализация USART (baudrate 9600) */
 /* (4) Инициализация передачи данных (TX) */
 /* (5) Задаем предделитель таймера */
 /* (6) Указываем, до скольки будет таймер тикать */
 /* (7) Разрешаем прерывания по обновлению таймера */
 /* (8) Запускаем таймер */
 /* (9) Включаем прерывания таймера TIM3 */

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* (1) */
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; /* (2) */
 
 usart1_init(833); /* (3) */
 usart1_tx_init(); /* (4) */
 
 TIM3->PSC = 7999; /* (5) */
 TIM3->ARR = 999; /* (6) */
 TIM3->DIER |= TIM_DIER_UIE; /* (7) */
 TIM3->CR1 |= TIM_CR1_CEN; /* (8) */
 
 NVIC_EnableIRQ(TIM3_IRQn); /* (9) */
 
 while (1);
}

После прошивки в терминале должен получиться секундомер, примерно так:

Таймер имеет множество настроек: можно считать вверх или вниз (зависит от бита DIR), обновлять налету TIM_ARR сразу или после переполнения (бит APRE) и так далее. Можно было бы расписать назначение каждого регистра, благо у таймеров их много, но практика показывает, что в отрыве от контекста эти регистры никому не уперлись (ну или просто мне лень :-)). Поэтому, сразу переходим к...

5. Альтернативные функции
К примеру, для реализации ШИМ требуется настроить порты в режим альтернативной функции (AF). Таймер TIM3 имеет 4 канала, из них в нашем мк доступно три — TIM3_CH1, TIM3_CH2, TIM3_CH4 — порты PA6, PA7 и PB1 соответственно (см. даташит).

Например, настроим первый канал — TIM3_CH1. Для начала, переведем PA6 в режим AF. В заголовочном файле def.h назначим константу для PA6 (и PA7 — на будущее):

#define TIM31          6
#define TIM32          7

Настройки альтернативной функции можно посмотреть в Reference Manual (раздел GPIO):

В основной программе пишем:

GPIOA->MODER |= MODER_10(TIM31);
GPIOA->OSPEEDR |= OSPEEDR_11(TIM31);
GPIOA->AFR[0] |= 1 << TIM31*4;

В первых двух строках мы макросами из def.h OSPEEDR_11 и MODER_10 настраиваем порт PA6 (TIM31) на максимальную скорость в режиме альтернативной функции (OSPEEDR = 11b, MODER = 10b).

А дальше начинается магия. Почему в третьей строке сдвиг на 1 влево? Почему порт умножается на 4, что это вообще за магические числа?! Топаем в Reference Manual, раздел регистров AFRL и AFRH:

Видно, что для настройки функции на каждый порт выделяется по 4 бита (AFSEL). Для того, чтобы настроить порты PA0-PA7, используется GPIOA->AFR[0] (регистр AFRL). Для настройки портов 8-15 используется регистр AFRH (команда GPIOA->AFR[1]). Допустим, мы хотим (абстрактно) настроить альтернативную функцию AF5 для порта PA3. Значит, мы должны сдвинуть число 5 (AF5) на 3 тетрады влево:

GPIOA->AFR[0] |= 5 << 3*4;

Если же мы хотим настроить AF5 для порта PA12, то нужно из 12 вычесть 8 (потому что в регистре AFRH отсчет опять идет с нуля).

GPIOA->AFR[1] |= 5 << 4*4

Альтернативные функции (AF) прописаны в даташите (таблица 12, стр. 34):

Подведем итоги. Если номер порта 0-7 (например, PA6), то заполняется AFR[0]:

GPIOA->AFR[0] |= X << Y*4

Где X - это номер альтернативной функции (если 0, то сдвига просто нет), а Y - номер порта.

Если номер 8-15 (например, PA9), то заполняется AFR[1]:

GPIOA->AFR[1] |= X << ((Y-8)*4)

"А что же с константами в stm32f0xx.h?", — спросит вдумчивый читатель, — "наверняка, там это предусмотрено!". Увы, но там предусмотрели только константы для очистки AFRL/AFRH:

Если нужно сбросить альтернативную функцию в ноль для порта PA6 или PA9, то будут следующие команды соответственно:

GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL6;
GPIOA->AFR[1] &= ~GPIO_AFRH_AFRH1;

Не слишком наглядно получилось. В CMSIS более старших микроконтроллеров реализована более удобная работа с альтернативными функциями. Например, для назначения AF3 девятому порту, используется константа GPIO_AFRH_AFRH9_3. А что мешает нам сделать нечто подобное и запихнуть это в def.h? Сказано — сделано:

Нужно назначить AF1 для PA6? Не проблема:

GPIOA->AFR[0] |= AFRL61;

И все еще неудобно: нельзя подставить константу (TIM31 в нашем случае). Решаем проблему макросами. После небольшого колдунства (файлик def.h с макросами найдете в архиве), получаем следующее:

GPIOA->AFR[0] |= AFRL(TIM31,1);

Нужно сбросить порт? Нет ничего проще:

GPIOA->AFR[0] &= ~AFRL(TIM31,);

Назначить AF5 для порта PA10? Пожалуйста! (обратите внимание, что [0] поменялось на [1] и AFRL на AFRH):

GPIOA->AFR[1] |= AFRH(10,5);

Так про что мы начали говорить? Про ШИМ? Что же, продолжим.

6. ШИМ
Существует две реализации шим. "Железная", когда специальные порты дергаются в зависимости от TIM3_CCR и TIM3_ARR. И "программный", когда мы сами дергаем порты по прерыванию. Рассмотрим первый вариант. Чуть выше мы уже начали настраивать ШИМ:

GPIOA->MODER |= MODER_10(TIM31);
GPIOA->OSPEEDR |= OSPEEDR_11(TIM31);
GPIOA->AFR[0] |= AFRL(TIM31,1);

Теперь нужно указать, в каком режиме будет работать ШИМ. Это определяется OCxM в регистре TIM3->CCMR1 (в нашем случае, для первого канала, это OC1M; для четвертого канала это были бы биты OC4M в регистре TIM3_CCMR2). Если OC1M = 110b, то порт будет в активном состоянии, пока счетчик (TIM3_CNT) не досчитает до TIM3_CCR. Если же OC1M = 111b, то порт станет активным только тогда, когда счетчик будет равен значению TIM3_CCR.

Например, пусть TIM3_ARR равен 9, TIM3_CCR = 1, а OC1M = 110b. И на порту весит светодиод.

 TIM3->ARR = 9;
 TIM3->CCR1 = 1;
 TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;

Счетчик постоянно делает 10 тиков (от 0 до 9), при этом диод будет гореть в 10% от полной силы (каждый раз, когда счетчик досчитает до 1, он сбросит напряжение на порту в ноль). Поменяем OC1M на 111b:

 TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_0;

теперь светодиод уже горит на 90%. Аналогичного эффекта (инверсия сигнала) можно добиться, если выставить бит CC1P в регистре CCER:

 TIM3->CCER |= TIM_CCER_CC1P;

Наконец, нужно настроить порт ШИМ на выход, иначе никакой диод гореть не будет:

 TIM3->CCER |= TIM_CCER_CC1E

Ура! Мы готовы что-то потестировать. Наша программа будет принимать число в терминале (от 0 до 9), регулируя тем самым яркость светодиода.

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"


void main(void) {
  

 /* (1,2) Подключаем тактование GPIOA и TIM3 */
 /* (3,4) Настраиваем порт на AF и скорость на максимум */
 /* (5) Настраиваем альтернативную функцию порта */
 /* (6) До скольки будет таймер тикать */
 /* (7) Ширина импульса */
 /* (8) По достижению CCR1 — сбрасываться в ноль */
 /* (9) Порт — на выход! */
 /* (10) Запустить таймер */
 /* (11,12,13) Инициализация USART, TX, RX */
 /* (14) Превратить символ RX (пришел через терминал) в число */
 /* (15) Если число не получилось, значит, ноль */
 /* (16) Светить так, как приказано в терминале */

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN; /* (1) */
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; /* (2) */
 
 
 GPIOA->MODER |= MODER_10(TIM31); /* (3) */
 GPIOA->OSPEEDR |= OSPEEDR_11(TIM31); /* (4) */
 GPIOA->AFR[0] |= AFRL(TIM31,1); /* (5) */
 
 TIM3->PSC = 799;
 TIM3->ARR = 9; /* (6) */
 TIM3->CCR1 = 1; /* (7) */
 TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; /* (8) */
 
 TIM3->CCER |= TIM_CCER_CC1E; /* (9) */
 
 TIM3->CR1 |= TIM_CR1_CEN; /* (10) */

 usart1_init(833); /* (11) */
 usart1_tx_init(); /* (12) */
 usart1_rx_init(); /* (13) */

 int brg;
 while (1) {
   brg = (rx_buf[0] - '0'); /* (14) */
   if (brg > 10 || brg < 0) brg = 0; /* (15) */
   TIM3->CCR1 = brg; /* (16) */
 }
}

Прошиваем, заливаем, через терминал отправляем последовательно: "*1", "*4", "*9", "*0" (без кавычек), в результате светодиод будет загораться все ярче, а под конец погаснет. Забегая вперед, звездочка перед числом, это стоп-символ, объявленный в заголовочном файле usart.h — очищает буфер приема данных (rx_buf).

Есть еще ШИМ с точной фазой (center-aligned pwm), но частота у него вдвое ниже, а использование его в любительской практике встречается не слишком часто, потому пока мы не будем его касаться, а перейдем к режиму таймера, который позволяет определять период и коэффициент заполнения ШИМ.

7. Таймер в режиме захвата
В прошлом примере мы разобрали ШИМ, где в регистре TIM3_ARR задавали период, а в регистре TIM3_CCR1 — ширину импульса. Что представляет собой период? Правильно, время между двумя возрастающими (передними) фронтами. Длительность импульса — это время между возрастающим (передним) и убывающим (задним) фронтами. И если повесить прерывания на возрастание и убывание фронтов и параллельно тикать таймером, то можно узнать период ШИМ и длительность импульса:

На рисунке сверху изображен внешний сигнал с определенным периодом и скважностью. Это может быть сигнал с другой ножки того же микроконтроллера, с другого мк или вообще импульсы с какой-то левой схемы (про согласование уровней только не забываем, ага). Число тиков между прерываниями записывается в TIM3_CCR1 (период сигнала) и TIM3_CCR2 (длительность импульса). Если посмотреть на картинку выше и посчитать число тиков таймера (вот прям так и посчитать, как в детском садике, пальчиком: раз... два...), то видно, что CCR1 и CCR2 содержат значения на единицу меньше — ну а что вы хотели, отсчет ведется от нуля, так что все логично.

Для следующего эксперимента вам понадобятся две тестовые платы (как это у вас только одна? не ленитесь, спаяйте еще!). Назовем эти платы (условно) Захватчик и Генератор. На плату Генератор заливаем уже знакомую прошивку генерации импульсов:

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"


void main(void) {
  

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
 
 GPIOA->MODER |= MODER_10(TIM31);
 GPIOA->OSPEEDR |= OSPEEDR_11(TIM31);
 GPIOA->AFR[0] |= AFRL(TIM31,1);
 
 TIM3->PSC = 799;
 TIM3->ARR = 9;
 TIM3->CCR1 = 3;
 TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;
 
 TIM3->CCER |= TIM_CCER_CC1E;
 
 TIM3->CR1 |= TIM_CR1_CEN;

 usart1_init(833);
 usart1_tx_init();
 usart1_rx_init();

 while (1);
}

После прошивки подключите к PA6 светодиод — он должен тускло светиться. Период нашего ШИМ — 10 тиков (TIM->ARR = 9), длительность импульса — 3 тика (TIM3->CCR = 3). Значит, при захвате этого сигнала мы должны будем в CCR1 иметь "9", а в CCR2 — "2". Вот это мы и проверим. Отложим пока прошитую плату Генератор и займемся программой-захватчиком.

Для захвата сигнала нужно настроить порт PA6 все так же в режим альтернативной функции AF1. Далее, укажем делитель для таймера TIM3->PSC = 799 (такой же, как и у платы Генератор). Регистры ARR и CCR заполнять не нужно. Теперь настроим TIM3_CCR1 (бит TIM_CCMR1_CC1S) и TIM3_CCR2 (бит TIM_CCMR1_CC2S) на вход (подробней — смотрите в RM регистр TIM3_CCMR1):

 TIM3->CCMR1 |= TIM_CCMR1_CC2S_1 | TIM_CCMR1_CC1S_0;

Следующий шаг — настроим триггер (бит TIM_SMCR_TS) и сброс регистров на переднем фронте (бит TIM_SMCR_SMS), чтобы в CCR1/CCR2 всегда лежали свежие значения (см. Reference Manual, регистр TIM3_SMCR):

 TIM3->SMCR |= TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2;

Теперь осталось включить режим захвата для CCR1/CCR2 (биты TIM_CCER_CC1E и TIM_CCER_CC2E) и захват по заднему фронту (TIM_CCER_CC2P):

TIM3->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P;

Прерывание по захвату:

 TIM3->DIER |= TIM_DIER_CC1IE;

Осталось написать обработку прерывания и вывод на экран, в результате программа выглядит так:

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"

volatile int ccr1, ccr2, over;

void main(void) {
  
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN; 
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
 
 GPIOA->MODER |= MODER_10(TIM31); 
 GPIOA->OSPEEDR |= OSPEEDR_11(TIM31); 
 GPIOA->AFR[0] |= AFRL(TIM31,1); 
 
 TIM3->PSC = 799;
 
 /* Настройка таймера на захват ШИМ */
 /* (1) CCR1/CCR2 — на вход */
 /* (2) Настройка триггеров и сброс регистров на переднем фронте */
 /* (3) Включить захват CCR1/CCR2 и настроить захват по переднему фронту */

 TIM3->CCMR1 |= TIM_CCMR1_CC2S_1 | TIM_CCMR1_CC1S_0; /* (1) */

 TIM3->SMCR |= TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2; /* (2) */
 
 TIM3->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P; /* (3) */
 
 TIM3->DIER |= TIM_DIER_CC1IE;
 
 TIM3->CR1 |= TIM_CR1_CEN; 

 usart1_init(833); 
 usart1_tx_init(); 
 usart1_rx_init();
 

 NVIC_EnableIRQ(TIM3_IRQn); 
 
 while (1) {
   /* Вывод на экран результата захвата */
   /* Период ШИМ */
   /* Длительность импульса */
   /* Просрали захват, печалька */

   if (ccr1 != 0) { /* (1) */
     usart1_tx_str("ccr1: "); 
      usart_tx_num((char)ccr1, 4); 
      usart1_tx_str("\r\n"); 
      ccr1 = 0;
    }
    if (ccr2 != 0) { /* (2) */
      usart1_tx_str("ccr2: ");
      usart_tx_num((char)ccr2, 4); 
      usart1_tx_str("\r\n"); 
      ccr2 = 0;
    }
    if (over == 1) { /* (3) */
      usart1_tx('(');
      over = 0;
    }
 }
}

/* Обработка прерываний таймера TIM3*/

void TIM3_IRQHandler (void) {
  /* (1) Если захват произошел, складируем регистры по переменным ccr1/ccr2 */
  /* (2) Если предыдущий захват не был обработан — поднимаем флаг over */
  
  if ((TIM3->SR & TIM_SR_CC1IF) == TIM_SR_CC1IF) { /* (1) */
   TIM3->SR &= ~TIM_SR_CC1IF;
   ccr1 = TIM3->CCR1;
   ccr2 = TIM3->CCR2;
  }
  if ((TIM3->SR & TIM_SR_CC1OF) == TIM_SR_CC1OF) { /* (2) */
   TIM3->SR &= ~TIM_SR_CC1OF;
   over = 1;
  }
}

Компилируем, заливаем на плату Захватчик. Соединяем земли плат и порты PA6. Запускаем терминал (Захватчик подключен по USART), резетим Захватчик, в результате должно получиться так:

Это вполне отражает действительность: длина импульса 3 тика, период ШИМ — 10 тиков (терминал показывает на единицу меньше, помним?). Для более точного подсчета можно в Захватчике уменьшить делитель:

 TIM3->PSC = 79;

Тогда после прошивки получим:

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

8. Подключение энкодера
Программных решений для работы с энкодером существует великое множество, STM32 же предлагает "железную" реализацию — мы просто настраиваем два канала таймера и регистр TIM3->CNT будет сам считать "щелчки" энкодера. Код довольно простой, смотрите комменты:

#include "stm32f0xx.h"
#include "def.h"
#include "usart.h"

void main(void) {
  
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
 
 /* (1-3) Настройка каналов CH1,CH2 таймера TIM3 в режим AF1 на максимальной скорости */
 /* (4-5) Подтяжка каналов к питанию */
 /* (6) Максимальное значение таймера */
 /* (7) Настраиваем каналы 1,2 на вход */
 /* (8) Настройка триггеров в режим энкодера */
   GPIOA->MODER |= MODER_10(TIM31) | MODER_10(TIM32); /* (1) */
 GPIOA->OSPEEDR |= OSPEEDR_11(TIM31) | OSPEEDR_11(TIM32) ; /* (2) */
 GPIOA->AFR[0] |= AFRL(TIM31,1) | AFRL(TIM32,1); /* (3) */
 GPIOA->PUPDR &= ~(PUPDR_11(TIM31) | PUPDR_11(TIM32)); /* (4) */
 GPIOA->PUPDR |= (PUPDR_01(TIM31) | PUPDR_01(TIM32)); /* (5) */
   TIM3->ARR = -1; /* (6) */
  TIM3->CCMR1 |= TIM_CCMR1_CC2S_0 | TIM_CCMR1_CC1S_0; /* (7) */
  TIM3->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1; /* (8) */
   TIM3->CR1 |= TIM_CR1_CEN;

 usart1_init(833);
 usart1_tx_init();
 usart1_rx_init();
 
 int tmp;
 while (1) {
    if (TIM3->CNT != tmp) {
		usart1_tx_str("cnt: "); 
		usart_tx_num((char)TIM3->CNT, 4); /* (7) */
		usart1_tx_str("\r\n"); 
		tmp = TIM3->CNT;
		}
 }
}

Компилируем, прошиваем, подключаем каналы энкодера на PA6 и PA7. Средний контакт - на землю.

Обратите внимание!
Каналы энкодера подтянуты к питанию (комменты 4-5). В идеале их надо бы подтянуть физически через резисторы в 10 кОм. 

 Запускаем терминал, крутим ручку вправо:

За один щелчок выдает по 4 значения. Крутим ручку назад, значения уменьшаются:

Обратите внимание, что максимальное значение "тиков" энкодера 0хFFFF (пункт (4) в коде выше), т.е. 65535. Если превысить это значение, то счетчик сбросится в ноль. Если же значение окажется ниже нуля, то энкодер продолжит отсчет от 0xFFFF. Терминале вывод ограничен одним байтом, потому максимальное значение 255.

8. Синхронизация таймеров.
Существует несколько способов синхронизации таймеров. Рассмотрим самый простой — запуск одного таймера (например, TIM3) при старте другого (TIM1). О запуске таймера TIM3 мы узнаем по загоранию светодиода. Код аналогичен первому примеру запуска таймера, разберем отличия:

#include "stm32f0xx.h"
#include "def.h"
 
volatile int timer_sec;
 
#define LED1    5

void main(void) {
   
 /* (1) Включить тактование таймера TIM1 */
 /* (2) Настроить PA9 в режим push-pull */
 /* (3) Настроить событие обновления таймера TIM1, как триггер (MMS = 010b) */
 /* (4) Период таймера TIM1 */
 /* (5,6) Ставим большой делитель, чтобы сравнивать число тиков */
 /* (7) Настроить запуск таймера по триггеру (SMS = 110b) */
 /* (8) Запустить таймер TIM1 */
 /* (9) Если таймер TIM3 запустился... */
 /* (10) ...одновременно с TIM1, загорится LED1  */
 
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
 
 RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; /* (1) */
 GPIOA->MODER |= MODER_01(LED1); /* (2) */
  
 TIM1->CR2 |= TIM_CR2_MMS_1; /* (3) */
 TIM1->ARR = 999;            /* (4) */
 TIM1->PSC = 65000;          /* (5) */
 TIM3->PSC = 65000;          /* (6) */
  
 TIM3->SMCR |= TIM_SMCR_SMS_1 | TIM_SMCR_SMS_2; /* (7) */
 TIM3->ARR = 999;
 
 TIM1->CR1 |= TIM_CR1_CEN; /* (8) */
  
 int tim1, tim3;

 while (1) {
   if ((TIM3->CNT > 100)) {  /* (9) */
     tim1 = TIM1->CNT;
     tim3 = TIM3->CNT;
     if (tim1 == tim3) GPIOA->BSRR |= BS(LED1); /* (10) */
   }
 }
}

Обратите внимание, в этом примере мы не запускали TIM3 явно (как делали до этого), он запускается после TIM1. При помощи делителя (TIMx->PSC) заставим таймеры тикать достаточно медленно (чтобы присвоить переменным tim1, tim3 количество тактов каждого таймера). Теперь легко убедиться, что таймеры тикают синхронно (tim3 == tim1).

Используя синхронизацию, можно столкнуться с такой подлянкой IAR. Допустим, хотим настроить TIM14 синхронно с TIM3. Набираем привычно:

Система радостно предлагает выбрать регистр CR2, мы спокойно продолжаем настройку и... ничего не работает! А весь прикол в том, что регистра CR2 у таймера TIM14 попросту... нет! Ну не умеет этот таймер синхронизацию, как не умеют и таймеры TIM16, TIM17 (у них-то CR2 есть, но очень куцый). Таймер TIM15 — умеет, но у него другая проблема... его просто нет в нашем мк :-) Так что будьте бдительны и не ленитесь курить доки.

9. Сторожевой таймер
Существует две реализации сторожевого таймера (aka watchdog, aka сторожевая псина) — обычный и оконный. В первом случае мы должны периодически сбрасывать таймер, иначе он досчитает до верхней планки и ребутнёт весь микроконтроллер. Оконный же таймер сбросит мк еще и в том случае, если мы обнулим его слишком рано (обнулять сторожевой таймер данного типа нужно в определенном промежутке — "окне"). Поскольку оконные таймеры в любительской практике встречаются чуточку реже, чем никогда, остановимся пока только на обычных "собаках". Накидаем простейшую программку с тупой задержкой:

#include "stm32f0xx.h"
#include "def.h"
 
volatile int timer_sec;

void delay (int a);
 
#define LED1    5 //PA5

void main(void) {
   
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

 GPIOA->MODER |= MODER_01(LED1);

 while (1) {
  delay(500000);
  GPIOA->BSRR |= BS(LED1);
 }
 
}

void delay (int a) {
 int i,j;
 for (i=0; i < a; i++) {
  j++;
 }
}

Ничего особенного, прошиваем, подключаем светодиод на порт PA5, загорается. Теперь подключим watchdog (вставьте перед циклом while):

/* (1) Запустить IWDG (тактуется от LSI 40 kHz ! )*/
/* (2) Разрешить запись в регистры IWDG */
/* (3) Делитель таймера = 8 */
/* (4) Таймер считает до 4096 (0xFFF) */
/* (5) Дождаться, пока обнулится регистр статуса */
 
 IWDG->KR = 0xCCCC; /* (1) */
 IWDG->KR = 0x5555; /* (2) */
 IWDG->PR = IWDG_PR_PR_1; /* (3) */
 IWDG->RLR = 0xFFF; /* (4) */
 while(IWDG->SR); /* (5) */

Прошиваем, запускаем. Если все сделали правильно, светодиод будет уже не просто гореть, а... мигать. Что логично, микроконтроллер примерно каждую секунду перезагружается. Почему каждую секунду? Все просто. Собака тактуется от внутреннего низкочастотного RC-генератора LSI, его частота примерно 40 Гц. Делитель мы выбрали равным восьми (регистр PR = 010b):

000: divider /4
001: divider /8
010: divider /16
011: divider /32
100: divider /64
101: divider /128
110: divider /256
111: divider /256

В регистре RLR мы указали, что сторожевой таймер будет считать до 4095 (итого, с учетом нуля, 4096 тиков). В итоге, максимальное время "терпения" собаки:

t = 4096 * (8 / 40 000) = 0,8192 сек.

Если не сбросить псину раньше, то мк ребутнется. Давайте добавим в цикл while сброс таймера и последим за поведением программы:

 /* (1) Сбросить таймер */
 while (1) {
  delay(500000);
  GPIOA->BSRR |= BS(LED1);
  IWDG->KR = 0xAAAA; /* (1) */
 }

Прошиваем, запускаем... Ура! Светодиод перестал мигать, значит, "собака" работает нормально. В заключение, разберем простейший таймер и пока закроем тему.

10. Системный таймер SYSTICK
Да... это будет самая приятная часть этой статьи :) Потому что для запуска этого таймера потребуется только одна команда: SysTick_Config(ch), где ch — до скольки будет считать таймер. А он у нас 24-разрядный, т.е. он может тикать аж до 0xFFFFFF = 16 777 215! Благодаря этому, например, легко делать весьма точные секундные паузы, не нагружая при этом проц. Как помним, счет у нас идет от нуля, так что при частоте 8 000 000 Гц, мы должны сделать счетчик равным 7 999 999. Ну и прерывание... Обрабатываться обновление таймера будет в функции void SysTick_Handler(void) — можете проверить в файлике startup_stm32f0xx.s. Давайте помигаем диодиком каждую секунду:

#include "stm32f0xx.h"
#include "def.h"

#define LED1    5
 
volatile int led;

void main(void) {
   
 RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
 GPIOA->MODER |= MODER_01(LED1);
 SysTick_Config(7999999); /* (1) */
 
 while (1) {
  if (led == 1) GPIOA->BSRR |= BS(LED1);
  else GPIOA->BSRR |= BR(LED1);
 }
}

void SysTick_Handler(void) { /* (2) */
  led ^= 1;
}

Как видите, ничего сложного: запуск таймера (1) и обработка прерывания (2). После запуска диод должен мигать каждую секунду. Получилось? Отлично! На этом предлагаю и закончить, в архиве вы найдете библиотеки и заголовочные файлы к статье, а впереди у нас USART, DMA, SPI, потом... потом сделаем что-то для практики :)

Прикрепленные файлы:

Теги:

Опубликована: Изменена: 20.09.2020 0 0
Я собрал 0 0
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 0 Проголосовало: 0 чел.

Комментарии (4) | Я собрал (0) | Подписаться

0
Kritik #
Плохо что без практических примеров и без реальной демонстрации.
Ответить
0
ktoj #
DySprozin, спасибо за статьи, все доступно и понятно ! Если будет возможность напишите про настройку RCC для этого камня. Спасибо.
Ответить
0

[Автор]
DySprozin #
Спасибо
А что именно интересует? Резеты? Подключение внешнего кварца?
Ответить
+1
Gennadiy #
Большое спасибо. Нашел то что искал.
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется электрическая мощность?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Pickit 2 - USB-программатор PIC-микроконтроллеров
Pickit 2 - USB-программатор PIC-микроконтроллеров
Набор 4WD Kit Bluetooth Паяльник с регулировкой температуры
вверх