ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TLC5615十位DAC驱动全解析:51/Arduino/STM32实战与避坑指南

TLC5615十位DAC驱动全解析:51/Arduino/STM32实战与避坑指南 TLC5615 这颗十位 DAC 芯片在单片机圈子里算是老面孔了。价格便宜、SPI 接口简单、供电范围宽很多做数控电源、波形发生器、传感器校准的玩家都会选它。但我在社区里看到大量提问核心都卡在同一个地方代码烧进去了万用表一量输出电压要么是 0要么是满量程要么跟算出来的值对不上。问题往往不在芯片本身而是时序、参考电压接法、数据格式这三处出了偏差。这篇内容面向三类人正在用 51 单片机做课程设计的学生、用 Arduino 搭原型的创客、以及用 STM32 做产品开发的工程师。我会把 TLC5615 的驱动逻辑从底层讲清楚然后分别给出三个平台的完整可运行代码最后把调试过程中最容易踩的坑一个个拆开说。代码你可以直接抄但更重要的是理解为什么这么写。1. TLC5615 到底是怎么把数字变成电压的1.1 芯片内部结构与输出电压公式TLC5615 是十位分辨率的电压输出型 DAC内部结构可以拆成三块一个 10 位的数据寄存器、一个 R-2R 电阻网络、一个输出缓冲放大器。你送进去的 10 位二进制数经过 R-2R 网络转换成对应的模拟电流再由缓冲放大器转成电压输出。输出电压的公式是Vout Vref × (D / 1024)其中 Vref 是参考电压D 是你写入的 10 位数值范围 0 到 1023。这里有个细节很多人搞错分母是 1024 而不是 1023。因为 10 位能表示 1024 个状态0 到 1023满量程输出对应的是 D1023 时的 1023/1024 × Vref而不是完整的 Vref。如果你用 2.048V 的基准满量程输出约等于 2.046V差了 2mV。对精度要求高的场合这个偏差要算进去。TLC5615 的参考电压输入范围是 2V 到 VDD-2V。什么意思如果你用 5V 供电Vref 只能在 2V 到 3V 之间选。这个限制来自内部缓冲放大器的共模输入范围超出这个范围输出会失真甚至锁死。很多人直接拿 5V 当参考电压结果输出怎么调都不对就是踩了这个坑。1.2 三种封装与引脚功能对照TLC5615 常见的有三种封装DIP8、SOIC8、以及 TSSOP8。引脚定义完全一样只是焊接方式不同。手工搭电路建议用 DIP8插面包板上方便。引脚编号名称功能说明1DIN串行数据输入在 SCLK 上升沿采样2SCLK串行时钟输入最高 14MHz3CS片选信号低电平有效4DOUT串行数据输出用于级联5AGND模拟地6REFIN参考电压输入7OUT模拟电压输出8VDD电源正极4.5V 到 5.5VDOUT 这个引脚很多人不知道怎么用。它其实是把 DIN 的数据延迟 16 个时钟周期后输出用于多片 TLC5615 级联。单片使用时悬空即可不用管它。1.3 16 位数据帧的格式陷阱TLC5615 要求一次写入 16 位数据但只有低 10 位是有效的 DAC 数据。高 6 位里前 4 位是无关位通常写 0后 2 位是功能位。具体格式如下Bit15 Bit14 Bit13 Bit12 | Bit11 Bit10 | Bit9 ... Bit0 无关 无关 无关 无关 | 模式位 模式位 | 10位DAC数据Bit11 和 Bit10 这两位控制工作模式。常见配置是都写 0表示正常工作模式。如果你看到别人的代码里数据左移了 2 位再发送就是在给这两位留位置。实际发送时先发高位还是低位TLC5615 要求高位先发MSB First。每次 SCLK 上升沿DIN 上的数据被移入内部寄存器。16 个时钟周期后CS 拉高数据锁存到 DAC 寄存器输出更新。注意CS 必须在 16 个时钟周期内保持低电平中途拉高会导致数据帧不完整输出不会更新。我见过有人用软件 SPI 时在循环里误操作了 CS调了一整天才发现。2. 51 单片机驱动 TLC5615 的完整实现2.1 硬件连接与参考电压方案选择用 51 单片机驱动 TLC5615硬件上需要三根 IO 线一根接 DIN一根接 SCLK一根接 CS。我一般这样分配P1.0 接 DINP1.1 接 SCLKP1.2 接 CS参考电压这块如果你手头有 TL431可以用它产生 2.5V 基准精度比电阻分压好得多。TL431 的接法是阴极接 Vref 引脚阳极接地参考端接阴极再配两个电阻分压设定输出电压。具体电路网上很多这里不展开。如果只是做实验用两个精度 1% 的电阻从 5V 分压得到 2.5V 也能凑合但温漂和负载调整率差输出值会有几十毫伏的波动。我实测过用电阻分压时DAC 输出从 0 调到满量程参考电压会变化约 30mV导致非线性误差明显。2.2 软件 SPI 时序的精确控制51 单片机没有硬件 SPI需要用 IO 口模拟。关键在于时序的精确控制。TLC5615 的 SCLK 最高频率是 14MHz51 单片机跑 12MHz 晶振时机器周期是 1usIO 翻转速度远达不到这个上限所以不用担心太快反而要确保每个电平有足够的保持时间。下面是我常用的驱动函数#include reg52.h sbit DIN P1^0; sbit SCLK P1^1; sbit CS P1^2; void TLC5615_Write(unsigned int dat) { unsigned char i; dat 0x03FF; // 只保留低10位 dat 2; // 左移2位给模式位留位置 CS 0; // 片选拉低 for(i 0; i 16; i) { SCLK 0; if(dat 0x8000) // 判断最高位 DIN 1; else DIN 0; dat 1; // 数据左移 SCLK 1; // 上升沿数据被采样 } CS 1; // 片选拉高锁存数据 }这段代码的逻辑是先把 10 位数据左移 2 位凑成 12 位高 4 位自然为 0。然后循环 16 次每次先拉低 SCLK把当前最高位放到 DIN 上再拉高 SCLK 产生上升沿。16 次之后 CS 拉高完成一次写入。有个细节dat 2之后原来的 Bit9 变成了 Bit11Bit0 变成了 Bit2。而 TLC5615 的 Bit11 和 Bit10 是模式位Bit9 到 Bit0 才是数据。所以左移 2 位后数据正好落在正确的位置上模式位默认为 0。这个操作很巧妙但如果不理解数据帧格式看代码会一头雾水。2.3 输出指定电压的换算与实测校准假设你要输出 1.5V参考电压是 2.5V那么D 1.5 / 2.5 × 1024 614.4取整后 D 614。写入 614理论输出是 2.5 × 614 / 1024 1.499V。实际测量可能会有几毫伏偏差来源包括参考电压精度、DAC 积分非线性、输出缓冲失调等。如果你需要更精确的输出可以做一个两点校准先写入 0测量实际输出 V0再写入 1023测量实际输出 V1。然后对任意目标电压 Vtarget计算D (Vtarget - V0) / (V1 - V0) × 1023这样能消除增益误差和失调误差精度可以做到 1mV 以内。我在一个传感器校准项目里用这个方法效果很好。3. Arduino 平台驱动 TLC5615 的两种写法3.1 用 shiftOut 函数快速上手Arduino 有现成的shiftOut函数可以直接输出 8 位数据。但 TLC5615 需要 16 位所以要调用两次。下面是最简实现const int DIN_PIN 11; const int SCLK_PIN 13; const int CS_PIN 10; void TLC5615_Write(uint16_t dat) { dat 0x03FF; dat 2; digitalWrite(CS_PIN, LOW); shiftOut(DIN_PIN, SCLK_PIN, MSBFIRST, highByte(dat)); shiftOut(DIN_PIN, SCLK_PIN, MSBFIRST, lowByte(dat)); digitalWrite(CS_PIN, HIGH); } void setup() { pinMode(DIN_PIN, OUTPUT); pinMode(SCLK_PIN, OUTPUT); pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); } void loop() { TLC5615_Write(614); // 输出约1.5V delay(1000); }shiftOut的第三个参数MSBFIRST表示高位先发正好符合 TLC5615 的要求。highByte和lowByte分别取出 16 位数据的高 8 位和低 8 位。这个写法简单直接适合快速验证。但要注意shiftOut在 Arduino UNO 上SCLK 的频率大约在 100kHz 到 200kHz 之间取决于编译优化。这个速度对 TLC5615 来说完全没问题但如果你需要更快的更新率就得自己写寄存器操作。3.2 直接操作寄存器提升刷新率如果你用 Arduino UNOATmega328P可以通过直接操作 PORT 寄存器来大幅提升速度#define DIN_HIGH PORTB | (1 3) // D11 PB3 #define DIN_LOW PORTB ~(1 3) #define SCLK_HIGH PORTB | (1 5) // D13 PB5 #define SCLK_LOW PORTB ~(1 5) #define CS_HIGH PORTB | (1 2) // D10 PB2 #define CS_LOW PORTB ~(1 2) void TLC5615_Write_Fast(uint16_t dat) { dat 0x03FF; dat 2; CS_LOW; for(uint8_t i 0; i 16; i) { SCLK_LOW; if(dat 0x8000) DIN_HIGH; else DIN_LOW; dat 1; SCLK_HIGH; } CS_HIGH; }这种写法省去了digitalWrite的函数调用开销SCLK 频率可以跑到 1MHz 以上。我实测在 16MHz 的 UNO 上一次写入大约 20us刷新率能到 50kHz。做音频级别的波形输出可能不够但做一般的控制信号绰绰有余。提示直接操作寄存器时要确保引脚编号和端口位的对应关系正确。UNO 的 D10、D11、D13 分别对应 PB2、PB3、PB5这个在 ATmega328P 数据手册里能查到。如果你用的是别的板子对应关系可能不同。3.3 Arduino 上的参考电压与输出范围Arduino UNO 的 5V 供电直接给 TLC5615 的 VDD参考电压需要单独提供。我建议用一颗 TL431 产生 2.5V或者用 Arduino 的 3.3V 输出引脚如果板子有的话经过分压得到 2V 左右的参考。有个取巧的办法用 Arduino 的 PWM 输出经过 RC 滤波产生参考电压。但 PWM 纹波会影响 DAC 输出精度除非你加两级滤波否则输出会有几十毫伏的抖动。我试过做实验可以做产品不行。4. STM32 驱动 TLC5615硬件 SPI 与软件 SPI 的取舍4.1 硬件 SPI 配置要点STM32 的 SPI 外设可以直接驱动 TLC5615配置起来比软件模拟更稳定。以 STM32F103 为例用 SPI1SCK 接 PA5MOSI 接 PA7CS 用 PA4普通 GPIO 控制配置 SPI1 的关键参数参数设置值说明模式主模式STM32 提供时钟数据宽度8 位分两次发送时钟极性低电平空闲时 SCK 为低时钟相位第一个边沿上升沿采样波特率预分频8 或 16得到 4.5MHz 或 2.25MHz位序MSB First高位先发TLC5615 要求 SCLK 空闲时为低电平数据在上升沿被采样这对应 SPI 的 Mode 0CPOL0CPHA0。配置错了模式数据会错位输出值完全不对。初始化代码大致如下void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5SCK, PA7MOSI 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); // PA4CS 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); 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_8; 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) { dat 0x03FF; dat 2; GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS低 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, dat 8); // 高8位 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, dat 0xFF); // 低8位 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS高锁存 }注意最后要等 BSY 标志清零再拉高 CS否则最后一个时钟可能还没发完数据帧不完整。4.2 软件 SPI 在 STM32 上的适用场景有些项目里 SPI 引脚被其他外设占用了或者你需要把 TLC5615 接到任意 GPIO 上这时候软件 SPI 更灵活。STM32 的 IO 翻转速度很快软件 SPI 也能跑到几 MHz。软件 SPI 的代码和 51 的类似只是 IO 操作换成 STM32 的库函数或寄存器。我一般用寄存器操作#define DIN_HIGH GPIOA-BSRR GPIO_Pin_7 #define DIN_LOW GPIOA-BRR GPIO_Pin_7 #define SCLK_HIGH GPIOA-BSRR GPIO_Pin_5 #define SCLK_LOW GPIOA-BRR GPIO_Pin_5 #define CS_HIGH GPIOA-BSRR GPIO_Pin_4 #define CS_LOW GPIOA-BRR GPIO_Pin_4 void TLC5615_SoftWrite(uint16_t dat) { dat 0x03FF; dat 2; CS_LOW; for(uint8_t i 0; i 16; i) { SCLK_LOW; if(dat 0x8000) DIN_HIGH; else DIN_LOW; dat 1; SCLK_HIGH; } CS_HIGH; }BSRR 和 BRR 是 STM32 的位设置/清除寄存器操作它们只需要一个时钟周期比库函数快得多。4.3 用定时器触发 DAC 更新实现波形输出如果你想用 TLC5615 输出正弦波、三角波这类波形可以用定时器中断来触发 DAC 更新。思路是在定时器中断里查表把波形数据依次写入 TLC5615。以 1kHz 正弦波为例一个周期采 64 个点定时器中断频率就是 64kHz。用 STM32 的 TIM2配置为 64kHz 更新中断在中断服务函数里写const uint16_t sine_table[64] {512, 562, 612, ...}; // 预先算好的正弦表 void TIM2_IRQHandler(void) { static uint8_t index 0; if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); TLC5615_Write(sine_table[index]); index (index 1) 0x3F; } }正弦表的数据要按 TLC5615 的 10 位格式生成范围 0 到 1023中心值 512。输出频率 定时器中断频率 / 每周期点数。这个方案我做过 10Hz 到 5kHz 的波形输出效果稳定谐波失真主要来自 10 位分辨率的量化误差。5. 调试过程中最容易翻车的五个地方5.1 参考电压超出允许范围导致输出锁死前面提过TLC5615 的参考电压必须在 2V 到 VDD-2V 之间。5V 供电时Vref 范围是 2V 到 3V。如果你直接接 5V芯片内部缓冲器会饱和输出要么固定在接近 0V要么固定在接近 VDD怎么调数据都不变。我遇到过一个案例有人用 5V 参考电压输出一直是 4.98V换了三片芯片都一样最后查数据手册才发现是参考电压超限。改成 2.5V 后立刻正常。5.2 CS 信号时序错误导致数据帧不完整CS 必须在整个 16 个时钟周期内保持低电平。如果你用硬件 SPINSS 引脚配置成硬件管理可能会在每 8 位数据之间自动拉高导致 TLC5615 收到两个不完整的数据帧输出乱跳。解决办法是把 NSS 配置成软件管理SPI_NSS_Soft用普通 GPIO 手动控制 CS。这个坑在 STM32 上特别常见因为默认配置就是硬件 NSS。5.3 数据移位方向搞反输出值完全错误TLC5615 要求 MSB First如果你写成 LSB First数据位会完全颠倒。比如写入 1实际收到的可能是 512输出值差了 512 倍。检查方法写入 512二进制 1000000000如果输出是 Vref/2说明移位方向正确。如果输出是 Vref/1024 或接近 0说明位序反了。5.4 电源去耦不足导致输出噪声大TLC5615 的 VDD 引脚需要加 0.1uF 陶瓷电容去耦位置尽量靠近芯片。我见过有人在面包板上不加电容输出用示波器看有 50mV 左右的尖峰噪声加了电容之后降到 5mV 以内。另外AGND 和数字地要在芯片附近单点连接避免数字开关噪声串入模拟输出。5.5 输出缓冲器带载能力有限TLC5615 的输出缓冲器可以驱动 2kΩ 以上的负载如果你接的负载阻抗太低输出电压会被拉低。比如你直接接一个 100Ω 的电阻到地输出会明显低于设定值。如果必须驱动低阻抗负载要在 TLC5615 输出后面加一级运放缓冲比如用 LM358 做电压跟随器。这个在数控电源项目里很常见。6. 三个平台的选型建议与实战心得6.1 什么场景选 51什么场景选 Arduino什么场景选 STM32如果你是在学单片机原理或者课程设计要求用 51那就老老实实用 51。TLC5615 的驱动逻辑简单正好用来练 IO 口模拟时序。STC89C52 加上 TLC5615成本不到十块钱能做出一个数控电压源。如果你要快速验证想法或者做互动装置、艺术项目Arduino 更合适。库函数封装好了几行代码就能跑起来不用关心寄存器配置。但要注意 Arduino 的shiftOut速度有限高刷新率场景要自己写寄存器操作。如果你做产品开发或者需要多路 DAC、高速波形输出STM32 是首选。硬件 SPI 稳定可靠定时器触发 DAC 更新可以做到微秒级精度。但 STM32 的开发环境配置比前两者复杂新手容易在时钟树和引脚复用上卡住。6.2 提高输出精度的几个实用技巧第一参考电压用专用基准芯片比如 REF5025 或 LM4040温漂可以做到 10ppm/°C 以内。电阻分压的方案温漂在 100ppm/°C 以上温度变化 10°C 输出就漂几十毫伏。第二做两点校准。前面讲过方法这里再强调一下校准时要等芯片温度稳定刚上电时输出会有几十毫伏的漂移等五分钟再测。第三输出端加 RC 低通滤波。TLC5615 的更新率如果是 10kHz输出会有阶梯波加一个截止频率 1kHz 的 RC 滤波比如 1kΩ 0.1uF波形会平滑很多。第四PCB 布局时模拟部分和数字部分分开AGND 铺铜隔离。这个在洞洞板上做不到但打板时一定要注意。6.3 从 TLC5615 延伸到其他 DAC 芯片的选型思路TLC5615 是 10 位、单通道、电压输出。如果你需要更高分辨率可以看 12 位的 MCP4921 或 DAC8551需要多通道可以看 TLC5620四通道或 DAC8563双通道需要电流输出可以看 AD5421 或 XTR111。选型时主要看四个参数分辨率、通道数、接口类型、输出范围。TLC5615 的优势是便宜和简单劣势是分辨率只有 10 位参考电压范围窄。做一般控制够用做精密仪器就不行了。我在一个项目里用 TLC5615 做温度控制器的设定值输出10 位分辨率对应 0 到 100°C步进约 0.1°C完全够用。但另一个项目做应变片信号模拟需要 16 位分辨率就换成了 DAC8552。6.4 常见问题速查表现象可能原因排查方法输出始终为 0CS 未拉低、SCLK 无输出、DIN 接反用示波器看三个信号波形输出始终为满量程参考电压超限、数据全 1测量 Vref 引脚电压输出值约为设定值一半数据移位多移了一位检查左移位数输出值跳动大电源去耦不足、参考电压不稳加 0.1uF 电容、换基准芯片输出无法调到 0V输出缓冲器失调正常现象最小输出约几毫伏多片级联时数据错位DOUT 连接错误检查级联顺序和时钟极性这张表是我从实际调试记录里整理出来的覆盖了九成以上的问题。遇到异常时按表排查基本能定位到原因。7. 写在最后TLC5615 这颗芯片我用了快八年从最早的 51 开发板到后来的 STM32 产品它一直是我做电压输出时的默认选择。便宜、好买、不容易坏驱动逻辑也足够简单适合用来理解 DAC 的基本工作原理。如果你刚开始接触 DAC建议先用 Arduino 加 TLC5615 搭一个最小系统写个循环让输出电压从 0 慢慢升到满量程用万用表看着数值变化。这个过程能帮你建立对 DAC 的直观感受。然后再换到 51 或 STM32 上理解时序控制和硬件 SPI 的差异。实际做项目时别省那几毛钱的基准芯片和去耦电容。我见过太多因为参考电压不稳导致输出漂移的案例最后花在排查上的时间远超元件成本。把硬件基础打牢软件调试会顺利很多。
返回列表