FPGA基础入门【17】开发板音频接口控制配置

上一篇教程介绍的是NEXYS4开发板上的温度传感器,用上了串口通信和I2C接口,这次使用的加速度传感器使用的是SPI接口,是除了I2C之外另一种常用的接口,实用性很高

FPGA基础入门【17】开发板音频接口控制配置

  • 开发板音频控制
    • 麦克风接口简介
    • 音频输出接口简介
  • 逻辑设计
    • 顶层代码
  • 模拟仿真
    • Testbench
    • 仿真脚本
    • 仿真结果
  • 编译烧写
    • 结果
  • 总结

开发板音频控制

NEXYS 4文档中写着它的麦克风芯片是Analog Device的ADMP421,从前面两篇教程可以看出,NEXYS4开发板基本上使用的都是Analog Device的芯片,所以它上面还打着这个公司的标签。它和FPGA的连接如下
FPGA基础入门【17】开发板音频接口控制配置_第1张图片
这个芯片的文档在这里可以看到:ADMP421。 这个芯片已经很老了,以至于文档中都打着“过时”水印,不过毕竟NEXYS4也是很老的开发板了,是作为教育型开发板存在的,更多的是学习用

这个芯片的样子比较奇特,4个引脚加上一个收音孔,在开发板上看到标着MIC的那个孔不是用来焊接外设设备的,孔的背面就贴着这个芯片

麦克风接口简介

虽然芯片和接口的连接很简单,但驱动方式还是要好好学一下的。这个芯片使用的是脉冲密度调制Pulse Density Modulation (PDM)

时钟一般在1MHz到3MHz中,我们可以把100MHz系统时钟降低40倍成2.5MHz使用。在PDM中,1对应正脉冲,0是负脉冲,保持1代表最大正值,保持0代表最大负值,就像下面所展示那样
FPGA基础入门【17】开发板音频接口控制配置_第2张图片
它和外接输入的模拟信号对应关系如下,这个过程叫做delta-sigma调制,可以这么理解:输入的模拟电压越大,积分器累计的越快,就越容易超过积分器的阈值,激活寄存器。当积分器的阈值是输入模拟电压最大值的一半时,模拟输入为0产生的一直是0,模拟输入为最大值V产生的一直是1,模拟输入为最大值一半V/2时产生的是持续翻转的方波
FPGA基础入门【17】开发板音频接口控制配置_第3张图片
麦克风还分左右声道,引脚L/R SEL的高电平代表左声道,低电平代表右声道,虽然我看不出来这个芯片怎么测左声道和右声道。在读取数据时,可以使用时钟的上升沿和下降沿分别探测两个声道的数据
FPGA基础入门【17】开发板音频接口控制配置_第4张图片

音频输出接口简介

NEXYS4开发板上的音频输出驱动不是用的某一款芯片,而是Sallen-Key Butterworth四阶低通滤波器,从开发板的设计图纸看到它的设计如下:
FPGA基础入门【17】开发板音频接口控制配置_第5张图片
其中AUD_PWM连接到FPGA的A11引脚,AUD_SD连接到FPGA的D12引脚。它的作用是将PWM方波转换成相应的正弦波给3.5mm音频输出口。如果AUD_SD为低电平,则不经过放大,高电平使能放大器

PWM波和输出对应如下,它载体是一定周期的方波,其中高电平占的比例越高,代表输出模拟电压越高。如果输出信号的频率最高是5kHz,那PWM波的频率最好是50kHz以上以保证精度。
FPGA基础入门【17】开发板音频接口控制配置_第6张图片
FPGA基础入门【17】开发板音频接口控制配置_第7张图片
PWM波和PDM波有一定的区别,如果要把麦克风信号传到音频输出,需要一定的转换。

人耳能识别的频率范围是20Hz到20kHz,一般好的音质需要到达44kHz左右。我们选择20kHz,那么需要PWM周期对应200kHz,在100MHz系统时钟下是500个时钟周期,为了二进制方便改成512个时钟周期,此时PWM周期为19.5kHz。

麦克风用的是2.5MHz,使用7位精度,最大值128,计算密度需要51.2us,但PWM每5.12us就需要一个数据,因此我们需要10个计数器来交替计算。

为了解释这个工作原理,可以看NEXYS4文档中给出的例子
FPGA基础入门【17】开发板音频接口控制配置_第8张图片
这里假设麦克风时钟周期是2.4MHz,应用需要7位数据,也就是最大值128,采样率24kHz,那样就需要像图中所示交替使用两个计数器

