阅读视图

发现新文章,点击刷新页面。
☑️ ⭐

ARM Cortex M0移植到FPGA

综述

本文将介绍如何将DesignStart Eval的示例SoC移植到FPGA上,该SoC使用M0内核。在开始之前,简明阐述本文实现思路,移植可分为两部分:硬件与软件。

  • 硬件移植:Verilog代码的移植、综合、布局布线与调试;在DesignStart Eval的包文件中,...\systems\cortex_m0_mcu\verilog文件夹下实现的有一参考示例,复制所需文件即可。
  • 软件移植:基于CMSIS与参考代码制作自己的库函数,寄存器的配置均需要与RTL中配置的保持一致。

移植该项目时,参考了许多官方文档与第三方书籍,其中最有价值的列举如下:

  • DUI0926B_cortex_m0_designstart_eval_guide.pdf,该手册介绍了示例SoC的结构与细节,用于硬件搭建。
  • DDI0479C_cortex_m_system_design_kit_r1p0_trm.pdf,该手册介绍了CMSDK系统模块,在后期构建库函数时颇有用处。
  • AMBA总线规范中文版V2.0.pdf,介绍了AMBA规范下各个接口的协议。
  • ARM Cortex-M0全可编程SoC原理及实现.pdf,总体上描述了M0的技术细节。

SoC概括

该SoC来自官方示例,可在systems文件夹下找到,顶层文件为cmsdk_mcu.v,我们要做的是:

  • 复制所需文件,包括M0核心与CMSDK文件。
  • 例化ROM/RAM,在FPGA内部二者均使用SRAM实现,在顶层文件中将cmsdk_ahb_romcmsdk_ahb_ramMEM_TYPE均修改为2,即用于FPGA。
  • 开启SWD调试的电源,找到cmsdk_mcu_system.v,在其核心例化文件中修改即可。
  • 新建顶层文件,加入MMCM用于配置时钟。
  • 将IO口绑定引脚,综合、布局布线、下载调试,笔者使用的是CMSIS-DAP。

实现的SoC总体框架为:

Cortex M0 SoC

该SoC的AHB地址映射为:

内存地址 功能
0x00000000-0x003FFFFF ROM,大小为2^AW
0x20000000-0x207FFFFF RAM,大小为2^AW
0x40000000-0x4000FFFF APB外设挂载地址
0x40010000-0x40010FFF GPIO #0
0x40011000-0x40011FFF GPIO #1
0x4001F000-0x4001FFFF 系统控制寄存器
0xF0000000-0xF0000400 系统ROM表

该SoC的APB地址映射为:

内存地址 功能
0x40000000-0x40000FFF Timer0
0x40001000-0x40001FFF Timer1
0x40002000-0x40002FFF Dual Timer
0x40004000-0x40004FFF UART0
0x40005000-0x40005FFF UART1
0x40006000-0x40006FFF UART2
0x40008000-0x40008FFF Watchdog
0x4000B000-0x4000BFFF APB test slave

准备文件

示例SoC的顶层视图:

进入到.../systems/cortex_m0_mcu/verilog文件夹,除去tb_cmsdk_mcu.v、tbench_M0_DS.vc、cmsdk_uart_capture.v、cmsdk_clkreset外均用于本SoC的构建。其余文件功能描述如下(参考自DUI0926B 2.3.1):

  • cmsdk_mcu:示例SoC的顶层文件,包含SoC核心与存储器。
  • cmsdk_mcu_system:SoC核心,包含M0核心与CMSDK外设。
  • cmsdk_mcu_clkctrl:SoC的时钟与复位控制模块。
  • cmsdk_mcu_pin_mux:用于GPIO端口的功能复用,在输入、输出、复用三种功能内选择。
  • cmsdk_mcu_addr_decode:根据地址映射关系生成选择器信号。
  • cmsdk_mcu_defs:头文件,用于配置存储器的顺序与非顺序等待。
  • cmsdk_mcu_sysctrl:SoC的系统控制模块。
  • cmsdk_mcu_stclkctrl:SysTick信号的控制模块,用于生成系统嘀嗒信号。
  • cmsdk_ahb_cs_rom_table.v:CoreSight系统ROM表,用于调试。

准备完成MCU部分后,需要复制M0内核与CMSDK,通过顶层文件向下遍历需要的模块即可。该SoC主要包含如下内容:

  • M0内核(.../cores/cortexm0_designstart_r2p0/logical/):在DesignStart Kit中代码已被混淆,仅可使用SWD调试,AHB-Lite总线仅支持单主机。(参考自DUI0926B 1.3.2)
  • AHB与APB总线:AMBA定义的标准总线协议,APB主要用于挂在低速外设接口,二者通过AHB to APB桥进行转接。
  • RAM/ROM:FPGA内部均使用SRAM实现。
  • Timer:定时器,包括Simple Timer与Dual Timer。
  • UART:串口,用于数据交互。
  • Watchdog:看门狗,用于程序错误执行时的复位。

一个完整的设计包括有如下文件,注意cmsdk_fpga_rom模块同样使用SRAM实现,同时需修改入口参数为filename:

例化存储器

该SoC的存储器例化使用cmsdk_ahb_rom.v与cmsdk_ahb_ram.v,具体的文件结构如下:

cmsdk_ahb_rom模块的例化参数有:

  • MEM_TYPE:选择为2,意为AHB_ROM_FPGA_SRAM_MODEL。
  • AW:地址宽度,实际的存储器大小为2^AW。
  • filename:运行在M0上的程序镜像。
  • WS_N、WS_S:连续与非连续读写,意为Non-Sequential或Sequential。
  • BE:指定SoC程序的大小端,意为Big endian。

当RAM与ROM的大小均设置为16KB时,工程综合布局布线后的资源占用为(使用Vivado 2018.2,XC7Z010CLG400-1):

Post-Imple后资源占用

配置SWD

在该示例SoC中,默认关闭了SWD调试的电源,因此需要手动开启。在cmsdk_mcu_system.v中例化M0内核时,将调试线的REQ与ACK连接,如下:

wire debug_power_req_ack;
...
.CDBGPWRUPREQ  (debug_power_req_ack),
.CDBGPWRUPACK  (debug_power_req_ack),
...

新建顶层

由于需要添加加速器与时钟控制,因此新建顶层文件添加MMCM模块,注意在不同的平台上时钟IP核不同,需要额外修改。示例如下,本设计使用50M系统时钟:

module top(
    input in_clk,
    input in_rst_n,
    
    inout SWD,
    input SWCLK,
    input SWRST,
    
    inout [15:0] P0,
    inout [15:0] P1
);
wire sys_clk,sys_rst_n,mmcm_locked;
assign sys_rst_n = mmcm_locked & in_rst_n;
mmcm u_mmcm(
    // Clock out ports
    .clk_out1(sys_clk),     // output clk_out1
    // Status and control signals
    .resetn(in_rst_n), // input resetn
    .locked(mmcm_locked),       // output locked
    // Clock in ports
    .clk_in1(in_clk)    // input clk_in1
);      

parameter BE              = 0;   // Big or little endian
parameter BKPT            = 4;   // Number of breakpoint comparators
parameter DBG             = 1;   // Debug configuration
parameter NUMIRQ          = 32;  // NUM of IRQ
parameter SMUL            = 0;   // Multiplier configuration
parameter SYST            = 1;   // SysTick
parameter WIC             = 0;   // Wake-up interrupt controller support
parameter WICLINES        = 34;  // Supported WIC lines
parameter WPT             = 2;    // Number of DWT comparators
cmsdk_mcu #(
    .BE(BE),
    .BKPT(BKPT),
    .DBG(DBG),
    .NUMIRQ(NUMIRQ),
    .SMUL(SMUL),
    .SYST(SYST),
    .WIC(WIC),
    .WICLINES(WICLINES),
    .WPT(WPT)
) 
u_cmsdk_mcu(
    .XTAL1(sys_clk), // input
    .XTAL2(), // output
    .NRST(sys_rst_n),  // active low reset
    .P0(P0),
    .P1(P1),

    .nTRST(SWRST),
    .TDI(),
    .TDO(),
    .SWDIOTMS(SWD),
    .SWCLKTCK(SWCLK)
);
endmodule

