做高精度模拟量采集的项目第一道坎往往不在算法上而在ADC本身的选型和驱动。我之前手头一个项目要采集05V的传感器信号精度要求1mV以内换算下来至少要14位有效分辨率。STM32F103RCT6内部ADC是12位的满量程3.3V时1个LSB就接近0.8mV加上参考电压噪声、增益误差和温漂实际有效位数做到10位都很吃力所以外挂高精度ADC成了必然选择。我最终选了TI的ADS8866一颗16位、500kSPS采样率的SAR型ADC接口是标准SPI。这个芯片本身不算难驱动但它的三线SPI模式和我们常见的SPI从设备时序不太一样。很多朋友照着普通SPI Flash的写法去读ADS8866结果不是数据全零就是高两位丢位折腾半天发现是时钟极性和相位配错了或者是CS的控制时机不对。这篇文章把我基于STM32F103RCT6驱动ADS8866三线模式的过程、原理、代码和踩坑记录完整写出来希望能帮到正在做高精度采集的朋友。1. 为什么弃用内置ADC选ADS8866项目需求与选型思路1.1 高精度采集到底难在哪先说说这个项目的实际需求。现场传感器输出的是05V模拟信号经过前端调理电路后要进入ADC采集系统要求最终测量误差不超过1mV。STM32F103RCT6内置ADC是12位逐次逼近型理论上3.3V参考电压下分辨率约为0.8mV/LSB但从工程角度看内置ADC的实际有效位数受几个因素影响很大参考电压直接取VDDA时3.3V电源纹波会直接叠加到转换结果上内部比较器的失调电压、电容匹配误差会引入增益误差和零点误差采样保持电容容量有限对应高阻抗信号源时需要很长的采样时间否则结果偏低所以12位ADC全量程才4096个码覆盖03.3V时每码0.8mV仅仅满足理论精度实际误差放大几倍就超了。把信号缩放到03.3V再采集也只能改善前端的信噪比改变不了ADC本身有效位数不足的问题。1.2 ADS8866的关键参数与同类方案对比ADS8866是TI推出的16位SAR ADC采用单端输入输入范围0到VREF最高采样率500kSPSSPI兼容串行接口。它的几个核心参数很贴合这个项目分辨率16位无失码INL典型值为±1LSB供电范围宽AVDD为2.7V到5.5VDVDD支持1.65V到5.5V可以直连STM32的3.3V IO采样率高500kSPS对于5kSPS左右的实际采样需求绰绰有余功耗低3.3V供电下功耗在mW级别长期运行不会带来明显的温漂我当时也看了其他方案这里列一个对比表供参考方案分辨率采样率接口价格区间适用场景STM32内置ADC12位可达1MSPS内部无额外成本对精度要求不高的场合ADS125624位30kSPSSPI中高高精度但速率较低适合低速采集ADS886616位500kSPSSPI中等高速高精度采集接口简单外部独立高精度ADC如Σ-Δ音频ADC24位192kSPSI2S中音频或低频高分辨率采集但输入范围窄ADS8866的优势在于既有16位分辨率又有500kSPS的采样率同时SPI接口非常简洁驱动逻辑相比Σ-Δ型ADC要简单得多。项目中实际只需5kSPS左右它的速率余量很大后续做过采样平均还能进一步提升有效位数。1.3 三线模式带来的硬件收益ADS8866支持多种串行接口模式通过SDI引脚在CS下降沿时刻的电平来选择。我们开发中用到的三线SPI模式3-Wire CS Mode只使用CS、SCLK、SDO三根信号线SDI引脚直接接GND即可。相比四线模式三线模式少了一根数据输入线硬件上能省下MCU的GPIO资源如果做数字隔离隔离通道也能少一路。对F103RCT6这种引脚不算特别富余的芯片来说三线模式接线简单软件上不需要关心SDI的指令写入整体可靠性也更高。2. 三线模式的三根线怎么配合CS、SCLK、SDO的时序逻辑2.1 三线模式不等于普通SPI从机很多人在这一步就已经理解偏了。STM32的SPI外设作为Master时通常会输出SCK、MOSI、MISO三条线CS一般由软件控制或硬件NSS输出。ADS8866三线模式下只有SCLK、CS、SDO没有MOSI方向的数据链路。这意味着我们不需要向ADC写任何配置寄存器只要给时钟它就把转换结果从SDO移出来。这里容易被忽略的是ADS8866表面上看起来是SPI从机但它的SCLK不仅仅用于移位输出还充当了SAR转换的位时钟。换句话说CS拉低后芯片开始对模拟输入进行采样转换转换的每一步都与SCLK同步完成16位数据移出的过程就是SAR逐次逼近的过程。所以不存在先启动转换、等待完成、再读数据这种分离的流程读数据的16个SCLK周期本身就是转换过程。2.2 一次完整转换的时序拆解以一个简单的单次转换流程说明CS由高拉低这个下降沿启动一次新的采样转换SDO在CS下降沿之后输出转换结果的最高位DB15随后每来一个SCLK下降沿SDO依次输出下一位数据连续16个SCLK后16位数据全部移出CS拉高本次转换结束从时序图上可以直观看到SDO在CS下降沿后很快输出MSB后续每一位数据在SCLK下降沿后更新所以MCU应该在SCLK上升沿附近采样SDO此时数据已经稳定。这也是后面配置时钟极性和相位的根据。关键时序参数大致如下以数据手册典型值描述参数含义典型值f(SCLK,max)SCLK最大频率40MHzDVDD3.3Vt(convert)转换时间16个SCLK周期t(MSB)CS下降沿到MSB有效约2030nst(SDO)SCLK下降沿到数据更新约20nst(CSH)CS拉高最小保持时间约几ns至几十ns实际项目中STM32F103RCT6的SPI1挂在APB2总线上最高72MHz分频到18MHz或者9MHz都远低于40MHz上限完全满足要求。但要注意SCLK也不能太慢因为SCLK同时驱动SAR转换太慢会导致采样保持电容泄漏影响精度。通常建议SCLK不低于几百kHz我们在工程里用的是9MHz。2.3 CS的控制时机为什么要自己管ADS8866的数据输出完全由CS控制。如果CS一直拉低不释放SCLK持续给时钟芯片会继续输出后面无效的位看到的现象就是数据后几位乱跳。所以每次读取必须严格控制CS的拉低和拉高时机。STM32F103的SPI外设有硬件NSS功能但硬件NSS的自动控制时序和ADS8866对CS的要求并不完全一致。硬件NSS在SPI传输开始前自动拉低、结束后自动拉高看起来好像也能用但F103的硬件NSS在单Master模式下配置起来有不少限制比如NSS输出使能时引脚会被外设占用且片选时机和SPI总线的帧边界不一定完全符合ADC的需求。稳妥的做法是用任意普通GPIO做软件片选完全手动控制拉低和拉高。我在代码中用的是PA4作为软件片选引脚不去碰SPI1的硬件NSS功能省心也灵活。3. STM32F103RCT6侧硬件连接引脚分配与设计要点3.1 引脚连接与GPIO分配我的硬件连接如下STM32F103RCT6ADS8866说明PA5 (SPI1_SCK)SCLKSPI时钟PA6 (SPI1_MISO)SDOADC数据输出PA4 (普通GPIO)CS软件片选3.3VAVDD、DVDD供电GNDGND共地不接SDI直接接GND选择三线模式3.3V或外部参考REF参考电压输入信号调理输出AINP模拟输入正端GNDAINM模拟输入负端这里有几个设计细节要特别留意。首先AINM虽然是负输入端但在单端输入模式下直接接模拟地不能浮空。其次REF引脚是ADC的转换参考它直接决定满量程电压。如果用3.3V供电并直接把REF接到AVDD那么参考电压会随3.3V电源波动测量结果也会跟着漂。我在这个项目里用的是外部精密参考源输出3.0V参考电压这样满量程就是03.0V配合前端05V调理电路把信号按比例缩放可以充分利用ADC量程。3.2 电源和参考电压的布局ADS8866虽然功耗不高但毕竟是16位精密ADC电源和参考电压的处理不能太随意。我在PCB布局上做了这样的处理AVDD和DVDD分别用10uF钽电容并联100nF陶瓷电容去耦电容尽量靠近芯片电源引脚参考电压输出端加一级RC滤波比如10Ω串联100uF电容滤除参考源的高频噪声ADC芯片周围的地平面不要被数字信号线穿过SCLK这类时钟信号尽量远离AINP走线模拟输入信号如果来自前级运放信号源阻抗要控制在几十欧姆以内否则会因ADC内部采样电容的充电时间不够而引入误差这些细节看起来不起眼但对16位ADC的稳定性影响很大。我在第一版PCB上因为REF去耦离得太远实测数据在低两位上不断跳动后来优化布局后噪声明显下降。3.3 电平匹配与IO方向确认ADS8866的DVDD支持1.65V到5.5V我直接给3.3V和STM32F103RCT6的IO电平完全兼容。需要注意SDO引脚是ADC的输出连接STM32的PA6MISO而SPI1的MOSI引脚PA7在这个三线模式下是不需要用的可以让它保持空闲或做其他功能。SPI外设配置成全双工时MOSI会输出一些数据但ADS8866的SDI已经硬接地所以MOSI上的电平不会影响ADC工作。如果你用的是STM32其他型号引脚分配要留意SPI1和SPI2在不同总线上APB1上的SPI2最高36MHz分频后也够用但SPI1有72MHz总线优势配出更高SCLK更方便。4. 驱动代码实现HAL库下的初始化、读取与滤波4.1 SPI外设初始化要点我在STM32F103RCT6上用的是SPI1HAL库初始化时需要重点确认以下几个配置项ModeMaster模式DataSize8位这里用两字节拼16位数据方便控制SCLK数量CLKPolarityLOWCPOL0CLKPhase2EDGECPHA1NSSSoft软件NSSBaudRatePrescaler8分频得到9MHzFirstBitMSB firstCPOL和CPHA的配置是整个驱动开发里最容易出问题的地方。我之前也用CPOL0、CPHA0试过结果读出来的数据和实际电压对不上后来抓波形才发现问题。为什么最终用CPOL0、CPHA1原理上可以这样理解ADS8866在SCLK下降沿更新SDO上的数据所以MCU在SCLK的第二个边沿采样即下降沿之后的稳定窗口采样更可靠。CPHA1模式下Master在SCLK第二个边沿采样数据配合从机在下降沿更新数据正好满足建立时间要求。当然不同批次的芯片或不同的SCLK频率下CPOL0、CPHA0也可能能工作但从波形分析来看CPHA1的裕量更大也符合多数类似SAR ADC的推荐配置。初始化代码如下#include spi.h SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 10; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }CubeMX生成的代码基础上只需要确认这些参数即可。需要注意的是如果CubeMX配置的是16位数据帧那么一次HAL_SPI_TransmitReceive的Size参数含义会变成16位帧的数量代码写法略有差异。我更推荐8位帧因为两字节传输正好对应16个SCLK逻辑更直观。4.2 软件片选GPIO初始化PA4作为CS引脚在初始化时配置为推挽输出默认输出高电平。注意PA4默认可能是SPI1的NSS功能在CubeMX里如果复用了PA4做SPI NSS要重新配置为GPIO输出。软件管理NSS时F103内部SSI位总是置1硬件NSS引脚释放所以PA4可以安全地作为普通GPIO使用。void ADS8866_CS_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }4.3 单次读取函数最基本的读取函数如下uint16_t ADS8866_ReadSingle(void) { uint8_t tx[2] {0x00, 0x00}; uint8_t rx[2] {0x00, 0x00}; uint16_t raw 0; // CS拉低启动转换 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 发送两字节产生16个SCLK同时接收ADC数据 HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); // CS拉高结束本次转换 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 高位在前拼接16位结果 raw (rx[0] 8) | rx[1]; return raw; }这个函数有几个细节值得说明。第一tx数组发送0x00即可因为MOSI不接任何ADC引脚但SPI全双工模式下Master必须发送数据才能产生SCLK所以发什么无所谓。第二HAL_SPI_TransmitReceive在8位帧模式下Size2表示传输2字节会精确产生16个SCLK不多不少。第三CS拉低后没有加入额外延时因为SCLK启动后芯片内部会把采样转换和移位输出重叠完成。如果信号源阻抗偏高可以在CS拉低后加一个几百纳秒的延时确保采样电容充满电。4.4 连续采样与DMA方式的扩展如果只是偶尔读取一次电压上面的轮询函数完全够用。但项目里需要持续采集并做滤波这时轮询方式会占用CPU大量时间。可以改用SPI DMA接收把从MISO接收两个字节的工作交给DMA在传输完成中断里拉高CS并处理数据。DMA方式的关键点在于CS控制与DMA传输完成的配合。流程是拉低CS启动DMA接收由SPI产生SCLKDMA传输完成中断中拉高CS解析数据这里要注意DMA传输完成中断发生时SPI可能还有最后一个字节正在处理需要稍微延时或检查SPI总线空闲标志确保数据完整。DMA通道选择上SPI1的RX DMA对应DMA1的Channel2如果用的标准库或HAL库映射规则不同需要查对应手册。DMA模式下一次转换的数据读取流程可以简化为volatile uint8_t ads_tx_buf[2] {0x00, 0x00}; volatile uint8_t ads_rx_buf[2] {0x00, 0x00}; volatile uint16_t ads_adc_value 0; volatile uint8_t ads_data_ready 0; void ADS8866_StartDMA(void) { // 确保DMA没有在传输 HAL_SPI_DMAStop(hspi1); ads_data_ready 0; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(hspi1, (uint8_t*)ads_tx_buf, (uint8_t*)ads_rx_buf, 2); } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 传输完成拉高CS HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); ads_adc_value (ads_rx_buf[0] 8) | ads_rx_buf[1]; ads_data_ready 1; } }实际调试中发现SPI DMA传输完成回调里直接操作GPIO拉高CS时序上有一点点紧稳妥起见可以在回调里读一下SPI总线忙状态或者加几个空指令延时。这个细节不同芯片版本可能表现不同我在F103RCT6上实测直接操作GPIO也没问题。4.5 软件滤波提升有效分辨率ADS8866本身16位分辨率已经不错但实际采样中总会有一些随机噪声尤其是参考电压噪声和前端运放噪声。我在驱动里加了一层简单的滑动平均滤波对连续N次采样取平均。如果按实际过采样率64倍来算理论上能额外提升约3位有效分辨率当然这取决于噪声是否是白噪声。具体采样次数和效果我在实测章节会提到。滤波函数很简单#define ADS_FILTER_SIZE 16 uint16_t ADS8866_ReadFiltered(void) { static uint16_t buf[ADS_FILTER_SIZE]; static uint8_t index 0; uint32_t sum 0; uint8_t i 0; buf[index] ADS8866_ReadSingle(); index (index 1) % ADS_FILTER_SIZE; for (i 0; i ADS_FILTER_SIZE; i) { sum buf[i]; } return (uint16_t)(sum / ADS_FILTER_SIZE); }注意这里用了静态数组存历史数据起始阶段数组未填满时平均值会偏低实际工程可以在初始化时先连续采样填满数组或者根据需求决定是否丢弃前几次结果。5. 用逻辑分析仪验证时序实测波形与数据判读5.1 抓取CS、SCLK、SDO波形驱动写完后不能急着接传感器先用逻辑分析仪抓三根信号线的波形确认时序和配置一致。我用的是几百块钱的逻辑分析仪采样率几十MHz足够看9MHz的SPI波形。抓到的波形应该符合以下特征CS平时为高读取瞬间拉低CS拉低后SDO从高阻变成驱动状态输出高位数据SCLK连续输出16个脉冲频率接近配置的9MHz第16个SCLK结束后CS拉高如果在逻辑分析仪上看到CS拉低后SDO一直为高或一直为低没有随SCLK变化的数据说明SPI通信有问题重点检查SDO和MISO的连线是否正确以及SCLK极性和相位是否匹配。5.2 用已知电压校验读码波形正常后把ADC输入端接到几个已知电压点检查转换结果是否在预期范围内。我常用的校准方法是输入接GND理论上十六进制码应为0x0000实测可能有几个LSB的偏移输入接VREF即3.0V理论上码值为0xFFFF实测接近65535输入接VREF的一半1.5V理论码值约32768如果输入端接一个稳定的1.5V基准源读出码值大约是32768左右说明转换正确。计算公式为[ V_{in} \frac{ADC_CODE}{65535} \times V_{REF} ]比如标称1.5V电压如果读码是32768则计算电压为[ \frac{32768}{65535} \times 3.0 1.50002V ]实际测量会有几十微伏的误差这属于正常范围。我曾碰到一种典型故障输入端接GND时读数为0x0000但接1.5V时读数变成0x7FFF而不是预期的0x8000。看波形发现数据错了一位排查后确认是时钟相位问题——CPHA从1改成0后第一位数据被当成无效位跳过整个数据右移了1位。改成CPHA1后恢复正常。5.3 连续采样查看噪声与稳定性单次读取正确后我会在循环里连续采样用调试器或串口把原始码值发出来观察噪声情况。如果采样值在几百个LSB范围内跳动肯定不正常。正常情况下的波动一般在几个到几十个LSB取决于参考电压和PCB布局。我实测使用外部3.0V参考、16次滑动平均后码值波动可以控制在23个LSB以内对应的电压抖动大约是[ \frac{3}{65536} \times 3 0.00014V 0.14mV ]对于1mV精度要求来说有充足裕量。如果不用滤波原始码值波动在10个LSB左右大约0.46mV也基本满足要求但加上滤波更稳。6. 实战中踩过的四个坑从现象到根因完整记录6.1 坑一读出的数据全是0问题出在SDI没有固定电平第一版硬件上我偷懒没把SDI引脚接地让它悬空。结果上电后怎么读都是0x0000用逻辑分析仪看SDO一直是低电平。后来查数据手册发现ADS8866的接口模式是在CS下降沿采样SDI电平来选择的SDI悬空时内部逻辑状态不确定芯片可能进入了其他接口模式导致SDO根本不输出有效数据。把SDI直接接到GND确保CS下降沿时SDI是确定低电平芯片就会固定工作在三线CS模式。这也是三线模式下三根线的由来SDI并不参与通信只是模式选择引脚接死就行。6.2 坑二数据整体偏移一位是时钟相位配置错的表象有一次更换SPI初始化代码后发现输入1.5V时读码变成了0xBFFF左右换算成电压超过2.2V明显不对。用逻辑分析仪对比SDO和SCLK的相位关系后发现SCLK的第一个边沿采样时SDO上的MSB还没稳定导致最高位被漏读数据整体左移了一位。这个问题的本质是SPI时钟极性和相位的匹配。ADS8866在SCLK下降沿更新数据所以MCU应该在数据稳定后的窗口采样。按之前的分析CPOL0、CPHA1能保证采样发生在下降沿之后也就是数据已更新的稳定时刻。我把CPHA改回1后数据恢复正常。6.3 坑三CS拉低后立即读取数据高位是上一帧的残留在DMA版本调试时我一开始在CS拉低后立刻启动DMA接收没有等MCU和ADC之间的时序对齐结果发现高速连续采样时偶发出现某个值突然跳到接近满量程的情况。分析后怀疑是CS拉低瞬间ADC还没有完全释放SDO总线而SPI已经开始采样导致第一位数据不确定。解决方法是CS拉低后加一个极短延时比如几个空循环让ADC的SDO在CS下降沿后稳定输出MSB再启动DMA接收。实测增加约100ns延时后偶发跳变消失。这个延时在9MHz SCLK下占一个时钟周期都不到对采样率影响微乎其微。6.4 坑四参考电压直接接AVDD导致采样值随电源波动早期调试为了省事把ADC的REF引脚直接接到了3.3V AVDD上结果连续采样时读数波动非常大。原因很简单DVDD和AVDD来源于同一个LDO而STM32运行时电流变化会在LDO输出上产生毫伏级的纹波这个纹波直接成了ADC的参考电压导致满量程电压跟着波动测量值自然不稳定。后来在REF引脚上加独立的3.0V精密参考源并在REF引脚前加RC滤波后噪声水平显著下降码值波动从几十个LSB降到十几个LSB再配合滑动平均滤波波动控制在个位数LSB。如果只是要求能用直接接AVDD也能跑但要做精测参考源省不得。6.5 关于三线模式的一个额外提醒最后提醒一点ADS8866三线模式占用的SCLK数量必须精确控制。如果SPI传输的不是16位而是更多时钟比如用16位帧但Size参数写错、或者SPI没配置好导致多发了几个时钟SDO在16位后会继续输出低位数据或进入不确定状态最终拼接出的结果就会错乱。确保一次读取恰好16个SCLK是驱动稳定工作的前提之一。我在DMA版本中吃过这个亏当时把HAL_SPI_TransmitReceive的Size参数写成了2但SPI数据帧配置的是16位导致一次传输发出了32个时钟读取结果彻底乱套。后来统一用8位帧模式Size2表示两字节逻辑清晰后就没再出过这类问题。