Мы добрались до технологии, которая кардинально меняет правила игры. DMA (Direct Memory Access) — это периферийный модуль, который может пересылать данные между памятью и периферией без участия CPU. Это как если бы у вас появился второй процессор, который занимается только пересылкой данных.
Проблема: На ATmega328P (Arduino Uno) нет аппаратного DMA. Это недостаток архитектуры AVR. Но мы можем эмулировать DMA с помощью таймеров и прерываний, создавая программный «DMA-контроллер».
Решение: Используем таймер 2 (8-битный) для генерации прерываний с фиксированной частотой. В обработчике прерывания мы читаем байт из памяти и отправляем его в периферию.
Программный DMA для WS2812B (светодиодная лента)
Самый сложный протокол для Arduino без DMA — это WS2812B, где каждый бит кодируется длительностью импульса 0.8-1.2 мкс. Обычно мы используем библиотеку Adafruit_NeoPixel, которая занимает процессор на 100%.
Хак: Используем таймер 1 для создания точных задержек на уровне ассемблера, освобождая CPU для других задач:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#define LED_COUNT 60
#define BUFFER_SIZE (LED_COUNT * 3) // 3 байта на пиксель (RGB)
uint8_t ledBuffer[BUFFER_SIZE];
// Настройка таймера 1 для генерации прерываний каждые 1.25 мкс
void setupDMATimer() {
// Настраиваем пин для LED (пин 6 на PORTD)
DDRD |= (1 << PD6);
// Таймер 1 в режим Fast PWM с частотой 800 кГц
TCCR1A = (1 << COM1A0) | (1 << WGM10); // OC1A (пин 9) не используется
TCCR1B = (1 << WGM12) | (1 << CS10); // Прескалер 1 = 16 МГц
// Устанавливаем период: 16 МГц / 800 кГц = 20 тактов
OCR1A = 19; // 800 кГц
// Включаем прерывание по совпадению
TIMSK1 = (1 << OCIE1A);
}
volatile uint8_t bitIndex = 0;
volatile uint8_t byteIndex = 0;
volatile uint8_t currentByte = 0;
ISR(TIMER1_COMPA_vect) {
// Кодируем бит для WS2812B
if (bitIndex == 0) {
// Загружаем следующий байт
if (byteIndex < BUFFER_SIZE) {
currentByte = ledBuffer[byteIndex++];
bitIndex = 8;
} else {
// Конец передачи
PORTD &= ~(1 << PD6); // LOW
return;
}
}
bitIndex--;
// Отправляем бит
if (currentByte & (1 << bitIndex)) {
// Бит = 1: HIGH на 0.8 мкс, LOW на 0.4 мкс
PORTD |= (1 << PD6);
_delay_us(0.8);
PORTD &= ~(1 << PD6);
_delay_us(0.4);
} else {
// Бит = 0: HIGH на 0.4 мкс, LOW на 0.8 мкс
PORTD |= (1 << PD6);
_delay_us(0.4);
PORTD &= ~(1 << PD6);
_delay_us(0.8);
}
}
void setup() {
setupDMATimer();
sei();
// Заполняем буфер случайными цветами
for (int i = 0; i < BUFFER_SIZE; i++) {
ledBuffer[i] = random(255);
}
}
void loop() {
// CPU свободен! Можно делать что угодно
// DMA отправляет данные в фоне
}