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

Первый, на мой взгляд, хороший пример, это извлечение звука ногодрыгом. Здесь используется сразу два таймера. Принцип я когда то уже описывал в 7 уроке AVR. Для тех кому лень искать: звук это колебание воздуха, чтобы воздух колебался, мы гоняем его колебанием мембраны динамика. Мембрана колеблется за счет того, что внутри динамика есть катушка. Когда подаем ток, катушка притягивается к постоянному магниту, когда не подаем ток, то катушка возвращается в исходное состояние. К катушке непосредственно прикреплена мембрана, таким образом она колеблется.

Чем быстрее колеблется мембрана, тем «писклявее» звук, чем медленнее тем более «пердячий», «басистый» 🙂 То каким образом мы будем втягивать, будет влиять на форму звука. Мы можем врубить сразу максимум тока и динамик резко втянется до упора, а можем постепенно прибавлять ток и катушка будет постепенно втягиваться, т.е. формально это закон, по которому изменяется ток за секунду. Это будет влиять на то, какой звук слышим, например гитара и пианино играют одну и ту же ноту, но звучат совершенно по разному. Музыканты любят называть это «тембром».

Самый простой пример это формировать меандр, т.е. сначала включить на полную ток, а потом выключить. Как раз в этом нам поможет таймер. В прошлый раз для формирования меандра я использовал прерывание, на самом деле не обязательно так делать. У stm32 есть замечательный режим Output compare. Мы настраиваем таймер на определенную частоту и задаем ему количество тиков (период). Как только он отсчитает это количество, ножка сама по себе инвертируется, а таймер перезапустится. Т.е. чтобы играть ноты нам просто достаточно менять значение регистра задающего период таймера.

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

Аппаратная часть проста. Динамик подключается к ножке через транзистор.
sch_sound

Программная часть. Я решил использовать ножку PD15, просто потому что она с краю, как раз на ней оказался TIM4_CH4. В окошке слева нужно выбрать тактирование Clock source и output compare для четвертого канала.
tim4_Ch4

Для третьего таймера достаточно включить тактирование
tim3_interrupt

Далее следует настройка общего тактирования. Для этого задействуем внешний кварц. Хоть мы и используем генератор кода, но на этом моменте нужно достаточно четко представлять, на какой шине сидит таймер, ибо частота на которой работает таймер не факт что равна частоте на которой работает камень ибо по пути есть куча предделителей шин. Поэтому не брезгуем референс мануалом и смотрим что таймер4 сидит на APB1.
pwm_clk_stm32

В разделе configuration настраиваем предделитель и прерывание для таймера3. На вкладке NVIC settings нужно поставить галку TIM3 global interrupt
tim3_setup

Для таймера4 настраиваем период и предделитель.
tim4_setup

Далее генерим проект, main.c нас в принципе не интересует, ибо весь код выполняется в прерывании. Поэтому нас интересует только stm32f4xx_it.c

#include "stm32f4xx_hal.h"
#include "stm32f4xx.h"
#include "stm32f4xx_it.h"
#include <stdint.h>
 
#define SIZE 96
 
#define BPM 320 
#define N16 BPM/16
#define N8 BPM/8
#define N4 BPM/4
#define N2 BPM/2
#define N1 BPM
 
//first octave
#define C1 152
#define Cd1 143 
#define D1 135
#define Dd1 127
#define E1 120
#define F1 113
#define Fd1 107
#define G1 101
#define Gd1 95
#define A1 90
#define B1 84
#define H1 80
 
//second octave
#define C2 75
#define Cd2 71 
#define D2 67
#define Dd2 63
#define E2 60
#define F2 56
#define Fd2 53
#define G2 50
#define Gd2 47
#define A2 44
#define B2 42
#define H2 39
#define P 0
 
uint16_t mass2[SIZE]=//{E2,N1,E2,N1,E2,N8};
{
E2,N16,E2,N8,E2,N16,P,N16,C2,N16,E2,N8,G2,N4,G1,N4,
C2,N8,P,N16,G1,N16,P,N8,E1,N16,P,N16,A1,N8,H1,N16,P,N16,B1,N16,A1,N8,//38
G1,N16,E2,N16,G2,N16,A2,N8,F2,N16,G2,N16,P,N16,E2,N8,P,N16,C2,N16,D2,N16,H1,N16, //62
C2,N8,P,N16,G1,N16,P,N8,E1,N16,P,N16,A1,N8,H1,N16,P,N16,B1,N16,A1,N8, //84
};
 
