☰
AD7606与STM32 SPI DMA数据采集方案详解
2026/9/28 6:09:36 网站建设 项目流程

1. 项目缘起与整体方案拆解

1.1 为什么选AD7606搭配STM32做数据采集

做过电力监测、工业振动分析或者多路传感器阵列的朋友,大概率都绕不开一个需求:同时采集多路模拟信号,而且对精度和同步性有硬要求。市面上常见的MCU内置ADC,12位分辨率、通道间轮询采样,遇到需要8通道同步、16位精度的场景就力不从心了。AD7606这颗芯片就是冲着这个痛点来的——8通道同步采样、16位逐次逼近型ADC、内置模拟输入钳位保护、二阶抗混叠滤波器,单颗芯片就能搞定一台小型数据采集设备的前端。

我手头这个项目是做一台便携式多路信号记录仪,需要同时监测8路0-5V的传感器输出,采样率要求不低于10kSPS每通道,数据通过串口实时上传到上位机做波形显示。选型阶段对比过几种方案:外挂多颗单通道ADC成本高且同步困难,用MCU内置ADC加多路模拟开关切换则牺牲了同步性。AD7606的并行接口读取速度快,但会占用大量IO口,在STM32F103这类资源有限的芯片上不太划算。最终敲定用SPI接口读取,虽然速度比并行慢一些,但引脚占用少,配合DMA可以做到不占用CPU资源,整体性价比最高。

这里要说明一点,AD7606原生支持并行、串行和字节三种接口模式,SPI属于串行模式的一种。通过硬件配置PAR/SER/BYTE SEL引脚选择串行模式后,再用SPI时序去读转换结果就行。很多人第一次接触会疑惑:AD7606的SPI和标准SPI协议是不是完全一样?答案是基本兼容,但有几个细节需要特别注意,后面会详细展开。

1.2 整体架构与数据流设计

整个系统的数据流是这样的:AD7606在CONVST引脚上升沿触发8通道同步采样,转换完成后BUSY引脚拉低表示数据就绪。STM32通过SPI接口依次读取8个通道的16位转换结果,每次读取需要两个字节。为了不阻塞主循环,我用DMA把SPI接收数据直接搬运到内存缓冲区,转换完成中断里只做标志位处理和缓冲区切换。

这个架构的核心考量是实时性与CPU占用率的平衡。如果不用DMA,每读一个通道就要进一次中断或者轮询等待,8个通道下来CPU基本被占满,根本没法做其他事情。用DMA之后,SPI的时钟和数据搬运完全由硬件完成,CPU只需要在DMA传输完成中断里处理数据即可。实测下来,10kSPS采样率下CPU占用率不到5%,留给串口上传和数据处理的空间非常充裕。

STM32CubeMX在这个项目里扮演的是“硬件抽象层生成器”的角色。以前配SPI和DMA要翻参考手册一个个算寄存器值,现在图形化点选就能生成初始化代码,省下来的时间可以专注在业务逻辑上。不过CubeMX生成的代码只是起点,AD7606的时序控制、数据解析、校准逻辑还是得自己写。下面我就按实际开发顺序,把每个环节拆开来讲。

2. 硬件连接与CubeMX关键配置

2.1 引脚分配与硬件设计要点

先看硬件连接。AD7606和STM32F103C8T6的连线如下表所示:

AD7606引脚STM32引脚功能说明
CONVST A/BPA8转换启动,上升沿触发
BUSYPA9转换状态指示,高电平表示正在转换
RESETPA10复位,高电平有效
CSPA4SPI片选,低电平有效
SCLKPA5SPI时钟
MISOPA6数据输出(AD7606到STM32)
MOSIPA7数据输入(STM32到AD7606,本项目中未使用)
FRSTDATAPB0第一通道数据指示,可选
OS0/OS1/OS2GND过采样设置,全接地表示无过采样
RANGEGND量程选择,接地为±5V,接VCC为±10V
PAR/SER/BYTE SELGND接口模式选择,接地为并行,接VCC为串行

这里有个容易踩坑的地方:PAR/SER/BYTE SEL引脚必须接高电平才能进入串行模式。我第一次调试时忘了这个引脚,结果SPI读出来全是0xFF,排查了半天才发现是接口模式没配对。另外RANGE引脚决定输入量程,接GND时是±5V,接VCC时是±10V,要根据传感器输出范围提前确定。

