简介:压缩包面向电子设计中的高精度模拟信号采集与输出场景,整合了TI ADS131A02(16位Σ-Δ型ADC)和DAC8552双通道DAC的驱动代码,可用于STM32F4等嵌入式平台,帮助开发者解决器件初始化、寄存器配置、数据读取和模拟电压输出等问题。包内共195个文件、约6.34MB,以48个.c源文件和47个.h头文件为主体,同时包含编译生成的axf、hex、map文件、Keil工程配置(uvprojx/uvoptx)以及清理脚本,可直接用MDK打开工程查看或重新编译。从文件结构中可看出,代码基于STM32F4标准外设库编写,覆盖定时器、RTC、RCC、ADC、Flash、CAN等模块,便于理解ADS131A02与DAC8552在实际MCU工程中的挂接和调用方式。已有908人学习下载。包内保留的调试配置、链接脚本和辅助文件,既适合作为驱动移植范本,也可作为排查时序与转换异常的参考。
1. 别被“高精度ADC”这三个字带偏
做信号链的工程师拿到一块 ADS131A02 和 DAC8552 的驱动代码,多半会先翻数据手册对着寄存器看半天。这个经验在 STM32F4 上并不成立——真正花时间的从来不是器件本身,而是 SPI 总线上的帧格式、DRDY 电平时序,以及一批 Keil 工程里特有但没什么人解释的文件。你手上这份ADS131A02和DAC8552.zip就是一个典型的 MDK-ARM 工程:ADC.uvguix.Administrator是界面布局缓存,ADC.axf是上一次编译好的可执行镜像,keilkilll.bat用来清理中间文件,真正的驱动代码则混在stm32f4xx_adc.c这类标准外设库文件里。这篇文章把它拆开讲:先补两个器件的选型逻辑和时序模型,再给出可复现的驱动写法、参数配置和移植到其他板子时的坑,适合正在做数据采集、传感回环或闭环输出的开发者。
2. ADS131A02 驱动拆开看:Σ-Δ ADC 不是普通 SPI 设备
2.1 为什么这个场景非选 Σ-Δ 架构不可
ADS131A02 是 TI 出的一颗双通道 24 位(工程里以 16 位模式使用)Σ-Δ 型 ADC,最高采样率可达 250 ksps。Σ-Δ 架构的核心思路是“过采样 + 噪声整形”:用远高于奈奎斯特频率的采样率把量化噪声推到高频段,再通过数字滤波器把带外噪声滤掉。这套机制带来的直接好处有两个——其一,在 50Hz/60Hz 工频干扰较强的工业环境里,配合数字滤波器可以做出很高的信噪比;其二,失调电压和增益漂移远小于逐次逼近型(SAR)ADC,对于长时间运行的采集系统来说,基线漂移是致命的,Σ-Δ 在这点上优势明显。
但使用 Σ-Δ ADC 要付出的代价是时延。数字滤波器有一个群延迟,通常等于滤波器阶数除以输出数据率。如果你把 ADS131A02 用在闭环控制里,带宽不能做太宽,否则相位裕度会吃掉控制稳定性。所以拿到代码后,第一件事不是看 SPI 读写,而是确认当前配置下 OSR(过采样率)对应的输出数据率是多少,这直接决定了你的控制环能跑多快。
2.2 寄存器和 SPI 帧的前 8 位命令
ADS131A02 的 SPI 协议和普通外设不太一样。它的所有读写帧都是 24 位对齐的,前 8 位是指令头(命令字节),后 16 位是数据部分。读取转换结果时,主机需要至少连续发送两个 24 位帧,第一帧返回的是器件状态和 DRDY 相关的 flag,第二帧才是真正的通道数据。
命令帧格式拆开看,RREG和WREG的编码规则是:
0000 rrrr读寄存器,rrrr是寄存器地址,紧接着必须跟一个 dummy 字节0010 rrrr写寄存器,数据字节随命令帧后移,地址自动步进0xA0进入待机模式,0x20等用于配置 CLOCK2 寄存器
从这段代码的源码来看,驱动里最常见的操作是先写CLOCK寄存器定好时钟分频,再写CLOCK2选通内部基准,然后等 DRDY 拉低取数据。需要注意,芯片默认上电后通道是关闭的,必须显式配置PGA和CHANNEL寄存器把对应通道打开,否则读回来永远是 0。
以下是驱动中常见的寄存器初始化代码:
static void ADS131A02_WriteReg(uint8_t addr, uint8_t value) { uint8_t buf[3]; buf[0] = 0x20 | (addr & 0x1F); // WREG 命令头,0x20 表示写操作 buf[1] = 0x00; // 寄存器数据前的保留字节,必须为 0 buf[2] = value; // 要写入的寄存器值 GPIO_ResetBits(ADC_CS_GPIO_PORT, ADC_CS_PIN); // 拉低片选,开始一次传输 SPI_I2S_SendData(ADC_SPI, buf[0]); while (SPI_I2S_GetFlagStatus(ADC_SPI, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(ADC_SPI, buf[1]); while (SPI_I2S_GetFlagStatus(ADC_SPI, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(ADC_SPI, buf[2]); while (SPI_I2S_GetFlagStatus(ADC_SPI, SPI_I2S_FLAG_RXNE) == RESET); (void)SPI_I2S_ReceiveData(ADC_SPI); // 丢弃有效位,非应答信号 GPIO_SetBits(ADC_CS_GPIO_PORT, ADC_CS_PIN); // 拉高片选 delay_us(1); // 保证相邻帧之间至少一个 SPI 时钟间隔 }这段代码后两处参数值得留意。第一,addr & 0x1F只取了低 5 位地址,ADS131A02 的寄存器空间能覆盖到,但如果你的驱动是从 ADS131M02 改过来的,地址位宽不同,直接套会写到一个不存在的寄存器上。第二,delay_us(1)不是随便加的,这保证了片选释放后到下一帧开始前有一个时钟周期的死区,这是 Σ-Δ ADC 对 SPI 时序的硬性要求,STM32F4 的 SPI 外设本身不会替你处理这个时间。
读数据帧相对简单,向芯片连续发送两个 0x00 帧,等第二个帧返回时读取数据即可。若总线上还有别的从机,片选必须全程保持低电平,中途拉高会导致 ADC 判定当前帧传输中止,数据直接丢弃。
2.3 初始化顺序与关键参数表
初始化时要先复位芯片再写寄存器,这和多数 TI ADC 一致。复位后 ADS131A02 会自己拉低 DRDY,之后主机才能开始配置,否则配置命令可能被忽略。驱动中的初始化顺序通常是:拉低 RESET 引脚保持至少 10 个系统时钟周期 → 拉高 → 等待 DRDY 有效 → 写编码器配置。
配置时几个关键参数直接决定采集质量:
| 寄存器 | 位域 | 默认值 | 建议值 | 说明 |
|---|---|---|---|---|
| CLOCK | PLL 分频 | 0x06 | 按所需采样率计算 | 决定调制器时钟频率。采样率 = fCLKIN / (分频系数 × OSR) |
| CLOCK2 | 基准电压选择 | 0x00 | 内部基准 1.2V 或外部 2.4V | 外部基准精度更高,但会增加引脚开销 |
| PGA | 增益 | 0x00 即 1 倍 | 按信号幅度选择 | 增益过高会让 Σ-Δ 调制器进入饱和,输出出现“粘滞”现象 |
| CONFIG | 数据率配置 | 0x00 | 根据带宽选 OSR | OSR 越大,有效位数越高,但时延线性增大 |
内部基准电压选 1.2V 时,输入范围约 ±1.2V / 增益;选 2.4V 输入范围更大但 LSB 变大。若你的信号本来就在 0-5V 区间,前端没有电平调理直接进 ADC,那 PGA 就得放小且偏置必须抬到共模范围内,否则器件工作点就不对了。
采样率计算的口诀是:输出数据率 = 调制器时钟频率 / OSR。而调制器频率由 CLOCK 寄存器和外部时钟源决定,外部晶振是 8.192MHz 时,分频到 2.048MHz 再除以 16 倍 OSR,得到的就是 128ksps 附近的输出率,具体数值要看代码里实际写入 CLOCK 寄存器的分频值。
3. DAC8552 输出链路:一个函数把数字量变成 0~5V
3.1 双通道输出为什么够用
DAC8552 是 TI 的单片双通道 DAC,输出范围 0~VREF,电源电压即参考电压上限,串行接口写入即刷新输出端。和并行 DAC 相比,它引脚少、占板面积小、也不存在数据总线竞争问题;和 I2C 接口的 DAC 比,SPI 的优势是写入速率没有时钟拉伸,更新频率可以推得更高。
这套 ADC+DAC 的组合常见于信号回放、波形发生器和闭环输出场景。ADS131A02 采到的数字量,经 MCU 处理后由 DAC8552 重建为模拟电压输出,整个过程不需要外部挂运放做 I/V 转换,DAC 的输出引脚直连即可。
3.2 写寄存器与输出电压的代码实现
DAC8552 的 SPI 帧是 24 位:第 23~22 位控制通道选择和数据格式,第 21~20 位控制增益与输出缓冲,低 16 位是数据。数据按二进制补码格式写入,满量程对应 0xFFFF,零输出对应 0x8000。如果不做编码换算,直接用原码会让输出在零点上下跳变。
void DAC8552_WriteValue(uint8_t channel, uint16_t value) { uint32_t frame = 0; uint16_t tmp = 0; // 帧组织:bit23~22 通道选择,bit21 增益,bit20 输出缓冲 // channel 0 = 0b00,channel 1 = 0b10 tmp = (uint16_t)((channel << 14) | (0x01 << 13) | (value & 0x1FFF)); frame = ((uint32_t)tmp << 8) | (value & 0xFF); GPIO_ResetBits(DAC_CS_GPIO_PORT, DAC_CS_PIN); // 先发高 16 位,再发低 8 位 SPI_I2S_SendData(DAC_SPI, (uint16_t)(frame >> 16)); while (SPI_I2S_GetFlagStatus(DAC_SPI, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(DAC_SPI, (uint16_t)(frame & 0xFFFF)); while (SPI_I2S_GetFlagStatus(DAC_SPI, SPI_I2S_FLAG_RXNE) == RESET); GPIO_SetBits(DAC_CS_GPIO_PORT, DAC_CS_PIN); // SYNC 上升沿时锁存数据,因此必须保证发送完再拉高片选 delay_us(1); }这段代码中值得解释的参数有三个。第一,channel左移 14 位是因为通道选择位在 bit23~22,对应到 16 位暂存器里就是高两位。第二,注释里特意留了 bit21 置 1,这是增益选择位,置 1 表示输出不加缓冲、直接由外部基准驱动。若你的负载阻抗很低,必须改这个位启用内置输出缓冲,否则带载后电压会跌掉几十毫伏。第三,发送顺序是先高 16 位后低 8 位,如果把两个半字交换,DAC 会解析到错误的帧边界,输出直接乱跳。用逻辑分析仪看波形时,最明显的特征是 SYNC 保持低电平的时间跨越完整 24 个时钟沿,差一位都不行。
3.3 与 ADS131A02 的信号链接线
在实际工程里,ADC 和 DAC 都挂在同一条 SPI 总线上,分时使用两套片选。典型的接线方式如下:
| 信号 | ADS131A02 | DAC8552 | STM32F4 |
|---|---|---|---|
| SPI_SCLK | SCLK | SCLK | SPI1_SCK |
| SPI_MOSI | DIN | DIN | SPI1_MOSI |
| 片选 | CS | SYNC | PB12 / PB13 |
| 数据就绪 | DRDY | - | 外部中断引脚或 GPIO 查询 |
把 DRDY 接到 STM32F4 的外部中断引脚上,比在主循环里轮询要省 CPU。AD 转换完成拉低中断,ISR 里读走数据、计算、再写 DAC,整个环路的延迟只有软件执行时间和 DAC 的建立时间。DAC8552 的典型建立时间在微秒级,和 ADC 的数据率不在一个量级上,不会成为瓶颈。
4. STM32F4 工程移植:SPI 复用、时钟方向和那些隐藏文件
4.1 与 STM32 标准外设库对接的 SPI 配置
这个工程从文件列表能看出来,用的是 ST 标准外设库。stm32f4xx_spi.c属于外设库源码,stm32f4xx_gpio.c对应管脚配置。ADC 和 DAC 共用 SPI1,常用的引脚是 PA5(SCK)、PA7(MOSI),片选分别接普通 GPIO。初始化时需注意 SPI 的时钟极性和相位,ADS131A02 和 DAC8552 都要求 SPI Mode 1——即 CPOL=0、CPHA=1,字符长 8 位。若直接用默认的 Mode 0,ADC 的命令帧能发出去,但返回的数据会整体错位一位。
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; 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_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }SPI 时钟分频选择了SPI_BaudRatePrescaler_16,即主频 168MHz 时 SPI 时钟为 10.5MHz。ADS131A02 手册标识 SPI 最高可达 25MHz,DAC8552 稍低一些。这里刻意没有往极限上跑,原因是两个器件共用一条总线,时钟太快时帧间干扰会把 DAC 的 SYNC 边沿弄脏。需要提速时,优先把 DAC8552 放到独立的 SPI 外设上,而不是单纯调大分频系数。
若你的板子上这两个器件挂在 SPI2/SPI3 上,只需把 GPIO 复用配置和 SPI 外设名替换掉,代码逻辑不用动。但要注意,SPI2 和 SPI3 挂在 APB1 总线上,时钟上限是 42MHz,分频后实际速率比 SPI1 低一截。
4.2 Keil 工程里那些不被解释的文件
打开这个 zip 包,会看到一堆名字看起来无足轻重的文件,实际上它们各有用途。ADC.uvguix.Administrator是 uVision 图形界面的布局配置文件,保存了上次打开工程的窗口位置和断点状态,你的用户名不是 Administrator 时,Keil 会忽略它并重新生成。ADC.axf是编译链接后生成的可执行镜像,里面带了调试符号,烧录用的是 axf 提取出的 hex/bin,如果在没源码的机器上想直接看固件逻辑,可以用fromelf工具反汇编。kernel相关的keilkilll.bat是一个批处理脚本,双击它会删除当前目录下的中间文件,只保留源码和工程文件,方便重新完整编译,也可以用它在压缩包里清掉自己的编译缓存,防止发给别人时打包进无关的 .o 文件。stm32f4xx_adc.c这类文件是标准外设库的驱动源码,和题目里主角芯片无关,主要用来配置 STM32 自家的 ADC,不少初学者会在这里面翻找 ADS131A02 的寄存器定义,其实搜索的头绪完全错了。
4.3 三类实测问题与定位方法
移植过程中最容易踩的是三个点。第一,读回的数据全是 0x7FFFFF 或 0x800000 这种中间值,基本可以断定 SPI 的 CPOL/CPHA 配错了,数据在时钟沿采样位置不对,输出经过滤波器落到中点。第二,DAC8552 的某个通道输出始终为 0,CS 波形测量正常,多半是 SYNC 拉低后总线字节序反了,需要对照逻辑分析仪检查 24 位帧里的高低位顺序。第三,ADC 的 DRDY 引脚长时间无下降沿,先查复位引脚的时序,再查 CLOCK 寄存器是否写了有效分频值,最后考虑外部时钟源是否起振。这三点排查顺序按成本从低到高排列,先看波形再改代码,别一上来就把驱动全部推翻重写。
5. 进阶:用一条信号回环验证 ADC 与 DAC 的配合
单独验证两个器件能读写并不难,难的是确认整条链路是通的。这里有一个非常实用的自测方法:把 ADC 的某一通道输入短接到 DAC8552 的输出端,程序里不断读取转换结果,再做一次数字映射写回 DAC。若链路无误差,示波器上看到的 DAC 输出波形应该与原输入信号波形幅值和形状一致。这样做的价值在于,ADC 的滤波器和 DAC 的建立时间会同时体现到波形上,任何一端配置错误都会让波形形态失真。
void SignalLoop_Test(void) { int32_t adc_value = 0; uint16_t dac_value = 0; // 等待 DRDY 引脚拉低,表示新的转换结果已经就绪 if (GPIO_ReadInputDataBit(DRDY_PORT, DRDY_PIN) == RESET) { adc_value = ADS131A02_ReadChannel(0); // 读取通道 0 原始值 // 符号位直接保留,幅度做 1:1 映射到 DAC 输出 // 0xFFFF 对应 +VREF,0x8000 对应 0V dac_value = (uint16_t)((adc_value >> 8) + 0x8000); DAC8552_WriteValue(0, dac_value); // 写回到 DAC 通道 0 } }这段回环代码中有两个重要参数。adc_value >> 8是把 16 位 ADC 结果压缩到 DAC 的 8 位有效数据位,去掉低 8 位噪声,代价是分辨率降低,但信号完整性验证阶段完全够用。+ 0x8000是偏移量调整,把 ADC 双极性输出转换到 DAC 的单极性输出范围内,这样才能保证零点处两边的电压是一一对应的。
回环测试跑通后,建议再做一次校准:给 ADC 输入端加一个标准电压,比如 1.000V,然后调整代码里的增益系数,使得 DAC 输出电压等于输入电压。记录此时 ADC 读数对应 DAC 编码的比值,这就是整套信号的增益误差。把误差值和失调值固化在配置结构体里,每块板子生产时单独校准一次,比靠元器件精度硬扛要稳定不少。
本文还有配套的精品资源,点击获取