В этой статье описан способ генерации синусоидального сигнала на ПЛИС через использование 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. Всё это доступно на схемах:
//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.
По двойному щелчку по pROM откроется диалоговое окно, в котором указываются необходимые параметры. В примере использованы 4096 12-битных отсчётов.
Необходимо обратить внимание на то, что ЦАП DAC904 14-битный и для него необходимо использовать 14-битные отсчёты, но лень.
Процедура генерации синусоидального сигнала в 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.
Для использования файла коэффициентов в качестве 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. Необходимо только вручную удалить запятую в самом конце файла:
Теперь файл готов для использования в качестве Memory Initialization File в диалоговом окне IP Customization pROM ide GOWIN FPGA Designer. GOWIN FPGA Designer предложит добавить сгенерированные файлы в текущий проект. Остаётся только согласиться:
Вернёмся к генератору синуса. Создадим ещё один 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 как в этой статье, то получится такое изображение:
На изображении сигнал с частотой 2500 КГц. Если воспользоваться формулой в комментарии на строке 8 в модуле dds_addr , и подставить значение 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. - прим. Авт.).
Данный ЦАП на отладочной плате допускает подключение питания и логики 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-омным, поэтому посмотреть его обычным щупом осциллографа не получится. С помощью специального щупа на выходе можно увидеть такой сигнал:
Чтобы изменить частоту в модуль dds_addr_inst (подключён в модуле top. - прим. Авт.) необходимо передать другое значение. Например, на выходе необходима частота 5 МГц. Из формулы необходимо вывести неизвестную . 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)
);
//----------------------------------------------------------
// ...
И получим вот такой сигнал:
Сигнал уже не такой "красивый", но его частота 5 МГц.
Скрытый текст
Дальнейшее увеличение частоты делает изображение ещё более непривлекательным:
А уменьшение частоты снова приводит к красивым картинкам:
Получается, что, изменяя значение 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:
На этом всё. Такой генератор синуса использовался на FPGA Xilinx для отладки софта, который отвечал за передачу на компьютер принятого тестового сигнала радиочастотной микросхемой. Чтобы понять, что микросхема сконфигурирована правильно и работает корректно на приём, необходимо было визуализировать сигнал, который она приняла. А для этого его необходимо было передать на компьютер. И когда было непонятно, а что именно не работает - микросхема на приём или канал передачи данных, генератор синуса очень помог, потому что был использован в качестве заведомого рабочего источника сигнала. Ещё генератор синуса очень пригодился, когда отлаживался канал передачи данных из ПЛИС Xilinx через USB-to-FIFO микросхему на компьютер. Имея заведомо рабочий генератор сигнала, можно с высокой вероятностью утверждать работает канал передачи данных или не работает. В этот раз генератор помог с передачей информации в КВ диапазоне. Хочется добавить, что подобным образом запросто можно реализовать передачу сигналов азбуки Морзе, FT8 и ещё многих других видов сигналов и модуляций. Причём имея радиолюбительский позывной, качественную антенну, усилитель и фильтр, удаляющий паразитные (вторые, третьи и т.д.) гармоники и зеркальный канал, можно передавать свой сигнал на очень существенные расстояния, в основном ночью, но это уже другая история.
Спасибо.
С. Н.
Комментарии (10)
vadimr
04.12.2024 14:59А прямо в питоне нельзя было посчитать синус? Зачем тут матлаб?
Pisikak Автор
04.12.2024 14:59В питоне тоже можно. Просто я когда-то это сделал в matlab и решил поделиться таким способом в статье.
slog2
04.12.2024 14:59А прямо в питоне нельзя было посчитать синус? Зачем тут матлаб?
Более того, чтобы получить синус не нужны FPGA и ПЛИС. Но это же не интересно :)
VT100
04.12.2024 14:59Рекомендую "A Technical Tutorial on Digital Signal Synthesis." Кратко:
Разрядность данных таблицы синуса - разрядность ЦАП плюс 2 бита;
Использовать симметрию синуса для сжатия ROM;
Фильтровать выход ЦАП.
Va_sil
04.12.2024 14:59Вероятно самый громоздкий и дорогой генератор синуса . Китайский модуль за 2 бакса умеет ... ну вы знаете . Зачем тут ПЛИС ? Реализуется на голой stm32 / esp32 без вот этого вот всего если Вы хотите в математику или на 1 транзисторе и колебательном контуре со средней точкой . Без питонов и прочих .
avitek
Используя нехитрые целочисленные математические приёмы, можно ощутимо улучшить качество синуса.
Pisikak Автор
Спасибо!