FPGA SDR(30)EP2C5T144 AM/FMステレオラジオ


2019/03/06 追記:フィルタの構成を CIC 1/24 + FIR 1/8 に変更しました。
2018/07/29 追記:自作ATAN2を改良してFMの音質が改善しました。
2018/07/26 追記:CICフィルタのパラメータを調整して、CICフィルタ以降の処理を16ビットに変更しました。
2018/07/25 追記:ステレオ復調を修正しました。CICフィルタ以降の処理を13ビットに変更しました。

 

Cyclone II EP2C5T144のAM/FMラジオもステレオ化しました。Cyclone IV EP4CE6E22のAM/FMラジオを次のように変更しました。

  • Cyclone IIのALTPLLの逓倍・分周の選択範囲が狭いので、ADCのサンプリング周波数が73.809524MHzになりました。
  • メモリが少ないのでFIRフィルタに使う2ポートRAMのサイズを16×128に変更して、フィルタの係数を99個にしました。
  • ステレオ復調用NCOの出力ビット数を12ビットに変更しました。

Cyclone IV EP4CE6E22のAM/FMラジオと同様、

FMは、CICフィルタ+FIRフィルタで384kHzに間引いてFM復調し、FIRフィルタで48kHzに間引いてI2S DACへ。

AMは、CICフィルタ+FIRフィルタで384kHzに間引いて、さらにFIRフィルタで48kHzに間引いてAM復調しI2S DACへ。

ADC前のFETとI2S DACのあとのオペアンプを削除してシンプルにしたところ、Cyclone IV EP4CE6E22と同様にノイズが気付かないレベルになりました。

 

こちらのESP32もタッチセンサーに4本配線を繋いで選局、音量調節をできるようにしました。左側の2本で選局、右側の2本で音量調節できます。ADCのサンプリング周波数が73.809524MHzになったのでESP32のスケッチを一部変更します。

unsigned long freqToPhaseInc(double freq) { // in Hz
  double clk = 73809524; // 73.809524MHz
  double phaseInc360 = (double)0x80000000UL * 2; // 32bits full scale
  
  if (freq >= clk)
    freq -= clk;
    
  return (unsigned long)(phaseInc360 * (freq / clk));
}

void sendFreq(double freq) { // in Hz
  unsigned long i = freqToPhaseInc(freq);
  
  Serial.println(freq);
  responseSize = ap.write(0x10, i, response);
  ap.printBytes(response, responseSize);
}

pll:MegaWizardのALTPLLで73.809524MHzを作ります。本当は48kHzの1536倍の73.728MHzにしたかったのですが妥協します。

QsysCore:FPGAラジオ(2)アンテナ、ADC、DACを接続

MyNCO:FPGAラジオ(13)自作NCO

MyCIC:FPGAラジオ(12)自作CICフィルタ

MyMEMFIR:FPGAラジオ(27)メモリベースFIRフィルタ

MyATAN2:FPGAラジオ(19)自作ATAN2

MyNCO2X:FPGAラジオ(32)ステレオ復調用NCO

MyAverage:FPGAラジオ(28)ステレオ復調
19kHzのNCOの位相とパイロット信号の位相の合わせるためと、AM復調出力のDC成分を打ち消すために使っています。

MyMEMLPF8:FPGAラジオ(27)メモリベースFIRフィルタ

MyDeEmphasis:FPGAラジオ(31)ディエンファシス

sqrt:MegaWizardのALTSQRTを使います。

MyI2S:FPGAラジオ(21)PCM5102A I2S DAC

Topモジュールは次のようになります。

