STM32驱动AD7616:16通道同步电压采集与换算详解
2026/9/16 18:23:57 网站建设 项目流程

简介:这是一份基于STM32F103微控制器的AD7616电压采集工程源码,面向嵌入式开发者与工业数据采集学习者,演示16通道同步采样ADC的驱动移植、时序控制与数据处理方法。资源同时覆盖串行与并行两种接口方式,附TFT液晶显示和多文件工程组织结构,可直接导入Keil查看参考。压缩包共148个文件,以24个C源文件和22个头文件为核心,配合uvprojx/uvoptx工程配置、hex/axf固件、map/lst编译输出等辅助文件,便于从源码到烧录、调试全流程学习,整体约2.97MB。已有220人浏览学习。读者可从中获得完整的可编译工程范例,包括寄存器初始化、SPI/并口读取时序、电压换算、显示刷新逻辑,以及工业采集场景下的排错思路,特别适合在公司内部培训、ADC驱动入门或快速搭建数据采集原型时作为参考蓝本。

1. 采集电压不是读个 ADC 那么简单,AD7616 值得单独写一套驱动

一块 AD7616 采集板最快能在多大程度上帮你把电压数据拿到手?这里讨论的不是 STM32 片内 12 位 ADC,而是 16 通道、16 位、双路同步采样的独立 ADC。当 8 路电压和 8 路电流需要同时采样,或者一台配电终端要求在同一时刻锁定三相电压的瞬时值,STM32 片内 ADC 的通道数和同步性就不够用。AD7616 的输入范围可按 ±10V、±5V、±2.5V 配置,后端可直接对接互感器调理电路,前端用 STM32 的 FSMC 或 SPI 读取,读出来的是二进制补码,要经过换算才是真正的电压值。这篇文章从硬件接法、寄存器操作、电压换算到现场排错完整铺开,适合做电力采集、电机驱动监测和电池电压巡检的嵌入式工程师,也适合第一次把并行 ADC 接到 STM32 上的人。

2. 硬件接线怎么选:FSMC 映射还是 SPI 采集

AD7616 的数字接口有并行和串行两种,STM32 侧选哪种,取决于芯片型号、封装,以及 FSMC 是否可用。选错接口方案,后面代码写得再漂亮也跑不起来。

2.1 AD7616 的关键引脚与量程选择

AD7616 有 16 个模拟输入通道,按 A/B 两组各 8 路排列,两组内部各有一个 ADC 内核,由同一个 CONVST 信号触发同步采样。也就是说,A 组和 B 组的数据是在同一个时刻被采样保持的,这一点在计算功率、相位时特别重要。

引脚类型引脚名方向说明
模拟输入VA1~VA8,VB1~VB8输入16 路双极性电压输入
电压基准REFP/REFM输入外部基准,建议用高精度基准源
转换控制CONVST输入启动 A/B 两组同步转换
状态输出BUSY输出转换期间为高,转换结束拉低
接口模式PAR/SER,W/B输入选择并行/串行、16 位/8 位
范围选择RANGE输入选择 ±10V/±5V/±2.5V
地址/数据A0~A3,DB0~DB15输入/输出并行模式下的通道选择与数据线

量程配置可以由硬件引脚完成,也可以通过内部配置寄存器写值,具体看芯片版本和固件包的支持情况。工程上最常见的是用 RANGE 引脚整体设定一个量程,因为大部分应用场景里,一路调理电路下来,所有通道的满量程是一致的。如果要逐通道配不同量程,需要确认所选型号有没有开放对应的寄存器位,不能只靠改一个全局宏就认为全部通道都生效了。

2.2 并行模式:把 AD7616 映射成 STM32 的一段地址空间

AD7616 的并行读时序和 SRAM 非常接近,CS 拉低、RD 下降沿把数据送上总线,这让它可以直接挂在 STM32 的 FSMC 上。要注意,STM32F1 系列只有 RE 后缀以上的型号才有 FSMC,比如 F103ZE,而最常见的 F103C8T6 没有这个外设。F407、F429、H7 系列的 FMC 同样支持这种接法。

AD7616STM32 FSMC 接口
DB0~DB15FSMC_D0~FSMC_D15
CSFSMC_NE1,基地址 0x60000000
RDFSMC_NOE
A0~A3FSMC_A0~FSMC_A3
CONVST任意 GPIO 或定时器输出
BUSY任意 GPIO 输入

这样接好以后,读某一个通道的数据,其实就是读一段内存地址。代码上可以写得非常简洁:

#define AD7616_BASE_ADDR (0x60000000U) #define AD7616_READ(ch) (*(volatile uint16_t *)(AD7616_BASE_ADDR + (((uint32_t)(ch)) << 1U))) uint16_t ad7616_read_parallel(uint8_t ch) { return AD7616_READ(ch); /* 读地址即选通道,FSMC自动产生CS/RD时序 */ }

这个函数里的ch取值范围是 0~15,对应 VA1~VB8。并行模式下,AD7616 的低 4 位地址线 A0~A3 直接参与通道选择,所以读不同地址就是读不同通道。因为是 16 位数据总线宽度,写入到 FSMC 时的字节地址偏移是ch << 1,也就是每个通道间隔 2 字节,这和普通 SRAM 的访问方式一样。

FSMC 的时序参数刚开始可以给得宽松一些,比如在 HAL 里这样配置:

FSMC_NORSRAM_TimingTypeDef timing = {0}; timing.AddressSetupTime = 15; /* 地址建立时间,先放宽 */ timing.DataSetupTime = 15; /* 数据建立时间,先放宽 */ timing.AccessMode = FSMC_ACCESS_MODE_A;

跑通以后再用逻辑分析仪抓 RD 信号宽度,逐步把这两个值往小调。很多新手一上来就把时序压到极限,结果数据偶尔对偶尔错,很难排查。先保证功能正确,再谈性能。

2.3 串行模式:没有 FSMC 的芯片怎么接

如果用的是 F103C8T6 这类没有 FSMC 的芯片,最常见方案是 SPI 或 GPIO 模拟。AD7616 串行模式下主要用到 SCLK、SDO、SDI、CS 这四根线,另外需要一根 GPIO 接 BUSY。以 F103C8T6 接 SPI1 为例:

AD7616STM32F103C8T6
SCLKPA5(SPI1_SCK)
SDOPA6(SPI1_MISO)
SDIPA7(SPI1_MOSI)
CSPA4(GPIO 输出)
BUSYPA3(GPIO 输入)

串行模式每次读数据前,需要按数据手册的 SPI 映射表发送命令字,这个命令字可能是读某个通道转换结果,也可能是写内部配置寄存器。不同版本资料里的寄存器地址定义可能不一样,移植固件时不要只照抄宏定义,一定要对着手头的芯片型号看一遍。SPI 时钟先用 1MHz 起步,确认读写正常后再逐步提高频率。

3. 用 STM32 HAL 库驱动 AD7616:初始化、触发转换与读取

这套代码用 HAL 库组织,核心是三条:GPIO 初始化、CONVST 触发、BUSY 等待。很多采集异常都出在触发和等待的顺序上,先把这三个动作理清楚,后面加量程、加通道就顺了。

3.1 初始化:GPIO、复位与供电检查

AD7616 的控制引脚不多,初始化也简单。CONVST 配置成推挽输出,BUSY 配置成输入,不要开上下拉,因为 BUSY 由芯片内部驱动,外部再挂一个上下拉反而可能影响电平判断。

__HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = AD7616_CONVST_Pin; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(AD7616_CONVST_GPIO_Port, &gpio); gpio.Pin = AD7616_BUSY_Pin; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_NOPULL; HAL_GPIO_Init(AD7616_BUSY_GPIO_Port, &gpio);

复位时序按数据手册来,一般是给 RESET 引脚一个低脉冲。如果硬件上没有引出 RESET,可以通过寄存器软复位,但要确认芯片上电后寄存器处于已知状态。一个简单的ad7616_reset()可以这样写:

void ad7616_reset(void) { HAL_GPIO_WritePin(AD7616_RESET_GPIO_Port, AD7616_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(AD7616_RESET_GPIO_Port, AD7616_RESET_Pin, GPIO_PIN_SET); HAL_Delay(1); }

如果卡在后面的 BUSY 等待上,先回头检查 AD7616 的模拟电源、数字电源和基准电压。AD7616 需要外部基准,基准源不上电或电压不对,芯片不会正常工作。用万用表量基准电压是排除问题的第一步。

3.2 一次完整转换:先触发,再等待,最后读数据

AD7616 的转换流程是:CONVST 给一个触发边沿,BUSY 拉高,转换完成后 BUSY 拉低。主控要等 BUSY 拉低以后才能去读数据。这里有一个容易犯的错,就是读完一个通道马上读下一个,中间没有等 BUSY 准备下一次转换,导致读到的还是上一轮的数据。

void ad7616_start_convert_pulse(void) { HAL_GPIO_WritePin(AD7616_CONVST_GPIO_Port, AD7616_CONVST_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(AD7616_CONVST_GPIO_Port, AD7616_CONVST_Pin, GPIO_PIN_RESET); } void ad7616_wait_busy(void) { while (HAL_GPIO_ReadPin(AD7616_BUSY_GPIO_Port, AD7616_BUSY_Pin) != GPIO_PIN_RESET) { /* 等待转换完成,必要时加超时保护 */ } }

CONVST 的触发极性以数据手册为准,这里用的是拉高再拉低的脉冲方式。等待 BUSY 的循环一定要加超时,比如定义一个计数器,超过一定次数就跳出并报错,否则模拟输入悬空或者芯片损坏时,程序会死在一个 while 循环里。DEBUG 阶段尤其需要超时保护。

3.3 真正同步的读法:一次触发,连读 16 通道

如果你需要 16 路同一时刻的电压值,顺序必须是这样:先发一次 CONVST,等 BUSY,然后连续读 16 个通道。因为 AD7616 的采样保持是在 CONVST 边沿同时发生的,后续的读取只是把已经锁存的数据搬出来。反过来,如果每个通道都先触发再读取,那 16 个通道之间就有了时间差,三相功率和相位计算会偏差很大。

uint16_t samples[16]; ad7616_start_convert_pulse(); ad7616_wait_busy(); for (uint8_t ch = 0; ch < 16; ch++) { samples[ch] = ad7616_read_parallel(ch); }

这才是 AD7616 双路同步采样的正确用法。read_parallel本身不做转换,它只是通过地址线选通道并读回数据。16 路数据在总线上是分时传输的,但在模拟端它们是同一时刻被锁存的。

3.4 固定采样率:用定时器触发 CONVST

采集工频信号一般需要固定采样率,比如每周期 128 点。这时可以把 CONVST 接到定时器某个输出通道,由硬件产生触发脉冲,采样时刻不受中断延迟影响。如果手里只有一个普通 GPIO 控制 CONVST,那就在定时器中断里拉脉冲。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { ad7616_start_convert_pulse(); ad7616_wait_busy(); read_all_channels(samples); } }

注意这段代码里的read_all_channels依然是轮询读取。当采样率超过 10kHz 时,中断里等待 BUSY 和 16 次读取会占用大量 CPU 时间,这时候要把读取动作改到状态机或 DMA 里去做。中断里只做触发,主循环根据标志位再读数据,是比较稳妥的过渡方案。

4. 把原始码换算成电压:补码、量程与校准

AD7616 的输出编码是二进制补码,和 STM32 片内 ADC 的无符号输出完全是两回事。直接拿 uint16_t 去显示,负数会变成很大的正数,这就是很多人第一次读 AD7616 时觉得“数据怪怪的”的原因。

4.1 补码输出和 C 语言 int16_t 的关系

以 ±10V 量程为例,AD7616 的正满量程对应 0x7FFF,负满量程对应 0x8000,零点就是 0。这个编码方式刚好和 C 语言里的int16_t一致,读取以后直接强转成int16_t,符号位自然就正确了。

输入电压(±10V 量程)原始码int16_t 值
接近 +10V0x7FFF32767
0V0x00000
接近 -10V0x8000-32768

raw & 0xFFFF先拿到 16 位无符号值,再转成int16_t得到有符号值,这个转换不能省略。直接用 uint16_t 做运算,后面所有换算公式都会出错。

4.2 电压换算公式与不同量程的 LSB

换算电压的公式很简单:把有符号读数除以 32768,再乘以当前量程对应的满幅值。

float ad7616_voltage(int16_t raw, float range) { return ((float)raw / 32768.0f) * range; }

调用时range分别传 10.0、5.0、2.5,对应 ±10V、±5V、±2.5V 三种量程。这个公式里除以 32768 而不是 32767,是因为补码的负半轴比正半轴多一个点,按 32768 做对称除法在工程上误差小于 1 LSB,而且代码更简洁。

量程LSB 电压值
±10V约 305.19 µV
±5V约 152.59 µV
±2.5V约 76.29 µV

LSB 的计算公式是2 × range / 65536。如果前端有互感器或分压电阻,最终显示的实际电压 = ADC 口电压 × 分压比。在代码里要把分压比放在单独一个宏或配置项里,不要写死在换算函数里面,否则换一次互感器就要改一遍算法。

4.3 零点偏移和增益误差的校准方法

AD7616 的线性度很好,但系统误差会来自调理电路、基准电压和 PCB 布局。最常见的两种误差是零点偏移和增益误差,用两点校准就能修掉。每通道准备两个浮点数,一个增益系数,一个偏移量。

typedef struct { float gain; float offset; } ad7616_cal_t; float ad7616_apply_cal(float measured, const ad7616_cal_t *cal) { return measured * cal->gain + cal->offset; }

标定方法也简单:先让输入接地,多次采样取平均,得到mean_zero,这时的offset = -mean_zero。再输入一个已知电压Vref,多次采样取平均得到mean_ref,然后计算:

cal->offset = -mean_zero; cal->gain = Vref / (mean_ref - mean_zero);

这个Vref最好用实际测量值而不是理论值,因为信号源本身也有误差。校准参数可以在生产时写进 Flash 或 EEPROM,每通道存一组。这样现场更换调理板或者基准芯片后,不需要改固件,只需要重新标定参数。

4.4 采集交流电压时的 RMS 计算

很多电压采集项目最终要的是有效值,不是瞬时值。连续采样 N 个点后计算 RMS,是最常用的做法:

float rms_value = 0.0f; for (uint16_t i = 0; i < N; i++) { rms_value += samples[i] * samples[i]; } rms_value = sqrtf(rms_value / N);

计算前最好先减去直流分量,否则一个小的零点偏移会让 RMS 结果明显偏大。N 要覆盖整数个工频周期,比如 50Hz 信号用 20ms 窗口,采样率 10kHz 就取 200 个点。窗口不是整数周期时,RMS 结果会出现周期性波动,这个现象在调试时很常见。

5. 读回来不对时怎么排错,以及过采样技巧

最后这部分给几个排错方向和过采样的用法。AD7616 这类芯片问题通常不是代码逻辑难,而是硬件状态没确认就急着看数据。

5.1 典型异常现象与定位顺序

现象优先检查
一直读到 0x7FFF输入是否过压,量程是否选对,模拟电源是否正常
一直读到 0x8000输入欠压或接线断,量程和负满量程不匹配
16 个通道全是 0CS/RD 没拉低,FSMC 映射地址不对
通道数据错位A0~A3 接线顺序反了,SPI 命令字位序不对
结果上下波动大基准源噪声,SPI 时钟过快,采样保持时间不足

排查顺序我一般是:先量电源和基准电压,再用逻辑分析仪抓 CS、RD、BUSY 的时序,最后看代码里的地址偏移。并行模式下最常见的问题就是 16 位总线宽度下的地址计算,base + chbase + 2*ch差一倍,表现为通道 0 正常、通道 1 读到通道 2 的数据。用 ST-Link 或 J-Link 在线调试时,可以直接把 FSMC 地址当指针读,配合 Memory 窗口查看连续地址的数据,能快速判断地址映射是否正确。

5.2 用直流信号判断是前端还是 ADC 的问题

把输入通道接到一个已知直流电压上,比如给 ±5V 量程输入 2.5V,理想读数应该是 16384 左右。如果读数和万用表量出来的一致,说明 AD7616 这条链路是通的,问题往往在前端调理电路。再把输入对地短接,读数应该贴近 0。如果短接后还有明显偏移,先怀疑调理电路的零点,再回头查 PCB 铺地和 AGND/DGND 的连接方式。

5.3 过采样倍率和采样率怎么取舍

AD7616 的过采样功能可以压低随机噪声、提高有效位数,但代价是转换时间成倍增加。如果单次转换时间是 t_conv,过采样倍率设为 8,那么每次有效转换时间大约是 8 × t_conv。定时器触发周期必须大于这个值,否则上一次转换还没结束,下一次 CONVST 又来了,BUSY 永远等不到拉低。

工频采样里,每周波 128 点是比较常用的配置,这个速率下过采样开 2 倍或 4 倍是常见的做法。过采样开得越大,能支持的采样率越低。判断是否合适的办法很简单:看 BUSY 两次下降沿之间的时间,用示波器或逻辑分析仪量,确认它比定时器中断周期短。

最后说一个我常用的习惯:把通道数、量程、过采样倍率和每通道校准参数统一放到一个ad7616_config_t结构体里,初始化时给一个默认实例,现场换互感器或改接线时只改结构体,不碰驱动函数。这样每次切换工况后,验证数据正确性只需要对比结构体参数和实际输入,不用重新编译整个工程。

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

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

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

立即咨询