STM32: FreeRTOS и пьезокерамический излучатель

    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. То же самое, но сделанное своими руками на флагах и прерываниях с таймерами хоть и кушало памяти меньше, чем ОСРВ — но в плане читабельности, переносимости и удобства использования было на порядок хуже.

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      0
      А это разводка Топора там, на плате, в самом верху?
        0
        Ага =)
        Но только шины данных и адреса для NOR flash и OLED
        0
        Жуткая АЧХ — ниже 2,5 кГц бузер практически не пищит.
        Хотя пищалка, конечно…
        Круто было-бы реализовать вывод «аналогового» звука — через ШИМ на сотню килогерц или поболе, хотя вопрос потянет ли это процессор.
        А «просто» пищать меандром — не так интересно.
          0
          Любопытно, а в чём конкретно _ужас_ аппаратного обработчика энкодера? У меня в добром десятке проектов на STM32 он использован и всё работает без замечаний, считает каждый тик как… часы, простите за каламбур :)
            0
            У меня он работал очень плохо, без преувеличесний. Много ложных срабатываний. Ради интереса покажу позже код инициализации и то, как это работает.

            А серия тоже f103? Может, в старших камнях исправили?
              0
              Серии я использовал разные: F103, F4xx, F7xx. Не думаю, что у них сильная разница в реализации. Встроенный обработчик хорош как раз тем, что обладает настраиваемым фильтром, позволяющим эффективно бороться с дребезгом. Хотя, может быть у Вас такой энкодер трещащий, конечно…
            0
            Разводка радует :) Сразу вспоминается, «там на неведомых дорожках, следы невиданных зверей»
              0
              Ммм… о какой переносимости вы говорите? вот если бы у BuzzerConfig(void) были параметры ввиде вывода GPIO, таймера и номера его канала тогда не придётся весь код заново перелопачивать.
                0
                О хорошей переносимости между программами, использующими FreeRTOS. Точнее даже, о сложной переносимости самодельных способов синхронизации между задачами. Мне лично проще потратить двадцать минут и исправить конфигурацию функций инициализации руками, чем создавать полностью универсальные функции.

                ST уже пробовали так сделать и что вышло? Ага, SPL. Не то, что бы совсем плохо, но ругаться есть на что. Каждый сам выбирает себе эту грань, главное не вдаваться в крайности.
                  0
                  Кстати, в отдельном файле у меня все же есть макросы BUZZER_PIN, BUZZER_PORT, BUZZER_CLK_PINS (BuzzerConfig(void)). Туда же, при желании, можно записать таймер:

                  #define BUZZER_TIM TIM2
                  #define BUZZER_CH  CH1
                  
                  0
                  А есть у этих МК аппаратная возможность управлять мощным пьезопреобразователем?
                  Нужна автоподстройка частоты.
                  0
                  Спасибо за даташит, изучаю.
                  У AVR есть механизм zero-crossing, который позволяет сделать настройку на резонанс.
                  Разрабатываю управление мощным пьезопреобразователем 1-2 кВт.
                  А сам проект, ультразвуковая обработка молока.
                    0
                    Проще же сделать это на внешних компонентах, а МК использовать любой, который удобно программировать.
                    К примеру, я когда-то делал Zero-crossing на компаратор + аналоговый мультиплексор (Low Capacitance, Low Charge Injection)

                    image

                    Нужно было измерять только положительную полуволну синуса микроконтроллером (сигнал симметричен был). В схеме компаратор кроме того, что детектирует переход через 0, так еще и переключает мультиплексор так, что бы в положительную полуволну сигнал шел на АЦП МК, а в отрицательную выдавал 0.

                    Only users with full accounts can post comments. Log in, please.