基于FPGA的目标反射回波检测算法及其实现(准备篇)
:用Verilog-HDL状态机控制硬件接口
前段时间,开发了一个简单的目标反射回波信号识别算法,我会分几篇文章分享这个基于FPGA的回波识别算法的开发过程和原码,欢迎大家不吝赐教。“工欲善其事,必先利其器”,调试FPGA上的数字信号处理算法,最直接的办法是进行行为仿真(前仿)。但有时想通过testbench产生验证算法所需的特定激励信号,并不是一件容易的事情。往往导致通过行为仿真验证/调试FPGA数字信号处理算法的效率低下。
随着任意信号发生器(Arbitrary Wave Generator)的普及,通过matlab或numpy等算法开发工具产生的激励信号,可以轻松的通过任意波形发生器变成模拟信号,极大的方便了数字信号处理算法的开发。但想利用任意信号发生器产生的模拟激励信号,提升算法的开发和调试效率,必须先对这些模拟激励进行A/D转换,并通过D/A转换器显示算法中各个节点上产生的中间信号。——为了方便算法的开发,我尝试实现了一组基于PMOD接口的串行A/D、D/A。这些串行芯片的驱动电路则采用Verilog HDL实现的摩尔型状态机。以下原创内容欢迎网友转载,但请注明出处:https://www.cnblogs.com/helesheng
很多Xilinx公司的FPGA开发板提供一种称为PMOD的接口,如下图所示。
图1 PMOD接口
PMOD只有8个有效I/O,且采用了低成本的低速接插件,只能驱动低速的串行接口ADC、DAC。但对于FPGA数字信号处理算法的开发,最重要的是能验证算法行为的逻辑正确性。在没有高速且可靠的A/D、D/A板卡的情况下,采用PMOD接口和低速串口A/D和D/A转换器开发验证算法,不失为一种有效、快捷且廉价的方法。
A/D和D/A转换器方面,我选择了Microchip公司廉价的12bits MCP3202和MCP4822:MCP3202在3.3V下的转换速度为50KSPS,有两个模拟输入通道;MCP4822在3.3V下的建立时间为4.5uS,也有两个模拟输出通道。两个芯片的SPI接口的控制方法基本类似,下面先以较为复杂的MCP4822为例,介绍Verilog HDL状态机流程控制的实现方式。
一、 接口时序
控制MCP4822可以使用标准SPI接口的模式(1,1)和(0,0),对单个DAC通道进行控制时,官方手册上提供的时序如下图所示。
图2 MCP4822单个通道的控制时序
每次SPI传输的长度为16bits,包含D/A输出的数据信息、输出通道、模拟增益和关断信息。具体如下表所示。
由于MCP4822有A、B两个输出通道,为实现两个通道同时刷新数据的功能,MCP4822还有一个数据加载引脚LDAC(以下简称LD)。当通过SPI口完成D/A转换数据寄存器的刷新后,还需要在LD上给出一个低电平来将数值同时刷新到MCP4822的两个模拟输出通道。
根据上述时序要求,我规划了下图所示的双通道输出时序。
图3 双通道D/A输出时序及控制标志
上图描述了SPI接口的时钟SCK、片选CS和数据加载LD信号的时序,而数据引脚MOSI在并未在图中给出。整个输出时序包括了对两个通道的分别赋值及统一的加载等阶段,Verilog-HDL状态机要给出的状态除了两个通道的分别发送及数据加载状态之外还应包括这些状态之间的间隔状态。
图中的START信号是整个电路的触发信号,它的高电平启动电路按照事先设定的流程依次产生所需的信号。上层电路只要控制START信号产生的频率,就可以控制D/A转换的频率。而上图下半部分的PROC_CHA、PROC_INTV1、PROC_CHB、PROC_INTV1和PROC_LD等信号则是摩尔状态机中标志各个状态的wire型标志信号。
二、 状态及其迁移条件
根据上图所示的时序要求,我设计了如下图所示的状态转换图。
图4 双通道D/A转换器状态转换图
上图所示的状态机共分为:空闲状态(IDLE_STATE)、发送A通道数据状态(SEND_CHA_STATE)、两通道发送间隙状态(INTV1_STATE)、发送B通道数据状态(SEND_CHB_STATE)、数据发送完到加载数据之间的间隙状态(INTV2_STATE)、加载数据状态(LD_STATE)等共六个状态。每个状态都分别对应各自的标志位(即时序图中的PROC_CHA、PROC_INTV1、PROC_CHB、PROC_INTV1和PROC_LD等标志信号),各个状态按照各自的顺序逐次出现。而状态切换的标志则是对应的标志位被清零。
根据结构化程序/电路设计的思路,每个状态中要实现的具体功能不在状态机模块中实现,而是在各自对应的Verilog-HDL Module中实现;各模块则通过各自控制的“标志信号”和状态机模块交互。由于状态机模块不直接控制输出,只负责检测模块输出的状态信号,状态机显然属于“摩尔型”(Moore)。这种思路设计的摩尔型状态机虽然使电路结构更清晰易懂,但会带来状态切换时产生1个时钟周期的延迟的劣势。考虑到FPGA的工作频率可达100MHz以上,而设计的MCP4822的刷新速度只有50KHz左右,所以状态切换时产生1个时钟周期的延迟并不会对控制电路性能产生实质性影响。
三、 状态机程序的设计
根据上述设计思路,采用两段式摩尔型状态机设计的Verilog-HDL原码如下所示。
1 module mcp4822_state_machine 2 (clk,rst_n,state_flag,START,PROC_CHA,PROC_INTV1,PROC_CHB,PROC_INTV2,PROC_LD); 3 //这是用于控制MCP4822两个通道输出的状态机 4 input clk; 5 input rst_n; 6 input START; //起始信号,高电平表示开始一次转换 7 input PROC_CHA; 8 //通道A输出信号标志位,高电平表示正在输出通道A数据,低电平表示输出完成。 9 input PROC_INTV1; 10 //两通道数据输出之间的间隔标志位,高电平表示状态机正在两通道数据输出之间的间隔期。 11 input PROC_CHB; 12 //通道B输出信号标志位,高电平表示正在输出通道B数据,低电平表示输出完成。 13 input PROC_INTV2; //数据发送完成到加载数据之间的间隔标志位,高电平表示状态机正在数据发送和加载信号之间的间隔期。 14 input PROC_LD; //模拟数据加载阶段标志位,高电平表示状态机正处在数据加载阶段 15 output[5:0] state_flag;//状态机表示的状态 16 parameter IDLE_STATE = 6'B000001; //空闲状态 17 parameter SEND_CHA_STATE = 6'B000010; //发送通道A数据状态 18 parameter INTV1_STATE = 6'B000100; //两个发送之间的间隔状态 19 parameter SEND_CHB_STATE = 6'B001000; //发送通道B数据状态 20 parameter INTV2_STATE = 6'B010000; //发送数据和加载模拟信号之间的等待状态 21 parameter LD_STATE = 6'B100000; //加载模拟信号状态 22 reg[5:0] cur_state; //当前状态 23 reg[5:0] next_state; //下一个状态 24 assign state_flag[5:0] = cur_state[5:0];//用当前状态作为输出 25 //两段式moore状态机的第一段,时序逻辑控制 26 always @ (posedge clk or negedge rst_n) 27 begin 28 if(!rst_n) 29 cur_state[5:0] <= IDLE_STATE; 30 else 31 cur_state[5:0] <= next_state[5:0]; 32 end 33 //两段式moore状态机的第2段,控制下一个状态的组合逻辑 34 always @ (*) 35 begin 36 case(cur_state) 37 IDLE_STATE: begin 38 if(START) //收到开始信号,则进入发送状态 39 next_state <= SEND_CHA_STATE; 40 else 41 next_state <= IDLE_STATE; 42 end 43 SEND_CHA_STATE: begin 44 if(!PROC_CHA) //发送通道A标志为0,表示通道A数据发送完成,进入两通道之间的等待状态 45 next_state <= INTV1_STATE; 46 else 47 next_state <= SEND_CHA_STATE; 48 end 49 INTV1_STATE: begin 50 if(!PROC_INTV1) //两通道发送间隔标志为0,发送间隔完成,进入通道B数据发送状态 51 next_state <= SEND_CHB_STATE; 52 else 53 next_state <= INTV1_STATE; 54 end 55 SEND_CHB_STATE: begin 56 if(!PROC_CHB) //发送通道B标志为0,表示通道A数据发送完成,进入两通道之间的等待状态 57 next_state <= INTV2_STATE; 58 else 59 next_state <= SEND_CHB_STATE; 60 end 61 INTV2_STATE: begin 62 if(!PROC_INTV2) //数据发送完成等待加载间隔标志为0,等待间隔完成,模拟数据加载状态 63 next_state <= LD_STATE; 64 else 65 next_state <= INTV2_STATE; 66 end 67 LD_STATE: begin 68 if(!PROC_LD) //模拟数据加载标志0,数据加载标志完成,空闲状态 69 next_state <= IDLE_STATE; 70 else 71 next_state <= LD_STATE; 72 end 73 default: begin 74 next_state <= IDLE_STATE; 75 end 76 endcase 77 end 78 endmodule
上述代码注释详细,这里就不在一一解释了。
四、 SPI接口控制电路
SPI接口电路需要产生SCK、CS和MOSI,共3个接口信号;以及SPI忙的标志信号(例化为PROC_CHA和PROC_CHB)。三个接口信号分别用三个过程语句块实现,而标志信号则由CS取反产生。具体代码如下。
1 module mcp4822_spi(send_data16,en,clk,sck,mosi,cs,PROCING); 2 //这个模块用于产生控制MCP4822所需的SPI信号 3 input[15:0] send_data16;//用SPI口向外发送的数据 4 input clk; 5 input en; //使能信号,只在这个信号为高电平时发送SPI数据;这个信号是由状态机输入和本身所属的状态编号比较得到,相同时en为1 6 output sck; 7 output mosi; 8 output cs; 9 output PROCING; //处理标志,1表示本模块正在发送 10 reg[15:0] shift_reg;//移位寄存器,en为0时读取send_data16输入的数据,en为0时左移输出 11 wire clk_int;//移位寄存器的时钟,en为0时clk做时钟存储并行输入的数据,en为0时SCK为时钟,移位输出数据 12 assign clk_int = (en&(!sck)) | ((!en)&clk);//数据是在SCK的下降沿刷新的 13 reg sck; 14 reg[5:0] cnt_sck;//产生SPI时钟SCK的计数器 15 parameter CNT_SCK_NUM = 10'D5; //5计数后产生一次反转,100M输入产生10M输出 16 reg cs; 17 reg[9:0] cnt_cs;//产生SPI片选信号CS的计数器 18 parameter CNT_CS_NUM = CNT_SCK_NUM*16*2; //CS选中的时间长度为16个SCK周期 19 20 always @ (posedge clk or posedge cs) 21 begin 22 if(cs) 23 begin 24 sck <= 0; 25 cnt_sck[5:0] <= 6'd0; 26 end 27 else begin 28 if(cnt_sck[5:0] == CNT_SCK_NUM-1) 29 begin 30 cnt_sck[5:0] <= 0; 31 sck <= !sck; 32 end 33 else begin 34 cnt_sck[5:0] <= cnt_sck[5:0] + 6'd1; 35 sck <= sck; 36 end 37 end 38 end 39 40 assign PROCING = !cs; 41 always @ (negedge clk or negedge en) 42 begin 43 if(!en) 44 begin 45 cs <= 1'b1; 46 cnt_cs[9:0] <= 10'd0; 47 end 48 else begin 49 if(cnt_cs[9:0] < CNT_CS_NUM) 50 begin 51 cnt_cs[9:0] <= cnt_cs[9:0] + 10'd1; 52 cs <= 1'b0; 53 end 54 else begin 55 cnt_cs[9:0] <= CNT_CS_NUM; 56 cs <= 1'b1; 57 end 58 end 59 end 60 61 assign mosi = shift_reg[15]; 62 always @ (posedge clk_int) 63 begin 64 //由CS控制的数据选择电路,选择外部设置信号还是移位信号作为输入, 65 //即在移位寄存器的D触发器的输出端选择:当cs为1时设置初值,CS为0时移位。 66 shift_reg[0] <= (cs & send_data16[0]); 67 shift_reg[1] <= (cs & send_data16[1]) | ((!cs) & shift_reg[0]); 68 shift_reg[2] <= (cs & send_data16[2]) | ((!cs) & shift_reg[1]); 69 shift_reg[3] <= (cs & send_data16[3]) | ((!cs) & shift_reg[2]); 70 shift_reg[4] <= (cs & send_data16[4]) | ((!cs) & shift_reg[3]); 71 shift_reg[5] <= (cs & send_data16[5]) | ((!cs) & shift_reg[4]); 72 shift_reg[6] <= (cs & send_data16[6]) | ((!cs) & shift_reg[5]); 73 shift_reg[7] <= (cs & send_data16[7]) | ((!cs) & shift_reg[6]); 74 shift_reg[8] <= (cs & send_data16[8]) | ((!cs) & shift_reg[7]); 75 shift_reg[9] <= (cs & send_data16[9]) | ((!cs) & shift_reg[8]); 76 shift_reg[10] <= (cs & send_data16[10]) | ((!cs) & shift_reg[9]); 77 shift_reg[11] <= (cs & send_data16[11]) | ((!cs) & shift_reg[10]); 78 shift_reg[12] <= (cs & send_data16[12]) | ((!cs) & shift_reg[11]); 79 shift_reg[13] <= (cs & send_data16[13]) | ((!cs) & shift_reg[12]); 80 shift_reg[14] <= (cs & send_data16[14]) | ((!cs) & shift_reg[13]); 81 shift_reg[15] <= (cs & send_data16[15]) | ((!cs) & shift_reg[14]); 82 end 83 endmodule
其中产生移位输出的信号clk_int是由系统时钟clk和SPI输出时钟SCK结合产生的clk_int = (en&(!sck)) | ((!en)&clk);。在en(SPI模块使能信号)为高电平期间,clk_int是系统时钟clk,使模块不停地从并行的数据输入send_data16读取发送数据;en一旦变低SPI开始工作,则clk_int切换为SPI输出时钟SCK,以便于在SCK的驱动下逐位发送数据。
SPI模块的忙状态标志信号PROCING(后被例化为PROC_CHA和PROC_CHB)为CS取反得到,实现SPI模块和状态机模块之间的交互功能。但如果由状态机输出的CS信号取反产生,则有可能产生“先有鸡还是先有蛋”的矛盾:状态切换为SPI模块工作状态(SEND_CHA_STATE或SEND_CHB_STATE)后en将产生高电平,但作为被en使能的过程赋值语句所赋值的寄存器,CS只能在下一个时钟周期中才能产生响应。而摩尔型状态机将在下一个时钟周期结束时检测到被PROCING(也就是!CS)仍未变高,从而离开SPI输出状态SEND_CHA_STATE或SEND_CHB_STATE。仿真时序如下图所示。
图5 状态切换失败
从图中可以看到状态寄存器state_flag,被切换为SEND_CHA_STATE(编号为0x02)一个时钟周期后,由于PROCING(即cs取反)信号没有被置位,所以直接进入了下一个状态INTV1_STATE(编号为0x04)。我采取的解决办法,如上述代码所示:改为在时钟clk的下降沿切换CS的状态,即驱动CS的顺序执行块敏感信号变为:always @ (negedge clk or negedge en)。这样使CS和PROCING信号在en变高的半个clk周期后就被切换为工作状态,在下一个clk上升沿到来时PROCING已经被切换为了工作状态,从而保持住了SPI的工作状态。另外这样做还是的摩尔型状态机的状态切换的延迟时间降低为半个时钟周期。仿真时序如下图所示。
图6 状态切换正常的时序仿真图
五、 其他状态电路模块
除去两个SPI工作状态SEND_CHA_STATE和SEND_CHB_STATE之外,还有两通道发送间隙状态(INTV1_STATE)、数据发送完到加载数据之间的间隙状态(INTV2_STATE)和加载数据状态(LD_STATE)等三个状态只要根据MCP4822的数据手册产生适当时间的延迟即可。Verilog-HDL代码如下。
1 module INTV_2_CH(en,clk,PROCING); 2 //两个通道SPI数据加载之间的空闲时间 3 input en; 4 input clk; 5 output PROCING; //高电平表示正在进行延时处理 6 reg PROCING; 7 reg[19:0] cnt; 8 parameter CNT_NUM = 20'D5; //延时时间 9 always @(negedge clk or negedge en) 10 begin 11 if(!en) 12 begin 13 cnt[19:0] <= 20'd0; 14 PROCING <= 1'b0; 15 end 16 else begin 17 if(cnt[19:0] < CNT_NUM) 18 begin 19 PROCING <= 1'b1; 20 cnt[19:0] <= cnt[19:0] + 20'd1; 21 end 22 else begin 23 PROCING <= 1'b0; 24 cnt[19:0] <= cnt[19:0]; 25 end 26 end 27 end 28 endmodule
1 module INTV_LD(en,clk,LD,PROCING); 2 input en; 3 input clk; 4 output PROCING; //高电平表示正在进行延时处理 5 reg PROCING; 6 output LD; //控制MCP4822的模拟加载信号 7 reg LD; 8 reg[19:0] cnt; 9 parameter CNT_NUM = 20'D5; //模拟加载信号脉冲宽度 10 always @(negedge clk or negedge en) 11 begin 12 if(!en) 13 begin 14 cnt[19:0] <= 20'd0; 15 PROCING <= 1'b0; 16 LD <= 1'b1; 17 end 18 else begin 19 if(cnt[19:0] < CNT_NUM) 20 begin 21 PROCING <= 1'b1; 22 cnt[19:0] <= cnt[19:0] + 20'd1; 23 LD <= 1'b0; 24 end 25 else begin 26 PROCING <= 1'b0; 27 cnt[19:0] <= cnt[19:0]; 28 LD <= 1'b1; 29 end 30 end 31 end 32 endmodule
这两段代码都注意了状态标志信号的产生时钟更应该是clk的下降沿这个问题,从而解决了状态切换失败的问题。
六、 顶层驱动模块
顶层驱动模块的作用有二:其一,准备D/A输出的数据;其三,例化所有模块。具体代码如下所示。
1 module MCP4822(clk100m,rst_n,start,dac_data_a,dac_data_b,cs,mosi,sck,ld); 2 //驱动MCP4822的顶层模块 3 input clk100m; 4 input rst_n; 5 input start;//高电平表示DA转换起始的信号 6 input [11:0] dac_data_a;//DA的A通道转换数据 7 input [11:0] dac_data_b;//DA的B通道转换数据 8 output cs; 9 output mosi; 10 output sck; 11 output ld; 12 wire [15:0] data_cha;//DA的A通道SPI输入的数据,含有通道控制的4个位的数据 13 wire [15:0] data_chb;//DA的B通道SPI输入的数据,含有通道控制的4个位的数据 14 wire clk; 15 assign data_cha[15:0]={1'b0,1'b0,1'b1,1'b1,dac_data_a[11:0]};//这是MCP4822输出数据的格式,通道A,无关位,增益为1,不关断 16 assign data_chb[15:0]={1'b1,1'b0,1'b1,1'b1,dac_data_b[11:0]};//通道B,无关位,增益为1,不关断 17 18 wire[5:0] state_flag;//状态机输出的状态线 19 parameter IDLE_STATE = 6'B000001; //空闲状态 20 parameter SEND_CHA_STATE = 6'B000010; //发送通道A数据状态 21 parameter INTV1_STATE = 6'B000100; //两个发送之间的间隔状态 22 parameter SEND_CHB_STATE = 6'B001000; //发送通道B数据状态 23 parameter INTV2_STATE = 6'B010000; //发送数据和加载模拟信号之间的等待状态 24 parameter LD_STATE = 6'B100000; //加载模拟信号状态 25 wire PROC_CHA; //通道A输出信号标志位,高电平表示正在输出通道A数据,低电平表示输出完成。 26 wire PROC_INTV1; //两通道数据输出之间的间隔标志位,高电平表示状态机正在两通道数据输出之间的间隔期。 27 wire PROC_CHB; //通道B输出信号标志位,高电平表示正在输出通道B数据,低电平表示输出完成。 28 wire PROC_INTV2; //数据发送完成到加载数据之间的间隔标志位,高电平表示状态机正在数据发送和加载信号之间的间隔期。 29 wire PROC_LD; //模拟数据加载阶段标志位,高电平表示状态机正处在数据加载阶段 30 wire cs_cha; 31 wire mosi_cha; 32 wire sck_cha; 33 wire cs_chb; 34 wire mosi_chb; 35 wire sck_chb; 36 assign cs = (state_flag[5:0]==SEND_CHA_STATE)? cs_cha : cs_chb; 37 assign mosi = (state_flag[5:0]==SEND_CHA_STATE)? mosi_cha : mosi_chb; 38 assign sck = (state_flag[5:0]==SEND_CHA_STATE)? sck_cha : sck_chb; 39 assign clk=clk100m; 40 41 mcp4822_state_machine i_state_machine(//状态机例化模块 42 .clk(clk), 43 .rst_n(rst_n), 44 .state_flag(state_flag), 45 .START(start), 46 .PROC_CHA(PROC_CHA), 47 .PROC_INTV1(PROC_INTV1), 48 .PROC_CHB(PROC_CHB), 49 .PROC_INTV2(PROC_INTV2), 50 .PROC_LD(PROC_LD) 51 ); 52 53 mcp4822_spi i_mcp4822_spi_cha(//A通道SPI控制模块的例化 54 .send_data16(data_cha), 55 .en((state_flag[5:0]==SEND_CHA_STATE)), 56 .clk(clk), 57 .sck(sck_cha), 58 .mosi(mosi_cha), 59 .cs(cs_cha), 60 .PROCING(PROC_CHA) 61 ); 62 63 mcp4822_spi i_mcp4822_spi_chb(//B通道SPI控制模块的例化 64 .send_data16(data_chb), 65 .en((state_flag[5:0]==SEND_CHB_STATE)), 66 .clk(clk), 67 .sck(sck_chb), 68 .mosi(mosi_chb), 69 .cs(cs_chb), 70 .PROCING(PROC_CHB) 71 ); 72 73 INTV_2_CH i_intv_between_ch(//两个通道SPI通信之间的间隙模块例化 74 .en((state_flag[5:0]==INTV1_STATE)), 75 .clk(clk), 76 .PROCING(PROC_INTV1) 77 ); 78 INTV_2_CH i_intv_behind_ch(//两个通道SPI通信完成后到模拟数据加载之间的间隙模块例化 79 .en((state_flag[5:0]==INTV2_STATE)), 80 .clk(clk), 81 .PROCING(PROC_INTV2) 82 ); 83 84 INTV_LD i_intv_ld(//模拟数据加载模块例化 85 .en((state_flag[5:0]==LD_STATE)), 86 .clk(clk), 87 .LD(ld), 88 .PROCING(PROC_LD) 89 ); 90 91 endmodule
其中,SPI通信所需的16bits数据,除了12bits的D/A数据外,还有四个bits的配置信息,可由MCP4822数据手册查询其具体含义。实现代码为:
assign data_cha[15:0]={1'b0,1'b0,1'b1,1'b1,dac_data_a[11:0]};//这是MCP4822输出数据的格式,通道A,无关位,增益为1,不关断
assign data_chb[15:0]={1'b1,1'b0,1'b1,1'b1,dac_data_b[11:0]};//通道B,无关位
在START信号触发下,上述MCP4822电路的整体仿真情况如下图所示,完全满足数据手册的要求。
图7 MCP4822驱动模块整体仿真时序图
七、 A/D转换器MCP3202的接口电路设计
MCP3202是SPI接口的双通道A/D转换器,用状态机控制其工作的方式与MCP4822相近,在这里就不在赘述了。