mikroPascal for AVR. Урок 8. Программный ШИМ

В прошлой статье-уроке была затронута тема ШИМ. А точнее — использование аппаратного ШИМ в МК семейства AVR в программах, написанных в mikroPascal. Но, как вы уже заметили, число каналов аппаратно реализованных ШИМ сильно ограничено. В лучшем случае, их количество около шести. Конечно, если вам нужно всего лишь регулировать яркость небольшого количества светодиодов, то хватит и их. Но если нужно большее количество каналов (например, если захотите на основе МК сделать 3-хфазный генератор)? Вот тут и пригодится программный ШИМ.

В предыдущих статьях уже проскакивало понятие «программный ШИМ», но я на нем не останавливался. Сейчас постараюсь восполнить этот пробел.

Как и аппаратный, программный ШИМ так же работает за счет таймеров МК. Но в отличии от него, проверяется на совпадение не регистр МК, а какая-то произвольная переменная.

Реализовать программный ШИМ можно по-разному. К примеру, в прерывании по переполнению таймера просто инкрементировать значение счетной переменной, а уже в основной программе проверять на совпадение с заданным значением. Если же у вас программа не из простых, то лучше это делать в прерывании. В случае простой программы, не перегруженной задержками, можно все действия перенести в главный цикл, и отказаться от использования прерываний. Как будет лучше поступить в вашем случае, решать вам, в этом уроке я постараюсь показать различные реализации программного ШИМ а так же библиотеку для простой работы с ним.

Начнем с простого: программный ШИМ, сформированный в главном цикле программы. Будем управлять яркостью светодиода (к сожалению, у меня сечйчас нет под рукой макетной платы, так что все испытания будут проходить в Proteus 8.2).

Схема предельно проста: микроконтроллер, светодиод и токоограничительный резистор (хотя в симуляторе он не очень то и нужен).

На предполагаемый выход ШИМ я «подцепил» канал А виртуального осциллографа, потому что наблюдать изменение яркости светодиода у нас не получится, в силу некоторых особенностей Proteus.

Код, загруженный в МК, выглядит так:

program _demo_pwm_light;

var set_duty, n: byte;
begin
PORTB := 0xff; //Настраиваем порт
DDRB := 0xff;

set_duty := 35; //Задаем длительность

while TRUE do
begin
if (set_duty = 0) then //Проверка на граничные значения
PORTB.0 := 0
else
if (set_duty = 255) then // —//—
PORTB.0 := 1
else
if (set_duty <= n) then //Если нам не нужна инверсия, ставим set_duty > n
PORTB.0 := 1
else
PORTB.0 := 0;

inc(n);
end;
end.

Логика программы предельно проста. Первым делом настраиваем порт, далее нам необходимо создать бесконечный цикл, в котором мы и будем имитировать ШИМ. Проверки на совпадение с 0 и 255 необходимы для предотвращения неприятных моментов, когда вроде бы сигнала быть не должно, а он есть. Сам сигнал генерируется следующим образом (возможно, мои слова не совсем соответствуют истине, но постараюсь объяснить так, как сам это представляю):

Верхняя строка соответствует переменной n = 255. Нижняя — переменной set_duty = 120. Для получения ШИМ сигнала нам необходимо увеличивать в цикле значение перменной n от 0 до 255. Пока переменная n > set_duty, на выходе низкий логический уровень, как лишь n > set_duty, то устанавливаем высокий лог. уровень. После обнуления переменной n все повторяется и т.д. Тоесть для генерации ШИМ сигнала нам требуется проверять переменные на совпадение и устанавливать лог. уровни в зависимости от результата.

Вот скриншот полученной осциллограммы:

Но, то что было показано выше, далеко не всегда можно применять. К примеру, у вас в программе используется большое количество задержек (либо ваш код критичен к задержкам) тогда первый вариант не подходит. Но на помощь приходят прерывания.

В целом, отличий между первым и вторым примером не много. Главное из них — это использования прерывания по переполнению таймера T0. В остальном же, полная аналогия с предыдущим примером.

Ниже привожу код второго примера:

program _demo_pwm_interrupt;

var set_duty, n: byte;

