Руководство · Средний · 35 min

Таймеры 8051 и программный PWM для управления двигателем

В 8051 нет аппаратного PWM: его создают по прерыванию таймера 0. Разбираем автоперезагрузку, компромисс между частотой и разрешением и управление H-мостом.

Введение

Arduino задаёт скорость двигателя вызовом analogWrite(pin, 180). За этим единственным вызовом скрывается аппаратный периферийный блок, который непрерывно и бесплатно генерирует сигнал, пока ЦП занимается другими делами.

У 8051 такого периферийного блока нет. Ни один элемент микросхемы не может создавать сигнал с переменным рабочим циклом. Если нужно задать двигателю рабочий цикл ШИМ 70%, придётся сформировать сигнал самостоятельно через прерывание таймера, заплатив за это временем ЦП (CPU). Создание такого сигнала — самое полезное упражнение на этой микросхеме, поскольку делает видимым именно то, что analogWrite() делал за вас.

Здесь предполагается, что вы уже знакомы с портами. Если PWM ещё не знаком, руководство PWM и H-мост объясняет, почему быстрое включение и выключение двигателя эквивалентно изменению его напряжения.

Доступные таймеры

У классического 8051 есть два 16-битных таймера/счётчика — таймер 0 и таймер 1; AT89S52 добавляет третий. Каждый считает машинные циклы — по двенадцать периодов генератора, — поэтому при 11.0592 MHz счётчик каждого таймера увеличивается каждые 1.085 µs.

На практике один из них фактически занят. Таймер 1 почти всегда нужен последовательному порту, который получает скорость передачи из переполнений таймера 1. Поэтому таймером общего назначения остаётся таймер 0, и именно на нём строят PWM.

Два регистра настраивают таймеры:

  • TMOD выбирает режим. Младший ниббл относится к таймеру 0, старший — к таймеру 1.
  • TCON хранит биты запуска (TR0, TR1) и флаги переполнения (TF0, TF1).

У таймера 0 есть четыре режима, но для периодического тика подходит только один:

Режим Младший ниббл TMOD Что делает
0 0x00 13-битный счёт — реликт совместимости
1 0x01 16-битный счёт, перезагрузка вручную при каждом переполнении
2 0x02 8-битный счёт с автоматической перезагрузкой
3 0x03 Разделяет таймер 0 на два 8-битных таймера

Нужен режим 2. При переполнении аппаратура сама копирует TH0 обратно в TL0, поэтому период остаётся точным. В режиме 1 обработчик прерывания должен сам перезагружать счётчик, и каждый машинный цикл между переполнением и записью — это цикл накопления ошибки, который со временем только растёт.

Настройка тика

В режиме 2 таймер считает от TH0 до 255 и переполняется, поэтому период равен расстоянию, которое он должен пройти:

cycles per tick = 256 − TH0
tick period     = (256 − TH0) × 12 / f_osc

TH0 = 156, f_osc = 11.0592 MHz
  cycles = 100
  tick   = 100 × 12 / 11 059 200 = 108.5 µs

Для включения нужны пять строк:

#include <reg52.h>

TMOD = (TMOD & 0xF0) | 0x02;   // Timer 0 -> mode 2, leave Timer 1 alone
TH0  = 156;                    // auto-reload value: 100 machine cycles
TL0  = 156;                    // first count starts from the same place
ET0  = 1;                      // enable the Timer 0 interrupt
EA   = 1;                      // global interrupt enable
TR0  = 1;                      // start counting

Здесь важна часть TMOD & 0xF0. TMOD не поддерживает побитовую адресацию, поэтому запись TMOD = 0x02 затрёт биты режима таймера 1 вместе с остальными — а если UART работает, вы только что сломали последовательный вывод, настраивая двигатель.

PWM из счётчика

Когда периодическое прерывание готово, PWM сводится к учёту. Считайте тики до длины периода N и удерживайте выход высоким, пока счётчик меньше заданного рабочего значения ШИМ:

#define PWM_LEVELS 20          // ticks per PWM period

sbit ENA = P2^0;               // H-bridge enable — the PWM'd pin

volatile unsigned char tick   = 0;
volatile unsigned char duty_a = 0;    // 0 .. PWM_LEVELS

void timer0_isr(void) interrupt 1 {
    if (++tick >= PWM_LEVELS) tick = 0;
    ENA = (tick < duty_a);     // high for the first duty_a ticks of each period
}

interrupt 1 — вектор таймера 0 (адрес 0x000B). Аппаратура сбрасывает флаг переполнения TF0 при переходе к вектору, поэтому, в отличие от флагов, которые опрашивают вручную, его сбрасывать не нужно.

Теперь задание скорости сводится к одной переменной:

duty_a = 14;     // 14/20 = 70% duty
Временная диаграмма за четыре миллисекунды показывает программный PWM, построенный на таймере 0. Ряд равномерно расположенных отметок тиков вдоль нижней части обозначает одно прерывание таймера 0 каждые 108.5 микросекунды при TH0, равном 156. Над ними прямоугольный сигнал чередует 0 вольт и 5 вольт, оставаясь высоким в течение первых семи тиков каждого периода из двадцати тиков и низким в течение оставшихся тринадцати. Двусторонняя стрелка охватывает полный период и подписана как двадцать тиков равно 2.17 миллисекунды равно 460.8 герц, а вторая короткая стрелка охватывает высокую часть и подписана как семь на двадцать равно 35 процентов.
Двадцать тиков образуют период; заданное значение определяет, сколько из них находятся на высоком уровне. Частота прерываний фиксирована — меняется только отношение. Скачать SVG

