Таймеры stm32 HAL - часть вторая





Здравствуйте.

Это продолжение статьи про таймеры stm32.

В прошлой части мы разбирались с тактированием таймера и выходными сигналами (ШИМ и прочее), а сейчас пришла пора познакомится с входными сигналами, и более сложными механизмами таймера.


Вначале нам нужно познакомиться с режимом захвата сигнала, а потом будем изучать схему таймера, и всё остальное.

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

CCRCapture/Compare Register.


Один из вариантов использования режима захвата, это измерение длины импульса входящего сигнала…


tдлина импульса.


В Кубе делаем так…


Здесь мы настраиваем два канала — основной (direct) и косвенный (indirect), при этом у нас активировался только один вход (РА8).

Тут дело вот в чём: чтоб измерить длину импульса, нам нужно захватить сигнал два раза, первый раз в момент перехода с LOW на HIGH (передний фронт), а второй в момент перехода с HIGH на LOW (задний фронт). Соответственно первый канал будет ловить передний фронт, а второй — задний. Внутри микроконтроллера есть механизм позволяющий первому и второму каналам обмениваться сигналами, благодаря которому мы можем подключить внешний сигнал только к одному входу. Чуть попозже, это будет рассмотрено на схеме подробно.


Таймер настраиваем так…



Prescaler — таймер будет тикать со скоростью один тик за одну микросекунду.

Counter Period — счётчик будет переполняться за 65мс (65000мкс = 65мс). Можно записать максимальное значение — 65535, но половина миллисекунды нам погоды не сделают, а целыми числами удобнее оперировать.

Измерение длины импульса происходит так:

• таймер тикает, счётчик (Counter Period) считает, переполняется, сбрасывается, и вновь считает. В общем работает как обычно.

• приходит передний фронт ⇨ происходит захват на первом канале — текущее значение счётчика копируется в регистр CCR1, но нам интересно не оно, а генерируемое прерывание в обработчике которого мы обнуляем счётчик.

• приходит задний фронт ⇨ происходит захват на втором канале — текущее значение счётчика копируется в регистр CCR2.

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


Polarity Selection — выбор фронта сигнала.

Prescaler Division Ratio — делитель входящего сигнала, если указать 2, то захват будет происходить по каждому второму фронту, и т.д.

Input Filterфильтр сигнала, описан в первой части.


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




В программе добавляем массив для вывода инфы в USART…

/* USER CODE BEGIN Includes */
#include "string.h"
/* USER CODE END Includes */
...
/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
/* USER CODE END PV */


Колбек захвата…

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // RISING с LOW на HIGH
		{
			__HAL_TIM_SET_COUNTER(&htim1, 0x0000); // обнуление счётчика
		}

		else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) // FALLING с HIGH на LOW
		{
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); // чтение значения в регистре захвата/сравнения
			snprintf(trans_str, 63, "Pulse %lu mks\n", falling);
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}

Получили передний фронт (HAL_TIM_ACTIVE_CHANNEL_1) — обнулили счётчик, получили задний фронт (HAL_TIM_ACTIVE_CHANNEL_2) — скопировали значение регистра захвата/сравнения в переменную falling, и вывели её в уарт.

Запускаем два канала в режиме захвата…

/* USER CODE BEGIN 2 */
  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
/* USER CODE END 2 */


В бесконечном цикле создаём источник импульсов…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  HAL_Delay(20); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(30);
          ...


Соединяем РА5 и РА8, прошиваем, и любуемся результатом…


Длина импульса двадцать с хвостиком миллисекунд. Что хотели — то и получили.

Поскольку счётчик у нас переполняется и начинает отсчёт заново через каждые 65мс, тогда если поступающий импульс будет длится дольше чем 65мс, мы не сможем его измерить. Это легко проверить если увеличить паузу с 20мс до 70…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  HAL_Delay(70); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(30);
          ...


Результат будет ожидаемый…


Пришёл передний фронт — счётчик обнулился, досчитал до 65000, обнулился ещё раз, досчитал до 5000, и вот тут наконец-то пришёл задний фронт — в регистр записалось 5000.

