51单片机+TLC5615串行DAC正弦波发生器设计与Proteus仿真
2026/9/16 19:25:30 网站建设 项目流程

简介:采用51单片机与TLC5615构建的正弦信号发生器完整设计方案,覆盖电路原理图、C语言源代码与Proteus仿真工程,适合电子设计竞赛备赛者、嵌入式初学者以及模拟电路爱好者学习参考。压缩包内共16个文件,以Keil C51工程文件(uv2、c、hex、lst)、Proteus仿真文件(dsn、dbk、pwi)及配套备份(bak、obj等)组成,整体仅72KB,目录简洁、便于按需查看。目前已有3619人下载学习。项目展示了单片机通过串行接口控制12位DAC生成正弦波的完整流程,包含初始化配置、查表法产生波形数据、输出滤波电路设计与仿真验证等关键内容;既能训练硬件原理图分析与电路搭建能力,也能提升C语言编程与在线调试技巧,是软硬结合、理论联系实际的入门佳例。

1. 用51单片机加串行DAC做正弦源,卡点在时序不在位数

做放大器调测时手头急需一个标准正弦信号源,如果只靠51单片机输出PWM再滤波,RC网络要把几十kHz的载波压下去,同时又要让1kHz基波通过,两级滤波后幅度衰减和相位偏移很难平衡。换成并行DAC虽然数据吞吐快,但P0和P2口全占满,后续想再接键盘或显示就捉襟见肘。TLC5615这种三线串行接口的DAC只占三根IO,配合查表法就能输出几百赫兹到一两千赫兹的比较干净的正弦波,适合课程设计、毕业设计和放大器前级调试这类场景。

这里有个常见的认知误区:决定波形质量的首要因素不是DAC位数,而是查表点数和更新时序的配合。TLC5615实际是10位DAC,但串行帧长度是12位,很多人照着12位去写驱动,移位次数和数据对齐全错,波形直接出不来。这个项目把电路原理图、完整C源代码和Proteus仿真工程打包在一起,正好用来验证一条完整的信号链路:51查表生成数字正弦序列,TLC5615转换成阶梯电压,RC低通滤波后输出连续正弦波。下面从接口逻辑、固件实现到仿真验证逐层拆开。

2. TLC5615是10位DAC,串行帧却是12位,先把这个接口逻辑理清

2.1 12位帧里藏着的移位陷阱

TLC5615的串行协议是标准的SPI写入方式,数据从DIN引脚按MSB-first顺序移入,片选CS低有效。和一般SPI器件不同的是,一次完整传输需要12个SCLK周期:前10位是DAC的有效数据D9~D0,后2位必须是0。也就是说,10位精度用了12位的帧来装载,低2位是强制填充的空位。如果直接把一个10位数值逐位移出去,结果就是数据整体向右错了两位,输出电压和查表值完全对不上。

因为在同一帧里最高位和最低位的含义不同,写驱动时要么先判断当前位的位置再决定发送0还是数据位,比较绕;更常见的做法是在发送前先把10位数据左移2位,让它占满12位帧的高位,然后从最高位开始送12次,后面2位自然就是0。TLC5615原理图上DIN接的是单片机P1口第0位,SCLK和CS各占一位,三根线就能完成全部数据交换,这也是它在51课程设计里受欢迎的原因。

2.1.1 管脚功能与关键参数
管脚名功能与51单片机连接说明
DIN串行数据输入P1.0MSB-first,12位帧
SCLK串行时钟输入P1.1空闲为低,上升沿采样
CS片选,低有效P1.2整个12位传输期间保持低
DOUT级联输出悬空菊花链下传数据用
REFIN参考电压输入接2.5V或VCC决定满量程输出范围
OUT模拟输出接RC低通滤波输出为单极性电压
VDD正电源+5V与AGND之间加0.1μF去耦
AGND模拟地系统地数字地模拟地单点相连

REFIN的接法直接决定输出幅度:内部运放增益默认是2倍,当REFIN接2.5V基准时,满量程输出范围是0~5V,对应10位码值0x000~0x3FF。如果REFIN直接接VCC,理论上满量程会到10V,但受电源轨限制实际上限在5V上下,所以实用中最好给REFIN单独提供2.5V参考,或者直接用电阻分压加运放跟随。

2.2 外围电路按这个思路搭,输出纹波能小一个数量级

