☰
基于HAL库的ADS124s08 24位ADC驱动与SPI采集实现
2026/10/21 18:46:24 网站建设 项目流程

简介:面向STM32嵌入式开发的HAL库驱动ADS124s08例程,适合需要掌握高精度24位Σ-Δ ADC采集与SPI通信的开发者。资源围绕ADS124s08在STM32上的驱动实现,讲解SPI接口初始化、寄存器读写及中断收发,并给出可参考的驱动源码,便于单片机、工业信号采集与传感器数据测量场景直接借鉴。压缩包共4个文件,包含2个C源文件和2个头文件,分别对应ADS124s08 SPI驱动与底层spi接口封装,整体仅6KB,结构精简,方便快速移植到HAL库工程中。已有891人学习浏览,适用于有一定STM32基础、希望缩短驱动开发周期并提升测量精度的嵌入式工程师。

1. HAL库驱动ADS124s08:这个24位ADC例程到底解决什么问题

做嵌入式采集板的人,迟早会碰到“传感器输出太小,16位ADC不够用”的尴尬。ADS124s08是某厂家推出的一款8通道、24位Δ-Σ型ADC,自带PGA和内部基准,适合热电偶、RTD、称重传感器这类小信号测量场景。问题是它的寄存器多、时序细,ST的HAL库又不像标准库那样有现成的外设示例可抄,很多人卡在“SPI通了,读回来全是废数据”这一步。这个例程提供了一套完整的HAL库驱动代码,覆盖从SPI初始化、寄存器配置到DRDY中断读数的完整链路,适合正在做STM32+ADS124s08项目、希望快速跑通采集通道的工程师参考复用。

2. 为什么选ADS124s08与HAL库:SPI工程骨架搭建

2.1 芯片能力和使用场景:8通道24位、内部PGA与参考源

ADS124s08最核心的价值在于“小信号直采”。它内置了可编程增益放大器,增益范围从1倍到128倍,可以把毫伏级的传感器信号直接放大到ADC的满量程附近,省掉一级外部仪表放大器。加上24位分辨率,理论上在低增益下能分辨微伏级别的变化,这对热电偶测温、称重传感器桥路采集来说很关键。

除了主ADC通道,它还内置了内部基准和两组恒流源,可以直接驱动RTD或二极管测温。内部基准的初始精度不算特别好,但做一般工业采集足够了。功耗方面,正常工作电流在几毫安级别,比外部接独立ADC加运放的方案要省不少空间和器件。

需要特别提醒的是,这芯片虽然叫“8通道”,但并不是所有通道都能同时做到最高速度的全差分测量。手册里的表写得很清楚:单端模式能用满8路,差分模式最多4对,而且数据速率越高,有效分辨率下降越明显。我一般把它当“多路复用低速高精度采集器”用,而不是当高速同步采集器用。

2.2 搭建HAL工程:CubeMX初始化SPI的时钟相位、速率和引脚

驱动ADS124s08的第一步是SPI初始化,这里最容易翻车。芯片手册要求SPI模式1(CPOL=0,CPHA=1),即空闲时钟为低、第二个边沿采样数据。HAL库里对应的参数组合是SPI_POLARITY_LOW和SPI_PHASE_2EDGE。时钟频率方面,手册给出的上限和电源电压有关,常见做法是取1MHz左右,保证长线缆下时序仍稳定。

/* SPI2 初始化参数,使用模式1(CPOL=0, CPHA=1) */ hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_2LINES; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; hspi2.Init.CLKPhase = SPI_PHASE_2EDGE; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi2);

这里的BaudRatePrescaler要根据主频算,假设主频64MHz,除以64就是1MHz的SPI时钟,刚好落在ADS124s08推荐的通信速率范围内。如果后续发现通信不稳定,优先把分频调大而不是调小,SPI慢一点问题不大,快起来反而容易出噪音。

片选引脚不要用硬件NSS,HAL库的硬件NSS在主模式下行为很别扭,常见做法是随便挑一个普通GPIO手动拉低拉高。这里把NSS设置为SPI_NSS_SOFT就是为了避免CubeMX生成额外干扰代码。另外,DRDY引脚必须配置成外部中断输入,它是ADS124s08告诉主机“数据已就绪”的唯一信号。

2.3 读寄存器验证链路:第一步先确认SPI通信正常

写完整驱动之前,先用最简单的方式验证SPI收发链路。ADS124s08的寄存器读取命令格式是固定字节流,读命令是0x20加上寄存器地址(需左移2位),之后跟着一个dummy字节,返回的才是寄存器内容。HAL库的HAL_SPI_TransmitReceive是全双工操作,收发同时进行,所以要注意接收缓冲区的第一个字节。

uint8_t readRegister(uint8_t reg_addr) { uint8_t cmd[2], rx[2]; cmd[0] = 0x20 | (reg_addr << 2); /* 读命令,地址左移2位 */ cmd[1] = 0x00; /* dummy,用于产生时钟 */ HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi2, cmd, rx, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return rx[1]; }

逻辑上,SPI是全双工协议,主机发送命令字节的同时,芯片会在MISO上拉低或输出数据。第一个字节发送完,芯片还没解析出命令,所以返回数据无意义;第二个字节开始,芯片才把目标寄存器的值放在MISO线上。rx[1]才是真正读到的寄存器内容。

上电后先把芯片复位一下,再读寄存器A3的值,正常情况下应该能读回0x00,如果读回0xFF或者乱跳,多半是SPI模式不对或者CS时序有问题。这一步通过,基本就确认物理链路没问题,后面的配置才有意义。

3. 把寄存器配置整理成结构体:初始化流程与关键参数表

3.1 寄存器映射一览和配置值怎么算

ADS124s08的寄存器从A0开始,每个寄存器8位宽,按功能分组。第一次接触时会很不适应“地址左移2位才能拼进命令字节”的设计,这是芯片内部为了统一命令格式做的特殊处理。我在例程里直接用宏定义把每个寄存器地址展平,避免每次调用都算移位。

寄存器功能关键位典型值
A0设置寄存器PGA增益、输入极性0x06(PGA=8,双极)
A1输入多路复用器MUX[3:0]0x0F(单端模式,AIN0)
A2系统控制内部基准、恒流源0x20
A3数据速率DR[3:0]0x20(400SPS)
A8/A9失调校准校准系数0x00

这里PGA增益和输入极性的组合直接影响量程。PGA=8时,满量程输入范围约为±0.3V,适合热电偶这类毫伏级信号;如果是称重传感器桥路输出,增益可以先从64倍起步,再根据实际信号幅度调整。输入极性务必和传感器接线方式对应,差分模式接反了,读出来的数据符号就是反的。

3.2 初始化序列:复位、设置数据速率、配PGA和参考源

寄存器配置顺序有讲究。先复位芯片清掉上电随机状态,再配置数据速率,最后配PGA和MUX。因为改MUX会导致内部缓冲刷新,如果顺序反了,可能第一次采样丢数据。复位命令是0x06,连续发两个字节进行双次复位更稳妥,然后必须等待至少50ms让内部LDO稳定。

void ADS124s08_Init(void) { uint8_t reset_cmd[2] = {0x06, 0x06}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi2, reset_cmd, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); HAL_Delay(50); writeRegister(A3, 0x20); /* 数据速率 400SPS */ writeRegister(A0, 0x06); /* PGA=8,双极性输入 */ writeRegister(A1, 0x0F); /* MUX选择AIN0单端 */ writeRegister(A2, 0x20); /* 内部基准使能 */ }

HAL_Delay(50)不是随便拍的,ADS124s08上电后内部模拟模块需要建立时间,特别是基准电容充电,给50ms是保守且稳妥的做法。实际量产时如果不想等这么久,可以轮询芯片的START引脚状态或读取寄存器A3确认配置写入成功,但我通常不在这上面省时间。

A2寄存器的0x20代表使能内部基准,注意这里只是“基准使能”,MUX引脚选择还要看A1寄存器的高位。如果用了外部基准,A2要改成0x00,同时硬件上REF0/REF1引脚要接到正确的电压源。内部基准和外部基准混用是常见乌龙,后面避坑章节会详细说。

3.3 使用结构体封装驱动:初始化函数和寄存器写函数