Чтобы измерять импульсы большей длительности, можно поступить по разному. Можно увеличить предделитель таймера, например, до 720, тогда один тик таймера будет происходить за 10мкс и соответственно счётчик досчитает до 65000 за 650мс. То есть мы сможем измерить импульс длиной до 650мс. Однако во-первых у нас увеличится погрешность (плюс-минус 10мкс), и во-вторых, что делать если импульс будет дольше 650мс — опять увеличивать предделитель — нет, это не наш метод.

Мы поступим по другому — оставим предделитель как есть, активируем прерывание по переполнению…



В программе добавим глобальную переменную…

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint8_t count_overflow = 0;
/* USER CODE END PV */


Эту переменную будем увеличивать в колбеке по переполнению…

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1)
	{
		count_overflow++;
	}
}


Немного изменяем колбек захвата…

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // RISING с LOW на HIGH
		{
			__HAL_TIM_SET_COUNTER(&htim1, 0x0000); // обнуление счётчика
			count_overflow = 0; // обнуляем переменную
		}

		else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) // FALLING с HIGH на LOW
		{
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); // чтение значения в регистре захвата/сравнения
			snprintf(trans_str, 63, "Pulse %lu mks\n", falling + (65000 * count_overflow));
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}


При получении переднего фронта (HAL_TIM_ACTIVE_CHANNEL_1) обнуляем счётчик и переменную. При каждом последующем переполнении, пока не пришёл задний фронт, эта переменная будет увеличиваться на единицу. При получении заднего фронта (HAL_TIM_ACTIVE_CHANNEL_2) копируем содержимое регистра CCR в переменную falling и прибавляем к ней количество переполнений умноженное на значение переполнения.

Цифру 65000 можно заменить на HALовский макрос — __HAL_TIM_GET_AUTORELOAD(&htim1), тогда при изменении Counter Period, ничего не нужно будет менять в коде.

snprintf(trans_str, 63, "Pulse %lu\n", falling + (__HAL_TIM_GET_AUTORELOAD(&htim1) * count_overflow));


В результате получим искомую длину импульса…



Теперь можно поэкспериментировать с любой длиной импульса.

точные показания
Чтобы показания были точными нужно во-первых указать предделитель с вычетом единицы (про это написано в первой части), то есть так — 71.

И во-вторых, функция HAL_Delay() не совсем точная, поэтому вместо неё можно использовать DWT (Data Watchpoint and Trace unit) — это модуль трассировки и поддержки контрольных точек, который умеет считать такты, а значит измерять время.

В программе добавляем несколько вещей…

/* USER CODE BEGIN PD */
#define DWT_CONTROL *(volatile unsigned long *)0xE0001000
#define SCB_DEMCR *(volatile unsigned long *)0xE000EDFC
/* USER CODE END PD */


/* USER CODE BEGIN 0 */
void DWT_Init(void)
{
    SCB_DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // разрешаем использовать счётчик
    DWT_CONTROL |= DWT_CTRL_CYCCNTENA_Msk;   // запускаем счётчик
}

void delay_us(uint32_t us)
{
    uint32_t us_count_tic =  us * (SystemCoreClock / 1000000);
    DWT->CYCCNT = 0U; // обнуляем счётчик
    while(DWT->CYCCNT < us_count_tic);
}


/* USER CODE BEGIN Init */
DWT_Init();
/* USER CODE END Init */


И теперь длину импульса указываем так:

while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  delay_us(31420); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(30);
          ...


Результат…


Исходник delay_us и delay_ms.


Сейчас у нас много различных действий выполняются «вручную», однако у таймера есть такие механизмы, что можно вообще отключить все прерывания, нигде ничего не обнулять, всё это будет происходить аппаратно, да ещё и данные из CCR забирать при помощи DMA. Обо всём этом будет рассказано позже, а сейчас нужно поизучать устройство таймера.





Схема таймера




