DDS генератор на Arduino

Как работает алгоритм DDS синтеза (Direct Digital Synthesizer). Цифровой генератор сигналов звуковой частоты

Немного про методы синтеза аналоговых сигналов
Цифровые генераторы сигналов
Плюсы и минусы простых DDS генераторов на микроконтроллерах
Как работает АЦП (Аналого-Цифровой Преобразователь)
Цифро-Аналоговый преобразователь типа R2R
Принципиальная схема DDS генератора на Arduino
Как работает простой табличный синтез?
Переходим к DDS алгоритму
Регулировка частоты DDS генератора

Привет друзья. сегодня мы рассмотрим цифровой метод генерации аналоговых сигналов на примере генератора звуковой частоты на основе платы Arduino Uno. Данный генератор спроектирован в основном с целью демонстрации работы принципа работы DDS алгоритма. Прошивка написана исключительно на языке Си для Arduino без ассемблерных вставок, поэтому быстродействие скетча относительно невысокое и не реализует все возможности микроконтроллера Atmegs328.

DDS генератор на Arduino

Тем не менее, даже этот простой генератор создает синусоидальный сигнал в диапазоне частот от нуля до 30 кГц, что перекрывает весь диапазон звуковых частот (более совершенный генератор на контроллере ATMegа с диапазоном до 300 кГц будет описан в следующей статье).

В качестве цифро-аналогового преобразователя используется простая и дешевая схема R2R на резисторах. Регулировка частоты осуществляется переменным резистором, подключенным к АЦП платы Arduino. Нужно иметь в виду, что в этом учебном генераторе качество выходного сигнала очень сильно зависит от точности подбора резисторов самодельного R2R преобразователя. Чем точнее будут номиналы резисторов, тем более «правильная » будет у нас синусоида на выходе.

ARDUINO КАК Работает DDS Генератор? Подробно об алгоритме DDS

Немного про методы синтеза аналоговых сигналов

Генераторы сигналов низкой (звуковой) частоты в радиолюбительской практике необходимы для настройки различных самодельных устройств, таких как усилители мощности звуковой частоты, предварительные усилители, различные каскады, преобразователи и т.д. В электронике существует два основных метода генерации сигналов — аналоговый и цифровой.

В «доцифровую эпоху» практически все самодельные лабораторные генераторы были аналоговыми. Синусоидальные генераторы в основном строились на основе схемы с мостом Вина, либо по классической схеме с фазосдвигающей цепочкой а функциональные генераторы (которые кроме синуса могли выдавать также прямоугольный и треугольный сигнал) — на основе схемы «интегратор — компаратор«. Такая схема отлично производит прямоугольный и треугольный сигнал, но «синусоидальный» сигнал приходилось получать из треугольного с помощью нелинейных элементов (диодов или полевых транзисторов), что не позволяло создавать «синусоиду» с низким уровнем искажений.

В отличие от генератора «интегратор-компаратор», генератор на основе усилителя с мостом Вина в цепи обратной связи, и особенно генераторы с фазосдвигающей цепью позволят сравнительно простыми средствами сделать генератор синусоидального сигнала с малыми искажениями. Такой генератор может потребоваться например для измерения коэффициента гармоник УНЧ.

DDS генератор на Arduino

классическая схема генератора с фазосдвигающей цепочкой

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

Генератор сигналов на биениях НЧ РГ3-124

Промышленный генератор сигналов на биениях НЧ РГ3-124

DDS генератор на Arduino

Платы Arduino купить недорого…

Цифровые генераторы сигналов

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

Промышленные генераторы используют специализированные микросхемы DDS. В качестве примера можно привести популярный чип генератора AD9850. В промышленных условиях создание генератора только на одном микроконтроллере нецелесообразно . Однако это легко можно сделать в любительских условиях и получить очень дешевый и простой функциональный генератор,

Плюсы и минусы простых DDS генераторов на микроконтроллерах

Таким образом, если мы хотим себе высококачественный универсальный цифровой генератор, то лучше купить промышленный прибор, либо сделать генератор с использованием специализированных микросхем. Но если нас устроит простой и дешевый, но довольно универсальный генератор среднего качества, то можно сделать его на основе одного единственного дешевого микроконтроллера AVR. Такой генератор успешно послужит вам в лаборатории для проверки работоспособности и настройки различных радиолюбительских конструкций (в основном в области звуковых и ультразвуковых частот), но он не подойдет для таких точных лабораторных применений как например измерение коэффициента гармоник усилителя или точное измерение фазовых сдвигов.

Как работает АЦП (Аналого-Цифровой Преобразователь)

В общем виде способ оцифровки, обработки/хранения и последующего воспроизведения аналоговых сигналов называется PCM или импульсно — кодовая модуляция. Мы берем какой либо аналоговый сигнал, например наш голос с микрофона. и подаем его на вход специального устройства, которое называется Аналогово Цифровой Преобразователь, АЦП или ADC. На выходе этого преобразователя мы вместо звука имеем последовательность обычных чисел, которые следуют с определенной частотой, которая называется «Частота Дискретизации» или Sampling Rate или Sampling Frequency. Например в стандарте «компакт-диск» или CD-AUDIO принята частота дискретизации, равная 44100 Герц. В данном случае наш АЦП производит замеры уровня сигнала с нашего микрофона 44100 раз в секунду. Числа на выходе АЦП пропорциональны напряжению сигнала. Кроме частоты дискретизации Аналого-цифровой преобразователь имеет также такой параметр как «разрядность» или «разрешение». Этот параметр характеризует точность работы преобразователя. Например если разрядность АЦП равна 8 бит, то максимальному уровню входного напряжения будет соответствовать число равное 2 в степени 8. Получается что если на входе такого АЦП будет напряжение, равное нулю, то АЦП выдаст на выходе число 0. А если на входе будет максимально возможное для данного АЦП напряжение (пусть это будет 5 вольт в нашем примере), то на выходе АЦП выдаст число 255. Таким образом для 8 — разрядного АЦП у нас будет всего 256 различных уровней напряжения. То есть фактически непрерывный аналоговый сигнал на входе такой преобразователь превратит в последовательность ступенек напряжения с шагом 5/256 = 0.01953125В, то есть приблизительно 0.2B.

