尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 GPIO模拟SPI读取AD7606 8通道同步采集驱动详解

STM32 GPIO模拟SPI读取AD7606 8通道同步采集驱动详解 简介AD7606作为8通道16位同步采样ADC在电力监测、振动分析等需要多通道同步采集的场景中应用广泛。本资源是基于STM32F429单片机的AD7606驱动软件工程源码采用GPIO模拟SPI接口方式读写芯片全套代码含bsp驱动层、应用Demo及菜单显示可直接运行验证。工程共685个文件以C源码441个和头文件165个为主另有启动汇编、链接脚本、工程配置EWP/UVPROJ与Hex文件方便在IAR或Keil中编译烧录还附带PDF说明文档便于快速上手。压缩包约4.85MB结构清晰适合正在学习STM32与高精度同步采样ADC的开发者参考。已有233人学习下载读者可从bsp_spi_InitAD7606、Demo_spi_AD7606等入口逐步理解寄存器配置、时序模拟和数据处理流程节省自行移植调试时间。1. GPIO 模拟 SPI 读 AD7606为什么这么干不少调试过 AD7606 的工程师第一反应是开硬件 SPI但真正到了布板阶段却发现 STM32 的 SPI2 已经被传感器占了SPI3 又要接屏幕最后只能把 CS、SCLK、DOUT 往三个空的 GPIO 上一挂用软件模拟把 8 通道数据读回来。这个做法在 STM32F103 和 STM32F407 上都成立AD7606 一次串行读帧是 8 通道 x 16 位一共 128 个 bitGPIO 翻转速度即使只有 1MHz整帧也能在 130us 内读完而 AD7606 的转换时间才 3~4us用软件 SPI 根本不会把采样率拖垮。这个 .zip 标题里的工程核心就是把 AD7606 的驱动程序、GPIO 模拟 SPI 读写函数、主循环采集逻辑打包在一起适合手里有现成板子、不想重新画 PCB、又需要在 F1/F4 上快速跑通 8 路同步采集的人。下面按工程落地的顺序把引脚分配、驱动代码、触发时序、验证方法一次讲完。2. AD7606 的 SPI 引脚分配与 GPIO 工作模式配置2.1 先定引脚硬件片选还是软件片选AD7606 串行接口最少需要 4 根线CS、SCLK、DOUTA、BUSY再加上 CONVST 触发引脚总共 5 个 GPIO。如果只读取一个 AD7606CS 可以直接接地SCLK、DOUT、CONVST、BUSY 四根线就够了。但在实际工程里我更建议保留软件 CS原因很简单你需要在读每一帧之前用 CS 低电平把 DOUT 拉出来读完之后 CS 再拉高这个低电平窗口本身就是 SPI 帧的边界完全由软件控制不用去跟硬件 NSS 自动片选的时序较劲。引脚MCU 方向GPIO 配置作用SCLK输出推挽输出高速产生串行移位时钟DOUTA输入浮空输入或上拉输入读取串行数据位CS输出推挽输出高速片选信号低电平有效CONVST输出推挽输出高速启动 8 通道同步转换BUSY输入浮空输入或上拉输入判断转换是否结束RANGE输出推挽输出选择 ±5V 或 ±10V 量程OS0/OS1/OS2输出推挽输出配置过采样倍数硬件片选和软件片选的差别在这个驱动里非常明显。硬件 SPI 的 NSS 引脚由外设自动控制虽然省一条 GPIO但片选时序往往与 AD7606 的“CS 拉低后 DOUT 立即输出第一位”特性对不齐。软件片选则可以在任意时刻把 CS 拉低再手动翻转 SCLK读完后拉高哪天要接两片 AD7606只需要复制一份 CS 控制逻辑驱动主体完全不用动。2.2 GPIO 的 8 种工作模式里这五种能干活STM32 的 GPIO 有输入浮空、输入上拉、输入下拉、推挽输出、开漏输出、复用推挽、复用开漏等模式。做 GPIO 模拟 SPI 时真正用到的只有两类输出引脚配推挽输出输入引脚配浮空输入。这里有个常见错误把 DOUT 配成开漏输入希望靠外部上拉把电平拉高实际上 AD7606 的 DOUT 本身是推挽输出不需要外部上拉配开漏反而会让高电平变成弱上拉在高速翻转时出现边沿变缓的问题。输出引脚的速度参数同样影响很大。在 STM32F407 上GPIO 输出速度有 2MHz、25MHz、50MHz、100MHz 四档。模拟 SCLK 时如果选了最低速SCLK 翻转边沿会被拉得很宽虽然能工作但多通道连续读取时每一帧时间会明显变长。我一般把 SCLK、CS、CONVST 全部配成 GPIO_SPEED_FREQ_HIGH这样 GPIO 翻转延迟大约在 5~10ns再做软件延时反而更容易控制时序余量。void AD7606_GpioConfig(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); /* 输出引脚SCLK PB13、CS PB12、CONVST PB11 */ gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Pin GPIO_PIN_13 | GPIO_PIN_12 | GPIO_PIN_11; HAL_GPIO_Init(GPIOB, gpio); /* 输入引脚DOUTA PB14、BUSY PE8 */ gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Pin GPIO_PIN_14; HAL_GPIO_Init(GPIOB, gpio); gpio.Pin GPIO_PIN_8; HAL_GPIO_Init(GPIOE, gpio); /* 初始状态CS 高、CONVST 低避免上电误触发 */ AD7606_CS_HIGH(); AD7606_CONVST_LOW(); }这段代码里有几个参数值得说明。GPIO_MODE_OUTPUT_PP是推挽输出SCLK、CS、CONVST 都必须用它换成开漏输出后电平需要外部上拉反而多出故障点。GPIO_NOPULL对应不上拉不下拉AD7606 的输出是强推挽MCU 输入侧不需要内部上拉。GPIO_SPEED_FREQ_HIGH决定 I/O 驱动强度输出速度越高边沿越陡抗干扰能力越好代价是高速翻转时可能引入振铃所以 PCB 走线短的场景优先选择高速。2.3 从 CubeMX 到 Keil 的对接方式拿到一个 .zip 工程时我通常先看MX_GPIO_Init()里有没有上面这张表里的引脚。CubeMX 生成的 GPIO 初始化代码带一个GPIO_InitStruct如果原工程是在 CubeMX 里手工把引脚标成 GPIO_Output 或 GPIO_Input 再生成代码结构会是标准 HAL 写法。要注意的是如果原工程用了 PB3、PB4 做 GPIO 模拟 SPI必须在GPIO_Init之前把 JTAG 释放掉否则这两个引脚仍然被调试功能占用__HAL_AFIO_REMAP_SWJ_NOJTAG();这一句只对 STM32F1 系列生效F4 系列默认把 PB3、PB4 作为 GPIO 使用但如果你用 ST-Link 的 SWD 调试SWDIO 和 SWCLK 两个引脚不要占用。压缩包里如果出现stm32f4xx_hal_msp.c多半是 CubeMX 生成的工程源头引脚配置可以在.ioc文件里直接看到这样能快速确认作者用的是哪几个 GPIO省得反向读代码。3. 模拟 SPI 读写 AD7606bit-bang 驱动核心代码与 SPI 时序对齐3.1 为什么不能用硬件 SPI 的默认相位直接替代用 GPIO 模拟 SPI最核心的问题是 SCLK 空闲电平、数据建立时间和采样点的位置。AD7606 的串口读时序与普通 SPI 从机有一点差异CS 拉低后第一通道的第一个 bit 就已经出现在 DOUTA 上不需要等 SCLK后面每来一个 SCLK 下降沿DOUTA 才更新下一位。换句话说数据是在 SCLK 边沿之后才稳定的采样点必须放在 SCLK 的低电平区间并且给数据留出建立时间。很多 CubeMX 里配置的硬件 SPI 是 CPOL0、CPHA1看起来能匹配但实际上硬件 SPI 的第一个采样边沿发生在 CS 拉低之后、SCLK 第一个边沿之前这个相位受芯片内部移位寄存器控制常常因为 MCU 与 AD7606 的电气延迟不一致而丢位。GPIO 模拟 SPI 的优势就在于SCLK 的翻转完全由软件语句决定你可以在读数据位之前插入任意数量的空操作把采样点从边沿上拉开从“贴边采样”变成“平台采样”信号余量充足得多。3.2 读取一个 16 位字的软件 SPI 内核先写最底层的 16 位读函数它只负责在 CS 有效期间把 DOUTA 上的串行位收下来static uint16_t AD7606_ReadWord(void) { uint16_t word 0; for (uint8_t i 0; i 16; i) { word 1; if (AD7606_READ_DOUT()) { word | 0x0001; } /* SCLK 先拉高再拉低形成一个完整时钟周期 */ AD7606_SCLK_HIGH(); AD7606_SCLK_LOW(); /* 可选插入 2~8 个 NOP延长 SCLK 低电平时间 */ __NOP(); __NOP(); } return word; }这段代码的移位逻辑是每次进入循环先把已有结果左移一位再读当前 DOUT。第一次循环读到的 bit 被放在 bit0第二次循环后它被移到 bit1直到第 16 次循环后变成 bit15最终得到 MSB-first 排列。AD7606_SCLK_HIGH()和AD7606_SCLK_LOW()建议直接用寄存器操作不要调 HAL 的HAL_GPIO_WritePin因为 HAL 函数内部有assert_param和分支判断翻转一次可能吃掉几百 ns。用 BSRR 寄存器最直接#define AD7606_SCLK_PIN GPIO_PIN_13 #define AD7606_GPIO_PORT GPIOB #define AD7606_SCLK_HIGH() (AD7606_GPIO_PORT-BSRR AD7606_SCLK_PIN) #define AD7606_SCLK_LOW() (AD7606_GPIO_PORT-BSRR (uint32_t)AD7606_SCLK_PIN 16)BSRR 低 16 位写 1 对应置高高 16 位写 1 对应置低两个操作互不影响。这段代码里没有关中断如果这个函数被中断打断SCLK 波形会拉宽但不影响正确采样因为 AD7606 只认边沿数量不要求占空比严格一致。真正要小心的是不要在 SCLK 翻转过程中被一个长时间中断打断导致 CS 拉高后还差几个时钟没给完那种情况会读到错位数据。3.3 连续读取 8 通道的完整帧AD7606 的串行总线整帧数据是 8 个通道顺序排列的CS 拉低一次DOUTA 会依次送出 V1、V2……V8中间不能拉高 CS。所以读 8 通道不是反复拉 CS而是一次性读 128 个 bituint8_t AD7606_ReadAllChannels(uint16_t *adcValues) { uint8_t ch; if (adcValues 0) { return 0; } AD7606_CS_LOW(); for (ch 0; ch 8; ch) { adcValues[ch] AD7606_ReadWord(); } AD7606_CS_HIGH(); return 1; }参数adcValues是长度为 8 的uint16_t数组函数按通道索引填写。CS 必须在循环结束后再拉高如果提前拉高后面的通道数据会被截断典型现象是只有第一通道有值其余全是 0。AD7606_ReadWord()是前面那个静态函数8 次循环共用 128 个 SCLK整帧读完后 CS 高电平至少保持一个时钟周期再允许下一次转换触发。这个 CS 低电平持续时间就是软件片选帧的边界也是区别于硬件 SPI 自动片选的关键。读完这一帧后原始数据是二进制补码格式0V 对应 0正满量程接近 32767负满量程接近 -32768。后面做电压换算时要用int16_t类型转换不能直接当成无符号数否则负电压会全部变成 65535 附近的数这是 AD7606 移植中最常见的坑。4. 8 通道同步采集参数设置CONVST 触发、BUSY 判断、量程与 OS 引脚4.1 CONVST 上升沿触发BUSY 低电平后读取8 通道同步采样的含义是一个 CONVST 上升沿同时锁存 V1~V8 的模拟输入之后 AD7606 进入转换状态BUSY 引脚拉高转换完成后 BUSY 拉低这时才能去读串行数据。如果主循环在 BUSY 还没拉低时就去读读到的还是上一帧旧数据。标准采集函数可以这样组织uint8_t AD7606_ReadSample(uint16_t *adcValues) { uint32_t timeout; /* CONVST 拉高启动转换 */ AD7606_CONVST_HIGH(); __NOP(); __NOP(); AD7606_CONVST_LOW(); /* 等待 BUSY 从高变低 */ timeout 100000; while (AD7606_READ_BUSY() (timeout-- 0)) { /* 等待转换完成 */ } if (timeout 0) { return 0; } return AD7606_ReadAllChannels(adcValues); }CONVST 的高电平持续时间至少要满足数据手册要求一般 25~50ns 足够STM32F4 在 168MHz 下两次__NOP()大约能提供 20~30ns如果还担心余量不够可以把__NOP()加到 4 个。AD7606_READ_BUSY()是一个读取 GPIO 输入电平的宏在等待循环里BUSY 高电平表示转换中BUSY 低电平表示转换结束。timeout 的作用防止硬件故障时程序卡死在 while 循环里100000 次循环在 168MHz 上大约是 2ms远比 AD7606 的 4us 转换时间长一旦真的超时优先级应该放在检查 CONVST 和电源上而不是调大超时值。4.2 RANGE 和 OS 引脚下发必须在首帧转换前完成AD7606 的满量程由两根或者一根引脚决定。RANGE 引脚为高电平时是 ±10V为低电平时是 ±5V这个引脚属于模拟前端配置修改之后必须重新触发一次转换不能边采集边切换。OS0、OS1、OS2 三个引脚控制数字过采样过采样倍数越高等效采样率越低但噪声越低。实际工程里默认配置建议先全部拉低跑通通信后再试过采样。OS2OS1OS0过采样倍数说明0001无过采样默认0012数字滤波器开启0104适合噪声较大信号源0118有效位数提升明显10016常用最大倍数10132高倍率数据速率低11064仅低速传感器使用写引脚函数时注意 STM32 的 GPIO 位操作要保证三个 OS 引脚同时更新。如果逐位操作中间状态可能瞬间变成 001 或 010导致 AD7606 在中间状态启动一次不想要的滤波配置。好在 AD7606 在 CONVST 下降沿才会锁存这一次配置只要在触发之前把三个引脚稳定住即可。void AD7606_ConfigRangeOS(uint8_t range5V, uint8_t os) { if (range5V) { AD7606_RANGE_LOW(); } else { AD7606_RANGE_HIGH(); } AD7606_OS0_WRITE(os 0x01); AD7606_OS1_WRITE(os 0x02); AD7606_OS2_WRITE(os 0x04); }os参数直接使用上面表格里的二进制值例如要配 16 倍过采样就传 4。这段代码本身不复杂但有两个隐藏点一是 RANGE 引脚电平与量程的关系在不同批次 AD7606 上完全一致不需要芯片型号级别区分二是过采样只在正常转换模式下生效如果后面用外部 PWM 触发 CONVST过采样会降低最大可达到的吞吐率需要根据采样率重新算一遍。4.3 用中断把 BUSY 判断从主循环里拿出来如果系统里还有其他任务主循环死等 BUSY 会浪费 CPU。常见做法是把 BUSY 配置成 EXTI 下降沿中断转换完成时置一个全局标志主循环只检查标志再读取volatile uint8_t g_ad7606BusyFalling; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin AD7606_BUSY_PIN) { g_ad7606BusyFalling 1; } }在中断回调里只做标志置位不要调用AD7606_ReadAllChannels()。原因是读取 128 个 bit 的过程需要保持 SCLK 连续虽然中断打断不会丢位但会让整个读取时间拉长更重要的是如果中断优先级比系统 tick 高频繁的 SPI 读取会把调度延迟拉大。把触发和读取拆到两个上下文可以让采样触发更精确也能避免主循环长延时导致 BUSY 判断滞后。同步采样系统的采样周期抖动往往不是 ADC 转换造成的而是主循环读到 BUSY 的时机不稳用 EXTI 能够把抖动压到纳秒级。5. 源码包到手后的验证清单直流校准、异常定位与双线提速5.1 先用恒压输入验证量程和二进制补码不要一上来就接传感器信号先给通道 1 输入一个已知直流电压。把 AD7606 配成 ±5V 量程接一个 1.000V 的基准电压或者用精密可调电源读回来的裸码应该接近 6554。这个数的计算逻辑是32768 对应 5V 满量程1V 除以 5V 再乘 32768等于 6553.6。换算电压时使用下面的函数float AD7606_RawToVoltage(uint16_t raw, float fullScale) { int16_t signedValue (int16_t)raw; return ((float)signedValue * fullScale) / 32768.0f; }调用时fullScale传 5.0 或 10.0对应 ±5V 和 ±10V。注意这行代码里的(int16_t)raw不能省否则 32768 到 65535 之间的负电压码会被当成正数换算结果完全错误。如果接 1V 读到的是 32768 附近或者 0说明二进制补码被当作无符号数处理了检查驱动里word | 0x0001之后的变量类型。5.2 异常现象快速定位全通道读 0先查 CS 有没有拉低。用示波器探头直接量 CS 引脚CS 低电平期间 DOUT 上应该能看到一串脉冲。如果 CS 正常但 DOUT 没有数据检查 RANGE、OS 引脚有没有悬空AD7606 的数字量输入悬空时内部虽然没有上拉但走线过长会产生不确定电平导致芯片进入未知状态。数据顺序错乱用两个通道分别接不同直流电压看数组下标 0 和下标 1 是否对应 V1 和 V2不对就把通道映射表调整一下。偶发毛刺多半是 SCLK 翻转太快、DOUT 建立时间不够尝试在AD7606_ReadWord()的循环里加两个__NOP()把 SCLK 脉冲宽度从 60ns 放宽到 100ns看看故障是否消失。如果消失问题就是 PCB 走线电容太大而不是程序逻辑。5.3 双线 DOUTA/DOUTB 读取提速如果单帧 128 个 bit 太慢AD7606 还支持双线输出DOUTA 送出 V1~V4DOUTB 同时送出 V5~V8。这时一次 SCLK 能同时收两个 bit整帧时钟数从 128 减到 64读取时间接近减半。实现方法是把原来的单 bit 读取换成两个引脚并行读取for (ch 0; ch 4; ch) { uint16_t wordA 0; uint16_t wordB 0; for (bit 0; bit 16; bit) { wordA 1; wordB 1; if (AD7606_READ_DOUTA()) { wordA | 1; } if (AD7606_READ_DOUTB()) { wordB | 1; } AD7606_SCLK_HIGH(); AD7606_SCLK_LOW(); } adcValues[ch] wordA; adcValues[ch 4] wordB; }这需要 PCB 上多引一根 DOUTB同时把 AD7606 的 PAR/SER、BYTE 引脚配置到对应模式。软件层面要注意的是DOUTA 和 DOUTB 的时序相位完全一致两个 IF 判断之间存在几纳秒的时间差如果系统时钟不够快、数据建立时间也不足可能出现某一根线上的对应位采样提前表现为特定通道的 LSB 变化不同步。遇到这种问题把两个读取判断合并成一个 16 位变量读取再分别判断 bit可以缩小采样时间差。验证提速是否可靠最直接的办法是在 CS 拉低前后翻转另一个空闲 GPIO用逻辑分析仪量 CS 低电平持续了多少时间单线和双线的耗时差就应该接近 64 个 SCLK 周期。本文还有配套的精品资源点击获取
返回列表