Cправа нарисованы каналы, когда они работают как выходы (основные и комплементарные). Частота поступающая с предделителя PSC, тактирует счётчик CNT (counter). CNT считает до значения переполнения, которое берётся из регистра AutoReload (это то, что в Кубе называется Counter period), и постоянно сравнивает своё значение со значением сравнения записанным в регистре Capture/Compare (это то, что в Кубе называется Pulse). Как только значение CNT совпадает со значением в Capture/Compare, тут же происходит событие (прерывание CC1ICapture/Compare Interrupt, канал №1) и подаётся сигнал OC1REF. Сигнал OC1REF проходит через DTG (механизм dead-time), расщепляется на основной и инвертированный, и проходит через систему включения/отключения каналов (output control). Далее эти сигналы выдаются на основной и комплементарный выход, либо на какой-то один (в зависимости от настроек). То есть, даже если мы настроили генерецию ШИМа вот так — PWM Generation CH1, то всё равно будет сформировано два сигнала, прямой и инвертированный, а какой из них выпускать наружу будет решать система output control. У четвёртого канала нет ни dead-time, ни комплементарного выхода.

Слева-снизу вход break-сигнала (TIMx_BKIN). Сигнал поступающий либо с пина TIMx_BKIN, либо от системы CSS воздействует на каналы через DTG (dead-time generator) или через систему output control.

Выше обозначены те же самые каналы что и справа, только когда они работают как входы. Внешний сигнал, поступивший на TIMx_CH1, проходит через мультиплексор механизма XOR, оттуда попадает в систему фильтрации сигнала и детектирования фронтов (Input Filter & Edge detector), и разделяется на два сигнала разной полярности (TI1FP1 и TI1FP2). TI1FP1 проходит через свой мультиплексор, а TI1FP2 уходит на мультиплексор второго канала. Дальше эти сигналы проходят через делители, которые в Кубе называются Prescaler Division Ratio, генерируют прерывания, и наконец текущее значение CNT копируется в регистр Capture/Compare. Так происходит работа примера по измерению длины импульса описанного выше. Третий и четвёртый канал можно настроить в таком же режиме, и измерять другой сигнал, независимо.

Механизм XOR (исключающее ИЛИ) — описание в конце статьи.

TIMx_ETR — вход для внешнего тактового или триггерного (см. ниже) сигнала.

Всё остальное, что находится выше регистра AutoReload относится к Master/Slave режимам управления таймером. Таймер может быть мастером по отношению к другим таймерам (управлять ими) и некоторой периферии, а может быть слейвом (подчинённым), как по отношению к другим таймерам, так и по отношению к самому себе.




Дальше изучать схему лучше всего на практике, поэтому давайте вернёмся к нашему последнему примеру и немного его изменим.

В Кубе делаем так…


Пояснения этих пунктов будет позже, а все остальные настройки оставьте как есть.


Настраиваем DMA на второй канал таймера…


Включаем циклический режим, чтоб не перезапускать DMA, отключаем инкремент адреса, и указываем целое слово.


И отключаем все прерывания (остаётся только от DMA)




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

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
uint32_t ic_ccr = 0;
/* USER CODE END PV */


В бесконечном цикле добавляем ещё один импульс, чтоб было видно как они меняются:

while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  delay_us(31420); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(200);

	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  delay_us(53100); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(200);
          ...

Импульс делаем такой, чтоб он умещался в 65мс (один цикл переполнения).

Запускать таймер будем всего одной командой:

/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_DMA(&htim1, TIM_CHANNEL_2, (uint32_t*)&ic_ccr, 1);
/* USER CODE END 2 */


Прерывание от DMA приходит в тот же колбек, что и прерывания по захвату, и мы можем в нём выводить данные:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		snprintf(trans_str, 63, "Pulse %lu mks\n", ic_ccr);
		HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	}
}

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


Прошиваем и смотрим результат…




Можно сделать так, чтоб сохранялось несколько значений. Включим инкремент памяти в настройках DMA…



Превратим переменную в массив и в команде запрашиваем два значения:

/* USER CODE BEGIN 2 */
uint32_t ic_ccr[2] = {0,};
HAL_TIM_IC_Start_DMA(&htim1, TIM_CHANNEL_2, (uint32_t*)ic_ccr, 2);
/* USER CODE END 2 */


А в бесконечном цикле сделаем так:

while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  delay_us(31420); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(200);

	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  delay_us(53100); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(200);

	  snprintf(trans_str, 63, "Pulses %lu  %lu\n", ic_ccr[0], ic_ccr[1]);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
          ...

В колбеке всё закомментируйте.




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


Режимы управления таймерами Master/Slave.