procedure _TIMER0(); iv IVT_ADDR_TIMER0_OVF; ics ICS_AUTO;
begin
//Мы просто взяли кусок кода из прошлого примера
if (set_duty = 0) then //Проверка на граничные значения
PORTB.0 := 0
else
if (set_duty = 255) then // —//—
PORTB.0 := 1
else
if (set_duty < n) then //Если нам не нужна инверсия, ставим set_duty > n
PORTB.0 := 1
else
PORTB.0 := 0;

inc(n);
end;

begin
PORTB := 0xff;
DDRB := 0xff;

TCCR0 := %00000001; //Настраиваем предделитель таймера (1:1)
TIMSK := %00000001; //Разрешаем прерывания по переполнению T0
SREG_I_bit := 1; //Глобальное разрешение прерываний

while TRUE do
begin
set_duty := 100; //Тут можно дать волю фантазии =)
delay_ms(1000);
set_duty := 200;
delay_ms(1000);
set_duty := 30;
delay_ms(1000);
end;
end.

Использование прерываний позволило освободить главный цикл программы. В остальном, как я уже говорил, различий нет.

Вот результат выполнения этой программы:

Похожим образом можно сделать и многоканальный ШИМ.

Хочу представить вашему вниманию библиотеку для mikroPascal foe AVR , которая позволяет использовать программный ШИМ. На ее примере и будет рассмотрена возможность создания многоканального ШИМ. Файл библиотеки прикреплен в конце статьи.

Посредством этой библиотеки возможно реализовать программный ШИМ на несколько каналов. Что немаловажно, для ее использования совершенно что-либо знать о прерываниях, достаточно всего лишь убедиться, что у вашего микроконтроллера имеется Таймер/Счетчик 0 (пока что реализовано для микроконтроллеров ATmega8, 48 и т.д. , регистры Таймера/Счетчика 0 не отличаются), и он не используется. Сверх этого, придется ещё посчитать, на какой частоте будет работать ШИМ. Но об этом несколько позже.

Первое, что бросается в глаза при открытии файла libpwm.pas (файл библиотеки), это большое количество меток.

////////////////////////////////////////////////////////////////////////////////
var CH0: sbit at PORTB.0; //
var CH1: sbit at PORTB.1; //
var CH2: sbit at PORTB.2; //
var CH3: sbit at PORTB.3; //
var CH4: sbit at PORTB.4; //
var CH5: sbit at PORTB.5; //
var CH6: sbit at PORTB.6; //
var CH7: sbit at PORTB.7; //
//
var CHD0: sbit at DDRB.0; //
var CHD1: sbit at DDRB.1; //
var CHD2: sbit at DDRB.2; //
var CHD3: sbit at DDRB.3; //
var CHD4: sbit at DDRB.4; //
var CHD5: sbit at DDRB.5; //
var CHD6: sbit at DDRB.6; //
var CHD7: sbit at DDRB.7; //
////////////////////////////////////////////////////////////////////////////////

Это сделано для упрощения обращения к портам, а так же для универсальности. Достаточно изменить строки

var CH0: sbit at PORTB.0
…..
var CHD0: sbit at DDRB.0

на следующие

var CH0: sbit at PORTC.0

var CHD0: sbit at DDRC.0

как для первого канала ШИМ будет использоваться не нулевой бит порта B, а аналогичный бит порта C. Как уже стало понятным из вышесказанного, для использования библиотеки, необходимо как минимум проверить (если вас устраивает использование порта B), есть ли порт, указанный в заголовке, и свободен ли он.

Следующий шаг — это прерывание по переполнению Таймера/Счетчика 0. В нем и происходит обработка и вывод ШИМ сигнала.

procedure _TIMER0(); iv IVT_ADDR_TIMER0_OVF; ics ICS_AUTO;
begin
if (CH_array[n] = 0) then
begin
case n of
0: CH0 := 0;
1: CH1 := 0;
2: CH2 := 0;
3: CH3 := 0;
4: CH4 := 0;
5: CH5 := 0;
6: CH6 := 0;
7: CH7 := 0;
end;
end
else
if (CH_array[n] = 255) then
begin
case n of
0: CH0 := 1;
1: CH1 := 1;
2: CH2 := 1;
3: CH3 := 1;
4: CH4 := 1;
5: CH5 := 1;
6: CH6 := 1;
7: CH7 := 1;
end;
end
else
begin
case n of
0: if (CH_array[n] > count) then CH0 := 1 else CH0 := 0;
1: if (CH_array[n] > count) then CH1 := 1 else CH1 := 0;
2: if (CH_array[n] > count) then CH2 := 1 else CH2 := 0;
3: if (CH_array[n] > count) then CH3 := 1 else CH3 := 0;
4: if (CH_array[n] > count) then CH4 := 1 else CH4 := 0;
5: if (CH_array[n] > count) then CH5 := 1 else CH5 := 0;
6: if (CH_array[n] > count) then CH6 := 1 else CH6 := 0;
7: if (CH_array[n] > count) then CH7 := 1 else CH7 := 0;
end;
end;
if (n < n_ — 1) then
inc(n)
else
n := 0;
inc(count);
end;

