Январь оказался достаточно продуктивным, в плане практического опыта. Основная цель: прокачать свои навыки в области разработки и сборки законченных устройств. Побочная задача: использовать мозги микроконтроллера в связке с быстродействующей ПЛИС. Как результат сделать себе годный DDS генератор.
Когда то давно я уже пробовал, сделать себе DDS генератор, случилось это еще во времена, когда еще и сайт не существовал, да и в микроконтроллерах ничего не понимал. Где то, что то вычитал, кое что повторил из чужих устройств, где то подсказали, в итоге родил генератор. Частота на выходе генерировалась прерыванием по совпадению, внутри прерывания просто инвертировалось состояние ножки. Частота изменялась значением регистра совпадения OCRnx. Этот подход описан уже в 7 уроке, генерация звука.
На тот момент, получившийся результат меня полностью устроил. На выходе регулируемый меандр с частотой до 20кГц, а вот синус получился ужасным, около 2кГц уже выдавал фигню. На практике мне было достаточно меандра на нескольких частотах до 10кГц, поэтому данный генератор много лет выполнял свои функции. Кроме того, на крайний случай у меня на работе был нормальный генератор.
Первое о чем я подумал, когда начал разбираться с ПЛИСинами, это то что хочу перепилить генератор заново. Каким образом? Идея пришла не сразу, но в итоге мое понимание вылилось в то, что обработка интерфейса пользователя будет висеть на микроконтроллере, а молотить будет ПЛИСина. Можно было бы реализовать все на одной FPGA? Можно, но пока еще нет понимания как организовано взаимодействие с внешней памятью, да и мой скилл фоторезиста пока настолько крут чтобы делать такие мелкие платки.
Именно поэтому изначально решено, что это будет atmega8+epm240, дисплейчик wh0802 и энкодер для управления.
Блок питая городил свой из транса и кучи кренок. В итоге 3.3В для питания мк и плис, 5 на дисплей, +-12 для усилителя.
Схему усилителя приводить нет смысла — обычный инвертирующий усилитель на lm324. В итоге с железом все вышло достаточно просто. Задающая плата:
«Железный» подготовительный этап длился достаточно долго, но не сказать, чтобы было много проблем. А вот софтовая часть… Случилось то чего я больше всего боялся, но кто не рискует, тот не пьет шампанское 🙂
Самый прикол ожидал меня с самого начала. Опыта построения подобных девайсов у меня нет, поэтому я взял генератор для тактирования ПЛИС пожирнее, аж 100МГц. Начал вспоминать опыт первого генератора, попробовал и ничего не вышло. Представим себе на 100МГц, если сделать счетчик, допустим считать до 1 и инвертировать ногу, это будет 50МГц на выходе. Если считать до 2х, то на выходе будет 25МГц, воу воу, но мы же юзаем резисторную матрицу, а это значит что результат нужно поделить еще на 8. Но в любом случае получается большая дискретность шага.
Я сразу заподозрил неладное и начал гуглить про принцип DDS. В итоге сразу стало понятно, что предыдущий генератор совсем не DDS, более того даже прочитав тонны статей, просветления не наступило. В общем, принцип объяснить на пальцах действительно не так просто. Главное нужно понять разницу, предыдущий ген построен на таком принципе: запустили таймер, сработало прерывание, подставили значение из таблицы, увеличили указатель на таблицу.
Правильный же DDS работает совсем иначе, значение из таблицы подставляется каждый такт, каждый. Да да, тут никакой ошибки нет. Попробую объяснить, представим что тактовая 1Гц, таблица из 8 значений. Значит подставляя значения из таблицы, можно получить частоту 1/8 = 0.125Гц. Выглядело бы это наверно так:
PORTD = sin[i++]; |
Как же регулируется частота? Тут есть маленькая хитрость. Cчетчик инкрементируется не на 1, а на какое то более мелкое число, таким образом можно уменьшить частоту, например в 10 раз:
while(1) PORTD = sin[i = i + 0.1]; |
Или увеличить в 2 раза:
while(1) PORTD = sin[i = i + 2]; |
Знаю, знаю для мк этот пример совсем не корректен, ибо операции будут выполняться разное количество тактов, но если утрированно взглянуть на код, то суть можно понять, кроме того в ПЛИС можно несколько операций выполнять по настоящему параллельно, т.е. для нее такой проблемы с тактами не существует!
В итоге вся теория выливается в две важных формулы:
1. Какой нам нужен шаг перестройки частоты.
Fdelta = Fclk/(2^N)
Обычно народ любит говорить о 0.01 или даже 0.001, честно мне по жизни не приходилось сталкиваться с тем, чтобы была нужна такая точность, но на всякий случай заложил поболее 🙂 Вычисляем так: тактовая Fclk = 100МГц, Fdelta нужна 0.01Гц, в итоге 0.01 = 100*10^6/(2^N), хехе вспоминаем математику, это обычный логарифм 🙂
n = log2 (100*10^8) = 33
2. Вычисление тактовой частоты
fout=m*(Fclk/n_tabl)/(2^n)
где m это число которое мы должны получить, fout требуемая на выходе частота, n_tabl количество табличных значений синуса(128), n — разрешение синуса, то что считали выше. Скажу сразу, для удобства я взял 32 бита, ибо это 4 байта.
fout=m*(100 000 000/128)/(2^32)
В итоге после всех сокращений получилось так:
m = f_out/0.000181898940354585647583;
Со стороны мк все совсем просто, крутим энкодер видим циферки на дисплее, как только циферка установлена, нажимаем кнопку энкодера, когда все циферки установлены — отсылаем 4 байта по spi:
#include <mega8.h> #include <spi.h> #include <delay.h> #include <alcd.h> #include <stdio.h> const float x = 0.000181898940354585647583; volatile long int m; volatile int NewState,OldState,upState,downState; volatile char posX = 0, ps = 0; volatile char temp_freq[6]={0,0,0,0,5,0}; volatile long int f_out = 50; interrupt [TIM1_COMPA] void timer1_compa_isr(void) { NewState=PINB & 0b00000011; if(NewState!=OldState) { switch(OldState) { case 2: { if(NewState == 3) upState++; if(NewState == 0) downState++; break; } case 0: { if(NewState == 2) upState++; if(NewState == 1) downState++; break; } case 1: { if(NewState == 0) upState++; if(NewState == 3) downState++; break; } case 3: { if(NewState == 1) upState++; if(NewState == 2) downState++; break; } } OldState=NewState; } TCNT1H=0x00; TCNT1L=0x00; } void main(void) { char lcd_buf[17]; DDRC = (0<<DDC1) | (0<<DDC0); PORTC =(0<<PORTC1) | (1<<PORTC0); // Function: Bit7=In Bit6=In Bit5=Out Bit4=In Bit3=Out Bit2=Out Bit1=In Bit0=In DDRB=(0<<DDB7) | (0<<DDB6) | (1<<DDB5) | (0<<DDB4) | (1<<DDB3) | (1<<DDB2) | (0<<DDB1) | (0<<DDB0); // State: Bit7=T Bit6=T Bit5=0 Bit4=T Bit3=0 Bit2=0 Bit1=T Bit0=T PORTB=(0<<PORTB7) | (0<<PORTB6) | (0<<PORTB5) | (0<<PORTB4) | (0<<PORTB3) | (1<<PORTB2) | (1<<PORTB1) | (1<<PORTB0); // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 1000,000 kHz // Mode: CTC top=OCR1A TCCR1A=0x00; TCCR1B=0x0A; TCNT1=0x00; OCR1AH=0x03; OCR1AL=0xE8; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x10; // SPI initialization // SPI Type: Master // SPI Clock Rate: 62,500 kHz // SPI Clock Phase: Cycle Start // SPI Clock Polarity: Low // SPI Data Order: MSB First SPCR=(0<<SPIE) | (1<<SPE) | (0<<DORD) | (1<<MSTR) | (0<<CPOL) | (0<<CPHA) | (1<<SPR1) | (1<<SPR0); SPSR=(0<<SPI2X); lcd_init(8); lcd_puts("sine_gen"); lcd_gotoxy(0,1); lcd_puts("000050"); #asm("sei") lcd_gotoxy(0,1); _lcd_write_data(0xe); while (1) { //scan button if(PINC.0 == 0 && ps ==0) { posX++; if(posX > 5) { posX=0; f_out = temp_freq[0]*100000 + temp_freq[1]*10000 + temp_freq[2]*1000 + temp_freq[3]*100 + temp_freq[4]*10 + temp_freq[5]*1; //fout=m*(100 000 000/128)/(2^n) m = f_out/x; PORTB.2=0; delay_ms(1); spi((char)(m)); //1 byte delay_ms(1); spi((char)(m >> 8)); //2 byte delay_ms(1); spi((char)(m >> 16)); //3 byte delay_ms(1); spi((char)(m >> 24)); //4 byte delay_ms(1); PORTB.2=1; } ps = 1; lcd_gotoxy(posX,1); delay_ms(10); } if(upState >= 4) { if(temp_freq[posX]<9)temp_freq[posX]++; upState = 0; lcd_gotoxy(posX,1); lcd_putchar(temp_freq[posX]+0x30); lcd_gotoxy(posX,1); _lcd_write_data(0xe); } if(downState >= 4) { if(temp_freq[posX]>0) temp_freq[posX]--; upState = 0; lcd_gotoxy(posX,1); lcd_putchar(temp_freq[posX]+0x30); lcd_gotoxy(posX,1); _lcd_write_data(0xe); downState = 0; } if(PINC.0 == 1) ps = 0; } } |
А вот со стороны ПЛИСины, все вышло через ж. Если установить значение add_Counter(которое m на мк) вручную, то частота регулируется просто ништяк, я тестил генератор на разных частотах до 100кГц, выше осцилл не позволяет все просто шикарно, герц в герц. Но как только значение add_Counter начал читать по spi с мк, то приходит лажа, проблема совсем не верилоге и не в самом spi модуле, проблема в понимании работы ПЛИС и прохождении сигналов. На практике это выражается в том что 1 из 5 раз данные не приходят, следовательно частота не меняется. Не большая проблема щелкнуть кнопкой лишний раз, но все таки это не круто. В общем, пока я эту проблему решить не смог, вернусь к ней немного позже, после проведения нескольких дополнительных экспериментов на отдельной плате.
Пояснять исходник верилога пока смысла нет, ибо он подлежит корректировке
module test_sin(clk,mosi,data,ss,s_clk); input ss; //reset for nBit counter input clk; //main clock input mosi; //data input s_clk; //spi clock reg [7:0] sinLUT[127:0]; //table of sin reg [38:0] i; //dds counter reg [4:0] nBit; //number of received bit reg [4:0] n_Byte; //number of reseived byte reg [7:0] spi_data; //output r2r data reg [31:0] temp_resByte; //temp addeder reg [31:0] add_Counter; //addeder reg byte_readed; //high when a byte has been received reg bit_readed; reg [1:0]posedge_flag = 0; //flag s_clk reg [7:0]temp_data = 0; reg prev_sclk; reg front_edge; reg back_edge_ss; reg back_edge; reg prev_ss; reg sclk1; reg sclk2; reg sclk3; reg sclk4; output reg [7:0] data; //output r2r data initial begin $readmemh("data.txt",sinLUT); //init from file data.txt end initial begin i=0; nBit=0; add_Counter = 274878; end ///---------------------------------------///// /// SINUS OUTPUT ///---------------------------------------///// always @(posedge clk) begin data[7:0] <= sinLUT[i[38:32]]; //38-32 = 2^7 i <= i + add_Counter; //54000 = 9.82Hz ///---------------------------------------///// /// SPI RECEIVE DATA ///---------------------------------------///// //on negedge ss prev_ss <= ss; back_edge_ss <= prev_ss & ~ss; //if no data if(back_edge_ss == 1) begin nBit <= 0; n_Byte <= 0; spi_data[7:0] <= 0; end //if ss down else begin sclk2 <= sclk1; sclk1 <= s_clk; sclk3 <= sclk2; /* //on posedge s_clk & bit is not readed prev_sclk <= s_clk; front_edge <= ~prev_sclk & s_clk; if((front_edge == 1) && (bit_readed == 0)) */ if(( sclk3 == 0)&&(sclk2 == 1)) begin //read from miso spi_data[7:0] <= {spi_data[6:0],mosi}; nBit <= nBit + 1'b1; bit_readed <= 1; end //on negedge s_clk, enable read back_edge <= prev_sclk & ~s_clk; if(back_edge == 1) begin bit_readed <= 0; end if(nBit == 8) begin byte_readed <= 1; nBit <= 0; end end ///---------------------------------------///// /// BYTE PROTOCOL ///---------------------------------------///// if(byte_readed == 1) begin byte_readed <= 0; case(n_Byte) 0: begin temp_resByte[7:0] <= spi_data[7:0]; n_Byte <= 1; end 1: begin temp_resByte[15:8] <= spi_data[7:0]; n_Byte <= 2; end 2: begin temp_resByte[23:16]<= spi_data[7:0]; n_Byte <= 3; end 3: begin temp_resByte[31:24]<= spi_data[7:0]; n_Byte <= 0; add_Counter[31:0] <= temp_resByte[31:0]; end endcase end //end of byte readed end endmodule |
Ну и оставшаяся часть, ради которой по большому счету было задумана эта поделка, превратилась в полный эпик фейл.
Много где накосячил, особенно пока пропиливал окошко для дисплея.
Хоть это уже и не первый мой девайс, но с корпусами ранее я предпочитал не связываться. Я охренел от того, что на такой большой город как Санкт-Петербург нигде не купить обыкновенных винтов и гаек на М2 и М2.5, столько магазинов пришлось обойти, это кошмар, тоже и касается стоек для печатных плат. Тоже касается корпуса, был классный приборный корпус, но если я буду покупать еще и корпуса за 700р, то меня точно из дома выгонят. Другого нужного размера не было, по сути пришлось брать что есть.
Сильно меня разочаровал мой любимый IDC шлейф, опять же купил на распродаже целую бухту по дешевке и очень радовался, вылилось это в то, что кабель просто рассыпался в руках, такого за 5 лет использования подобных шлейфов никогда не видел.
Вместо чудесной коробки с чудесными надписями и крутыми ручками, со светящимся дисплеем вы можете лицезреть моего «гену» 🙂
Если подвести итог. Я его слепила из того что было… И как мог, потратив на это около месяца. Все делалось исключительно по собственному желанию. Продукт получился сыроват и требует допиливания, однозначно. Можно ли им пользоваться? Можно. Если вернуться к изначальной желаемой цели — повысить скилл в разработке устройств, то тут конечно опыта была просто река. Удалось ли реализовать побочную задачу? Да, хотя был высокий шанс заглохнуть на полпути. Кроме того, теперь я наверняка понял, курс дальнейшего саморазвития.
В целом, для желающих освоить FGPA/CPLD уровень девайса как раз подходит для начинающих. Не обязательно повторять мое устройство, ибо здесь можно придумать кучу различных вариаций, но к собрать подобный девайс настоятельно рекомендую.
Admin, а почему бы просто не сделать в ПЛИС один 32-разрядный сдвиговой регистр, вместо 8-разрядного spi_data. И потом просто, при низко уровне сигнала SS вход этого регистра подключать на вход моси. Примерно так:
if(ss==0)
begin
spi_data[31:0] <= {spi_data[30:0],mosi};
end
else
begin
add_Counter[31:0]<=spi_data[31:0];
end
Тут идею саму описывал, если есть ошибки синтаксические — извините 🙂
Плюс еще — можно модуль SPI описывать затактировать от s_clk, а всю остальную логику — от обычного clk.
И еще 🙂 Встречал рекомендацию от профессиональных программистов-верилоговцев — логику последовательную (регистры и тп) и комбинационную описывать отдельными блоками always. Последовательную — по посэджам соотв., а комбинационную — always@(*) — т.е. по изменению любого сигнала. А сам подход примерно такой.
reg[7:0] counter; //Будет триггерами
reg[7:0] _counter; //Будет комбинационной логикой
reg[7:0] out;
wire clk, rst;
always@(posedge clk or negedge rst)
begin
if(!rst)
counter[7:0]<=0;
else
counter[7:0]<=_counter[7:0];
end
always@(*)
begin
_counter[7:0]=counter[7:0]+1;
end
Оно кажеться, возможно, избыточным и навороченым, но на самом деле — позволяет ясно и четко понимать, что и где создано будет. И легче избежать различных неожиданных эффектов и спецэффектов 😀
Ether, сделать 32 битный регистр, наверно это правильно при текущем подходе, просто изначальная прошивка была совсем другой, это ее отголоски 🙂
К клокам s_clk привязаться можно, так и было изначально, но переменные которые изменяются внутри одного цикла, нельзя менять в других, тот код что Вы привели не скомпилится. Пока я не понял как это обойти. Поэтому пришлось все запихать в clk цикл. Еще одна проблема это то, что не стал осваивать средства отладки, они отличаются от привычных микроконтроллерных, поэтому планировал их освоить позже, но видимо без этого никак.
В общем то информации в сети навалом, главное знать что искать, в одних и тех же статьях, с каждым разом открываю все больше нового. Думаю это не та проблема, из за которой стоит париться, чуть позже она разрешится.
Если создавать на плисине counter от мк, кажется, её надо в настройках указать как виртуальный.
Я в ПЛИС полный 0, не понимаю, как можно сделать там sin[i = i + 0.1] ? В МК индекс массива-это адрес в памяти, где лежит значение, поэтому индекс может быть только целым (i+2 можно, i+0.2 нельзя). А в ПЛИС как такое может быть? (sinLUT[i[38:32]], если я правильно понял). Там не таблица адрес-значение что-ли, а, хз, счетчики какие-то хитрые генерятся?
зы: я делал подобный девайс с DDS без ПЛИС, используя МК с DMA и встроенным ЦАП (PIC24, AtmegaX). Таблица с синусом (у меня было несколько для разных частот, скажем, один период-16 точек, другой-32) в памяти. DMA копирует точки в ЦАП с частотой ядра (скажем, 32МГц). Если таблица состоит из 32 точек, на выходе получаем 1МГц, если из 16-ти точек — 2МГц и т.п. На прерываниях у меня получалось генерить синус максимально 25кГц при тактовой 32Мгц, при этом МК 90% проводил в обработчике прерывания. А DMA работает как-бы параллельно ядру, не занимая никаких циклов, поэтому частота ограничена только частотой ядра и быстродействием ЦАП. Недостатки-нельзя регулировать частоту с мелким шагом, надо иметь или кучу таблиц в памяти, или как-то рулить частотой ядра (встроенный PLL имеет очень небольшое число делителей).
sin[i = i + 0.1] это слишком утрированно, но если так переписать:
i = i + 0.1;
PORTD = sin[(char)i];
тогда вроде ничего не обычного
То что Вы сделали не есть DDS, весь прикол DDS в том, что нужна 1 таблица на все случаи жизни изменяется как бы число пропущенных тактов (nop) между следующей подстановкой значения. Можно сделать без ПЛИС, хоть на меге8 и получить вполне приличный результат. Для меня было сутью ковыряние с ПЛИСиной.
Не DDS так не DDS, спорить не буду. 😥 Хотя, я не видел такой классификации, что если генерация идёт методом пропуска тактов, то это DDS, а если как-то по другому, то не DDS. Вроде как, если нет ФАПЧ и PLL, а есть преобразование числа в аналог — значит DDS. А схемы там бывают всякие. Мне нравится, как у Ридико написано: http://ra3ggi.qrz.ru/UZLY/dds.htm
Я так понял, ваш вариант-это первая схема в статье.
Надеюсь не обижу никого этим комментом, то что я использовал это именно DDS. Хоть у Ридико написано все технически правильно, но если копнуть всерьез эту тему, то окажется куча форумов с ссылкой на эту статью и куча вопросов, так как все таки оно работает? И поводу ответа по классификации, говорит о том что Вы тоже не поняли эту статью на которую дали ссылку, посмотрите внимательно на формулы и попробуйте разобраться что к чему. Преобразование числа в аналог это ЦАП. Я же надеялся своими изысканиями исправить ситуацию и объяснить как это работает, но видимо тоже не вышло.
Да, туплю что-то. Вроде разобрался. 38 битный счетчик, тикает с частотой кварца ПЛИС. 32 бита счетчика могут загружаются по SPI от МК, получается делитель частоты (при больших значениях делителя частотой можно управлять с очень маленьким шагом). Таблица синуса 16 байт, выборка идёт от 6 старших бит счётчика. Интересная штука, эта ПЛИС. Тоже себе хочу.
Единственное таблица 128 байт sinLUT[127:0], она отдельным файлом идет, а в целом все именно так, хорошо что разобрались)) По сути тут заюзана просто быстрая логика, да и писать простые вещи под ПЛИСину не сложнее, чем на микроконтроллере, так что «переходите к нам на Темную сторону, у нас есть печеньки…»