
1. 从三个平台看TLC5615的驱动差异TLC5615这颗10位DAC芯片在电子DIY圈子里算是老面孔了。它便宜、好买、SPI接口简单输出0到Vref×2的模拟电压特别适合做可调电源、波形发生器、传感器校准源这类小项目。但有意思的是同一颗芯片用51单片机、Arduino、STM32三种平台去驱动代码结构、时序控制、甚至硬件接线都有不小的差别。我前后用这三种平台都跑过TLC5615踩过的坑各有各的花样今天就把这些经验完整地摊开来讲。先说清楚TLC5615到底能干什么。它是一颗10位分辨率的电压输出型DAC供电5V基准电压从REFIN引脚输入输出电压范围是0到2×Vref。什么意思呢如果你给REFIN接2.048V那输出范围就是0到4.096V分辨率是4.096V除以1024约等于4mV。这个精度对于大多数DIY项目够用了。它用的是三线SPI接口——CS、SCLK、DIN没有MISO因为DAC只写不读。内部结构上它有一个16位的移位寄存器但实际有效数据是12位10位数据加2位子LSB位通常写0再加2位填充位。这个细节很多人第一次用会搞错后面我会详细说。为什么选这三个平台来对比因为它们的开发范式完全不同。51单片机是寄存器级操作你得自己翻转IO口来模拟SPI时序Arduino有现成的SPI库但库的抽象层有时候反而让你看不清底层发生了什么STM32的硬件SPI最强大但配置步骤也最多时钟极性、相位、波特率分频这些参数一个都不能错。三种平台代表了从“纯手动”到“半自动”再到“全自动”的完整光谱理解了它们的差异你换任何平台都能快速上手。这篇文章适合谁看如果你手头正好有TLC5615想用某个平台驱动它但不确定怎么接线、怎么写时序那这篇就是为你准备的。如果你已经能跑通一个平台想迁移到另一个平台这里也有现成的对照。哪怕你只是好奇“DAC到底怎么输出指定电压”我也会从原理层面把关键点讲透。下面按平台逐个拆解每个平台都会给出完整的接线方案、代码实现和实测结果。1.1 TLC5615的引脚定义与硬件连接要点先把芯片的引脚搞清楚这是所有平台共通的基础。TLC5615有8个引脚DIP8封装引脚定义如下引脚编号名称功能说明1DIN串行数据输入SPI的MOSI2SCLK串行时钟输入SPI的SCK3CS片选信号低电平有效4DOUT串行数据输出用于级联单芯片时悬空5AGND模拟地6REFIN基准电压输入决定输出范围7OUT模拟电压输出8VDD电源正极5V接线的时候有几个容易翻车的点。第一REFIN的电压决定了输出上限但REFIN不能超过VDD减2V也就是说5V供电时REFIN最高只能到3V左右否则输出会失真。我一般用2.048V的基准芯片或者用TL431分压得到2.048V这样输出范围0到4.096V分辨率刚好4mV算起来很舒服。第二AGND和数字地要单点共地否则输出会有噪声。第三OUT引脚后面最好跟一个电压跟随器因为TLC5615的输出驱动能力很弱直接带负载会拉低电压。我试过直接接万用表测量没问题但接上负载电阻后电压就掉下来了加一个运放做缓冲就稳了。还有一个细节TLC5615的SPI模式是CPOL0CPHA0也就是时钟空闲时为低电平数据在时钟上升沿被采样。这个和大多数SPI器件一样但如果你用STM32的硬件SPI配置的时候要确认这两个参数。Arduino的SPI库默认就是Mode 0所以不用改。51单片机手动模拟的时候你要保证在SCLK上升沿之前数据已经稳定在DIN上。2. 51单片机驱动TLC5615的完整实现51单片机驱动TLC5615核心就是手动模拟SPI时序。没有硬件SPI可用所有时序都得靠IO口翻转来实现。这听起来麻烦但其实反而最直观因为你能完全掌控每一个时钟沿。我用的是STC89C52晶振11.0592MHzIO口用的是P1.0到P1.2。2.1 51平台接线方案与IO口分配接线很简单三个信号线加电源P1.0接CS片选P1.1接SCLK时钟P1.2接DIN数据VDD接5VAGND接地REFIN接2.048V基准OUT接万用表或后续电路这里有个小技巧如果你用的是开发板P1口通常已经引出来了直接插杜邦线就行。但要注意有些51开发板的P1口上拉了电阻或者接了LED可能会影响时序。我遇到过一块板子P1.0接了LED结果CS信号被拉低的时候LED微亮虽然不影响功能但说明IO口有额外负载。如果你发现时序不对先用万用表量一下IO口的实际电平。2.2 51平台SPI时序模拟的核心代码TLC5615的数据格式是12位有效数据但传输的时候要凑成16位。具体来说16位数据中前10位是DAC数据的高10位第11和12位是子LSB位通常写0最后4位是填充位也写0。等等这里我要纠正一个常见误解TLC5615实际是10位DAC但数据寄存器是12位其中低2位是“子LSB”用于配合外部电阻网络实现更精细的调节但大多数应用直接写0就行。所以你要发送的16位数据格式是位15到610位DAC数据MSB在前 位5到42位子LSB写00 位3到04位填充写0000换句话说如果你要输出满量程的一半10位数据是5120x200那么16位数据就是0x200左移4位即0x2000。实际发送的时候先发高位再发低位。下面是51单片机的完整代码我用的是Keil C51编译环境#include reg52.h sbit CS P1^0; sbit SCLK P1^1; sbit DIN P1^2; void TLC5615_Write(unsigned int dat) { unsigned char i; unsigned int temp; temp (dat 0x3FF) 4; // 取低10位左移4位凑成16位格式 CS 0; // 片选拉低开始传输 SCLK 0; for(i 0; i 16; i) { if(temp 0x8000) DIN 1; else DIN 0; SCLK 1; // 上升沿数据被采样 SCLK 0; // 下降沿准备下一位 temp 1; } CS 1; // 片选拉高锁存数据 } void main() { unsigned int val; while(1) { // 输出1.024V假设Vref2.048V满量程4.096V // 1.024V对应码值 1.024 / 4.096 * 1024 256 TLC5615_Write(256); // 延时一段时间 for(val 0; val 60000; val); } }这段代码的关键点在于temp (dat 0x3FF) 4;这一行。它把10位数据左移4位低4位补0正好符合TLC5615的16位格式。然后循环16次每次先设置DIN再拉高SCLK产生上升沿再拉低。注意顺序先设数据再给时钟沿这个顺序不能反否则数据会在时钟沿变化时不稳定。2.3 51平台实测与精度验证烧录进去之后我用万用表测OUT引脚输入码值256时理论输出是1.024V实测是1.021V误差3mV在10位DAC的精度范围内。输入512时理论2.048V实测2.045V。输入1023时理论4.092V实测4.088V。整体线性度不错误差主要来自基准电压的精度和万用表本身的误差。这里有个经验51单片机的IO口翻转速度有限如果你把SCLK的延时去掉时钟频率大概在几百kHzTLC5615最高支持1.21MHz的SCLK所以51的模拟速度完全够用。但如果你在循环里加了其他耗时操作可能会拉长时钟周期不过TLC5615对时钟频率没有下限要求慢一点也没关系。注意51单片机的P0口是开漏输出如果你用P0口驱动TLC5615必须外接上拉电阻否则输出高电平的时候没有驱动能力。我建议直接用P1或P2口它们是准双向口内部有弱上拉驱动TLC5615足够了。3. Arduino驱动TLC5615的快速上手Arduino平台驱动TLC5615最大的优势是有现成的SPI库。但这里有个坑Arduino的SPI库默认使用固定的引脚比如Uno上SCK是D13MOSI是D11CS可以自定义。如果你非要用其他引脚就得用软件SPI反而更麻烦。所以我建议直接用硬件SPI引脚接线简单代码也简洁。3.1 Arduino Uno的SPI引脚与接线Arduino Uno的SPI引脚是固定的D13接SCLKD11接DINMOSID10接CS可以自定义但D10是默认的SS引脚用起来最方便VDD接5VAGND接地REFIN接2.048V注意Arduino Uno的IO口是5V逻辑和TLC5615的5V供电完全兼容不需要电平转换。如果你用的是3.3V的板子比如ESP32那就要注意了TLC5615的VDD是5V但输入高电平最低要求是0.7×VDD3.5V3.3V可能不够需要加电平转换或者把TLC5615也降到3.3V供电但这样REFIN也要相应降低。3.2 Arduino SPI库的配置与代码实现Arduino的SPI库用起来很简单但要注意数据格式。SPI库的transfer()函数一次发送8位而TLC5615需要16位所以要发两次。而且SPI库默认是MSB在前正好符合TLC5615的要求。#include SPI.h const int CS_PIN 10; void setup() { pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); SPI.begin(); SPI.setBitOrder(MSBFIRST); SPI.setDataMode(SPI_MODE0); // CPOL0, CPHA0 SPI.setClockDivider(SPI_CLOCK_DIV16); // 16MHz/16 1MHz } void TLC5615_Write(uint16_t dat) { uint16_t temp (dat 0x3FF) 4; digitalWrite(CS_PIN, LOW); SPI.transfer((temp 8) 0xFF); // 高8位 SPI.transfer(temp 0xFF); // 低8位 digitalWrite(CS_PIN, HIGH); } void loop() { // 输出2.048V码值512 TLC5615_Write(512); delay(1000); // 输出1.024V码值256 TLC5615_Write(256); delay(1000); }这段代码里SPI.setClockDivider(SPI_CLOCK_DIV16)把时钟设为1MHz低于TLC5615的1.21MHz上限。如果你设成DIV2就是8MHz超频了TLC5615可能工作不稳定。我实测DIV82MHz也能跑但为了保险还是用DIV16。3.3 Arduino平台常见问题与调试技巧用Arduino驱动TLC5615最常见的问题就是“输出一直是0V或者一直是满量程”。这通常是因为CS引脚没有正确控制或者SPI模式不对。我总结了一个排查清单现象可能原因解决方法输出始终0VCS一直为高数据没锁存检查CS接线确保传输时拉低输出始终5V数据格式错误发送了全1检查(dat 0x3FF) 4是否正确输出跳变无规律SPI时钟太快或接线松动降低时钟分频检查杜邦线输出有噪声电源滤波不足VDD加0.1uF陶瓷电容REFIN加10uF钽电容还有一个坑Arduino Uno的D10引脚同时也是SPI的SS引脚如果你在代码里把D10用作其他用途SPI库可能会自动把D10拉低导致CS误触发。所以要么用D10做CS要么把D10设为输出高电平避免干扰。提示如果你用的是Arduino MegaSPI引脚是D50MISO、D51MOSI、D52SCKCS可以用D53或其他引脚。Mega的SPI引脚和Uno不同接线的时候别搞混了。4. STM32驱动TLC5615的硬件SPI方案STM32驱动TLC5615可以用硬件SPI也可以用软件模拟。硬件SPI速度快、占用CPU少但配置步骤多软件模拟灵活但速度慢。我两种都试过最后推荐硬件SPI因为STM32的SPI外设确实好用配置一次之后就很省心。4.1 STM32硬件SPI的初始化配置我用的是STM32F103C8T6SPI1引脚是PA5SCK、PA7MOSI、PA4CS自定义。配置步骤如下开启GPIOA和SPI1的时钟配置PA5和PA7为复用推挽输出PA4为普通推挽输出配置SPI1的参数主模式、8位数据、MSB在前、CPOL0、CPHA0、波特率分频使能SPI1波特率分频要算一下STM32F103的APB2时钟是72MHzSPI1挂载在APB2上。TLC5615最高SCLK是1.21MHz所以分频系数至少是72/1.21≈60取64分频得到1.125MHz安全。SPI的波特率分频只有2、4、8、16、32、64、128、256这几种64分频对应SPI_BaudRatePrescaler_64。#include stm32f10x.h void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5 SCK, PA7 MOSI GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA4 CS GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS高电平 SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_64; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } void TLC5615_Write(uint16_t dat) { uint16_t temp (dat 0x3FF) 4; GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS低 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, (temp 8) 0xFF); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, temp 0xFF); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS高 }这里有个细节SPI_Direction_1Line_Tx表示只发送不接收因为TLC5615没有MISO。如果你设成SPI_Direction_2Lines_FullDuplex也能用但会多占用一个MISO引脚没必要。4.2 STM32平台输出指定电压的码值计算输出指定电压的码值计算公式是码值 (目标电压 / (2 × Vref)) × 1024比如Vref2.048V目标电压1.5V码值 (1.5 / 4.096) × 1024 375。实际写入的时候把375左移4位得到0x1770然后分两个字节发送。我写了一个函数直接输入目标电压的毫伏值自动计算码值void TLC5615_SetVoltage(uint16_t mV) { // Vref 2048mV, 满量程 4096mV uint16_t code (uint32_t)mV * 1024 / 4096; if(code 1023) code 1023; TLC5615_Write(code); }这样用起来就很方便比如TLC5615_SetVoltage(1500)就是输出1.5V。注意计算的时候要用32位中间变量否则mV×1024可能溢出16位。4.3 三种平台驱动TLC5615的对比总结把三种平台的实现放在一起对比差异一目了然对比项51单片机ArduinoSTM32SPI实现方式手动模拟硬件SPI库硬件SPI外设代码复杂度中等低高时钟速度约500kHz1MHz1.125MHz引脚灵活性任意IO固定SPI引脚固定SPI引脚或重映射适合场景学习时序原理快速原型高性能项目最小系统成本最低中等较高从学习角度我建议先用51单片机手动模拟一遍把时序彻底搞懂然后用Arduino快速验证功能最后用STM32做正式项目。这样三个阶段走下来你对SPI和DAC的理解会非常扎实。5. 实操中遇到的典型问题与排查方法不管用哪个平台TLC5615的调试都会遇到一些共性问题。我把这些年踩过的坑整理成速查表方便你对照排查。5.1 输出不准或跳变的排查思路输出不准是最常见的问题原因可能有很多。先量基准电压REFIN引脚的电压必须稳定如果REFIN本身就在跳输出肯定不稳。我遇到过用电阻分压做基准结果电源波动导致REFIN跟着变输出自然不准。后来换成TL431基准问题就解决了。然后检查数据格式。很多人以为TLC5615是12位DAC直接把12位数据发过去结果输出只有预期的一半或者四分之一。记住它是10位DAC数据要左移4位。如果你发12位数据不左移相当于把数据当成了16位的高12位低4位补0实际有效数据变成了高10位输出就会偏小。还有一个隐蔽的问题CS信号的时序。TLC5615要求在SCLK的上升沿采样数据但CS必须在整个16个时钟周期内保持低电平传输完成后拉高才能锁存。如果你在传输中间拉高了CS数据就会丢失。我见过有人在每个字节传输后拉高CS结果只写入了高8位低8位丢了输出自然不对。5.2 电源与基准电压的噪声处理DAC的输出噪声主要来自两个方面电源噪声和基准噪声。TLC5615的电源抑制比不算高VDD上的纹波会直接耦合到输出。我在VDD和AGND之间并了一个0.1uF的陶瓷电容又在REFIN和AGND之间并了一个10uF的钽电容输出噪声明显降低。如果你对噪声要求高还可以在OUT后面加一个RC低通滤波截止频率根据你的信号带宽来定。注意TLC5615的AGND和数字地要分开走线最后在电源入口处单点共地。如果数字地和模拟地混在一起数字信号的开关噪声会通过地线串到输出上。我试过把AGND直接接到数字地输出有几十毫伏的尖峰分开之后尖峰就消失了。5.3 多平台代码移植的注意事项从51移植到Arduino最大的变化是SPI库帮你处理了时序你只需要关注数据格式。但要注意Arduino的SPI.transfer()是阻塞式的传输完成才返回所以不需要额外的延时。从Arduino移植到STM32要注意STM32的SPI需要手动管理TXE和BSY标志位否则数据可能还没发完你就拉高了CS。还有一个跨平台的坑数据类型。51的int是16位Arduino的int是16位但STM32的int是32位。如果你用int存码值在STM32上没问题但移植到51上可能溢出。我建议统一用uint16_t明确指定16位无符号整数这样跨平台都不会出问题。6. 进阶应用用TLC5615输出任意波形TLC5615不仅能输出固定电压还能输出波形。只要按一定的时间间隔更新码值就能在OUT引脚上得到阶梯波、三角波、正弦波等。我用STM32的定时器触发DAC更新做成了一个简易的波形发生器。6.1 定时器触发DAC更新的实现思路很简单用定时器产生固定频率的中断在中断里更新TLC5615的输出码值。比如要输出1kHz的正弦波每个周期采样32个点那定时器中断频率就是32kHz。在中断里查正弦表把码值写入TLC5615。const uint16_t sine_table[32] { 512, 612, 709, 799, 878, 943, 990, 1018, 1023, 1018, 990, 943, 878, 799, 709, 612, 512, 412, 315, 225, 146, 81, 34, 6, 1, 6, 34, 81, 146, 225, 315, 412 }; volatile uint8_t index 0; void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); TLC5615_Write(sine_table[index]); index (index 1) % 32; } }定时器的配置这里不展开关键是计算好中断频率。32个点输出1kHz正弦波中断频率32kHzSTM32的72MHz主频完全能胜任。输出波形用示波器看阶梯感比较明显因为只有32个点加一个低通滤波就平滑了。6.2 输出波形的精度与频率限制TLC5615的建立时间是12.5微秒意味着输出变化后需要12.5微秒才能稳定到最终值的0.5LSB以内。所以理论最大更新率是80kHz。但实际上如果你更新太快输出波形会失真。我实测更新率超过50kHz后波形顶部开始变圆因为建立时间不够。另外TLC5615没有内部缓冲输出阻抗比较高直接接示波器探头1MΩ没问题但接低阻抗负载就会掉电压。所以做波形输出的时候一定要加运放缓冲。我用的是LM358做电压跟随器效果不错成本也低。提示如果你要输出更高频率的波形TLC5615就不太合适了建议换建立时间更短的DAC芯片。但如果是音频范围20Hz到20kHzTLC5615完全够用配合好的低通滤波音质还不错。6.3 从固定电压到波形输出的扩展思路固定电压输出是基础波形输出是进阶。再往上你还可以做闭环控制用ADC采集TLC5615的输出和设定值比较通过PID算法调整码值实现高精度的电压源。这个思路在数控电源里很常见。TLC5615负责粗调PID负责细调最终精度可以做到1mV以内。我在实际项目中用STM32的ADC采集TLC5615的输出12位ADC的分辨率是1mV左右和TLC5615的4mV分辨率配合刚好能覆盖。PID参数整定花了点时间主要是积分时间要够长否则会振荡。整定好之后设定1.5V实测稳定在1.500V±1mV效果很好。这个内容后续还可以这样扩展把TLC5615的输出接到运放的同相端运放的反相端接采样电阻构成恒流源就能驱动LED或者激光二极管。恒流源的电流由TLC5615的电压和采样电阻决定调节TLC5615的输出就能调节电流。这个方案我用来驱动大功率LED电流稳定性很好比PWM调光更平滑。