extern TIM_HandleTypeDef htim3;
uint16_t x = 0, i = 0, g = 0, p = 0;
 
void SysTick_Handler(void)
{
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();
}
 
void TIM3_IRQHandler(void)
{
  /* USER CODE BEGIN TIM3_IRQn 0 */
 
  /* USER CODE END TIM3_IRQn 0 */
  HAL_TIM_IRQHandler(&htim3);
  /* USER CODE BEGIN TIM3_IRQn 1 */
 
	//choose note freq	
	if(p == 0)
	{
		//play
		TIM4->CNT = 0;
		TIM4->ARR = mass2[i];
		i++;
		g = 0;
 		p = 1;
	}
	else
	{
		g++;
		if(g > mass2[i])
		{
			p = 0;
			i++;
			g = 0;
		}
 
		if(g > (mass2[i]-5))
		{
			TIM4->CNT = 0;
			TIM4->ARR = 0;
		}
 
	}
	if(i == (SIZE))
	{
			g = 0;
			i = 0;
			p = 0;
	}
}

Пояснения сделать стоит, есть массив mass2 содержащий ноты c1-до первой октавы,e2-ми второй, нечто похожее на то что используют гитаристы. Следующий за нотой элемент массива это длительность ноты, т.е. ноты идут через одну и длительности через одну. Сама нота это тупо дефайн, с заранее посчитанной величиной регистра формирующий период таймера(ARR).

Допустим, нужно посчитать сколько нужно тиков, чтобы сформировать частоту, например нота До 1 октавы 261Гц. Для этого исходные 4 000 000 таймера делим на предделитель 50. Предделитель просто позволяет сделать величины нот в разумными, т.е. в пределах uint16_t.
4000 000/50= 80 000 это рабочая частота после всех делителей.

Теперь нужно найти число, которое нужно положить в ARR, чтобы из исходных 80 000 получить нужные нам 261Гц.
(80 000/(261*2))-1= 152

Почему тут умножаем на 2, потому что при достижении таймером регистра ARR, ножка инвертирует свое состояние, а нам нужно чтобы она успела включиться и выключиться.

Примечательно то, что если значение счетного регистра CNT превысит значение ARR, то вся эта система зависнет. Поэтому при каждой смене ноты, лучше обнулять CNT руками. Флаг P показывает нужно ли играть ноту или нет, g отсчитывает длительности нот, i это текущая нота. P в массиве это пауза. В общем то и все.

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

Выглядит это чудо так:
12a

Хочется отметить, что рвет мотор просто жестко, такая мелочь, а того и гляди из рук вырвется. Кроме того он высокооборотистый. К сожалению даташит, который был в комплекте как то не блистал технической информацией, поэтому кроме того, что работает он на 12В, жрет около 1А. Управляется хоть от 5В, хоть от 3.3В норм.

С программной части все довольно таки просто. На регулятор подается ШИМ с частотой 50Гц. Все аналогично управлению сервоприводом. При запуске скважность должна быть 900мкс иначе движок не стартанет(такая вот защита от дурака), чтобы руки винтом не пообрубало. Если все ок, то он пропищит и тогда можно врубать газу, крайнее значение 2000мкс.

Чтобы настроить возьмем тот же TIM4.
pwm_setup_stm32

Весь код в main, для удобства добавил кнопку — жмакаем, заполнение увеличивается на 50 мкс, мотор газует. Если достигли максимума, то начинаем с минимальных оборотов.

#include "stm32f4xx_hal.h"
 
TIM_HandleTypeDef htim4;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM4_Init(void);
 
 
int main(void)
{
 
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM4_Init();
 
  HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4);
 
  uint16_t x = 900;
 
  while (1)
  {
	if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==1)
	{
	   if(x < 2000)
	   {
	       x = x + 50;
	   }
	   else
	   {
	       x = 900;
	   }
                //write pwm  
		TIM4->CCR4 = x;
                //delay
		for(uint32_t i= 0; i < 100000; i++);			
	}
  }
}
 
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct;
  RCC_ClkInitTypeDef RCC_ClkInitStruct;
 
  __PWR_CLK_ENABLE();
 
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);
 
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  HAL_RCC_OscConfig(&RCC_OscInitStruct);
 
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1
                              |RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0);
 
  HAL_RCC_EnableCSS();
 
}
 
