Как добавить звук в 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-ом пине.


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

2
2

About Кирилл Васин

Прохожий из шапки сайта

25 Comments

    1. Поменяйте переменную speakerPin с 11 на 10. Звук должен быть на 10-ом пине. Пины таймеров у Arduino Mega и Arduino Uno/Duemilanove разные.

  1. Большое спасибо за информацию. Будем пробовать.
    вот вам скомпилированный экзешник, плохо вы искали в половине интернета 8) .
    Надо просто перетащить wav на экзешник программы и в том месте, где лежит этот wav, появится моментально желаемый файл с расширением *.с

    1. Сильно ли это влияет на качество звука? Честно говоря, не совсем понял как его кодировать в ADPCM.

    1. В принципе — да. Atmega8 имеет нужные таймеры. Нужно сконфигурировать в скетче порт PB3(MOSI/OC2A) как выход, с него и будет поступать звуковой сигнал. Также придется использовать звуковой файл меньшего размера, либо снизить частоту дискретизации, так как у atmega8 всего 8кб памяти.

  2. Добрый день.
    А где можно посмотреть схему подключения динамика? С указанием параметров резисторов, динамика и прочих (если есть) комплектующих. Что-то мне подсказывает, что динамик не подключается напрямую к разъемам ардуино. А экспериментировать самому страшно — очень долго из Китая платы идут. 🙂

    1. Добрый день.
      Если в качестве эксперимента, то можно подключать 8-ми омный (или больше) динамик напрямую, либо еще через резистор на 100Ом (так советуют на Arduino.cc). Если же хотите высокого качества, то нужно ставить RC цепочку () и подключать через переменный резистор на 100кОм к усилителю любого класса. Если проект типа: дверной звонок, то дело обойдется RC цепочкой и одним транзистором с динамиком. 🙂

  3. Скажите, а можно ли подключить какой-нибудь звуковой модуль и что бы при каждом включении кнопки он воспроизводил аудио файлы, записанные в него в хаотичном порядке?

    1. Да, есть модуль WaveShield, либо можете изготовить его сами. Есть еще по-серьезнее штука: Mp3Shield.
      Для случайного воспроизведения по кнопке нужно сделать нечто подобное:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      int i;
       .......................
       if (кнопка нажата) {
         i = random(кол-во треков);
         while(i>0) {
           PlayNext();
           --i;
         }
       }

      В данном случае, чтобы не создавать массив с перечнем файлов, можно просто переключать записи случайное количество раз. Тут зависит, что за модуль вы будете использовать.

  4. Скажите какой таймер нужно настраивать для запуска вашего скетча на arduino mega 2560?

    1. Вам нужно поменять значение speakerPin на 10.
      Звук будет на десятом порту, а таймеры менять не нужно.

  5. Спасибо, все завелось с полоборота. Если не сложно, какие изменения в скетче для вывода звука на 3 (Digital pin 3)выводе платы UNO (OC2B) или 5 выводе (Digital pin5)(OC0B)?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *