DDS генератор на ATMega8 с управлением от компьютера


Функциональный цифровой генератор сигналов на микроконтроллере Atmega8

DDS генератор на ATMega8 с управлением от компьютера

Продолжаем тему DDS генераторов на микроконтроллерах. В предыдущей статье мы рассмотрели принцип работы алгоритма DDS синтезатора аналоговых сигналов. также мы сделали простой DDS генератор синусоидальных сигналов на базе платы Arduino Nano. Сегодня попробуем сделать более продвинутый генератор на том же прицепе.

Я не буду повторяться и подробно описывать принцип работы DDS генератора. Если вы еще не читали первую большую статью на эту тему, то ознакомиться с ней вы сможете по этой ссылке. Наш первый учебный генератор работал на базе платы Arduino. Сегодня же мы сделаем устройство на «независимом» дешевом микроконтроллере ATMega8.

Основные отличия генератора от Arduino версии из предыдущей статьи

Несмотря на то, что контроллер ATMega8 уступает по своим характеристикам ATMega328 платы Arduino Nano, наш новый генератор будет превосходить предыдущий по возможностям.

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

Кроме того, наш генератор будет производить сигналы разной формы. Это синус, восходящая и нисходящая пила, треугольник и прямоугольный сигнал. Кроме этого можно загрузить пять дополнительных «пользовательских» форм сигнала.

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

DDS генератор на ATMega8 с управлением от компьютера

Также как и управляющая программа, приложение написано в бесплатной среде Delphi 11 community edition. Исходники программы и исполняемый файл вы найдете в архиве проекта в конце статьи.

Таблицы волновых форм хранятся во Flash памяти микроконтроллера и если использовать контроллер с большим объемом флэш памяти, то можно разместить в нем большое количество таблиц волновых форм.

Делаем Крутой Генератор на ATmega8 с управлением от компьютера. C+ Assembler | Не Arduino

Посмотреть видео про этот генератор

Общие сведения о конструкции и программах

Конструкция генератора максимально проста. В нем нет ни дисплея, ни кнопок управления. генератором мы будем управлять о компьютера через последовательный порт. Для этого я написал небольшую программу на языке Depphi (я использую бесплатную dерсию Delphi Community Edition). Такое решение не очень удобно, зато быстро и дешево (при условии, что у вас есть компьютер и переходник USB-UART)

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

Для создания прошивки я использовал очень удобный компилятор MikroC Pro for AVR. Это проприетарный IDE и компилятор языка С сербской компании Mikroelectronica. В отличие от GCC, который используется в оболочке Arduino, на MikroC крайне удобно делать ассемблерные вставки. Из подпрограммы на ассемблере можно напрямую использовать переменные объявленные в коде на C без всяких заморочек с символами типа «;;…#&». Кто когда либо делал ассемблерные вставки в Ардуино, тот знает насколько это неудобно. В то время как в MikroC это делается просто, понятно и код остается легко читаемым. Нужно добавить, что у Mikroelectronica есть подобные компиляторы для контроллеров с ARM ядром и для PIC-контроллеров.

Печатная плата генератора

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

DDS генератор на ATMega8 с управлением от компьютера

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

DDS генератор на ATMega8 с управлением от компьютера

Печатная плата генератора. 3D вид

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

Принципиальная схема и прошивка генератора

DDS генератор на ATMega8 с управлением от компьютера

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

Кварцевый резонатор используем на 16 МГц. все конденсаторы кроме C5 — миниатюрные керамические. C5 — обычный электролит на 10 — 47 мкФ 16В. Микроконтроллер необходимо установить на панельку. Если вы хотите программировать микроконтроллер непосредственно в схеме генератора, не извлекая его из панельки, придётся установить на плату дополнительный разъем, который нужно соединить с соответствующими пинами контроллера. разъем не показан на схеме. Предполагается что вы запрограммировали контроллер в отдельном программаторе и потом установили его на панельку в схему генератора. если у меня будет время, я сделаю версию печатной платы со встроенным разъемом для внутрисхемного программирования контроллера.

Параллельный R2R цифроаналоговый преобразователь на резисторах R1 — R19 подключен к выводам порта D микроконтроллера.

Преобразователь USB-UART

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

При заказе переходника не путайте его с адаптером USB-RS232. Нужен именно переходник на уровни напряжения TTL (UART). Обычно такой простой USB-UART стоит примерно в 2 раза дешевле чем USB-RS232. для управления генератором нам нужны именно простые TTL уровни, а не полноценный RS232.

Адаптер USB-UART выглядит как обычная «флэшка», которая подключается к USB порту компьютера:

DDS генератор на ATMega8 с управлением от компьютера

Преобразователь интерфейсов USB на UART (TTL)

Резисторы R2R ЦАП

Для цифроаналогового преобразователя желательно подобрать номиналы резисторов как можно точнее. От этого зависит качество формы выходного сигнала. В данном случае у нас используются резисторы номиналов 20 и 10 килоом. Можно использовать и другие номиналы, главное чтобы они отличались ровно в 2 раза. Например 2 и 1 кОм, 24 и 12 кОм и т.д. Не стоит применять резисторы слишком низкого сопротивления, например 200 и 100 Ом. дело в том, что для качественной работы R2R схемы сопротивление резисторов должно быть во много раз больше внутреннего сопротивления транзисторного ключа внутри микроконтроллера. С другой стороны, слишком высокое сопротивление резисторов ухудшит нагрузочную способность генератора и сделает сигнал подверженным помехам. оптимальное сопротивление лежит в диапазоне примерно от 1 до 30 килоом.

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

Питание генератора

Генератор можно питать от USB порта компьютера через тот же самый USB-UART переходник. Во всех таких переходниках есть выводы напряжения 5 V. Однако омпьютерный блок питания может создавать значительные импульсные помехи, которые неизбежно проникнут в выходной сигнал генератора. Кроме того, в случае использования внутрисхемного программирования контроллера категорически не рекомендуется пытаться питать генератор от USB-UART адаптера. При подключении программатора, подключаемого к тому же самому компьютеру, может возникнуть петля по плюсу питания, что в худшем случае повредит USB порты вашего компьютера.

Лучше всего использовать для питания отдельный независимый источник. Это может быть адаптер на напряжение 5 вольт (как тот, что используется для зарядки смартфона), либо источник с напряжением от 7 до 16 вольт.

Идеальный вариант — это аккумуляторная батарея напряжением 9 вольт, либо две ячейки Li-Ion аккумуляторов с общим напряжением в районе 7.4V. При этом мы подключаем USB-UART адаптер к генератору только по 3 проводам: TX, RX и Земля. Вывод адаптера +5V оставляем не подключенным.

Для питания от внешнего источника на схеме есть стабилизатор на микросхеме TL78L05. на вход такого стабилизатора мы подаем напряжение в диапазоне 7 — 16 вольт, а на выходе имеем стабильное напряжение 5 Вольт. Если вы планируете использовать адаптер с выходным напряжением 5 вольт, то стабилизатор можно не устанавливать на плату. Вместо него нужно будет установить проволочную перемычку, соединяющую вход и выход микросхемы.

DDS генератор на ATMega8 с управлением от компьютера

Установка перемычки вместо 78L05 при питании от адаптера с напряжением ровно 5V

Можно попробовать запитать генератор от одного Li-ion аккумулятора напряжением 3.7В также исключив стабилизатор напряжения. В этом случае быстродействие и стабильность работы контроллера может ухудшиться, также уменьшится уровень выходного сигнала.

Прошивка микроконтроллера на языке C

Ниже приведен полный текст программы для микроконтроллера ATMega8 для компилятора MikroC Pro for AVR:

//фюзы в авр студио для мега 16
//HIGH = 0xD9
//LOW  = 0xFF

void SignalOut(void);
void receiveData (void);


const char SineTbl[256]   = {
  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
} absolute  0x100 ;

const char SawTbl[256]   = {
  0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
  0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,
  0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,
  0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x3b,0x3c,0x3d,0x3e,0x3f,
  0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,
  0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0x5b,0x5c,0x5d,0x5e,0x5f,
  0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,
  0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x7b,0x7c,0x7d,0x7e,0x7f,
  0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
  0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
  0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xab,0xac,0xad,0xae,0xaf,
  0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0xbe,0xbf,
  0xc0,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xcb,0xcc,0xcd,0xce,0xcf,
  0xd0,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xdb,0xdc,0xdd,0xde,0xdf,
  0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xeb,0xec,0xed,0xee,0xef,
  0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
} absolute  0x200 ;

const char RevSawTbl[256]   = {
  0xff,0xfe,0xfd,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,0xf6,0xf5,0xf4,0xf3,0xf2,0xf1,0xf0,
  0xef,0xee,0xed,0xec,0xeb,0xea,0xe9,0xe8,0xe7,0xe6,0xe5,0xe4,0xe3,0xe2,0xe1,0xe0,
  0xdf,0xde,0xdd,0xdc,0xdb,0xda,0xd9,0xd8,0xd7,0xd6,0xd5,0xd4,0xd3,0xd2,0xd1,0xd0,
  0xcf,0xce,0xcd,0xcc,0xcb,0xca,0xc9,0xc8,0xc7,0xc6,0xc5,0xc4,0xc3,0xc2,0xc1,0xc0,
  0xbf,0xbe,0xbd,0xbc,0xbb,0xba,0xb9,0xb8,0xb7,0xb6,0xb5,0xb4,0xb3,0xb2,0xb1,0xb0,
  0xaf,0xae,0xad,0xac,0xab,0xaa,0xa9,0xa8,0xa7,0xa6,0xa5,0xa4,0xa3,0xa2,0xa1,0xa0,
  0x9f,0x9e,0x9d,0x9c,0x9b,0x9a,0x99,0x98,0x97,0x96,0x95,0x94,0x93,0x92,0x91,0x90,
  0x8f,0x8e,0x8d,0x8c,0x8b,0x8a,0x89,0x88,0x87,0x86,0x85,0x84,0x83,0x82,0x81,0x80,
  0x7f,0x7e,0x7d,0x7c,0x7b,0x7a,0x79,0x78,0x77,0x76,0x75,0x74,0x73,0x72,0x71,0x70,
  0x6f,0x6e,0x6d,0x6c,0x6b,0x6a,0x69,0x68,0x67,0x66,0x65,0x64,0x63,0x62,0x61,0x60,
  0x5f,0x5e,0x5d,0x5c,0x5b,0x5a,0x59,0x58,0x57,0x56,0x55,0x54,0x53,0x52,0x51,0x50,
  0x4f,0x4e,0x4d,0x4c,0x4b,0x4a,0x49,0x48,0x47,0x46,0x45,0x44,0x43,0x42,0x41,0x40,
  0x3f,0x3e,0x3d,0x3c,0x3b,0x3a,0x39,0x38,0x37,0x36,0x35,0x34,0x33,0x32,0x31,0x30,
  0x2f,0x2e,0x2d,0x2c,0x2b,0x2a,0x29,0x28,0x27,0x26,0x25,0x24,0x23,0x22,0x21,0x20,
  0x1f,0x1e,0x1d,0x1c,0x1b,0x1a,0x19,0x18,0x17,0x16,0x15,0x14,0x13,0x12,0x11,0x10,
  0x0f,0x0e,0x0d,0x0c,0x0b,0x0a,0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00
} absolute  0x300 ;


const char TriangleTbl[256]   = {
  0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
  0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
  0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
  0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
  0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
  0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
  0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
  0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
  0xff,0xfd,0xfb,0xf9,0xf7,0xf5,0xf3,0xf1,0xef,0xed,0xeb,0xe9,0xe7,0xe5,0xe3,0xe1,
  0xdf,0xdd,0xdb,0xd9,0xd7,0xd5,0xd3,0xd1,0xcf,0xcd,0xcb,0xc9,0xc7,0xc5,0xc3,0xc1,
  0xbf,0xbd,0xbb,0xb9,0xb7,0xb5,0xb3,0xb1,0xaf,0xad,0xab,0xa9,0xa7,0xa5,0xa3,0xa1,
  0x9f,0x9d,0x9b,0x99,0x97,0x95,0x93,0x91,0x8f,0x8d,0x8b,0x89,0x87,0x85,0x83,0x81,
  0x7f,0x7d,0x7b,0x79,0x77,0x75,0x73,0x71,0x6f,0x6d,0x6b,0x69,0x67,0x65,0x63,0x61,
  0x5f,0x5d,0x5b,0x59,0x57,0x55,0x53,0x51,0x4f,0x4d,0x4b,0x49,0x47,0x45,0x43,0x41,
  0x3f,0x3d,0x3b,0x39,0x37,0x35,0x33,0x31,0x2f,0x2d,0x2b,0x29,0x27,0x25,0x23,0x21,
  0x1f,0x1d,0x1b,0x19,0x17,0x15,0x13,0x11,0x0f,0x0d,0x0b,0x09,0x07,0x05,0x03,0x01
} absolute  0x400 ;

const char SqrTbl[256]   = {
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
} absolute  0x500 ;

const char user1[256]   = {
 113,114,115,117,118,121,122,122,125,126,128,129,131,132,133,136,
137,138,140,141,143,144,145,147,148,150,150,152,153,153,155,155,
156,156,157,158,158,159,159,159,160,160,161,161,161,161,161,162,
162,163,163,167,170,173,174,175,175,176,176,175,174,174,172,163,
163,163,162,162,162,162,162,161,160,160,159,158,158,157,157,155,
155,154,153,152,151,150,147,147,146,144,143,141,140,140,137,135,
133,133,132,130,128,126,125,124,123,121,120,119,118,118,118,117,
117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,
117,117,117,117,117,117,117,117,119,121,122,126,127,132,133,137,
138,140,143,144,148,148,150,154,154,156,158,159,160,161,162,163,
163,164,164,165,166,166,167,168,168,169,169,170,170,172,175,178,
178,179,179,179,179,179,179,179,177,176,175,171,171,170,170,170,
170,168,168,167,167,166,165,164,164,162,161,160,159,158,157,156,
154,154,152,150,149,148,146,145,144,141,139,137,134,134,132,130,
129,128,127,127,126,126,124,124,123,122,122,122,121,120,120,120,
119,119,119,119,119,119,119,119,119,119,119,119,119,119,119,119

} absolute  0x600 ;