裸函数写多了容易乱,这套例程里我习惯用一个结构体把设备状态打包起来,把SPI句柄、片选引脚、当前增益、当前速率全部塞进去。好处是以后如果同一块板子上挂两片ADC,只要new两个结构体实例就行,互不干扰。

typedef struct { SPI_HandleTypeDef *hspi; GPIO_TypeDef *cs_port; uint16_t cs_pin; uint8_t pga_gain; uint8_t data_rate; } ADS124s08_t; void ADS124s08_writeReg(ADS124s08_t *dev, uint8_t reg, uint8_t val) { uint8_t cmd[2]; cmd[0] = 0x40 | (reg << 2); cmd[1] = val; HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET); HAL_SPI_Transmit(dev->hspi, cmd, 2, 100); HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET); }

写命令的格式是0x40加寄存器地址左移2位,紧跟一个数据字节。这里用一个延时保证CS释放时间足够,一般拉高CS后延时1μs再拉低发下一条命令,防止芯片内部状态机没复位。结构体方式的另一层好处是,后续加校准参数或滤波系数时,直接往结构体里加字段就行,不用改函数签名。

4. 单次与连续采样:从DRDY中断到数据拼接

4.1 时序基础:RREG/WREG、单次转换和DRDY下降沿

ADS124s08的转换流程可以用一句话概括:拉高START引脚开始转换,DRDY引脚变低表示数据准备好了,此时通过SPI读出24位结果,再拉低START进入下一轮。DRDY是开漏输出,外部必须接上拉电阻,这是数据手册里明确画的,很多人忘记接,导致中断触发不了。

单次模式和数据手册里的“连续转换模式”区别仅在于START引脚的电平状态。单次模式是把START当作触发信号,每一次转换都要重新拉一次;而连续模式直接拉高START不放,芯片会按照A3寄存器里设置的速率不断转换,DRDY每次变低都代表新数据已就绪。

4.2 单次转换流程:拉低START、等待DRDY、读24位数据

单次模式适合需要“按需采集”的场景,比如每隔100ms读一次传感器,中间还可以让ADC休眠省电。伪代码里最关键的一点是DRDY等待必须放在START拉高之后,而且要用超时保护,不能死等。

uint32_t ADS124s08_readSingle(ADS124s08_t *dev) { uint8_t data[3] = {0}; uint8_t cmd = 0x12; /* RDATA命令,读取一次转换结果 */ uint32_t raw = 0; HAL_GPIO_WritePin(START_Port, START_Pin, GPIO_PIN_SET); /* 启动转换 */ while (HAL_GPIO_ReadPin(DRDY_Port, DRDY_Pin) == GPIO_PIN_SET) { /* 这里加超时判断,500ms没等到就返回错误码 */ } HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(dev->hspi, &cmd, data, 3, 100); HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET); raw = ((uint32_t)data[0] << 16) | ((uint32_t)data[1] << 8) | data[2]; HAL_GPIO_WritePin(START_Port, START_Pin, GPIO_PIN_RESET); return raw; }

RDATA命令是0x12,发送后需要再产生24个时钟把转换结果读出来。这里用TransmitReceive,发送内容是一个命令加两个dummy字节,接收缓冲区里对应位置的数据就是ADC结果。注意24位数据是MSB在前,拼接时用左移16位和8位,别搞反了。

等待DRDY的过程一定要加超时,否则芯片SPI引脚配置错误或供电异常时,程序会死循环在这里,看门狗都救不回来。我习惯写一个500ms的等待上限,超时直接返回一个特殊值0xFFFFFFFF,调用方可以据此判断采集异常。

4.3 连续转换和中断读取:用HAL库回调配合GPIO外部中断

连续模式的效率更高,DRDY引脚下降沿触发外部中断,在中断服务函数里读数据,这样主循环可以干别的事。HAL库的外部中断回调函数在HAL_GPIO_EXTI_Callback里统一分发,首先要判断是哪个引脚触发。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DRDY_Pin) { uint8_t data[3]; uint8_t cmd = 0x12; HAL_GPIO_WritePin(CS_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi2, &cmd, data, 3, 1); HAL_GPIO_WritePin(CS_Port, CS_Pin, GPIO_PIN_SET); g_adc_raw = ((uint32_t)data[0] << 16) | ((uint32_t)data[1] << 8) | data[2]; g_data_ready = 1; } }

中断里做SPI传输要格外小心,HAL_SPI_TransmitReceive是阻塞函数,在中断里调用会占住CPU直到传输完成。好在ADS124s08的SPI速率即使只有1MHz,读3字节也就是24μs,对大部分系统来说可以接受。如果你的主频不高或中段里还有其他长任务,建议改用DMA方式,后面第六章会提到这个方案。

主循环只需要检查g_data_ready标志位,置1就把g_adc_raw取走处理,然后清零。这种“中断置标志、主循环消费”的模式比在中断里做数据处理要安全得多,不会因为浮点运算或滤波导致中断时间过长。

4.4 数据拼接与负数处理:24位二进制补码的转换公式

ADS124s08输出的是24位二进制补码,正数直接转换,负数需要做符号扩展。C语言里把24位值硬转成int32_t时,第23位是符号位,所以要先判断再手动扩展。

int32_t raw_to_signed(uint32_t raw24) { if (raw24 & 0x800000) { return (int32_t)(raw24 | 0xFF000000); /* 符号扩展 */ } else { return (int32_t)raw24; } }

如果漏掉这步,负半轴的信号读出来会是一个奇怪的大正数,在热电偶测温里表现为“温度突然跳到几百万度”,最容易让人误判成传感器开路。实际上传感器开路时芯片输出会拉到正满量程(0x7FFFFF),和负数误读的数值仍有明显区别,排查时可以用这个特征区分故障类型。

电压换算公式是:实际电压 = 转换结果 × (2×参考电压) / (增益 × 2^23)。以内部基准2.048V、PGA=8为例,每个LSB对应约0.61μV,这样量级的热电偶信号变化肉眼可见。把这个公式封装成一个函数,三路不同PGA增益的换算参数可以做成查表方式,避免每次浮点除法耗CPU。

5. 高频踩坑记录:SPI时钟相位、DRDY时序与参考电压不稳

5.1 采集数据全是0x7FFFFF或者跳动剧烈

现象是读回来的数据稳定在正满量程附近,或者高三位一直在跳,看起来像芯片坏了。原因排查下来通常是两种:一是PGA增益设置得太高,输入信号早已超过线性范围,ADC输出顶到满量程;二是MUX通道配置错,把没接传感器的悬空引脚选了进来,悬空引脚的等效噪声经过高增益放大后就是满量程跳变。

解决方法是先用低增益(PGA=1)跑一遍,把输入电压用万用表实测确认在量程内,再逐步提升增益。检查A1寄存器的高四位是否配置了正确的MUX模式,差分模式下必须确保负输入端对应引脚接对了地或传感器负端。这个方法能过滤掉八成以上的“数据异常”问题。

5.2 连续转换时中途丢数,一帧错后面全错

现象是开连续模式后前几百个数据正常,之后突然出现第一个字节固定为0x00,后面数据跟着错位。原因是DRDY下降沿之后没有等足够的时间就发起了SPI读操作,芯片还在更新输出寄存器,主机读到了未就绪的数据。很多人以为DRDY一旦拉低,数据就立刻可读,忽略了芯片内部还有一级锁存延迟。

解决方法是DRDY触发中断后,先做一个极小延时(比如用几条NOP指令)再启动SPI读取,或者在读操作完成后等待DRDY再次变高再拉低。更稳妥的做法是:只在DRDY变低之后的下一个SPI时钟沿开始读取,不要试图在DRDY的下降沿同时启动SPI。这个时序差了1μs就会造成偶发错位。

5.3 STM32的SPI片选和DRDY竞争,导致半字节错位

现象是数据有时候读对了有时候对不上,用示波器看MISO波形发现有半个字节的毛刺。原因是CS拉低的时间和DRDY变低后的数据稳定时间重叠了,芯片输出还没稳定,主机已经开始采样。ADS124s08的数据手册里对CS到DOUT有效的延迟有明确参数,但很多人不看。

解决方法是把CS拉低操作放在SPI时钟开始之前至少1μs,而不是CS和CLK同时动作。HAL库的HAL_SPI_TransmitReceive内部会先等TXE标志,但这个时间不足以覆盖CS到DOUT稳定。我自己在驱动里加了个宏定义,CS拉低后执行几条空操作的宏,成本很低,换来的稳定性提升很明显。

