十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32与DAC60501 I2C通信实战:从寄存器配置到高精度电压输出

STM32与DAC60501 I2C通信实战:从寄存器配置到高精度电压输出 DAC60501与STM32的I2C通信实战从寄存器配置到电压输出做模拟量输出项目时最常用的方案就是STM32加DAC芯片。这次我选的是TI的DAC6050116位分辨率、I2C接口、内置2.5V参考电压搭配STM32的硬件I2C外设可以实现0到2.5V或者通过增益配置到更宽范围的高精度电压输出。整个项目踩了不少坑尤其是寄存器配置和I2C时序对接的细节网上中文资料不多所以把完整的调试过程整理出来给正在做信号发生器、可调电源、模拟量输出模块的开发者做个参考。内容会从器件选型、硬件连接、I2C协议要点、寄存器配置到最终代码实现和实测验证一步步讲清楚。DAC60501的寄存器不算复杂但配置字写错一位就可能完全没输出所以我会把每个关键位的含义和为什么要这样配都讲明白方便你直接抄作业也能在遇到问题时快速定位。1. 项目概述与整体设计思路1.1 为什么选择DAC60501这颗芯片选DAC60501主要有几个考虑。首先是分辨率16位意味着在2.5V满量程下一个LSB对应大约38微伏做精密电压源、波形发生器、激光器电流控制这类需要细粒度调节的场景完全够用。其次是内置参考电压一颗2.5V的基准源如果外置的话精度稍好的至少要几块钱还得多占PCB面积DAC60501直接集成在片内省事也省钱。接口方面I2C只需要SCL和SDA两根线不像SPI需要额外的CS引脚对于引脚紧张的STM32项目更友好。而且DAC60501支持标准模式100kHz和快速模式400kHz数据更新速率足够快。芯片本身支持2.7V到5.5V供电3.3V系统直接供电非常方便。同类竞品比如MCP4725也是16位I2C DAC但内部没有集成高精度参考满量程由VDD决定电源波动会直接影响输出精度。DAC60501用独立参考则把电源纹波隔离在了参考之外精度稳定性更好。1.2 整体技术架构与实现路径这个项目的软件结构很简单从下往上分了三层驱动层STM32的I2C1外设初始化基于HAL库封装寄存器读写函数芯片层DAC60501的初始化、寄存器配置、电压转换与写入应用层设定目标电压调用转换函数输出STM32作为I2C主机DAC60501是唯一的从机设备。主机通过I2C总线对DAC芯片的寄存器进行读写。写DAC寄存器后芯片立即将数字码值转换为模拟电压输出不需要额外的更新命令。选用STM32F103C8T6的硬件I2C外设而不是GPIO模拟I2C是为了让时序由硬件保证。早期STM32F1的硬件I2C确实有各种争议但绝大多数问题其实出在配置上比如GPIO模式选错、时钟配置不对。HAL库成熟之后硬件I2C非常稳定而且不占用CPU去翻转引脚代码也简洁很多。1.3 核心知识点前置整个项目有三个关键点需要提前理解寄存器配置DAC60501的核心寄存器只有几个最常用的是DAC数据寄存器、配置寄存器、增益寄存器。配置寄存器的位定义直接决定了参考电压是否启用、是否分频、DAC是否处于正常工作模式。电压计算公式最终输出电压由DAC码值、参考电压、增益共同决定。I2C时序DAC60501要求16位数据高字节先发寄存器地址只有8位。读写时序略有差异读操作中间需要一次总线重启。这三个点串起来就是完整的DAC60501驱动流程下面我从硬件开始逐步展开。2. 硬件连接与电路设计2.1 所需器件清单实际测试用到的器件如下STM32F103C8T6最小系统板3.3V供电DAC60501模块或裸片我用的是贴好引脚的模块方便手工搭线4.7kΩ贴片电阻两个用于I2C上拉100nF陶瓷电容一个10µF电解电容一个用于电源滤波USB转TTL模块用于串口打印调试信息逻辑分析仪调试I2C通信必备建议人手一台精度不低于0.5%的数字万用表用于测量输出电压如果手头没有DAC60501模块直接买芯片焊接也可以但要注意它是10引脚VSON封装手工焊接需要热风枪或者比较好的烙铁头建议先在废板上练手。2.2 引脚连接表与电路说明接线非常简单把STM32和DAC60501对应引脚连起来即可STM32引脚DAC60501引脚说明PB6 (I2C1_SCL)SCLI2C时钟线PB7 (I2C1_SDA)SDAI2C数据线3.3VVDD电源正极GNDGND电源地-VOUTDAC模拟输出接万用表GNDADDR0地址引脚0接地GNDADDR1地址引脚1接地这里有个细节需要注意STM32F103的I2C1复用引脚就是PB6和PB7如果用I2C2则对应PB10和PB11。DAC60501的VDD和GND之间要放一个100nF陶瓷电容就近滤波VDD输入处再放一个10µF的电解电容吸收低频纹波。如果VOUT要驱动负载建议加一级运放缓冲。DAC60501的输出驱动能力有限带不了重负载不过我们测试阶段只接万用表芯片直接输出没有问题。2.3 I2C地址配置与通信速率选择DAC60501的7位I2C地址由ADDR0和ADDR1两个引脚的电平状态决定ADDR1ADDR07位地址GNDGND0x48GNDVDD0x49VDDGND0x4AVDDVDD0x4B我们使用默认接法两个都接地所以7位地址是0x48。在STM32的HAL库中地址参数需要左移一位作为8位地址最低位是读写位所以实际使用的地址是0x90写和0x91读。这是我最初调试时忘掉的一个坑后面还会细说。通信速率方面DAC60501最高支持400kHz快速模式STM32的I2C1也支持所以初始化时直接配置为400kHz。如果你的I2C走线很长超过10cm或者有寄生电容建议降到100kHz避免信号失真导致通信失败。我的板子走线很短400kHz跑得很稳定。3. I2C协议要点与STM32配置3.1 DAC60501的I2C读写时序详解DAC60501的寄存器是8位地址、16位数据宽度。写一个寄存器的完整时序是主机发送START条件主机发送从机地址加写位0x90从机应答ACK主机发送8位寄存器地址从机应答ACK主机发送数据高字节DAC寄存器是16位先高后低从机应答ACK主机发送数据低字节从机应答ACK主机发送STOP条件读一个寄存器的时序稍微复杂主机发送START条件主机发送从机地址加写位0x90指定要读的寄存器从机应答ACK主机发送8位寄存器地址从机应答ACK主机发送STOP条件或者Repeated START条件主机发送从机地址加读位0x91从机应答ACK从机发送数据高字节主机应答ACK从机发送数据低字节主机发送NACK表示接收完成主机发送STOP条件第6步用Repeated START还是先STOP再STARTDAC60501数据手册中两种方式都支持。HAL库的两次独立传输先Transmit后Receive会在中间自动插入STOP和START实际测试没有问题。如果只是想验证I2C通信是否正常可以往NOP寄存器地址0x00写任意数据芯片不会产生任何效果但会在总线上回复ACK。如果连NOP寄存器写入都失败那通信链路肯定有问题。3.2 STM32硬件I2C初始化基于STM32CubeMX生成初始化代码手动补充GPIO配置。关键配置项如下// I2C1句柄 I2C_HandleTypeDef hi2c1; void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz快速模式 hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; // 占空比2:1 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(); } }GPIO配置是I2C能否工作的关键。PB6和PB7必须配置为开漏复用模式GPIO_MODE_AF_OD并启用内部上拉。开漏输出是为了满足I2C的线与逻辑多设备共享总线时不会产生冲突。void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (hi2c-Instance I2C1) { __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; // 开漏复用 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); } }有人会问既然引脚配了内部上拉为什么还要外部上拉电阻STM32的内部上拉电阻大约在30kΩ到50kΩ之间阻值偏大在400kHz下驱动能力不足上升沿会变得很缓导致通信不稳定。我实测过只靠内部上拉在100kHz下勉强能工作在400kHz下大概率出现随机通信失败。所以外部必须加上拉阻值选2.2kΩ到4.7kΩ比较合适。3.3 上拉电阻选型的实际经验上拉电阻的选择直接影响I2C信号的上升时间和功耗。阻值越小上升沿越陡峭信号质量越好但总线空闲时流过电阻的静态电流也越大。阻值越大功耗越低但上升沿越缓限制了最高通信速率。对于3.3V供电、400kHz快速模式、总线电容典型情况4.7kΩ是比较推荐的起始值。如果总线很长或者挂载设备很多可以考虑降到2.2kΩ。如果只追求低功耗且速率只有100kHz10kΩ也能用但这是我的底线再大就容易出问题。实测中还发现一个现象上拉电阻如果焊接不良或者虚焊I2C通信会是间歇性失败的有时候上电能通过一会又不行了。排查这类问题最好的工具就是逻辑分析仪和示波器直接看SCL和SDA的波形一眼就能看出信号质量问题。4. DAC60501寄存器详解与配置流程4.1 寄存器地图速览DAC60501的寄存器少而精实际用到的只有两三个。完整地图如下寄存器名称地址读写属性功能说明NOP0x00只写空操作用于验证通信DAC0x02可读写DAC数据寄存器16位码值CONFIG0x03可读写配置寄存器控制参考和断电模式GAIN0x04可读写增益寄存器配置输出增益GENERAL0x08可读写通用寄存器含软复位位SYNC0x0A可读写同步寄存器用于多芯片同步SYNC寄存器在单芯片场景下不用管保持默认值即可。如果做多通道同步输出需要研究一下这个寄存器的配置但那是更复杂的应用场景这篇实战暂时不展开。4.2 配置寄存器与增益寄存器的关键位配置寄存器地址0x03是我们初始化时第一个要写的寄存器。它的关键位定义如下bit15 REF_PWDWN内部参考掉电控制位写0表示内部参考上电启用写1则禁用bit14 DIVIDER参考分频控制位写0表示参考电压直通2.5V写1表示参考电压除以21.25Vbit13-8保留位写0bit7-6 DAC_PWDWNDAC掉电模式控制位写00表示正常工作写01表示输出通过1kΩ电阻接地写10表示输出通过100kΩ电阻接地写11表示输出高阻态bit5-3保留位写0bit2 R2R内部电阻梯形网络控制位一般写0bit1-0保留位写0所以初始化时配置寄存器写入0x0000含义是内部参考上电、参考不分频2.5V、DAC正常工作。增益寄存器地址0x04更简单bit1 GAIN增益选择位写0为1倍增益写1为2倍增益bit0 GAIN_UPDATE增益更新位写1表示将GAIN位的配置立即应用到输出写0则GAIN位的改动不会生效写入0x0003表示设置为1倍增益并立即更新。为什么1倍增益对应的值是0x0003而不是0x0001这就是GAIN_UPDATE位的作用。如果只写0x0001GAIN位虽然设置了但不会生效输出依然是默认状态。很多人在这一步踩坑电压怎么调都不对就是这个位没置1。DAC数据寄存器地址0x02就是纯16位数据对应输出码值范围从0x0000到0xFFFF。写入后DAC立即可变输出不需要额外触发信号。4.3 输出电压计算公式与常用码值DAC60501的输出电压计算公式为Vout (CODE / 65536) × VREF × GAIN / DIVIDER其中CODE是写入DAC寄存器的十进制码值VREF是参考电压内部参考一般认为是2.5V实际值建议测量确认GAIN是增益配置1或2DIVIDER是分频系数1或2。默认配置下VREF2.5VGAIN1DIVIDER1所以输出范围是0到2.5V。如果想输出0到5V可以将GAIN配置为2但要注意DAC60501的VDD不能低于输出电压如果VDD3.3V即使GAIN2输出也最多到接近3.3V就会饱和所以3.3V供电时老老实实用1倍增益就好。下面是一些常用输出电压对应的码值目标电压码值(hex)码值(dec)理论输出(V)0.000V0x000000.0000.500V0x3333131070.5001.000V0x6666262141.0001.250V0x8000327681.2502.000V0xCCCD524282.0002.500V0xFFFF655352.5001.000V对应的码值计算过程CODE (1.0 / 2.5) × 65536 26214.4取整后26214写成十六进制就是0x6666实际输出约为0.9999V误差在1个LSB以内。4.4 初始化流程的合理顺序DAC60501初始化建议按以下顺序操作写GENERAL寄存器0x08为0x0001触发软复位让芯片回到默认状态延时10ms等待复位完成写CONFIG寄存器为0x0000配置参考和正常工作模式写GAIN寄存器为0x0003设置1倍增益并立即生效写DAC寄存器为0x0000初始输出0V先复位再配置可以保证芯片处于已知状态。如果跳过复位直接配置芯片可能处于掉电模式或错误配置输出行为不可预期。5. 实操从代码到实测电压5.1 寄存器读写函数的完整实现基于HAL库封装DAC60501的读写函数代码可以直接复制使用#include main.h #define DAC60501_ADDR 0x48 // ADDR0GND, ADDR1GND #define DAC60501_ADDR_W (DAC60501_ADDR 1) // 0x90 #define DAC60501_ADDR_R ((DAC60501_ADDR 1) | 1) // 0x91 #define DAC60501_REG_NOP 0x00 #define DAC60501_REG_DAC 0x02 #define DAC60501_REG_CFG 0x03 #define DAC60501_REG_GAIN 0x04 #define DAC60501_REG_GENERAL 0x08 // 写16位数据到指定寄存器 HAL_StatusTypeDef DAC60501_WriteReg(uint8_t reg, uint16_t data) { uint8_t buf[3]; buf[0] reg; buf[1] (data 8) 0xFF; // 高字节先发 buf[2] data 0xFF; // 低字节后发 return HAL_I2C_Master_Transmit(hi2c1, DAC60501_ADDR_W, buf, 3, 100); } // 从指定寄存器读取16位数据 HAL_StatusTypeDef DAC60501_ReadReg(uint8_t reg, uint16_t *data) { HAL_StatusTypeDef status; uint8_t buf[2]; // 先发送寄存器地址 status HAL_I2C_Master_Transmit(hi2c1, DAC60501_ADDR_W, reg, 1, 100); if (status ! HAL_OK) { return status; } // 重新启动并读取2字节数据 status HAL_I2C_Master_Receive(hi2c1, DAC60501_ADDR_R, buf, 2, 100); if (status HAL_OK) { *data (buf[0] 8) | buf[1]; } return status; }这里注意HAL_I2C_Master_Transmit的地址参数是8位地址含读写位所以要把7位地址左移一位。我在这个细节上翻过车写0x48进去HAL库实际发送的地址就完全不对了从机永远不回ACK。5.2 初始化与电压设置函数初始化函数按前面说的5步流程实现void DAC60501_Init(void) { // 1. 软复位 DAC60501_WriteReg(DAC60501_REG_GENERAL, 0x0001); HAL_Delay(10); // 2. 配置寄存器内部参考使能不分频正常工作 DAC60501_WriteReg(DAC60501_REG_CFG, 0x0000); // 3. 增益寄存器1倍增益立即更新 DAC60501_WriteReg(DAC60501_REG_GAIN, 0x0003); // 4. 初始输出0V DAC60501_WriteReg(DAC60501_REG_DAC, 0x0000); }设置目标电压的函数输入是浮点电压值内部计算码值并写入DAC寄存器HAL_StatusTypeDef DAC60501_SetVoltage(float voltage) { uint16_t code; uint32_t code_raw; // 默认配置下满量程2.5V if (voltage 0.0f) voltage 0.0f; if (voltage 2.5f) voltage 2.5f; // CODE (Vout / 2.5) * 65536 code_raw (uint32_t)((voltage / 2.5f) * 65536.0f); // 处理浮点截断误差防止越界 if (code_raw 65535) code_raw 65535; code (uint16_t)code_raw; return DAC60501_WriteReg(DAC60501_REG_DAC, code); }如果需要更高精度的电压设置可以加入实际参考电压的校准。比如实测内部参考为2.496V那么计算公式就改为CODE (Vout / 2.496) × 65536。这个在后面输出精度分析一节再展开。主函数的调用示例int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); // 初始化DAC60501 DAC60501_Init(); // 回读配置寄存器确认初始化成功 uint16_t cfg_value 0; if (DAC60501_ReadReg(DAC60501_REG_CFG, cfg_value) HAL_OK) { // 应该读到0x0000 } // 输出1.000V DAC60501_SetVoltage(1.0f); while (1) { // 主循环可以动态调整电压 } }5.3 用逻辑分析仪验证I2C时序代码写完后不要急着用万用表量电压先用逻辑分析仪把I2C波形抓出来看一眼。把逻辑分析仪的通道0接到SCL通道1接到SDA地线接GND设置采样率1MHz以上然后触发一段写DAC寄存器的操作。正常波形应该是这样的SCL是规则的方波SDA在SCL低电平时变化在SCL高电平时保持稳定。抓包后检查几个关键点是否有有效的START条件SDA在SCL高电平时拉低第一个字节是否0x90地址加写位每个字节后是否有ACK低电平数据字节顺序是否高字节在前我第一次调试时因为地址写错从机一直没有ACK波形上所有数据字节后面都是NACK。用逻辑分析仪一眼就发现了问题省去很多猜测时间。如果没有逻辑分析仪也可以用示波器但解码效率低很多。5.4 实测输出电压记录配置好逻辑分析仪和万用表后依次设置几个目标电压记录实测值目标电压写入码值实测电压误差0.000V0x00000.001V1mV0.500V0x33330.499V-1mV1.000V0x66661.000V0mV1.250V0x80001.250V0mV2.000V0xCCCD2.001V1mV2.500V0xFFFF2.498V-2mV整体误差在正负2mV以内对于16位DAC的约38µV LSB来说这个结果考虑到万用表本身精度和参考电压偏差已经完全满足一般应用需求。如果追求更高精度可以实测内部参考电压后在代码里做校准。校准的思路其实很简单先让DAC输出理论上的最大码值0xFFFF用万用表量到实际输出电压假设是2.498V说明实际参考电压偏低。那么软件计算码值时把公式里的2.5替换为2.498即可。实际操作中可以测两点比如0.5V和2.0V做线性校准效果更好。6. 常见问题与排查技巧实录6.1 I2C总线无应答现象HAL_I2C_Master_Transmit返回HAL_ERROR或者HAL_BUSY逻辑分析仪上看到芯片不回复ACK。排查步骤先检查I2C地址。很多人直接把数据手册的0x48填进HAL库函数实际应该左移一位变成0x90。这个错误占无应答问题的五成以上。检查SCL和SDA有没有接反。I2C两根线接反后从机需要地址匹配时候选条件满足才能应答但SCL和SDA互换后它的状态机根本收不到完整地址表现为一直无应答。确认上拉电阻确实存在且阻值合理。用万用表量SCL和SDA对VDD的电压空闲时应该都接近3.3V。检查DAC60501的供电。VDD引脚必须有2.7V以上的电压芯片才会启动I2C接口。如果以上都正常把通信速率降到100kHz再试一次。6.2 输出电压始终为0现象I2C通信正常寄存器写入返回成功但VOUT引脚始终是0V。排查步骤检查配置寄存器的DAC_PWDWN位。如果之前配置过掉电模式输出会被拉低到地或者变成高阻。回读CONFIG寄存器确认当前值是否为正常的0x0000。检查增益寄存器的GAIN_UPDATE位。只写了GAIN位没用必须同时置1GAIN_UPDATE才会生效但GAIN配错一般只是电压范围不对不会完全为0。万用表表笔是否接触良好VOUT引脚是否有虚焊。如果排插了DAC寄存器写入的数据字节顺序。16位数据必须先发高字节再发低字节顺序反了会写进一个错误的码值。6.3 输出电压偏高或偏低现象输出有电压但和目标值有固定偏差比如目标1.000V实测1.005V。排查步骤确认内部参考电压的真实值。数据手册上参考电压有初始误差虽然不是大问题但会影响输出精度。用万用表量VOUT在满码值下的输出反推实际参考电压。确认万用表精度。手持万用表直流电压档精度一般在0.5%左右测量小电压时还有分辨率限制建议至少用4位半万用表。如果输出接了负载还要考虑DAC的输出阻抗和负载电流带来的电压跌落。这时候需要运放缓冲。6.4 排查利器回读寄存器DAC60501支持寄存器回读这是排查问题最好用的功能。每次写入后立即回读确认写入的值和读到的值一致。排查步骤建议这样初始化完成后依次回读CONFIG寄存器应为0x0000、GAIN寄存器应为0x0003、DAC寄存器应为设定值。如果回读值不对说明写入过程有数据位错误重点检查I2C时序和数据字节顺序。我习惯在调试阶段把所有I2C函数返回值通过串口打印出来哪个步骤出错一目了然HAL_StatusTypeDef status; status DAC60501_WriteReg(DAC60501_REG_CFG, 0x0000); printf(Write CONFIG: %d\r\n, status);这样即便没有逻辑分析仪也能通过串口日志快速定位是哪一步失败。6.5 一批容易忽略的小细节DAC60501的I2C地址是7位地址但HAL库参数要求8位地址两者差一个左移操作DAC数据寄存器是16位但有些类似芯片只用了高12位或高14位比如MCP4725就是12位数据左对齐DAC60501是完整16位不需要左移处理写DAC寄存器后输出电压是立即更新的不需要额外写触发寄存器模块如果带有VIO引脚电平转换引脚要接到和MCU相同的电平否则上拉电压不匹配电源上电瞬间DAC输出是不确定的如果下游设备对电压敏感建议在DAC输出端加一个放电电阻或者由MCU控制的外部开关7. 项目扩展与个人体会这个DAC60501加STM32的方案调试完成后我把它用在了两个实际项目中一个是给激光二极管驱动板提供精确的电流设定电压另一个是做简易信号发生器。第一代信号发生器只做了固定频率的三角波和正弦波查表输出利用DAC60501的高分辨率波形量化噪声很低配合运放做输出级效果比预想的好。如果想扩展到更高频率可以考虑提高I2C速率或者换STM32G4系列但那是另一个话题了。最后分享一个我踩过的坑当时为了省事没有念清楚DAC60501的数据手册就按MCP4725的时序写代码结果地址没有左移、数据字节顺序也搞反了通信建立不起来。后来老老实实把数据手册的I2C章节读了把所有寄存器位定义抄到本子上半小时就调通了。DAC60501本身不难难的是不要想当然所有细节都要回到数据手册上找依据。
返回列表