Таймеры, при возникновении тех или иных событий, могут аппаратно посылать различные управляющие сигналы (триггеры) другим таймерам и некоторой периферии — режим Master. Таймер, получающий сигнал от другого таймера, является подчинённым — режим Slave. При этом таймер может быть одновременно и Слейвом и Мастером, то есть получать сигнал от одного таймера, и посылать сигнал другому таймеру. Так же таймер может быть подчинён сам себе (если выражаться образно), то есть, например, таймер совершил захват на каком-то канале, сгенерировал событие, и на основании этого события послал сам себе сигнал, который обнуляет счётчик (это как раз наш пример).


Итак, возвращаемся к вопросу — как же у нас сейчас работает таймер? Да очень просто, вся магия кроется в этих двух пунктах…



Slave Mode — этот пункт указывает, что должен делать таймер находясь в подчинённом режиме когда поступит триггерный сигнал.

Reset Mode — означает, что при поступлении триггерного сигнала таймер должен сбросить (обнулить) счётчик CNT.

другие варианты
Gated Mode — таймер тикает пока подаётся триггерный сигнал высокого уровня, и останавливается когда поступает сигнал низкого уровня. Счётчик при этом не сбрасывается. То есть подали HIGH — считает, подали LOW — остановился (см. ниже раздел про частотомер).

Trigger Mode — счётчик запускается (но не сбрасывается) высоким уровнем триггерного сигнала. В отличие от режима Gated Mode, можно только запустить счётчик, остановить нельзя.

External Clock Mode 1 — в прошлой части мы изучали External Clock Mode 2 (ETR2), а это ETR1. В этом режиме таймер может тактироваться или управляться как внешним сигналом поступающим на пин TIM_ETR, так и различными внутренними сигналами — ITRx, TI1F_ED, TI1FP1 и TI2FP2




Trigger Source — а этот пункт указывает на то, что будет служить триггерным сигналом для таймера находящегося в подчинённом режиме. В данном случае мы указываем TI1FP1.


В итоге, сейчас наш таймер работает следующим образом…


Таймер подчинён сам себе.

На первом канале появляется передний фронт ⇨ превращается в сигнал TI1FP1 проходит через мультиплексор TRGI (Trigger Input) попадает в Slave Mode Controller приходит в CNT и обнуляет его. Ну, а дальше ничего нового — на втором канале появляется задний фронт, текущее значение счётчика помещается в регистр захвата/сравнения второго канала, а оттуда копируется в массив с помощью DMA. В результате вся работа происходит аппаратно.



другие варианты Trigger Source
Trigger Source ⇨ ETR1 — триггером будет служить сигнал поступающий на пин TIM1_ETR.

Trigger Source ⇨ TI1F_ED — Если выбрать его в качестве триггера, то счётчик будет сбрасываться (или делать что-то другое, в зависимости от того, что указано в Slave Mode) как при низком уровне на входе первого канала, так и при высоком. То есть любые сигналы возникающие на входе первого канала будут являться триггером. Есть только у первого канала.

Trigger Source ⇨ ITRx — триггерные сигналы от других таймеров (см. далее).


Итак, сейчас мы можем измерять импульс в пределах 65мс, а чтоб измерять большую длину, нужно опять же подсчитывать количество переполнений таймера №1. В самом начале статьи мы делали это за счёт увеличения переменной (count_overflow) в обработчике прерывания по переполнению. Сейчас же вместо этой переменной будем использовать счётчик таймера №2. То есть, таймер №1 будет обнулять сам себя (при получении переднего фронта), а далее, при каждом переполнении, он будет посылать триггерный сигнал таймеру №2. Таймер №2 получая триггерный сигнал будет увеличивать свой счётчик на единицу. В результате подсчёт количества переполнений будет происходить полностью аппаратно.


У таймера №1 оставляем всё как было…



Кроме одного пункта…



Раздел Trigger Output (TRGO) Parameters отвечает за триггерные сигналы которые таймер посылает другим таймерам или АЦП (аппаратный запуск АЦП, см. ниже).



Trigger Event Selection — отвечает за то, при каких событиях таймер будет посылать триггерный сигнал.

Update Event — таймер пошлёт триггерный сигнал (TRGO — TRiGger Output) в момент переполнения.