逻辑设计

设计计划是,从麦克风以2.5MHz读取PDM数据,采样128个数据取密度,用它来产生PWM波,这个PWM波同时输出给LED和音频输出,然后戴上耳机看能不能听到相应声音

顶层代码

从上面对PDM波和PWM波的分析,我们做如下设计:

  1. 100MHz系统时钟(周期10ns)用来生成PWM波,512个时钟为一个周期,用一个阈值来控制PWM波中高电平的占比
  2. 100MHz系统时钟减缓40倍到2.5MHz(周期400ns)作为PDM波的读取时钟
  3. PDM波读取128个样本的时间为400ns128=51.2us,PWM一个周期为10ns512=5.12us,我们需要10个计数器交替计数,就像原理分析时候的例子
  4. 以PDM波的128个采样计数为基准,定出10个参考点,每当PDM计数到这个参考点就清空相应计数器的值,重新计数,这样每当到达参考点时,相应计数器就记录了128个采样的高电平比例。10个参考点位置为floor(128*i/10)={0, 12, 25, 38, 51, 64, 76, 89, 102, 115}
  5. 由于数字逻辑中只有整数,设置另一个计数器,频率是100MHz,以5120为周期(128个PDM采样周期),在十个PDM参考点的40倍即计数器完成计数时读取密度,乘以4倍(上限128转换到512),作为下一个PWM周期的阈值

基于上面的设计,新建代码文件microphone.v,源代码如下:

顶层定义,时钟复位LED,加上一个开关sd_sw,用来控制音频输出的放大器使能,另外加上麦克风和音频输出的接口

module microphone(
   input      clk,
   input      rst,
   output reg led,
   input      sd_sw,   // a switch to control the amplifier
   
   // Port to microphone
   output reg MIC_CLK,
   input      MIC_DATA,
   output reg MIC_LR_SEL,
   
   // Port to mono audio output
   output reg AUD_PWM,
   output reg AUD_SD
);

系统时钟减缓40倍生成麦克风读取时钟2.5MHz,并探测其上升沿