5.4 内部基准不准导致温漂:校准和外部基准的选择

现象是同一块板子在25℃下校准好的数据,到60℃环境里整体偏了一个固定值,而且不是线性变化。这就是内部基准的温度系数在起作用。ADS124s08的内部基准做通用采集够用,但要达到标称的温漂性能,需要自己做两点校准或改用外部精密基准。

我一般分两步解决:软件上在初始化时写入失调校准寄存器A8/A9,把零点偏移吃掉;然后在系统里存一个温度补偿系数表,按芯片附近温度传感器的读数做线性插值。如果项目对精度要求很高,最靠谱的还是外部基准,比如REF5025这类精密基准芯片,代价是板子上多一个器件和多一路电源。

5.5 HAL库SPI中断和FreeRTOS并发冲突

现象是跑裸机程序一切正常,加了个RTOS后数据偶尔错乱,而且越到高负载越频繁。原因是SPI中断处理和任务调度共同访问了同一个SPI外设,两个上下文切换导致SPI状态机错乱。HAL库本身不是线程安全的,同一个外设的传输必须串行化。

解决方法是给SPI访问加一个互斥锁,任务里读数据和中断里读数据都先获取锁;如果连续模式的中断优先级较低,可以考虑把SPI读取放到专用任务里,用信号量通知,而不是在中断里直接操作外设。这是从裸机转向RTOS时最容易忽略的结构性变化,排查起来特别耗时间。

6. 进阶:把采集逻辑改成DMA方式并加四字节对齐

6.1 用DMA接收实现多通道轮询

中断里读SPI虽然快,但总归占中断时间。改成DMA方式后,SPI传输完全由硬件搬运数据,CPU只在传输完成中断里做个标记。对ADS124s08来说,一次读3字节,DMA的配置比普通SPI传输多几步,但逻辑更清晰。

uint8_t rx_buf[3]; HAL_SPI_Receive_DMA(&hspi2, rx_buf, 3); void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI2) { g_sample_ready = 1; } }

这里用HAL_SPI_Receive_DMA而不是TransmitReceive_DMA,是因为读ADC结果这个操作的核心是接收,发送的RDATA命令只有一字节,可以先发完再切换成接收模式。实际做的时候要注意rx_buf必须声明成全局或静态变量,原因很简单——DMA传输完成后中断回调要能访问这块内存,局部变量早就出栈了。

6.2 加一个软件平均滤波

24位ADC虽然分辨率高,但在工业现场里工频干扰和随机噪声仍然存在。最简单的处理办法是做滑动平均滤波,取最近8次或16次转换结果求平均,能有效压低白噪声。因为是按2的幂次取平均,用移位代替除法,节省CPU开销。

uint32_t moving_average(uint32_t new_sample) { static uint32_t buf[16]; static uint8_t idx = 0; static uint32_t sum = 0; sum -= buf[idx]; buf[idx] = new_sample; sum += new_sample; idx = (idx + 1) & 0x0F; return sum >> 4; }

窗口长度取16时,相位滞后约16个采样周期。如果数据速率是400SPS,滞后40ms,对多数工业采集场景完全可接受。滤波后分辨率能提升一到两位有效位,但代价是响应变慢,调PID控制类项目时要权衡,不能一味拉长窗口。

6.3 设置一份自检脚本,跑一晚上观察漂移

写驱动只是第一步,ADC采集系统真正的考验在长时间运行。我的习惯是拿到例程后先做24小时漂移测试,输入端短接或接精密电压源,每5分钟记录一次数据,早上起来看曲线。如果一夜之间数据漂移了几个LSB,通常是基准温漂或PCB漏电;如果出现突然的大值跳变,多半是数字干扰耦合进模拟电路。

从那以后我每次移植新的ADC驱动,都会强制走一遍“上电读寄存器、单次采样、连续采样、24小时漂移”这套流程,一遍跑完才敢放到正式项目里用。ADS124s08这颗芯片本身性能是过硬的,真出问题的地方往往是外围电路和初始化时序,这套例程帮你把这些坑基本都填平了,希望帮到你。

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

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

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

立即咨询