В стандарте CD-Audio принято разрешение 16 бит. То есть «ступенька» в данном случае будет равна 5/65536 = 0.00007В, и у нас будет уже не 256 а 65536 уровней измерения напряжения.

Цифро-Аналоговый преобразователь типа R2R

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

Однако самый простой цифро-аналоговый преобразователь — это схема R2R. Это параллельный преобразователь цифрового кода в аналоговый сигнал на основе простого резистивного делителя напряжения. Такой преобразователь легко сделать в радиолюбительских условиях, так как он состоит из резисторов одинакового номинала, которые легко приобрести и подобрать. Требуются резисторы всего двух номиналов, которые отличаются ровно в 2 раза. Например 10к (R) и 20к (2R). Мне нравятся номиналы 10 и 20 так как эти величины присутствуют в стандартной сетке номиналов. Можно сделать преобразователь из резисторов одинаковых номиналов, Предположим что у вас есть много резисторов на 10 к. Тогда в качестве резисторов 2R вы можете использовать одиночные резисторы на 10 к., а в качестве резисторов R — по два параллельно соединенных резистора 10к. суммарное сопротивление двух параллельно соединенных резисторов будет как раз 5к, что нам и требуется.

На рисунке ниже изображена схема четырехразрядного R2R преобразователя на резисторах номиналами 10к и 20к. Для такого преобразователя потребуется 4 выходных порта условного микроконтроллера и на выходе мы сможем получить 16 градаций аналогово напряжения.(2 в степени 4).

DDS генератор на Arduino

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

DDS генератор на Arduino

В ‘этом варианте сопротивления R2R у нас будут 5к и 10к.

Рассматривая R2R преобразователь мы допускаем что КМОП ключи на выходах микроконтроллерах идеальны и в замкнутом состоянии имеют нулевое сопротивление. Однако на самом деле ключи не идеальны и сопротивление не нулевое. Небольшое (но не нулевое) сопротивление замкнутого ключа может повлиять на форму выходного сигнала. Для уменьшения влияния сопротивления ключей номиналы резисторов делителя преобразователя должны быть во много раз больше сопротивления кличей контроллера. Для того, чтобы влияние этих паразитных сопротивлений можно было не учитывать, рекомендуется выбирать сопротивление R в 2n (n — разрядность ЦАП) раз больше сопротивления замкнутого ключа. Сопротивление замкнутого ключа можно узнать из документации на используемый контроллер. В любом случае номиналы 1k/2k, 10k/20k или 5k/10k можно считать близким к идеалу.

Таким образом для построения четырехразрядного преобразователя нам потребуется 3 резистора на 10к и 6 резисторов на 20к. Для 8-разрядного преобразователя будет нужно 7 и 9 резисторов соответственно. В схеме нашего генератора мы будем использовать именно 8 разрядный ЦАП.

DDS генератор на Arduino

Платы Arduino купить недорого…

Принципиальная схема DDS генератора на Arduino

Я собрал этот учебный генератор на макетной плате breadbord на основе дешевого китайского клона Arduino Nano. Первый вариант был сделан с платой Arduino Uno, но потом я заменил Uno на Nano просто с целью экономии места. Nano удобна тем, что ее можно вставить непосредственно в Breadboard. Платы Arduino Uno и Nano совместимы между собой и практически отличаются только размерами (есть также небольшие схемные отличия, несущественные для нашего проекта). Так что вы можете использовать либо Nano либо Uno, в зависимости от того что есть в наличии.

Купить плату Arduinj Nano дешево…

DDS генератор на Arduino
DDS генератор на Arduino

Восьмиразрядный R2R ЦАП собран на резисторах R1-R16 номиналами 10 и 20к. R17 и C1 образуют фильтр, устраняющий высокочастотные помехи. Осциллограф подключаем к клеммам X1 и X2. Сюда можно также подключить например звуковой усилитель, чтобы услышать сигнал нашего генератора.

Для того чтобы нагрузка не влияла на работу делителя нашего ЦАП хорошо было бы на выходе использовать буферный повторитель на операционном усилителе. Однако для правильной работы обычного ОУ однополярное напряжение +5В недостаточно. Нужно либо использовать дополнительный источник питания для ОУ, либо использовать специальный Rail-to-Rail операционный усилитель, что выходит за рамки данной статьи.

Кнопка SB1 служит для включения и выключения режима установки частоты генератора. При этом включается или отключается встроенный в плату Arduino светодиод. Кнопка подключена к пину D8 Arduino Nano (порт PB0 микроконтроллера).

