В прошлых уроках мы рассмотрели чтение и вывод цифрового сигнала (GPIO) и чтение аналогового сигнала (ADC). А есть ли у МК возможность выдать аналоговый сигнал? То есть не 0 или 5 Вольт, а что-то среднее между ними. Например для воспроизведения звука динамиком или управления яркостью светодиода. Например так, как мы управляли яркостью светодиода при помощи потенциометра, подключив его как делитель напряжения - он выдавал напряжение от 0 до 5V с плавной регулировкой.
На некоторых МК есть ЦАП - цифро-аналоговый преобразователь (DAC, Digital-to-Analog Converter), он преобразует численное значение из программы в соответствующее напряжение в Вольтах и выводит его на пин. Простейший ЦАП можно собрать из десятка резисторов - R2R ЦАП, рассмотрим его в отдельном уроке.
Для большинства задач ЦАП на самом деле и не нужен - его заменяет ШИМ сигнал.
"Ручной" ШИМ #
Давайте рассмотрим его работу на примере классического мигания светодиодом, но возьмём короткий период: например 100 мс - по 50
на включенное и выключенное состояние:
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(50);
digitalWrite(LED_BUILTIN, LOW);
delay(50);
}
Теперь изменим задержки так, чтобы суммарное время (100 мс) не изменилось, например 90
и 10
мс:
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(90);
digitalWrite(LED_BUILTIN, LOW);
delay(10);
}
Светодиод начал гореть дольше (вспышки стали длиннее) и "средняя" яркость визуально увеличилась. Поменяем местами - 10
на активное и 90
на неактивное состояние:
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(10);
digitalWrite(LED_BUILTIN, LOW);
delay(90);
}
Теперь светодиод даёт меньше света!
Также он ощутимо мерцает, поэтому уменьшим задержку в 10 раз: попробуйте запустить скетч с 5
/5
, 1
/9
и 9
/1
мс задержками - получится управление яркостью светодиода, причём мерцания почти не будет заметно из-за высокой частоты переключения, а суммарно получится 11 возможных значений яркости (от 0 до 10 с шагом 1). Такой эффект достигается из-за инертности зрения: светодиод всё так же мерцает, но глаз не успевает засечь эти вспышки точно так же, как увидеть лопасти вращающегося вентилятора.
Давайте ещё сильнее увеличим частоту и перейдем к задержкам в микросекундах. Задержкой будем управлять при помощи потенциометра, подключенного к пину A0
. Функция analogRead
вернёт значение от 0
до 1023
, так что просто используем его следующим образом - первая задержка по значению, а вторая - 1023 - значение
:
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
int v = analogRead(0);
digitalWrite(LED_BUILTIN, HIGH);
delayMicroseconds(v);
digitalWrite(LED_BUILTIN, LOW);
delayMicroseconds(1023 - v);
}
То есть сумма задержек всегда будет равна 1023
. Загрузите скетч и покрутите рукоятку - яркость светодиода меняется и мерцания уже не видно, но аналоговым сигналом тут и не пахнет. Мы сейчас "на коленке" реализовали ШИМ сигнал - Широтно-Импульсная Модуляция (PWM, Pulse-Width Modulation), это цифровой сигнал, имеющий две основные характеристики: частота и ширина импульса.
Частота ШИМ #
Частота измеряется в Герцах, Гц (Hertz, Hz), 1 Гц равен одному периоду колебаний в секунду (T
на графике). Период и частота - взаимно обратные величины, период измеряется в секундах: частота = 1 / период
. Периодом считается время между одинаковыми точками сигнала:
T - период, Ti - ширина импульса, Vm - напряжение сигнала, Va - среднее напряжение
Чем выше частота, тем более незаметны импульсы для потребителя, например со светодиодом при частоте 10 Гц мы увидим мерцание, а 100 Гц - уже нет. Светодиод загорается практически мгновенно, а вот глаз не способен уловить короткие вспышки, он их "интегрирует" из-за инертности зрения. Если подавать ШИМ на нагреватель, то можно регулировать мощность нагрева. Если нагреватель большой и инертный - частота может быть 1 Гц и ниже, например у электрической плиты или мультиварки в некоторых моделях отчётливо слышно включение и выключение нагревательного элемента. Если подавать ШИМ на электромотор - то из-за неидеальности конструкции у него начнут вибрировать катушки с частотой ШИМ, частоты от 100 Гц до ~16 кГц замечательно слышны человеческим ухом как гудение и высокий писк соответственно, поэтому для управления моторами частота обычно выбирается в районе 20 кГц.
Ширина импульса #
Ширина или длительность импульса (Ti
на графике) измеряется в секундах в диапазоне от 0
до периода колебаний T
. Чем длиннее импульс, тем выше среднее напряжение сигнала (Va
на графике) - оно пропорционально отношению длины импульса к периоду. Например период 1с, импульс 0.25с, среднее напряжение сигнала при питании 5V (Vm
на графике) будет равно Va = Vm * (Ti / T) = 5 * (0.25 / 1) = 1.25
V.
Отношение длины импульса к периоду (Ti / T
) также называется коэффициентом заполнения ШИМ - это более удобная безразмерная величина, которая уже не зависит от частоты и периода - имеет значение от 0.0
до 1.0
(или заполнение ШИМ 0.. 100%
). Чтобы получить результирующее напряжение, нужно просто умножить коэффициент заполнения на напряжение питания ШИМ. Величина, обратная заполнению, называется скважностью ШИМ - на практике используется редко.
Аппаратный ШИМ #
В примерах выше мы сделали программный ШИМ сигнал, то есть вручную засекали время и переключали состояние пина. Также в нашей реализации "на задержках" микроконтроллер сможет заниматься только одной этой задачей - генерировать один ШИМ сигнал. У многих МК есть возможность генерировать ШИМ аппаратно - при помощи периферии, вообще не используя ресурсы процессора. Обычно это делается при помощи аппаратного таймера - универсального блока, который может засекать время и самостоятельно дёргать пином.
analogWrite #
В фреймворке Arduino предусмотрена готовая функция для запуска аппаратного ШИМ сигнала на пине analogWrite(pin, value)
:
pin
- номер GPIO, который поддерживает ШИМ сигнал. См. описание к конкретной плате/МК, также на распиновке такие пины помечены символом~
. Например на Arduino Nano/UNO это пины 3, 5, 6, 9, 10, 11 - выводы аппаратных таймеровvalue
- заполнение от0
до2^разрядность - 1
, где разрядность (разрешение) ШИМ в битах, см. документацию на конкретную плату/МК. В большинстве случаев по умолчанию это 8 бит - значения от0
до255
. Значенияvalue
от0
домаксимума
соответствуют заполнению ШИМ 0.. 100%
Среднее напряжение сигнала в Вольтах будет равно Vcc * value / 255
для 8 бит ШИМ - роли ширины импульса и периода в данном случае играют значение и разрядность
Дополнительная информация:
- Генерация происходит автоматически - достаточно один раз вызвать
analogWrite
и ШИМ будет генерироваться самостоятельно и асинхронно, на него не влияютdelay
-задержки - Частота ШИМ через
analogWrite
не стандартизирована и может отличаться на разных платформах или даже на разных пинах одного МК, см. описание к конкретной модели - На некоторых платформах доступны функции
analogWriteFrequency
- настроить частоту ШИМ иanalogWriteResolution
- разрешение - Перед вызовом
analogWrite
не нужно вручную переключать пин в режимOUTPUT
, это сделается автоматически - При вызове
digitalWrite
запущенный на пине ШИМ отключается и пин переходит в обычный GPIO режим с указанным уровнем - При вызове
analogWrite
со значением0
илимаксимальным
ШИМ отключается, пин переходит в обычный режим выхода с соответствующим сигналом - На AVR Arduino (Nano, UNO, Mega...) запуск ШИМ через
analogWrite
на некоторых пинах может конфликтовать с некоторыми библиотеками и функциейtone
(о ней позже), т.к. для этих задач используется один и тот же таймер. См. описание к конкретной плате
Пример 1 #
Давайте поуправляем яркостью светодиода при помощи analogWrite
. Подключите RGB светодиод к пинам 3, 5, 6.
Для начала просто плавно помигаем красным цветом, для чего используем цикл и задержки. Пример демонстрирует независимость работы ШИМ от наличия задержек - программа ждёт задержку, а ШИМ продолжает генерироваться с установленным заполнением внутри delay
:
#define LED_R 3
void setup() {
}
void loop() {
for (int i = 0; i < 255; i += 10) {
analogWrite(LED_R, i);
delay(20);
}
for (int i = 255; i > 0; i -= 10) {
analogWrite(LED_R, i);
delay(20);
}
}
Пример 2 #
Добавим потенциометр для ввода данных в программу:
В Arduino Nano analogRead
возвращает от 0 до 1023 (10 бит), а analogWrite
принимает от 0 до 255 (8 бит), поэтому просто поделим результат чтения потенциометра на 4, чтобы получить полный диапазон ШИМ. В программу для наглядности добавим задержку:
#define LED_R 3
#define POT_PIN 0
void setup() {
}
void loop() {
analogWrite(LED_R, analogRead(POT_PIN) / 4);
delay(100);
}
Теперь потенциометр управляет яркостью красного светодиода.
Пример 3 #
Сделаем чтобы один цвет перетекал в другой, вместо того чтобы просто гаснуть:
#define LED_R 3
#define LED_G 5
#define POT_PIN 0
void setup() {
}
void loop() {
int v = analogRead(POT_PIN) / 4;
analogWrite(LED_R, v);
analogWrite(LED_G, 255 - v);
delay(100);
}
Теперь вращение рукоятки приводит к тому, что цвет перетекает из красного в зелёный через жёлтый (смесь зелёного и жёлтого на средней яркости). Другие примеры с RGB светодиодом можно найти в уроке про RGB светодиод и цветовые алгоритмы.
Напоминаю, что подключать мощные потребители напрямую к пинам нельзя - чтобы управлять нагревом или например скоростью моторчика, нужно использовать внешний драйвер, подробнее об этом рассказано в других уроках