注意:AD7606的模拟输入范围是±5V或±10V,如果传感器输出是0-3.3V单极性信号,需要加一个偏置电路把信号抬到以0V为中心的范围,否则负半周会被钳位。

电源部分也不能马虎。AD7606的AVCC建议用5V供电,DVCC用3.3V与STM32电平匹配。模拟电源和数字电源之间要用磁珠或者0欧电阻隔离,模拟地和数字地单点连接。我在第一版PCB上忽略了这一点,结果采集到的数据噪声很大,后来在AVCC引脚旁边加了10uF钽电容和0.1uF陶瓷电容去耦,噪声立刻降下来了。

2.2 CubeMX中SPI与DMA的配置细节

打开STM32CubeMX,新建工程选择STM32F103C8T6。先配置时钟树,外部晶振8MHz,PLL倍频到72MHz,APB2总线72MHz,APB1总线36MHz。SPI1挂在APB2上,时钟源就是72MHz。

SPI1的配置参数如下:

  • Mode:Full-Duplex Master(虽然只用MISO,但全双工模式配置最简单)
  • Data Size:8 Bits(AD7606每次传输16位,分两个字节读)
  • Clock Polarity (CPOL):High
  • Clock Phase (CPHA):2 Edge
  • NSS:Software(用普通GPIO控制片选,方便灵活控制时序)
  • Baud Rate Prescaler:8(SPI时钟=72MHz/8=9MHz)
  • First Bit:MSB First

这里重点说CPOL和CPHA的选择。AD7606的SPI时序要求是时钟空闲为高电平,数据在第二个边沿采样,对应CPOL=High、CPHA=2Edge。这个配置如果搞反了,读出来的数据会整体移位或者完全错误。我建议先用示波器抓一下SCLK和MISO的波形,确认时序匹配后再继续调试。

波特率预分频选8,SPI时钟9MHz。AD7606的SCLK最高支持23MHz(读取模式),9MHz留了足够余量,同时避免高速下信号完整性变差。如果你用杜邦线连接,建议再降一档到4.5MHz,长线传输时更稳定。

DMA配置在SPI1的DMA Settings标签页里添加:

  • DMA Request:SPI1_RX
  • Channel:DMA1 Channel2
  • Direction:Peripheral To Memory
  • Priority:High
  • Mode:Normal(每次转换触发一次DMA传输,传完8个通道后重新配置)

提示:AD7606一次转换产生8个通道数据,每个通道16位即2字节,总共16字节。DMA缓冲区大小设为16,传输宽度设为Byte,这样一次DMA请求就能把8通道数据全部搬完。

GPIO配置方面,PA8配置为GPIO_Output(CONVST),PA9配置为GPIO_Input(BUSY),PA10配置为GPIO_Output(RESET),PA4配置为GPIO_Output(CS)。初始电平:CONVST低,RESET低,CS高。

生成代码时选择MDK-ARM或者STM32CubeIDE,根据你的开发环境来。我习惯用VSCode加PlatformIO,但CubeMX生成的代码结构是通用的,移植起来不麻烦。

3. AD7606驱动实现与SPI时序控制

3.1 复位与初始化流程

AD7606上电后必须执行一次复位,否则内部寄存器状态不确定。复位时序很简单:RESET引脚拉高至少50ns,然后拉低,等待至少25ns后就可以开始转换了。实际代码里我延时1ms,确保稳定。

void AD7606_Reset(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_RESET); HAL_Delay(1); }

复位之后建议做一次“空转”转换,即触发一次CONVST但不读取数据,让芯片内部电路进入稳定状态。这个技巧是我从AD7606数据手册的“上电序列”章节里悟出来的,实测能减少首次采样的异常值。

初始化还包括过采样设置。OS0/OS1/OS2三个引脚全接地表示无过采样,此时采样率最高。如果需要降低噪声,可以设置过采样倍数,但会牺牲吞吐率。我的项目对速度要求高,所以选择无过采样,靠硬件滤波和软件平均来改善信噪比。

3.2 转换触发与BUSY等待策略

AD7606的转换启动靠CONVST引脚上升沿。给一个至少25ns的高脉冲,芯片就开始转换。转换期间BUSY引脚变高,转换完成后变低。8个通道同步转换,总转换时间约4us(无过采样时)。

等待BUSY变低有两种策略:轮询和中断。轮询简单但浪费CPU,中断效率高但需要额外引脚。我一开始用轮询,后来改成用BUSY下降沿触发外部中断,在中断里启动DMA读取。不过STM32F103的EXTI不支持下降沿触发DMA,所以实际做法是在中断里手动启动SPI DMA传输。

void AD7606_StartConversion(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); __NOP(); __NOP(); __NOP(); // 约30ns延时 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); } void EXTI9_5_IRQHandler(void) // BUSY接PA9 { if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_9) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_9); AD7606_ReadData_DMA(); } }

这里有个细节:BUSY下降沿触发中断后,要尽快启动SPI读取,因为AD7606在下次CONVST之前数据有效,但如果你用很高的采样率,读取窗口会很窄。10kSPS时周期100us,转换4us,读取16字节在9MHz SPI下约14us,时间余量充足。

3.3 SPI读取时序与数据拼接

AD7606的SPI读取时序和标准SPI略有不同。CS拉低后,第一个SCLK下降沿输出DB15(最高位),之后每个下降沿依次输出下一位。8个通道的数据连续输出,每个通道16位。所以总共需要128个SCLK周期。

用STM32的SPI读取时,发送16个字节的哑数据(0x00),同时接收16个字节。接收到的数据按大端格式拼接:

void AD7606_ParseData(uint8_t *buf, int16_t *channels) { for(int i = 0; i < 8; i++) { channels[i] = (int16_t)((buf[i*2] << 8) | buf[i*2 + 1]); } }

注意AD7606输出的是二进制补码格式,正负量程对应-32768到+32767。如果RANGE接GND(±5V),实际电压值 = channels[i] / 32768.0 * 5.0。这个换算关系要记牢,不然算出来的电压全是错的。

实操心得:SPI读取时CS必须在整个128个SCLK周期内保持低电平。如果CS中途拉高,AD7606会复位输出移位寄存器,后续数据全乱。我建议用DMA传输完成中断来拉高CS,确保时序准确。

4. DMA传输与数据缓冲设计

4.1 DMA配置与双缓冲机制

前面在CubeMX里已经配好了SPI1_RX的DMA,现在要在代码里启动它。每次BUSY下降沿触发后,调用以下函数:

uint8_t spi_rx_buf[16]; int16_t adc_channels[8]; void AD7606_ReadData_DMA(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // CS低 HAL_SPI_Receive_DMA(&hspi1, spi_rx_buf, 16); } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS高 AD7606_ParseData(spi_rx_buf, adc_channels); // 数据处理或存入环形缓冲区 } }

这个方案在10kSPS下工作稳定,但如果你要跑到50kSPS以上,单缓冲可能来不及处理。这时候可以上双缓冲:定义两个16字节缓冲区,DMA传输完成中断里切换缓冲区,主循环处理另一个。STM32F103的DMA不支持自动双缓冲,需要手动切换。

uint8_t spi_rx_buf[2][16]; volatile uint8_t active_buf = 0; void AD7606_ReadData_DMA(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Receive_DMA(&hspi1, spi_rx_buf[active_buf], 16); } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); uint8_t ready_buf = active_buf; active_buf ^= 1; // 处理spi_rx_buf[ready_buf] }

双缓冲的关键是处理速度要快于采样速度。如果主循环处理一帧数据的时间超过采样周期,缓冲区还是会溢出。我的做法是在中断里只做数据搬运和标志置位,复杂计算放到主循环,并且用环形缓冲区做二级缓冲。

4.2 定时器触发采样与精确时序

用软件延时触发CONVST精度不够,10kSPS下误差可能到几百Hz。更可靠的方式是用定时器PWM或者定时器中断来触发。我用TIM2的更新中断,每100us触发一次转换:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { AD7606_StartConversion(); } }

TIM2挂在APB1上,时钟36MHz,预分频36-1,自动重装载值100-1,得到100us周期即10kHz。这个精度比软件延时高两个数量级,而且不占用CPU。

