В одном из предыдущих примеров я уже показывал, как на ATtiny13 можно реализовать ШИМ регулятор с управлением потенциометром (изменением уровня сигнала на входе АЦП). Теперь, по просьбам трудящихся, усложним задачу и рассмотрим, как можно управлять ШИМ сигналом на выходе с помощью кнопок.
Сложность тут заключается в том, что при управлении кнопками, скважность ШИМ сигнала на выходе будет изменяться ступенчато при удерживании кнопки, а между этими «ступенями» необходимо реализовать определённые временные задержки для более или менее плавного изменения ШИМ сигнала. Кроме того, даже если мы будем управлять выходом более грубо, отдельными короткими нажатиями кнопки, задержки всё равно потребуются — для устранения эффекта «дребезга» контактов при нажатии и отпускании кнопок. Но, так как на выходе у нас должен постоянно генерироваться ШИМ сигнал, то вносить задержки в основной цикл программы (например с помощью библиотеки delay) мы не можем, потому что при этом наш ШИМ сигнал будет прерываться во время выдержки каждой паузы. Поэтому для обработки нажатий на кнопки логично будет задействовать внешние прерывания, а временные задержки (паузы) организовать с помощью таймера. Тут вроде бы опять незадача: ведь таймер на борту у тини13 всего один, а он у нас уже задействован для формирования ШИМ сигнала… Но на самом деле всё совсем не так плохо. Ведь таймер, при желании, может выполнять и сразу несколько задач…
Все опыты я провожу на своей отладочной плате, соответственно код привожу применительно к ней:
/*
* tiny13_board_pcint_pwm
* Демо-прошивка отладочной платы на ATtiny13.
* Демонстрация работы ШИМ-регулятора (в режиме коррекции фазы):
* неинверсный сигнал на выходе OC0A.
* ШИМ-сигнал регулируется двумя кнопками (+/-):
* по нажатию кнопки срабатывает прерывание pcint0
* и, в зависимости от того, какая из кнопок нажата,
* увеличивается либо уменьшается значение регистра сравнения OCR0A для таймера ШИМ.
*/
#define F_CPU 1200000UL
#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#define PWM PB0 // OC0A
#define BUTTONPLUS PB3 // PCINT3
#define BUTTONMINUS PB4 // PCINT4
#define BUTTONPLUS_PRESSED (!((PINB >> BUTTONPLUS) & 1))
#define BUTTONMINUS_PRESSED (!((PINB >> BUTTONMINUS) & 1))
volatile uint8_t state = 0; // зафиксированное состояние
volatile uint8_t counter = 0; // счётчик для обработки нажатий кнопок
int main(void)
{
// Выход ШИМ:
DDRB |= (1 << PWM); // выход = 1
PORTB &= ~(1 << PWM); // по умолчанию отключен = 0
// Входы кнопок:
DDRB &= ~((1<<BUTTONPLUS)|(1<<BUTTONMINUS)); // входы = 0
PORTB |= (1<<BUTTONPLUS)|(1<<BUTTONMINUS); // подтянуты = 1
// Настройка таймера T0 для ШИМ и обработки нажатий кнопок:
TCCR0A = 0b10000001; // режим коррекции фазы ШИМ, неинверсный сигнал на выходе OC0A
TCCR0B = 0b00000010; // предделитель тактовой частоты CLK/8
TCNT0 = 0; // начальное значение счётчика
OCR0A = 0; // начальное значение регистра сравнения A (для формирования ШИМ сигнала на выходе)
TIMSK0 = 0b00000010; // разрешаем прерывания по переполнению счётчика
// Настройка внешних прерываний
GIMSK |= (1<<PCIE); // Разрешаем внешние прерывания PCINT0.
PCMSK |= (1<<BUTTONPLUS)|(1<<BUTTONMINUS); // Разрешаем по маске прерывания на ногак кнопок
sei(); //разрешаем глобально прерывания
// Основной цикл
while(1)
{
}
}
// Обработчик прерывания PCINT0
ISR(PCINT0_vect)
{
if ( state == 0 )
{
if ( BUTTONPLUS_PRESSED && !BUTTONMINUS_PRESSED ) // если нажата кнопка "+"
state = 1;
else if ( !BUTTONPLUS_PRESSED && BUTTONMINUS_PRESSED ) // если нажата кнопка "-"
state = 2;
}
}
ISR(TIM0_OVF_vect)
{
if ( state != 0 )
{
if (counter < 29)
counter++; // отсчитываем ~100мс
else
{
if (state == 1)
{
if (OCR0A <= 250)
OCR0A += 5; // увеличиваем значение регистра совпадения для ШИМ таймера, пока не достигнет максимума
if ( !BUTTONPLUS_PRESSED ) // если кнопка "+" была отпущена
state = 0; // сбрасываем состояние
}
else if (state == 2)
{
if (OCR0A >= 5)
OCR0A -= 5; // уменьшаем значение регистра совпадения для ШИМ таймера, пока не достигнет нуля
if ( !BUTTONMINUS_PRESSED ) // если кнопка "-" была отпущена
state = 0; // сбрасываем состояние
}
counter = 0; // сбрасываем счётчик
}
}
}
Я взял код из предыдущего примера ШИМ регулятора и переделал его под нашу задачу — управление двумя кнопками: при нажатии на кнопку «+» (PCINT3) скважность ШИМ сигнала будет уменьшаться, а среднее значение сигнала при этом будет увеличиваться до тех пор, пока коэффициент заполнения не станет максимальным; а при нажатии на кнопку «-» (PCINT4) — соответственно, наоборот.
Теперь поподробнее рассмотрим ключевые моменты нашего кода.
Так как кнопок у нас будет две, то использовать аппаратное прерывание INT0 тут не катит, поэтому используем программное прерывание PCINT0, которому по маске PCMSK разрешаем использование ножек PCINT3 и PCINT4, к которым у нас подключены кнопки. В обработчике прерывания PCINT0_vect проверяем, какая из кнопок была нажата и присваиваем соответствующее значение глобальной переменной state. Таким образом мы, как бы, фиксируем факт нажатия кнопки и всё. Всю же остальную рутину, включая реализацию антидребезга, оставляем на откуп прерыванию по таймеру.
Итак, мы дошли до таймера, давайте разбираться с ним.
В данном случае таймер у нас настраивается для генерирования ШИМ сигнала на выходе OC0A. Изменяя значение регистра сравнения OCR0A, мы будем изменять скважность ШИМ сигнала. Но, пока таймер генерирует ШИМ сигнал, ничто нам не мешает, так же, использовать его прерывания. Тут нам доступны два прерывания по совпадению с регистрами сравнения: OCR0A (он у нас занят для генерирования ШИМ сигнала) и OCR0B. А так же, прерывание по переполнению счётчика — вот его мы и будем использовать для выполнения всей основной рутины. Чтобы разрешить прерывания по переполнению счётчика, выставляем бит TOIE0 регистра TIMSK0.
Таким образом, одним таймером мы убили сразу двух зайцев. И при этом, у нас ещё в запасе осталось прерывание по совпадению с OCR0B, на которое, при необходимости, тоже можно повесить выполнение какой-нибудь полезной работы!
Ну и, наконец, посмотрим, что же у нас происходит в обработчике прерывания по переполнению таймера TIM0_OVF_vect.
Каждый раз, при срабатывании, обработчик проверяет значение глобальной переменной state. Если значение state отлично от нуля, значит была нажата одна из кнопок, поэтому нужно выдержать некоторую паузу, в течение которой утихнет дребезг контактов, в данном случае — 100мс. Для этого, внутри обработчика, образно говоря, запускается свой счётчик, с каждым очередным прерыванием увеличивающий значение глобальной переменной counter. Значение, до которого должен увеличиваться счётчик, определяется следующим образом:
counter = t*Fcpu/(8*2*T) = 0,1с*1200000Гц/(8*2*256) = 29,29
Где:
- t — требуемое время отсчёта (100мс = 0,1с);
- Fcpu — тактовая частота МК;
- 8 — предделитель частоты, выставленный в TCCR0B;
- T — период счёта таймера (от 0 до 255);
- 2 — т.к. используется ШИМ с коррекией фазы, то переполнение счётчика будет наступать каждые 2 периода счёта.
Таким образом, для выдержки паузы в 100мс, прерывание по переполнению счётчика должно наступить примерно 29 раз.
Затем, проверив значение переменной state, обработчик проверяет, какая именно из двух кнопок была нажата и, соответственно, изменяет значение регистра сравнения OCR0A в ту или иную сторону. После этого остаётся только проверить, осталась ли кнопка нажатой: если да, то процесс повторяется (при этом паузы в 100мс будут играть роль задержек между «ступеньками» изменения ШИМ сигнала). Если же кнопка была отпущена, то значение state сбрасывается в 0 и при следующем срабатывании прерывания обработчик, проверив значение state, просто завершится, ничего больше не делая.
Salim говорит:
Автор профи! Практически всё работает на прерываниях, код оптимизирован на все 100. Сам только учусь, и многое почерпнул из Ваших исходников. Спасибо!
Kamu говорит:
Здравствуйте. Не могу понять, почему в протеусе прошивка работает, а в железе нет. Подскажите в чём причина. Прошивку заливал на отладочную плату собранную по Вашей схеме. Другие примеры с ШИМ на плате работают. Только в примере регулятора ШИМ с использованием АЦП в коде есть опечатка.
OSBoy говорит:
Так какой пример не работает — этот, с кнопками? Или тот что с АЦП? Я в Протеусе их не пробовал, сразу опробовал в железе и оба прекрасно работают. И почему Вы решили, что опечатка в коде? Компилируются оба примера без ошибок, тем более в Протеусе, сами говорите, что работают нормально… Может у Вас в железе какая-то проблема? Чем компилируете?
Kamu говорит:
Извиняюсь, опечатка в бегущем огне volatile _Bool direction = 0.
Не работает ШИМ с кнопками. При нажатии кнопки на входе PB4 сразу загорается светодиод на выходе PB0. Гаснет только при сбросе микроконтроллера. Компилирую с помощью atmel studio 6,2.
OSBoy говорит:
Код ШИМ с кнопками я, как будет возможность, попробую ещё раз на отладочной плате протестировать.
А что не так с «volatile _Bool direction = 0» ? У Вас компилятор на тип переменной _Bool ругается? Если у Вас в студии 6.2 проект на C++, попробуйте заменить «_Bool» на «bool», хотя по идее должно и так, и так работать. Я в 4 студии компилирую, код на С написан и всё отлично работает. Если что, типы переменных _Bool или bool определены в стандартной библиотеке stdbool.h для С и С++ соответственно.
Kamu говорит:
Да, при компиляции программа выдавала ошибку. Заменил на bool ошибка пропала.
Alex говорит:
какой софт разработки используется?
OSBoy говорит:
AVR studio, в основном 4 версией пользуюсь.
Alex говорит:
Спасибо за подсказку и код! Вчера в протеусе все опробовал. шикарно.
А можно ли переделать этот код на два шим внутри одного микроконтроллера, используя скажем Tiny2313.
4 кнопки (две на регулировку оборотов кулера и две на регулировку температуры). Не будет ли проблем с прерываниями, да и вообще еще каких аномалий?
*Достался строй_фен со сломанной платой управления (понимаю что дешевле купить на Али).
OSBoy говорит:
Учитывая, что на Tiny2313 два таймера — думаю, что проблем не должно возникнуть, если сделать по аналогии. Хотя в принципе, думаю, что можно даже попробовать обойтись одним таймером на tiny13, используя вместо кнопок — две крутилки потенциометров и подавая с них напряжение на два входа АЦП. Например, для выходов ШИМ тогда можно использовать ноги OC0A (PB0) и OC0B (PB1), а для АЦП входов — ноги ADC3 (PB3) и ADC2 (PB4). А дальше дело техники — главное чтобы код в 1кБ влез.