В статье описывается опыт использования 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 шину:

Рис. Настройка SPI в IP generator
Рис. Настройка SPI в IP generator

В качестве проверки работы можно использовать пример от 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

Рис. Интерфейс MyltiPSK с декодированными сообщениями
Рис. Интерфейс MyltiPSK с декодированными сообщениями

Сообщения декодируются, а это значит, что Cortex-M3 в ПЛИС GW1NSR-LV4CQN48PC6/I5 инициализируется и передаёт посчитанные значения через SPI в генератор сигналов в ПЛИС. Встроенные в FPGA микроконтроллеры открывают большие возможности для использования в различных проектах. Например, при использовании радиочастотной микросхемы-трансивера AD9361 очень удобно инициализировать её из встроенного в ZYNQ-7000 ARM ядра или софт-процессора Microblaze на языке С, тогда как приём, передача и/или обработка IQ сигнала ведётся в логике FPGA. А как Вы используете встроенные в ПЛИС микропроцессоры?

Спасибо.

С. Н.

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


  1. lesha108
    18.12.2024 19:35

    Прочитав последний абзац, хотел бы узнать о том как китайские LibreSDR и аналоги программировать


    1. Pisikak Автор
      18.12.2024 19:35

      В статье нашел краткое описание LibreSDR, в основе которого ad9363, а это значит, что можно пользоваться примерами Analog Devices. На wiki AD написано очень много чего интересного, а в их репозиториях есть примеры hdl описаний и прошивок для ARM ядра ZYNQ или софт-процессора Microblaze. Там же есть примеры работы из под Linux для ZYNQ.