简介:一套基于51单片机与TLC5615的正弦信号发生器设计资料,面向电子、嵌入式方向的初学者及电子设计竞赛学生,帮助理解单片机控制DAC生成波形的完整流程。压缩包内含16个文件,涵盖C语言源代码、Proteus仿真DSN工程与DBK备份、HEX烧录文件、Keil工程配置等类型,整体仅72KB,精简但资料链完整。目前已有3619人学习下载,适用于希望以最小成本快速上手单片机模拟输出的读者。资源将电路原理图、控制源码与仿真验证集中呈现,可对照学习单片机串行通信、TLC5615的12位DAC转换原理、正弦波查表生成及输出滤波设计;通过Proteus直观查看波形输出,能有效排查硬件连接与代码逻辑问题。对想通过一个完整案例串联51编程、数模转换和电路仿真技能的初学者,是很有价值的迷你实践项目。
1. 用51单片机+TLC5615做正弦信号发生器:先搞清这套方案要解决什么
实验室里临时要一路几十赫兹到几百赫兹的正弦信号,用来驱动传感器线圈或者给音频电路做扫频测试,手边没有信号发生器,翻元件盒却找出了几片AT89C52和TLC5615。于是就用最直接的查表法:把正弦波按等间隔采成128个点存进单片机ROM,每隔固定时间取出一个点送给DAC,输出端用运放缓冲再接一个低通滤波,一条正弦波就这么搭出来了。TLC5615是10位串行DAC,只占三根IO,波形离散精度对多数演示和低频测量都够用。对刚做完51课程设计、想把手头单片机真正用起来的人来说,这是一个性价比很高的组合。下文把原理、电路图、源代码、Proteus仿真串成一条完整链路,照着搭就能自己看到波形。
2. TLC5615的工作原理与正弦波查表方法:先搭对理论再动手
2.1 TLC5615的引脚、输出公式与串行时序
TLC5615是TI公司的10位电压输出DAC,采用三线SPI兼容接口。选它而不是并行DAC,原因很实际:DAC0832这类并行芯片要占8根甚至12根数据线,51单片机的P0、P1口基本被吃光,还得额外加拉锁存器;TLC5615只要DIN、SCLK、CS三根线,剩下IO还能接按键、LCD和编码器。对于IO本来就紧张的51单片机来说,这是第一位的选型理由。
引脚功能集中在DIN、SCLK、CS、REF、VOUT以及AGND、DGND上。DIN是串行数据输入,SCLK上升沿把数据一位一位移入寄存器,CS拉低后开始一次写入,拉高时DAC才把新数据锁存到输出端。REF是基准电压输入,它直接决定满幅输出范围。VOUT是模拟输出脚,不能直接带重负载,后面必须接运放缓冲。
| 引脚 | 名称 | 作用 | 接线要点 |
|---|---|---|---|
| 1 | DIN | 串行数据输入 | 接51的P3.5,数据高位在前 |
| 2 | SCLK | 串行时钟 | 接51的P3.4,空闲时为低 |
| 3 | CS | 片选 | 接51的P3.6,低电平有效 |
| 4 | REF | 基准电压 | 接2.5V基准或电阻分压 |
| 5 | VOUT | 模拟输出 | 接运放同相输入端 |
| 6/7 | AGND/DGND | 模拟地与数字地 | 建议单点接地 |
| 8 | VDD | 电源 | 4.5V到5.5V,就近放0.1μF电容 |
输出公式是固定的增益2结构:VOUT = VREF × D / 1024 × 2。当REF接2.5V时,数字量0对应0V,数字量1023对应5V,中间每一步约4.88mV。如果直接把5V接到REF,满幅输出是10V,运放和电源轨全部饱和,所以REF不能简单接VCC。
串行帧是最容易写错的地方。TLC5615每次完整写入是16位:前4位是填充位,可以写0;中间10位是有效DAC数据;后2位必须为0。数据从最高位开始逐位移入。网上很多代码只发12位或者忘记低2位补零,结果要么完全没有输出,要么波形幅度和相位全乱。后面第4章源码会给出严格按16位帧实现的模拟SPI程序。
2.2 查表法生成正弦波:512+511×sin()是怎么算出来的
正弦波是连续信号,单片机只能在离散时刻给出离散电平,因此要先对周期采样。把一个完整正弦周期均匀切成N份,每份的幅度用10位DAC能表示的数字量来近似:
value_i = 512 + 511 × sin(2πi/N),i = 0, 1, ..., N-1
这里512是零点。因为DAC的数字量0对应0V、1023对应5V,满幅5V正弦波的中心是2.5V,对应数字量512,所以偏移量取512。振幅部分取511而不是512,是为了避免峰值超过1023,因为1024超出了10位表示范围,会出现削顶或卷绕。如果你把表格算成512+512×sin,结果在峰顶变成1024,输出瞬间翻到0附近,波形直接撕裂。
N取多少合适?128点是常见选择:点数越多波形越接近理想正弦,但同样的目标频率需要更高的DAC刷新率;点数太少,比如16点,输出看起来就像带圆角的锯齿波。做Proteus演示和低频信号源,128点是一个兼顾精度与速度的起点。生成这张表不需要手算,后面用一段Python代码就能一次性输出全部数组值。
2.3 波形频率的计算与定时器初值的选取
定时器中断每隔t秒取出一个点送给DAC,正弦表有N个点,输出波形一个周期需要N×t秒,所以输出频率为:
f_out = 1 / (N × t)
这是一切频率设计的出发点。晶振12MHz时,标准12T的51单片机机器周期是1μs,定时器工作方式1是16位计数器,溢出间隔为t = (65536 - 初值) × 1μs。想要128点、40Hz附近的输出,t = 1/(128×40) = 195.3μs,定时器初值大约为65536 - 195 = 65341,十六进制就是0xFF3D,实际取0xFF38时输出约39Hz。
但有一个非常现实的限制:中断服务程序内部要完成查表、16位模拟SPI发送,这部分在12MHz的51上单帧就要消耗100μs以上。如果中断间隔小于200μs,波形会出现周期性停顿,表现为频率上不去或者输出像被打断的阶梯。所以标准12T单片机加模拟SPI时,这套方案实际适合工作在几十赫兹到一百多赫兹。想要更高频率,要么提高晶振到24MHz,要么换1T内核,这些放到第6章展开。
3. 电路原理图设计:51单片机与TLC5615的接线、基准电压与运放输出
3.1 原理图整体结构:最小系统、DAC、运放三段式
原理图按三段来画思路最清晰:51最小系统、TLC5615转换电路、运放输出缓冲。TLC5615的DIN、SCLK、CS分别接到51的P3.5、P3.4、P3.6。为什么选P3口而不是P1口?因为部分STC51型号的P3.4、P3.5对应硬件SPI引脚,以后升级到带硬件SPI的单片机时,改动的代码量最少;P3.6做片选也是很多Proteus仿真教程的默认习惯,照这个连法查资料最省事。
最小系统这部分和“51单片机点亮一个LED灯”的基础电路没有本质区别:晶振两端各接一个20~30pF负载电容,复位电路用10μF电容加10kΩ电阻。Proteus仿真里晶振频率设置为12MHz,复位电路画不画都不影响仿真结果,但实板上缺了它程序根本跑不起来,所以原理图里必须保留。
DAC输出端要接电压跟随器。TLC5615的VOUT内部等效输出阻抗不低,直接接示波器或后级负载时,负载电流会引起输出电压跌落,正弦波幅度失真。常见做法是接一个LM358或TL072,同相输入端接VOUT,反相输入端直接连输出端,输出变成低阻抗的干净信号。Proteus元件库里LM358可以搜到,单电源5V供电即可工作;TL072需要双电源或单电源抬高参考点,LM358在这个低频场景里更省事。
3.2 基准电压REF怎么给:分压电阻或TL431,别直接接电源
REF引脚决定满幅输出,这是原理图里最容易连错的地方。直接把5V VCC接到REF后,输出公式变成VOUT = 5 × D / 1024 × 2,满幅10V,远超出5V运放的输出范围,正弦波顶部会被硬生生削平。因此REF必须接一个低于供电电压的基准,常见取2.5V。
最简单的做法是两个10kΩ电阻把5V分压成2.5V后接REF,Proteus里也可以直接放一个2.5V的DC电压源。电阻分压胜在便宜,但REF会抽取一定电流,分压点电压会略低于理论值,而且电源纹波会直接串进DAC输出,示波器上能看到正弦波表面附着毛刺。讲究一点就用TL431搭2.5V基准,或者用专门的REF5025这类基准芯片。调试阶段建议先用理想2.5V电压源验证代码,确认波形正确后再换分压方案看精度差异。
验证REF和DAC是否匹配有一个很有效的静态方法:临时把正弦表改成固定输出数字量512,也就是写入中值,运放输出端理论上应该等于REF电压。如果测出来不对,问题基本出在REF接法或运放跟随器接错,不必怀疑程序。
3.3 输出缓冲与电源去耦:TLC5615输出不能直接带重负载
TLC5615虽然功耗不大,但数字地与模拟地在芯片内部是分开的,原理图上要遵循单点接地原则:AGND和DGND先汇合到芯片附近的一点,再统一连接到电源地平面,避免数字开关噪声通过地线串进模拟输出。
去耦电容的摆放顺序是:DAC电源脚就近放0.1μF陶瓷电容,运放电源脚同样放0.1μF,电源入口处再放一个10μF电解电容吸收低频波动。Proteus仿真环境本身是理想化的,不加这些电容也能通过,但实板上缺少去耦电容时,输出正弦波上会看到高频毛刺,频率越高越明显,所以原理图里必须预留位置。
还要注意不要直接把滤波电容并联在运放输出端。运放输出直接接容性负载会引起振铃,波形边缘出现衰减振荡。正确的滤波方式是在运放输出之后串电阻再接电容到地,构成RC低通,这部分在第6章会给出具体参数计算。
4. C语言源代码实现:查表驱动TLC5615生成正弦波的最小工程
4.1 完整C51源代码:正弦表、模拟SPI、定时器中断
最小工程只需要一个main.c,在Keil C51里新建工程、选择AT89C52、勾选Output选项卡里的Create HEX File,然后把下面代码编译出HEX文件,Proteus里直接加载。正弦表部分用Python生成后粘贴,代码里不要相信手算出来的128个浮点数值。
#include <reg52.h> #include <math.h> #define POINTS 128 #define TIMER_H 0xFF #define TIMER_L 0x38 sbit TLC_SCLK = P3^4; sbit TLC_DIN = P3^5; sbit TLC_CS = P3^6; /* 由右边的Python语句生成后粘贴替换 */ /* python -c "import math; print(', '.join(str(round(512+511*math.sin(2*math.pi*i/128))) for i in range(128)))" */ unsigned int code sin_table[POINTS] = { /* 数组太长,实际工程中从生成命令输出复制 */ }; void TLC5615_Write(unsigned int dat); void main(void) { TMOD = 0x01; /* 定时器0,16位定时方式1 */ TH0 = TIMER_H; TL0 = TIMER_L; ET0 = 1; /* 打开定时器0中断 */ EA = 1; /* 打开总中断 */ TR0 = 1; /* 启动定时器 */ while (1) { /* 主循环空跑,波形输出全部交给中断 */ } } void Timer0_ISR(void) interrupt 1 { static unsigned char idx = 0; TH0 = TIMER_H; /* 重装初始值,保证间隔稳定 */ TL0 = TIMER_L; TLC5615_Write(sin_table[idx]); idx++; if (idx >= POINTS) idx = 0; } void TLC5615_Write(unsigned int dat) { unsigned char i; unsigned int frame; frame = (dat & 0x03FF) << 2; /* 10位数据挪到bit11-bit2,低2位补0 */ TLC_CS = 0; for (i = 0; i < 16; i++) { TLC_SCLK = 0; if (frame & 0x8000) TLC_DIN = 1; else TLC_DIN = 0; frame <<= 1; TLC_SCLK = 1; /* 上升沿到来,数据被移入芯片 */ } TLC_CS = 1; /* 拉高片选,DAC输出更新 */ }正弦表用下面这行Python生成,运行结果直接粘贴到sin_table数组花括号里:
import math vals = [round(512 + 511 * math.sin(2 * math.pi * i / 128)) for i in range(128)] print(", ".join(str(v) for v in vals))TLC5615_Write是整套代码的核心。frame = (dat & 0x03FF) << 2先把10位有效数据放到16位帧的bit11到bit2,高4位和低2位全部补0,正好对应TLC5615的16位串行帧结构。发送顺序从MSB开始,循环里检测frame & 0x8000就是判断当前最高位,每次左移一位把下一位顶到最高。外层TLC_CS = 0在16个时钟前拉低,最后一个上升沿之后TLC_CS = 1,DAC输出才更新,这个时序不能反。
4.2 关键参数说明:点数、初值、幅值的修改位置
如果要换输出频率,只需要改TIMER_H和TIMER_L。计算步骤是:先确定目标频率f和点数N,算出中断间隔t = 1/(f×N),再算定时器初值 = 65536 - t/机器周期。12MHz晶振、12T单片机时机器周期为1μs,所以初值直接等于65536减去以μs为单位的t。下面表格列出的三组常用配置都基于128点正弦表:
| 目标频率 | 中断间隔 | 定时器初值 | 12MHz标准51能否运行 |
|---|---|---|---|
| 19.5Hz | 400μs | 0xFE70 | 可以,波形干净 |
| 39Hz | 200μs | 0xFF38 | 可以,模拟SPI刚好来得及 |
| 78Hz | 100μs | 0xFF9C | 很紧张,需24MHz或1T内核 |
幅值的修改有三个途径。第一是改表生成公式中的振幅系数511,但要注意实际上限仍是1023;第二是改REF电压,波形幅度线性跟随REF变化,最推荐;第三是在运放输出端加可调电阻分压,实板和仿真都有效。第一种会动到整张表,建议只在重新生成数组时处理。
4.3 模拟SPI的三个坑:帧长度、SCLK极性和CS时序
先说帧长度。TLC5615写入必须是完整16位,不是12位也不是8位。少发几位,DAC拿到的有效数据会整体错位,输出的正弦波幅度明显偏小或出现直流偏移;多发几位,多余的时钟会继续移入寄存器,把最后的数据挤出去。代码里循环固定执行16次,这是最稳妥的写死方式,不要试图用while加计数器偷懒。
第二个坑是SCLK极性。TLC5615要求空闲时SCLK为低,数据在SCLK上升沿被移入,是标准的SPI模式0。如果从网上复制的代码约定空闲为高,数据会在下降沿采样,结果是所有位都错开一拍,波形看起来完全不对。调试时用示波器看SCLK和DIN的相对关系,上升沿时刻DIN必须是稳定的有效电平。
第三个坑是CS时序。CS在16位发送过程中必须保持低,不能中途拉高,否则内部移位寄存器被提前锁存,输出更新到一半的数据。每次发送结束再拉高CS,这时DAC才真正更新输出。另外复位后进入main函数之前,最好先把CS引脚置为高电平,避免上电瞬间TLC5615处于不确定的锁存状态,这一点Proteus偶尔会出现,实板更容易遇到。
5. Proteus仿真电路搭建与波形验证:从编译到示波器出波形
5.1 Proteus元件库添加与电路连线步骤
Proteus 8 Professional新建工程后,从左侧元件模式点“P”打开元件库,搜索关键词AT89C52,放置单片机。TLC5615需要在元件库里搜“TLC5615”,通常位于Data Converters分类下,封装为DIP8。如果搜索不到,检查Proteus版本或者把过滤器切换到全部类别再试,主流版本元件库里都有这个器件。
连线顺序按电路图结构来:先把单片机晶振频率在属性面板改成12MHz,然后放置TLC5615,把DIN、SCLK、CS三脚分别连到P3.5、P3.4、P3.6。接着放置LM358,同相输入接TLC5615的VOUT,反相输入端和输出端短接。REF引脚放一个2.5V的DC电压源,或者用两个电阻从5V分压。最后把所有地连到同一个网络,电源电压统一设为5V。
连线完成后不要急着仿真,先检查网络连接状态。Proteus里连好线后引脚附近会出现绿色小方块,表示网络连接成功;如果出现悬浮的红色或黄色标记,说明引脚没接到有效网络。特别是AGND和DGND,很多新手只接了数字地,模拟地悬空,结果VOUT输出异常。
5.2 用Proteus虚拟示波器看波形并判断失真
Proteus的虚拟示波器在左侧“Virtual Instruments”菜单里,图标是一个带波形的示波器面板。拖到原理图上,把运放输出接到A通道,示波器地线接电路地。双击示波器打开面板,耦合方式选DC,垂直方向每格调到1V左右,水平方向先设成20ms/格,运行仿真后应该能看到规则的正弦波。
如果看到的是明显阶梯状而不是平滑圆弧,先区分两种情况。在TLC5615的VOUT直接测波形,可以看到一个点一个点的保持台阶,这是DAC的采样保持特性,属正常现象。运放输出端如果依然全是台阶,说明缺少低通滤波,波形没有经过平滑;如果台阶幅度忽大忽小,多半是定时器中断间隔和模拟SPI发送时间冲突,把中断间隔调大到200μs以上再观察。
Proteus里判断频率对不对很简单:数示波器一格内有多少个完整周期。200μs中断、128点表时,一个周期是25.6ms,也就是20ms/格下约1.28格一个周期。如果实测周期和理论计算偏差超过5%,优先检查晶振频率是否真的设置为12MHz,而不是看默认的1MHz。
5.3 仿真不动、频率不对、DAC没输出三个常见问题
仿真完全不动时,第一嫌疑是HEX文件没加载。Keil工程必须勾选“Create HEX File”重新编译,编译成功后Output窗口里会显示HEX文件路径;然后在Proteus里双击AT89C52,在Program File栏指定这个HEX文件。注意路径不要含中文和空格,否则Proteus偶尔加载失败。
频率不对基本上就是定时器初值取整问题。例如想要50Hz,200μs?让我准确算:50Hz、128点,周期20ms,中断间隔156.25μs,初值为65536-156=65380,十六进制0xFF64,实际频率是50.08Hz,误差不到0.2%;但如果想要100Hz,中断间隔78.125μs,初值取0xFFB1或0xFFB2,对应频率分别是99.6Hz或104Hz左右,取整跳变会明显一些。这种场景建议改DDS累加器方案,连续变频更平滑,第6章会写。
DAC没输出时按这个顺序排查:先用电压探针量REF脚,确认2.5V存在;再量TLC5615的VOUT,看是否有固定直流电压;如果VOUT始终是0V,代码里的CS或SCLK初始化有问题;如果VOUT有直流但不变化,说明串行数据没有正确写入。把TLC5615_Write里的循环次数从16改成24,如果波形乱了但位置偏移,就能侧面确认原来是帧长度不匹配。
6. 进阶技巧:用低通滤波压失真,用DDS思想实现连续变频
6.1 输出端加二阶低通滤波,把阶梯感压下去
DAC输出的原始信号是阶梯波,幅度谱在采样频率处有一簇谐波。采样频率就是中断频率,128点表、200μs中断时采样频率为5kHz,而正弦波只有39Hz,两者隔了很远,非常适合用简单RC低通把台阶抹平。
一阶RC低通的截止频率fc = 1/(2πRC)。以39Hz输出为例,采样台阶在5kHz,把截止频率设在500Hz左右,正弦基波完全不受影响,而5kHz处已有明显衰减。一种常用取值是R=3.3kΩ、C=100nF,fc约482Hz;想衰减更大就再做一级,两级之间用1kΩ内阻的运放隔开,避免两级互相影响。Proteus里直接放两个电阻两个电容串联就能看到效果,VOUT处的台阶在滤波后变成光滑圆弧。
一阶RC在5kHz处只衰减约20dB,阶梯的残余还能看到一点;二阶RC衰减约40dB,肉眼基本分辨不出台阶。注意电容不要并连在运放反馈环内,滤波必须放在运放输出之后,否则运放会因容性负载产生振铃。
6.2 用相位累加器实现连续可调频率
固定间隔加查表索引累加的方式,改频率时必须重新计算定时器初值,每次改动都会带来相位跳变,而且定时器初值取整会引入频率误差。更好的做法是把相位用32位变量维护,中断间隔固定不变,每次中断只做加法:
static unsigned long phase_acc = 0; unsigned long step = 0; void Timer0_ISR(void) interrupt 1 { TH0 = TIMER_H; TL0 = TIMER_L; phase_acc += step; /* 32位相位,取高7位作为128点表的索引 */ idx = (unsigned char)((phase_acc >> 25) & 0x7F); TLC5615_Write(sin_table[idx]); }32位相位累加器里,2^32对应一个完整正弦周期。step与目标频率的关系是:step = f_target × 2^32 × t,其中t是固定中断间隔。比如t=200μs时,想要39Hz,step = 39 × 4294967296 × 0.0002 ≈ 33500700,十六进制约为0x01FF2ABC。改频率只需要改step这一个变量,中断间隔完全不用动,相位连续性也得到保证,输出不会出现周期性突跳。这是DDS的直接相位累加思想,TLC5615的10位分辨率恰好够用,查表索引精度和DAC量化误差都不会成为瓶颈。
6.3 频率上限的边界在哪里
标准12T的51单片机在12MHz晶振下,模拟SPI发一帧16位数据需要上百微秒,这直接锁死了输出频率上限。128点表时,中断间隔最小也约100μs,对应输出频率约78Hz,再往下压缩中断间隔,波形就支离破碎了。实板想要几百Hz的正弦波,常见方案有:换STC8G这类1T内核单片机,机器周期缩短12倍,同样代码能跑到几百赫兹;或者用带硬件SPI的单片机,SCLK由外设自动产生,中断服务里只写一个字节,发送时间降到几十微秒以内;再或者把正弦表从128点降到32点,频率上限提高4倍,但谐波明显增大。
如果目标是1kHz以上,TLC5615本身能工作,瓶颈全在单片机侧。实际项目里要做更高频信号,不如直接换DDS专用芯片,TLC5615方案的优势区间就是几十赫兹到几百赫兹的低频信号源,电路简单、代码直观、Proteus验证方便。把这套边界记清楚,就不会出现“程序看起来对、示波器波形却乱掉”的尴尬。
本文还有配套的精品资源,点击获取