前言

AD7606 是工业采集、电源监控、运动控制和多通道数据采集系统中常见的一颗同步采样 ADC。它支持 8/6/4 通道同步采样,分辨率为 16 位,输入范围支持 ±10V 和 ±5V,内部集成输入钳位、1MΩ 模拟输入阻抗、二阶抗混叠滤波器、2.5V 基准、过采样数字滤波器,并支持高速串行和并行接口。官方资料也明确说明,AD7606 支持 SPI/QSPI/MICROWIRE/DSP 兼容的串行接口,最高可实现所有通道 200kSPS 的采样吞吐率。(模拟器件)

在 STM32 项目中,AD7606 可以使用并行接口读取,也可以使用串行接口读取。并行接口速度快,但占用 IO 多;串行接口占用 IO 少,连接更简单,更适合多数中低速工业控制场景。本文以 STM32 通过 GPIO 或 SPI 思路串行读取 AD7606 为主线,从硬件连接、关键时序、数据格式、APP/BSP 软件分层几个方面完整说明。


一、AD7606 串行驱动的硬件连接

STM32 串行驱动 AD7606 时,核心信号可以分为五类:

信号类型 典型引脚 作用
转换控制 CONVST_A、CONVST_B 启动 ADC 同步转换
转换状态 BUSY 判断转换是否正在进行
串行读取 CS、RD/SCLK、DOUTA、DOUTB 读取转换结果
模式配置 RANGE、OS0、OS1、OS2、PAR/SER/BYTE SEL 配置量程、过采样、接口模式
系统控制 RESET、STBY、REF_SELECT 复位、待机、基准选择

硬件连接示意如下:

在这里插入图片描述

AD7606 采用单 5V 模拟供电,同时提供 VDRIVE 引脚用于匹配数字接口电平。若 STM32 使用 3.3V IO,通常应使 VDRIVE 也工作在 3.3V 逻辑域。AD7606 产品资料中给出的 VDRIVE 范围为 2.3V 至 5V,因此可以兼容常见 MCU 电平。(模拟器件)


二、硬件设计重点

1. 串行模式选择

AD7606 支持灵活的并行/串行接口。若采用串行读取方式,PAR/SER/BYTE SEL 必须配置为串行模式。否则,STM32 即使产生了 CSSCLK,也无法从 DOUTA 得到正确的串行数据。

串行模式下,一般使用以下信号完成读数:

CS      :片选信号
RD/SCLK :串行时钟
DOUTA   :串行数据输出 A
DOUTB   :串行数据输出 B,可选

如果只使用 DOUTA,所有通道数据会依次从 DOUTA 移出;如果同时使用 DOUTADOUTB,可以提高读取效率。


2. 量程选择 RANGE

AD7606 支持两种真双极性输入范围:

RANGE = 1:±10V
RANGE = 0:±5V

具体电平逻辑应以所用芯片手册和原理图为准。工程调试时,应先明确系统前端输入范围。例如电源模块、模拟量采集模块中常见的是 ±10V 或 0~10V。若硬件配置为 ±10V,而上位机只关心 0~10V,则软件协议层还需要做额外映射。


3. 过采样 OS0~OS2

AD7606 提供硬件引脚配置过采样倍率。过采样可以降低噪声、提高稳定性,但会降低有效带宽和采样响应速度。官方资料中也说明,AD7606 内部带有可由引脚驱动的数字滤波功能,可提升 SNR 并降低 3dB 带宽。(模拟器件)

常见调试建议:

初始调试:关闭过采样
噪声较大:再逐步开启 2 倍、4 倍或更高过采样
响应要求高:不要盲目开启高倍率过采样

4. CONVST_A 和 CONVST_B

AD7606 是同步采样 ADC,转换由 CONVST_ACONVST_B 触发。对于多通道同步采样,工程上常见做法是让 CONVST_ACONVST_B 同时动作,或者在硬件上将两者短接。

