STM32F103通过SPI驱动AD7606实现8通道同步采样
2026/9/16 15:14:08 网站建设 项目流程

简介:面向使用STM32F103芯片与AD7606/AD7608模数转换器的嵌入式开发者,这套驱动资源提供了完整的SPI通信模式源码与配套说明,覆盖多通道十六位同步数据采集中涉及的接口初始化、引脚配置、时序控制、命令收发以及中断响应等关键环节。整个压缩包共包含567个文件,体积约2.06MB。文件类型以HTML参考文档、PNG示意图、C语言源码、H头文件、汇编启动文件以及IAR/Keil工程配置文件为主,其中46个C源文件和65个头文件构建了可直接编译的工程,216个HTML文档则用于查阅寄存器与调用说明,整体目录结构清晰,便于学习与二次开发。该资料目前已有1307人学习下载。压缩包内还包含cstart_thumb2.asm等底层启动文件、stm32f10x系列标准外设库源文件,以及PDF用户手册和示例工程,详细展示了初始化流程、SPI数据读写函数和中断处理逻辑。通过学习,开发者能够快速掌握STM32F103通过SPI驱动AD7606的完整方法,并能方便地迁移到AD7608或其他同步采样系统中。

1. 为什么在 STM32F103 上给 AD7606 写驱动,而不是直接用现成库

做 8 通道同步采样时,AD7606 几乎是 STM32F103 项目里的首选 ADC——一颗芯片就能同时锁存 8 路模拟输入,16 位分辨率,最高 200kSPS 吞吐率。但很多人在驱动层就被卡住:并行接口要占 16 根数据线,和 LQFP48 的 F103 相比引脚完全不够用;换成串行 SPI 后,又因为 CONVST、BUSY、OS 这几个引脚的状态配合问题读出不稳定的数据。我实际把 AD7606 挂在 SPI1 上,用 5 根信号线就完成了 8 通道读取,配合标准外设库 V3.5 和 ST-LINK 调试器,在电机驱动板上稳定运行。这篇就把这套驱动方案拆开讲:时序怎么对,初始化怎么配,读回来的 16 位补码怎么换算成电压,以及常见坑的定位方法。

2. AD7606 的采样机制与接口时序:并行总线 vs SPI

2.1 AD7606 的关键引脚和内部工作原理

AD7606 的真正常用引脚分三组:模拟输入 V1~V8,电源 AVCC/VDRIVE/GND,数字接口 CONVST、BUSY、CS、SCLK、DB[15:0]。芯片内部每个输入通道都有独立的采样保持放大器,因此所有通道的采样动作发生在同一个 CONVST 上升沿,这是它与其他多路复用型 ADC 最大的区别。你关心的“同步采样”实际上就是靠这个上升沿完成的:上升沿锁存模拟输入,之后内部逐次逼近寄存器(SAR)对每个通道同时转换,所有结果在约 3.2us 后同时就绪。

数字接口上,DB[15:0] 是并行数据总线,配合 CS 和 RD 读取;在串行模式下,数据从 DOUTA 和 DOUTB 引脚逐位移出。F103 的 GPIO 资源通常不够并行总线使用,尤其 LQFP48 封装下再要接按键或串口就会很紧张,所以我会优先使用 SPI 读取。FSMC 当然也能把 AD7606 映射到外部内存地址,但 F103 的 FSMC 一般服务于外部 RAM 和 TFT,为 AD7606 单独配置一套时序性价比不高,而且并行连接还容易因为信号线不等长导致高速读取误码。因此本文后面只讨论 SPI 单线输出模式。

2.2 CONVST 与 BUSY 的时序关系

一次完整的转换周期从 CONVST 拉高开始。手册要求 CONVST 高电平脉冲不小于 25ns,实际用 GPIO 翻转时不需要额外延时,因为 F103 的 GPIO 输出翻转本身就有几百纳秒。CONVST 上升沿锁存模拟输入,随后 BUSY 拉高,内部开始转换;转换完成后 BUSY 拉低,16 位结果已经锁存在移位寄存器里,等待 SCLK 读出。以下是简化后的关键时间参数,实际值以 AD7606 数据手册为准。

