Таймеры stm32 HAL - часть вторая
Здравствуйте.
Это продолжение статьи про таймеры stm32.
В прошлой части мы разбирались с тактированием таймера и выходными сигналами (ШИМ и прочее), а сейчас пришла пора познакомится с входными сигналами, и более сложными механизмами таймера.
Вначале нам нужно познакомиться с режимом захвата сигнала, а потом будем изучать схему таймера, и всё остальное.
Захват — это в некотором роде противоположность сравнению. Если в режиме сравнения (в момент совпадения значения счётчика со значением записанным в регистре CCR) подаётся/переключается выходной сигнал, то в режиме захвата всё наоборот, в момент появления какого-то сигнала на входе канала, в регистр CCR записывается текущее значение счётчика.
CCR — Capture/Compare Register.
Один из вариантов использования режима захвата, это измерение длины импульса входящего сигнала…
t — длина импульса.
В Кубе делаем так…
Если не указываем источник тактирования, то по умолчанию включается системная частота таймера.
Здесь мы настраиваем два канала — основной (direct) и косвенный (indirect), при этом у нас активировался только один вход (РА8).
Тут дело вот в чём: чтоб измерить длину импульса, нам нужно захватить сигнал два раза, первый раз в момент перехода с LOW на HIGH (передний фронт), а второй в момент перехода с HIGH на LOW (задний фронт). Соответственно первый канал будет ловить передний фронт, а второй — задний. Внутри микроконтроллера есть механизм позволяющий первому и второму каналам обмениваться сигналами, благодаря которому мы можем подключить внешний сигнал только к одному входу. Чуть попозже, это будет рассмотрено на схеме подробно.
Таймер настраиваем так…
Измерение длины импульса происходит так:
• таймер тикает, счётчик (Counter Period) считает, переполняется, сбрасывается, и вновь считает. В общем работает как обычно.
• приходит передний фронт ⇨ происходит захват на первом канале — текущее значение счётчика копируется в регистр CCR1, но нам интересно не оно, а генерируемое прерывание в обработчике которого мы обнуляем счётчик.
• приходит задний фронт ⇨ происходит захват на втором канале — текущее значение счётчика копируется в регистр CCR2.
Поскольку один тик таймера у нас равен одной микросекунде, то соответственно значение в счётчике в момент захвата равно количеству микросекунд прошедших с момента появления переднего фронта. Нам остаётся только вызвать колбек прерывания по захвату на втором канале, и посмотреть что лежит в регистре CCR2, это и будет длина импульса.
Включите прерывание…
В программе добавляем массив для вывода инфы в 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);
}
}
}
И добавляем запуск таймера в обычном режиме прерывания…
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
/* USER CODE END 2 */
При получении переднего фронта (HAL_TIM_ACTIVE_CHANNEL_1) обнуляем счётчик и переменную. При каждом последующем переполнении, пока не пришёл задний фронт, эта переменная будет увеличиваться на единицу. При получении заднего фронта (HAL_TIM_ACTIVE_CHANNEL_2) копируем содержимое регистра CCR в переменную
Цифру 65000 можно заменить на HALовский макрос —
snprintf(trans_str, 63, "Pulse %lu\n", falling + (__HAL_TIM_GET_AUTORELOAD(&htim1) * count_overflow));
В результате получим искомую длину импульса…
Теперь можно поэкспериментировать с любой длиной импульса.
точные показания
Чтобы показания были точными нужно во-первых указать предделитель с вычетом единицы (про это написано в первой части), то есть так — 71.
И во-вторых, функцияHAL_Delay() не совсем точная, поэтому вместо неё можно использовать DWT (Data Watchpoint and Trace unit) — это модуль трассировки и поддержки контрольных точек, который умеет считать такты, а значит измерять время.
В программе добавляем несколько вещей…
И теперь длину импульса указываем так:
Результат…
Исходник delay_us и delay_ms.
И во-вторых, функция
В программе добавляем несколько вещей…
/* 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права нарисованы каналы, когда они работают как выходы (основные и комплементарные). Частота поступающая с предделителя
Слева-снизу вход break-сигнала (TIMx_BKIN). Сигнал поступающий либо с пина TIMx_BKIN, либо от системы CSS воздействует на каналы через
Выше обозначены те же самые каналы что и справа, только когда они работают как входы. Внешний сигнал, поступивший на TIMx_CH1, проходит через мультиплексор механизма
Механизм
TIMx_ETR — вход для внешнего тактового или триггерного (см. ниже) сигнала.
Всё остальное, что находится выше регистра
Дальше изучать схему лучше всего на практике, поэтому давайте вернёмся к нашему последнему примеру и немного его изменим.
В Кубе делаем так…
Пояснения этих пунктов будет позже, а все остальные настройки оставьте как есть.
Настраиваем 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мс, но сейчас это не важно. Давайте разберёмся как же так получается, что счётчик мы не обнуляем, а данные получаем правильные.
Таймеры, при возникновении тех или иных событий, могут аппаратно посылать различные
Итак, возвращаемся к вопросу — как же у нас сейчас работает таймер? Да очень просто, вся магия кроется в этих двух пунктах…
•
другие варианты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…
В итоге, сейчас наш таймер работает следующим образом…
На первом канале появляется передний фронт ⇨ превращается в сигнал TI1FP1
другие варианты Trigger SourceTrigger Source ⇨ ETR1 — триггером будет служить сигнал поступающий на пин TIM1_ETR .
Trigger Source ⇨ TI1F_ED — Если выбрать его в качестве триггера, то счётчик будет сбрасываться (или делать что-то другое, в зависимости от того, что указано в Slave Mode) как при низком уровне на входе первого канала, так и при высоком. То есть любые сигналы возникающие на входе первого канала будут являться триггером. Есть только у первого канала.
Trigger Source ⇨ ITRx — триггерные сигналы от других таймеров (см. далее).
Итак, сейчас мы можем измерять импульс в пределах 65мс, а чтоб измерять большую длину, нужно опять же подсчитывать количество переполнений таймера №1. В самом начале статьи мы делали это за счёт увеличения переменной (count_overflow) в обработчике прерывания по переполнению. Сейчас же вместо этой переменной будем использовать счётчик таймера №2. То есть, таймер №1 будет обнулять сам себя
У таймера №1 оставляем всё как было…
Кроме одного пункта…
Раздел
•
другие варианты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, и выдержав небольшую паузу стартанёт сам. Таким образом оба таймера запустятся синхронно.
Вух, надеюсь вы всё поняли.
Этот режим полезен когда требуется синхронизация двух таймеров одним внешним событием. То есть, допустим нам нужно чтоб таймер №1 запустил таймеры №2 и №3 синхронно, но, поскольку таймер может послать только один триггерный сигнал (только одному таймеру), то таймер №1 будет посылать триггер таймеру №2, а таймер №2 в свою очередь пошлёт триггер таймеру №3. Так вот, если
Вух, надеюсь вы всё поняли.
Включаем прерывание по захвату…
Таймер №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
Удалите все настройки у первого таймера, и сделайте как на картинке. Подчинённый режим, источник триггера и два канала станут неактивными — они настраиваются автоматически.
В конфиге нужно указать только предделитель и переполнение…
В разделе
В разделе
Включаем прерывание при захвате…
Запускам таймер:
/* 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 дюже нафаршированные, и вариантов настроек очень много, поэтому удержать всё в голове затруднительно.
Продолжая тему захвата сигнала, рассмотрим работу таймера с инкрементальным энкодером…
В Кубе указываем режим
Я настроил таймер №3.
Как и в предыдущем примере, всё настроилось автоматически. Активировались
На схеме это выглядит так…
У первого и второго канала для этого есть специальный интерфейс.
В данном режиме, щелчки энкодера это просто импульсы, которые тактируют таймер и изменяют значение в счётчике.
Механизм энкодера устроен так…
При повороте в одну сторону сначала контакт А замыкается с центральным (GND), а потом с небольшим запозданием контакт В. При повороте в другую сторону последовательность замыканий происходит в обратном порядке. Отсюда вытекает простейшая процедура обработки сигналов энкодера: при появлении сигнала на контакте A, проверяется состояние контакта B. Если он равен нулю, тогда увеличиваем счетчик, если равен единице, тогда уменьшаем счётчик. В настройках таймера это можно изменять.
На основании этой последовательности таймер определяет направление вращения, и соответственно увеличивает или уменьшает счётчик…
Конфигурация таймера…
Чтобы счётчик не переходил через максимальное и минимальное значение, то есть просто увеличивался/уменьшался в рамках от 0 до 20, нужно в
При каждом щелчке энкодера счётчик будет увеличиваться на два.
Подключите контакты А и В энкодера к каналам (РА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 используется для подключения датчиков Холла с помощью которых можно измерять скорость вращения и управлять трёхфазными электродвигателями…
Второй и третий каналы таймера связываются с первым через логический элемент «исключающее ИЛИ». Во время вращения двигателя и срабатывания датчиков происходят захваты сигналов, текущее значение счётчика (
Сброса счётчика происходит аппаратно, по сигналу 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 таймером описан в статье про АЦП.
На этом всё.
Всем спасибо
Эта статья, равно как и предыдущая, не претендуют на полноценное описание так как охватить все возможные комбинации различных функций таймера достаточно сложно, поэтому обязательно читайте мануал, и творите
Телеграм-чат istarik
Телеграм-чат STM32
- 0
- stD
147270
Поддержать автора
Комментарии (0)