В этой статье описан способ генерации синусоидального сигнала на ПЛИС через использование 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 и ещё многих других видов сигналов и модуляций. Причём имея радиолюбительский позывной, качественную антенну, усилитель и фильтр, удаляющий паразитные (вторые, третьи и т.д.) гармоники и зеркальный канал, можно передавать свой сигнал на очень существенные расстояния, в основном ночью, но это уже другая история.

Спасибо.

С. Н.

Комментарии (10)


  1. avitek
    04.12.2024 14:59

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

    Используя нехитрые целочисленные математические приёмы, можно ощутимо улучшить качество синуса.


    1. Pisikak Автор
      04.12.2024 14:59

      Спасибо!


  1. iliasam
    04.12.2024 14:59

    По-моему, канал передачи данных проще генератором треугольника сделать, который многократно проще реализуется.


    1. Pisikak Автор
      04.12.2024 14:59

      Спасибо, тогда попробую и посмотрю что получится.


  1. vadimr
    04.12.2024 14:59

    А прямо в питоне нельзя было посчитать синус? Зачем тут матлаб?


    1. Pisikak Автор
      04.12.2024 14:59

      В питоне тоже можно. Просто я когда-то это сделал в matlab и решил поделиться таким способом в статье.


    1. slog2
      04.12.2024 14:59

      А прямо в питоне нельзя было посчитать синус? Зачем тут матлаб?

      Более того, чтобы получить синус не нужны FPGA и ПЛИС. Но это же не интересно :)


  1. VT100
    04.12.2024 14:59

    Рекомендую "A Technical Tutorial on Digital Signal Synthesis." Кратко:

    Разрядность данных таблицы синуса - разрядность ЦАП плюс 2 бита;

    Использовать симметрию синуса для сжатия ROM;

    Фильтровать выход ЦАП.


  1. Va_sil
    04.12.2024 14:59

    Вероятно самый громоздкий и дорогой генератор синуса . Китайский модуль за 2 бакса умеет ... ну вы знаете . Зачем тут ПЛИС ? Реализуется на голой stm32 / esp32 без вот этого вот всего если Вы хотите в математику или на 1 транзисторе и колебательном контуре со средней точкой . Без питонов и прочих .


  1. alekseypro
    04.12.2024 14:59

    Даёшь PSK31 в массы на МК или ПЛИС