ARM Cortex-M4: таймеры #3

Механизмов, следящих за временем, у SAM4C хватает: шесть 16бит таймеров/счетчиков, четыре 16бит таймеров для ШИМ, один системный таймер, RTT, RTC.

 
 
 
 

Базовые таймеры

У SAM4C их два: TC0 и TC1. Каждый таймер/счетчик включает в себя три 16бит настраиваемых канала, соответственно позволяющие:

  • Генерировать: импульсы определенной частоты (с прерываниями), PWM.
  • Замерять (Захват): считать события, измерять период/частоту.

Ashampoo_Snap_2014.07.03_09h10m02s_006_ (1)
Вся система завязана на обыкновенном двоичном счетчике, который считает по фронту тактовой частоты до определенного события, которое его сбрасывает. И все! 🙂 Событием может являться переполнение или достижение определенного значения, которое описывается в регистре RC. В момент достижения происходит прерывание; регистры RA и RB — тоже умеют генерировать прерывания, но не влияют на работу счетчика. Последние напрямую связаны с пинами TIOAx-TIOBx.

В режиме захвата выводы TIOA и TIOB настраиваются на ввод. В режиме генератора TIOA всегда настроен на вывод, а TIOB действует также как выход, кроме ситуации, когда ему назначена функция входа внешнего запуска.

Включим TC0, канал-1 через PMC:

1
PMC->PMC_PCER0 = PMC_PCER0_PID24; //TC0 CH1

Тактовая частота для каждого канала настраивается отдельно: (про тактирование читайте в части-1)

tcc_cm

У каналов есть два важных регистра: TC_CCR — регистр команд, TC_CMR — регистр режимов/настройки. С помощью первого мы даем определенную команду таймеру/счетчику.
Ashampoo_Snap_2014.07.02_22h15m19s_002_
CLKEN — включает тактирование, а CLKDIS — выключает. SWTRG — нужен для «ручного» сброса таймера, чтобы тот начал что-то делать.

Второй регистр занимает очень важное место: он настраивает почти весь функционал таймера/счетчика.

Ashampoo_Snap_2014.07.25_15h30m53s_001_

С ним есть одна загвоздка: он I/O типа, поэтому туда нужно писать все сразу за раз. Для начала, настроем частоту работы c помощью псевдо-команды TCCLKS.

Имя Значение
TIMER_CLOCK1 MasterClock / 2
TIMER_CLOCK2 MasterClock / 8
TIMER_CLOCK3 MasterClock / 32
TIMER_CLOCK4 MasterClock / 128
TIMER_CLOCK5 SlowClock

Часы тикают — счетчик считает. Прерывания происходят по достижении значений «Rx«, затем таймер нужно сбросить: он может сделать это сам при переполнении, при достижении значения «RC» и при еще некоторых обстоятельствах, описываемых в регистре TC_IER.
tc_int
Обработчик у таймера всего один, чтобы определить прерывание,  предусмотрен регистр статуса. Также не забудьте спросить разрешение у NVIC.

1
2
3
4
5
void TC0_Handler(void) {
  if (TC0->TC_CHANNEL[0].TC_SR & TC_SR_CPCS) { //Прерывание по RC
    //Код…
  }
}

Вернемся к регистру настройки.
Режим генерации довольно обширен, для его использования необходимо установить бит TC_CMR_WAVE в регистре управления. Есть несколько под режимов:

Ashampoo_Snap_2014.07.25_20h21m59s_002_

  • UP_NO_AUTO — счетчик считает до своего предела, затем сбрасывается.
  • UP_RC — считает до значения RC.
  • UP_AUTO — объединяет предыдущие режимы.
  • UPDOWN_NO_AUTO — считает до переполнения, затем начинает счет в обратную сторону.
  • UPDOWN_RC — считает до значения RC, затем начинает счет в обратную сторону.
  • UPDOWN_AUTO — объединяет предыдущие два режима.

 

Для генерации прерываний с определенной частотой код будет следующий:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PMC->PMC_PCER0 = PMC_PCER0_PID23;  //Включаем TC0
 
TC0->TC_WPMR = 0x54494D; //Очень черная магия
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKDIS; //Отключаем тактирование
TC0->TC_CHANNEL[0].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1 | // Частота = MasterClock / 2
TC_CMR_WAVE | //Режим «генерации»
TC_CMR_WAVSEL_UP_RC | //Счет до значения RC, затем сброс
TC_CMR_CPCTRG; //Триггер  RC
 