推荐原则:

需要全部通道同步采样:CONVST_A 和 CONVST_B 同步触发
只采部分通道:根据具体芯片型号和通道分组处理
不确定硬件连接:优先通过示波器确认 CONVST_A/B 实际波形

5. 空载输入不等于 0V

AD7606 的模拟输入阻抗为 1MΩ,属于高阻输入。输入端悬空时,通道电压不会自然等于 0V,而是容易受到漏电流、PCB 杂散、电磁干扰、前级电路状态等影响。官方资料明确列出 AD7606 具有 1MΩ 模拟输入阻抗。(模拟器件)

因此,空载时不同通道读数不同,并不能直接判定 ADC 异常。正确验证方式是:

1. 将输入端短接到对应模拟地
2. 观察原始码是否接近 0
3. 再接入标准电压源验证线性关系

若产品要求未接线时显示稳定 0,可在模拟前端增加合适阻值的下拉电阻,例如 100kΩ、470kΩ 或 1MΩ,具体取值应结合输入阻抗、采样精度和前级电路决定。


三、AD7606 串行读取时序

一次完整采样通常包含以下步骤:

1. 产生 CONVST 上升沿,启动转换
2. BUSY 拉高,表示 AD7606 正在转换
3. 等待 BUSY 拉低,表示转换完成
4. 拉低 CS
5. 输出 SCLK
6. 从 DOUTA 或 DOUTB 读取数据
7. 读完后拉高 CS

推荐采样流程如下:

在这里插入图片描述

在调试阶段,推荐采用“先启动转换、等待 BUSY 完成、再读取数据”的方式。该方式逻辑最直观,便于示波器和逻辑分析仪对照波形。

稳定后,也可以采用流水线方式:

第 N 次周期:读取第 N-1 次转换结果,同时启动第 N 次转换
第 N+1 次周期:读取第 N 次转换结果,同时启动第 N+1 次转换

流水线方式效率更高,但要求初始化阶段已经先完成一次转换触发,否则第一次读取可能是无效数据。


四、STM32 软件结构设计

推荐的软件架构如下:

main.c
  └── 系统主循环

app.c
  ├── 周期任务调度
  ├── 1ms / 10ms / 采样周期任务
  └── 系统状态管理

app_ad7606.c
  ├── AD7606 采样任务
  ├── 电压/电流模式切换
  ├── 数据格式转换
  ├── 滤波与标定
  └── 对外协议数据更新

bsp_ad7606.c
  ├── AD7606 初始化
  ├── AD7606 复位
  ├── AD7606 启动转换
  ├── AD7606 等待 BUSY
  └── AD7606 串行读数据

这种结构的核心思想是:BSP 只负责芯片时序,APP 负责业务逻辑。


五、为什么要区分 APP 层和 BSP 层?

1. BSP 层保持干净

BSP 层只需要关心 AD7606 本身:

复位时序
CONVST 触发
BUSY 等待
CS/SCLK/DOUT 读取
基础 GPIO 控制

BSP 层不应该关心客户协议、EtherCAT 对象字典、0~10V 映射、电流/电压模式切换等业务逻辑。

这样做的好处是:后续即使把 GPIO 模拟时序改成硬件 SPI,APP 层也不需要大改。


2. APP 层负责业务逻辑

APP 层更适合处理:

采样周期管理
通道模式管理
电压/电流输入切换
原始码转换
滤波处理
异常判断
协议上传
客户数据格式适配

例如客户要求输出 uint16_t,这不应该影响底层 AD7606 驱动。底层仍然读取 AD7606 原始 16 位数据;APP 层再根据协议要求转换成客户需要的数据格式。


3. 更容易定位问题

分层后,调试问题可以快速归类:

读数完全不变化:优先查 BSP 时序和硬件连接
BUSY 没有变化:优先查 CONVST、RESET、供电和模式脚
接标准电压源正常,空载乱跳:优先查模拟输入悬空
本地读数正确,上位机显示异常:优先查 APP 层数据编码
电压值有固定偏差:优先查标定和量程换算

六、AD7606 数据格式:补码与无符号输出

AD7606 是双极性 ADC,输出编码为二进制补码。官方数据资料中列出其输出编码为 two’s complement。(模拟器件)

这意味着,原始 16 位数据不能简单按普通无符号数理解。

以 ±10V 量程为例:

AD7606 原始码 按无符号数看 按补码理解 电压含义
0x8000 32768 -32768 接近 -10V
0xFFFF 65535 -1 接近 0V,略小于 0
0x0000 0 0 约 0V
0x0001 1 +1 接近 0V,略大于 0
0x7FFF 32767 +32767 接近 +10V

因此,当原始数据出现 0xFFFF 时,它并不代表正满量程,而是补码意义下的 -1,也就是非常接近 0V。


可以。建议博客里补上**“示范代码”**,但不要写成项目源码复盘,而是写成“新手可快速移植的最小驱动模板”。下面这一段可以直接插入到博客的软件章节中。

配图仍可使用:

图1:STM32 串行驱动 AD7606 硬件连接框图
图2:AD7606 推荐软件采样流程图


七、STM32 串行驱动 AD7606 示范代码

下面给出一份基于 STM32 HAL GPIO 模拟串行时序 的 AD7606 最小驱动示范代码。该代码重点用于快速调试 CONVSTBUSYCSSCLKDOUTA 这几根关键信号。

AD7606 支持 8/6/4 通道同步采样,输入范围支持 ±10V 和 ±5V,数字接口支持灵活的并行/串行方式,并兼容 SPI/QSPI/MICROWIRE/DSP 串行接口;其模拟输入阻抗为 1MΩ,VDRIVE 支持 2.3V 到 5.25V 逻辑电平范围。(Analog Devices)

7.1 文件结构建议

Core
├── Inc
│   ├── bsp_ad7606.h
│   └── app_ad7606.h
│
└── Src
    ├── bsp_ad7606.c
    └── app_ad7606.c

推荐分层方式如下:

bsp_ad7606.c:只处理 AD7606 底层时序
app_ad7606.c:处理采样流程、数据转换、协议输出
main.c/app.c :周期调用 APP 层采样函数

这种结构的好处是:底层驱动保持干净,后续即使从 GPIO 模拟时序改成硬件 SPI,APP 层业务逻辑也不需要大改。


1. BSP 层:AD7606 底层驱动

1.1 bsp_ad7606.h

下面的宏名采用 CubeMX 常见命名方式。实际移植时,只需要在 CubeMX 中配置对应 GPIO,然后让宏名与工程中的引脚名称保持一致。

#ifndef __BSP_AD7606_H__
#define __BSP_AD7606_H__

#include "main.h"
#include <stdint.h>

#define AD7606_CHANNEL_COUNT        4U

/*
 * 根据硬件原理图修改以下 GPIO 名称。
 * 推荐在 CubeMX 中把引脚命名为:
 * AD7606_CS
 * AD7606_SCLK
 * AD7606_DOUTA
 * AD7606_BUSY
 * AD7606_CONVSTA
 * AD7606_CONVSTB
 * AD7606_RESET
 * AD7606_STBY
 * AD7606_RANGE
 * AD7606_OS0
 * AD7606_OS1
 * AD7606_OS2
 * AD7606_SER
 * AD7606_REF_SELECT
 */

#define AD7606_CS_LOW()          HAL_GPIO_WritePin(AD7606_CS_GPIO_Port,     AD7606_CS_Pin,     GPIO_PIN_RESET)
#define AD7606_CS_HIGH()         HAL_GPIO_WritePin(AD7606_CS_GPIO_Port,     AD7606_CS_Pin,     GPIO_PIN_SET)

