В этой статье описан способ генерации синусоидального сигнала на ПЛИС через использование ROM памяти и реальный пример практического применения этого генератора для коротковолнового радиопередатчика RTTY (Radioteletype. - прим. Ред.). Будет описан способ передачи значения частоты из микроконтроллера в ПЛИС через SPI (англ. Serial Peripheral Interface, SPI bus — последовательный периферийный интерфейс, шина SPI - прим. Ред.). Используются отладочная плата LilyGO T-FPGA, в составе которой ПЛИС GW1NSR-LV4CQN48PC6/I5 и микроконтроллер ESP32-S3, ЦАП на основе DAC904, ide GOWIN FPGA Designer, Visual Studio Code с расширением PlatformIO и matlab 2020.

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

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

Генератор синуса

При реализации этого генератора можно отталкиваться от этой статьи (ссылка работает через раз. С рабочего компьютера не открывается, а с домашнего открывается. - прим. Ред.). Большую помощь в освоении плис GOWIN может оказать блог Марсоход. В качестве исходного проекта для ПЛИС можно использовать пример для T-FPGA. Но необходимо обратить внимание на Constraints файл. В данном примере сигнал clk подключён к кнопке (46 номер на схеме). Для работы от кварца необходимо подключиться к номеру 45. Всё это доступно на схемах:

Рис 1. Схема для T-FPGA
Рис 1. Схема для T-FPGA
//Copyright (C)2014-2021 Gowin Semiconductor Corporation.
//All rights reserved. 
//File Title: Physical Constraints file
//GOWIN Version: 1.9.8.01
//Part Number: GW1NSR-LV4CQN48PC6/I5
//Device: GW1NSR-4C
//Created Time: Tue 02 21 14:47:43 2023

IO_LOC "clk" 45; // это выход кварца 27 МГц
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=UP;

Для создания ROM памяти необходимо вызвать меню tools -> IP Core Generator или нажать иконку на панели инструментов. Дальше открыть Memory -> Block Memory -> pROM.

Рис. 2. IP Core Generetor
Рис. 2. IP Core Generetor

По двойному щелчку по pROM откроется диалоговое окно, в котором указываются необходимые параметры. В примере использованы 4096 12-битных отсчётов.

Необходимо обратить внимание на то, что ЦАП DAC904 14-битный и для него необходимо использовать 14-битные отсчёты, но лень.

Рис. 3. Параметры для экземпляра ROM памяти.
Рис. 3. Параметры для экземпляра ROM памяти.

Процедура генерации синусоидального сигнала в matlab

Стоит отдельно остановиться на Memory Initialization File. Наверняка существует немало способов сформировать .mi файл. Про него подробно написано в 7 главе UG285E. Здесь же предлагается пройти по пути генерации файла в matlab, а потом привести его к необходимому виду с помощью скрипта на python.

В matlab необходимо открыть скрипт содержащий код:

clear
clc
n = 0:4095 ;
yn = sin(2*pi/4096*n) ;
 
yn = round((yn+1)*2047); 
 
plot(n,yn);
 
fid = fopen('C:\work\rom_test_3.coe','wt');
fprintf(fid,'#File_format=Hex,\n#Address_depth=4096,\n#Data_width=12,');
 
for i = 1 : 4096
    if mod(i-1,1) == 0 
        fprintf(fid,'\n');
    end
    fprintf(fid,'%03X,',yn(i));
end

При запуске этого скрипта будет построен график и в указанном каталоге появится файл коэффициентов .coe.

Рис. 4. Интерфейс matlab и построенный график синуса
Рис. 4. Интерфейс matlab и построенный график синуса

Для использования файла коэффициентов в качестве Memory Initialization File для pROM GOWIN необходимо избавиться от запятых в конце каждой строки. Для этого можно воспользоваться скриптом на python:

import os
import sys

res = ''

if len(sys.argv)<2 :
	print("Not enough arguments, need file with data name param")
filename = sys.argv[1]
print(filename)

def read_txt_file(filename):
    output = ""  # инициализация
    with open(filename, 'r') as f:
        for line in f:
            output = output + line.replace(',\n', '\n') # strip() # rstrip(",\n")
    f.close()
    return output
    
