Механизмов, следящих за временем, у SAM4C хватает: шесть 16бит таймеров/счетчиков, четыре 16бит таймеров для ШИМ, один системный таймер, RTT, RTC.
Базовые таймеры
У SAM4C их два: TC0 и TC1. Каждый таймер/счетчик включает в себя три 16бит настраиваемых канала, соответственно позволяющие:
- Генерировать: импульсы определенной частоты (с прерываниями), PWM.
- Замерять (Захват): считать события, измерять период/частоту.
Вся система завязана на обыкновенном двоичном счетчике, который считает по фронту тактовой частоты до определенного события, которое его сбрасывает. И все! 🙂 Событием может являться переполнение или достижение определенного значения, которое описывается в регистре RC. В момент достижения происходит прерывание; регистры RA и RB — тоже умеют генерировать прерывания, но не влияют на работу счетчика. Последние напрямую связаны с пинами TIOAx-TIOBx.
В режиме захвата выводы TIOA и TIOB настраиваются на ввод. В режиме генератора TIOA всегда настроен на вывод, а TIOB действует также как выход, кроме ситуации, когда ему назначена функция входа внешнего запуска.
Включим TC0, канал-1 через PMC:
1 | PMC->PMC_PCER0 = PMC_PCER0_PID24; //TC0 CH1 |
Тактовая частота для каждого канала настраивается отдельно: (про тактирование читайте в части-1)
У каналов есть два важных регистра: TC_CCR — регистр команд, TC_CMR — регистр режимов/настройки. С помощью первого мы даем определенную команду таймеру/счетчику.
CLKEN — включает тактирование, а CLKDIS — выключает. SWTRG — нужен для «ручного» сброса таймера, чтобы тот начал что-то делать.
Второй регистр занимает очень важное место: он настраивает почти весь функционал таймера/счетчика.
С ним есть одна загвоздка: он 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.
Обработчик у таймера всего один, чтобы определить прерывание, предусмотрен регистр статуса. Также не забудьте спросить разрешение у NVIC.
1 2 3 4 5 | void TC0_Handler(void) { if (TC0->TC_CHANNEL[0].TC_SR & TC_SR_CPCS) { //Прерывание по RC //Код… } } |
Вернемся к регистру настройки.
Режим генерации довольно обширен, для его использования необходимо установить бит TC_CMR_WAVE в регистре управления. Есть несколько под режимов:
- 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. Картина должна получиться следующей:
Этот генератор меандра, вы можете использовать его как ШИМ, изменяя значение 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-х разрядный.
Мы загружаем значение в регистр 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). Но сразу скажу — не используйте такие методы для генерации сигналов.
Вообще, старайтесь использовать прямое управление выводами через PIO как можно реже, только в случаях, когда задачи не могут решиться встроенной периферией или являются второстепенными, не требующими эффективно использовать каждый квант времени.
Большое спасибо автору за статьи про SAM4. Очень хочется увидеть статьи про ЦАП, АЦП и PDC по данной серии микроконтроллеров.
Пожалуйста 🙂
Сейчас планируется статья про UART, затем что-нибудь из предложенного вами.
Спасибо!
Буду ждать с нетерпением.