#define AD7606_SCLK_LOW()        HAL_GPIO_WritePin(AD7606_SCLK_GPIO_Port,   AD7606_SCLK_Pin,   GPIO_PIN_RESET)
#define AD7606_SCLK_HIGH()       HAL_GPIO_WritePin(AD7606_SCLK_GPIO_Port,   AD7606_SCLK_Pin,   GPIO_PIN_SET)

#define AD7606_CONVSTA_LOW()     HAL_GPIO_WritePin(AD7606_CONVSTA_GPIO_Port, AD7606_CONVSTA_Pin, GPIO_PIN_RESET)
#define AD7606_CONVSTA_HIGH()    HAL_GPIO_WritePin(AD7606_CONVSTA_GPIO_Port, AD7606_CONVSTA_Pin, GPIO_PIN_SET)

#define AD7606_CONVSTB_LOW()     HAL_GPIO_WritePin(AD7606_CONVSTB_GPIO_Port, AD7606_CONVSTB_Pin, GPIO_PIN_RESET)
#define AD7606_CONVSTB_HIGH()    HAL_GPIO_WritePin(AD7606_CONVSTB_GPIO_Port, AD7606_CONVSTB_Pin, GPIO_PIN_SET)

#define AD7606_RESET_LOW()       HAL_GPIO_WritePin(AD7606_RESET_GPIO_Port,  AD7606_RESET_Pin,  GPIO_PIN_RESET)
#define AD7606_RESET_HIGH()      HAL_GPIO_WritePin(AD7606_RESET_GPIO_Port,  AD7606_RESET_Pin,  GPIO_PIN_SET)

#define AD7606_STBY_HIGH()       HAL_GPIO_WritePin(AD7606_STBY_GPIO_Port,   AD7606_STBY_Pin,   GPIO_PIN_SET)

#define AD7606_RANGE_10V()       HAL_GPIO_WritePin(AD7606_RANGE_GPIO_Port,  AD7606_RANGE_Pin,  GPIO_PIN_SET)
#define AD7606_RANGE_5V()        HAL_GPIO_WritePin(AD7606_RANGE_GPIO_Port,  AD7606_RANGE_Pin,  GPIO_PIN_RESET)

#define AD7606_SER_MODE()        HAL_GPIO_WritePin(AD7606_SER_GPIO_Port,    AD7606_SER_Pin,    GPIO_PIN_SET)

#define AD7606_REF_EXTERNAL()    HAL_GPIO_WritePin(AD7606_REF_SELECT_GPIO_Port, AD7606_REF_SELECT_Pin, GPIO_PIN_RESET)
#define AD7606_REF_INTERNAL()    HAL_GPIO_WritePin(AD7606_REF_SELECT_GPIO_Port, AD7606_REF_SELECT_Pin, GPIO_PIN_SET)

#define AD7606_OS0_LOW()         HAL_GPIO_WritePin(AD7606_OS0_GPIO_Port,    AD7606_OS0_Pin,    GPIO_PIN_RESET)
#define AD7606_OS1_LOW()         HAL_GPIO_WritePin(AD7606_OS1_GPIO_Port,    AD7606_OS1_Pin,    GPIO_PIN_RESET)
#define AD7606_OS2_LOW()         HAL_GPIO_WritePin(AD7606_OS2_GPIO_Port,    AD7606_OS2_Pin,    GPIO_PIN_RESET)

#define AD7606_BUSY_READ()       HAL_GPIO_ReadPin(AD7606_BUSY_GPIO_Port,    AD7606_BUSY_Pin)
#define AD7606_DOUTA_READ()      HAL_GPIO_ReadPin(AD7606_DOUTA_GPIO_Port,   AD7606_DOUTA_Pin)

typedef enum
{
    AD7606_OK = 0,
    AD7606_ERROR_BUSY_START_TIMEOUT,
    AD7606_ERROR_BUSY_DONE_TIMEOUT,
    AD7606_ERROR_NULL_POINTER
} ad7606_status_t;