другие варианты
Reset (UG bit from TIMx_EGR) — TRGO будет послан при сбросе счётчика с помощью бита UG (Update Generation).

Enable (CNT_EN) — TRGO будет послан в момент активации счётчика и будет удерживаться до остановки таймера. Этот режим можно использовать для одновременного запуска нескольких таймеров, а так же и для других целей (см. ниже раздел про частотомер).

Compare Pulse (ОС1) — TRGO посылается в момент установки флага CC1IF (даже если он уже содержал значение 1), проще говоря, сигнал будет послан при захвате или сравнении на первом канале.

Output Compare (OCxREF) — TRGO будет послан при возникновении сигнала OCxREF, то есть в момент сравнения на указанном канале (x).


Master/Slave Mode (MSM bit)
Если режим включён, тогда вводится задержка между сигналом запуска на входе TRGI и его воздействием на таймер для достижения точной синхронизации между данным таймером и его подчинёнными таймерами (которые управляются сигналом TRGO).

Этот режим полезен когда требуется синхронизация двух таймеров одним внешним событием. То есть, допустим нам нужно чтоб таймер №1 запустил таймеры №2 и №3 синхронно, но, поскольку таймер может послать только один триггерный сигнал (только одному таймеру), то таймер №1 будет посылать триггер таймеру №2, а таймер №2 в свою очередь пошлёт триггер таймеру №3. Так вот, если MSM bit ⇨ Disable, то получится так, что таймер №2 сначала стартанёт (получив триггер от таймера №1), и только после этого пошлёт триггер таймеру №3, то есть они запустятся не синхронно. Если же указать MSM bit ⇨ Enable, то таймер №2 сначала пошлёт триггер таймеру №3, и выдержав небольшую паузу стартанёт сам. Таким образом оба таймера запустятся синхронно.

Вух, надеюсь вы всё поняли.



Включаем прерывание по захвату…






Таймер №2 настраиваем так...



Slave Mode ⇨ External Clock Mode 1 — таймер будет работать в подчинённом режиме с внешним источником тактирования. В данном случае, под словом внешний, подразумевается ни какой-то сигнал поступающий извне на микроконтроллер, а внешний по отношению к самому таймеру. В нашем случае это будут сигналы от таймера №1. Таймер №1 переполнился, послал триггерный сигнал, таймер №2 получил это сигнал и «тикнул» один раз — счётчик увеличился на единичку, получил ещё сигнал — счётчик опять увеличился, и т.д.

Trigger Source ⇨ ITR0 — источником тактирования выбираем таймер №1. ITRx (Input Trigger) определяет от какого таймера получать триггерный сигнал...


Таймер №2 (TIM2) у нас находится в подчинённом режиме (Slave), а триггерный сигнал посылает таймер №1 (TIM1), соответственно указываем ITR0. Если бы слейвом был таймер №4, а его должен был «толкать» таймер №3, тогда бы мы указали ITR2.

для TIM1 и TIM8



Схема прохождения сигнала в таймере №2…




В конфигурации нужно прописать только переполнение…


Если указать максимальное переполнение, то при условии что таймер №1 переполняется каждые 65мс, мы сможем измерить импульс длиной до 4259775мс = 71 минута (65мс * 65535 = 4259775мс). Разумеется вряд ли придётся измерять сигналы такой длительности, но пусть будет так, как говорится «кушать не просит».


В программе запускаем таймер №1 в режиме захвата на первом и втором канале, и включаем таймер №2:

/* USER CODE BEGIN 2 */
  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
  HAL_TIM_Base_Start(&htim2);
/* USER CODE END 2 */


В бесконечном цикле указываем длину импульса больше 65мс:

while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  //delay_us(131420); // длина импульса
	  HAL_Delay(285); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(150);
          ...


Колбек по захвату делаем так:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // RISING с LOW на HIGH
		{
			__HAL_TIM_SET_COUNTER(&htim2, 0x0000); // обнуление счётчика таймера №2
		}

		else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) // FALLING с HIGH на LOW
		{
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); // чтение значения в регистре захвата/сравнения
			snprintf(trans_str, 63, "Pulse %lu mks\n", falling + (65000 * __HAL_TIM_GET_COUNTER(&htim2)));
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}


