FPGA SDR(30)EP2C5T144 AM/FMステレオラジオ
2020/05/19 追記:q <= adc * -sin に修正しました。
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°進めたアドレスで読み出しています。