// Generate 2.5MHz to MIC_CLK, and rising edge detection
reg [7:0] MIC_CLK_count;
reg       MIC_CLK_d;
wire      MIC_CLK_posedge;
always @(posedge clk or posedge rst) begin
    if(rst) begin
        MIC_CLK <= 1'b0;
        MIC_CLK_count <= 8'd0;
    end
    else if(MIC_CLK_count < 8'd19) begin
        MIC_CLK_count <= MIC_CLK_count + 8'd1;
    end
    else begin
        MIC_CLK <= ~MIC_CLK;
        MIC_CLK_count <= 8'd0;
    end
end

always @(posedge clk) begin
    MIC_CLK_d <= MIC_CLK;
end
assign MIC_CLK_posedge = ({MIC_CLK_d, MIC_CLK}==2'b01) ? 1'b1 : 1'b0;

生成十个计数器,在每个MIC_CLK的上升沿累积MIC_DATA的高电平数量,并在相应参考点清空计数器(第一个数据还是要记录的)

// PWM counter increase every rising edge of MIC_CLK
// period is 128
genvar i;
reg [7:0] PDM_counter;
always @(posedge clk or posedge rst) begin
    if(rst) begin
        PDM_counter <= 8'd0;
    end
    else if(MIC_CLK_posedge) begin
        if(PDM_counter == 8'd127) begin
            PDM_counter <= 8'd0;
        end
        else begin
            PDM_counter <= PDM_counter + 8'd1;
        end
    end
end

// ten counters control, start by floor(12.8*i)
localparam [8*10-1:0] PWM_thresh_counter_start = 
{8'd115, 8'd102, 8'd89, 8'd76, 8'd64, 
8'd51, 8'd38, 8'd25, 8'd12, 8'd0};

reg [7:0] PWM_thresh_counter[9:0];
generate
    for(i=0; i<10; i=i+1) begin : PWM_COUNTERS
        always @(posedge clk or posedge rst) begin
            if(rst) begin
                PWM_thresh_counter[i] <= 8'd0;
            end
            else if(MIC_CLK_posedge && (PDM_counter == PWM_thresh_counter_start[8*i+7:8*i])) begin
                PWM_thresh_counter[i] <= (MIC_DATA) ? 8'd1 : 8'd0;
            end
            else if(MIC_CLK_posedge) begin
                PWM_thresh_counter[i] <= (MIC_DATA) ? PWM_thresh_counter[i] + 8'd1 : PWM_thresh_counter[i];
            end
        end
    end
endgenerate

定义一个5120系统时钟周期的计数器PWM_count,在十个参考点的40倍位置获取相应计数器累积的PDM波密度,并左移2位(乘以4倍),作为PWM波的阈值

reg [15:0] PWM_count;
reg [15:0] led_threshold;
always @(posedge clk or posedge rst) begin
    if(rst) begin
        PWM_count <= 16'd0;
        led_threshold <= 16'h0;
        MIC_LR_SEL <= 1'b0;
    end
    else begin
        PWM_count <= (PWM_count == 16'd5119) ? 16'd0 : PWM_count + 16'd1;
        case(PWM_count)
        16'd0    : begin led_threshold <= {6'h0, PWM_thresh_counter[0], 2'b00}; end
        16'd480  : begin led_threshold <= {6'h0, PWM_thresh_counter[1], 2'b00}; end
        16'd1000 : begin led_threshold <= {6'h0, PWM_thresh_counter[2], 2'b00}; end
        16'd1520 : begin led_threshold <= {6'h0, PWM_thresh_counter[3], 2'b00}; end
        16'd2040 : begin led_threshold <= {6'h0, PWM_thresh_counter[4], 2'b00}; end
        16'd2560 : begin led_threshold <= {6'h0, PWM_thresh_counter[5], 2'b00}; end
        16'd3040 : begin led_threshold <= {6'h0, PWM_thresh_counter[6], 2'b00}; end
        16'd3560 : begin led_threshold <= {6'h0, PWM_thresh_counter[7], 2'b00}; end
        16'd4080 : begin led_threshold <= {6'h0, PWM_thresh_counter[8], 2'b00}; end
        16'd4600 : begin led_threshold <= {6'h0, PWM_thresh_counter[9], 2'b00}; end
        endcase
        MIC_LR_SEL <= 1'b0;
    end
end

用刚刚生成的PWM阈值生成PWM波,驱动LED灯

// Drive led based on MIC_DATA, by generating PWM wave
reg [15:0] led_count;
always @(posedge clk or posedge rst) begin
    if(rst) begin
        led_count <= 16'd0;
    end
    else if(led_count < 16'd512) begin
        led_count <= led_count + 16'd1;
    end
    else begin
        led_count <= 16'd0;
    end
end

always @(posedge clk or posedge rst) begin
    if(rst) begin
        led <= 1'b0;
    end
    else begin
        if(led_count

把生成好的PWM波直接连接到音频输出,顺便驱动放大器使能

// Audio output drive, directly use the PWM above
always @(posedge clk or posedge rst) begin
    if(rst) begin
        AUD_SD <= 1'b0;
        AUD_PWM <= 1'b0;
    end
    else begin
        AUD_SD <= sd_sw;
        AUD_PWM <= led;
    end
end

endmodule

模拟仿真

ModelSim仿真模拟需要一个Testbench和一个仿真脚本,和过去一样

Testbench

Testbench代码tb_microphone.v源代码如下:

随着输出的2.5MHz MIC_CLK生成一个随机数据,作为虚拟PDM波,理论上它的密度在50%左右波动

`timescale 1ns/1ns

module tb_microphone;

reg         clock;
reg         reset;
wire        led;

// Ports to MIC
wire        MIC_CLK;
reg         MIC_DATA;
wire        MIC_LR_SEL;

// Ports to audio
wire        AUD_PWM;
wire        AUD_SD;

initial begin
    clock = 1'b0;
    reset = 1'b0;
    
    MIC_DATA = 1'b0;
    
    // Reset for 1us
    #100 
    reset = 1'b1;
    #1000
    reset = 1'b0;
    
    // Generate random data to MIC port
    while(1) begin
        @(negedge MIC_CLK) MIC_DATA = $random%2;
    end
end

// Generate 100MHz clock signal
always #5 clock <= ~clock;

microphone microphone(
    .clk        (clock),
    .rst        (reset),
    .led        (led),
    .sd_sw      (1'b0),
    
    // Port to microphone
    .MIC_CLK    (MIC_CLK),
    .MIC_DATA   (MIC_DATA),
    .MIC_LR_SEL (MIC_LR_SEL),
    
    // Port to mono audio output
    .AUD_PWM    (AUD_PWM),
    .AUD_SD     (AUD_SD)
);

endmodule

仿真脚本

新建脚本文件sim.do,源代码如下:

由于PDM波是2.5MHz驱动,因此需要跑长一点看结果

vlib work
vlog ../src/microphone.v ./tb_microphone.v
vsim work.tb_microphone -voptargs=+acc +notimingchecks
log -depth 7 /tb_microphone/*
#do wave.do
run 2ms

调用前面全部的代码,打开ModelSim后转到脚本在的路径,使用命令do sim.do即可开始仿真。

仿真时可以添加想要的信号到waveform窗口中观察,然后可以保存为wave.do,这样下次可以通过调用它来加入一样的信号,节省一个一个加入的时间,这时你可以把sim.do中被#注释掉的那行去注释

仿真结果

调用仿真脚本后,加入想看的相应信号,得到波形如下:
waveform

这里led_threshold就是控制PWM波的阈值,将其配置为波形模式,可以看到在初始化后,其大小保持在最大值512的50%,也就是256附近

编译烧写

新建一个叫microphone的project,配置为开发板NEXYS4。添加代码文件microphone.v

下一步加入约束constraint文件microphone.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):

## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]


##Switches

set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]


## LEDs

set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports led]
set_property -dict {PACKAGE_PIN L16 IOSTANDARD LVCMOS33} [get_ports sd_sw]

##Omnidirectional Microphone

set_property -dict {PACKAGE_PIN J5 IOSTANDARD LVCMOS33} [get_ports MIC_CLK]
set_property -dict {PACKAGE_PIN H5 IOSTANDARD LVCMOS33} [get_ports MIC_DATA]
set_property -dict {PACKAGE_PIN F5 IOSTANDARD LVCMOS33} [get_ports MIC_LR_SEL]


##PWM Audio Amplifier

set_property -dict {PACKAGE_PIN A11 IOSTANDARD LVCMOS33} [get_ports AUD_PWM]
set_property -dict {PACKAGE_PIN D12 IOSTANDARD LVCMOS33} [get_ports AUD_SD]


##USB-RS232 Interface

#set_property -dict { PACKAGE_PIN C4    IOSTANDARD LVCMOS33 } [get_ports { UART_TXD_IN }]; #IO_L7P_T1_AD6P_35 Sch=uart_txd_in
#set_property -dict { PACKAGE_PIN D4    IOSTANDARD LVCMOS33 } [get_ports { UART_RXD_OUT }]; #IO_L11N_T1_SRCC_35 Sch=uart_rxd_out
#set_property -dict { PACKAGE_PIN D3    IOSTANDARD LVCMOS33 } [get_ports { UART_CTS }]; #IO_L12N_T1_MRCC_35 Sch=uart_cts
#set_property -dict { PACKAGE_PIN E5    IOSTANDARD LVCMOS33 } [get_ports { UART_RTS }]; #IO_L5N_T0_AD13N_35 Sch=uart_rts

到这里可以点击 Run Synthesis做综合,几分钟完成后用Set Up Debug配置ChipScope,加入和麦克风以及音频输出有关的接口,并设置长度为8192这里我们只需要看到相应的信号即可,真正的结果要连上耳机听:
FPGA基础入门【17】开发板音频接口控制配置_第9张图片
FPGA基础入门【17】开发板音频接口控制配置_第10张图片
下面就可以Run Implementation和Generate Bitstream生成bitstream了。

和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件。

结果

从右向左第一个开关对应复位,第二个开关对应放大器使能,复位关闭,连接一个3.5mm接口耳机到开发板相应位置,放大器使能拨到0即可听到声音。打开之前记得不要戴着耳机,万一做的不对会有很大的噪音,对耳朵不好,确定没发出剧烈噪音再戴上听,别问我为什么知道,耳朵疼……

从ChipScope里看到的波形如下:
FPGA基础入门【17】开发板音频接口控制配置_第11张图片

敲一敲开发板,或者对着麦克风吹口气能听到声音。怎么说呢,音质真的挺差的,可以听到很多白噪声,一方面是麦克风质量问题,另一方面更重要的是内部没有加任何滤波器。光是看仿真结果就可以看到白噪声,要滤出人耳最敏感的1kHz左右音频,应该加上一定的带通滤波器,滤除100Hz以下以及4kHz以上的噪音。

一个好的麦克风和音频播放器除了硬件之外,软件也是很重要的,这部分我们可以留着以后解决

总结

这篇读取了麦克风信号,并转换成音频输出。虽然效果不是特别好,但今后加上滤波器后相信还是可以的。下一篇是FPGA开发板最后一个外接接口,MicroSD卡接口天坑,希望能够填上吧

你可能感兴趣的:(FPGA)