При получении переднего фронта, счётчик таймера №1 обнуляется аппаратно, а счётчик таймера №2 программно (HAL_TIM_ACTIVE_CHANNEL_1). При каждом последующем переполнении, пока не пришёл задний фронт, счётчик таймера №2 будет аппаратно увеличиваться на единицу. При получении заднего фронта (HAL_TIM_ACTIVE_CHANNEL_2) содержимое регистра CCR копируется в переменную falling и к ней прибавляется значение переполнения таймера №1 умноженное на количество переполнений (значение в счётчике таймера №2).

К сожалению в данном случае аппаратно обнулить счётчик таймера №2 не получится, приходится использовать прерывания, но всё же их теперь меньше, и не нужно «вручную» увеличивать переменную (count_overflow). Плюс, этот пример продемонстрировал работу таймера в подчинённом режиме, и использование счётчика нестандартным способом.





Если настроить таймер №3 так же как и №2, а у №2 добавить выходной триггер…



… тогда счётчики этих таймеров будут работать каскадно…


Таймер №1 переполняется каждые 65мс и тактирует таймер №2, таким образом таймер №2 будет переполнятся каждые ~70 минут (65мс * 65535 = 70мин) и выдавать импульс на таймер №3, соответственно таймер №3 переполнится через 4587450 минут (70мин * 65535) = 76458 часов = 3186 суток = 106 месяцев = 9 лет. В общем доооолгий импульс можно измерить

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




Помимо описанного выше, есть более простой способ настройки таймера №1 (или любого другого) для измерения импульса, он называется захват ШИМ'а (PWM Input). Таймер будет настроен так же как и в предыдущем примере с использованием одного таймера и аппаратным сбросом счётчика, а делается это всего лишь одной строчкой в Кубе…


Удалите все настройки у первого таймера, и сделайте как на картинке. Подчинённый режим, источник триггера и два канала станут неактивными — они настраиваются автоматически.


В конфиге нужно указать только предделитель и переполнение…


В разделе PWM Input CH1 указаны триггерный сигнал — TI1FP1, поступающий от первого канала, и то, что должно произойти — сброс счётчика. То есть эти две настройки как-бы должны находится в притушенных пунктах Slave Mode и Trigger Source, но в данном режиме они просто перенесены сюда, и изменить их нельзя.

В разделе Trigger Output (TRGO) Parameters можно настроить исходящие триггерные сигналы.


Включаем прерывание при захвате…




Запускам таймер:

/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
/* USER CODE END 2 */


В бесконечном цикле ничего нового:

while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  HAL_Delay(15); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(150);
          ...


В колбеке ловим данные:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // RISING с LOW на HIGH
		{
			//__HAL_TIM_SET_COUNTER(&htim2, 0x0000); // обнуление счётчика
		}

		else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) // FALLING с HIGH на LOW
		{
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); // чтение значения в регистре захвата/сравнения
			snprintf(trans_str, 63, "Pulse %lu mks\n", falling);
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}

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


Да, совсем забыл, если добавить захват и первого канала, то мы получим не только длину импульса но и период…

/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
HAL_TIM_IC_Start(&htim1, TIM_CHANNEL_1); // без прерывания
/* USER CODE END 2 */


void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // RISING с LOW на HIGH
		{
			//__HAL_TIM_SET_COUNTER(&htim2, 0x0000); // обнуление счётчика
		}

		else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) // FALLING с HIGH на LOW
		{
                        uint32_t rising = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1);
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); // чтение значения в регистре захвата/сравнения
			snprintf(trans_str, 63, "Pul %lu Per %lu\n", falling, rising);
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}


while (1)
  {
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_SET);
	  HAL_Delay(20); // длина импульса
	  HAL_GPIO_WritePin(pa5_GPIO_Port, pa5_Pin, GPIO_PIN_RESET);
	  HAL_Delay(30);
          ...



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


Можно сделать наоборот, первый канал запустить в режиме прерывания, а второй просто…

/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start(&htim1, TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1); 
/* USER CODE END 2 */


В колбеке ловить данные по прерыванию на первом канале…

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // колбек по захвату
{
	if(htim->Instance == TIM1)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) 
		{
                        uint32_t rising = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1);
			uint32_t falling = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_2); 

			snprintf(trans_str, 63, "Pul %lu Per %lu\n", falling, rising);
			HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
		}
	}
}

