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

в 9:36, , рубрики: arduino, avr, PWM, Программинг микроконтроллеров, метки: , ,

Что такое ШИМ и как он работает я особо подробно расписывать не буду, информацию без труда найдёте на просторах интернета. Коснусь лишь общих понятий. ШИМ — это Широтно-Импульсная Модуляция, (по-английски 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


  1. riko_81:

    Неплохая реализация )) А что если сделать на той же Тини13 , полностью автономны

  2. riko_81:

    й трёканальный драйвер с управлением по I2C ? )) вот есть пример:
    http://forum.easyelectronics.ru/viewtopic.php?f=16&t=9532
    и частоты можно настраивать от 460 до 36 000 Гц ! ))

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js