FPGA 20个例程篇:20.USB2.0/RS232/LAN控制并行DAC输出任意频率正弦波、梯形波、三角波、方波(一)

       在最后一个例程中笔者精挑细选了一个较为综合性的项目实战,其中覆盖了很多知识点,也是从一个转产产品中所提炼出来的,所以非常贴近实战项目。

       整个工程实现了用户通过对上位机PC端人机界面的操作,即可达到控制豌豆开发并行DAC输出给定频率和初始相位的正弦波、三角波、方波、梯形波的效果,上位机通信接口同时支持USB2.0、串口RS232和千兆网口LAN,但是同一时刻只能选择一种接口与开发板通信,为此笔者专门用Labview搭建了上位机人机界面环境并压缩成安装包,大家直接解压安装到PC端即可,上位机的具体操作将在后面详细介绍,如图1所示是豌豆开发板Artix7上并行DAC的电路。

       数字变频是FPGA信号处理算法和实现的一项关键性技术,当笔者在工作中第一次用到相关技术时,也查阅了很多网络博客、收费教程和官方手册等,但遗憾的是大部分文章内容都很雷同并且很少有深入地分析或者讲解,所以在最后一个例程当中笔者会尽力把这些令人费解的地方说明清楚,整个例程既包含了数字信号处理,也包含了接口通信设计,同时结合上位机控制,朋友们完全可以将其看成一个程控波形发生器。

图1 豌豆开发板Artix7上并行DAC的电路

       如图2所示是例程整体设计示意图,大家可以将其看成两个部分:前一部分是外设接口设计,后一部分是数字信号处理,这也是FPGA工程中一个非常经典的模型即外接接口和数字信号融合在一个项目里面。

       伴随ADC/DAC转换速度越来越快,接口从传统意义的SPI接口到多位宽并口再到CMOS和LVDS接 口,直到现在已经发展到了JESD204B接口,这种接口则是一种新型的基于高速SERDES的ADC/DAC传输数据接口,板子与板子间的数据传输也已经引入了SFP万兆光口等,同时板子与PC端的数据传输从RS232接口到USB2.0接口再到千兆网口和USB3.0接口,最后发展到PCIE接口,所以高速接口设计和数字信号处理往往会出现在一个项目。

 图2 USB2.0/RS232/LAN控制并行DAC输出任意频率正弦波、梯形波、三角波、方波整体设计示意图

        ADC和DAC作为数字信号处理的中间桥梁,在现实的项目中更有着极为广泛的应用,笔者曾读过一本国外的经典书籍“understanding_digital_signal_procesing”,里面提到了数字信号处理的两大利器即数字滤波和傅里叶变化,当然这只是针对ADC端模拟转数字芯片的采集而言,而对于DAC端数字转模拟芯片的转换来说,数字变频可以说是一个常用的重要关键性技术。

       如图3所示是DSP处理器在数字信号处理中的应用示意图,即通过用ADC去采集目标信号再根据项目需求进行数字信号处理,这里涉及到采样与量化理论、离散傅里叶变化等知识在这里就不再展开了,当然实际项目当中对于ADC采集和DAC转换因为硬件上肯定存在误差还需要引入校准等操作,最后根据实际需求可能还需要通过DAC把数字信号处理的结果通过一个模拟量传输出去,大家可以看到整个DSP处理器一直工作“数字区域”对数字信号进行处理再通过ADC或者DAC实现对“模拟区域”的控制,这也是数字信号处理中一个非常经典的模型。

图3 DSP处理器在数字信号处理中的应用示意图

       步入正文前,笔者在这里想多费点笔墨为大家扩展一些项目背景知识,因为都是从学生时代走过来,所以都经历了从理论到实践,从模糊到清晰的过程,可以说想建立一个完整的知识体系并不是一件蹴而就的事,需要在理论中不断理解吸收,在项目中不断学习深化,更在实践中不断归纳总结。

        ADC和DAC在嵌入式学习当中可以说耳熟能详的名词了,首先早在学生时代学习数字信号处理或者信号理论等相关课程这个名词就常常在耳边响起;其次再到单片机学习,刚刚接触MCU各种教程就告诉我们其自带低速ADC和DAC即可实现数模或者模数转换的目的;再次可能用过DSP的同学都知道其强于浮点数运算,那么浮点运算又在做什么呢?于是乎ADC和DAC又久久回响在耳畔;最后再到读研或者工作用到FPGA的时候,可能真的是才一入门,铺天盖地的信息又不断地为大家传达类似的观点:FPGA具有并行、快速的特点,是高速接口设计和数字信号处理的主战场。可以说非常有趣的是,学校教学或者网络培训等都好像在做知识科普:在引出一些前沿理论和先进概念,但是在抛出内容、亮明观点以后,刚到核心部分的学习就戛然而止了,所以网络上可能只能看到同一个概念的不断地一次次被重复或者来来回回炒冷饭。

       有关ADC的采样FIR、IIR、CIC等滤波器设计和FFT时域到频域变化,因为篇幅有限不过于发散,所以在这里并不详细说明,将会放在后续数字信号处理专栏结合具体实例讲解,下面我们主要围绕例程中的核心内容DAC数字变频展开,如图4所示是数字变频FPGA内部实现的示意图。

图4 数字变频FPGA内部实现的示意图

       基于FPGA的数字变频实现,网络上也有很多教程和博客等介绍,看起来好像学习资源很多,但是大部分都在不断地旧调重弹或者在重复地复制粘贴,实际上会发现很多作者本身也不是非常理解这块的知识,所以传来传去还可能会带来一些错误的思想去误导大家,尤其对于初学者而言。

      开门见山地对于数字变频包含了三个核心的知识点即1. 累加器步长;2. 地址间映射;3. 频率分辨率,搞明白这些对于后面理解DDS IP核的配置和任意波的实现起到重要作用,所以请大家花些时间去认真思考,如图5、图6、图7所示是数字变频的示意图,可以说这三幅示意图非常形象地把数字变频技术勾勒出来。

       数字变频用最通俗易懂的话说,即给定任意一种波形就可以人为地在不改变形状的前提下自由变化频率,这项技术广泛地应用在很多领域,笔者结合这三幅图为大家详细说明数字变频中的三个知识点。

        如图5所示是数字变频的内部结构示意图,大家可以清楚地看到当外部给定了“频率控制字”、“相位控制字”和外部时钟fclk后,FPGA即开始以fclk为时钟参考,每个时钟周期进行“频率控制字”的累加,其结果会加上“相位控制字”一起送至任意波的查找表,FPGA再把查找表中对应到数值送到外部DAC数模转换芯片上,最后通过硬件上运放调理输出期望频率的fout,这幅示意图很好地描绘了数字变频的内部结构,而其中的“频率控制字”即为累加器步长。

图5 数字变频的内部结构示意图

       如图6所示是数字变频的累加器和地址映射示意图,通过累加器步长的介绍,大家了解到数字变频的内部存在一个以fclk为时钟周期的计数器一直在做累加操作,但是累加器应该以什么方式去映射到任意波形的查找表呢,于是很自然地引入了第二个知识点即地址间映射,显然以累加器总位宽作为波形查找表位宽是不现实的,那么这时候就需要一种映射关系即查找表地址放在累加器位宽的最高位,如下图所示,累加器位宽是12位而波形查找表的位宽仅仅是4位。

图6 数字变频的累加器和地址映射示意图

      如图7所示是数字变频的累加器步长和频率关系示意图,在简单地介绍完累加器步长和地址间映射后,就涉及到第三个知识点即频率分辨率了,那么在数字变频的过程中频率分辨率又是怎么计算出来的呢,对于分辨率相信大家也耳熟能详,比如现实生活中的显示屏有各种各样的分辨率如1920*1080、1024*768、640*480等,显然分辨率越高精确度越大,对于数字变频中其实也存在相似的说法,比如设定的精度理论上可以达到小数点前两位、前一位还是后一位、后两位、后三位等,而这个最小分辨率实际上参考时钟fclk与频率累加器位宽的比值。

图7 数字变频的累加器步长和频率关系示意图

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
以下是使用Linux STM32MP157板子的DMA+DAC输出正弦的示代码: ```c #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <stdbool.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/stat.h> #include <linux/dma-buf.h> #include <linux/dmaengine.h> #include <linux/dma-mapping.h> #include <linux/dma-fence.h> #include <linux/types.h> #include <linux/ioctl.h> #define DAC_BASE_ADDRESS 0x40007400 #define DAC_DHR12R1 0x08 #define DAC_DHR12L1 0x0C #define DAC_CR 0x00 #define PI 3.14159265 #define SAMPLE_RATE 48000 #define AMPLITUDE 2047 #define FREQUENCY 1000 int main() { int fd_mem = open("/dev/mem", O_RDWR | O_SYNC); if (fd_mem < 0) { printf("Failed to open /dev/mem\n"); return -1; } void* mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd_mem, DAC_BASE_ADDRESS); if (mem == MAP_FAILED) { printf("Failed to mmap memory\n"); return -1; } uint32_t* dac = (uint32_t*)mem; // Enable DAC and DMA dac[DAC_CR / 4] |= (1 << 0) | (1 << 12); // Configure DMA int fd_dma = open("/dev/dma-buf/0", O_RDWR | O_SYNC); if (fd_dma < 0) { printf("Failed to open /dev/dma-buf/0\n"); return -1; } uint32_t* buf = mmap(0, SAMPLE_RATE * sizeof(uint16_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd_dma, 0); if (buf == MAP_FAILED) { printf("Failed to mmap DMA buffer\n"); return -1; } uint16_t* samples = (uint16_t*)buf; for (int i = 0; i < SAMPLE_RATE; i++) { double t = (double)i / (double)SAMPLE_RATE; samples[i] = (uint16_t)((double)AMPLITUDE * sin(2 * PI * FREQUENCY * t) + (double)AMPLITUDE); } struct dmaengine_cmd cmd = { .flags = DMA_PREP_INTERRUPT | DMA_CTRL_ACK, .wait_for_completion_timeout = 1000, .size = SAMPLE_RATE * sizeof(uint16_t), .direction = DMA_MEM_TO_DEV, .src_start = buf, .dst_start = &dac[DAC_DHR12R1 / 4], }; int ret = ioctl(fd_dma, DMA_BUF_IOCTL_CMD, &cmd); if (ret < 0) { printf("Failed to configure DMA\n"); return -1; } printf("Press enter to stop the program\n"); getchar(); // Disable DAC and DMA dac[DAC_CR / 4] &= ~((1 << 0) | (1 << 12)); // Unmap memory munmap(buf, SAMPLE_RATE * sizeof(uint16_t)); munmap(mem, 0x1000); close(fd_dma); close(fd_mem); return 0; } ``` 这段代码会生成一个1kHz的正弦,并使用DMA将数据传输到DAC输出。你可以根据需要修改参数,比如采样率、幅值、频率等。同时,请注意实际硬件的数据手册和引脚定义,确保代码正确配置DAC输出

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值