调试

编写XDC约束文件,比特流生成完成后下载到FPGA开发板上,将调试器正确连接。注意,必须使用支持SWD的调试器,譬如ST-Link、CMSIS-DAP、Jink等。

在XADC中看到芯片温度有明显上升,一般可将此判断芯片成功下载的一个依据。

XADC温度上升
  • 连接调试端口SWD,SCLK,GND与串口UART0(用于打印调试信息),打开.../systems/cortex_m0_mcu/testcodes/hello下的Keil工程。

    注意在打开前,最好在Keil官网下载安装CMSIS的支持包,名称为Keil.V2M-MPS2_CMx_BSP.1.7.1.pack。

  • 修改内存地址,在Target选项卡的Memory Areas。我这里RAM/ROM均修改为16K,其中Start保持不变,改变Size即可。

    修改内存地址
  • 打开工程配置界面的Debug标签,选择正确的调试器,我这里选择的是CMSIS-DAP Debugger。

  • 在调试器Settings中将Port设置为SW,一切正常的话可以在SW Device中看到当前连接的设备。

  • 取消对Flash的操作,在FPGA内部对其操作并无太大意义,在此跳过烧录时的Flash操作。

    取消Flash操作
  • 点击Keil主界面上的Start/Stop Debug Session,由于还未添加Flash烧录算法此时会提示No Flash Operation,不影响运行与调试。

    Start/Stop Debug Session
  • 设置UartStdOutInit();函数,计算合适的BAUDDIV与CTRL。CMSIS外设均在DDI0479C文章中有所记载,下节将详述,本节给出50M下的配置代码。

    void UartStdOutInit(void)
    {
      CMSDK_UART0->BAUDDIV = 434; //The APB clock is 50M, the register is 50,000,000/115200 = 434
      CMSDK_UART0->CTRL    = 0x03; // Enable TX&RX
      CMSDK_GPIO1->ALTFUNCSET = (1<<1); // Port enable
      return;
    }
    
  • 打开串口调试助手,设置波特率到115200即可收到调试信息。

    Hello world from srT!

    其它

    在完成硬件移植并成功启动一起Hello World示例后,将会逐步移植Systick与GPIO,将其封装在库函数中供调用。读者可参考DDI0479C手册进行移植,下节将详细介绍。

☑️ ⭐

信号发生器的设计与实现

设计目标

在本文中,我们将在XO2-4000HC开发平台上,借助外部高速DAC实现:

  • DC~20MHz的正弦波,方波,三角波,锯齿波
  • 通过旋转编码器控制产生的信号频率与幅度(1 Vpp)
  • 波形的信息可以实时通过OLED显示屏查看
DDS发生器

DDS原理及实现

DDS全称为直接数字式频率合成计(Direct Digital Synthesizer),将需要输出的数据储存在ROM中,按照一定的频率去读取后通过DAC输出即可。一个完整的DDS模块应当包括:查找表、相位控制器、DAC、外部滤波器,后文将详细叙述。如果希望更加直观地了解到DDS的工作原理以及设计细节,可以访问ADI DDS仿真器,在该在线工具中你可以直观地看到每个周期的采样过程以及后置滤波器的原理。

DDS实现信号发生器需要以下参数:

  • 信号的采样率:根据奈奎斯特采样定理fs2fhf_s\ge2f_h,理论上每个周期采样两个点即可恢复原始波形。但是请注意,以正弦波为例采样波形此时为三角波,这就对滤波器的设计提出了很高的要求。因为在实际应用中我们常常会在一周期采样多个点,本设计中采样率为120MHz,即一个周期采样6个点。

  • 相位控制字宽度:相位控制字决定了信号采样的增量间隔,理论上一个信号的最小分辨率res=fs2nres=\frac{f_s}{2^n},在本设计中取28 bit以达到0.447Hz左右的频率分辨率。

  • 相位控制字增量:在上文中我们确定了频率的分辨率,将频率与分辨率相除即可得到单位增量。

ADI DDS仿真器

查找表

DDS的实现通过查找ROM表实现,ROM表保存了一个正弦信号在不同相位时的信号幅值,当以一定频率取出时即实现了信号的发生。在改设计中,使用了一个长度为256,宽度为10 bit的正弦表。由于正弦信号的幅值存在对称性,因此实际只储存64*9 bit的正弦表。实现的代码如下所示,通过地址的高两位来判断信号所处的象限,进而实现对幅值的补偿。

module lut(
	input [7:0] address,
	output reg [9:0] cos
);

wire [1:0] section;
reg [5:0] lut_address;
reg [8:0] lut_cos;
assign section = address[7:6]; //Get the sections of cos wave to reduce memory

always@(address)begin
	case(section)
		2'b00: begin
					lut_address = address[5:0];
					cos = 9'h1ff + lut_cos;
			   end
		2'b01: begin
					lut_address = ~address[5:0];
					cos = 9'h1ff + lut_cos;
			   end
		2'b10: begin
					lut_address = address[5:0];
					cos = 9'h1ff - lut_cos;
			   end
		2'b11: begin
					lut_address = ~address[5:0];
					cos = 9'h1ff - lut_cos;
			   end			   
	endcase
end

always @(lut_address) begin
	case(lut_address)	
      6'h0: lut_cos=9'h0;
      6'h1: lut_cos=9'hC;
      6'h2: lut_cos=9'h19;
      6'h3: lut_cos=9'h25;
      6'h4: lut_cos=9'h32;
      6'h5: lut_cos=9'h3E;
      6'h6: lut_cos=9'h4B;
      6'h7: lut_cos=9'h57;
      6'h8: lut_cos=9'h63;
      6'h9: lut_cos=9'h70;
      6'ha: lut_cos=9'h7C;
      6'hb: lut_cos=9'h88;
      6'hc: lut_cos=9'h94;
      6'hd: lut_cos=9'hA0;
      6'he: lut_cos=9'hAC;
      6'hf: lut_cos=9'hB8;
      6'h10: lut_cos=9'hC3;
      6'h11: lut_cos=9'hCF;
      6'h12: lut_cos=9'hDA;
      6'h13: lut_cos=9'hE6;
      6'h14: lut_cos=9'hF1;
      6'h15: lut_cos=9'hFC;
      6'h16: lut_cos=9'h107;
      6'h17: lut_cos=9'h111;
      6'h18: lut_cos=9'h11C;
      6'h19: lut_cos=9'h126;
      6'h1a: lut_cos=9'h130;
      6'h1b: lut_cos=9'h13A;
      6'h1c: lut_cos=9'h144;
      6'h1d: lut_cos=9'h14E;
      6'h1e: lut_cos=9'h157;
      6'h1f: lut_cos=9'h161;
      6'h20: lut_cos=9'h16A;
      6'h21: lut_cos=9'h172;
      6'h22: lut_cos=9'h17B;
      6'h23: lut_cos=9'h183;
      6'h24: lut_cos=9'h18B;
      6'h25: lut_cos=9'h193;
      6'h26: lut_cos=9'h19B;
      6'h27: lut_cos=9'h1A2;
      6'h28: lut_cos=9'h1A9;
      6'h29: lut_cos=9'h1B0;
      6'h2a: lut_cos=9'h1B7;
      6'h2b: lut_cos=9'h1BD;
      6'h2c: lut_cos=9'h1C3;
      6'h2d: lut_cos=9'h1C9;
      6'h2e: lut_cos=9'h1CE;
      6'h2f: lut_cos=9'h1D4;
      6'h30: lut_cos=9'h1D9;
      6'h31: lut_cos=9'h1DD;
      6'h32: lut_cos=9'h1E2;
      6'h33: lut_cos=9'h1E6;
      6'h34: lut_cos=9'h1E9;
      6'h35: lut_cos=9'h1ED;
      6'h36: lut_cos=9'h1F0;
      6'h37: lut_cos=9'h1F3;
      6'h38: lut_cos=9'h1F6;
      6'h39: lut_cos=9'h1F8;
      6'h3a: lut_cos=9'h1FA;
      6'h3b: lut_cos=9'h1FC;
      6'h3c: lut_cos=9'h1FD;
      6'h3d: lut_cos=9'h1FE;
      6'h3e: lut_cos=9'h1FF;
      6'h3f: lut_cos=9'h1FF;
   endcase