Выглядит она несколько громоздко, но на самом деле делится на три блока, из которых выполняется всегда лишь один (конструкции if…else). Первоначално, происходит проверка на граничные значения(0, 255), это сделано для предотвращения появления лог. единицы или нуля на выходе, когда их там быть не должно. Массив «CH_array[]» является образцом — с ним сравнивается значения переменной — счетчика «count». Пока значение соответствующего элемента массива меньше счетчика, то на выходе будет лог. ноль, в противном случае — единица (это не считая граничных значений, когда элемент массива равен нулю или 255). Так как переменная «count» является 8-ми битной, то ее нет смысла обнулять, при ее переполнении (после значения 255) она вновь равна 0.

Это были основные моменты, все остальное является лишь «оберткой».

procedure sPWM_init(clock_div: word; n: byte);
var i: byte;
begin
n_ := n;
for i := 0 to n_ — 1 do
begin
case i of
0: CHD0 := 1;
1: CHD1 := 1;
2: CHD2 := 1;
3: CHD3 := 1;
4: CHD4 := 1;
5: CHD5 := 1;
6: CHD6 := 1;
7:CHD7 := 1;
end;
end;
//
case clock_div of
1: TCCR0 := %00000001;
8: TCCR0 := %00000010;
64: TCCR0 := %00000011;
256: TCCR0 := %00000100;
1024: TCCR0 := %00000101;
end;
TOIE0_bit := 1;
end;

procedure sPWM_set_duty(channel, duty: byte);
begin
CH_array[channel] := duty;
end;

procedure sPWM_start();
begin
SREG_I_bit := 1;
end;

procedure sPWM_stop();
begin
SREG_I_bit := 0;
end;

procedure sPWM_destroy();
var i: byte;
begin
for i := 0 to n_ — 1 do
begin
case i of
0: CHD0 := 0;
1: CHD1 := 0;
2: CHD2 := 0;
3: CHD3 := 0;
4: CHD4 := 0;
5: CHD5 := 0;
6: CHD6 := 0;
7: CHD7 := 0;
end;
end;
//
TCCR0 := %00000000;
TOIE0_bit := 0;
end;

Ещё заслуживают внимания лишь две процедуры: «procedure sPWM_init(clock_div: word; n: byte)», и «procedure sPWM_destroy».

Первая , как понятно из названия, отвечает за инициализацию ШИМ, вторая за его «уничтожение». Конечно, о никаком реальном уничтожении речь не идет, просто при ее вызове отключается предделитель Таймера/Счетчика 0, сбрасываются каналы и отключается прерывание по переполнению Т0. Это может быть полезным, если вам в программе стал необходим Т0.

Что касается «sPWM_init(clock_div: word; n: byte)», то тут также все просто. С помощью цикла for подключается нужное число каналов, а следом и предделитель к Таймеру/Счетчику 0.

Собственно, вот и все библиотека. На закуску мизерная программка с использованием libpwm.pas:

program _demo_pwm;

uses libpwm;

begin
sPWM_init(1, 6);
sPWM_start();
sPWM_set_duty(0, 100);
sPWM_set_duty(1, 200);
sPWM_set_duty(2, 100);
sPWM_set_duty(3, 200);
sPWM_set_duty(4, 100);
sPWM_set_duty(5, 200);
delay_ms(5000);
sPWM_stop();
delay_ms(1000);
sPWM_destroy();
end.

Комментарии я писать не стал, по причине того, что просто незачем. Названия процедур говорят сами за себя, да и всегда можно заглянуть в исходник библиотеки.

Результат:

Спасибо за внимание! Надеюсь, эта статья хотя бы немного вам помогла!


Прикрепленные файлы:

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

Ваш адрес email не будет опубликован.