const char user2[256]   = {
121,121,123,124,125,126,127,128,129,130,131,132,132,133,133,134,
134,135,135,135,136,136,137,137,138,138,139,139,140,140,140,140,
141,141,145,150,153,163,200,207,210,213,221,225,225,224,223,219,
218,216,212,211,209,206,204,201,200,198,194,192,191,187,185,184,
184,179,179,176,174,172,168,167,164,162,155,155,154,153,153,153,
154,155,155,156,156,156,156,157,157,157,157,157,157,157,158,158,
158,158,158,158,158,158,158,158,158,158,158,158,158,158,158,158,
158,158,158,158,158,158,158,158,158,158,158,158,158,158,158,157,
157,157,157,157,157,157,157,157,157,157,157,156,156,155,154,154,
154,154,153,153,152,155,157,158,163,165,168,172,174,178,180,181,
189,195,197,199,207,210,211,217,223,225,229,234,237,237,238,233,
225,203,195,183,179,175,171,164,156,154,151,149,149,147,147,146,
145,145,144,143,143,143,141,141,141,140,140,140,140,138,138,137,
137,136,136,136,134,134,133,132,132,132,130,130,130,129,129,127,
127,126,125,124,123,122,121,120,119,117,117,115,114,114,112,112,
110,109,109,108,106,105,105,104,103,101,99,99,96,91,81,0

} absolute  0x700 ;

const char user3[256]   = {
  131,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,
132,131,131,131,131,131,131,131,131,131,150,172,172,173,173,173,
173,174,174,174,174,174,174,175,175,175,175,175,175,175,175,175,
175,175,175,175,175,175,175,175,175,175,175,175,175,175,185,192,
197,210,218,230,230,231,231,231,232,232,233,233,233,234,234,234,
234,234,234,234,234,234,233,233,233,233,232,232,232,188,186,186,
183,183,181,181,181,181,181,181,181,181,181,181,181,181,181,181,
181,181,181,181,181,161,160,159,157,156,155,154,151,149,145,144,
144,144,144,144,144,144,144,144,144,144,144,144,144,144,144,144,
144,144,144,144,145,145,145,145,145,145,120,120,120,120,120,166,
166,167,167,167,167,167,168,168,169,169,169,169,169,169,169,169,
169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,
169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,
169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,169,
169,169,169,155,153,148,145,145,145,145,144,144,144,144,144,144,
144,144,144,112,112,112,112,112,112,112,112,112,112,112,112,112

} absolute  0x800 ;

const char user4[256]   = {
  112,113,113,114,114,115,115,116,116,117,192,196,199,203,205,207,
212,213,215,217,219,220,221,222,224,225,225,226,226,226,227,227,
227,227,227,227,161,162,164,165,166,168,169,170,171,171,172,173,
174,174,175,176,176,177,178,178,179,180,180,181,181,181,182,182,
182,182,182,183,183,183,183,183,183,183,183,183,183,183,183,182,
182,180,179,177,175,172,170,168,164,160,157,153,148,142,139,136,
132,128,125,125,125,126,225,225,225,225,226,226,226,226,227,227,
227,227,228,228,228,228,229,229,229,183,183,184,205,206,206,206,
206,205,205,204,203,203,202,201,200,200,199,198,197,196,195,194,
192,191,190,189,188,187,186,184,180,180,178,176,176,175,174,174,
174,174,174,174,160,159,157,155,153,151,149,147,145,143,139,136,
133,131,129,128,124,122,120,116,110,104,102,98,93,91,90,87,
84,82,80,80,79,78,78,78,81,82,82,85,85,86,83,83,
82,80,81,83,91,94,94,95,93,90,87,85,84,82,81,81,
81,82,82,82,83,83,83,84,85,85,86,86,84,83,80,76,
74,68,64,59,107,106,105,105,104,103,103,102,101,101,100,99

} absolute  0x900 ;

