在Soil Xilinx Zynq 7000上开发接口卡,以模拟和数字格式进行语音记录



在本文中,我们将分享我们在开发基于SoC ARM + FPGA Xilinx Zynq 7000的接口单元的接口卡方面的经验。这些板卡旨在记录模拟和数字PRI / BRI格式(ISDN,E1 / T1)的语音信号。 最终设备本身将用于记录民航谈判。

铁:设备硬件平台的选择


硬件平台的选择取决于PRI / BRI协议的支持,该协议只能在FPGA端实现。 微控制器(MCU)和微处理器(MPU)不合适。

一个人可以选择两个解决方案来解决此问题:

  1. Microblaze IP核心合成
  2. SoC Zynq-7000。

我们选择了Zynq 7000芯片(SoC)上的系统,因为 编写软件应用程序更容易,并为当前和将来的任务提供更多功能。

总的来说,该项目收集了以下铁清单:

1.Xilinx Zynq 7020Mars-ZX3Mars EB1

Enclustra火星ZX3 SOM

Enclustra Mars EB1底板

2. TI TLV320AIC34tlv320aic34evm-k和USB主板)。


TLV320AIC34的调试板(TLV320AIC34EVM-K)


适用于TLV320AIC34EVM-K的USB-MODEVM扩展板

3. IDT82P2288-PRI,XHFC-4SU-BRI微电路,因为没有调试工具包,所以我们只是为测试的ip内核奠定了基础,并且在制作原型板后就在过程中发生了洗礼。

在Xilinx Zynq 7000芯片上使用系统




SoC Xilinx Zynq 7000的内部结构


为Xilinx Zynq生成启动文件的步骤

Zynq的刷新/下载可执行文件与MPU的常规下载不同。 Cortex-A处理器的通常工作是加载u-boot,内核linux,rootfs。 在Zynq上,出现比特流,这是FPGA的固件文件。 比特流包含对FPGA上硬件模块以及与处理器内部通信的描述。 该文件在系统启动时加载。 同样在linux端,有一种机制允许您在操作过程中立即刷新PL部分,这种设备称为xdevcfg( 自2018.1开始为ZYNQ FPGA管理器 )。

PRI / BRI接口



数字网络PRI / BRI的功能

主速率接口(PRI)是标准的ISDN网络接口,它定义了将ISDN站连接到连接本地和中央交换机或网络交换机的宽带中继的原则。


PRI的传输帧类型


BRI传输帧的视图


PRI物理的内部结构-IDT82P2288


BRI物理的内部结构-XHFC-4SU

音频编解码器TLV320AIC34


用于便携式音频和电话的四通道低功耗TLV320AIC34音频编解码器是用于模拟电话的良好解决方案。


Tlv320aic34 A-部分,音频编解码器包含两个此类功能块

数据可以通过I2S接口以及DSP,PCM,TDM进行传输。

I2S是串行总线接口标准,用于连接数字音频设备,并电气表示从有源设备到无源设备的3条导体,以及与之对应的4条信号,如下所示:

  1. 位时钟(BCLK)。
  2. 时钟信号帧(根据字)同步(WCLK)。
  3. 可以发送或接收2个时分通道(DIN / DOUT)的数据信号。

接收和发送数据的通道是分开的,也就是说,有一个单独的通道用于接收数据和一个发送通道。 控制器接收由音频编解码器传输的数据,但是相反的操作也是可能的。


I2S框架,I2S接口的功能

选择所有硬件组件后,我们解决了将音频编解码器与Xilinx Zynq 7020连接的问题。

搜索I2S内核


在Xilinx Zynq 7020中使用音频流时,最困难的时刻可能是该系统的处理器部分上基本上没有I2S总线,因此我必须找到I2S内核。 ip内核应为空闲状态,使此任务变得复杂。

我们确定了几个IP内核。 发现用于裸机核心I2S Digilent 。 我们在opencore上找到了几个ip内核,也许对我们来说最好的选择是ADI公司的 ip内核。 他们为自己的设备生产IP内核,以进行FPGA / FPGA交互。

我们对称为AXI-I2S-ADI的IP核感兴趣 ADI公司本身正在为其硬件平台推广这些IP内核。

用例列表:

  1. 裸机-I2S的IP内核(Digilent ZYBO音频)
  2. opencores.org
  3. AXI-I2S-ADI控制器 (模拟设备)

AXI-I2S-ADI IP内核


ip核心本身看起来像这样:它包含bclk,wclk,din和dout行。 它连接到DMA Xilinx Zynq 7000,在我们的示例中,使用了DMA PS部分。 所有数据交换都通过DMA进行。 DMA可以是独立单元,也可以是PS SoC的组成部分。

配置此ip内核时,重要的是不要忘记将mclk主频率提交给tlv320aic34本身,作为对tlv320aic34使用调试工具包的一个选项-提交外部主频率。


功能块,已连接axi-i2s-adi

完成配置过程后,任务是在Linux OS中启动功能。

启动和配置TLV320AIC34的设备树


配置i2c(在此接口上配置了tlv320aic34):

i2c0: i2c@e0004000 { ... tlv320aic3x: tlv320aic3x@18 { #sound-dai-cells = <0>; compatible = "ti,tlv320aic3x"; reg = <0x18>; gpio-reset = <&axi_gpio_0 0 0>; ai3x-gpio-func = <&axi_gpio_0 1 0>, /* AIC3X_GPIO1_FUNC_DISABLED */ <&axi_gpio_0 2 0>; /* AIC3X_GPIO2_FUNC_DIGITAL_MIC_INPUT */ AVDD-supply = <&vmmc2>; DRVDD-supply = <&vmmc2>; IOVDD-supply = <&vmmc2>; DVDD-supply = <&vmmc2>; ai3x-micbias-vg = <1>; }; ... }; 

配置i2(通过此接口传输音频数据):

 i2s_clk: i2s_clk { #clock-cells = <0>; compatible = "fixed-clock"; clock-frequency = <11289600>; clock-output-names = "i2s_clk"; }; axi_i2s_adi_0: axi_i2s_adi@43C00000 { compatible = "adi,axi-i2s-1.00.a"; reg = <0x43C00000 0x1000>; xlnx,bclk-pol = <0x0>; xlnx,dma-type = <0x1>; xlnx,has-rx = <0x1>; xlnx,has-tx = <0x1>; xlnx,lrclk-pol = <0x0>; xlnx,num-ch = <0x1>; xlnx,s-axi-min-size = <0x000001FF>; xlnx,slot-width = <0x18>; }; &axi_i2s_adi_0 { #sound-dai-cells = <0>; compatible = "adi,axi-i2s-1.00.a"; clocks = <&clkc 15>, <&i2s_clk>; clock-names = "axi", "ref"; dmas = <&dmac_s 0 &dmac_s 1>; dma-names = "tx", "rx"; }; 

在设备树中设置声卡(声卡):

  sound { compatible = "simple-audio-card"; simple-audio-card,name = "TLV320AIC34"; simple-audio-card,format = "i2s"; simple-audio-card,bitclock-master = <&dailink0_master>; simple-audio-card,frame-master = <&dailink0_master>; simple-audio-card,widgets = ... simple-audio-card,routing = ... dailink0_master: simple-audio-card,cpu { clocks = <&i2s_clk>; sound-dai = <&axi_i2s_adi_0>; }; simple-audio-card,codec { clocks = <&i2s_clk>; sound-dai = <&tlv320aic3x>; }; }; }; 

在Linux的设备树中配置和配置了编解码器的所有操作完成之后,令人垂涎的声卡出现了,我们能够听到音乐了(我们的第一个音乐曲目是AC / DC的“地狱之路”)。

这是我们要做的:

  • 使用clk_wiz(计时向导)生成了必要的频率
  • 正确配置了tlv320aic34的DTS
  • 添加了对tlv320aic3x驱动程序的支持
  • 向buildroot添加了音频包以播放音频流(aplay,madplay等)

在开发终端设备的过程中,我们面临着连接4个tlv320aic34微电路的任务。 上述tlv320aic34芯片包含2个用于处理音频流的模块,每个模块都有自己的i2c线,用于配置和设置音频参数。 一个块只能分别具有四个地址,不可能将四个tlv320aic34微电路连接到一个i2c接口,您需要使用两个i2c接口(8个独立的音频块)。 对于每个模块,如果分别启动mclk,blck,wclk,din / dout,则总共需要添加40条信号线,这对于我们选择的som模块来说,从电路的角度来看是不可能的,而且是不合理的,因为除了这些信号之外,您还必须连接许多其他线路,接口。

结果,我们决定将声卡切换为TDM模式 ,在该模式下 ,将所有mclk,bclk,din,dout线组合在一起,从而减少了通信线的总数。 该决定影响了axi-i2s-adi的操作,因为ip内核本身在主模式下工作。 同样,此更改不允许我们在TDM模式下使用ip-core,因此我们坚决决定放弃使用选定的ip-core。 我必须编写一个ip内核来侦听i2s流量并将其发送到dma,该解决方案使我们能够创建一个通用接口来接收数据,该接口不依赖于用于记录呼叫的卡的类型(模拟卡和数字卡)。

通过i2s接口接收音频流及其处理的初始架构:



通过i2s接口接收音频流及其处理的最终架构:



接收PRI流的结构及其处理:



BRI流接收和处理的体系结构:



阿西玛


这是dma同步系统的重要元素。


Xilinx Vivado中的AXI DMA配置窗口

在打印屏幕上,将显示AXI DMA模块本身。 它有很多参数。 您可以配置总线传输多少数据。 数据可以对齐或采用任何格式。 技术文档中描述与axi dma的操作和交互的详细说明(从一个版本到另一个版本,说明中存在不正确之处的补充和更正,以及ip内核的完善)。

通过AXI DMA,AXI DMA测试选项验证数据传输


在开发驱动程序时,我们决定寻找开源并使其适应我们的任务。 结果,我们选择了github-project ezdma (双关语,简称为easy dma)的源代码。

下一步是测试驱动程序的开发,这是从FPGA开发部门获得具有现成功能的ip内核到我们(描述的开发过程由嵌入式程序员形成)的时刻的准备阶段。 在此之前,我们决定采用AXI DMA,AXI DATA FIFO并进行环回,以确保将来不会出现错误。 我们循环了数据的发送和接收,因此我们检查了工作结果和驱动程序的性能。 我们对功能进行了一些调整,使其在交互界面上达到了我们的期望,并再次检查了驱动程序的可操作性和所选的交互原理。


回溯模块设计,测试AXI DMA的第一种方法

设备树中的DMA和ezdma描述的示例:

 / { amba_pl: amba_pl { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; ranges ; axi_dma_1: axi_dma { #dma-cells = <1>; compatible = "xlnx,axi-dma-1.00.a"; reg = <0x40400000 0x10000>; clock-names = "s_axi_lite_aclk", "m_axi_sg_aclk", "m_axi_mm2s_aclk", "m_axi_s2mm_aclk"; clocks = <&clkc 15>, <&clkc 15>, <&clkc 15>, <&clkc 15>; interrupt-parent = <&intc>; interrupts = <0 29 4 0 30 4>; xlnx,addrwidth = <0x20>; xlnx,include-sg; dma-channel@40400000 { compatible = "xlnx,axi-dma-mm2s-channel"; dma-channels = <0x1>; interrupts = <0 29 4>; xlnx,datawidth = <0x20>; xlnx,device-id = <0x0>; xlnx,include-dre ; }; dma-channel@40400030 { compatible = "xlnx,axi-dma-s2mm-channel"; dma-channels = <0x1>; interrupts = <0 30 4>; xlnx,datawidth = <0x20>; xlnx,device-id = <0x0>; xlnx,include-dre ; }; }; ezdma0 { compatible = "ezdma"; dmas = <&axi_dma_1 0 &axi_dma_1 1>; dma-names = "loop_tx", "loop_rx"; // used when obtaining reference to above DMA core using dma_request_slave_channel() ezdma,dirs = <2 1>; // direction of DMA channel: 1 = RX (dev->cpu), 2 = TX (cpu->dev) }; ... }; }; 

您可以使用“ 设备树生成器”工具轻松生成dts / dtsi文件。

我们开发过程的第二步是创建一个测试ip-kernel,用于检查驱动程序的性能,只有这次数据才有意义,并通过AXIS传输到AXI_DMA(将在ip-kernel的最终版本中进行)。


AXIS界面工作流程

我们实现了ip内核的两个变体来生成数据,第一个经过测试的版本是通过verilog实现的,第二个是在HLS上实现的(在这种情况下,HLS出现在“ stylish-fashion-youth”的口号下)。

开发此类ip内核时,verilog数据生成器(通常使用hdl系列的语言-verilog,vhdl等)是一种标准解决方案。 以下是中间ip内核的一些代码段:

 module GenCnt ( …. assign HandsHake = m_axis_din_tready & m_axis_dout_tvalid; always @(posedge Clk) begin if (Rst) begin smCnt <= sIDLE; end else begin case (smCnt) sIDLE: begin smCnt <= sDATA; end sDATA: begin if (Cnt == cTopCnt - 1) begin smCnt <= sLAST; end end ... endmodule 

由于这是FPGA设计人员的典型任务,因此无需进行更详细的描述。

这里更有趣的“野兽”是HLS。 Vivado HLS(高级综合)是新的Xilinx CAD软件,用于使用高级语言(例如OpenCL,C或C ++)创建数字设备。

C / C ++是嵌入式软件工程师的主要语言,因此使用这些语言解决问题对于将来的项目的实现和比较分析而言更为有趣。

这是使用HLS的两个小示例。 第一个示例是AXI_DMA的数据生成器,第二个示例是通过s_axilite接口在处理器部分和可编程逻辑之间交换数据。

实现了通过s_axilite接口进行数据交换(第二个示例),以便可以随时随时减去加载的位流,从而可以通过对SoC的PL部分进行版本控制来跟踪工作的正确性。 在这里,使用s_axilite出现了一个非常有趣的观点:Vivado HLS为Linux生成了一个驱动程序(该驱动程序又相应地通过procfs进行了修改,以保留编写的遗传信息)。 下面是为Linux生成的代码的示例(源解决方案1 ​​/ impl / ip / drivers / name_xxx / src /的路径)。


HLS合成和rtl代码生成的阶段

HLS数据生成器,用于检查AXI_DMA的操作:

 #include <ap_axi_sdata.h> #include <hls_stream.h> #define SIZE_STREAM 1024 struct axis { int tdata; bool tlast; }; void data_generation(axis outStream[SIZE_STREAM]) { #pragma HLS INTERFACE axis port=outStream int i = 0; do{ outStream[i].tdata = i; outStream[i].tlast = (i == (SIZE_STREAM - 1)) ? 1 : 0; i++; }while( i < SIZE_STREAM); } 

获取接口板版本和类型的示例:

 #include <stdio.h> void info( int &aVersion, int &bSubVersion, int &cTypeBoard, int version, int subVersion, int typeBoard ){ #pragma HLS INTERFACE s_axilite port=aVersion #pragma HLS INTERFACE s_axilite port=bSubVersion #pragma HLS INTERFACE s_axilite port=cTypeBoard #pragma HLS INTERFACE ap_ctrl_none port=return aVersion = version; bSubVersion = subVersion; cTypeBoard = typeBoard; } 

您已经注意到,对于hls的开发,了解各种编译指示(HLS编译指示)的工作和应用非常重要,因为合成过程直接与编译指示相关。

生成的s_axilite驱动程序:

 // ============================================================== // File generated by Vivado(TM) HLS - High-Level Synthesis from C, C++ and SystemC // Version: 2016.4 // Copyright (C) 1986-2016 Xilinx, Inc. All Rights Reserved. // // ============================================================== #ifdef __linux__ /***************************** Include Files *********************************/ #include "xinfo.h" /***************** Macros (Inline Functions) Definitions *********************/ #define MAX_UIO_PATH_SIZE 256 #define MAX_UIO_NAME_SIZE 64 #define MAX_UIO_MAPS 5 #define UIO_INVALID_ADDR 0 /**************************** Type Definitions ******************************/ typedef struct { u32 addr; u32 size; } XInfo_uio_map; typedef struct { int uio_fd; int uio_num; char name[ MAX_UIO_NAME_SIZE ]; char version[ MAX_UIO_NAME_SIZE ]; XInfo_uio_map maps[ MAX_UIO_MAPS ]; } XInfo_uio_info; /***************** Variable Definitions **************************************/ static XInfo_uio_info uio_info; /************************** Function Implementation *************************/ static int line_from_file(char* filename, char* linebuf) { char* s; int i; FILE* fp = fopen(filename, "r"); if (!fp) return -1; s = fgets(linebuf, MAX_UIO_NAME_SIZE, fp); fclose(fp); if (!s) return -2; for (i=0; (*s)&&(i<MAX_UIO_NAME_SIZE); i++) { if (*s == '\n') *s = 0; s++; } return 0; } static int uio_info_read_name(XInfo_uio_info* info) { char file[ MAX_UIO_PATH_SIZE ]; sprintf(file, "/sys/class/uio/uio%d/name", info->uio_num); return line_from_file(file, info->name); } static int uio_info_read_version(XInfo_uio_info* info) { char file[ MAX_UIO_PATH_SIZE ]; sprintf(file, "/sys/class/uio/uio%d/version", info->uio_num); return line_from_file(file, info->version); } static int uio_info_read_map_addr(XInfo_uio_info* info, int n) { int ret; char file[ MAX_UIO_PATH_SIZE ]; info->maps[n].addr = UIO_INVALID_ADDR; sprintf(file, "/sys/class/uio/uio%d/maps/map%d/addr", info->uio_num, n); FILE* fp = fopen(file, "r"); if (!fp) return -1; ret = fscanf(fp, "0x%x", &info->maps[n].addr); fclose(fp); if (ret < 0) return -2; return 0; } static int uio_info_read_map_size(XInfo_uio_info* info, int n) { int ret; char file[ MAX_UIO_PATH_SIZE ]; sprintf(file, "/sys/class/uio/uio%d/maps/map%d/size", info->uio_num, n); FILE* fp = fopen(file, "r"); if (!fp) return -1; ret = fscanf(fp, "0x%x", &info->maps[n].size); fclose(fp); if (ret < 0) return -2; return 0; } int XInfo_Initialize(XInfo *InstancePtr, const char* InstanceName) { XInfo_uio_info *InfoPtr = &uio_info; struct dirent **namelist; int i, n; char* s; char file[ MAX_UIO_PATH_SIZE ]; char name[ MAX_UIO_NAME_SIZE ]; int flag = 0; assert(InstancePtr != NULL); n = scandir("/sys/class/uio", &namelist, 0, alphasort); if (n < 0) return XST_DEVICE_NOT_FOUND; for (i = 0; i < n; i++) { strcpy(file, "/sys/class/uio/"); strcat(file, namelist[i]->d_name); strcat(file, "/name"); if ((line_from_file(file, name) == 0) && (strcmp(name, InstanceName) == 0)) { flag = 1; s = namelist[i]->d_name; s += 3; // "uio" InfoPtr->uio_num = atoi(s); break; } } if (flag == 0) return XST_DEVICE_NOT_FOUND; uio_info_read_name(InfoPtr); uio_info_read_version(InfoPtr); for (n = 0; n < MAX_UIO_MAPS; ++n) { uio_info_read_map_addr(InfoPtr, n); uio_info_read_map_size(InfoPtr, n); } sprintf(file, "/dev/uio%d", InfoPtr->uio_num); if ((InfoPtr->uio_fd = open(file, O_RDWR)) < 0) { return XST_OPEN_DEVICE_FAILED; } // NOTE: slave interface 'Axilites' should be mapped to uioX/map0 InstancePtr->Axilites_BaseAddress = (u32)mmap(NULL, InfoPtr->maps[0].size, PROT_READ|PROT_WRITE, MAP_SHARED, InfoPtr->uio_fd, 0 * getpagesize()); assert(InstancePtr->Axilites_BaseAddress); InstancePtr->IsReady = XIL_COMPONENT_IS_READY; return XST_SUCCESS; } int XInfo_Release(XInfo *InstancePtr) { XInfo_uio_info *InfoPtr = &uio_info; assert(InstancePtr != NULL); assert(InstancePtr->IsReady == XIL_COMPONENT_IS_READY); munmap((void*)InstancePtr->Axilites_BaseAddress, InfoPtr->maps[0].size); close(InfoPtr->uio_fd); return XST_SUCCESS; } #endif 

告诉您变量(寄存器)在地址空间中的位置的重要文件是文件x#your_name#_hw.h。 您始终可以使用devmem工具来验证书面ip内核的正确性。

该文件的内容:

 // ============================================================== // File generated by Vivado(TM) HLS - High-Level Synthesis from C, C++ and SystemC // Version: 2016.4 // Copyright (C) 1986-2016 Xilinx, Inc. All Rights Reserved. // // ============================================================== // AXILiteS // 0x00 : reserved // 0x04 : reserved // 0x08 : reserved // 0x0c : reserved // 0x10 : Data signal of aVersion // bit 31~0 - aVersion[31:0] (Read) // 0x14 : Control signal of aVersion // bit 0 - aVersion_ap_vld (Read/COR) // others - reserved // 0x18 : Data signal of bSubVersion // bit 31~0 - bSubVersion[31:0] (Read) // 0x1c : Control signal of bSubVersion // bit 0 - bSubVersion_ap_vld (Read/COR) // others - reserved // 0x20 : Data signal of cTypeBoard // bit 31~0 - cTypeBoard[31:0] (Read) // 0x24 : Control signal of cTypeBoard // bit 0 - cTypeBoard_ap_vld (Read/COR) // others - reserved // (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake) #define XINFO_AXILITES_ADDR_AVERSION_DATA 0x10 #define XINFO_AXILITES_BITS_AVERSION_DATA 32 #define XINFO_AXILITES_ADDR_AVERSION_CTRL 0x14 #define XINFO_AXILITES_ADDR_BSUBVERSION_DATA 0x18 #define XINFO_AXILITES_BITS_BSUBVERSION_DATA 32 #define XINFO_AXILITES_ADDR_BSUBVERSION_CTRL 0x1c #define XINFO_AXILITES_ADDR_CTYPEBOARD_DATA 0x20 #define XINFO_AXILITES_BITS_CTYPEBOARD_DATA 32 #define XINFO_AXILITES_ADDR_CTYPEBOARD_CTRL 0x24 

该文件描述了寄存器的地址,这些寄存器与函数中参数的位置相对应。 项目综合后,您可以看到如何以周期方式执行创建的项目。


项目节拍示例

与hls的合作表明,该工具适用于快速解决任务,尤其是它已经证明可以解决计算机视觉的数学问题,可以轻松地用C ++或C描述,以及创建用于交互和交换的小型ip内核。标准FPGA接口提供的信息。

同时,HLS不适合实现特定的硬件接口,例如,在我们的情况下为I2S,并且生成的rtl代码在FPGA上的占用空间比用标准hdl语言编写的要多。

驱动程序测试的最后一步是开发I2S流量生成器。 该ip内核重复了以前ip内核的功能,除了它会生成与TDM模式下的实际I2S数据相对应的增量数据(流量)。


未来定制I2S核心测试和I2S流量生成器的模块设计

结果,我们得到了hls,axi dma和s_axilite的结果,检查了我们的软件和驱动程序的性能。

结论


我们设法开发了必要类型的接口卡,以及用于tdm,pri和bri的ip内核。 我们已大大改进了当前开发此类设备的方法,并创建了一个全面的解决方案,可以与Asterickpatton等公司的类似接口板竞争。 我们解决方案的优势在于,开发人员不需要PC和PCI之间的中间链接即可进行数据传输,他将能够通过以太网直接传输接收到的信息。

Source: https://habr.com/ru/post/zh-CN415435/


All Articles