Потенциометр R18 подключен к аналоговому входу A0 Ардуино (PC0/ADC0 микроконтроллера). Потенциометр служит для регулировки выходной частоты нашего генератора. Напряжение с движка потенциометра поступает на вход аналого-цифрового преобразователя Ардуино и преобразуется программой в код для установки частоты. такая регулировка потенциометром очень удобна и наглядна, однако не позволяет точно установить частоту. Что собственно и не требуется для нашего учебного проекта.

Генератор DDS с платой Arduino Nano

Генератор DDS с платой Arduino Nano

Как работает простой табличный синтез?

DDS — это англоязычная аббревиатура от Direct digital synthesis, что переводится как «прямой цифровой синтез [аналоговых сигналов]». В теории, DDS является разновидностью табличного метода синтеза (Wave Table synthesis) , но имеет принципиальные отличия в подходе к опросу таблицы значений волны.

для понимания DDS с нуля давайте рассмотрим сначала принцип простого табличного синтеза.

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

DDS генератор на Arduino

Предположим что у нас четырехразрядный ЦАП подключенный к половине выводов порта нашего условного микроконтроллера (как на этом рисунке).

В таком случае минимальному напряжению на выходе ЦАП у нас соответствует число 0, а максимальному — число 15. То есть у нас всего 16 возможных уровней напряжения. Для генерации пилообразного напряжения нам потребуется массив (таблица) следующих значений:

ИндексДвоичное значениедесятичное значение
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
10101010
11101111
12110012
13110113
14111014
15111115

Индекс — это порядковый номер ячейки в нашей таблице. Как видим в случае с пилообразной волной и длиной таблицы в 16 ячеек у нас значение ячейки совпадает с индексом, но мы это не специально:). Например, в случае с синусоидальной волной, значения будут разные. да и длина таблицы не обязательно должна быть 16 ячеек. В общем то она может быть любой. Я взял 16 для простоты. 16 отсчетов у 4-з разрядного ЦАП и 16 ячеек длина таблицы волны.

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