const char user5[256]   = {
  182,181,180,180,181,182,182,182,182,183,184,186,187,188,188,188,
187,187,188,190,193,194,196,198,199,200,200,201,201,201,200,200,
200,200,200,200,201,202,204,205,206,206,203,194,178,154,123,86,
48,17,0,1,21,57,101,146,187,218,238,250,255,254,249,242,
235,227,219,210,203,198,194,191,190,189,188,187,187,186,185,184,
184,184,184,184,184,184,184,183,183,183,182,182,181,180,180,179,
178,177,176,175,174,173,172,171,169,167,164,162,159,157,155,153,
151,148,146,143,140,137,134,132,130,129,128,128,128,128,128,129,
130,131,134,136,139,142,146,150,153,157,160,163,166,168,171,174,
176,178,179,180,181,182,183,185,186,187,188,188,188,187,187,187,
186,186,186,186,186,186,186,185,184,183,182,182,181,181,180,180,
180,180,180,180,181,181,182,182,182,182,183,183,183,184,184,184,
184,184,184,184,185,185,185,186,186,186,186,186,185,185,185,186,
187,187,188,188,188,188,189,189,189,190,190,190,190,190,190,190,
190,191,191,192,192,191,191,190,190,190,190,190,190,190,191,191,
191,191,191,191,191,191,190,190,190,189,188,187,186,184,183,182

} absolute  0xA00 ;



// Константа temp - это постоянная величина, используемая для пересчета
// частоты генерации в код частоты для функции вывода сиграла
// (Частота Кварца  / к-во циклов вывода сэмпла )/2^24(24 - разрядность
// аккумулятора фазы)
// В нашем случае: коэффициент = 16'000'000 /10)16777216
  const float temp =  0.095367431640625;
  unsigned long DDSFreq; //Код установки частоты
  float freq; //Частота (здесь максимум 300000 минимум - 0.1 Гц)
  //  unsigned long UartFreqency;
  unsigned ddsaddr;
  sbit stopdds at PINB.B0;
  //sbit led at PINB.B3;
  char i;
  char error;
  char byte_freq_1;//младший байт частоты
  char byte_freq_2;
  char byte_freq_3;
  char byte_freq_4;//старший байт частоты
  char byte_wave;//код формы волны
  char byte_start;//код старт/стоп (1/0)
  char read_ok;

#include <built_in.h>


void SignalOut(){
     DDSFreq = freq / temp; //вычислим код частоты
     r24 = Lo(DDSFreq);
     r25 = Hi(DDSFreq);
     r26 = Higher(DDSFreq);

     r31  = Hi(ddsaddr);//адрес таблицы в рабочие регистры
     r30  = Lo(ddsaddr);
     
     r29 = 0;
     r28 = 0;
 asm{
     //SEI
     LOOP1:
           add      r28,r24      ; 1
           adc      r29,r25      ; 1
           adc      r30,r26      ; 1
           lpm                   ; 3
           out      PORTD,r0     ; 1
           sbic     PINB, 0       ; 1
           rjmp     LOOP1        ; 2=>10
  }
  PORTD = 0;
  do {} while (stopdds==0);
      
} //-----------------------------------------------------

void receiveData (void) {
  char tmp;
  char *pbyte;
  unsigned long f;
  
       if (byte_start == 0){tmp = Soft_UART_Read(&error);}
       byte_start  = Soft_UART_Read(&error);
       byte_wave   = Soft_UART_Read(&error);
       byte_freq_1 = Soft_UART_Read(&error);
       byte_freq_2 = Soft_UART_Read(&error);
       byte_freq_3 = Soft_UART_Read(&error);
       byte_freq_4 = Soft_UART_Read(&error);
       pbyte = &f;
       *pbyte = byte_freq_1;
        *(pbyte+1) = byte_freq_2;
          *(pbyte+2) = byte_freq_3;
            *(pbyte+3) = byte_freq_4;
       freq = f;
}
//-----------------------------------------------------


void main() {

     asm {CLI}; //глобальное отключение прерываний
     ACD_bit = 1;         //Компаратор выключен
     DDRD = 0xFF;         // Весь Порт D на вывод
     DDB0_bit = 0;        // Set PORTB pin 0 as input
     DDB1_bit = 1;        // Set PORTB pin 1 as input
     DDB2_bit = 1;        // Set PORTB pin 2 as Outout
     DDB3_bit = 1;        // Set PORTB pin 3 as Outout

     byte_start = 0; //по умолчанию DDS отключен
     error = Soft_UART_Init(&PORTB,1,2,14400,0);
     Delay_ms(100);
     ddsaddr = &SineTbl;
     freq = 440;

  do {
  
     if (byte_start == 1) {SignalOut();}
     delay_ms(100);
     
      receiveData();
      switch (byte_wave) {
        case 0 : ddsaddr = &SineTbl; break;
        case 1 : ddsaddr = &TriangleTbl; break;
        case 2 : ddsaddr = &SawTbl; break;
        case 3 : ddsaddr = &RevSawTbl; break;
        case 4 : ddsaddr = &SqrTbl; break;
        case 5 : ddsaddr = &user1; break;
        case 6 : ddsaddr = &user2; break;
        case 7 : ddsaddr = &user3; break;
        case 8 : ddsaddr = &user4; break;
        case 9 : ddsaddr = &user5; break;
        
        default: ddsaddr = &SineTbl;

      }

  
  } while(1);

}

