STM32F4与ADS8860高速ADC数据采集:SPI+DMA+定时器连续采样实战 📅 发布时间:2026/8/30 9:23:12 👁 浏览次数: 简介本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F4平台高精度数据采集实践方案聚焦TI ADS8860模数转换器与STM32的SPI接口协同开发解决多通道模拟信号同步采集、实时处理与工业级可靠性保障等典型工程问题。压缩包共32个文件含8个C源文件如ads8860.c、spi.c、main.c实现驱动与应用逻辑8个头文件ads8860.h、spi.h等定义接口与配置1个.ioc工程配置文件CubeMX生成、1个.uvprojx Keil工程文件及调试配置、启动文件与HAL底层支持代码整体仅70KB结构精炼、开箱即用。已有70人学习下载资源完整包含硬件连接说明、SPI时序适配细节、DMA双缓冲机制、16位ADC标度变换及三种采样模式单次/连续/事件触发的软件实现所有代码均基于HAL库分层设计便于理解外设配置逻辑、调试通信异常及拓展CAN/以太网协议栈。 先聊个项目背景。ADS8860是TI出的16位、1MSPS逐次逼近型ADC接口是SPI兼容协议但和普通SPI从设备比起来有几个特点没有MOSI纯只读采样触发靠独立的CONVST引脚数据是16位一帧MSB在前。STM32F4集成SPI外设最高SCLK能到42MHz配合DMA和定时器完全可以把这个ADC喂饱。这套组合做出来的数据采集系统既能跑单次读取做低速高精度测量也能用定时器DMA打满1MSPS连续采样覆盖工业现场、医疗设备、电机控制里绝大多数模拟量采集需求。这篇内容适合正在做信号采集、传感器调理、或者想搞明白“SPI外设到底怎么和高速ADC配合”的嵌入式工程师。我尽量把数据手册里不会明说的坑、实测中容易翻车的地方都写出来内容和流程都是我实际调通的方案。1. 为什么选ADS8860搭配STM32F41.1 这颗ADC的定位和选型理由先讲选型逻辑。ADC选型无非看分辨率、采样率、输入类型、接口、参考电压这几个维度。ADS8860是16位、1MSPS、单端输入、SPI接口VREF范围支持2.5V到5V供电3.3V也能跑。相比同系列的ADS8861双端差分输入、ADS8363双通道同步采样它更适合大多数单端电压采集场景而且封装小、外围器件少。还有个容易被忽略的点ADS8860的模拟输入可以配置成单极性0~VREF也能通过引脚配置成双极性±VREF。这个灵活性在实际项目中很实用比如采集±10V的工业信号前端做好电平搬移ADC这边直接进双极性模式省掉一级运放。为什么选STM32F4而不是用C8T6或者F1因为ADS8860是1MSPS采样率16位数据数据量为2MB/s。虽然F103的SPI也能跑18MHz但F4的SPI时钟上限42MHz、有完整的DMA控制器、主频168MHz处理数据和跑算法都更从容。而且F4系列的定时器资源丰富可以产生精准的CONVST脉冲这是连续采样方案的基础。1.2 SPI接口在数据采集中的角色ADS8860的SPI接口沿用了TI经典的三线模式SCLK、SDO、CS没有MOSI因为ADC不需要接收配置数据。它的数据输出时序参考ADS886x系列手册SCLK空闲为低SDO在SCLK下降沿更新数据主机在SCLK上升沿锁存对应标准SPI模式0CPOL0CPHA0。这里有个容易踩的坑——有的人用模式1去读读出来的数据位错乱还以为是硬件问题。STM32F4的SPI外设支持8位和16位数据帧ADS8860一帧正好16位所以直接把SPI数据宽度配置成16位一次接收搞定不需要手动拼两个字节。硬件上只需要6根线SCLK、SDO、CS、CONVST、VIOIO供电、GND这就是网上常说的“6针SPI”的由来。2. 硬件连接与信号完整性处理2.1 SPI引脚连接与电源设计先给一个实测可用的连接表基于STM32F407VET6和ADS8860ADS8860引脚STM32F4引脚说明SCLKPB13SPI2_SCK串行时钟建议串联22Ω~33Ω电阻SDOPB14SPI2_MISO数据输出ADC的SDO接主机的MISOCSPB12普通GPIO软件片选控制低有效CONVSTPA1TIM2_CH2定时器PWM输出上升沿触发转换VIO3.3VIO供电决定SDO高电平幅值VDD3.3VADC模拟电源需加去耦电容VREF2.5V或3.3V参考电压建议独立基准源GNDGND单点接地电源这里值得多说一句。VREF如果直接接3.3V电源电源纹波会直接耦合进测量结果。1mV的参考电压抖动在16位ADC、3.3V基准下约相当于21个LSB的误差。实际项目中我推荐用REF5050或者ADR4525这类精密基准哪怕用TL431也比直接怼电源强。2.2 时钟线串联电阻和地平面处理SPI时钟线的串阻问题经常有人问串多大合适。我实测下来SCLK串联22Ω~33Ω效果最好。STM32F4的IO翻转速率很快上升沿非常陡如果PCB走线超过3cmSCLK线的振铃可能超过1V导致ADS8860误采样。串一个33Ω电阻能把振铃压到300mV以内代价是SCLK最高速率略降但对1MSPS采样来说完全够用。另外一个常规但重要的经验模拟地和数字地要在ADC下方单点连接SDO、SCLK这些数字信号不要穿过模拟输入区域。这个项目如果用洞洞板飞线尽量让模拟输入走短粗线数字信号走细线避免数字噪声感应进模拟输入端。2.3 CONVST触发信号设计ADS8860的转换由CONVST上升沿触发转换过程中SCLK接口可以保持空闲。触发信号有两种实现方式主循环/中断里GPIO翻转适合低速采样频率不高的时候完全够用定时器PWM输出适合连续采样频率可精确控制我实测下来如果目标采样率超过10kSPS主循环GPIO翻转的方式就已经不太靠谱了抖动会直接影响采样间隔的均匀性。低速测量可能无所谓但做FFT、谐波分析这类频域处理采样抖动就是个隐性问题。定时器PWM的抖动可以控制在几十纳秒级别这才是连续采样的正确打开方式。3. STM32F4 SPI配置细节和代码实现3.1 CubeMX配置要点用STM32CubeMX初始化工程SPI2配置为Full-Duplex Master虽然用不到发送但HAL库的只接收模式单独配置比较绕用全双工模式、发送哑数据的方法更稳妥数据大小16Bits时钟极性Low时钟相位1 Edge对应CPOL0CPHA0即SPI模式0NSSSoftware预分频2分频84MHz/242MHzF407的SPI2挂在APB1上APB1时钟84MHz先发送MSB这里特别说一下只接收模式的坑。STM32F4的SPI如果配置成“Receive Only”模式BIDIMODE0RXONLY1主模式也能产生SCLK时钟。但HAL库的HAL_SPI_Receive函数在这种模式下需要额外配置而且很多人不知道SPI_CR1寄存器里的RXONLY位必须在SPE1之后才能设置。所以第一次调的时候直接用默认全双工模式每次接收前往SPI_DR写0xFFFF产生时钟反而最省心。等整个链路跑通了再优化成只接收模式也不迟。3.2 单次转换读取的完整代码这是最基础的单次采样流程逻辑是拉低CS → 写哑数据产生SCLK → 接收16位数据 → 拉高CS。// 具体引脚定义根据实际接线调整 #define ADC_CS_PIN GPIO_PIN_12 #define ADC_CS_PORT GPIOB #define ADC_CONVST_PIN GPIO_PIN_1 #define ADC_CONVST_PORT GPIOA // CONVST上升沿触发转换 void ADS8860_StartConvert(void) { HAL_GPIO_WritePin(ADC_CONVST_PORT, ADC_CONVST_PIN, GPIO_PIN_RESET); // 拉低时间保持手册要求至少几十纳秒 for (volatile int i 0; i 10; i); HAL_GPIO_WritePin(ADC_CONVST_PORT, ADC_CONVST_PIN, GPIO_PIN_SET); // 触发后等待转换完成至少等tCONV时间 for (volatile int i 0; i 50; i); } uint16_t ADS8860_ReadSample(void) { uint16_t raw 0; uint16_t dummy 0xFFFF; // 拉低CS启动SPI传输 HAL_GPIO_WritePin(ADC_CS_PORT, ADC_CS_PIN, GPIO_PIN_RESET); // 全双工方式读取发送哑数据产生SCLK HAL_SPI_TransmitReceive(hspi2, (uint8_t*)dummy, (uint8_t*)raw, 1, 10); // 拉高CS结束本次读取 HAL_GPIO_WritePin(ADC_CS_PORT, ADC_CS_PIN, GPIO_PIN_SET); return raw; } // 单极性模式换算电压 float ADS8860_ToVoltage(uint16_t raw, float vref) { return (float)raw * vref / 65535.0f; }注意几个细节。HAL_SPI_TransmitReceive的第三个参数是数据长度配置成16位数据帧后长度1就代表传输1个16位数据。CS的拉低时序要在SPI传输之前完成而且要保证CS拉低到第一个SCLK上升沿之间有足够的建立时间。ADS8860手册里对CS到SCLK的建立时间有明确要求我实际用下来GPIO直接操作的速度足够不需要额外延时但保险起见加几个空指令也不亏。3.3 转换结果的数据格式和换算ADS8860默认单极性模式输出是16位二进制补码之外的普通二进制码这里其实有点讲究我第一次用的时候也被坑过。手册里写得清楚单极性配置下输出编码是直接二进制Straight Binary0x0000对应0V0xFFFF对应VREF。双极性配置下输出是二进制补码Twos Complement0x0000对应-VREF0x8000对应0V0xFFFF对应VREF - 1LSB。很多人在双极性模式下直接用“除以65535再乘VREF”的公式结果负电压全算错了。双极性模式的计算公式应该是// 双极性模式换算电压 float ADS8860_BipolarToVoltage(uint16_t raw, float vref) { int16_t signed_raw (int16_t)raw; return (float)signed_raw * vref / 32768.0f; }把uint16_t强转成int16_t编译器自动完成补码到原码的转换再除以32768乘VREF正负电压就全对了。这个细节强烈建议写进注释里防止后面维护的人踩同样的坑。4. 定时器触发DMA实现连续采样4.1 定时器频率和采样率设计连续采样要解决两件事一是等间隔触发CONVST二是每次转换完成后快速把数据读回来中间不能丢点。我用的方案是TIM2产生PWM驱动CONVSTSPIDMA把数据搬到内存缓冲区。先算一笔账。假设目标采样率500kSPS采样周期2us。ADS8860转换时间典型值约0.5us具体看手册不同批次有差异那么留给SPI读取的时间就是1.5us。SCLK用42MHz的话16位数据需要16/42M ≈ 381ns加上CS建立和释放时间总共不到0.5us时间完全够。但如果采样率提满到1MSPS周期1us转换0.5us后只剩0.5usSCLK还是42MHzSPI读取要381ns加上CS时序余量就很小了。TIM2配置为PWM模式ARR寄存器和CCR寄存器的值决定频率和占空比。比如PCLK184MHz要输出500kHzARR设168-1167CCR设84-183占空比50%注意ARR1才是分频系数这个公式很多人第一次配容易算错。4.2 连续采样代码框架下面给一个完整可用的定时器DMA连续采样核心代码// 采样缓冲区DMA直接写入 #define ADC_BUFFER_SIZE 1024 uint16_t adc_buffer[ADC_BUFFER_SIZE]; volatile uint8_t adc_buffer_done 0; // DMA传输完成回调 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI2) { adc_buffer_done 1; // 在这里处理数据滤波、FFT、存储、发送 } } // 启动连续采样 void ADS8860_StartContinuousSampling(void) { // 1. 启动TIM2 PWM产生CONVST脉冲 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); // 2. CS拉低后续DM A传输期间保持低电平 HAL_GPIO_WritePin(ADC_CS_PORT, ADC_CS_PIN, GPIO_PIN_RESET); // 3. SPI接收DMA传输1024个16位数据 // 注意这里必须用SPI_TransmitReceive_DMA发射哑数据产生SCLK HAL_SPI_TransmitReceive_DMA(hspi2, (uint8_t*)dummy_tx, (uint8_t*)adc_buffer, ADC_BUFFER_SIZE); } // 停止连续采样 void ADS8860_StopContinuousSampling(void) { HAL_TIM_PWM_Stop(htim2, TIM_CHANNEL_2); HAL_SPI_DMAStop(hspi2); HAL_GPIO_WritePin(ADC_CS_PORT, ADC_CS_PIN, GPIO_PIN_SET); }dummy_tx是一个长度等于ADC_BUFFER_SIZE的数组全部填充0xFFFF用于在发送模式下产生SCLK时钟。当然也可以把SPI配置成只接收模式就不用维护这个数组了但第一次调通之前我建议还是用全双工哑数据的方式少踩一个坑是一个。4.3 DMA中断和缓冲区管理DMA配置要点外设地址是SPI2-DR内存地址是adc_buffer数组首地址数据宽度两边都是HalfWord16位内存地址递增外设地址固定传输方向PeripheralToMemory模式Normal不是Circular。为什么不用Circular模式循环模式的好处是不用重新启动DMA但问题是配满1024个点后会从头覆盖如果主循环来不及处理数据会被静默覆盖掉。Normal模式配合DMA传输完成中断每采满一轮就做一次处理逻辑更清晰。等以后要无间隙连续采集再升级成双缓冲切换DMA半传输中断处理前半段全传输中断处理后半段这是后话。实际测下来这套方案在500kSPS下可以稳定运行CPU占用极低因为数据搬运全部由DMA完成主循环只负责在中断里处理数据。如果不用DMA、直接在SPI接收中断里读数据500kSPS下CPU占用率会在30%以上而且很容易因为中断响应不及时丢数据。5. 常见问题排查与实测避坑记录5.1 ADS8860读出来全是0或者固定值这是最常遇到的问题。先看CS是否正常拉低再看SCLK有没有波形然后用示波器量SDO是否在变化。如果SDO一直是低电平检查CONVST有没有触发——很多人忘了给CONVST脉冲直接去读数据ADC从来没启动转换SDO自然没有有效数据。另一个可能SPI模式不对。CONVST触发后CS拉低ADS8860会在第一个SCLK下降沿把转换结果最高位放到SDO上主机在上升沿采样。这个时序对应模式0。如果误配成模式3CPOL1CPHA1虽然也能读完16位但数据整体错位读出来的值和实际电压完全对不上。5.2 低字节和高字节顺序颠倒STM32F4的SPI配置成16位数据帧后如果设置的字节序是LSB first那么SPI_DR里的数据会和实际发送顺序相反。ADS8860输出是MSB first所以必须把SPI配置成MSB first。另外注意如果用8位数据帧模式去读要先读高字节再读低字节然后拼成uint16_t。很多人直接两个字节拼反了结果数据变成实际值乘以256的效果一眼就能看出来不对。5.3 SPI时钟线串电阻到底串多少前面说了22Ω~33Ω这里补充一个实测对比。我做过一组对比实验不串电阻SCLK线长5cm时上升沿过冲约1.2V偶发误码串10Ω电阻过冲降到600mV基本稳定串33Ω电阻过冲200mV以下波形干净但SCLK上升沿变缓实测最高SCLK速率从42MHz降到约30MHz。对1MSPS采样来说30MHz的SCLK足够所以串33Ω是性价比最高的选择。如果PCB走线很短、板层规划合理不串电阻也能正常工作但作为量产设计串电阻的可靠余量更高。5.4 DMA接收和定时器触发的时序配合连续采样时如果TIM2 PWM频率和SPI DMA传输速率匹配不好会出现数据错位的现象。原因是CS一直拉低ADC在每一个CONVST上升沿转换转换完成后数据放在移位寄存器里SPI始终在输出时钟就会连续读出数据。如果SPI的SCLK和CONVST频率没有对齐可能上一次转换的数据还没读完下一次转换就来了读到的帧会整体偏移一位。解决办法是让SPI的传输时间远小于采样周期留足空闲时间。比如500kSPS周期2usSPI传输16位需要不到0.5us还有1.5us的空闲这样即使轻微抖动也不会影响帧对齐。但如果跑满1MSPS建议降低SCLK到20MHz传输800ns或者用两个DMA缓冲区做帧对齐校验读出来的数据如果连续两个点跳变超过合理范围就说明发生了帧错位。5.5 关于参考电压的噪声问题这是最容易忽视、也最难排查的坑。如果测量结果跳动幅度超过10个LSB先别怀疑ADC本身用示波器AC耦合看VREF脚上的纹波。常规开关电源的纹波通常在10mV~50mV对16位ADC来说就是200~1000个LSB的噪声根本不是软件能滤掉的。解决思路VREF引脚接一个10uF钽电容100nF陶瓷电容电容紧贴引脚如果还不行上独立基准芯片。我实测用REF5050做基准配合RC滤波ADC噪声能压到3个LSB以内直接用开发板3.3V供电噪声在8~12个LSB之间波动差距很明显。6. 从单次采样到完整采集系统的扩展思路6.1 双缓冲DMA实现真正无间隙采样单次DMA传输方案在每轮缓冲区满之后会有一段处理间隙如果这段间隙内来了新的CONVST脉冲SPI仍在跑但DMA已经停止数据就丢了。要解决这个问题用DMA的双缓冲模式也叫乒乓缓冲DMA半传输完成中断触发CPU处理缓冲区前半段DMA全传输完成中断触发CPU处理缓冲区后半段这样DMA始终在跑CPU和DMA交替访问不同的缓冲区实现了真正意义上的无间隙连续采样。STM32F4的DMA2支持双缓冲功能CubeMX里直接把DMA模式配置成Double Buffer即可内存地址设置成两个数组的地址然后启用对应的DMA中断回调。6.2 数据滤波和FFT处理16位ADC的原始数据直接拿去用通常会带一些高频噪声。我的习惯是先做一个简单的滑动平均滤波窗口大小根据采样率调整然后再做FFT或者PLC上报。500kSPS下1024点FFT只需要约2msSTM32F4跑起来很轻松可以在DMA中断里直接算完输出频谱数据。如果后面要接Modbus或者串口上报建议把FFT结果和原始ADC值分组打包加上帧头、帧尾、CRC校验这样上位机解析起来会很顺手。6.3 FreeRTOS和任务划分项目升级到FreeRTOS后我的任务划分是这样的DMA中断只负责置标志位和唤醒任务采集任务等待标志位对缓冲区做滤波和FFT通信任务Modbus从站把转换结果发送给上位机注意一个细节HAL库的SPI DMA回调是在中断上下文里执行的不能在回调里直接调用HAL_Delay或者vTaskDelay这类阻塞函数。正确做法是回调里只置标志位或者用osSemaphoreRelease释放信号量唤醒采集任务处理数据。这个习惯能避免很多诡异的死机和优先级翻转问题。7. 实测数据和最终效果用这个系统实际测下来500kSPS连续采样SPI时钟42MHzDMA双缓冲FreeRTOS跑Modbus上报整体运行非常稳定。ADC噪声水平在3~5个LSB以内使用独立基准源采样间隔抖动小于100ns连续运行24小时丢包率为0。如果项目只要求100kSPS左右的采样率配置还可以更简单SPI轮询读取就够了不需要DMA定时器PWM频率f100kHz主循环里每次检测到转换完成就读取数据完全不浪费资源。驱动ADS8860的过程本质上就是理解SPI时序、管理好片选信号、处理好DMA和定时器配合。这套思路摸透了换成AD7616、AD2S1210这些芯片原理都是相通的只是数据帧长度、触发方式略有差异。我后来做AD2S1210的时候就是直接沿用这套SPIDMA定时器的框架改改寄存器配置就调通了省了不少时间。本文还有配套的精品资源点击获取