Search Suggestions
Mojo FPGA Microphone 声音定位案例
在本教程中,我们将使用麦克风来定位声音的方向。 这是一个相当高级的教程,可以让你对 Mojo FPGA 开发有一个更加深刻的了解,我们将首先介绍理论并研究如何在FPGA上实现这一点。

Mojo FPGA Microphone 声音定位案例

在本教程中,我们将使用麦克风来定位声音的方向。 这是一个相当高级的教程,可以让你对 Mojo FPGA 开发有一个更加深刻的了解,我们将首先介绍理论并研究如何在FPGA上实现这一点。

TONYLABS 04 Jul, 2019

麦克风有七个 PDM 脉冲调制麦克风。 这种类型的麦克风具有数字接口。 你提供一个时钟,他们输出一系列的位。 这些位的密度表示被检测音频的大小。
Microphone Shield 还有16个LED,这些LED排列成环状,并在Mojo现有的LED上多路复用。 
我们将使用这些LED在视觉上输出声音方向。

基础理论

我们的想法是,将在同一时间录制来自所有七个麦克风的简短音频样本。 然后我们可以使用这些样本来计算麦克风环和中心麦克风之间的延迟。 这些延迟可用于检测声音源的方向。 

为此,我们需要做出一些相当合理的假设。 首先是所有声音都来自电路板的两侧,而不是来自上方或下方。 这是必需的,因为我们只有一个2D网格的麦克风。 第二个假设是声波具有直的波前。 这通常不正确,因为源自点的声音将具有弯曲的波前,但只要源不太近,它将是合理的近似值。 实际上,我们假设样本中的每个频率都来自单一方向。

那么我们如何计算麦克风之间的延迟呢?如果我们有一个简单的脉冲并且一切都很安静,只需查看每个样本中的脉冲峰值就可以了。然而,现实世界并不是那么好。相反,我们将使用每个样本中不同频率的相位。这有两个主要好处:使用快速傅里叶变换(FFT)通过FPGA计算相位非常容易,另一个是我们可以检测多个声源,只要它们的频率成分足够不同即可。想象一下,鸟儿鸣叫,有人说话。鸟唧唧的音高比说话的人高得多,我们应该能够同时检测到它们。 

如果您不熟悉 FFT,请不要太担心。对于这个例子,您需要知道的是傅里叶变换在时域中获取信号,这意味着样本的x轴是时间,并将其转换为频域。它告诉您需要将正弦波(及其幅度)加在一起以获得完全相同的信号。如果您曾经见过音乐均衡器,那么您已经看到了FFT的运行情况。这正是Clock / Visualizer Shield的演示。 

因此,在从所有七个麦克风收集一个短样本后,我们可以通过FFT运行每个麦克风以获得频率分量。每个频率的FFT输出是一个复数。数字的实部对应于正弦部分的大小,虚部对应于余弦部分。通过将不同幅度的正弦波和余弦波相加,您可以创建任意相位的正弦波。 

FFT的原始输出对我们并不特别有用。相反,如果我们知道每个频率的相位和幅度会更好。要做到这一点,我们需要转换复数,这可以被认为是将笛卡尔坐标(如果有帮助)转换为极坐标。基本上,如果我们要在常规2D空间上绘制复数,而不是坐标的x和y位置,我们想要知道它与原点的角度和距离。再次,这对于FPGA来说并不算太糟糕,您将在后面看到。 

通过计算每个频率的相位,我们可以从周围的六个麦克风中的每个麦克风中减去中心麦克风的相位,以获得每个麦克风的相位偏移。使用公式延迟=相位偏移/频率,我们可以计算该频率的延迟。但是,通过所有六个麦克风的恒定因子(频率)来缩放延迟将不会产生差异,我们可以利用这一事实来避免FPGA中的高成本划分。相反,我们将简单地使用相位偏移,就像它们是延迟一样,因为它们与它们成比例。 

现在我们对每个麦克风相对于中心麦克风有一个延迟,我们需要将它们组合起来以获得整体方向。为此,我们需要通过相应的延迟来缩放每个麦克风的位置矢量并将它们相加。这将为我们提供指向声源方向的单个矢量。 

下图以几何形式显示了这一点。为简单起见,我只画了三个麦克风。添加其他三个将使缩放矢量的总和两倍,但由于对称性而不会改变方向。我还画了这个,以便声音来自y方向,麦克风旋转φ而不是以角度φ进入的声音。这样可以更容易地显示此方法稍后有效。黑色圆圈表示麦克风的位置,并标记它们的坐标。

每个麦克风到原点(中心麦克风)的延迟与麦克风位置的y值成比例。 我们可以绘制这些,如下所示。

如果我们取出每个麦克风的位置并按相应的延迟进行缩放,我们会得到下图中显示的新紫色线条。 请注意,底部麦克风具有负延迟,因此矢量指向相反方向。

最后,我们可以采用三个缩放的向量,并通过逐个移动它们将它们相加。 这由下面的浅紫色线表示。 橙色矢量是三个缩放矢量的总和的结果。

请注意,求和的缩放矢量的x分量取消,因此得到的和仅指向正y方向。为了证明这种方法有效,我们需要证明x分量取消,y分量总和为任何φ值的正值。我们不关心最终矢量的大小,只关心方向。 

证明这是真的很容易,但我不打算在这里重现证明。如果你很好奇,我的书中就完全涵盖了它。 

现在我们有了频率的角度,我们需要将每个角度聚合成有用的东西。我选择将它们分成16个方向,以便更容易使用并在LED上显示它们。我使用中心麦克风的幅度来衡量每个频率贡献的重要性。这是通过迭代每个频率,确定它所属的箱子,以及保持每个箱子的大小的运行总计来完成的。 

最终输出是16个值,表示从该方向传来多少噪音。

开始实施

现在我们已经了解了这将如何工作,我们需要提出一个在硬件中实现这一点的计划。 

首先,我们需要在同一时间从所有七个麦克风收集音频样本。这个屏蔽上的麦克风是PDM麦克风,意味着它们以高速率(在这种情况下为2.5 MHz)提供一系列1位脉冲,我们可以通过低通滤波器(基本上是移动平均线)来恢复音频信号。我们还将信号抽取50倍,因此我们的采样率变为50 KHz。 

在捕获了七个音频样本后,我们需要通过FFT为每个音频样本提供频率信息。 FFT的输出是复数,但我们需要它以相位幅度形式,因此我们将这些值传递给计算新值的模块。 

利用所有样本的相位幅度表示,我们可以从中心一个减去六个周围麦克风的相位以获得延迟。这里我们需要小心,因为在减法之后,相位差可以在+/- pi范围之外。如果是,我们需要加上或减去2 pi以使其恢复到范围内。 

计算出的相位差等于延迟乘以频率。因为我们一次使用一个频率,所以实际上只是由常数缩放的延迟。我们可以使用这个事实来避免必须除以频率。 

然后,我们通过相应的相位差(延迟)来缩放六个麦克风位置矢量并对它们的分量求和。这为我们提供了一个指向该频率声源方向的矢量。但是,我们只关心这个向量的方向,因为它的大小毫无意义。我们可以通过使用与之前相同的模块将笛卡尔矢量转换为相位幅度表示来提取相位(角度)。 

对所有频率重复此过程为我们提供了每个频率的声音方向的角度。我们可以将这些方向中的每一个与来自中心麦克风的频率的大小(音量)配对,以找出它的相关性。 

这本身可能是我们设计的输出,但将方向分成几个角度更有用。在我们的例子中,我们将每个分配给16个等间隔的箱子中的一个。落入箱中的所有频率的大小被求和以获得该箱的总体幅度。这16个总和是最终输出,表示来自每个箱子方向的声音量。 

我们可以将这个设计实现为一个完整的管道,每个阶段都可以简单地进入下一个阶下图显示了它的外观。

这种设计具有最高的吞吐量,但它也会占用大量资源。事实上,它将比Mojo的FPGA中提供的更多。但是,我们可以按顺序执行每个步骤,并利用许多步骤需要相同操作的事实,仅针对不同的数据。在完整的流水线中,我们需要七个FFT和八个CORDIC(笛卡尔到相位幅度转换)。但是,我们可以只重用其中一个并节省大量资源。 

以下是电路数据路径的图示。数据路径显示数据流经设计的方式,但为简单起见,它并未显示控制多路复用器和其他流量决策的控制逻辑。完全流水线版本不需要任何控制逻辑,因为数据只是从一端流向另一端。但是,我们需要创建一个FSM来控制紧凑版本。以下段落概述了FSM需要采取的步骤。

首先,来自麦克风的样本通过抽取滤波器并存储在RAM中。 RAM分为七组,每组两个(总共14个RAM),每个16位宽。七组对应于七个麦克风,每组中的两个RAM将分别存储偶数和奇数样本。 7 x 2安排的原因将在稍后变得清晰。 

当RAM块充满采样数据时,数据一次一个通道传递到FFT。数据也通过Hanning窗口(未示出)以最小化FFT中的泄漏。这样做的目的超出了本书的范围,但它归结为将样本乘以存储在ROM中的Hann函数。 FFT的输出馈入CORDIC,将其转换为相位幅度格式,然后写回RAM,覆盖原始通道的采样数据。当写回RAM时,七个组仍然对应于每个通道,但现在每个组中的两个值用于相位和幅度值,而不是偶数和奇数样本。通过将这两个值放在不同的RAM中,我们可以轻松地同时读取或写入它们。这重复七次,每个频道一次。完成此步骤后,RAM包含每个通道的相位和幅度数据。 

因为我们输入FFT的样本数据都是实数(没有虚数分量),所以FFT的输出是对称的。这意味着即使每个频率都有两个与之相关的值,我们的频率数量也只是采样的一半,因此我们可以在RAM中存储完全相同的值。 

下一步是获取每个频率的相位幅度数据,并将其传递给方向计算器,以获得该频率的方向矢量。然后使用与之前相同的CORDIC提取该矢量的角度(相位)。然后将输出保存回RAM。这次,我们将数据写入组0,因为我们不再需要此麦克风(麦克风1-5)的相位幅度数据。 

最后,我们将来自最后一步的相位数据和来自麦克风6(中心麦克风)的幅度数据馈送到聚合器中。聚合器将每个样本添加到其对应的bin中并输出最终结果。 

即使具有所有这些重用,该设计仍然使用77%的LUT,32%的DFF,并占据Mojo中94%的切片。它恰到好处!

动手做

现在我们已经有了我们需要设计的路线图,让我们进入代码。您可以在Mojo IDE中找到完整源代码作为示例项目。要查看它,请创建一个新项目,然后从“新建项目”对话框的“从示例”下拉列表中选择“声音定位器”。 

我们将从麦克风开始,逐步完成声音定位器的步骤。 

PDM麦克风 

该项目依赖于PDM麦克风。这些是常见类型的麦克风,并且易于与FPGA接口,因为它们具有数字输出。如前所述,PDM代表脉冲密度调制,这意味着脉冲密度与麦克风上的压力相关。由于这个简单的接口,我们需要大量的脉冲才能获得底层信号的任何真实定义。麦克风护罩上的麦克风每秒可输出1到325万个脉冲,具体取决于提供给它的时钟。在我们的设计中,我们将使用每秒250万的漂亮中间值。 

为了将这种高频,低分辨率脉冲序列转换为更有用的低频,高分辨率信号,我们将使用级联积分梳(CIC)滤波器。这种类型的滤波器可用于改变采样率,抽取(减少)或插值(增加)。幸运的是,Xilinx的CoreGen工具可用于生成滤波器。 

如果您打开了完整的Sound Locator项目,则可以启动CoreGen并查看decimation_filter核心。它是从CIC编译器3.0版创建的,可以在数字信号处理→过滤器→CIC编译器下找到,如下图所示。

这里我们指定它是抽取型滤波器,抽取率应为50.这将把2.5 MHz输入转换为50 KHz输出。看一下频率响应图表,它表明它是一个低通滤波器。您可以使用其他参数来使其更多地以更多硬件为代价来衰减更高频率。对于我们的使用,它并没有真正有所作为。 

如果你看第二页,如下所示,它表明过滤器能够输出每个样本25位。但是,这设置为20,因此最后5位被截断。根据经验发现这是一个很好的灵敏度值,无论如何我们每个样本只使用16位。额外的MSB将用于检查溢出,否则将被忽略。 

我们也有这套不使用Xtreme DSP Slices,因为我们在FPGA中没有足够的余量。此选项将在选择时使用内置乘法器,而不是使用FPGA的常规结构。但是,每个滤波器使用两个乘法器,我们有七个滤波器。 Mojo上的FPGA有16个乘法器,因此这个阶段的14个太多了。

有关CIC滤波器的更多信息,请查看Xilinx LogiCORE文档。 

有了过滤器,我们现在可以查看pdm_mics.luc文件以了解它的使用方法。 

在这个模块中,我们需要为麦克风生成一个时钟。 这是一个2.5 MHz信号,是50 MHz系统时钟的1/20。 为此,我们可以使用一个从0-9(10个周期)计数的计数器,并在每次溢出时切换时钟。 我们可以在MSB下降时检测溢出。 

在麦克风时钟的每个上升沿,我们从每个麦克风获得另一位PDM数据。 我们首先需要将单比特值0或1转换为CIC滤波器的2比特有符号值-1或1。 

剩下的就是将数据提供给CIC滤波器,并在将数据转换为16位后从它们输出数据。 CIC滤波器数据是有符号的,所以当我们转换为16位并希望在溢出时饱和时,我们需要分别检查负溢出和正溢出: 

此处请查看例程源码

FFT

在我们进入sound_locator模块之前,让我们看一下CoreGen需要的其他两个核心:FFT和CORDIC核心。再次,打开Sound Locator项目,从Mojo IDE启动CoreGen并查看xfft_v8_0核心。

FFT向导的第一页(如下所示)允许您选择通道数(一次计算的FFT数),变换长度(采样数),系统时钟和架构。我们将每次迭代使用512个样本,这似乎是延迟和准确性之间的良好平衡。 FFT架构设置为Radix-2 Lite,Burst I / O,这将导致以速度为代价的最小实现。我们对速度并不在乎,但资源非常宝贵。即使在最慢的架构类型下,计算转换也只需要5,671个周期。使用我们的50 MHz时钟,这是一个小的113.4μs(约为每秒1 / 10,000秒)。最快的架构速度提高了大约五倍,但却占用了我们没有的更多资源。我们可能会使用下一个尺寸进行适度的速度改进,但同样,它不会对这种用途产生明显的影响。我们首先捕获样本需要100倍的时间。

在第二页,我们指定了有关FFT内部的更多细节。我们使用定点数据,因此数据格式设置为固定点。来自麦克风的样本为16位,因此输入数据宽度设置为。相位因子宽度是存储在ROM中的一些值的大小,用于计算FFT时。值越高,结果就越准确。我们目的的准确性并不太重要,因为16位绰绰有余。 

缩放用于节省资源。如果不进行缩放,则值会在需要更宽数据路径的不同FFT步骤中继续增长。但是,如果启用缩放,则需要提供缩放计划,以告知FFT核心何时截断数据。这是在运行时设置的,我发现一个使用Xilinx Matlab模块模拟FFT的麦克风数据效果很好。 

舍入模式选项是准确性和资源的另一个权衡。截断基本上是免费的,而收敛舍入的成本很低。 

控制信号相当不言自明;如果需要,可以启用复位和时钟使能。我们只需要重置。 

FFT将自然地以位反转顺序产生值。对于八个样本的FFT,输出顺序为0,4,2,6,1,5,3,7。在二进制中,这是000,100,010,110,001,101,011,111。你反转了这些位,它变成0,1,2,3,4,5,6,7。如果你能按此顺序使用输出,核心可以同时加载和卸载数据。否则,装载和卸载必须在不同阶段进行。通过在Optional Output Fields部分中启用XK_INDEX,我们得到每个输出值的索引,因此顺序对我们来说并不重要。 

最后,节流方案是指定接收输出的是否可以阻止核心输出数据。换句话说,使用输出的硬件是否无法接受数据?如果因任何原因无法使用,则需要使用非实时。这使m_axis_data_tready标志能够发出信号,表明可以输出数据。如果将其设置为实时,则无论如何都会在数据准备就绪时立即吐​​出数据。在我们的例子中,我们将数据馈送到CORDIC核心,这可能很忙,所以这需要非实时。

向导的最后一页(如下所示)是关于硬件使用的。第一部分“内存选项”允许您选择是使用Block RAM还是分布式RAM。我们的设计中剩下大量的Block RAM,所以只有充分利用它才有意义。 

在“优化选项”部分中,选择使用DSP乘数。 Mojo上的FPGA有16个,应该尽可能使用。对于CIC滤波器,我们设置它不使用它们,因为它们将各使用两个,并且我们有七个滤波器,因此总共需要14个乘法器。这太多了(我们的设计只有八个额外的)。在这种情况下,FFT将只使用三个,我们可以省去它们。您可以在向导左侧的“实现详细信息”选项卡下查看它将使用的数量。 

通常,如果您有可用的特殊资源(在这种情况下为Block RAM和DSP),请使用它们。它们通常会使您的设计变得更快更小,如果您不利用它们,它们仍将位于您的FPGA中。

有关FFT内核的更多信息,请查看Xilinx LogiCORE文档。 

CORDIC 
坐标旋转数字计算机(CORDIC)是一种有效计算双曲线和三角函数的算法。该算法能够旋转矢量,可巧妙地用于转换为笛卡尔和极坐标(幅度和角度),计算sin和cos,计算sinh和cosh,计算弧度,计算弧度,甚至计算平方根。我们将使用它从笛卡尔坐标转换为极坐标。 

在CoreGen中打开mag_phase_calculator核心。 

在向导的第一页上,如下面的第一张图所示,我们可以选择指定操作模式以及准确性,延迟和区域权衡。 

功能选择选项选择操作模式。在我们的例子中,选择Translate进行笛卡尔到极坐标的转换。 

“体系结构配置”选项提供了区域与延迟之间的权衡。 Parallel选项允许内核通过复制一堆硬件在每个时钟周期吐出一个新值。 Word Serial选项重用硬件,但一次只能处理一个值。针对区域进行优化,选择了Word Serial模式。 

流水线模式是性能(最大时钟速度)与面积和延迟权衡的关系。 Optimal选项将在不使用额外LUT的情况下尽可能地进行流水线操作。 Maximum选项将在每个阶段之后进行管道传输。 

阶段格式选项很重要,因为它决定了输出格式。任一选项都将输出一个定点值,其中三个MSB是整数部分。例如,01100000为3.0,00010000为0.5。在Radians情况下,该值是以弧度表示的角度。对于Scaled Radians,此值是角度除以pi。我们使用弧度来简化。 

Inout / Output Options部分非常明显。我们使用16位输入和输出,并截断内部舍入,因为它是最便宜的选项。 

在“高级配置参数”下,将“迭代次数”和“精度”保留为0将使向导根据输出宽度自动设置这些参数。 Coarse Rotation选项允许我们使用整个圆而不是第一个象限。补偿缩放用于补偿CORDIC算法的副作用。通过启用此功能,核心将以某些资源为代价输出未缩放的正确值。您可以在下拉列表中选择要使用的资源。在我们的例子中,我们使用嵌入式乘法器,因为我们仍然使用一些DSP48A1。我们也可以使用BRAM选项,因为我们也有很多选项。 

在向导的第二页上,如下面的第二张图所示,您可以配置输入和输出流。 TLAST和TUSER选项为模块提供额外的输入,输出处理相应输入时看到的值。这是一种传递数据并保持同步的方法。在我们的例子中,我们需要值的地址和最后一个样本的位置,因此两者都被启用。地址大小为9位,因此我们将TUSER的宽度设置为9。 

当指定阻塞时,Flow Control选项将启用输入上的缓冲区。当使用两个输入流时CORDIC处于不同模式时,这会更有用,因为它会强制它们同步。 NonBlocking选项使用的资源更少,阻止对我们没有任何实际好处。 

最后,我们不关心复位,因为CORDIC在复位时FSM到达它时会自动冲洗。

捕获状态 

CAPTURE状态非常简单。我们只是等待新样本从麦克风进入并将它们写入RAM,递增我们的地址计数器直到RAM被填满。 

将地址值分配给所有14个RAM模块有一点花哨的表示法,因为它们是7 x 2阵列。此状态中的第一行将addr.q(不包括LSB)复制到7 x 2 x 8阵列中。 

addr的LSB用于在偶数和奇数RAM之间进行选择。这个小怪癖是因为我们希望将两个RAM分开,以便稍后将幅度和相位数据存储在其中。我们为偶数和奇数RAM分配相同的数据,但仅在相应的数据上启用写入。 

最后,在RAM满了之后,我们为下一个状态执行一些初始化并继续进行FFT:

此处请查看源代码

FFT状态 
在计算的这个阶段,我们正在进行两个过程。第一个负责保持FFT馈送,第二个负责将CORDIC的结果写入RAM。 FFT直接馈入CORDIC。 

进给过程需要一点点技巧,因为从RAM读取需要一个时钟周期。标志wait_ram用于确保RAM在我们启动时输出地址0的值。如果在任何时候FFT都不能接受更多的数据,但我们正在为它提供数据,我们需要保存从RAM中出来的值,因为在下一个周期它将消失。当恢复向FFT馈送数据时,我们在恢复从RAM读取之前将其提供给保存的值。 

在馈送FFT之前,我们还通过汉宁窗口传递数据。 Hann ROM具有像RAM一样的单周期延迟,因此如果FFT不能接受值,我们还需要保存其值。 Hann值和麦克风样本相乘。 Hann值是1.15定点数(1个整数位,15个十进制位),因此乘法的结果在传递到FFT之前向右移15位。这个想法就像你乘以两个十进制数一样。例如,2 x 1.3可以看作(2 x 13)/ 10.请注意,乘法应该是有符号乘法,因此两个操作数都由$ signed函数包装以确保这一点。 

一旦我们填充了FFT,我们就增加通道并等待FFT准备接受更多数据。加载完七个通道后,我们等待状态改变:

此处请查看源码

我们需要将FFT输出连接到CORDIC以获得幅度 - 角度表示。 CORDIC方便地让我们传递地址和最后一个标志,以便它与其他数据保持同步。 tready和tvalid握手标志确保只有在有数据且CORDIC可以接受时才传输数据:

xfft.m_axis_data_tready = mag_phase.s_axis_cartesian_tready;

mag_phase.s_axis_cartesian_tdata = xfft.m_axis_data_tdata;

mag_phase.s_axis_cartesian_tvalid = xfft.m_axis_data_tvalid;

// pass the address info through the user channel so it is available

// when the mag_phase data has been processed

mag_phase.s_axis_cartesian_tuser = xfft.m_axis_data_tuser[addr.WIDTH-1:0];

mag_phase.s_axis_cartesian_tlast = xfft.m_axis_data_tlast;

最后,我们需要将CORDIC数据卸载到RAM中。 我们有从tuser字段写入的地址,我们通过计算tlast标志来跟踪通道号。 

在我们从最后一个频道卸载最后一个数据后,我们可以切换到下一个状态,DIFFERENCE:

// recover the address from the user channel

ram.waddr =

  7x{{2x{{mag_phase.m_axis_dout_tuser[addr.WIDTH-2:0]}}}};

ram.write_en[unload_ch_ctr.q] =

  2x{mag_phase.m_axis_dout_tvalid

  // write only first half of values

  & ~mag_phase.m_axis_dout_tuser[addr.WIDTH-1]};

ram.write_data[unload_ch_ctr.q] =

  // phase, mag

  {mag_phase.m_axis_dout_tdata[31:16], mag_phase.m_axis_dout_tdata[15:0]};

        

// if we have processed all the samples we need to

if (mag_phase.m_axis_dout_tvalid && mag_phase.m_axis_dout_tlast) {

  unload_ch_ctr.d = unload_ch_ctr.q + 1;   // move onto the next channel

  // if we have processed all 7 channels

  if (unload_ch_ctr.q == 6) {

    state.d = state.DIFFERENCE;            // move to the next stage

    addr_pipe.d = 2x{{addr.WIDTHx{1b0}}};

  }

}

 

差异状态 

在这个阶段,我们从六个外部麦克风中的每个麦克风中减去中心麦克风的相位。 然后,我们使用此差异来缩放麦克风的位置矢量,然后对矢量求和。 单个结果矢量通过CORDIC馈送以获得其角度,然后将其写回RAM。

发生的第一个操作是减相。 这里唯一有趣的部分是我们需要保持+/- pi范围内的结果差异。 这是通过检查溢出并加上或减去2pi来完成的。 这里的所有操作都是要签名的,所以所有内容都再次以$ signed包装,以确保:

state.DIFFERENCE:

  for (i = 0; i < 6; i++) {

    // we care about the difference in phase between the center microphone

    // and the outer microphones

    // as this is proportional to the delay of the sound (divided by the

    // frequency)

    temp_phase[i] =

      $signed(ram.read_data[i][1]) - $signed(ram.read_data[6][1]);

          

    // we need to keep the difference in the +/- pi range for the next

    // steps

    // 25736 = pi (4.13 fixed point)

    if ($signed(temp_phase[i]) > $signed(16d25736)) {

      // 51472 = 2*pi (4.13 fixed point)

        temp_phase[i] = $signed(temp_phase[i]) - $signed(17d51472);

      } else if ($signed(temp_phase[i]) < $signed(-16d25736)) {

        temp_phase[i] = $signed(temp_phase[i]) + $signed(17d51472);

      }

    }

由于所有乘法,矢量的缩放和求和它们在两个时钟周期内发生。这增加了一点复杂性:因为CORDIC可以告诉我们它不能随时接受新数据,我们需要能够停止管道。 

为此,我们检测管道数据丢失并恢复到丢弃地址的具体情况。这种情况发生的唯一时间是管道在停止之前已经激活了至少两个周期。此方法可能会导致某些值被多次计算,具体取决于暂停/恢复模式,但这并不是什么大问题,只要每个值至少计算一次并且我们继续取得进展。最糟糕的模式将是运行,运行,暂停,运行,运行,暂停等。在这种情况下,我们每次运行只运行一个地址,运行,停止循环。但是,我们仍然会取得进展并涵盖所有价值观。 CORDIC的实际行为在单次运行之间会有更多停顿,并且使用此模式我们不会重复任何值。 

一些缩放非常简单;例如,第一个麦克风位于(-1,0),因此x值只是负相位差,y分量始终为0.但是,有些需要乘以sqrt(3)/ 2。这是使用定点乘法和位移完成的。 

在计算出所有缩放的相位值之后,将它们相加,然后除以8,这是2的最接近的幂大于6(麦克风的数量)。除法用于将值保持在16位范围内:

/* Sample coordinates

   0: (-1,    0)

   1: (-1/2,  sqrt(3)/2)

   2: ( 1/2,  sqrt(3)/2)

   3: ( 1     0)

   4: ( 1/2, -sqrt(3)/2)

   5: (-1/2, -sqrt(3)/2)

   6: ( 0,    0)

*/

        

addr_pipe.d[0] = addr.q; // output address of the ram

        

if (mag_phase.s_axis_cartesian_tready) {

  /*

     Here we are scaling each microphone's location vector by

     he delay (phase difference). This will give us a vector

     roportional to that microphone's contribution to the total

     direction.

  */

  scaled_phase.d[0][0] = -temp_phase[0];

  scaled_phase.d[0][1] = 0;

          

  mult_temp = -temp_phase[1];

  scaled_phase.d[1][0] = c{mult_temp[16],mult_temp[16:1]};

          

  mult_temp = $signed(temp_phase[1]) * $signed(17d56756);

  // phase * sqrt(3)/2

  scaled_phase.d[1][1] = mult_temp[mult_temp.WIDTH-2:16];

          

  scaled_phase.d[2][0] = c{temp_phase[2][16], temp_phase[2][16:1]};

          

  mult_temp = $signed(temp_phase[2]) * $signed(17d56756);

  scaled_phase.d[2][1] = mult_temp[mult_temp.WIDTH-2:16];

          

  scaled_phase.d[3][0] = temp_phase[3];

  scaled_phase.d[3][1] = 0;

          

  scaled_phase.d[4][0] = c{temp_phase[4][16], temp_phase[4][16:1]};

          

  mult_temp = $signed(temp_phase[4]) * $signed(-17d56756);

  scaled_phase.d[4][1] = mult_temp[mult_temp.WIDTH-2:16];

          

  mult_temp = -temp_phase[5];

  scaled_phase.d[5][0] = c{mult_temp[16], mult_temp[16:1]};

          

  mult_temp = $signed(temp_phase[5]) * $signed(-17d56756);

  scaled_phase.d[5][1] = mult_temp[mult_temp.WIDTH-2:16];

          

  addr_pipe.d[1] = addr_pipe.q[0]; // address of scaled vector values

          

  /*

     With all the scaled vectors, we simply need to sum them to get

     the overall direction of sound for this frequency.

  */

  summed_phase[0] = 0;

  summed_phase[1] = 0;

  for (i = 0; i < 6; i++) {

    summed_phase[0] = $signed(scaled_phase.q[i][0]) +

      $signed(summed_phase[0]);

    summed_phase[1] = $signed(scaled_phase.q[i][1]) +

      $signed(summed_phase[1]);

  }

          

  // if there are more samples to go, advance the addr

  if (addr.q != SAMPLES/2)

    addr.d = addr.q + 1;

          

  // use the summed vectors (divided by 8) to calculate the overall

  // direction of sound

  mag_phase.s_axis_cartesian_tdata =

    c{summed_phase[1][3+:16], summed_phase[0][3+:16]};

  // only valid for the first half of addr

  mag_phase.s_axis_cartesian_tvalid = ~addr_pipe.q[1][addr.WIDTH-1];

          

  // feed in the address for later use

  mag_phase.s_axis_cartesian_tuser = addr_pipe.q[1];

  mag_phase.s_axis_cartesian_tlast = addr_pipe.q[1] == SAMPLES/2 - 1;

} else if (&mag_phase_ready.q) {

  // if we were ready but now aren't we need to go back an address so

  // that we don't skip one

  addr.d = addr_pipe.q[0];

}

我们现在只需要将CORDIC的输出反馈到RAM中。 这基本上与FFT状态的最后部分相同:

// write the phase data into the RAM channel 0

ram.waddr = 7x{{2x{{mag_phase.m_axis_dout_tuser[addr.WIDTH-2:0]}}}};

ram.write_data[0] =

  {mag_phase.m_axis_dout_tdata[31:16], mag_phase.m_axis_dout_tdata[15:0]};

ram.write_en[0] = 2x{mag_phase.m_axis_dout_tvalid};

        

// if we are on the last sample move onto the next stage

