20、AD/DA数据采集:ADC接口时序(SPI/并行)、DAC波形发生、数字示波器原型设计
这一章,我们来啃一块硬骨头——AD/DA数据采集。说实话,很多初学者一听到“高速采集”就头大,觉得那是高手才碰的东西。其实不然。你只要把ADC的接口时序搞明白,把DAC的波形发生逻辑理清楚,一个简易的数字示波器原型就能搭起来。我在项目中做过好几版采集系统,从低速的工业传感器到高速的射频采样,踩过的坑不少。今天我把这些经验掰开揉碎了讲给你听。
20.1 ADC接口时序:SPI与并行
ADC的接口,说白了就两大类:串行和并行。串行里最常见的是SPI,并行则多见于高速场合。咱们一个一个说。
20.1.1 SPI接口ADC
SPI接口的ADC,优点是引脚少,缺点是速度慢。一般用在几十K到几M采样率的场景。比如我常用的AD7689,16位精度,500kSPS,SPI接口,非常适合做精密测量。
SPI的时序其实不复杂,但有个细节容易翻车——数据是在SCK的哪个边沿采样的。不同的ADC,CPOL和CPHA可能不一样。我建议你养成一个习惯:拿到芯片手册,先看时序图,确认是“上升沿采样”还是“下降沿采样”。
下面是一个典型的SPI ADC读取时序代码(Verilog):
// SPI ADC读取模块(以AD7689为例)
module spi_adc_read (
input wire clk, // 系统时钟,比如50MHz
input wire rst_n, // 复位
input wire adc_start, // 启动转换
output reg [15:0] adc_data, // 读取到的16位数据
output reg adc_done, // 转换完成标志
// SPI接口
output reg sclk, // SPI时钟
output reg cs_n, // 片选(低有效)
output reg mosi, // 主机输出,从机输入
input wire miso // 主机输入,从机输出
);
// 状态机定义
localparam IDLE = 3'd0;
localparam START = 3'd1;
localparam SHIFT = 3'd2;
localparam DONE = 3'd3;
reg [2:0] state, next_state;
reg [4:0] bit_cnt; // 位计数器,0~15
reg [15:0] shift_reg; // 移位寄存器
// 状态机主逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
sclk <= 0;
cs_n <= 1;
mosi <= 0;
adc_done <= 0;
adc_data <= 16'd0;
end else begin
case (state)
IDLE: begin
cs_n <= 1;
sclk <= 0;
adc_done <= 0;
if (adc_start) begin
cs_n <= 0; // 拉低片选
state <= START;
end
end
START: begin
// 发送配置命令(这里简化,只读通道0)
mosi <= 1'b0; // 配置位
bit_cnt <= 5'd0;
state <= SHIFT;
end
SHIFT: begin
if (bit_cnt < 5'd16) begin
// 产生SCK时钟
sclk <= ~sclk;
if (sclk) begin // 下降沿采样MISO
shift_reg <= {shift_reg[14:0], miso};
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
state <= DONE;
end
end
DONE: begin
cs_n <= 1; // 释放片选
adc_data <= shift_reg; // 锁存数据
adc_done <= 1; // 完成标志
state <= IDLE;
end
endcase
end
end
endmodule
20.1.2 并行接口ADC
并行ADC,比如AD9280、AD9226这些,采样率动辄几十M到上百M。接口也简单——数据线直接输出,外加一个时钟信号。但简单归简单,时序约束反而更严格。
并行ADC的时序,核心就两个参数:建立时间(tSU)和保持时间(tH)。数据在时钟沿之后,必须稳定一段时间才能被正确采样。我在做一款100M采样率的采集卡时,就因为PCB走线长了那么几厘米,导致高速数据出错。后来加了等长走线和源同步时钟,问题才解决。
下面是一个并行ADC的读取示例(使用IDDR原语):
// 并行ADC数据捕获(使用IDDR)
module parallel_adc_capture (
input wire clk_adc, // ADC输出时钟(比如50MHz)
input wire [11:0] adc_data, // ADC数据总线(12位)
output reg [11:0] data_out, // 同步后的数据
output reg data_valid // 数据有效标志
);
// 使用IDDR原语捕获双沿数据(如果ADC是DDR模式)
wire [11:0] data_rise, data_fall;
genvar i;
generate
for (i = 0; i < 12; i = i + 1) begin : gen_iddr
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED")
) u_iddr (
.Q1(data_rise[i]),
.Q2(data_fall[i]),
.C(clk_adc),
.CE(1'b1),
.D(adc_data[i]),
.R(1'b0),
.S(1'b0)
);
end
endgenerate
// 这里以上升沿数据为例(单沿模式)
always @(posedge clk_adc) begin
data_out <= data_rise;
data_valid <= 1'b1;
end
endmodule
20.2 DAC波形发生
说完了ADC,咱们聊聊DAC。DAC的作用正好反过来——把数字信号变成模拟信号。波形发生,说白了就是让DAC按照你想要的形状输出电压。
常见的波形有正弦波、三角波、方波。实现方法其实都一样:查表法。你事先算好一个周期的波形数据,存到ROM里,然后按一定速率循环读取,DAC就输出对应的波形了。
20.2.1 正弦波发生器
正弦波是最常用的。我一般用MATLAB或者Python算好1024个点的正弦值,存成coe文件,然后例化一个ROM。下面是一个简单的实现:
// 正弦波发生器(查表法)
module sine_wave_gen (
input wire clk, // 系统时钟
input wire rst_n, // 复位
input wire [31:0] freq_word, // 频率控制字
output reg [11:0] dac_data // 输出到DAC的数据(12位)
);
// 相位累加器
reg [31:0] phase_acc;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_acc <= 32'd0;
end else begin
phase_acc <= phase_acc + freq_word;
end
end
// 取高10位作为ROM地址(1024点)
wire [9:0] rom_addr;
assign rom_addr = phase_acc[31:22];
// 例化正弦波ROM
sine_rom u_sine_rom (
.clka(clk),
.addra(rom_addr),
.douta(dac_data)
);
endmodule
20.2.2 三角波和方波
三角波更简单,不需要ROM。你用一个计数器,先递增到最大值,再递减到0,循环往复就行。方波就更简单了——输出高电平,延时,输出低电平,延时。
不过要注意,DAC的输出有建立时间。你切换数据太快,模拟输出可能跟不上。我遇到过一个问题:方波频率设到1MHz,结果DAC输出变成了三角波。后来查手册,发现DAC的建立时间是1us,1MHz的周期才1us,根本来不及建立。所以,DAC的更新速率要匹配它的建立时间。
20.3 数字示波器原型设计
好了,ADC和DAC都讲完了。现在把它们组合起来,就是一个简易的数字示波器原型。说白了,就是:ADC采集信号 → FPGA处理 → 显示到屏幕(或通过DAC输出)。
我当年做第一个示波器原型时,结构大概是这样的:
- 前端调理电路:信号进来,先经过运放做阻抗匹配和电平搬移,把信号调整到ADC的输入范围(比如0~2V)。
- ADC采样:用并行ADC(比如AD9280,8位,32M采样率)把模拟信号变成数字信号。
- 触发控制:这是示波器的灵魂。你需要一个触发模块,检测信号是否满足触发条件(比如上升沿超过阈值)。只有触发到了,才开始存储数据。
- 存储与显示:把触发后的数据存到FIFO或BRAM里,然后通过VGA或HDMI显示到屏幕上。如果只是原型,也可以用串口发到PC上显示。
下面是一个简单的触发检测模块:
// 上升沿触发检测
module trigger_detect (
input wire clk,
input wire rst_n,
input wire [7:0] adc_data, // ADC采样数据
input wire [7:0] trigger_level,// 触发电平
output reg trigger_out // 触发信号
);
reg [7:0] data_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_dly <= 8'd0;
trigger_out <= 1'b0;
end else begin
data_dly <= adc_data; // 延迟一拍
// 上升沿触发条件:上一拍低于阈值,当前拍高于阈值
if (data_dly < trigger_level && adc_data >= trigger_level) begin
trigger_out <= 1'b1;
end else begin
trigger_out <= 1'b0;
end
end
end
endmodule
20.4 本章小结
这一章内容不少,但核心就三件事:ADC怎么读、DAC怎么写、示波器怎么搭。SPI接口的ADC,注意时序模式;并行接口的ADC,注意建立保持时间。DAC波形发生,查表法最实用。数字示波器原型,触发模块是关键。
嗯,说实话,这些内容我当年也是反复调试才搞明白的。尤其是并行ADC的高速捕获,第一次做的时候,波形全是毛刺,查了两天才发现是PCB走线问题。所以,如果你第一次做没成功,别灰心。多看看时序图,多量量信号,问题总能找到。
下一章,我们会讲更高级的DDS信号发生器。到时候,波形发生这块会玩出更多花样。咱们下章见。