Как добавить звук в Arduino //0x0

Наверно многие хотели добавить в свой проект голосовое сопровождение или просто приятный звук. С помощью чего его можно воспроизводить? Динамик? — Ясное дело. Однако контроллер у нас цифровой, поэтому кроме как единицей и нулем он оперировать ничем не может. О цифро-аналоговых преобразованиях я писал в статье про R2R-ЦАП, здесь мы рассмотрим более экономичный метод.

ШИМ (PWM)

Широтно-Импульсная-Модуляция — оперирует двумя значениями ВКЛ/ВЫКЛ, генерируя их с определенной частотой. Путем смещения соотношения длительности единицы и нуля в заданной частоте, мы изменяем среднее значение амплитуды сигнала на нагрузке.

Как любой ЦАП, этот метод обладает важной характеристикой — разрядностью. Здесь этот параметр определяет количество возможных соотношений длительности единицы и нуля (скважности).

AVR (Микроконтроллер)

У контроллера Arduino имеется встроенных генератор ШИМ, разрядностью 8бит. У контроллера Atmega168 их не так много:

Так проходит инициализация ШИМ на порту PB3 или 11 по-ардуиновски:
1
2
3
4
5
6
7
8
9
10
11
pinMode(11, OUTPUT); //Наш порт — выход
 
ASSR &= ~(_BV(EXCLK) | _BV(AS2)); //Используем внутренний генератор тактовой для ШИМ
 
TCCR2A |= _BV(WGM21) | _BV(WGM20); //Устанавливаем режим FAST-PWM
TCCR2B &= ~_BV(WGM22);
 
TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0); //Настройка работы с портом
TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
 
TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10); //Отключение делителей

Для записи уже аналогово значения амплитуды используется следующий регистр:

1
OCR2A = 255; //Максимум</div>

Возможные значений может быть 255, включая ноль. Шаг значений где-то 16-19 мВ.

Проигрыватель

Для начала нам нужен закодированный PCM звуковой фрагмент в виде массива. Если кто не помнит, то PCM представляет собой набор цифровых значений амплитуд сигнала в определенный промежуток времени. При проигрывании, нам необходимо просто подставлять в регистр ШИМ значения из массива с определенной частотой (дискретизация), поэтому нам необходим Таймер.

1
2
3
4
5
6
7
8
9
10
11
12
cli(); //Запрещаем все прерывания (от греха по дальше)
 
TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12); //Устанавливаем режим CTC
TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));
 
TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10); //Никаких предделителей
 
OCR1A = F_CPU / SAMPLE_RATE; //Записываем частоту
 
TIMSK1 |= _BV(OCIE1A); //Включаем обработчик прерываний
 
sei(); //Разрешаем глобальные прерывания

Таймер работает в режиме CTC, в котором мы можем задать ему частоту вызова прерываний, которая определяется константой SAMPLE_RATE — это и будет частота дискретизации.

1
2
3
4
ISR(TIMER1_COMPA_vect) { //Обработчик
  //Передаем значение в ЦАП…
  OCR2A = xxx;
}

Принцип прост: Таймер считает до определенного значения, затем данные передаются в ШИМ.  Соберем же все вместе! 🙂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
 
#include "sounddata.h"
 
int speakerPin = 11;
volatile uint16_t sample;
byte lastSample;
 
// This is called at 8000 Hz to load the next sample.
ISR(TIMER1_COMPA_vect) {
  if (sample >= data_size) {
    if (sample == data_size + lastSample) {
      stopPlayback();
    }
    else {
      OCR2A = data_size + lastSample — sample;
    }
  }
  else {
    OCR2A = pgm_read_byte(&data[sample]);
  }
 
  ++sample;
}
 
void startPlayback() {
  pinMode(speakerPin, OUTPUT);
 
  ASSR &= ~(_BV(EXCLK) | _BV(AS2));
  TCCR2A |= _BV(WGM21) | _BV(WGM20);
  TCCR2B &= ~_BV(WGM22);
  TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
  TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
  TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
  OCR2A = pgm_read_byte(&data[0]);
  cli();
  TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
  TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));
  TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
  OCR1A = F_CPU / sample_rate;
  TIMSK1 |= _BV(OCIE1A);
 
  lastSample = pgm_read_byte(&data[data_size-1]);
  sample = 0;
  sei();
}
 
void stopPlayback() {
  TIMSK1 &= ~_BV(OCIE1A);
  TCCR1B &= ~_BV(CS10);
  TCCR2B &= ~_BV(CS10);
  digitalWrite(speakerPin, LOW);
}
 
void setup() {
  pinMode(ledPin, OUTPUT);
  startPlayback();
}
 
void loop() {
  while (true);
  //Nothing…
}

Проигрыватель работает по прерыванию, поэтому мы можем делать все посторонние задачи в loop()-е. Про звуковой файл см.ниже.

Звуковой файл

Спешу вас огорчить. Так как памяти у Arduino не так много, звуковой файл в PCM формате будет очень большим, поэтому его размер ограничен парой секунд. Подбираем файл:

  • Формат: wav
  • Частота дискретизации: < 9000Гц
  • Разрядность: 8бит
  • Каналы: моно

Затем конвертируем его утилитой wav2c, путем перетаскивания файла на exe-шник. И приводим его в соответствующий вид:

1
2
3
4
5
6
7
8
9
10
11
12
const int sample_rate = частота;
const int data_size = размер в байтах;
 
const unsigned char data[] PROGMEM = {53, 47, 56, 64, 63, 61, 56, 54, 52, 36, 16, 22, 51, 66, 67, 70, 76, 88, 99, 92,
77, 74, 85, 100, 106, 97, 83, 85, 96, 108, 133, 160, 164, 144, 113, 96, 91, 82, 74, 76,
89, 97, 97, 97, 82, 54, 40, 41, 41, 43, 56, 74, 78, 64, 55, 64, 72, 72, 84, 102,
108, 116, 126, 127, 124, 127, 134, 134, 138, 148, 152, 156, 164, 165, 169, 171, 160, 156, 157, 152,
151, 145, 133, 136, 153, 166, 165, 163, 165, 161, 156, 158, 155, 147, 148, 160, 185, 209, 215, 220,
220, 204, 200, 208, 205, 200, 202, 209, 214, 213, 205, 198, 194, 194, 203, 219, 231, 235, 230, 219,
200, 184, 177, 170, 170, 177, 172, 164, 163, 158, 156, 160, 163, 161, 142, 116, 103, 96, 89, 93,
101, 105, 111, 116, 120, 110, 89, 80, 78, 75, 73, 80, 93, 91, 77, 69, 70, 77, 91, 98,
89, 87, 93, 95, 95, 94, 97, 96, 91, 94......................

Называем этот файл sounddata.h и кидаем в папку со скетчем. Тестовый файлик можно взять здесь.

Звук будет на 11-ом пине. Прослушать запись получившегося: здесь.

Автор основного кода: Michael Smith

PS: Скетч для ArduinoMega будет отличаться. Нужно изменить значение переменной speakerPin на 10. Сигнал появится на 10-ом пине.


Все части статьи

Вы можите оставить комментарий, или поставить трэкбек со своего сайта.

25 комментариев к “Как добавить звук в Arduino //0x0”