image

Керамический пьезоизлучатель (buzzer) — простая деталь, наравне со светодиодом требующая минимального набора ресурсов для управления и настолько же легко подключаемая к микроконтроллеру. Как и светодиоду с возможностью плавной регулировки яркости, от микроконтроллера ему требуется не более одного канала таймера и внешний вывод.

Много в интернете уроков «Подключаем пищалку к ардуино», только вот заканчиваются они проигрыванием «В траве сидел кузнечик» или озвучкой срабатывания RFID датчика. Наверное тем, кто занят этим профессионально и серьезно, не до ведения блогов и записи видеоуроков.

А ведь миниатюрный керамический динамик — шаг в сторону более дружелюбного интерфейса с человеком. Нажатия кнопок, касания сенсорной панели, реакция на различные события… Такая вот обратная связь в виде звукового отклика!

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

Железки


Использовать будем самодельную плату с микроконтроллером stm32f103 в 144-ногом корпусе и пьезоизлучатель PKLCS1212E40A1-R1 фирмы Murata.

image

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

image

Пьезодинамик включен через транзистор и сделано это для большей громкости звучания (раскачивается амплитудой 5V), хотя можно вешать напрямую на ногу микроконтроллера (3.3V). Документация на него содержит АЧХ, из ко��орого видно, что максимальная амплитуда достигается при входном сигнале 4 кГц. Да и в парт-номере компонента (PKLCS1212E40A1-R1) это отражено (Expressed resonant frequency by two-digit alphanumerics. The unit is in 100 hertz (Hz.) 4kHz (4000Hz) is denoted as «40.»).

image

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

u16 GL_BuzzerAllNotes[] = {
	261, 277, 294, 311, 329, 349, 370, 392, 415, 440, 466, 494,
	523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
	1046, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
	2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
	4186, 4434, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902};

#define OCTAVE_ONE_START_INDEX		(0)
#define OCTAVE_TWO_START_INDEX		(OCTAVE_ONE_START_INDEX + 12)
#define OCTAVE_THREE_START_INDEX	(OCTAVE_TWO_START_INDEX + 12)
#define OCTAVE_FOUR_START_INDEX		(OCTAVE_THREE_START_INDEX + 12)
#define OCTAVE_FIVE_START_INDEX		(OCTAVE_FOUR_START_INDEX + 12)

#define BUZZER_DEFAULT_FREQ		(4186) //C8 - 5th octave "Do"
#define BUZZER_DEFAULT_DURATION		(20) //20ms
#define BUZZER_VOLUME_MAX		(10)
#define BUZZER_VOLUME_MUTE		(0)

Драйвер пьезодинамика


Пьезодинамик — не светодиод, широтно-импульсной модуляцией с постоянной частотой и переменной скважностью импульсов тут не отделаешься. Ножку, на которой висит управляющий транзистор (PA15, TIM2, CH1), настраиваем в режиме PWM:

void BuzzerConfig(void)

void BuzzerConfig(void)
{
	GPIO_InitTypeDef GPIO_Options;
	TIM_TimeBaseInitTypeDef TIM_BaseOptions;
	TIM_OCInitTypeDef TIM_PWM_Options;

	RCC_APB2PeriphClockCmd(BUZZER_CLK_PINS | RCC_APB2Periph_AFIO, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);

	//PA.15 TIM2_CH1, BUZZER
	GPIO_Options.GPIO_Pin = BUZZER_PIN;
	GPIO_Options.GPIO_Speed = GPIO_Speed_10MHz;
	GPIO_Options.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_Init(BUZZER_PORT, &GPIO_Options);

	TIM_BaseOptions.TIM_Period = 2 * BUZZER_VOLUME_MAX - 1;
	TIM_BaseOptions.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_BaseOptions.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM2, &TIM_BaseOptions);

	TIM_PWM_Options.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_PWM_Options.TIM_OutputState = TIM_OutputState_Enable;
	TIM_PWM_Options.TIM_OutputNState = TIM_OutputNState_Disable;
	TIM_PWM_Options.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_PWM_Options.TIM_Pulse = 0;
	TIM_OC1Init(TIM2, &TIM_PWM_Options);

	TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
	TIM_ARRPreloadConfig(TIM2, ENABLE);

	TIM_Cmd(TIM2, ENABLE);
}

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

Очевидно, что смена частоты сигнала приводит к изменению звучания, а вот как быть со скважностью импульсов? Я не нашел ничего полезного по этому вопросу в документации, но было предположение, что изменение скважности влечёт за собой смену громкости. Если это правда, то меандр (скважность = 50%) будет давать максимальную громкость, а схождение к 0% (или симметрично, к 100%) ослабит громкость, в конце концов, до нуля. Реально это работает так себе, поэтому я только включаю и выключаю пищалку, используя два следующих макроса:

#define BUZZER_VOLUME_MAX	10
#define BUZZER_VOLUME_MUTE	0

BUZZER_VOLUME_MAX — это такое количество импульсов, которое дважды уложится в необходи��ый период работы, который обратно пропорционален частоте. Нужную частоту (установку) мы знаем, период тоже понятен (x2), а значит и предделитель для таймера найти не составит труда. В STM32 это любое число от 1 до 0xFFFF.

Оборачиваем все действия в функцию установки частоты:


void BuzzerSetFreq(u16 freq)
{
	TIM2->PSC = (SYSCLK_FREQ  / (2 * BUZZER_VOLUME_MAX * freq)) - 1; //prescaller
}

И смена скважности для задания громкости:


void BuzzerSetVolume(u16 volume)
{
	if(volume > BUZZER_VOLUME_MAX)
		volume = BUZZER_VOLUME_MAX;

	TIM2->CCR1 = volume;
}

Всё, драйвер пищалки готов. Можно сыграть что-нибудь, предварительно создав массив частот (и длительностей неплохо бы).

Happy Birthday

u32 HappyBirthday[] = {
	262, 262, 294, 262, 349, 330, 262,
	262, 294, 262, 392, 349, 262, 262,
	523, 440, 349, 330, 294, 466, 466,
	440, 349, 392, 349};

for(i = 0; i < sizeof(HappyBirthday) / sizeof(u32); i++)
{
	BuzzerSetFreq(HappyBirthday[i]);
	BuzzerSetVolume(BUZZER_VOLUME_MAX);
	DelayTime(400);
	BuzzerSetVolume(BUZZER_VOLUME_MUTE);
}

Пьезодинамик, как совместно используемый ресурс


Глобальная идея состоит в создании удобного интерфейса псевдопараллельного доступа различных задач к аппаратному модулю пьезодинамика средствами FreeRTOS. О самой FreeRTOS рассказывать не буду, эта тема не для одной статьи, которых уже очень не мало (в том числе и неплохая онлайн документация на www.freertos.org. На русском могу посоветовать этот ресурс).

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

typedef struct
{
	u16 freq;
	u16 volume;
	u16 duration;
} BuzzerParameters_t;

Для использования пищалки в качестве ресурса, которому любая задача может отдать на «озвучивание» какие-то данные, будем использовать стандартный механизм межзадачной коммуникации и синхронизации FreeRTOS — очередь.

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

Создадим очередь длиной 10 элементов, состоящую из кирпичиков типа BuzzerParameters_t:

#define BUZZER_QUEUE_LEN	10
QueueHandle_t BuzzerQueue = xQueueCreate(BUZZER_QUEUE_LEN, sizeof(BuzzerParameters_t);

Обработкой событий пищалки будет заниматься задача динамика. Задачи во FreeRTOS — это маленькие подпрограммы, имеющие точку входа и бесконечный цикл, return из которого запрещен (допускается либо приостановка задачи, либо удаление). До начала выполнения задачу нужно создать, передав первым параметром указатель на функцию задачи, а последним — необязательный хендл.

TaskHandle_t BuzzerHandle;

xTaskCreate(vTask_BuzzerBeep, "BuzzerBeep", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &BuzzerHandle);

В бесконечном цикле задача будет ждать появления данных в очереди. Параметр portMAX_DELAY означает, что задача заблокирована планировщиком до тех пор, пока очередь пуста. Как только это становится не так, драйвер пищалки инициализируется переданными через очередь параметрами, а считанный элемент удаляется из очереди (если удалять не требуется, есть функция xQueuePeek()).
Вместо задержки, основанной на бездействии микроконтроллера в течение какого-то времени, используется функция vTaskDelay(), блокирующая задачу на заданное количество времени в миллисекундах (на самом деле, на количество системных тиков ОСРВ, но у меня 1 тик = 1 мс). Таким образом, задача блокируется снова на время воспроизведения звука, а по истечении времени блокировки прекращает его генерацию.

void vTask_BuzzerBeep(void *pvParameters)
{
	BuzzerParameters_t buzzerParameters;

	for(;;)
	{
		xQueueReceive(BuzzerQueue, &buzzerParameters, portMAX_DELAY);

		BuzzerSetFreq(buzzerParameters.freq);
		BuzzerSetVolume(buzzerParameters.volume);
		vTaskDelay(buzzerParameters.duration);
		BuzzerSetVolume(BUZZER_VOLUME_MUTE);
	}
}

Выглядит несложно и логично, в отличие от шаманства с таймерами, прерываниями и флагами без использования ОСРВ. Попробуем теперь этот механизм в деле.

Дано:

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

image

Начнём с кнопки. Она может находится в одном из трёх состояний:

typedef enum
{
	BUTTON_RELEASED = 0,
	BUTTON_SHORT_PRESSED,
	BUTTON_LONG_PRESSED
} BUTTON_PARAMETERS_t;

Инициализируем ножку микроконтроллера, настроим прерывание:

void StartButtonConfig(void)
void StartButtonConfig(void)
{
	GPIO_InitTypeDef GPIO_Options;
	EXTI_InitTypeDef EXTI_Options;
	NVIC_InitTypeDef NVIC_Options;

	RCC_APB2PeriphClockCmd(START_BUTTON_CLK_PINS | RCC_APB2Periph_AFIO, ENABLE);

	GPIO_Options.GPIO_Pin = START_BUTTON_PIN;
	GPIO_Options.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(START_BUTTON_PORT, &GPIO_Options);

	GPIO_EXTILineConfig(START_BUTTON_PORTSOURCE, START_BUTTON_PINSOURCE);

	EXTI_Options.EXTI_Line = START_BUTTON_EXTI_LINE;
	EXTI_Options.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_Options.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_Options.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_Options);

	NVIC_Options.NVIC_IRQChannel = EXTI2_IRQn;
	NVIC_Options.NVIC_IRQChannelPreemptionPriority = 13;
	NVIC_Options.NVIC_IRQChannelSubPriority = 0;
	NVIC_Options.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_Options);
}

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

void EXTI2_IRQHandler(void)
void EXTI2_IRQHandler(void)
{
	static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
	EXTI_InitTypeDef EXTI_Options;

	EXTI_ClearITPendingBit(EXTI_Line2);

	EXTI_Options.EXTI_Line = EXTI_Line2;
	EXTI_Options.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_Options.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_Options.EXTI_LineCmd = DISABLE;
	EXTI_Init(&EXTI_Options);

	xSemaphoreGiveFromISR(StartButtonSemaphore, &xHigherPriorityTaskWoken);

	if(xHigherPriorityTaskWoken == pdTRUE)
	{
		portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
	}
}

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

К этому времени задача vTask_GetStartButton() с хендлом StartButtonHandle уже должна быть создана и заблокирована функцией xSemaphoreTake(), ожидающей семафора из прерывания. Логика работы следующая:

  1. Ждем, пока xSemaphoreTake() получит желаемое из прерывания
  2. Пикаем динамиком (с помощью очереди, ага!) и блокируем задачу на 1/4 секунды
  3. Пикаем каждые 100 мс в течение 300 мс, если кнопка в зажатом состоянии. Используем разные ноты в сторону повышения частоты из массива GL_BuzzerAllNotes[]
  4. В бесконечном цикле ждем, пока кнопку отпустят окончательно (обязательно внутри делаем задержку средствами ОСРВ, иначе ожидание заберет все процессорное время себе — а вдруг пользователь поставит бутылку виски на кнопку, как это было в Silicon Valley =) )
  5. Определяем по переменной notePointer, как долго удерживали кнопку (BUTTON_LONG_PRESSED или BUTTON_SHORT_PRESSED)
  6. Пикаем в последний раз, возобновляем реакцию на прерывание