51单片机最小系统部分不做赘述,晶振和复位电路按标准接法即可。重点在DAC周边:TLC5615的VDD和AGND之间要就近放0.1μF陶瓷电容,REFIN引脚对地再加一个10μF电解电容和0.1μF瓷片电容并联,参考电压不稳会直接变成输出波形上的低频噪声。DOUT悬空,如果需要多路同步输出,可以把上一级的DOUT接到下一级的DIN,所有芯片共用SCLK,CS同时拉低,一次写入就能刷新多路输出。

输出端接一阶RC低通滤波,这是把阶梯波变成光滑正弦波的关键。截止频率按公式fc = 1 / (2πRC)选择,常见做法是把fc取在信号频率的3到5倍,同时远低于DAC更新率,这样对基波衰减不大,又能把采样阶梯的高频分量压下去。比如输出1kHz正弦、DAC更新率64kHz,一阶RC取R=1kΩ、C=0.1μF,fc≈1.6kHz,基波衰减约0.8dB,阶梯分量被压掉20dB以上。不要试图用单级RC同时做到载波压低和基波无损,一阶滤波的衰减斜率只有20dB/十倍频,阶梯频率和信号频率隔得不够远时,考虑加二级RC或改用Sallen-Key有源滤波器。

注意:RC滤波的输出接下一级电路时,负载阻抗要远大于R,否则形成分压导致幅度下跌。示波器探头用1×档时输入电容约100pF,和R=1kΩ组合会引入一阶低通,对高频测量有影响,测波形时优先用10×档。

3. 查表法固件:先用Python生成正弦表,再写51的SPI模拟时序

3.1 用Python脚本生成256点对称正弦码表

正弦信号发生器最常见的数据生成方式是查表法,预先算好一个周期内每个采样点对应的DAC码值,运行时按固定步长取表中数据送给DAC。256个点是使用最广的档位:表够密,阶梯波的高频分量集中在更新率附近,便于滤波;表长度又是2的幂,用8位指针循环访问时不必处理溢出回绕。码值映射要考虑TLC5615是单极性输出、满量程0~5V,正弦波只能围绕中间电压2.5V上下摆动,因此每个采样点先归一化到0~1,再乘上1023。生成脚本如下。

import math N = 256 # 查表点数 V_MAX = 5.0 # 满量程0~5V V_REF = 2.5 # 中间电平对应REFIN table = [] for i in range(N): # 角度从0到2π,对应一个完整正弦周期 angle = 2 * math.pi * i / N # 单极性DAC偏置到中间电平,幅度占满量程 vout = V_REF + (V_MAX / 2.0) * math.sin(angle) code = int(round((vout / V_MAX) * 1023.0)) code = max(0, min(1023, code)) # 钳位到10位范围 table.append(code) # 按C语言数组格式输出,每行8个数据 for i in range(N): end = ",\n" if (i + 1) % 8 == 0 else ", " print(f"0x{table[i]:03X}{end}", end="") print()

这段脚本输出的码值范围是0x1FF到0x3FF再到0x1FF,中间值约0x200,也就是2.5V附近。取整后最小码不会到0、最大码不会到1023,因为正弦波峰值只在单点上出现,量化取整后天然留出约半个LSB的余量。钳位操作虽然这里用不上,但保留它可以在以后修改幅度系数时防止越界写入,这是写查表程序时容易被忽略的一层保护。

3.2 模拟SPI驱动:左移两位再送12个时钟

标准51单片机没有硬件SPI模块,用普通IO口模拟时序。驱动按之前说的思路实现:10位数据先左移2位变成12位帧,然后逐位发送,SCLK空闲置低,上升沿写入DIN。

#include <reg52.h> sbit DAC_DIN = P1^0; // TLC5615 DIN sbit DAC_SCLK = P1^1; // TLC5615 SCLK sbit DAC_CS = P1^2; // TLC5615 CS,低有效 // 向TLC5615写入一个10位DAC码值 void tlc5615_write(unsigned int value) { unsigned char i; value &= 0x03FF; // 只保留低10位有效数据 value <<= 2; // 左移2位,形成12位帧,低2位补0 DAC_CS = 0; // 拉低片选,启动一次传输 for (i = 0; i < 12; i++) { DAC_SCLK = 0; // 先拉低时钟,准备建立数据 if (value & 0x0800) // 判断最高位,0x0800是bit11 DAC_DIN = 1; else DAC_DIN = 0; value <<= 1; // 数据左移,下一位到达最高位 DAC_SCLK = 1; // 时钟上升沿,TLC5615锁存当前位 } DAC_SCLK = 0; DAC_CS = 1; // 传输出结束,拉高片选 }

