做配电终端项目那阵子,要在主控板上同时采 16 路交流模拟量,电压、电流都要进,还得支持软件切量程、留出同步采样的余地。当时对比过好几颗 ADC,最后定了 AD7616,MCU 选了 GD32F303。原因很直接:AD7616 一颗片子就能覆盖 16 路双极性输入、16bit 分辨率,最高 1MSPS 采样率,内部基准和输入调理也省了不少外围;GD32 在同等外设资源下成本更友好,SPI+DMA 这套组合做数据采集也足够稳。这篇文章把我在这个平台上的驱动思路、关键寄存器配置、GD32 外设初始化和调试中踩过的坑都整理出来。不管是从 STM32 转到 GD32 的工程师,还是第一次碰 AD7616 这类 SAR ADC 的,按这条思路走一遍,大概率能少走很多弯路。
1. 方案选型与硬件连接
1.1 为什么是 AD7616 加 GD32
先说项目需求。我要采的是多路工频交流信号,电压、电流加在一起超过 8 路,如果用 AD7606 这种 8 通道片子就得放两片,不仅面积变大,片间同步也要额外处理。AD7616 是真正的 16 通道双极性输入 ADC,一组 8 通道输入可以用 CONVST A 触发,另一组用 CONVST B 触发,也可以把两组拼起来用,非常适合多回线监测、配电终端这类场景。
AD7616 的几个关键点放在项目里非常讨喜:
- 16 通道差分/单端灵活配置,每一路由软件或硬件选择 ±10V、±5V、±2.5V 量程;
- 16bit 无失码,最高 1MSPS 吞吐率;
- 内置 2.5V 基准,也可以外部输入基准,省掉高精度基准芯片选型;
- 数据输出支持串行 SPI 和并行接口,串行模式下有 SDOA、SDOB 两个输出,可以降低 SPI 时钟压力;
- 内置过采样和数字滤波器,对电力现场的噪声有一定抑制能力。
GD32 这边的优势就更明显了。同样主频和 Flash 容量,GD32F303 的成本比同级芯片低一截,而且 SPI、DMA、定时器资源都不缺,GPIO 兼容 5V 输入,和 AD7616 这类 3.3V/5V 混供电的模拟器件对接也方便。实际项目里 GD32F303 跑 120MHz,SPI 时钟配到 9MHz 左右,配合 DMA 连续采集 16 通道完全跑得动。
有一点要注意,GD32 虽然和 STM32 的引脚兼容性做得不错,但外设库函数、寄存器位定义并不完全一致。直接拿 STM32 的工程改芯片型号这种事我试过,看起来能编译,跑起来就是各种玄学问题。老老实实按 GD32 标准外设库来写,后面会省心很多。
1.2 关键引脚连接建议
我项目里的连接方式如下,板子是自己画的,MCU 是 GD32F303VET6,ADC 是 AD7616BSTZ,全部用 SPI 串行模式。
| AD7616 引脚 | 功能 | 接到 GD32F303 | 说明 |
|---|---|---|---|
| CS | 片选 | PB12 | 普通 GPIO,软件控制 |
| SCLK | 串行时钟 | PB13 | SPI1_SCK,复用模式 |
| SDI | 配置/命令输入 | PB15 | SPI1_MOSI |
| SDOA | A 组数据输出 | PB14 | SPI1_MISO |
| SDOB | B 组数据输出 | 暂不接或接 PB3 | 单 SDO 模式可不接 |
| BUSY | 转换结束指示 | PB4 | 配成外部中断输入,下降沿触发 |
| RESET | 复位 | PB5 | 普通 GPIO |
| CONVST A | A 组转换启动 | PB6 | 定时器输出或 GPIO 脉冲 |
| CONVST B | B 组转换启动 | PB7 | 可并到 CONVST A |
| SER/PAR | 串行/并行选择 | 接 GND | 低电平选串行 |
| H/S | 硬件/软件配置 | 接 3.3V | 高电平选软件配置 |
| VDRIVE | SDO 电平参考 | 接 3.3V | 决定 SPI 输出电平 |
| REFIN/REFOUT | 基准输入输出 | 2.5V | 使用内部基准时加去耦电容 |
这里最容易忽略的是 VDRIVE。它决定 SDOA/SDOB 输出的高电平是多少,必须和 MCU 的 IO 电平匹配。我用的是 3.3V MCU,所以 VDRIVE 接 3.3V,否则可能出现 MCU 读不到高电平,或者长期工作损伤 IO 的问题。
模拟电源 AVCC 我用了 5V,DVCC 用 3.3V。AVCC 从系统 5V 经过 LC 滤波进来,L 选 10uH 功率电感,C 用 10uF + 0.1uF 组合。REFCAP、REGCAPA、REGCAPD 这些引脚旁边必须放高质量陶瓷电容,位置尽量靠近引脚,这在 SAR ADC 布线上是老规矩了,不要省。
1.3 开发环境与调试工具
项目用的 IDE 是 KEIL MDK,也测试过 GD32 Embedded Builder,两个都能编译 GD32F303 的工程。如果刚开始接触 GD32,我建议直接去官网下对应型号的固件库和 Pack,KEIL 里把 Device 型号选对,否则后面调试时会遇到 JTAG/SWD 无法连接或者 internal command error 这类问题。
调试器我这边用了 STLINK 和 JLink 两种。STLINK 接 GD32 需要确认 SWD 驱动正常,在 KEIL 里选择 CMSIS-DAP Debugger 或 ST-Link Debugger,再设置好 SW 模式;JLink 烧录 GD32 时要注意 DLL 版本和芯片型号选择,如果提示无法识别内核,最好用 JLink 官方软件更新一下驱动。
串口打印用的 CH340 和 CP2102 转接板手上都有,常规做法是预留一个 UART,把采集到的裸值和换算后的电压值定时往 PC 发,用来做数据验证。这个简单手段在驱动调试阶段比仿真器单步还实用。
2. AD7616 工作模式和寄存器配置要点
2.1 先搞清楚硬件模式还是软件模式
AD7616 的 H/S 引脚直接决定你是用硬件跳线配置还是用寄存器配置。硬件模式下,量程、通道选择这些靠外部引脚电平决定,好处是驱动简单,上电就能跑,适合前期验证链路;缺点是不够灵活,改量程得动板子。
软件模式下,量程、通道选择、过采样等全部通过 SPI 写寄存器实现,项目后期维护方便,远程切换量程也容易。我项目里 H/S 接高电平,走软件配置模式。
还有一点,AD7616 支持串行和并行两种接口,SER/PAR 引脚拉低选串行。并行模式虽然吞吐更高,但占用的 IO 太多,GD32 的 GPIO 资源虽然够,但为了给其他外设留位置,我最终没有用并行方式。
2.2 软件模式下的寄存器配置流程
AD7616 的寄存器访问我是按数据手册的串行寄存器映射表做的。项目里用到的寄存器主要是通道选择、量程配置、过采样配置这几个,具体地址和位段定义我以当时用的 AD7616BSTZ 版本 datasheet 为准,换批次或换封装时最好重新核对一遍。
| 寄存器 | 作用 | 项目中的配置 |
|---|---|---|
| 通道选择寄存器 | 决定参与转换的通道序列 | 先配置 16 通道全开 |
| 量程配置寄存器 | 分别设置各路通道量程 | 全部设为 ±10V |
| 过采样/滤波寄存器 | 设置过采样倍数 | 设为 x4,折中采样率和信噪比 |
| 诊断/状态寄存器 | 读回状态位 | 用来校验配置是否生效 |
写寄存器流程我总结成固定套路:复位芯片后延时一小段时间,再写配置,写完必须读回校验。有些工程师图省事,写完寄存器不读回,结果实际量程和预期不一样,排查了半天才发现是寄存器位写错了。
给一段示意代码:
void ad7616_init(void) { ad7616_reset(); delay_us(200); /* 按手册要求进入配置模式 */ ad7616_enter_config_mode(); /* 配置通道和量程 */ ad7616_write_reg(AD7616_REG_CH_SEL, 0xFFFF); /* 16通道全开 */ ad7616_write_reg(AD7616_REG_RANGE_A, 0x0000); /* A组通道量程配置 */ ad7616_write_reg(AD7616_REG_RANGE_B, 0x0000); /* B组通道量程配置 */ ad7616_write_reg(AD7616_REG_FILTER, 0x0002); /* 过采样x4 */ /* 读回校验 */ if (ad7616_read_reg(AD7616_REG_CH_SEL) != 0xFFFF) { while (1); /* 配置失败,停在调试点 */ } }实际项目中读回校验不一定要全卡死,可以打日志。但这个过程必须有,尤其是板子第一次上电调试时,能帮你快速区分是 SPI 通信问题还是寄存器配置问题。
2.3 SPI 数据帧格式与转换结果
AD7616 在串行模式下,每个操作帧本质上是 16bit 为主,SCLK 连续给 16 个脉冲完成一次读写。CS 拉低后,SDI 上送命令或配置字,同时 SDOA 上输出对应的读回数据或转换结果,这就是 SPI 全双工的好处。
SPI 的极性和相位必须对准。AD7616 的串行时序是 SCLK 空闲为高,数据在下降沿由 ADC 输出,MCU 用上升沿采样比较稳妥,所以 GD32 的 SPI 参数我配成 CPOL=1、CPHA=1,对应 GD32 库里的 SPI_CK_PL_HIGH_PH_2EDGE。如果这里配反了,表现就是寄存器读回全 0 或者数据错位,非常容易被误判成硬件故障。
转换结果默认是 16bit 二进制补码。满量程对应关系大概是:
- ±10V 量程:0x7FFF 对应约 +9.9997V,0x8000 对应 -10V;
- ±5V 量程:0x7FFF 对应约 +4.9998V,0x8000 对应 -5V;
- ±2.5V 量程:0x7FFF 对应约 +2.4999V,0x8000 对应 -2.5V。
换算公式统一用:
float voltage = (int16_t)raw_value * full_scale / 32768.0f;注意要把 raw_value 先转成 int16_t 再做除法,否则无符号数除以 32768 出来的结果永远都是正值。
3. GD32 驱动实现细节
3.1 GD32 SPI 和 GPIO 初始化
GD32 的标准外设库和 STM32 SPL 风格很像,但函数名和宏定义有差异。我直接用 GD32 库的写法,SPI1 用 PB13/14/15 三个引脚,复用功能要一起配置。
void bsp_spi1_init(void) { rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_SPI1); gpio_af_set(GPIOB, GPIO_AF_5, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); spi_parameter_struct spi_para; spi_struct_para_init(&spi_para); spi_para.trans_mode = SPI_TRANSMODE_FULLDUPLEX; spi_para.device_mode = SPI_MASTER; spi_para.frame_size = SPI_FRAMESIZE_16BIT; spi_para.clock_polarity = SPI_CK_PL_HIGH_PH_2EDGE; spi_para.prescale = SPI_PSC_8; /* PCLK1 = 60MHz,除以8得到7.5MHz */ spi_para.endian = SPI_ENDIAN_MSB; spi_init(SPI1, &spi_para); spi_enable(SPI1); }这几个参数里最容易错的是 frame_size。默认很多人的库初始化模板是 8bit,如果不清零直接改,后面读写 16bit 命令就会错位。另一个是 prescale,SPI 时钟不是越高越好,AD7616 有最大 SCLK 限制,还要考虑布线长度,一般先配到 7.5MHz 或 9MHz,跑稳定后再往上提。
3.2 AD7616 底层读写函数
AD7616 的寄存器操作和读取转换结果本质上都是 SPI 收发。封装成两个函数,项目其他地方不要直接裸操作 SPI,方便以后换接口。
uint16_t ad7616_spi_xfer(uint16_t tx_data) { uint16_t rx_data = 0; AD7616_CS_LOW(); while (RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_TBE)); spi_i2s_data_transmit(SPI1, tx_data); while (RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_RBNE)); rx_data = spi_i2s_data_receive(SPI1); AD7616_CS_HIGH(); return rx_data; }这里要注意 CS 的时序。有些 SAR ADC 允许 CS 在 16bit 帧内保持低电平,连续读取多个通道,但 AD7616 的寄存器配置阶段我更习惯每帧都完整拉高再拉低,保证边界干净。CS 高电平持续时间太短会导致下一帧数据错位,这个问题在后面调试部分还会提到。
寄存器写函数:
void ad7616_write_reg(uint8_t reg, uint16_t val) { uint16_t cmd = ((uint16_t)reg << 12) | (val & 0x0FFF); /* 具体命令字格式以 datasheet 为准,这里保留位组合示例 */ ad7616_spi_xfer(cmd); }寄存器读函数类似,先发读命令,再读回 SDOA 上的数据。不同芯片版本对命令字位定义可能有差异,所以我的代码里都留了注释,移植时务必对照手册。
3.3 采集主流程:CONVST 触发加 BUSY 判断
我用的采集流程是定时器触发 CONVST,BUSY 下降沿中断通知 MCU,再通过 SPI 把结果读回来。这样做的好处是采样周期完全由定时器控制,不依赖 SPI 读取时间,采样率稳定。
主流程伪代码:
void main_loop(void) { while (1) { if (ad7616_conv_complete) { ad7616_conv_complete = 0; /* 按通道顺序读取结果 */ for (int i = 0; i < AD7616_CH_NUM; i++) { raw[i] = ad7616_read_conversion_result(i); } /* 换算电压,缓存到数组 */ for (int i = 0; i < AD7616_CH_NUM; i++) { volts[i] = (int16_t)raw[i] * 10.0f / 32768.0f; } /* 串口打印或送入上层算法 */ } } }BUSY 中断里只拉高一个标志位,不要在中断里做 SPI 读取和浮点运算,否则会把定时器采样周期打乱,甚至导致中断嵌套冲突。这个习惯对做采集的人来说很重要。
3.4 用 DMA 提升连续采集效率
如果只是低速看波形,轮询 SPI 就够了。但项目里要做连续的电压电流记录,16 通道一路一路轮询会占用大量 CPU 时间。我后来把 SPI 读取改成 DMA 方式,SPI1 的接收请求挂到 DMA1 对应通道上,配合循环模式实现不定长数据的后台搬运。
GD32 的 DMA 配置大致如下:
void bsp_dma_init(void) { rcu_periph_clock_enable(RCU_DMA1); dma_parameter_struct dma_para; dma_deinit(DMA1, DMA_CH2); dma_para.direction = DMA_PERIPHERAL_TO_MEMORY; dma_para.periph_addr = (uint32_t)&SPI_DATA(SPI1); dma_para.memory_addr = (uint32_t)rx_buf; dma_para.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_para.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_para.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; dma_para.memory_width = DMA_MEMORY_WIDTH_16BIT; dma_para.number = BUF_SIZE; dma_para.priority = DMA_PRIORITY_HIGH; dma_init(DMA1, DMA_CH2, &dma_para); dma_circulation_enable(DMA1, DMA_CH2); dma_channel_enable(DMA1, DMA_CH2); }DMA 循环模式是重点。普通模式搬完指定数量的数据就停了,采集场景下还要不断重新开启 DMA,中间可能丢数据。循环模式让 DMA 自动重新装载,配合半传输中断和传输完成中断,可以做成双缓冲,上层在读取一块数据时 DMA 正在写另一块,吞吐量能上来不少。
4. 调试过程与问题排查实录
4.1 读出来全是 0xFFFF 或 0x0000
这是最典型的第一次上板问题。我遇到的场景是:SPI 已经初始化,发出读命令,但 SDOA 返回全 0 或者全 1。
排查步骤按优先级来:
- 用逻辑分析仪抓 CS、SCLK、SDI、SDOA 四根线,看 CS 是否有明显下降沿,SCLK 是否完整给到 16 个脉冲;
- 看 SDOA 在 SCLK 下降沿之后是否有数据变化,如果一直是固定电平,说明 ADC 可能没进串行模式或者 SDOA 引脚虚焊;
- 检查 SER/PAR 是否真的接了低电平,H/S 是否按预期接了高电平,这两个引脚如果悬空,内部上下拉可能导致模式不对;
- 用万用表量 VDRIVE 电压,SDOA 输出高电平不会超过 VDRIVE,如果 VDRIVE 没电,SDOA 永远读不到高电平。
我那次问题的最终原因就是 SER/PAR 引脚虚焊,芯片实际跑在并行模式,SPI 自然读不到数据。重新补焊后一次通过。
4.2 寄存器写不进去,读回全 0
SPI 通信正常,但寄存器写不进,读回来是 0。这种问题多半不是硬件,而是 SPI 帧格式或者时序保持时间不满足。
我总结的检查点:
- RESET 后必须给足够的延时再操作寄存器,我项目里用的是 200us,比手册最小值放宽了几倍;
- 寄存器写命令的位长必须是 16bit,配成 8bit 或者 24bit 都会出错;
- CS 拉高后的保持时间太短,下一帧 CS 拉低时 ADC 还处在上一帧的结尾状态,命令就接收不到;
- 写完寄存器马上读回时,中间加一点延时,不要像普通 SPI EEPROM 那样以为写完立即生效。
这里的本质是 SAR ADC 的串行接口不是简单的移位寄存器,它内部有状态机,CS 的高低电平宽度都会被当成时序条件。遇到这种问题,用一个低速 SPI 时钟先跑,比如 1MHz,排除掉时序余量不足的可能,再慢慢提高时钟。
4.3 数据偶尔跳变,通道错位
数据能出来了,但偶尔跳变,或者通道顺序对不上。这个问题曾经困扰我一个下午。
通道错位大概率是触发和数据读取的次序不对。AD7616 的 16 个通道是按组转换的,CONVST A 触发后,A 组转换结果按固定顺序在 SDOA 上输出;如果读取时没按这个顺序取,或者中间混入了配置阶段的返回数据,通道肯定错位。
跳变问题的检查点:
- 每个采样周期只拉一次 CONVST,不要在 BUSY 还没拉低前又触发下一次;
- BUSY 下降沿中断里只置标志,主循环读到标志后再去读 SPI,避免在转换过程中打断数据输出;
- SPI 时钟频率过高也会导致采样不稳定,尤其是飞线连接时,我最后稳定在 7.5MHz;
- 过采样配置不同,BUSY 高电平时间也不同,如果配置了过采样,等待时间要重新评估。
4.4 采样率达不到预期
16 通道全开,每通道想跑到 50kSPS,但实测只有 20kSPS 左右。这个不用怀疑片子,多半是读取效率太低。
粗略算一下。AD7616 最高 1MSPS,如果 16 通道同时转换,单通道等效采样率就是 1MSPS 除以 16,即 62.5kSPS。但这是在 CONVST 触发、SPI 读取足够快的前提下。实际链路里每次读 16bit,SPI 时钟 7.5MHz,单帧理论只要 2.1us,16 路加上 CS 切换和命令开销,大约需要 60us 左右,对应 16kSPS 左右,和实测接近。
想提高采样率,方向有几个:
- 用双 SDO 模式,SDOA 读 A 组,SDOB 读 B 组,SPI 需要两个 MISO 通道,可以用两路 SPI 或者 GPIO 模拟第二路读取;
- 提高 SPI 时钟到 15MHz 左右,前提是 ADC 手册允许且 PCB 走线质量过关;
- 减少配置帧和读结果帧之间的空闲时间,用 DMA 连续拉数据。
如果对同步采样有更高要求,可以考虑切到并行模式,用 DB 总线直接读,不过 IO 占用会多很多。
4.5 电源和布局带来的噪声问题
采样值整体跳动范围比理论偏大,或者波形上有固定频率毛刺,大概率不是驱动代码问题,而是电源和布局。
AVCC 上不能直接并数字芯片的开关电源纹波,AD7616 的 AVCC 和 DVCC 要分开走线,在靠近引脚处汇合。模拟地尽量用完整地平面,不要被数字信号走线切断。飞线调试时这个问题尤其明显,我早期用杜邦线连接,采样噪声能到十几毫伏,改成 PCB 贴片连接后明显改善。
还有一个容易被忽略的是基准去耦。REFCAP 引脚上的电容大小和材质直接影响内部基准噪声,我用了 10uF 钽电容并联 0.1uF 陶瓷电容,位置紧贴芯片,效果比单只 0.1uF 好很多。
5. 数据换算与后续扩展
5.1 电压转换与两点校准
原始值转电压的公式在上面提过,但实机上直接用理论比例算出来的电压往往有零点偏移和增益误差,温度一变误差更明显。项目里我做了两点校准,方法很简单。
用标准源给每一路分别输入 0V 和接近满量程的电压,比如 ±10V 量程下输入 +9V,记录原始码值,然后算校准系数:
/* raw0 是0V输入时的原始码,raw1 是+9V输入时的原始码 */ gain = 9.0f / ((int16_t)raw1 - (int16_t)raw0); offset = -(int16_t)raw0 * gain; /* 校准后的电压 */ voltage = (int16_t)raw_value * gain + offset;每块板子的校准系数可以存在 Flash 末尾,出厂时写好,运行中读取。这个做法在做批量设备时几乎是必须的,否则 ADC 本身的增益误差就能带来几十毫伏的偏差。
5.2 多片级联与同步采样扩展
单颗 AD7616 的 16 通道不够用时,可以级联多片。我的做法是把所有 AD7616 的 CONVST A/B 并联,由同一个定时器输出触发,这样所有片子同时开始转换,天然实现同步采样。CS 用不同 GPIO 控制,读取时逐片选通,互不干扰。
同步采样在多路电力信号分析里很重要,如果各通道之间有几微秒的时间差,计算相位角时误差会被放大。用一片 GD32 驱动两片 AD7616,正好可以覆盖 32 路模拟量,这也是这个项目的后续扩展方案。
如果只是通道多但不需要同步,也可以用 GPIO 模拟片选轮询读取,每条 SPI 总线上挂多片,程序上更省事。
最后再分享一个实际经验:做这种 ADC 驱动,第一版不要追求高采样率和花哨功能,先把单通道 1kHz 正弦波采出来,在串口工具里看波形,确认数据和时序没问题,再慢慢扩展通道数、提高采样率、加 DMA。我见过太多同事一上来就写 16 通道 DMA 环形缓冲,最后出问题根本定位不到是寄存器配置错还是 DMA 配置错。一步步来,反而最快。