Баланс, которого нельзя избежать

Частота ШИМ, разрешение по скважности и нагрузка на ЦП связаны одной формулой:

f_pwm = f_osc / (12 × C × N)

C = machine cycles per tick  (256 − TH0)
N = ticks per PWM period     (also your number of duty steps)

Зафиксируйте C на 100 и меняйте N: разрешение покупается ценой частоты:

N Разрешение по скважности Частота PWM Прерываний/с
10 10% 921.6 Hz 9 216
20 5% 460.8 Hz 9 216
50 2% 184.3 Hz 9 216
100 1% 92.2 Hz 9 216

Частота прерываний не меняется, потому что зависит только от C. Меняется число тиков до повторения шаблона — а примерно ниже 100 Hz небольшой двигатель постоянного тока перестаёт усреднять импульсы и начинает заметно проходить шаг за шагом.

Поэтому следующий очевидный шаг звучит так: сохранить 460.8 Hz и получить разрешение 1%, задав N = 100 и C = 20. Арифметика сходится — и микросхема умирает. При C = 20 прерывание срабатывает каждые 20 машинных циклов, а приведённый выше ISR требует примерно 25 только для сохранения регистров, сравнения и возврата. Он никогда не завершается до прихода следующего, и больше ничего не выполняется.

Вот настоящий предел. При стоимости ISR около 25 машинных циклов C = 100 тратит четверть ЦП на PWM, а C = 50 — половину. Аппаратный PWM в Arduino не стоит ничего при любой частоте и разрешении. Это одно сравнение полезнее любой таблицы функций.

Два действия недорого дают запас: переключайтесь на банк регистров 1 внутри ISR (using 1), чтобы компилятор менял два бита вместо сохранения восьми регистров, и оставляйте ISR для счёта и записи выводов. Всё, что вычисляется, должно выполняться в main.

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

H-мост принимает два входа направления и по одному входу разрешения на двигатель. Подавайте на выводы направления обычные выводы порта, а на разрешение — вывод с ШИМ:

sbit IN1 = P2^1;
sbit IN2 = P2^2;
sbit ENA = P2^0;    // PWM'd by the ISR

void motor_forward(unsigned char duty) {
    IN1 = 1; IN2 = 0;
    duty_a = duty;
}

void motor_reverse(unsigned char duty) {
    IN1 = 0; IN2 = 1;
    duty_a = duty;
}

void motor_stop(void) {
    duty_a = 0;
    IN1 = 0; IN2 = 0;
}

Второй двигатель — это вторая переменная рабочего значения и вторая выходная линия в том же ISR: счётчик тиков общий, поэтому оба канала остаются синхронными:

void timer0_isr(void) interrupt 1 using 1 {
    if (++tick >= PWM_LEVELS) tick = 0;
    ENA = (tick < duty_a);
    ENB = (tick < duty_b);
}

Одна особенность подключения именно этой микросхемы. Как показано в руководстве по GPIO, вывод 8051 подаёт только десятки микроампер. Для входа CMOS этого достаточно, но у L298N или L293D биполярные входы могут потреблять до 100 µA при высоком логическом уровне — ровно на границе возможностей вывода. Подтяжка 4.7 kΩ от каждого входа драйвера к +5 V снимает неоднозначность за несколько центов и превращает плату, которая работает на стенде, но не на роботе, в просто работающую.

Когда всё идёт не так

  • Двигатель гудит, но почти не вращается. Частота PWM слишком низкая либо рабочее значение ШИМ ниже порога трения покоя. Поднимите частоту выше ~100 Hz и ожидайте, что настоящему двигателю постоянного тока (DC) потребуется 25–40% только для начала движения.
  • После добавления таймера сломался последовательный вывод. Вы записали TMOD целиком и стёрли биты режима таймера 1. Замаскируйте их: TMOD = (TMOD & 0xF0) | 0x02;.
  • Период PWM плывёт. Вы работаете в режиме 1 и перезагружаете счётчик вручную. Перейдите в режим 2 и позвольте аппаратуре сделать это.
  • Вся программа работает медленно. ISR слишком дорог для частоты тиков. Поднимите TH0 к 200, добавьте using 1 и вынесите арифметику из прерывания.
  • Изменение рабочего значения ШИМ вызывает сбои выхода. duty_a записывается из main, пока ISR его читает. Запись одного байта на этом ЦП атомарна, поэтому char безопасен, но для более широких значений обновление нужно выполнять при отключённых прерываниях.
  • Один двигатель полностью игнорирует команды. Проверьте, что ISR действительно управляет нужным выводом разрешения и что у входов драйвера есть подтяжки.

Исследуйте граф

Входит в эти сборки

Проекты и учебные пути, в которые входит это руководство.

Дополнительные материалы

Источники