Продолжаем изучение библиотеки 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.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий