Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC)

в 11:46, , рубрики: dac, К1986ВЕ92QI, микроконтроллеры, миландр, отладка, программирование микроконтроллеров, прямоугольный сигнал., Работа со звуком, синусоида, синусоидальный сигнал, цап, Электроника для начинающих

Вступление

В предыдущей статье мы поговорили о настройке тактовой частоты микроконтроллера. Сейчас мне хотелось бы рассмотреть варианты работы со звуком: его генерирование и воспроизведение. По началу мне хотелось написать одну большую статью, в которой было бы рассмотрено все. От генерации прямоугольных импульсов до воспроизведения FLAC с microSD карты. Но статья получилось просто гигантской. Так что я решил разбить ее на несколько статьей поменьше. В каждой из которых я разбираю по одному периферийному модулю.

Для тех, кому не понравятся реализации задач в статье.

Сразу же оговорюсь. Решать поставленные в статье задачи я буду лишь с помощью тех знаний, которые уже были изложены. Я понимаю, что где-то более верным будет использование DMA, где-то таймеров, и так далее. Но все это будет рассмотрено далее.

Общее представление о простом методе генерирования звука.

До момента написания статьи я имел лишь смутное представление о том, как происходит генерирование звука, но после прочтения этой статьи все встало на свои места. Из приведенной статьи можно выделить самое главное — принцип.

Чтобы создать звук нам нужно заставить колебаться мембрану динамика с определенной частотой. Каждой ноте соответствует своя частота, например ноте До 1 октавы, соответствует частота 261Гц. Т.е. дрыгая ногой микроконтроллера, подключенной к динамику, со скоростью 261 раз в секунду мы будем слышать звучание этой ноты. Для тех кто не силен в музыкальной теории, звук ближе от 1кГц и выше будет более писклявый, ниже 300Гц будет басить.

На нашей плате установлен разъем Jack 3.5 и усилитель к нему. Рассмотрим принципиальную схему.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 1

Как мы видим, усилитель подключен к контроллеру через пин PE0. После переключения джампера «DAC_OUT_SEL» на плате, мы можем генерировать звук, который можем услышать, подключив, к примеру, наушники к Jack-у.

Реализация простой пищалки

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

//---------------------------------------------------------
//Настраиваем выход, подключенный к усилителю. 
//---------------------------------------------------------
#define PER_CLOCK_PORTE              (1<<25)      //Бит включения тактирования порта E.
#define PORT_OE_OUT_PORTE_0          (1<<0)       //Включение этого бита переводит PORTE_0 в "выход". 
#define ANALOG_EN_DIGITAL_PORTE_0    (1<<0)       //Включаем цифровой режим бита порта PORTE_0.
#define PWR_MAX_PORTE_0              (3<<0)       //Включение данных бит переключает PORTE_0 в режим максимальной скорости.

#define PORT_RXTX_PORTE_0_OUT_1      (1<<0)       //Маска порта для подачи "1" на выход.

void Buzzer_out_init (void)
{
	RST_CLK->PER_CLOCK |= PER_CLOCK_PORTE;          //Включаем тактирование порта E.
	PORTE->OE |= PORT_OE_OUT_PORTE_0;               //Выход. 
	PORTE->ANALOG |= ANALOG_EN_DIGITAL_PORTE_0;     //Цифровой.
	PORTE->PWR |= PWR_MAX_PORTE_0;                  //Максимальная скорость (около 10 нс).
}

Теперь нужно решить, с какой частотой мы будем «пищать». В этом мне помогла эта таблица частот. Для наглядности представлена ниже.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 2

Нота до ( C ) первой октавы имеет частоту 261.63 Герца. Это значит, что за одну секунду проходит 261.63 периода. В каждом из которых мы 2 раза меняем состояние бита. И того нам нужно менять состояние бита 523.26 раз в секунду. Поделив 1 секунду на 523.26 мы получим 0,0019110958223445, что примерно равняется 191*10^(-5) секунды. Это «пауза» между переключениями.

Теперь, когда мы знаем примерное значение задержки, мы можем настроить SysTick таймер и задержку к нему. Настроить нам его нужно на прерывания раз в 10^(-5) секунды. Для этого немного изменим нашу функцию из этой статьи. Получим следующее.

void Init_SysTick (void)                          //Прерывание раз в 10^(-5) секунды. 
{
	SysTick->LOAD = (8000000/100000)-1;                 
	SysTick->CTRL |= CLKSOURCE|TCKINT|ENABLE;
}

volatile uint32_t Delay_dec = 0;                  //Прерывание от SysTick таймера.
void SysTick_Handler (void)
{
	if (Delay_dec) Delay_dec--;
}

void Delay (uint32_t Delay_Data)                  //Функция задержки на основе SysTick таймера. 
{
	Delay_dec = Delay_Data;
	while (Delay_dec) {};
}

Ну и теперь, наконец-то, воспользуемся всем вышеописанным.

int main (void)
{
  Buzzer_out_init();                               //Инициализируем пин звукового генератора. 
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний раз в 10^(-5) секунды.  
  while (1)                                        //Создаем колебания с частотой 261.63 гц.
  {
    PORTE->RXTX |= PORT_RXTX_PORTE_0_OUT_1;        //Включаем "1" на порту, подключеному к усилителю.
    Delay(191);
    PORTE->RXTX = 0;
    Delay(191);
  }
}

Все бы хорошо, но тут первые грабли. Как вы могли заметить, в основной функции нет ни строчки кода для настройки тактирования. Контроллер тактируется от HSI. В нашем примере это привело к тому, что вместо ноты до первой октавы, играла нота ля малой октавы (на 2 целых тона ниже). Чтобы исправить эту ошибку — добавим функцию переключения источника тактовой частоты с HSI на HSE (внешний кварцевый резонатор).

О системе тактирования было рассказано в этом уроке.

int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_init();                               //Инициализируем пин звукового генератора. 
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний раз в 10^(-5) секунды.  
  while (1)                                        //Создаем колебания с частотой 261.63 гц.
  {
    PORTE->RXTX |= PORT_RXTX_PORTE_0_OUT_1;        //Включаем "1" на порту, подключеному к усилителю.
    Delay(191);
    PORTE->RXTX = 0;
    Delay(191);
  }
}

Чуть усложним задачу. Напишем программу, которая будет играть нам гамму из 12 полутонов (7 белых клавиш и 5 черных, если смотреть на фортепиано). Для этого создадим массив с длительностями всех задержек. Рассчитываем их так же, как и предыдущую: 100000/частота_ноты/2 = длительность_задержки. Делим 100000 потому, что у нас прерывание раз в 0.00001 секунды (10^(-5)).

const uint32_t MES[13] = {191, 180, 170, 161, 152, 143, 135, 128, 120, 114, 107, 101, 96};

Теперь немного изменим основную функцию.

int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_init();                               //Инициализируем пин звукового генератора. 
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний раз в 10^(-5) секунды.  
  while (1)                                        //Создаем колебания с частотой 261.63 гц.
  {
    for (uint32_t Nambe = 0; Nambe<13; Nambe++)    //Играем по очереди ноты. 
    {
      for (uint32_t LoopN = 0; LoopN<MES[12-Nambe]*3; LoopN++) //Некоторое время играем каждую из них.
      {
         PORTE->RXTX |= PORT_RXTX_PORTE_0_OUT_1;   //Включаем "1" на порту, подключеному к усилителю.
         Delay(MES[Nambe]);
         PORTE->RXTX = 0;
         Delay(MES[Nambe]);
      }
    }
  }
}

Небольшое пояснение к коду. Так как длинна звучания сэмпла (одного периода звуковой волны) у каждой ноты разное, то для того, чтобы более-менее сделать звучание каждой ноты одинаковым, цикл ожидания составлялся следующим образом. Число самой длинной ноты (нота с большим числом задержки) звучало «задержку раз» самой короткой. Поясню. Нота до первой октавы (191) игралась 96 * 3 раз, а нота до второй октавы (96) играла 191 раз * 3. Тройка здесь коэффициент длительности. Далее будет рассмотрен более правильный способ измерения задержки.

Вот так выглядит наша волна.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 3

Если присмотреться по ближе, то можно увидеть ее несовершенность. Она даже близко не похожа на прямоугольные импульсы.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 4

Скачать звуковой файл можно здесь. Файл простой пищалки.

Освоение ЦАП

Рассматривая рисунок нашей «волны» и в голову не придет, что там синусоида. Изучая вопрос создания синусоиды и вообще сигнала любой формы на микроконтроллере я наткнулся на данную статью. Данная статья попадалась мне очень давно. И тогда я прочел ее просто ради интереса. Сейчас же она представляет из себя практический интерес. В статье описано создание и использования простого ЦАП (цифро-аналоговый преобразователь). ЦАП — это устройство для преобразования значения напряжения в, непосредственно, аналоговое напряжение на выходе. Я уже было хотел собирать схему из статьи, но в очередной раз изучая документацию увидел пункт.

Контроллер MDR_DAC ...........................................................................................................................326

Это стало для меня очень приятным сюрпризом. Никогда ранее я не видел ЦАП-а непосредственно в самом микроконтроллере. Но для начала нужно понять, можно ли подключить ЦАП к усилителю. Для этого открываем схему платы и видим следующее.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 5

Вывод нашего усилителя подключается непосредственно к PE0, к которому подключен ЦАП. Отлично. Можно начать настройку. Но перед этим немного изучим ЦАП.

В микроконтроллере реализовано два ЦАП. Для включения ЦАП необходимо установить бит Cfg_ON_DACx в 1, используемые выводы ЦАП порта Е были сконфигурированы как аналоговые и были отключены какие-либо внутренние подтяжки. Оба ЦАП могут работать независимо или совместно. При независимой работе ЦАП (бит Cfg_SYNC_A=0) после записи данных в регистр данных DACx_DATA на выходе DACx_OUT формируется уровень напряжения, соответствующий записанному значению. При синхронной работе (бит Cfg_SYNC_A=1) данные обоих ЦАП могут быть обновлены одной записью в один из регистров DACx_DATA. ЦАП может работать от внутренней опоры Cfg_M_REFx=0, тогда ЦАП формирует выходной сигнал в диапазоне от 0 до напряжения питания AUCC. В режиме работы с внешней опорой Cfg_M_REFx=1 ЦАП формирует выходное напряжение в диапазоне от 0 до значения DACx_REF.

Тут, можно сказать, описана вся настройка. Взглянем на регистры.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 6

Их здесь всего три регистра на два ЦАП. Из них два регистра для хранения значения на выходе у каждого из ЦАП. Рассмотрим регистр настройки.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 7

Приступаем к настройке. Так же не забываем про тактирование DAC. Ну и для теста выставим на выход максимальное напряжение (0xFFF = 4095).

//---------------------------------------------------------
//ЦАП.
//---------------------------------------------------------
#define PCLK_EN_DAC             (1<<18)                                //Маска включения тактирования ЦАП. 
#define CFG_Cfg_ON_DAC0         (1<<2)                                 //Маска включения ЦАП1.                    

void ADC_Init (void)
{
	RST_CLK->PER_CLOCK |= PCLK_EN_DAC;             //Включаем тактирование ЦАП.
	DAC->CFG = CFG_Cfg_ON_DAC0;                    //Включаем ЦАП1. Ассинхронно. От внутреннего источника.
}

Далее не забудем и о выходе. В предыдущем уроке мы настраивали его как цифровой выход. Сейчас же, согласно рекомендации, нужно настроить как аналоговый.

void Buzzer_out_DAC_init (void)
{
	RST_CLK->PER_CLOCK |= PER_CLOCK_PORTE;          //Включаем тактирование порта E.
	PORTE->OE |= PORT_OE_OUT_PORTE_0;               //Выход. 
	PORTE->ANALOG = 0;                              //Аналоговый.
	PORTE->PWR |= PWR_MAX_PORTE_0;                  //Максимальная скорость (около 10 нс).
}

Ну и добавим все это в основную функцию.

int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_DAC_init();                           //Настраиваем порт для ЦАП.
  ADC_Init();                                      //Настраиваем ЦАП.
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний раз в 10^(-5) секунды. 
  DAC->DAC1_DATA = 0xFFF;                          //Загружаем максимум (тестовый).
  while (1)                                        
  {

  }
}

Сейчас, если замерить напряжение на пине, мы должны получить около трех вольт. НО. Этого не происходит. На пине у нас около 0.08 вольт. Что не есть хорошо. Идем разбираться. Прежде всего я проверил, затактирован ли ЦАП, Все было хорошо. Отладчик сообщает, что все регистры заполнены верно. Далее я решил взглянуть на таблицу пинов и обнаружил следующее.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 8

Вот это новость. PE0 подключен не к DAC1, а к DAC2! Вот и еще одна ошибка… Меняем функцию ЦАП-а.

//---------------------------------------------------------
//ЦАП.
//---------------------------------------------------------
#define PCLK_EN(DAC)             (1<<18)                                //Маска включения тактирования ЦАП. 
#define CFG_Cfg_ON_DAC0          (1<<2)                                 //Маска включения ЦАП1.                    
#define CFG_Cfg_ON_DAC1          (1<<3)
void ADC_Init (void)
{
	RST_CLK->PER_CLOCK |= PCLK_EN(DAC);             //Включаем тактирование ЦАП.
	DAC->CFG = CFG_Cfg_ON_DAC1;                     //Включаем ЦАП2. Ассинхронно. От внутреннего источника.
}

Пробуем запустить. Теперь все хорошо. На выходе 3.28 вольта. Теперь по примеру простой пищалки попробуем сгенерировать звук прямоугольными импульсами. Для этого чуть изменим код предыдущего проекта.

const uint32_t MES[13] = {191, 180, 170, 161, 152, 143, 135, 128, 120, 114, 107, 101, 96};
int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_DAC_init();                           //Настраиваем порт для ЦАП.
  ADC_Init();                                      //Настраиваем ЦАП.
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний раз в 10^(-5) секунды. 
  while (1)                                        
  {
    for (uint32_t Nambe = 0; Nambe<13; Nambe++)    //Играем по очереди ноты. 
    {
      for (uint32_t LoopN = 0; LoopN<MES[12-Nambe]*3; LoopN++) //Некоторое время играем каждую из них.
      {
         DAC->DAC2_DATA = 0xFFF;
         Delay(MES[Nambe]);
         DAC->DAC2_DATA = 0; 
         Delay(MES[Nambe]);
      }
    }
  }
}

Чисто по ощущениям звук намного преятнее, чем в предыдущем примере. Да и звучит он куда громче. Вот запись звукового сигнала. Вот файл этого проекта. А вот, для сравнения, наша волна.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 9
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 10
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 11

Отступление: усилитель на плате безумно сильно греется. Если оставить его в таком режиме минут на 10, то он превращается в печь… Поэтому я отключаю джампер после того, как послушал звук. Так он не нагревается.

Генерация синусоидальной волны.

Разобравшись с тем, как генерировать напряжение на выходе разного уровня я задумался, откуда брать значения этого напряжения? Буквально сразу же после начала поисков я наткнулся на эту статью. В ней я нашел самое главное. Код получения значений синуса волны. Немного переделав код я получил программу, которая запрашивая длину волны и частоту сэмпла генерирует массив значений напряжения для нашего кода. Вот код на Pascal ABC (Все таки нужно готовиться к ЕГЭ и временами писать и на паскале.).

Program Sin_wav;
Var Real_Data,
    PR: double;           //Периуд волны.
    samplerate: word;     //Частота сэмпла.
    wavefrequency: double;//Частота волны. 
    Loop: word;           //Счетчик.
    Name: string;         //Имя массива.
Begin
    write('Частота сэмпла: '); readln(samplerate);   //Получаем данные.
    write('Частота волны: ');  readln(wavefrequency);
    write('Имя массива:  ');   readln(Name);
    write('const uint16_t ', Name, '[', samplerate, '] = {');
    PR:=samplerate/2;                                //Вычисляем периуд.
    for Loop:=0 to samplerate-1 do                   //-1, т.к. считаем с 0.
    Begin
    Real_Data := 2047*sin(Loop*pi/PR) + 2047;        //Вычисление sine-волны.
    //Т.к. мы не можем создать отрицательое напряжение - поднимим 0 до середины.
    //Таким образом 2048-1 (от 0 до 4095) = 0, а 2045 = -2.
    //2047 - половина разрешения нашего ЦАП. Половина +, половина -. И 0.
    write(Round(Real_Data));
    if (Loop<>samplerate-1) then write(', ');
    End;
    write('};')
End.

Немного поясню. Синусоида может принимать как положительные, так и отрицательные значения. Наш ЦАП может генерировать лишь положительное напряжение. Так как колебания, грубо говоря, происходят из-за изменения напряжения, я решил, что 0 будет на уровне половины разрешения ЦАП. Иначе говоря 2047 = 0, 2045 = -2, 2049 = 2. Общая амплитуда 4095 (Если вести счет от 0). Вот пример исполнения кода, генерирующего синусоиду ноты до первой октавы (по таблице частота волны 261.63 Герца). Мы разобьем эту синусоиду на 100 участков.

Нота до ( C ) первой октавы, 100 участков.

Частота сэмпла: 100
Частота волны: 261.63
Имя массива:  C_4
const uint16_t C_4[100] = {2047, 2176, 2304, 2431, 2556, 2680, 2801, 2919, 3033, 3144, 3250, 3352, 3448, 3539, 3624, 3703, 3775, 3841, 3899, 3950, 3994, 4030, 4058, 4078, 4090, 4094, 4090, 4078, 4058, 4030, 3994, 3950, 3899, 3841, 3775, 3703, 3624, 3539, 3448, 3352, 3250, 3144, 3033, 2919, 2801, 2680, 2556, 2431, 2304, 2176, 2047, 1918, 1790, 1663, 1538, 1414, 1293, 1175, 1061, 950, 844, 742, 646, 555, 470, 391, 319, 253, 195, 144, 100, 64, 36, 16, 4, 0, 4, 16, 36, 64, 100, 144, 195, 253, 319, 391, 470, 555, 646, 742, 844, 950, 1061, 1175, 1293, 1414, 1538, 1663, 1790, 1918};

Эксперименты с синусоидальной волной.

Получив синусоиду разбитую на 100 частей, вспоминаем, что эта синусоида должна быть проиграна с частотой 261,63 герца. Теперь рассчитаем интервал прерывания. Секунда/(100 частей * 261, 63) = 0.00003822191 секунды. Ну чтож. Скажу сразу. Я провел море эксперементов, чтобы получить звук. Вкратце расскажу о них. Так как частоты в 8 Мгц уже явно не хватало для такой скорости, то я решил себя побаловать и разогнал чип до 80 МГц, надеясь, что этого мне точно хватит. Но не тут то было. Настроив прерывания SysTick на 10000000 раз в секунду, контроллер даже не доходил до цикла, в котором выводились данные. После я решил, что куда проще будет выдавать данные сразу в прерывании. Получилось следующее.

void Init_SysTick (void)                          //Прерывание 10000000 раз в секунду. 
{
	SysTick->LOAD = (80000000/10000000)-1;                 
	SysTick->CTRL |= CLKSOURCE|TCKINT|ENABLE;
}

const uint16_t C_4[100] = {2047, 2176, 2304, 2431, 2556, 2680, 2801, 2919, 3033, 3144, 3250, 3352, 3448, 3539, 3624, 3703, 3775, 3841, 3899, 3950, 3994, 4030, 4058, 4078, 4090, 4094, 4090, 4078, 4058, 4030, 3994, 3950, 3899, 3841, 3775, 3703, 3624, 3539, 3448, 3352, 3250, 3144, 3033, 2919, 2801, 2680, 2556, 2431, 2304, 2176, 2047, 1918, 1790, 1663, 1538, 1414, 1293, 1175, 1061, 950, 844, 742, 646, 555, 470, 391, 319, 253, 195, 144, 100, 64, 36, 16, 4, 0, 4, 16, 36, 64, 100, 144, 195, 253, 319, 391, 470, 555, 646, 742, 844, 950, 1061, 1175, 1293, 1414, 1538, 1663, 1790, 1918};
volatile uint16_t Loop = 0;
volatile uint32_t Delay_dec = 0;                  //Прерывание от SysTick таймера.
void SysTick_Handler (void)
{
	Delay_dec++; if (Delay_dec==(382-1))
	{
    DAC->DAC2_DATA = C_4[Loop];
    if (Loop<99) Loop++; else Loop = 0;
		Delay_dec=0;
	}
}

Основная функция имела вид:

int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_DAC_init();                           //Настраиваем порт для ЦАП.
  ADC_Init();                                      //Настраиваем ЦАП.
  HSE_PLL(10);                                     //8 Мгц -> 80 Мгц. 
  Init_SysTick();                                  //Инициализируем системный таймер для прерываний.
  while (1)                                        
  {

  }
}

Звук получался таким:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 12

При детальном рассмотрении видно следующее:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 13

Вот приближенное возрастание «синусоиды»:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 14

Видна огромная погрешность. А так же звук получился очень низким. Может быть ля малой октавы. Не выше. Что свидетельствует о том, что код в прерывании просто не успевает выполняться. Даже при частоте 80 Мгц. Поступим иначе. Немного снизим качество. Сделаем прерывание чуть реже. И округлим цикл ожидания в прерывании. Получаем следующее.

void Init_SysTick (void)                          //Прерывание 10000000 раз в секунду. 
{
	SysTick->LOAD = (80000000/1000000)-1;                 
	SysTick->CTRL |= CLKSOURCE|TCKINT|ENABLE;
}

const uint16_t C_4[100] = {2047, 2176, 2304, 2431, 2556, 2680, 2801, 2919, 3033, 3144, 3250, 3352, 3448, 3539, 3624, 3703, 3775, 3841, 3899, 3950, 3994, 4030, 4058, 4078, 4090, 4094, 4090, 4078, 4058, 4030, 3994, 3950, 3899, 3841, 3775, 3703, 3624, 3539, 3448, 3352, 3250, 3144, 3033, 2919, 2801, 2680, 2556, 2431, 2304, 2176, 2047, 1918, 1790, 1663, 1538, 1414, 1293, 1175, 1061, 950, 844, 742, 646, 555, 470, 391, 319, 253, 195, 144, 100, 64, 36, 16, 4, 0, 4, 16, 36, 64, 100, 144, 195, 253, 319, 391, 470, 555, 646, 742, 844, 950, 1061, 1175, 1293, 1414, 1538, 1663, 1790, 1918};
volatile uint16_t Loop = 0;
volatile uint32_t Delay_dec = 0;                  //Прерывание от SysTick таймера.
void SysTick_Handler (void)
{
	Delay_dec++; if (Delay_dec==(38-1))
	{
    DAC->DAC2_DATA = C_4[Loop];
    if (Loop<99) Loop++; else Loop = 0;
		Delay_dec=0;
	}
}

Теперь прерывание успевает обработаться. Мы получаем звук практически идентичный с нотой До. Но все же на слух (при сравнении с фортепиано) можно услышать неточность. Прослушать можно тут. Файл проекта здесь.
Наша звуковая волна имеет следующий вид:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 15

Подъем «синусоиды» имеет следующий вид:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 16

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

Частота сэмпла: 20
Частота волны: 261.63
Имя массива:  C_4
const uint16_t C_4[20] = {2047, 2680, 3250, 3703, 3994, 4094, 3994, 3703, 3250, 2680, 2047, 1414, 844, 391, 100, 0, 100, 391, 844, 1414};

Рассчитаем частоту прерывания теперь. Секунду/(20 частей * 261.63) = 0.00019110958 секунды ~ 191*10^(-6). Это уже лучше, чем было раньше. Настраиваем прерывания и задержку. Получаем следующее.

void Init_SysTick (void)                          //Прерывание 1000000 раз в секунду. 
{
	SysTick->LOAD = (80000000/1000000)-1;                 
	SysTick->CTRL |= CLKSOURCE|TCKINT|ENABLE;
}

const uint16_t C_4[20] = {2047, 2680, 3250, 3703, 3994, 4094, 3994, 3703, 3250, 2680, 2047, 1414, 844, 391, 100, 0, 100, 391, 844, 1414};
volatile uint16_t Loop = 0;
volatile uint32_t Delay_dec = 0;                  //Прерывание от SysTick таймера.
void SysTick_Handler (void)
{
	Delay_dec++; if (Delay_dec==(191-1))
	{
    DAC->DAC2_DATA = C_4[Loop];
    if (Loop<19) Loop++; else Loop = 0;
		Delay_dec=0;
	}
}

Мы получили звук еще более приближенный к ноте До. Звук можно взять здесь.

Взглянем на волну:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 17
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 18

К моему удивлению передо мной снова практически прямоугольные импульсы! Хотя должна была быть синусоида. Где-то я ошибся… «А что, если снизить амплитуду колебания?» — подумал я. Изменил в программе на паскале параметр, показывающий «высоту волны» от «0» до «предела» с 2047 на 1500. Но это ни к чему не привело. И тут я взглянул на меню программы подробнее и увидел.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 19

От -1 вольта до 1 вольта! Иначе говоря, амплитуда 2 вольта! А у меня было 3 + усилитель! Мне было лень искать документацию на усилитель, поэтому, путем подбора я узнал, что идеальная амплитуда — 70*2. Вот код измененный программы на паскале.

Program Sin_wav;
Var Real_Data,
    PR: double;           //Периуд волны.
    samplerate: word;     //Частота сэмпла.
    wavefrequency: double;//Частота волны. 
    Loop: word;           //Счетчик.
    Name: string;         //Имя массива.
Begin
    write('Частота сэмпла: '); readln(samplerate);   //Получаем данные.
    write('Частота волны: ');  readln(wavefrequency);
    write('Имя массива:  ');   readln(Name);
    write('const uint16_t ', Name, '[', samplerate, '] = {');
    PR:=samplerate/2;                                //Вычисляем периуд.
    for Loop:=0 to samplerate-1 do                   //-1, т.к. считаем с 0.
    Begin
    Real_Data := 70*sin(Loop*pi/PR) + 2047;        //Вычисление sine-волны.
    //Т.к. мы не можем создать отрицательое напряжение - поднимим 0 до середины.
    //Таким образом 2048-1 (от 0 до 4095) = 0, а 2045 = -2.
    //2047 - половина разрешения нашего ЦАП. Половина +, половина -. И 0.
    write(Round(Real_Data));
    if (Loop<>samplerate-1) then write(', ');
    End;
    write('};')
End.

Вот удачный массив:

const uint16_t C_4[20] = {2047, 2069, 2088, 2104, 2114, 2117, 2114, 2104, 2088, 2069, 2047, 2025, 2006, 1990, 1980, 1977, 1980, 1990, 2006, 2025};

Аудиозапись. Теперь взглянем на наш сигнал. Наконец-то что-то похожее на синусоиду!

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 20

Теперь, когда я смог создать синусоиду из 20 частей, воспользуемся кодом, рассмотренным ранее, и попытаемся сделать синусоиду из 100 частей. Вот получившийся массив.

Частота сэмпла: 100
Частота волны: 261.63
Имя массива:  C_4
const uint16_t C_4[100] = {2047, 2051, 2056, 2060, 2064, 2069, 2073, 2077, 2081, 2085, 2088, 2092, 2095, 2098, 2101, 2104, 2106, 2108, 2110, 2112, 2114, 2115, 2116, 2116, 2117, 2117, 2117, 2116, 2116, 2115, 2114, 2112, 2110, 2108, 2106, 2104, 2101, 2098, 2095, 2092, 2088, 2085, 2081, 2077, 2073, 2069, 2064, 2060, 2056, 2051, 2047, 2043, 2038, 2034, 2030, 2025, 2021, 2017, 2013, 2009, 2006, 2002, 1999, 1996, 1993, 1990, 1988, 1986, 1984, 1982, 1980, 1979, 1978, 1978, 1977, 1977, 1977, 1978, 1978, 1979, 1980, 1982, 1984, 1986, 1988, 1990, 1993, 1996, 1999, 2002, 2006, 2009, 2013, 2017, 2021, 2025, 2030, 2034, 2038, 2043};

Заменяем массив используя старый код и получаем это:

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC) - 21

Мы получили очень качественную синусоиду! Послушать можно здесь. Скачать проект тут.

Лирическое отступление

Во время записи звука с амплитудой >3 Вольт на колонках, подключенных к той же самой звуковой карте ноутбука, появлялся чуть измененный звук с устройства. По началу я думал, что это из-за включенной фоном программы. Но как только до меня дошло снизить амплитуду — понял, что нет. Так как звук пропал. Подозреваю, что еще чуть-чуть и я бы сжег звуковую карту.

Для записи и анализа звука использовал полностью бесплатную программу Audacity. Она позволяет записывать звук в сколь угодно высоком качестве без ограничений и позволяет сохранять его в любом формате (в том числе и FLAC, в котором я и приводил примеры).

Так же хочу добавить, что у меня нет осциллографа. Эта программа заменила мне его.

Вместо заключения.

В следующей статье будет разобран DMA модуль и его спряжение с DAC. Файлы к урокам.

Автор: Vadimatorikda

Источник


  1. Данила:

    В таблице частот и какие это ноты, у вас написано B – а это СИ БЕМОЛЬ= ЛЯ ДИЕЗ(A#), а должно быть(я так понимаю) просто СИ, а то H. Это все “музыкальная латиница”. исправьте) пожалуйста, а то порядочные музыканты запутаются))

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js