简介:面向STM32嵌入式开发者,这份基于HAL库的ADS124s08驱动例程提供了一套完整的SPI通信与数据采集实现方案。ADS124s08是一款24位高精度、低噪声的Σ-Δ模数转换器,内置可编程增益放大器,能处理微伏级输入信号,配合STM32的硬件SPI接口,可满足工业信号采集、传感器数据检测、电源监控等对测量精度要求较高的应用场景。资源压缩包共含4个文件,其中2个C源码文件实现SPI初始化、寄存器读写、中断收发与误差校正逻辑,2个头文件完成外部接口声明,整体仅6KB,结构紧凑,便于直接复制到现有工程中快速适配。驱动代码不仅演示了HAL库SPI外设的标准配置流程,还提供了典型的数据读取和校正思路,可帮助开发者理解ADS124s08的工作时序与驱动编写方法。目前已有891人学习下载,无论正在调试ADS124s08,还是希望学习STM32 SPI驱动的单片机工程师,都能从中获得可直接参考的实现模板。
1. 为什么用HAL库驱动ADS124S08:一个24位采样器的工程打开方式
做高精度采集时,我遇到过一批奇怪的数据:温度传感器接上去,读数在零点几度范围内乱跳,怎么滤波都压不住。换了仪表放大器、加了RC滤波,最后把锅定位到ADC上——那块24位芯片的时序和初始化配置完全没被正确对待。于是我把目光落到ADS124S08上:8通道、24位ΔΣ架构、自带PGA和内部基准,非常适合桥式传感器和RTD测温这类小信号场景。但芯片再好,驱动写不好照样翻车,尤其是用STM32的HAL库去控制SPI时序,很多细节是标准库时代不需要操心的。
这套例程面向的不是看Datasheet从头研究协议的人,而是手头有STM32、想快点把ADS124S08跑起来做数据采集的嵌入式工程师。你不需要重新发明寄存器配置,但得理解每一步在做什么。下面从硬件接线开始,一路讲到寄存器读写、数据换算和现场坑位。
2. 接线与SPI时序:先把硬件的底子打牢
2.1 SPI模式为什么非选 MODE_1 不可
ADS124S08的SPI时序要求是:SCLK空闲时为低电平,数据在SCLK的上升沿被设备采样、下降沿输出。翻成通用SPI模式,就是CPOL=0、CPHA=1,也就是ST库里的SPI_MODE_1。很多第一次用HAL的人会沿用驱动其他IC时的MODE_0或MODE_3,结果读回来的数据要么整体错位,要么偶尔出现一个跳变的粗大误差。
我用CubeMX时的Pinout配置大抵如下,注意CS不受SPI硬件管理,必须单独拉GPIO:
| STM32引脚 | 接到ADS124S08 | 说明 |
|---|---|---|
| PB13 | SCLK | SPI时钟,1MHz左右起步 |
| PB14 | DOUT/DRDY | 在ADS124S08上,DOUT和DRDY是同一个引脚复用 |
| PB15 | DIN | SPI主输出从输入 |
| PB12 | CS | 软件控制,低电平有效 |
| PE0 | RESET | 可选,上电复位用 |
| PE1 | START | 可选,硬件START引脚拉低表示连续转换 |
这里最容易混淆的是DOUT和DRDY共用一脚。ADS124S08的数据线是双向的:正常情况下DOUT输出转换数据,而当数据准备好时,这一脚会拉低。也就是说,在SPI通信之前,DRDY拉低就是一个“数据就绪”的硬通知。你如果不接这根线,就得靠延时猜数据有没有准备好,这在高速率下基本不可用。
SPI的速率设置,我建议不要一上来就干到8MHz。ADS124S08虽然支持较高的SCLK,但实际项目里你的走线长度、隔离器件、逻辑电平都会影响时序余量。先把速率压到1MHz,功能验证通过后,再根据示波器看到的建立时间逐级往上提。具体在HAL里的体现见第3章的代码。
2.2 电源、基准电压和模拟输入的处理
ADS124S08同时有模拟电源AVDD和数字电源DVDD,内部虽然有LDO选项,但工程上我习惯给AVDD单独加一颗低噪声LDO,DVDD和主控的3.3V共用。模拟电源上的纹波会直接折算进ADC结果,HAL驱动写得再漂亮也消除不了电源带来的底噪。
基准电压方面,芯片内置了2.5V基准,正常情况下直接配置VREFCON寄存器的相应位就能用内部基准,省掉外部基准源。但如果你有多片ADI/TI的精密数据采集板卡同时在飞的经历,你会习惯外部基准芯片的方案,因为根因排查时少一个“内部基准是否稳定”的疑点。用REF5025之类的外部基准,把REF+接到芯片的REFP,REF-接到REFN,然后屏蔽内部基准。
模拟输入端的RC滤波不能省。我用1kΩ电阻串联加100nF电容到地,构成大约1.6kHz的截至频率,既能抗射频干扰,又不至于拖慢信号建立。注意RC时间常数必须小于驱动源的输出阻抗要求,否则在高增益设置下会把增益误差拉偏。
2.3 上电时序与复位电路
ADS124S08复位后内部寄存器都回到默认值,默认的数据速率是20SPS,PGA增益为1,输入通道为AIN0。上电后要至少等待2ms再开始SPI通信,否则芯片可能还没准备好接收命令。
RESET引脚不能悬空。我见过有人把RESET空着,靠内部上拉,结果在电源掉电再上电的瞬间,芯片进入不确定状态。加一颗10kΩ上拉到VDD,再接一个0.1μF电容到地,做成简单RC复位。如果你用HAL驱动,可以在初始化时把RESET拉低至少10μs,再拉高,之后延时10ms,确保内部振荡器稳定。
3. HAL库读写ADS124S08寄存器:底层原语决定上层稳定
3.1 CubeMX里SPI参数的落法
用CubeMX生成了基础工程后,SPI的参数应该这样填:Mode选择Full-Duplex Master,Data Size选8位,First Bit选MSB First,Prescaler设为32(这取决于你的APB时钟频率,目的是让SCLK落在1MHz附近),Clock Polarity为Low,Clock Phase为1Edge。下面这段是HAL配置代码的核心部分:
SPI_InitTypeDef spi_init = {0}; spi_init.Mode = SPI_MODE_MASTER; spi_init.Direction = SPI_DIRECTION_2LINES; spi_init.DataSize = SPI_DATASIZE_8BIT; spi_init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=1 spi_init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 spi_init.NSS = SPI_NSS_SOFT; // 由GPIO接管CS spi_init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; spi_init.FirstBit = SPI_FIRSTBIT_MSB; spi_init.TIMode = SPI_TIMODE_DISABLE; spi_init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi1);这几个参数里,CLKPhase和CLKPolarity是必须和ADS124S08时序对齐的,错一个就全错。BaudRatePrescaler选多少取决于你的APB总线时钟,比如APB2是72MHz,除以32得到2.25MHz,这在实际使用中已经够稳。不要为了炫技把时钟压到极限,SPI通信本身不占瓶颈,ADC转换时间才是主流程的大头。
3.2 用HAL_SPI_TransmitReceive封装寄存器读写
HAL库的SPI发送和接收是同一个函数:发送一个字节的同时必然收到一个字节。ADS124S08的命令帧结构正好用得上这一点:先发一个命令字节,随后那些“读回的数据”会在主机继续发时钟时从DOUT上冒出来。我封装了一组底层原语,项目中所有寄存器操作都走这两个函数:
static void ADS124S08_CS_Low(void) { HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET); } static void ADS124S08_CS_High(void) { HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET); } // 写寄存器:cmd = 0x40 | 寄存器地址,count = 要写的寄存器个数-1 void ADS124S08_WriteRegister(uint8_t addr, uint8_t count, uint8_t *data) { uint8_t buf = 0; ADS124S08_CS_Low(); buf = 0x40 | addr; HAL_SPI_TransmitReceive(&hspi1, &buf, &buf, 1, 10); buf = count; HAL_SPI_TransmitReceive(&hspi1, &buf, &buf, 1, 10); for (uint8_t i = 0; i <= count; i++) { HAL_SPI_TransmitReceive(&hspi1, &data[i], &buf, 1, 10); } ADS124S08_CS_High(); } // 读寄存器:先发 0x20|addr,再发 0x00,之后连续读 void ADS124S08_ReadRegister(uint8_t addr, uint8_t count, uint8_t *data) { uint8_t buf = 0; ADS124S08_CS_Low(); buf = 0x20 | addr; HAL_SPI_TransmitReceive(&hspi1, &buf, &buf, 1, 10); buf = 0; HAL_SPI_TransmitReceive(&hspi1, &buf, &buf, 1, 10); for (uint8_t i = 0; i <= count; i++) { HAL_SPI_TransmitReceive(&hspi1, &buf, &data[i], 1, 10); } ADS124S08_CS_High(); }说明一下:命令字节中的高两位分别表示WREG(0x40)还是RREG(0x20),低6位是寄存器地址。第二个字节写的是“要操作的寄存器数量减一”,如果你只写一个寄存器,这里就是0x00。循环里发一个哑元字节并同时接收数据,正好利用了SPI全双工特性。超时时间设为10ms,对寄存器访问来说绰绰有余,但如果你把速率压到很低,建议按实际波特率放宽超时。
3.3 让DRDY沿中断来驱动逻辑
数据就绪通知是整个驱动里最关键的一环。我在初始化时把DOUT/DRDY引脚配置为外部中断输入,下降沿触发。这样每次ADC转换完成,DRDY拉低,MCU立即进中断,然后发RDATA命令取数据。
static void ADS124S08_DRDY_Callback(void) { uint8_t cmd = 0x12; // RDATA命令 uint8_t in[3] = {0}; ADS124S08_CS_Low(); HAL_SPI_TransmitReceive(&hspi1, &cmd, &in[0], 1, 10); HAL_SPI_TransmitReceive(&hspi1, &dummy, &in[1], 1, 10); HAL_SPI_TransmitReceive(&hspi1, &dummy, &in[2], 1, 10); ADS124S08_CS_High(); raw_value = (int32_t)((in[0] << 16) | (in[1] << 8) | in[2]); data_ready = 1; }这里RDATA命令发出后,紧接着的两个哑元字节用于产生SCLK,把DOUT上的24位数据移出来。注意:在24位数据全部移出后,DRDY会自动回到高电平。如果你在数据还没移完就去读别的寄存器,数据顺序就会错乱。把data_ready置1后,主循环只需检查这个标志。
4. 配置与换算:把24位原始数据变成有效电压
4.1 写入PGA、输入通道和数据速率
ADS124S08的通道配置由INPMUX寄存器控制,PGA增益由PGA寄存器控制,数据速率由DATARATE寄存器控制。这是一组典型的配置序列:
uint8_t buf[2]; // 选择AIN0与AIN1差分输入:INPMUX[5:3]为AINP,[2:0]为AINN buf[0] = 0x01; // AINP=AIN0,AINN=AIN1,内部基准 ADS124S08_WriteRegister(0x01, 0, buf); // PGA增益 8:寄存器值为 0010 buf[0] = 0x02; // 即 PGA[4:1] = 0010,增益8 ADS124S08_WriteRegister(0x02, 0, buf); // 数据速率 1000 SPS:DATARATE[4:0] 对应值 buf[0] = 0x08; ADS124S08_WriteRegister(0x03, 0, buf);INPMUX寄存器的名字容易误导人:它不是简单地选一个通道,而是分别指定正端和负端。0x01表示正端为AIN0,负端为AIN1,这就是差分测量的标准玩法。如果做单端测量,通常把负端固定接到AVDD/2或者COM脚。
PGA增益的选择要结合信号幅度来看。ADS124S08内部基准是2.5V,满量程差分输入是±VREF/增益。增益8意味着满量程约±312.5mV,如果信号本身有1V,这个配置会直接削顶。我在项目里习惯先做一次增益1的扫描,观察信号大致范围,再回头确定PGA值,避免一上来就陷入“数据全满码”的困惑。
数据速率寄存器与滤波效果直接相关。1000SPS时内部数字滤波器对50Hz工频的抑制能力有限,如果你测的是桥式传感器,建议先把速率降到400SPS以下。具体选多少取决于你想要的响应速度和噪声指标的权衡。
4.2 单次转换的完整读取顺序
单次转换的流程不是发个命令就等结果,而是要驱动芯片从STANDBY回到正常模式,再发START命令,之后轮询DRDY。
void ADS124S08_SingleConvert(void) { uint8_t cmd; // 唤醒芯片 cmd = 0x02; // WAKEUP ADS124S08_SPI_Command(cmd); // 发送START命令,开始转换 cmd = 0x04; // START ADS124S08_SPI_Command(cmd); // 等待DRDY引脚变低 uint32_t timeout = 0; while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) == GPIO_PIN_SET) { if (++timeout > 0xFFFFFF) { break; // 超时保护,防止死等 } } // 此时可取出24位数据 ADS124S08_ReadData(); }这个流程里还有一个容易忽略的点:WAKEUP和START是命令字节,不是寄存器写入。很多例程会把这两步漏掉,直接读数据,结果读到的是上一次转换的旧值。超时保护必须有,因为DRDY如果因为配置错误一直不拉低,主控就会卡死在循环里。我见过在调试时没有超时保护的程序,现场只能断电重启。
4.3 偏移补码换算成电压的公式
ADS124S08输出的是24位二进制补码,范围从0x800000(负满量程)到0x7FFFFF(正满量程)。HAL库读到的是原始字节,需要先拼成int32再做符号扩展:
int32_t adc_code = (int32_t)((raw[0] << 16) | (raw[1] << 8) | raw[2]); // 补码符号扩展 if (adc_code & 0x800000) { adc_code |= 0xFF000000; } float voltage = (float)adc_code / 0x7FFFFF * (2.5f / 8.0f);0x800000是负满量程的边界,24位数据的最高位是符号位。不扩展符号位,你会看到负电压被算成一个巨大的正数,这是所有初次用ADS124S08的人都会撞一次墙的地方。分母用0x7FFFFF而不是0x800000,是因为正满量程的编码就是0x7FFFFF。基准2.5V除以PGA增益8,得到每个LSB对应的模拟电压,再乘上数据编码的比例,就是实际电压。
在双极性输入下,0x7FFFFF对应正的满量程电压,0x800000对应负满量程电压,零点自然是0。如果换算出来的结果零点偏移很大,优先检查两个输入端是否有偏置电流造成的压降,而不是怀疑公式。
5. HAL库驱动ADS124S08避坑手册:五个现场翻车案例
5.1 DRDY毛刺触发误中断,读数全是0x800000
现象:中断触发频率明显高于配置的SPS,读回来的数据集中在负满量程。
原因:DRDY引脚在SPI通信过程中会随着DOUT输出数据而出现电平翻转,如果主控侧的滤波电容漏放,或者外部干扰耦合到DRDY走线,会在不该触发的时候触发外部中断。你在中断里强行发RDATA命令,此时寄存器映射已经错乱,自然读回负满量程。
解决:给DRDY引脚加一个10kΩ串联电阻并在对地并联10nF电容,把毛刺滤掉。同时在中断回调里加状态判断:只有在上一次数据已经被读走、芯片处于等待转换状态时才响应。代码上就是检查data_ready在上一次读取后是否已被清零。
5.2 SPI速度一提高读数就开始漂
现象:SCLK设到4MHz以上,数据偶尔出现低位跳变,且跳变幅值随温度改变。
原因:SPI时钟线对DOUT信号存在串扰,尤其是没有端接电阻的飞线实验板上。ADS124S08对SCLK的建立时间有明确要求,SCLK过快时,芯片内部输出驱动来不及在采样沿之前稳定,数据传输就会逼近时序极限。
解决:把BaudRatePrescaler往大调,降到2MHz以下。4MHz只是芯片支持上限,不是推荐工作点。如果必须用高速率,考虑缩短SPI线缆、加串联端接电阻,并在SCLK引脚上并联一个33pF电容抑制振铃。我在量产板上是用2.25MHz跑的,连续采集一整天没有出过错。
5.3 寄存器写进去后回读是0xFF
现象:用RREG读寄存器,读出来的字节全是0xFF,或者写入后立刻回读是上电默认值。
原因:大概率是CS引脚没被控制好。HAL库的NSS = SPI_NSS_SOFT表示芯片不接管CS,但如果你忘记把CS引脚在通信前后拉低拉高,芯片会以为外部主机还在通信中途,命令字节被当成噪声丢弃。
解决:检查CS的GPIO配置是否为推挽输出、初始电平为高。每次通信前后调用CS低和CS高,并且确保两个动作之间有足够的建立时间,我一般会加一个__NOP()至少两个空操作来保证GPIO写入生效。再用逻辑分析仪抓一下CS低到第一个SCLK上升沿的时间,应该大于芯片要求的tCSSCLK时间。
5.4 单端输入在低电平时正负电压读数不对称
现象:做单端采集时,输入接近零伏,读数却不是0,而是几十个LSB的偏移。
原因:单端模式下,负端被固定到某个参考电平,通常是AVDD/2。如果你的信号源与ADC共地不好,或者印制板布局让模拟地和数字地之间存在压降,这个偏移就会表现出来。这不是芯片的问题,是地平面处理的问题。
解决:确保模拟地和数字地单点连接,尽量靠近ADC的GND引脚。单端输入信号源的地电位,要与ADS124S08的模拟地在同一点取参考,必要时用一根星形地线把所有模拟参考点汇合。不要指望寄存器能消掉这种偏移,它是物理层面的。
5.5 HAL库SPI总线锁死在BUSY状态
现象:程序运行一段时间后,HAL_SPI_TransmitReceive一直返回HAL_BUSY,主循环卡死在超时等待里。
原因:HAL库的SPI外设是状态机管理的。如果在一次SPI通信未完成时又发起新的传输,或者上一次传输在中断中被打断,SPI句柄的State字段会停留在BUSY,不会自动恢复。
解决:在每次调用HAL_SPI_TransmitReceive前增加__HAL_SPI_DISABLE(&hspi1)再重新使能,或者直接调用HAL_SPI_DeInit再HAL_SPI_Init重置状态机。我后来习惯把SPI底层访问加一个互斥标志:只有在上一次传输完成标志置位后才能发起下一次传输,优先级高于DRDY中断,避免中断里和主循环抢SPI。
6. 进阶技巧:DMA + 连续转换实现无掉帧数据流
如果你做的是多通道循环采样,比如周期性地扫描8个单端通道,上面那种“读一个数据就切换通道”的做法效率很低。每一次切换通道都要写INPMUX,且要丢弃切换后的第一次转换结果,因为滤波器还没稳定。进阶做法是让ADS124S08工作在连续转换模式,配合SPI的DMA传输,把数据搬运交给硬件。
先配置DATARATE寄存器确定转换速度,然后发RDATA命令,之后芯片会按固定速率连续输出转换结果,每次DRDY拉低时,SCLK上的下一个24位就是当前数据。为此可以把DRDY接到SPI的NSS?实际中常用方案是:初始化时把SPI的Rx用DMA连接,DRDY触发一次外部中断后,主控只发一个哑元字节,同时DMA接收三个字节的数据。代码骨架大致是这样:
void ADS124S08_DMA_Init(void) { // 配置SPI1_RX的DMA通道 hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_spi1_rx.Init.BufferSize = 3; hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE; HAL_DMA_Init(&hdma_spi1_rx); __HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx); } void ADS124S08_DRDY_Callback(void) { uint8_t tx_dummy = 0x00; HAL_SPI_TransmitReceive_DMA(&hspi1, &tx_dummy, dma_rx_buf, 3); } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { // 从 dma_rx_buf 提取24位数据,交给数据处理环 process_sample(dma_rx_buf); } }DMA的BufferSize固定为3,每次DRDY中断触发一次传输,速度足够快,而且主循环完全不需要等待SPI。要注意的是,DMA传输期间DRDY可能再次拉低,此时新的中断会嵌套进来。为了避免在DMA还没完成时又启动新一轮DMA,在回调里加一个dma_busy标志:忙时直接置data_lost = 1,直到下一轮再覆盖。
把DRDY中断优先级设为略低于SPI DMA完成中断,可以避免“上次数据还没读完,下次转换已完成”的覆盖问题。实际项目中,我一般把SPI DMA中断优先级设在最高,DRDY中断略低一级,这样数据通道永远不会被外部中断打断。从那以后我每次配置高精度ADC,都会强制走一遍“先验证SPI时序余量、再开DMA、最后调中断优先级”的流程,这套顺序帮我省掉了很多半夜调板的痛苦。希望这套HAL库驱动的思路,能帮你少走几步弯路。
本文还有配套的精品资源,点击获取