STM32 Воспроизведение звука. Расширение возможностей программы

В предыдущей статье приводился пример того, как можно воспроизвести звук с помощью STM32. Пришло время расширить возможности этой программы, но прежде давайте определимся: для чего это нужно. Не проще ли купить недорогой китайский mp3-проигрыватель. Его возможности будут больше, да и цена…

Конечно, соревноваться китайпромом бесполезно. Если нужен проигрыватель, то лучше его купить. Но у нас задача другая. Во-первых, сможем ближе познакомиться с возможностями контроллера. Во-вторых, можно будет это устройство использовать там, где обычный проигрыватель сложно применить.

Например, недавно я сделал по заказу GSM-сигнализацию. Работает она исправно, но общается с пользователем через смс-ки, что не всегда удобно. Заказчик хочет, чтобы она могла “говорить”. Сработал датчик –> звонок пользователю –> голосовое сообщение “сработал такой-то датчик”.

Еще одно применение -  сделать небольшой модуль, состоящий из контроллера и карты памяти. Этот модуль можно подключать к устройству по USART, через который передавать команды для воспроизведения слов, фраз, предложений. Таким образом можно озвучить разные устройства.

Не будем забывать о проекте “Умный дом”. Для этого проекта воспроизведение звука желательно необходимо. Поэтому займемся усовершенствованием программы для ее дальнейшего практического применения. Назовем ее “Wave Player”, или сокращенно WP. Соответственно все функции, относящиеся к ней, будут иметь префикс wp.

Для начала, научим “Wave Player” понимать заголовок файла, в котором указан формат данных.

Для этого понадобиться структура, которая будет хранить информацию о формате данных:

1
2
3
4
5
6
7
8
typedef struct
{
  uint8_t  Channels;          //количество каналов                                             /
  uint16_t SamplesPerSec;     //частота дискретизации аудиосигнала, Гц
  uint8_t  BitsPerSample      //разрядность данных (8,16)
  uint32_t Size;              //количество полезных данных, байт  
  uint16_t AvgBytesPerSec;    //количество байт в секунду (поток данных)
} wp_format;

Первые три параметра нужны для настройки преобразования данных. Благодаря двум последним можно, например, вычислить время воспроизведения всего файла (в секундах): t = Size / AvgBytesPerSec

Еще понадобится перечисленный тип данных, который будет использоваться для определения возвращаемых функциями значений:

1
2
3
4
5
6
7
8
9
10
11
12
/*                значения, которые могут возвращать функции                                 */
typedef enum
{
  WP_OK = 0,                  //файл успешно воспроизведен
  WP_ERROR_FILE,              //файл не открыт (не найден, не верный формат и т.п.)
  WP_ERROR_READ_FILE,         //возникла ошибка чтения при воспроизведении открытого файла
  WP_STOP,                    //воспроизведение было остановлено программно, файл закрыт
  WP_PAUSE,                   //пауза воспроизведения
  WP_PLAY,                    //возобновить воспроизведение
  WP_NONE,                    //
  WP_ERROR                    //просто ошибка
}WPRESULT;

