上一篇教程介绍的是NEXYS4开发板上的温度传感器,用上了串口通信和I2C接口,这次使用的加速度传感器使用的是SPI接口,是除了I2C之外另一种常用的接口,实用性很高
NEXYS 4文档中写着它的麦克风芯片是Analog Device的ADMP421,从前面两篇教程可以看出,NEXYS4开发板基本上使用的都是Analog Device的芯片,所以它上面还打着这个公司的标签。它和FPGA的连接如下
这个芯片的文档在这里可以看到:ADMP421。 这个芯片已经很老了,以至于文档中都打着“过时”水印,不过毕竟NEXYS4也是很老的开发板了,是作为教育型开发板存在的,更多的是学习用
这个芯片的样子比较奇特,4个引脚加上一个收音孔,在开发板上看到标着MIC的那个孔不是用来焊接外设设备的,孔的背面就贴着这个芯片
虽然芯片和接口的连接很简单,但驱动方式还是要好好学一下的。这个芯片使用的是脉冲密度调制Pulse Density Modulation (PDM)
时钟一般在1MHz到3MHz中,我们可以把100MHz系统时钟降低40倍成2.5MHz使用。在PDM中,1对应正脉冲,0是负脉冲,保持1代表最大正值,保持0代表最大负值,就像下面所展示那样
它和外接输入的模拟信号对应关系如下,这个过程叫做delta-sigma调制,可以这么理解:输入的模拟电压越大,积分器累计的越快,就越容易超过积分器的阈值,激活寄存器。当积分器的阈值是输入模拟电压最大值的一半时,模拟输入为0产生的一直是0,模拟输入为最大值V产生的一直是1,模拟输入为最大值一半V/2时产生的是持续翻转的方波
麦克风还分左右声道,引脚L/R SEL的高电平代表左声道,低电平代表右声道,虽然我看不出来这个芯片怎么测左声道和右声道。在读取数据时,可以使用时钟的上升沿和下降沿分别探测两个声道的数据
NEXYS4开发板上的音频输出驱动不是用的某一款芯片,而是Sallen-Key Butterworth四阶低通滤波器,从开发板的设计图纸看到它的设计如下:
其中AUD_PWM连接到FPGA的A11引脚,AUD_SD连接到FPGA的D12引脚。它的作用是将PWM方波转换成相应的正弦波给3.5mm音频输出口。如果AUD_SD为低电平,则不经过放大,高电平使能放大器
PWM波和输出对应如下,它载体是一定周期的方波,其中高电平占的比例越高,代表输出模拟电压越高。如果输出信号的频率最高是5kHz,那PWM波的频率最好是50kHz以上以保证精度。
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文档中给出的例子
这里假设麦克风时钟周期是2.4MHz,应用需要7位数据,也就是最大值128,采样率24kHz,那样就需要像图中所示交替使用两个计数器
设计计划是,从麦克风以2.5MHz读取PDM数据,采样128个数据取密度,用它来产生PWM波,这个PWM波同时输出给LED和音频输出,然后戴上耳机看能不能听到相应声音
从上面对PDM波和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代码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中被#注释掉的那行去注释
这里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这里我们只需要看到相应的信号即可,真正的结果要连上耳机听:
下面就可以Run Implementation和Generate Bitstream生成bitstream了。
和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件。
从右向左第一个开关对应复位,第二个开关对应放大器使能,复位关闭,连接一个3.5mm接口耳机到开发板相应位置,放大器使能拨到0即可听到声音。打开之前记得不要戴着耳机,万一做的不对会有很大的噪音,对耳朵不好,确定没发出剧烈噪音再戴上听,别问我为什么知道,耳朵疼……
敲一敲开发板,或者对着麦克风吹口气能听到声音。怎么说呢,音质真的挺差的,可以听到很多白噪声,一方面是麦克风质量问题,另一方面更重要的是内部没有加任何滤波器。光是看仿真结果就可以看到白噪声,要滤出人耳最敏感的1kHz左右音频,应该加上一定的带通滤波器,滤除100Hz以下以及4kHz以上的噪音。
一个好的麦克风和音频播放器除了硬件之外,软件也是很重要的,这部分我们可以留着以后解决
这篇读取了麦克风信号,并转换成音频输出。虽然效果不是特别好,但今后加上滤波器后相信还是可以的。下一篇是FPGA开发板最后一个外接接口,MicroSD卡接口天坑,希望能够填上吧