Таблицы волновых форм

Волновые таблицы хранятся в памяти микроконтроллера как константы — массивы чисел типа char. Каждый массив имеет размер 256 элементов.
Объявление массивов выглядит так:

const char SineTbl[256] = { 0x80, 0x83, 0x86, 0x89, 0x8c, и т.д……} absolute 0x100 ;

Константы — массивы располагаются в памяти программ (Flash) микроконтроллера. Алгоритм вывода сигнала устроен так, что для его корректной работы необходимо чтобы таблицы были расположены во флэш — памяти строго определенным образом. А именно, первый элемент каждой таблицы должен располагаться точно в начале 256-байтного блока ячеек памяти. Для указания компилятору точного абсолютного адреса расположения массива и служит слово absolute в конце объявления таблицы.

Дело в том, что алгоритм DDS в данном случае никак не проверяет номер текущего элемента в таблице. Это сделано для быстродействия. Циклический переход от последнего к первому элементу таблицы осуществляется просто за счет «естественного» переполнения старшего байта аккумулятора фазы. Как это все работает — читайте в первой статье по теме DDS.

Функция вывода сигнала на ассемблере

Следующий отрывок текста программы представляет собственно алгоритм DDS синтеза сигнала. давайте разберем его более подробно.

void SignalOut(){

     DDSFreq = freq / temp; //вычислим код частоты
     r24 = Lo(DDSFreq);
     r25 = Hi(DDSFreq);
     r26 = Higher(DDSFreq);
//адрес таблицы в рабочие регистры
    r31  = Hi(ddsaddr);
    r30  = Lo(ddsaddr); //Старший байт аккумулятора фазы
    // очистка двух младших байтов аккумулятора
    r29 = 0;
    r28 = 0;

 asm{
     LOOP1:
           add      r28,r24      ; 1
           adc      r29,r25      ; 1
           adc      r30,r26      ; 1
           lpm                   ; 3
           out      PORTD,r0     ; 1
           sbic     PINB, 0       ; 1
           rjmp     LOOP1        ; 2=>10
  }
  PORTD = 0;
  do {} while (stopdds==0);
      
} //-----------------------------------------------------

void SignalOut() — это объявление функции вывода сигнала

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

DDSFreq = freq / temp; В этой строке вычисляется код частоты в зависимости от того, что содержится в переменной freq. Эта переменная содержит именно частоту в герцах. То есть, если перед входом в функцию мы присвоим переменной значение 100, то генератор должен выдать нам сигнал частотой 100 герц. Собственно, код частоты, или переменная DDSFreq — это как раз то, что в цикле добавляется к переменной — аккумулятору фазы. Константа freq — это коэффициент для вычисления кода частоты. Значение константы мы задаем вручную во время объявления константы temp:

const float temp = 0.095367431640625;

это число получено по следующей формуле:
частота кварца (16000000) / число циклов контроллера для вывода сэмпла (10) / 16777216, где 16777216 — это двойка в степени 24, а 24 — это разрядность аккумулятора фазы (3 байта).

Внимательный читатель мог заметить, что в программе у нас нет «сишной» переменной, которая представляла бы аккумулятор фазы. Роль аккумулятора фазы у нас играют три регистра общего назначения микроконтроллера ATMega, а именно r28, r29 и r30. использование внутренних регистров контроллера вместо хранения значений в оперативной памяти обеспечивает нам максимальное возможное быстродействие.

Вся прелесть компилятора MikroC Pro именно в том, что в компиляторе уже «зашиты», объявлены внутренние регистры микроконтроллера. мы можем совершенно спокойно обращаться к ним как к обычным переменным ПРЯМО из кода на Си. Например мы можем написать в сишном коде r30 = 100 и в регистр контроллера r30 будет загружено число 100. Потом, в ассемблерной вставке, мы можем делать с этим значением все что угодно, но уже из кода на ассемблере. Это очень удобно по сравнению с тем, как это реализовано в компиляторе GCC для Ардуино.

В следующих трех ассемблерных строках мы раскладываем код переменную DDSFreq (код частоты) на отдельные байты и присваиваем значение этих байтов напрямую регистрам общего назначения r24r, 25 и r24 микроконтроллера

     r24 = Lo(DDSFreq);
     r25 = Hi(DDSFreq);
     r26 = Higher(DDSFreq);