/* TIM4 init function */
void MX_TIM4_Init(void)
{
 
  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
  TIM_OC_InitTypeDef sConfigOC;
 
  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 3;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 20000;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim4);
 
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig);
 
  HAL_TIM_PWM_Init(&htim4);
 
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig);
 
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 900;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_ENABLE;
  HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_4);
 
}
 
void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct;
 
  /* GPIO Ports Clock Enable */
  __GPIOE_CLK_ENABLE();
  __GPIOC_CLK_ENABLE();
  __GPIOH_CLK_ENABLE();
  __GPIOA_CLK_ENABLE();
  __GPIOB_CLK_ENABLE();
  __GPIOD_CLK_ENABLE();
 
  /*Configure GPIO pin : PA0 */
  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_EVT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

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

для ШИМ канала нужно вызвать функцию:

HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_4);

Для регистра сравнения:

HAL_TIM_OC_Start(&htim4,TIM_CHANNEL_4);

Для прерывания:

HAL_TIM_Base_Start_IT(&htim3);

Видео работы всего этого безобразия.

Прошивки пока не будет( Ибо HAL исходники больно жирные. Как разберусь с git, туда закину.

11 комментариев: Примеры таймеров STM32

  • в STM32CubeMX в настройках проекта можна выбрать:
    1) копировать все библиотеки в папку с проектом
    2) копировать только нужные
    3) только создавать ссылки на библиотеки (в проекте выбраной IDE прописать идреса до папок с инклюдами и либами туда где они хранятся с CubeMX)

  • Да, но правильно когда все копии лежат рядом с проектом. Иначе будет фигня.

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

  • такой вопрос, при использовании HAL основной код и прерывания разделяются на 2 файла, как связать код, чтобы глобально заданная переменная изменялась в прерывании таймера и обрабатывалась в основном коде?
    грубо говоря в прерывании таймера переменная увеличивается на единицу каждый тик, а в основном коде следим за этим значением.
    при добавлении в прерывание глобально обозначенной переменной выдает identifier «count» is undefined, но в основном коде она обозначена

  • Как вызвать функцию в прерывании из main?

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

  • Короче, как обычно, банальная невнимательность — забыл запустить таймер HAL_TIM_Base_Start_IT(&htim1); и тупил почему не обрабатывается переменная в прерывании
    extern само собой

  • Странная статья, причем с косяками.
    В первом примере автор забыл указать(но написал почему то в конце статьи) две строчки в файле main.c после инициализации таймеров их запуск —
    HAL_TIM_OC_Start(&htim4,TIM_CHANNEL_4);
    HAL_TIM_Base_Start_IT(&htim3);
    Далее я вообще не понял — автор говорит ставьте галочку Internal Clock, а чуть ниже пишет — «задействуем внешний кварц». Что за ???
    Ну и напоследок крайне рекомендую в файле main.c выставить htim3.Init.Period = 40000;
    Или около того. Ибо по умолчанию 0 и это ну очень быстро :)))

  • Читайте внимательнее, и про internal clock, и про внешний кварц, и про старт таймера, все написано.

  • На сколько я понимаю если включить внешнее тактирование в пунктике Slave Mode,
    internal clock — автоматом выключается. Что я должен внимательно прочитать? Не нашел. Зачем тогда вообще про internal clock писать, если он потом отключается? Смысл?
    Про старт написано, но в конце второго примера — сиё не есть гуд.
    Ну и прерывание таймера TIM3 нужно скорость уменьшить — про это в статье не написано.

    Ну а вообще у себя на stm32f103T8 прогу завел — работает отлично.

  • Нет, внешнее тактирование это внешнее тактирование = HSE, то откуда берется частота для камня, в принципе это не имеет большого значения у стм достойный внутренний генератор, можно брать HSI. Internal clock это галка в пункте таймера, откуда ему тактироваться, если ее не врубить таймер работать не будет.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Последние комментарии
  • Загрузка...
Счетчик
Яндекс.Метрика