void AD7606_Init(void);
void AD7606_Reset(void);
void AD7606_StartConvst(void);
ad7606_status_t AD7606_WaitBusyFinish(uint32_t start_timeout_us, uint32_t done_timeout_us);
ad7606_status_t AD7606_ReadRaw(uint16_t *raw_buf, uint8_t channel_count);

#endif

1.2 bsp_ad7606.c

下面是 GPIO 模拟串行方式的底层示范代码。为了让新手更容易调通,时序延时写得比较保守。正式产品中,可以根据示波器实测结果进一步优化。

#include "bsp_ad7606.h"

static void AD7606_DelayShort(volatile uint32_t n)
{
    while (n--)
    {
        __NOP();
    }
}

static void AD7606_DelayUs(uint32_t us)
{
    /*
     * 简单阻塞延时,适合调试。
     * 不同主频下需要根据示波器微调。
     * 正式项目建议改成 DWT 或硬件定时器延时。
     */
    while (us--)
    {
        AD7606_DelayShort(80);
    }
}

void AD7606_Reset(void)
{
    AD7606_RESET_LOW();
    AD7606_DelayUs(1);

    AD7606_RESET_HIGH();
    AD7606_DelayUs(2);

    AD7606_RESET_LOW();
    AD7606_DelayUs(1);
}

void AD7606_StartConvst(void)
{
    /*
     * 如果硬件已经将 CONVST_A 和 CONVST_B 短接,
     * 只驱动其中一路也可以。
     * 为了通用性,示范代码同时驱动 A/B。
     */
    AD7606_CONVSTA_LOW();
    AD7606_CONVSTB_LOW();

    AD7606_DelayShort(100);

    AD7606_CONVSTA_HIGH();
    AD7606_CONVSTB_HIGH();
}

void AD7606_Init(void)
{
    /*
     * 1. 选择参考源
     * 外部基准:AD7606_REF_EXTERNAL()
     * 内部基准:AD7606_REF_INTERNAL()
     */
    AD7606_REF_EXTERNAL();

    /*
     * 2. 选择输入量程
     * RANGE = 1:±10V
     * RANGE = 0:±5V
     */
    AD7606_RANGE_10V();

    /*
     * 3. 选择串行模式
     */
    AD7606_SER_MODE();

    /*
     * 4. 关闭过采样:OS2 OS1 OS0 = 000
     */
    AD7606_OS0_LOW();
    AD7606_OS1_LOW();
    AD7606_OS2_LOW();

    /*
     * 5. 默认状态
     */
    AD7606_CS_HIGH();
    AD7606_SCLK_HIGH();
    AD7606_CONVSTA_HIGH();
    AD7606_CONVSTB_HIGH();
    AD7606_STBY_HIGH();

    /*
     * 6. 复位芯片
     */
    AD7606_Reset();

    /*
     * 7. 先启动一次转换。
     * 如果后续采用“读上一次,启动下一次”的流水线模式,
     * 初始化阶段需要先触发一次转换。
     */
    AD7606_StartConvst();
}

ad7606_status_t AD7606_WaitBusyFinish(uint32_t start_timeout_us, uint32_t done_timeout_us)
{
    /*
     * 等待 BUSY 拉高,说明转换开始。
     * 如果采用的是“读上一次数据”的流水线方式,
     * 也可以只等待 BUSY 为低后读取。
     */
    while ((AD7606_BUSY_READ() == GPIO_PIN_RESET) && (start_timeout_us > 0U))
    {
        start_timeout_us--;
        AD7606_DelayUs(1);
    }

    if (start_timeout_us == 0U)
    {
        return AD7606_ERROR_BUSY_START_TIMEOUT;
    }

    /*
     * 等待 BUSY 拉低,说明转换完成。
     */
    while ((AD7606_BUSY_READ() == GPIO_PIN_SET) && (done_timeout_us > 0U))
    {
        done_timeout_us--;
        AD7606_DelayUs(1);
    }

    if (done_timeout_us == 0U)
    {
        return AD7606_ERROR_BUSY_DONE_TIMEOUT;
    }

    return AD7606_OK;
}

