В статье описывается опыт использования ARM ядра, встроенного в ПЛИС GOWIN GW1NSR-4C, в качестве процессора общего назначения для формирования PSK31 сигнала. Сигнал формируется с помощью генератора синуса, который был описан в предыдущей статье. Используются отладочная плата LilyGO T-FPGA, в составе которой ПЛИС GW1NSR-LV4CQN48PC6/I5, ЦАП на основе DAC904, ide GOWIN FPGA Designer и образовательная версия GMD.
Дисклеймер
Туториал описывает процесс радиопередачи, поэтому прежде чем шалить, ознакомьтесь с нормативно-правовой базой государства, на территории которого предполагается шалость, получите радиопозывной и избавьтесь от паразитных гармоник.Здесь будут описаны конкретный опыт за последние пару дней и несколько экспериментов с отладочными платами. Это не опыт и эксперименты профессионального разработчика под ПЛИС и микроконтроллеры. Может быть, это прочитают специалисты, которые хорошо разбираются в затронутых темах и дадут конструктивную критику.
При возникновении вопросов по запуску ARM ядра в ПЛИС GOWIN изучение официального сайта GOWIN приведёт на страницу GITHUB. В файле README достаточно подробно описан процесс запуска ARM ядра в ПЛИС GW1NSR-LV4CQN48PC6/I5 и несколько программ для примера.
Для управления генератором синуса в ПЛИС и формирования модуляции psk можно использовать встроенное ARM ядро Cortex-m3. Чтобы связать логику процессора и логику ПЛИС можно использовать SPI интерфейс. При добавлении в дизайн IP MCU необходимо дополнительно включить SPI шину:
В качестве проверки работы можно использовать пример от LiLyGO, в котором управление миганием светодиода, подключённого к FPGA, осуществляется из микроконтроллера. В оригинальном примере в качестве микроконтроллера выступает ESP32-S3, размещённый на отладочной плате. Для текущего эксперимента предлагается использовать встроенный в ПЛИС Cortex-M3.
На самом деле автором было проделано больше промежуточных экспериментов. Например, при первом запуске Cortex-M3 в ПЛИС было запрограммировано классическое мигание светодиодом прямо из микроконтроллера. Для этого сигнал
led
был включён прямо в экземпляр Gowin_EMPU_Top.gpio(led)
(не выходной регистрoutput reg led
как в примере, а именноoutput led
. Иначе не пройдёт синтез, потому что экземпляр Cortex-M3 не допускает включение регистров в качестве GPIO. - прим. Авт.) .
Промежуточный эксперимент. Cortex-M3 SPI blink
После добавления экземпляра MCU в файл дизайна верхнего уровня необходимо подключить сигналы MCU к соответствующим портам верхнего уровня.
wire cs;
wire sck;
wire MOSI;
wire MISO;
wire [7:0]gpio;
//--------Copy here to design--------
Gowin_EMPU_Top cortexM3_inst(
.sys_clk(clk_60M), //input sys_clk
.gpio(gpio[7:0]), //inout [15:0] gpio
.uart0_rxd(1'b1), //input uart0_rxd
.uart0_txd(UART_TX), //output uart0_txd
.mosi(MOSI), //output mosi
.miso(MISO), //input miso
.sclk(sck), //output sclk
.nss(cs), //output nss
.reset_n(1'b1) //input reset_n
);
Здесь тактовый сигнал - это выход из экземпляра PLL. Работа с этими IP описана и в туториале про использование Cortex-M3 в ПЛИС, и в блоге Marsohod.org. GPIO объявлены как wire, потому что в проекте они не используются. Сигналы SPI объявлены как wire. Микроконтроллер встроен внутрь ПЛИС, поэтому выводить сигналы для связи процессорной логики и логики ПЛИС наружу совсем необязательно.Всё остальное подключено как в оригинальной статье. После объявления constraints и проверки синтеза, имплементации и генерации bitstream на наличие ошибок можно приступить к программированию микроконтроллера. Как и в оригинальной статье будет использована Gowin's MCU Designer.
Под спойлером полный исходный код для SPI blink со стороны ПЛИС.
Скрытый текст
module led(
input clk,
input rst,
output reg led,
output UART_TX,
output rxd_flag
);
wire cs;
wire sck;
wire MOSI;
wire MISO;
wire [7:0] rxd_out;
//--------------------------------------------
reg [7:0] txd_dat;
wire clk_60M;
//--------------------------------------------
wire pll_out_clk;
//-------------------------------------------
wire [7:0]gpio;
//--------Copy here to design--------
Gowin_EMPU_Top cortexM3_inst(
.sys_clk(clk_60M), //input sys_clk
.gpio(gpio[7:0]), //inout [15:0] gpio
.uart0_rxd(1'b1), //input uart0_rxd
.uart0_txd(UART_TX), //output uart0_txd
.mosi(MOSI), //output mosi
.miso(MISO), //input miso
.sclk(sck), //output sclk
.nss(cs), //output nss
.reset_n(1'b1) //input reset_n
);
//--------Copy end-------------------
always@(posedge rxd_flag or negedge rst)begin
if(!rst)
txd_dat <= 8'b11000011;
else
begin
txd_dat <= rxd_out + 1'b1; //отправить данные +1 отправителю
end
end
always@(posedge rxd_flag or negedge rst)begin
if(!rst)
led<=1'b0;
else if(rxd_out<8'h80)
begin
led<=1'b1;
end
else
begin
led<=1'b0;
end
end
//--------Copy here to design--------
Gowin_PLLVR1 your_instance_name(
.clkout(clk_60M), //output clkout
.clkin(clk) //input clkin
);
//--------Copy end-------------------
spi_slaver spi_slaver1(
.clk(clk_60M), // clk_30M
.rst(rst),
.cs(cs),
.sck(sck),
.MOSI(MOSI),
.MISO(MISO),
.rxd_out(rxd_out),
.txd_data(txd_dat),
.rxd_flag(rxd_flag)
);
endmodule
Под этим спойлером файл .cst, который можно использовать для SPI blink.
Скрытый текст
//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 "led" 15;
IO_PORT "led" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8;
IO_LOC "clk" 45;
IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=UP;
IO_LOC "rst" 46;
IO_PORT "rst" IO_TYPE=LVCMOS33 PULL_MODE=UP;
IO_LOC "UART_TX" 22;
IO_PORT "UART_TX" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8 OPEN_DRAIN=ON;
//-------------------------------------------------------------
IO_LOC "rxd_flag" 42;
IO_PORT "rxd_flag" IO_TYPE=LVCMOS33 PULL_MODE=NONE DRIVE=8;
Прошивка на языке С в IDE GMD
В оригинальном туториале предлагается ссылка для скачивания Gowin's MCU Designer. Но у автора не получилось скачать по данной ссылке программу по неизвестной причине. На сайте GOWIN необходимое ПО скачивается без каких-либо проблем.
По аналогии с оригинальным туториалом в комплекте SDK от Gowin для GW1NS(R)-4C можно обнаружить пример для работы с SPI. Отличием от оригинала является другая тактовая частота работы микроконтроллера. В настоящей статье предлагается использовать 60 МГц. Эта частота была настроена в экземпляре PLL (.clkout(clk_60M), //output clkout
. - прим. Авт.) и подключена в экземпляр MCU через wire clk_60M;
. Именно эту частоту необходимо будет указать в файле lib/CMSIS/DeviceSupport/system/system_gw1ns4c.c
.
#define __SYSTEM_CLOCK (60000000UL) /* 60MHz */
В туториале указано, что ядро микроконтроллера может тактироваться и от 80 МГц, но в качестве опорного тактового генератора на отладочной плате T-FPGA выбран кварц с частотой 27 МГц. В примерах для работы с SPI есть делители на 8, 6, 4, 3 и 2, но IP generator PLL не может подобрать делители для 80 МГц. Зато может подобрать для 60 МГц из 27 МГц и тогда для SPI можно использовать делитель 6.
Теперь, если скопировать код под спойлером в main.c, светодиод, который подключён к логике ПЛИС, начнёт мигать под управлением Cortex-M3. Как и в оригинальном примере через SPI поочерёдно отправляется значение 0x01 или 0x81 и в зависимости от этого логика FPGA переключает состояние светодиода.
Скрытый текст
/*
* *****************************************************************************************
*
* Copyright (C) 2014-2021 Gowin Semiconductor Technology Co.,Ltd.
*
* @file main.c
* @author Embedded Development Team
* @version V1.x.x
* @date 2021-01-01 09:00:00
* @brief Main program body.
******************************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "gw1ns4c.h"
#include <stdio.h>
/* Includes ------------------------------------------------------------------*/
void SPIInit(void);
void initializeUART();
void initializeTimer();
void delayMillis(uint32_t ms);
/* Functions ------------------------------------------------------------------*/
int main(void)
{
SystemInit(); //Initializes system
SPIInit(); //Initializes SPI
initializeUART();
initializeTimer();
SPI_Select_Slave(0x01) ; //Select The SPI Slave
SPI_WriteData(0x01); //Send Jedec
printf("init complete\r\n");
uint32_t counter = 0;
while(1)
{
counter++;
printf("GowinFPGA says hi! Count: %d\r\n", counter);
if(~SPI_GetToeStatus() && SPI_GetTrdyStatus() == 1)
{
SPI_WriteData(0x81);
}
delayMillis(500);
if(~SPI_GetToeStatus() && SPI_GetTrdyStatus() == 1)
{
SPI_WriteData(0x01);
}
delayMillis(500);
}
}
//Initializes SPI
void SPIInit(void)
{
SPI_InitTypeDef init_spi;
init_spi.CLKSEL= CLKSEL_CLK_DIV_6; //60MHZ / 6
init_spi.DIRECTION = DISABLE; //MSB First
init_spi.PHASE =DISABLE; //ENABLE;//posedge
init_spi.POLARITY =DISABLE; //polarity 0
SPI_Init(&init_spi);
}
//Initializes UART0
void initializeUART()
{
UART_InitTypeDef uartInitStruct;
//Enable transmission
uartInitStruct.UART_Mode.UARTMode_Tx = ENABLE;
//Disable reception
uartInitStruct.UART_Mode.UARTMode_Rx = DISABLE;
//9600 baud rate typical of Arduinos
uartInitStruct.UART_BaudRate = 9600;
//Initialize UART0 using the struct configs
UART_Init(UART0, &uartInitStruct);
}
void initializeTimer() {
TIMER_InitTypeDef timerInitStruct;
timerInitStruct.Reload = 0;
//Disable interrupt requests from timer for now
timerInitStruct.TIMER_Int = DISABLE;
//Disable timer enabling/clocking from external pins (GPIO)
timerInitStruct.TIMER_Exti = TIMER_DISABLE;
TIMER_Init(TIMER0, &timerInitStruct);
TIMER_StopTimer(TIMER0);
}
#define CYCLES_PER_MILLISEC (SystemCoreClock / 1000)
void delayMillis(uint32_t ms) {
TIMER_StopTimer(TIMER0);
TIMER_SetValue(TIMER0, 0); //Reset timer just in case it was modified elsewhere
TIMER_EnableIRQ(TIMER0);
uint32_t reloadVal = CYCLES_PER_MILLISEC * ms;
//Timer interrupt will trigger when it reaches the reload value
TIMER_SetReload(TIMER0, reloadVal);
TIMER_StartTimer(TIMER0);
//Block execution until timer wastes the calculated amount of cycles
while (TIMER_GetIRQStatus(TIMER0) != SET);
TIMER_StopTimer(TIMER0);
TIMER_ClearIRQ(TIMER0);
TIMER_SetValue(TIMER0, 0);
}
Теперь, когда микропроцессор инициализируется и данные передаются в ПЛИС, можно приступить к проектированию передатчика PSK31.
Коротковолновый радиопередатчик PSK31
В проекте используется DAC904 на отладочной плате (как и в прошлой статье. - прим. Авт.). При программировании PSK был использован исходный код из репозитория GitHub. Для формирования PSK модуляции необходимо изменять фазу. Для используемого генератора это возможно. Чтобы управлять фазой, необходимо в модуле dds_addr
сделать PWORD
входным сигналом:
module dds_addr (clk, rst_n, addr_out, strobe, FWORD, PWORD);
input clk, rst_n; // Resetting the system clock
input [31:0] FWORD;
input [15:0] PWORD;
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
// parameter FWORD = 3316669189; // слово управления частотой F_out = B * (F_clk / 2 ** 32), fword = B 5KHZ // 858994
reg [N-1: 0] addr; // 32-bit battery
// reg [11:0] addr;
reg [15:0] pword;
always @ (posedge clk)begin
pword <= PWORD;
end
reg [31:0] fword;
always @ (posedge clk)begin
fword <= FWORD;
end
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
// addr <= addr + 1;
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
При подключении экземпляра dds_addr
в модуль верхнего уровня необходимо передавать значение фазы. Фаза может быть задана значением 2^12
. Это значит, что единичная окружность разбита на 4096 значений и чтобы задать фазу сигнала, необходимо рассчитать значение PWORD
. PWORD = (x/360) * 4096. Где это может быть использовано? Известно, что SIN и COS различаются по фазе сигнала на 90 градусов. Тогда, воспользовавшись формулой (90/360)*4096=1024. То есть если в модуле верхнего уровня использовать два экземпляра dds_addr и в одном задать значение PWORD=0
, а в другом PWORD=1024
, эти два сигнала будут различаться по фазе на 90 градусов.
// --------------Phase-based module------------------------
dds_addr dds_addr_inst (
.clk(clk), // input wire clk
.rst_n(1'b1), // input wire rst_n // 1 enable
.addr_out(addr_out), // output wire [11 : 0] addr_out
.strobe(strobe_sin),
.FWORD(32'd1613094272), // F_out = B * (F_clk / 2 ** 32) частота равна 10.1406 КГц
.PWORD(16'd2048) // (x/360) * 4096 фаза равна Pi
);
//----------------------------------------------------------
В случае коротковолнового передатчика частота и фаза рассчитываются в микроконтроллере и передаются по SPI. Для приёма значения частоты на стороне FPGA можно использовать немного переписанный конечный автомат из прошлой статьи:
reg [31:0] fword;
reg [31:0] oneBytes_f;
reg [31:0] twoBytes_f;
reg [31:0] thrBytes_f;
reg [ 3:0] state_reg_f;
always@(posedge rxd_flag or negedge rst)begin
if(!rst)begin
fword <= 1'b0;
end
else if(rxd_out==8'h01)
begin
state_reg_f <= 0;
oneBytes_f <= 0;
end
else
begin
case(state_reg_f)
4'd0: begin
oneBytes_f <= oneBytes_f + rxd_out;
state_reg_f <= 1;
end
4'd1: begin
twoBytes_f <= oneBytes_f + (rxd_out << 8); // +tmp
state_reg_f <= 2;
end
4'd2: begin
thrBytes_f <= twoBytes_f + (rxd_out << 16); // +tmp
state_reg_f <= 3;
end
4'd3: begin
fword <= thrBytes_f + (rxd_out << 24); // +tmp
state_reg_f <= 4;
// led <= !led;
end
default: begin
state_reg_f <= 4;
end
endcase
end
end
А для приёма значения фазы использовать вот такой:
reg [15:0] oneBytes_p;
reg [15:0] pword_reg;
reg [ 3:0] state_reg_p = 4;
always@(posedge rxd_flag or negedge rst)begin
if(!rst)begin
pword_reg <= 0;
end
else if(rxd_out==8'h02)
begin
state_reg_p <= 0;
end
else
begin
case(state_reg_p)
4'd0: begin
oneBytes_p <= rxd_out;
state_reg_p <= 1;
end
4'd1: begin
pword_reg <= oneBytes_p + (rxd_out << 8);
state_reg_p <= 4; // "другое" состояние, чтобы частота не мешалась
led <= !led;
end
default: begin
state_reg_p <= 4; // "другое" состояние, чтобы частота не мешалась
end
endcase
end
end
Весь модуль верхнего уровня под спойлером.
Скрытый текст
module led(
input clk,
input rst,
output reg led,
output [11: 0] sin,
output UART_TX,
output clk_o,
output rxd_flag
);
wire cs;
wire sck;
wire MOSI;
wire MISO;
wire [7:0] rxd_out;
//--------------------------------------------
reg [7:0] txd_dat;
wire clk_60M;
//--------------------------------------------
wire [11: 0] addr_out;
wire pll_out_clk;
//-------------------------------------------
wire [7:0]gpio;
//--------Copy here to design--------
Gowin_EMPU_Top cortexM3_inst(
.sys_clk(clk_60M), //input sys_clk
.gpio(gpio[7:0]), //inout [15:0] gpio
.uart0_rxd(1'b1), //input uart0_rxd
.uart0_txd(UART_TX), //output uart0_txd
.mosi(MOSI), //output mosi
.miso(MISO), //input miso
.sclk(sck), //output sclk
.nss(cs), //output nss
.reset_n(1'b1) //input reset_n
);
//--------Copy end-------------------
//assign led = gpio[7];
always@(posedge rxd_flag or negedge rst)begin
if(!rst)
txd_dat <= 8'b11000011;
else
begin
txd_dat <= rxd_out + 1'b1; //отправить данные +1 отправителю
end
end
reg [31:0] fword;
reg [31:0] oneBytes_f;
reg [31:0] twoBytes_f;
reg [31:0] thrBytes_f;
reg [ 3:0] state_reg_f;
always@(posedge rxd_flag or negedge rst)begin
if(!rst)begin
fword <= 1'b0;
end
else if(rxd_out==8'h01)
begin
state_reg_f <= 0;
oneBytes_f <= 0;
end
else
begin
case(state_reg_f)
4'd0: begin
oneBytes_f <= oneBytes_f + rxd_out;
state_reg_f <= 1;
end
4'd1: begin
twoBytes_f <= oneBytes_f + (rxd_out << 8); // +tmp
state_reg_f <= 2;
end
4'd2: begin
thrBytes_f <= twoBytes_f + (rxd_out << 16); // +tmp
state_reg_f <= 3;
end
4'd3: begin
fword <= thrBytes_f + (rxd_out << 24); // +tmp
state_reg_f <= 4;
// led <= !led;
end
default: begin
state_reg_f <= 4;
end
endcase
end
end
reg [15:0] oneBytes_p;
reg [15:0] pword_reg;
reg [ 3:0] state_reg_p = 4;
always@(posedge rxd_flag or negedge rst)begin
if(!rst)begin
pword_reg <= 0;
end
else if(rxd_out==8'h02)
begin
state_reg_p <= 0;
end
else
begin
case(state_reg_p)
4'd0: begin
oneBytes_p <= rxd_out;
state_reg_p <= 1;
end
4'd1: begin
pword_reg <= oneBytes_p + (rxd_out << 8);
state_reg_p <= 4; // "другое" состояние, чтобы частота не мешалась
led <= !led;
end
default: begin
state_reg_p <= 4; // "другое" состояние, чтобы частота не мешалась
end
endcase
end
end
//--------Copy here to design--------
Gowin_PLLVR1 your_instance_name(
.clkout(clk_60M), //output clkout
.clkin(clk) //input clkin
);
spi_slaver spi_slaver1(
.clk(clk_60M), // clk_30M
.rst(rst),
.cs(cs),
.sck(sck),
.MOSI(MOSI),
.MISO(MISO),
.rxd_out(rxd_out),
.txd_data(txd_dat),
.rxd_flag(rxd_flag)
);
//-------------------------------------------
// --------------Phase-based module------------------------
dds_addr dds_addr_inst (
.clk(clk), // input wire clk
.rst_n(1'b1), // input wire rst_n // 1 enable
.addr_out(addr_out), // output wire [7 : 0] addr_out
.strobe(strobe_sin),
.FWORD(fword), // fword // fword_valid
.PWORD(pword_reg) // tmp5 // 16'd2048
);
//----------------------------------------------------------
// 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 // 0 enable
.ad(addr_out) //input [11:0] ad
);
assign clk_o = clk;
endmodule
Теперь программирование микроконтроллера. В первую очередь в проект для Cortex-M3 необходимо включить файлы hfbeacon.c
и hfbeacon.h
. Файлы отличаются от оригинальных отсутствием классов и способом изменения частоты и фазы генератора сигнала. В структурах языка C отсутствует понятие методов, поэтому для отправки значений частоты и фазы в генератор реализована простая функция checkGen(struct Gen *gen)
. Когда функция вызывается из hfbeacon.c
, проверяется какое значение (частоты и/или фазы. - прим. Авт.) изменилось, и именно оно передаётся по SPI в логику ПЛИС.
extern uint8_t checkGen(struct Gen *gen){
uint8_t res = 0;
if(old_freq != gen->freq){
res = 1;
send_frequency(&gen->freq);
old_freq = gen->freq;
}
if(old_phase != gen->phase){
res = 2;
// printf("old_phase\r\n");
send_phase(&gen->phase);
old_phase = gen->phase;
}
return res;
}
Значение частоты передаётся в функцию void send_frequency(uint32_t *freq)
из прошлой статьи. Функция такая же, но с учётом работы с SPI в Cortex-M3 в ПЛИС GW1NSR-4C. Значение фазы передаётся в свою аналогичную функцию подобным образом только с тем отличием, что это значение 2-байтовое.
Перед экспериментами с T-FPGA был проведён промежуточный эксперимент с Arduino Nano и AD9833 на фиолетовой отладочной плате. Исходный код этого эксперимента доступен по ссылке. Если обратить внимание на исходный код в файле
hfbeacon.c
, то там можно найти закомментированные строчки работы и с ad9850, и ad9833. Там очень хорошо видны особенности формирования значения фазы вhfbeacon.c
и передачи этого значения в ad9833 в формате угла в градусах на окружности.
В файле hfbeacon.c
значение фазы формируется в формате 5-битного значения. Это значит, что окружность разбита на 32 части по 11,25 градуса (2^5=32, 360/32=11,25. - прим. Авт.). То есть чтобы перевести значение фазы из hfbeacon.c
в градусы, необходимо умножить его на 11.25. Тогда, учитывая особенности настройки фазы, полученное значение угла в градусах делится на 360, а результат умножается на 4096. И именно это значение передаётся по SPI в логику ПЛИС. Перед отправкой значения фазы передаётся признак 0x02
того, что сейчас будет передаваться значение фазы.
void send_phase(uint32_t *phase){
// printf("send_phase\r\n");
uint32_t pword;
pword=(((float)(*phase)*11.25)/360)*4096;
uint8_t buff[2];
buff[0] = pword & 0xff;
buff[1] = pword >> 8 & 0xff;
if(~SPI_GetToeStatus() && SPI_GetTrdyStatus() == 1)
{
SPI_WriteData(0x02); // признак передачи значения фазы
}
for(uint8_t i=0;i<2;++i){
if(~SPI_GetToeStatus() && SPI_GetTrdyStatus() == 1)
{
SPI_WriteData(buff[i]);
}
}
}
Под спойлером файл hfbeacon.c
Скрытый текст
#include <hfbeacon.h>
/***************************************************************************
* CW
***************************************************************************/
void cwTx(long freqCw, char * stringCw, int cwWpm, struct Gen *gen){ // AD9833 *genPtr
static int const morseVaricode[2][59] = { // PROGMEM
{0,212,72,0,144,0,128,120,176,180,0,80,204,132,84,144,248,120,56,24,8,0,128,192,224,240,224,168,0,136,0,48,104,64,128,160,128,0,32,192,0,0,112,160,64,192,128,224,96,208,64,0,128,32,16,96,144,176,192},
{7,6,5,0,4,0,4,6,5,6,0,5,6,6,6,5,5,5,5,5,5,5,5,5,5,5,6,6,0,5,0,6,6,2,4,4,3,1,4,3,4,2,4,3,4,2,2,3,4,4,3,3,1,3,4,3,4,4,4}
};
int tempo = 1200 / cwWpm; // Duration of 1 dot
uint8_t nb_bits,val;
int d;
int c = *stringCw++;
while(c != '\0'){
c = toupper(c); // Uppercase
if(c == 32){ // Space character between words in string
// DDS.setfreq(0,0); // 7 dots length spacing
// genPtr->ApplySignal(SQUARE_WAVE,REG0,0); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = 0;
checkGen(gen);
delayMillis(tempo * 7); // between words
}
else if (c > 32 && c < 91) {
c = c - 32;
d = morseVaricode[0][c]; // Get CW varicode // int(pgm_read_word(& ))
nb_bits = morseVaricode[1][c]; // Get CW varicode length // int(pgm_read_word(& ))
if(nb_bits != 0){ // Number of bits = 0 -> invalid character #%<>
for(int b = 7; b > 7 - nb_bits; b--){ // Send CW character, each bit represents a symbol (0 for dot, 1 for dash) MSB first
val=bitRead(d,b); //look varicode
// DDS.setfreq(freqCw,0); // Let's transmit
// gen.ApplySignal(SQUARE_WAVE,REG0,((freqCw)*1000ul)); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
// genPtr->ApplySignal(SQUARE_WAVE,REG0,(freqCw)); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = freqCw;
checkGen(gen);
delayMillis(tempo + 2 * tempo * val); // A dot length or a dash length (3 times the dot)
// DDS.setfreq(0,0); // 1 dot length spacing
// genPtr->ApplySignal(SQUARE_WAVE,REG0,0); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = 0;
checkGen(gen);
delayMillis(tempo); // between symbols in a character
}
}
// DDS.setfreq(0,0); // 3 dots length spacing
// genPtr->ApplySignal(SQUARE_WAVE,REG0,0); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = 0;
checkGen(gen);
delayMillis(tempo * 3); // between characters in a word
}
c = *stringCw++; // Next caracter in string
}
// DDS.setfreq(0, 0); // No more transmission
// genPtr->ApplySignal(SQUARE_WAVE,REG0,0); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = 0;
checkGen(gen);
}
/********************************************************
* PSK
********************************************************/
#if 1
void pskTx(long freqPsk, char * stringPsk, int modePsk, int baudsPsk, struct Gen *gen) // AD9833 *genPtr
{
static int const PskVaricode[2][128] = { // PROGMEM
{683,731,749,887,747,863,751,765,767,239,29,879,733,31,885,939,759,757,941,943,859,875,877,
855,891,893,951,853,861,955,763,895,1,511,351,501,475,725,699,383,251,247,367,479,117,53,
87,431,183,189,237,255,375,347,363,429,427,439,245,445,493,85,471,687,701,125,235,173,181,
119,219,253,341,127,509,381,215,187,221,171,213,477,175,111,109,343,437,349,373,379,685,503,
495,507,703,365,735,11,95,47,45,3,61,91,43,13,491,191,27,59,15,7,63,447,21,23,5,55,123,107,
223,93,469,695,443,693,727,949},
{10,10,10,10,10,10,10,10,10,8,5,10,10,5,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
10,1,9,9,9,9,10,10,9,8,8,9,9,7,6,7,9,8,8,8,8,9,9,9,9,9,9,8,9,9,7,9,10,10,7,8,8,8,7,8,8,9,7,
9,9,8,8,8,8,8,9,8,7,7,9,9,9,9,9,10,9,9,9,10,9,10,4,7,6,6,2,6,7,6,4,9,8,5,6,4,3,6,9,5,5,3,6,
7,7,8,7,9,10,9,10,10,10}
};
static int const QpskConvol[32] = {16,8,-8,0,-8,0,16,8,0,-8,8,16,8,16,0,-8,8,16,0,-8,0,-8,8,16,-8,0,16,8,16,8,-8,0}; // PROGMEM
int shreg = 0; // Shift register qpsk
int phase = 0;
if(0) // rsidTxEnable == 1
{
// 0 bpsk31
// 1 qpsk31
// 2 bpsk63
rsidTx(freqPsk, (baudsPsk >> 4) - (modePsk == 'B'), gen); // 3 qpsk63 // genPtr
// 6 bpsk125
// 7 qpsk125
}
pskIdle(freqPsk, baudsPsk, gen); // A little idle on start of transmission for AFC capture //
uint8_t nb_bits,val;
int d,e;
int c = *stringPsk++;
while (c != '\0')
{
d = PskVaricode[0][c]; // Get PSK varicode // int(pgm_read_word(& ))
nb_bits = PskVaricode[1][c]; // Get PSK varicode length // int(pgm_read_word(& ))
d <<= 2; //add 00 on lsb for spacing between caracters
e = d;
for(int b = nb_bits + 2; b >= 0; b--) //send car in psk
{
val=bitRead(e,b); //look varicode
if(modePsk == 'B') // BPSK mode
{
if (val == 0)
{
phase = (phase ^ 16) &16; // Phase reverted on 0 bit
}
}
else if(modePsk == 'Q'){ // QPSK mode
shreg = (shreg << 1) | val; // Loading shift register with next bit
d=(int)QpskConvol[shreg & 31]; // Get the phase shift from convolution code of 5 bits in shit register // int(pgm_read_word(& ))
phase = (phase + d) & 31; // Phase shifting
}
// DDS.setfreq(freqPsk, phase); // Let's transmit
// gen.ApplySignal(SQUARE_WAVE,REG0,((freqPsk)*1000ul), REG0,phase); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
// genPtr->ApplySignal(SQUARE_WAVE,REG0,freqPsk, REG0,(float)phase*11.25); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = freqPsk;
gen->phase = phase; // (float)phase*11.25;
checkGen(gen);
delayMillis((961 + baudsPsk) / baudsPsk); // Gives the baud rate
}
c = *stringPsk++; // Next caracter in string
}
pskIdle(freqPsk, baudsPsk, gen); // A little idle to end the transmission // genPtr
// DDS.setfreq(0, 0); // No more transmission
// genPtr->ApplySignal(SQUARE_WAVE,REG0,0); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = freqPsk;
checkGen(gen);
}
#endif
#if 1
void pskIdle(long freqIdle, int baudsIdle, struct Gen *gen) // AD9833 *genPtr
{
int phaseIdle = 0;
for(int n = 0; n < baudsIdle; n++)
{
phaseIdle = (phaseIdle ^ 16) & 16; // Idle is a flow of zeroes so only phase inversion
// DDS.setfreq(freqIdle, phaseIdle); // Let's transmit
// gen.ApplySignal(SQUARE_WAVE,REG0,((freqIdle)*1000ul), REG0,phaseIdle); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
// genPtr->ApplySignal(SQUARE_WAVE,REG0,freqIdle, REG0,(float)phaseIdle*11.25); // SINE_WAVE // SQUARE_WAVE // HALF_SQUARE_WAVE
gen->freq = freqIdle;
gen->phase = phaseIdle; // (float)phase*11.25;
checkGen(gen);
delayMillis((961 + baudsIdle) / baudsIdle); // Gives the baud rate
}
}
#endif
Под спойлером файл hfbeacon.h
Скрытый текст
#ifndef HFBEACON_H
#define HFBEACON_H
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#include <stdbool.h>
#include <ctype.h>
#include <stdio.h>
#if 1
struct Gen{
uint32_t freq;
uint32_t phase;
};
#endif
void rsidToggle(bool rsidEnable);
void cwTx(long freqCw, char * stringCw, int cwWpm, struct Gen*); // , AD9833*
void pskTx(long freqPsk, char * stringPsk, int modePsk, int baudsPsk, struct Gen*); // , AD9833*
void rttyTx(long freqRtty, char * stringRtty);
void hellTx(long freqHell, char * stringHell);
void wsprTx(long freqWspr, char * callWsprTx, char * locWsprTx, char * powWsprTx);
void wsprEncode(char * callWsprProc, char * locWsprProc, char * powWsprProc);
void ddsPower(int powDds, struct Gen*); // , AD9833*
uint8_t wsprSymb[162];
int wsprSymbGen;
void rsidTx(long freqRsid, int modeRsid, struct Gen*); // , AD9833*
void pskIdle(long freqIdle, int baudsIdle, struct Gen*); // , AD9833*
void rttyTxByte (long freqRttyTxbyte, char c);
uint8_t parity(unsigned long tempo);
//uint8_t rsidTxEnable = 0;
//extern HFBEACON Beacon;
#endif
Перед psk модуляцией запускалась обычная морзянка для отладки. На этом этапе и были пойманы баги, после которых конечный автомат стал переводиться в "4" состояние. В оригинальном файле реализовано ещё несколько модуляций. Используя описанные примеры, не составит труда, применить их в своих проектах.
Исходный код проекта для ПЛИС доступен на GItHub в ветке feature-articleCortexM3
, а для Cortex-M3 в ветке feature-articleBpsk31
. Теперь, вызывая в main.c
функцию pskTx(freq, txString, 'B', 31, &gen);
из приёмного динамика станет доноситься необходимый звук:
Для декодирования снова можно использовать MyltiPSK
Сообщения декодируются, а это значит, что Cortex-M3 в ПЛИС GW1NSR-LV4CQN48PC6/I5 инициализируется и передаёт посчитанные значения через SPI в генератор сигналов в ПЛИС. Встроенные в FPGA микроконтроллеры открывают большие возможности для использования в различных проектах. Например, при использовании радиочастотной микросхемы-трансивера AD9361 очень удобно инициализировать её из встроенного в ZYNQ-7000 ARM ядра или софт-процессора Microblaze на языке С, тогда как приём, передача и/или обработка IQ сигнала ведётся в логике FPGA. А как Вы используете встроенные в ПЛИС микропроцессоры?
Спасибо.
С. Н.
lesha108
Прочитав последний абзац, хотел бы узнать о том как китайские LibreSDR и аналоги программировать
Pisikak Автор
В статье нашел краткое описание LibreSDR, в основе которого ad9363, а это значит, что можно пользоваться примерами Analog Devices. На wiki AD написано очень много чего интересного, а в их репозиториях есть примеры hdl описаний и прошивок для ARM ядра ZYNQ или софт-процессора Microblaze. Там же есть примеры работы из под Linux для ZYNQ.