PWM -- штука складна, див. вікі, наприклад. Це спосіб модулювати високочастотний сигнал низькочастотним, змінюючи ширину імпульсів, за постійної їх частоти. Тут ми розглядатимемо його варіант, що широко використовується у вбудованій техніці, такий собі спосіб отримувати аналоговий сигнал з цифрового, без ЦАП.
Нехай є імпульси певного періоду. Відношення періоду до ширини імпульсу називається прогальністю (скважность російською). Величина, обернена до неї -- коефіцієнт заповнення, (duty cycle англійською), часто зручніша. Якщо такий імпульс згладити (наприклад -- конденсатором великої ємності), можна отримати частку максимальної напруги (для контролера -- тієї, що рівна його логічній одиниці, зазвичай 5В або 3.3В), рівну коефіцієнту заповнення:
(с) wiki |
Іншим прикладом такого "інтегруючого" пристрою може бути двигун, інерція ротора якого згладжуватиме коливання. Для керування серводвигунами теж використовується ШІМ. Якщо частота ШІМ велика, його можна безпосередньо використовувати для керування яскравістю світлодіодів -- мерехтіння буде непомітним для ока. Детальніше див., наприклад тут: "AVR. Учебный курс. Использование ШИМ", а ми перейдемо до генерації ШІМ з використанням таймерів STM32.
Будемо плавно змінювати яскравість світлодіоду, змінюючи ширину імпульсів -- коефіцієнт заповнення.
Будемо плавно змінювати яскравість світлодіоду, змінюючи ширину імпульсів -- коефіцієнт заповнення.
Апаратна частина примітивна -- до PA7 плати STM32VLDiscovery, через резистор обмеження струму, (величина якого знаходиться із того, що мікроконтролер не здатен віддати більше 20мА на пін + обмеження сумарного струму всіх пінів зразу, див. даташіти), під'єднується світлодіод:
Щодо вибору резистора. Світлодіод не підкоряється закону Ома. В першому наближення спад напруги на ньому фіксований, складає трохи менше пари вольт (оцінку див. тут, детальніше див. даташіти на ваші світлодіоди). Яскравість світлодіода залежить від струму, що протікає через нього. В цьому ж наближенні, струм через діод, згідно закону Ома, буде \(I = \frac{U-\Delta U}{R}\), де U -- напруга логічної одиниці, R -- опір резистора, \(\Delta U\) -- спад напруги на світлодіоді.
Як відомо, таймери мають декілька (зазвичай - 4) канали. Кожен канал може керувати своїм піном. Період ШІМ визначається таймером, і є єдиним для всіх каналів, однак коефіцієнт заповнення можна задавати для кожного каналу свій, користуючись регістром TIMx_CCRx.
Канали можуть працювати у двох режимами ШІМ (PWM) --- PWM mode 1 (біти OCxM в TIMx_CCMRx рівні 110b) або PWM mode 2 (OCxM = 111b). Крім того, він може бути вирівняним по краю чи по центру -- якщо відлік відбувається вверх чи вниз, ШІМ вирівняний по краю, якщо туди-потім-назад -- по центру.
Генерація ШІМ,
центральний режим. Решта як на попередній діаграмі. CMS -- біти вибору
центрального режиму із TIMx_CR1. (Подробиці див. даташіт). (c) STMicroelectronics |
- В режимі 1 (PWM mode 1) -- канал активний (OCxREF==1) поки TIMx_CNT<=TIMx_CCRx (хоча в даташіті сформульовано хитро, якщо я правильно зрозумів, так буде не залежно від напрямку відліку)
PWM1, полярність: активний - високий. - В режимі 2 (PWM mode 2) -- навпаки, канал активний (OCxREF==1) поки
TIMx_CNT>TIMx_CCRx і не активний в іншому випадку.
PWM2, полярність: активний - високий.
Можна сказати, що в PWM1 CCRx задає коефіцієнт заповнення, а в PWM2 -- прогальність.
Що значить "активний", визначається полярністю, яка обирається бітом CCxP регістра TIMx_CCER. Якщо в ньому 0, активним вважається високий рівень (3.3В у нашому випадку), якщо 1 -- низький (0В). Тобто, зміна полярності інвертує ШІМ.
Що значить "активний", визначається полярністю, яка обирається бітом CCxP регістра TIMx_CCER. Якщо в ньому 0, активним вважається високий рівень (3.3В у нашому випадку), якщо 1 -- низький (0В). Тобто, зміна полярності інвертує ШІМ.
Заміна режиму з mode 1 на mode 2, або навпаки, з одночасною зміною полярності, дає той же результат на піні.
PA7 може керуватися другим каналом (CH2) TIM3. Вивід, яким керуватиме таймер, слід встановити в режим AF (Alternative function), PP (push-pull).
Поїхали.
Включаємо тактування таймера та порту А:
RCC->APB1ENR |= RCC_APB1Periph_TIM3; // Дозволяємо тактування TIM3
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A
Налаштовуємо пін:
Далі слід налаштувати таймер:
Таймер працює, однак світлодіод поки ледь жевріє --- заповнення дуже мале, бо малий CCR2. Щоб було цікавіше, допишемо такі рядки:
де delay_some_us() -- функція затримки, що траплялася в попередніх постах, яка приймає час затримки в мікросекундах.
Цей код кожних 50 мілісекунд збільшує CCR2 на 10, або яскравість світлодіода на 10/1000 = 1/100 = 1% (ARR = 1000). Коли яскравість досягає максимуму, записуємо в нього дуже мале значення, майже вимикаючи світлодіод, і починаємо з початку.
Все. Проект, котрий демонструє матеріал цього тексту, можна скачати тут.
В наступному пості спробуємо зробити це ж, користуючись HAL.
Проект, котрий демонструє матеріал цього тексту, можна скачати тут.
В наступному пості спробуємо зробити це ж, користуючись HAL.
GPIOA->CRL &= ~GPIO_CRL_CNF7_0; // 10 -- AF PP (Alternative function -- push-pull) GPIOA->CRL |= GPIO_CRL_CNF7_1; GPIOA->CRL |= GPIO_CRL_MODE7; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz
Далі слід налаштувати таймер:
- Встановити подільник частоти (для отримання високої частоти імпульсів ШІМ, беремо його рівним одиниці -- PSC = 0).
- Дозволити Capture compare для каналу, встановивши біт CC2E регістра TIM3_CCER -- власне ця можливість дозволяє змінювати щось (наприклад -- логічний рівень піна), коли лічильник досягнув TIM3_CCR2.
- Вибрати режим PWM (біти OC2M з TIM3_CCMR1, описані вище).
- Задати період в TIM3_ARR, коефіцієнт заповнення в TIM3_CCR2.
- Також слід встановити preload-біт OC2PE в TIM3_CCMR1 (CCMR1 описує канали 1 і 2), який вказує, що оновлення вмісту CCR2 слід здійснювати лише під час події оновлення таймера та біт ARPE з TIM3_CR1, який вказує це ж для TIM3_ARR -- інакше коректне функціонування PWM не гарантується (крім одноімпульсного режиму, про який -- окремо) -- документація вимагає зробити це.
- Вибрати напрямок роботи піна -- вивід, встановивши біти CC2S = 00b, (TIM3_CCMR1).
- Вибрати полярність в біті CC2P TIM3_CCER.
- За бажанням -- змінити напрямок відліку чи/і вибрати центральний режим.
Зауважте, що не всі операції, перераховані вище, є конче необхідними -- частина бітів матимуть адекватні (принаймні -- для простих прикладів) значення по замовчуванню (напрямок відліку, полярність, і т.д.), крім того, скажімо, навіть якщо не встановити preload для CCRx та ARR, таймер, ймовірно, функціонуватиме (ну, поки їх не чіпають, як мінімум). [Стільки слів невпевненості, бо не тестував систематично такі нештатні режими.] Тому, часто, в прикладах, які можна знайти в Інтернеті, дій робиться менше. Однак, так як приклад навчальний, демонструю всі перераховані етапи. Див. також відповідні appnote (AN4013) та даташіти. В таких простих програмах можна покладатися на те, що налаштування периферії зберігають свої значення по замовчуванню, отримані при перевантаженні контролера. Однак, в загальному випадку, не завжди відомо, в якому стані знаходиться таймер на початку роботи.
Також, традиційно (і обґрунтовано), якщо в якесь трьох-бітове поле треба записати 101b, спочатку туди записують нулі, ("REG &= ~XXX_BITS") потім встановлюють в одиницю перший та останній біти ("REG |= (XXX_FIRST_BIT | XXX_LAST_BIT)"). Однак, в прикладах, для демонстрації, окремо записую нулі, окремо -- одиниці. Принаймні там, де це не заважає -- не приводить контролер у "нездоровий" стан.
Важливе зауваження: просунуті таймери, TIM1, TIM8 і т.д., мають break-пін, і здатні, за сигналом на ньому, переходити у фіксований, заданий наперед, стан. Досягається це очищенням біту MOE в регістрі TIMx_BDTR. Так ось, по замовчуванню цей біт рівний нулю! І таймер не даватиме на вихід нічого. Тобто, якщо у вас все працює -- таймер рахує, прилітають переривання, а ШІМ-му немає -- перевірте, чи причина не в біті MOE. Аналогічно, MOE слід перевірити, якщо для інших таймерів якийсь код працює, а для TIM1 -- ні. Забігаючи наперед, Cube про цю подробицю піклується автоматично.Все, таймер можна запускати. Для наочності, кожне пункт написано окремим рядком (ввімкнення PWM mode 1 аж в двох, хоча для PWM mode 2 достатньо одного -- див. закоментований рядок). Зрозуміло, що в реальному коді їх слід об'єднати. (Трапляються такі біти, які одночасно змінювати не можна, але тут такої проблеми немає).
TIM3->PSC = 0; // Подільник частоти - 1 TIM3->ARR = 1000; // Оновлюємо кожних 1000 тіків TIM3->CCR2 = 10; // На початку -- майже не світимо TIM3->CCER |= (TIM_CCER_CC2E); // Capture/Compare 2 output enable -- PA7 TIM3->CCER &= ~TIM_CCER_CC2P; // Полярність -- active high (Default)
// PWM1 -- OCxM=0b110
TIM3->CCMR1|=(TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); TIM3->CCMR1&=~TIM_CCMR1_OC2M_0;
// Або PWM2 -- OCxM=0b111 // TIM3->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); TIM3->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default) TIM3->CCMR1|= TIM_CCMR1_OC2PE; // Preload для CCR2, вимагається Datasheet для PWM TIM3->CR1 |= TIM_CR1_ARPE; // Preload для ARR, вимагається Datasheet для PWM TIM3->CR1 &= ~TIM_CR1_DIR; // Відлік -- вверх. (Default) TIM3->CR1 |= TIM_CR1_CEN;
Таймер працює, однак світлодіод поки ледь жевріє --- заповнення дуже мале, бо малий CCR2. Щоб було цікавіше, допишемо такі рядки:
while(1) { delay_some_us(50000); if( TIM3->CCR2<TIM3->ARR ) TIM3->CCR2 += 10; else TIM3->CCR2 = 10; }
де delay_some_us() -- функція затримки, що траплялася в попередніх постах, яка приймає час затримки в мікросекундах.
Цей код кожних 50 мілісекунд збільшує CCR2 на 10, або яскравість світлодіода на 10/1000 = 1/100 = 1% (ARR = 1000). Коли яскравість досягає максимуму, записуємо в нього дуже мале значення, майже вимикаючи світлодіод, і починаємо з початку.
Все. Проект, котрий демонструє матеріал цього тексту, можна скачати тут.
В наступному пості спробуємо зробити це ж, користуючись HAL.
main.c
#include <stm32f10x.h> volatile uint32_t curTicks; #ifdef __cplusplus extern "C"{ #endif void SysTick_Handler(void) { curTicks++; /* Зроблено ще один тік */ } #ifdef __cplusplus } #endif #define MICROSECONDS_GRANULARITY 10 // Потрібна частота спрацювань таймера SysTick в герцах #define FREQ ((1000000)/(MICROSECONDS_GRANULARITY)) #define TICKS ((SystemCoreClock)/(FREQ)) /*! Затримка в мікросекундах, грануляція -- microseconds_granularity мікросекунд, * одну не витягує сам контролер. 24МГц -- один такт це 42нс=0.042мкс */ inline static void delay_some_us(uint32_t mks) { uint32_t ticks=mks/MICROSECONDS_GRANULARITY; int stop_ticks=ticks+curTicks; while (curTicks < stop_ticks) {}; } int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A RCC->APB1ENR |= RCC_APB1Periph_TIM3; // Дозволяємо тактування TIM3 GPIOA->CRL &= ~GPIO_CRL_CNF7_0; // 10 -- AF PP (Alternative function -- push-pull) GPIOA->CRL |= GPIO_CRL_CNF7_1; GPIOA->CRL |= GPIO_CRL_MODE7; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz TIM3->PSC = 0; // Подільник частоти - 1 TIM3->ARR = 1000; // Оновлюємо кожних 1000 тіків TIM3->CCR2 = 10; // На початку -- майже не світимо TIM3->CCER |= (TIM_CCER_CC2E); // Capture/Compare 2 output enable -- PA7 TIM3->CCER &= ~TIM_CCER_CC2P; // Полярність -- active high (Default) TIM3->CCMR1|=(TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM1 -- OCxM=0b110 TIM3->CCMR1&=~TIM_CCMR1_OC2M_0; // TIM3->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); // PWM2 -- OCxM=0b111 TIM3->CCMR1&= ~TIM_CCMR1_CC2S; // Зануляємо -- на вивід. (Default) TIM3->CCMR1|= TIM_CCMR1_OC2PE; // Preload для CCR2, вимагається Datasheet для PWM TIM3->CR1 |= TIM_CR1_ARPE; // Preload для ARR, вимагається Datasheet для PWM TIM3->CR1 &= ~TIM_CR1_DIR; // Відлік -- вверх. (Default) TIM3->CR1 |= TIM_CR1_CEN; // Ініціалізуємо таймер SysTick -- з його допомогою генеруватимемо затримки if (SysTick_Config (TICKS)) { // Помилка -- таймер не ініціалізувався TIM1->CR1 &= ~TIM_CR1_CEN; GPIOC->BSRR = GPIO_BSRR_BS8; // Синім позначатимемо помилку while(1); // Зависаємо } while(1) { delay_some_us(50000); if( TIM3->CCR2<TIM3->ARR ) TIM3->CCR2 += 10; else TIM3->CCR2 = 10; } }
Проект, котрий демонструє матеріал цього тексту, можна скачати тут.
В наступному пості спробуємо зробити це ж, користуючись HAL.
Немає коментарів:
Дописати коментар