TC0->TC_CHANNEL[0].TC_RC = 176; //Какое-то значение. Генерируемая частота = (MasterClock/2)/176
TC0->TC_CHANNEL[0].TC_RA = 0;
TC0->TC_CHANNEL[0].TC_RB = 0;
TC0->TC_CHANNEL[0].TC_IER = TC_IER_CPCS; //Прерывание по RC
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKEN; //Включаем тактирование
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG; //Запускаем таймер
 
NVIC_EnableIRQ(TC0_IRQn); //Разрешаем прерывания от таймера

Таким образом таймер считает до 176, затем программа уходит в прерывание:

1
2
3
4
5
6
void TC0_Handler(void) {
  register uint32_t dummy;
  /****Ваш код****/
  dummy = (TC0->TC_CHANNEL[0].TC_SR & TC_SR_CPCS);
  (void) dummy ;
}

Для генерации в порт в нашем распоряжении имеются регистры сравнения RA, RB. Однако, нужно предварительно выбрать подходящий TIOx. Номер и буква порта соответствуют каналу таймера и регистрам сравнения, соответственно.  Пусть это будет — TIOA0 (PA13).

Затем, таймеру необходимо указать  какие регистры отвечают за установку лог.нуля и лог. единицы. Для этого есть специальные команды ACPx.

Команда
Значение
ACPA_SET / ACPA_CLEAR / ACPA_TOGGLE Установить HI,LOW, INV на TIOA при достижении RA
ACPC_SET / ACPC_CLEAR / ACPC_TOGGLE Установить HI,LOW, INV на TIOA при достижении RC 
BCPB_SET / BCPB_CLEAR / BCPB_TOGGLE Установить HI,LOW, INV на TIOB при достижении RB 
BCPC_SET / BCPC_CLEAR / BCPC_TOGGLE Установить HI,LOW, INV на TIOB при достижении RC 

В нашем случае: RC — LOW, RA — HI или наоборот.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
PMC->PMC_PCER0 = PMC_PCER0_PID11;
PIOA->PIO_WPMR = 0x50494F;
PIOA->PIO_IDR = PIO_IDR_P13; //Наш пин — PA13 — (TIOA0)
PIOA->PIO_OER = PIO_OER_P13;
//Сейчас нужный переф. (B)
uint32_t last0 = PIOA->PIO_ABCDSR[0];
PIOA->PIO_ABCDSR[1] &= (~PIO_ABCDSR_P13 & last);
uint32_t last1 = PIOA->PIO_ABCDSR[1];
PIOA->PIO_ABCDSR[0] = (PIO_ABCDSR_P13 | last);
PIOA->PIO_PDR = PIO_PDR_P13;
 
PMC->PMC_PCER0 = PMC_PCER0_PID23;
 
TC0->TC_WPMR = 0x54494D;
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKDIS;
TC0->TC_CHANNEL[0].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1 | TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_CPCTRG
TC_CMR_ACPA_SET |  //По совпадению RA — лог.1
TC_CMR_ACPC_CLEAR; //По совпадению RC — лог.0
 
TC0->TC_CHANNEL[0].TC_RC = 176;
TC0->TC_CHANNEL[0].TC_RA = 176/2; //Значение для нашего пина TIOA0. 50% несущей
TC0->TC_CHANNEL[0].TC_RB = 0;
TC0->TC_CHANNEL[0].TC_IER = TC_IER_CPCS;
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKEN;
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG;
 
NVIC_EnableIRQ(TC0_IRQn);

По-прежнему, таймер считает до 176, и лишь попутно устанавливает лог.единицу на выводе по значению RA. Картина должна получиться следующей:

pic_234_2

Этот генератор меандра, вы можете использовать его как ШИМ, изменяя значение RA. Но в этом случае отключите прерывания.

Режим подсчета таймера устанавливается путем сброса бита TC_CMR_WAVE в регистре управления. Для этого режима используются те же выводы TIOAx-TIOBx, но здесь они являются источниками прерываний. При возникновении события текущее значение счетчика записывается в регистры RA, RB. Какого события? — Это описывается командой LDRx.

Команда
Значение
LDRA_RISING По фронту TIOA записать в RA
LDRA_FALLING По спаду TIOA записать в RA
LDRA_EDGE По изменении TIOA записать в RA
LDRB_RISING По фронту TIOB записать в RB
LDRB_FALLING По спаду TIOB записать в RB
LDRB_EDGE По изменении TIOB записать в RB

