STM Урок 149. LL. PWM (ШИМ). Мигаем светодиодами плавно
Продолжаем изучение библиотеки LL. Также продолжаем работу с таймерами и в данном занятии мы изучим возможность аппаратной реализации широтно-импульсной модуляции (ШИМ или PWM).
Что такое широтно-импульсная модуляция, думаю, уже никому объяснять не надо, у нас было уже очень много уроков по данной теме, в том числе и для STM, правда использована была библиотека HAL, но зато с понятием данного способа управления силой сигнала мы знакомы неплохо.
Поэтому можно приступить к изучению возможностей библиотеки LL для реализации PWM в контроллере STM32.
Но прежде чем приступить к возможностям самой библиотеки, нам желательно знать, как вообще настраивается ШИМ в контроллере STM32F1.
ШИМ в контроллере STM32 обеспечивается за счёт возможностей таймеров, поэтому давайте рассмотрим сначала регистры, которые управляют данной модуляцией. Очень немало регистров мы уже изучили в уроке 147, поэтому рассмотрим только те, с которыми мы пока не знакомы, но и их не все, а только те, которые так или иначе будут участвовать в сегодняшнем нашем проекте. Давайте также, чтобы попусту не тратить время, биты регистров будем изучать те, которые будем использовать.
Начнём знакомство с регистров CCMR1 и CCMR2 (capture/compare mode register) — регистров настройки режима захвата/сравнения.
Эти регистры полностью аналогичны, разница лишь в том, что CCMR1 управляет настройками 1 и 2 каналов таймера, а CCMR2 — 3 и 4 каналов.
Поэтому рассмотрим только первый из них
Под каждый канал в данном регистре выделено целых 8 байт. Причём данные биты могут иметь разное значение в зависимости от направления использования канала. Мы будем каналы использовать на выход, поэтому для нас верхняя строка назначений битов, а нижняя — для использования каналов на вход (в режиме захвата).
Битовое поле CCxS (Capture/Compare selection) — бит установки режима работы ножки канала
00 — канал работает на выход
01 — канал работает на вход, ICx отображается на TI2
10 — канал работает на вход, ICx отображается на TI1
11 — канал работает на вход, ICx отображается на TRC. Этот режим работает только в том случае, если через бит TS выбран внутренний триггерный вход (регистр TIMx_SMCR).
Бит OCxPE (Output compare preload enable) — бит буферизации значения сравнения.
Нам здесь интересно ещё битовое поле OCxM. Данное поле состоит из трёх битов и служит для назначения конкретного режима канала.
Так как мы будем использовать канал в режиме PWM, то использовать мы можем следующие комбинации битов в поле:
110 — прямой PWM,
111 — инверсный PWM.
Прямой режим ШИМ — это когда коэффициент заполнения тем больше, чем больше число сравнения, а при инверсном — наоборот.
Также у нас есть регистр CCER (capture/compare enable register) — регистр включения определенных каналов для использования в режиме захвата/сравнения, а также PWM
Под каждый канал в данном регистре выделено всего по 2 бита.
Назначение их следующее
CCxE (Capture/Compare output enable): включение ножки канала
0 — ножка не активна
1 — ножка активна.
CCxP (Capture/Compare output polarity): полярность выхода
В случае использования на выход:
0 — активный уровень высокий
1 — активный уровень низкий.
Если используется на вход:
0 — не инвертирован, то есть захватывается фронт сигнала
1 — инвертирован и захватывается спад сигнала.
Также будет использоваться регистр CCRx (capture/compare register) — регистр, в котором хранится число захвата/сравнения
В случае сравнения или PWM, то есть в режиме выхода, сюда мы заносим число, до которого будет считать таймер (в случае ШИМ ножка переключится на противоположный уровень), а в случае использования на вход (в режиме захвата) в момент обнаружения нужного изменения сигнала сюда сохранится значение счётчика, то есть то значение, до которого в данный момент досчитает таймер.
Вот, в принципе, и всё в отношении низкоуровневых настроек таймера для режима PWM.
Теперь можно уже непосредственно перейти и к проекту.
Схема у нас будет та же, что и в уроке 147, только светодиоды мы все использовать не сможем, да нам и не надо их столько. Для того, чтобы увидеть, как работает широтно-импульсная модуляция, я думаю, нам вполне будет достаточно шести из них.
А проект мы сделаем из проекта урока 147 с именем LL_TIM2 и назовём его LL_PWM01.
Откроем наш проект в Cube MX и первым делом отключим все ножки портов, которые мы использовали для управления светодиодами, в том числе и ножку кнопки