if (mag_phase.m_axis_dout_tlast && mag_phase.m_axis_dout_tvalid) {

  addr.d = CROP_MIN;

  state.d = state.AGGREGATE_WAIT;

}

AGGREGATE STATE

在这个阶段,我们将通过计算出的方向并将它们相应的大小相加到16个方向箱中。 

即使我们有256个频率可供使用,我们只会将9到199之间的频率相加。最低频率不太有用,最高频率超出听觉范围。 这些值由CROP_MIN和CROP_MAX设置。 

使用一系列if语句执行bin选择,该语句检查角度是否位于特定bin的范围内。 仅使用8个MSB来节省比较的大小。 如果bin边界不是非常精确,则没有区别。 这些比较都是签名的,因此常量包含在$ signed中。 信号角度声明为signed,因此不需要$ signed函数:

state.AGGREGATE:

  addr.d = addr.q + 1;

  angle = ram.read_data[0][1][15:8]; // angle calculated in the last step

  magnitude = ram.read_data[6][0];   // use the magnitude from the center mic

        

  /*

     We now need to go through each frequency and bin them into one of 16 groups.

     This makes it easier to get an idea of where the sound is coming from as

     many frequencies will point in the same direction of a single source. If

     we have multiple sources then multiple bins will receive a lot of values.

     A more advanced grouping method could be done in software off chip such as

     K-means to get a more accurate picture, but this method works relatively well

     and is simple to implement in hardware.

  */

  if (angle >= $signed(ANGLE_BOUNDS[7]) || angle < $signed(-ANGLE_BOUNDS[7])) {

    sums.d[0] = sums.q[0] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[6]) && angle < $signed(ANGLE_BOUNDS[7])) {

    sums.d[1] = sums.q[1] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[5]) && angle < $signed(ANGLE_BOUNDS[6])) {

    sums.d[2] = sums.q[2] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[4]) && angle < $signed(ANGLE_BOUNDS[5])) {

    sums.d[3] = sums.q[3] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[3]) && angle < $signed(ANGLE_BOUNDS[4])) {

    sums.d[4] = sums.q[4] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[2]) && angle < $signed(ANGLE_BOUNDS[3])) {

    sums.d[5] = sums.q[5] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[1]) && angle < $signed(ANGLE_BOUNDS[2])) {

    sums.d[6] = sums.q[6] + magnitude;

  } else if (angle >= $signed(ANGLE_BOUNDS[0]) && angle < $signed(ANGLE_BOUNDS[1])) {

    sums.d[7] = sums.q[7] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[0]) && angle < $signed(ANGLE_BOUNDS[0])) {

    sums.d[8] = sums.q[8] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[1]) && angle < $signed(-ANGLE_BOUNDS[0])) {

    sums.d[9] = sums.q[9] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[2]) && angle < $signed(-ANGLE_BOUNDS[1])) {

    sums.d[10] = sums.q[10] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[3]) && angle < $signed(-ANGLE_BOUNDS[2])) {

    sums.d[11] = sums.q[11] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[4]) && angle < $signed(-ANGLE_BOUNDS[3])) {

    sums.d[12] = sums.q[12] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[5]) && angle < $signed(-ANGLE_BOUNDS[4])) {

    sums.d[13] = sums.q[13] + magnitude;

  } else if (angle >= $signed(-ANGLE_BOUNDS[6]) && angle < $signed(-ANGLE_BOUNDS[5])) {

    sums.d[14] = sums.q[14] + magnitude;

  } else {

    sums.d[15] = sums.q[15] + magnitude;

  }

        

  // stop once we reach the highest frequency to count (we only care about audible ones)

  if (addr.q == CROP_MAX)

    state.d = state.OUTPUT;

最后,在不同的箱子已满,我们可以输出值。 我们首先检查是否有溢出,如果有的话,就会使垃圾箱饱和。 

完成后,我们返回空闲状态并等待命令再次启动该进程:

state.OUTPUT:

  for (i = 0; i < 16; i++) {

    sum = sums.q[i][sums.q.WIDTH[1]-1:0];

    if (sum > 65535) // if it overflowed, saturate it

      sum = 65535;

          

    result[i] = sum[15:0]; // use the 16 LSBs for decent sensitivity

  }

  result_valid = 1;

        

  state.d = state.IDLE;

这个项目是一个相当复杂的例子,但希望它能让你了解一些你可以用FPGA做的有趣事情。


订阅并保持联系

输入您的电子邮箱即可在第一时间获得 TONYLABS 资讯