//таблица волны из 16 ячеек
uint8_t wave[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
uint8_t i;  //указатель на ячейку таблицы

void setup{
   // 4 младших разряда порта D  контроллера - на вывод 
   // Это пины D0 - D3 ардуино
    DDRD = B00001111;
    i = 0;
}

void loop() {
    PORTD = wave[i]; //вывод значения в порт ЦАП
    i++; Приращение индекса на единицу
    if (i == 16) { i = 0; } //переход к нулевому элементу если прошли последний
}

Этот маленький скетч на языке Си для Arduino как раз показывает как вывести значение таблицы в порт D контроллера, к четырем выводам которого подключен 4-х разрядный ЦАП. Данная программа не имеет большого практического смысла, потому что 16 уровней напряжения довольно мало для аппроксимации аналогового сигнала. На выходе мы получим примерно вот такое ступенчатое напряжение:

DDS генератор на Arduino

На практике мы будем использовать 8 линий порта D, 8-разрядный ЦАП и таблицу из 256 ячеек. Но об этом чуть позже.

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

DDS генератор на Arduino

Скорость прорисовки формы волны вы можете регулировать движком «частоты дискретизации», также можно выбрать несколько таблиц с разными формами сигналов, такие как синусоида, пила, треугольник и т.д.

Вернемся к нашему примеру для микроконтроллера.
Поскольку в главном цикле нет кода для временной задержки, цикл вывода будет «крутиться» с максимальной частотой, которая возможна для данного контроллера и для данного компилятора языка Си (если бы мы писали на Ассемблере, то все было бы быстрее, но сейчас для нас это не имеет значения). Очевидно, что данная частота будет максимальной для конкретного контроллера/компилятора. Получается что частота полученной синусоиды будет равна этой максимальной частоте поделенной на длину таблицы. Если нам нужен генератор с перестраиваемой частотой, то мы можем изменять тактовую частоту двумя способами: либо вводя в цикл временную задержку, либо запуская вывод значений таблицы не в главном цикле программы, а по прерыванию от встроенного таймера микроконтроллера. Тогда меняя параметры таймера мы сможем оперативно менять частоту сигнала на выходе.

Переходим от простого табличного синтеза к алгоритму DDS

Предположим что у нас есть какой-то условный микроконтроллер, который может выводить нашу условную таблицу синусоидальной волны через ЦАП c условной максимальной частотой 10 килогерц. Если нам нужна меньшая частота, то мы тем или иным способом уменьшаем частоту опроса таблицы и радуемся результату. Но что нам делать, если мы хотим получить от нашего устройства сигнал с частотой 20 килогерц? Наш контроллер ведь не может уже работать быстрее чем он работает! Ответ очевиден: если мы не можем увеличить частоту опроса таблицы, то единственный вариант — это уменьшить длину этой таблицы (помним формулу, что выходная частота равна частоте опроса поделенной на длину таблицы). То есть, если мы будем опрашивать в цикле не каждый элемент таблицы, а каждый второй, то при той же частоте опроса на выходе ЦАП мы получим уже не 10 килогерц нашей условной синусоиды, а требуемые 20.

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

Вот здесь мы и подходим к алгоритму DDS. Этот алгоритм обеспечивает быстрое вычисление пропуска нужного количества ячеек для получения нужной частоты на выходе генератора. Главное его отличие от простого табличного синтеза — это то что DDS генератор всегда работает на строго фиксированной частоте опроса таблицы, а изменение частоты выходного аналогового сигнала осуществляется остроумным быстрым способом с применением двух взаимодействующих переменных — т.н. аккумулятора фазы (Phase Аccumulator) и «величины приращения» (adder), также известной как «Код Частоты».

Поскольку DDS алгоритм работает на строго фиксированной тактовой частоте, а частота синтеза вычисляется математически, он обеспечивает очень точную установку выходной частоты генератора. Некоторые DDS генераторы обеспечивают точность установки частоты в миллионные доли герца.

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

DDS генератор на Arduino

Попробую доступно рассказать как это работает в данном случае. На самом деле существует несколько вариантов реализации генераторов DDS, в том числе и полностью математические, без применения таблиц значений. Мы используем способ с таблицей, так как быстродействие нашего микроконтроллера ограничено, а табличный метод самый быстрый.

Ниже приведен скетч прошивки нашего генератора:

#include "PinChangeInterrupt.h" 
#include <CyberLib.h>
#include <avr/interrupt.h>

#define btnint 8
//------------------------------------------------------------
const uint8_t  sinewave[] =
{
  0x80,0x83,0x86,0x89,0x8c,0x8f,0x92,0x95,0x98,0x9c,0x9f,0xa2,0xa5,0xa8,0xab,0xae,
  0xb0,0xb3,0xb6,0xb9,0xbc,0xbf,0xc1,0xc4,0xc7,0xc9,0xcc,0xce,0xd1,0xd3,0xd5,0xd8,
  0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xed,0xef,0xf0,0xf2,0xf3,0xf5,
  0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfc,0xfd,0xfe,0xfe,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfd,0xfc,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,
  0xf6,0xf5,0xf3,0xf2,0xf0,0xef,0xed,0xec,0xea,0xe8,0xe6,0xe4,0xe2,0xe0,0xde,0xdc,
  0xda,0xd8,0xd5,0xd3,0xd1,0xce,0xcc,0xc9,0xc7,0xc4,0xc1,0xbf,0xbc,0xb9,0xb6,0xb3,
  0xb0,0xae,0xab,0xa8,0xa5,0xa2,0x9f,0x9c,0x98,0x95,0x92,0x8f,0x8c,0x89,0x86,0x83,
  0x80,0x7c,0x79,0x76,0x73,0x70,0x6d,0x6a,0x67,0x63,0x60,0x5d,0x5a,0x57,0x54,0x51,
  0x4f,0x4c,0x49,0x46,0x43,0x40,0x3e,0x3b,0x38,0x36,0x33,0x31,0x2e,0x2c,0x2a,0x27,
  0x25,0x23,0x21,0x1f,0x1d,0x1b,0x19,0x17,0x15,0x13,0x12,0x10,0x0f,0x0d,0x0c,0x0a,
  0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x03,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x02,0x03,0x03,0x04,0x05,0x06,0x07,0x08,
  0x09,0x0a,0x0c,0x0d,0x0f,0x10,0x12,0x13,0x15,0x17,0x19,0x1b,0x1d,0x1f,0x21,0x23,
  0x25,0x27,0x2a,0x2c,0x2e,0x31,0x33,0x36,0x38,0x3b,0x3e,0x40,0x43,0x46,0x49,0x4c,
  0x4f,0x51,0x54,0x57,0x5a,0x5d,0x60,0x63,0x67,0x6a,0x6d,0x70,0x73,0x76,0x79,0x7c
};//------------------------------------------------------------

uint16_t PhaseShift; //код частоты
uint16_t PhaseAccum; //аккумулятор фазы
volatile int state = false; //флаг для переключения режима в прерывании
//------------------------------------------------------------

void ADC_init(){
  //НАСТРОЙКА АЦП
// Сбрасываем регистр ADCSRB
  ADCSRB = 0;             
//Опорное напряжение - ИСТОЧНИК ПИТАНИЯ АРДУИНО:
  bitClear(ADMUX, REFS1);
  bitSet(ADMUX, REFS0);
//формат результата
  bitSet(ADMUX, ADLAR); //8 бит ADCH + 2 бита ADCL
//Выбираем КАНАЛ АЦП = AD0 
  bitClear(ADMUX, MUX3);
  bitClear(ADMUX, MUX2);
  bitClear(ADMUX, MUX1);
  bitClear(ADMUX, MUX0);
//Режим АЦП ВКЛЮЧЕН
  bitSet(ADCSRA, ADEN);
//Автозапуск ВКЛЮЧЕН
  bitSet(ADCSRA, ADATE);
//запрещаем прерывания АЦП 
  bitClear(ADCSRA, ADIE);
//Предделитель на 128
  bitSet(ADCSRA, ADPS2);
  bitSet(ADCSRA, ADPS1); 
  bitSet(ADCSRA, ADPS0); 
//Преобразовение остановлено
  bitClear(ADCSRA, ADSC);
}//------------------------------------------------------------

//Обработка прерывания по завершению преобразования АЦП
ISR(ADC_vect){

  PhaseShift = result; //обновляем код частоты

}//------------------------------------------------------------

void setup() {
// Настройка параметров микроконтроллера
  ADC_init(); // Все настройки АЦП вынесены в отдельную процедуру
  DDRD = B11111111; //порт D (DDS) на выход
  pinMode(btnint, INPUT_PULLUP); //пин кнопки прерывания - на вход
  pinMode(LED_BUILTIN, OUTPUT); //пин встроенного светодиода на вывод

  attachPCINT(digitalPinToPCINT(btnint), btnpressed, FALLING); //подключаем прерывания для кнопки

  PhaseAccum = 0;  //обнуляем аккумулятор фазы
  PhaseShift = 12; //Код частоты в момент запуска генератора

// Цикл вывода таблицы синуса по алгоритму DDS
  Start
    PORTD = sinewave[highByte(PhaseAccum)]; //отправляем значение в порт D
    PhaseAccum = PhaseAccum + (PhaseShift << 4)+1; //Наращиваем аккумулятор фазы
  End
  
}//------------------------------------------------------------

//Прерывание по нажатию на кнопку
void btnpressed(void) {
  state = !state;
  digitalWrite(LED_BUILTIN,state);
  if (state) { 
      bitSet(ADCSRA, ADIE);  
      bitSet(ADCSRA, ADSC);
      }
 
    else  { bitClear(ADCSRA, ADIE);}     
}//------------------------------------------------------------

void loop() { }

Итак, Мы имеем 8-разрядный микроконтроллер (в данном случае ATMega328 в плате Ардуино).

Мы имеем таблицу значений синусоидального сигнала. Это — обычный массив байтов длиной 256 ячеек (в данном случае длина массива имеет значение, она должна быть точно равна «вместимости» байта, то есть 256 значений). Эту таблицу синуса я когда-то давно нашел в интернете.
В нашем скетче таблица объявлена следующим образом:


const uint8_t  sinewave[] =
{
  0x80,0x83,0x86,0x89,0x8c,0x8f,0x92,0x95,0x98,0x9c,0x9f,0xa2,0xa5,0xa8,0xab,0xae,
  0xb0,0xb3,0xb6,0xb9,0xbc,0xbf,0xc1,0xc4,0xc7,0xc9,0xcc,0xce,0xd1,0xd3,0xd5,0xd8,
  0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xed,0xef,0xf0,0xf2,0xf3,0xf5,
  0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfc,0xfd,0xfe,0xfe,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xfe,0xfe,0xfd,0xfc,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,
  0xf6,0xf5,0xf3,0xf2,0xf0,0xef,0xed,0xec,0xea,0xe8,0xe6,0xe4,0xe2,0xe0,0xde,0xdc,
  0xda,0xd8,0xd5,0xd3,0xd1,0xce,0xcc,0xc9,0xc7,0xc4,0xc1,0xbf,0xbc,0xb9,0xb6,0xb3,
  0xb0,0xae,0xab,0xa8,0xa5,0xa2,0x9f,0x9c,0x98,0x95,0x92,0x8f,0x8c,0x89,0x86,0x83,
  0x80,0x7c,0x79,0x76,0x73,0x70,0x6d,0x6a,0x67,0x63,0x60,0x5d,0x5a,0x57,0x54,0x51,
  0x4f,0x4c,0x49,0x46,0x43,0x40,0x3e,0x3b,0x38,0x36,0x33,0x31,0x2e,0x2c,0x2a,0x27,
  0x25,0x23,0x21,0x1f,0x1d,0x1b,0x19,0x17,0x15,0x13,0x12,0x10,0x0f,0x0d,0x0c,0x0a,
  0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x03,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x02,0x03,0x03,0x04,0x05,0x06,0x07,0x08,
  0x09,0x0a,0x0c,0x0d,0x0f,0x10,0x12,0x13,0x15,0x17,0x19,0x1b,0x1d,0x1f,0x21,0x23,
  0x25,0x27,0x2a,0x2c,0x2e,0x31,0x33,0x36,0x38,0x3b,0x3e,0x40,0x43,0x46,0x49,0x4c,
  0x4f,0x51,0x54,0x57,0x5a,0x5d,0x60,0x63,0x67,0x6a,0x6d,0x70,0x73,0x76,0x79,0x7c
};

Напомню, что «ардуиновский» тип данных uint8_t соответствует обычному байту, то есть восьмибитное целое число без знака.

Переменная uint16_t PhaseAccum; — это у нас двухбайтовый аккумулятор фазы. Чем больше разрядность аккумулятора фазы, тем с большей точностью мы можем устанавливать выходную частоту. В данном случае для учебных целей нам достаточно двухбайтового числа «ардуиновского» типа uint16_t, то есть шестнадцать бит целое без знака.

uint16_t PhaseShift — это Adder или величина приращения, которая в каждом цикле выборки из таблицы добавляется к переменной аккумулятора фазы. Фактически это код частоты. Изменяя значение переменной PhaseShift мы можем оперативно менять выходную частоту генератора. В данном скетче мы не утруждаем себя задачей получения точного значения частоты, так как для этого нужно знать фактическую частоту с которой крутится цикл выборки из таблицы. Для программы на языке высокого уровня точно определить эту частоту довольно проблематично.

В следующей статье на тему DDS с AVR контроллерах я расскажу про реализацию алгоритма DDS на ассемблере и там мы уже будем рассчитывать количество циклов контроллера требующиеся для выполнения ассемблерной процедуры и будем задавать частоту с высокой точностью именно в герцах. В данном же скетче на «чистом Си» код частоты — это просто абстрактное число, добавляемое к аккумулятору фазы. Что получится на выходе мы посмотрим опытным путем.

DDS генератор на Arduino

Платы Arduino купить недорого…

Я попытался выжать из «чистого Си» по максимуму. Как видите из скетча, у нас нет вообще никакого кода в стандартной ардуиновской процедуре
void loop() { }
Она просто оставлена пустой. Скетч устроен так, что программа никогда не попадает в процедуру void loop(). Вместо этого я сделал свой бесконечный цикл, который находится в конце процедуры void setup(). Это сделано ради быстродействия. Стандартный ардуиновский бесконечный цикл void loop() { } выполняется сравнительно медленно.

Я использовал стороннюю довольно популярную библиотеку CyberLib. Она подключается в самом начале программы в строке
#include <CyberLib.h>


Эта библиотека реализует быстрый бесконечный цикл, который выглядит так:
Start
//делаем что-то
End


На самом деле, внутри библиотеки CyberLib.h этот цикл реализован с помощью «старого доброго» goto, то есть безусловного перехода к метке в коде. В современном C это считается дурным тоном, но работает гораздо быстрее. Вы можете в этом убедиться, если перенесете код из цикла StartEnd в стандартный ардуиновский цикл void loop() { }. Вы увидите насколько уменьшится выходная частота.

Собственно, весь код, который «делает» вывод сигнала по алгоритму DDS у нас выглядит так:

  Start
    PORTD = sinewave[highByte(PhaseAccum)];
    PhaseAccum = PhaseAccum + (PhaseShift << 4)+1;
  End

Как видим, весь наш DDS уместился в две строки кода.

Первая строка
PORTD = sinewave[ highByte( PhaseAccum ) ];

Здесь производится вывод очередного значения из таблицы синуса в порт D контроллера. Самый важный момент здесь заключается в том, что индексация ячеек таблицы производится только СТАРШИМ БАЙТОМ аккумулятора фазы.

Старший байт двухбайтового числа выделяется функцией highByte().

Вторая строка
PhaseAccum = PhaseAccum + (PhaseShift << 4)+1;

Здесь к значению в аккумуляторе фазы добавляется значение кода частоты из переменной PhaseShift. Не обращайте внимание пока на оператор сдвига влево <<4. Он служит в программе для изменения диапазона регулировки частоты. Это просто аналог умножения числа на 16 (2 в степени 4). Можно было бы написать обычное умножение, но битовый сдвиг работает быстрее.

+1: единица в конце выражения добавляется для того, чтобы значение кода частоты никогда не стало нулевым

Также важный момент: размер таблицы синуса у нас = 256 ячеек, то есть индекс может принимать значения в диапазоне от 0 до 255. Старший байт аккумулятора фазы, который индексирует таблицу синуса тоже может содержать число от 0 до 255. Если к байту, значение которого уже равно 255 прибавить единицу, то произойдет переполнение и значение байта будет 0. То есть 255 + 1 = 0; 255 + 4 = 3 и т.д. С точки зрения математики это бессмысленно, но с точки зрения цифровой техники все правильно. Поэтому что бы мы не прибавляли к двухбайтовому аккумулятору фазы, его старший байт будет всегда указывать на какое-то значение внутри таблицы длиной 256 ячеек.

На этом в данном случае и основана лаконичность алгоритма DDS: нам не нужно дополнительно проверять значение индексной переменной (у нас ее вообще нет) и тратить на это время микроконтроллера. Сам байт делает это за нас, переполняясь автоматически.

Несмотря на то, что индексируем мы таблицу только старшим байтом аккумулятора фазы, но добавляем мы код частоты (в каждом цикле) именно ко ВСЕМУ двухбайтовому числу. Чтобы понять как это работает необходимо посмотреть внимательнее что происходит со значениями старшего и младшего байтов в двухбайтовом числе при добавлении к нему разных значений. Вы можете очень подробно посмотреть как это происходит в написанной мною той самой программе, про которую я рассказывал выше.

Небольшой пример.

Предположим что значение аккумулятора фазы = 0.
старший байт = 0
младший байт = 0
Старший байт указывает на нулевую ячейку в массиве таблицы волны.
————
Предположим что код частоты равен 256. Добавляем к аккумулятору фазы число 256: Получаем 0 + 256 = 256, НО!:
старший байт = 1
младший байт = 0
Старший байт стал указывать на первую ячейку в массиве таблицы волны.

————
следующий цикл:
Добавляем к аккумулятору фазы число 256: Получаем 1 + 256 = 512, НО!:
старший байт = 2
младший байт = 0

Теперь старший байт аккумулятора фазы стал указывать на вторую ячейку в массиве таблицы волны.

Таким образом, в нашем случае, при коде частоты, равном 256 каждый цикл DDS у нас будет индексироваться следующая ячейка таблицы. То есть за 256 циклов в порт ЦАП будут выведены все 256 ячеек таблицы.

Но что, если нам нужно, получить частоту на выходе больше, чем при значении кода частоты 256? Тогда мы просто увеличиваем значение кода частоты. Вы сами легко можете проследить, используя мою программу или даже обычный калькулятор windows, что если мы начнем прибавлять число 512 вместо 256, то в каждом цикле старший байт аккумулятора фазы будет увеличиваться на 2, а не на 1, поэтому будет индексироваться каждая вторая ячейка таблицы, и частота на выходе будет в 2 раза выше. Младший байт аккумулятора при этом всегда будет равен нулю из за переполнения. Он будет равен нулю при всех значениях кода частоты, кратных 256, то есть 256, 512, 1024, 2048 и т.д. Это понятно.

Интересное начинается, когда мы будем добавлять к аккумулятору числа, не кратные 256. давайте попробуем установить код частоты = 400 и посмотрим что будет происходить:

старший байт = 0 (номер ячейки таблицы волны)
младший байт = 0
———————— + 400
старший байт = 1 (номер ячейки таблицы волны)
младший байт = 144
———————— + 400
старший байт = 3 (номер ячейки таблицы волны)
младший байт = 32
———————— + 400
старший байт = 4 (номер ячейки таблицы волны)
младший байт = 176
———————— + 400
старший байт = 6 (номер ячейки таблицы волны)
младший байт = 64
———————— + 400
старший байт = 7 (номер ячейки таблицы волны)
младший байт = 208
———————— + 400
старший байт = 9 (номер ячейки таблицы волны)
младший байт = 96
———————— + 400
старший байт = 10 (номер ячейки таблицы волны)
младший байт = 240
———————— + 400
старший байт = 12 (номер ячейки таблицы волны)
младший байт = 128
———————— + 400
И так далее…

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

Давайте теперь предположим что нам нужно уменьшить частоту на выходе ниже той, которая получается при добавлении числа 256 (когда старший байт аккумулятора адресует каждый цикл каждую ячейку, то есть «один в один»)

мы конечно можем уменьшить тактовую частоту, вводя в цикл задержку, но оказывается нам это не нужно. Алгоритм DDS делает все автоматически.

Нам нужно просто начать прибавлять к аккумулятору число, которое будет меньше чем 256. давайте посмотрим, что будет происходить скажем при значении кода частоты = 100:

старший байт = 0 (номер ячейки таблицы волны)
младший байт = 0
———————— + 100
старший байт = 0 (номер ячейки таблицы волны)
младший байт = 100
———————— + 100
старший байт = 0 (номер ячейки таблицы волны)
младший байт = 200
———————— + 100 (переполнение младшего бита)
старший байт = 1 (номер ячейки таблицы волны)
младший байт = 44
———————— + 100
старший байт = 1 (номер ячейки таблицы волны)
младший байт = 144
———————— + 100
старший байт = 1 (номер ячейки таблицы волны)
младший байт = 244
———————— + 100 (переполнение младшего бита)
старший байт = 2 (номер ячейки таблицы волны)
младший байт = 88
———————— + 100
старший байт = 2 (номер ячейки таблицы волны)
младший байт = 188
———————— + 100 (переполнение младшего бита)
старший байт = 3 (номер ячейки таблицы волны)
младший байт = 32
———————— + 100
старший байт = 3 (номер ячейки таблицы волны)
младший байт = 132
———————— + 100
И так далее…

Видим что при этом указатель ячейки (старший байт аккумулятора фазы) начинает так сказать «топтаться» на некоторых адресах таблицы, выводя одни и те же ячейки таблицы в течение двух а иногда и тех циклов. Это происходит потому, что младший байт начинает переполняться не так часто, и старший байт «ждет» пока переполнится младший. При этом таблица начинает как бы растягиваться, алгоритм вставляет в некоторые циклы дубликаты ячеек таблицы. Это равносильно тому, как если бы длина таблица стала больше. Соответственно уменьшается выходная частота. И это происходит автоматически без добавления каких либо задержек в коде, при этом тактовая частота (частота «кручения» цикла) всегда остается постоянной.

DDS генератор на Arduino

Купить платы Arduino Nano дешево…

Регулировка частоты DDS генератора

Вернемся к схеме генератора:

DDS генератор на Arduino

Для регулировки частоты генератора я использую потенциометр R18 сопротивлением 10к. Нижний (по схеме) вывод потенциометра подключен к минусу питания, верхний (по схеме) вывод — к плюсу питания платы Arduino (+5V). Таким образом, в зависимости от угла поворота, на движке потенциометра у нас будет напряжение в диапазоне от 0 до 5V. Напряжение с движка потенциометра подается на аналоговый вход A0 платы Arduino.

Кроме потенциометра у нас еще есть кнопка SB1, которая служит для перевода генератора в режим установки частоты. Кнопка подключена между пином D8 ардуинки и минусом питания. Пин D8 подтянут к плюсу питания через резистор R19 таким образом, если кнопка не нажата то на D8 всегда подается высокий уровень.

нажатие на кнопку фиксируется по прерыванию на контакте D8 платы Arduino. У вас может возникнуть законный вопрос, откуда в моем скетче появились прерывания на пине D8? действительно, Arduino Nano (как и Uno) по умолчанию предоставляет нам только два внешних прерывания — на пинах D2 и D3, которые в нашей конструкции уже заняты под порт вывода сигнала DDS.

Внешнее перекрывание на пине D8 у нас появилось благодаря использованию прекрасной внешней библиотеки PinChangeInterrupt, которая позволяет реализовать внешние прерывания на нескольких дополнительных выводах контроллера. Библиотека подключается в начале скетча:
include «PinChangeInterrupt.h»

После подачи питания сначала устанавливаются необходимые параметры работы АЦП платы Arduino. Все что касается установок АЦП вынесено в отдельную фкнкцию void ADC_init():

void ADC_init(){
  //НАСТРОЙКА АЦП
// Сбрасываем регистр ADCSRB
  ADCSRB = 0;             
//Опорное напряжение - ИСТОЧНИК ПИТАНИЯ АРДУИНО:
  bitClear(ADMUX, REFS1);
  bitSet(ADMUX, REFS0);
//формат результата
  bitSet(ADMUX, ADLAR); //8 бит ADCH + 2 бита ADCL
//Выбираем КАНАЛ АЦП = AD0 
  bitClear(ADMUX, MUX3);
  bitClear(ADMUX, MUX2);
  bitClear(ADMUX, MUX1);
  bitClear(ADMUX, MUX0);
//Режим АЦП ВКЛЮЧЕН
  bitSet(ADCSRA, ADEN);
//Автозапуск ВКЛЮЧЕН
  bitSet(ADCSRA, ADATE);
//запрещаем прерывания АЦП 
  bitClear(ADCSRA, ADIE);
//Предделитель на 128
  bitSet(ADCSRA, ADPS2);
  bitSet(ADCSRA, ADPS1); 
  bitSet(ADCSRA, ADPS0); 
//Преобразовение остановлено
  bitClear(ADCSRA, ADSC);
}

Функция void ADC_init() вызывается из функции void setup() один раз в самом начале работы программы.

void setup() {
// Настройка параметров микроконтроллера
  ADC_init(); // Все настройки АЦП вынесены в отдельную процедуру
  DDRD = B11111111; //порт D (DDS) на выход
  pinMode(btnint, INPUT_PULLUP); //пин кнопки прерывания - на вход
  pinMode(LED_BUILTIN, OUTPUT); //пин встроенного светодиода на вывод

 //подключаем прерывания для кнопки:
  attachPCINT(digitalPinToPCINT(btnint), btnpressed, FALLING); 

  PhaseAccum = 0;  //обнуляем аккумулятор фазы
  PhaseShift = 12; //Код частоты в момент запуска генератора

// Цикл вывода таблицы синуса по алгоритму DDS
  Start
    PORTD = sinewave[highByte(PhaseAccum)]; //отправляем значение в порт D
    PhaseAccum = PhaseAccum + (PhaseShift << 4)+1; //Наращиваем аккумулятор фазы
  End

Далее мы объявляем все пины порта D микроконтроллера как выходы. Это будет наш порт для вывода сигнала DDS:
DDRD = B11111111;

Объявляем пин, к которому подключена кнопка как вход. Это сделано просто для наглядности, так как по умолчанию после включения питания все пины ардуинки настроены как входа. Эту строку можно удалить.
pinMode(btnint, INPUT_PULLUP);

pinMode(LED_BUILTIN, OUTPUT); //пин встроенного светодиода объявляем как выход.

В следующей строке мы подключаем прерывания от кнопки на пине D8. Прерывание создается с помощью сторонtq библиотеки PinChangeInterrupt о которой я уже упоминал:
attachPCINT(digitalPinToPCINT(btnint), btnpressed, FALLING);
Псевдоним для вывода платы D8 объявлен в самом начале скетча:
#define btnint 8

Далее мы обнуляем переменную аккумулятора фазы: PhaseAccum = 0;
Задаем код частоты генератора по умолчанию: PhaseShift = 12;

После этого программа входит в бесконечный цикл генерации сигнала DDS, работу которой мы уже рассматривали ранее:
Start
PORTD = sinewave[highByte(PhaseAccum)];
PhaseAccum = PhaseAccum + (PhaseShift << 4)+1;
End

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

При нажатии на кнопку сработает прерывание и будет вызвана функция void btnpressed(void):

//Прерывание по нажатию на кнопку
void btnpressed(void) {
  state = !state;
  digitalWrite(LED_BUILTIN,state);
  if (state) { 
      bitSet(ADCSRA, ADIE);  
      bitSet(ADCSRA, ADSC);
      }
 
    else  { bitClear(ADCSRA, ADIE);}     

В этой функции содержимое переменной state меняется на противоположное и в зависимости от состояния переменной включаются или выключаются прерывания от встроенного АЦП контроллера. Переменная state объявлена в начале скетча с модификатором volatile. Это требование библиотеки PinChangeInterrupt:
volatile int state = false; //флаг для переключения режима в прерывании

Если на выходе из функции void btnpressed(void) у нас оказались включены прерывания от АЦП, то теперь мы можем менять частоту генератора, поворачивая ручку потенциометра. Следующее нажатие на кнопку отключит режим установки частоты.

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

Итак, в режиме установки частоты у нас будут периодически происходить прерывания от АЦП. То есть, АЦП будет постоянно изменять напряжение на движке потенциометра. Но эти измерения не происходят мгновенно. Процесс измерения занимает некоторое время, которое задается настройками регистров АЦП в начале программы. Прерывание возникает в тот момент, когда завершается процесс преобразования и мы можем получить достоверное значение с нашего АЦП. Функция обработчика прерывания просто присваивает переменной кода частоты результат преобразования АЦП. Все это будет повторяться, пока включены прерывания от АЦП, то есть пока мы снова не нажмем на кнопку SB1.

Данный проект прекрасно симулируется в Proteus 8:

DDS генератор на Arduino

Проект для Proteus находится в том же архиве, что и код программы.

Все вопросы и отзывы по этому проекту вы можете оставлять в комментариях к статье. Ниже вы найдете ссылки на все файлы проекта и на дополнительные библиотеки.

Плата Arduino Nano
Плата Arduino Uno
Потенциометры
Bredboard — «беспаечная» макетная плата
Архив файлов проекта. В архиве скетч программы и проект для симуляции в Proteus8
Библиотека PinChangeInterrupt
Библиотека CyberLib
Моя программа симуляции табличного синтеза
Моя программа симуляции DDS генератора
Arduino IDE

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

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