Архитектура AVR в примерах/Широтно-импульсный модулятор

В этой работе мы рассмотрим простейшие примеры программирования одного из встроенных в МК ATmega8 16-разрядных счетчиков-таймеров[1] в режиме широтно-импульсной модуляции (ШИМ), а также основы использования энергосберегающего режима,[2] системы прерываний, и сторожевого таймера.[3]

Перед началом

править

Для данной работы, простейшее устройство на МК ATmega8 (или подобном) необходимо дополнить цепью, состоящей из последовательно соединенных резистора номиналом порядка 250 Ω и светодиода, подключаемой между выводом PB1 МК и «общим».

  • В качестве варианта, можно отключить уже имеющуюся на макетной панели цепь от вывода PB5 и подключить ее к PB1.
  • Отметим, что при использовании платы Arduino Uno или Arduino Nano, соответствующий вывод МК соединен с контактом D 10 одного из разъемов платы.

Все прочие требования предыдущей работы к среде разработки и устройству сохраняются и в данной.

Мерцание светодиода

править

/*** pwm.c — Feed a sawtooth-like wave to a LED at PB1  -*- C -*- */
#include <avr/interrupt.h>      /* for sei (), ISR () */
#include <avr/io.h>             /* for DDRB, PORTB, etc. */
#include <avr/sleep.h>          /* for sleep_enable (), etc. */
#include <avr/wdt.h>            /* for wdt_enable (), etc. */

#if (defined (TIMSK) && ! defined (TIMSK1))
/* ATmega8 compatibility */
#define TIMSK1  TIMSK
#endif

static volatile uint8_t seen_interrupt_p  = 0;
ISR (TIMER1_OVF_vect) {
  seen_interrupt_p   = 1;
}

int
main ()
{
  /* expecting a reset about every 10 ms */
  wdt_enable (WDTO_1S);

  /* set up Port B */
  /* PB1 (D 10) is OC1A;
     PB2 (D 11) is OC1B */
  DDRB    |= ((1    << DDB1));

  /* set up Timer/Counter 1 */
  TCCR1A   = ((1    << COM1A1)
              /* COM1A  =   10 _2: clear on match, set on BOT */
              /* WGM1   = 0111 _2: Fast PWM, 10 bit (1024) */
              | (1  << WGM11)
              | (1  << WGM10));
  TCCR1B   = ((1    << WGM12)
              /* CS1    =  011 _2: Divide clock by 64 */
              | (1  << CS11)
              | (1  << CS10));
  /* PWM frequency is thus F_CPU / 64 / 1024,
     or 112 Hz to 305 Hz for F_CPU of 7.3728 MHz to 20 MHz */

  /* NB: ignoring the datasheet recommendation! */
  sleep_enable ();

  /* enable interrupts */
  TIMSK1  |= (1     << TOIE1);  /* timer 1 overflow */
  sei ();

  /* main loop */
  uint16_t i;
  for (i = 0; ; ) {
    if (seen_interrupt_p) {
      /* NB: truncate to the least significant 10 bits */
      OCR1A  = (i & 0x3ff);
      i++;
      seen_interrupt_p  = 0;
    }

    /* reset the watchdog timer */
    wdt_reset ();

    /* sleep until the next event */
    sleep_cpu ();
  }

  /* not reached */
  /* . */
  return 0;
}
/*** pwm.c ends here */

Чтение кода

править

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

Управление модулятором

править
  1. Строка OCR1A = (i & 0x3ff); загружает в канал «A» широтно-импульсного модулятора младшие 10 бит значения целочисленной переменной i.

    Сразу же после этого i++; увеличивает значение i на единицу. При этом используется арифметика по модулю 65536; другими словами: 65535 + 1 = 0 (mod 65536). (В таком случае также говорят, что возникает переполнение разрядной сетки переменной i.)

  2. Переменная i объявлена строкой uint16_t i;, что означает:

    • значение переменной понимается как целое число без знака;
    • под хранение значения выделяется 16 двоичных разрядов (бит);
    • вышесказанное определяет диапазон значений — [0; 65535], а равно и использование арифметики по модулю 65536;
    • поскольку переменная объявлена перед циклом, значение i сохраняется между его итерациями (и доступно после его завершения.)
  3. Битовые поля управляющих регистров TCCR1A и TCCR1B инициализированы следующим образом:

    COM1A
    10₂ — на выходе канала «A» ШИМ будет установлен низкий уровень по достижении загруженного в канал значения, высокий — по установке счетчика в 0;
    WGM1
    0111₂ — будет использована 10-разрядная «быстрая» модуляция;
    CS1
    011₂ — на вход счетчика будут поступать деленные на 64 импульсы тактовой частоты МК (что, вместе с использованием 10-разрядной модуляции, дает диапазон несущей частоты ШИМ порядка 112 ÷ 305 Hz для тактовых частот МК 7.3728 ÷ 20 MHz.)
  4. Строка DDRB |= ((1 << DDB1)); настраивает вывод PB1 МК (он же выход канала «A» ШИМ — OC1A; он же вывод D 10 плат Arduino Uno, Arduino Nano) для использования в качестве выхода.

Обработка событий

править
  1. Условие if (seen_interrupt_p) { }, в которое заключены загрузка числа в канал ШИМ и инкремент переменной i, выполняется на очередной итерации основного цикла, если перед проверкой условия произошло переполнение счетчика-таймера 1 (используемого также как модулятор.)

    Разрядность счетчика ограничена 10 битами, так что условие будет выполняться не реже, чем один раз за каждые 1024 входных импульса.

  2. Строка seen_interrupt_p = 0; сбрасывает проверяемый условием выше флаг события «переполнение» seen_interrupt_p. Условие будет выполнено в том и только том случае, если после очередного такого сброса произойдет очередное переполнение.

  3. Установка флага seen_interrupt_p выполняется обработчиком прерывания TIMER1_OVF_vect (и является единственным действием, выполняемым этим обработчиком.)

  4. Сам флаг события объявлен строкой static volatile uint8_t seen_interrupt_p = 0; как 8-разрядная целочисленная переменная.

    • Эта же строка задает начальное (на момент начала выполнения функции main) значение для этой переменной.
    • Ключевое слово volatile указывает компилятору на возможность изменения значения этой переменной вне «обычной» последовательности выполнения программы.
  5. Используемое для определения обработчика прерывания макроопределение ISR определено заголовком avr/interrupt.h.

  6. Строка TIMSK1 |= (1 << TOIE1); разрешает формирование прерывания по переполнению счетчика-таймера 1.

    Вызов sei (); разрешает, собственно, прерывания — передачу управления установленным обработчикам прерываний перед выполнением очередной инструкции машинного кода при наступлении соответствующих (разрешенных) событий.

  7. Строка sleep_cpu (); осуществляет перевод МК в режим ожидания очередного события, в данном случае — прерывания по переполнению счетчика-таймера 1.

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

  8. Строка sleep_enable (); в инициализационной части кода разрешает переход в режим ожидания. Без такого разрешения, вызов sleep_cpu (); окажется недействителен.

Управление сторожевым таймером

править
  1. Чтобы обеспечить сброс системы в случае, если выполнение основного цикла по какой-либо причине прекратится (например, как следствие входа в «бесконечный цикл» внутри основного, или же вызова функции sleep_cpu при запрещенных прерываниях), строкой wdt_enable (WDTO_1S); в начале программы активируется сторожевой таймер.

  2. Строка wdt_reset (); основного цикла, в свою очередь, сбрасывает сторожевой таймер на каждой итерации (или же, учитывая настройки таймера и тактовую частоту не ниже порядка 7.3728 MHz, — не реже, чем каждые порядка 10 ms), предотвращая аварийный сброс МК.

  3. Функции wdt_enable и wdt_reset объявлены заголовком avr/wdt.h.

Сборка