def write_txt_file(input):
    with open('1.mi', 'w') as file:
        file.write(input)  # перезапись файла
    
res = read_txt_file(filename)
write_txt_file(res)

Если скрипт на python лежит в том же самом каталоге, что и файл коэффициентов сгенерированный в matlab и в командной строке написать C:\Users\s.novikov\Documents\work\271124>python3 probe1.py rom_test_3.coe , то в рабочем каталоге появится файл 1.mi. Необходимо только вручную удалить запятую в самом конце файла:

Рис. 5. При автоматическом удалении запятых из файла коэффициентов последнюю запятую необходимо удалить вручну��.
Рис. 5. При автоматическом удалении запятых из файла коэффициентов последнюю запятую необходимо удалить вручную.

Теперь файл готов для использования в качестве Memory Initialization File в диалоговом окне IP Customization pROM ide GOWIN FPGA Designer. GOWIN FPGA Designer предложит добавить сгенерированные файлы в текущий проект. Остаётся только согласиться:

Рис. 6. Добавление сгенерированного IP в проект.
Рис. 6. Добавление сгенерированного IP в проект.

Вернёмся к генератору синуса. Создадим ещё один IP:

module dds_addr (clk, rst_n, addr_out, strobe, FWORD);
    input clk, rst_n;          // Resetting the system clock
    output [11: 0] addr_out;    // The output address corresponding to the data in the ROM
    output strobe;
    parameter N = 32;
    parameter PWORD = 2048;     // Phase control word (x/360) * 256
    input [31:0] FWORD;