Результат будет такой же.

Извиняюсь за некоторую непоследовательность, просто таймеры у stm32 дюже нафаршированные, и вариантов настроек очень много, поэтому удержать всё в голове затруднительно.




Продолжая тему захвата сигнала, рассмотрим работу таймера с инкрементальным энкодером…



В Кубе указываем режим Encoder Mode


Я настроил таймер №3.

Как и в предыдущем примере, всё настроилось автоматически. Активировались (притушились) первый и второй каналы, к которым подключается энкодер.

На схеме это выглядит так…


У первого и второго канала для этого есть специальный интерфейс.

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


Механизм энкодера устроен так…


Схема энкодера с доп. кнопкой.

При повороте в одну сторону сначала контакт А замыкается с центральным (GND), а потом с небольшим запозданием контакт В. При повороте в другую сторону последовательность замыканий происходит в обратном порядке. Отсюда вытекает простейшая процедура обработки сигналов энкодера: при появлении сигнала на контакте A, проверяется состояние контакта B. Если он равен нулю, тогда увеличиваем счетчик, если равен единице, тогда уменьшаем счётчик. В настройках таймера это можно изменять.

На основании этой последовательности таймер определяет направление вращения, и соответственно увеличивает или уменьшает счётчик…




Конфигурация таймера…



Counter Period ⇨ 20 — при вращении в одну сторону счётчик будет увеличиваться до 20, обнуляться и снова увеличиваться. При вращении в другую сторону счётчик будет уменьшаться, и при переходе через ноль отсчёт начнётся от 20.

Чтобы счётчик не переходил через максимальное и минимальное значение, то есть просто увеличивался/уменьшался в рамках от 0 до 20, нужно в Counter Mode указать любое из "выравниваний по центру". Можно прямо сейчас сделать.

При каждом щелчке энкодера счётчик будет увеличиваться на два.

Encoder Mode ⇨ Encoder Mode TI1 — счёт будет вестись по сигналам от первого канала. Можно указать второй канал или оба. Если указаны оба канала, тогда за один щелчок счётчик будет увеличиваться на 4.

Polarity ⇨ Rising Edge — счёт ведётся по переднему фронту. Зависит от того как «подтянуты» каналы, на «землю» или на «плюс».

Input Filter — фильтр от дребезга.


Подключите контакты А и В энкодера к каналам (РА6, РА7) и подтяните их к «земле» резисторами 10 — 20кОм (можно воспользоваться внутренней подтяжкой). Центральный контакт соедините с «плюсом». Можно наоборот, каналы подтянуть к «плюсу», а центральный на «землю».


Запускаем процесс:

/* USER CODE BEGIN 2 */
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_1);
//HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_2); // если работать по второму каналу
/* USER CODE END 2 */


В бесконечном цикле выводим данные:

while (1)
  {
	  uint32_t count = __HAL_TIM_GET_COUNTER(&htim3);
	  uint8_t direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3);
	  snprintf(trans_str, 63, "Encoder %lu %s\n", count, direct ? "DOWN" : "UP");
	  //snprintf(trans_str, 63, "Encoder %lu %s\n", count, TIM3->CR1 & TIM_CR1_DIR ? "DOWN" : "UP");
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  HAL_Delay(150);
          ...


В переменную count копируем значение из счётчика.
В переменную direct копируем значение бита DIR из регистра CR1, определяющее направление счёта.

DIR = 0 — счёт идёт вверх.
DIR = 1 — счёт идёт вниз.


В закомментированной строчке показана работа непосредственно с регистром.


Если в Кубе активировать прерывание и запустить так:

/* USER CODE BEGIN 2 */
HAL_TIM_Encoder_Start_IT(&htim3, TIM_CHANNEL_1);
/* USER CODE END 2 */


Тогда можно забирать данные в колбеке:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) 
{
	if(htim->Instance == TIM3)
	{
		uint32_t count = __HAL_TIM_GET_COUNTER(htim);
		uint8_t direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(htim);
		snprintf(trans_str, 63, "Encoder %lu %s\n", count, direct ? "DOWN" : "UP");
		HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	}
}