ad7606_status_t AD7606_ReadRaw(uint16_t *raw_buf, uint8_t channel_count)
{
    uint8_t ch;
    uint8_t bit;

    if (raw_buf == 0)
    {
        return AD7606_ERROR_NULL_POINTER;
    }

    /*
     * 调试阶段推荐 CS 拉低后连续读取所有通道。
     * 这样更容易确认通道顺序和串行时序。
     */
    AD7606_CS_LOW();
    AD7606_DelayShort(20);

    for (ch = 0; ch < channel_count; ch++)
    {
        uint16_t data = 0;

        for (bit = 0; bit < 16; bit++)
        {
            AD7606_SCLK_LOW();
            AD7606_DelayShort(30);

            data <<= 1;

            if (AD7606_DOUTA_READ() == GPIO_PIN_SET)
            {
                data |= 1U;
            }

            AD7606_SCLK_HIGH();
            AD7606_DelayShort(40);
        }

        raw_buf[ch] = data;
    }

    AD7606_CS_HIGH();

    return AD7606_OK;
}

2. APP 层:采样流程与数据转换

2.1 app_ad7606.h

#ifndef __APP_AD7606_H__
#define __APP_AD7606_H__

#include "main.h"
#include <stdint.h>

#define APP_AD7606_CHANNEL_COUNT        4U

typedef struct
{
    uint16_t raw_code[APP_AD7606_CHANNEL_COUNT];
    int16_t  signed_code[APP_AD7606_CHANNEL_COUNT];
    uint16_t offset_code[APP_AD7606_CHANNEL_COUNT];
    float    voltage[APP_AD7606_CHANNEL_COUNT];
} app_ad7606_data_t;

void APP_AD7606_Init(void);
void APP_AD7606_Task(void);

const app_ad7606_data_t *APP_AD7606_GetData(void);

#endif

2.2 app_ad7606.c

APP 层不直接关心底层 GPIO 如何翻转,只关心“什么时候采样、如何解释数据、如何给上层使用”。

AD7606 的输出编码是 two’s complement(二进制补码),因此 0xFFFF 不是正满量程,而是补码意义下的 -1,代表接近 0V 的负方向 1LSB。官方数据手册中也列出 AD7606 输出编码为 Twos complement。(Analog Devices)

#include "app_ad7606.h"
#include "bsp_ad7606.h"

#define AD7606_BUSY_START_TIMEOUT_US    20U
#define AD7606_BUSY_DONE_TIMEOUT_US     100U

/*
 * ±10V 量程:
 * FSR = 20V
 * LSB = 20 / 65536 V
 */
#define AD7606_BIPOLAR_10V_LSB          (20.0f / 65536.0f)

static app_ad7606_data_t s_ad7606_data;

static int16_t AD7606_RawToSigned(uint16_t raw)
{
    /*
     * AD7606 原始输出为 16bit 补码。
     * 直接强转 int16_t 即可得到有符号码值。
     */
    return (int16_t)raw;
}

static uint16_t AD7606_RawToOffsetBinary(uint16_t raw)
{
    /*
     * 将 AD7606 补码转换为偏移二进制。
     *
     * 原始补码:
     * 0x8000 ≈ -FS
     * 0x0000 ≈ 0V
     * 0x7FFF ≈ +FS
     *
     * 偏移二进制:
     * 0x0000 ≈ -FS
     * 0x8000 ≈ 0V
     * 0xFFFF ≈ +FS
     */
    return (uint16_t)(raw ^ 0x8000U);
}

