Многоканальный программный ШИМ в AVR

Что такое ШИМ и как он работает особо подробно расписывать не буду, информацию без труда найдёте на просторах интернета (например, неплохо расписано здесь). ШИМ – это Широтно-Импульсная Модуляция, (по-английски PWM – Pulse Width Modulation) уже из самого названия ясно, что здесь что-то связанное с импульсами и их шириной. Если изменять ширину (длительность) импульсов постоянной частоты, то можно управлять, например, яркостью источника света, скоростью вращения вала электродвигателя или температурой какого-либо нагревательного элемента. Обычно, именно с помощью ШИМ микроконтроллер управляет подобной нагрузкой. Микроконтроллеры имеют аппаратную реализацию ШИМ, но, к сожалению, количество аппаратных ШИМ-каналов ограничено, например, в AТmega88 их аж шесть штук, в ATtiny2313 – четыре, в ATmega8 – три, а в ATtiny13 только два. В AVR ШИМ-каналы используют таймеры и их регистры сравнения OCRxx. Изменяя их содержимое и задавая параметры таймеров, в зависимости от задач, можно управлять состоянием, связанного с регистром, выхода – подавать на него 1 либо 0. То же самое можно организовать программно, управляя любым выводом контроллера, а главное, реализовать большее количество ШИМ-каналов, чем имеется на борту аппаратных. Практически, количество каналов ограничено лишь количеством ножек-выводов микроконтроллера (по крайней мере, если говорить о семействах Mega или Tiny). Как оказалось, алгоритм довольно прост.

Данный алгоритм подробно изложен в оригинальном AVR136: Low-Jitter Multi-Channel Software PWM (ссылка на сайт Atmel. PDF). Ссылка на оригинальный архив с их кодом реализации здесь. Принцип работы программного ШИМ заключается в имитации работы таймера в режиме ШИМ.  Требуемая длительность импульсов задаётся переменными, соответственно,  по одной на каждый канал (в моём коде 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-биты (см. выше).

Скачать архив с исходниками примеров.

Оставить комментарий

Ваш e-mail адрес не будет опубликован.
Обязательные поля отмечены *

*

  +  75  =  79

Я ознакомился и соглашаюсь с правилами