ШИМ сигнал #
На практике может пригодиться программная реализация ШИМ сигнала, например если жалко тратить аппаратный таймер, каналы аппаратного ШИМ уже закончились или если нужен низкочастотный ШИМ.
По счётчику #
Данный подход близок к тому, как работает аппаратный ШИМ на таймере: есть счётчик, который бегает от 0
до максимального значения top
. При нулевом значении включаем пин, при совпадении с величиной заполнения ШИМ - выключаем. Всю конструкцию нужно вызывать по таймеру (программному или аппаратному) с нужным периодом, либо просто в loop
:
const uint8_t pin = 13;
uint8_t count, duty, top = 255; // 8 бит ШИМ
void setup() {
pinMode(pin, OUTPUT);
duty = 128; // яркость 0-255
}
void tickPWM() {
if (!count && duty) digitalWrite(pin, 1);
if (count >= duty && duty != top) digitalWrite(pin, 0);
count = (count == top) ? 0 : count + 1;
}
void loop() {
// по таймеру или прямо так, но частота будет нестабильная
tickPWM();
}
Либо по программному таймеру, например так (период 100 мкс):
void loop() {
static uint32_t tmr;
if (micros() - tmr >= 100) {
tmr += 100;
tickPWM();
}
}
Особенности реализации:
- Происходит однократное переключение состояния пина, что хорошо для производительности
- Для большей скорости можно использовать библиотеку GyverIO (AVR, ESP8266, ESP32)
- Легко переносится на любой другой МК или фреймворк - достаточно заменить
digitalWrite
на свой аналог - Частота вызова тикера должна быть в
top+1
раз больше желаемой частоты ШИМ. Например для 8-бит ШИМ с частотой 1 кГц тикер нужно вызывать с частотой 256 кГц - Можно вызывать в прерывании детектора нуля при управлении сетевым напряжением через симистор или SSR, получится ШИМ с частотой 2.5 секунды для управления инерционными процессами без использования таймера и без выбросов в сеть как при фазовом управлении симистором
- При значениях
0
иtop
(максимум) не происходит "дребезга", т.е. пин остаётся полностью выключенным или полностью включенным - Для разрешения больше 8 бит нужно использовать соответствующий тип данных, т.е.
uint16_t
- Для разрешения 8 бит можно слегка упростить код:
const uint8_t pin = 13;
uint8_t count, duty;
void setup() {
pinMode(pin, OUTPUT);
duty = 128; // яркость 0-255
}
void tickPWM() {
if (!count && duty) digitalWrite(pin, 1);
if (count >= duty && duty != 255) digitalWrite(pin, 0);
++count; // автоматически перейдёт с 255 в 0
}
void loop() {
tickPWM();
}
Также данная реализация легко расширяется на несколько каналов, которые могут работать на одном таймере:
#define SOFT_PWM_N 3
uint8_t count, top = 255;
uint8_t duty[SOFT_PWM_N];
uint8_t pins[SOFT_PWM_N] = {11, 12, 13};
void setup() {
for (uint8_t pin : pins) pinMode(pin, OUTPUT);
duty[0] = 10;
duty[1] = 128;
duty[2] = 200;
}
void tickPWM() {
if (!count) {
for (uint8_t i = 0; i < SOFT_PWM_N; i++) {
if (duty[i]) digitalWrite(pins[i], 1);
}
}
for (uint8_t i = 0; i < SOFT_PWM_N; i++) {
if (count >= duty[i] && duty[i] != top) digitalWrite(pins[i], 0);
}
count = (count == top) ? 0 : count + 1;
}
void loop() {
tickPWM();
}
По двум периодам #
Второй вариант - переключать пин по двум периодам. Пример реализации:
const uint8_t pin = 13;
bool state;
uint16_t tmr, high, low;
const uint16_t depth = 8; // разрешение, бит
const uint16_t freq = 50; // частота ШИМ, Гц
// установить заполнение
void setPWM(uint16_t duty) {
if (duty == 0 || duty >= (1 << depth) - 1) {
// граничные значения - отключаем таймер и подаём сигнал
high = 0;
digitalWrite(pin, duty);
} else {
uint32_t prd = 1000000ul / freq;
high = (duty * prd) >> depth;
low = prd - high;
}
}
// тикер, вызывать в loop
void tickPWM() {
if (high && uint16_t(uint16_t(micros()) - tmr) >= (state ? high : low)) {
tmr = micros();
state ^= 1;
digitalWrite(pin, state);
}
}
void setup() {
pinMode(pin, OUTPUT);
setPWM(200);
}
void loop() {
tickPWM();
}
Сигма-дельта, PDM #
PDM - импульсно-плотностная модуляция (Pulse-Density Modulation). Как и ШИМ, позволяет представить аналоговый сигнал в виде цифровых импульсов. В отличие от ШИМ, имеет фиксированную ширину импульсов, но разный период между ними - сигнал состоит из одиночных импульсов, а среднее значение соответствует плотности этих импульсов на единицу времени. PDM даёт более высокочастотный шум, который проще сгладить фильтром для получения аналогового сигнала, поэтому часто используется для модуляции аналоговых сигналов (ЦАП), в том числе для воспроизведения звука.
Для управления моторами лучше использовать PWM (ШИМ), он эффективнее сглаживается индуктивностью мотора, меньше шумит и гораздо стабильнее работает
Для генерирования PDM можно использовать сигма-дельта алгоритм, который работает как алгоритм Брезенхема для рисования растровых линий - накапливает ошибку и переключает состояние при превышении порога, таким образом ошибка распределяется по времени:
const uint8_t pin = 13;
const int16_t top = 255; // "разрешение" сигнала
uint16_t value = 100; // значение, от 0 до top
int16_t accum = 0;
void setup() {
pinMode(pin, OUTPUT);
}
void tickSD() {
accum += value;
if (accum >= top) {
accum -= top;
digitalWrite(pin, 1);
} else {
digitalWrite(pin, 0);
}
}
void loop() {
tickSD();
}
Вызывать тикер нужно точно так же по таймеру или для эксперимента просто в loop
, частота зависит от необходимой частоты дискретизации выходного аналогового сигнала.
Для сглаживания PDM и получения аналогового сигнала можно использовать RC фильтр:
Номиналы подбираются под задачу и частоту сигнала. Например для первичной фильтрации звукового сигнала можно взять 10 кОм и 0.001 мкФ, а частота PDM - порядка нескольких МГц.
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками
