Чуть ранее (в этой статье) мы рассмотрели в общих чертах таймеры в STM32 и написали простенькую программку. Теперь, как и обещал, поподробнее покопаем генерацию ШИМ при помощи все того же таймера TIM4. Итак, начинаем!
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также небольшая подборка статей с ШИМ и таймерами:
- ПИД-регулятор. Пример ПИД-регулятора температуры на STM32.
- STM32 и Timer Input Capture. Режим захвата сигнала.
Честно говоря, писать-то особо нечего. Думаю многие знают что такое ШИМ и с чем его едят, а если нет то об этом можно прочитать, например, в Википедии, так что нет, наверное, смысла отдельно описывать то, что уже многократно и хорошо описано...
Давайте создадим пример для генерации ШИМ. Просто сгенерировать такой сигнал не так интересно, так что давайте хоть немного усложним себе задачу. Будем генерировать ШИМ с разной длительностью импульса в зависимости от состояния кнопки. Если кнопка нажата – генерируем сигнал с периодом 2.5 мс и длительностью импульса 1.5 мс, а если кнопка не нажата – то 2.5 мс и 0.5 мс соответственно.
Так что создаем новый проект и пишем код. Для начала набор includ’ов:
#include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" #include "stm32f10x_tim.h"
Добавили все файлы, необходимые нам для работы. Объявим переменные:
#define TIM_CCER_CC3NE ((uint16_t)0x0400) #define TIMER_PRESCALER 720 #define EXT_TIM_PULSE 150 #define TIM_PULSE 50
- TIMER_PRESCALER мы уже упоминали - это предделитель частоты.
- EXT_TIM_PULSE – увеличенная длительность импульса (то есть при нажатой кнопке), аналогично TIM_PULSE – обычная длительность (кнопка не нажата).
Продолжаем:
uint16_t previousState; GPIO_InitTypeDef port; TIM_TimeBaseInitTypeDef timer; TIM_OCInitTypeDef timerPWM; uint16_t buttonPreviousState;
Просто переменные, которые нам понадобятся в проекте. Пока все просто! И вот наконец-то кое-что поинтереснее, а именно наша разросшаяся функция инициализации:
void initAll() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); GPIO_StructInit(&port); port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_6; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &port); port.GPIO_Mode = GPIO_Mode_IPD; port.GPIO_Pin = GPIO_Pin_3; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &port); TIM_TimeBaseStructInit(&timer); timer.TIM_Prescaler = TIMER_PRESCALER; timer.TIM_Period = 250; TIM_TimeBaseInit(TIM4, &timer); TIM_OCStructInit(&timerPWM); timerPWM.TIM_Pulse = 50; timerPWM.TIM_OCMode = TIM_OCMode_PWM1; timerPWM.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM4, &timerPWM); }
Давайте прямо по строчкам смотреть, что тут происходит.
Вначале уже привычное включение тактирования необходимой периферии. Шестую ножку порта GPIOB настраиваем на работу в режиме альтернативной функции(!). Лезем в даташит на контроллер и видим, что альтернативной функцией у этого вывода является первый канал таймера TIM4. То что надо:
Вывод PA3 настраиваем на вход – там будет наша воображаемая кнопка. Итак, с инициализацией портов закончили, идем настраивать таймер. Поначалу все конфигурируем, как и в предыдущем проекте. А вот дальше кое-что новенькое:
TIM_OCStructInit(&timerPWM); timerPWM.TIM_Pulse = 50; timerPWM.TIM_OCMode = TIM_OCMode_PWM1; timerPWM.TIM_OutputState = TIM_OutputState_Enable; TIM_OC1Init(TIM4, &timerPWM);
Для использования режима генерации ШИМ нам понадобилась структура TIM_OCInitTypeDef. Поле TIM_Pulse – длительность заполнения, пусть будет сначала 50 тиков (0.5 мс). Далее задаем режим - TIM_OCMode_PWM1. Помимо PWM1 есть еще PWM2. Это всего лишь разные режимы ШИМ – с выравниванием по границе и по центру. В поле TIM_OutputState забиваем – Enable и инициализируем таймер. Готово!
Осталось совсем немного... Функция main() – как же без нее:
int main() { __enable_irq(); initAll(); buttonPreviousState = 0; TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); TIM_Cmd(TIM4, ENABLE); NVIC_EnableIRQ(TIM4_IRQn); while(1) { __NOP(); } }
Все как и раньше – включаем прерывание по переполнению. Оно нам нужно, чтобы изменять параметры генерируемого ШИМ-сигнала именно в момент окончания периода. Вот и сам код обработчика:
void TIM4_IRQHandler() { uint16_t button = 0; button = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3); TIM_ClearITPendingBit(TIM4, TIM_IT_Update); if ((button == 1) && (buttonPreviousState == 0)) { TIM4->CCR1 = EXT_TIM_PULSE; buttonPreviousState = 1; } if ((button == 0) && (buttonPreviousState == 1)) { TIM4->CCR1 = TIM_PULSE; buttonPreviousState = 0; } }
Опрашиваем кнопку, ничего нового, знакомая функция 🙂 Возможно возникнет вопрос – зачем так усложнять:
(button == 1) && (buttonPreviousState == 0)
А чтобы работа с регистром таймера шла не на каждое прерывание, а только когда состояние кнопки изменяется. Упомянутая работа с регистром заключается в прямой записи длительности заполнения в регистр таймера TIM_CCR.
Компилируем, идем в отладчик, запускаем программу и эмулируем нажатие кнопки на PA3. Если кто забыл, открываем окно General Purpose I/O A (GPIOA) и вручную выставляем бит на входе PA3. В окошке логического анализатора видим:
При изменении состояния кнопки меняется и скважность импульсов, как и задумывалось. И на сегодня это все, до скорого 🤝