Но лучше прочесть комментарии в коде — они более последовательны:

void vTask_GetStartButton(void *pvParameters)
void vTask_GetStartButton(void *pvParameters)
{
	BuzzerParameters_t buzzerLocalParameters;
	u32 localStartButtonState;
	EXTI_InitTypeDef EXTI_Options;
	u32 notePointer = 0;

	EXTI_Options.EXTI_Line = START_BUTTON_EXTI_LINE;
	EXTI_Options.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_Options.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_Options.EXTI_LineCmd = ENABLE;

	buzzerLocalParameters.volume = BUZZER_VOLUME_MAX;
	buzzerLocalParameters.duration = BUZZER_DEFAULT_DURATION;

	/*
	 * first semaphore take after creation (NEED!! it issued after power up)
	 */
	xSemaphoreTake(StartButtonSemaphore, portMAX_DELAY);

	for(;;)
	{
		/*
		 * take semaphore from button interrupt
		 */
		xSemaphoreTake(StartButtonSemaphore, portMAX_DELAY);

		/*
		 * buzzer "pick" on button click and wait
		 */
		buzzerLocalParameters.freq = NOTE_C7;
		xQueueSend(BuzzerQueue, (void *)&buzzerLocalParameters, portMAX_DELAY);
		vTaskDelay(250);

		/*
		 * "pick" new note while button pressed, but not more 3 times
		 */
		while(GPIO_ReadInputDataBit(START_BUTTON_PORT, START_BUTTON_PIN) == 1)
		{
			buzzerLocalParameters.freq = GL_BuzzerAllNotes[OCTAVE_FOUR_START_INDEX + notePointer];
			xQueueSend(BuzzerQueue, (void *)&buzzerLocalParameters, portMAX_DELAY);
			vTaskDelay(100);

			if(notePointer++ >= 3)
				break;
		}

		/*
		 * wait while button pressed
		 */
		while(GPIO_ReadInputDataBit(START_BUTTON_PORT, START_BUTTON_PIN) == 1)
		{
			vTaskDelay(100);
		}

		localStartButtonState = (notePointer >= 3) ? (BUTTON_LONG_PRESSED) : (BUTTON_SHORT_PRESSED);
		xQueueSend(StartButtonQueue, (void *)&localStartButtonState, 0);

		/*
		 * "pick" the last time and re-enable interrupt on click
		 */

		buzzerLocalParameters.freq = NOTE_C8;
		xQueueSend(BuzzerQueue, (void *)&buzzerLocalParameters, portMAX_DELAY);

		EXTI_Init(&EXTI_Options); //Enable interrupt (disabled in interrupts.c)
		notePointer = 0;

		vTaskDelay(100);
	}
}

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

StartButtonQueue = xQueueCreate(1, sizeof(u32));

После обработки нажатия очередь будет хранить результат до тех пор, пока какая-либо задача не считает его оттуда.

Тут стоит отдельно заострить внимание на политике добавления в очередь данных. Нам в помощь третий параметр функции xQueueSend(). Если это 0 и очередь заполнена, то игнорируем запись и идем дальше по коду. portMAX_DELAY наоборот же, позволяет блокировать выполнение задачи, пока в очереди не будет свободен хотя бы один элемент для записи. В общем случае этот параметр есть время, на которое нужно блокировать задачу для ожидания появления свободного места. Нажатие кнопки, например, можно и проигнорировать, но вот озвучить это надо всегда, учитывая, что озвучка не занимает много времени при разумном па��аметре duration.

То же самое делаем с кнопкой энкодера (отдельное прерывание, отдельная очередь EncoderButtonQueue, отдельная задача обработки, отправляющая данные в общую очередь динамика)

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

