首页 / 资讯中心 / 文章详情

51单片机与DAC0832波形发生器设计:低成本竞赛方案全解析

51单片机与DAC0832波形发生器设计:低成本竞赛方案全解析 ★ FEATURED ARTICLE
简介全国电子设计大赛的波形发生器设计赛题方案覆盖方波、三角波、锯齿波、正弦波等多种波形的产生原理与实现路径采用DAC0832数模转换芯片搭配单片机进行D/A转换与波形调整。面向电子竞赛参赛者、嵌入式学习者以及信号发生器设计入门者解决从器件选型、硬件电路图绘制到控制程序编写与调试观察的完整实践问题。压缩包共1个doc文件约496KB内容包含课程设计论文、电路原理图、程序流程图及程序清单便于依照章节逐步搭建与验证。已有4242人学习下载。方案详细拆分四种波形的产生过程并给出键盘切换波形、调节频率幅度的软件实现思路帮助读者快速获得可运行的参考设计并在人机交互与信号调理层面建立完整认知。1. 全国电子设计大赛波形发生器51单片机与DAC0832的低成本方案为什么值得复现做电子设计竞赛和课程设计的人十有八九都碰过波形发生器这道题。正弦波、三角波、方波、锯齿波要频率可调、幅度可调还得用键盘切换、液晶显示——听起来基础但真要把每个波形的生成逻辑讲清楚、把DAC0832的双缓冲时序搞明白再在Proteus里仿真通过、焊到板上用示波器看到稳定波形中间隔着一堆细节。这份以AT89C51和DAC0832为核心的波形发生器设计走的是最小系统加D/A转换的经典路线不需要昂贵的DDS专用芯片也没有复杂的FPGA逻辑价格低廉、结构紧凑特别适合课程设计和竞赛入门。我拆完这份文档后有个很直接的感受它把从波形原理到汇编子程序、再到仿真的整套链路都串起来了对于想快速搭出一台函数信号发生器的人来说是非常有参考价值的落地模板。2. 方案论证与波形生成原理为什么最终选单片机加DAC而不是专用函数发生器IC2.1 四种常见实现方案的取舍函数信号发生器的实现路径业内大概能分成四类。第一类用分立元件搭比如用比较器、积分器加晶体管自己拼这种方案频率上不去工作点漂移严重调试起来非常折磨人只适合练手。第二类用专用函数发生器IC像L8038、BA205、XR2207这些功能简单、精度一般频率上限只有300kHz左右关键在于频率和占空比不能独立调节两个参数互相拉扯这在要求波形参数独立可调的场合很难用。第三类用DDS专用芯片能做到很高的频率和任意波形但成本和外围电路复杂度都上去了竞赛和课设场景里有点浪费。第四类就是这份设计选用的方案AT89C51加DAC0832把P1口接键盘和显示P2口接DAC输出波形不需要额外的扩展芯片成本和功耗都压得很低而且所有波形都能靠查表或累加算法生成灵活性反而比专用IC更高。从实际制作的角度看这个方案还有一个优势就是样机成功率有保障。芯片全是通用器件坏了随时能换引脚功能清楚参考资料也容易找。相比之下DDS芯片的焊接和配置对新手不算友好一旦某个寄存器配错波形出不来都不知道从哪里查起。2.2 波形生成的四条基本思路四种波形的生成逻辑其实可以统一到“数字量按序送入DAC转换”这个框架里。锯齿波最简单设定一个初值后不断加1加到溢出或到边界就重新回到初值这样DAC输出的模拟电压就从低到高线性爬升再瞬间跳回低点循环反复。三角波是锯齿波的变体先加后减从00H开始加1到边界再减1回到初值注意这里的“减到初值”是指减到起始值而不是减到0否则波形会不对称。方波更直接输出一个高电平值持续一段时间再输出一个低电平值持续相同时间两个时间必须严格相等否则就成了占空比不为50%的脉冲波这一点在程序里要靠同一个延时子程序来保证。正弦波最麻烦因为DAC输出的是按时间递增的电压序列要把正弦函数的数值离散化算出每个采样点的输出值做成一个表程序里循环查表送数。这里面有个关键点需要注意正弦表不是从0度到360度完整存储的而是利用对称性只存四分之一周期通过取反和增减序来生成后三个象限。这个技巧能用256个字节甚至更少的存储空间输出完整周期的正弦波对4K字节Flash的AT89C51来说非常有必要。如果直接把360度每个点的值都存下来其实也就多存几百个字节但这个设计选择了存储更省的对称方案这也是典型竞赛题里“资源紧张下怎么优化”的思路。3. 硬件电路搭建DAC0832双缓冲接线、运放电流转电压与键盘分配3.1 DAC0832的三种工作方式这个设计用的是哪一种DAC0832内部有两级寄存器分别是输入寄存器和DAC寄存器芯片因此支持直通、单缓冲、双缓冲三种方式。直通方式是让所有控制信号都处于有效状态数据线直接送到D/A转换器这种方式占用引脚最少但数据线上稍有毛刺就会直接影响模拟输出单缓冲方式锁存一级适合单路输出双缓冲方式两级都锁存适合多路DAC需要同步输出的场景。这份设计的接口方式值得仔细看。传送控制端接地输入锁存允许ILE接5V利用一个地址码进行二次输出操作。第一次操作时P2.6为高电平把P0口数据线上的数据锁存到输入寄存器第二次操作时写控制信号有效传送控制端为低电平输入寄存器的内容被锁存入DAC寄存器D/A转换器开始对锁存的8位数据进行转换约经过半个时钟周期后在Iout1和Iout2端建立稳定的电流输出。这个流程对应程序里的MOVX指令访问地址7FFFH因为P2.6参与地址译码地址最高位和P2.6的组合决定了访问的是DAC端口。实际接线时如果把P2.6直接作为片选地址就是0x7FFF这个数值在程序里反复出现一定要和硬件接线对应上换一个引脚就要换地址这是初学最容易忽略的地方。3.2 运放电路与电流转电压DAC0832的输出是电流形式Iout1在输入全为1时电流最大Iout2与Iout1之和为一个常数芯片内部有一个反馈电阻Rfb。需要模拟电压信号时要通过一个高输入阻抗的线性运算放大器实现电流转电压。运放的反相输入端接Iout1同相输入端接地反馈电阻接到Vref端这样输出端的电压就等于电流乘以反馈电阻的负值。常见的做法是用LM358或NE5532LM358单电源供电即可工作适合5V系统NE5532噪声更低但需要双电源具体选哪个要看你的系统里有什么电源轨。竞赛环境里经常只有单路5V所以LM358更常见但要注意LM358的输出摆幅到不了轨到轨靠近电源轨的地方会削顶如果你要求幅值精度高就得选轨到轨运放或者在电源上做文章。硬件连线还有两个接地细节。AGND模拟地和DGND数字地最好在基准电源处共地也就是单点接地不要让模拟电流流过数字地回路。Vref基准电压的稳定性直接影响输出波形的精度如果基准源有纹波波形上就会叠一层噪声这在示波器上观察正弦波时特别明显看起来像是线条发毛。3.3 键盘分配与波形切换逻辑P1口接开关K0到K4开关状态决定输出哪种波形。K0向上拨产生锯齿波K1向上拨产生三角波K2对应方波K3对应正弦波K4用于频率或幅度的调整没有开关拨动时处于等待状态。这里的设计逻辑是查询方式而非中断方式程序在主循环里不断读P1口的状态检测到某个开关变高就跳转到对应的波形发生子程序。查询方式的优点是简单直接不涉及中断优先级和现场保护缺点是主循环里如果某个波形子程序不返回其他按键就响应不了。文档里提到一个很关键的操作细节切换波形前必须把当前开关关掉否则识别不了其他按键。这在硬件上对应的是“互斥检测”——你同时拨了两个开关程序不知道该执行哪个。处理方式也很朴素就是规定同一时间只允许一个开关向上。想做得更完善一点可以在程序里加一个优先级的判断比如K0优先于K1但竞赛做演示时直接靠操作约定来避免冲突也是最省事的办法。4. 软件设计与波形程序实现从汇编子程序到查表法的完整拆解4.1 总体程序框架键盘查询加DAC输出的主循环软件部分的核心结构是初始化、键盘扫描、波形输出三个循环模块交替执行。初始化包括设置堆栈指针、DAC端口地址、正弦表指针等。键盘扫描读P1口状态判断哪个键为1。波形输出根据键值调用对应的子程序输出完一个周期后回到键盘扫描重新检测按键是否发生变化。这种结构就是典型的“查询-执行-再查询”循环每个波形子程序内部都是一个死循环直到检测到按键状态变化才退出。4.2 锯齿波子程序最简单的DAC送数循环锯齿波的汇编代码非常短核心逻辑就一段。先设置数据指针DPTR指向端口地址7FFFH把R7清零作为初始输出值然后循环里把R7的值通过MOVX写到DAC端口R7加1后再跳回重新写入。当R7从0加到255产生溢出时下次循环写入的值又变回0波形自然跳变回低电平形成锯齿波。MOV DPTR,#7FFFH ; DAC端口地址对应P2.6译码 MOV R7,#0 ; 初始输出值决定波形起始电平 DASAW: MOV A,R7 ; 把计数值送入累加器 MOVX DPTR,A ; 写入DAC触发D/A转换 INC R7 ; 计数值加1对应锯齿波上升沿 NOP ; 空操作延长转换稳定时间 NOP NOP SJMP DASAW ; 循环回到下一次写入这段代码里的NOP有三个作用是在两次DAC写入之间插入延时给转换留出稳定时间。DAC0832的电流建立时间是1μs如果写入频率太快上一笔数据还没建立稳定就被下一笔覆盖输出波形就会有毛刺。但NOP只是权宜之计想要精确控制频率还是得用定时器中断来定延时后面章节会单独说这个问题。这段程序有个隐含信息它的频率由循环周期决定而循环周期由指令执行时间决定NOP数量决定了最小步进。12MHz晶振下单字节NOP是1个机器周期约1μs整个循环大约7到8个机器周期所以单步方波频率在130kHz左右这是这个设计能输出的最高频率参考值实际还要算上MOVX和SJMP的周期只能是大概估算。4.3 三角波子程序递增到边界再递减注意两个边界的含义三角波比锯齿波多一个递减段。L1处R7清零开始递增循环A加1后如果非零就跳回L1继续写当A从FFH加1溢出变为00H时JNZ指令检测到零就落到L3L3开始递减循环写外部存储器。这个写法和理论部分的“先加后减”略有出入实际代码里递增段从00H到FFH溢出后进入递减段递减段从00H开始往下减输出的是另一个方向的斜坡。L1: MOV R7,#00H ; 清零确定三角波起始点 MOV A,R7 L2: MOVX DPTR,A ; 写入DAC上升段输出 NOP ; 延时等待转换建立 NOP NOP INC A ; 数值递增 JNZ L1 ; 未溢出则继续上升段 L3: MOVX DPTR,A ; 溢出后进入下降段继续写DAC NOP NOP DEC A ; 数值递减 JNZ L3 ; 未减到0则继续下降段 SJMP L1 ; 减到0重新开始下一个周期要注意这里的溢出机制是8位累加器的自然溢出FFH加1变成00H所以三角波的峰值永远在FFH附近波形的满幅输出是固定的。如果你的运放电路把DAC输出电流转成0到5V电压那这个三角波的峰峰值就是接近满量程的如果你想要半幅输出就得在循环里设置边界判断而不是依赖自然溢出。这个细节在调幅度时经常用到。4.4 方波子程序与50%占空比的保证方波生成的关键在于高电平持续时间和低电平持续时间必须相等。代码里先输出A的值调用DELAY然后CPL取反再输出并调用同一个延时子程序。因为延时子程序是同一个输出高电平和低电平的时间完全一致占空比天然就是50%。如果把两个延时改成不同的常数波形就变成脉冲波这在需要任意占空比的场合反而是个扩展方向。FANGB: MOV A,R7 ; R7中存的是方波电平值 MOVX DPTR,A ; 输出高电平 ACALL DELAY ; 定时高电平持续时间 CPL A ; 取反高变低 MOVX DPTR,A ; 输出低电平 ACALL DELAY ; 定时低电平持续时间 SJMP FANGB ; 循环 DELAY: MOV R7,#200 ; 延时初值决定方波频率 DEL1: MOV R6,#123 DEL2: DJNZ R6,DEL2 ; R6减到0大约123个机器周期 DJNZ R7,DEL1 ; 外层循环200次 RET延时子程序里R7既是方波电平的存储器又是延时计数器这里其实是复用了寄存器。方波发生前先把#FFH送R7执行FANGB时A等于FFH输出高电平取反后A等于00H输出低电平。这里容易踩坑的是DELAY子程序会破坏R7的值下一轮FANGB执行MOV A,R7时R7已经被DELAY改成0了所以每次循环开头重新给A赋值是必要的。实际正常代码里R7在延时前后都要重新加载或者采用另一个寄存器做延时计数避免数据被覆盖。4.5 正弦波查表法只用四分之一周期数据和位取反生成完整波形正弦波的汇编代码是整个程序里最复杂的一段。先把正弦表从程序存储区搬到内部RAM从地址6DH开始存放然后设置DAC端口地址和表指针按照四个象限分别输出。第一象限从表头开始顺序查表递增输出第二象限递减输出同一段表数据第三象限取值后先取反再输出并递增指针第四象限取值后取反再输出并递减指针。这样只用四分之一周期的采样值加上CPL取反操作就还原出完整的正弦波。MOV DPTR,#SINTAB ; 正弦表首地址 MOV R0,#6DH ; 内部RAM存储区起始地址 LOOP: CLR A MOVC A,ADPTR ; 查表读取正弦值 MOV R0,A ; 存储到内部RAM INC DPTR INC R0 CJNE R0,#80H,LOOP ; 未到存储区末尾则继续 MOV DPTR,#7FFFH ; DAC端口地址 MOV R0,#6DH ; 重新设置表指针 LOOP1: MOV A,R0 ; 第一象限查表 MOVX DPTR,A ; D/A转换 ACALL DELAY DEC R0 ; 指针递减 CJNE R0,#6DH,LOOP1 ; 未到表头则继续 LOOP2: MOV A,R0 MOVX DPTR,A ; 第二象限反向输出 ACALL DELAY INC R0 ; 指针递增 CJNE R0,#6DH,LOOP2 ; 注意这里的结束边界 SJMP LOOP1这段代码和文档中的后半部分写法有些细节需要修正。LOOP3和LOOP4用了CPL取反来输出第三第四象限但中间连续用DEC和INC的边界条件容易出错特别是LOOP2的CJNE R0,#6DH判断会让指针回到表头后立即跳出实际调试时应该在LOOP1和LOOP2的出口处加SJMP跳转来控制象限切换而不是靠CJNE自然落到下一段。比如第一象限输出完应该SJMP到第二象限的入口第二象限输出完SJMP到第三象限这样才能顺序走完四个象限否则程序会乱序执行。正弦表本身是一组8位数值从7FH开始递增到FFH对应正弦波从过零点到峰值的离散采样。采样点数为256个时每个点之间的相位间隔是360度除以256约1.4度波形的平滑度已经足够示波器观察。如果你用C语言重写可以直接用一个const数组存256个点的完整周期数据程序会简单非常多但用汇编做对称处理更省内存两种思路都可以参考。4.6 频率与幅度调整拨码开关修改延时参数的理论频率调整的本质是改变DAC写入间隔。查表法里每个采样点写入后都调用DELAYDELAY的时间长度直接决定了一个完整波形周期的时间也就是频率。延时越长频率越低延时越短频率越高。幅度调整则简单粗暴要么在查表数据上做位移和缩放比如把采样值右移一位就得到半幅波形要么在运放电路的反馈电阻上做文章改变反馈电阻的值改变增益。后一种属于硬件调幅前一种是软件调幅。这个设计里用的方式是在输出到DAC前对数字量做处理比如CPL取反或者移位配合拨码开关选择是否缩放。5. 调试与避坑Proteus仿真通过不代表实物能出波六个高频踩坑点排查5.1 现象Proteus仿真波形有毛刺正弦波边缘不光滑原因DAC0832的电流建立时间约1μs两次写入间隔太短上一次转换没完成就开始下一次。汇编程序里虽然加了NOP延时但调试时如果对照仿真波形发现毛刺多半是延时不足。解决先给所有波形子程序里的DAC写入操作统一加一个延时子程序调用比如DELAY套DELAY把写入间隔拉到10μs以上再看波形。确认波形光滑后再逐步减小延时逼近目标频率不要一上来就追求最高频率。5.2 现象波形只有半幅正弦波上半部分正常下半部分削底原因运放单电源供电时输出不能到负电压DAC输出电流经过运放转换后电压摆幅受限于电源轨。如果你的设计用5V单电源DAC输出在0到5V范围内运放输出接近0V时会进入饱和区。解决把运放改成双电源供电或者把参考电压抬高让波形中心落在Vcc/2而不是GND常见做法是用两个电阻分压把运放同相输入端偏置到2.5V。这样即使单电源也能输出完整的正负交替波形形状只是整体有一个直流偏置示波器要用DC耦合观察或用隔直电容去掉偏置。5.3 现象K0到K4开关切换波形没反应拨动K1时还在输出锯齿波原因查询式键盘扫描要求某个开关断开后程序才能跳出当前波形子程序但如果当前波形子程序内部是死循环且没有检测键盘语句程序永远跳不出来。这个设计里波形子程序用SJMP自我循环没有在循环体里加入对P1口的重新扫描。解决在每个波形子程序的循环体里加一段键盘检测代码每输出一个周期的波形后读一次P1口发现有其他键变高就跳转到对应子程序。常见的做法是把P1口的值和一个全局变量保存的旧值比较变化了就重新分支。5.4 现象三角波上升段和下降段不对称波峰处有明显平顶原因递增段用INC A且JNZ判断溢出递减段用DEC A且JNZ判断零但溢出点是FFH到00H跳变波形从峰值瞬间掉到谷值而不是从峰值逐渐下降。这是因为递增溢出后进入的是下降段但操作数顺序如果从00H开始递减波形会先出现一段低电平再下降看起来就像被削了一刀。解决三角波应该先增后减且峰值处不跳变。正确写法是递增到FEH时判断一下A等于FEH就进入递减段递减到01H又回到递增不让A出现FFH到00H的自然溢出。或者用两个边界值判断从0增到255再从255减到0中间不断。5.5 现象正弦波输出频率换挡后幅度明显变小原因查表时每个采样点都在DELAY如果DELAY被用来调频改变了延时时间但不改变查表数据的幅值理论上幅度不该变化。但实际上如果写入DAC的间隔小于DAC建立时间转换不完全导致模拟输出幅度下降可能出现在高频档。解决先确认基准电压Vref稳定再把调频方式从“减小DELAY”改成“跳过采样点”比如每隔一个点输出一次这样频率翻倍但每个点仍有充足转换时间幅度保持平稳。这是查表法调频里比较实用的技巧。5.6 现象Proteus仿真正常实物接示波器波形噪声大原因数字地和模拟地没有单点接地键盘和显示电路的开关噪声通过地线串入运放输出。DAC0832的数据线翻转时会在地线上产生尖峰电流被运放放大后就是噪声。解决数字部分和模拟部分的地线在DAC0832的AGND处单点汇合Vref基准源用钽电容和0.1μF陶瓷电容并联滤波运放电源脚也加去耦电容。如果还不行把DAC输出到运放输入之间的走线尽量短避开数字信号线。6. 进阶与验证用定时器中断做频率粗调细调替换延时法的关键改动前文所有波形子程序都用DELAY延时来控制输出节奏这会带来一个明显的问题——主循环被占用键盘扫描得不到实时响应而且延时精度受汇编指令周期影响频率只能粗略定档。想把它做成更实用的信号源一个很自然的升级就是换成定时器查询或中断方式。以方波为例P1.0输出一个高电平后等待定时器T0计满溢出再取反输出低电平这样频率由定时器初值决定精度远高于DELAY循环而且主程序可以把空闲时间让给键盘扫描和LCD刷新。用C语言的实现思路是这样的初始化时设置TMOD为模式1即16位定时器TH0和TL0装入初值开启TR0。每次溢出时进入中断服务程序把输出引脚取反重新装载初值。频率计算公式是周期等于定时时间乘2因为一个完整方波周期包含一次高电平一次低电平而定时时间等于(65536减初值)乘机器周期。#include reg51.h #define DAC_PORT P0 #define DAC_ADDR XBYTE[0x7FFF] unsigned int freq_code 0x1000; // 频率码对应定时器初值 void timer0_isr(void) interrupt 1 { TH0 (65536 - freq_code) 8; TL0 (65536 - freq_code) 0xFF; DAC_PORT ^ 0xFF; // 取反输出产生方波边沿 } void main() { TMOD | 0x01; // T0工作于16位定时器模式 TH0 (65536 - freq_code) 8; TL0 (65536 - freq_code) 0xFF; ET0 1; EA 1; TR0 1; while (1) { // 主循环里扫描键盘调整freq_code值即可实时改变频率 if (key_increase()) freq_code - 100; if (key_decrease()) freq_code 100; } }这个代码里DAC_PORT每次中断取反输出的方波频率等于定时器溢出频率的一半。freq_code间接控制周期初值越小中断越频繁、频率越高。把freq_code做成两个字节高位负责粗调、低位负责细调这样一个按键每次改变100就能实现频率的精细步进。如果想扩展成正弦波发生器就把中断服务程序改成查表每中断一次输出表中的一个采样点并把查表指针加一表长为256时输出频率等于中断频率除以256这样调频率就等于调定时器初值调幅度就用一个乘法因子对采样值做缩放后送DAC。用这种方式重构之后LCD显示频率就变得顺理成章了——定时器初值是已知量频率可以直接算出来不需要像DELAY法那样靠示波器读频率再反推延时参数。当年我做完波形发生器那会儿还没想通这点直到有一次拿示波器测DELAY法输出的三角波发现频率比计算值差了快一倍才意识到NOP延时的累加误差有多大。从那以后我每次调整波形参数都强制走一遍“定时器算频率、示波器验证、误差归因到指令周期”的流程再也不靠掐指算NOP来估频率了。希望这个思路能帮你在复现这套设计时少走一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站