ARTICLE DETAIL

资讯详情

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

STM32驱动DAC60501:I2C接口16位DAC精密电压源设计与实现

STM32驱动DAC60501:I2C接口16位DAC精密电压源设计与实现 做精密电压源这个项目的时候我在STM32上通过I2C总线驱动了一颗TI的DAC60501。这颗16位单通道DAC内部自带2.5V基准还支持二倍增益输出范围能从02.5V扩展到05V做信号源、滤波器调谐、激光器偏置、温控电路都很合适。网上讲DAC60501用法的资料不算多很多朋友卡在I2C时序和寄存器配置上。这篇把我实际调通的工程拆开讲从选型原因、硬件连接、寄存器地图、帧格式到基于STM32 HAL库的驱动代码一步一步过一遍最后是调试中踩过的坑。整个过程按我实际开发顺序来写先想清楚为什么选这颗芯片再搭硬件然后读手册配置寄存器最后写代码、调通、量产。你能直接照着复现也可以只挑自己关心的部分看。1. 项目整体设计与方案选型为什么选DAC605011.1 从需求出发一路高精度可调电压源需要什么我做的是一个便携式信号源模块模拟前端需要一路02.5V可调的精密电压基准用来给后面的VCO和滤波器提供偏置。这路电压有两个硬性要求一是分辨率要够细二是纹波要小三是MCU要能随时改值。刚开始想过三种方案。第一种是数字电位器比如MCP41010这类简单是简单但分辨率普遍只有8到10位温漂也大做精密偏置不太够看。第二种是PWM加RC滤波加运放这个方案便宜占用的引脚也少但PWM分辨率加上RC滤波的特性纹波很难压下去而且输出建立时间跟纹波是矛盾的想做12位以上的精细调节很费劲。第三种就是直接用集成的I2C接口DAC也就是最后选的方案。用DAC做电压源本质上就是把数字量转成模拟量SPI接口或者I2C接口都能实现。我选I2C的原因很直接只占两根线SDA和SCL总线还能同时挂温湿度传感器、存储芯片这些I2C设备对STM32的引脚压力小。DAC更新的速度要求又不高我这边输出电压变化一次也就是毫秒级的事I2C的100k或400k速率完全够用。1.2 DAC60501芯片特性与选型优势DAC60501是TI推出的16位单通道DAC供电范围2.7V到5.5V我用3.3V供电跑得很稳。它的几个关键特性我实际用下来觉得特别适合这种应用第一16位分辨率意味着2.5V满量程下单LSB大约是38uV二倍增益后5V满量程下单LSB约76uV。这个精度做常见的电压控制、偏置调节完全够肉眼在万用表上几乎看不出跳变。第二芯片内部自带2.5V基准源外部省掉一颗基准芯片BOM成本下来不少。第三它支持1倍和2倍两档增益通过寄存器切灵活度很高。第四上电默认输出0V对后级电路友好不用担心上电瞬间出现意外高压。第五I2C地址可以通过A0引脚组合出四个不同地址一条总线上挂四颗同样型号的DAC都没问题。相比同类产品DAC60501的手册里还带了偏移校准和内部基准微调寄存器做量产校准时很好用。我后面量产的板子就用TRIM寄存器对内部基准做了校正把初始误差拉回来不少。1.3 硬件连接方案引脚分配、上拉电阻与PCB布局DAC60501外围电路非常简单核心就几个点VDD接3.3V旁边放0.1uF和10uF两个去耦电容SDA和SCL各用一个4.7k电阻上拉到3.3VA0接GND把地址设成0x48VOUT直接引出。下面是参考连接STM32F103C8T6的PB6接SCLPB7接SDA对应I2C1外设。A0接GND时7位地址为0x48HAL库需要的8位写地址是0x90。输出端VOUT建议接一个10到100欧的小电阻再进负载必要时加一级运放缓冲能有效降低容性负载对建立时间的影响。如果要用二倍增益输出05VVDD最好选5V供电避免输出接近5V时压差不够。I2C总线为什么一定要上拉电阻这背后是不难理解的电气逻辑I2C协议用的是开漏输出结构设备只能把总线拉低不能主动拉高所以高电平必须靠外部电阻把SCL、SDA拉到电源电压。所有设备都靠这个机制共享总线任何一台设备拉低总线都能被其他设备感知到不会出现两个设备同时输出高低电平打架的问题。上拉电阻的取值则直接影响边沿速度阻值太大总线电容放电结束时充电太慢上升沿就软绵绵的远端设备直接判不出高电平阻值太小灌电流偏大功耗上去了某些从机的开漏能力还拉不低总线。3.3V系统常用4.7k总线长或设备多的时候换成2.2k甚至1k我实测下来4.7k在400k速率下最稳。2. 核心细节解析寄存器、I2C时序与数据格式2.1 寄存器地图写驱动前先背下来的几个关键寄存器DAC60501的操作本质上就是读写寄存器所以驱动代码的骨架就围绕寄存器展开。我实际用到的关键寄存器列在下表寄存器地址名称读写作用0x00NOP只写空操作用来验证通信链路0x01DEVICE_CONFIG读/写软件复位、禁用写保护等配置0x02DATA读/写写入DAC编码输出立即更新0x03GAIN_CONFIG读/写选择内部/外部基准设置1倍/2倍增益0x04OFFSET_CONFIG读/写偏移校准校正0x06DAC_CODE只读当前DAC编码回读校验用写驱动时最容易混淆的是0x02和0x06。0x02是写入寄存器你把想要的16位DAC编码写进去输出电压立刻跟着变0x06是只读寄存器用来把当前DAC编码读回来验证比如写完以后检查一下写入是否真的成功。这两个地址弄反代码还能编译通过逻辑分析仪上也看不出明显错误但输出电压就是纹丝不动或者读回来的数据始终是0。DEVICE_CONFIG寄存器里还藏着软件复位位需要做恢复默认配置时往0x01寄存器写一个带复位位的值就能把芯片复位到上电状态。我一般在初始化函数的最前面做一次软复位确保芯片从已知状态开始避免前一次运行残留的配置影响本次输出。2.2 I2C帧格式与读写时序从START到STOP逐位拆解I2C时序看起来很玄实际就是“起始、地址、寄存器、数据、停止”这几段拼起来的。DAC60501的写入帧格式是这样的在一张图上就能看清START条件SCL保持高电平期间SDA从高跳低。发送8位从机写入地址0x90对应7位地址0x48加最低位写标志0。等待从机ACK从机把SDA拉低表示收到。发送寄存器地址比如0x02。等待ACK。发送16位数据先高字节后低字节两个字节都要等ACK。STOP条件SCL高电平期间SDA从低跳高。读取帧稍微特殊一点因为从机需要先知道你要读哪个寄存器。常见做法是先发一次START写入寄存器地址然后通过重复起始条件再发一次读地址0x91从机这时候才会把指定寄存器的数据放到SDA上。主机连续读两个字节第一个字节后回ACK第二个字节后回NACK然后发STOP。用STM32 HAL库的时候HAL_I2C_Mem_Write和HAL_I2C_Mem_Read两个函数已经把重复起始这些细节封装好了不需要手动去拉SDA。但理解帧格式依然重要因为你后面做故障排查拿着逻辑分析仪看波形时必须能一眼认出哪段是地址、哪段是数据、哪个ACK丢了。2.3 16位数据与电压的换算写代码前先算明白DAC60501是16位DAC内部DAC编码范围是0到65535。输出电压和编码的关系用下面这个公式表示VOUT CODE / 65536 × VREF × GAIN默认情况下VREF是内部2.5V基准GAIN是1所以编码0对应0V编码65535对应约2.5V。如果通过GAIN_CONFIG把增益改成2满量程就变成约5V编码对应的电压也翻倍。我用一个实际例子走过一遍目标是输出2.0V使用内部2.5V基准、1倍增益。CODE 2.0 / 2.5 × 65536 52428.8四舍五入取52429换算成十六进制就是0xCCCD写入DATA寄存器后输出就是约2.0V。再看一个二倍增益的例子目标是4.5VVREF仍为2.5VGAIN设为2满量程5V。CODE 4.5 / 5.0 × 65536 58982.4取58982十六进制是0xE666。实际操作中浮点计算后一定要做四舍五入否则输出会稳定地偏差一两个LSB。我习惯用“(uint16_t)(voltage / vref / gain * 65536.0f 0.5f)”这种写法比如0.5V时理论编码13107.2四舍五入后13107printf打印出来误差只有0.3mV级别。3. 实操过程基于STM32 HAL库的驱动代码实现3.1 CubeMX配置I2C外设三步走我用的是STM32CubeMX配合HAL库工程从初始化到生成代码大概三步。第一步在Pinout配置页面把PB6设为I2C1_SCLPB7设为I2C1_SDA如果用的是其他芯片选对应的I2C引脚就行。第二步在Connectivity下面的I2C1配置里把速度设为Fast Mode也就是400kHz地址不填因为我们是主机。第三步时钟树确认APB1时钟正常I2C的工作频率就是按这个时钟算出来的。CubeMX生成以后I2C初始化函数长这样static void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }这里要注意Fast Mode 400kHz在STM32F103这种老芯片上APB1时钟必须正确配置否则实际I2C频率会偏。我一开始晶振起振有问题I2C怎么调都不稳定后来发现是时钟树没配好I2C速率偏了一大截。3.2 驱动代码核心寄存器读写函数逐行拆解驱动代码我分成了dac60501.c和dac60501.h两个文件这样移植到其他工程也方便。先看头文件里定义的寄存器和地址#ifndef DAC60501_H #define DAC60501_H #include main.h #define DAC60501_ADDR 0x48 #define DAC60501_WRITE_ADDR (DAC60501_ADDR 1) #define DAC60501_REG_NOP 0x00 #define DAC60501_REG_DEVICE_CONFIG 0x01 #define DAC60501_REG_DATA 0x02 #define DAC60501_REG_GAIN_CONFIG 0x03 #define DAC60501_REG_OFFSET_CONFIG 0x04 #define DAC60501_REG_TRIM 0x05 #define DAC60501_REG_DAC_CODE 0x06 void DAC60501_Init(void); uint8_t DAC60501_WriteReg(uint8_t reg, uint16_t data); uint8_t DAC60501_ReadReg(uint8_t reg, uint16_t *data); void DAC60501_SetVoltage(float voltage); #endif寄存器写函数用的是HAL_I2C_Mem_Write它会把设备地址、寄存器地址、数据一起打包发送比手动拼I2C帧省事太多uint8_t DAC60501_WriteReg(uint8_t reg, uint16_t data) { uint8_t buf[2]; buf[0] (data 8) 0xFF; buf[1] data 0xFF; if (HAL_I2C_Mem_Write(hi2c1, DAC60501_WRITE_ADDR, reg, I2C_MEMSIZE_8BIT, buf, 2, 100) ! HAL_OK) { return 1; } return 0; }注意两个细节。第一个数据要拆分高字节和低字节I2C传输是高字节在前这个顺序一旦反了输出值就会整个错位。第二个地址参数用的是DAC60501_WRITE_ADDR也就是0x48左移一位后的0x90因为HAL库期望的是包含读写标志的8位地址。读函数稍微绕一点HAL_I2C_Mem_Read内部会自动处理“先写寄存器地址再发读地址”的重复起始流程外部调用看起来只是一个大函数uint8_t DAC60501_ReadReg(uint8_t reg, uint16_t *data) { uint8_t buf[2]; if (HAL_I2C_Mem_Read(hi2c1, DAC60501_WRITE_ADDR, reg, I2C_MEMSIZE_8BIT, buf, 2, 100) ! HAL_OK) { return 1; } *data ((uint16_t)buf[0] 8) | buf[1]; return 0; }这两个函数是整个驱动的地基后面所有功能都建立在它们上面。如果你做主从双向通信里面某些参数可能需要改但就驱动DAC60501来说这两个函数已经够跑通全部寄存器操作了。3.3 输出电压控制从电压值到CODE的转换函数写电压的函数就是把前面公式变成代码。我考虑了两种增益情况并在输入电压超出量程时做了钳位避免计算出超出65535的非法CODEvoid DAC60501_SetVoltage(float voltage) { uint16_t code; float full_scale; full_scale 2.5f; if (voltage 0.0f) { voltage 0.0f; } if (voltage full_scale) { voltage full_scale; } code (uint16_t)(voltage / full_scale * 65536.0f 0.5f); DAC60501_WriteReg(DAC60501_REG_DATA, code); }上面的代码默认使用内部2.5V基准和1倍增益所以满量程就是2.5V。如果需要二倍增益输出05V初始化时先写GAIN_CONFIG寄存器把增益设为2然后同样调用SetVoltage内部逻辑里满量程就需要改成5.0V。初始化函数我写的是先软复位再显式配置增益和内部基准void DAC60501_Init(void) { DAC60501_WriteReg(DAC60501_REG_DEVICE_CONFIG, 0x8000); HAL_Delay(1); DAC60501_WriteReg(DAC60501_REG_GAIN_CONFIG, 0x0000); DAC60501_WriteReg(DAC60501_REG_DATA, 0x0000); }GAIN_CONFIG写0就是使用内部基准、1倍增益输出范围02.5V写1则把增益改成2量程翻倍。DEVICE_CONFIG写0x8000触发软件复位这个值写入后芯片内部会自动清除不需要额外恢复。3.4 完整工程结构一览CubeMX生成的工程加我新增的驱动文件目录结构大概这样Core/Inc/dac60501.hCore/Src/dac60501.cCore/Src/main.cCore/Src/i2c.cDrivers/STM32F1xx_HAL_Driver/其中i2c.c和main.c是CubeMX生成的我只在main.c里加了调用。dac60501.c和dac60501.h是我手动加的不依赖任何特定板卡换一块STM32板子只需要把头文件里的hi2c1改成对应的I2C句柄就行。主程序调用示例int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); DAC60501_Init(); while (1) { DAC60501_SetVoltage(1.25f); HAL_Delay(1000); DAC60501_SetVoltage(0.0f); HAL_Delay(1000); } }这段代码跑起来后用万用表量VOUT引脚能看到电压在1.25V和0V之间来回切换DAC60501驱动就算基本打通了。4. 常见问题与排查技巧实录4.1 最常见的坑地址、上拉电阻与总线电平I2C不通信十有八九是下面几个问题。第一个是地址不对。DAC60501的7位地址默认是0x48这个前提是A0引脚接GND。如果A0接了VDD、SCL或者SDA地址会变成其他值比如0x4B。用HAL库的时候还要把7位地址左移1位有人直接把0x48塞进HAL_I2C_Mem_Write总线上的实际地址就成了0x48对应的8位格式跟芯片不匹配自然没反应。第二个是上拉电阻。我遇到过上升沿太缓导致通信时好时坏的情况。总线电容大、上拉电阻10k400kHz模式下边沿根本来不及拉高芯片就会漏判。排查方法很简单用示波器看SCL或者SDA的波形如果上升沿是慢慢爬上去的不是陡峭的方波就把上拉电阻往小换。3.3V系统用4.7k设备多、总线长就换2.2k甚至1k。第三个是总线被拉死。如果SDA一直为低多半是某个从机没供电或者处于异常状态把总线卡住了。把DAC60501的VDD断开看总线能不能恢复高电平就能定位问题。4.2 输出电压不对先查基准和增益配置输出跟预期差两倍或者怎么调都是0排查思路按优先级来。先用逻辑分析仪或直接读DATA寄存器确认写入的CODE是否正确。如果CODE本身就是错的回头看寄存器地址是不是写成了0x01而不是0x02这个错误很隐蔽因为0x01是配置寄存器写了也不会报错。再确认基准源。我有一块板子误开了外部基准模式内部2.5V基准没启用输出一直不对。初始化里明确写一次GAIN_CONFIG把内部基准和增益固定下来就能排除这类遗漏。如果输出值比预期小了一半多半是GAIN_CONFIG增益没有设为2倍而代码里又按5V满量程算CODE。反过来输出被钳在2.5V说明你设置的电压超过当前满量程但DAC60501的满量程还是2.5VSetVoltage函数里做过钳位这个也容易被忽略。4.3 逻辑分析仪抓时序怎么看出问题在哪调试I2C逻辑分析仪是我最常用的工具几十块钱的8通道设备就够了关键是采样率要够至少设到20MHz以上否则400kHz的信号细节抓不全。接线就是SDA和SCL分别接到逻辑分析仪的通道共地一定要接。抓完波形以后优先做三件事。第一确认起始信号的位置是否出现在数据的最前面也就是SCL高电平期间SDA从高跳低。第二确认从机地址那一串电平换算成十六进制是不是0x90。如果有人改了A0接线这一眼就能看出来。第三确认ACK位从机收到地址后SDA要被拉低一个时钟周期。如果ACK一直没出现说明从机根本没响应要么地址不对要么VDD没供上要么复位没释放。有一次我写寄存器老失败逻辑分析仪上看ACK也正常后来才发现是软件里把数据高字节和低字节传反了DAC60501收到了错误的DATA值输出电压完全不对。这类问题光看代码不容易发现但对着波形一帧一帧对很容易定位。最后再分享几个经验调试这类I2C器件我的一般流程是先让芯片响应再写寄存器最后调精度。第一步确认能识别设备地址哪怕只做一次NOP写入都行。第二步写DATA寄存器并读回确认读写链路完整。第三步接万用表量电压确认数值换算没问题。这套流程走完DAC60501这个级别的芯片基本就稳定了。一个小技巧如果总线上打算挂多颗同型号DACA0引脚接GND、VDD、SCL、SDA分别对应四个地址理论上一条I2C总线可以控制四颗DAC60501。我有一条I2C总线上同时挂过两颗DAC和一颗温度传感器的板子只要每颗芯片的地址不冲突初始化顺序合理跑起来很稳定几乎没有额外的心跳代码。驱动代码整体量不大但里面的细节不少。把寄存器地图、帧格式和电压换算这三样吃透DAC60501其实就是一个“写寄存器就能干活”的简单外设。后面做精密电流源、程控增益放大器这些项目时直接拿这套驱动改一改就能用。
返回列表