end

endmodule

相位控制器

相位控制器是DDS设计中的核心,此设计可以直接影响信号的精度与稳定性。在前文叙述参数时已经提及,信号的控制字增量满足:Add=fresAdd = \frac{f}{res},即增量等于信号频率除以分辨率。由于乘除法操作在没有DSP单元的FPGA内部十分消耗资源,因此此处采用移位相加,如下所示。将频率与分辨率的除法转化为乘法,对乘法因子转换为二进制后取一定的精度即可。

assign dds_phase_add = (wave_freq << 1) + (wave_freq >> 3) + (wave_freq >> 4) + (wave_freq >> 5) + (wave_freq >> 6) + (wave_freq >> 9) + (wave_freq >> 11) + (wave_freq >> 13); //wave_freq*2.2369384765625(10.00111100101010);

相位增量的控制单元有如下实现,可以观察到查找表的输出取决于当前的信号分量。

always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		dds_phase <= 28'd0;
	end else begin
		dds_phase <= dds_phase + dds_phase_add;
	end
end

lut u_lut(
	.address(dds_phase[27:20]),
	.cos(cos_dac)
);

同时,在输出正弦波信号的同时我们需要输出方波、三角波、锯齿波,这些波形的产生均可通过对相位控制字截位进行。

//Code below is designed to generate Square wave
assign square_dac = {10{dds_phase[27]}};
//Code below is designed to generate Trig wave
assign trig_dac = dds_phase[27] ? ~dds_phase[26:17] : dds_phase[26:17];
//Code below is designed to generate Saw wave
assign saw_dac = dds_phase[27:18];

将所有信号通过一个多路选择器送出,即可实现不同类型的信号输出。

DAC与滤波器

本设计中高速DAC的时钟输入为120MHz,直接使用内部的锁相环IP核即可。注意在一般的应用中通常将外部的时钟信号经过锁相环稳定后再用作系统时钟,以保证时钟质量,使用锁相环的locked信号与外部复位信号一起用作系统复位。将该信号直接输出到DAC的时钟引脚,同时在D9~D0上并行送出数据即可实现数模转换。

wire pll_locked;
assign sys_rst_n = in_rst_n & pll_locked;
pll u_pll(
	.CLKI(in_clk),
	.CLKOP(dac_clk),
	.CLKOS(sys_clk),
	.LOCK(pll_locked)
);

为保证模拟信号的质量,滤除高次谐波,往往会在DAC的输出级加入一个低通滤波器。在选择滤波器的截止频率与阶次时需注意,信号越接近需要发射的频点最后的波形信噪比越高,但同时会引入些许衰减。我一般的做法是选择一个高阶的滤波器,同时设置较高的截止频率。

旋转编码器译码

板子上选用的编码器为增量式触电电刷编码器,型号为EC11。FPGA通过对A、B、S引脚的读取,进而识别出按键的状态:左转、右转、按下。其中S与传统按键类似,本电路中按下输出高电平,松手下拉到低电平。

旋转编码器原理图

按键S的消抖操作如下,对其进行20ms的消抖并输出一个时钟的高电平信号。

parameter CNT_20MS_MAX = 240_000; //To scan keys

//To D-reg the S input to sync data
reg r_encoder_s;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		r_encoder_s <= 1'b0;
	end else begin
		r_encoder_s <= encoder_s;
	end
end

