一、前言
在上期文章中讲述了小助手的系统结构和原理。在PYNQ的框架开发中,我们一般可以将PL端当做PS端的一个外设,通过读写寄存器的方式来操作外设的功能,就类似于在开发ARM和DSP中操作外设一样,不同时的是,我们可以通过FPGA定制我们自己需要的外设结构,例如本次文章需要提到的DDS外设。
二、外设功能介绍
在开发外设之前首先要明确这个外设的功能:
1、由于外部电流采用的是八路DA7512单通道DAC所以要实现DDS功能我们就需要再FPGA中搭建一个正弦波发生器,当然正弦波发生器有多种多样的实现方式,可以采用ROM查表的方式,也可以使用DSP实时计算。我这里就直接使用了Vivado提供的DDS IP核了(其本质也是通过ROM查表的方式),IP设置如下图1所示
2、DDS生成的正弦波需要可以控制其频率和初始相位功能;
3、虽然有DDS功能但是也同样要兼容普通DA功能;
4、一共八个通道,所以需要对八个通道的状态进行设置;
综上,我们可以设计出DAC控制寄存器和DAC波形控制器如下图2和图3所示
三、外设搭建
在ZYNQ中,搭建数据传输比较慢的外设时可以使用AXI_GP接口总线;AXI_GP接口总线可以视作为一种寄存器映射总线。最后将总线和逻辑功能封装成一个IP核,如下图4所示(这里就不放具体代码了,详细代码请移步到开源地址)
该模块具有一个AXI_GP接口的从机和七个DAC波形控制器数据,和一个DAC控制器数据,后续的DDS模块就利用这些寄存器数据来对DAC外设控制,DDS顶层代码如下:
//!DAC_DDS顶层,这里用来通过PS端写来的寄存器控制DAC的输出
module dac_dds_Top(
input sysClk, //系统时钟
input dacClk, //!DAC的时钟,最大为30M
input sysRst,
//DAC外设接口
input [31:0]Dac_Ch2_reg, //!通道2数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch3_reg, //!通道3数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch4_reg, //!通道4数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch5_reg, //!通道5数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch6_reg, //!通道6数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch7_reg, //!通道7数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ch8_reg, //!通道8数据寄存器,详细参考寄存器手册
input [31:0]Dac_Ctrl_reg, //!DAC控制寄存器,DAC模式:0=关闭DAC;1=普通DAC输出;2=正弦波输出
//DAC7512引脚
output [5:0]dac_Clk, //!DAC的时钟
output [5:0]dac_Din, //!DAC数据引脚
output [5:0]dac_Syn //!DAC同步引脚
);
localparam DAC_CHx = 6; //6个通道
//系统时钟域的寄存器和网线
wire [31:0]Dac_chx_wire[0:DAC_CHx-1]; //!输入的寄存器线网数组
reg DDS_Run_sysClk[0:DAC_CHx-1]; //!DDS运行-系统时钟域
reg [15:0]DDS_Fre_sysClk[0:DAC_CHx-1]; //!DDS的频率-系统时钟域
reg [15:0]DDS_Phase_sysClk[0:DAC_CHx-1]; //!DDS相位-系统时钟域
reg [15:0]DAC_OutData_sysClk[0:DAC_CHx-1]; //!普通模式下DAC的输出
reg DAC_enable_sysClk[0:DAC_CHx-1]; //!DAC使能信号-系统时钟域
//DAC时钟域的寄存器和网线
wire DDS_Run_dacClk[0:DAC_CHx-1]; //!DDS运行-DAC时钟域
wire [15:0]DDS_Fre_dacClk[0:DAC_CHx-1]; //!DDS的频率-DAC时钟域
wire [15:0]DDS_Phase_dacClk[0:DAC_CHx-1]; //!DDS相位-DAC时钟域
wire [15:0]DDS_Phase_out_dacClk[0:DAC_CHx-1]; //!驱动DDS的相位
wire [15:0]DDS2Dac_dacClk[0:DAC_CHx-1]; //!DDS->DAC
wire [15:0]DAC_OutData_dacClk[0:DAC_CHx-1]; //!普通模式下DAC的输出-DAC时钟域
reg [15:0]DAC_Data[0:DAC_CHx-1]; //!DAC模块的输入数据
wire DAC_enable_dacClk[0:DAC_CHx-1]; //!DAC使能信号-DAC时钟域
genvar i;
//!输入寄存器线网整理 这里的数量与 DAC_CHx 有关
assign Dac_chx_wire[0][31:0] = Dac_Ch3_reg;
assign Dac_chx_wire[1][31:0] = Dac_Ch4_reg;
assign Dac_chx_wire[2][31:0] = Dac_Ch5_reg;
assign Dac_chx_wire[3][31:0] = Dac_Ch6_reg;
assign Dac_chx_wire[4][31:0] = Dac_Ch7_reg;
assign Dac_chx_wire[5][31:0] = Dac_Ch8_reg;
//! DDS运行控制和DAC使能控制
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:AXI_REG_RUN
always @(posedge sysClk)begin
if(sysRst)begin
DDS_Run_sysClk[i] <= 1'd0;
DAC_enable_sysClk[i] <= 1'd0;
end
else begin
DDS_Run_sysClk[i] <= Dac_Ctrl_reg[(i+2)*4+:4]==4'd2 ? 1'd1 : 1'd0; //DDS使能信号
DAC_enable_sysClk[i] <= Dac_Ctrl_reg[(i+2)*4+:4]==4'd0 ? 1'd0 : 1'd1; //DAC使能信号
end
end
end
endgenerate
//! DDS频率、相位和普通DAC数据
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:AXI_REG_DATA
always @(posedge sysClk)begin
if(sysRst)begin
DDS_Fre_sysClk[i][15:0] <= 16'd0;
DDS_Phase_sysClk[i][15:0] <= 16'd0;
DAC_OutData_sysClk[i][15:0] <= 16'd0;
end
else begin
DDS_Fre_sysClk[i][15:0] <= Dac_chx_wire[i][31:16]; //频率数据
DDS_Phase_sysClk[i][15:0] <= Dac_chx_wire[i][15:0]; //相位数据
DAC_OutData_sysClk[i][15:0] <= Dac_chx_wire[i][15:0]; //普通DAC数据
end
end
end
endgenerate
//!时钟域数据转换--DDS运行信号
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:CLOCK_DDS_RUN
shift # (
.WIDTH(1),
.SHIFT_MUN(3)
)
shift_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.inData(DDS_Run_sysClk[i]),
.outData(DDS_Run_dacClk[i])
);
end
endgenerate
//!时钟域数据转换--DDS频率
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:CLOCK_DDS_FRE
shift # (
.WIDTH(16),
.SHIFT_MUN(3)
)
shift_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.inData(DDS_Fre_sysClk[i][15:0]),
.outData(DDS_Fre_dacClk[i][15:0])
);
end
endgenerate
//!时钟域数据转换--DDS相位
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:CLOCK_DDS_PHASE
shift # (
.WIDTH(16),
.SHIFT_MUN(3)
)
shift_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.inData(DDS_Phase_sysClk[i][15:0]),
.outData(DDS_Phase_dacClk[i][15:0])
);
end
endgenerate
//!时钟域数据转换--普通模式下的DAC输出
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:CLOCK_OUTDATA
shift # (
.WIDTH(16),
.SHIFT_MUN(3)
)
shift_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.inData(DAC_OutData_sysClk[i][15:0]),
.outData(DAC_OutData_dacClk[i][15:0])
);
end
endgenerate
//!时钟域数据转换--DAC使能信号
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:CLOCK_DAC_ENABLE
shift # (
.WIDTH(1),
.SHIFT_MUN(3)
)
shift_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.inData(DAC_enable_sysClk[i]),
.outData(DAC_enable_dacClk[i])
);
end
endgenerate
//!DDS相位生成
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:DdsPhase
DDS_Phase # (
.CH_NUM(DAC_CHx)
)
DDS_Phase_u0 (
.sysClk(dacClk),
.sysRst(sysRst),
.Run(DDS_Run_dacClk[i]),
.Fre(DDS_Fre_dacClk[i][15:0]),
.Phase(DDS_Phase_dacClk[i][15:0]),
.DDS_Phase_Data(DDS_Phase_out_dacClk[i][15:0])
);
end
endgenerate
//!DDS输出,该模块输出到DAC
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:DDS
dds_compiler_0 dds_compiler_u0 (
.aclk(dacClk), // input wire aclk
.s_axis_phase_tvalid(1'd1), // input wire s_axis_phase_tvalid
.s_axis_phase_tdata(DDS_Phase_out_dacClk[i][15:0]), // input wire [15 : 0] s_axis_phase_tdata
.m_axis_data_tvalid(), // output wire m_axis_data_tvalid
.m_axis_data_tdata(DDS2Dac_dacClk[i][15:0]) // output wire [15 : 0] m_axis_data_tdata
);
end
endgenerate
//!DAC数据控制
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:DAC_DATA
always @(posedge dacClk)begin
if(sysRst)begin
DAC_Data[i][15:0] <= 16'd0;
end
//关闭了DAC
else if(DAC_enable_dacClk[i]==0)begin
DAC_Data[i][15:0] <= 16'd0;
end
//DDS模式
else if(DDS_Run_dacClk[i])begin
DAC_Data[i][15:0] <= 16'h8000 - DDS2Dac_dacClk[i][15:0];
end
//普通模式
else begin
DAC_Data[i][15:0] <= DAC_OutData_dacClk[i][15:0];
end
end
end
endgenerate
//!DAC7512驱动
generate
for(i=0;i<DAC_CHx;i=i+1)
begin:DAC
DAC7512 DAC7512_inst (
.sysClk(dacClk),
.sysRst(sysRst),
.Data(DAC_Data[i][15:4]),
.Start(DAC_enable_dacClk[i]),
.Done(),
.dac_Clk(dac_Clk[i]),
.dac_Din(dac_Din[i]),
.dac_Syn(dac_Syn[i])
);
end
endgenerate
endmodule
在代码中,DAC使用了30M的时钟,AXI总线是100M时钟,所以控制数据在这里需要做跨时钟域处理,由于是快速时钟到慢速时钟,我们这里对数据进行打拍处理,并且做多周期约束即可,约束如下所示
#创建30M的DAC时钟时钟,主时钟对该时钟做多周期约束。 3个周期
create_clock -period 33.333 -name dacClk -waveform {0.000 16.667} [get_pins design_1_i/clk_wiz_0/clk_30M]
set_multicycle_path -setup -start -from [get_clocks *fpga*] -to [get_clocks *dacClk*] 3
set_multicycle_path -hold -start -from [get_clocks *fpga*] -to [get_clocks *dacClk*] 3
四、结论
在使用PYNQ框架开发ZYNQ时,PL端都可以视作PS端的外设。本文章只介绍了一种使用慢速数据的外设,如果你写的外设是一个需要高速数据传输的,例如视频解码等。就可以考虑使用AXI_HP接口配合PL端的DMA来实现。
下一个一篇文章是讲述小助手项目的OLED显示是如何在PYNQ架构上实现的。