参数手册要求我常用的配置
CONVST 高电平宽度最小 25nsGPIO 直接输出,不额外延时
BUSY 高电平保持时间约 3.2us 或更长(过采样时更长)轮询等待或下降沿中断
SCLK 最高频率25MHz18MHz(72MHz 主频 4 分频)
CS 有效到 SCLK 第一个边沿设置保持时间等待 BUSY 拉低后再拉低 CS

等待 BUSY 下降沿是最常见的同步方式,轮询和外部中断都可以。下面是一个伪代码骨架,展示触发转换和等待的过程:

// 触发一次转换,并等待 BUSY 下降沿 AD7606_CONVST_HIGH(); // 拉高 CONVST,上升沿锁存输入 AD7606_CONVST_LOW(); // 拉低,触发内部转换 while (AD7606_BUSY_READ() == SET); // BUSY 拉高期间等待 AD7606_CS_LOW(); // 转换完成后允许输出 read_8_channels(); // 读 8 个 16 位结果 AD7606_CS_HIGH(); // 释放片选

这段代码中 CONVST 用普通 GPIO 模拟,而不是 PWM,因为 PWM 无法在上升沿之后精确控制拉低时机。BUSY 是推挽输出,所以 F103 的输入方向不需要上拉或下拉;但为了防止外部强干扰,我会配成下拉输入。wait 循环里最好加超时计数,如果 AD7606 没上电或 CONVST 没触发,BUSY 会一直保持高电平,没有超时会直接卡死主循环。

2.3 SPI 模式下读出数据的位顺序

串行模式下,CS 拉低后,SCLK 的每个下降沿把移位寄存器里的数据从 DOUTA 逐位移出,最高位在前。由于 AD7606 在下降沿更新数据,F103 需要在上升沿采样,所以 SPI 的空闲极性为低(CPOL=0),相位选择第二个边沿(CPHA=2Edge)或第一个边沿都可能在不同板卡上工作,原因是 CS 到第一个 SCLK 的建立时间不同。我会在初始化代码里先固定一组参数,再用逻辑分析仪看波形;如果发现读出数据整体右移一位或最高位变成符号位,就把 CPOL 和 CPHA 同时翻转后重试。

实际在标准库中,我用的组合是SPI_CPOL_LowSPI_CPHA_2Edge。SPI1 的波特率先用 4 分频,也就是 18MHz,如果波形质量不好就降到 9MHz。这个细节在调试章节会展开。

3. STM32F103 驱动 AD7606:最小系统与硬件连接要点

3.1 最小系统搭建:晶振、电源和复位

STM32F103 最小系统通常包括 8MHz 晶振、两颗 20pF 负载电容、BOOT0 下拉到地、复位引脚上拉加 100nF 电容。如果只是想验证 AD7606 驱动,用最小系统板就够,但要注意电源布局:AD7606 的模拟电源 AVCC 需要 5V,而 STM32F103 是 3.3V,两者不能共用同一路 LDO 输出。常见做法是用一块 5V 电源给 AD7606 的 AVCC 和模拟输入供电,另外用低压差稳压器把 5V 转成 3.3V 给 F103 和 VDRIVE 供电。VDRIVE 必须接 3.3V,因为它决定了 AD7606 数字引脚的输出高电平范围;这样 F103 的 SPI 引脚可以直接连接 AD7606,不需要额外电平转换。

模拟地和数字地建议在电源入口处用一颗 0Ω 电阻单点连接。AD7606 的 AGND 和 DGND 引脚分开,如果地平面分割不当,SPI 时钟线的噪声会耦合进模拟输入,表现为读数最低位跳动明显。实际项目中我会先把所有 DGND 接到数字地铜皮,再把 AGND 单独走线到 ADC 附近,最后通过磁珠或 0Ω 电阻合并到主地。

3.2 引脚分配表与接线图

下面是驱动代码里实际使用的引脚分配,基于 STM32F103C8T6 的 LQFP48 封装。注意 CS 用了 SPI1 的硬件 NSS 引脚 PA4,但在代码中把它当普通 GPIO 控制,不使用硬件 NSS 自动管理。

信号AD7606 引脚STM32F103 引脚说明
CONVSTCONVST A/B 并联PA0普通推挽输出,下降沿触发转换
BUSYBUSYPA1输入,转换期间高电平
RESETRESETPA2输出,低有效复位
CSCSPA4软件控制片选,低有效
SCLKSCLKPA5SPI1_SCK
DOUTADOUTA/RDPA6SPI1_MISO,串行数据输出
OS[2:0]OS0/OS1/OS2PB12/PB13/PB14过采样率配置,可接 GPIO
RANGERANGE3.3V 或 GND选择 ±5V 或 ±10V

AD7606 在串行单输出模式下,只需要 DOUTA,DOUTB 和并行数据引脚悬空即可。OS[2:0] 如果直接接 3.3V 或 GND 也可以,但我想在软件里动态切换过采样率,所以接到了 GPIO。注意 RANGE 引脚不能悬空,必须接高或接低,否则内部逻辑不确定会导致量程出错。

3.3 软件工程准备:标准库 V3.5 与调试工具

STM32F103 的驱动工程,我一般从标准外设库 V3.5.0 起步,而不是用 HAL。原因是寄存器级的 SPI 操作和 DMA 配置在标准库下更直接,而且很多老项目都在 V3.5 上固化了。把 Libraries/CMSIS 和 Libraries/StdPeriph_Driver 拷进工程,在预编译选项里加上USE_STDPERIPH_DRIVER, STM32F10X_MD,启动文件选择startup_stm32f10x_md.s(中密度)或_hd.s(高密度)。如果用的是 C8T6,选 md 即可。

调试和下载工具方面,ST-LINK 需要先安装 stlink 驱动,否则 Keil 里会报No ST-LINK detected;串口打印则依赖 CH340 或 CP2102 这类 USB 转 TTL 的驱动。另一个容易忽略的是在工程里加入延时函数,比如一个简单的delay_us,因为 AD7606 上电后需要一小段时间稳定,复位后也需要等待。下面的复位函数就是为此准备的:

void AD7606_HardwareReset(void) { AD7606_RESET_LOW(); // 拉低,进入复位状态 delay_us(20); // 复位脉冲保持 20us AD7606_RESET_HIGH(); // 释放复位,芯片开始工作 delay_us(1); // 等待内部逻辑稳定 }

复位引脚是低电平有效,所以这里先拉低再拉高。复位脉冲的最小宽度通常 10ns 以上,我给 20us 完全足够。释放复位后芯片默认处于待机状态,OS 引脚电平在复位期间被锁存,所以 OS 引脚的电平必须在复位释放前设置好。这一点容易漏:很多人上电后直接拉高 RESET,但 OS 引脚还是高阻,导致过采样率随机。

4. 使用标准库在 SPI 模式下配置并读取 AD7606 数据

4.1 SPI1 和 GPIO 的初始化代码

核心驱动分两步:GPIO 管脚状态放在AD7606_GPIO_Init,SPI 外设配置放在AD7606_SPI_Init。先看 GPIO 初始化,这里把 CONVST、RESET、CS 设为推挽输出,BUSY 设为输入,SCK/MISO 设为复用推挽。

void AD7606_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOA、GPIOB 和 SPI1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); // CONVST -> PA0, RESET -> PA2, CS -> PA4,推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_2 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // BUSY -> PA1,下拉输入,防止上电期间误触发 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOA, &GPIO_InitStructure); // SCK -> PA5, MISO -> PA6,复用推挽 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // OS[2:0] -> PB12/PB13/PB14,推挽输出,先全部拉低 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14); }

这里把 OS 引脚也初始化了,初始值全 0 对应无过采样。SPI_Speed_50MHz只代表 GPIO 输出翻转速率,不是实际 SPI 时钟。BUSY 用下拉输入是因为 AD7606 推挽输出,上电瞬间状态未知;下拉后默认低电平,避免第一次读取时误认为转换完成。

SPI 初始化要特别注意 CPOL 和 CPHA。下面的配置对应 SCLK 空闲低电平,第二个边沿采样,数据大小 16 位:

void AD7606_SPI_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

SPI_BaudRatePrescaler_4在 72MHz 主频下得到 18MHz SCLK。AD7606 的 SCLK 上限是 25MHz,但 18MHz 已经接近上限,长杜邦线下容易产生过冲;如果发现采样值不稳定,把这行改成SPI_BaudRatePrescaler_8,降到 9MHz 后大多能解决。SPI_NSS_Soft表示不使用硬件片选,因此 PA4 完全由普通 GPIO 控制。

4.2 启动转换并读取 8 通道结果

接下来是驱动核心的读取函数。这个函数完成一次完整采样:触发转换、等待 BUSY 结束、连续拉取 8 个 16 位结果。

int16_t AD7606_ReadChannels(int16_t *buf, uint8_t len) { uint32_t timeout; if (len < 8) return -1; // CONVST 输出一个下降沿,触发所有通道同步采样 AD7606_CONVST_LOW(); AD7606_CONVST_HIGH(); AD7606_CONVST_LOW(); // 等待 BUSY 拉低,表示转换完成 timeout = 10000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) != Bit_RESET) { if (--timeout == 0) return -2; } // CS 拉低后,SPI 连续读取 8 个 16 位数据 AD7606_CS_LOW(); for (uint8_t i = 0; i < 8; i++) { uint16_t raw; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, 0x0000); // 发送空数据,同时接收 MISO while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); raw = SPI_I2S_ReceiveData(SPI1); buf[i] = (int16_t)raw; // 补码转有符号数 } AD7606_CS_HIGH(); return 0; }

AD7606_CONVST_HIGH/LOW对应 PA0 的GPIO_SetBits/ResetBitsAD7606_CS_LOW/HIGH对应 PA4。这里 CONVST 拉高再拉低,之间没有延时;由于 C 语句执行时间在百纳秒级,满足手册的 25ns 最小脉冲宽度。如果之后发现触发不稳定,在HIGHLOW之间加一个delay_us(1)即可。

SPI 的SendDataReceiveData必须成对调用。标准库的SPI_I2S_SendData其实是往数据寄存器写值,写完后硬件立刻开始发送,同时 MISO 上的数据被采样到接收寄存器;所以每次发送前要等待TXE标志,发送后等待RXNE标志再读接收寄存器。这里发送的数据是无意义的 0x0000,真正的目的是产生 SCLK 时序。

返回值设计为 0 成功,负值为错误码。-1表示传入缓冲区长度不足,-2表示 BUSY 超时。在主循环里可以根据返回值打印错误信息,避免出现全零数据时无从排查。

4.3 关键参数配置:量程、过采样和采样率

AD7606 的量程由 RANGE 引脚决定,过采样由 OS[2:0] 决定。下表是常用配置和对应特性:

RANGE 引脚模拟输入范围1 LSB 对应电压
接 GND±5V10V / 65536 ≈ 152.6µV
接 VDRIVE±10V20V / 65536 ≈ 305.2µV
OS[2:0]过采样倍数最大吞吐率(典型)
000200kSPS
0012 倍100kSPS
0104 倍50kSPS
0118 倍25kSPS
10016 倍12.5kSPS
10132 倍6.25kSPS
11064 倍3.125kSPS

在驱动代码里设置 OS 引脚很简单,比如GPIO_SetBits(GPIOB, GPIO_Pin_12);代表 OS2=1。但要记住,OS 引脚电平必须在释放复位前设置好,否则过采样倍数不确定。采样率由 CONVST 的触发频率决定,如果只是主循环里丢下降沿,实际采样率会随着指令执行长度波动;要精确 200kSPS 必须用定时器产生稳频的 CONVST 信号,这部分在下一章细说。

5. 驱动优化:定时器触发 CONVST,BUSY 中断和 DMA 搬数据

5.1 用定时器产生精确采样时钟

软件触发 CONVST 适合读取直流或慢变信号,但测量交流信号时需要等间隔采样,否则重建波形会看到频率抖动。常见做法是把 TIM1 的 CH1 输出 PWM 波形,接到 AD7606 的 CONVST 引脚,用定时器周期决定采样率。比如要 200kSPS,就把 PWM 频率设成 200kHz,占空比 50%,TIM1 的计数时钟是 72MHz,所以周期寄存器值就是 360。

void AD7606_Timer_Trigger_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_TimeBaseStructure.TIM_Period = 360 - 1; // 200kHz 采样率 TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse = 180 - 1; // 50% 占空比 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开主输出 TIM_Cmd(TIM1, ENABLE); }

这段代码把 TIM1_CH1 接到 PA8,PA8 复用映射到 TIM1_CH1。如果板子上 PA8 被占用,也可以把 CH1 重映射到其他端口,但对于 C8T6 只能用 PA8。PWM 模式 1 下,计数小于比较值时输出高电平,因此 CONVST 的上升沿出现在计数归零时,下降沿出现在比较匹配时。AD7606 需要的是下降沿触发,所以用 PWM1 模式刚好合适。

TIM_CtrlPWMOutputs(TIM1, ENABLE)是高级定时器特有的主输出使能,漏掉这行 PWM 引脚不会有波形。采样率改起来也很方便,在TIM_Period里填72000000 / target_sps - 1即可,注意 target_sps 不要超过 200000。下表给出常用配置:

目标采样率TIM_Period 设置值(72MHz 时钟)是否支持 8 通道连续 DMA 读取
200kSPS360-1是,需 18MHz SCLK
100kSPS720-1是,可降 SPI 时钟
50kSPS1440-1是,稳定性更好
10kSPS7200-1是,几乎无压力

5.2 BUSY 下降沿中断与 SPI 双 DMA 读取

定时器触发转换后,主循环去轮询 BUSY 会浪费大量 CPU。更好的方案是把 BUSY 接到一个外部中断输入,在下降沿中断里启动 SPI 的 DMA 接收,让外设直接把 8 个通道数据搬到内存。DMA 使用两路通道:一路把内存中固定的 0x0000 循环发送到 SPI1_DR 以产生 SCLK,另一路把 SPI1_DR 接收到的数据搬到 ADC 缓冲区。

外设DMA 通道方向模式说明
SPI1_TXDMA1_Channel2内存到外设循环模式发送 0,产生 SCLK
SPI1_RXDMA1_Channel3外设到内存普通模式接收 8 个通道数据

TX DMA 的源地址要指向一个内容全部为 0 的uint16_t数组,目标地址是&SPI1->DR。RX DMA 的初始化如下:

void AD7606_DMA_Init(uint16_t *buf) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 8; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); }