Теперь настроим наши таймеры, их будет два.
Первым делом настроим таймер 2, который у нас уже работал, только мы теперь его настроим в режиме PWM, а вернее два его канала. Сначала включим каналы

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

Настроим наш таймер
Прерывания можно отключить

Также включим все каналы таймера 3

Также у нас включились нужные ножки портов

Таймер настроим аналогично таймеру 2

Перейдём в расширенные настройки проекта и включим использование библиотеки LL и для 3 таймера

Сгенерируем проект и откроем его в Keil, настроим программатор на автоматическую перезагрузку, а также уровень оптимизации установим в 0.
Соберём наш проект и, если всё нормально собралось, давайте посмотрим, как происходит инициализация таймеров для работы в режиме ШИМ.
Особенно нам интересна не вся инициализация, а та её часть, которая отсутствует при инициализации таймера в режиме обычного счётчика.
При настройке регистров и занесении данных из первой структуры TIM_InitStruct у нас, в принципе, ничего не изменилось, разве что только величина предделителя и периода.
Но у нас теперь в инициализации появилась ещё одна структура — TIM_OC_InitStruct. Данная структура служит для подготовки и дальнейшего занесения в регистры таймера данных для каждого канала, который будет управлять конкретной ножкой порта.
После того, как первая структура заполнилась и при помощи функции её данные занеслись в необходимые регистры, вызывается функция LL_TIM_OC_EnablePreload, которая в регистре CCMRx включит бит OCxPE, соответствующий каналу, переданному в её параметре
|
1 |
LL_TIM_OC_EnablePreload(TIM2, LL_TIM_CHANNEL_CH3); |
Далее заполняется структура TIM_OC_InitStruct и вызывается функция LL_TIM_OC_Init.
Зайдём в её тело и посмотрим, что там творится.
А там, в зависимости от номера канала, в соответствующем кейсе вызывается функция OCxConfig. Зайдём в тело любой из них (например для канала 3 (CH3)) и посмотрим, что в нём происходит.
Сначала идёт проверка существования параметров в структуре, а затем в регистре CCER сбрасывается бит CCxE, то есть ножка порта сначала отключается
|
1 |
CLEAR_BIT(TIMx->CCER, TIM_CCER_CC3E); |
Затем читаются полностью регистры CCER, CR2 и CCMRx во временную переменную
|
1 2 3 4 5 6 7 8 |
/* Get the TIMx CCER register value */ tmpccer = LL_TIM_ReadReg(TIMx, CCER); /* Get the TIMx CR2 register value */ tmpcr2 = LL_TIM_ReadReg(TIMx, CR2); /* Get the TIMx CCMR2 register value */ tmpccmr2 = LL_TIM_ReadReg(TIMx, CCMR2); |
Затем мы работаем с данными переменными также как и с регистрами, только изменения вносятся в биты в данных переменных.
Вначале в регистре CCMRx мы сбрасываем бит CCxS, тем самым мы включаем ножку канала на выход
|
1 |
CLEAR_BIT(tmpccmr2, TIM_CCMR2_CC3S); |
Следующей командой мы сначала сбрасываем все биты поля OCxM, а затем устанавливаем в нём нужный нам режим, взяв значение из поля структуры OCMode, то есть режим PWM1 (110)
|
1 |
MODIFY_REG(tmpccmr2, TIM_CCMR2_OC3M, TIM_OCInitStruct->OCMode); |
Затем в регистре CCER мы сначала сбрасываем бит CCxP, а затем устанавливаем необходимую полярность из структуры (активный уровень высокий, то есть 0)
|
1 |
MODIFY_REG(tmpccer, TIM_CCER_CC3P, TIM_OCInitStruct->OCPolarity << 8U); |
Затем сначала сбрасываем бит CCxE в том же регистре, а затем устанавливаем или сбрасываем его в зависимости от настроек поля структуры, отвечающего за включение ножки. В данном случае она пока отключается
|
1 |
MODIFY_REG(tmpccer, TIM_CCER_CC3E, TIM_OCInitStruct->OCState << 8U); |
Затем идёт условие настройки таймера 1, но так как мы такой таймер не используем, то условие данное мы не рассматриваем.
Далее два регистра заполняются данными из переменных
|
1 2 3 4 5 |
/* Write to TIMx CR2 */ LL_TIM_WriteReg(TIMx, CR2, tmpcr2); /* Write to TIMx CCMR2 */ LL_TIM_WriteReg(TIMx, CCMR2, tmpccmr2); |
Потом идёт вызов функции LL_TIM_OC_SetCompareCH3, поэтому давайте посетим и её тело, в котором регистр CCR3 заполняется значением, до которого будет считать таймер, отмеряя период высокого сигнала, то есть до момента, когда произойдёт изменение уровня ножки с высокого на низкий (спад). Затем таймер будет считать до конца, но уже ножка будет находиться в низком уровне. Значение берётся из структуры. В инициализации у нас там ноль, так как коэффициентом заполнения ШИМ мы будем управлять позже
|
1 |
WRITE_REG(TIMx->CCR3, CompareValue); |
Затем у нас сохраняется значение последнего регистра из временной переменной
|
1 2 |
/* Write to TIMx CCER */ LL_TIM_WriteReg(TIMx, CCER, tmpccer); |
После всего этого идёт возврат из функции OC3Config с положительным результатом, с которым мы затем возвращаемся также и из функции LL_TIM_OC_Init.
Далее при помощи функции LL_TIM_OC_DisableFast очищается бит OCxFE регистра CCMRx
|
1 |
LL_TIM_OC_DisableFast(TIM2, LL_TIM_CHANNEL_CH3); |
Подобные настройки потом применяются для всех каналов всех таймеров, которые мы используем в нашей программе.
Затем с помощью функции LL_TIM_SetTriggerOutput мы сбрасываем все биты поля MMS в регистре CR2
|
1 |
LL_TIM_SetTriggerOutput(TIM2, LL_TIM_TRGO_RESET); |
Потом отключается режим MASTER/SLAVE таким же образом, как и при использовании таймера в режиме обычного счётчика
|
1 |
LL_TIM_DisableMasterSlaveMode(TIM2); |
Далее идёт настройка ножек порта, задействованных для каналов
|
1 2 3 4 5 6 7 8 9 10 11 |
/* USER CODE END TIM2_Init 2 */ LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOA); /**TIM2 GPIO Configuration PA2 ------> TIM2_CH3 PA3 ------> TIM2_CH4 */ GPIO_InitStruct.Pin = LL_GPIO_PIN_2|LL_GPIO_PIN_3; GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE; GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL; LL_GPIO_Init(GPIOA, &GPIO_InitStruct); |
Вот, в принципе, и вся инициализация таймера. Вроде бы и не такая лёгкая, но так как мы уже знаем назначение битов регистров, то для нас это и не тяжело.
Подобным образом инициализируется и следующий таймер.
Ну, наконец-то, настало время написания собственного кода.
Сначала подчистим остатки прошлого проекта.
Удалим полностью все наши глобальные макросы и системную переменную в области /*USER CODE BEGIN PV*/.
Также удалим и тело функции-обработчика прерывания, саму функцию на всякий случай оставим
|
1 2 3 4 5 |
/* USER CODE BEGIN 4 */ void TIM2_Callback(void) { } /* USER CODE END 4 */ |
В функции main() удалим вот эти строки
tim2_count=0;
LED1_OFF();LED2_OFF();LED3_OFF();LED4_OFF();LED5_OFF();
LED6_OFF();LED7_OFF();LED8_OFF();LED9_OFF();LED10_OFF();
LL_TIM_EnableIT_UPDATE(TIM2);
LL_TIM_EnableCounter(TIM2);
Включим каналы таймеров
|
1 2 3 4 |
/* USER CODE BEGIN 2 */ LL_TIM_CC_EnableChannel(TIM2, LL_TIM_CHANNEL_CH3 | LL_TIM_CHANNEL_CH4); LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH1 | LL_TIM_CHANNEL_CH2 | \ LL_TIM_CHANNEL_CH3 | LL_TIM_CHANNEL_CH4); |
В данных функциях включается соответствующий для каждого канала бит CCxE в регистре CCER.
Включим счётчики таймеров
|
1 2 3 |
LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH4); LL_TIM_EnableCounter(TIM2); LL_TIM_EnableCounter(TIM3); |
Добавим две локальные переменные
|
1 2 |
/* USER CODE BEGIN 1 */ uint32_t i,d; |
Ну, и в заключении в бесконечном цикле немного поиграем коэффициентами заполнения наших каналов, настроенных в режиме PWM, чем и обеспечим поочерёдное плавное зажигание и гашение светодиодов, подключенных к ножкам данных каналов
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* USER CODE BEGIN 3 */ for(i=0;i<768432;i++) { if(i<65536) LL_TIM_OC_SetCompareCH3(TIM2,i); else if ((i>65535)&&(i<131072)) LL_TIM_OC_SetCompareCH3(TIM2,131071-i);//TIM2->CCR3=131071-i; else if((i>131071)&&(i<196608)) LL_TIM_OC_SetCompareCH4(TIM2,i-131072);//TIM2->CCR4=i-131072; else if ((i>196607)&&(i<262144)) LL_TIM_OC_SetCompareCH4(TIM2,262143-i);//TIM2->CCR4=262143-i; else if((i>262143)&&(i<327680)) LL_TIM_OC_SetCompareCH1(TIM3,i-262144);//TIM3->CCR1=i-262144; else if ((i>327679)&&(i<393216)) LL_TIM_OC_SetCompareCH1(TIM3,393215-i);//TIM3->CCR1=393215-i; else if((i>393215)&&(i<458752)) LL_TIM_OC_SetCompareCH2(TIM3,i-393216);//TIM3->CCR2=i-393216; else if ((i>458751)&&(i<524288)) LL_TIM_OC_SetCompareCH2(TIM3,524287-i);//TIM3->CCR2=524287-i; else if((i>524287)&&(i<589824)) LL_TIM_OC_SetCompareCH3(TIM3,i-524288);//TIM3->CCR3=i-524288; else if ((i>589823)&&(i<655360)) LL_TIM_OC_SetCompareCH3(TIM3,655359-i);//TIM3->CCR3=655359-i; else if((i>655359)&&(i<720896)) LL_TIM_OC_SetCompareCH4(TIM3,i-655360);//TIM3->CCR4=i-655360; else LL_TIM_OC_SetCompareCH4(TIM3,768431-i);//TIM3->CCR4=768431-i; for(d=0;d<75;d++) {} } |
Задержку мы используем не библиотечную, так мне показалось удобнее.
Соберём код, прошьём контроллер и полюбуемся на то, как наши светодиоды будут поочерёдно и плавно мигать.
Конечно, в картинках я не могу это показать, поэтому смотрите видеоверсию, кто ещё не смотрел


Итак, на данном занятии мы настроили таймеры на работу в режиме PWM (ШИМ), используя возможности библиотеки LL.
Всем спасибо за внимание!
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)







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