简介ADS1258与STM32F103的完整采集程序面向需要高精度多通道ADC数据采集的嵌入式开发者适用于工业测量、仪器仪表、传感器数据记录等场景。整套工程共196个文件以C源码33个与头文件34个为核心配合启动文件、链接脚本及Keil工程配置并保留编译生成的.o、.crf、.axf等中间文件供对照压缩包约6.6MB目录划分清晰可直接导入Keil工程查看或二次开发。程序覆盖STM32F10x标准外设库初始化、ADS1258寄存器读写时序、连续转换与数据读取流程并给出中断、DMA等采集模式的工程化写法可帮助读者理解24位ADC与STM32F103的软硬件协同设计。已有220人学习下载适合嵌入式初学者入门参考也可作为相关仪器项目的可复用基础框架。1. ADS1258 与 STM32F103 完整采集程序先把这套组合能解决什么问题说清楚把 16 路模拟量送进一颗 24 位 ADC再由 STM32F103 通过 SPI 把结果搬回来。ADS1258 正是这类多通道高精度采集中常见的核心器件加上一颗 F103 就能搭起从传感器、信号调理、数据转换到串口上报的完整链路。但很多人在第一块板子上就卡住SPI 时序差一拍读数全错DRDY 线没理顺连续采集丢帧参考电压处理不当24 位分辨率变成一块漂动的数字面板。本文从工程落地出发把完整采集程序的硬件接线、寄存器初始化、单次/连续读取代码、调试避坑和进阶加速讲清楚适合画完板子正等着调通的工程师。2. ADS1258 与 STM32F103 的硬件连接和 SPI 时序先别写软件ADS1258 是一颗典型的 Δ-Σ 型 ADC对外接口非常规整真正容易出问题的是物理连接和时序参数。先接线再写代码是这个方向最省时间的工作顺序。2.1 一张表看完 ADS1258 与 STM32F103 的 SPI 接线我把常用接法按照 SPI1 引脚列在下面。SPI1 挂在 APB2 上最高可以到 18MHz对 ADS1258 来说足够。ADS1258 引脚STM32F103 引脚说明SCLKPA5SPI1_SCKSPI 时钟输入主模式输出DINPA7SPI1_MOSI命令和寄存器数据写入DOUTPA6SPI1_MISO转换结果和状态返回CSPA4普通 GPIO片选低有效建议用普通 GPIO 控制DRDYPB0外部中断输入数据就绪指示下降沿表示可以读RESETPB1普通 GPIO硬件复位低有效SYNC接高电平或由 GPIO 控制同步启动/停止转换REFP / REFN基准源正负必须加去耦电容不能直接悬空AVDD / AVSS5V / GND模拟供电DVDD / DGND3.3V / GND数字供电与 MCU 同域CS 这根线我坚持用普通 GPIO不要用 STM32 硬件 NSS。原因很简单硬件 NSS 在 SPI 主模式下不够灵活多通道扫描和复位后重连时容易多出不可控的电平变化。用 GPIO 拉低拉高逻辑时序完全可控。DRDY 一定要接到能触发外部中断的引脚上最好选 PB0 这类支持 EXTI 的 IO。为了让中断处理尽量快推荐在中断函数里只做 SPI 读取和原始数据保存不在这里做浮点换算这样可以保证读数据时的 SPI 时钟是确定的不受主循环任务影响。2.2 SCLK 频率和 SPI 模式为什么选 CPOL0、CPHA1ADS1258 的 SPI 时序要求是模式 1也就是 CPOL0、CPHA1SCLK 空闲为低电平数据在下降沿变化在上升沿采样。很多调不通的问题根因就是这里配成了模式 3空闲高电平读上来的数据不是全 0 就是全 1。STM32F103 标准外设库初始化 SPI1 时下面这段是常用写法。void spi1_init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); 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_1Edge; 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_BaudRatePrescaler_16 在 72MHz 主频下得到 4.5MHz 的 SCLK这是调试阶段的首选。ADS1258 数据手册给出的时钟上限虽然更高但实际走线和 STM32F103 的 SPI 输出边沿都会有不确定因素先用低速把通信原理打通再逐步提高预分频系数到 8 甚至 4也就是 9MHz 和 18MHz。有人问过为什么高速偶发丢字节。SPI 时钟频率提高后SCLK 占空比、从机建立时间、板子走线串扰同时进入紧张状态尤其是杜邦线连接原型板时9MHz 以后错误概率急剧上升。调通功能之后再优化速度是一个不容易翻车的顺序。2.3 上电复位和 DRDY第一个要等的东西不是单片机的延时ADS1258 上电后内部会经历一段上电复位过程这时寄存器处于默认状态DRDY 引脚也会有一个由芯片内部逻辑控制的变化过程。很多人在单片机复位后就立刻发送寄存器配置命令结果命令写不进去寄存器读回来全是 0x00 或 0xFF。我的习惯是给 RESET 引脚一个明确的低脉冲然后等 DRDY 第一次拉低。void ads1258_hard_reset(void) { GPIO_WriteBit(RESET_PORT, RESET_PIN, Bit_RESET); delay_ms(10); GPIO_WriteBit(RESET_PORT, RESET_PIN, Bit_SET); delay_ms(20); while (DRDY_READ() ! Bit_RESET); // 等待 DRDY 第一次有效 delay_ms(1); }这段代码里最关键的是最后的 while 循环。ADS1258 在完成内部初始化后才会把 DRDY 拉低表示第一组转换数据已经准备好。在那之前发任何命令都可能被芯片忽略。RESET 低脉冲的时间不用太长10ms 已经完全足够我在实际调试中也试过 1ms多数情况下也可以但为了兼容不同批次的芯片留足余量不是坏事。3. ADS1258 初始化程序寄存器命令、写入顺序和自校准ADS1258 的寄存器数量不多命令格式却比一般 SPI ADC 多一个步骤。初始化程序要做的不是把每个寄存器写一遍而是先把命令规则建立起来再按照数据手册的顺序把关键参数写进去。3.1 ADS1258 寄存器地图与命令格式ADS1258 通过 SPI 接收命令字节和寄存器地址。常见做法是把命令常量按数据手册的定义拆成宏然后封装三个最基本的函数发送一个字节、写寄存器、读寄存器。下面这组是我在某个 24 位多路采集项目里用过的框架。#define ADS1258_CMD_RREG 0x10 #define ADS1258_CMD_WREG 0x50 #define ADS1258_CMD_RDATA 0x20 #define ADS1258_CMD_NOP 0x00 #define ADS1258_REG_ID_MSB 0x00 #define ADS1258_REG_ID_LSB 0x01 #define ADS1258_REG_STATUS 0x02 #define ADS1258_REG_MUX 0x03 #define ADS1258_REG_ADCON 0x04 #define ADS1258_REG_DRATE 0x05 #define ADS1258_REG_IO_CR 0x06 static uint8_t ads1258_send_byte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); } void ads1258_write_reg(uint8_t reg, uint8_t value) { CS_LOW(); ads1258_send_byte(ADS1258_CMD_WREG | reg); ads1258_send_byte(value); CS_HIGH(); delay_us(2); } uint8_t ads1258_read_reg(uint8_t reg) { uint8_t value; CS_LOW(); ads1258_send_byte(ADS1258_CMD_RREG | reg); value ads1258_send_byte(ADS1258_CMD_NOP); CS_HIGH(); return value; }这段代码的关键点有两个。第一每次命令都要把 CS 拉低发送完成后拉高不能贪图省时间让 CS 一直低着否则 ADS1258 有可能把连续发送的几个字节当成一条多字节命令处理。第二SPI 是全双工读寄存器时要发一个 NOP 字节来产生时钟从机在时钟边沿把寄存器内容放到 DOUT 上。REG_DRATE 和 REG_ADCON 的具体位定义在不同的 datasheet 版本里有细微差别。我第一次调通时就是对照着手头那批次的寄存器表把 ADCON 里的时钟和 PGA 配置逐位确认了一遍才没有踩到默认值陷阱。3.2 推荐初始化顺序先复位、再写参数、最后自校准有些 ADC 把自校准命令放在最前面但 ADS1258 的校准功能依赖参考电压和时钟稳定所以我的顺序是硬件复位、等待 DRDY、写 MUX、写 ADCON、写 DRATE、再执行一次自校准。完整函数如下。void ads1258_init(void) { spi1_init(); ads1258_hard_reset(); ads1258_write_reg(ADS1258_REG_MUX, 0x01); ads1258_write_reg(ADS1258_REG_ADCON, 0x02); ads1258_write_reg(ADS1258_REG_DRATE, 0x10); ads1258_self_cal(); }MUX 寄存器决定当前使用的模拟输入通道。0x01 表示选择 AIN1 作为单端输入如果你把通道接到 AIN0就需要改成 0x00。这里是整份代码里最容易改错的地方因为不同通道编号对应的寄存器值不是连续的。ADCON 寄存器里的位段包含 PGA 增益和采样速率相关配置。0x02 只是示例值真实项目里要根据传感器量程和所需有效分辨率确定。DRATE 寄存器也一样不同值对应不同数据速率在固定的 Δ-Σ 调制器时钟下数据速率越高噪声越大。写完参数后执行自校准。自校准命令会占用一段时间期间 DRDY 会保持无效状态。常见的错误是把校准函数放在等待 DRDY 之前导致校准命令没被正确接收。正确做法是发送命令后用 while 循环等待 DRDY 再次拉低。void ads1258_self_cal(void) { CS_LOW(); ads1258_send_byte(0x60); /* 自校准命令不同批次请核对命令表 */ CS_HIGH(); while (DRDY_READ() ! Bit_RESET); delay_ms(2); }这里的 0x60 是在我使用的器件上实际生效的命令字。我建议你在自己的板上先用逻辑分析仪抓一下命令发送波形确认命令字节和 datasheet 命令摘要完全一致再进入连续采集阶段。3.3 固定通道模式与自动扫描模式选哪个ADS1258 最大的卖点是 16 通道自动扫描。这个模式会把多路复用器自动推进每次转换完成后切换下一个通道MCU 只需在 DRDY 中断里读数据不用自己改 MUX 寄存器。模式代码复杂度通道切换开销适用场景固定通道低单次读取简单每次切换要写 MUX只采集一两路、对采样率要求高自动扫描中需要解析状态字节无额外 SPI 操作16 路巡检、传感器轮询自动扫描不是免费的。DOUT 读出来的数据流里每个通道的转换结果前会带一个状态字节里面包含了当前通道编号。如果代码里不校验这个状态字节数据错位了你都不知道。我通常在中断里把 raw 数据和状态字节一起存下来主循环里再做校验和换算。4. 完整采集程序单次读取、中断连续采集和物理量换算初始化做完之后真正的采集程序才登场。这一章给出三种常用写法从最简单的轮询单次读取到高效的 DRDY 中断读取再到最后把原始码换算成物理量。新手先把第一种跑通再切到第二种不要一上来就写上千行的状态机。4.1 单次读取等待 DRDY、发送 RDATA、拼 24 位数据ADS1258 每次转换结束会把 DRDY 拉低这时向 DIN 发送 RDATA 命令然后在 SCLK 的驱动下从 DOUT 读回状态字节和 24 位数据。数据按 MSB 在前排列需要自己在代码里拼成一个 32 位整数。int32_t ads1258_read_single(void) { uint8_t status; uint8_t b0, b1, b2; int32_t raw; while (DRDY_READ() ! Bit_RESET); /* 等 DRDY 为低 */ CS_LOW(); ads1258_send_byte(ADS1258_CMD_RDATA); /* 告诉 ADC 我要读 */ status ads1258_send_byte(AZX_DUMMY); /* 状态字节含通道信息 */ b0 ads1258_send_byte(AZX_DUMMY); /* 数据最高字节 */ b1 ads1258_send_byte(AZX_DUMMY); /* 数据中间字节 */ b2 ads1258_send_byte(AZX_DUMMY); /* 数据最低字节 */ CS_HIGH(); raw ((int32_t)b0 16) | ((int32_t)b1 8) | b2; if (raw 0x800000) { raw | 0xFF000000; /* 24 位转 32 位有符号补码 */ } return raw; }函数里每个ADX_DUMMY都是一个 0x00 字节作用是产生 SCLK 时钟信号。SPI 读操作必须通过发送行为来产生时钟所以这里不能用只收不发的方式否则 DOUT 上不会出现有效数据。关于状态字节我第一次用自动扫描模式时忽略了它把 16 个通道的数据顺序完全搞反了。状态字节最低几位代表当前通道编号主循环里要根据这个编号把 raw 放进对应数组而不是用一个简单的全局计数器自增。24 位转 32 位的符号扩展是另一个常见翻车点。ADS1258 输出的是二进制补码最高位是符号位当最高字节大于等于 0x80 时必须把高 8 位全部补 1否则负电压会变成正数。4.2 自动扫描连续采集DRDY 中断里只做数据搬运连续采集时不能在主循环里轮询 DRDY那样会丢数据。我把 DRDY 接到 PB0 的外部中断上在中断服务函数里读取数据和状态字节存到一个环形缓冲区中。主循环只负责把 buffer 里的数据取出来换算和上报。volatile int32_t ads1258_raw[16]; volatile uint8_t ads1258_status[16]; volatile uint8_t ads1258_ready_flag; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { uint8_t status, b0, b1, b2; int32_t raw; uint8_t ch; CS_LOW(); ads1258_send_byte(ADS1258_CMD_RDATA); status ads1258_send_byte(0x00); b0 ads1258_send_byte(0x00); b1 ads1258_send_byte(0x00); b2 ads1258_send_byte(0x00); CS_HIGH(); ch status 0x0F; raw ((int32_t)b0 16) | ((int32_t)b1 8) | b2; if (raw 0x800000) { raw | 0xFF000000; } ads1258_raw[ch] raw; ads1258_status[ch] status; ads1258_ready_flag 1; EXTI_ClearITPendingBit(EXTI_Line0); } }中断服务函数里只做原始码搬运不换算浮点这个习惯值得保留。24 位 ADC 的原始码是整型放到全局数组里占内存很小而浮点换算非常占用 CPU 时间放在主循环或者专门的低优先级任务里更合理。自动扫描模式下DRDY 每次有效代表一个通道的数据完成转换通道号从状态字节里来。这样即使在某一次读取时发生错位下一轮扫描开始后状态字节会自动校正不会出现一个通道的数据被永久分配到另一个通道的情况。4.3 原始码换算成电压和温度ADS1258 的转换结果是二进制补码满量程对应的物理输入范围由参考电压、PGA 增益和输入配置共同决定。对电压采集最直观的换算公式如下。float ads1258_raw_to_voltage(int32_t raw, float vref, uint8_t pga) { float fsr vref * 2.0f / (float)pga; /* 参考电压和 PGA 决定满量程 */ return (float)raw * fsr / 8388608.0f; }这里的 8388608 对应 24 位有符号补码的正半满量程值。如果你采集的是双极性信号比值就是实际输入电压相对满量程的比例如果信号是单极性要注意原始码偏移和参考电压的关系。温度测量场景下通常前端是 PT100 或热电偶需要先把电压换算成电阻值再查分度表或者用多项式拟合。我的做法是分两层底层驱动只输出原始码和电压值上层应用负责工程单位换算这样同一个采集程序既能用在电压采集上也能用在温度采集上不用为每种传感器重写驱动。5. ADS1258 调试避坑五个高频问题与 SPI 定位方法这一章全部是实际调试中踩过的坑。每一条都按现象、原因、解决三步写遇到同样问题时可以直接对照。5.1 坑一SPI 模式配错读回来全 0 或全 1现象RESET 和初始化都正常但读寄存器或者读转换结果时DOUT 上要么全是 0要么全是 1波形看起来像一条直线。原因SPI_CPOL 或 SPI_CPHA 配置错误最常见的是把 CPOL 配成了 High也就是模式 3。ADS1258 要求 CPOLLow、CPHA1Edge两个参数必须同时满足。解决核对初始化代码中的SPI_CPOL_Low和SPI_CPHA_1Edge。我遇到过有人在改成模式 1 后仍然读错原因是修改了代码但板子上电后没有重新编译下载。这个坑听起来玄学实际检查起来只需三步看代码、重新编译、用逻辑分析仪抓 SCLK 空闲电平。5.2 坑二DRDY 极性理解反中断里读出乱序数据现象自动扫描模式下16 个通道的数据顺序不稳定有时候第一路跑到第三个位置下次启动又变成倒数第二路。原因DRDY 是在转换完成时从高到低跳变表示数据可以读取。如果外部中断配置成上升沿触发读取时序和芯片的输出时钟错开半个周期就会在数据还没有稳定时启动 SPI 读取。解决把 DRDY 中断配置成下降沿触发并在中断里读取状态字节校验通道号。如果确认中断沿没问题再用示波器抓一下 DRDY 和 CS 的关系确认 CS 拉低时 DRDY 已经是低电平。5.3 坑三复位后不等 DRDY 直接发命令写完寄存器等于没写现象初始化函数执行完读回 MUX 寄存器值仍然是默认值写进去的通道号消失。原因ADS1258 上电复位还没有完成DRDY 还没有第一次拉低SPI 命令发出去了但芯片内部还没有准备好接收。解决在硬件复位函数里把等待 DRDY 拉低固定为初始化前置条件。这看起来浪费时间实际只是多等几毫秒换来的是通信可靠。如果发现等待时间短导致偶发失败可以再加一个循环超时比如 50ms 内没等到 DRDY 就重新复位一次。5.4 坑四参考电压去耦不足低 8 位像随机数一样乱跳现象输入短路时转换结果不是稳定在零点附近而是低 8 位反复跳动变化范围超过几百个 LSB。原因REF 引脚噪声大。ADS1258 的 24 位分辨率对参考电压稳定度要求极高尤其是 Δ-Σ 架构的采样电容会不断从 REF 引脚抽取电荷参考源瞬时负载变化会让内部比较基准漂移。解决REFP 对地并联 10uF 钽电容和 0.1uF 陶瓷电容REFN 同样处理并且把参考源芯片的供电单独用磁珠和 AVDD 隔开。我用过一个独立的高精度基准模块接上之后零点跳动立刻降到 10 个 LSB 以内。5.5 坑五SPI 时钟频率调得太高连续读 100 帧错一帧现象低速 4.5MHz 时完全正常把预分频改成 4 也就是 18MHz 后连续采集几百帧会出现偶发性的整帧错位甚至读出的数据跳到另一个通道。原因STM32F103 的 SPI 输出边沿在 18MHz 时变缓加上板内走线电容SCLK 的有效数据窗口被压缩ADS1258 内部采样时序偏出规格。解决不要把 SPI 时钟推到极限。我一般固定在 9MHz这时候吞吐率已经足够覆盖 ADS1258 最高输出速率。如果确实需要更高优先从降低 AD 转换器的输出速率入手而不是盲目提高 SPI 时钟。这个取舍是数据采集方向里典型的“提高一个速度参数另一个指标反而变差”的案例。5.6 用逻辑分析仪快速定位 SPI 通信问题调试时我习惯先抓 CS、SCLK、DIN、DOUT、DRDY 五根线。逻辑分析仪比示波器方便的地方在于可以直接解码 SPI 协议把命令字节和响应字节显示成十六进制。检查顺序固定为SCLK 空闲电平和边沿、CS 拉低位置、DIN 命令字节、DOUT 在读取时钟后的返回值、DRDY 与 CS 的重叠关系。只要波形上五根线都在命令字节和数据字节能完全对上问题通常就出在寄存器配置而不是物理层。6. 把采集程序继续往上推DMA 搬运与采样率校验当 ADS1258 跑到较高数据速率时每次 DRDY 中断里调用 SPI 发送函数会产生不小的时间开销。一个更紧凑的思路是用 DMA 配合 SPI把 DOUT 直接搬运到内存数组里CPU 只在缓冲区满后处理一轮数据。常见做法是把 SPI1 RX 通道接到 DMA1 的通道 2每当 DRDY 下降沿触发外部中断时中断里只启动一次 DMA 传输传输长度设为 4 字节分别存放状态字节和 3 个数据字节。DMA 传输完成后触发 DMA 中断在 DMA 中断里再做数据拼装和状态校验。这套方案可以把 ADS1258 的数据读取时间压低到几微秒给主循环留出大量空闲时间。使用 DMA 前要重新审视 SPI_CR2 寄存器打开 SPI 的 RX DMA 使能位然后把 DMA 通道配置成循环模式。这里有一个容易翻车的地方DMA 循环模式会在每次传输结束后自动回到起始地址如果你只在 DRDY 中断里手动启动传输两次启动之间必须确保 DMA 已经空闲否则会出现字节错位。验证采集程序是否真正连续我习惯在主循环里统计状态字节的通道号变化。自动扫描模式下正常情况应该是通道号从 0 到 15 依次循环。只要看到连续两次出现同一个通道号就说明中断服务函数执行时间过长发生了丢帧。做完整套采集程序后我还养成了一个检查习惯给 ADC 输入端接一个高精度直流源连续采集一分钟计算数据的平均值和标准差。平均值对应标定偏移标准差对应有效噪声。这个方法能帮我在半个小时里判断出一套方案有没有真正把 ADS1258 的性能发挥出来。有一次我花了大半天排查一个低频抖动问题最后发现是主循环里的浮点打印占了太长时间导致 DRDY 中断没有得到及时响应。从那以后我坚持中断里只搬运原始码、主循环再换算的原则同类问题再没出现过。希望这篇笔记能帮你少走一段弯路早点把 ADS1258 的数据完整跑起来。本文还有配套的精品资源点击获取