注意 SPI1_RX 在 DMA1 上固定使用通道 3,SPI1_TX 是通道 2,切勿配错。DMA_Mode_Normal表示传输 8 个半字后自动停止,要在下一次中断里重新启动。buf必须是uint16_t数组,并且注意对齐。BUSY 的外部中断服务函数这样写:

void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) != RESET) { AD7606_CS_LOW(); // 允许 AD7606 移出数据 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, 8); DMA_Cmd(DMA1_Channel3, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_RX, ENABLE); EXTI_ClearITPendingBit(EXTI_Line1); } }

为什么先DMA_Cmd(DISABLE)再重新使能?因为 DMA 在 Normal 模式下传输结束后,当前数据计数器变成 0,必须把计数器重新设回 8 才能进行下一次传输。如果不做这一步,第二次中断进来时 DMA 不会工作。TX 循环 DMA 在初始化后一直运行,所以 SCLK 始终存在;中断里只需要重新触发 RX DMA 就能抓住当前通道的数据。

5.3 双缓冲区持续采样

单缓冲区在 DMA 传输完成中断里做数据搬运时,主循环如果想读同一份数据,可能读了一半被 DMA 覆盖。常见的解法和串口双缓冲一样:准备两个 8 数据的缓冲区,DMA 只操作其中一个,另一个给应用层读取,两块交替使用。这个方案在 FreeRTOS 里也适用:DMA 传输完成中断里把满缓冲区地址放到队列,任务从队列取出后做滤波或傅里叶变换。注意 DMA 的源地址永远是SPI1->DR,目标地址在切换时要通过DMA_Cmd(DISABLE)后修改DMA_MemoryBaseAddr再重新使能。这样在 200kSPS、8 通道的前提下,CPU 只需要在每次 DMA 完成时处理一次数据,占用率远低于逐位轮询。

6. 验证驱动是否正常:逻辑分析仪波形与串口数值检查

6.1 用逻辑分析仪验证时序

如果代码写完但读回来的数据不对,第一步不是调算法,而是用逻辑分析仪看 SCLK、CS、BUSY、DOUTA 四根线的实际时序。采样率要设到 100MHz 以上,否则 18MHz 的 SCLK 抓不出细节。正常波形应该是:CONVST 下降沿后 BUSY 立即拉高,保持约 3.2us 后拉低;CS 拉低后 DOUTA 输出 8 组 16 位脉冲,每组之间有一个极短的空闲周期,但 CS 在整个读取期间不能被拉高。如果看到 CS 在读取中间拉高,检查主循环里是否有其他外设占用了 SPI1。

现象可能原因调整方法
读出数据整体右移一位SPI 采样沿不对把 CPOL/CPHA 同时取反
DOUTA 只有一半脉冲CS 持续时间不够或 SCLK 频率过高降低 SPI 分频到 9MHz
BUSY 一直为高CONVST 脉冲过窄或 AD7606 未复位检查 CONVST 是否真的翻转,重新执行复位函数
所有通道都是同一个值CS/DOUTA 接错或 MISO 没有断开其他设备用万用表量 PA6 和 DOUTA 之间通断

6.2 用串口打印 8 通道电压值

时序正常后,把转换结果通过串口打印出来验证数值。CH340 驱动装好后,在代码里重定向fputc,然后用printf输出每个通道的有符号数和电压值。下面是最小验证程序:

// 串口1重定向printf int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; } void Print_Voltage(int16_t raw, float range) { float voltage = raw * range / 32768.0f; // range 为 5.0 或 10.0 printf("raw=%5d voltage=%.3f V\r\n", raw, voltage); }

这段浮点运算里的range对应量程的一半。如果 RANGE 接地,量程是 ±5V,所以满量程正数 32767 对应 +5V,计算时range传 5.0 即可。把 V1 输入端短接到 GND,打印结果应该在 0 附近;加一个 1.5V 干电池到 V1 与 GND 之间,读到的电压值应该在 1.5V 左右,误差几十毫伏以内。如果数值完全不对,先用万用表确认输入电压真实值。

6.3 常见坑:SPI 相位、VDRIVE、PA11

最后集中说几个容易踩的硬件和软件坑。

  • SPI 相位错位是频率最高的坑。用逻辑分析仪看清 SCLK 与 DOUTA 变化沿的关系,F103 必须在上升沿读取,如果不是,交换 CPOL/CPHA。
  • VDRIVE 没接的时候,AD7606 输出的高电平可能只有 1V 左右,F103 识别不到,表现为数据全是 0 或乱跳。VDRIVE 必须接到 3.3V,并且与 F103 的 VDD 同源。
  • 复位释放前要设置好 OS[2:0] 和 RANGE,否则过采样倍数和量程随机。我遇到过 32 倍过采样导致吞吐率只有 6.25kSPS 的案例,排查半天才发现 OS 引脚悬空。
  • 如果 BUSY 或 CS 接到了 PA11/PA12,在启用 USB 外设的工程里会出现上电后程序跑飞的问题。这个问题在 F103 的部分批次上有迹可循,表现为 ST-LINK 下载正常,但复位后卡死,拔掉 USB 线才恢复。所以驱动 AD7606 时避开 PA11/PA12,把它们留给 USB 专用引脚。
  • 一个有用的自检技巧:把 AD7606 的 REFIN/REFOUT 引脚(2.5V 基准输出)用短跳线接到 V1 输入,连续读取 100 次求平均值,如果结果落在 16300 ± 100 范围内,说明 SPI 相位、符号转换和量程配置都正确。这个范围对应 2.5V 电压在 ±5V 量程下的理论值 16384 附近,偏差主要来自基准精度和输入噪声。

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

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

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

立即咨询