Для выделения отдельных байтов числа в MikroC есть удобный функции Lo(), Hi(), Higher() и Highest(); С помощью этих функций можно получить 4 отдельных байта 32х битного числа. В данном случае мы используем три байта переменной DDSFreq от младшего к старшему.

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

    r31  = Hi(ddsaddr);
    r30  = Lo(ddsaddr); //Старший байт аккумулятора фазы

После этого обнуляем два младших байта аккумулятора фазы. Для ассемблерной функции они представлены регистрами r28 и r29:

    r29 = 0;
    r28 = 0;

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

asm{
     LOOP1:
           add      r28,r24      ; 1
           adc      r29,r25      ; 1
           adc      r30,r26      ; 1
           lpm                   ; 3
           out      PORTD,r0     ; 1
           sbic     PINB, 0       ; 1
           rjmp     LOOP1        ; 2=>10
  }
 PORTD = 0;
 do {} while (stopdds==0);

Дла написания ассемблерной вставки в MikroC достаточно поместить ассемблерный код в блок asm{};

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

LOOP1: — это метка для создания «бесконечного» цикла. Она содержит адрес первой инструкции ассемблерной процедуры.

Три последующие инструкции осуществляют операцию сложения аккумулятора фазы и кода частоты:

add r28,r24 ; 1
adc r29,r25 ; 1
adc r30,r26 ; 1

Видим что общее время выполнения равно трем тактам процессора

Далее следует инструкция lpm. Это очень важная ассемблерная команда. Она осуществляет косвенную адресацию данных, расположенных во флэш памяти микроконтроллера. Именно наличие в системе команд AVR инструкции lpm сделало возможным создание эффективной ассемблерной процедуры генерации сигнала.

Команда загружает один байт, адресованный регистрами r30 и r31, в регистр R0. Команда обеспечивает эффективную загрузку констант или выборку постоянных данных. То есть с помошью одной этой команды мы берем из таблицы волны очередное значение и помезаем его в нулевой регистр контроллера. При этом таблица физически расположена во флэш памяти программ контроллера. Это очень эффективная команда. Выполняется она за 3 такта процессора.

В следующей строке
out PORTD,r0
мы просто выводим полученной значение в порт D. В этот момент на выходе нашего R2R ЦАП появляется напряжение, оссоветствующее очередному значению из таблицы волны.

В следующей строке мы проверяем состояние ножки B0 микроконтроллера. Это нужно для того, чтобы мы могли по нашему желанию выйти из «бесконечного» цикла генерации сигнала. Ради этого приглост пожертвовать одним процессорным циклом:
sbic PINB, 0
Команда проверяет состояние бита в регистре I/O и, если этот бит очищен, пропускает следующую команду. А следующая команда у нас это:
rjmp LOOP1
То есть безусловный переход на метку LOOP1, то есть в начало ассемблерной процедуры.

таким образом, цикл будет крутиться бесконечно, пока на ножке PINB, 0 контроллера будет оставаться высокий уровень. Если на ножку подать низкий уровень напряжения, то rjmp LOOP1 не будет выполнена, и программе перейдет к следующей после rjmp LOOP1 команде, то есть выйдет из ассемблерной процедуры и передет к «сишной» строке
PORTD = 0;

Следующая строка ждет, пока переменная stopdds равна нулю. это сделано для предотвращения немедленного перехода к генерации сигнала после её остановки.
do {} while (stopdds==0);

Если мы сложим время выполнения всех вссемблерных команд, то получитя, что один сэмпл из таблицы ыолны выводится за 10 циклов микроконтроллера. Вот откуlа берется число 10 в формуле рассчета кода частоты:
16000000 / 10 / 16777216
На этом код функции void SignalOut() заканчивается.

Управление генератором

Данный генератор разрабатывался как абсолютно «минималистическая» конструкция. В схеме нет ни дисплея, ни кнопок управления. Единственная кнопка, которую я бы посоветовал добавить в схему — это кнопка «сброс». Она подключается между ножкой 1 (Reset) микроконтроллера и минусом питания («землёй»).

Управление работой генератора осуществляется от персонального компьютера через последовательный интерфейс. У такого решения есть как плюсы, так и минусы. Плюс в том, что упрощается и удешевляется конструкция самого генератора. Минус — что генератор привязан к компьютеру и вам потребуется небольшой переходник — преобразователь USB в последовательный интерфейс UART для связи с микроконтроллером. Такой переходник очень дешев, и сейчас есть практически у любого радиолюбителя, который занимается микроконтроллерами. Кроме того, такой адаптер можно собрать самому по схеме в этой статье.


Программный UART

