简介:基于51单片机与TLC5615芯片的D-A转换器应用资料包,面向单片机学习者与电子设计初学者,解决D/A转换编程与Proteus仿真验证问题。资料围绕TLC5615十位数模转换器展开,主函数中通过修改A_MONI变量即可改变输出电压,配合后三位数码管显示与电压表实测对比,帮助理解U=D*475/1023的换算关系及SPI时序控制。压缩包共17个文件,以C源码、Proteus仿真DSN、Hex文件、Keil工程及说明文档为主,其中.c与.hex提供可编译烧录的程序,.dsn/.dbk用于仿真电路,.txt为程序说明,另有备份及中间文件,整体仅47KB。已有313人学习,适合需要快速上手51单片机D/A转换并配合Proteus验证的读者。资源内含完整程序源码、仿真工程和注释清晰的驱动函数TLC5615,可直接修改数据观察输出电压变化,边看边动手验证,尤其适用于课程设计或实验环节的参考与二次开发。
1. 一块没有 DAC 的 51 板子,怎么精准输出 0 到 5V 电压
在这块实验板上,你只要改主函数里uint A_MONI = ????的初值,后三位数码管显示的电压就会跟着变,拿电压表去量板子右边下接插件的第 2 脚,读数基本一致。这个能力来自 TLC5615,一颗 10 位串行 D-A 转换器。51 单片机内部没有 DAC,想在不过度占用 I/O 的前提下输出可调模拟电压,挂它属于性价比较高的做法。配套的 Proteus 仿真工程把单片机、TLC5615、数码管和电压表连在一起,适合正在做课程设计的同学直接改参数看效果,也适合想搞清楚串行 D/A 时序的嵌入式工程师用来验证自己的驱动写法。
2. TLC5615 时序:10 位有效数据为什么要左移 6 位、发 12 个脉冲
2.1 串行 D/A 的选型逻辑
51 单片机输出不了模拟量,常见的模拟输出扩展方案有四类。PWM 加 RC 滤波电路最简单,但纹波大,精度受 RC 时间常数影响;R-2R 电阻网络需要一组精密电阻,温漂和阻值误差会直接体现在输出电压上;并口 DAC 如 DAC0832 速度快,可 8 个 I/O 起步,对引脚紧张的 51 系统不友好。TLC5615 走的是三线串行接口,DIN、SCLK、CS 三个引脚就能完成数据写入,10 位分辨率对应 1024 个电压台阶,在 5V 参考电压下每步约 5mV,对实验板上的电压输出场景足够了。Proteus 自带的 TLC5615 模型可以直接从元件库调出,仿真行为与实物基本一致。
2.2 数据帧结构:16 位变量里只发了 12 个时钟
TLC5615 的输入移位寄存器是 16 位宽度,但真正有效的只有 10 位 DAC 数据,写入时还需要跟着两个填充 0,所以完整数据帧是 12 位。看源码里的关键注释:“两个字节有十六位,去掉高六位剩下十位有效位”,这是理解整个驱动最核心的一句话。10 位数据存在一个uint的低 10 位里,直接发送会把多余的位也带进去,所以要先dat <<= 6,把 10 位数据推到 16 位变量的最高 10 位上,低 6 位清零,然后发送 12 个时钟周期,TLC5615 只采样高 10 位加后 2 位,其余部分自动忽略。
uint dat; // 16 位变量,低 10 位是 DAC 数值 dat <<= 6; // 高 6 位清零,10 位数据挪到 bit15~bit6 CLK = 0; CS = 0; // 片选拉低,开始一次写操作 for (i = 0; i < 12; i++) { DA = (bit)(dat & 0x8000); // 每次取最高位,送给 DIN CLK = 1; // 上升沿,TLC5615 采样 DIN dat <<= 1; // 下一位移动到最高位 CLK = 0; } CS = 1; // 片选拉高,锁存本次转换结果这段代码的节奏是:先片选有效,再在时钟上升沿前把数据准备好,上升沿到来时 TLC5615 锁存 DIN 状态。dat & 0x8000取出 bit15,(bit)强制转换成 0 或 1 赋给 DA 引脚,数据是高位先出。循环 12 次而不是 10 次,就是因为后面要多跟 2 个时钟的填充位。原工程注释里提到“亦可用 DA=CY 来代替”,指的是用移位产生的进位标志直接送 DA,省一次与运算,效果相同。
2.3 输出电压换算与引脚接法
TLC5615 的输出公式在数据手册里写作 Vout 与输入 D 值、参考电压 Vref 的乘积关系,而本工程注释直接给了一个更朴素的标定式:U = D × 475 / 1023。注意 475 不是芯片固有的物理常量,是实验板上实测出来的系数,含义是 D 取最大 1023 时输出电压约 4.75V,可能是参考电压跌落或分压造成的。仿真是理想模型,实际硬件按这个算也不会差太远。引脚接法上,DIN 接 P1.0,SCLK 接 P1.1,CS 接 P1.2,模拟输出 OUT 接到板子右边下接插件第 2 脚,电压表并联测量。
| 引脚 | 功能 | 本工程接法 |
|---|---|---|
| DIN | 串行数据输入 | P1.0 |
| SCLK | 串行时钟输入 | P1.1 |
| CS | 片选,低有效 | P1.2 |
| OUT | 模拟电压输出 | 右侧插件第 2 脚 |
| Vref | 参考电压输入 | 接 VDD |
| DOUT | 级联数据输出 | 单芯片悬空 |
3. 5616.c 驱动源码拆解:从dat <<= 6到数码管动态刷新
3.1 完整驱动函数与主循环
工程里 5616.c 的核心是 TLC5615 写入函数和主函数里的 A_MONI 变量。把这个函数放到 reg52.h 头文件后面,配合 P1 口的三个位定义就能工作。驱动函数里dat <<= 6是整个时序的起点,决定了一次写入是 12 个还是 10 个时钟,也是新手最容易漏掉的一步。
#include <reg52.h> #define uint unsigned int #define uchar unsigned char sbit DIN = P1^0; // 串行数据 sbit CLK = P1^1; // 串行时钟 sbit CS = P1^2; // 片选,低有效 void TLC5615(uint dat) { uchar i; dat <<= 6; // 10 位数据移到高 10 位 CLK = 0; CS = 0; // 片选拉低,开始传输 for (i = 0; i < 12; i++) { DIN = (bit)(dat & 0x8000); // 最高位送出 CLK = 1; // 上升沿采样 dat <<= 1; CLK = 0; } CS = 1; // 片选拉高,DAC 输出更新 }DIN 要写在 CLK=1 之前,因为 TLC5615 在时钟上升沿读取数据线状态,数据必须提前建立。CLK = 0放在循环末尾,是让时钟恢复低电平,为下一个上升沿做准备。CS = 1放在循环之后,表示一帧数据送完,TLC5615 内部锁存并更新模拟输出。12 次循环结束后低 6 位已经全部被移出,CS 拉高的时机并不影响已移出的数据。
3.2 主函数里怎么改 A_MONI
主函数结构很直接:循环里先调用 TLC5615 送出 D 值,再把 D 值换算成电压送到数码管显示。A_MONI 的取值范围是 0 到 1023,超过 1023 的部分 TLC5615 根本不认,因为它的 DAC 只有 10 位。
void main() { uint A_MONI = 512; // 修改这里,可以送出不同的电压 uint volt; while (1) { TLC5615(A_MONI); volt = (uint)((A_MONI * 475L) / 1023); // 电压值放大 100 倍 show_volt(volt); // 后三位数码管显示 } }A_MONI 是 10 位 DAC 输入值,volt 是换算后的电压值放大 100 倍。比如 A_MONI = 512,512 * 475 / 1023约等于 238,数码管显示 2.38,表示 2.38V。475L里的 L 是强制把乘法按 long 计算,避免 uint 溢出,这一行如果直接写A_MONI * 475,16 位乘法结果会溢出,显示值就会乱。想输出指定电压,D 值按D = U * 1023 / 475反算,2.50V 对应约 538,直接把 538 填给 A_MONI 即可。
| A_MONI 取值 | 数码管显示 | 电压表预期读数 |
|---|---|---|
| 0 | 0.00 | 约 0V |
| 256 | 1.19 | 约 1.19V |
| 512 | 2.38 | 约 2.38V |
| 768 | 3.56 | 约 3.56V |
| 1023 | 4.75 | 约 4.75V |
3.3 后三位数码管的动态扫描实现
动态扫描的原理是让三只数码管轮流点亮,每只点亮 2 到 3 毫秒,利用人眼视觉暂留形成同时点亮的效果。段码表要跟数码管的共阴共阳匹配,本工程用的是共阴数码管,段码表以 0x3f 开头,对应数字 0。
sbit DU = P2^6; // 段选锁存 sbit WE = P2^7; // 位选锁存 uchar code seg[] = {0x3f,0x06,0x5b,0x4f,0x66, 0x6d,0x7d,0x07,0x7f,0x6f}; // 共阴 0-9 void show_volt(uint v) { uchar b, s, g; b = v / 100; // 整数位 s = v % 100 / 10; // 小数点后第一位 g = v % 10; // 小数点后第二位 P0 = 0xfe; WE = 1; WE = 0; P0 = seg[b] | 0x80; DU = 1; DU = 0; // 整数位带小数点 delay(2); P0 = 0xfd; WE = 1; WE = 0; P0 = seg[s]; DU = 1; DU = 0; delay(2); P0 = 0xfb; WE = 1; WE = 0; P0 = seg[g]; DU = 1; DU = 0; delay(2); }v / 100取出百位,v % 100 / 10取出十位,v % 10取出个位,正好对应 x.xx 的三位显示。seg[b] | 0x80是把段码最高位置 1,点亮小数点位,这样显示 2.38 而不是 238。P0 口先送位选再送段选,锁存器隔离了两路信号,这是实验板最常见的数码管接法,如果你的硬件把位选段选接到其他端口,改对应 sbit 定义即可。
4. Proteus 仿真:加载 DSN、挂电压表、改 A_MONI 看对比
4.1 压缩包里文件结构与打开方式
解压后有两个目录:仿真文件里是DA转换仿真.DSN和DA转换仿真.PWI,程序包里是 Keil 工程DA应用.Uv2、源码5616.c、编译产物5616.hex。.DSN是 Proteus 的主电路文件,双击就能打开完整仿真;.PWI是工程附属文件,记录窗口布局和上一次运行状态;Last Loaded DA转换仿真.DBK是 Proteus 崩溃时留下的备份,如果 DSN 文件损坏打不开,把 DBK 扩展名改成 DSN 再试。
仿真文件/ DA转换仿真.DSN DA转换仿真.PWI Last Loaded DA转换仿真.DBK 程序包/ 5616.c 5616.hex DA应用.Uv2Proteus 8 Professional 直接双击 DSN 文件就能打开,打开后如果提示版本升级,点是即可。老版本用户遇到打不开的情况,优先检查 DSK 备份恢复。打开后你会看到 51 单片机、TLC5615、三只数码管、右侧插件和一个电压表,整个电路已经连线完毕,不需要额外焊接。
4.2 给单片机加载 5616.hex
仿真运行前必须给单片机加载程序,否则按下运行键什么都不会发生。双击原理图中的单片机芯片,弹出属性对话框,在 Program File 一栏找到程序包里的5616.hex,Crystal Frequency 一栏要填和 Keil 工程一致的晶振频率。这个工程源码里没有写晶振宏定义,默认 12MHz 或 11.0592MHz 都行,只要 Proteus 和 Keil 保持一致,因为 51 的串口、定时器计时都依赖这个频率。
// 改 A_MONI 之前,先用这个宏换算目标电压对应的 D 值 #define D_FROM_V(v) ((uint)((v) * 1023.0 / 475.0)) // 例:要输出 2.50V -> D = 2.50 * 1023 / 475 ≈ 538 uint A_MONI = 538;如果双击单片机没有 Program File 选项,说明 Proteus 元件库里当前选的是纯逻辑器件,右键点单片机,Pick Device 换成 AT89C51 或 AT89C52,STC 系列在 Proteus 里一般用 AT89C52 代替仿真。加载完成后点左下角绿色三角运行,电压表应该有读数。
4.3 电压表测量流程与误差判断
仿真运行后,电压表并联在右侧下接插件的第 2 脚和 GND 之间,注意是并联不是串联。数码管显示的是程序算出来的理论电压,电压表显示的是电路实际输出,两者对比可以验证驱动时序是否正常。按下表记录几组数据,误差在 ±0.05V 以内都算正常。
| A_MONI 取值 | 数码管显示值 | 电压表读数 | 误差 |
|---|---|---|---|
| 538 | 2.50 | 2.47 | -0.03V |
| 768 | 3.56 | 3.53 | -0.03V |
| 1023 | 4.75 | 4.72 | -0.03V |
这个误差主要来自参考电压 Vref 不是理想 5V,以及 TLC5615 输出级在接近满量程时有一点饱和压降。仿真是理想模型,误差会小一些;真实硬件上误差会更大,但只要数码管显示和电压表读数在同一数量级、电压跟随 A_MONI 单调变化,驱动时序就是对的。
4.4 仿真里最常见的几个坑
数码管乱码或全亮,先查段码表是共阴还是共阳,0x3f 开头是共阴,共阳要用 0xc0 开头那套表。电压表始终为零,检查三根控制线是不是接对引脚,DIN、CLK、CS 接反了数据根本进不去。单片机没跑起来,确认 hex 文件已加载且晶振频率没填成 0。TLC5615 的 VDD 必须接 5V,电源引脚漏接会导致 OUT 无输出。
提示:项目注释里说的“右边下接插件 2 脚”,在仿真图上找到丝印标注为 2 的那个引脚,电压表红笔接它,黑笔接 GND。分不清就沿着 TLC5615 的 OUT 引脚走线,看它最终连到哪个网络标号。
5. 进阶:重新标定系数、64 点正弦波查表与示波器验证
5.1 换一块板子怎么重新标定 475 这个系数
475 是这块实验板的实测系数,换到另一块板子上,参考电压 Vref 的跌落程度不同,满量程可能不是 4.75V。标定方法很简单:先把 A_MONI 改成 1023 满量程输出,用万用表量 OUT 引脚的实际电压,比如量到 4.83V,就把主函数里的 475 改成 483。这样 U = D × K / 1023 在满量程点就完全对齐了,中间点的线性度由 TLC5615 本身保证。
uint K = 483; // 实测满量程 4.83V,替换原来的 475 volt = (uint)((A_MONI * K) / 1023);5.2 用查表法让 TLC5615 输出正弦波
数码管显示电压只是静态用法,TLC5615 真正的价值在于能快速更新模拟输出,配合查表可以生成波形。把正弦函数的一个周期按 64 个采样点离散化,幅度映射到 0 到 255,再乘 4 扩展成 10 位 D 值,就能在 OUT 引脚得到阶梯状正弦波。
// 64 点正弦波表,幅度 0~255,中心 128 uchar code sine_table[64] = { 128,141,153,165,177,189,199,208, 216,223,228,232,235,236,236,234, 231,227,221,214,206,198,188,178, 167,156,145,134,123,112,102, 92, 83, 75, 68, 62, 57, 54, 52, 51, 51, 53, 56, 60, 65, 72, 79, 88, 97,107,117,127,137,147,156,164, 171,177,182,186,188,190,190,189 }; void main() { uchar i; while (1) { for (i = 0; i < 64; i++) { TLC5615(sine_table[i] * 4); // 8 位扩展到 10 位 delay(2); } } }sine_table[i] * 4把 8 位正弦表映射到 0 到 1020,TLC5615 的 10 位分辨率正好吃满这个范围。delay 的值决定输出频率,64 个点总共延时 128 个 delay(2) 周期,想调快 10 倍就把 delay(2) 改成 delay(1) 再微调,把示波器探头接到 OUT 引脚,能看到锯齿状的波形逐步逼近正弦。
5.3 从仿真搬到真实硬件前的验证顺序
先别急着焊板,在仿真里把三种波形都跑通:直流电压、手动改 A_MONI 的跳变电压、查表正弦波。示波器观察正弦波时,把采样点从 32 加到 64,台阶变密,波形毛刺明显减少,这就是 D-A 转换器输出质量最直观的判断方式。最后最后,如果要做实物,强调一件最容易被忽略的事:先拔掉输出负载,用万用表量 OUT 对 GND 电压,如果与数码管显示差 0.2V 以上,多半是 Vref 被拉低或电源走线压降,逐个排除比改软件更快。
本文还有配套的精品资源,点击获取