Прошейтесь и повертите крутилку.


Теперь можно с помощью энкодера поуправлять светодиодом. Настройте таймер №4 для генерации ШИМ…


Указываем только предделитель и переполнение.


Добавляем запуск:

/* USER CODE BEGIN 2 */
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
/* USER CODE END 2 */


В бесконечном цикле записываем значение счётчика энкодера в регистр сравнения первого канала четвёртого таймера, короче говоря длиной импульса (Pulse) управляем:

while (1)
  {
	  uint32_t count = __HAL_TIM_GET_COUNTER(&htim3);
	  
	  __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, count); // длина импульса
	  
	  uint8_t direct = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3);
	  snprintf(trans_str, 63, "Encoder %lu %s\n", count, direct ? "DOWN" : "UP");
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  HAL_Delay(150);
          ...


Подключаем светик к РВ6, прошиваем, и вертим крутилку.

Чтоб было более плавно, увеличьте переполнения обоих таймеров.




Частотомер — используя несколько таймеров, можно превратить stm32 во вполне приличный измеритель входящей частоты. Этому посвещена отдельная статья.




Механизм XOR используется для подключения датчиков Холла с помощью которых можно измерять скорость вращения и управлять трёхфазными электродвигателями…



Второй и третий каналы таймера связываются с первым через логический элемент «исключающее ИЛИ». Во время вращения двигателя и срабатывания датчиков происходят захваты сигналов, текущее значение счётчика (CNT) копируется в регистр захвата/сравнения первого канала, счётчик тут же сбрасывается и отсчёт начинается заново.


Сброса счётчика происходит аппаратно, по сигналу TI1F_ED



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


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

Режим работы с датчиками активируется одной строчкой…




Конфиг…


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


У меня нет электромотора поэтому я просто имитирую работу одного датчика с помощью GPIO_Output.

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



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

/* USER CODE BEGIN 0 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(htim == &htim2)
	{
		char str[16] = {0,};
		snprintf(str, 16, "Speed: %lu\n", __HAL_TIM_GET_COMPARE(&htim2, TIM_CHANNEL_1));
		HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
	}
}
/* USER CODE END 0 */

Забираем значение в регистре сравнения и выводим в USART.

Запускаем таймер…

/* USER CODE BEGIN 2 */
HAL_TIMEx_HallSensor_Start_IT(&htim2);
/* USER CODE END 2 */


В бесконечном цикле имитируем датчик…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  // "датчик" №1
	  HAL_GPIO_WritePin(pa3_GPIO_Port, pa3_Pin, GPIO_PIN_SET);
	  HAL_Delay(10);
	  HAL_GPIO_WritePin(pa3_GPIO_Port, pa3_Pin, GPIO_PIN_RESET);
	  HAL_Delay(20);
          ...


Соединяем пин PA3 (это наш «датчик») с любым из трёх каналов таймера №2 (PA0, PA1, PA2), прошиваем микроконтроллер и смотрим…


Получаем время между импульсами в микросекундах. Погрешность создают функции HAL_Delay и snprintf, так что не обращайте внимания.

Можете активировать ещё пару пинов («датчиков»), задать разные паузы (в пределах 65мс) и подключить их к оставшимся двум каналам.



Подключение электродвигателя с датчиками Холла выглядит так…



Таймер №1 настраивается так…




Работает это примерно так…



Коммутация ключей происходит в обработчике прерывания таймера №2 в зависимости от состояния сигналов датчиков Холла.

Коэффициент заполнения PWM (длина импульса) задается потенциометром где-нибудь в бесконечном цикле.

Дальше описывать всё это хозяйство я не буду по двум причинам: во-первых у меня нет такого моторчика, а во-вторых, те кто работал с такими вещами лучше меня знают что да как. Если же вы хотите узнать побольше про управление электродвигателями, то могу посоветовать этот ресурс — человек очень хорошо и понятно всё объясняет. У него там есть цела куча статей и видосов на эту тему в контексте stm32.



Ну и последнее — аппаратный запуск ADC таймером описан в статье про АЦП.




На этом всё.


Всем спасибо


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


Форум

Телеграм-чат STM32


  • 0
  • 8394
Поблагодарить автора




Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.