前段时间调一块带模拟输出的板子需要在STM32上通过I2C挂一颗单通道DAC一开始准备直接用SPI接口的后来发现引脚被占了就换成了这颗DAC60501。这颗芯片是12位精度的单通道数模转换器I2C接口带内部基准外围几乎为零非常适合从单片机往外输出可调电压的场景。这篇文章就把整个驱动代码的踩坑过程、HAL库实现、工程结构和排查经验写出来希望能帮到正在用STM32和DAC60501的工程师尤其是第一次用I2C DAC的新手。1. 先聊清楚这颗芯片和接口1.1 为什么我推荐DAC60501做单路模拟输出DAC60501是TI出品的一颗12位单通道DAC和很多单片机自带DAC不同它是通过I2C接口控制的所以你在选型时根本不用关心单片机到底有没有DAC外设。我用它主要是两个原因一是它内部带了2.5V基准源外部不需要再买一个基准芯片省面积也省成本二是它的封装非常小SOT-23还是WSON我记不太清了反正焊在板子上不占地方很适合做可调偏置、可调阈值、传感器标定这类功能。从性能上讲12位分辨率虽然不算高但绝大多数应用足够。比如做电源的反馈微调、光模块的功率设定、仪表里的零点校准满量程2.5V或者5V12位对应大约0.6mV到1.2mV的步进作为粗调和微调完全够用。而且这颗芯片的功耗极低待机电流微安级别对电池供电的场景很友好。另外它的I2C接口兼容100kHz和400kHz两种速率STM32的硬件I2C直接就能搞定。不需要像模拟I2C那样去延时翻转GPIO可靠性高很多。我用的是STM32F103系列的HAL库直接调HAL_I2C_Mem_Write这个函数就能完成一次寄存器写入代码量非常少。1.2 I2C通信的基本概念与HAL库的对应关系I2C总线就两根线SCL时钟线和SDA数据线。所有设备都并联在这两根线上靠不同的地址区分为谁通信。STM32作为主机DAC60501作为从机主机发起通信从机应答。HAL库把这些底层的时序封装好了我们不需要手动翻转电平只要调用API就行。HAL库中I2C发送数据最常用的是两个函数HAL_I2C_Master_Transmit和HAL_I2C_Mem_Write。前者适合往一个寄存器地址连续写裸数据后者适合写“内存/寄存器型”设备因为它会自动在数据前面加上一个寄存器地址正好匹配DAC60501这类芯片。我初次接触时有个误区以为I2C通信只要把数据发出去就算成功忽略了从机应答。实际上HAL函数返回值代表整个传输是否被ACK如果从机没应答返回HAL_ERROR。这个在排查硬件问题时非常关键后面我会专门讲。2. 驱动代码设计前必须搞懂的I2C细节2.1 I2C帧格式与寄存器写入流程DAC60501是通过寄存器方式控制的写一个寄存器的时候I2C总线上实际发送的内容是下面这个格式起始位START设备地址 写标志寄存器地址一个字节数据高8位数据低8位停止位STOP要注意的是DAC60501的数据寄存器虽然是12位有效但寄存器读写采用16位数据格式也就是高8位和低8位两个字节。比如要写入0x800这个DAC code高字节是0x08低字节是0x00。如果只看数据手册里的寄存器地址不搞清楚数据位宽很容易写错。用HAL_I2C_Mem_Write时第四个参数是内存地址宽度DAC60501的寄存器地址是8位所以要用I2C_MEMADD_SIZE_8BIT。如果选成16BITHAL库会在寄存器地址前额外发一个字节从机就收不到正确命令了这个问题网上很多求助帖子都是这个原因导致通信失败的。2.2 设备地址的坑7位地址和8位地址到底怎么填这是我在调试过程中最容易卡住的地方也是新手一定会踩的坑。DAC60501的I2C地址是由硬件引脚电平决定的常见配置下7位地址是0x48数据手册里会写“I2C address 0x48”。但STM32的HAL库函数比如HAL_I2C_Mem_Write它的第二个参数DevAddress要求填的是8位地址也就是7位地址左移一位后的值0x48 1 0x90。如果直接把0x48填进去I2C总线上发出来的从机地址就是0x24对应的地址DAC60501根本不会应答函数一直超时读寄存器永远是0xFF。所以这里一定要记住HAL库的地址参数是“8位写地址”你必须在7位地址基础上左移1位或者写0x48 1。有的板子DAC60501的地址引脚接了高电平这时7位地址就会变成0x49那调用参数就要改成0x92。这个细节设计驱动时最好用宏定义写清楚方便后续换板子时修改。2.3 上拉电阻为什么要外部加I2C和普通GPIO输出不一样它的SDA和SCL是开漏结构也就是说芯片只负责把总线拉低不主动拉高。高电平是依靠外部上拉电阻把信号线拉到VDD的。既然这样如果电路板上没有接上拉电阻总线就永远停留在低电平通信肯定失败。关于阻值选择我习惯在3.3V系统上用4.7kΩ400kHz快速模式也稳定。如果总线设备多、线长可以适当减小到2.2kΩ增强驱动能力。相反上拉电阻太大比如用到100kΩ波形上升沿变得很缓在高速下会读不到正确电平。很多“I2C偶尔通信正常偶尔不行”的问题最后查出来就是上拉电阻值选得不好。还有个常见疑问既然STM32内部有上拉电阻是不是可以不外部接我试过STM32内部上拉阻值大约在30k到50k对400kHz的快速模式来说太弱了波形边沿不够陡容易出错。所以做板子时老老实实加外部上拉不要省那两颗电阻。3. 完整工程实现从零写DAC60501驱动3.1 工程结构和CubeMX配置这套代码基于STM32CubeMX生成的HAL工程芯片用STM32F103RCT6只用I2C1外设。CubeMX里的配置很简单把I2C1打开速率设成400kHz地址模式用7位其余保持默认。注意I2C引脚如果和板上的上拉电阻共地一般不需要额外配置内部上拉如果实在没有外部上拉可以在GPIO设置里把SDA和SCL的上拉打开但只能作为临时测试用。完整工程文件就四个核心文件main.c、i2c.c、dac60501.c、dac60501.h。dac60501.c负责封装所有和芯片相关的操作main.c只管调用结构非常清晰。实际项目里如果还有别的传感器照这个模式扩展就行。3.2 头文件接口定义先看头文件我习惯把设备地址和寄存器地址全部用宏定义写出来这样移植到其他芯片或者改硬件时只需要改一个头文件就行。DAC60501的数据寄存器地址我这里是按0x06举例具体以你手上的数据手册为准因为不同批次可能有些差异移植时一定要核对。#ifndef __DAC60501_H #define __DAC60501_H #include main.h /* I2C设备地址7位地址0x48HAL函数中需要左移1位 */ #define DAC60501_ADDR 0x48U #define DAC60501_ADDR_WRITE (DAC60501_ADDR 1) /* DAC60501寄存器地址 */ #define DAC60501_REG_DEVICE_CONFIG 0x01U #define DAC60501_REG_SYS_CONFIG 0x02U #define DAC60501_REG_DAC_CONFIG 0x03U #define DAC60501_REG_DAC_DATA 0x06U void DAC60501_Init(I2C_HandleTypeDef *hi2c); HAL_StatusTypeDef DAC60501_WriteRegister(uint8_t reg, uint16_t value); HAL_StatusTypeDef DAC60501_SetOutput(uint16_t code); #endif这里接口只暴露了初始化、写寄存器、写DAC输出三个函数已经能覆盖绝大多数使用场景。你可以根据自己的需求再增加读寄存器、复位、关断输出等接口。3.3 驱动实现代码核心实现很简单写寄存器函数就是准备两个字节然后调用HAL_I2C_Mem_Write。需要注意的是DAC60501内部数据是高字节在前所以value 8放第一个字节value 0xFF放第二个字节。这个顺序不能反反了输出数值就会完全不对。#include dac60501.h static I2C_HandleTypeDef *dac_i2c_handle; void DAC60501_Init(I2C_HandleTypeDef *hi2c) { dac_i2c_handle hi2c; /* 写寄存器函数至少要能正常调用否则说明接线或地址有问题 */ DAC60501_WriteRegister(DAC60501_REG_DAC_CONFIG, 0x0000); } HAL_StatusTypeDef DAC60501_WriteRegister(uint8_t reg, uint16_t value) { uint8_t data[2]; data[0] (uint8_t)((value 8) 0xFF); data[1] (uint8_t)(value 0xFF); return HAL_I2C_Mem_Write(dac_i2c_handle, DAC60501_ADDR_WRITE, reg, I2C_MEMADD_SIZE_8BIT, data, 2, HAL_MAX_DELAY); } HAL_StatusTypeDef DAC60501_SetOutput(uint16_t code) { if(code 0x0FFFU) { code 0x0FFFU; } /* 12位DAC数据左对齐到高12位低4位写0 */ return DAC60501_WriteRegister(DAC60501_REG_DAC_DATA, (code 4) 0xFFF0U); }这里SetOutput函数里把12位code左移4位是因为DAC60501的数据寄存器16位格式中有效数据放在高12位。如果你的芯片手册显示的是右对齐那这里就不要左移直接写code。我最初按右对齐方式写输出完全不对查了一下午才发现是左对齐。这个细节不写在寄存器配置里而是写在寄存器数据格式那一页。3.4 主程序调用与实测主程序里只需要初始化I2C然后调用DAC60501_Init再调用DAC60501_SetOutput就能在输出引脚上量到电压。比如写入0x800也就是输出满量程的一半如果基准是2.5V、增益为1理论上应该量到约1.25V。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); DAC60501_Init(hi2c1); DAC60501_SetOutput(0x800); while(1) { } }我实测时用万用表量DAC输出引脚写入0x000时是0V写入0xFFF时大约是2.5V写入0x800时是1.25V左右整个线性度还算不错。用示波器看波形在写入新值的瞬间会有一个很小的毛刺这是DAC更新输出的正常现象只要应用中对毛刺不敏感就无所谓敏感的话可以在后面加一级RC滤波。4. 电压换算、精度分析及功能扩展4.1 12位DAC的输出电压到底怎么算DAC输出电压的基础公式是Vout (DAC_CODE / 4096) * VREF * GainDAC60501内部的基准默认是2.5V如果配置寄存器里增益选1那输出范围就是0到2.5V如果增益选2输出范围是0到5V但前提是AVDD电源电压要足够一般要大于5.5V左右。举个例子我要输出1V电压基准2.5V增益1那么DAC_CODE 1V / 2.5V * 4096 1638.4四舍五入取1638对应十六进制是0x666。如果直接算出来是0x666写入后实际电压会接近1V有一定误差主要来自量化舍入和基准精度。实际做产品时如果需要精确输出某个电压我建议做一个“软件校准”先用高精度万用表量出满量程和零点的实际电压然后通过增益寄存器做校准或者在应用层做线性映射把误差修掉。4.2 精度不够先别急着换芯片很多时候发现DAC输出和理论值差很多不一定是芯片精度差很可能是电源和地的问题。DAC输出端的模拟电压它的参考源就是AVDD或者内部基准如果AVDD纹波大输出也会跟着抖。我在调试时发现用同一个3.3V开关电源给DAC供电输出毛刺明显偏大后来单独加了一颗10μF和0.1μF去耦电容情况好了很多。还有一点是布局走线。DAC60501的输出引脚、VREF引脚和去耦电容要尽量靠近芯片。不要把模拟输出线和I2C的SCL/SDA并行走很长否则时钟信号会耦合到输出上示波器一看就一团糟。如果你要求输出电压分辨率更高可以考虑在DAC输出后面加一级运放既能做缓冲也能通过电阻比例把电压范围放大到自己需要的区间。实际上很多工业控制板都是这样做的DAC只负责产生一个小信号真正驱动负载的是后级运放。4.3 后续可以扩展的几个方向这套驱动代码最基础的功能是写DAC输出但DAC60501还有不少功能可以在原工程上扩展。比如通过配置寄存器实现输出关断让DAC输出变成高阻态适合多路模拟输出需要隔离的场景。再比如DAC60501支持LDAC触发模式可以让DAC输出不随I2C写入立即更新而是等触发信号到来后才同步更新多个DAC可以做到同步输出。如果你手头还有别的I2C器件比如EEPROM、温湿度传感器、OLED屏那这套I2C驱动的调试经验完全通用。核心就是那几个检查点地址左移没、寄存器宽度对不对、数据字节顺序反没反、上拉电阻加没加。把这几个点记住了I2C基本就不会再坑你。5. 调试实录常见I2C/DAC问题速查5.1 总线卡住不会自己恢复I2C总线上有时会出现SCL正常、SDA一直为低的情况这种情况大概率是主机和从机之间状态错乱从机还在等待后续数据SDA被某个设备拉住了。最简单粗暴的办法是把单片机和DAC60501都断电重来或者给STM32做一次硬件复位。在代码层面可以在初始化I2C之前对PB6和PB7这两个引脚做几次手动翻转模拟SCL时钟把从机的状态机“推”回空闲状态。我写过一个小函数出问题时先操作GPIO输出9个SCL时钟然后再重新初始化I2C外设实测能恢复绝大多数卡死情况。不过放到产品里根治方法还是硬件设计上保证上拉电阻合适、通信速率匹配以及必要时在从机电源上加复位控制。5.2 输出跟预期差很多甚至不动如果I2C通信已经通了但DAC输出始终是0V或者一直满量程先别怀疑芯片坏了多半是数据格式或寄存器地址有问题。我遇到过一次非常隐蔽的问题HAL_I2C_Mem_Write返回正常逻辑分析仪也能抓到I2C波形但DAC输出纹丝不动。后来发现是把DAC_DATA寄存器地址写成了0x00而0x00其实是NO_OP寄存器写进去什么都不执行。这时候一定要翻数据手册把寄存器地址表核对清楚不要凭记忆写。另一种情况是写入的数据没有左移直接写0x800到寄存器结果实际输出的是0x800 4之后的满量程或者错误值。出现这种问题时用万用表量一下输出再对照公式算一下基本就能定位。5.3 逻辑分析仪怎么看I2C时序调试I2C强烈建议用逻辑分析仪哪怕是最便宜的24MHz 8通道也能用。接线很简单逻辑分析仪的通道0接SCL通道1接SDA共地后打开软件抓波形。关键是抓到的波形要能看到起始条件、地址字节、寄存器地址和数据字节并且地址字节要等于0x900x481才对。如果逻辑分析仪上显示地址是0x48说明软件里传入的DevAddress直接用了7位地址没有左移。从波形上看还能确认数据字节的顺序对不对高字节在第一位低字节在第二位。很多I2C问题不需要问别人抓一次波形自己就明白了。5.4 问题速查表下面把我踩过的坑整理成一个速查表移植和调试时照着排查能省不少时间。现象可能原因处理办法HAL_I2C_Mem_Write返回HAL_ERROR从机地址错误或接线错误确认7位地址左移1位检查SDA/SCL是否接反返回HAL_OK但输出不变寄存器地址错误核对数据手册重点检查DAC_DATA地址输出一直是0V数据左对齐未处理或DAC没上电检查code 4位检查AVDD电源输出一直是满量程DAC数据寄存器被反复写入非法值确认输入code范围0~4095偶尔通信失败上拉电阻太大或布线过长换4.7k或2.2k上拉缩短走线SDA卡低从机状态机错乱手动输出SCL时钟恢复或断电复位最后再分享一点个人经验DAC60501这颗芯片本身不复杂真正花时间的往往是I2C底层和硬件细节。写驱动时不要急着往寄存器里塞数据先把I2C波形抓出来确认地址、寄存器、数据的每一帧都对再调DAC输出问题会好定位得多。我把完整工程放在自己的例程库里后续做其他I2C设备驱动时也是直接从这个框架复制换一下地址和寄存器定义就能用效率高很多。