static float AD7606_SignedToVoltage_10V(int16_t signed_code)
{
    return ((float)signed_code) * AD7606_BIPOLAR_10V_LSB;
}

void APP_AD7606_Init(void)
{
    for (uint8_t i = 0; i < APP_AD7606_CHANNEL_COUNT; i++)
    {
        s_ad7606_data.raw_code[i]    = 0;
        s_ad7606_data.signed_code[i] = 0;
        s_ad7606_data.offset_code[i] = 0;
        s_ad7606_data.voltage[i]     = 0.0f;
    }

    AD7606_Init();
}

void APP_AD7606_Task(void)
{
    ad7606_status_t status;

    /*
     * 调试阶段推荐:
     * 1. 启动转换
     * 2. 等待 BUSY 完成
     * 3. 读取数据
     */
    AD7606_StartConvst();

    status = AD7606_WaitBusyFinish(AD7606_BUSY_START_TIMEOUT_US,
                                   AD7606_BUSY_DONE_TIMEOUT_US);

    if (status != AD7606_OK)
    {
        /*
         * 可在此处添加错误计数或日志输出。
         * 例如:
         * AD7606_ERROR_BUSY_START_TIMEOUT:CONVST 可能没有触发成功
         * AD7606_ERROR_BUSY_DONE_TIMEOUT :BUSY 一直未结束
         */
        return;
    }

    status = AD7606_ReadRaw(s_ad7606_data.raw_code, APP_AD7606_CHANNEL_COUNT);

    if (status != AD7606_OK)
    {
        return;
    }

    /*
     * 将原始码转换成不同形式,方便调试和协议输出。
     */
    for (uint8_t ch = 0; ch < APP_AD7606_CHANNEL_COUNT; ch++)
    {
        s_ad7606_data.signed_code[ch] = AD7606_RawToSigned(s_ad7606_data.raw_code[ch]);
        s_ad7606_data.offset_code[ch] = AD7606_RawToOffsetBinary(s_ad7606_data.raw_code[ch]);
        s_ad7606_data.voltage[ch]     = AD7606_SignedToVoltage_10V(s_ad7606_data.signed_code[ch]);
    }
}

const app_ad7606_data_t *APP_AD7606_GetData(void)
{
    return &s_ad7606_data;
}

3. main.c 中的最小调用示范

对于刚开始调试的项目,可以先不用复杂定时任务,直接在主循环中每隔一段时间采样一次。

#include "main.h"
#include "app_ad7606.h"
#include <stdio.h>

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();

    APP_AD7606_Init();

    while (1)
    {
        const app_ad7606_data_t *adc;

        APP_AD7606_Task();

        adc = APP_AD7606_GetData();

        printf("CH1 raw=0x%04X, signed=%d, offset=%u, voltage=%.4f V\r\n",
               adc->raw_code[0],
               adc->signed_code[0],
               adc->offset_code[0],
               adc->voltage[0]);

        printf("CH2 raw=0x%04X, signed=%d, offset=%u, voltage=%.4f V\r\n",
               adc->raw_code[1],
               adc->signed_code[1],
               adc->offset_code[1],
               adc->voltage[1]);

        printf("CH3 raw=0x%04X, signed=%d, offset=%u, voltage=%.4f V\r\n",
               adc->raw_code[2],
               adc->signed_code[2],
               adc->offset_code[2],
               adc->voltage[2]);

        printf("CH4 raw=0x%04X, signed=%d, offset=%u, voltage=%.4f V\r\n\r\n",
               adc->raw_code[3],
               adc->signed_code[3],
               adc->offset_code[3],
               adc->voltage[3]);

        HAL_Delay(500);
    }
}

调试时建议同时打印四种值:

raw_code    :AD7606 原始 16bit 数据
signed_code :按 int16_t 解释后的补码值
offset_code :转换后的 uint16_t 偏移二进制值
voltage     :换算后的电压值

这样可以避免把 0xFFFF 误判成正满量程。对于 AD7606 原始补码来说,0xFFFF 实际上是 -1,非常接近 0V。


4. 如果只采集 0~10V

有些工业产品虽然使用 AD7606 的 ±10V 量程,但业务上只采集 0~10V。这种情况下,也可以把 0~10V 映射成 0~65535:

static uint16_t AD7606_ToUint16_0To10V(uint16_t raw)
{
    int32_t signed_code;

    signed_code = (int16_t)raw;

    if (signed_code <= 0)
    {
        return 0;
    }

    if (signed_code >= 32767)
    {
        return 65535;
    }

    return (uint16_t)((signed_code * 65535L) / 32767L);
}

这种方式的协议含义是:

0      ≈ 0V
65535  ≈ +10V
负电压钳位为 0

如果系统未来需要支持负电压,不推荐这种方式;如果产品明确只测 0~10V,这种方式对客户最直观。


八、推荐调试步骤

1. 先确认硬件模式

重点检查:

5V 模拟供电是否正常
VDRIVE 是否匹配 MCU IO 电平
REF_SELECT 与实际基准电路是否一致
RANGE 是否符合目标量程
PAR/SER/BYTE SEL 是否为串行模式
OS0~OS2 是否处于预期过采样配置
RESET 是否有正确释放
CONVST_A/B 是否有触发脉冲
BUSY 是否有高低变化

2. 再确认数字时序

建议用示波器或逻辑分析仪同时观察:

CONVST_A
CONVST_B
BUSY
CS
SCLK
DOUTA
FRSTDATA

正常情况下,CONVST 触发后,BUSY 应先拉高再拉低;BUSY 结束后,CS 拉低,SCLK 输出固定数量脉冲,DOUTA 依次输出通道数据。


3. 最后确认模拟输入

不要用悬空输入判断 ADC 是否准确。

推荐测试顺序:

输入短接 AGND,验证 0V 附近读数
输入 1V,验证正向线性
输入 5V,验证中间点
输入 10V,验证满量程附近
如果支持负电压,再输入 -1V、-5V、-10V 验证补码关系

九、常见问题总结

问题 1:所有通道读数一样

优先检查:

串行/并行模式脚是否配置正确
DOUTA 是否接错
CS/SCLK 时序是否正确
CONVST_A/B 是否都被正确触发
通道读取数量是否与芯片型号一致

问题 2:空载时通道读数不同

这是高阻模拟输入悬空后的常见现象。AD7606 输入阻抗为 1MΩ,未接信号源时输入点没有确定电位。应通过短接 AGND 或加下拉电阻验证。(模拟器件)


问题 3:0V 附近出现 65535

这是二进制补码显示导致的误解。65535 = 0xFFFF,在 int16_t 补码含义下是 -1,代表接近 0V 的负方向 1LSB。


问题 4:客户要 uint16_t,但 AD7606 是补码怎么办?

推荐在协议层明确编码方式:

如果保留原始数据:UINT16 + two’s complement
如果希望客户直观看数:UINT16 + offset binary
如果只测 0~10V:UINT16 + 0~10V 线性映射

十、结语

STM32 串行驱动 AD7606 的核心不是代码量,而是对硬件模式、转换时序和数据编码的理解。

硬件上要确认供电、参考、量程、串行模式、CONVST/BUSY 时序;软件上要把 BSP 和 APP 分开,BSP 只处理 AD7606 底层时序,APP 层负责采样调度、模式切换、滤波标定和客户协议输出。

对于工业项目,推荐采用如下结构:

BSP 层:负责芯片驱动
APP 层:负责业务处理
协议层:负责数据定义和上传

这样做的好处是清晰、稳定、可维护。后续无论是从 GPIO 模拟串行改为硬件 SPI,还是增加 DMA、EtherCAT 上传、滤波标定、电压/电流模式切换,都不会破坏整体软件结构。

十一、看到结尾的奖励

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