`define CYCLE_1SEC 50000000


module SPIbridge
(
	input wire RST_N,
	input wire CLK,
	
	input wire SPI_NSS,
	input wire SPI_SCLK,
	output wire SPI_MISO,
	input wire SPI_MOSI,
	
	output wire [2:0] LED,
	
	input wire [7:0] ADC,
	output wire ENCODE,

	output wire I2S_SCK,
	output wire I2S_BCK,
	output wire I2S_LRCK,
	output wire I2S_DATA,
	
	output reg [7:0] DACA,
	output reg [7:0] DACB
);


	localparam CIC_WIDTH = 19;
	localparam FIR_WIDTH = 16;
	localparam PI = 17'sb0011_0010_0100_0011_1; // pi = 0011 . 0010 0100 0011 1111 0110 1010
	localparam NCO19K_WIDTH = 12;


	wire clk; // 73.809524M

	wire [31:0] pio0;
	wire [31:0] pio1;
	
	wire fm;
	wire [3:0] gain;
	wire [3:0] volume;
	
	reg [7:0] uadc_r;
	wire signed [7:0] adc;

	wire signed [11:0] sin;
	wire signed [11:0] cos;
	
	reg signed [CIC_WIDTH-1:0] i;
	reg signed [CIC_WIDTH-1:0] q;

	wire signed [CIC_WIDTH-1:0] icic;
	wire icic_valid;
	wire signed [CIC_WIDTH-1:0] qcic;
	wire qcic_valid;

	wire signed [FIR_WIDTH-1:0] ifir;
	wire ifir_valid;
	wire signed [FIR_WIDTH-1:0] qfir;
	wire qfir_valid;

	reg signed [FIR_WIDTH+FIR_WIDTH-1:0] i2q2;
	wire [FIR_WIDTH-1:0] mag;
	wire [FIR_WIDTH-1:0] mag_dc;
	
	wire signed [FIR_WIDTH-1:0] phase;
	reg signed [FIR_WIDTH-1:0] phase_r;
	wire signed [FIR_WIDTH:0] phase_diff;
	reg signed [FIR_WIDTH:0] freq;

	wire signed [NCO19K_WIDTH-1:0] sin19k;
	wire signed [NCO19K_WIDTH-1:0] sin38k;
	wire signed [FIR_WIDTH+NCO19K_WIDTH-1:0] freqsin19k;
	wire signed [FIR_WIDTH+NCO19K_WIDTH-1:0] freqsin19k_ave;
	wire signed [FIR_WIDTH+NCO19K_WIDTH-1:0] freqsin19k_fb;
	
	wire signed [FIR_WIDTH+NCO19K_WIDTH-1:0] freqsin38k;
	wire signed [FIR_WIDTH-1:0] freq_LPR;
	wire signed [FIR_WIDTH-1:0] freq_LMR;
	wire freq_LPR_valid;
	wire signed [FIR_WIDTH-1:0] freq_L;
	wire signed [FIR_WIDTH-1:0] freq_R;
	wire signed [FIR_WIDTH-1:0] freq_LD;
	wire signed [FIR_WIDTH-1:0] freq_RD;

	wire [7:0] daca;
	wire [7:0] dacb;

	
	pll	pll_inst (
		.inclk0 (CLK),
		.c0 (clk)
	);	

	QsysCore QsysCore_inst (
		.clk_clk                                                                                         (clk),
		.reset_reset_n                                                                                   (RST_N),
		.spi_slave_to_avalon_mm_master_bridge_0_export_0_mosi_to_the_spislave_inst_for_spichain          (SPI_MOSI),
		.spi_slave_to_avalon_mm_master_bridge_0_export_0_nss_to_the_spislave_inst_for_spichain           (SPI_NSS),
		.spi_slave_to_avalon_mm_master_bridge_0_export_0_miso_to_and_from_the_spislave_inst_for_spichain (SPI_MISO),
		.spi_slave_to_avalon_mm_master_bridge_0_export_0_sclk_to_the_spislave_inst_for_spichain          (SPI_SCLK),
		.pio_0_external_connection_export                                                                (pio0),
		.pio_1_external_connection_export                                                                (pio1)
	);

	assign LED = ~pio0[2:0];
	
	assign fm = (pio1 < 32'd31422535 || 32'd127463534 <= pio1) ? 1 : 0; // (pio1 < 540kHz || 2.190476MHz <= pio1) ? FM : AM
	assign gain = fm ? 3 : 5; // pio0[3:0];
	assign volume = pio0[3:0];
	
	always @(posedge clk) begin
		uadc_r <= ADC;
	end
	assign ENCODE = clk;
	assign adc = (uadc_r[7] == 0) ? uadc_r + 8'h80 : uadc_r - 8'h80;

	MyNCO #(
		.OUT_WIDTH(12)
	) nco_inst (
		.clk       (clk),
		.reset_n   (RST_N),
		.clken     (1'b1),
		.phi_inc_i (pio1),
		.fsin_o    (sin),
		.fcos_o    (cos),
		.out_valid ()
		);

	always @(posedge clk) begin
		i <= adc * cos;
		q <= adc * sin;
	end

	MyCIC #(
		.DATA_WIDTH(CIC_WIDTH)
	) cic_inst_i (
		.clk       (clk),
		.reset_n   (RST_N),
		.in_error  (2'b00),
		.in_valid  (1'b1),
		.in_ready  (),
		.in_data   (i),
		.out_data  (icic),
		.out_error (),
		.out_valid (icic_valid),
		.out_ready (1'b1)
	);

	MyCIC #(
		.DATA_WIDTH(CIC_WIDTH)
	) cic_inst_q (
		.clk       (clk),
		.reset_n   (RST_N),
		.in_error  (2'b00),
		.in_valid  (1'b1),
		.in_ready  (),
		.in_data   (q),
		.out_data  (qcic),
		.out_error (),
		.out_valid (qcic_valid),
		.out_ready (1'b1)
	);
	
	MyMEMFIR8 #(
		.DATA_WIDTH(FIR_WIDTH)
	) fir_inst_i (
		.clk       (clk),
		.reset_n   (RST_N),
		.ast_sink_data (icic[CIC_WIDTH-1 -gain -: FIR_WIDTH]),
		.ast_sink_valid (icic_valid),
		.ast_sink_error (2'b00),
		.ast_source_data (ifir),
		.ast_source_valid (ifir_valid),
		.ast_source_error ()
	);
 
	MyMEMFIR8 #(
		.DATA_WIDTH(FIR_WIDTH)
	) fir_inst_q (
		.clk       (clk),
		.reset_n   (RST_N),
		.ast_sink_data (qcic[CIC_WIDTH-1 -gain -: FIR_WIDTH]),
		.ast_sink_valid (qcic_valid),
		.ast_sink_error (2'b00),
		.ast_source_data (qfir),
		.ast_source_valid (qfir_valid),
		.ast_source_error ()
	);

	MyATAN2 #(.XY_WIDTH(FIR_WIDTH), .Q_WIDTH(FIR_WIDTH)) MyATAN2_inst (
		.clk    (clk),
		.areset (~RST_N),
		.en     (fm ? ifir_valid : freq_LPR_valid),
		.x      (fm ? ifir : freq_LPR),
		.y      (fm ? qfir : freq_LMR),
		.q      (phase)
	);

	always @(posedge clk) begin
		if (ifir_valid) begin
			phase_r <= phase;

			if (phase_diff > PI) begin
				freq <= phase_diff - (PI <<< 1);
			end
			else if (phase_diff < -PI) begin
				freq <= phase_diff + (PI <<< 1);
			end
			else begin
				freq <= phase_diff;
			end
		end
	end
	assign phase_diff = phase - phase_r;

	MyNCO2X #(
		.OUT_WIDTH(NCO19K_WIDTH)
	) MyNCO2X_inst (
		.clk       (clk),
		.reset_n   (RST_N),
		.clken     (ifir_valid),
		.phi_inc_i (32'sd212276680 - freqsin19k_fb),
		.fsin_o    (sin19k),
		.fsin2x_o    (sin38k),
		.out_valid ()
		);
	assign freqsin19k = $signed(freq[FIR_WIDTH-1:0]) * sin19k;
	assign freqsin38k = $signed(freq[FIR_WIDTH-1:0]) * sin38k;

	MyAverage #(
		.DATA_WIDTH(FIR_WIDTH+NCO19K_WIDTH),
		.AVERAGE_WIDTH(9),
		.AVERAGE(384),
		.MOVING_AVERAGE_WIDTH(2)
	) MyAverage_LOOP_inst (
		.clk (clk),
		.reset_n (RST_N),
		.in_data (freqsin19k),
		.in_valid (ifir_valid),
		.out_data (freqsin19k_ave),
		.out_valid (freqsin19k_ave_valid)
	);
	assign freqsin19k_fb = freqsin19k_ave_valid ? freqsin19k_ave : 0;

	MyMEMLPF8 #(
		.DATA_WIDTH(FIR_WIDTH)
	) MyLPF8_LPR_inst (
		.clk       (clk),
		.reset_n   (RST_N),
		.ast_sink_data (fm ? freq[FIR_WIDTH-1:0] : ifir),
		.ast_sink_valid (ifir_valid),
		.ast_sink_error (2'b00),
		.ast_source_data (freq_LPR),
		.ast_source_valid (freq_LPR_valid),
		.ast_source_error ()
	);

	MyMEMLPF8 #(
		.DATA_WIDTH(FIR_WIDTH)
	) MyLPF8_LMR_inst (
		.clk       (clk),
		.reset_n   (RST_N),
		.ast_sink_data (fm ? freqsin38k[FIR_WIDTH+NCO19K_WIDTH-1-1 -: FIR_WIDTH] : qfir),
		.ast_sink_valid (ifir_valid),
		.ast_sink_error (2'b00),
		.ast_source_data (freq_LMR),
		.ast_source_valid (),
		.ast_source_error ()
	);
	
	assign freq_L = freq_LPR + freq_LMR;
	assign freq_R = freq_LPR - freq_LMR;

	MyDeEmphasis #(
		.DATA_WIDTH(FIR_WIDTH)
	) MyDeEmphasis_L_inst (
		.clk (clk),
		.reset_n (RST_N),
		.in_data (freq_L),
		.in_valid (ifir_valid),
		.out_data (freq_LD),
		.out_valid ()
	);

	MyDeEmphasis #(
		.DATA_WIDTH(FIR_WIDTH)
	) MyDeEmphasis_R_inst (
		.clk (clk),
		.reset_n (RST_N),
		.in_data (freq_R),
		.in_valid (ifir_valid),
		.out_data (freq_RD),
		.out_valid ()
	);

	always @(posedge clk) begin
		if (freq_LPR_valid) begin
			i2q2 <= freq_LPR * freq_LPR + freq_LMR * freq_LMR;
		end
	end

	sqrt	sqrt_inst (
		.radical (i2q2),
		.q (mag),
		.remainder ()
	);	

	MyAverage #(
		.DATA_WIDTH(FIR_WIDTH),
		.AVERAGE_WIDTH(12),
		.AVERAGE(4096),
		.MOVING_AVERAGE_WIDTH(2)
	) MyAverage_DC_inst ( // 85ms
		.clk (clk),
		.reset_n (RST_N),
		.in_data (mag),
		.in_valid (freq_LPR_valid),
		.out_data (mag_dc),
		.out_valid ()
	);
	
	MyI2S #(
		.IN_WIDTH(FIR_WIDTH)
	) MyI2S_inst (
		.clk (clk),
		.reset_n (RST_N),	
		.volume (4'b1111 - volume),
		.in_left (fm ? freq_LD : mag - mag_dc),
		.in_right (fm ? freq_RD : mag - mag_dc),
		.in_valid (freq_LPR_valid),
		.SCK (I2S_SCK),
		.BCK (I2S_BCK),
		.LRCK (I2S_LRCK),
		.DATA (I2S_DATA)
	);

	assign daca = 0;
	assign dacb = 0;
//	assign daca = cos[9 -: 8];
//	assign dacb = sin[9 -: 8];
//	assign daca = i[CIC_WIDTH-1 -: 8];
//	assign dacb = q[CIC_WIDTH-1 -: 8];
//	assign daca = ifir[FIR_WIDTH-1 -: 8];
//	assign dacb = qfir[FIR_WIDTH-1 -: 8];
//	assign daca = phase[FIR_WIDTH-1 -: 8];
//	assign dacb = freq[FIR_WIDTH-1 -: 8];
//	assign daca = freq_LPR[FIR_WIDTH-1 -: 8];
//	assign dacb = freq_LMR[FIR_WIDTH-1 -: 8];
//	assign daca = mag[FIR_WIDTH-1 -: 8];
//	assign dacb = mag_dc[FIR_WIDTH-1 -: 8];
//	assign daca = sin19k[NCO19K_WIDTH-1 -: 8];
//	assign dacb = sin38k[NCO19K_WIDTH-1 -: 8];
//	assign daca = freqsin19k_ave[FIR_WIDTH+NCO19K_WIDTH-1-1-5 -: 8];
//	assign dacb = freqsin19k_fb[FIR_WIDTH+NCO19K_WIDTH-1-1-5 -: 8];
	always @(posedge clk) begin
		DACA <= (daca[7] == 0) ? daca + 8'h80 : daca - 8'h80;
		DACB <= (dacb[7] == 0) ? dacb + 8'h80 : dacb - 8'h80;
	end


endmodule

リソースはこんな感じです。メモリを節約するため、NCOのSINテーブルを2ポートROMに変更してCOSもSINテーブルを90°進めたアドレスで読み出しています。