Руководство · Средний · 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
Баланс, которого нельзя избежать
Частота ШИМ, разрешение по скважности и нагрузка на ЦП связаны одной формулой:
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 действительно управляет нужным выводом разрешения и что у входов драйвера есть подтяжки.
Исследуйте граф
Входит в эти сборки
Проекты и учебные пути, в которые входит это руководство.
Дополнительные материалы