这段代码里有几个位置容易出错。第一,value <<= 2必须在取10位掩码之后执行,顺序反了会导致有效数据溢出丢失。第二,判断最高位时用的掩码是0x0800,对应12位帧的bit11,不是10位数据的bit9,因为这个值已经被左移过了。第三,CS在12个SCLK期间必须保持低电平,如果提前拉高或者全程接地,都会导致数据错位,前者会截断传输,后者虽然能工作但会在多帧连续写入时产生片选毛刺,影响DOUT级联时序。SCLK空闲电平为低,最后传完要恢复初始状态,方便下一次调用。

3.3 定时器更新率决定输出频率,先算清这笔账再选点数

查表法输出频率由DAC更新率除以表长决定:f_signal = f_update / N,其中f_update是每秒送入DAC的码值个数。以12MHz晶振的标准51为例,软件SPI循环每送一帧大约要执行几十条指令,算上中断进出开销,f_update被压在几万赫兹以内。下表给出几个典型组合,能直观看到点数和频率如何相互制约。

查表点数目标信号频率所需DAC更新率12MHz标准51可行性
256100Hz25.6kHz可行,定时器中断较宽松
256200Hz51.2kHz紧张,软件SPI已接近上限
128200Hz25.6kHz可行,表长减半后时序充裕
64500Hz32kHz可行,阶梯感需滤波补偿
641kHz64kHz不可行,需24MHz晶振或1T单片机

想让1kHz正弦波有足够平滑的观感,至少得64点以上。64点每周期意味着相邻采样点间相位步进5.6度,最大幅值跌落约(1-cos2.8°)≈0.12%,阶梯波的高频分量落在更新率附近,经RC滤波后肉眼基本看不出台阶。定时器选工作方式1,16位手动重装模式,中断服务函数里做查表和DAC写入。定时常数按下式计算:初值 = 65536 - f_osc / (12 × f_update),例如12MHz晶振要25.6kHz更新率,一个周期39个机器周期,初值取65497。中断里做完整帧写入时,软件SPI本身耗时接近定时周期,此时要避免在中断中调用延时函数或做浮点运算,否则会丢失定时节拍。

// 全局变量 unsigned int code sin_table[256] = { /* 上一步生成的256个码值 */ }; unsigned char phase = 0; // 定时器0初始化,12MHz晶振,更新率25.6kHz void timer0_init(void) { TMOD &= 0xF0; // 定时器0设为16位模式 TMOD |= 0x01; TH0 = 0xFF; // 初值65497,高字节 TL0 = 0xD9; // 低字节 ET0 = 1; // 开定时器0中断 EA = 1; TR0 = 1; // 启动定时 } // 定时器0中断服务,每次取一点送入DAC void timer0_isr(void) interrupt 1 { TH0 = 0xFF; // 重装初值 TL0 = 0xD9; tlc5615_write(sin_table[phase]); // 查表并发送 phase++; // 指针递增,256点自动回绕 }

中断函数里没有做phase的溢出判断,因为unsigned char加到0xFF后再加1会自然溢出为0,查表索引正好回到表头,这是把表长设为2的幂带来的便利。如果改成非2的幂点数的表,就必须在中断里判断回绕,否则输出波形周期会突变。初值0xFFD9对应十进制65497,在12MHz晶振下每隔39个机器周期触发一次中断,这39个周期里有约30个要花在tlc5615_write的循环上,剩余约9个周期留给查表和重装初值,整体时序已经比较紧,调试时先用虚拟示波器确认更新率,再调滤波参数。

4. Proteus仿真电路验证:把原理图跑起来,示波器上看毛刺和阶梯

4.1 Proteus工程文件与元件模型

打开资料里的pdsprj工程前,先确认Proteus版本能兼容8.0以上格式。DSN是旧版Proteus 7的工程格式,pdsprj是Proteus 8的项目格式,两个文件内容一致,优先打开pdsprj。工程里已经放好51单片机、TLC5615、RC滤波和示波器连接。双击单片机芯片,把Program File指向Keil编译生成的HEX文件路径,晶振频率设为12MHz,这一步要和Keil工程里选的晶振参数完全一致,否则软件仿真的信号频率和频率计读数对不上。

TLC5615在Proteus元件库里的模型封装名一般是TLC5615,放在模拟IC或混合信号类别下。如果用的Proteus版本元件库太老找不到,可以改用TI的TLV5615模型,引脚定义和串行帧格式几乎一致,但必须在电路图上确认参考电压和输出的连接方式,不能直接替换后不做检查。整个仿真电路不需要额外接电平转换,51的5V逻辑电平正好匹配TLC5615的输入阈值。