//    parameter FWORD = 159072862;  // слово управления частотой F_out = B * (F_clk / 2 ** 32), fword = B 5KHZ // 858994
    reg [N-1: 0] addr;         // 32-bit battery
    
    reg strobe_r;
    always @ (posedge clk or negedge rst_n)
    begin
       if (!rst_n)
           begin
              addr <= 0;  
           end
      else
          begin
              //Each word size outputs an address, if the word control frequency is 2, then the output of the address counter is 0, 2, 4...
              addr <= addr + FWORD;
              if (addr[N-1:N-12] + PWORD == 12'hc00) begin
                  strobe_r <= 1'b1;
              end
              else begin
                  strobe_r <= 1'b0;
              end
          end     
    end 
    //Assign the top eight bits of the battery address to the output address (ROM address
    assign addr_out = addr[N-1:N-12] + PWORD;

    assign strobe = strobe_r;
endmodule

От оригинального кода из статьи (ссылка работает через раз. С рабочего компьютера не открывается, а с домашнего открывается. - прим. Ред.) этот код отличается заменой параметра FWORD на входной 32-битный сигнал FWORD. Это сделано для того, чтобы менять частоту в проекте коротковолнового передатчика. Теперь эти два IP можно включить в модуль для генерации синусоидального сигнала:

module top(
    input          clk,
    input		   rst,
    output [11: 0] sin,
    output clk_o        // это выход для сигнала тактирования ЦАП
);

reg [31:0] fword;
wire [11: 0] addr_out; // 12-битный адрес, соответствующий данным в ПЗУ
wire [11: 0] sin_out;

// --------------Phase-based  module------------------------   
dds_addr dds_addr_inst (
    .clk(clk),           // input wire clk
    .rst_n(1'b1),        // input wire rst_n
    .addr_out(addr_out), // output wire [7 : 0] addr_out
    .strobe(),
    .FWORD(397682157)
);  
//----------------------------------------------------------

// Waveform Data Module       
Gowin_pROM rom_inst (
    .dout(sin),   //output [11:0] dout
    .clk(clk),    //input clk
    .oce(),       //input oce
    .ce(1'b1),    //input ce
    .reset(1'b0), //input reset
    .ad(addr_out) //input [11:0] ad
);

// assign clk_o = clk; это необходимо раскомментировать при подключении ЦАП к T-FPGA

endmodule

Чтобы посмотреть на сгенерированный сигнал, можно воспользоваться Gowin Analyzer Oscilloscope. Это встроенный в ide инструмент для записи выборок и просмотра осциллограмм сигналов как SignalTap у Altera или ChipScope у Xilinx. Подробнее про использование этого инструмента можно почитать на Marsohod. Если проделать все шаги, которые написаны в статье про Использование Gowin Analyzer Osciloscope в FPGA проекте и открыть это в Gtkwave как в этой статье, то получится такое изображение:

Рис. 7. Изображение сигнала в Gtkwave
Рис. 7. Изображение сигнала в Gtkwave

На изображении сигнал с частотой 2500 КГц. Если воспользоваться формулой в комментарии на строке 8 в модуле dds_addr Fout = B * (Fclk / 2**32), и подставить значение FWORD = 397682157, то получится, что F_out = 2500000 Гц. Чем больше будет значение частоты выходного сигнала, тем безобразнее будет выглядеть изображение сигнала, потому что выбрана частота тактирования всего 27 МГц. Это будет хорошо видно, если увеличивать частоту и смотреть на сигнал.

Промежуточный эксперимент. Генерация сигнала на ЦАП

Для последующих экспериментов был подключён ЦАП на основе DAC904. Чтобы его добавить в проект, потребуется только выход для сигнала тактирования ЦАП и раскомментировать assign для этого выхода, который приравнивается к входному тактовому сигналу. (ЦАП 14-битный, а сигнал формируется 12-битный, подключены младшие биты. - прим. Авт.). В файле constraints потребуется сделать необходимые назначения, например:

//Copyright (C)2014-2021 Gowin Semiconductor Corporation.
//All rights reserved. 
//File Title: Physical Constraints file
//GOWIN Version: 1.9.8.01
//Part Number: GW1NSR-LV4CQN48PC6/I5
//Device: GW1NSR-4C
//Created Time: Tue 02 21 14:47:43 2023

IO_LOC "clk" 45;
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=UP;

IO_LOC "clk_o" 29; // 23
IO_PORT "clk_o" IO_TYPE=LVCMOS33 PULL_MODE=UP;

IO_LOC "sin[0]" 20;
IO_PORT "sin[0]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[1]" 21;
IO_PORT "sin[1]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[2]" 18;
IO_PORT "sin[2]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[3]" 19;
IO_PORT "sin[3]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[4]" 16;
IO_PORT "sin[4]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[5]" 17;
IO_PORT "sin[5]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[6]" 13;
IO_PORT "sin[6]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[7]" 14;
IO_PORT "sin[7]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[8]" 34;
IO_PORT "sin[8]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[9]" 35;
IO_PORT "sin[9]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[10]" 31;
IO_PORT "sin[10]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

IO_LOC "sin[11]" 32;
IO_PORT "sin[11]" IO_TYPE=LVCMOS33 PULL_MODE=NONE;

В экспериментах в качестве ЦАП была использована отладочная плата. Она была запитана от T-FPGA (на фото с обложки видно два фиолетовых провода, которые подключены к гребёнке 3.3V T-FPGA. - прим. Авт.).

Рис. 8. Провода для питания отладочной платы ЦАП.
Рис. 8. Провода для питания отладочной платы ЦАП.

Данный ЦАП на отладочной плате допускает подключение питания и логики 3.3 В. Чтобы изменить напряжение на I/O пинах FPGA, можно воспользоваться примером LilyGO.

При клонировании репозитория для автоматической настройки PlatformIO необходимо изменить версию библиотеки "arduino-esp32" на 2.0.6 иначе проект не настраивался автоматически

platform_packages = framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#2.0.6

Для использования напряжения на I/O FPGA 3,3 В необходимо изменить парамет��ы, которые передаются в методы setALDO3Voltage() и setALDO4Voltage() на 3300:

Скрытый текст
#include "Arduino.h"
#include "Wire.h"
#include "XPowersLib.h" //https://github.com/lewisxhe/XPowersLib
#include "pins_config.h"

XPowersAXP2101 PMU;

void led_task(void *param);

void setup()
{
    Serial.begin(115200);
    Serial.println("Hello T-FPGA-CORE");
    xTaskCreatePinnedToCore(led_task, "led_task", 1024, NULL, 1, NULL, 1);

    bool result = PMU.begin(Wire, AXP2101_SLAVE_ADDRESS, PIN_IIC_SDA, PIN_IIC_SCL);

    if (result == false) {
        Serial.println("PMU is not online...");
        while (1)
            delay(50);
    }

    PMU.setDC4Voltage(1200);   // Here is the FPGA core voltage. Careful review of the manual is required before modification.
    PMU.setALDO1Voltage(3300); // BANK0 area voltage
    PMU.setALDO2Voltage(3300); // BANK1 area voltage
    PMU.setALDO3Voltage(3300); // BANK2 area voltage
    PMU.setALDO4Voltage(3300); // BANK3 area voltage

    PMU.enableALDO1();
    PMU.enableALDO2();
    PMU.enableALDO3();
    PMU.enableALDO4();
}

void loop()
{
    PMU.setChargingLedMode(XPOWERS_CHG_LED_ON);
    delay(20);
    PMU.setChargingLedMode(XPOWERS_CHG_LED_OFF);
    delay(random(300, 980));
}

void led_task(void *param)
{
    pinMode(PIN_LED, OUTPUT);
    while (true) {
        digitalWrite(PIN_LED, 1);
        delay(20);
        digitalWrite(PIN_LED, 0);
        delay(random(300, 980));
    }
}

Этим исходным кодом необходимо запрограммировать ESP32-S3 до подключения ЦАП.

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

Рис. 9. Сигнал на высокочастотном выходе ЦАП. FWORD =  397682157
Рис. 9. Сигнал на высокочастотном выходе ЦАП. FWORD = 397682157

Чтобы изменить частоту в модуль dds_addr_inst (подключён в модуле top. - прим. Авт.) необходимо передать другое значение. Например, на выходе необходима частота 5 МГц. Из формулы Fout = B * (Fclk / 2**32)необходимо вывести неизвестную B = Fout * (2**32/Fclk). 5000000 * 4294967296 / 27000000 = 795364314. Укажем это значение в подключаемом экземпляре:

// ... это кусок кода из top модуля
// --------------Phase-based  module------------------------   
dds_addr dds_addr_inst (
    .clk(clk),           // input wire clk
    .rst_n(1'b1),        // input wire rst_n
    .addr_out(addr_out), // output wire [7 : 0] addr_out
    .strobe(),
    .FWORD(795364314)
);  
//----------------------------------------------------------
// ...

И получим вот такой сигнал:

Рис. 10. Сигнал на высокочастотном выходе ЦАП. FWORD =   795364314
Рис. 10. Сигнал на высокочастотном выходе ЦАП. FWORD = 795364314

Сигнал уже не такой "красивый", но его частота 5 МГц.

Скрытый текст

Дальнейшее увеличение частоты делает изображение ещё более непривлекательным:

Рис. 11. Сигнал на высокочастотном выходе ЦАП. FWORD = 1590728628
Рис. 11. Сигнал на высокочастотном выходе ЦАП. FWORD = 1590728628

А уменьшение частоты снова приводит к красивым картинкам:

Рис. 12. Сигнал на высокочастотном выходе ЦАП. FWORD = 198841078
Рис. 12. Сигнал на высокочастотном выходе ЦАП. FWORD = 198841078

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

Коротковолновый радиопередатчик на основе описанного генератора синуса

Для управления частотой можно использовать микроконтроллер ESP32-S3 на плате T-FPGA. Для создания передатчика можно воспользоваться этим исходным кодом. В этом проекте изменяется частота аппаратного DDS генератора сигналов. Чтобы сделать передатчик на основе описанного в этой статье генератора, необходимо считать значение FWORD и передавать его в ПЛИС. В репозитории T-FPGA есть примеры для ESP32-S3 и FPGA, в которых мигание светодиода, подключённого к FPGA, управляется из микроконтроллера. Из ESP32 в FPGA по SPI передаётся 8-битное значение, в зависимости от которого светодиод либо включается, либо выключается.

В случае управления частотой генератора из этой статьи необходимо передавать 32-битное значение. Поэтому предлагается разбить вычисленное на ESP32-S3 32-битное значение на 4 части по 8 бит, передать их по SPI в ПЛИС, а там собрать и отправить в dds_addr_inst.

По аналогии с исходным кодом создана функция send_frequency(uint32_t &freq):

void send_frequency(uint32_t &freq){
    uint32_t fword;
    uint64_t tmp;
    tmp = (uint64_t)freq*(uint64_t)4294967296;
    fword = tmp / (uint32_t)27000000;

    uint8_t buff[4];

    buff[0] = fword       & 0xff;
    buff[1] = fword >>  8 & 0xff;
    buff[2] = fword >> 16 & 0xff;
    buff[3] = fword >> 24 & 0xff; // старший

    fpga_spi_blink(true);

    for(uint8_t i=0;i<4;++i){
        digitalWrite(PIN_FPGA_CS, 0);
        SPI.beginTransaction(SPISettings(1000000, SPI_MSBFIRST, SPI_MODE3));
        uint8_t fpga_output = SPI.transfer(buff[i]);
        SPI.endTransaction();
        digitalWrite(PIN_FPGA_CS, 1);
    }
}

На 14 строке передаётся значение 0x01 для синхронизации посылки. Тогда для приёма посылки на стороне FPGA можно использовать небольшой конечный автомат:

always@(posedge clk)begin
    if(ready_fword == 1'b1)  // если приняли все 4 части
        fword_valid = fword; // переписываем в регистр конечный результат
end

reg [3:0] state_reg;
always@(posedge rxd_flag or negedge rst)begin
    if(!rst)
        led<=1'b0;
    else if(rxd_out==8'h01) // значение 0x01 для синхронизации посылки
        begin
            ready_fword <= 1'b0;
            state_reg <= 0;
            fword <= 0;
        end
    else
        begin
            case(state_reg)
                4'd0: begin
                    fword <= fword + rxd_out;
                    state_reg <= 1;
                end

                4'd1: begin
                    fword <= fword + (rxd_out << 8);
                    state_reg <= 2;
                end

                4'd2: begin
                    fword <= fword + (rxd_out << 16);
                    state_reg <= 3;
                end

                4'd3: begin
                    fword <= fword + (rxd_out << 24);
                    state_reg <= 0;
                    ready_fword <= 1'b1;
                end

                default: begin
                    fword <= 0;
                    state_reg <= 0;
                    ready_fword <= 1'b0;
                end
            endcase
        end
end

Весь исходный код проекта доступен на Github. При программировании ESP32 иногда возникает необходимость повторного программирования FPGA. Теперь, если подключить к высокочастотному разъёму ЦАП 50-омную антенну (антенны для КВ достигают в размерах 160-ти метров. Для эксперимента на столе подойдёт даже обычная телескопическая антенна, которую можно даже и не выдвигать. - прим. Авт.) и поставить на столе рядом с передатчиком радиоприёмник, то в динамик будет слышен специфический звук RTTY:

Для декодирования RTTY можно использовать программу MultiPSK:

Рис. 13. Интерфейс программы MultiPSK и результат декодирования радиопередачи
Рис. 13. Интерфейс программы MultiPSK и результат декодирования радиопередачи

На этом всё. Такой г��нератор синуса использовался на FPGA Xilinx для отладки софта, который отвечал за передачу на компьютер принятого тестового сигнала радиочастотной микросхемой. Чтобы понять, что микросхема сконфигурирована правильно и работает корректно на приём, необходимо было визуализировать сигнал, который она приняла. А для этого его необходимо было передать на компьютер. И когда было непонятно, а что именно не работает - микросхема на приём или канал передачи данных, генератор синуса очень помог, потому что был использован в качестве заведомого рабочего источника сигнала. Ещё генератор синуса очень пригодился, когда отлаживался канал передачи данных из ПЛИС Xilinx через USB-to-FIFO микросхему на компьютер. Имея заведомо рабочий генератор сигнала, можно с высокой вероятностью утверждать работает канал передачи данных или не работает. В этот раз генератор помог с передачей информации в КВ диапазоне. Хочется добавить, что подобным образом запросто можно реализовать передачу сигналов азбуки Морзе, FT8 и ещё многих других видов сигналов и модуляций. Причём имея радиолюбительский позывной, качественную антенну, усилитель и фильтр, удаляющий паразитные (вторые, третьи и т.д.) гармоники и зеркальный канал, можно передавать свой сигнал на очень существенные расстояния, в основном ночью, но это уже другая история.

Спасибо.

С. Н.