[Программирование микроконтроллеров, DIY или Сделай сам] Учим железки разговаривать, или ESP32 DAC и немного таймера
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В ходе разработки одного очень интересного устройства (эх, лишь бы силенок хватило) я решил, что будет неплохо, если устройство это будет говорящим. Как нельзя кстати здесь пригодилось наличие в целевом микроконтроллере, ESP32 компании Espressif Systems, двухканального 8-битного ЦАПа.
В этом туториале (если его можно так назвать) я покажу, как можно быстро и довольно просто организовать проигрывание аудиофайла силами микроконтроллера ESP32.
Немного теории
Как нам сообщает Википедия, ESP32 — это серия недорогих микроконтроллеров с низким энергопотреблением. Они представляют собой систему на кристалле (SoC) с интегрированными контроллерами Wi-Fi и Bluetooth, и антеннами. Основаны на ядре Tensilica Xtensa LX6 в вариантах с одним и двумя ядрами. В систему интегрирован радиочастотный тракт. МК создан и разработан китайской компанией Espressif Systems, а производится компанией TSMC по техпроцессу 40 нм. Подробнее о возможностях чипа можно прочитать на странице в Википедии и в официальной документации.
Однажды в рамках освоения этого контроллера мне захотелось воспроизвести на нем звук. Поначалу я думал, что придется использовать ШИМ. Однако, внимательнее почитав документацию, я обнаружил наличие двух каналов 8-битного ЦАПа. Разумеется, это в корне меняло дело.
В Technical Reference сказано, что ЦАП в ESP32 построен на цепочке резисторов (видимо, имеется ввиду R2R цепочка) с применением некоего буфера. Выходное напряжение может изменяться в пределах от 0 вольт до напряжения питания (3,3 вольта) с разрешением 8 бит (то есть 256 значений). Преобразование двух каналов независимое. Также имеется встроенный генератор косинуса (CW generator) и поддерживается DMA.
Я решил пока что не лезть в DMA, ограничившись построением проигрывателя на основе таймера. Как известно, чтобы воспроизвести простейший WAV-файл формата PCM, достаточно с указанной в файле частотой дискретизации читать из него сырые данные и распихивать по каналам ЦАПа, предварительно приводя (при необходимости) разрядность данных к разрядности ЦАП. Мне повезло: у меня нашелся набор звуков в формате WAV PCM 8 bit 11025 Hz mono, выдранный из ресурсов одной старой игры. Значит, будем использовать только один канал ЦАП.
Также нам понадобится таймер, способный генерировать прерывания с частотой 11025 Гц. Согласно все тому же Technical Reference, ESP32 имеет на борту два модуля таймеров по два таймера в каждом, итого четыре таймера. Они имеют разрядность 64 бита, у каждого имеется 16-битный предделитель и возможность генерации прерывания по уровню или фронту.
От теории к практике
Вооружившись примером wave_gen из esp-idf, я отправился писать код. Я не стал упарываться созданием файловой системы: цель была получить звук, а не сделать из ESP32 полноценный плеер.
Для начала перегнал один из WAV-файлов в сишный массив. В этом мне очень помогла встроенная в Дебиан утилита xxd. Простой командой
$ xxd -i file.wav > file.c
получаем сишный файл с массивом данных в шестнадцатеричном виде внутри и даже с отдельной переменной, в которую помещен размер файла в байтах.
Далее я закомментировал первые 44 байта массива — заголовок WAV-файла. Попутно я разобрал его по полям и узнал всю необходимую мне информацию о нем:
const uint8_t sound_wav[] = {
// 0x52, 0x49, 0x46, 0x46, // chunk "RIFF"
// 0xaa, 0xb4, 0x01, 0x00, // chunk length
// 0x57, 0x41, 0x56, 0x45, // "WAVE"
// 0x66, 0x6d, 0x74, 0x20, // subchunk1 "fmt"
// 0x10, 0x00, 0x00, 0x00, // subchunk1 length
// 0x01, 0x00, // audio format PCM
// 0x01, 0x00, // 1 channel, mono
// 0x11, 0x2b, 0x00, 0x00, // sample rate
// 0x11, 0x2b, 0x00, 0x00, // byte rate
// 0x01, 0x00, // bytes per sample
// 0x08, 0x00, // bits per sample per channel
// 0x64, 0x61, 0x74, 0x61, // subchunk2 "data"
// 0x33, 0xb4, 0x01, 0x00, // subchunk2 length, bytes
Отсюда видно, что наш файл имеет один канал, частоту дискретизации 11025 герц и разрешение 8 бит на семпл. Заметим, что если бы я захотел анализировать заголовок программно, то мне нужно было бы учитывать порядок байт: в WAV это Little-endian, то есть младшим байтом вперед.
В итоге я создал тип структуры для хранения информации о звуке:
typedef struct _audio_info
{
uint32_t sampleRate;
uint32_t dataLength;
const uint8_t *data;
} audio_info_t;
И создал собственно экземпляр структуры, заполнив ее следующим образом:
const audio_info_t sound_wav_info =
{
11025, // sampleRate
111667, // dataLength
sound_wav // data
};
В этой структуре поле sampleRate — это значение одноименного поля заголовка, поле dataLength — это значение поля subchunk2 length, а поле data — это указатель на массив с данными.
Далее я подключил заголовочные файлы
#include "driver/timer.h"
#include "driver/dac.h"
и создал прототипы функций для инициализации таймера и обработчика его прерывания Alarm, как это сделано в примере wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
После чего принялся за наполнение функции инициализации.
Таймеры в ESP32 тактируются в конечном итоге от APB_CLK_FREQ, равного 80 МГц:
driver/timer.h:
#define TIMER_BASE_CLK (APB_CLK_FREQ) /*!< Frequency of the clock on the input of the timer groups */
soc/soc.h:
#define APB_CLK_FREQ ( 80*1000000 ) //unit: Hz
Чтобы получить значение счетчика, при котором нужно генерировать прерывание Alarm, нужно частоту тактирования таймера поделить на значение предделителя, а затем на требуемую частоту, с которой должно срабатывать прерывание (у нас это 11025 Гц). В обработчик прерывания мы будем передавать указатель на структуру с данными, которые мы хотим воспроизводить.
Таким образом, функция инициализации таймера получает следующий вид:
static void timerInit()
{
timer_config_t config = {
.divider = 8, // Предделитель
.counter_dir = TIMER_COUNT_UP, // Считать вверх
.counter_en = TIMER_PAUSE, // Состояние - пауза
.alarm_en = TIMER_ALARM_EN, // Включить прерывание Alarm
.intr_type = TIMER_INTR_LEVEL, // Прерывание по уровню
.auto_reload = 1, // Автоматически перезапускать счет
};
// Применить конфиг
ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config));
// Установить начальное значение счетчика
ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL));
// Установить значение счетчика для срабатывания прерывания Alarm
ESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate));
// Разрешить прерывания
ESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0));
// Зарегистрировать обработчик прерывания
timer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL);
// Запустить таймер
timer_start(TIMER_GROUP_0, TIMER_0);
}
Частота тактирования таймера не делится нацело на 11025, какой бы предделитель мы не установили. Поэтому я подобрал такой делитель, при котором частота максимально близка к требуемой.
Теперь переходим к написанию обработчика прерывания. Здесь все просто: берем очередной байт из массива, скармливаем его ЦАПу, продвигаемся по массиву дальше. Однако прежде всего нужно очистить флаги прерываний таймера и перезапустить прерывание Alarm:
static uint32_t wav_pos = 0;
static void IRAM_ATTR timer0_ISR(void *ptr)
{
// Очистить флаги прерываний
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);
// Перезапустить прерывание Alarm
timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);
audio_info_t *audio = (audio_info_t *)ptr;
if (wav_pos >= audio->dataLength) wav_pos = 0;
dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos));
wav_pos ++;
}
Да, работа со встроенным ЦАПом в ESP32 сводится к вызову одной встроенной функции dac_output_voltage (на самом деле нет).
Собственно, все. Теперь нужно внутри функции app_main() разрешить работу нужного нам канала ЦАП и инициализировать таймер:
void app_main(void)
{
…
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
Собираем, прошиваем, слушаем :) В принципе, можно подключить динамик напрямую к ножке контроллера — играть будет. Но лучше все же воспользоваться усилителем. Я использовал завалявшуюся у меня в закромах TDA7050.
На этом все. Да, когда у меня наконец запело, я тоже подумал, что все оказалось гораздо проще, чем я думал. Однако может быть, эта статья чем-нибудь поможет тем, кто только начал осваивать ESP32.
Может быть, когда-нибудь (и если эта недо-статья кому-нибудь понравится) я заведу ЦАП ESP32 с использованием DMA. Там все еще интереснее, потому что в этом случае работать придется со встроенным модулем I2S.
===========
Источник:
habr.com
===========
Похожие новости:
- [Работа с 3D-графикой, Робототехника, 3D-принтеры, DIY или Сделай сам] Как я делал свой самолёт
- [DIY или Сделай сам] Листовые материалы в корпусостроении — обзор и технологии
- [Научно-популярное, Звук, Мозг, Здоровье] «На одной волне с мозгом»: что можно услышать и увидеть — согласно подсказкам из поп-культуры
- [Разработка под Arduino, DIY или Сделай сам] Механико-цифровые часы из стальных шариков (перевод)
- [Машинное обучение, Искусственный интеллект, Звук, Будущее здесь] Hey, Google: умные устройства будут активироваться без команд
- [Программирование микроконтроллеров] Сравнение компиляторов ARMCC, IAR и GCC
- [История IT, Старое железо, DIY или Сделай сам, Игры и игровые приставки] Как заставить Arduino петь как ZX Spectrum. Часть 2: музыка Dizzy IV на Arduino Nano
- [Open source, Облачные сервисы, Финансы в IT, Звук] Обсуждение: почему индустрия подкастов все больше походит на стриминг сериалов и фильмов
- [Промышленное программирование, Разработка робототехники, Программирование микроконтроллеров, Производство и разработка электроники] ModBus Slave RTU/ASCII без смс и регистрации. Версия 3
- [API, DIY или Сделай сам, Голосовые интерфейсы] Вентилятор для zwift с алисой
Теги для поиска: #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_diy_ili_sdelaj_sam (DIY или Сделай сам), #_esp32, #_dac, #_timer, #_wav, #_zvuk (звук), #_mikrokontrollery (микроконтроллеры), #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
), #_diy_ili_sdelaj_sam (
DIY или Сделай сам
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:53
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В ходе разработки одного очень интересного устройства (эх, лишь бы силенок хватило) я решил, что будет неплохо, если устройство это будет говорящим. Как нельзя кстати здесь пригодилось наличие в целевом микроконтроллере, ESP32 компании Espressif Systems, двухканального 8-битного ЦАПа. В этом туториале (если его можно так назвать) я покажу, как можно быстро и довольно просто организовать проигрывание аудиофайла силами микроконтроллера ESP32. Немного теории Как нам сообщает Википедия, ESP32 — это серия недорогих микроконтроллеров с низким энергопотреблением. Они представляют собой систему на кристалле (SoC) с интегрированными контроллерами Wi-Fi и Bluetooth, и антеннами. Основаны на ядре Tensilica Xtensa LX6 в вариантах с одним и двумя ядрами. В систему интегрирован радиочастотный тракт. МК создан и разработан китайской компанией Espressif Systems, а производится компанией TSMC по техпроцессу 40 нм. Подробнее о возможностях чипа можно прочитать на странице в Википедии и в официальной документации. Однажды в рамках освоения этого контроллера мне захотелось воспроизвести на нем звук. Поначалу я думал, что придется использовать ШИМ. Однако, внимательнее почитав документацию, я обнаружил наличие двух каналов 8-битного ЦАПа. Разумеется, это в корне меняло дело. В Technical Reference сказано, что ЦАП в ESP32 построен на цепочке резисторов (видимо, имеется ввиду R2R цепочка) с применением некоего буфера. Выходное напряжение может изменяться в пределах от 0 вольт до напряжения питания (3,3 вольта) с разрешением 8 бит (то есть 256 значений). Преобразование двух каналов независимое. Также имеется встроенный генератор косинуса (CW generator) и поддерживается DMA. Я решил пока что не лезть в DMA, ограничившись построением проигрывателя на основе таймера. Как известно, чтобы воспроизвести простейший WAV-файл формата PCM, достаточно с указанной в файле частотой дискретизации читать из него сырые данные и распихивать по каналам ЦАПа, предварительно приводя (при необходимости) разрядность данных к разрядности ЦАП. Мне повезло: у меня нашелся набор звуков в формате WAV PCM 8 bit 11025 Hz mono, выдранный из ресурсов одной старой игры. Значит, будем использовать только один канал ЦАП. Также нам понадобится таймер, способный генерировать прерывания с частотой 11025 Гц. Согласно все тому же Technical Reference, ESP32 имеет на борту два модуля таймеров по два таймера в каждом, итого четыре таймера. Они имеют разрядность 64 бита, у каждого имеется 16-битный предделитель и возможность генерации прерывания по уровню или фронту. От теории к практике Вооружившись примером wave_gen из esp-idf, я отправился писать код. Я не стал упарываться созданием файловой системы: цель была получить звук, а не сделать из ESP32 полноценный плеер. Для начала перегнал один из WAV-файлов в сишный массив. В этом мне очень помогла встроенная в Дебиан утилита xxd. Простой командой $ xxd -i file.wav > file.c
получаем сишный файл с массивом данных в шестнадцатеричном виде внутри и даже с отдельной переменной, в которую помещен размер файла в байтах. Далее я закомментировал первые 44 байта массива — заголовок WAV-файла. Попутно я разобрал его по полям и узнал всю необходимую мне информацию о нем: const uint8_t sound_wav[] = {
// 0x52, 0x49, 0x46, 0x46, // chunk "RIFF" // 0xaa, 0xb4, 0x01, 0x00, // chunk length // 0x57, 0x41, 0x56, 0x45, // "WAVE" // 0x66, 0x6d, 0x74, 0x20, // subchunk1 "fmt" // 0x10, 0x00, 0x00, 0x00, // subchunk1 length // 0x01, 0x00, // audio format PCM // 0x01, 0x00, // 1 channel, mono // 0x11, 0x2b, 0x00, 0x00, // sample rate // 0x11, 0x2b, 0x00, 0x00, // byte rate // 0x01, 0x00, // bytes per sample // 0x08, 0x00, // bits per sample per channel // 0x64, 0x61, 0x74, 0x61, // subchunk2 "data" // 0x33, 0xb4, 0x01, 0x00, // subchunk2 length, bytes Отсюда видно, что наш файл имеет один канал, частоту дискретизации 11025 герц и разрешение 8 бит на семпл. Заметим, что если бы я захотел анализировать заголовок программно, то мне нужно было бы учитывать порядок байт: в WAV это Little-endian, то есть младшим байтом вперед. В итоге я создал тип структуры для хранения информации о звуке: typedef struct _audio_info
{ uint32_t sampleRate; uint32_t dataLength; const uint8_t *data; } audio_info_t; И создал собственно экземпляр структуры, заполнив ее следующим образом: const audio_info_t sound_wav_info =
{ 11025, // sampleRate 111667, // dataLength sound_wav // data }; В этой структуре поле sampleRate — это значение одноименного поля заголовка, поле dataLength — это значение поля subchunk2 length, а поле data — это указатель на массив с данными. Далее я подключил заголовочные файлы #include "driver/timer.h"
#include "driver/dac.h" и создал прототипы функций для инициализации таймера и обработчика его прерывания Alarm, как это сделано в примере wave_gen: static void IRAM_ATTR timer0_ISR(void *ptr)
{ } static void timerInit() { } После чего принялся за наполнение функции инициализации. Таймеры в ESP32 тактируются в конечном итоге от APB_CLK_FREQ, равного 80 МГц: driver/timer.h: #define TIMER_BASE_CLK (APB_CLK_FREQ) /*!< Frequency of the clock on the input of the timer groups */
soc/soc.h: #define APB_CLK_FREQ ( 80*1000000 ) //unit: Hz
Чтобы получить значение счетчика, при котором нужно генерировать прерывание Alarm, нужно частоту тактирования таймера поделить на значение предделителя, а затем на требуемую частоту, с которой должно срабатывать прерывание (у нас это 11025 Гц). В обработчик прерывания мы будем передавать указатель на структуру с данными, которые мы хотим воспроизводить. Таким образом, функция инициализации таймера получает следующий вид: static void timerInit()
{ timer_config_t config = { .divider = 8, // Предделитель .counter_dir = TIMER_COUNT_UP, // Считать вверх .counter_en = TIMER_PAUSE, // Состояние - пауза .alarm_en = TIMER_ALARM_EN, // Включить прерывание Alarm .intr_type = TIMER_INTR_LEVEL, // Прерывание по уровню .auto_reload = 1, // Автоматически перезапускать счет }; // Применить конфиг ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config)); // Установить начальное значение счетчика ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL)); // Установить значение счетчика для срабатывания прерывания Alarm ESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate)); // Разрешить прерывания ESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0)); // Зарегистрировать обработчик прерывания timer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL); // Запустить таймер timer_start(TIMER_GROUP_0, TIMER_0); } Частота тактирования таймера не делится нацело на 11025, какой бы предделитель мы не установили. Поэтому я подобрал такой делитель, при котором частота максимально близка к требуемой. Теперь переходим к написанию обработчика прерывания. Здесь все просто: берем очередной байт из массива, скармливаем его ЦАПу, продвигаемся по массиву дальше. Однако прежде всего нужно очистить флаги прерываний таймера и перезапустить прерывание Alarm: static uint32_t wav_pos = 0;
static void IRAM_ATTR timer0_ISR(void *ptr) { // Очистить флаги прерываний timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0); // Перезапустить прерывание Alarm timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0); audio_info_t *audio = (audio_info_t *)ptr; if (wav_pos >= audio->dataLength) wav_pos = 0; dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos)); wav_pos ++; } Да, работа со встроенным ЦАПом в ESP32 сводится к вызову одной встроенной функции dac_output_voltage (на самом деле нет). Собственно, все. Теперь нужно внутри функции app_main() разрешить работу нужного нам канала ЦАП и инициализировать таймер: void app_main(void)
{ … ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1)); timerInit(); Собираем, прошиваем, слушаем :) В принципе, можно подключить динамик напрямую к ножке контроллера — играть будет. Но лучше все же воспользоваться усилителем. Я использовал завалявшуюся у меня в закромах TDA7050. На этом все. Да, когда у меня наконец запело, я тоже подумал, что все оказалось гораздо проще, чем я думал. Однако может быть, эта статья чем-нибудь поможет тем, кто только начал осваивать ESP32. Может быть, когда-нибудь (и если эта недо-статья кому-нибудь понравится) я заведу ЦАП ESP32 с использованием DMA. Там все еще интереснее, потому что в этом случае работать придется со встроенным модулем I2S. =========== Источник: habr.com =========== Похожие новости:
Программирование микроконтроллеров ), #_diy_ili_sdelaj_sam ( DIY или Сделай сам ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:53
Часовой пояс: UTC + 5