Создадим функцию wp_open, которая будет открывать требуемый файл, читать его заголовок и загружать данные о формате данных в структуру wp_format. При успешном открытии файла и прочтении заголовка функция будет возвращать 0, иначе (например, не поддерживаемый формат данных) файл будет закрыт и функция вернет значение, отличное от нуля.  

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
//********************************************************************************************
//function открывает файл и заполняет структуру формата данных format                       //
//argument указатель на файловую структуру, имя файла, указатель на структуру формата данных//
//result   файл открыт нормально, ошибка открытия                                           //
//********************************************************************************************
WPRESULT wp_open( FIL *file,  const char *FileName, wp_format *format )
{
  unsigned char res;                          //для хранения результата
  unsigned char buff[64];                     //буфер для заголовка файла
  unsigned char cnt;                          //количество прочитанных байт
  unsigned long i;                            //вспом. переменная
 
  res = f_open ( file, FileName, FA_READ );   //открыть файл для чтения
  if(res) return WP_ERROR_FILE;               //ошибка открытия
  res = f_read ( file, &buff[0], 64, &cnt );  //загрузить первые 64 байт
  if(res || cnt!=64) return WP_ERROR_FILE;    //ошибка чтения
 
  //байты 0..3 - должен быть маркер "RIFF"
  if(buff[0]!='R' || buff[1]!='I' || buff[2]!='F' || buff[3]!='F')     res = 2;
 
  //байты 8..11 - должен быть маркер "WAVE"
  if(buff[8]!='W' || buff[9]!='A' || buff[10]!='V' || buff[11]!='E')   res = 2;
 
  //байты 12..15 - должен быть маркер "fmt "
  if(buff[12]!='f' || buff[13]!='m' || buff[14]!='t' || buff[15]!=' ') res = 2;
 
  //байты 20,21  - 1 и 0
  if(buff[20]!=1 || buff[21]!=0) res = 2;
 
  //вычислить размер чанка "fmt "
  i = buff[16] | (buff[17]<<8) | (buff[18]<<16) | (buff[19]<<24);
 
  //после чанка "fmt " должен следовать чанк "data"
  if(buff[20+i]!='d' || buff[21+i]!='a' || buff[22+i]!='t' || buff[23+i]!='a') res = 2;
 
  if(res==2)
  {
    f_close(file);                          //закрыть файл
    return WP_ERROR_FILE;                   //ошибка формата файла
  }
 
  /*                    Загрузить формат данных                        */
  format->Channels       = buff[22];
  format->SamplesPerSec  = buff[24] | (buff[25]<<8) | (buff[26]<<16) | (buff[27]<<24);
  format->AvgBytesPerSec = buff[28] | (buff[29]<<8) | (buff[30]<<16) | (buff[31]<<24);
  format->BitsPerSample  = buff[34] | (buff[35]<<8);
  format->Size           = buff[24+i]|(buff[25+i]<<8)|(buff[26+i]<<16)|(buff[27+i]<<24);
 
  //файловый указатель на начало данных
  f_lseek(file,i+28);
 
  return WP_OK;
}

Небольшое пояснение к листингу функции wp_open:

  • Строки 13..16 – открываем файл и загружаем первые 64 байта в буфер.
  • Строки 19..25 – убеждаемся, что это WAV-файл.
  • Строка 31 – вычисляем положение начала чанка “data”, в нем содержаться полезные данные.
  • Строка 34 – убеждаемся, что чанк “data” действительно присутствует в нужном месте.
  • Строки 43..47 – считываем информацию о формате данных в структуру по указателю format.
  • Строка 50 – устанавливаем файловый указатель в позицию, с которой начинаются аудио данные.

Функция wp_open заполнит структуру, по которой необходимо настроить преобразователь цифровых данных в аналоговый сигнал. Здесь под преобразователем подразумевается не только сам модуль DAC, но и модули, обслуживающие его: ДМА и таймер.

Следующий шаг – создание функции,  выполняющей настройку преобразователя. Назовем эту функцию wp_init. Ей в качестве аргумента будет передаваться указатель на структуру, содержащей информацию о формате данных.

Прежде, чем создать функцию, определимся с возможностями нашего “Wave Player”. Для начала наделим его следующими возможностями:

  • воспроизведение  монофонического сигнала;
  • разная частотой дискретизации;
  • данные разрядностью 8 бит;
  • управление процессом воспроизведения – пауза, воспроизведение, стоп

Как видим, одно из ограничений – пока только монофонического сигнала. Почему так? Во-первых, wav-файлы имеют приличный объем, а для стереосигнала потребуется в два раза больший объем. Во-вторых, цель не в том, чтобы создать hi-end проигрыватель. Задачи у нас более приземленные, для которых достаточно одного канала.

Почему только 8 бит? Для первой модернизации этого будет вполне достаточно.

Учитывая оговоренные требования, напишем функцию, которая будет инициализировать преобразователь:

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
#define SIZE_DAC_BUFF  512                  //размер буфера в байтах
#define FREQ_CLK       24000000             //тактовая частота, Гц (в моем случае 24000000Гц )
 
unsigned char DAC_Buff[SIZE_DAC_BUFF];      //буфер данных ЦАПа
 
//********************************************************************************************
//function настраивает преобразователь под выбранный формат данных                          //
//argument указатель на структуру, содержащей информацию о формате данных                   //
//********************************************************************************************
void wp_init (wp_format *format)
{
  static char StartInit = 0;                     //признак стартовой инициализации
 
  //При первом обращении к функции выполнить стартовую инициализацию
  if(StartInit==0)
  {
    //Настраиваем первый канал ЦАПа для приема данных от ДМА
    RCC->APB2ENR        |= RCC_APB2ENR_IOPAEN;     //тактирование линий GPIOA
    RCC->APB1ENR        |= RCC_APB1ENR_DACEN;      //тактирование ЦАПа
    DAC->CR             |= DAC_CR_DMAEN1;          //разрешить прием данных канала №1 от ДМА
    DAC->CR             |= DAC_CR_BOFF1;           //отключить выходной буфер
    DAC->CR             |= DAC_CR_EN1;             //включить канал 1
    //Настроить 3-й канал ДМА для работы с ЦАПом
    RCC->AHBENR         |= RCC_AHBENR_DMA1EN;      //тактирование DMA
    DMA1_Channel3->CMAR  = (uint32_t)&DAC_Buff[0]; //указатель на начало буфера в памяти
    DMA1_Channel3->CCR   =  DMA_CCR3_DIR;          //передача из памяти в периферию
    DMA1_Channel3->CCR  &= ~DMA_CCR3_PINC;         //адрес периферии не инкрементируем
    DMA1_Channel3->CCR  |=  DMA_CCR3_MINC;         //адрес памяти инкрементируем
    DMA1_Channel3->CCR  |=  DMA_CCR3_CIRC;         //включить циклический режим передачи данных
    //Запуск передачи очередного значения через ДМА будет происходить от TIM6
    RCC->APB1ENR        |=  RCC_APB1ENR_TIM6EN;    //подаем тактирование TIM6
    TIM6->PSC            =  0;                     //предделитель не используем (кофф =1)
    TIM6->DIER          |=  TIM_DIER_UDE;          //разрешить запрос DMA при обновлении
    //Стартовая инициализация выполнена, устанавливам признак этого
    StartInit            = 1;
  }
 
  DMA1_Channel3->CCR  &= ~DMA_CCR3_EN;           //отключить третий канал DMA
  DMA1_Channel3->CPAR  = (uint32_t)&DAC->DHR8R1; //указатель на регистр-приемник
  DMA1_Channel3->CNDTR = SIZE_DAC_BUFF;          //размер буфера
  DMA1_Channel3->CCR  |=  DMA_CCR3_EN;           //разрешаем работу третьего канала DMA
 
  //задаем частоту дискретизации
  TIM6->ARR = (uint32_t)FREQ_CLK / format->SamplesPerSec;
}

Думаю, листинг функции в особых комментариях не нуждается. Строки с 15 по 38 — первоначальная инициализация преобразователя, которая выполняется всего один раз, при первом вызове функции.

Строка 42 – настраиваем таймер на требуемую частоту дискретизации. Этот параметр корректируется каждый раз при вызове функции, так как он может отличаться от файла к файлу.

И наконец функция wp_play, которая будет выполнять проигрывание файла:

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
//********************************************************************************************
//function воспроизводит требуемый файл                                                     //
//argument имя файла(с указанием пути к нему)                                               //
//result   смотри значения WPRESULT в заголовочном файле                                    //
//********************************************************************************************
WPRESULT wp_play ( const char *FileName )
{
  FIL           file;                       //файловая переменная
  wp_format     format;                     //хранит инфу о формате звуковых данных
  unsigned int cnt;                        //для хранения количества прочитанных байт
  WPRESULT      res;
 
  res = wp_open(&file, FileName, &format);  //открыть файл для воспроизведения
  if(res) return WP_ERROR_FILE;             //ошибка открытия файла
 
  wp_init(&format);                         //настроить преобразователь согласно формату
 
  /*     Загрузить весь буфер ЦАПа данными из файла и запустить проигрывание     */
  if( f_read (&file, &DAC_Buff[0], SIZE_DAC_BUFF, &cnt) )
  {
    f_close(&file);                         //ошибка при чтении!!! Закрываем файл
    return WP_ERROR_READ_FILE;              //и выходим
  }
 
  WP_ENABLE                                 //запустить преобразование
 
  /*                  Воспроизведение файла                  */
  while(1)
  {
    while(!(DMA1->ISR & DMA_ISR_HTIF3));      //ждем освобождение первой части буфера
    DMA1->IFCR |= DMA_ISR_HTIF3;              //сбросить флаг
    //заполнить освободившуюся часть буфера
    res = f_read ( &file, &DAC_Buff[0], SIZE_DAC_BUFF/2, &cnt );
    if(res) {res = WP_ERROR_READ_FILE; break;}//ошибка при чтении!!!
    if(cnt<SIZE_DAC_BUFF/2) break;            //файл закончился
 
    while(!(DMA1->ISR & DMA_ISR_TCIF3));      //ждем освобождение второй части буфера
    DMA1->IFCR |= DMA_ISR_TCIF3;              //сбросить флаг
    //заполнить освободившуюся часть буфера
    res = f_read ( &file, &DAC_Buff[SIZE_DAC_BUFF/2], SIZE_DAC_BUFF/2, &cnt );
    if(res) {res = WP_ERROR_READ_FILE; break;}//ошибка при чтении!!!
    if(cnt<SIZE_DAC_BUFF/2) break;            //файл закончился
 
    //управление процессом воспроизведения
    res = wp_control();
    if(res!=WP_NONE)
    {
      if(res==WP_PAUSE)                       //сделать паузу
      {
         WP_DISABLE;                          //остановить преобразование
         while(res!=WP_PLAY&&res!=WP_STOP)    //ожидать play или stop
         res = wp_control();
      }
      if(res==WP_STOP) break;                 //была команда стоп, закрываем файл
      WP_ENABLE;                              //возобновить преобразование
    }
  }
 
  WP_DISABLE                                  //остановить преобразование
  f_close(&file);                             //закрыть файл перед выходом
  return res;                                 //возвратить результат
 
}