void EncoderConfig(void)
void EncoderConfig(void)
{
	GPIO_InitTypeDef GPIO_Options;
	EXTI_InitTypeDef EXTI_Options;

	RCC_APB2PeriphClockCmd(ENCODER_CLK_PINS | RCC_APB2Periph_AFIO, ENABLE);
	GPIO_Options.GPIO_Pin = ENCODER_A_PIN | ENCODER_B_PIN;
	GPIO_Options.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(ENCODER_PORT, &GPIO_Options);

	GPIO_EXTILineConfig(ENCODER_PORTSOURCE, ENCODER_PINSOURCE); //Only one line interrupt!

	EXTI_Options.EXTI_Line = ENCODER_EXTI_LINE;
	EXTI_Options.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_Options.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
	EXTI_Options.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_Options);
}

По входу в прерывание определим, куда же повернули вал: по часовой стрелке или против:

void EXTI0_IRQHandler(void)
u32 localEncoderAction;

if(GPIO_ReadInputDataBit(ENCODER_PORT, ENCODER_A_PIN) == 1)
{
	if(GPIO_ReadInputDataBit(ENCODER_PORT, ENCODER_B_PIN) == 1)
	{
		localEncoderAction = ENCODER_WAS_INCR;
	}
	else
	{
		localEncoderAction = ENCODER_WAS_DECR;
	}
}
else
{
	if(GPIO_ReadInputDataBit(ENCODER_PORT, ENCODER_B_PIN) == 1)
	{
		localEncoderAction = ENCODER_WAS_DECR;
	}
	else
	{
		localEncoderAction = ENCODER_WAS_INCR;
	}
}

EXTI_ClearITPendingBit(EXTI_Line0);

Все в той же функции обработки прерывания, на основании информации о направлении поворота будем изменять переменную buzzerRotationCounter, которая определяет индекс проигрываемой ноты из массива GL_BuzzerAllNotes[]. Вращая энкодер, получим увеличение или уменьшение частоты звучания на +-15 едениц от значения 25. Далее формируем и отправляем элемент в очередь динамика, семафорим о событии энкодера и выходим из прерывания:

void EXTI0_IRQHandler(void), продолжение
static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
static TickType_t xLastTime;
static s32 buzzerRotationCounter = 15;
BuzzerParameters_t localParameters;

if((xTaskGetTickCount() - xLastTime) > 300)
{
	buzzerRotationCounter = 25;
}

if(localEncoderAction == ENCODER_WAS_INCR)
{
	buzzerRotationCounter++;

	if(buzzerRotationCounter > 39)
	{
		buzzerRotationCounter = 39;
	}
}
else //ENCODER_WAS_DECR
{
	buzzerRotationCounter--;

	if(buzzerRotationCounter < 10)
	{
		buzzerRotationCounter = 10;
	}
}

xLastTime = xTaskGetTickCount();

localParameters.duration = 10;//BUZZER_DEFAULT_DURATION;
localParameters.freq = GL_BuzzerAllNotes[buzzerRotationCounter];
localParameters.volume = BUZZER_VOLUME_MAX;

xQueueSendFromISR(BuzzerQueue, (void *)&localParameters, &xHigherPriorityTaskWoken);
xQueueSendFromISR(EncoderQueue, (void *)&localEncoderAction, &xHigherPriorityTaskWoken);

if(xHigherPriorityTaskWoken == pdTRUE)
{
	portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
}

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



Ну и зачем всё это?


Не то, что бы вышеописанное очень сложно и обязательно надо было разобрать это по шагам. Серьезно, суть публикации глобально можно свести к предложению — создадим задаче динамика очередь и согласно придуманным алгоритмам будем запихивать туда данные. Однако мне показалось, что подобный пример будет не плох для демонстрации распараллеливания доступа различных задач к аппаратным ресурсам железа средствами FreeRTOS. То же самое, но сделанное своими руками на флагах и прерываниях с таймерами хоть и кушало памяти меньше, чем ОСРВ — но в плане читабельности, переносимости и удобства использования было на порядок хуже.

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