从零构建稳定可靠的ADS1248 SPI驱动:硬件SPI、DMA与工程实践详解
2026/9/4 20:53:13 网站建设 项目流程

简介:这是一份面向嵌入式开发工程师与STM32/ARM等MCU初学者的ADS1248高精度ADC驱动代码资源,聚焦SPI接口通信实现,解决在工业传感、医疗测量等场景中快速集成16位模数转换器的核心难题。压缩包共3个文件(1个C源文件、1个头文件、1个README说明文档),总大小仅4KB,轻量易集成;其中ads1248.c封装了SPI初始化、寄存器读写、PGA增益配置、数字滤波器设置及单次/连续转换控制等关键函数,ads1248.h定义了结构体、命令宏与函数原型,便于直接纳入现有工程调用。已有306人学习下载,适用于需快速验证传感器信号采集方案、避免从零编写底层驱动的项目开发场景。读者可直接复用该驱动完成多通道微伏级信号采集,显著缩短ADC适配周期,并通过README快速掌握配置逻辑与典型使用流程。

1. 项目概述:从零构建一个可靠的ADS1248 SPI驱动

最近在做一个高精度数据采集的项目,核心传感器前端选用了TI的ADS1248。这玩意儿是24位Δ-Σ ADC,性能没得说,但驱动起来也确实需要花点心思。网上能找到的驱动代码要么是TI官方例程的简单移植,要么就是只实现了最基础的读写,在实际项目中用起来总是差点意思——抗干扰不行、初始化流程不严谨、或者缺少必要的错误处理。所以,我决定自己从头撸一个ADS1248-Drivers-master,目标不仅仅是“能用”,而是要“稳定、可靠、易用”,把SPI通信的细节、芯片的配置逻辑以及实际工程中的坑都填上。如果你也在用STM32、GD32或者其他任何带SPI外设的MCU来驱动ADS1248,希望这篇从实战中总结出来的经验能帮你省下不少调试时间。

2. 驱动整体架构与设计思路

2.1 为什么选择硬件SPI而非模拟SPI?

首先面临的选择是硬件SPI还是软件模拟SPI。对于ADS1248这种通信速率要求不高(通常工作在几MHz以内)但时序要求严格的器件,两者似乎都可以。但我强烈建议,只要MCU有富余的硬件SPI外设,务必使用硬件SPI

原因有三:第一是可靠性。硬件SPI由时钟发生器硬件保证时序,几乎不受中断和任务调度的影响,通信稳定性远超用GPIO翻转模拟的软件SPI。第二是CPU占用率。一次完整的ADS1248数据读取可能涉及多个寄存器操作,使用硬件SPI+DMA可以几乎零CPU开销完成数据传输,把MCU算力留给更重要的滤波算法或系统任务。第三是开发效率。硬件SPI的驱动通常由厂商HAL库或标准外设库提供,基础通信框架稳定,我们只需关注应用层协议,避免了在GPIO时序调试上耗费大量精力。

当然,如果你的硬件SPI引脚被其他关键功能占用,或者MCU资源极其紧张,软件SPI也是一个备选方案。但在本驱动设计中,我们以硬件SPI为默认和推荐方案,并会在关键处指出软件SPI需要特别注意的时序差异。

2.2 驱动层抽象:硬件隔离与可移植性

一个好的驱动不应该和具体的硬件平台绑定死。我的设计是将驱动分为两层:

  1. 硬件抽象层(HAL):这一层只负责最底层的SPI“发送”和“接收”字节操作。它向上提供一个统一的接口,比如spi_transmit_receive(uint8_t *tx_data, uint8_t *rx_data, uint16_t size)。底层可以是STM32的HAL_SPI_TransmitReceive,也可以是GD32的spi_i2s_data_transmit,甚至是模拟SPI的函数。驱动核心不关心具体实现。
  2. 设备驱动层:这一层基于HAL层提供的接口,实现ADS1248的所有具体操作:写寄存器、读寄存器、发送命令(如复位、启动转换)、读取转换数据。它包含了芯片的命令集、寄存器地址映射、以及配置逻辑。

这样的好处显而易见。当你需要把驱动从STM32F103移植到STM32F407,或者换到国民技术的芯片上时,你只需要重新实现或适配那个薄薄的HAL层,设备驱动层的几百行核心代码完全不用动,极大地提升了代码的复用性和可维护性。

2.3 关键数据结构设计

在C语言中,用结构体来管理芯片的配置状态是最清晰的方式。我定义了一个ads1248_dev_t的结构体:

typedef struct { /* 硬件依赖部分 */ spi_handle_t spi_handle; // SPI硬件句柄,由HAL层定义 gpio_pin_t cs_pin; // 片选引脚 gpio_pin_t drdy_pin; // 数据就绪引脚(用于中断或查询) gpio_pin_t reset_pin; // 复位引脚(可选,但建议硬件连接) /* 设备状态与配置 */ uint8_t reg_map[ADS1248_REG_NUM]; // 本地维护的寄存器映射缓存 ads1248_input_mux_t mux; // 输入多路复用器配置 ads1248_pga_gain_t gain; // PGA增益配置 ads1248_data_rate_t data_rate; // 数据输出速率配置 uint8_t is_continuous; // 连续转换模式标志 // ... 其他状态标志 } ads1248_dev_t;

这个结构体在驱动初始化时被创建并填充。维护一个本地的reg_map缓存非常重要。每次我们通过ads1248_write_reg函数修改寄存器后,除了发送到芯片,也会同步更新这个缓存。当需要读取某个寄存器值时,我们可以直接从缓存中读取,避免了不必要的SPI通信开销,也使得配置状态的查询非常快速。

3. SPI通信细节与底层实现

3.1 理解ADS1248的SPI模式与时序

ADS1248的SPI模式是CPOL=1, CPHA=1,也就是SPI模式3。这意味着:

  • SCLK空闲时为高电平
  • 数据在SCLK的第二个边沿(即下降沿)被采样(对于ADS1248是主机在下降沿读取数据,上升沿输出数据)。

注意:务必在你的MCU SPI初始化配置中确认模式匹配。很多驱动出问题,第一步就是SPI模式设错了,导致数据错位。用逻辑分析仪抓一下时序,SCLK空闲状态和采样边沿一目了然。

关于片选,ADS1248要求在一次完整的命令或数据帧传输期间,CS引脚必须保持低电平。帧与帧之间,CS需要拉高至少4个SCLK周期的时间。这里我强烈建议使用硬件SPI的硬件片选(NSS)功能。如果MCU支持,配置SPI外设的硬件NSS输出,可以自动管理片选信号,减轻软件负担并保证时序精确。如果不支持,就用GPIO进行软件片选,但要在驱动函数里严格把控CS拉低和拉高的时机。

3.2 底层字节传输函数的实现

这是整个驱动的基石。我以STM32 HAL库为例,展示HAL层的实现:

// ads1248_hal_spi.c static int8_t spi_xfer(ads1248_dev_t *dev, uint8_t *tx_data, uint8_t *rx_data, uint16_t len) { HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET); // 软件片选拉低 HAL_Delay(1); // 短暂延时,等待信号稳定(根据SCLK速度可调整) HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(dev->hspi, tx_data, rx_data, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET); // 软件片选拉高 HAL_Delay(1); // 帧间隔延时 return (status == HAL_OK) ? 0 : -1; }

几个关键点:

  1. 片选控制:在传输开始前拉低,传输结束后拉高。这个CS控制必须包裹住整个HAL_SPI_TransmitReceive调用。
  2. 延时:片选拉低后和拉高后的微小延时(微秒级)不是必须的,但在SCLK频率较高或布线较长时,有助于保证信号建立时间,增强通信鲁棒性。具体值需要根据你的SPI时钟频率来调整。
  3. 错误处理:函数返回了一个错误码。虽然这里简单返回了-1,但在更完善的驱动中,应该将HAL_SPI_TransmitReceive返回的status详细地传递出去,便于上层诊断是超时、总线错误还是其他问题。

3.3 命令与寄存器读写协议实现

ADS1248的指令集很简单,但组合起来要小心。所有对芯片的操作都始于一个命令字节。

写寄存器(WREG:命令字节格式是0x40 | (起始寄存器地址 & 0x0F)。紧接着的第二个字节是要写入的寄存器数量减一(N-1),然后是要写入的N个字节数据。

int8_t ads1248_write_reg(ads1248_dev_t *dev, uint8_t reg_addr, uint8_t reg_val) { uint8_t tx_buf[3] = {0}; uint8_t rx_buf[3] = {0}; tx_buf[0] = 0x40 | (reg_addr & 0x0F); // WREG命令 + 地址 tx_buf[1] = 0x00; // 写1个寄存器,所以 N-1 = 0 tx_buf[2] = reg_val; if (spi_xfer(dev, tx_buf, rx_buf, 3) != 0) { return ADS1248_ERR_SPI; } dev->reg_map[reg_addr] = reg_val; // 更新本地缓存 return ADS1248_OK; }

读寄存器(RREG:命令字节格式是0x20 | (起始寄存器地址 & 0x0F)。紧接着的第二个字节是要读取的寄存器数量减一(N-1),然后主机需要发送N个哑元字节(通常为0xFF)来驱动SCLK,从而从芯片读回N个寄存器的值。

int8_t ads1248_read_reg(ads1248_dev_t *dev, uint8_t reg_addr, uint8_t *reg_val) { uint8_t tx_buf[3] = {0}; uint8_t rx_buf[3] = {0}; tx_buf[0] = 0x20 | (reg_addr & 0x0F); // RREG命令 + 地址 tx_buf[1] = 0x00; // 读1个寄存器 tx_buf[2] = 0xFF; // 哑元字节,用于产生SCLK读回数据 if (spi_xfer(dev, tx_buf, rx_buf, 3) != 0) { return ADS1248_ERR_SPI; } *reg_val = rx_buf[2]; // 读回的数据在第三个字节 dev->reg_map[reg_addr] = rx_buf[2]; // 更新本地缓存 return ADS1248_OK; }

实操心得RREG操作时,tx_buf的后几个字节填充0xFF是标准做法。有些工程师会填充0x00,这在逻辑上没问题,因为MOSI线上的数据在读取时被忽略。但填充0xFF是一个更通用的习惯,因为对于某些SPI从设备,MOSI在空闲时需要保持高电平。

4. 驱动核心功能实现与配置

4.1 设备初始化流程:不止是上电

一个健壮的初始化流程是稳定工作的前提。绝不能只是上电后随便配几个寄存器。

  1. 硬件复位(可选但推荐):如果连接了硬件RESET引脚,拉低至少4个时钟周期再拉高,这是最彻底的复位方式。
  2. 软件复位(必须):发送RESET命令(0x06)。这个命令需要等待一段时间(典型值18个CLKIN周期)让芯片内部复位完成。我的做法是发送命令后,延时至少50微秒。
  3. 自校准(关键!):发送SELFOCAL命令(0xF0)进行偏移和增益自校准。这是影响精度的最关键步骤之一。校准期间DRDY会保持高电平,校准完成后才变低。必须等待校准完成(通过查询DRDY引脚或等待足够长时间)才能进行下一步。我通常会等待100毫秒以上。
  4. 配置寄存器:按照你的应用需求(输入通道、PGA增益、数据速率、滤波器模式等)依次配置MUX1VBIASMUX0SYS0IDAC0IDAC1GPIOCFGGPIODIR等寄存器。配置顺序有时有讲究,比如要先配好输入多路复用器和PGA增益,再启动校准,效果最好。
  5. 启动连续转换:最后,发送START命令(0x08)或者将SYS0寄存器的DRDY/DOUT位配置为数据输出模式,并发送RDATAC命令(0x10)进入连续读数据模式。

我的初始化函数大致如下:

int8_t ads1248_init(ads1248_dev_t *dev) { // 1. 初始化GPIO和SPI外设(略) // 2. 硬件复位 ads1248_hard_reset(dev); // 3. 软件复位 ads1248_send_command(dev, ADS1248_CMD_RESET); delay_us(100); // 4. 等待芯片就绪(通过DRDY引脚) if(ads1248_wait_drdy(dev, 500) != ADS1248_OK) { // 超时500ms return ADS1248_ERR_NOT_RESPONDING; } // 5. 执行自校准 ads1248_send_command(dev, ADS1248_CMD_SELFOCAL); if(ads1248_wait_drdy(dev, 1000) != ADS1248_OK) { // 校准超时1s return ADS1248_ERR_CAL_TIMEOUT; } // 6. 配置寄存器组 ads1248_configure_default(dev); // 一个封装了所有寄存器配置的子函数 // 7. 启动转换 ads1248_send_command(dev, ADS1248_CMD_START); // 或者 ads1248_send_command(dev, ADS1248_CMD_RDATAC); return ADS1248_OK; }

4.2 数据读取的两种模式与实现

ADS1248提供两种主要的数据读取方式:

1. 命令触发读取(RDATA: 这是最直接的方式。当DRDY引脚变低(表示新数据就绪),发送RDATA命令(0x12),然后连续读取3个字节(24位数据)。这种方式软件控制清晰,但效率较低,因为每次读取都需要命令交互。

int8_t ads1248_read_data_once(ads1248_dev_t *dev, int32_t *raw_data) { if (ads1248_wait_drdy(dev, 100) != ADS1248_OK) { return ADS1248_ERR_DRDY_TIMEOUT; } uint8_t tx_buf[4] = {ADS1248_CMD_RDATA, 0xFF, 0xFF, 0xFF}; uint8_t rx_buf[4] = {0}; if (spi_xfer(dev, tx_buf, rx_buf, 4) != 0) { return ADS1248_ERR_SPI; } *raw_data = ((int32_t)rx_buf[1] << 16) | ((int32_t)rx_buf[2] << 8) | (int32_t)rx_buf[3]; // 注意符号扩展:24位有符号数转换为32位有符号数 if (*raw_data & 0x00800000) { *raw_data |= 0xFF000000; } return ADS1248_OK; }

2. 连续读取模式(RDATAC: 发送RDATAC命令(0x10)后,芯片进入一种特殊模式。此后,只要DRDY变低,你就可以直接读取3个字节的数据,而无需再发送RDATA命令。读取完成后,DRDY会自动拉高,直到下一次转换完成。这种方式效率高,特别适合配合DMA进行高速连续采集。

// 先发送RDATAC命令进入连续模式 ads1248_send_command(dev, ADS1248_CMD_RDATAC); // 然后在一个循环或中断中,检测到DRDY变低就直接读取数据 void drdy_interrupt_handler(void) { // DRDY引脚下降沿中断 uint8_t rx_buf[3]; // 注意:此时不需要发送命令字节,直接读3个数据字节 spi_xfer(dev, NULL, rx_buf, 3); // 发送NULL或全0xFF,产生SCLK int32_t raw_data = (rx_buf[0] << 16) | (rx_buf[1] << 8) | rx_buf[2]; // ... 处理数据 }

重要提示:在连续读取模式下,如果你想停止连续读取或发送其他命令,必须先发送SDATAC命令(0x11)来退出连续模式。否则,你的命令字节会被芯片当作数据字节的一部分,导致通信混乱。

4.3 PGA增益、数据速率与滤波器的配置权衡

ADS1248的精度和速度需要根据应用场景权衡配置,主要在SYS0寄存器中设置。

  • PGA增益(GAIN):可选1~128。增益越高,对小信号的灵敏度越高,但输入范围越小,噪声也可能被放大。如果你的信号幅度很小(如热电偶),需要高增益。但如果信号幅度较大,使用高增益会导致ADC饱和,输出全为正值或负值最大值。一般原则是让信号尽可能占满ADC的量程(接近±VREF/GAIN)而不饱和,这样可以获得最佳的信噪比。
  • 数据速率(DR):从5 SPS到2000 SPS。速率越高,输出数据越快,但噪声也越大,有效位数(ENOB)会下降。ADS1248在低速模式下(如20SPS)通过更强大的数字滤波器可以实现接近24位的无噪声分辨率。在数据手册里通常有一张“有效分辨率 vs 数据速率”的图表,是你的重要参考。对于直流或慢变信号(如温度、压力),选择低数据速率(如20SPS)以获得最高精度;对于需要快速响应的动态信号,才需要提高数据速率。
  • 滤波器模式:除了默认的sinc滤波器,ADS1248还支持脉冲抑制等模式。对于工频干扰(50/60Hz)严重的环境,可以将数据速率设置为工频的整数倍(如20SPS, 25SPS, 100SPS),并利用sinc滤波器的陷波特性来抑制干扰。

我的配置函数通常会提供一个预设:

typedef enum { ADS1248_MODE_HIGH_RES, // 高分辨率模式:低速率,高增益(如20SPS, GAIN=128) ADS1248_MODE_HIGH_SPEED, // 高速模式:高速率,低增益(如1000SPS, GAIN=1) ADS1248_MODE_POWER_SAVE, // 低功耗模式:低速率,关闭PGA等 } ads1248_work_mode_t; int8_t ads1248_set_mode(ads1248_dev_t *dev, ads1248_work_mode_t mode) { uint8_t sys0_val = 0; switch(mode) { case ADS1248_MODE_HIGH_RES: sys0_val = (0x00 << 5) | (0x03 << 2) | 0x00; // DR=20SPS, PGA=128 break; case ADS1248_MODE_HIGH_SPEED: sys0_val = (0x06 << 5) | (0x00 << 2) | 0x00; // DR=1000SPS, PGA=1 break; // ... 其他模式 } return ads1248_write_reg(dev, ADS1248_REG_SYS0, sys0_val); }

5. 高级话题与工程实践优化

5.1 利用DMA实现后台高速数据流

对于需要连续高速采集的应用(如振动分析、音频采集),使用查询或中断方式读取SPI数据会大量占用CPU。此时,SPI DMA是必选项。

配置思路

  1. 将MCU的SPI Rx配置为DMA模式,目标地址是一个循环缓冲区(circular buffer)。
  2. 在连续读取模式(RDATAC)下,由于不需要发送命令,可以配置SPI为只接收模式(或者发送固定哑元数据,如0xFF)。
  3. 启动DMA传输,设置传输数据宽度为字节,传输数量为3的倍数(因为每个数据点是3字节)。
  4. 使能DMA传输完成中断或半传输完成中断。在中断服务程序里,从缓冲区中取出完整的3字节数据,拼接成24位采样值。

关键点:你需要精确计算DMA缓冲区大小和中断触发频率,确保数据处理速度跟得上ADC的输出速度,避免缓冲区溢出。同时,要处理好24位数据的字节序(MSB first)和符号扩展。

5.2 基准电压(VREF)管理与噪声抑制

ADS1248的精度极度依赖一个干净、稳定的基准电压(VREF)。即使ADC本身性能再好,一个糟糕的基准也会毁掉一切。

  • VREF选择:可以使用内部2.048V基准,也可以使用外部基准。内部基准方便但温漂和噪声相对较大(典型值5ppm/°C)。对于高精度测量,强烈推荐使用外部低噪声、低温漂的基准源芯片,如TI的REF5025、REF5040等。
  • PCB布局:VREF引脚到ADC的走线要尽可能短而粗,并用地线包围。在VREF引脚和AGND之间紧贴芯片放置一个高质量的X7R或X5R陶瓷去耦电容(如10μF + 0.1μF)。
  • 模拟电源滤波:AVDD和DVDD(即使它们连接在同一电源)都应该分别用磁珠或0Ω电阻隔离,并各自配备去耦电容。模拟地(AGND)和数字地(DGND)通常在芯片下方单点连接。

5.3 多片ADS1248的同步与级联

在一些多通道同步采集系统中,可能需要使用多片ADS1248。这时会面临两个问题:同步启动转换和同步读取数据。

  • 同步启动:所有ADS1248的START引脚可以连接在一起,由MCU的一个GPIO统一控制。拉高START引脚可以同时启动所有ADC的转换。注意,START引脚是电平敏感,不是边沿敏感。
  • 同步读取与SPI总线共享:多片ADC可以共享同一个SPI总线,但片选(CS)独立。这样可以用最少的MCU SPI外设驱动多片ADC。挑战在于如何同步读取数据。因为每片ADC的转换完成时间(DRDY变低)可能有微小差异。一种方法是采用“主-从”触发:以第一片ADC的DRDY作为主机的中断源,当中断到来时,依次读取所有ADC的数据(通过各自的CS选通)。由于SPI速度很快(MHz级别),而ADC输出速率较低(最高2kSPS),这个读取过程的时间差通常可以忽略不计。更精确的同步需要用到ADC的SYNC功能,但这涉及到更复杂的时序控制。

6. 调试技巧与常见问题排查

驱动开发过程中,问题不可避免。以下是我踩过坑后总结的排查清单:

现象可能原因排查步骤与解决方案
SPI通信完全无响应1. 硬件连接错误(电源、地、线序)
2. SPI模式(CPOL/CPHA)不匹配
3. 片选(CS)信号问题
4. 芯片未正确复位
1. 用万用表检查电源电压(AVDD, DVDD)和地连接。
2.用逻辑分析仪抓取SPI时序,确认SCLK空闲电平、采样边沿与ADS1248要求(模式3)一致。
3. 确认CS信号在传输期间为低,帧间为高。
4. 尝试发送RESET命令(0x06),并等待足够时间。检查DRDY引脚状态,复位后应为高。
能读写寄存器,但读取的ADC数据全为0或固定值1. 转换未启动
2. 输入通道(MUX)配置错误
3.DRDY信号未正确监控
4. PGA增益过高,输入信号饱和
1. 确认已发送START命令或RDATAC命令。
2. 检查MUX1MUX0寄存器,确认选择了正确的正负输入对。
3. 在读取数据前,务必等待DRDY引脚变低(或查询状态寄存器)。
4. 降低PGA增益,或检查输入信号是否在允许的范围内(±VREF/GAIN)。
读取的数据噪声大,跳动剧烈1. 电源噪声
2. 基准电压(VREF)不稳定
3. 数据速率(DR)设置过高
4. PCB布局不佳,数字噪声耦合
1. 用示波器观察AVDD和VREF引脚,看是否有高频毛刺。加强电源滤波。
2. 换用更稳定的外部基准源。
3. 降低数据速率,牺牲速度换取精度和更低的噪声。
4. 检查模拟部分走线,是否远离数字信号线(特别是时钟线和SPI线)。确保AGND平面完整。
在连续读取模式(RDATAC)下,数据错乱或驱动卡死1. 未正确处理DRDY与读取的时序
2. 未发送SDATAC就尝试发送其他命令
3. SPI传输被更高优先级中断打断
1. 确保在DRDY变低才发起SPI读取。在中断服务程序中,读取操作应尽可能快。
2.在切换出连续读取模式前,必须先发送SDATAC命令(0x11)
3. 提高SPI中断优先级,或在SPI传输期间临时关闭全局中断。
不同通道间读数存在固定偏移1. 输入多路复用器(MUX)的导通电阻不一致
2. 通道间校准未做
1. 这是ADC本身的特性。对于需要极高精度的场合,需要对每个通道进行单独的偏移校准(使用SYSOCAL命令)。
2. 在驱动中实现一个校准函数,记录每个通道的零点偏移值,在后续读数中软件补偿。

调试利器——逻辑分析仪:一个支持SPI协议解码的逻辑分析仪(即使是几十块的国产货)是调试此类驱动不可或缺的工具。它能直观地显示SCLK、MOSI、MISO、CS上的每一个比特,让你清楚地看到发送的命令、收到的数据以及精确的时序,绝大多数通信问题都能靠它定位。

最后,分享一个我个人的体会:编写一个像ADS1248-Drivers-master这样的驱动,最难的不是实现基本功能,而是构建一个健壮、容错、易于调试的框架。比如,在每一个SPI通信函数里都加入超时判断和错误码返回;在初始化流程中,增加对芯片ID寄存器的读取验证,确保通信链路基本正常;提供丰富的调试日志接口,可以在开发时打印出所有的寄存器配置和通信数据。这些“额外”的工作,在项目后期排查那些偶发性的、匪夷所思的问题时,会成为你的救命稻草。把驱动当作一个产品来打磨,而不仅仅是一个让芯片跑起来的工具,这份投入在长期的工程实践中一定会带来回报。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询