//To provide 20ms counter to filter keys
reg [19:0] cnt_20ms;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cnt_20ms <= 20'd0;
	end else if(encoder_s == 1'b0)begin
		cnt_20ms <= 20'd0;
	end else if(cnt_20ms == CNT_20MS_MAX - 1 && encoder_s == 1'b1)begin
		cnt_20ms <= cnt_20ms;
	end else begin
		cnt_20ms <= cnt_20ms + 1'b1;
	end
end
                
//To provide 1 clock cycle signal
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		encoder_ok <= 1'b0;
	end else if(cnt_20ms == CNT_20MS_MAX - 2)begin
		encoder_ok <= 1'b1;
	end else begin
		encoder_ok <= 1'b0;
	end
end

通过对A、B输出的信号相位进行判断即可得到编码器的旋转方向,当顺时针旋转时A信号提前B信号90°,逆时针旋转时B信号提前A信号90°。根据A、B信号相位的变化可以得到下述判断准则:

  • A信号上升沿B信号低电平,A信号下降沿B信号高电平,编码器顺时针旋转。
  • A信号上升沿B信号高电平,A信号下降沿B信号低电平,编码器逆时针旋转。
旋转编码器示意

编码器旋转方向的判断如下,在边沿进行检测即可。

parameter CNT_1MS_MAX = 12_000; //To get 1k sample rate 

//The counter of 1k signal
reg [15:0] cnt_1ms;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cnt_1ms <= 16'd0;
	end else if(cnt_1ms == CNT_1MS_MAX - 1)begin
		cnt_1ms <= 16'd0;
	end else begin
		cnt_1ms <= cnt_1ms + 1'b1;
	end
end

//To D-reg the A&B input to sync data using 1k clock
reg r_encoder_a,rr_encoder_a;
reg r_encoder_b,rr_encoder_b;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		r_encoder_a <= 1'b0;
		r_encoder_b <= 1'b0;
		rr_encoder_a <= 1'b0;
		rr_encoder_b <= 1'b0;
	end else if(cnt_1ms == CNT_1MS_MAX - 1)begin
		r_encoder_a <= encoder_a;
		r_encoder_b <= encoder_b;
		rr_encoder_a <= r_encoder_a;
		rr_encoder_b <= r_encoder_b;
	end
end

wire state_a = encoder_a && r_encoder_a && rr_encoder_a;
wire state_b = encoder_b && r_encoder_b && rr_encoder_b;
reg r_state_a;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		r_state_a <= 1'b0;
	end else begin
		r_state_a <= state_a;
	end
end

//To get the rise_edge and fall_edge
wire encoder_a_rise;
wire encoder_a_fall;
assign encoder_a_rise = (r_state_a | state_a)&(r_state_a == 1'b0);
assign encoder_a_fall = (r_state_a | state_a)&(state_a == 1'b0);
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		encoder_left <= 1'b0;
		encoder_right <= 1'b0;
	end else if((encoder_a_rise && rr_encoder_b)||(encoder_a_fall && !rr_encoder_b))begin
		encoder_left <= 1'b1;
		encoder_right <= 1'b0;
	end else if((encoder_a_fall && rr_encoder_b)||(encoder_a_rise && !rr_encoder_b))begin
		encoder_right <= 1'b1;
		encoder_left <= 1'b0;
	end else begin
		encoder_left <= 1'b0;
		encoder_right <= 1'b0;
	end
end

注意,在上述实现的代码中均加入了信号的触发器锁存,此举的目的为了消除亚稳态与跨时钟域同步。

菜单调节

在本设计中,需要调节的共有三个参量:信号类型、信号频率、信号幅度。信号的调节过程通过状态机实现,共有四个状态:类型调节、频率调节、幅度调节、闲置。画出状态转移图后即可轻松完成该部分的设计,状态通过encoder_ok进行跳转。

module menu(
	input sys_clk,
	input sys_rst_n,
	
	input encoder_left,
	input encoder_right,
	input encoder_ok,
	
//	output [7:0] debug,
	
	output [1:0] type_ctrl,
	output [31:0] freq_ctrl,
	output [3:0] amp_ctrl
);

parameter TYPE_CTRL = 2'b00;
parameter FREQ_CTRL = 2'b01;
parameter AMP_CTRL = 2'b10;
parameter IDLE = 2'b11;

reg [1:0] state;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		state <= TYPE_CTRL;
	end else if(encoder_ok)begin
		state <= state + 1'b1;
	end
end

reg [1:0] buff_type_ctrl;
reg [31:0] buff_freq_ctrl;
reg [3:0] buff_amp_ctrl;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		buff_type_ctrl <= 2'd0;
	end else if(state == TYPE_CTRL && encoder_right)begin
		buff_type_ctrl <= buff_type_ctrl + 2'd1;
	end else begin
		buff_type_ctrl <= buff_type_ctrl;
	end
end

always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		buff_freq_ctrl = 32'd100_000;
	end else begin
		if(state == FREQ_CTRL && encoder_right)begin
			buff_freq_ctrl = buff_freq_ctrl + 32'd100_000;
		end else if(state == FREQ_CTRL && encoder_left)begin
			buff_freq_ctrl = buff_freq_ctrl - 32'd100_000;
		end
		
		if(buff_freq_ctrl >= 32'd20_000_000)begin
			buff_freq_ctrl = 32'd20_000_000;
		end else if(buff_freq_ctrl <= 32'd100_000)begin
			buff_freq_ctrl = 32'd100_000;
		end
	end 
end

always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		buff_amp_ctrl = 4'd5;
	end else begin
		if(state == AMP_CTRL && encoder_right)begin
			buff_amp_ctrl = buff_amp_ctrl + 4'd1;
		end else if(state == AMP_CTRL && encoder_left)begin
			buff_amp_ctrl = buff_amp_ctrl - 4'd1;
		end
		
		if(buff_amp_ctrl >= 4'd10)begin
			buff_amp_ctrl = 4'd10;
		end else if(buff_amp_ctrl <= 4'd1)begin
			buff_amp_ctrl = 4'd1;
		end
	end
end

assign type_ctrl = buff_type_ctrl;
assign freq_ctrl = buff_freq_ctrl;
assign amp_ctrl = buff_amp_ctrl;

endmodule

结果测试

条件受限,家里只有一台20M带宽的示波器,且无硬件触发。因此在显示高频信号时候存在衰减,同时精度过低,幅度只进行了初校。显示方波这种由不同频率合成的信号,失真程度更甚,测试如下:

  • 100KHz正弦波,0.5Vpp

  • 500KHz方波,0.6Vpp

  • 1MHz三角波,0.7Vpp

  • 2MHz锯齿波,1.0Vpp

  • 20MHz正弦波,1.0Vpp

完整工程资料可在此处下载:Signal_generate_XO2-4000HC.zip

☑️ ☆

iCE40UP5K实现Σ-Δ ADC采集及电压表

在本文中,我们将使用硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现一个Σ-Δ ADC采集,并制作一个简易的电压表。在了解相关内容与原理时,发现了许多学习过的知识,通信/电信人狂喜。

目标

基于Lattice iCE40UP5K实现一个Σ-Δ ADC采集,采集后的电压将会在OLED屏幕上显示,实现一个简易的电压表,效果如下图所示:

简易电压表效果图

Σ-Δ ADC采集

在大多数FPGA芯片上均无ADC外设,当需要低成本/多通道采集模拟量时,可以考虑此方案。同样地,此学习平台上也没有使用集成ADC与DAC模块,其ADC采集使用了PWM+电压比较器实现Σ-Δ ADC,其DAC输出使用了R-2R权电阻网络来实现。本节将详述ADC实现原理,仿真,参数的选择,代码实现。

ADC参数

在讨论一块ADC性能的时候,往往关注两个指标:采样率、量化位数。比如我们常用的黑金AN108模块上,采用了AD9280作为其ADC,查阅ADI官网其介绍如下:

AD9280是一款单芯片、8位、32 MSPS模数转换器(ADC),采用单电源供电,内置一个片内采样保持放大器和基准电压源。它采用多级差分流水线架构,数据速率达32 MSPS,在整个工作温度范围内保证无失码。

采样率与量化位数作为两个重要的指标被显著标注,我们可以得知该芯片的采样率为32MSPS(Million Samples Per Second)、量化位数8 bit。后文我们将会通过理论分析来确定这两个参数。

ADC实现原理

在该学习平台上,其PWM+电压比较器实现Σ-Δ ADC的原理图如下:

Σ-Δ ADC原理图

可以看到,在该电路图中包含一个比较器,其同相输入端接模拟输入,反向输入端接PWM输入,比较后输出结果。在反相输入端PWM_V2连接着一个电阻和一个电容,其构成一个简易的一阶RC滤波器。该一阶RC滤波器截至频率为fc=12πRCf_{c}=\frac{1}{2\pi RC},工程上将幅度值下降到原来的0.707倍(-3dB)称作截止频率点,电路的带宽也由-3dB点定义。通过对输入的PWM波形进行滤波可以得到一个近似的直流值,与Ain2进行比较输出,通过不断调节占空比(假设由低到高),当输出C_OUT2由高变低时便可使用占空比来表示模拟输入量。

滤波的目的是得到直流量,一个PWM波进行傅里叶变换后可观察到其存在许多高次谐波,一阶RC的目的便是将基频与谐波滤除。

我们知道,一个典型的PWM波形脉冲区间包括高电平区间tHt_H与低电平区间tLt_L,其占空比常定义为duty=tHtH+tLduty=\frac{t_H}{t_H+t_L}。接触过单片机的读者可能会联想起PWM控制电机转速,LED亮度的实验,当调节占空比时可以改变上述参量,本电路中经过RC低通滤波器便可得到一个近似的直流电压值。

PWM波形示意图

当对不同占空比的PWM波形(频率均为200K)做傅里叶变换进行频谱分析时,可以得到如下频谱图。可以观察到,当改变不同占空比时,PWM波的主要频率分量仍集中在200K,400K附近,改变的主要是直流分量。

不同占空比下PWM波形频谱图

当给此PWM信号加以截至频率100KHz,阻带衰减60dB的理想低通滤波器后可以得到如下波形图。可以观察到随着时间的推移信号逐渐趋近于直流,也可以理解为200K与400K的分量被滤除得到直流分量。

200K PWM经过100K低通滤波器

仿真所用的MATLAB代码如下:

close all;clear;clc;

fs = 1e6; %sample rate is 1M
t = 0:1/fs:1e-1-1/fs; %generate the data between 0-1ms to rise the fft resulation
n = length(t);
n_index = 0:n-1;
f_index = n_index*fs/n;

% 25%
x = square(2*pi*200e3*t,25) + 1; %generate the PWM with special duty
subplot(321);stem(t,x);axis([0 2e-5 0 2]);title("Time: 25% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(322);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 25% Duty");

% 50%
x = square(2*pi*200e3*t,50) + 1; %generate the PWM with special duty
subplot(323);stem(t,x);axis([0 2e-5 0 2]);title("Time: 50% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(324);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 50% Duty");

% 75%
x = square(2*pi*200e3*t,75) + 1; %generate the PWM with special duty
subplot(325);stem(t,x);axis([0 2e-5 0 2]);title("Time: 75% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(326);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 75% Duty");

ADC参数选取

前文所述,ADC有两个重要的指标:采样率、量化位数。本节将介绍如何选取这两个颇为重要的参数,很多时候参数的选择涉及多方面的权衡。本涉及采用了如下参数:

量化位数N=8bitN=8 bit

采样率fs=200KHzf_s=200KHz

PWM产生模块时钟fclk=51.2Mf_{clk}=51.2M

RC滤波器元件R=1000Ω,C=1000pFR=1000\Omega,C=1000pF

RC滤波器截至频率fc=160KHzf_c=160KHz

在参数的选择过程中,可以参考如下步骤进行综合考量:

  • fsf_s的选择是否满足要求?当需要采样一个非直流信号时,需要满足奈奎斯特采样定理,即fs2fHf_s\ge2f_H

  • NN的选择是否满足要求?一个ADC的分辨率很大程度上取决于量化位数,分辨率受供电电压与位数二者共同决定,即有fres=VDD2nf_{res}=\frac{V_{DD}}{2^n}。同时,其信噪比满足SNR=6NSNR=6N,即每提高一位可以提高6dB6dB的信噪比。例如在一个3.3V供电的系统中,8位量化最高可以做到0.012890625V的分辨率。

  • fs>fcf_s > f_c是否满足?很多情况下要求采样率应尽可能大于截止频率,这样信号的直流分量便可以较好地滤除出来。

  • fclkf_{clk}是否能满足FPGA布局布线的要求?fclkf_{clk}的大小满足如下条件:fclk=2NT=2Nfsf_{clk}=\frac{2^N}{T}={2^N}*{f_s},可以观察到模块时钟的频率随着量化位数的增加呈指数倍增加,因此Σ-Δ ADC常用于低频下高精度的检测。当时钟频率提高时会给FPGA的布线带来困难,考虑到iCE40的定位属于低功耗FPGA,故此处选择51.2M作为时钟频率(当然我建议您可以尝试提高频率以获得更稳定的采样效果)。

    51.2M时钟生成模块

RC较大

在ADC实现原理一节中我们详细分析了滤波器存在的必要,可以观察到当RC越大时滤波器的截止频率也就越高,更有利于PWM信号直流分量的提取。但同时存在一个时间常数的概念,时间常数τ0.69RC\tau \approx 0.69RC ,当不受限制地提高RC将会提高时间常数进而减缓电路的响应时间。故我们可以总结如下特性:

优点:便于直流分量的提取,减小滤波后直流信号出现波动。

缺点:提高时间常数,电路的响应速度降低。

RC较小

RC较小时的特性与之相反,总结如下:

优点:时间常数小,电路响应速度快。

缺点:由于截至频率提高,因此需要更大的采样率才能达到较好的效果。

如何克服?

加入电感,提高滤波阶次,加入有源滤波,提高采样率。

Verilog代码实现

该模块包含三个输入与两个输出,具体介绍如下:

  • sys_clksys_rst_n分别为模块的时钟输入与复位输入
  • pwm_adc_in连接到比较器的输出,用于获取比较信息
  • pwm_val为该模块输出的ADC数值,范围0-255
  • pwm_adc_out连接到比较器的反相输入端,通过改变占空比以获得不同的直流电压

检测的原理为pwm_adc_out不断提高占空比,当pwm_adc_in由高到低产生下降沿变化时,输出此时的占空比数值,此时的数值即为ADC采样的数值。

module pwm_adc(
	input sys_clk,
	input sys_rst_n,
	input pwm_adc_in,
	output reg [7:0] pwm_val,
	output pwm_adc_out
);

reg r_adc_in;//Thought the D-reg to get the buffer I/O level.
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		r_adc_in <= 1'b0;
	end else begin
		r_adc_in <= pwm_adc_in;
	end
end

wire adc_in_fall;//When the pwm_adc_in is falling,the adc_in_fall will output high.
assign adc_in_fall = (r_adc_in | pwm_adc_in)&(pwm_adc_in == 1'b0);

//-----The pwm generation-----
reg [7:0] pwm_adder;
reg pwm_adder_overflow; //Complete a counter and generate the overflow(one clock period)
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		pwm_adder <= 8'd0;
		pwm_adder_overflow <= 1'b0;
	end else if(pwm_adder == 8'hff) begin
		pwm_adder <= pwm_adder + 1'b1;
		pwm_adder_overflow <= 1'b1;
	end else begin
		pwm_adder <= pwm_adder + 1'b1;
		pwm_adder_overflow <= 1'b0;
	end
end

reg [7:0] pwm_set;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		pwm_set <= 8'd0;
	end else if(adc_in_fall == 1'b1)begin
		pwm_val <= pwm_set;
		pwm_set <= 8'd0;
	end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b0)begin
		pwm_set <= pwm_set;
	end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b1)begin
		pwm_set <= pwm_set + 1'b1;
	end
end

assign pwm_adc_out = (pwm_adder <= pwm_set) ? 1'b1 : 1'b0;

endmodule

BCD码生成

由于ADC模块产生的数值为十进制,如需将其显示出来则需要一个BCD码型转换模块提取个、十、百、千位。不同于单片机内部使用乘除法获取各位的操作,在FPGA内部乘除法十分消耗资源,因此往往采用移位判断法,具体代码参考野火。

module  bcd_8421
(
    input   wire            sys_clk     ,   //系统时钟,频率50MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效
    input   wire    [19:0]  data        ,   //输入需要转换的数据

    output  reg     [3:0]   unit        ,   //个位BCD码
    output  reg     [3:0]   ten         ,   //十位BCD码
    output  reg     [3:0]   hun         ,   //百位BCD码
    output  reg     [3:0]   tho         ,   //千位BCD码
    output  reg     [3:0]   t_tho       ,   //万位BCD码
    output  reg     [3:0]   h_hun           //十万位BCD码
);

//********************************************************************//
//******************** Parameter And Internal Signal *****************//
//********************************************************************//

//reg   define
reg     [4:0]   cnt_shift   ;   //移位判断计数器
reg     [43:0]  data_shift  ;   //移位判断数据寄存器
reg             shift_flag  ;   //移位判断标志信号

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//cnt_shift:从0到21循环计数
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_shift   <=  5'd0;
    else    if((cnt_shift == 5'd21) && (shift_flag == 1'b1))
        cnt_shift   <=  5'd0;
    else    if(shift_flag == 1'b1)
        cnt_shift   <=  cnt_shift + 1'b1;
    else
        cnt_shift   <=  cnt_shift;
       
//data_shift:计数器为0时赋初值,计数器为1~20时进行移位判断操作
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_shift  <=  44'b0;
    else    if(cnt_shift == 5'd0)
        data_shift  <=  {24'b0,data};
    else    if((cnt_shift <= 20) && (shift_flag == 1'b0))
        begin
            data_shift[23:20]   <=  (data_shift[23:20] > 4) ? (data_shift[23:20] + 2'd3) : (data_shift[23:20]);
            data_shift[27:24]   <=  (data_shift[27:24] > 4) ? (data_shift[27:24] + 2'd3) : (data_shift[27:24]);
            data_shift[31:28]   <=  (data_shift[31:28] > 4) ? (data_shift[31:28] + 2'd3) : (data_shift[31:28]);
            data_shift[35:32]   <=  (data_shift[35:32] > 4) ? (data_shift[35:32] + 2'd3) : (data_shift[35:32]);
            data_shift[39:36]   <=  (data_shift[39:36] > 4) ? (data_shift[39:36] + 2'd3) : (data_shift[39:36]);
            data_shift[43:40]   <=  (data_shift[43:40] > 4) ? (data_shift[43:40] + 2'd3) : (data_shift[43:40]);
        end
    else    if((cnt_shift <= 20) && (shift_flag == 1'b1))
        data_shift  <=  data_shift << 1;
    else
        data_shift  <=  data_shift;

//shift_flag:移位判断标志信号,用于控制移位判断的先后顺序
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        shift_flag  <=  1'b0;
    else
        shift_flag  <=  ~shift_flag;

//当计数器等于20时,移位判断操作完成,对各个位数的BCD码进行赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
            unit    <=  4'b0;
            ten     <=  4'b0;
            hun     <=  4'b0;
            tho     <=  4'b0;
            t_tho   <=  4'b0;
            h_hun   <=  4'b0;
        end
    else    if(cnt_shift == 5'd21)
        begin
            unit    <=  data_shift[23:20];
            ten     <=  data_shift[27:24];
            hun     <=  data_shift[31:28];
            tho     <=  data_shift[35:32];
            t_tho   <=  data_shift[39:36];
            h_hun   <=  data_shift[43:40];
        end

endmodule

BCD模块在OLED模块内部例化,其例化代码如下。注意此处对ADC采样进来的数值进行了近似操作以减少乘法器的使用,由于在3.3V下8位量化的分辨率为0.012890625,故将其扩大10000倍取128恰对应着左移8位的操作。显示数据的拼接在dis_dat_buff内进行。

	wire [15:0]				dis_dat_mult = dis_dat << 7; //The voltage multed by 128(3.3/256 = 0.01289) to get a similar number
	wire [(6*8-1):0]		dis_dat_buff; //The buffer of dis_dat
	wire [3:0]				dis_ten;
	wire [3:0]				dis_hun;
	wire [3:0]				dis_tho;
	wire [3:0]				dis_t_tho;
	assign dis_dat_buff = {4'd0,dis_t_tho,".",4'd0,dis_tho,4'd0,dis_hun,4'd0,dis_ten,"V"};
	bcd_8421 u_bcd_8421(
	.sys_clk(clk), //系统时钟,频率50MHz
	.sys_rst_n(rst_n), //复位信号,低电平有效
    .data(dis_dat_mult), //输入需要转换的数据

    .unit(), //个位BCD码
    .ten(dis_ten), //十位BCD码
    .hun(dis_hun), //百位BCD码
    .tho(dis_tho), //千位BCD码
    .t_tho(dis_t_tho), //万位BCD码
    .h_hun() //十万位BCD码
	);

顶层例化

受限于iCE40UP5K布线资源的问题,当系统锁相环时钟由外部输入时,外部时钟便不可再用于其它模块的时钟。因此此处使用了该FPGA芯片的内部时钟,通过HSOSC原语进行例化,注意Radiant中的原语与其它IDE不相同。

module voltmeter(
//	input in_clk, //Use the internal clock source to avoid restrict
	input in_rst_n,
	input pwm_adc_in,
//	input debug, //The debug wire is connected to switch
//	output oled_csn, //The cs pin is disconnected
	output oled_rst,
	output oled_dcn,
	output oled_clk,
	output oled_dat,
	output pwm_adc_out
);

wire sys_clk;
HSOSC 
#( 
  .CLKHF_DIV ("0b10") 
) u_HSOSC ( 
  .CLKHFEN (1'b1), 
  .CLKHFPU (1'b1), 
  .CLKHF   (sys_clk) 
);

wire sys_rst_n,clk_gen_locked,clk_pwm_adc;
assign sys_rst_n = in_rst_n & clk_gen_locked;
clk_gen u_clk_gen(
	.ref_clk_i(sys_clk),
	.rst_n_i(in_rst_n),
	.lock_o(clk_gen_locked),
	.outcore_o(),
	.outglobal_o(clk_pwm_adc) //The pll out is connected with the global clock network
);

wire [7:0] pwm_val;
pwm_adc u_pwm_adc(
	.sys_clk(clk_pwm_adc),
	.sys_rst_n(sys_rst_n),
	
	.pwm_adc_in(pwm_adc_in),
	
	.pwm_val(pwm_val),
	.pwm_adc_out(pwm_adc_out)
);

oled12864 u_oled12864(
	.clk(sys_clk),		//The system clock
	.rst_n(sys_rst_n),		//The system reset
	
	.dis_dat(pwm_val),
//	.debug(debug),
	
	.oled_csn(),	//OLED ENABLE
	.oled_rst(oled_rst),	//OLED RESET
	.oled_dcn(oled_dcn),	//OLED DATA/COMMAND CONTROL
	.oled_clk(oled_clk),	//OLED CLOCK
	.oled_dat(oled_dat)	//OLED DATA
);

其它

之前一直使用Xilinx、Altera的FPGA进行开发,相比于Lattice而言,其开发工具更显繁琐。Lattice的开发工具使用十分清爽且快,其综合布线一个OLED显示的工程只需要30s,在Vivado上都不够软件的加载时间。

当然,Lattice的开发流程缺点也蛮显著。譬如:时钟网络的布线资源受限;Lattice LSE的综合工具并不好用,从其带有一个Synplify Pro的选项便可看出,很多时候LSE综合不出来,更换Synplify便可解决。当然,以上这些缺点在学习时综合布线飞快面前显得就不那么重要。

参考资料

1、硬禾学堂. 基于iCE40UP5K的FPGA学习平台[EB/OL]. [2022-1-23]. https://www.eetree.cn/project/detail/131.

2、电子森林. PWM的应用及相应的Verilog代码[EB/OL]. [2022-1-23]. https://www.eetree.cn/wiki/pwm_verilog.

3、吴大正. 《信号与线性系统分析》[M]. 第四版. 高等教育出版社, 2005-8.

4、童诗白、华成英. 《模拟电子技术基础(第五版)》[M]. 第五版. 高等教育出版社, 2015-1.

5、邱关源. 《电路》[M]. 第五版. 高等教育出版社, 2006-5.

完整工程资料可在此处下载:Voltmeter_iCE40UP5K.zip

🔲 ⭐

AM调制解调的MATLAB与FPGA实现

新手福利 --- AM信号调制解调

在模拟通信系统中,基带信号通过对载波波形幅度,相位,频率的调制以达到将信号在载波上传输信息的目的。根据基带信号的类型可分为:模拟调制 & 数字调制。

调制原理

本文所述AM(Amplitude Modulation)调制,即是模拟信号对载波幅度进行调制,通过与载波信号相乘来进行频谱搬移。其中第一个公式为时域表达式,第二个公式为频域表达式。
SAM=(A0+m(t))cos(wct+ϕc)S_{AM}=(A_0+m(t))*cos(w_ct+\phi_c)

SAM(ω)=A02[M(ω+ωc)+M(ωωc)]S_{AM}(\omega)=\frac{A_0}{2}*[M(\omega+\omega_c)+M(\omega-\omega_c)]

在上文的公式中m(t)m(t)为基带信号(一般为低频信号),A0A_0为直流偏置,用来将基带信号叠加到正半轴,使得在调制后信号仍然保持着m(t)m(t)的包络。其中cos(wct+ϕc)cos(w_ct+\phi_c)为载波信号,ωc\omega_c为载波角频率。

在描述AM信号的调制深度时,常定义为m(t)maxA0\frac{m(t)_{max}}{A_0},当调制深度大于1时为过调幅现象,此时因为存在相位突变,调制后的信号包络无法反映原始信号m(t)m(t)

MATLAB仿真

在开始之前,先阐述一下各项参数:

  • 采样率:10M
  • 基带信号频率:100K
  • 载波信号频率:1M
  • 采样点数:1000点(为了对齐一个周期的正弦波,避免FPGA仿真时出现相位的突变)

调制

在MATLAB调制的仿真中,我们需要将基带信号输出到TXT文档,供FPGA仿真时读取,作为AD采样的数值。同时绘制调制后的频域波形与时域波形,便于直观对照。

close all;clear;clc;
fs=10e6;
f1=100e3;
f2=1e6;
n=1000;
t=0:1/fs:(n-1)/fs;
s1=sin(2*pi*f1*t); %基带信号
s=(s1+max(s1)).*sin(2*pi*f2*t); %调制后的信号
s_fft=fft(s);s_fft_abs=abs(s_fft);
subplot(311);plot(s1(1:500));title("基带信号时域波形");
subplot(312);plot(s(1:500));title("调制信号时域波形");
subplot(313);plot(s_fft_abs(1:length(s_fft)/2));title("调制信号频域波形");

Q=8; %此处是量化位数,为了与ADC对应此处设置为8位
s1=s1/max(abs(s1));
Q_s=round(s1*(2^(Q-1)-1));
Q_s=Q_s+(2^(Q-1)-1);
fid=fopen("am_mod.txt","w");
for i=1:length(Q_s)
    Q_data=dec2bin(Q_s(i),Q); %与ADC对应,输出为无符号二进制数,注意在新版MATLAB中此函数有变化
    fprintf(fid,"%s\r\n",Q_data);
end
fprintf(fid,";");
fclose(fid);

其绘制的图片如下,可以观察到在调制后的频域上有三个尖峰,分别对应0.9,1.0,1.1三个频点:

image-20211008090850756

解调

其实不用写代码的

在MATLAB信号分析其中拖入变量s,分析器中选择包络即可提取信号包络,然后再经过一个低通滤波器即可。提取上包络后的波形如下图:

image-20211008150157310

FPGA仿真

本仿真在Vivado 2018.2上测试,元件选择为Xilinx XC7Z010CLG400,需要用到的IP核介绍如下。

IP核

Clocking Wizard

该IP核通过MMCM与PLL来输出特定频率到所需的模块,其中PLL可看作MMCM的子集。该IP核较为简单,介绍一个容易出问题的地方。

image-20211008091817238

如上图所示,在Input Clock Information的Source一栏中可以选择时钟来源,分别为:

  • 单端时钟:在后续的引脚选择上,必须与器件的专用时钟端口对应。
  • 差分时钟:类似于HDMI中的CLK+,CLK-,通过差分来抵消传输过程中的干扰。
  • 全局时钟:当该IP核的输入来自工程内的其他模块时,需要选择此。
  • 不缓冲输入:当从普通IO口引入时钟时,由于已经添加了一个Buffer,故此处不必再添加Buffer。

DDS Compiler

该IP核可以用于输出特定频率的正余弦波以及相位变化,可以通过AXI4接口配置其初始相位以及相位增量(改变输出频率),相关介绍如下:

Configuration

  • Configuration Options:配置为相位生成或正余弦波生成,或二者兼有。
  • System Requirements:配置输入频率,通道数与参数设置,这里一般选择为System Parameters,便于在后面直接配置输出频率。
  • System Parameters:可配置杂散范围与频率分辨率的数值,此配置将会决定相位宽度。

Implementation

  • Phase Increment Programmability:配置相位增量是否可调,可用来改变输出频率。
  • Phase Offset Programmability:配置相位偏移是否可调。
  • Output:配置输出波形(正,余弦或二者兼有),输出极性(是否反转,幅度范围),是否有相位输出(我经常不勾选)。

Output Frequencies

此处可用于配置各个通道的频率,前提是在Configuration中勾选到System Requirements。

本设计所用到的IP核Summary如下:

image-20211008083826319

Multiplier

该IP核用于将两个输出相乘,最后输出的位宽等于两个输入位宽之和,即P=ABP=A*B。在使用该IP核时,一定要注意输入位宽与是否为有符号数匹配。有符号与无符号数在该IP核内的处理方式不同,可能会造成乘法错误。

FIR Compiler

该IP核用于将输入信号进行FIR滤波,系数可从MATLAB的FDATOOL中导入,注意导入的数据需要在FDATOOL中量化为定点。

Filter Options

  • Filter Coefficients:此处可以手动输入抽头系数,或者由MATLAB导出。
  • Filter Specification:配置多速率FIR滤波器,出于性能问题在实际设计中常常使用CIC滤波器做多速率转换。

Channel Specification

本页面需要关注Hardware Oversampling Specification,该选项可以指定时钟频率与采样率的关系。

Implementation

  • Coefficient Type:输入抽头系数的数据类型,从MATLAB导入后保持默认即可。Coefficient Structure,此项用来指定抽头系数的类型:由系统推断,非对称与对称。
  • Data Path Options:设置输入输出数据的位宽,输出数据有一个Rounding Mode需要注意。Truncate LSBs可用于直接将低位舍去,保留高位,但我更倾向于仿真后手动截位。
image-20211008103433205

本设计所用到的IP核Summary如下:

image-20211008103639198

调制源文件

`timescale 1ns / 1ps

module am_mod(
    input sys_clk,
    input sys_rst_n,
    input [7:0] adc_data_unsigned,
    output adc_clk,
    output [15:0] am_mod
    );

wire clk_10m,locked,valid;
assign valid = sys_rst_n & locked;
assign adc_clk = valid ? clk_10m : 1'b0; //clk_10m时钟有效时输出到adc_clk,以10M的ADC时钟采样到adc_data_unsigned
ip_pll inst0(
    // Clock out ports
    .clk_10m(clk_10m),     // output clk_10m
    // Status and control signals
    .resetn(sys_rst_n), // input resetn
    .locked(locked),       // output locked
   // Clock in ports
    .clk_in(sys_clk)
);      // input clk_in

wire signed [7:0] carry_data;
wire carry_valid;
ip_dds inst1 (
  .aclk(sys_clk),                              // input wire aclk
  .aresetn(sys_rst_n),                        // input wire aresetn
  .m_axis_data_tvalid(carry_valid),  // output wire m_axis_data_tvalid
  .m_axis_data_tdata(carry_data)    // output wire [7 : 0] m_axis_data_tdata
);

parameter depth_inv = 2; //用来配置调制深度
wire signed [15:0] mult_data;
wire signed [7:0] adc_data;
assign adc_data = ((adc_data_unsigned - 8'd128) + depth_inv * 8'd128)/(depth_inv + 1);
assign am_mod = mult_data;
ip_mult inst2 (
  .CLK(sys_clk),  // input wire CLK
  .A(adc_data),      // input wire [7 : 0] A
  .B(carry_data),      // input wire [7 : 0] B
  .CE(valid),   // input wire CE
  .P(mult_data)      // output wire [15 : 0] P
);
    
endmodule

调制TestBench

注意此处的文件读写操作,用来输入基带信号,输出调制信号,输出到TXT的数据可在MATLAB中进一步的验证正确性。此外,fdisplay这个函数会根据wire或者reg在程序中定义的类型进行输出,此处定义为signed,默认为unsigned。注意将所需要的调试文件设为顶层设计。

`timescale 1ns / 1ps

module am_mod_tb(
    );

reg sys_clk;
reg sys_rst_n;  
reg [7:0] adc_data_unsigned;
wire adc_clk;
wire signed [15:0] am_mod;  
am_mod inst0(
    .sys_clk (sys_clk),
    .sys_rst_n (sys_rst_n),
    .adc_data_unsigned (adc_data_unsigned),
    .adc_clk (adc_clk),
    .am_mod (am_mod)
);
parameter clk_period = 20;
parameter data_num = 1000;

initial begin
    $readmemb("am_mod.txt",stimulus);
    sys_clk = 1'b0;
    sys_rst_n = 1'b0;
    adc_data_unsigned = 8'd0;
    #100 sys_rst_n = 1'b1;   
end

always #10 sys_clk = ~sys_clk;

integer pattern;
reg [7:0] stimulus [1:data_num];
always@(posedge adc_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
        adc_data_unsigned <= 8'd0;
        pattern = 1; 
    end else if (pattern == data_num)begin
        adc_data_unsigned <= stimulus[pattern];
        pattern = 1;
    end else begin
        adc_data_unsigned <= stimulus[pattern];
        pattern = pattern + 1;
    end
end

integer file_out;
initial begin
    file_out = $fopen("am_mod_data.txt");
    if(!file_out) begin
        $display("Cloud open file!");
        $finish;
    end
end
wire write_clk;
assign write_clk = adc_clk & sys_rst_n;
always@(posedge write_clk) begin
    $fdisplay(file_out,"%d",am_mod);
end
    
endmodule

调制结果

该图为仿真结果,可以看到am_mod信号的波形幅度与输入信号幅度同步变化,其包络大致与基带信号相同。此外16位am_mod的14位和15位同步变化,在后文将am_mod截断为8位无符号时将会利用此波形进行变换。

image-20211008105707918

将调试后输出的数据导入到MATLAB中分析频谱既可以观察到在归一化频率的0.19、0.2、0.21附近有尖峰,满足设计预期。

image-20211008110042168

调制截位

由于我们使用的AD/DA模块是黑金的AN108,其输入输出均为8bit无符号数据,因此需要进行转换。注意此处因为第15位与14位相同,故此处直接将第15位作为符号位,以提高截位后的精度。

assign am_mod = {mult_data[15],mult_data[13:7]} + 8'd128;

解调源文件

AM信号的调制可以直接采用包络检波,在大信噪比的时候其性能与相干解调差距不大。使用Verilog来进行包络检波总体分为:整流&滤波。其中整流即是对原始信号取绝对值操作,本工程中通过判断am_mod最高位来实现。滤波即使用FIR IP核来进行,通过在FDATOOL中设计一个通带归一化频率为0.02的滤波器,将系数导入到Vivado即可。注意在仿真数据出来以后可以将其放入信号分析器观察频谱,调整通阻带增益来达到更好的解调效果。

`timescale 1ns / 1ps

module am_demod(
    input sys_clk,
    input sys_rst_n,
    input [7:0] am_mod_unsigned,
    output adc_clk,
    output [7:0] m
    );

wire clk_10m,locked,valid;
assign valid = sys_rst_n & locked;
assign adc_clk = valid ? clk_10m : 1'b0; //clk_10m时钟有效时输出到adc_clk,以10M的ADC时钟采样到adc_data_unsigned
ip_pll inst0(
    // Clock out ports
    .clk_10m(clk_10m),     // output clk_10m
    // Status and control signals
    .resetn(sys_rst_n), // input resetn
    .locked(locked),       // output locked
   // Clock in ports
    .clk_in(sys_clk)
);      // input clk_in    

wire signed [7:0] am_mod;
reg signed [7:0] am_abs;
assign am_mod = am_mod_unsigned - 8'd128;
always@(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
        am_abs <= 8'd0;
    end else if(am_mod[7]) begin
        am_abs <= -am_mod;
    end else begin
        am_abs <= am_mod;
    end
end

wire inst1_s_axis_data_tready,inst1_m_axis_data_tvalid;
wire [31:0] inst1_m;
assign m = inst1_m[26:19] + 8'd128;
ip_fir inst1 (
  .aclk(clk_10m),                              // input wire aclk
  .s_axis_data_tvalid(valid),  // input wire s_axis_data_tvalid
  .s_axis_data_tready(inst1_s_axis_data_tready),  // output wire s_axis_data_tready
  .s_axis_data_tdata(am_abs),    // input wire [7 : 0] s_axis_data_tdata
  .m_axis_data_tvalid(inst1_m_axis_data_tvalid),  // output wire m_axis_data_tvalid
  .m_axis_data_tdata(inst1_m)    // output wire [31 : 0] m_axis_data_tdata
);
    
endmodule

解调TestBench

`timescale 1ns / 1ps

module am_demod_tb(
    );
reg sys_clk,sys_rst_n;
reg [7:0] adc_data_unsigned;
wire adc_clk,inst1_adc_clk;
wire [7:0] am_mod;    
wire [7:0] m;
am_mod inst0(
    .sys_clk (sys_clk),
    .sys_rst_n (sys_rst_n),
    .adc_data_unsigned (adc_data_unsigned),
    .adc_clk (adc_clk),
    .am_mod (am_mod)
);    

am_demod inst1(
    .sys_clk (sys_clk),
    .sys_rst_n (sys_rst_n),
    .am_mod_unsigned (am_mod),
    .adc_clk (inst1_adc_clk),
    .m (m)
);

parameter clk_period = 20;
parameter data_num = 1000;

initial begin
    $readmemb("am_mod.txt",stimulus);
    sys_clk = 1'b0;
    sys_rst_n = 1'b0;
    adc_data_unsigned = 8'd0;
    #100 sys_rst_n = 1'b1;   
end

always #10 sys_clk = ~sys_clk;

integer pattern;
reg [7:0] stimulus [1:data_num];
always@(posedge adc_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
        adc_data_unsigned <= 8'd0;
        pattern = 1; 
    end else if (pattern == data_num)begin
        adc_data_unsigned <= stimulus[pattern];
        pattern = 1;
    end else begin
        adc_data_unsigned <= stimulus[pattern];
        pattern = pattern + 1;
    end
end

integer file_out;
initial begin
    file_out = $fopen("am_demod_data.txt");
    if(!file_out) begin
        $display("Cloud open file!");
        $finish;
    end
end
wire write_clk;
assign write_clk = adc_clk & sys_rst_n;
always@(posedge write_clk) begin
    $fdisplay(file_out,"%d",m);
end
    
endmodule

解调结果

FPGA仿真后的波形如下图所示,FIR滤波器存在相位延迟,故在一段时间内输出的波形不正确。

image-20211008104710570

将输出的波形导入到信号分析器,既可以观察到在0(直流分量)与0.02(100KHz的基带信号)附近有频点。如果希望波形还原更加完善,可以在FDATOOL中提高阻带权重,以滤除0.4(二次谐波)与0.8附近的频率。

image-20211008105049483

其他

  • AM调制的调制效率最大为13\frac{1}{3},信号利用率较低,频带利用率也较低。但是由于其可通过包络检波直接提取原始信号,故其成本极低,现在仍被用于无线电广播。
  • 在FPGA仿真后,需要对仿真数据进行分析。此时建议使用MATLAB的“信号分析器”来对频谱进行分析,此APP的功能强大且易用。可用于查看信号频谱,滤波器初步设计,包络提取等功能,推荐。
  • FPGA的操作中受限于资源所限,常常会对数据进行截位,此时可以根据信号的仿真波形以及IP核的Implementation上的接口说明来撰写截位代码。

参考文献

《通信原理(第七版)》樊昌信 曹丽娜著

《数字调制解调技术的MATLAB与FPGA实现》 杜勇著

☑️ ☆

关于

由于之前站点的备份丢失,Markdown源文件已无法恢复。原站点已作归档处理,归档在blog.raincorn.top

博主是谁

一名普通到不能再普通的在校学生,目前大三。

擅长什么

软硬件都略有涉猎,目前的精力主要集中在FPGA/ASIC开发上,当然通信是老本行。

联系方式

我的邮箱为:rain_corn@foxmail.com 。

❌