Несмотря на то, что в ATMega8 есть встроенный аппаратный последовательный порт UART, Для управления генератором мы будем использовать программный UART. Дело в том, что аппаратный UART задействует порты PD0 и PD1 контроллера, а весь порт D у нас используется для вывода аналогового сигнала через R2R АЦП. Порт D — единственный порта Atmega8, который мы можем для этого использовать. Нам нужны все 8 разрядов порта. А 2 вывода порта B у нас уже задействованы под внешний кварцевый резонатор. У нас есть еще порт C, но он в Atmega8 не полный. В нем только 7 разрядов.

В компиляторе MikroC Pro есть встроенная библиотека для реализации программного UART на любых двух выводах контроллера. Мы будем использовать выводы PB1 (RXD) и PB2(TXD).

Логика управления генератором

Посмотрев на схему, вы можете заметить, что сигнал, поступающий с внешнего UART на вывод PB1(RXD) одновременно подается на вывод PB0.

Программный UART настраивается для работы на скорости 14400 в следующей строке:

error = Soft_UART_Init(&PORTB,1,2,14400,0);

Управляющая программа для компьютера написана на бесплатном Delphi 11 Community Edition с использованием библиотеки BComPort.

DDS генератор на ATMega8 с управлением от компьютера

Программа для управления генератором на Delphi

Блок данных, который управляющая программа посылает из компьютера в микроконтроллер генератора состоит из 7 байт. Сначала идет байт со значением 0. Физически, при передаче по UART этот байт выглядит как низкий уровень на протяжении восьми тактов передачи по UART. После этого следует задержка 200 миллисекунд. Этот байт используется просто как сигнал низкого уровня на ножке PB0 микроконтроллера. Ассемблерная процедура генерации сигнала

void SignalOut(){

постоянно опрашивает эту ножку контроллера в строке:

sbic     PINB, 0       ; 1

Если на ножку приходит логический ноль, то ассемблерная функция генерации сигнала завершает работу и включается режим приема команд по программному UART (функция void receiveData (void) )


После задержки в 200 мс идет собственно информационный блок данных. Это 6 байт, которые передаются один за другим:
1 байт — запуск/остановка генератора ( 1 = запуск, 0 = остановка)
2 байт — код формы сигнала (0=синус, 1=треугольник, 2=пила, 3=обратная пила, 4=прямоугольник, от 5 до 9 = пять дополнительных волновых форм пользователя)
3, 4, 5 и 6 байты — в них содержится 32-битное число — требуемая частота генерации.

void receiveData (void) {
  char tmp;
  char *pbyte;
  unsigned long f;
  
       if (byte_start == 0){tmp = Soft_UART_Read(&error);}
       byte_start  = Soft_UART_Read(&error);
       byte_wave   = Soft_UART_Read(&error);
       byte_freq_1 = Soft_UART_Read(&error);
       byte_freq_2 = Soft_UART_Read(&error);
       byte_freq_3 = Soft_UART_Read(&error);
       byte_freq_4 = Soft_UART_Read(&error);
       pbyte = &f;
       *pbyte = byte_freq_1;
        *(pbyte+1) = byte_freq_2;
          *(pbyte+2) = byte_freq_3;
            *(pbyte+3) = byte_freq_4;
       freq = f;
}

Функция void receiveData (void) принимает информацию, устанавливает соответствующие параметры генератора и запускает функцию void SignalOut() если байт запуск/остановка равен 1. На время установки параметров, как вы поняли, генерация прерывается в любом случае.

Управляющая последовательность из 7 байтов выглядит в терминале следующим образом:

DDS генератор на ATMega8 с управлением от компьютера

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

Прошивка микроконтроллера

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

DDS генератор на ATMega8 с управлением от компьютера

USB — программатор для AVR микроконтроллеров размером с флешку

Для работы с этим программатором необходимо установить драйвер. «Правильный» драйвер для windows 10 64 bit я поместил в архив проекта.

Для программирования я использую очень удобную программу Extreme Burner AVR. Ее можноь скачать с офф. сайта, кроме того вы также можете ее найти в архиве проекта.

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

Значения байтов фьюзов для нашей прошивки:
LOW = 0xFF
HIGH = 0xD9

DDS генератор на ATMega8 с управлением от компьютера

Программа Extreme Burner AVR для прошивки микроконтроллера через программатор USBASP

Все материалы к данному проекту, включая прошивку генератора, программу на Delphi 11 и на C#, проект печатной платы вы можете скачать по этой ссылке>>.

Оставляйте ваши комментарии по этому проекту под данной статьёй, а также подписывайтесь на наш телеграм канал здесь>>

1 comment

  1. Спасибо! Очень полезный материал. Буду собирать, экспериментировать.

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

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