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

TLC5615 DAC驱动实战:SPI时序与三大平台代码详解

TLC5615 DAC驱动实战:SPI时序与三大平台代码详解 ★ FEATURED ARTICLE
1. TLC5615芯片特性与通信原理1.1 芯片基本参数TLC5615是TI德州仪器出品的一款10位电压输出型DAC芯片采用SPI串行接口通信最大输出电压可以达到参考电压的两倍。这里有个容易搞混的点10位分辨率意味着满量程输出为2的10次方也就是1024个台阶但它的输出范围是0到2倍VREF参考电压而不是常见的0到VREF这点很多刚入门的同学会踩坑。具体参数方面TLC5615支持3V到5.5V的电源电压范围典型功耗极低只有几毫瓦级别非常适合电池供电的场景。它的参考电压VREF决定输出电压的上限当VREF为2.5V时最大输出为5V。数据更新速率最快可到1.25MHz的时钟频率这意味着对于一个10位的命令字理论上每秒可以更新上百万次实际在单片机系统中完全够用。芯片封装方面常见的TLC5615CP是DIP8封装引脚间距2.54mm洞洞板焊接和面包板插接都很方便。引脚定义依次为DIN数据输入、SCLK时钟、CS片选、DOUT菊花链级联输出、AGND模拟地、REFIN参考电压输入、VOUT模拟电压输出、VDD电源正极。这个DOUT引脚是它的特色功能支持多片级联通过CS控制可以实现多通道扩展下一篇进阶内容我会专门聊聊级联设计。1.2 SPI通信协议深度拆解TLC5615的通信协议本质上是一个非常标准的SPI写操作12位数据一次性移入其中前4位是控制/填充位高阻态或前导零后10位才是真正的DAC数据。但这里有个细节实际上芯片只需要10位数据但为了配合菊花链结构协议规定必须发送完整的12位帧。具体时序是这样的CS拉低开始一次数据传输时钟上升沿数据从DIN一位一位移入MSB在前12位数据全部移入后CS拉高数据锁存到DAC寄存器输出更新DOUT引脚会在锁存的前一位输出数据用于级联时把数据传给下一片。也就是说每片TLC5615需要12个时钟周期完成一次更新。如果你用的是16位SPI发送高四位必须置零低12位有效数据。用Arduino的SPI库默认是8位模式所以通常要发送两字节——第一个字节的高4位填充0低4位是DAC数据的高4位第二个字节是DAC数据的低8位。这条经验我最初是在51上用手写时序调通的后来转到STM32发现HAL库默认8位模式也一样处理逻辑完全通用。1.3 参考电压和输出范围的对应关系TLC5615的输出公式是VOUT 2 × VREF × DAC值 / 1024。这个2倍关系来自芯片内部的跟随放大结构很多初次使用的人会疑惑为什么输出超过参考电压实际上是芯片设计就是这样。我列一个实际对应表方便大家调电压的时候心中有数DAC值十进制VREF2.5V时输出VVREF3.3V时输出VVREF5V时输出V00002561.251.652.55122.53.35.010234.9956.5949.98其中1024个量化级从0到1023输出最小步进为 2×VREF/1024。VREF为2.5V时步进约4.88mV为5V时步进约9.77mV。要输出指定电压其实就是反推这个公式DAC值 目标电压 × 1024 / (2 × VREF)然后取整写入寄存器。很多教程直接给代码不解释这个换算过程导致换一个参考电压源输出就跑偏这里一定要自己动手算一遍。2. 硬件连接与电源处理这是最容易翻车的环节2.1 与三大平台的接线对照无论你用的是51、Arduino还是STM32TLC5615的接线思路都是一模一样的区别只在于具体的GPIO引脚编号。核心就是四根线DIN、SCLK、CS、VOUT再加上VDD、AGND、REFIN。信号线STC89C52 (51)Arduino (UNO)STM32F103SCLKP1.0D13 (SCK)PB13 (SPI2_SCK)DINP1.1D11 (MOSI)PB15 (SPI2_MOSI)CSP1.2D10 (SS)PB12 (SPI2_NSS)VOUT测量点测量点测量点REFIN接2.5V基准源接2.5V基准源接3.3V或外部基准这里特别注意如果VDD接5VREFIN不建议直接接5V因为输出会到10V超过运放供电范围容易削顶。我的习惯是REFIN接2.5V精密基准源比如TL431或者LM336-2.5输出范围0到5V覆盖大部分常见需求线性度也比较可靠。如果只需要0到3.3VREFIN接1.65V用两个电阻分压加一个跟随器。2.2 去耦电容和地线布局的实战经验模拟电路最害怕电源噪声。TLC5615虽然本身抗干扰能力不错但参考电压和电源的噪声会直接叠加到输出上。我的做法是VDD引脚就近放一个0.1uF陶瓷电容再加一个10uF钽电容做低频滤波REFIN引脚近地放一个1uF的陶瓷电容滤掉高频毛刺AGND和数字地单点连接不要形成地环路VOUT测量线尽量短避免形成天线效应引入噪声。这些细节初看好像无所谓等你用示波器看输出波形就会发现不加去耦电容时毛刺能到30mV左右加上之后能压到5mV以内。用在纯数字电路里可能无所谓但DAC输出的电压通常是给别人做精密控制的几十毫伏的波动足以让后续电路误动作。2.3 上电顺序与输出毛刺TLC5615在上电瞬间如果CS引脚悬空或者处于低电平输出可能跳到一个随机电压。解决的办法有两个一是用RC电路做上电延迟让CS在上电后先保持高电平几十毫秒二是在单片机初始化代码里先把CS引脚拉高再初始化SPI外设。第二种最简单可靠我所有项目都这么做。3. Arduino平台用最短代码点亮DAC输出3.1 硬件连接Arduino是这三个平台里上手最快的SPI接口直接复用硬件外设不需要模拟时序。接线如下VDD → 5VAGND → GNDSCLK → D13DIN → D11CS → D10REFIN → 接一个2.5V基准比如Arduino的AREF引脚如果配置成外部基准或者干脆用TL431原理非常简单Arduino UNO的SPI口默认SCK时钟可达8MHz而TLC5615最高支持1.25MHz时钟所以SPI速率必须设置为1MHz以下否则芯片跟不上时序数据就会串位。3.2 代码实现SPI方式#include SPI.h // CS引脚定义 #define DAC_CS 10 // 初始化 void setup() { pinMode(DAC_CS, OUTPUT); digitalWrite(DAC_CS, HIGH); SPI.begin(); SPI.setBitOrder(MSBFIRST); // MSB先发送 SPI.setDataMode(SPI_MODE0); // CPOL0, CPHA0 SPI.setClockDivider(SPI_CLOCK_DIV16); // 1MHz } // 写电压函数targetVoltage单位伏特 void setVoltage(float targetVoltage, float vref 2.5) { // 反推DAC数值 uint16_t dacValue (uint16_t)(targetVoltage * 1024.0 / (2.0 * vref) 0.5); if (dacValue 1023) dacValue 1023; // 钳位保护 // 拼装12位数据前2位为0后10位为数据 uint16_t data (dacValue 2) 0x0FFF; // 拆成两字节发送 uint8_t highByte (data 8) 0x0F; // 高字节只有低4位有效 uint8_t lowByte data 0xFF; digitalWrite(DAC_CS, LOW); SPI.transfer(highByte); SPI.transfer(lowByte); digitalWrite(DAC_CS, HIGH); } void loop() { setVoltage(1.25); // 输出1.25V delay(1000); setVoltage(3.75); // 输出3.75V delay(1000); }这段代码最核心的地方就是那句data (dacValue 2) 0x0FFF我当时第一次接触时没搞明白为什么移位2位。后来翻数据手册才发现TLC5615的12位帧格式是[2位填充位0][10位DAC数据]。所以DAC数据必须左移2位腾出低2位的空间但由于我们只对低12位感兴趣所以和0x0FFF做与运算把无关的高位都清掉。3.3 实测验证代码烧录后拿万用表测VOUT对AGND的电压。没有精密万用表的话用四位半的数字万用表也可以。实测结果一般会有千分之几的误差主要来源是参考电压的精度。如果REFIN接的2.5V实际上是2.498V那么输出自然也会跟着偏一点。追求更高精度有两个方向用高精度基准源如ADR4525精度0.02%或者做软件校准——用实测值反推实际VREF代回公式算出更准确的DAC值。另外提醒一下如果代码里用的是SPI.setClockDivider(SPI_CLOCK_DIV16)在UNO上SPI时钟是16MHz/161MHz刚好满足要求。如果你用的是Arduino Due84MHz主频要选SPI_CLOCK_DIV84或者直接SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0))这种写法更规范而且能与其他设备共用总线时避免冲突。4. STM32平台硬件SPI的高效驱动方式4.1 为什么STM32用硬件SPI更合适STM32系列比如F103、F407内置了多个SPI外设硬件自动完成移位操作不占用CPU很多开销。对于DAC这种高频更新的应用硬件SPI优势非常明显你可以在定时器中断里快速更新输出电压而无须像51那样靠软件循环去模拟时序。不过有个细节必须注意STM32的SPI外设数据帧长度可配置为8位或16位而TLC5615帧长度是12位。简单粗暴的方法是用16位帧直接发但16位帧会将数据对齐到16位多出来的前4位可能被芯片误读。解决办法是发送两个8位字节和Arduino部分的做法一致。这里给出HAL库的实现方式。4.2 CubeMX配置要点使用STM32CubeMX配置SPI接口SPI模式选择Full-Duplex Master数据大小8 Bits第一比特MSB First时钟极性Low时钟相位1 Edge对应SPI_MODE0波特率分频选择使得SCLK不超过1.25MHz比如PCLK72MHz时选Prescaler 64 → 1.125MHz软件NSS管理禁用硬件片选用普通GPIO控制CS这里我特别强调一下HAL库发送数据时如果CS由硬件NSS控制容易因为NSS极性设置不一致导致片选异常。我习惯的做法是把SPI配置里的NSS设为Disable然后在代码里用GPIO手动拉低拉高逻辑和Arduino完全一致调试也更直观。4.3 HAL库代码实现#include main.h // 假设SPI2挂载在PB13/PB15/PB12 #define DAC_CS_PIN GPIO_PIN_12 #define DAC_CS_PORT GPIOB // 发送12位数据给TLC5615 void TLC5615_Write(uint16_t dacValue) { uint8_t highByte, lowByte; uint16_t data; if (dacValue 1023) dacValue 1023; // 10位数据左移2位得到12位帧 data (dacValue 2) 0x0FFF; highByte (data 8) 0x0F; // 只取低4位 lowByte data 0xFF; HAL_GPIO_WritePin(DAC_CS_PORT, DAC_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi2, highByte, 1, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi2, lowByte, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(DAC_CS_PORT, DAC_CS_PIN, GPIO_PIN_SET); } // 输出指定电压 void Set_DAC_Voltage(float voltage, float vref) { uint16_t dacValue (uint16_t)(voltage * 1024.0f / (2.0f * vref) 0.5f); TLC5615_Write(dacValue); }这里要注意HAL_SPI_Transmit每次调用都有函数调用开销和状态检查如果在高频中断里使用建议关掉SPI的HAL锁机制或者直接用寄存器操作。对于一般应用HAL库足够。如果你踩过坑可能会遇到这种现象SPI发送完毕但输出没变化。这个时候可以先检查CS时序——CS必须在12个时钟全部结束后才拉高。用逻辑分析仪看CS低电平持续期间是否恰好有12个下降沿如果不是说明发送的数据长度不对。还有一个常见问题是HAL_SPI_Transmit发送高字节时如果SPI FIFO深度不够两个字节之间会有一个微小间隔导致CS低电平期间的时钟不连续TLC5615在这种不连续时钟下是能正常工作的所以我通常不纠结这个。4.4 与Arduino的延迟和精度对比实测下来STM32配合HAL库的SPI从调用函数到输出稳定的时间大约在几十微秒级别Arduino因为有digitalWrite的开销差不多要100微秒以上。两者都不算慢但如果要生成音频波形级别的DAC输出STM32优势会更明显——可以用DMA把波形表直接搬到SPI数据寄存器完全不需要CPU参与。而Arduino在DAC波形输出这块就会吃力一些。5. 51系列平台纯IO模拟SPI从零手写时序5.1 为什么51要自己模拟SPI51系列单片机典型如STC89C52、AT89S52大部分没有硬件SPI外设所以必须用普通GPIO逐位模拟SPI时序。别觉得这样做落后恰恰因为要自己控制每一位的翻转反而能最深刻地理解TLC5615的通信协议。我在给新手讲DAC时特别喜欢让他们先用51手写一遍时序再去用Arduino这样协议层面的理解会扎实很多。51单片机的系统时钟通常是12MHz或者11.0592MHz机器周期是时钟的12分频也就是1MHz左右。指令周期从1到4个机器周期不等所以模拟SPI的频率大概在100kHz到500kHz之间完全低于TLC5615的1.25MHz上限时序余量非常充足。5.2 完整代码实现#include reg52.h // 引脚定义 sbit TLC_DIN P1^0; // 数据输入 sbit TLC_SCLK P1^1; // 时钟 sbit TLC_CS P1^2; // 片选 #define VREF 2.5 // 参考电压单位V // 发送12位数据到TLC5615 void TLC5615_Write(unsigned int dacValue) { unsigned char i; unsigned int data dacValue 0x03FF; // 10位有效 TLC_CS 0; // 片选拉低开始传输 _nop_(); // 小延时 // 先发送2位填充位0 for (i 0; i 2; i) { TLC_DIN 0; TLC_SCLK 1; _nop_(); TLC_SCLK 0; _nop_(); } // 再发送10位数据从高位到低位 for (i 0; i 10; i) { if ((data 0x0200) ! 0) TLC_DIN 1; else TLC_DIN 0; TLC_SCLK 1; _nop_(); TLC_SCLK 0; _nop_(); data 1; // 左移取下一位 } TLC_CS 1; // 片选拉高锁存数据 } // 输出指定电压 void SetVoltage(float targetVoltage) { unsigned int dacValue; float temp; temp targetVoltage * 1024.0f / (2.0f * VREF); dacValue (unsigned int)(temp 0.5); // 四舍五入 if (dacValue 1023) dacValue 1023; TLC5615_Write(dacValue); } void main() { // 上电先把CS置高避免输出乱跳 TLC_CS 1; TLC_SCLK 0; TLC_DIN 0; while (1) { SetVoltage(1.0); // 输出1V delay_ms(1000); SetVoltage(3.3); // 输出3.3V delay_ms(1000); } }这个代码里_nop_()的延时不是必须的但在51上加上它可以让时序更稳定尤其当编译器优化级别较高、代码执行顺序可能出现不可预期变化时适当的空操作能保证每个CLK高电平和低电平的宽度足够。如果不加在12MHz晶振下实测也能工作但为了保险起见建议保留。5.3 51模拟SPI的时序调试技巧调试51模拟SPI最有效的手段是逻辑分析仪。8通道的低成本逻辑分析仪在市面上很便宜把SCLK、DIN、CS三根线接上去抓一段波形数一下CS低电平期间是不是正好12个SCLK上升沿、CS拉高时DIN是否已经稳定。如果没有逻辑分析仪也可以用一个笨办法让程序连续输出同一个电压然后用万用表测VOUT。如果输出和预期一致说明时序基本正确如果不一致多半是位序方向搞反了。TLC5615是MSB先发如果你写成LSB先发数据就会完全错乱输出会是随机电压。51平台的电压精度受限于两个因素一是float运算在8位单片机上比较慢虽然DAC更新速率要求不高但如果每次都用浮点计算1秒更新一次没问题更新1000次/秒就会比较吃力二是参考电压VREF的精度直接决定输出电压精度。建议在51平台用整型运算代替浮点运算例如直接把VREF_volt×1024/2这个常量预先算好写成整数宏// VREF2.5V时DAC值 目标电压(mV) * 1024 / 5000 // 如果目标电压用mV为单位传入直接整数运算 unsigned int mVToDac(unsigned int milliVolt) { return (milliVolt * 1024) / 5000; }这种方式比浮点快好几倍而且精度完全满足需求。注意milliVolt最大到5000乘法不会溢出16位整型5000×1024 5120000需要long所以要用unsigned long存放中间结果。6. 三大平台调试踩坑记录与校准心得6.1 电源噪声引起的输出跳动我最早做TLC5615实验时用的是USB供电的Arduino输出1.25V万用表读数在1.248到1.253之间跳动。这其实不算大问题但当我用示波器看输出波形时在AC耦合模式下能看到约20mV的工频纹波。这是因为USB电源的5V上有100Hz的纹波而REFIN直接引用的VDD分压导致参考电压跟着波动。解决办法很简单REFIN不要从VDD分压而是用独立的基准源。我用TL431搭建了一个2.5V基准电路输出纹波立刻降到5mV以下。如果手头没有TL431用LM317调成2.5V也凑合但温漂会稍大。6.2 SPI模式不匹配导致的输出乱码这是一个特别典型的错误TLC5615要求SPI_MODE0CPOL0, CPHA0在时钟上升沿采样数据下降沿移位。如果你把CPOL配置成1空闲时时钟为高那么第一个上升沿还没到片选就已经低了芯片可能会采到不稳定的电平。更隐蔽的是CPHA配置错误。同样是CPOL0如果CPHA1采样点会从上升沿变成下降沿从逻辑分析仪上看只是数据稳定的时间差了半个周期数据本身很可能在临界处被采错位。症状是输出偶尔正确、偶尔完全不对这种问题最难排查。建议无论在哪个平台上都先用固定的数字比如512对应1.25V 2.5V VREF测试确认无误后再改SPI模式。6.3 高精度应用中的校准方法如果你需要TLC5615输出精确到10mV以内单纯靠公式计算DAC值是不够的。原因有三个参考电压VREF的精度、芯片本身的零点和增益误差、以及输出负载对VOUT的影响。我在项目里用的校准方法是两点校准第一在代码里加一个校准参数calGain和calOffset把实际输出电压和目标电压的偏差拟合成一条直线。操作方法写DAC值600用万用表测实际输出V1写DAC值800测实际输出V2计算实际增益 (V2 - V1) / ((800-600) × 2 × VREF / 1024)实际零偏 V1 - (600 × 2 × VREF / 1024)在校准函数里先算理论DAC值再除以实际增益加上零偏修正最后写入。第二软件里用浮点换算后保存修正参数下次上电自动加载。这种方法可以把绝对误差从±15mV压缩到±3mV以内对于10位DAC来说已经是极致了。6.4 输出负载能力与缓冲建议TLC5615的输出引脚VOUT能提供的电流很小大概在1mA量级。如果你要驱动一个LED、一个继电器、或者给某个电路的基准电压端供电都会把输出电压拉低。典型的表现是空载时输出3.3V接一个1kΩ负载就跌到3.1V。解决办法是在VOUT后面加一个运放做电压跟随器常见选型是LM358、OP07之类。注意运放的供电电压必须大于DAC的最大输出范围如果你输出到5V运放供电要至少7V以上否则输出会提前限幅。这个点很容易被忽略尤其是在5V供电的系统中你觉得自己需要0到5V的输出结果运放供电只有5V输出最高只能到约4.3V离5V差了一大截。6.5 更新速率的选择TLC5615标称最大时钟频率1.25MHz12位帧一次更新需要12个时钟周期换算下来最高更新速率约为100kHz。但实际以我的经验受制于MCU的IO翻转速度和SPI通信开销Arduino能稳定跑到40kHz左右STM32硬件SPI能跑到接近90kHz51模拟SPI最高也就20kHz左右。如果你的应用需要更高的更新率比如输出音频信号有两个方向换用更高性能的DAC芯片如MCP4921、DAC8552或者用STM32的DMA配合定时器触发SPI实现无CPU干预的波形输出。前者简单粗暴后者代码复杂度高但效果极佳我会在另一篇专门讲STM32 DMA驱动DAC的文章里详细展开。7. 从代码到应用TLC5615的典型场景与扩展思路7.1 可调电源/基准源控制我自己做过一个小项目用STM32加TLC5615做一个数控可调电压源。通过按键或者上位机输入期望电压MCU换算成DAC值输出给后级运放来驱动线性稳压器。TLC5615的10位分辨率在0到5V范围内步进约5mV对大多数可调电源的应用完全够用。硬件上注意在DAC输出和后级之间加一级RC低通滤波比如1kΩ100nF截止频率约1.6kHz滤掉DAC转换过程中的毛刺和时钟串扰。滤波不影响直流精度但能把输出波形的干净度提升一个档次。7.2 任意波形发生器的雏形既然TLC5615能快速更新输出那自然可以生成简单的波形——正弦波、三角波、方波都可以。方法是预先把一个周期的波形采样点存成数组DAC循环输出这些采样点。对于10位DAC、1kHz正弦波如果采样率50kHz一个周期50个点代码量非常小。51平台因为更新速度限制适合低频波形STM32配合DMA可以做到更高频率。这个用法我在Arduino上验证过D13引脚输出SCLKD11输出DIND10控制CS在VOUT端加上一个简单的RC滤波器之后示波器能看到非常平滑的正弦波形。代码核心就是查表发数据逻辑和前面完全一致只是把固定的电压值换成数组循环索引而已。7.3 多通道级联设计TLC5615的DOUT引脚可以让多片芯片级联。在12位数据的第12位最后1位移出到DOUT后如果CS保持低电平下一片芯片会把数据继续移入。这样只需要同一组SPI总线就能控制多片DAC。级联时发送的数据顺序是先发送最后一片芯片的数据再发送第一片的数据因为数据是从第一片开始移入经过DOUT流向第二片。实际代码和单片几乎一样只是数据长度变成12×N位。比如两片级联发送24位数据前12位给第二片后12位给第一片。命令发送顺序别搞反我刚接触时在这个细节上吃了亏输出和预期正好对调。7.4 移植到其他MCU平台的思路这套驱动逻辑其实完全通用。ESP32、RP2040、GD32、PIC等平台只要具备SPI或者GPIO模拟能力都能驱动TLC5615。移植时只需要改三个地方引脚定义、SPI初始化、片选控制方式。真正不需要改的是那12位帧结构和DAC值换算公式。所以我的建议是先看懂协议代码怎么改都不怕如果只是复制粘贴换个平台就抓瞎。8. 整体操作流程和避坑清单从我个人的项目经验来看把TLC5615跑通最快30分钟但如果踩坑可能要一整天。这里给一个排错优先级清单按顺序排查能省不少时间第一确认REFIN电压。拿万用表测REFIN对GND的电压确保在正常范围1V到5V之间。如果REFIN悬空输出电压会随机乱跳这是最常见的“DAC芯片是坏的”的假象。第二确认电源和地线。VDD和AGND不要接反AGND和数字地必须连通在面包板上尤其容易遗漏AGND。TI的芯片尽管很皮实但AGND悬空会导致输出完全无规律。第三确认CS上电状态。用示波器或逻辑分析仪检查上电后CS是否保持高电平如果被下拉到低电平芯片可能进入不明状态。第四确认通信帧格式。用我们之前写的示例代码先输出一个固定电压比如512对应1.25VVREF2.5V时如果实测这个电压对得上再改其他值。第五确认负载。接上运放跟随器之后再测VOUT如果直接测芯片引脚且电压偏低先检查负载阻抗是否太小。第六校准。如果静态精度不够按前面说的两点校准法做一次软件校正。我还想强调另一个很容易被忽略的点TLC5615的DAC输出在CS拉高瞬间才会正式更新如果你在CS保持低电平期间反复发送数据输出是不变化的。很多人在循环里忘记拉高CS写了一个晚上代码结果输出纹丝不动检查半天才发现是CS一直处于低电平。以上这些坑我都踩过尤其是SPI模式不匹配和REFIN悬空这两个问题几乎每次帮人调试都会碰到。只要按顺序排查一遍绝大多数问题都能解决。希望这篇文章能帮你少走弯路快速让TLC5615输出你想要的电压。
阅读完成 · 觉得有帮助?
咨询建站