Что такое ШИМ и как он работает я особо подробно расписывать не буду, информацию без труда найдёте на просторах интернета. Коснусь лишь общих понятий. ШИМ — это Широтно-Импульсная Модуляция, (по-английски PWM — Pulse Width Modulation) уже из самого названия ясно, что здесь что-то связанное с импульсами и их шириной. Если изменять ширину (длительность) импульсов постоянной частоты, то можно управлять, например, яркостью источника света, скоростью вращения вала электродвигателя или температурой какого-либо нагревательного элемента. Обычно, именно с помощью ШИМ микроконтроллер управляет подобной нагрузкой. Микроконтроллеры имеют аппаратную реализацию ШИМ, но, к сожалению, количество аппаратных ШИМ-каналов ограничено, например, в AТmega88 их аж шесть штук, в ATtiny2313 — четыре, в ATmega8 — три, а в ATtiny13 только два. В AVR ШИМ-каналы используют таймеры и их регистры сравнения OCRxx. Изменяя их содержимое и задавая параметры таймеров, в зависимости от задач, можно управлять состоянием, связанного с регистром, выхода — подавать на него 1 либо 0. То же самое можно организовать программно, управляя любым выводом контроллера, а главное, реализовать большее количество ШИМ-каналов, чем имеется на борту аппаратных. Практически, количество каналов ограничено лишь количеством ножек-выводов микроконтроллера (по крайней мере, если говорить о семействах Mega или Tiny). Как оказалось, алгоритм довольно прост, но у меня ушло некоторое время на его понимание и полное осознание.
Данный алгоритм подробно изложен в оригинальном AVR136: Low-Jitter Multi-Channel Software PWM. Принцип работы программной реализации заключается в имитации работы таймерa в режиме ШИМ. Требуемая длительность импульсов задаётся переменными, соответственно, по одной на каждый канал (в моём коде lev_ch1, lev_ch2, lev_ch3), а так же задаются «близнецы» этих переменных, которые хранят значение для конкретного периода работы таймера (в моём коде buf_lev_ch1, buf_lev_ch2, buf_lev_ch3). Восьмибитный таймер запускается на основной частоте МК и генерирует прерывание по переполнению, то есть, каждые 256 тактов. Это накладывает ограничение на длительность процедуры обработки прерывания — необходимо уложиться в 256 тактов, чтобы не пропустить следующее прерывание. В результате, один полный период ШИМ равняется 256*256=65536-и тактам. Восьмибитная переменная-счетчик (в моём примере counter) увеличивается на единицу каждое прерывание и действует, как указатель позиции внутри цикла ШИМ. Всё это обеспечивает разрешение (минимальный шаг) ШИМ в 1/256, а частоту импульсов в ƒ/(256*256), где ƒ-частота задающего генератора микроконтроллера. Следует заметить, что тактовая частота микроконтроллера должна быть довольно высокой. В моём примере ATtiny13 работает на максимально возможной частоте, без применения внешнего генератора — 9,6МГц. Это даёт период ШИМ в 9600000/65536≈146,5Гц чего вполне достаточно в большинстве случаев.
Код на C, пример реализации идеи для МК ATtiny13 (три канала ШИМ на выводах PB0, PB1, PB2):
#define F_CPU 9600000 //fuse LOW=0x7a
#include <avr/interrupt.h>
#include <util/delay.h>
uint8_t counter=0;
uint8_t lev_ch1, lev_ch2, lev_ch3;
uint8_t buf_lev_ch1, buf_lev_ch2, buf_lev_ch3;
void delay_ms(uint8_t ms) //функция задержки
{
while (ms)
{
_delay_ms(1);
ms--;
}
}
int main(void)
{
DDRB=0b00000111; // установка PortB пины 0,1,2 выходы
TIMSK0 = 0b00000010; // включить прерывание по переполнению таймера
TCCR0B = 0b00000001; // настройка таймера, делитель выкл
sei(); // разрешить прерывания
lev_ch1=0; //начальные значения
lev_ch2=64; //длительности ШИМ
lev_ch3=128; //трёх каналов
while (1) //бесконечная шарманка
{
for (uint8_t i=0;i<255;i++)
{
lev_ch1++; //увеличеваем значения
lev_ch2++; //длительности ШИМ
lev_ch3++; //каждого канала
delay_ms(50); //пауза 50мс
}
}
}
ISR (TIM0_OVF_vect) //обработка прерывания по переполнению таймера
{
if (++counter==0) //счетчик перехода таймера через ноль
{
buf_lev_ch1=lev_ch1; //значения длительности ШИМ
buf_lev_ch2=lev_ch2;
buf_lev_ch3=lev_ch3;
PORTB |=(1<<PB0)|(1<<PB1)|(1<<PB2); //подаем 1 на все каналы
}
if (counter==buf_lev_ch1) PORTB&=~(1<<PB1); //подаем 0 на канал
if (counter==buf_lev_ch2) PORTB&=~(1<<PB0); //по достижении
if (counter==buf_lev_ch3) PORTB&=~(1<<PB2); //заданной длительности.
}
Думаю, всё достаточно наглядно и пояснения излишни. Для значений длительности и их буферов, при большем числе каналов, возможно, будет лучше использовать массивы, но в данном примере, я этого делать не стал, ради большей наглядности.
Проверено на avr-gcc-4.7.1 и avr-libc-1.8.0. Компиляция и получение файла прошивки:
avr-gcc -mmcu=attiny13 -Wall -Wstrict-prototypes -Os -mcall-prologues -std=c99 -o softPWM.obj softPWM.c
avr-objcopy -O ihex softPWM.obj softPWM.hex
Для правильной работы нужно выставить младшие fuse-биты в 0x7a (частота 9,6МГц). в avrdude это, например, делается так:
avrdude -p t13 -c usbasp -U lfuse:w:0x7a:m
Мой вариант реализации на ассемблере. Программа делает абсолютно то же самое, что и предыдущий код на C.
;чтобы не тянуть include-файл
.list
.equ DDRB= 0x17
.equ PORTB= 0x18
.equ RAMEND= 0x009f
.equ SPL= 0x3d
.equ TCCR0B= 0x33
.equ TIMSK0= 0x39
.equ SREG= 0x3f
;это лишь демонстрация, потому регистров и не жалеем
.def temp=R16
.def lev_ch1=R17
.def lev_ch2=R18
.def lev_ch3=R19
.def buf_lev_ch1=R13
.def buf_lev_ch2=R14
.def buf_lev_ch3=R15
.def counter=R20
.def delay0=R21
.def delay1=R22
.def delay2=R23
.cseg
.org 0
;таблица прерываний из даташита:
rjmp RESET ; Reset Handler
rjmp EXT_INT0 ; IRQ0 Handler
rjmp PIN_CHG_IRQ ; PCINT0 Handler
rjmp TIM0_OVF ; Timer0 Overflow Handler
rjmp EE_RDY ; EEPROM Ready Handler
rjmp ANA_COMP ; Analog Comparator Handler
rjmp TIM0_COMPA ; Timer0 CompareA Handler
rjmp TIM0_COMPB ; Timer0 CompareB Handler
rjmp WATCHDOG ; Watchdog Interrupt Handler
rjmp ADC_IRQ ; ADC Conversion Handler
;RESET:
EXT_INT0:
PIN_CHG_IRQ:
;TIM0_OVF:
EE_RDY:
ANA_COMP:
TIM0_COMPA:
TIM0_COMPB:
WATCHDOG:
ADC_IRQ:
reti
RESET:
ldi temp,0b00000111 ; назначаем PortB пины PB0, PB1
out DDRB,temp ; и PB2 выходами
ldi temp,0 ; выставляем все выводы
out PORTB,temp ; PortB в 0
ldi temp,low(RAMEND) ; инициализация
out SPL,temp ; стека
ldi temp,0b00000001 ; вкл. таймер
out TCCR0B,temp ; без делителя
ldi temp,0b00000010 ; вкл. прерывание
out TIMSK0,temp ; таймера по переполнению
sei ; разрешить прерывания
start_pwm: ; бесконечная шарманка
inc lev_ch1 ; увеличиваем значения
inc lev_ch2 ; длительности ШИМ
inc lev_ch3 ; по всем каналам
rcall delay ; небольшая пауза для плавности
rjmp start_pwm
delay: ; процедура задержки
ldi delay2,$01 ; выставляем число
ldi delay1,$77 ; до скольки считать
ldi delay0,$00 ; $017700 - даст задержку в 50мс
loop:
subi delay0,1 ; считаем
sbci delay1,0 ; считаем
sbci delay2,0 ; считаем
brcc loop
ret
TIM0_OVF: ; обработка прерывания таймера
push temp ; на всякий пожарный сохраняем
in temp,SREG ; temp и SREG в стеке
push temp
inc counter ; счетчик перехода таймера через 0
cpi counter,0 ; если не 0, то проверяем
brne ch1_off ; не надо ли чего погасить
mov buf_lev_ch1,lev_ch1 ; если счетчик 0
mov buf_lev_ch2,lev_ch2 ; то задаем новые
mov buf_lev_ch3,lev_ch3 ; значения длительности ШИМ каналов
ldi temp,0b00000111 ; включить все
out PORTB,temp ; три выхода
ch1_off: ; а не погасить ли нам
cp counter,buf_lev_ch1 ; первый канал?
brne ch2_off ; нет, рано - проверяем второй
cbi PORTB,0 ; да погасить
ch2_off: ; а не погасить ли нам
cp counter,buf_lev_ch2 ; второй канал?
brne ch3_off ; нет, рано - проверяем третий
cbi PORTB,1 ; да погасить
ch3_off: ; а не погасить ли нам
cp counter,buf_lev_ch3 ; третий канал?
brne irq_end ; нет, рано - двигаемся к выходу из прерывания
cbi PORTB,2 ; да, погасить
irq_end: ; достаем из стека
pop temp ; SREG и temp
out SREG,temp
pop temp
reti ;выходим из прерывания
Компилируется с помощью avra или tavrasm. Не забыть про fuse-биты (см. выше).
Автор: lnx
Неплохая реализация )) А что если сделать на той же Тини13 , полностью автономны
й трёканальный драйвер с управлением по I2C ? )) вот есть пример:
http://forum.easyelectronics.ru/viewtopic.php?f=16&t=9532
и частоты можно настраивать от 460 до 36 000 Гц ! ))
Дешевле и проще купить готовый, например, WS2801SO.