Строка 13 — открываем файл и читаем его заголовок.
Строка 16 — настраиваем преобразователь согласно прочитанным данным.
Строка 19 — загружаем весь буфер полезными данными.
Строка 25 — запускаем преобразование. После этого пойдет звук.
Строки 28..57 — процесс воспроизведения всего файла. Принцип такой: ждем освобождения первой половины буфера. Когда освободится, загружаем ее данными, и делаем то же для второй половины.
Об окончании файла узнаем, когда прочитанных из файла данных будет меньше запрошенных для чтения(строки 35 и 42).
По окончанию останавливаем преобразователь и закрываем файл (строки 59 и 60).
Для управления процессом воспроизведения предназначены строки 45..56. Возможна приостановка воспроизведения (PAUSE), возобновление воспроизведения (PLAY) и остановка с закрытием файла (STOP). Для задания требуемого действия предназначена фнукция wp_control:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//********************************************************************************************
//function управление процессом воспроизведения                                             //
//result   требуемое действие                                                               //
//********************************************************************************************
WPRESULT wp_control(void)
{
  /*Здесь можно написать код для управления воспроизведением.
    Например, при нажатии кнопки №1 вернуть WP_PAUSE
    при нажатии кнопки №2 вернуть WP_PLAY
    при нажатии кнопки №3 вернуть WP_STOP
  */
 
  return WP_NONE;
}

Это все нужные нам функции. Воспроизведение файла будет выглядеть следующим образом:

1
2
3
4
.......                         //что-то делаем
f_mount(0, &fs);                //монтируем диск
.......                         //что-то делаем                                               /
wp_play("0:sound.wav");         //воспроизводим файл

На этом первое усовершенствование программы закончено.
Скачать файлы WavePlayer.c и WavePlayer.h можно здесь: скачать

Комментарии (8) на “STM32 Воспроизведение звука. Расширение возможностей программы”

  • Vladimir:

    Линк исправте на RU вместо COM :)

  • Alex:

    Хотелось бы увидеть по ссылке законченный проект — и main, и звуковую и fat-части…
    Спасибо.

  • Бросайте всё и давайте продолжение звукового проекта. :)
    Спасибо!

  • Роберт:

    Здравствуйте! На сайте у Вас мелькнула информация про GSM сигнализацию. Подскажите, Вы использовали готовый модем или GSM модуль? Может быть Вы мне поможете, вопрос такой, ради интереса хочу связать stm32 и gsm модуль mc52i (у данного модуля есть аппаратный usart) . Нужно ли между контроллером и модулем ставить микросхему max232?

    • kontroller:

      Я использовал SIM900
      У него уровень сигналов совместим с STM32 (но я на всякий случай подключил через резисторы 100 Ом и использовал выводы толерантные к 5В)
      Как обстоит дело с mc52i не знаю.
      А какое у нее питание?

  • boomer:

    Для сравнения строк есть более компактные strcmp,strncmp,memcmp

Оставить комментарий

Spam Protection by WP-SpamFree