简介:这是一份基于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 同样支持这种接法。
| AD7616 | STM32 FSMC 接口 |
|---|---|
| DB0~DB15 | FSMC_D0~FSMC_D15 |
| CS | FSMC_NE1,基地址 0x60000000 |
| RD | FSMC_NOE |
| A0~A3 | FSMC_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 为例:
| AD7616 | STM32F103C8T6 |
|---|---|
| SCLK | PA5(SPI1_SCK) |
| SDO | PA6(SPI1_MISO) |
| SDI | PA7(SPI1_MOSI) |
| CS | PA4(GPIO 输出) |
| BUSY | PA3(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 值 |
|---|---|---|
| 接近 +10V | 0x7FFF | 32767 |
| 0V | 0x0000 | 0 |
| 接近 -10V | 0x8000 | -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 个通道全是 0 | CS/RD 没拉低,FSMC 映射地址不对 |
| 通道数据错位 | A0~A3 接线顺序反了,SPI 命令字位序不对 |
| 结果上下波动大 | 基准源噪声,SPI 时钟过快,采样保持时间不足 |
排查顺序我一般是:先量电源和基准电压,再用逻辑分析仪抓 CS、RD、BUSY 的时序,最后看代码里的地址偏移。并行模式下最常见的问题就是 16 位总线宽度下的地址计算,base + ch和base + 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结构体里,初始化时给一个默认实例,现场换互感器或改接线时只改结构体,不碰驱动函数。这样每次切换工况后,验证数据正确性只需要对比结构体参数和实际输入,不用重新编译整个工程。
本文还有配套的精品资源,点击获取