4.2 示波器与频率计的接线和参数设置

把示波器的A通道接到RC滤波的输出端,地线接到系统GND,打开通道菜单把衰减设为1×,耦合选DC,时间基数设为200μs或500μs/格。频率计放在输出端,测量模式选Frequency,Gate Time选1s,读数稳定后再记录。Proteus默认仿真步长对DAC这种微秒级更新的器件来说偏大,会在示波器上看到失真甚至漏采的波形,推荐在菜单System → Simulation Options里把最大步长(Maximum Step Size)设为1μs,此时SPI的SCLK翻转和DAC输出更新都能被仿真内核捕获。

仿真跑起来以后,正确的现象是:示波器上出现以2.5V为中心的对称正弦波,峰峰值接近5V,频率计读数与理论值偏差在百分之几以内;如果看到锯齿状波形,多半是RC滤波截止频率设太高;如果看到直线或满幅方波,先检查REFIN是不是悬空。REFIN在Proteus里如果只标了网络名而没有实际连接电压源,DAC模型会输出零电平,这是个很隐蔽的仿真错误。

4.3 波形异常时按信号链路顺序排查

正弦信号发生器输出不正常,按“数据对不对、时序对不对、滤波对不对”三段排查法和这段代码所在链路匹配。数据层面先查程序里有没有把0x03FF掩码丢掉,字节变量存储10位码值时高2位被截断是常见错误,仿真里表现为输出波形在某几个采样点出现台阶错位。时序层面用示波器同时观察SCLK、DIN和CS三根线,CS拉低期间应当完整看到12个时钟周期,DIN的数据在SCLK上升沿前已经稳定,如果CS提前拉高,DAC只收到部分帧,输出会随机跳动。滤波层面看OUT引脚原始波形,如果OUT处阶梯清晰而滤波后幅度过小,用公式1/(2πRC)核算截止频率,RC乘积不要随意取。

提示:仿真和实物有一个重要差异,Proteus的TLC5615模型不会真实反映参考电压的温漂和电源纹波,所以仿真波形会比实板干净得多。仿真只能验证数字时序和整体链路正确性,滤波效果的优劣最终要以实测为准。

5. 进阶用法:改一个变量就能调频率的相位累加器

查表法改频率需要同时改定时器初值和查表点数,非常不灵活。做信号源频率要连续可调时,常见的做法是把线性查表改成DDS思路:用32位相位累加器,每个更新周期累加一个频率控制字,取高8位做256点正弦表的索引。这样改频率只改一个寄存器,查表点数、定时器更新率、滤波参数全都不用动。

// DDS式查表定义 unsigned long phase_acc = 0; // 32位相位累加器 unsigned long freq_word = 0; // 频率控制字 // 设置输出频率:f_out为目标频率,f_update为DAC更新率 // freq_word = f_out * 4294967296 / f_update void set_freq(unsigned int f_out, unsigned long f_update) { freq_word = (unsigned long)((unsigned long)f_out * 4294967296UL / f_update); } // 在定时器中断里替代原来的phase++方式 void timer0_isr(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xD9; phase_acc += freq_word; // 相位累加 tlc5615_write(sin_table[(unsigned char)(phase_acc >> 24)]); // 取高8位查表 }

高8位查表相当于把32位相位空间均分给256个采样点,频率控制字是整数运算,不会产生浮点误差。注意freq_word的计算结果在f_out远小于f_update时有可能被截断成0,输出频率低于0.01Hz量级时要用64位中间量或增大频率控制字的位宽。这套结构再往上扩展只需要在表里存1/4周期正弦值加上符号判断,能把表容量再压掉75%,但因为取高位索引已经用了8位,实际做半波镜像要改成9位索引并分离符号位,代码复杂度上升,对51这类算力有限的平台收益不算大。

验证时在Proteus里分别设置f_out为100Hz、1kHz两个档位,测量输出频率并对比理论值,同时观察相位累加器频率字改变前后波形是否连续。量化误差的检查方法是打开示波器的FFT模式看谐波分量:10位DAC的量化噪声理论信噪比约62dB,如果观察到明显的二次或三次谐波,先怀疑正弦表取整方式,把Python生成脚本的四舍五入改成截断再对比;如果频谱基线整体抬高,则要检查RC滤波的截止频率是否过于靠近信号频率。最后用示波器CURSOR功能量出波形峰峰值,确认幅度在0~5V范围内且偏移不超过一个LSB对应电压(约4.9mV),这个值就是DAC在实际电路里的绝对分辨率边界。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询