注意:定时器中断里只触发转换,不要做SPI读取。SPI读取要等BUSY下降沿,放在EXTI中断里。两个中断优先级要设置合理,TIM2中断优先级低于EXTI,避免转换触发打断数据读取。

4.3 数据校准与软件滤波

AD7606出厂时已经校准过,但实际电路中由于基准电压源精度、PCB走线阻抗等因素,还是会有零点偏移和增益误差。我在项目里做了两点校准:短接模拟输入到地,采集1000个样本取平均得到零点偏移;输入标准5V参考电压,采集1000个样本取平均计算增益系数。

float offset[8] = {0}; float gain[8] = {1.0}; float AD7606_GetVoltage(uint8_t ch) { return (adc_channels[ch] - offset[ch]) * gain[ch] / 32768.0f * 5.0f; }

软件滤波方面,如果信号带宽不高,可以做滑动平均或者一阶低通滤波。我的项目信号最高频率1kHz,采样率10kSPS,用16点滑动平均后噪声明显降低,而且相位延迟可以接受。

#define FILTER_LEN 16 int32_t filter_buf[8][FILTER_LEN] = {0}; uint8_t filter_idx = 0; void AD7606_Filter(void) { for(int ch = 0; ch < 8; ch++) { filter_buf[ch][filter_idx] = adc_channels[ch]; int32_t sum = 0; for(int i = 0; i < FILTER_LEN; i++) sum += filter_buf[ch][i]; adc_channels[ch] = sum / FILTER_LEN; } filter_idx = (filter_idx + 1) % FILTER_LEN; }

这个滤波在10kSPS下每帧计算量约128次加法,STM32F103在72MHz下耗时不到10us,完全跟得上。

5. 常见问题排查与实战避坑指南

5.1 SPI读数据全为0或0xFF的排查思路

这是新手最常遇到的问题,我至少遇到过三次,每次原因都不一样。整理成排查表:

现象可能原因排查方法
全0xFFPAR/SER/BYTE SEL引脚未接高万用表测引脚电平
全0x00CS未拉低或SCLK无输出示波器抓CS和SCLK波形
数据错位CPOL/CPHA配置错误对照AD7606时序图调整
只有第一通道有数CS提前拉高检查DMA传输完成前CS状态
数据跳变剧烈模拟电源噪声大示波器看AVCC纹波

我第一次调试时SPI读出来全是0xFF,查了半天以为是SPI配置问题,最后发现是PAR/SER/BYTE SEL引脚悬空了。AD7606内部有弱上拉,悬空时默认高电平进入串行模式,但悬空引脚容易受干扰,还是老老实实接VCC靠谱。

第二次遇到数据错位,读出来的值整体左移了一位。原因是CPHA设成了1Edge,改成2Edge后正常。这个问题的本质是AD7606在SCLK下降沿输出数据,STM32要在下一个上升沿采样,对应CPHA=2Edge。

5.2 BUSY信号异常与转换失败处理

BUSY信号一直为高,说明AD7606没有完成转换。可能原因有:CONVST脉冲太窄(小于25ns)、RESET未正确执行、芯片供电异常。我的做法是在启动转换后加一个超时检测:

HAL_StatusTypeDef AD7606_WaitBusy(uint32_t timeout) { uint32_t tick = HAL_GetTick(); while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9) == GPIO_PIN_SET) { if(HAL_GetTick() - tick > timeout) return HAL_TIMEOUT; } return HAL_OK; }

如果超时,执行一次复位并重新初始化。这个保护机制在电磁干扰强的工业现场特别有用,我有个项目在变频器旁边工作,偶尔会出现BUSY卡死,加了超时复位后连续运行一周没再出问题。

5.3 高采样率下的数据丢帧与DMA溢出

当采样率超过20kSPS时,SPI读取时间加上中断处理时间可能超过采样周期,导致DMA还没传完下一次转换就开始了。解决办法有三个:提高SPI时钟到18MHz、减少中断处理时间、用双缓冲。

我实测STM32F103在72MHz下,SPI 9MHz读取16字节约14us,中断响应加处理约5us,总共19us。10kSPS周期100us绰绰有余,但50kSPS周期20us就非常紧张了。如果非要跑50kSPS,建议换STM32F4系列,SPI时钟可以到42MHz,读取时间缩短到3us以内。