править
  1. Внесем в созданный ранее Makefile (управляющий файл Make) следующие зависимости:

    default: pwm.hex pwm
    
    pwm:    pwm.c
    
  2. Создадим файл pwm.c приведенного выше содержания.

  3. Соберем рассматриваемый пример выполнив команду make:

    $ make pwm.hex 
    avr-gcc -O2 -Wall -std=gnu11 -mmcu=atmega8 -DF_CPU=7372800   ../pwm.c   -o pwm
    avr-objcopy -O ihex pwm pwm.hex
    $ 
    
    NB

    При использовании микроконтроллера, отличного от ATmega8, или же кварцевого резонатора на частоту, отличную от 7.3728 MHz, следует явно указать параметры сборки MCU или F_CPU, соответственно.

    Так, для платы Arduino Uno R3, поставляемой с МК ATmega328P и кварцевым резонатором на 20 MHz, команда сборки может быть следующей:

    $ make MCU=atmega328p F_CPU=20000000 pwm.hex 
    
  4. Удостоверимся в отсутствии ошибок сборки и в появлении файла pwm.hex, содержащего результирующий машинный код в формате Intel hex.

Загрузка и проверка работоспособности

править
  1. Подключим устройство к USB-порту основной системы. Удостоверимся в появлении соответствующего файла устройства/dev/ttyUSB1 или подобного.

  2. Проверим возможность связи с загрузчиком Optiboot сохранив образ текущего состояния flash-памяти МК в файл формата Intel hex, подобно:

    $ avrdude -P /dev/ttyUSB1 -c arduino -b 115200 -p atmega8 \
          -U flash:r:"$(mktemp --suffix=.hex -- ./XXXX)":i 
    
    NB
    Данное ранее замечание о сбросе устройства и запуске Optiboot остается справедливым и для данной работы.
  3. Загрузим полученный образ во flash-память МК, подобно:

    $ avrdude -P /dev/ttyUSB1 -c arduino -b 115200 \
          -p atmega8 -U flash:w:pwm.hex:i 
    
  4. Удостоверимся в правильности работы программы и устройства оценив период вспышек светодиода и сопоставив его с вычисленным исходя из тактовой частоты МК по формуле T = 2²⁶ ∕ ƒMCU.

Исследование

править
  1. В предложенном коде задействован сторожевой таймер, автоматически сбрасывающий МК если в течение заданного параметром wdt_enable интервала не будет выполнен его сброс (wdt_reset.)[3] Исследуйте, как изменится поведение системы при «ошибочном» удалении из кода следующих строк (по-отдельности или в каких-либо сочетаниях):

    • установки флага TOIE1 регистра TIMSK1;
    • разрешения прерываний sei ();
    • сброса таймера wdt_reset ().
  2. В данном примере, несущая частота для широтно-импульсной модуляции образуется делением частоты процессора на 64 ((1 << CS11) | (1 < CS10)).[4] Исследуйте работу системы при использовании других делителей:

    • 8 — (1 << CS11);
    • 256 — (1 << CS12).
  3. Программа выше плавно увеличивает яркость свечения светодиода от минимума до максимума, после чего яркость скачком вновь изменяется до минимальной. Попробуйте изменить код так, чтобы изменение яркости от максимума до минимума также было плавным.

  4. Попробуйте реализовать в программе изменение скважности импульсов ШИМ по синусоидальному (или близкому к нему кубическому) закону. Если удастся, реализуйте алгоритм с использованием как чисел с плавающей запятой, так и чисел с фиксированной запятой, и сравните объемы результирующего кода.

  5. Используя паспорт (англ. datasheet) на используемый МК, попробуйте изменить код для получения модулированного сигнала на выводе PB2 (OC1B; D 11 для Arduino Uno и подобных).

Примечания

править
  1. 16-bit Timer/Counter1. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.
  2. Power Management and Sleep Modes. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.
  3. 3,0 3,1 Watchdog Timer. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.
  4. Timer/Counter0 and Timer/Counter1 Prescalers. ATmega8, ATmega8L: 8-bit Atmel with 8 KBytes In-System Programmable Flash. Проверено 29 апреля 2013.