ARTICLE DETAIL

资讯详情

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

STM32驱动DAC60501:16位I2C DAC硬件设计到HAL库实现

STM32驱动DAC60501:16位I2C DAC硬件设计到HAL库实现 最近做的一个项目里需要一路精确可调的模拟电压输出主控是STM32F103原先方案想用PWM加RC滤波但纹波和线性度都撑不住需求后来换了TI的DAC60501这颗16位I2C接口DAC顺手把驱动代码完整沉淀了下来。这篇文章会把DAC60501从硬件电路到HAL库驱动的完整实现拆开讲清楚包括I2C协议里那些经常让人踩坑的细节上拉电阻、时序、地址配置以及我在实测中遇到的一些问题和排查思路。如果你正在用STM32驱动I2C接口的DAC或者其他I2C从设备这篇文章应该能帮你少走不少弯路。1. DAC60501芯片特性和选型分析1.1 这颗芯片解决什么问题在很多嵌入式系统里单片机的输出要么是数字量GPIO高低电平、要么是PWM脉冲但很多场景需要的是连续可调的模拟电压。比如可调电源的给定电压、传感器校准用的偏置电压、射频模块的功率控制、甚至仪表放大器调零这些都需要真正的高精度模拟电压输出而不是靠PWM滤波硬凑。DAC60501就是专门干这个的。它是TI推出的一颗16位单通道电压输出DAC分辨率为65535级理论上写1个LSB对应约76微伏以0到5V满量程计算。这个精度如果用PWM来做不说滤波电路面积光线性度和温漂就够折腾的。1.2 芯片核心特性速览我用了一段时间后整理了一份DAC60501的核心特性表方便大家在选型时快速对比特性参数说明分辨率16 bit实际有效精度受基准源和布局影响通道数1单通道多路输出可级联多颗接口I2C支持标准模式100kbps、快速模式400kbps、高速模式3.4Mbps内置基准2.5V温漂典型值约5ppm/°C输出范围0~2.5V / 0~5V通过配置寄存器切换增益供电电压2.7V~5.5VAVDD供电范围上电状态输出0V带有上电复位POR功能地址配置3位硬件引脚同一I2C总线最多挂8颗这颗芯片最方便的地方是内置了2.5V基准省掉了外部基准源电路。如果项目对精度要求更高也可以关闭内部基准从REF引脚外部输入更高精度的基准电压。1.3 为什么选它而不是其他方案我选DAC60501主要基于三个理由第一是精度和位数正好够用。16位分辨率在大多数工业控制场景下已经非常充裕相比12位的MCP4725DAC60501在整体精度、温漂和集成度上都高一个档次。第二是I2C接口通用性强。STM32自带硬件I2C外设不用额外占用定时器和GPIO资源做软件模拟时序。而且I2C只需要两根线PCB布线压力小。第三是芯片体积小。DAC60501有WSON和SOT-23两种封装SOT-23-8封装非常小不占板子面积。如果项目里有多路输出需求可以用它自带的地址引脚在同一I2C总线上挂多颗互不干扰。当然如果预算非常敏感且对精度要求不高MCP4725或者普通的8位DAC方案也不是不行但就我实际测试下来的效果看DAC60501的线性度和输出稳定性明显优于同价位的其他方案。2. I2C通信协议关键机制2.1 从时序图理解I2C的完整通信过程I2C这个协议网上教程一大堆但我发现很多刚接触的读者卡在时序图上。用一句话概括I2C的通信过程主机在总线上发起一个带有从机地址的帧从机地址匹配的芯片应答后双方按约定好的格式收发数据每收一个字节应答一次直到主机发出停止信号或无需应答为止。拿STM32和DAC60501通信举例一次完整的寄存器写操作在示波器上看起来是这样的时序主机拉低SCL和SDA生成起始条件START主机发送7位从机地址加1位写标志总共8位最后一位为0例如0x48左移1位变成0x90从机在第9个时钟周期拉低SDA作为应答信号ACK主机发送8位寄存器地址例如0x00对应DAC数据寄存器从机再次应答主机发送16位数据高字节在前低字节在后分两个字节发送从机每收到一个字节应答一次主机在SCL高电平期间将SDA从低拉高生成停止条件STOP通信结束这个过程中最关键的一点是SDA线上的数据变化必须发生在SCL低电平期间SCL高电平期间SDA必须保持稳定。这就是I2C和UART这类异步协议最大的区别。如果两条线上信号的建立时间和保持时间不符合要求从机就会产生误判。很多人在用逻辑分析仪抓I2C数据时发现波形非常乱多半是时序问题或者是上拉电阻阻值选错了这个问题后面专门说。2.2 为什么I2C必须开漏输出加外部上拉I2C协议规定所有连接到总线上的设备其SDA和SCL引脚必须是开漏Open-Drain结构。所谓开漏就是芯片内部只能把引脚拉低到地不能主动输出高电平。引脚悬空时的电平状态完全由外部上拉电阻决定。这是I2C总线接入多个设备时能够正常工作的重要机制。想象一下总线上挂了MCU、DAC、温度传感器、EEPROM四个设备如果每个设备都能主动输出高电平当一个设备输出高、另一个设备输出低时两个设备的输出级就会形成直接回路轻则逻辑混乱重则烧毁芯片。而开漏加外部上拉的结构天然实现了线与逻辑所谓线与就是只要总线上任何一个设备把线拉低整条总线就是低电平只有在所有设备都不拉低的情况下上拉电阻才会把线拉到高电平。这样一来多个设备共享一条总线既不会产生驱动冲突也方便通过应答机制让从机主动反馈状态从机应答时就是主动拉低SDA。STM32的GPIO配置成开漏输出模式其实也只是把引脚内部的上拉或推挽驱动关掉了。这里有一个细节点STM32的I2C引脚内部没有集成合适的上拉电阻所以外部必须接上拉电阻。有人问如果STM32内部配置了上拉是不是可以省掉外部上拉答案是不行的原因见下一节。2.3 上拉电阻阻值怎么选上拉电阻的选择本质上是在两个约束条件之间做权衡一个是低电平时的灌电流不能超过设备的承受能力另一个是高电平时SDA和SCL的上升时间不能太慢。电阻值太小总线空闲时流过上拉电阻的电流就大。以3.3V供电为例如果上拉电阻用330欧姆那么总线低电平时从机引脚内部下沉的电流就是10毫安这个电流对很多MCU的IO口来说已经处于极限边缘了发热和电压偏移都会成为问题。电阻值太大RC充电时间常数变大上升沿变缓。I2C协议对上升时间有明确要求标准模式100kHz下上升时间不超过1000纳秒快速模式400kHz下不超过300纳秒。如果上拉电阻配得太大波形上升沿过慢从机采样时看到的可能还是低电平通信自然就失败了。我实测过一组数据在3.3V供电条件下使用不同上拉电阻对400kHz快速模式的影响上拉电阻实测上升时间通信稳定性1kΩ约120ns稳定2.2kΩ约250ns稳定4.7kΩ约550ns基本稳定但边缘紧张10kΩ约1.15us不稳定偶发通信失败所以我的经验是400kHz快速模式下建议用2.2kΩ100kHz标准模式下用4.7kΩ超过400kHz的高速模式建议直接看数据手册给的推荐值。还有一个值得注意的是总线上挂的设备越多总线的等效电容就越大此时为了维持同样的上升时间可能需要适当减小上拉电阻。3. 硬件连接设计和常见坑3.1 STM32与DAC60501引脚连接表DAC60501的引脚不算多但有几个细节需要注意。我使用STM32F103C8T6搭配DAC60501连接方式如下DAC60501引脚功能连接到STM32说明SDA数据线PB7I2C1_SDA必须接上拉电阻到VDDIOSCL时钟线PB6I2C1_SCL必须接上拉电阻到VDDIOADDR0地址配置0GND或VDDIO决定I2C从机地址ADDR1地址配置1GND或VDDIO决定I2C从机地址VDD模拟电源3.3V必须加0.1uF退耦电容VIOIO电源3.3VI2C接口电平参考GND地GND注意单点接地VOUT模拟输出接负载/示波器输出范围由配置决定需要特别注意的是VDD和VIO这两个电源引脚。DAC60501的VDD是模拟部分供电VIO是数字接口的电平参考。如果VIO接的电压和STM32的IO电压不一致I2C通信可能会异常。比如STM32是5V供电、DAC60501的VIO接3.3V这时I2C的高电平只有3.3V接近STM32的IO高电平阈值边缘容易导致读不到正确的ACK应答。最稳妥的做法是VDD和VIO接同一个电源轨避免跨电压域通信。3.2 通过硬件引脚配置I2C从机地址DAC60501的I2C从机地址由ADDR0和ADDR1两个引脚以及一个固定前缀组成。不同厂家的DAC地址引脚数量和编码方式不一样DAC60501的7位从机地址格式为1001 ADDR1 ADDR0 R/W位所以同一总线上最多可以挂4颗不同地址的DAC60501。ADDR0和ADDR1的连接方式决定了7位地址的低两位ADDR1ADDR0设备地址7位写地址8位GNDGND0b10010000x90GNDVDDIO0b10010010x92VDDIOGND0b10010100x94VDDIOVDDIO0b10010110x96写地址是7位地址左移一位得到的。在STM32的HAL库中HAL_I2C_Master_Transmit函数接收的DevAddress参数是8位的即7位地址左移一位后加上读写位。这个细节非常容易搞错。如果你发现I2C从机始终无应答先用万用表量一下ADDR0和ADDR1的电平确认地址正确。3.3 电路设计中的常见坑DAC60501是模拟器件电路设计的心法和数字芯片不太一样。我最开始画的第一版板子VOUT输出电压总有几十毫伏的周期性纹波后来排查发现是电源退耦没做好。DAC60501的VDD引脚需要加一个0.1uF的陶瓷电容并且这个电容要尽量靠近引脚放置引线越短越好。如果可能的话再加一个10uF的钽电容做低频退耦。很多DAC芯片的输出噪声指标都是在电源干净的前提下测出来的草草处理电源实际输出精度会大打折扣。另一个坑是VOUT输出端的负载电容。DAC60501内部有一个输出缓冲放大器它能驱动一定的容性负载但如果VOUT脚直接接了一个大电容比如1uF以上可能会引起自激振荡导致输出波形异常。我在调试时遇到过反直觉的情况输出端接一个100nF的电容滤波结果输出电压反而开始抖动就是因为容性负载让运放不稳定了。解决办法是在VOUT和电容之间串一个几十欧姆的电阻。还有一个很多人容易忽略的点是地线的处理。DAC的输出电压是以GND为参考的如果STM32工作时地线上有较大的数字噪声比如驱动继电器、数码管这些噪声会叠加到DAC的输出上。我习惯将模拟部分的地和数字部分的地在电路板上单点连接必要时可以做成星形接地。4. STM32 HAL库驱动代码详解4.1 CubeMX工程配置要点STM32的I2C驱动可以用标准外设库、LL库或者HAL库。我这边用的是HAL库毕竟STM32CubeMX图形化配置确实方便生成代码后改动也比较小。在STM32CubeMX里配置I2C1外设时有几个关键参数需要确认I2C Speed Mode选择Fast Mode400kHzI2C Clock Speed填400000Rising Time通常保持默认值HAL库会根据时钟频率自动计算引脚PB6、PB7分别映射到I2C1_SCL和I2C1_SDA这些参数里最容易忽略的是GPIO的配置。在CubeMX生成的初始化代码里I2C引脚的GPIO工作模式是AF_OD复用开漏这不代表可以不接外部上拉电阻。前面说过开漏只是不主动输出高高电平必须由外部电阻提供。我在第一版调试时就在这点上栽过跟头——以为CubeMX配好了内部上拉结果SDA和SCL的高电平只有零点几伏从机根本不工作。CubeMX生成好工程后记得检查一下I2C初始化函数里的时钟频率。如果主频是72MHzAPB1总线时钟是36MHzI2C1挂在该总线上HAL库的分频器和时序寄存器计算如果出错实际通信速率可能和预期差很多。我习惯用示波器实测波形周期来验证实际速率而不是完全信任代码里的配置值。4.2 DAC60501寄存器映射与操作命令DAC60501的寄存器不多核心就是下面这几个寄存器地址寄存器名称读写功能0x00DAC数据寄存器读/写写入要输出的16位DAC码值0x01配置寄存器读/写配置基准源、增益、掉电模式等0x02增益寄存器读/写设置输出增益1x或2x0x03控制寄存器读/写控制通讯超时、SDO功能等0x04状态寄存器只读读取芯片状态DAC数据寄存器是日常往里面写电压值的地方。16位数据直接对应满量程的电压具体输出电压的计算公式为VOUT DAC_CODE / 65535 × VREF × GAIN其中VREF为2.5V使用内部基准时GAIN可以是1倍或2倍。如果GAIN1满量程就是2.5VGAIN2满量程就是5V。比如想要输出2.5V且增益配置为1倍那么DAC_CODE就是65535如果增益为2倍输出2.5V时DAC_CODE就是32768。配置寄存器的位定义较多我在代码里用注释标出了每个字段的含义实际使用中采用读-改-写的方式修改特定位。需要特别注意的是DAC60501有一个复位功能向配置寄存器写入特定值可以重置所有寄存器为默认状态。芯片上电时默认状态是内部基准上电、增益1倍、输出0V基本可以不初始化直接写数据寄存器就能工作。4.3 驱动代码寄存器读写基础函数先定义一个简单的头文件把寄存器地址和I2C地址宏定义好。我用的是STM32F103C8T6I2C1的地址为0x48ADDR0和ADDR1都接地。/* dac60501.h */ #ifndef __DAC60501_H #define __DAC60501_H #include main.h /* DAC60501寄存器地址 */ #define DAC60501_REG_DAC 0x00 #define DAC60501_REG_CONFIG 0x01 #define DAC60501_REG_GAIN 0x02 #define DAC60501_REG_CONTROL 0x03 #define DAC60501_REG_STATUS 0x04 /* I2C地址7位地址0x48左移1位 */ #define DAC60501_I2C_ADDR 0x90 /* 16位DAC数值宏 */ #define DAC60501_FULL_SCALE 0xFFFF #define DAC60501_ZERO_SCALE 0x0000 void DAC60501_Init(I2C_HandleTypeDef *hi2c); uint8_t DAC60501_SetVoltage(I2C_HandleTypeDef *hi2c, float voltage_mv, float ref_mv, uint8_t gain); uint16_t DAC60501_ReadReg(I2C_HandleTypeDef *hi2c, uint8_t reg); #endif接下来是实现文件中的寄存器读写基础函数。这里有一个取舍我选择用HAL_I2C_Mem_Write和HAL_I2C_Mem_Read因为DAC60501的寄存器地址是8位PAL库的Memory函数自动处理了发送寄存器地址的步骤比手动拼装HAL_I2C_Master_Transmit更简洁。/* dac60501.c */ #include dac60501.h /* 写寄存器reg为8位寄存器地址data为16位数据 */ static HAL_StatusTypeDef DAC60501_WriteReg(I2C_HandleTypeDef *hi2c, uint8_t reg, uint16_t data) { uint8_t buf[2]; buf[0] (data 8) 0xFF; buf[1] data 0xFF; return HAL_I2C_Mem_Write(hi2c, DAC60501_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, 2, HAL_MAX_DELAY); } /* 读寄存器返回16位数据 */ uint16_t DAC60501_ReadReg(I2C_HandleTypeDef *hi2c, uint8_t reg) { uint8_t buf[2] {0, 0}; if (HAL_I2C_Mem_Read(hi2c, DAC60501_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, 2, HAL_MAX_DELAY) ! HAL_OK) { return 0xFFFF; } return (uint16_t)((buf[0] 8) | buf[1]); }这里有一个容易出错的地方HAL_I2C_Mem_Write和HAL_I2C_Mem_Read的DevAddress参数传入的是8位地址而不是7位地址。数据手册上写的0x48是7位地址而HAL库函数的最后一个地址位是读写标志所以要左移一位变成0x90。如果直接用0x48从机收到的是地址0x48加读写位对应到7位地址就变成了0x24完全对不上。4.4 驱动代码初始化和输出电压实现初始化函数做的事情不多因为DAC60501的默认状态已经比较友好。我只做了一件事确认芯片在线读取状态寄存器或者尝试读取寄存器然后显式配置增益和工作模式。下面是完整的初始化函数void DAC60501_Init(I2C_HandleTypeDef *hi2c) { uint16_t config_val 0x0000; /* 上电延时等待芯片内部电源稳定 */ HAL_Delay(50); /* 读取当前配置寄存器避免干扰其他位 */ uint16_t status DAC60501_ReadReg(hi2c, DAC60501_REG_STATUS); // 如果读回0xFFFF说明通信失败可以在这里加入错误处理 /* 配置寄存器内部基准上电、正常DAC输出、16位直通格式 */ config_val 0x0000; DAC60501_WriteReg(hi2c, DAC60501_REG_CONFIG, config_val); /* 增益寄存器默认1x增益输出范围0~2.5V */ DAC60501_WriteReg(hi2c, DAC60501_REG_GAIN, 0x0001); /* 1x增益 */ }接下来是输出电压的核心函数。我的实现思路是传入毫伏值和满量程参数函数内部计算对应的16位DAC码值。为了避免编译器优化和浮点数精度问题我用整数运算完成计算uint8_t DAC60501_SetVoltage(I2C_HandleTypeDef *hi2c, uint16_t voltage_mv, uint16_t ref_mv, uint8_t gain) { uint32_t dac_code; uint32_t full_scale_range_mv; /* 根据增益计算满量程对应的毫伏数 */ full_scale_range_mv (uint32_t)ref_mv * (uint32_t)gain; if (voltage_mv full_scale_range_mv) { voltage_mv full_scale_range_mv; } /* DAC码值 (目标电压 / 满量程电压) * 65535全部用整数运算 */ dac_code ((uint32_t)voltage_mv * DAC60501_FULL_SCALE) / full_scale_range_mv; return DAC60501_WriteReg(hi2c, DAC60501_REG_DAC, (uint16_t)dac_code) HAL_OK; }这个函数设计上有一个细节我把满量程范围拆成参考电压和增益两个参数传入这样当配置变更比如换成外部基准源时调用处只需要修改参数即可不用动驱动函数本体。同时为了适应不同版本的应用场景我没有在初始化函数里强制指定增益让用户在初始化时显式传入。不过有一点让很多初学者疑惑的地方在于增益2倍时DAC60501的输出范围变成了0到5V但内部基准仍然是2.5V。这是通过内部输出放大器的2倍放大实现的不是改变基准电压本身所以反射电压这个参数仍然传2.5V就好增益传2。这个逻辑我专门在代码里用注释标了出来避免后续维护时产生误解。5. 完整工程集成和输出验证5.1 工程文件结构写驱动代码的时候我习惯把硬件相关的部分和外设驱动剥离开。这样做的目的很明显如果换了MCU平台只需要改I2C接口层DAC60501的逻辑不用动。这个工程的文件结构如下Project ├── Core │ ├── Inc │ │ ├── main.h │ │ └── dac60501.h │ └── Src │ ├── main.c │ ├── i2c.c │ └── dac60501.c ├── Drivers │ ├── CMSIS │ └── STM32F1xx_HAL_Driver └── MDK-ARMmain.c中只需要在初始化外设后调用DAC60501_Init然后就可以通过DAC60501_SetVoltage函数设置输出电压。我把调用示例写成一个小的演示程序通过串口接收指令来设置DAC输出这样即使没有屏幕也可以方便地做功能验证。5.2 串口控制DAC输出的演示逻辑演示程序的功能非常简单STM32通过串口接收一个十六进制字符串代表要输出的毫伏值解析后调用DAC60501_SetVoltage函数输出对应的电压同时通过串口回显当前的DAC码值和实际电压。核心代码如下/* main.c 中的演示代码片段 */ uint8_t receiveBuf[16] {0}; uint16_t voltage_mv 0; while (1) { if (HAL_UART_Receive(huart1, receiveBuf, 4, 100) HAL_OK) { /* 假设收到的是4字节ASCII码代表毫伏值例如2500 */ voltage_mv (receiveBuf[0] - 0) * 1000 (receiveBuf[1] - 0) * 100 (receiveBuf[2] - 0) * 10 (receiveBuf[3] - 0) * 1; if (DAC60501_SetVoltage(hi2c1, voltage_mv, 2500, 1) 0) { printf(DAC write failed, voltage%dmV\r\n, voltage_mv); } else { printf(Set VOUT %dmV, code 0x%04X\r\n, voltage_mv, (uint16_t)((uint32_t)voltage_mv * 65535 / 2500)); } memset(receiveBuf, 0, sizeof(receiveBuf)); } }这段代码虽然简单但演示了驱动函数的基本调用方式。实际项目中如果要用按键或者上位机控制把串口接收部分换成对应的输入方式即可。5.3 用万用表和示波器验证输出写完驱动后需要验证输出电压是否正确。最直接的方法是拿万用表量VOUT对地的电压。我在实际测试中得到了一组典型的输出值目标电压mV理论DAC码实测电压mV误差mV00x00000.10.15000x0CCD500.20.210000x199A1000.10.115000x26661499.8-0.220000x33332000.30.325000x7FFF2500.50.5实测结果说明DAC60501的线性度确实不错在室温下误差基本在几个毫伏以内。要注意的是万用表的精度和内部基准温漂都会影响测量结果所以如果项目对输出精度有严格要求建议先用高精度电压表对基准源进行校准。如果手头有示波器可以观察一下波形上升沿和长期稳定性。DAC输出在写完数据寄存器后通常会在几十微秒内稳定到目标值。如果观察到输出有明显过冲或者振荡大概率是输出端接的容性负载太大了按前面说的串一个电阻试试。6. 实测调试中的坑和解决思路6.1 逻辑分析仪抓I2C时序的要点调试I2C通信逻辑分析仪是我觉得最值得投资的工具。配合开源上位机软件比如PulseView可以直观地看到总线上发生了什么。抓I2C信号时主要有几个注意点。采样率要足够高至少要高于I2C时钟的4倍我一般用16MHz采样率抓400kHz的I2C总线这样波形看不出明显失真。触发条件设置为SDA下降沿触发这样每次通信开始时就能稳定捕获数据帧。真正分析I2C数据时关注点应该在起始条件、从机地址、应答位上。很多I2C通信失败的常见情况是在逻辑分析仪解码窗口中看不到ACK应答这说明从机根本没有响应主机。此时首先要检查地址是否匹配其次检查从机供电是否正常然后再看SDA和SCL线上拉电平是否正常。一次很典型的排查经历是我调试时发现主机发送地址后收不到ACK逻辑分析仪显示波形完全正常地址也匹配但总线高电平只有1.8V。用万用表一量发现上拉电阻焊成了100kΩ比色环如棕黑黄金实际应该是棕红红金电阻太大导致总线根本无法可靠拉到高电平。换掉电阻后通信恢复正常。6.2 示波器眼皮底下的波形陷阱逻辑分析仪可以显示逻辑电平但看不到模拟波形细节。如果I2C通信出现偶发故障比如有时候能通信、有时候又不能示波器就派上用场了。用示波器看I2C波形主要看两个东西上升沿的斜率以及高低电平的电压值。前面说过上拉电阻太大会让上升沿变得平缓如果正好卡在从机输入高电平阈值的临界点就会出现十次通信八次成功两次失败的诡异现象。这种偶发故障很难排查因为它不像完全不通那样有明确的错误现象。还有一个容易被忽视的问题是PCB走线的寄生电容。如果SDA和SCL走线过长或者经过过孔过多线上电容会增大同样会导致上升沿变慢。我在一块板上遇到了类似问题最后通过缩短走线、优化布局解决了。对于DAC60501这种小封装芯片尽量把它布置在MCU附近I2C走线不要太长。6.3 读回验证的重要性很多工程师写DAC驱动时只关注写操作不关注读操作这其实是个隐患。DAC60501的寄存器是支持回读的利用DRT功能数据寄存器回读可以验证上一步的写操作是否成功到达芯片内部。我使用回读功能的场景主要有两个。第一个是产品自检系统上电后向DAC数据寄存器写入一个已知的测试值然后读回来和期望值对比如果一致说明I2C链路和芯片工作正常。第二个是校准流程写入一个已知电压值用外部高精度表测量VOUT实际输出电压通过读回寄存器确认写入值没有被改动然后算出偏差因子存到Flash里后续输出电压时用这个因子修正。回读操作的实现代码如下uint16_t code_written 0x4000; DAC60501_WriteReg(hi2c1, DAC60501_REG_DAC, code_written); uint16_t code_read DAC60501_ReadReg(hi2c1, DAC60501_REG_DAC); if (code_read code_written) { /* 写读一致通信正常 */ } else { /* 写读不一致说明通信存在问题 */ }这里有一个很小的细节有些DAC芯片的数据寄存器是写后即清或者只写不读的但DAC60501是支持回读的所以我才能用这种方式做校验。设计驱动之前最好先确认目标芯片是否支持寄存器回读不然校验代码写好了才发现芯片不支持白折腾。6.4 多设备挂载时的地址冲突与总线仲裁I2C总线上可以挂多个设备但也意味着多了一份地址冲突的风险。我在一块板上同时挂了DAC60501和一个I2C温度传感器结果温度传感器的7位地址是0x48和DAC60501ADDR0和ADDR1都接地的地址完全一样。上电后两个设备都在总线上应答通信数据乱成一团。排查这个问题的过程是比较痛苦的因为逻辑分析仪上看到的地址帧有应答但数据完全对不上。后来一个一个断开设备才发现是地址冲突。解决办法也很简单把DAC60501的ADDR0接高电平地址从0x48变到0x49重新编译烧录后问题解决。这个案例提醒我们在一个新设计的电路板上要提前规划好所有I2C从机的地址尽量在产品设计阶段就去查看每个芯片的数据手册确认各自的地址范围和硬件配置引脚。等到板子已经生产出来再发现地址冲突只能飞线或者换芯片费力又费时。6.5 偶发通信超时的根源定位项目进入系统联调阶段后出现了一个更隐蔽的问题DAC60501偶尔通信超时但不是每次都失败大约每几十次操作会出现一次失败。用逻辑分析仪抓了很多次终于发现超时时刻的波形有一个共同特征SCL线上出现了明显的毛刺。排查后发现毛刺来源于板上一个继电器。继电器线圈断开时产生的反向电动势耦合到了I2C总线上干扰了SCL和SDA信号。找到根源后解决办法是给继电器线圈并一个续流二极管同时把I2C的两根线远离继电器驱动电路。这类电磁干扰引起的问题在实验室里很难复现但在实际产品环境中可能频繁出现。如果遇到这种偶发故障建议从以下几个方向排查应用环境里是否存在大电流开关器件I2C走线是否靠近干扰源是否有必要在SDA和SCL线上加小的串联电阻常见值为33到100欧姆来抑制过冲以及是否需要在I2C总线上增加TVS管做ESD保护。7. 我在实际使用中的几个补充建议整个DAC60501驱动的开发过程走下来有一些经验虽然不成系统但对提升可靠性和效率有实际帮助。第一驱动代码尽量做成可移植的模块。我的dac60501.c文件头部的I2C_HandleTypeDef *hi2c参数让这层驱动只依赖HAL库的I2C接口。如果你想从STM32F103换到GD32或者AT32只要把I2C外设的HandleTypeDef换成对应平台的句柄类型即可芯片逻辑部分基本不用动。第二初始化函数里一定要加上通信校验。最简单的方法就是读取状态寄存器如果返回0xFFFF说明I2C通信失败此时初始化函数应该返回一个错误码而不是让主流程继续跑下去。很多系统出现诡异问题都是因为外设初始化失败后系统还在继续运行后续的数据全是无效的。第三如果项目对DAC输出电压的精度要求较高建议做一个简单的软件校准。方法不在复杂在系统初始化时输出一个较低电压比如500mV和一个较高电压比如2000mV用高精度万用表实测然后计算出偏移和增益系数存到Flash中。后续每次输出电压时用这两个系数修正DAC码值。这个做法可以解决芯片个体差异和基准源偏差带来的误差。第四关于DAC60501的高速模式我实测在3.4Mbps下信号完整性要求比较高一般PCB布局如果处理得不好很容易出错。如果不是特别需要高速通信建议就跑400kHz快速模式足够用了。高速模式更多是为了在复杂系统里减少总线占用时间真正用得上的项目其实不多。最后再分享一个小技巧在调试I2C设备时如果怀疑是上拉电阻的问题可以在不拆板的情况下用一个小阻值电阻比如几百欧姆临时并联在现有的上拉电阻两端观察通信是否恢复。如果恢复了说明原上拉电阻值偏大反之则可以排除上拉电阻的嫌疑。这个方法在调试阶段非常高效不需要反复改板子。
返回列表