避坑技巧:DMA传输完成中断里不要调用HAL_Delay或者做浮点运算,这些操作耗时不可控。所有耗时操作放到主循环,中断里只做标志置位和缓冲区切换。

5.4 模拟输入保护与抗混叠滤波

AD7606内置了模拟输入钳位保护,可以承受±16.5V的过压,但这不代表可以随便接。如果传感器输出可能超过量程,建议外加电阻分压或者TVS管。我在一个电机电流检测项目里,因为电机反电动势导致输入电压瞬间冲到15V,虽然AD7606没坏,但采集数据全饱和了。后来加了BAV99双二极管钳位到5V和GND,问题解决。

抗混叠滤波方面,AD7606内置二阶巴特沃斯滤波器,截止频率约22kHz(无过采样时)。如果信号带宽低于这个值,可以直接用;如果信号包含高频噪声,建议外加RC低通。我的项目信号最高1kHz,在输入端加了1kΩ电阻和100nF电容组成RC滤波,截止频率1.6kHz,有效抑制了高频干扰。

6. 性能实测与优化经验分享

6.1 实测数据与精度评估

用信号发生器输入1kHz正弦波,幅度4Vpp,偏置0V,采集10000个样本做FFT分析。实测结果:SNR约85dB,THD约-90dB,ENOB约13.8位。这个成绩对于16位ADC来说还有提升空间,主要瓶颈在基准电压源的噪声和PCB布局。

后来我把基准电压源从MCU的3.3V换成ADR421(2.5V,噪声0.1Hz-10Hz仅1.5uVpp),ENOB提升到14.5位。所以如果你对精度要求高,基准电压源的投资不能省。

优化措施SNR(dB)ENOB(位)
初始方案8213.3
加RC滤波8513.8
换基准电压源8814.5
加屏蔽罩8914.7

6.2 CPU占用率与实时性优化

用DMA方案后,10kSPS下CPU占用率实测约4.7%。具体分布:TIM2中断0.3%,EXTI中断1.2%,SPI DMA完成中断2.1%,主循环数据处理1.1%。如果关掉滑动平均滤波,占用率降到3.2%。

优化建议:如果CPU资源紧张,可以把数据解析和滤波放到DMA完成中断里做,但要注意中断执行时间不能超过采样周期的50%。我的做法是在中断里只做数据拼接和存入环形缓冲区,滤波和上传在主循环做。

6.3 从F103到F4的移植经验

后来因为项目需要更高采样率,我把代码移植到了STM32F407。主要改动:SPI时钟从9MHz提到21MHz,DMA从DMA1换到DMA2,中断向量表重新映射。CubeMX重新生成代码后,AD7606驱动层几乎不用改,只需要调整SPI句柄和DMA通道。

移植后50kSPS下CPU占用率约8%,SPI读取时间从14us降到6us,时间余量充足。如果你也在考虑升级平台,F4系列是性价比很高的选择,而且CubeMX对F4的支持很完善。

6.4 几个容易被忽略的细节

最后分享几个我在实际项目中踩过的坑。第一,AD7606的CONVST脉冲宽度数据手册写最小25ns,但实际测试发现低于50ns时偶尔会转换失败,建议用__NOP()延时或者硬件RC延时确保100ns以上。第二,SPI的MISO引脚要配置为上拉输入,否则在CS切换瞬间可能产生毛刺。第三,如果多个AD7606级联,要注意FRSTDATA引脚的用法,它指示第一个通道数据是否就绪,级联时可以用来同步。

还有一点关于CubeMX的:生成的SPI初始化代码里,NSS引脚如果配置为Software,CubeMX会自动把NSS引脚设为GPIO输出并置高。但如果你用普通GPIO控制CS,记得在CubeMX里把NSS设为Disable,否则会多占用一个引脚。

这个项目从选型到稳定运行花了大约两周时间,其中调试SPI时序占了三分之一。现在回头看,如果一开始就用示波器抓波形对照数据手册,能省下至少三天。所以我的建议是:嵌入式调试,示波器比printf好用一百倍。

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

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

立即咨询