Но, LDRx не сбрасывает таймер, поэтому для сброса (если он вам нужен) используются иные команды. (Кроме того, не стоит забывать, что мы можем сами его сбросить через регистр TC_CCR.)

  • CPCTRG — сброс по достижении значения регистра RC.
  • ABETRG — сброс по событиям TIOA, TIOB.
  • Программный сброс через регистр управления TC_CCR.

Первые две команды не требуют каких-либо параметров — это просто биты. Для примера сделаем измеритель периода импульсов, поступающих на TIOA0 (PA13):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PMC->PMC_PCER0 = PMC_PCER0_PID11;
PIOA->PIO_WPMR = 0x50494F;
PIOA->PIO_IDR = PIO_IDR_P13; //Наш пин — PA13 — (TIOA0)
PIOA->PIO_ODR = PIO_ODR_P13; //Вход
uint32_t last0 = PIOA->PIO_ABCDSR[0];
PIOA->PIO_ABCDSR[1] &= (~PIO_ABCDSR_P13 & last);
uint32_t last1 = PIOA->PIO_ABCDSR[1];
PIOA->PIO_ABCDSR[0] = (PIO_ABCDSR_P13 | last);
PIOA->PIO_PDR = PIO_PDR_P13;
 
PMC->PMC_PCER0 = PMC_PCER0_PID23;
 
TC0->TC_WPMR = 0x54494D;
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKDIS;
TC0->TC_CHANNEL[0].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1 |
TC_CMR_ABETRG | //Сброс по TIOA, TIOB
TC_CMR_LDRA_RISING; //Заносим значение в RA по фронту пина TIOA
 
TC0->TC_CHANNEL[0].TC_RA = 0;
TC0->TC_CHANNEL[0].TC_RB = 0;
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKEN; //Включаем тактирование
TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG; //Запускаем таймер

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

1
uint32_t period = TC0->TC_CHANNEL[0].TC_RA;

Но будьте внимательны с рабочей частотой таймера, она устанавливает предел измеряемых частот, чтобы этого избежать можно останавливать счет. Для чего есть свои команды:

Команда
Значение
LDBSTOP При событии на TIOB остановить счет
CPCSTOP По значению RC остановить счет

Ну, а если вам не нужны все эти TIO, то все события и операции можно проводить в пределах программы. Для сброса и прерываний используйте SWTRG, RC или RA/RB в режиме генерации, а само значение можно получить из регистра счета TC_CV. 🙂

Системный таймер

Вам надоело разбираться с базовыми таймерами/счетчиками, где чёрт ногу сломит? Тогда используйте системный таймер ARM, имеющий всего одну функцию: генерация прерываний с определенной периодичностью. 🙂 Хочу заметить: он всего один и 24-х разрядный.

tcc_sys

 

Мы загружаем значение в регистр LOAD и ждем, когда счетчик досчитает до него, затем программа уходит в прерывание. Тактирование таймера происходит от MasterClock-а ядра.

В CMSIS уже имеется функция управления таймером, что освобождает нас от лишней возни с регистрами и NVIC:

1
2
3
4
5
6
7
8
9
10
11
12
13
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) {
  if ((ticks — 1) > SysTick_LOAD_RELOAD_Msk) return (1); /* Если значение выходит за пределы */
 
  SysTick->LOAD = ticks — 1; /* Загружаем значение */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS)1); /* Устанавливаем приоритет прерывания */
  SysTick->VAL = 0; /* Сброс таймера, путем записи в регистр счета */
 
  SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
  SysTick_CTRL_TICKINT_Msk |
  SysTick_CTRL_ENABLE_Msk; /* Запускаем таймер */
 
  return (0); /* Ок? */
}

Использование — элементарное:

1
SysTick_Config(7);

Функция обработчика прерывания выглядит таким образом:

1
2
3
void SysTick_Handler(void) {
  //Smth…
}

В качестве примера, мы можем повторить генератор частоты использую PIO контроллер с простой записью в порт:

1
2
3
4
5
6
7
8
9
10
bool stat = false;
 
void SysTick_Handler(void) {
  if (stat) {
    PIOA->PIO_CODR = PIO_CODR_P6; //Установить 0 на ножке PA6
  } else { 
    PIOA->PIO_SODR = PIO_SODR_P6; //Установить 1 на ножке PA6
  }
  stat = !stat;
}

Если все верно, то мы получим меандр с половинной частотой прерываний (без учета скорости работы PIO). Но сразу скажу — не используйте такие методы для генерации сигналов.

pic_316_1

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


Все части

Вы можите оставить комментарий, или поставить трэкбек со своего сайта.

3 комментария к “ARM Cortex-M4: таймеры #3”